本文面向:想统一管理多个 AI 编程工具对话数据的开发者。
预计阅读时间:10 分钟
最终效果:理解 Claude Code、Codex、Cursor、Trae、Copilot 五种对话格式的结构、优劣与解析陷阱,明白为什么需要统一抽象层。
当你用 Claude Code 写了一下午代码、用 Cursor 调了半天 bug、用 Copilot 补全了一堆函数之后,你的对话数据散落在三种完全不同的文件格式里。如果你想把这些对话统一管理------导入、搜索、总结------你需要先理解每种格式的结构、优缺点和解析陷阱。
本文深入分析 ChatCrystal 支持的 5 种 AI 对话数据格式。
一、Claude Code:JSONL 文件
文件位置: ~/.claude/projects/ 目录下,按项目路径组织
文件格式: JSONL(JSON Lines),每行一个 JSON 对象
Claude Code 的对话记录是最"朴素"的格式。每个 JSONL 文件代表一次会话,每行是一个消息对象,包含 role(user/assistant)、content(文本内容)、type 等字段。
结构示例:
jsonl
{"type":"user","message":{"role":"user","content":"帮我写一个 Fastify 插件..."}}
{"type":"assistant","message":{"role":"assistant","content":"好的,这是一个 Fastify 插件的模板..."}}
{"type":"tool_use","message":{"role":"assistant","content":[{"type":"tool_use","name":"write_file","input":{...}}]}}
{"type":"tool_result","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"...","content":"文件已写入"}]}}
解析难点:
-
系统标签污染。 Claude Code 的消息内容中经常混入
<system-reminder>、<command-name>等 XML 标签。这些标签是 UI 渲染用的,对内容理解没有帮助,反而会干扰 LLM 的摘要生成。ChatCrystal 的sanitizeContent()函数会正则匹配并移除这些标签。 -
工具调用的嵌套结构。 assistant 消息的
content有时不是纯字符串,而是一个数组,包含tool_use和text类型的混合。解析时需要递归处理。 -
连续工具消息的合并。 一个工具调用通常涉及三条消息:assistant 发起 tool_use、user 返回 tool_result、assistant 继续回复。这三条在语义上是一个整体,解析时需要把它们合并为一个"工具调用组"。
优势: 格式简单、可读性好、文件天然按会话分割、不需要额外依赖即可解析。
劣势: 没有元数据头(项目名、开始时间等需要从文件路径推断)、没有内置的索引机制。
二、Codex CLI:事件流 JSONL
文件位置: ~/.codex/sessions/ 目录下
文件格式: JSONL 事件流,每个 JSON 对象是一个事件
Codex CLI 的对话格式和 Claude Code 的 JSONL 有本质区别。它不是直接存储消息,而是存储事件流------一系列带时间戳的操作事件。
核心事件类型:
session_meta:会话创建,包含初始配置event_msg:用户输入消息response_item:AI 响应片段(可以是文本、工具调用、推理过程)function_call:工具执行的调用和返回
解析难点:
-
对话需要"重建"。 和 Claude Code 的直接消息列表不同,Codex 的对话需要从事件流中重建。你需要遍历所有事件,把
event_msg和response_item按时间顺序组装成对话流。 -
响应碎片化。 AI 的回复不是一个完整的消息,而是多个
response_item事件的拼接。有些是文本片段,有些是工具调用,有些是推理过程(chain of thought)。你需要把它们按类型分组,再组装成可读的对话。 -
事件顺序依赖时间戳。 如果时间戳精度不够,或者存在时钟回退,事件的顺序可能错乱。ChatCrystal 的解析器会做容错处理。
优势: 事件流保留了完整的操作历史(包括工具调用的输入输出),适合做审计和回放。
劣势: 解析复杂度高、文件体积大(每个事件都带完整元数据)、对话重建需要额外逻辑。
三、Cursor:SQLite 数据库
文件位置: Cursor 的 workspaceStorage 或 globalStorage 目录下的 state.vscdb
文件格式: SQLite 数据库,键值对存储
Cursor 没有使用文件系统来存储对话,而是用了 VS Code 的状态存储机制------一个 SQLite 数据库文件 state.vscdb。对话数据以键值对的形式存在数据库中。
数据结构:
- Composer 元数据: 键名类似
composer.composerData,存储所有对话的 ID、标题、创建时间等元信息 - Bubble 数据: 键名格式为
bubbleId:{composerId}:{bubbleId},存储每条消息的内容,包括用户输入和 AI 回复 - 工具调用数据: 工具调用的输入输出单独存储在特定的键下
解析难点:
-
键值对的间接引用。 对话元数据和消息内容分开存储。你需要先读取元数据获取对话列表和消息 ID,再逐个查询消息内容。这种间接引用模式在数据量大时会产生大量查询。
-
Blob 数据的编码。 某些值可能是二进制大对象(Blob)而非纯文本,需要判断类型并做相应解码。
-
数据库锁定。 如果 Cursor 正在运行,数据库文件可能被锁定。ChatCrystal 的 Cursor 适配器会在检测到锁定时给出提示,建议关闭 Cursor 后再导入。
-
数据量膨胀。 Cursor 会在同一个
state.vscdb里存储远超对话的数据------设置、扩展状态、工作区配置等。一个典型的工作区数据库可能有数百 MB,但对话数据只占很小的比例。
优势: 结构化存储、支持复杂查询(SQL)、单文件管理所有状态。
劣势: 解析依赖 SQLite 库(ChatCrystal 用 sql.js WASM 避免原生依赖)、需要处理数据库锁定、数据模型复杂。
四、Trae:SQLite 数据库(变体)
文件位置: Trae 的 workspaceStorage 目录下的 state.vscdb
文件格式: SQLite 数据库,键值对存储
Trae 基于 VS Code 架构,数据存储方式和 Cursor 类似,但对话的数据模型有显著差异。
数据结构:
Trae 的对话数据存储在 memento/icube-ai-agent-storage 这个键下。每条记录代表一个 Agent 任务(AgentTask),包含:
taskId:任务唯一标识taskTitle:任务标题agentTaskContent:一个 JSON 数组,包含任务中所有消息- 每条消息有
role、content、contentType等字段
解析难点:
-
Agent 任务 vs 普通对话。 Trae 的对话模型是"任务制"而非"会话制"。一个任务可能包含多轮对话,但所有消息打包在
agentTaskContent字段中。这和 Claude Code 的"一个文件一次会话"有本质区别。 -
内容类型的多样性。
contentType不只有纯文本------还有代码块、工具输出、图片描述等。解析时需要按类型分别处理。 -
响应内容的提取。 Agent 的回复不是直接存储在
content里,而是嵌套在特定的响应结构中。ChatCrystal 的 Trae 适配器会提取content字段中的文本内容,过滤掉结构化的元数据。
优势: 和 Cursor 类似,结构化存储、SQL 可查询。
劣势: 数据模型更复杂(嵌套 JSON 数组)、Agent 任务的粒度比会话更粗、某些字段的文档缺失需要逆向工程。
五、GitHub Copilot:JSONL 会话快照
文件位置: VS Code 的 workspaceStorage/<id>/chatSessions 和 globalStorage/emptyWindowChatSessions
文件格式: JSONL,每行一个会话快照
Copilot 的数据格式相对规整。每个 JSONL 文件包含多个会话快照,每个快照是一个完整的对话记录。
数据结构:
每个快照以 kind:0 标记为完整会话快照,包含:
requests:用户请求列表,每条包含message(用户输入)、timestamp、response(AI 响应数组)等。注意response是嵌套在每个 request 对象内部的字段,而非独立的顶层数组。
解析难点:
-
响应类型的多样性。 每个 request 内的
response数组包含多种类型的条目(text、thinking、toolInvocationSerialized等),需要按kind字段分别处理,提取有意义的文本内容。 -
会话快照的增量更新。 一个 JSONL 文件中可能包含同一会话的多个版本(每次保存都是完整快照)。解析时需要去重,只保留最新版本。
-
多窗口会话的分离。 Copilot 分
workspaceStorage和globalStorage两个位置存储对话。工作区对话在各自的工作区目录下,全局对话(没有打开工作区时的对话)在emptyWindowChatSessions中。完整的导入需要扫描两个位置。
优势: 格式规整、数据结构清晰、JSONL 易于逐行解析。
劣势: 快照式存储导致文件体积冗余(同一个会话的多个版本)、响应类型多样需要分类处理。
格式对比总结
| 特性 | Claude Code | Codex CLI | Cursor | Trae | Copilot |
|---|---|---|---|---|---|
| 文件格式 | JSONL | JSONL | SQLite KV | SQLite KV | JSONL |
| 对话粒度 | 会话 | 事件流 | Composer 对话 | Agent 任务 | 会话快照 |
| 解析复杂度 | 低 | 中高 | 中 | 高 | 中低 |
| 外部依赖 | 无 | 无 | sql.js | sql.js | 无 |
| 元数据丰富度 | 低 | 高 | 中 | 中 | 中 |
| 工具调用记录 | 有 | 有 | 有 | 有 | 有 |
| 增量更新支持 | 天然支持 | 天然支持 | 需查询 | 需查询 | 需去重 |
为什么格式统一很重要
5 种格式、5 种解析逻辑、5 种数据模型。如果每个工具都只能管理自己的对话,你最终会得到 5 个孤岛。
ChatCrystal 的 SourceAdapter 插件架构正是为了解决这个问题。每个适配器负责一种格式的解析,输出标准化的 Conversation 和 Message 类型。上层的导入服务、摘要服务、搜索服务不需要关心数据来自哪个工具------对它们来说,所有对话都是一样的。
这种抽象的价值在搜索时最为明显。当你搜"数据库连接池"时,搜索结果横跨所有 5 种工具的对话。你在 Claude Code 里讨论的架构设计、在 Cursor 里调试的连接泄漏、在 Copilot 里写的连接池代码,都被统一索引和检索。
这才是个人知识管理应有的样子------不被工具割裂。
项目地址:github.com/ZengLiangYi/ChatCrystal
如有疑问欢迎在 GitHub Issues 或私信交流,很乐意解答。