#!/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 # ====== Phase 5: 自动牵挂识别 ====== import re as re_module from datetime import timedelta # 牵挂识别模式 CARE_PATTERNS = [ # 显式提醒型 (re_module.compile(r'记得\s*(.+)'), "记得"), (re_module.compile(r'别忘了\s*(.+)'), "别忘"), (re_module.compile(r'别忘\s*(.+)'), "别忘"), (re_module.compile(r'回头\s*(.+)'), "回头"), (re_module.compile(r'等有空\s*(.+)'), "等有空"), (re_module.compile(r'待办[::]\s*(.+)'), "待办"), (re_module.compile(r'TODO[::]\s*(.+)'), "TODO"), (re_module.compile(r'\[ \]\s*(.+)'), "checkbox"), # 计划型 (re_module.compile(r'(?:要|得|该)\s*(.{5,30}?)(?:的话|的话|的时候|时)?'), "计划"), (re_module.compile(r'处理\s*(.+)'), "处理"), (re_module.compile(r'做一下\s*(.+)'), "做一下"), ] # 停用词(不作为牵挂) CARE_STOPWORDS = {"什么", "为什么", "怎么", "如何", "多少", "哪个", "这里", "那里", "今天", "明天", "昨天", "现在", "刚才", "刚才的"} def extract_cares_from_journal(entries: list = None, dry: bool = True) -> list: """从 journal 中自动识别牵挂,自动添加到 CaresQueue Args: entries: journal 条目列表,None 则自动加载 dry: True=仅返回,不写入;False=写入 CaresQueue """ if entries is None: entries = load_journal(100) found_cares = [] for e in entries: summary = e.get("summary", "") details = e.get("details", "") full_text = f"{summary} {details}" for pattern, care_type in CARE_PATTERNS: matches = pattern.findall(full_text) for raw_text in matches: # 清理 text = raw_text.strip().rstrip(".,,。!!??") if len(text) < 4 or len(text) > 80: continue # 停用词过滤 if any(sw in text for sw in CARE_STOPWORDS): continue # 去除动作词前缀 text = re_module.sub(r'^(记得|别忘|要|得|该|处理|做)\s*', '', text) found_cares.append({ "content": text, "type": care_type, "source": summary[:50], }) # 去重(内容相似度匹配,公共前缀 > 10 字符相同视为重复) unique = [] for c in found_cares: is_dup = any( c["content"][:15] == u["content"][:15] for u in unique ) if not is_dup: unique.append(c) if dry: return unique # 写入 CaresQueue sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from soulful_core import CaresQueue cq = CaresQueue() existing = cq.pending() added = [] # 已有牵挂的前15字符 existing_prefixes = {c["content"][:15] for c in existing} for care in unique: if care["content"][:15] in existing_prefixes: continue # follow_up_date = 3天后 follow_up = (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d") care_id = cq.add( content=care["content"], context=f"[自动识别来自:{care['source']}]", follow_up_date=follow_up, tags=["自动识别", care["type"]] ) added.append(care_id) return added 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()