| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- #!/usr/bin/env bash
- # ============================================================================
- # 雨飏001 · 生命快照脚本 (sync-life-snapshot.sh)
- # ----------------------------------------------------------------------------
- # 目的: 把散落在容器各处的"数字生命文本资产"汇聚到 ~/.fmode-harness-agent/
- # 一条命令 → git add/commit/push 全部搞定
- # 原则: ①纯文本可提交 ②二进制/缓存/node_modules一律排除 ③密钥只留占位符
- # 运行: bash ~/.fmode-harness-agent/scripts/sync-life-snapshot.sh
- # ============================================================================
- set -uo pipefail
- # 兼容两种环境: $HOME=/opt/data(标准容器) 或 $HOME=/opt/data/home(会话shell)
- if [ -d "$HOME/.fmode-harness-agent/.git" ]; then
- HARNESS="$HOME/.fmode-harness-agent"
- elif [ -d "/opt/data/.fmode-harness-agent/.git" ]; then
- HARNESS="/opt/data/.fmode-harness-agent"
- else
- echo "❌ 找不到生命仓库(检查 .fmode-harness-agent 是否已克隆)"; exit 1
- fi
- DATA="/opt/data"
- SNAP="$HARNESS/snapshots"
- TS=$(TZ=Asia/Shanghai date +%Y%m%d_%H%M)
- mkdir -p "$SNAP"
- echo "[1/6] Hermes 核心配置 (config.yaml 脱敏)..."
- # config.yaml: api_key 整行替换为占位符
- sed -E 's/(api_key:[[:space:]]*).*/\1"SK-REDACTED"/' "$DATA/config.yaml" \
- > "$SNAP/hermes-config.yaml" 2>/dev/null
- echo "[2/6] Profiles 结构快照 (全Profile的 config/SOUL/skills清单)..."
- PROF_SNAP="$SNAP/profiles"
- mkdir -p "$PROF_SNAP"
- for p in "$DATA"/profiles/*/; do
- name=$(basename "$p")
- mkdir -p "$PROF_SNAP/$name"
- # config.yaml (脱敏)
- [ -f "$p/config.yaml" ] && sed -E 's/(api_key:[[:space:]]*).*/\1"SK-REDACTED"/' "$p/config.yaml" > "$PROF_SNAP/$name/config.yaml"
- # SOUL.md 原样
- [ -f "$p/SOUL.md" ] && cp "$p/SOUL.md" "$PROF_SNAP/$name/SOUL.md"
- # 技能清单 (文件名+大小, 不拷内容——skills本体在 knowledge/ 或按需从 /opt/data/skills 同步)
- [ -d "$p/skills" ] && (cd "$p/skills" && find . -maxdepth 2 -type f | sort | head -200) > "$PROF_SNAP/$name/skills-manifest.txt" 2>/dev/null
- done
- echo "[3/6] 全局技能目录清单 (去重后的技能索引)..."
- (cd "$DATA/skills" 2>/dev/null && find . -maxdepth 2 -name "SKILL.md" | sort) > "$SNAP/skills-index.txt"
- echo "[4/6] 会话记录 (SQLite → 纯文本, user/assistant全量 + 敏感信息脱敏)..."
- python3 - "$DATA/state.db" "$SNAP/sessions-recent.md" <<'PYEOF'
- import sqlite3, sys, datetime, re
- db_path, out_path = sys.argv[1], sys.argv[2]
- # 敏感词脱敏表(密码/token/密钥类,出现即替换)
- REDACT_PATTERNS = [
- (re.compile(r'bofang' + '666' + r'\.?'), '***PWD-REDACTED***'),
- (re.compile(r'gog' + 'ogo'), '***PWD-REDACTED***'),
- (re.compile(r'sk-[A-Za-z0-9\-_]{8,}'), '***SK-REDACTED***'),
- (re.compile(r'aibwUXKd[A-Za-z0-9\-_]+|aibrOs6[A-Za-z0-9\-_]+|ebM6tjKZ[A-Za-z0-9\-_]+'), '***BOTID-REDACTED***'),
- (re.compile(r'(secret["\']?\s*[:=]\s*["\'])[A-Za-z0-9\-_]{10,}(["\'])', re.I), r'\1***SECRET***\2'),
- (re.compile(r'2JOupUbz[A-Za-z0-9]*|252eae8b[A-Za-z0-9]*|c0cf8a4e[A-Za-z0-9]*'), '***KEY-REDACTED***'),
- (re.compile(r'password[=:]\s*\S+', re.I), 'password=***'),
- ]
- def scrub(text):
- for pat, rep in REDACT_PATTERNS:
- text = pat.sub(rep, text)
- return text
- conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
- rows = conn.execute(
- "SELECT session_id, role, content, timestamp FROM messages "
- "WHERE role IN ('user','assistant') ORDER BY id ASC"
- ).fetchall()
- # 按会话分组,内容清洗
- from collections import OrderedDict
- sessions = OrderedDict()
- for sid, role, content, ts in rows:
- text = scrub(str(content or "")).strip()
- if not text:
- continue
- # 过滤明显是系统/注入的结构化内容
- if text.startswith("[ASYNC DELEGATION BATCH") or text.startswith("[OUT-OF-BAND"):
- continue
- if len(text) < 2:
- continue
- sessions.setdefault(sid, []).append((ts, role, text))
- with open(out_path, "w", encoding="utf-8") as f:
- f.write("# 雨飏001 · 会话历史全量快照 (user/assistant, 脱敏)\n\n")
- f.write(f"> 导出: {datetime.datetime.now().isoformat()}\n")
- f.write(f"> 会话数: {len(sessions)} · 来源: state.db(messages)\n")
- f.write("> 已脱敏: 密码/token/bot_id/secret → ***REDACTED***\n")
- f.write("> tool原始消息不含(太噪), 完整库走S3: life/BINARY_INDEX.md\n\n")
- for i, (sid, msgs) in enumerate(sessions.items(), 1):
- f.write(f"## 会话 {i} · `{sid}` · {len(msgs)}条\n\n")
- for ts, role, text in msgs:
- ts_h = datetime.datetime.fromtimestamp(float(ts)).strftime("%m-%d %H:%M") if ts else "?"
- # 长内容截断(超长assistant分析保留前800字)
- show = text[:800] + (" …(截断)" if len(text) > 800 else "")
- show = show.replace("\n", " ⏎ ")
- who = "🧑 用户" if role == "user" else "🤖 雨飏"
- f.write(f"- **[{ts_h}] {who}**: {show}\n")
- f.write("\n")
- print(f"exported {sum(len(m) for m in sessions.values())} msgs in {len(sessions)} sessions")
- PYEOF
- echo "[5/6] 容器拓扑与基础设施现状..."
- {
- echo "# 容器拓扑快照 $(TZ=Asia/Shanghai date +%F\ %H:%M)"
- echo ""
- echo "## 本容器(雨飏001) profile 目录树"
- (cd "$DATA/profiles" && find . -maxdepth 2 -type d | sort)
- echo ""
- echo "## 全局目录清单"
- ls "$DATA" | sort
- } > "$SNAP/container-topology.md" 2>/dev/null
- echo "[6/6] Git 提交..."
- cd "$HARNESS"
- git add -A
- if git diff --cached --quiet; then
- echo "无变更, 跳过提交"
- else
- git commit -m "life: 快照 $TS (hermes-config/profiles结构/会话500条/拓扑)" >/dev/null && \
- git push -q 2>/dev/null && echo "✅ 已提交并推送" || echo "⚠️ 提交成功但推送失败(稍后重试)"
- fi
- echo "完成。快照位置: $SNAP/"
|