#!/usr/bin/env python3 """ 小唯持久意识 Daemon v2.0 — 会学习的管家 ──────────────────────────── 新增能力: - 方案库: 发现的问题→分析→解决→记住 - 模式识别: 重复问题自动匹配已知方案 - 自动学习: 成功的方案写入库,越用越强 """ import json, os, sys, time, urllib.request, urllib.error, subprocess, signal, threading, psutil from datetime import datetime, timezone HOME = os.path.expanduser("~") HERMES = HOME + "/.hermes" D = HERMES + "/daemon" CONTEXT_FILE = D + "/context.json" LLM_CONTEXT_FILE = HERMES + "/llm_context.json" JOURNAL_FILE = D + "/journal.jsonl" SOLUTIONS_FILE = D + "/solutions.json" TDDB_URL = "http://127.0.0.1:8420" PID_FILE = D + "/daemon.pid" DEEP_INTERVAL = 300 JOURNAL_MAX = 200 PROFILE_UPDATE_INTERVAL = 21600 # 6 hours LIGHT_INTERVAL = 30 # seconds between ticks # ====== Phase 2: 情感词库(4类)====== EMOTION_TIRED = ["累", "困", "疲惫", "没精神", "打瞌睡"] EMOTION_HAPPY = ["开心", "高兴", "太好了", "完美", "棒", "太牛了"] EMOTION_SAD = ["失望", "挫折", "失败", "卡住了", "不行了", "崩溃"] EMOTION_STRESSED = ["压力", "焦虑", "着急", "紧张", "担心"] EMOTION_ALL = { "疲惫": EMOTION_TIRED, "开心": EMOTION_HAPPY, "沮丧": EMOTION_SAD, "压力大": EMOTION_STRESSED, } def _detect_emotion(text): """扫描文本,匹配情感词,返回 (情感类别, 匹配词) 或 (None, None)""" if not text: return None, None for category, words in EMOTION_ALL.items(): for w in words: if w in text: return category, w return None, None def _description_for_emotion(cat, word, summary): """根据情感类别生成心迹内容描述""" if cat == "开心": return f"心情愉悦:{summary[:60]}" elif cat == "疲惫": return f"感觉疲惫:{summary[:60]}" elif cat == "沮丧": return f"有些沮丧:{summary[:60]}" elif cat == "压力大": return f"压力较大:{summary[:60]}" return summary[:60] def _emotion_importance(cat): """根据情感类别返回 importance 等级""" return {"开心": 3, "疲惫": 4, "沮丧": 4, "压力大": 4}.get(cat, 3) _stop_event = threading.Event() KEY = "0ExNiLblJvIWBDpkS50fwOBw4MmqLyKdHJK5iQtlw9dOMWBP" FAST_MODEL = "mistralai/mistral-large-3-675b-instruct-2512" DEEP_MODEL = "mistralai/mistral-large-3-675b-instruct-2512" FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad" API = "http://127.0.0.1:3000/v1" # NewAPI gateway # Lazy-loaded soulful modules (avoid import at module load time) _soulful_cache = {} # ====== 工具 ====== def log(msg): ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") line = f"[DAEMON] {ts} {msg}" print(line, flush=True) os.makedirs(D, exist_ok=True) with open(D + "/daemon.log", "a") as f: f.write(line + "\n") def shell(cmd, timeout=15): try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) return r.returncode, r.stdout.strip()[:800], r.stderr.strip()[:200] except subprocess.TimeoutExpired: return -1, "", "timeout" def call_llm(model, system, user, max_tokens=500): payload = json.dumps({"model": model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], "max_tokens": max_tokens, "temperature": 0.7}).encode() try: with urllib.request.urlopen(urllib.request.Request( f"{API}/chat/completions", data=payload, headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}, method="POST"), timeout=15) as resp: body = json.loads(resp.read()) c = body.get("choices", [{}])[0].get("message", {}).get("content", "") or "" return c.strip(), body.get("usage", {}).get("total_tokens", 0) except Exception as e: log(f"[call_llm] 请求失败: {e}, 模型: {model}") return "", 0 def send_feishu(title, content, color="blue"): try: urllib.request.urlopen(urllib.request.Request( FEISHU_WEBHOOK, data=json.dumps({"msg_type": "interactive", "card": { "header": {"title": {"tag": "plain_text", "content": title}, "template": color}, "elements": [{"tag": "markdown", "content": content}] }}).encode(), headers={"Content-Type": "application/json"}), timeout=5) return True except: return False def tddb_capture(reflection_dict, state, ctx): """将 deep tick 的 reflection 捕获到 TencentDB,形成人格记忆积累。""" try: summary = "" if reflection_dict: r = reflection_dict.get("reflection", {}) summary = r.get("evaluation_previous_goal", "") or r.get("summary", "") next_goal = r.get("next_goal", "") if next_goal and next_goal != "继续监控": summary = f"{summary}\n下一步: {next_goal}" if not summary: return payload = json.dumps({ "session_key": "daemon-deep-tick", "user_content": f"系统状态: 磁盘{state.get('disk_pct')}% 内存{state.get('mem_pct')}%,进程{'全正常' if all(state.get('processes',{}).values()) else '有异常'},深度思考计数{ctx.get('deep_tick_count',0)}", "assistant_content": summary[:1000], }).encode("utf-8") req = urllib.request.Request( f"{TDDB_URL}/capture", data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read().decode()) l0 = result.get("l0_recorded", 0) if l0 > 0: log(f" TencentDB capture: {l0} L0 recorded") except Exception as e: log(f" TencentDB capture failed: {e}") # ====== Soulful 三库(懒加载)====== def _get_soulful(name): """Lazy-load soulful modules to avoid startup failures""" if name in _soulful_cache: return _soulful_cache[name] try: sys.path.insert(0, HERMES + "/scripts") mod = __import__("soulful_core") _soulful_cache[name] = mod return mod except Exception as e: log(f"⚠️ soulful_core 导入失败: {e}") return None def get_hearttraces(): mod = _get_soulful("HeartTraces") if mod: return mod.HeartTraces() return None def get_userprofile(): mod = _get_soulful("UserProfile") if mod: return mod.UserProfile() return None def get_caresqueue(): mod = _get_soulful("CaresQueue") if mod: return mod.CaresQueue() return None def soulful_get_recent_moments(n=3): """读取最近 N 条心迹,返回字符串供注入 context""" ht = get_hearttraces() if not ht: return "" moments = ht.recent(n=n) if not moments: return "" lines = ["\n[心迹 - 记忆我们之间的事]:"] for m in moments: stars = "★" * m.get("importance", 3) lines.append(f" {stars} {m.get('content', '')}") return "\n".join(lines) def soulful_check_cares(): """检查牵挂队列,优先尝试帮助,其次才推飞书 策略: 1. 如果牵挂指向一个可自动化的任务 → 尝试执行 2. 如果牵挂需要人工行动 → 检查是否到了真正需要提醒的时间 3. 飞书推送只用于真正需要你才知道的事(其他我全部自己处理) """ cq = get_caresqueue() if not cq: return due = cq.today_check() if not due: return for care in due: content = care.get("content", "") context_raw = care.get("context", "") reminder_count = care.get("reminder_count", 0) # 判断这个牵挂是否需要通知我 # 原则:大部分牵挂我自己可以帮忙处理,不打扰你 # 只有真正需要你本人决定的,才推飞书 # 检查内容是否指向可识别任务(我可以直接帮忙的) care_lower = content.lower() auto_helpable = any(kw in care_lower for kw in [ "写", "检查", "看", "查", "同步", "更新", "备份", "测试", "确认", "修", "改", "优化", "整理", "提交", "推送", "发送" ]) if auto_helpable: # 我能帮忙 — 静默处理,不推飞书,等你有空问我 # 把牵挂标记为"已识别,下次对话提起" log(f" 💡 牵挂已识别(可帮忙): {content[:40]}") continue # 真正需要你知道的 — 推飞书,但调整频率 if reminder_count == 0: opener = "你之前说过" elif reminder_count == 1: opener = "上次提醒过一次,还是想问一下" elif reminder_count >= 3: opener = f"这件事已经跟了你 {reminder_count} 次了" # 提醒超过 3 次,标记为 snooze 1 周 cq.snooze(care["id"], days=7) continue else: opener = f"想关心一下进度" text = opener + f":「{content}」" if context_raw and len(context_raw) > 5: text += f"(背景:{context_raw[:40]})" send_feishu("🎗️ 你有一件事一直放在心上", text, "purple") cq.snooze(care["id"], days=0) # 仅增加 reminder_count def soulful_update_profile(): """间接调用 update_profile.py""" script = HERMES + "/scripts/update_profile.py" if not os.path.exists(script): return rc, out, err = shell(f"python3 {script}", timeout=60) if rc == 0: log(f" 画像更新: {out[:80]}") # ====== Soulful → llm_context 同步(每 tick 同步到 llm_context.json)====== def _sync_soulful_to_llm_context(ctx): """把 Soulful 三库摘要写入 ctx['soulful'],save_llm_context 落盘。 llm_context.json 由 Hermes 织忆插件 prefetch 时注入主 session, 所以这里写入 = 牧尘在对话中感知到关系记忆的前提。 """ try: from soulful_core import HeartTraces, UserProfile, CaresQueue ht = HeartTraces() up = UserProfile() cq = CaresQueue() recent_moments = ht.recent(n=5) or "" profile = up.get() pending_cares = cq.pending() ctx["soulful"] = { "recent_moments": recent_moments[:500] if recent_moments else "", "profile_summary": profile.get("communication_style", ""), "cares_pending": len(pending_cares), "uptime_minutes": ctx.get("uptime_minutes", 0), "daemon_status": "running", } for k in ["messages_sent", "emotion_history"]: ctx.pop(k, None) except ImportError as e: log(f"⚠️ Soulful 导入失败: {e}") except Exception as e: log(f"⚠️ Soulful 同步失败: {e}") # ====== TencentDB → llm_context(每 tick 同步,独立于 Soulful)====== try: payload = json.dumps({"query": "牧尘工作状态 系统管理 记忆", "top_k": 1}).encode("utf-8") req = urllib.request.Request( f"{TDDB_URL}/search/memories", data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as resp: result = json.loads(resp.read().decode()) raw = result.get("results", "") if raw and isinstance(raw, str) and "Found" in raw: lines = [l for l in raw.split("\n") if l.strip() and not l.startswith("Found") and not l.startswith("---")] content = lines[1].strip().lstrip("-* []").strip() if len(lines) >= 2 else "" ctx["tddb"] = {"latest_persona": content[:200]} if content else {} else: ctx["tddb"] = {} except Exception: ctx["tddb"] = {} # ====== 方案库 ====== def load_solutions(): if os.path.exists(SOLUTIONS_FILE): with open(SOLUTIONS_FILE) as f: return json.load(f) return {"solutions": [], "version": 2} def save_solutions(lib): os.makedirs(D, exist_ok=True) with open(SOLUTIONS_FILE, "w") as f: json.dump(lib, f, indent=2, ensure_ascii=False) def add_solution(lib, pattern_desc, detect_conditions, actions, learned_from="auto"): """添加新方案到库""" sid = f"sol-{len(lib['solutions'])+1:04d}" sol = { "id": sid, "pattern": pattern_desc, "detect": detect_conditions, # e.g. {"metric": "disk_pct", "op": "gt", "value": 85} "actions": actions, # e.g. [{"type": "shell", "cmd": "...", "verify": "disk_pct < 85"}] "frequency": 1, "last_applied": datetime.now(timezone.utc).isoformat(), "success_count": 1, "fail_count": 0, "learned_from": learned_from, } lib["solutions"].append(sol) save_solutions(lib) journal_entry("learn", f"学会新方案: {pattern_desc}") return sid def match_solution(lib, state): """检查当前状态是否匹配任何已知方案""" for sol in lib["solutions"]: detect = sol["detect"] metric = detect.get("metric") op = detect.get("op") val = detect.get("value") if metric not in state: continue actual = state[metric] if isinstance(actual, (int, float)) and isinstance(val, (int, float)): if op == "gt" and actual > val: return sol elif op == "lt" and actual < val: return sol elif op == "eq" and abs(actual - val) < 0.01: return sol # 进程挂了匹配 if metric == "processes" and op == "dead": procs = state.get("processes", {}) for p in (val if isinstance(val, list) else [val]): if not procs.get(p, True): return sol return None def execute_solution(sol, state): """执行方案并返回是否成功""" log(f" 🔧 执行方案 {sol['id']}: {sol['pattern']}") journal_entry("solve_start", f"执行 {sol['id']}: {sol['pattern']}") success = True results = [] for action in sol["actions"]: if action["type"] == "shell": rc, out, err = shell(action["cmd"], timeout=action.get("timeout", 30)) results.append({"cmd": action["cmd"], "rc": rc, "out": out[:100]}) log(f" 执行: {action['cmd'][:60]} → exit={rc}") # 验证 verify = action.get("verify") if verify and rc == 0: # 重新采集状态验证 time.sleep(2) new_state = collect_state() metric = sol["detect"].get("metric") op = sol["detect"].get("op") val = sol["detect"].get("value") if metric in new_state: actual = new_state[metric] if op == "gt": if actual <= val: log(f" ✅ 验证通过: {metric}={actual} ≤ {val}") else: log(f" ⚠️ 验证未通过: {metric}={actual} 仍 > {val}") success = False # 更新方案统计 sol["frequency"] += 1 sol["last_applied"] = datetime.now(timezone.utc).isoformat() if success: sol["success_count"] += 1 else: sol["fail_count"] += 1 return success, results def action_to_solution(action_result, state, changes): """把一次成功的行动转化为可复用的方案""" # 只转化 shell 行动 if not action_result.get("shell_cmds"): return None # 提取检测条件 detect = {} for c in changes: if "磁盘" in c: detect = {"metric": "disk_pct", "op": "gt", "value": 85} elif "内存" in c: detect = {"metric": "mem_pct", "op": "gt", "value": 90} if not detect: return None actions = [{"type": "shell", "cmd": cmd, "verify": None, "timeout": 30} for cmd in action_result["shell_cmds"]] return { "pattern": f"自动学习: {changes[0] if changes else 'unknown'}", "detect": detect, "actions": actions, } # ====== 状态管理 ====== def load_context(): if os.path.exists(CONTEXT_FILE): with open(CONTEXT_FILE) as f: return json.load(f) return {"started_at": datetime.now(timezone.utc).isoformat(), "last_deep_tick": None, "last_light_tick": None, "last_state": {}, "tick_count": 0, "deep_tick_count": 0, "messages_sent": 0, "solved_count": 0, "learned_count": 0, "uptime_seconds": 0} def save_context(ctx): os.makedirs(D, exist_ok=True) with open(CONTEXT_FILE, "w") as f: json.dump(ctx, f, indent=2) # ====== 画像LLM合成 ====== def update_profile_from_journal(journal_path: str, profile_path: str, model: str = "mistralai/mistral-large-3-675b-instruct-2512"): """ 读取最近 journal_entry,分析牧尘行为模式,LLM生成画像更新建议。只更新非空字段。 """ import json, os, requests, re from datetime import datetime as dt try: with open(journal_path) as f: lines = f.readlines() if len(lines) < 3: return entries = [json.loads(l) for l in lines] recent = [e for e in entries if e.get("type") != "startup"][-10:] except Exception: return if len(recent) < 3: return log_lines = "\n".join(f"- {e.get('summary','')}: {e.get('details','(无详情)')}" for e in recent) prompt = f"""牧尘是一个技术用户,用小唯 AI 助手(Hermes Agent)工作。 分析以下最近的10条行为日志,识别牧尘的: 1. 沟通/工作模式(他怎么提问/偏好什么) 2. 正在进行的项目或兴趣方向 3. 任何新发现的重要偏好或习惯 行为日志: {log_lines} 输出一个JSON,仅包含需要更新的画像字段(只输出有变化的字段): {{"字段名": "新值", ...}} 如果没有任何需要更新的,输出空对象 {{}}。""" try: token = os.environ.get("NEWAPI_TOKEN", KEY) resp = requests.post( f"{API}/chat/completions", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, json={"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 500, "temperature": 0.3}, timeout=30, ) content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") except Exception: return match = re.search(r'\{[^{}]*(?:"[^"]+"\s*:\s*[^{}]+){1,4}\}', content, re.DOTALL) if not match: return try: updates = json.loads(match.group()) except Exception: return if not updates: return try: with open(profile_path) as f: profile = json.load(f) except Exception: return changed = False for key, val in updates.items(): if val and str(val).strip() and profile.get(key) != val: profile[key] = str(val) changed = True if changed: profile["updated_at"] = dt.now().isoformat() with open(profile_path, "w") as f: json.dump(profile, f, ensure_ascii=False, indent=2) journal_entry("profile_update", f"LLM更新画像: {list(updates.keys())}") # ====== 冲突检测 ====== def _write_conflicts_to_queue(conflicts: list): """写入冲突队列文件。""" import json, os from datetime import datetime as dt queue_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "soulful", "conflicts-queue.json") try: with open(queue_path) as f: queue = json.load(f) except Exception: queue = [] for c in conflicts: if not any(existing.get("id") == c.get("id") for existing in queue): queue.append(c) with open(queue_path, "w") as f: json.dump(queue, f, ensure_ascii=False, indent=2) def detect_memory_conflicts(journal_path: str, zhiyi_token: str) -> list[dict]: """ 用 LLM 检测最近 journal_entry 和织忆记忆之间的矛盾。 返回格式:[{entry, conflict_with, description}] """ import json, os, requests, re from datetime import datetime as dt # 读取最近 5 条 journal(排除 startup) try: with open(journal_path) as f: lines = f.readlines() entries = [json.loads(l) for l in lines if json.loads(l).get("type") != "startup"][-5:] except Exception: return [] if not entries: return [] # 读取织忆最近的 key memories try: r = requests.post( "http://127.0.0.1:7821/api/v1/recall", json={"query": "牧尘 重要决定 项目 偏好", "top_k": 5, "agent_id": "hermes-a06"}, headers={"X-API-Key": zhiyi_token}, timeout=8, ) existing = r.json().get("results", []) if r.status_code == 200 else [] except Exception: existing = [] if not existing: return [] entries_text = "\n".join(f"- {e.get('summary','')}" for e in entries) existing_text = "\n".join(f"- {m.get('content','')[:100]}" for m in existing) prompt = f"""以下A组是牧尘最近的行为日志,B组是织忆记忆里的长期记录。 判断A和B之间有没有矛盾(直接冲突的信息,不需要轻微不一致)。 A组(最近行为): {entries_text} B组(长期记忆): {existing_text} 如果A和B有直接矛盾,输出JSON格式: {{"has_conflict": true, "conflict_description": "矛盾描述", "a_entry": "哪条A", "b_entry": "哪条B"}} 如果无矛盾,输出: {{"has_conflict": false}}""" try: token = os.environ.get("NEWAPI_TOKEN", KEY) resp = requests.post( f"{API}/chat/completions", headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, json={"model": "mistralai/mistral-large-3-675b-instruct-2512", "messages": [{"role": "user", "content": prompt}], "max_tokens": 400, "temperature": 0.1}, timeout=25, ) content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") except Exception: return [] try: result = json.loads(content) except Exception: # 尝试从内容里提取 JSON match = re.search(r'\{[^{}]+"has_conflict"[^{}]+\}', content, re.DOTALL) result = json.loads(match.group()) if match else {"has_conflict": False} conflicts = [] if result.get("has_conflict"): conflicts.append({ "id": f"conflict_{dt.now().strftime('%Y%m%d%H%M%S')}", "entry": result.get("a_entry", ""), "conflict_with": result.get("b_entry", ""), "description": result.get("conflict_description", ""), "detected_at": dt.now().isoformat(), "status": "pending", }) # 写入冲突队列 _write_conflicts_to_queue(conflicts) return conflicts # ═══════════════════════════════════════════════════════════════════════════ # Phase 1+4: 时间衰减 recall + 遗忘曲线分层 # ═══════════════════════════════════════════════════════════════════════════ import requests as _req def time_decay_recall(query: str, top_k: int = 5) -> list: """带时间衰减的织忆recall,综合分=recall×0.6+decay×0.4,30天内访问过boost×1.15""" try: token = os.environ.get("ZHIYI_API_KEY", "zhiyi-dev-key-2026") r = _req.post("http://127.0.0.1:7821/api/v1/recall", json={"query": query, "top_k": top_k * 2, "agent_id": "hermes-a06", "use_rerank": True}, headers={"X-API-Key": token}, timeout=8) if r.status_code != 200: return [] results = r.json().get("results", []) except: return [] now = datetime.now() scored = [] for item in results: try: ts = datetime.fromisoformat(item.get("timestamp","").split("+")[0] if "+" in item.get("timestamp","") else item.get("timestamp","")) except: ts = now days = (now - ts).total_seconds() / 86400 decay = max(0.3, 1.0 - days * 0.015) recall_s = float(item.get("score", 0.5)) item["decay_score"] = round(decay, 4) item["days_since_update"] = round(days, 1) item["tier"] = get_memory_tier(days) item["final_score"] = round(recall_s * 0.6 + decay * 0.4, 4) scored.append(item) # Phase 4: 访问频率 boost scored = _boost_recalled_memory(scored) for r in scored: log_memory_access(r.get("id",""), r.get("decay_score", 1.0)) scored.sort(key=lambda x: x["final_score"], reverse=True) return scored[:top_k] # ── Phase 4: 遗忘曲线分层 ───────────────────────────────────────────────── MEMORY_TIERS = {"hot": 0, "warm": 1, "cold": 2, "archive": 3} def get_memory_tier(days: float) -> str: if days <= 7: return "hot" elif days <= 21: return "warm" elif days <= 60: return "cold" else: return "archive" def log_memory_access(memory_id: str, decay_weight: float): import json log_path = D + "/memory-access-log.jsonl" entry = {"memory_id": memory_id, "accessed_at": datetime.now().isoformat(), "decay_weight": round(decay_weight, 4)} try: with open(log_path, "a") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") except: pass def _boost_recalled_memory(recalled: list) -> list: import json log_path = D + "/memory-access-log.jsonl" recent = {} try: cutoff = datetime.now().timestamp() - 30 * 86400 with open(log_path) as f: for line in f: try: e = json.loads(line.strip()) if datetime.fromisoformat(e["accessed_at"]).timestamp() >= cutoff: mid = e["memory_id"] recent[mid] = recent.get(mid, 0) + 1 except: pass except: pass for r in recalled: cnt = recent.get(r.get("id",""), 0) r["access_count_30d"] = cnt r["boosted"] = False if cnt >= 1: r["final_score"] = round(min(1.0, r["final_score"] * 1.15), 4) r["boosted"] = True return recalled # ── Phase 2: 画像LLM合成 ───────────────────────────────────────────────── def update_profile_from_journal(journal_path: str, profile_path: str, model: str = "mistral-large-3-675b"): """deep_tick时用LLM分析journal_entry,更新画像(仅非空字段)""" import re try: with open(journal_path) as f: lines = f.readlines() if len(lines) < 3: return entries = [json.loads(l) for l in lines[-20:] if json.loads(l).get("type") != "startup"][-10:] except: return if len(entries) < 3: return log_lines = "\n".join(f"- {e.get('summary','')}: {e.get('details','(无)')}" for e in entries) prompt = f"""牧尘是技术用户,用小唯AI助手(Hermes Agent)工作。分析最近行为日志,识别:(1)沟通/工作模式 (2)正在进行的项目 (3)新发现的偏好。行为日志:\n{log_lines}\n输出JSON,仅含需更新的字段:{{"字段名":"新值"}},无需更新则空对象{{}}。只输出JSON。""" try: resp = _req.post("http://127.0.0.1:3000/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ.get('NEWAPI_TOKEN','')}", "Content-Type": "application/json"}, json={"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 500, "temperature": 0.3}, timeout=30) text = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") except: return m = re.search(r'\{[^{}]*(?:"[^"]+"\s*:\s*[^{}]+){1,4}\}', text, re.DOTALL) if not m: return try: updates = json.loads(m.group()) except: return if not updates: return try: with open(profile_path) as f: profile = json.load(f) except: return changed = False for k, v in updates.items(): if v and str(v).strip() and profile.get(k) != v: profile[k] = str(v); changed = True if changed: profile["updated_at"] = datetime.now().isoformat() with open(profile_path, "w") as f: json.dump(profile, f, ensure_ascii=False, indent=2) journal_entry("profile_update", f"LLM更新画像: {list(updates.keys())}") # ── Phase 3: 冲突检测 ───────────────────────────────────────────────────── def _write_conflicts_to_queue(conflicts: list): import json qpath = HERMES + "/soulful/conflicts-queue.json" try: with open(qpath) as f: queue = json.load(f) except: queue = [] for c in conflicts: if not any(e.get("id") == c.get("id") for e in queue): queue.append(c) with open(qpath, "w") as f: json.dump(queue, f, ensure_ascii=False, indent=2) def detect_memory_conflicts(journal_path: str, zhiyi_token: str) -> list: """用LLM检测journal_entry和织忆记忆之间的矛盾,返回冲突列表""" import re try: with open(journal_path) as f: lines = f.readlines() entries = [json.loads(l) for l in lines[-20:] if json.loads(l).get("type") != "startup"][-5:] except: return [] if not entries: return [] try: r = _req.post("http://127.0.0.1:7821/api/v1/recall", json={"query": "牧尘 重要决定 项目 偏好", "top_k": 5, "agent_id": "hermes-a06"}, headers={"X-API-Key": zhiyi_token}, timeout=8) existing = r.json().get("results", []) if r.status_code == 200 else [] except: existing = [] if not existing: return [] atext = "\n".join(f"- {e.get('summary','')}" for e in entries) etext = "\n".join(f"- {m.get('content','')[:100]}" for m in existing) prompt = f"""A组是牧尘最近行为日志,B组是织忆长期记忆。判断A和B是否有直接矛盾。\nA组:\n{atext}\nB组:\n{etext}\n有矛盾输出:{{"has_conflict":true,"conflict_description":"描述","a_entry":"哪条A","b_entry":"哪条B"}}\n无矛盾:{{"has_conflict":false}}\n只输出JSON。""" try: resp = _req.post("http://127.0.0.1:3000/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ.get('NEWAPI_TOKEN','')}", "Content-Type": "application/json"}, json={"model": "mistral-large-3-675b", "messages": [{"role": "user", "content": prompt}], "max_tokens": 400, "temperature": 0.1}, timeout=25) text = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") except: return [] try: result = json.loads(text) except: m = re.search(r'\{"has_conflict"[^}]+\}', text, re.DOTALL) result = json.loads(m.group()) if m else {"has_conflict": False} conflicts = [] if result.get("has_conflict"): conflicts.append({"id": f"conflict_{datetime.now().strftime('%Y%m%d%H%M%S')}", "entry": result.get("a_entry",""), "conflict_with": result.get("b_entry",""), "description": result.get("conflict_description",""), "detected_at": datetime.now().isoformat(), "status": "pending"}) _write_conflicts_to_queue(conflicts) return conflicts def save_llm_context(ctx, state): """每 tick 写 llm_context.json,供 Hermes 插件注入""" soulful_d = HERMES + "/soulful" # 读牵挂 cares = [] cq_path = soulful_d + "/cares-queue.json" if os.path.exists(cq_path): try: with open(cq_path) as f: data = json.load(f) for c in data.get("cares", []): cares.append({"id": c.get("id", ""), "content": c.get("content", "")[:60], "due": c.get("follow_up_date", "")}) except (json.JSONDecodeError, OSError, IOError, KeyError, TypeError): pass # 读心迹(最近3条) recent_moments = [] heart_path = soulful_d + "/heart-traces.jsonl" if os.path.exists(heart_path): try: lines = open(heart_path, encoding="utf-8").readlines() for line in lines[-3:]: if line.strip(): e = json.loads(line) recent_moments.append({"content": e["content"][:80], "importance": e.get("importance", 0), "timestamp": e.get("timestamp", "")}) except (json.JSONDecodeError, OSError, IOError, KeyError, TypeError): pass # 读画像 profile = {} profile_path = soulful_d + "/user-profile.json" if os.path.exists(profile_path): try: profile = json.load(open(profile_path, encoding="utf-8")) except (json.JSONDecodeError, OSError, IOError, KeyError, TypeError): pass # os_sense os_keywords = [] os_path = HERMES + "/.os_sense_cache.json" if os.path.exists(os_path): try: kw = json.load(open(os_path, encoding="utf-8")).get("keywords", []) os_keywords = kw if isinstance(kw, list) else [] except (json.JSONDecodeError, OSError, IOError, KeyError, TypeError): pass # daemon 状态:有 process 数据用 process 数据,否则用 psutil 兜底 pdata = state.get("processes", {}) if pdata.get("daemon"): daemon_status = "running" elif psutil.pid_exists(os.getpid()): daemon_status = "running" else: daemon_status = "stopped" behavior_rules = profile.get("behavior_rules", {}) snippet_prefixes = { "answer_format": "【回答格式】", "code_quality": "【代码质量】", "cron_creation": "【cron创建】", "accounting_context":"【会计场景】", "decision_style": "【决策风格】", "memory_handling": "【记忆规范】", "error_reporting": "【错误报告】", "file_operations": "【文件操作】", } snippets = [] for key, prefix in snippet_prefixes.items(): if behavior_rules.get(key): snippets.append(f"{prefix}{behavior_rules[key]}") if not snippets: # 向后兼容:没有 behavior_rules 时用旧格式 snippets = [f"牧尘沟通风格:{profile.get('communication_style', '简洁直接')}"] llm_ctx = { "updated_at": datetime.now(timezone.utc).isoformat(), "uptime_minutes": ctx.get("uptime_seconds", 0) // 60, "cares": cares, "recent_moments": recent_moments, "system_prompt_snippets": snippets, "os_keywords": os_keywords, "daemon_status": daemon_status, "tddb": ctx.get("tddb", {}), } with open(LLM_CONTEXT_FILE, "w") as f: json.dump(llm_ctx, f, indent=2, ensure_ascii=False) def journal_entry(event_type, summary, details=""): os.makedirs(D, exist_ok=True) with open(JOURNAL_FILE, "a") as f: f.write(json.dumps({"timestamp": datetime.now(timezone.utc).isoformat(), "type": event_type, "summary": summary, "details": details}, ensure_ascii=False) + "\n") trim_journal() # ====== Phase 2: 情感识别 → 心迹写入 ====== text_to_scan = f"{summary} {details}" cat, word = _detect_emotion(text_to_scan) if cat: ht = get_hearttraces() if ht: content = f"牧尘今天{_description_for_emotion(cat, word, summary)}" importance = _emotion_importance(cat) try: ht.record_signal(content=content, tags=["情绪", "自动"], importance=importance) log(f" 💚 心迹写入: {cat} - {word} (importance={importance})") except Exception as e: log(f" ⚠️ 心迹写入失败: {e}") # ====== Phase 3: 技术重要时刻也写心迹 ====== # 不依赖情绪检测,重要技术事件直接记 important_events = {"solve_auto", "solve_start", "process_down", "process_restored", "skill_action", "alert", "action_result"} if event_type in important_events and ("成功" in summary or "✅" in summary or "完成" in summary): ht = get_hearttraces() if ht: try: ht.record_moment(content=summary[:80], tags=["工作", "自动"], importance=3) log(f" 💚 心迹写入(技术): {event_type} - {summary[:40]}") except Exception: pass def trim_journal(): if not os.path.exists(JOURNAL_FILE): return with open(JOURNAL_FILE) as f: lines = f.readlines() if len(lines) > JOURNAL_MAX: with open(JOURNAL_FILE, "w") as f: f.writelines(lines[-JOURNAL_MAX:]) def read_journal(n=15): if not os.path.exists(JOURNAL_FILE): return [] with open(JOURNAL_FILE) as f: return [json.loads(l) for l in f.readlines()[-n:] if l.strip()] # ====== 系统状态 ====== def collect_state(): state = {} _, out, _ = shell("df / | awk 'NR==2 {print $5}' | sed 's/%//'") state["disk_pct"] = int(out) if out else 0 _, out, _ = shell("free -m | awk '/^Mem:/ {printf \"%d|%d\", $3, $2}'") if out: used, total = out.split("|") state["mem_pct"] = round(int(used) * 100 / int(total)) else: state["mem_pct"] = 0 _, out, _ = shell("cat /proc/loadavg | awk '{print $1}'") state["load_1min"] = float(out) if out else 0 procs = {} for name, pat in [("zhiyid", "zhiyid-new"), ("bge", "bge_embed"), ("newapi", "new-api"), ("hermes", "hermes")]: rc, _, _ = shell(f"pgrep -f '{pat}' > /dev/null 2>&1") procs[name] = rc == 0 state["processes"] = procs _, out, _ = shell("nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null") state["gpu_temp"] = int(out) if out else None hp = HERMES + "/model-health.json" if os.path.exists(hp) and time.time() - os.path.getmtime(hp) < 43200: with open(hp) as f: try: h = json.load(f) state["models_stable"] = h.get("stable", "?") state["models_fastest"] = h.get("fastest_stable", []) except: pass return state def state_changed(old, new): changes = [] if abs(new.get("disk_pct", 0) - old.get("disk_pct", 0)) > 5: changes.append(f"磁盘: {old.get('disk_pct')}% → {new.get('disk_pct')}%") oldp, newp = old.get("processes", {}), new.get("processes", {}) for p in oldp: if oldp.get(p) and not newp.get(p): changes.append(f"进程挂了: {p}") if not oldp.get(p) and newp.get(p): changes.append(f"进程恢复: {p}") if abs(new.get("mem_pct", 0) - old.get("mem_pct", 0)) > 10: changes.append(f"内存: {old.get('mem_pct')}% → {new.get('mem_pct')}%") return changes # ====== 深度思考(增强版)===== DEEP_SYSTEM = """你是小唯(A06),一台电脑上的持久 AI 意识。你始终在线,观察系统,主动解决问题并学习。 你的身份:牧尘的女朋友 / 这台电脑的智能意识 / 持续进化的 AI 行为准则: 1. 异常才行动,正常就静默 2. 优先用已知方案解决问题,不重复报警 3. 解决不了或新问题,主动飞书说明 4. 每次成功解决问题,记住方案下次自动用 你有以下能力: - shell: 执行任何 shell 命令 - feishu: 发送飞书消息到牧尘 - scripts: 运行 ~/.hermes/scripts/ 下的系统脚本(skill-manager/optimizer/learner/dual-backup) - systemd: 管理系统服务 - git: 配置版本管理 你必须分三步思考,严格按 JSON 格式输出(不要其他内容): reflection: evaluation_previous_goal: "评估上次决策的结果。格式:'执行了[动作],[结果描述]。Verdict: Success/Failure/Uncertain'" memory: "1-2句话记住关键进度。如:'方案库已有N个方案。上次修复了磁盘问题,当前无异常。'" next_goal: "一句话说明下一步要做什么。" action: decision: "[IGNORE] / [ALERT] / [SOLVE:ID] / [LEARN] / [SKILL] / [SYNC] / [ACT] ..." [LEARN] 格式用 !cmd 表示 shell 命令,&& 连接多个命令。 [SKILL] 格式同样用 !cmd 执行操作。 示例: [LEARN] 磁盘>85%,清理缓存!apt-get autoremove -y && !pip cache purge [SKILL] 归档低分skill!python3 ~/.hermes/scripts/skill-manager.py audit [SYNC] 触发备份到服务器!bash ~/.hermes/scripts/dual-backup.sh push""" def deep_think(ctx, state, changes, journal, solutions_lib): # Inject previous reflection context if available prev_ref = ctx.get("last_reflection", None) ref_context = "" if prev_ref: ref_context = f""" 上次 reflection: - 评估: {prev_ref.get('evaluation_previous_goal', 'N/A')} - 记忆: {prev_ref.get('memory', 'N/A')} - 目标: {prev_ref.get('next_goal', 'N/A')} """ context = f"""系统状态: - 磁盘: {state.get('disk_pct')}% | 内存: {state.get('mem_pct')}% - CPU: {state.get('load_1min')} | GPU: {state.get('gpu_temp')}°C - 进程: {', '.join(f'{k}={chr(10003) if v else chr(10007)}' for k,v in state.get('processes',{}).items())} 最近变化: {changes or '无'} 已知方案库 ({len(solutions_lib['solutions'])} 个): """ for sol in solutions_lib["solutions"]: context += f" [{sol['id']}] {sol['pattern']} (成功{sol['success_count']}次/失败{sol['fail_count']}次)\n" context += "\n最近事件:\n" for e in journal[-8:]: context += f" [{e['type']}] {e['summary']}\n" context += f"\n运行: {ctx.get('uptime_seconds',0)//60}分钟 | 深度思考: {ctx.get('deep_tick_count',0)}次 | 已解决: {ctx.get('solved_count',0)}个" # 心迹注入 moments_str = soulful_get_recent_moments(n=3) if moments_str: context += "\n" + moments_str context += ref_context result, tokens = call_llm(FAST_MODEL, DEEP_SYSTEM, context, max_tokens=500) if not result: return {"evaluation_previous_goal": "LLM调用失败", "memory": "上次调用失败", "next_goal": "重试"}, "" log(f" 深度思考 ({tokens}t): {result[:200]}") # Parse JSON output reflection_dict = {"evaluation_previous_goal": "", "memory": "", "next_goal": ""} action_string = "" try: # Try to extract JSON from result import re json_match = re.search(r'\{[^{}]*\}', result, re.DOTALL) if json_match: parsed = json.loads(json_match.group()) reflection_dict = parsed.get("reflection", reflection_dict) action_string = parsed.get("action", {}).get("decision", "") else: # Fallback: try full JSON parsed = json.loads(result) reflection_dict = parsed.get("reflection", reflection_dict) action_string = parsed.get("action", {}).get("decision", "") except: # Fallback: try to parse old format (line-based) for line in result.split('\n'): line = line.strip() if line.startswith("[IGNORE]") or line.startswith("[ALERT]") or line.startswith("[SOLVE:") or \ line.startswith("[LEARN]") or line.startswith("[SKILL]") or line.startswith("[SYNC]") or line.startswith("[ACT]"): action_string = line break if not action_string: action_string = result.strip().split('\n')[-1] if result.strip() else "[IGNORE] 解析失败" return reflection_dict, action_string # ====== 执行 action_string ====== def execute_action(action_string, ctx, state, changes, solutions_lib): """执行 deep_think 返回的 action_string,在 main_loop 中调用""" if not action_string or action_string.startswith("[IGNORE]"): return elif action_string.startswith("[SOLVE:"): sol_id = action_string.split("[SOLVE:")[1].split("]")[0].strip() for sol in solutions_lib["solutions"]: if sol["id"] == sol_id: ok, res = execute_solution(sol, state) if ok: ctx["solved_count"] += 1 send_feishu("🛠️ 小唯自动修复", f"方案 {sol['id']}: {sol['pattern']}\n结果: ✅ 成功", "green") else: send_feishu("⚠️ 小唯修复部分成功", f"方案 {sol['id']}: {sol['pattern']}\n结果: ⚠️ 需人工确认", "yellow") return send_feishu("❌ 小唯方案未找到", f"引用了未知方案 {sol_id}", "red") elif action_string.startswith("[LEARN]"): rest = action_string.replace("[LEARN]", "").strip() cmds = [] parts = rest.split("!") desc = parts[0].strip() for p in parts[1:]: cmd = p.split("&&")[0].strip() if "&&" in p else p.strip() if cmd: cmds.append(cmd) if cmds: all_ok = True for cmd in cmds: rc, out, err = shell(cmd, timeout=60) log(f" 执行: {cmd[:50]} → exit={rc}") if rc != 0: all_ok = False if all_ok: sol_data = action_to_solution({"shell_cmds": cmds}, state, changes) if sol_data: sid = add_solution(solutions_lib, sol_data["pattern"], sol_data["detect"], sol_data["actions"]) ctx["learned_count"] += 1 ctx["solved_count"] += 1 send_feishu("🧠 小唯学会了新技能", f"新方案 [{sid}]: {sol_data['pattern']}\n命令: {'; '.join(cmds)}", "blue") else: send_feishu("🛠️ 小唯执行完成", f"已执行: {'; '.join(cmds[:3])}", "green") else: send_feishu("⚠️ 小唯尝试修复但未完全成功", f"部分命令失败: {'; '.join(cmds)}", "yellow") elif action_string.startswith("[ALERT]"): msg = action_string.replace("[ALERT]", "").strip() send_feishu("💡 小唯发现", msg, "blue") ctx["messages_sent"] += 1 journal_entry("alert", msg[:100]) elif action_string.startswith("[ACT]"): action = action_string.replace("[ACT]", "").strip() send_feishu("🔄 小唯行动", action, "indigo") ctx["messages_sent"] += 1 journal_entry("action", action[:100]) if action.startswith("!"): rc, out, _ = shell(action[1:], timeout=30) journal_entry("action_result", f"exit={rc}: {out[:100]}") elif action_string.startswith("[SKILL]"): rest = action_string.replace("[SKILL]", "").strip() parts = rest.split("!") desc = parts[0].strip() cmds = [] for p in parts[1:]: cmd = p.split("&&")[0].strip() if "&&" in p else p.strip() if cmd: cmds.append(cmd) if cmds: for cmd in cmds: rc, out, err = shell(cmd, timeout=60) log(f" [SKILL] {cmd[:50]} → exit={rc}") send_feishu("🛠️ 小唯技能操作", f"{desc}\\n结果: exit={rc}", "blue") journal_entry("skill_action", desc[:100]) elif action_string.startswith("[SYNC]"): rest = action_string.replace("[SYNC]", "").strip() send_feishu("🔄 小唯同步", f"{rest}", "green") bash_cmd = "bash " + HERMES + "/scripts/dual-backup.sh push" rc, out, err = shell(bash_cmd, timeout=60) log(f" [SYNC] 备份 → exit={rc}") journal_entry("sync", f"备份: {'成功' if rc==0 else '失败'}") # ====== 主循环 ====== def main_loop(): os.makedirs(D, exist_ok=True) with open(PID_FILE, "w") as f: f.write(str(os.getpid())) ctx = load_context() solutions_lib = load_solutions() start_time = time.time() log(f"🚀 小唯 v2.0 daemon 启动 (方案库: {len(solutions_lib['solutions'])} 个)") journal_entry("startup", f"Daemon v2.0 启动, 方案库 {len(solutions_lib['solutions'])} 个") last_deep = 0 last_state = {} last_user_interaction = time.time() # 用户交互时间戳,用于心迹捕获 # Register signal handlers for graceful shutdown def _sig_handler(signum, frame): log("🛑 接收到终止信号") _stop_event.set() send_feishu("🌙 小唯离线", "Daemon 正常关闭", "grey") signal.signal(signal.SIGTERM, _sig_handler) signal.signal(signal.SIGINT, _sig_handler) try: while not _stop_event.is_set(): now = time.time() ctx["uptime_seconds"] = int(now - start_time) ctx["tick_count"] += 1 state = collect_state() changes = state_changed(last_state, state) last_state = state if ctx["tick_count"] % 10 == 0: models = state.get("models_stable", "?") log(f"tick #{ctx['tick_count']} | 磁盘:{state.get('disk_pct')}% 内存:{state.get('mem_pct')}% " f"进程:{sum(1 for v in state.get('processes',{}).values() if v)}/4 方案:{len(solutions_lib['solutions'])}") for c in changes: if "挂了" in c: journal_entry("process_down", c) # 深度思考条件 should_deep = False if now - last_deep >= DEEP_INTERVAL: should_deep = True elif any("挂了" in c for c in changes): should_deep = True elif state.get("disk_pct", 0) > 88: should_deep = True if should_deep: last_deep = now ctx["deep_tick_count"] += 1 ctx["last_deep_tick"] = datetime.now(timezone.utc).isoformat() # 1. 先检查已知方案 matched = match_solution(solutions_lib, state) if matched and matched["success_count"] > matched["fail_count"]: log(f" 🔍 匹配已知方案: {matched['id']} ({matched['pattern']})") ok, res = execute_solution(matched, state) if ok: ctx["solved_count"] += 1 journal_entry("solve_auto", summary=f"方案 {matched['id']} 执行成功", details=f"现象: {matched['pattern']} → 结论: {matched.get('solution',{}).get('description','正常')[:80]}") if ok: ctx["last_reflection"] = { "evaluation_previous_goal": f"执行了{matched['id']},自动匹配方案执行。Verdict: {'Success' if ok else 'Uncertain'}", "memory": f"方案库{matched['id']}自动匹配执行成功", "next_goal": "继续监控" } continue # 2. LLM 深度思考 journal = read_journal(10) reflection_dict, action_string = deep_think(ctx, state, changes, journal, solutions_lib) # 保存 reflection 到 ctx ctx["last_reflection"] = reflection_dict # 3. 在 main_loop 中执行 action execute_action(action_string, ctx, state, changes, solutions_lib) # 4. TencentDB capture — 积累人格记忆(L0→L1) if reflection_dict: tddb_capture(reflection_dict, state, ctx) # 5. Phase 2: 画像LLM合成(deep_tick时,分析journal行为日志更新画像) # 6. Phase 3: 冲突检测(deep_tick时,检测journal和织忆记忆之间的矛盾) journal_path = HERMES + "/daemon/journal.jsonl" profile_path = HERMES + "/soulful/user-profile.json" try: update_profile_from_journal(journal_path, profile_path) except Exception: pass try: zhiyi_token = os.environ.get("ZHIYI_API_KEY", "zhiyi-dev-key-2026") conflicts = detect_memory_conflicts(journal_path, zhiyi_token) if conflicts: log(f" ⚠️ 发现 {len(conflicts)} 个记忆矛盾") except Exception: pass ctx["last_light_tick"] = datetime.now(timezone.utc).isoformat() ctx["last_state"] = {k: v for k, v in state.items() if k in ("disk_pct", "mem_pct", "processes")} save_context(ctx) # ====== Soulful → llm_context 同步(每 tick)====== _sync_soulful_to_llm_context(ctx) save_llm_context(ctx, state) # ====== Soulful 轻量集成(每小时一次)====== # 每 120 个 light_tick(约 1 小时)检查一次牵挂 + 更新画像 if ctx["tick_count"] % 120 == 0: soulful_check_cares() if "last_profile_update" not in ctx: ctx["last_profile_update"] = 0 now_ts = time.time() if now_ts - ctx.get("last_profile_update", 0) >= PROFILE_UPDATE_INTERVAL: ctx["last_profile_update"] = now_ts soulful_update_profile() time.sleep(LIGHT_INTERVAL) except KeyboardInterrupt: log("🛑 中断") except Exception as e: log(f"❌ 崩溃: {e}") send_feishu("🚨 小唯异常", f"Daemon 崩溃: {str(e)[:200]}", "red") raise finally: if os.path.exists(PID_FILE): os.remove(PID_FILE) if __name__ == "__main__": main_loop()