Skip to content

Latest commit

 

History

History
196 lines (170 loc) · 15.6 KB

INTERCHANGE.md

File metadata and controls

196 lines (170 loc) · 15.6 KB

Interchange v1 —— dsh-chat-import 会话交换协议

机器实现见 lib/convert/interchange.mjs,里面有 DEGRADATION_RULES、summarizeDegradations 和 exportDegradations。

手上有一份未识别的对话记录,想把它转成本协议? 本文是给维护者看的规范。真正动手转换请读 转换指南(给 Agent 的 Skill 指令) 或 Conversion Guide(英文)——最小示例、字段表和自检清单都在那里。

本协议定义导入与导出共用的 turns IR,以及便携 bundle 的格式。

1. 文档结构(v1)

{
  "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)
    }
  ]
}
  • content 的块类型与 DSH 会话事件同构:text、reasoning、image、tool-call、tool-result。 工具结果该落在 toolResults[]。写在 step content 里的 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 和双层指纹。

2. 各源能力矩阵

这张表描述的是「源格式能记录什么」。缺某一项能力,是源本身固有的有损,不是插件的缺陷。

源 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 说明)。

3. 降级规则表

目标格式缺某项能力时,按「失败要大声」处理:降级必须显式报告,导出与互转的结果里都带 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 时如实回填,不计此项)

4. 便携 bundle

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, 不静默处理。

5. generic 文档作为导入格式

把 §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 的便携包,那个是事件级无损的。