---
title: 编写多格式 AI 会话解析器：字段映射与丢失边界
description: 从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵，检查工具标记、思考文本、顺序与快照覆盖范围。
image: "https://zengliangyi.com/og/blog/ai-conversation-parser-boundaries.png"
---

解析成功不等于会话已经完整保留。五种来源都能输出 ParsedConversation，但各自选择正文、工具信息、思考文本和顺序的规则不同。[多格式解析](/blog/multi-format-conversation-parsing/)说明输入与数据库定位；这里专注于输出验收，避免把统一字段误当成无损格式转换。

## 先建立五个来源的验收矩阵

以下结论对应固定提交的[五个内置适配器入口](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/index.ts)，不代表工具未来版本的存储规范。

| 来源 | 输入定位与正文选择 | 验收时必须观察的边界 |

| --- | --- | --- |

| 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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/claude-code.ts)的工具块只设置 hasToolUse，不保留工具输入和结果的完整轨迹。只有工具标记而无正文、thinking 的消息仍能保留；system-reminder 等标签会从正文移除。这是一种提取策略，不能证明被移除的信息对所有用途都无价值。

[Cursor](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/cursor.ts)在提取 thinking 和工具标记之前先跳过空助手文本，所以 thinking-only 的助手 bubble 也可能丢失。hasCode 可以来自代码围栏或 codeBlocks/suggestedCodeBlocks 数组，而不只是正文中的字符。

[Trae](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/trae.ts)优先使用直接 content；不存在时再组合 proposal、finish 等嵌套内容和 thinking。最终保留条件是正文或 thinking 非空，单独的工具标记不够。[Copilot](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/copilot.ts)也只在 text 或 thinking 有值时增加助手消息；因此不能把 Claude Code 的 tool-only 行为推广到所有来源。

## Codex 的去重和顺序有明确作用域

[Codex 解析器](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/codex.ts)先从 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 检查。



```js
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 的首行损坏可能直接得到空会话，不能写成统一的“坏行都能恢复”。

解析结果还会进入[导入链路](/blog/source-adapter-internals/)的少于两条消息过滤和内容比较。应将“解析器丢弃一条消息”“导入器跳过会话”“实际数据库未更新”分开定位。一个输出 JSON 结构合法，只能说明它符合结构要求，不能证明来源兼容或信息完整。

分享：

##  相关文章

[##  Vercel AI SDK Provider 抽象设计：多模型适配

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出，用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#TypeScript](/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter 核心设计：内容哈希与幂等增量导入

对照 ChatCrystal 的注册表、导入器和远端载荷，验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/source-adapter-internals)    [##  Biome 与 ESLint 职责分工：代码检查与格式化的工程化配置

阅读 ChatCrystal 的实际脚本与 Flat Config，用离线 ESLint 诊断验证规则，再区分语法检查、类型检查、格式化和 CI 门禁。          曾亮懿       2026年10月6日      阅读约 4 分钟

#ChatCrystal

#TypeScript

#Node.js](/blog/lint-format-tooling)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "编写多格式 AI 会话解析器：字段映射与丢失边界",
  "description": "从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵，检查工具标记、思考文本、顺序与快照覆盖范围。",
  "url": "https://zengliangyi.com/blog/ai-conversation-parser-boundaries/",
  "image": "https://zengliangyi.com/images/blog/ai-conversation-parser-boundaries.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-23T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/ai-conversation-parser-boundaries/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "编写多格式 AI 会话解析器：字段映射与丢失边界",
      "item": "https://zengliangyi.com/blog/ai-conversation-parser-boundaries/"
    }
  ]
}
```
