xiaowei-system/scripts/daemon.py

1301 lines
53 KiB
Python
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 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 = "stepfun-ai/step-3.5-flash"
DEEP_MODEL = "stepfun-ai/step-3.5-flash"
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 log_reasoning_step(step_type, message, data=None):
"""结构化推理日志"""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
payload = json.dumps({"type": step_type, "msg": message, "data": data}, ensure_ascii=False)
line = f"[DAEMON] {ts} [{step_type}] {message}"
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)
# Phase 2画像LLM合成 和 Phase 3冲突检测 在下方
# ═══════════════════════════════════════════════════════════════════════════
import requests as _req
def time_decay_recall(query: str, top_k: int = 5) -> list:
"""带时间衰减的织忆recall综合分=recall×0.6+decay×0.430天内访问过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]
# ═══════════════════════════════════════════════════════════════════════════
# Corrective RAG: LLM相关性评分 + 不相关时触发重新检索
# 参考: awesome-llm-apps/rag_tutorials/corrective_rag
# 流程: recall → LLM评分相关性 → 不相关>50%则改写query重新检索
# ═══════════════════════════════════════════════════════════════════════════
def _grade_single_relevance(query: str, result_item: dict) -> str:
"""用LLM判断单条记忆是否与query相关。返回: relevant / irrelevant / partially_relevant"""
content = result_item.get("content", "")[:300]
prompt = f"""判断以下记忆是否与查询相关。
查询: {query}
记忆: {content}
只输出一个词: relevant / irrelevant / partially_relevant"""
try:
token = os.environ.get("NEWAPI_TOKEN", KEY)
resp = _req.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": 10, "temperature": 0.1},
timeout=10,
)
grade = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip().lower()
if "relevant" in grade and "partially" not in grade and "irrelevant" not in grade:
return "relevant"
elif "irrelevant" in grade:
return "irrelevant"
return "partially_relevant"
except Exception:
return "partially_relevant"
def _expand_query(query: str) -> str:
"""将原query改写成更全面的检索表达用于重新检索"""
prompt = f"""将以下查询改写成更全面、可能包含同义词的检索表达。
原查询: {query}
改写(只输出改写后的查询,不要解释):"""
try:
token = os.environ.get("NEWAPI_TOKEN", KEY)
resp = _req.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": 60, "temperature": 0.2},
timeout=10,
)
expanded = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "").strip()
return expanded if expanded else query
except Exception:
return query
def corrective_recall(query: str, top_k: int = 5) -> list:
"""
Corrective RAG recall: 时间衰减recall + LLM相关性评分。
如果超过50%的结果不相关改写query重新检索并合并。
"""
results = time_decay_recall(query, top_k=top_k * 2)
# LLM相关性评分
graded = []
irrelevant = 0
for item in results:
grade = _grade_single_relevance(query, item)
item["relevance_grade"] = grade
if grade == "irrelevant":
irrelevant += 1
graded.append(item)
total = len(graded) or 1
ir_ratio = irrelevant / total
# 超过50%不相关 → 改写query重新检索
if ir_ratio > 0.5 and total >= 4:
expanded = _expand_query(query)
if expanded != query:
re_results = time_decay_recall(expanded, top_k=top_k)
for item in re_results:
item["relevance_grade"] = "relevance_from_expanded_query"
item["original_query"] = query
item["expanded_query"] = expanded
# 合并去重按id
seen = {r.get("id") for r in graded if r.get("id")}
merged = graded + [r for r in re_results if r.get("id") not in seen]
merged.sort(key=lambda x: x["final_score"], reverse=True)
return merged[:top_k]
graded.sort(key=lambda x: x["final_score"], reverse=True)
return graded[: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 = FAST_MODEL):
"""deep_tick时用LLM分析journal_entry更新画像仅非空字段"""
import re
try:
with open(journal_path) as f:
lines = f.readlines()
if len(lines) < 3: return
entries = (lambda L: [e for e in (json.loads(l) for l in L) if e.get("type") != "startup"][-10:])(lines[-20:])
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(f"{API}/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 = (lambda L: [e for e in (json.loads(l) for l in L) if e.get("type") != "startup"][-5:])(lines[-20:])
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": FAST_MODEL, "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()
# ── 推理轨迹deep_tick 开始 ─────────────────────────────────
log_reasoning_step("thinking", f"deep_tick #{ctx['deep_tick_count']} 触发 | disk={state.get('disk_pct')}%", {"changes": changes[:3]})
# 1. 先检查已知方案
matched = match_solution(solutions_lib, state)
if matched and matched["success_count"] > matched["fail_count"]:
log(f" 🔍 匹配已知方案: {matched['id']} ({matched['pattern']})")
log_reasoning_step("judge", f"方案匹配: {matched['id']}", matched)
ok, res = execute_solution(matched, state)
if ok:
ctx["solved_count"] += 1
log_reasoning_step("action", f"执行成功: {matched['id']}", res)
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 深度思考
log_reasoning_step("thinking", "调用 deep_think LLM", {"tick": ctx["deep_tick_count"]})
journal = read_journal(10)
reflection_dict, action_string = deep_think(ctx, state, changes, journal, solutions_lib)
# 保存 reflection 到 ctx
ctx["last_reflection"] = reflection_dict
log_reasoning_step("judge", f"deep_think 结论: {reflection_dict.get('next_goal','?')}", reflection_dict)
# 3. 在 main_loop 中执行 action
execute_action(action_string, ctx, state, changes, solutions_lib)
log_reasoning_step("action", f"action 执行完毕", {"action": action_string[:80] if action_string else None})
# 4. TencentDB capture — 积累人格记忆L0→L1
if reflection_dict:
tddb_capture(reflection_dict, state, ctx)
log_reasoning_step("recall", "TencentDB L1 提取完成", reflection_dict)
# 5. Phase 2: 画像LLM合成
# 6. Phase 3: 冲突检测
journal_path = HERMES + "/daemon/journal.jsonl"
profile_path = HERMES + "/soulful/user-profile.json"
try:
update_profile_from_journal(journal_path, profile_path)
log_reasoning_step("recall", "Phase2: 画像更新完成", {})
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)} 个记忆矛盾")
log_reasoning_step("recall", f"Phase3: 发现{len(conflicts)}个矛盾", {"conflicts": len(conflicts)})
except Exception:
pass
log_reasoning_step("action", f"deep_tick #{ctx['deep_tick_count']} 全部完成", {})
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()