feat: Soulful Phase 1 — 心迹/画像/牵挂三库 + daemon集成

- soulful_core.py: HeartTraces + UserProfile + CaresQueue 核心库
- update_profile.py: 从 journal 自动更新用户画像 + 心迹事件检测
- check_cares.py: 牵挂到期检查 + 温暖飞书推送
- daemon.py v2.1: 集成三库(心迹注入deep_think context、每小时牵挂检查、6h画像更新)
- cronjob: 每日9:00牵挂提醒 + 每日21:00牵挂清单
- 数据目录: ~/.hermes/soulful/

版本: 1.0.0
This commit is contained in:
小唯 A06 2026-07-09 21:32:30 +08:00
parent 29bcd7a2f1
commit 82ee9bf176
7 changed files with 939 additions and 0 deletions

168
scripts/check_cares.py Executable file
View File

@ -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()

View File

@ -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:

390
scripts/soulful_core.py Normal file
View File

@ -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 <command> [args]")
print("命令:", list(commands.keys()))
sys.exit(1)
commands[sys.argv[1]]()

212
scripts/update_profile.py Normal file
View File

@ -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()

38
soulful/cares-queue.json Normal file
View File

@ -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"
}

View File

@ -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}

19
soulful/user-profile.json Normal file
View File

@ -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"
}