机器实现见
lib/convert/interchange.mjs,里面有DEGRADATION_RULES、summarizeDegradations和exportDegradations。
手上有一份未识别的对话记录,想把它转成本协议? 本文是给维护者看的规范。真正动手转换请读 转换指南(给 Agent 的 Skill 指令) 或 Conversion Guide(英文)——最小示例、字段表和自检清单都在那里。
本协议定义导入与导出共用的 turns IR,以及便携 bundle 的格式。
-
content的块类型与 DSH 会话事件同构:text、reasoning、image、tool-call、tool-result。 工具结果该落在toolResults[]。写在 stepcontent里的tool-result块,会按toolCallId派生进step.toolResults(显式列表优先,与tool-call块的派生对称);写在promptBlocks或结果内层content里的结果块无处安放,会被丢弃并计入skippedBlocks。工具结果不允许 留在 assistant 或 user 消息的 content 里:宿主 V4 codec 一见到tool-result包装就拒载整份 日志("released tool-result wrapper"),不过校验层会先用retired-tool-result-wrapper点名。 -
回合模型:一条用户提问就是一个
turn,一条助手消息(连同它的工具调用与结果)就是一个step。 -
turns[i].time、steps[j].time、toolResults[k].time(可选,毫秒)是源转录里的逐记录时间戳, 也是宿主耗时统计的原料:模型耗时从 step.start 算到 assistant/message,工具耗时从 tool/call 算到 tool/result。事件时间只前进不倒退;三个都缺时取会话创建时间,这些耗时也就都是 0。 首 token 延迟与输出速度任何源都导不出来:外部转录没有流块时间戳,stream恒为[]。 -
steps[j].usage(可选)是 provider 回报的 token 用量,形状同 DSH TokenUsage。它写进assistant/message.data.usage,供宿主折叠 token 统计;input 或 output 不是非负整数时,整份 usage 丢弃。 -
配对不变量:每个
toolCalls[].id都要有对应的toolResults[].toolCallId。缺结果时synthesizeSession会兜底补发一条空结果,sourceEventSeqs的关联仍然成立。 -
图片块有两种状态。待落地的是
{ type:'image', data:<base64>, mediaType, name? },转换层 从源转录拿到字节时产出;已是引用的是{ type:'image', attachment:{ attachmentId, … } }, DSH 源回灌或导出再导入时带过来。落盘前,lib/attachments.mjs经宿主ctx.attachments把待落地 块存成不可变对象并替换为引用,base64 永不进会话日志(宿主的ImageBlock只认引用)。下列情况该块降级为
[image]文本,并计入imagesDegraded:宿主没有这个服务;类型不收 (第一版只收 PNG / JPEG / WebP / GIF);超出上限;源只给了引用,比如 Kimi 的blobref:。 目标代次低于 4 时(面板「导入到 → DSH(V3)」,或续写一条 V3 会话)也一律降级:附件引用 是当前世代才有的概念,旧宿主读不出来。宁可占位,也不产出旧宿主打不开的日志。 -
这个 IR 是进程内契约:没有版本号,也不落盘。改它要同步各转换器与
synthesizeSession。 对外可交换的格式是 §4 的便携 bundle,它有version和双层指纹。
这张表描述的是「源格式能记录什么」。缺某一项能力,是源本身固有的有损,不是插件的缺陷。
| 源 | toolResults | reasoning | images | cwd | branches | attachments | compacted | timestamps | usage |
|---|---|---|---|---|---|---|---|---|---|
| claude | ✅ | ✅ | ✅(提问 / 助手 / 工具结果内的 base64 截图) | ✅ | — | ✅ | — | ✅(逐行) | ✅(含 cache 读写桶) |
| codex | ✅ | ✅(summary 可读;密文不可读) | ✅(input_image 的 data URL) |
✅ | — | ✅ | — | ✅(行信封) | — |
| chatgpt | ✅(无结构化参数) | — | — | — | ✅(mapping DAG) | ✅ | — | ✅(create_time) |
— |
| cursor | —(导入器补空结果) | — | — | — | — | — | — | — | — |
| gemini | ✅ | ✅ | — | ✅ | — | — | — | — | — |
| reasonix | ✅ | ✅ | — | ✅ | — | — | — | ✅(v2 行 createdAt) |
✅(v2 行 usage,守卫映射) |
| opencode | ✅ | ✅ | ✅(带内联字节的 file part) | ✅ | — | ✅ | ✅ | ✅(消息级) | ✅(V1 message.data.tokens;kilocode / mimocode 同) |
| teleagent | ✅ | ✅ | — | ✅ | — | ✅ | ✅(样本 compaction 无 tail_start_id → 不裁剪,全量导入) | — | — |
| zcode | ✅ | ✅ | ✅(带内联字节的 file part) | ✅ | — | — | ✅ | ✅(消息级) | — |
| grokbuild | ✅ | ✅(summary 明文可读;encrypted_content 密文不可读) | ✅(images[] 的 data URL) |
✅ | — | ✅ | ✅(compaction_meta 交接摘要进原生检查点) | —(逐行无时间戳) | — |
| openclaw | ✅ | — | — | ✅ | — | — | — | ✅(逐行) | — |
| hermes | ✅ | ✅ | — | ✅ | — | — | — | ✅(逐消息 ts) |
— |
| pi | ✅ | ✅ | ✅(带字节的 image 块) | ✅ | ✅(树形) | — | ✅ | — | — |
| kimi | ✅ | ✅ | —(只有自有 blob 存储的 blobref: 引用,插件取不到字节 → 占位 + 计数) |
✅ | — | — | — | ✅(逐行) | — |
| workbuddy | ✅ | ✅ | — | ✅ | — | — | — | ✅(逐行) | — |
| codebuddy | ✅ | ✅ | — | ✅ | — | — | — | ✅(逐条 timestamp) |
✅(providerData.usage / rawUsage 的 input / output / cacheRead / reasoning 四桶) |
| continue | ✅ | ✅ | — | ✅ | — | — | ✅(history 不裁剪,摘要挂 reasoning 块) | —(history 项无时间戳) | — |
| copilot-vscode | ✅(仅源文件保存的结果;缺失计数) | ✅ | — | ✅(workingDirectory / workspace.json) | — | — | — | ✅ | ✅(每个 request 的 token 合计) |
| cline | ✅ | ✅ | — | ✅ | — | — | ✅(compaction 侧车不改写主转写) | ✅(仅 assistant 的 ts) |
— |
| goose | ✅ | ✅ | — | ✅ | — | — | — | ✅(逐消息) | — |
| zed | ✅ | ✅ | —(item.Image 无内联字节 → 占位 + 计数) |
✅ | — | — | ✅(Compaction 摘要挂 reasoning 块) | —(线程级才有时间) | — |
| crush | ✅ | ✅ | — | ✅ | — | — | ✅(自动摘要消息挂 reasoning 块) | ✅(created_at / finished_at) |
— |
| trae | ✅ | — | — | ✅ | — | — | — | ✅(逐消息) | — |
| vibe | ✅ | ✅ | ✅(内联 base64 图片块) | ✅ | — | ✅ | ✅(context_boundary compaction 原生检查点) | — | — |
| codearts | ✅ | ✅ | — | ✅ | — | — | — | ✅(逐消息) | ✅(prompt / completion / reasoning / cached 四桶) |
| dsh | ✅ | ✅ | ✅(原生附件引用原样带过,不重复存) | ✅ | — | ✅ | — | ✅(原生事件时间原样透传) | ✅(原生 usage 原样透传) |
「—」表示该源本来就没有这项能力,不是插件的缺陷。下面按列解释 ✅ 的具体含义:
images列的 ✅:该源能提供图片字节,导入后由宿主附件服务持久化。能不能真的落成,取决于宿主是否提供ctx.attachments,见 §3 的attachment-skipped。timestamps列的 ✅:逐记录时间戳会透传成会话事件时间,宿主据此折出真实的逐步模型耗时与工具耗时。usage列的 ✅:provider 回报的 token 用量会写进assistant/message.data.usage,宿主的 token 统计能看到。
首 token 延迟与输出速度,任何源都标不出来:外部转录没有流块时间戳,插件不伪造(见 §1 的 IR 说明)。
目标格式缺某项能力时,按「失败要大声」处理:降级必须显式报告,导出与互转的结果里都带 degradations 字段,不能静默。策略有三种:lossless(无损)、text-fallback(降级为文本块)、skip-placeholder(跳过并占位)。
| id | 能力缺口 | 策略 | 触发条件 |
|---|---|---|---|
tool-result-missing |
toolResults | skip-placeholder | 目标格式不记录工具结果(Cursor)→ 导入器兜底补发空结果 |
tool-result-text-fallback |
toolResults | text-fallback | 源格式工具消息无结构化参数(ChatGPT 网页导出)→ 按文本挂最近一步 |
reasoning-encrypted |
reasoning | skip-placeholder | 推理内容不可见(Codex 密文 encrypted_content,可读的 summary 仍照常导入)→ 密文部分无内容可导入 |
cwd-missing |
cwd | text-fallback | 无工作目录(ChatGPT / Grok Build)→ 落入专用导入工作区(见 architecture D16) |
branch-collapsed |
branches | text-fallback | 目标会话无分支概念 → 分支会话只导主线程 |
attachment-skipped |
attachments | skip-placeholder | 图片拿不到字节(宿主无 ctx.attachments、类型不收、超上限、源只有引用如 Kimi 的 blobref:)→ 该块以 [image] 文本占位并计入 imagesDegraded(导出方向读不回字节时同样计此项) |
compacted-unavailable |
compacted | text-fallback | 无压缩摘要 → 超长会话由预算三层保护被动截断 |
injection-skipped |
— | skip-placeholder | 非人类注入消息(system-reminder 等)不进入会话 → 跳过并计数 |
orphan-tool-result |
toolResults | skip-placeholder | 源日志无对应 tool/call 的工具结果(中途开始的 transcript)→ 丢弃并计数 |
usage-unknown |
— | text-fallback | 目标格式要求用量计数(opencode 的 cost / tokens 是解码必填)而事件没有 provider 回报 usage → 写 0 并显式报告(事件带 usage 时如实回填,不计此项) |
export_bundle 产出 .dshbundle.json,是 interchange v1 的备份编码(事件级无损):
{
"bundle": "dsh-chat-import",
"format": "interchange-v1",
"version": 1,
"exportedAt": 1710000000000,
"sourceSessionId": "<DSH 会话 id>",
"title": "…",
"originalCwd": "C:\\work", // 机器相关:A 机原路径(跨机器时 B 机不可达)
"landingHint": "work", // 建议落点(originalCwd basename)
"log": "{…session 头…}\n{…事件行…}", // 原始会话日志(无损,经 convertDshJsonl 还原)
"sha256": {
"session": "<hex>", // sha256(log) 会话级指纹
"bundle": "<hex>" // sha256(除 sha256.bundle 外全部字段规范化 JSON) 文件级指纹
}
}还原分三步:restore_bundle 先校验文件级指纹(用来发现损坏),再校验会话级指纹,最后经
convertDshJsonl 导入成可继续的 DSH 会话。
跨机器用法:A 机导出,把 bundle 拷到 B 机,在 B 机还原,0 skipped。如果 originalCwd 在 B 机
不可达,会话会退回到 bundle 文件所在目录归组,结果里报告 cwdAvailable: false 和 groupedTo,
不静默处理。
把 §1 的 turns 文档加上内容标记,它就是一个可导入的文件。面板「从文件导入」会自动命中;
也可以用 /import auto <path>,或者 import_chat({ format: "local-jsonl" }):
{
"interchange": "dsh-chat-import", // 内容标记:探测层第一级判据
"version": 1, // 只认 1;其它值整体拒绝并点名版本
"meta": { "id": "…", "createdAt": 1710000000000, "cwd": "C:\\work", "sourceId": "…" },
"title": "会话标题(可选)",
"provider": "source-tool(可选,缺省 generic)",
"model": "…(可选)",
"turns": [ /* 与 §1 同构 */ ]
}- 探测:文件导入的第一级判据就是内容标记。
"interchange"命中本格式,"bundle"(§4 的便携包)转给restore_bundle。按契约,标记出现在文档顶部,而探测只扫前 64KB。一旦命中,就以标记为准,不再逐个格式去猜。 - 校验与降级:按 D4「失败要大声」,绝不静默。下列情况全部计数,并随结果透出:未知内容块类型;拿不到合法载荷的图片(降级为
[image]文本并计入imagesDegraded);畸形的轮与步;没有对应调用的工具结果;input / output 不是非负整数的usage。0 轮的文档会给一个skipReason,导入端据此拒绝,不产出没有对话的空壳会话。 - 会话 id:一律由插件铸造,格式是
import-<slug>,slug 优先取meta.sourceId,其次取meta.id。文档只提供 slug 的来源;命名约定与副本命名(import-<id>-<n>)都不由来源决定。工具面可以用sessionId显式覆盖。 - 字段能力:
turns[i].time、steps[j].time、toolResults[k].time、steps[j].usage、turns[i].compaction与shadowed、promptBlocks,都与各源转换器产出的 IR 同构。§2 的能力矩阵与 §3 的降级规则逐条适用。 - 用途:长尾来源的落点。任意外部工具的转录,只要写成这份文档,就能按普通文件导入,不必为每个来源内置转换器(识别失败时的三个出口之一就是它)。它是导入侧的契约;导出与备份方向仍然走 §4 的便携包,那个是事件级无损的。
{ "interchange": "dsh-chat-import", "version": 1, "meta": { "id": "import-<sourceId 或会话 id>", "createdAt": 1710000000000, // 毫秒 "cwd": "C:\\work", // 机器相关路径;缺失(ChatGPT 等)则无键 "sourceId": "<源会话 id>" // 各源显式写入,不从 import- 前缀反解 }, "title": "会话标题(可选)", "provider": "claude-code", // 源标识(同 imports registry 记录的 format/来源标签) "model": "claude-opus-4-7", // 源模型(可选) "turns": [ { "prompt": "用户提问", // 提问的文本投影(标题、空轮判定用它) "promptBlocks": [ // 可选:提问带图时的完整内容块(缺省 = 单个 text 块) { "type": "text", "text": "看这张截图" }, { "type": "image", "data": "<base64>", "mediaType": "image/png", "name": "shot.png" } ], "steps": [ { "model": "claude-opus-4-7", // 可选:该步自己的模型(中途换模型时逐步骤记名) "content": [ { "type": "text", "text": "助手正文" }, { "type": "reasoning", "text": "推理" }, { "type": "tool-call", "id": "call-1", "name": "read", "arguments": "{\"path\":\"a\"}" }, { "type": "image", "data": "<base64>", "mediaType": "image/jpeg" } ], "toolCalls": [ { "id": "call-1", "name": "read", "arguments": "{\"path\":\"a\"}" } ], "toolResults": [ { "toolCallId": "call-1", "content": [{ "type": "text", "text": "…" }], "isError": false } ], "time": 1767224650000, // 可选:该步助手消息的源时间戳(毫秒) "usage": { "inputTokens": 100, "outputTokens": 20, "cacheReadTokens": 30 } // 可选:provider 回报用量 } ], "aborted": false, // 可选:该轮被中断 "compaction": { "summary": "…", "provider": "claude-code", "model": "…" }, // 可选:本轮之前有源侧压缩 "shadowed": false // 可选:该轮已被后续压缩遮蔽(log-only) } ] }