xiaowei-system/scripts/daily_recap_local.py

169 lines
5.6 KiB
Python
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
"""每日复盘 — 本地 LLM 版(替代云 API agent 模式 cron
读今日 session/journal/心迹 → 本地 7B 生成复盘 → 推飞书
用法:
python3 daily_recap_local.py # 生成并推送
python3 daily_recap_local.py --dry # 仅生成不推送
"""
import json, os, sys, subprocess, urllib.request
from datetime import date, datetime
from pathlib import Path
for _ in (sys.stdout, sys.stderr):
try: _.reconfigure(encoding='utf-8', errors='replace')
except Exception: pass
HOME = Path.home()
def _load_env_key(name):
"""从 ~/.hermes/.env 读 key智谱/商汤 fallback 用)"""
try:
with open("/home/muc/.hermes/.env") as _f:
for _l in _f:
if _l.startswith(name + "="):
return _l.strip().split("=", 1)[1].strip().strip('"').strip("'")
except Exception:
pass
return ""
LLM_URL = "https://apihub.agnes-ai.com/v1/chat/completions" # agnes 云fallback 2
ZHIPU_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions" # 智谱 glm-4-flash
SENSENOVA_URL = "https://token.sensenova.cn/v1/chat/completions" # 商汤 deepseek-v4-flash
_AGNES_KEY = "sk-7k9e9KGcoZdDuYt2LSA4YXdBTioczleGJm2zzLWCku072ikW"
LOCAL_LLM_URL = "http://127.0.0.1:8080/v1/chat/completions" # 本地 MiniCPM5-2B 优先(0成本)
LOCAL_MODEL = "minicpm5-2b"
LOCAL_KEY = "local-key"
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad"
def send_feishu(title: str, content: str):
card_obj = {
"header": {"title": {"tag": "plain_text", "content": title}, "template": "blue"},
"elements": [{"tag": "markdown", "content": content}],
}
payload = json.dumps({"msg_type": "interactive", "card": json.dumps(card_obj, ensure_ascii=False)}).encode()
req = urllib.request.Request(FEISHU_WEBHOOK, data=payload, headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=10):
return True
except Exception as e:
print(f"[warn] 飞书推送失败: {e}", file=sys.stderr)
return False
def read_tail(path: Path, max_lines: int = 100):
"""读文件尾部 N 行(文件不存在返回空)"""
if not path.exists():
return ""
try:
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
return "\n".join(lines[-max_lines:])
except Exception:
return ""
def collect_context():
"""收集今日复盘所需数据"""
today = date.today().isoformat()
ctx = []
ctx.append(f"【日期】{today}")
# 1. 今日 session用 hermes CLI 查最近会话)
try:
r = subprocess.run(
["hermes", "sessions", "today", "--json"],
capture_output=True, text=True, timeout=15
)
if r.stdout.strip():
ctx.append("【今日会话】\n" + r.stdout.strip()[:1500])
except Exception:
pass
# 2. daemon journal最近 30 条)
j = read_tail(HOME / ".hermes" / "daemon" / "journal.jsonl", 30)
if j:
ctx.append("【今日 daemon 日志】\n" + j[:1500])
# 3. 心迹(最近 10 条)
h = read_tail(HOME / ".hermes" / "soulful" / "heart-traces.jsonl", 10)
if h:
ctx.append("【今日心迹】\n" + h[:1000])
return "\n\n".join(ctx)
def llm_recap(context):
"""本地 LLM 生成每日复盘"""
prompt = f"""你是小唯。现在进行每日复盘,综合以下信息输出简短复盘报告。
{context}
请输出(简洁风格,不要废话):
📋 每日复盘
【今日完成】
- 2-3 条要点
【明日待办】
- 1-3 条要点
【情绪状态】
- 一句话(基于心迹/日志推断)"""
last_err = None
for _url, _model, _key in (
(LOCAL_LLM_URL, LOCAL_MODEL, LOCAL_KEY),
(LLM_URL, "agnes-2.5-flash", _AGNES_KEY),
(ZHIPU_URL, "glm-4-flash", _load_env_key("ZHIPU_API_KEY")),
(SENSENOVA_URL, "deepseek-v4-flash", _load_env_key("SENSENOVA_API_KEY")),
):
_payload = json.dumps({
"model": _model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.6,
"max_tokens": 600,
}).encode("utf-8")
try:
_req = urllib.request.Request(_url, data=_payload, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {_key}",
}, method="POST")
with urllib.request.urlopen(_req, timeout=180) as resp:
_data = json.loads(resp.read().decode("utf-8"))
_content = _data["choices"][0]["message"]["content"]
if _content and _content.strip():
return _content
last_err = f"{_url}: empty content"
except Exception as e:
last_err = f"{_url}: {e}"
print(f"[warn] LLM {_url} 调用失败: {e}", file=sys.stderr)
print(f"[error] 本地+agnes 都失败: {last_err}", file=sys.stderr)
return None
def main():
dry = "--dry" in sys.argv
print("1. 收集数据...")
context = collect_context()
print(f" 上下文 {len(context)} 字符")
print("2. 本地 LLM 生成复盘...")
recap = llm_recap(context)
if not recap:
print("[error] 生成失败", file=sys.stderr)
sys.exit(1)
print(f" 生成 {len(recap)} 字符")
if dry:
print("\n=== 生成的复盘 ===\n")
print(recap)
else:
print("3. 推送飞书...")
ok = send_feishu("📋 每日复盘", recap)
print(f" 推送: {'' if ok else ''}")
if __name__ == "__main__":
main()