#!/usr/bin/env bash # encrypt_kb.sh —— 交付副本加密打包(生产库不动,单一 tar)。 # 流程:① 一致性快照(Qdrant snapshot API / neo4j-admin dump,绝不动活动卷) # ② 断言导出文件 AES-256-GCM(raw_export.jsonl properties_json 字段 + parquet 整文件) # ③ tar 单一文件(不压缩,92G 二进制)+ sha256 # ④ 计数校验(backend_guard 常量) # 用法:bash encrypt_kb.sh <版本> [stage目录] set -euo pipefail umask 027 VERSION="${1:?用法: encrypt_kb.sh <版本>}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLING_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" STAGE_ROOT="${2:-$TOOLING_ROOT/build/$VERSION/knowledge-stage}" OUT_DIR="$TOOLING_ROOT/build/$VERSION" QDRANT_URL="${QDRANT_URL:-http://127.0.0.1:27333}" QDRANT_COLLECTION="${V15_QDRANT_COLLECTION:-plant_srt_pc2025_v3_20260817_normalized}" NEO4J_NORM_NAME="neo4j-plant-srt-pc2025-v3-20260817-normalized" NEO4J_DB="${NEO4J_DB:-neo4j}" # 2026-08-31 修正:KB 交付不含 canonical(18788 属 SDK KG 浏览依赖); # normalized 数据目录 08-24 起无写入(frozen),在线 dump 不支持(5.15 community),改冷拷 STAGING="/home/xzq/phase2b-5090-staging-20260823T191150Z/plant_srt_pc2025_merge_20260817_v3_normalized" ASSERT_DIR="$STAGING/frozen_inputs/ef0d58871e05afbb957d652a5fe932eda24b68b3655bbbc5feb40d5ae9c47dc8/baseline_assertions" REGISTRY_SRC="/home/xzq/5090/teamWorkspace/zh/PlantAI-rag/data/knowledge_base/paper_metadata_registry_v1_20260717/paper_metadata_registry_full.jsonl" KEY_FILE="${KB_KEY_FILE:-$OUT_DIR/kb.key}" TAR_FILE="$OUT_DIR/knowledge-$VERSION.tar" AVAIL_GB=$(df -BG "$OUT_DIR" 2>/dev/null | awk 'NR==2 {gsub("G","",$4); print $4}' || echo 0) [[ "${AVAIL_GB:-0}" -ge 200 ]] || { echo "磁盘余量不足(需 ≥200G, 当前 ${AVAIL_GB}G)" >&2; exit 1; } [ -f "$KEY_FILE" ] || { echo "缺少 KB 主密钥: $KEY_FILE (先运行 tooling/crypto/init_keys.sh)" >&2; exit 1; } mkdir -p "$STAGE_ROOT" "$OUT_DIR" # ---- ① 一致性快照 ---- echo "[1/5] Qdrant snapshot API 导出(在线一致,$QDRANT_COLLECTION)..." curl -sf -X POST "$QDRANT_URL/collections/$QDRANT_COLLECTION/snapshots" > /tmp/qdrant_snap_resp.json SNAP_NAME=$(python3 -c "import json; print(json.load(open('/tmp/qdrant_snap_resp.json'))['result']['name'])") echo " snapshot: $SNAP_NAME(生成中,轮询...)" for i in $(seq 1 120); do sleep 10 curl -sf "$QDRANT_URL/collections/$QDRANT_COLLECTION/snapshots" | grep -q "\"name\":\"$SNAP_NAME\"" && break done # 从容器拷贝 snapshot(容器内路径 /qdrant/snapshots//) docker cp "$(docker ps -q -f name=qdrant-plant-srt-pc2025-v3-20260817-normalized):/qdrant/snapshots/$QDRANT_COLLECTION/$SNAP_NAME" "$STAGE_ROOT/" 2>/dev/null \ || { echo "snapshot 拷贝失败" >&2; exit 1; } echo " Qdrant snapshot 拷贝完成: $(du -sh "$STAGE_ROOT/$SNAP_NAME" | cut -f1)" echo "[1/5] Neo4j normalized 冷拷(8.2G,数据目录 08-24 起无写入=frozen)..." NEO4J_NORM_SRC="/home/xzq/phase2b-5090-staging-20260823T191150Z/plant_srt_pc2025_merge_20260817_v3_normalized/databases/neo4j/data" rsync -a --info=progress2 "$NEO4J_NORM_SRC/" "$STAGE_ROOT/neo4j-normalized/data/" 2>&1 | tail -1 echo " normalized 冷拷: $(du -sh "$STAGE_ROOT/neo4j-normalized/data" | cut -f1)" # ---- ② 断言导出文件加密 ---- mkdir -p "$STAGE_ROOT/assertions" cp "$REGISTRY_SRC" "$STAGE_ROOT/registry.jsonl" echo "[2/5] AES-GCM 加密 raw_export.jsonl properties_json(9.5G,数分钟)..." python3 "$SCRIPT_DIR/kb_encrypt_backfill.py" \ --input "$ASSERT_DIR/raw_export.jsonl" \ --output "$STAGE_ROOT/assertions/raw_export.enc.jsonl" \ --key-file "$KEY_FILE" \ --fields properties_json echo "[2/5] AES-GCM 整文件加密 assertions.parquet(2.7G)..." python3 "$SCRIPT_DIR/kb_encrypt_backfill.py" \ --input "$ASSERT_DIR/assertions.parquet" \ --output "$STAGE_ROOT/assertions/assertions.parquet.enc" \ --key-file "$KEY_FILE" \ --file-encrypt # ---- ③ tar 单一文件(不压缩)+ sha256 ---- echo "[3/5] 打包单一 tar(92G+,约10分钟)..." tar -C "$STAGE_ROOT" -cf "$TAR_FILE" . sha256sum "$TAR_FILE" > "$TAR_FILE.sha256" echo "tar 完成: $TAR_FILE ($(du -sh "$TAR_FILE" | cut -f1))" # ---- ④ 计数校验 ---- echo "[4/5] 计数校验(backend_guard 常量,解密安装后由 QA 验证)..." cat > "$OUT_DIR/EXPECTED_COUNTS.json" <