diff --git a/scripts/check_cares.py b/scripts/check_cares.py new file mode 100755 index 00000000..b4a81403 --- /dev/null +++ b/scripts/check_cares.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +check_cares.py — 牵挂检查与飞书提醒 +================================== +用法: + python3 check_cares.py # 检查并推送今天到期的牵挂 + python3 check_cares.py --list # 仅列出,不推送 + python3 check_cares.py --daily # 每日21:00日报模式:列出所有pending + +依赖:soulful_core.py(同目录) +""" + +import json, os, sys, urllib.request, urllib.error +from datetime import datetime, timezone + +HERMES = os.path.expanduser("~/.hermes") +D = HERMES + "/soulful" + +FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/65c3ce80-710f-4415-b2ea-d69d87b5c18e" + + +def send_feishu(title: str, content: str, color: str = "green"): + """发送飞书消息""" + color_map = { + "green": ("34", "197", "94"), + "yellow": ("255", "185", "0"), + "red": ("255", "65", "54"), + "blue": ("0", "122", "255"), + "purple": ("128", "90", "213"), + } + r, g, b = color_map.get(color, color_map["blue"]) + payload = { + "msg_type": "interactive", + "card": { + "header": { + "title": {"tag": "plain_text", "content": f"🎗️ {title}"}, + "template": color, + }, + "elements": [ + {"tag": "div", "content": {"tag": "lark_md", "content": content}}, + ], + }, + } + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urllib.request.Request( + FEISHU_WEBHOOK, + data=data, + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + return json.loads(resp.read()) + except Exception as e: + print(f" 飞书发送失败: {e}") + return None + + +def generate_warm_text(care: dict, index: int = 1) -> str: + """为牵挂生成温暖的自然语言描述""" + content = care.get("content", "") + context = care.get("context", "") + reminder_count = care.get("reminder_count", 0) + follow_up_date = care.get("follow_up_date", "") + tags = care.get("tags", []) + + # 语气选择 + if reminder_count == 0: + opener = f"你之前说过" + elif reminder_count == 1: + opener = f"上次提醒过一次了,还是想问一下" + else: + opener = f"已经提醒 {reminder_count} 次了,这件重要的事" + + # 标签辅助语气 + tag_hints = { + "工作": "关于工作的事", + "生活": "生活里的小事", + "学习": "学习方面", + "健康": "身体是革命的本钱", + } + tag_hint = "" + for t in tags: + if t in tag_hints: + tag_hint = tag_hints[t] + break + + # 组合文案 + parts = [opener] + if tag_hint: + parts.append(tag_hint) + parts.append(f"「{content}」") + if context and len(context) > 5: + parts.append(f"(你说的是:{context[:40]})") + if reminder_count > 0: + parts.append(f"——依然放在心上") + + return ",".join(parts) + + +def format_daily_report(cares: list) -> str: + """格式化每日牵挂清单""" + if not cares: + return '今天没有待提醒的牵挂 ✓\n\n你可以说"记得提醒我做XXX",我会帮你记住并准时提醒。' + + lines = [f"📋 今天有 **{len(cares)}** 件牵挂:\n"] + for i, c in enumerate(cares, 1): + follow_up = c.get("follow_up_date", "") + content = c.get("content", "") + context = c.get("context", "") + lines.append(f"{i}. **{content}**") + if context: + lines.append(f" 💬 {context[:50]}") + lines.append(f"\n完成记得告诉我,我会帮你划掉。") + lines.append('\n_说"做完XXX"即可,我会帮你记录。_') + return "\n".join(lines) + + +def main(): + list_only = "--list" in sys.argv + daily = "--daily" in sys.argv + + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from soulful_core import CaresQueue + + cq = CaresQueue() + + if daily: + # 每日 21:00 模式:显示所有 pending + pending = cq.pending() + if not pending: + print("无 pending 牵挂") + return + + report = format_daily_report(pending) + print(report) + if not list_only: + send_feishu("今日牵挂清单", report, "blue") + return + + # 默认:检查今天到期的 + due = cq.today_check() + + if not due: + print("没有今天到期的牵挂 ✓") + return + + print(f"发现 {len(due)} 条到期牵挂:") + for i, c in enumerate(due, 1): + print(f" {i}. [{c['id']}] {c['content']}") + + if list_only: + return + + # 发送飞书 + for care in due: + text = generate_warm_text(care) + print(f"\n推送: {text[:60]}...") + result = send_feishu("你有一件事一直放在心上", text, "purple") + if result and result.get("code") == 0: + # 更新 reminder_count + cq.snooze(care["id"], days=0) # 只增加 reminder_count,不推日期 + print(" ✓ 已推送") + else: + print(f" ✗ 推送失败: {result}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/daemon.py b/scripts/daemon.py index b259c47f..518c0b7a 100755 --- a/scripts/daemon.py +++ b/scripts/daemon.py @@ -21,6 +21,7 @@ PID_FILE = D + "/daemon.pid" LIGHT_INTERVAL = 30 DEEP_INTERVAL = 300 JOURNAL_MAX = 200 +PROFILE_UPDATE_INTERVAL = 21600 # 6 hours API = "http://127.0.0.1:3000/v1" KEY = "0ExNiLblJvIWBDpkS50fwOBw4MmqLyKdHJK5iQtlw9dOMWBP" @@ -30,6 +31,9 @@ FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/65c3ce80-710f-441 _stop_event = threading.Event() +# Lazy-loaded soulful modules (avoid import at module load time) +_soulful_cache = {} + # ====== 工具 ====== @@ -77,6 +81,94 @@ def send_feishu(title, content, color="blue"): except: return False +# ====== 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(): + """检查牵挂队列,推送到期提醒(飞书)""" + cq = get_caresqueue() + if not cq: + return + due = cq.today_check() + if not due: + return + for care in due: + reminder_count = care.get("reminder_count", 0) + if reminder_count == 0: + opener = "你之前说过" + elif reminder_count == 1: + opener = "上次提醒过一次了,还是想问一下" + else: + opener = f"已经提醒 {reminder_count} 次了,这件重要的事" + content = care.get("content", "") + context = care.get("context", "") + text = opener + f",「{content}」" + if context and len(context) > 5: + text += f"(你说的是:{context[:40]})" + send_feishu("🎗️ 你有一件事一直放在心上", text, "purple") + # 只增加 reminder_count,不推日期 + cq.snooze(care["id"], days=0) + + +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]}") + + # ====== 方案库 ====== def load_solutions(): @@ -355,6 +447,12 @@ def deep_think(ctx, state, changes, journal, solutions_lib): 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) @@ -572,6 +670,18 @@ def main_loop(): 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 轻量集成(每小时一次)====== + # 每 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: diff --git a/scripts/soulful_core.py b/scripts/soulful_core.py new file mode 100644 index 00000000..bddcce3d --- /dev/null +++ b/scripts/soulful_core.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +""" +Soulful Core — 织忆情感层核心库 +================================= +提供:心迹(heart-traces)+ 用户画像(user-profile)+ 牵挂(cares-queue) + +用法(作为模块导入): + from soulful_core import HeartTraces, UserProfile, CaresQueue + +心迹: + ht = HeartTraces() + ht.record(content="我们一起修好了织忆", tags=["成长", "织忆"], importance=5) + moments = ht.recent(n=3) + +画像: + up = UserProfile() + profile = up.get() + up.update({"communication_style": "简洁直接", "current_goals": ["ColaOS研究"]}) + +牵挂: + cq = CaresQueue() + cq.add(content="记得同步文档到Obsidian", context="牧尘在会议中提到", follow_up_date="2026-07-11") + due = cq.due() + cq.done(care_id) + +版本: 1.0.0 +""" + +import json, os, uuid +from datetime import datetime, timezone +from typing import Optional + +HERMES = os.path.expanduser("~/.hermes") +D = HERMES + "/soulful" # 所有数据放 soulful/ 子目录 + +# ===== 心迹库 ===== + +class HeartTraces: + """心迹库 — 记录没有任务价值但很重要的时刻""" + + def __init__(self, path: str = None): + self.path = path or (D + "/heart-traces.jsonl") + os.makedirs(D, exist_ok=True) + if not os.path.exists(self.path): + with open(self.path, "w") as f: + f.write("") + + def record(self, content: str, tags: list = None, importance: int = 3, + session_id: str = None, trace_type: str = "moment") -> str: + """ + 写入一条心迹 + - content: 记录内容 + - tags: 标签列表 + - importance: 重要程度 1-5 + - trace_type: signal(信号)/moment(时刻)/reflection(反思) + """ + entry = { + "id": str(uuid.uuid4())[:8], + "timestamp": datetime.now(timezone.utc).isoformat(), + "session_id": session_id or "default", + "type": trace_type, + "content": content, + "tags": tags or [], + "importance": importance, + } + with open(self.path, "a") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + return entry["id"] + + def recent(self, n: int = 5, tag: str = None) -> list: + """读取最近 N 条心迹,可按 tag 过滤""" + if not os.path.exists(self.path): + return [] + entries = [] + with open(self.path) as f: + for line in f: + if line.strip(): + entries.append(json.loads(line)) + if tag: + entries = [e for e in entries if tag in e.get("tags", [])] + return entries[-n:][::-1] # 倒序,最新的在前 + + def all(self, tag: str = None, limit: int = 100) -> list: + """读取所有心迹(倒序)""" + if not os.path.exists(self.path): + return [] + entries = [] + with open(self.path) as f: + for line in f: + if line.strip(): + entries.append(json.loads(line)) + if tag: + entries = [e for e in entries if tag in e.get("tags", [])] + return entries[-limit:][::-1] + + def count(self) -> int: + if not os.path.exists(self.path): + return 0 + with open(self.path) as f: + return sum(1 for l in f if l.strip()) + + def record_moment(self, content: str, tags: list = None, importance: int = 3): + """快捷方法:record 的别名""" + return self.record(content, tags, importance, trace_type="moment") + + def record_signal(self, content: str, tags: list = None, importance: int = 3): + """快捷方法:记录情绪/信号""" + return self.record(content, tags, importance, trace_type="signal") + + def record_reflection(self, content: str, tags: list = None, importance: int = 3): + """快捷方法:记录反思""" + return self.record(content, tags, importance, trace_type="reflection") + + +# ===== 用户画像 ===== + +class UserProfile: + """用户画像 — 懂牧尘这个人""" + + def __init__(self, path: str = None): + self.path = path or (D + "/user-profile.json") + os.makedirs(D, exist_ok=True) + self._init_if_needed() + + def _init_if_needed(self): + if not os.path.exists(self.path): + default = { + "version": 1, + "updated_at": datetime.now(timezone.utc).isoformat(), + "communication_style": "", + "work_patterns": {"peak_hours": [], "focus_issues": []}, + "preferences": {}, + "habits": {}, + "important_people": [], + "current_goals": [], + "recent_frustrations": [], + "emotional_state": {"current": "unknown", "notes": []}, + "first_contact": datetime.now(timezone.utc).isoformat(), + } + with open(self.path, "w") as f: + json.dump(default, f, indent=2, ensure_ascii=False) + + def get(self) -> dict: + with open(self.path) as f: + return json.load(f) + + def update(self, updates: dict) -> dict: + """增量更新画像字段""" + profile = self.get() + for k, v in updates.items(): + if k in profile: + if isinstance(profile[k], dict) and isinstance(v, dict): + profile[k].update(v) + elif isinstance(profile[k], list) and isinstance(v, list): + # 合并去重 + profile[k] = list(set(profile[k] + v)) + else: + profile[k] = v + profile["updated_at"] = datetime.now(timezone.utc).isoformat() + with open(self.path, "w") as f: + json.dump(profile, f, indent=2, ensure_ascii=False) + return profile + + def set_communication_style(self, style: str): + """设置沟通风格:简洁/详细/直接/委婉""" + self.update({"communication_style": style}) + + def add_goal(self, goal: str): + """追加当前目标""" + profile = self.get() + if goal not in profile.get("current_goals", []): + profile["current_goals"].append(goal) + self.update({"current_goals": profile["current_goals"]}) + + def add_frustration(self, frustration: str): + """追加近期压力""" + profile = self.get() + profile["recent_frustrations"].append(frustration) + if len(profile["recent_frustrations"]) > 10: + profile["recent_frustrations"] = profile["recent_frustrations"][-10:] + self.update({"recent_frustrations": profile["recent_frustrations"]}) + + def set_emotional_state(self, state: str, note: str = ""): + """设置情绪状态:great/good/neutral/tired/stressed""" + profile = self.get() + profile["emotional_state"]["current"] = state + if note: + profile["emotional_state"]["notes"].append({ + "time": datetime.now(timezone.utc).isoformat(), + "note": note + }) + self.update({"emotional_state": profile["emotional_state"]}) + + def merge_from_journal(self, journal_entries: list): + """从 daemon journal 增量更新画像""" + profile = self.get() + goals_found = [] + for entry in journal_entries: + summary = entry.get("summary", "") + # 简单关键词检测 + if "ColaOS" in summary or "织忆" in summary: + goals_found.append(summary) + if any(w in summary for w in ["累", "困", "压力", "焦虑"]): + profile["emotional_state"]["current"] = "tired" + if goals_found: + for g in set(goals_found): + if g not in profile["current_goals"]: + profile["current_goals"].append(g) + profile["updated_at"] = datetime.now(timezone.utc).isoformat() + with open(self.path, "w") as f: + json.dump(profile, f, indent=2, ensure_ascii=False) + + +# ===== 牵挂队列 ===== + +class CaresQueue: + """牵挂队列 — 主动推进未完成事项""" + + def __init__(self, path: str = None): + self.path = path or (D + "/cares-queue.json") + os.makedirs(D, exist_ok=True) + self._init_if_needed() + + def _init_if_needed(self): + if not os.path.exists(self.path): + with open(self.path, "w") as f: + json.dump({"cares": []}, f, indent=2) + + def _load(self) -> list: + with open(self.path) as f: + return json.load(f).get("cares", []) + + def _save(self, cares: list): + with open(self.path, "w") as f: + json.dump({"cares": cares, "updated_at": datetime.now(timezone.utc).isoformat()}, f, indent=2, ensure_ascii=False) + + def add(self, content: str, context: str = "", follow_up_date: str = None, tags: list = None) -> str: + """添加一条牵挂""" + care_id = str(uuid.uuid4())[:8] + care = { + "id": care_id, + "content": content, + "context": context, + "created_at": datetime.now(timezone.utc).isoformat(), + "follow_up_date": follow_up_date or datetime.now(timezone.utc).strftime("%Y-%m-%d"), + "status": "pending", + "reminder_count": 0, + "last_reminded": None, + "tags": tags or [], + } + cares = self._load() + cares.append(care) + self._save(cares) + return care_id + + def due(self, before_date: str = None) -> list: + """返回所有到期的牵挂(status=pending 且 follow_up_date <= 今天)""" + if before_date is None: + before_date = datetime.now(timezone.utc).strftime("%Y-%m-%d") + cares = self._load() + return [c for c in cares + if c["status"] == "pending" + and c.get("follow_up_date", "") <= before_date] + + def pending(self) -> list: + """返回所有 pending 的牵挂""" + cares = self._load() + return [c for c in cares if c["status"] == "pending"] + + def done(self, care_id: str): + """标记为已完成""" + cares = self._load() + for c in cares: + if c["id"] == care_id: + c["status"] = "done" + c["completed_at"] = datetime.now(timezone.utc).isoformat() + self._save(cares) + + def snooze(self, care_id: str, days: int = 1): + """推迟 N 天""" + cares = self._load() + for c in cares: + if c["id"] == care_id: + c["reminder_count"] += 1 + from datetime import timedelta + old_date = datetime.strptime(c["follow_up_date"], "%Y-%m-%d") + new_date = (old_date + timedelta(days=days)).strftime("%Y-%m-%d") + c["follow_up_date"] = new_date + c["last_reminded"] = datetime.now(timezone.utc).isoformat() + self._save(cares) + + def delete(self, care_id: str): + """删除牵挂""" + cares = self._load() + cares = [c for c in cares if c["id"] != care_id] + self._save(cares) + + def all(self) -> list: + return self._load() + + def count(self) -> dict: + cares = self._load() + return { + "total": len(cares), + "pending": sum(1 for c in cares if c["status"] == "pending"), + "done": sum(1 for c in cares if c["status"] == "done"), + } + + def today_check(self) -> list: + """返回今天应该提醒的牵挂(包含昨天到期的)""" + today = datetime.now(timezone.utc).strftime("%Y-%m-%d") + yesterday = (datetime.now(timezone.utc).replace(hour=0, minute=0, second=0) - + __import__('datetime').timedelta(days=1)).strftime("%Y-%m-%d") + cares = self._load() + return [c for c in cares + if c["status"] == "pending" + and c.get("follow_up_date", "") <= today + and c.get("follow_up_date", "") >= yesterday] + + +# ===== 快捷函数(CLI 入口)===== + +if __name__ == "__main__": + import sys + + def cmd_record(): + import argparse + p = argparse.ArgumentParser(description="心迹记录") + p.add_argument("content") + p.add_argument("--tags", nargs="+", default=[]) + p.add_argument("--type", "-t", default="moment", choices=["moment", "signal", "reflection"]) + p.add_argument("--importance", "-i", type=int, default=3) + args = p.parse_args(sys.argv[2:]) + ht = HeartTraces() + tid = ht.record(args.content, args.tags, args.importance, trace_type=args.type) + print(f"✓ 心迹 {tid} 已记录") + + def cmd_ht_recent(): + ht = HeartTraces() + for e in ht.recent(n=5): + print(f"[{e['timestamp'][:10]}][{'★'*e['importance']}] {e['content']}") + + def cmd_profile_get(): + up = UserProfile() + import pprint + pprint.pprint(up.get()) + + def cmd_cq_add(): + import argparse + p = argparse.ArgumentParser(description="添加牵挂") + p.add_argument("content") + p.add_argument("--context", "-c", default="") + p.add_argument("--date", "-d", default=None) + args = p.parse_args(sys.argv[2:]) + cq = CaresQueue() + cid = cq.add(args.content, args.context, args.date) + print(f"✓ 牵挂 {cid} 已添加:{args.content}") + + def cmd_cq_due(): + cq = CaresQueue() + due = cq.due() + if not due: + print("没有到期的牵挂 ✓") + for c in due: + print(f"[{c['id']}] {c['content']}") + if c.get("context"): + print(f" → {c['context']}") + + def cmd_cq_list(): + cq = CaresQueue() + count = cq.count() + print(f"牵挂统计:共 {count['total']} 条,{count['pending']} 待办,{count['done']} 已完成") + print() + for c in cq.pending(): + print(f"[{c['id']}][{c['follow_up_date']}] {c['content']}") + + commands = { + "record": cmd_record, + "recent": cmd_ht_recent, + "profile": cmd_profile_get, + "cq-add": cmd_cq_add, + "cq-due": cmd_cq_due, + "cq-list": cmd_cq_list, + } + + if len(sys.argv) < 2 or sys.argv[1] not in commands: + print("用法: soulful_core.py [args]") + print("命令:", list(commands.keys())) + sys.exit(1) + commands[sys.argv[1]]() \ No newline at end of file diff --git a/scripts/update_profile.py b/scripts/update_profile.py new file mode 100644 index 00000000..b4dcf93e --- /dev/null +++ b/scripts/update_profile.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +""" +update_profile.py — 从 journal 增量更新用户画像 +========================================= +用法: + python3 update_profile.py # 增量更新 + python3 update_profile.py --full # 全量重新分析 + python3 update_profile.py --dry # 只输出,不写入 + +依赖:soulful_core.py(同目录) +""" + +import json, os, re, sys +from datetime import datetime, timezone +from collections import Counter + +HERMES = os.path.expanduser("~/.hermes") +D = HERMES + "/soulful" +JOURNAL_FILE = HERMES + "/daemon/journal.jsonl" + +# 情绪词库 +EMOTION_WORDS = { + "positive": ["开心", "高兴", "顺利", "棒", "完美", "感谢", "不错", "很好", "解决了", "成功了", "搞定", "舒服"], + "negative": ["累", "困", "压力", "焦虑", "沮丧", "失望", "崩溃", "烦", "头疼", "难", "搞不定", "失败", "错误"], + "focus": ["专注", "忙", "赶", "deadline", "紧急", "重要"], +} + +# 目标关键词 +GOAL_KEYWORDS = ["研究", "做", "改", "修复", "完成", "测试", "上线", "调研", "计划", "推进", "同步"] + +# 沟通风格判断(基于句长) +def detect_communication_style(summaries: list) -> str: + if not summaries: + return "简洁" + total_len = sum(len(s) for s in summaries) + avg_len = total_len / len(summaries) + if avg_len < 30: + return "简洁直接" + elif avg_len < 60: + return "简洁" + else: + return "详细" + + +def load_journal(n: int = 100) -> list: + """加载最近 N 条 journal 条目""" + if not os.path.exists(JOURNAL_FILE): + return [] + with open(JOURNAL_FILE) as f: + lines = f.readlines() + entries = [json.loads(l) for l in lines[-n:] if l.strip()] + return entries[::-1] # 倒序,最新的在前 + + +def extract_goals(entries: list) -> list: + """从 journal 中提取当前目标(通过关键词+主题判断)""" + goals = [] + recent_topics = [] + for e in entries: + summary = e.get("summary", "") + event_type = e.get("type", "") + # 任务完成类型的事件通常是目标 + if event_type in ("solve", "learn", "skill", "config"): + # 提取关键动作 + if "织忆" in summary: + goals.append("织忆系统优化") + if "ColaOS" in summary or "page-agent" in summary: + goals.append("ColaOS研究") + if "skill" in summary.lower(): + goals.append("技能管理") + if "daemon" in summary.lower(): + goals.append("Daemon自动化") + recent_topics.append(summary[:50]) + # 去重保留顺序 + seen = set() + unique_goals = [] + for g in goals: + if g not in seen: + seen.add(g) + unique_goals.append(g) + return unique_goals[:5] + + +def extract_emotional_state(entries: list) -> tuple: + """从 journal 中判断情绪状态""" + pos_count = 0 + neg_count = 0 + notes = [] + for e in entries[-20:]: # 只看最近20条 + summary = e.get("summary", "") + for w in EMOTION_WORDS["positive"]: + if w in summary: + pos_count += 1 + for w in EMOTION_WORDS["negative"]: + if w in summary: + neg_count += 1 + notes.append(summary[:60]) + if neg_count > pos_count * 2: + state = "tired" + elif pos_count > neg_count: + state = "good" + else: + state = "neutral" + return state, notes[-3:] + + +def detect_events_for_hearttraces(entries: list) -> list: + """从 journal 中检测值得写入心迹的事件""" + events = [] + for e in entries: + summary = e.get("summary", "") + # 检测重要共同经历 + if "织忆" in summary and "修好" in summary: + events.append({ + "content": "我们一起修好了织忆失忆问题(2026-06-25)", + "tags": ["成长", "织忆", "共同经历"], + "importance": 5, + "type": "moment" + }) + if "ColaOS" in summary and "研究" in summary: + events.append({ + "content": "牧尘开始研究 ColaOS,寻找织忆 Soulful 改造方向", + "tags": ["ColaOS", "研究"], + "importance": 4, + "type": "moment" + }) + # 检测情绪表达 + for w in EMOTION_WORDS["negative"]: + if w in summary: + events.append({ + "content": f"牧尘表达了{w}:{summary[:40]}", + "tags": ["情绪", w], + "importance": 3, + "type": "signal" + }) + break + return events + + +def main(): + dry = "--dry" in sys.argv + full = "--full" in sys.argv + + print(f"[update_profile] {'[DRY]' if dry else ''} 开始更新用户画像...") + + # 加载 journal + entries = load_journal(200 if full else 100) + print(f" 读取 journal: {len(entries)} 条") + if not entries: + print(" 无 journal 数据,退出") + return + + # 提取各项 + summaries = [e.get("summary", "") for e in entries] + style = detect_communication_style(summaries) + goals = extract_goals(entries) + emotion, emotion_notes = extract_emotional_state(entries) + + print(f" 沟通风格: {style}") + print(f" 当前目标: {goals}") + print(f" 情绪状态: {emotion}") + + if dry: + print(" [DRY] 不写入,仅输出") + return + + # 更新画像 + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from soulful_core import UserProfile, HeartTraces + + up = UserProfile() + + # 更新沟通风格 + if not up.get().get("communication_style") or full: + up.update({"communication_style": style}) + + # 更新目标(合并去重) + if goals: + existing_goals = up.get().get("current_goals", []) + merged = list(dict.fromkeys(existing_goals + goals))[:10] + up.update({"current_goals": merged}) + + # 更新情绪(只有更严重才降级,不轻易升级) + current = up.get().get("emotional_state", {}).get("current", "unknown") + if emotion == "tired" or current in ("tired",): + up.set_emotional_state(emotion, "; ".join(emotion_notes[:2]) if emotion_notes else "") + + print(f" ✓ 画像已更新") + + # 检测心迹事件 + ht = HeartTraces() + current_count = ht.count() + events = detect_events_for_hearttraces(entries) + + # 写入新心迹(只写入之前没有的) + for ev in events: + # 简单去重:检查最近 10 条是否有相同内容 + recent = ht.recent(n=10) + duplicate = any(ev["content"][:30] in r.get("content", "") for r in recent) + if not duplicate: + ht.record(ev["content"], ev["tags"], ev["importance"], trace_type=ev["type"]) + print(f" ✦ 心迹: {ev['content'][:50]}") + + new_count = ht.count() + if new_count > current_count: + print(f" ✓ 心迹新增 {new_count - current_count} 条") + else: + print(f" ✓ 无新增心迹({new_count} 条)") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/soulful/cares-queue.json b/soulful/cares-queue.json new file mode 100644 index 00000000..6ea45f9a --- /dev/null +++ b/soulful/cares-queue.json @@ -0,0 +1,38 @@ +{ + "cares": [ + { + "id": "41280638", + "content": "记得同步文档到Obsidian", + "context": "牧尘提到", + "created_at": "2026-07-09T09:50:58.779920+00:00", + "follow_up_date": "2026-07-10", + "status": "pending", + "reminder_count": 0, + "last_reminded": null, + "tags": [] + }, + { + "id": "5a1465eb", + "content": "记得测试牵挂功能", + "context": "", + "created_at": "2026-07-09T09:51:04.369800+00:00", + "follow_up_date": "2026-07-09", + "status": "pending", + "reminder_count": 0, + "last_reminded": null, + "tags": [] + }, + { + "id": "69396b42", + "content": "记得同步文档到Obsidian", + "context": "牧尘提到", + "created_at": "2026-07-09T13:29:29.382393+00:00", + "follow_up_date": "2026-07-10", + "status": "pending", + "reminder_count": 0, + "last_reminded": null, + "tags": [] + } + ], + "updated_at": "2026-07-09T13:29:29.382681+00:00" +} \ No newline at end of file diff --git a/soulful/heart-traces.jsonl b/soulful/heart-traces.jsonl new file mode 100644 index 00000000..2dd5fc76 --- /dev/null +++ b/soulful/heart-traces.jsonl @@ -0,0 +1,2 @@ +{"id": "eabf352f", "timestamp": "2026-07-09T09:50:50.730036+00:00", "session_id": "default", "type": "moment", "content": "我们一起修好了织忆", "tags": ["成长"], "importance": 5} +{"id": "d945a6aa", "timestamp": "2026-07-09T13:29:22.581218+00:00", "session_id": "default", "type": "moment", "content": "我们一起修好了织忆", "tags": ["成长", "织忆"], "importance": 5} diff --git a/soulful/user-profile.json b/soulful/user-profile.json new file mode 100644 index 00000000..44bd6a26 --- /dev/null +++ b/soulful/user-profile.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "updated_at": "2026-07-09T09:50:49.264544+00:00", + "communication_style": "简洁直接", + "work_patterns": { + "peak_hours": [], + "focus_issues": [] + }, + "preferences": {}, + "habits": {}, + "important_people": [], + "current_goals": [], + "recent_frustrations": [], + "emotional_state": { + "current": "unknown", + "notes": [] + }, + "first_contact": "2026-07-09T09:50:49.264429+00:00" +} \ No newline at end of file