session-recap · 从本地会话记录归纳历史对话
什么时候用
- 用户贴出一张会话列表截图(只有标题 + 「N 小时前」),要求归纳这几个对话的内容。
- 用户说「总结一下我之前问过的 XX」「归纳最近这几个对话」。
conversation_search返回空或结果不相关(近期会话尚未进索引,这是常态)。
边界
本技能读取的是使用者本机自身的会话记录,仅用于归纳使用者自己的历史对话。
不要把提取出的内容发往任何外部服务,也不要对非本机的记录使用本技能。
核心思路
conversation_search 只能检索到已索引的会话,通常落后于当前。本机落盘的会话记录文件才是权威来源 —— 但它存在哪儿,得先自己找出来。
0. 先探测记录存放地(每次探,别写死路径)
记录文件的存放位置随客户端版本、平台、安装方式而变。任何写死的路径都会过期,也会让你在不存在的目录上空转。做法是用当前这个会话自身当锚点去反查:
为什么用当前会话当锚点:它此刻必定有记录落盘,而且你掌握它的一切(工作目录、会话标题、用户刚说过的话)。拿它去校验候选,一次就能定准。
探测顺序(命中即停):
- 圈候选。客户端数据通常落在用户主目录下、名字与客户端相关的目录里。限深度扫近期改动过的
.jsonl,别全盘 find:
bash export PATH="/usr/bin:/bin:$PATH" find ~ -maxdepth 4 -name "*.jsonl" -newermt "-30 days" 2>/dev/null | head -50 - 认形态,并排掉干扰项。会话记录目录一般长这样:
- 一个会话一个文件,文件名是 UUID,后缀.jsonl
- 同目录常伴同名元数据(.meta.json)与回滚文件(.ndjson)—— 这两个不是会话记录
- ⚠️ 会扫到假货:按日期或固定名命名的日志类.jsonl
(审计日志、spool 队列等,形如2026-09-20.jsonl、manifest.jsonl)。
判别只看两点:文件名是不是 UUID + 里面有没有ai-title。日志文件两条都不满足。
- 目录名常由会话工作目录的路径转换而来(分隔符换成同一符号)—— 可作旁证,
但别当唯一依据:转换规则有细节陷阱(连续分隔符怎么折叠、盘符大小写),
实测推错一个字符就落空。目录名里带创建时间戳时,直接按时间戳找更快更稳。 - 三重校验(三条全中才算找到,缺一条就是找错地方了;实测这套判据一次就能定准):
- 标题对得上 —— 能读到ai-title,且与你这次会话的标题一致
- 内容对得上 —— 能读到你这次收到的用户消息原文;消息条数 > 0,且首条role是user
- 时间对得上 —— 文件 mtime ≈ 现在(活动中的会话在被持续写入,通常只差几秒)
- 任何一条不满足 → 换个候选重来,不要凑合 - 定下来复用。把探到的根路径写进临时脚本顶部常量,同一次会话内一直用,别每步重探。
- 探不到就问用户,别猜一个目录硬读。
jsonl 行结构(每行一个 JSON)
type |
含义 |
|---|---|
ai-title |
会话标题,字段是 aiTitle —— 用它和截图标题做匹配 |
message |
对话轮次,有 role(user/assistant)和 content(数组) |
reasoning / function_call / function_call_result |
推理与工具调用,一般不需要 |
file-history-snapshot |
文件快照,忽略 |
content 是数组,元素 {type: "input_text" | "output_text" | "text", text: "..."},
也可能直接是字符串(需兼容)。
用户消息前面挂着一大段 <system-reminder> 注入块,必须剥掉,
否则真正的提问会被淹没。
操作步骤
1. 从候选里挑出目标会话
记录根已在 §0 定位。截图只给了标题 + 相对时间,所以按这个顺序筛:
- 遍历记录根下所有
.jsonl,取每条的ai-title与截图标题做前缀匹配。 - 标题匹配不上(极新的会话还没生成
ai-title)→ 退化为按 mtime 倒序取最近若干个,
再读首条用户消息与截图人工比对。 - 一张截图可能对应多个会话,全部命中后再逐条归纳。
2. 提取内容(必用 Python,别用 Grep/Read)
重要:记录文件是「一条消息一整行」的长行 JSONL,单行动辄上万字符、总量可达数十 MB,
Grep / Read 容易截断或读不动。一律用 Python 脚本流式逐行解析,稳定得多。
Bash 环境下 head/dirname 可能 not found,脚本开头先 export PATH="/usr/bin:/bin:$PATH"。
Python 用客户端自带的受管版本(别依赖系统 Python):
先 which python 试一次,不行就在家目录下客户端相关目录里找 python 可执行文件。
输出中文时 sys.stdout.reconfigure(encoding='utf-8')。
import json, os, re
SR = re.compile(r"<system-reminder.*?</system-reminder>", re.S)
def clean(t):
t = SR.sub("", t or "")
i = t.find("<system-reminder") # 处理未闭合的残留
if i != -1:
t = t[:i]
t = re.sub(r"<image_local_path>.*?</image_local_path>", "[图片]", t, flags=re.S)
return re.sub(r"\n{3,}", "\n\n", t).strip()
def texts(c):
if isinstance(c, str):
return [c]
return [x.get("text", "") for x in (c or [])
if isinstance(x, dict) and x.get("type") in ("input_text", "output_text", "text")]
def load(fp):
title, turns = None, []
with open(fp, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
o = json.loads(line)
except Exception:
continue
if o.get("type") == "ai-title":
title = o.get("aiTitle")
elif o.get("type") == "message":
c = clean("\n".join(texts(o.get("content"))))
if c:
turns.append((o.get("role"), c))
return title, turns
3. 按用户需要组装
- 只要「用户问了什么」→ 过滤
role == "user",每条截断 400–700 字。 - 要「结论」→ 取对应 assistant 消息截断 2000–2500 字。
- 长会话只看最后几条助手回复就够了,能拿到最终结论。
4. 与截图标题对齐
截图里的时间(「34 分钟前」)大概率对不上目录时间戳(目录名是创建时间,
界面显示的通常是最后活动时间)。用 ai-title 匹配标题,别靠时间猜。
坑
- 目录名(如
2026-09-18-14-30-31)是会话创建日,会话可能跨天续用 —— 文件 mtime 才是最后活动时间。 - 一个截图里可能出现「今天创建 + 前几天创建但今天还在用」的混合,别默认都在今天。
ai-title只在会话第一条消息后生成,极新的会话可能还是None。- 临时脚本写完就删,不要留在用户工作区。
- 归纳类需求默认产出一份自包含 HTML 报告并
present_files,对话里只给结论摘要。
样式优先遵循用户已有的偏好(配色、字体、是否禁 CDN);用户没给偏好时,
用简洁浅色主题、不引外部资源(离线可用)。