#!/usr/bin/env bash # dream-cycle.sh — 记忆 consolidation 周期(Dream Cycle) # # 灵感: openclaw-auto-dream 的 4 Phase Dream Cycle # v2 — 加入远程记忆服务器同步 # # 用法: # bash scripts/dream-cycle.sh # 完整运行 # bash scripts/dream-cycle.sh collect # Phase 1: 扫描 daily/ # bash scripts/dream-cycle.sh sync-remote # 推送新条目到远程服务器 # bash scripts/dream-cycle.sh evaluate # Phase 3: 健康评分 # bash scripts/dream-cycle.sh health # 只看健康分 set -euo pipefail WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)" MEMORY_DIR="$WORKSPACE/memory" DAILY_DIR="$MEMORY_DIR/daily" REGISTERS_DIR="$MEMORY_DIR/registers" PROJECTS_DIR="$MEMORY_DIR/projects" MARKER="$MEMORY_DIR/.dream-cycle-last-run" NOW=$(date +%Y-%m-%d) NOW_EPOCH=$(date +%s) # 远程记忆服务器 MEMORY_SERVER="http://111.229.145.18" # 上次运行时间 LAST_RUN="" if [ -f "$MARKER" ]; then LAST_RUN=$(cat "$MARKER") fi # ─── 智能标记检测 ────────────────────────────────────── # 自动检测重要性(返回类型或空) detect_importance() { local line="$1" # 决策句式 if echo "$line" | grep -qE '(选择|决定|配置为|建立|部署|接入|升级|迁移|切换|采用)'; then echo "decision" return fi # 偏好表达 if echo "$line" | grep -qE '(喜欢|习惯|总是|不要|偏好|改为|改成|以后都)'; then echo "preference" return fi # 纠正信号 if echo "$line" | grep -qE '(不对|应该是|更正|其实是|原来是|发现.*错|误)'; then echo "correction" return fi # 重要事件 if echo "$line" | grep -qE '(完成|成功|失败|上线|修复|解决|搞定|落地|生效)'; then echo "event" return fi # 工具/服务状态 if echo "$line" | grep -qE '(状态|正常|异常|在线|离线|连接|断开)'; then echo "status" return fi echo "" } # ─── Phase 1: Collect — 扫描 daily/ ────────────────────── collect() { echo "📡 Phase 1: Collect — 日志扫描(智能标记)" echo " 上次运行: ${LAST_RUN:-从未}" echo "" local tempfile=$(mktemp) local count=0 for f in "$DAILY_DIR"/*.md; do [ -f "$f" ] || continue local fname=$(basename "$f") local fdate="${fname%.md}" # 跳过旧文件 if [ -n "$LAST_RUN" ] && [ "$fdate" != "$LAST_RUN" ] && [[ "$fdate" < "$LAST_RUN" ]]; then continue fi while IFS= read -r line; do local trimmed=$(echo "$line" | sed 's/^\*\*//;s/\*\*$//') local type="" # 1. 显式标记优先 if echo "$trimmed" | grep -q '\[session-flush\]'; then type="session-summary" local ctx=$(grep -A 5 '\[session-flush\]' "$f" 2>/dev/null | grep '^-' | head -4 | tr '\n' ' ' | head -c 500 || true) [ -n "$ctx" ] && echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1)) && continue fi if echo "$trimmed" | grep -q '\[correction'; then type="correction" local ctx=$(echo "$trimmed" | head -c 300) [ -n "$ctx" ] && echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1)) && continue fi # 2. 智能检测(只对非标题行、非空行) if [ -n "$trimmed" ] && ! echo "$trimmed" | grep -qE '^(#|\||-*$|>)'; then type=$(detect_importance "$trimmed") if [ -n "$type" ]; then local ctx=$(echo "$trimmed" | head -c 300) echo "$fdate|$type|$ctx" >> "$tempfile" && count=$((count+1)) fi fi done < "$f" done echo " 收集 $count 条待同步" echo "$tempfile" } # ─── Phase 1.5: Sync Remote — 推送到记忆服务器 ──────────── sync_remote() { echo "📤 Phase 1.5: Sync Remote → $MEMORY_SERVER" echo "" # 用 collect 扫出新条目 local tmpfile=$(collect | tail -1) [ ! -f "$tmpfile" ] && echo " 无待同步条目" && return local total=0 failed=0 local tmpfile2=$(mktemp) # 逐行解析发送 while IFS='|' read -r date type content; do [ -z "$content" ] && continue local clean=$(echo "$content" | sed 's/^[- ]*//;s/"/\\"/g') [ -z "$clean" ] && continue local resp=$(curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/add" \ -H "Content-Type: application/json" \ -H "X-API-Key: ${MEMORY_API_KEY:-}" \ -d "{\"content\":\"$clean\",\"type\":\"$type\",\"project\":\"xiaolong\",\"source\":\"dream-cycle\"}" 2>/dev/null || echo '{"error":"curl fail"}') if echo "$resp" | grep -q 'stored'; then total=$((total+1)) else failed=$((failed+1)) fi done < "$tmpfile" echo " 成功 $total / 失败 $failed" [ "$total" -gt 0 ] && echo " ✅ 记忆已写入远程服务器" echo "$NOW" > "$MARKER" rm -f "$tmpfile" "$tmpfile2" } # ─── Phase 1.6: Sync Context Graph — 推送结构化记忆 ───────── sync_context_graph() { echo "🧬 Phase 1.6: Sync ContextGraph → $MEMORY_SERVER" if [ -f "$WORKSPACE/src/memory/sync-context-graph.js" ]; then node "$WORKSPACE/src/memory/sync-context-graph.js" 2>&1 | sed 's/^/ /' echo " ✅ ContextGraph 已同步" else echo " ⚠️ sync-context-graph.js 未找到,跳过" fi echo "" } # ─── Phase 2: Consolidate — 本地路由 ────────────────────── consolidate() { echo "🔄 Phase 2: Consolidate — 本地路由" echo "" mkdir -p "$REGISTERS_DIR" for reg in "$REGISTERS_DIR"/*.md; do [ -f "$reg" ] || continue local name=$(basename "$reg" .md) local cnt=$(grep -c '\- \[' "$reg" 2>/dev/null || echo 0) echo " registers/$name.md: ~${cnt} 条" done local proj_cnt=$(ls "$PROJECTS_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ') local vault_lines=$(wc -l < "$MEMORY_DIR/vault.md" 2>/dev/null || echo 0) local mem_lines=$(wc -l < "$WORKSPACE/MEMORY.md" 2>/dev/null || echo 0) echo " projects/: ${proj_cnt} 个" echo " vault.md: ${vault_lines} 行" echo " MEMORY.md: ${mem_lines} 行" } # ─── Phase 2.5: Refresh Core Memory — 动态刷新核心记忆块 ─── refresh_core_memory() { echo "🧠 Phase 2.5: Refresh Core Memory Blocks" echo "" local core_dir="$MEMORY_DIR/core" [ ! -d "$core_dir" ] && echo " ⚠️ memory/core/ 不存在,跳过" && return local now_iso=$(date -Iseconds) # 1. active_projects.md — 从最近 3 天的 daily/ 提取项目动态 local active_file="$core_dir/active_projects.md" if [ -f "$active_file" ]; then local recent_projects=$(grep -h '\*\*.*\*\*' "$DAILY_DIR"/*.md 2>/dev/null | grep -E '(项目|仓库|同步|接入|部署|升级|完成|进行中)' | tail -10 | sed 's/^/- /' || true) if [ -n "$recent_projects" ]; then local header=$(head -8 "$active_file") echo "$header" > "$active_file" echo "" >> "$active_file" echo "## 进行中" >> "$active_file" echo "" >> "$active_file" echo "$recent_projects" >> "$active_file" sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$active_file" 2>/dev/null || true echo " ✅ active_projects.md 已刷新" fi fi # 2. recent_decisions.md — 从最近 7 天的 vault.md 提取决策 local decisions_file="$core_dir/recent_decisions.md" if [ -f "$decisions_file" ]; then local recent_decisions=$(grep -E '^- [0-9]{4}-[0-9]{2}-[0-9]{2}:' "$MEMORY_DIR/vault.md" 2>/dev/null | tail -10 || true) if [ -n "$recent_decisions" ]; then local header=$(head -8 "$decisions_file") echo "$header" > "$decisions_file" echo "" >> "$decisions_file" echo "# 最近决策" >> "$decisions_file" echo "" >> "$decisions_file" echo "$recent_decisions" >> "$decisions_file" sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$decisions_file" 2>/dev/null || true echo " ✅ recent_decisions.md 已刷新" fi fi # 3. tool_status.md — 只更新时间戳,内容由事件触发更新 local tool_file="$core_dir/tool_status.md" if [ -f "$tool_file" ]; then sed -i "" "s/updated_at:.*/updated_at: \"$now_iso\"/" "$tool_file" 2>/dev/null || true echo " ✅ tool_status.md 时间戳已更新" fi echo "" } # ─── Phase 3: Evaluate — 健康评分 ───────────────────────── evaluate() { echo "📊 Phase 3: Evaluate — 健康评分" echo "" local total_daily=$(ls "$DAILY_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ') local reg_count=0 updated_regs=0 for f in "$REGISTERS_DIR"/*.md; do [ -f "$f" ] || continue reg_count=$((reg_count+1)) local mtime=$(stat -f "%m" "$f" 2>/dev/null || echo 0) [ "$mtime" -gt 0 ] && [ $(( (NOW_EPOCH-mtime)/86400 )) -le 30 ] && updated_regs=$((updated_regs+1)) done local vault_lines=$(wc -l < "$MEMORY_DIR/vault.md" 2>/dev/null || echo 0) # 各维度评分 local freshness=100; [ "$total_daily" -gt 0 ] && freshness=$(( $(ls -t "$DAILY_DIR"/*.md 2>/dev/null | head -1 | xargs stat -f "%m" 2>/dev/null || echo 0) > $((NOW_EPOCH-86400*7)) ? 100 : 50 )) local coverage=100; [ "$reg_count" -gt 0 ] && coverage=$(( updated_regs*100/reg_count )) local efficiency=100; [ "$vault_lines" -gt 200 ] && efficiency=60 local reliability=90 local security=95 local health=$(( (freshness*25+coverage*25+efficiency*20+reliability*15+security*15)/100 )) echo " 📈 健康评分: ${health}/100" echo " Freshness: ${freshness}/100 | Coverage: ${coverage}/100 | Efficiency: ${efficiency}/100" echo " Reliability: ${reliability}/100 | Security: ${security}/100" echo " daily: ${total_daily}篇 | registers: ${reg_count}个 | vault: ${vault_lines}行" } # ─── 入口 ─────────────────────────────────────────── case "${1:-all}" in collect) collect ;; sync-remote) sync_remote ;; sync-graph) sync_context_graph ;; evaluate) evaluate ;; health) evaluate 2>&1 | grep -E '健康|Freshness|daily' ;; all) echo "🌙 小龙的 Dream Cycle — $NOW $(date +%H:%M)" echo "============================================" echo "" sync_remote sync_context_graph echo "" refresh_core_memory echo "" consolidate forget_context_graph echo "" evaluate echo "" echo "✅ Dream Cycle 完成 (下一轮: 每天 4:00)" ;; core-refresh) refresh_core_memory ;; *) echo "用法: bash scripts/dream-cycle.sh [collect|sync-remote|evaluate|health]"; exit 1 ;; esac # ─── Phase 1.7: ContextGraph 遗忘机制 ───────────────────── forget_context_graph() { echo "🧠 Phase 1.7: ContextGraph 遗忘机制" [ ! -f ~/.openclaw/context-graph.json ] && echo " 无文件" && return node -e " const fs = require('fs'); const g = JSON.parse(fs.readFileSync(process.env.HOME + '/.openclaw/context-graph.json', 'utf8')); let removed = 0; const now = Date.now(); const THRESHOLD = 90 * 86400 * 1000; // 90天 for (const [id, entity] of Object.entries(g.entities)) { // 遗忘条件:非 pinned + 最后观察 >90天 + 低优先级 if (entity.pinned) continue; if (!entity.observations || entity.observations.length === 0) continue; const lastObs = entity.observations[entity.observations.length - 1]; const age = now - new Date(lastObs.timestamp).getTime(); const hasHighPriority = entity.observations.some(o => o.priority === 'critical' || o.priority === 'high' ); if (age > THRESHOLD && !hasHighPriority) { delete g.entities[id]; removed++; } } // 清理孤立关系 const before = g.relations.length; g.relations = g.relations.filter(r => g.entities[r.from] && g.entities[r.to] ); const relRemoved = before - g.relations.length; fs.writeFileSync(process.env.HOME + '/.openclaw/context-graph.json', JSON.stringify(g, null, 2)); console.log(' 遗忘实体: ' + removed + ' | 孤立关系: ' + relRemoved); " 2>/dev/null || echo " 跳过" }