Files
16gagent/scripts/dream-cycle.sh
T
2026-06-06 10:40:48 +08:00

350 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 " 跳过"
}