#!/bin/bash # ============================================================ # memory-system-check.sh — 记忆系统统一自检(方案A架构) # 每小时由 cron 触发(no_agent 模式) # 正常静默,异常飞书报警 # ============================================================ set -e FEISHU_WEBHOOK="https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad" STATE_DIR="$HOME/.hermes/watchdog" STATE_FILE="$STATE_DIR/memory_state.json" mkdir -p "$STATE_DIR" ALERT=0 LINES=() log() { echo "[$(date '+%H:%M:%S')] $*"; } # ── L7 统一层:llm_context.json v2 ────────────────────────── check_unified_layer() { local ctx_file="$HOME/.hermes/llm_context.json" local issues=() if [ ! -f "$ctx_file" ]; then issues+=("llm_context.json 不存在") else # 验证 v2 格式 9 个关键字段 local keys keys=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(','.join(sorted(d.keys())))" 2>/dev/null || echo "ERROR") if [ "$keys" = "ERROR" ]; then issues+=("llm_context.json JSON 格式错误") else for field in user_profile observations patterns scenes policies distilled_rules cares recent_moments; do if ! echo "$keys" | grep -q "$field"; then issues+=("缺少字段: $field") fi done fi # 检查 daemon 在跑 if ! pgrep -f "daemon.py" > /dev/null 2>&1; then issues+=("daemon.py 未运行") fi fi if [ ${#issues[@]} -eq 0 ]; then upd=$(python3 -c "import json; d=json.load(open('$ctx_file')); print(d.get('updated_at','?'))" 2>/dev/null || echo "?") v=$(python3 -c "import json; d=json.load(open('$ctx_file')); v=d.get('version','N/A'); obs=len(d.get('observations',[])); pat=len(d.get('patterns',[])); sc=len(d.get('scenes',[])); pol=len(d.get('policies',[])); dr=len(d.get('distilled_rules',[])); print(f'v{v} L1={obs} L2={pat} L3={sc} L4={pol} L5={dr}')" 2>/dev/null || echo "?") log "[OK] 统一层: updated=$upd $v" else ALERT=1 log "[FAIL] 统一层: ${issues[*]}" LINES+=("统一层异常: ${issues[*]}") fi } # ── L1 织忆 ────────────────────────────────────────────────── check_zhiyi() { local issues=() local ok=true if ! pgrep -f "zhiyi" > /dev/null 2>&1; then issues+=("zhiyid进程不存在"); ok=false fi if ! pgrep -f "bge_embed" > /dev/null 2>&1; then issues+=("bge-embed进程不存在"); ok=false fi local health health=$(curl -s --max-time 3 -H "X-API-Key: zhiyi-dev-key-2026" http://127.0.0.1:7821/api/v1/health 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))" 2>/dev/null || echo "unreachable") if [ "$health" != "ok" ]; then issues+=("API=$health"); ok=false fi if [ "$ok" = true ]; then local stats nodes edges stats=$(curl -s --max-time 3 -H "X-API-Key: zhiyi-dev-key-2026" http://127.0.0.1:7821/api/v1/stats 2>/dev/null || echo "{}") nodes=$(echo "$stats" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('total_episodes','?'))" 2>/dev/null) # graph.db 里 pattern 节点数 local patterns patterns=$(python3 -c "import sqlite3; g=sqlite3.connect('$HOME/.hermes/graph.db'); c=g.cursor(); c.execute(\"SELECT COUNT(*) FROM graph_nodes WHERE type='pattern'\"); print(c.fetchone()[0]); g.close()" 2>/dev/null || echo "?") log "[OK] 织忆: nodes=$nodes patterns=$patterns" else ALERT=1 log "[FAIL] 织忆: ${issues[*]}" LINES+=("织忆异常: ${issues[*]}") fi } # ── L5/L6 Soulful ──────────────────────────────────────────── check_soulful() { local ok=true local issues=() for f in "$HOME/.hermes/soulful/cares-queue.json" "$HOME/.hermes/soulful/heart-traces.jsonl" "$HOME/.hermes/soulful/user-profile.json"; do if [ ! -f "$f" ]; then issues+=("文件缺失: $(basename "$f")"); ok=false fi done local cares heart_count cares=$(python3 -c "import json; print(len(json.load(open('$HOME/.hermes/soulful/cares-queue.json')).get('cares',[])))" 2>/dev/null || echo "?") heart_count=$(wc -l < "$HOME/.hermes/soulful/heart-traces.jsonl" 2>/dev/null || echo 0) local distilled distilled=$(python3 -c "import json; print(len(json.load(open('$HOME/.hermes/soulful/user-profile.json')).get('distilled_rules',[])))" 2>/dev/null || echo "?") # 过期牵挂检查 local overdue overdue=$(python3 -c " import json, datetime d = json.load(open('$HOME/.hermes/soulful/cares-queue.json')) today = datetime.date.today() for c in d.get('cares', []): due = c.get('follow_up_date','') if due: try: dd = datetime.date.fromisoformat(due) if (today - dd).days > 7: print('OLD') except: pass " 2>/dev/null) [ -n "$overdue" ] && issues+=("有过期>7天牵挂") if [ "$ok" = true ]; then log "[OK] Soulful: cares=$cares 心迹=$heart_count distilled=$distilled" else ALERT=1 log "[FAIL] Soulful: ${issues[*]}" LINES+=("Soulful异常: ${issues[*]}") fi } # ── L3/L4 TencentDB ────────────────────────────────────────── check_tddb() { local ok=true local issues=() # systemd 管理的服务,用 systemctl 检查 for svc in zhiyid.service bge-embed.service zhiyi-consolidate.service xiaowei-daemon.service tencentdb.service; do if ! systemctl --user is-active "$svc" &>/dev/null; then issues+=("$svc 未运行"); ok=false fi done # 检查端口 if ! ss -tlnp 2>/dev/null | grep -q ":8420"; then issues+=("TencentDB 端口未监听"); ok=false fi local health health=$(curl -s --max-time 3 http://127.0.0.1:8420/health 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))" 2>/dev/null || echo "unreachable") [ "$health" != "ok" ] && { issues+=("API=$health"); ok=false; } # 测试写入能力 local test_result test_result=$(curl -s --max-time 5 -H "Content-Type: application/json" \ -d '{"session_key":"health-check","user_content":"ping","assistant_content":"pong"}' \ http://127.0.0.1:8420/capture 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('l0_recorded','?'))" 2>/dev/null || echo "?") if [ "$test_result" = "?" ] || [ -z "$test_result" ]; then issues+=("capture写入失败"); ok=false fi if [ "$ok" = true ]; then log "[OK] TencentDB: status=$health l0=$test_result" else ALERT=1 log "[FAIL] TencentDB: ${issues[*]}" LINES+=("TencentDB异常: ${issues[*]}") fi } # ── CBM 代码记忆 ────────────────────────────────────────────── check_cbm() { local ok=true local issues=() if ! command -v codebase-memory-mcp &>/dev/null; then issues+=("CBM 未安装"); ok=false fi local projects projects=$(codebase-memory-mcp cli list_projects 2>/dev/null | python3 -c " import sys, json try: d = json.load(sys.stdin) for p in d.get('projects',[]): print(p['name'] + ':' + str(p['nodes']) + ':' + str(p['edges'])) except: print('FAIL') " 2>/dev/null) if [ -z "$projects" ] || [ "$projects" = "FAIL" ]; then issues+=("CBM 查询失败"); ok=false fi if [ "$ok" = true ]; then local total_nodes=0 total_edges=0 count=0 while IFS=: read -r name nodes edges; do total_nodes=$((total_nodes + nodes)) total_edges=$((total_edges + edges)) count=$((count + 1)) done <<< "$projects" log "[OK] CBM: ${count}项目 ${total_nodes}节点 ${total_edges}边" else ALERT=1 log "[FAIL] CBM: ${issues[*]}" LINES+=("CBM异常: ${issues[*]}") fi } # ── 主流程 ──────────────────────────────────────────────────── log "=== 记忆系统统一自检 $(date '+%Y-%m-%d %H:%M') ===" check_unified_layer check_zhiyi check_soulful check_tddb check_cbm # ── 告警 ───────────────────────────────────────────────────── if [ "$ALERT" = 1 ]; then msg="记忆系统异常: $(printf '%s,' "${LINES[@]}")" curl -s -X POST "$FEISHU_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{\"msg_type\":\"text\",\"content\":{\"text\":\"$msg\"}}" > /dev/null 2>&1 fi