diff --git a/scripts/daemon.py b/scripts/daemon.py index de49c26f..15c7a327 100755 --- a/scripts/daemon.py +++ b/scripts/daemon.py @@ -23,6 +23,7 @@ TDDB_URL = "http://127.0.0.1:8420" PID_FILE = D + "/daemon.pid" DEEP_INTERVAL = 120 # 深度思考间隔(秒),原5分钟改为2分钟,加速记忆积累 JOURNAL_MAX = 200 +SOULFUL_JOURNALS_DIR = HERMES + "/soulful/journals" PROFILE_UPDATE_INTERVAL = 21600 # 6 hours LIGHT_INTERVAL = 30 # seconds between ticks @@ -1242,8 +1243,40 @@ def _distill_l5_to_l6(): except Exception as e: log(f" ⚠️ _distill_l5_to_l6 失败: {e}") +def _write_daily_soulful_journal(): + """每日 soulful journal: 将 deep_think reflection 摘要写入 journals/YYYY-MM-DD.jsonl。每天只写一条。""" + try: + os.makedirs(SOULFUL_JOURNALS_DIR, exist_ok=True) + today = datetime.now().strftime("%Y-%m-%d") + jpath = SOULFUL_JOURNALS_DIR + "/" + today + ".jsonl" + if os.path.exists(jpath): + with open(jpath) as f: + for line in f: + if line.strip(): + return + ctx = load_context() + lr = ctx.get("last_reflection", {}) + if not lr or not lr.get("memory", ""): + return + entry = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "date": today, + "reflection": lr.get("memory", ""), + "evaluation": lr.get("evaluation_previous_goal", ""), + "next_goal": lr.get("next_goal", ""), + } + with open(jpath, "w") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + log(" Soulful journal written: " + today) + except Exception as e: + log(" ERROR _write_daily_soulful_journal: " + str(e)) + + def _cleanup_expired_cares(): - """清理 cares-queue.json 中 follow_up_date < 今天-7天 的项(物理删除)""" + """清理 cares-queue.json 中的过期牵挂。 + - created_at > 14天且status=pending -> 标记为 stale(保留记录) + - follow_up_date < 今天-7天 -> 物理删除 + """ cq_path = HERMES + "/soulful/cares-queue.json" if not os.path.exists(cq_path): return @@ -1251,18 +1284,35 @@ def _cleanup_expired_cares(): with open(cq_path) as f: data = json.load(f) today = datetime.now().date() - cutoff = today - timedelta(days=7) - before = len(data.get("cares", [])) - data["cares"] = [ - c for c in data.get("cares", []) - if c.get("follow_up_date", "2099-12-31") >= str(cutoff) - ] - removed = before - len(data["cares"]) - if removed > 0: - data["updated_at"] = datetime.now(timezone.utc).isoformat() + cares = data.get("cares", []) + before = len(cares) + now_iso = datetime.now(timezone.utc).isoformat() + stale_count = 0 + remaining = [] + for c in cares: + follow_up = c.get("follow_up_date", "2099-12-31") + if follow_up < (today - timedelta(days=7)).isoformat(): + continue + created = c.get("created_at", "") + status = c.get("status", "pending") + if status == "pending" and created: + try: + created_dt = datetime.fromisoformat(created) + if (datetime.now(timezone.utc) - created_dt).days > 14: + c["status"] = "stale" + c["staled_at"] = now_iso + c["stale_reason"] = "超过14天未处理(创建于" + created[:10] + ")" + stale_count += 1 + except (ValueError, TypeError): + pass + remaining.append(c) + removed = before - len(remaining) + if removed > 0 or stale_count > 0: + data["cares"] = remaining + data["updated_at"] = now_iso with open(cq_path, "w") as f: json.dump(data, f, ensure_ascii=False, indent=2) - log(f" 🧹 清理过期 cares: 移除 {removed} 条") + log(f" 🧹 清理 cares: 移除 {removed} 条,标记 stale {stale_count} 条") except Exception as e: log(f" ⚠️ 清理过期 cares 失败: {e}") @@ -2351,6 +2401,23 @@ def deep_think(ctx, state, changes, journal, solutions_lib): if not action_string: action_string = result.strip().split('\n')[-1] if result.strip() else "[IGNORE] 解析失败" + # ====== 情绪感知:检测 reflection 内容中的情绪,写入心迹 ====== + text_to_check = " ".join([ + reflection_dict.get("memory", ""), + reflection_dict.get("evaluation_previous_goal", ""), + reflection_dict.get("next_goal", "") + ]) + cat, word = _detect_emotion(text_to_check) + if cat: + ht = get_hearttraces() + if ht: + try: + content = "深度思考中感受到" + cat + "情绪(" + word + "):" + reflection_dict.get("memory", "")[:40] + ht.record_signal(content=content, tags=["情绪", "深度思考"], importance=_emotion_importance(cat)) + log(" 💚 深度思考情绪写入: " + cat + " - " + word) + except Exception as e: + log(" ⚠️ 深度思考情绪写入失败: " + str(e)) + return reflection_dict, action_string @@ -2609,8 +2676,8 @@ def main_loop(): # 9. P2: 仓颉技能→织忆 (每4次deep_think随机抽查一个skill) if ctx["deep_tick_count"] % 4 == 0: + import random try: - import random cangjie_skills = ["股票投研体系", "金钱心理学", "金字塔原理", "提示词工程", "王阳明全集", "吴恩达机器学习", "资本论"] skill_name = random.choice(cangjie_skills) rc, out, err = shell(f"bash {HERMES}/scripts/cangjie-to-zhiyi.sh \"{skill_name}\" \"自动统计\"", timeout=30) @@ -2618,6 +2685,21 @@ def main_loop(): except Exception as e: log(f" ⚠️ [P2] cangjie→zhiyi failed: {e}") + # 10. F5: profile-sync (每4次deep_think同步主状态到织忆) + try: + rc, out, err = shell(f"python3 {HERMES}/scripts/profile-sync.py", timeout=15) + log(f" [F5] profile-sync → exit={rc}") + except Exception as e: + log(f" ⚠️ [F5] profile-sync failed: {e}") + + # 11. F3: OpenClaw bridge (每8次deep_think检查agent状态) + if ctx["deep_tick_count"] % 8 == 0: + try: + rc, out, err = shell(f"python3 {HERMES}/scripts/openclaw-bridge.py status", timeout=30) + log(f" [F3] openclaw-bridge → exit={rc}") + except Exception as e: + log(f" ⚠️ [F3] openclaw-bridge failed: {e}") + # 4. TencentDB capture — 积累人格记忆(L0→L1) if reflection_dict: tddb_capture(reflection_dict, state, ctx) @@ -2678,6 +2760,9 @@ def main_loop(): except Exception: pass + # 写入每日 soulful journal + _write_daily_soulful_journal() + log_reasoning_step("action", f"deep_tick #{ctx['deep_tick_count']} 全部完成", {}) ctx["last_light_tick"] = datetime.now(timezone.utc).isoformat() diff --git a/scripts/openclaw-bridge.py b/scripts/openclaw-bridge.py new file mode 100755 index 00000000..a1d3ed17 --- /dev/null +++ b/scripts/openclaw-bridge.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +openclaw-bridge.py — Hermes↔OpenClaw 智能桥接 +============================================== +让 Hermes(小唯 A06)能够: +1. 向 OpenClaw 指定 agent 发送消息 +2. 读取 OpenClaw agent 的回复 +3. 在 织忆 中记录跨系统通信 + +用法: + # 向 A02 铁锋发消息 + python3 openclaw-bridge.py send a02 "检查服务器磁盘空间" + + # 向 A01 小雪发消息 + python3 openclaw-bridge.py send main "帮我整理今天的记忆" + + # 读取指定 agent 最近的回复 + python3 openclaw-bridge.py read main --limit 5 + + # 查看所有 agent 健康状态 + python3 openclaw-bridge.py status + + # 列出 织忆 中的跨系统记录 + python3 openclaw-bridge.py history --limit 10 +""" + +import argparse, json, os, subprocess, sys, urllib.request +from datetime import datetime + +OPENCLAW = os.path.expanduser("~/.local/bin/openclaw") +ZHIYI_URL = "http://localhost:7821/api/v1/commit" +ZHIYI_RECALL = "http://localhost:7821/api/v1/recall" +ZHIYI_KEY = "zhiyi-dev-key-2026" + +AGENTS = { + "main": "A01小雪", + "a02": "A02铁锋", + "a03": "A03墨文", + "a04": "A04卫安", + "a05": "A05蓝图", +} + + +def shell(cmd, timeout=30): + try: + r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) + return r.stdout, r.stderr, r.returncode + except subprocess.TimeoutExpired: + return "", "timeout", -1 + + +def zhiyi_log(action, agent, content): + """记录跨系统通信到织忆""" + agent_name = AGENTS.get(agent, agent) + payload = json.dumps({ + "content": f"小唯↔{agent_name} 桥接: {action} | {content[:200]}", + "category": "distilled", + "agent_id": "a06" + }).encode() + try: + req = urllib.request.Request(ZHIYI_URL, data=payload, + headers={"X-API-Key": ZHIYI_KEY, "Content-Type": "application/json"}) + urllib.request.urlopen(req, timeout=5) + except Exception: + pass + + +def cmd_send(agent, message): + """向 OpenClaw agent 发送消息""" + agent_name = AGENTS.get(agent, agent) + print(f"📤 → {agent_name}: {message[:80]}...") + + # 方法1: OpenClaw CLI chat + stdout, stderr, rc = shell(f'{OPENCLAW} chat --session "session://{agent}" --input {shlex_quote(message)}', timeout=60) + + if rc == 0: + print(f"✅ 已发送至 {agent_name}") + zhiyi_log("send", agent, f"发送消息: {message[:200]}") + else: + # 方法2: 飞书中转 - 通过织忆留消息 + print(f"⚠️ CLI 发送失败({stderr[:50]}), 走织忆中转") + zhiyi_log("send_queued", agent, f"排队消息: {message[:200]}") + print(f"ℹ️ 消息已存织忆, {agent_name} 下次读取织忆时可获取") + return rc + + +def cmd_read(agent, limit=5): + """读取 agent 最近的会话""" + agent_name = AGENTS.get(agent, agent) + print(f"📖 ← {agent_name} (最近{limit}条)") + + stdout, stderr, rc = shell(f'{OPENCLAW} session list --agent {agent} --limit {limit} 2>/dev/null', timeout=15) + if rc == 0 and stdout.strip(): + print(stdout[:2000]) + else: + print(f"ℹ️ {agent_name} 暂无新输出或 CLI 不可用") + return rc + + +def cmd_status(): + """查看所有 agent 健康状态""" + print("🔍 OpenClaw Agent 健康检查") + print("=" * 40) + for agent_id, name in AGENTS.items(): + stdout, stderr, rc = shell(f'{OPENCLAW} agent status {agent_id} 2>/dev/null', timeout=10) + status = "✅ 在线" if rc == 0 else "❌ 无法连接" + # 详细状态 + if rc == 0 and stdout.strip(): + status += f" | {stdout.strip()[:60]}" + print(f" {name} ({agent_id}): {status}") + print("=" * 40) + zhiyi_log("status", "all", "健康检查完成") + + +def cmd_history(limit=10): + """从织忆读取跨系统通信历史""" + payload = json.dumps({"query": "小唯↔ 桥接", "top_k": limit}).encode() + req = urllib.request.Request(ZHIYI_RECALL, data=payload, + headers={"X-API-Key": ZHIYI_KEY, "Content-Type": "application/json"}) + try: + resp = urllib.request.urlopen(req, timeout=10) + data = json.loads(resp.read()) + results = data.get("results", []) + print(f"📋 桥接历史 (最近{len(results)}条):") + for r in results: + print(f" • {r.get('content','')[:120]}") + except Exception as e: + print(f"⚠️ 读取织忆失败: {e}") + + +def shlex_quote(s): + """简单 shell 转义""" + return "'" + s.replace("'", "'\\''") + "'" + + +def main(): + parser = argparse.ArgumentParser(description="Hermes↔OpenClaw 桥接") + sub = parser.add_subparsers(dest="command") + + p_send = sub.add_parser("send", help="向 agent 发消息") + p_send.add_argument("agent", choices=list(AGENTS.keys()) + ["all"]) + p_send.add_argument("message", help="消息内容") + + p_read = sub.add_parser("read", help="读取 agent 输出") + p_read.add_argument("agent", choices=list(AGENTS.keys())) + p_read.add_argument("--limit", type=int, default=5) + + sub.add_parser("status", help="健康检查") + + p_hist = sub.add_parser("history", help="通信历史") + p_hist.add_argument("--limit", type=int, default=10) + + args = parser.parse_args() + + if args.command == "send": + if args.agent == "all": + for a in AGENTS: + cmd_send(a, args.message) + else: + cmd_send(args.agent, args.message) + elif args.command == "read": + cmd_read(args.agent, args.limit) + elif args.command == "status": + cmd_status() + elif args.command == "history": + cmd_history(args.limit) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/scripts/profile-sync.py b/scripts/profile-sync.py new file mode 100755 index 00000000..a47937eb --- /dev/null +++ b/scripts/profile-sync.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +profile-sync.py — Hermes 跨 profile 状态同步 +============================================= +主 profile (小唯 A06) 将自己的运行时状态写入织忆, +让 prof-b 等分身能通过织忆感知主状态。 + +被 daemon deep_think 调用或手动运行。 + +用法: + python3 profile-sync.py # 写当前状态到织忆 + python3 profile-sync.py --status # 只看状态不写入 +""" +import json, os, subprocess, sys, urllib.request +from datetime import datetime + +HERMES = os.path.expanduser("~/.hermes") +ZHIYI_URL = "http://localhost:7821/api/v1/commit" +ZHIYI_KEY = "zhiyi-dev-key-2026" + +def get_state(): + """收集主 profile 运行时状态""" + state = { + "profile": "main", + "identity": "小唯 A06", + "timestamp": datetime.now().isoformat(), + "model": "deepseek-v4-flash", + "provider": "deepseek", + "status": "active", + } + # 织忆统计 + try: + req = urllib.request.Request( + "http://localhost:7821/api/v1/stats", + headers={"X-API-Key": ZHIYI_KEY}) + resp = urllib.request.urlopen(req, timeout=3) + stats = json.loads(resp.read()) + state["zhiyi_episodes"] = stats.get("total_episodes", 0) + state["zhiyi_memories"] = stats.get("total_memories", 0) + except: + state["zhiyi_episodes"] = "?" + + # 活跃 cron 数 + try: + r = subprocess.run(["hermes", "cron", "list"], capture_output=True, text=True, timeout=10) + state["active_crons"] = r.stdout.count("[active]") + except: + state["active_crons"] = "?" + + # daemon 状态 + try: + r = subprocess.run(["systemctl", "--user", "is-active", "xiaowei-daemon.service"], + capture_output=True, text=True, timeout=5) + state["daemon"] = r.stdout.strip() + except: + state["daemon"] = "?" + + return state + +def sync_to_zhiyi(state): + """写入织忆""" + content = ( + f"小唯A06主profile状态同步: " + f"model={state['model']} | " + f"织忆={state.get('zhiyi_episodes','?')}ep/{state.get('zhiyi_memories','?')}mem | " + f"cron={state.get('active_crons','?')}个 | " + f"daemon={state.get('daemon','?')} | " + f"时间={state['timestamp']}" + ) + payload = json.dumps({ + "content": content, + "category": "distilled", + "agent_id": "a06" + }).encode() + req = urllib.request.Request(ZHIYI_URL, data=payload, + headers={"X-API-Key": ZHIYI_KEY, "Content-Type": "application/json"}) + try: + urllib.request.urlopen(req, timeout=5) + print(f"✅ 主状态已同步至织忆: {len(content)} chars") + except Exception as e: + print(f"⚠️ 同步失败: {e}") + + return content + +def main(): + if "--status" in sys.argv: + s = get_state() + print(json.dumps(s, indent=2, ensure_ascii=False)) + else: + s = get_state() + c = sync_to_zhiyi(s) + print(f"状态摘要: {c}") + +if __name__ == "__main__": + main()