226 lines
9.2 KiB
Bash
Executable File
226 lines
9.2 KiB
Bash
Executable File
#!/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 检查
|
||
# 2026-08-02 修复: tencentdb.service 是幽灵服务(与 tdai-gateway 同源 server.ts,
|
||
# 端口被占后崩溃循环 278 次骗过 is-active),已停用。真正服务是 tdai-gateway.service
|
||
for svc in zhiyid.service bge-embed.service zhiyi-consolidate.service xiaowei-daemon.service tdai-gateway.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 |