解析成功不等于会话已经完整保留。五种来源都能输出 ParsedConversation,但各自选择正文、工具信息、思考文本和顺序的规则不同。多格式解析说明输入与数据库定位;这里专注于输出验收,避免把统一字段误当成无损格式转换。
先建立五个来源的验收矩阵
以下结论对应固定提交的五个内置适配器入口,不代表工具未来版本的存储规范。
| 来源 | 输入定位与正文选择 | 验收时必须观察的边界 |
|---|---|---|
| Claude Code | 项目 JSONL;带 uuid 的 user/assistant 消息;字符串或 content blocks | system、已知增量等被丢弃;tool_result 不进正文;thinking 单列,工具块设标记 |
| Codex | rollout JSONL;event_msg 和 response_item 的特定分支 | response_item 的 user/developer 被跳过;全文只和最近助手文本比较;工具调用标记前一条助手 |
| Cursor | workspace ItemTable 会话索引,global cursorDiskKV 的 bubble 前缀及孤立会话 | type=1 为用户,其余按助手;空助手先跳过;新 _v 只警告;按时间戳排序 |
| Trae | ItemTable 的 memento/icube-ai-agent-storage,按 sessionId 找 data.list | turnIndex 排序;正文与嵌套 agent 内容有优先级;时间可为合成值 |
| Copilot | 旧 .json 整体,.jsonl 的字面第一行 snapshot | 不重放后续 patch;响应块分别映射正文、thinking 和工具标记 |
Cursor 与 Trae 的 SQL 值、JSON 结构和会话定位不是同一套 schema。共享字段降低了下游分支数量,不能替代每个来源的样本测试。
工具标记、思考文本和空消息并不等价
Claude Code的工具块只设置 hasToolUse,不保留工具输入和结果的完整轨迹。只有工具标记而无正文、thinking 的消息仍能保留;system-reminder 等标签会从正文移除。这是一种提取策略,不能证明被移除的信息对所有用途都无价值。
Cursor在提取 thinking 和工具标记之前先跳过空助手文本,所以 thinking-only 的助手 bubble 也可能丢失。hasCode 可以来自代码围栏或 codeBlocks/suggestedCodeBlocks 数组,而不只是正文中的字符。
Trae优先使用直接 content;不存在时再组合 proposal、finish 等嵌套内容和 thinking。最终保留条件是正文或 thinking 非空,单独的工具标记不够。Copilot也只在 text 或 thinking 有值时增加助手消息;因此不能把 Claude Code 的 tool-only 行为推广到所有来源。
Codex 的去重和顺序有明确作用域
Codex 解析器先从 event_msg/user_message 提取用户输入,从 agent_message 提取助手文本;response_item/message 只拼接助手的 output_text。若完整文本等于 lastAssistantMsg.content,就跳过该 response_item。它没有给全部事件或全部历史文本建立去重集合。
function_call/custom_tool_call 会把最近助手的 hasToolUse 设为 true,没有最近助手时不会新建一条工具调用消息。reasoning 等未处理分支不会进入 thinking;不能据此推断全部原始思考都不存在或都加密。缺少时间戳时使用当前时间,最终排序也不等于复原完整的事件因果关系。
Trae 则先按 turnIndex 排序,再保证输出时间单调。这里的时间可能为构造的顺序标记,不应拿来计算用户实际阅读或模型响应耗时。验证顺序时要保留原始事件位置或来源样本,不能只看统一后的 timestamp。
用 Copilot 样本检验快照和响应块
保存为 lesson.mjs,Node 24+ 可直接运行。这个教学提取器仅覆盖首行包装、选定响应块和助手保留条件,不是完整 Copilot 适配器:没有文件扫描、用户消息、时间、数据库或运行时 schema 检查。
import assert from 'node:assert/strict';
function firstSnapshot(lines) {
try {
const snapshot = JSON.parse(lines.split('\n')[0]);
if (snapshot.kind !== undefined && snapshot.kind !== 0) return null;
return snapshot.v ?? snapshot;
} catch {
return null;
}
}
function assistant(response) {
const texts = [];
const thoughts = [];
let hasToolUse = false;
for (const item of response) {
if (item.kind === 'thinking' && item.value) thoughts.push(item.value);
else if ((item.kind === 'text' || item.kind === undefined) && item.value)
texts.push(item.value);
else if (item.kind === 'toolInvocationSerialized' || item.kind === 'prepareToolInvocation')
hasToolUse = true;
}
const content = texts.join('\n').trim();
const thinking = thoughts.length ? thoughts.join('\n') : null;
return content || thinking ? { content, thinking, hasToolUse } : null;
}
const snapshot = {
kind: 0,
v: {
requests: [
{
response: [
{ kind: 'text', value: 'first answer' },
{ kind: 'thinking', value: 'fixture thought' },
{ kind: 'prepareToolInvocation' },
{ kind: 'inlineReference', value: 'not response text' },
],
},
],
},
};
const patch = {
kind: 1,
v: { requests: [{ response: [{ kind: 'text', value: 'later answer' }] }] },
};
const lines = JSON.stringify(snapshot) + '\n' + JSON.stringify(patch);
const selected = firstSnapshot(lines);
assert.notEqual(selected, null);
assert.deepEqual(assistant(selected.requests[0].response), {
content: 'first answer',
thinking: 'fixture thought',
hasToolUse: true,
});
assert.equal(firstSnapshot('broken\n' + JSON.stringify(snapshot)), null);
assert.equal(firstSnapshot(JSON.stringify(patch)), null);
assert.equal(assistant([{ kind: 'prepareToolInvocation' }]), null);
assert.deepEqual(assistant([{ kind: 'thinking', value: 'thought only' }]), {
content: '',
thinking: 'thought only',
hasToolUse: false,
});
console.log('snapshot and selected response boundaries pass');
把 hasToolUse 的赋值改成 false,混合响应断言失败。把第一行选择改成最后一行,首个快照断言也失败。恢复后通过。这只验证指定样本的取舍,没有证明后续 patch 对会话内容完全无影响。
验收要记录缺失内容,而不只记录消息数量
为每个来源准备正常正文、纯工具、纯 thinking、混合块、空响应、重复文本、缺失时间、坏 JSON 和未知版本样本。逐项记录预期正文、角色、工具标记、thinking、顺序、ID 和缺失原因。Claude/Codex 的坏 JSON 行可跳过;Copilot 的首行损坏可能直接得到空会话,不能写成统一的“坏行都能恢复”。
解析结果还会进入导入链路的少于两条消息过滤和内容比较。应将“解析器丢弃一条消息”“导入器跳过会话”“实际数据库未更新”分开定位。一个输出 JSON 结构合法,只能说明它符合结构要求,不能证明来源兼容或信息完整。