feat: 生态完美化修复 F1-F5
F1: Hermes全插件确认(browser/web/kanban已内置) F2: OpenClaw 5 agent技能体系(子任务执行中) F3: Hermes↔OpenClaw桥接(openclaw-bridge.py) F4: CBM→织忆通道确认(OpenClaw可读) F5: prof-b分身感知(profile-sync.py) + daemon.py集成 F3/F5 deep_think循环 + cron: profile同步每4h + OpenClaw状态每8h
This commit is contained in:
parent
3b340646dc
commit
3328f00b11
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue