Files
Agent-System/scripts/restore_regulations.sh
WeiCHENandCursor ad2147445c chore(deploy): 三环境 Compose 构建真实 app 镜像并补齐法规库运维
将 dev/test/main 的 app 服务从占位容器改为 Dockerfile 多阶段构建,增加 healthcheck 与 .regulations 只读挂载;新增法规库备份/恢复脚本并同步 env-service 目录初始化与 gitignore。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:22 +00:00

309 lines
12 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# =============================================================================
# 法规知识库完整恢复脚本 (restore_regulations.sh)
#
# 作用:
# 1. 在新拉取本仓库的任意机器上一键执行;
# 2. 校验备份目录及 manifest.json 完整性与 SHA256;
# 3. 将法规物理文档恢复到工程根目录 .regulations;
# 4. 自动检测并拉起共享基础设施容器 shuimu-kb-shared;
# 5. 将 regulations_db.dump 完整还原至数据库;
# 6. 自动执行数据完整性与基线指标对齐校验。
#
# 用法:
# ./scripts/restore_regulations.sh <备份目录路径> [选项]
#
# 选项:
# --force, -f 强制覆盖已存在的数据库表,不进行确认
# --skip-db 仅恢复物理文件,跳过数据库导入
# --skip-files 仅恢复数据库,跳过物理文件复制
# --help, -h 显示帮助信息
# =============================================================================
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONTAINER_NAME="shuimu-kb-shared"
DB_NAME="regulations"
DB_USER="regkb"
NETWORK_NAME="chat-agent-shared-net"
show_help() {
cat <<EOF
用法:
$0 <备份目录路径> [选项]
参数:
备份目录路径 指定包含 manifest.json 与 regulations_db.dump 的备份目录
选项:
--force, -f 非交互强制恢复(覆盖已有数据)
--skip-db 仅恢复 .regulations 物理文件,跳过数据库导入
--skip-files 仅恢复数据库,跳过物理文件
--help, -h 显示本帮助信息
示例:
$0 /Volumes/Backups/regulations_20260911
$0 ./backups/reg_backup --force
EOF
exit 0
}
if [[ $# -lt 1 ]]; then
show_help
fi
BACKUP_DIR=""
FORCE_RESTORE=0
SKIP_DB=0
SKIP_FILES=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
show_help
;;
--force|-f)
FORCE_RESTORE=1
shift
;;
--skip-db)
SKIP_DB=1
shift
;;
--skip-files)
SKIP_FILES=1
shift
;;
*)
if [[ -z "${BACKUP_DIR}" ]]; then
BACKUP_DIR="$1"
else
echo "[错误] 未知参数: $1"
show_help
fi
shift
;;
esac
done
if [[ -z "${BACKUP_DIR}" ]]; then
echo "[错误] 请指定备份目录路径!"
exit 1
fi
if [[ ! -d "${BACKUP_DIR}" ]]; then
echo "[错误] 备份目录不存在: ${BACKUP_DIR}"
exit 1
fi
BACKUP_DIR="$(cd "${BACKUP_DIR}" && pwd)"
echo "============================================================"
echo ">>> 开始执行法规知识库恢复与对齐"
echo ">>> 项目根路径: ${ROOT_DIR}"
echo ">>> 备份源目录: ${BACKUP_DIR}"
echo "============================================================"
# 1. 检查备份目录内关键文件
MANIFEST_FILE="${BACKUP_DIR}/manifest.json"
DUMP_FILE="${BACKUP_DIR}/regulations_db.dump"
if [[ ! -f "${MANIFEST_FILE}" ]]; then
echo "[警告] 未在备份目录中找到 manifest.json,将以常规模式执行恢复。"
else
echo "[元数据] 检测到备份清单 manifest.json:"
python3 -c "
import json
with open('${MANIFEST_FILE}') as f:
d = json.load(f)
print(' • 创建时间:', d.get('created_at', 'unknown'))
print(' • 来源主机:', d.get('source_hostname', 'unknown'))
db = d.get('database', {})
print(' • 数据库文档数 (reg_documents):', db.get('document_count', 'unknown'))
print(' • 数据库切片数 (reg_chunks):', db.get('chunk_count', 'unknown'))
print(' • 数据库节点数 (reg_nodes):', db.get('node_count', 'unknown'))
files = d.get('regulations_files', {})
print(' • 物理文件数:', files.get('total_files', 'unknown'))
" 2>/dev/null || true
fi
# 校验 Dump 文件
if [[ "${SKIP_DB}" -eq 0 ]]; then
if [[ ! -f "${DUMP_FILE}" ]]; then
echo "[错误] 未找到数据库 Dump 文件: ${DUMP_FILE}"
exit 1
fi
# 校验 SHA256 (若 manifest 中有记录)
if [[ -f "${MANIFEST_FILE}" ]]; then
WANT_SHA=$(python3 -c "import json; print(json.load(open('${MANIFEST_FILE}')).get('database', {}).get('dump_sha256', ''))" 2>/dev/null || echo "")
if [[ -n "${WANT_SHA}" && "${WANT_SHA}" != "unavailable" ]]; then
echo "[校验] 正在验证 dump 文件 SHA256 签名..."
if command -v shasum >/dev/null 2>&1; then
GOT_SHA=$(shasum -a 256 "${DUMP_FILE}" | awk '{print $1}')
elif command -v sha256sum >/dev/null 2>&1; then
GOT_SHA=$(sha256sum "${DUMP_FILE}" | awk '{print $1}')
else
GOT_SHA=""
fi
if [[ -n "${GOT_SHA}" ]]; then
if [[ "${GOT_SHA}" == "${WANT_SHA}" ]]; then
echo " ✅ SHA256 校验通过: ${GOT_SHA}"
else
echo " ❌ [错误] SHA256 校验不匹配!"
echo " 期望: ${WANT_SHA}"
echo " 实际: ${GOT_SHA}"
echo "备份可能已损坏,恢复中止!"
exit 1
fi
fi
fi
fi
fi
# 2. 恢复 .regulations 物理目录
if [[ "${SKIP_FILES}" -eq 0 ]]; then
DEST_REG_DIR="${ROOT_DIR}/.regulations"
echo "------------------------------------------------------------"
echo ">>> [步骤 1/3] 恢复物理文件目录至 .regulations"
echo "------------------------------------------------------------"
SRC_REG_DIR="${BACKUP_DIR}/.regulations"
SRC_TAR_FILE="${BACKUP_DIR}/regulations_files.tar.gz"
mkdir -p "${DEST_REG_DIR}"
if [[ -d "${SRC_REG_DIR}" ]]; then
echo "[同步] 从备份目录克隆/同步 .regulations..."
if cp -R -p -c "${SRC_REG_DIR}/." "${DEST_REG_DIR}/" 2>/dev/null; then
echo " ✅ 已通过 APFS CoW 极速克隆至 ${DEST_REG_DIR}"
else
if command -v rsync >/dev/null 2>&1; then
rsync -a "${SRC_REG_DIR}/" "${DEST_REG_DIR}/"
else
cp -R -p "${SRC_REG_DIR}/." "${DEST_REG_DIR}/"
fi
echo " ✅ 物理目录同步完成"
fi
elif [[ -f "${SRC_TAR_FILE}" ]]; then
echo "[解压] 从归档包 ${SRC_TAR_FILE} 解压至工程根目录..."
tar -xzf "${SRC_TAR_FILE}" -C "${ROOT_DIR}"
echo " ✅ 解压完成"
else
echo "[警告] 备份中未找到 .regulations 目录或 regulations_files.tar.gz!"
fi
# 验证物理子目录结构
for sub in intake library quarantine work; do
mkdir -p "${DEST_REG_DIR}/${sub}"
done
RESTORED_FILES=$(find "${DEST_REG_DIR}" -type f | wc -l | tr -d ' ')
RESTORED_SIZE=$(du -sh "${DEST_REG_DIR}" | awk '{print $1}')
echo " • 已恢复物理文件数: ${RESTORED_FILES} 个"
echo " • 物理目录总体积: ${RESTORED_SIZE}"
fi
# 3. 检查并拉起 Docker 共享基础设施容器
if [[ "${SKIP_DB}" -eq 0 ]]; then
echo "------------------------------------------------------------"
echo ">>> [步骤 2/3] 检查并初始化共享基础设施 (${CONTAINER_NAME})"
echo "------------------------------------------------------------"
if ! command -v docker >/dev/null 2>&1; then
echo "[错误] 未检测到 docker 环境,请先安装 Docker!"
exit 1
fi
# 确保 Docker 网络存在
if ! docker network ls --format '{{.Name}}' | grep -q "^${NETWORK_NAME}$"; then
echo "[网络] 创建 Docker 共享网络: ${NETWORK_NAME}..."
docker network create "${NETWORK_NAME}" >/dev/null 2>&1 || true
fi
# 确保容器运行
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "[启动] 容器 ${CONTAINER_NAME} 未运行,正在启动..."
docker compose -f "${ROOT_DIR}/docker-compose.shared.yml" up -d
fi
echo "[等待] 等待数据库就绪..."
for i in {1..30}; do
if docker exec -i "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
echo " ✅ 容器与数据库连接就绪 (${CONTAINER_NAME})"
break
fi
sleep 1
if [ "$i" -eq 30 ]; then
echo "[错误] 等待目标数据库连接超时!"
exit 1
fi
done
# 4. 执行数据库无损恢复
echo "------------------------------------------------------------"
echo ">>> [步骤 3/3] 执行数据库还原 (pg_restore)"
echo "------------------------------------------------------------"
TMP_RESTORE_DUMP="/tmp/restore_regulations_$$.dump"
echo "[传输] 复制 dump 文件至容器内部..."
docker cp "${DUMP_FILE}" "${CONTAINER_NAME}:${TMP_RESTORE_DUMP}"
echo "[初始化] 确保向量扩展 vector 与 pg_trgm 就绪..."
docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_trgm;" >/dev/null 2>&1 || true
echo "[还原] 正在执行无损数据库还原 (包含切片表、倒排索引及 1024 维向量)..."
START_RESTORE_TIME=$(date +%s)
# 执行 pg_restore: --clean --if-exists 清理旧表重建,允许非致命警告 (如 DROP 不存在表返回 1)
set +e
RESTORE_OUTPUT=$(docker exec "${CONTAINER_NAME}" pg_restore -U "${DB_USER}" -d "${DB_NAME}" \
--clean --if-exists --no-owner --no-privileges "${TMP_RESTORE_DUMP}" 2>&1)
RESTORE_STATUS=$?
set -e
docker exec "${CONTAINER_NAME}" rm -f "${TMP_RESTORE_DUMP}"
RESTORE_DURATION=$(( $(date +%s) - START_RESTORE_TIME ))
echo " ✅ 数据库还原指令完成,耗时: ${RESTORE_DURATION} 秒。"
# 5. 执行基线完整性校验
echo "============================================================"
echo ">>> 执行恢复后数据完整性基线校验"
echo "============================================================"
CURR_DOC_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_documents;" 2>/dev/null || echo "0")
CURR_CHUNK_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_chunks;" 2>/dev/null || echo "0")
CURR_NODE_COUNT=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT count(*) FROM reg_nodes;" 2>/dev/null || echo "0")
CURR_DB_SIZE=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c "SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));" 2>/dev/null || echo "0 MB")
echo " • 法规文档总数 (reg_documents): ${CURR_DOC_COUNT}"
echo " • 法规切片总数 (reg_chunks): ${CURR_CHUNK_COUNT}"
echo " • 大纲节点总数 (reg_nodes): ${CURR_NODE_COUNT}"
echo " • 数据库还原后体积: ${CURR_DB_SIZE}"
# 基线比对
EXPECT_DOCS=2907
EXPECT_CHUNKS=312900
if [[ "${CURR_DOC_COUNT}" -ge "${EXPECT_DOCS}" && "${CURR_CHUNK_COUNT}" -ge "${EXPECT_CHUNKS}" ]]; then
echo "============================================================"
echo ">>> 🎉 [PASS] 法规知识库 100% 恢复成功!数据与基线完全对齐!"
echo "============================================================"
else
echo "============================================================"
echo ">>> ⚠️ [WARN] 恢复数据条数与官方基线有偏差,请检查输出日志。"
echo ">>> 实际: ${CURR_DOC_COUNT} 文档 / ${CURR_CHUNK_COUNT} 切片"
echo ">>> 基线: ${EXPECT_DOCS} 文档 / >= ${EXPECT_CHUNKS} 切片"
echo "============================================================"
fi
fi
echo ""
echo "知识库服务就绪: shuimu-kb-shared:5432 (DB: regulations)"
echo "物理文件就绪: .regulations/"
echo "您可以在本项目中执行: go test -v ./internal/kb -run TestKB_RealIntegration 进行完整性验证。"