DeepSeek Harness 飞书桥接回复为空:session format v2 排查与兼容

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek Harness飞书机器人session format v2assistant/message桥接插件
DeepSeek Harness 升到 session format v2 后删除了 assistant/chunk,飞书桥接仍按旧事件取正文,turn 记完成但回复为空。本文给改读 assistant/message、回滚旧版、改用 session.event 三条路。

飞书侧回复突然全空,但会话日志里明明有文本,这是桥接的投影契约过期了,不是模型或网络的问题。 一个提交把 SESSION_FORMAT_VERSION 从 1 提到 2 并删掉了 assistant/chunk,正文的唯一持久载体变成 assistant/message.message.content;桥接仍按旧事件渲染、只从 message 里取 usage,于是 turn 正常完成而用户什么也收不到(Discussion #10074)。

两个现象是同一条契约断裂的两个阶段,回滚能恢复恰好证明断点在写入侧。 讨论里把边界钉在提交 f99b06eaed(feat(session)!: embed assistant streams in format v2):

  1. 0.1.5 代际:从 0.1.2-rc.1 升到 0.1.5-rc.1 后,飞书回复 100% 静默丢失,turn 记为 completed,日志与工具通道都正常。预期判据:会话事件里已经没有 assistant/chunk。
  2. 0.2.0 代际:桥接在 0.2.0-rc.2 上入站与工具卡片正常,正文恒为 (No content returned)。预期判据:assistant/message 里有文本,桥接只投影了 usage。
  3. 版本边界:该提交不在 dsh-v0.1.2-rc.1,从 dsh-v0.1.3-alpha.1 起存在,0.1.5-rc.1、0.1.7-rc.2、0.2.0-rc.2 都带。预期结论:升级跨过这条线就会中。
  4. 回滚验证:退回 0.1.2-rc.1 立即恢复。预期结论:旧版本仍写 format 1,旧渲染路径还有数据可读。

注意别把另一个问题混进来:同版本族里 provider/model 读不到属于 composed profile tree 的配置解析,和正文为空无关。

DeepSeek Harness session format v2:两代症状与正文位置

v2 之后 core 只保证「结算」,把结算投影成卡片正文是消费方的责任。 事件形状如下:

  1. assistant/message 携带 { turn, step, message, stream, usage?, interrupted? },其中 message.content 是组装好的内容块,stream 是产生这些块的带时序流。
  2. assistant/attempt 携带 { turn, step, stream },用于失败、重试、取消、流错误等没有提交可见消息的尝试。
  3. 逐 token 的 assistant/chunk 已被删除;agent/assistant-stream 是进程内事件域,唯一远程消费者是 Web 的会话跟随适配器,不是给外部通道用的投递面。
  4. 空回复的判定因此很直接:assistant/message 存在、message.content 里有文本块,而桥接输出为空,就是投影漏了。

据讨论,官方给出的低成本核对法是比对两个 tag 之间的 docs/persistence-catalog.md,里面为每种持久化类型钉了 SHA-256,事件形状一变哈希就变,不用读代码。

DeepSeek Harness 升级前四步判断会不会中招

判断依据是格式版本与订阅的事件,不是桥接自己的版本号。 四步可以在一台机器上做完:

  1. 确认目标宿主 tag 是否包含提交 f99b06eaed。预期结果:0.1.3-alpha.1 及之后的 tag 都包含,0.1.2-rc.1 不包含。
  2. 在桥接源码里搜 assistant/chunk。预期结果:有命中就说明它还在按旧契约渲染,升级后大概率空回复。
  3. 打开一个已有会话的事件日志,搜 assistant/message 并看 message.content。预期结果:v2 之后这里应有完整文本块,若为空才是模型侧问题。
  4. 对照桥接的 COMPATIBILITY.md 与实际依赖的 SDK 版本。预期结果:声明只覆盖旧代际时,先按路径一改投影,再升级宿主。

四步里第 2 步最关键:只要桥接还在等 chunk,正文就永远不会出现,日志再完整也没用。

DSH插件路径一:桥接改读 assistant/message.message.content

这是唯一能让桥接继续跟进新版本的做法,改动集中在一个投影函数。 步骤:

  1. 在桥接里找到处理会话事件的分支,确认它订阅了 assistant/chunk。预期结果:能看到按 chunk 累积文本的旧逻辑。
  2. 增加对 assistant/message 的处理:取 event.message.content,过滤出文本块并按顺序拼接。预期结果:非流式运行时也能拿到完整正文。
  3. 保留 usage 投影,但不要再把它当成正文来源。预期结果:计费统计不变,正文不再依赖 usage。
  4. 把未知事件类型改为忽略而不是报错。预期结果:后续 format 升级时桥接不会因为多出的事件类型直接崩。
  5. 用一条普通消息验证。预期结果:飞书侧收到完整正文,(No content returned) 消失。

如果桥接还想保留流式打字效果,可以从 assistant/message.stream 里回放,而不是回头去等已经删除的 chunk 事件。

DeepSeek Harness 路径二:回滚到 0.1.2-rc.1

这条路只适合救急,代价是把整个宿主钉在旧代际。 操作顺序:

  1. 确认当前桥接依赖的 SDK 版本,以及它的兼容声明覆盖到哪一代。预期结果:知道回滚到哪个宿主版本能同时满足桥接与现有插件。
  2. 把宿主降到 0.1.2-rc.1 并重启。预期结果:飞书回复恢复,日志里重新出现 assistant/chunk。
  3. 记录回滚原因与时间,避免下次升级又踩同一个坑。预期结果:团队知道这是一条临时路径,不是长期方案。
  4. 评估副作用:0.2.x 的能力与要求新代际的周边插件都用不了。预期结果:能明确回滚的业务代价。

回滚不会修好桥接,只是把契约换回旧的。只要还想升到 0.2.x,路径一或路径三迟早要做。

回滚期间不要再顺手升级桥接或 SDK,否则两个变量一起变,下次定位会更难。把宿主版本、SDK 版本、桥接版本三个号记在同一行,出问题能一眼看出是谁跨了代际。

DeepSeek Harness 路径三:改用 session.event 事件流

通道插件最稳的姿势是不解析会话日志,只消费 SDK 的稳定事件面;现成桥接插件的兼容声明可以在 DSH Plugin Hub 对照。 讨论给出的最小面很小:

  1. 订阅通知:session.event({ sessionId, event })、session.status(idle / running)、subagent.started、subagent.finished。预期结果:入站消息、状态指示、子代理进度都能覆盖。
  2. 使用请求:initialize、session/prompt、session/resume、session/wait、session/working-directory/get、session/working-directory/set、shutdown。预期结果:一个聊天通道所需的收发闭环齐全。
  3. 把 session.event 里的 assistant/message 投影成卡片正文,session.status 驱动「正在输入」。预期结果:不再依赖任何进程内事件。
  4. 按格式版本而不是客户端版本做兼容验证。预期结果:升级前先看 SESSION_FORMAT_VERSION 有没有变,而不是等用户报空回复。
  5. 用临时 DSH_HOME 跑一次真实收发。预期结果:新代际下正文正常,旧代际下也不报错。

据讨论,ACP 是自动化用途,不是通道 API,不建议拿它搭桥接。

DeepSeek Harness 迁移后的验收清单

验收只看四件事:正文、失败尝试、未知事件、工具块。 依次检查:

  1. 发一条纯文本消息。预期结果:正文完整,没有 (No content returned)。
  2. 触发一次模型失败或重试。预期结果:桥接看到 assistant/attempt,不会把它误当正文,也不会重复发送。
  3. 人为注入一个未知事件类型。预期结果:桥接忽略并继续工作,不抛致命错误。
  4. 回放一段带工具调用的历史。预期结果:工具卡片与正文各自呈现,正文不会被工具块覆盖。
  5. 用旧版宿主再跑一次同一组用例。预期结果:桥接能明确报出版本不匹配,而不是静默返回空正文。

据讨论,这个缺口需要第三方桥接自己改投影,宿主侧没有可回滚的开关。把「按 settlement 渲染」写进桥接的契约测试,比每次升级后人工试一条消息可靠。

如果桥接已有自己的测试目录,把上面四条固化成用例,升级宿主前先跑一遍,成本比在生产上等用户报空回复低得多。测试里记得把事件载荷当外部数据,不要依赖进程内状态。

这四个用例也可以直接当作升级 SDK 前的准入检查。

来源:Discussion #10074、deepseek-ai/deepseek-harness。飞书机器人不回复的其它原因另见 DSH 机器人不回复。

常见问题

飞书机器人升级 DeepSeek Harness 后回复全空但 turn 正常,是模型没输出吗?

不是模型没输出,是 DeepSeek Harness 桥接读错了事件。session format v2 删除了 assistant/chunk,正文唯一的持久载体变成 assistant/message.message.content;桥接如果仍从 chunk 渲染、只从 message 里投影 usage,就会在 turn 正常完成的同时打印空内容。

把飞书桥接回滚到旧版 DeepSeek Harness 0.1.2-rc.1 为什么就能恢复?

因为 DeepSeek Harness 0.1.2-rc.1 仍然写 format 1,日志里还有 assistant/chunk,旧桥接的渲染路径能拿到逐段文本。这个提交从 dsh-v0.1.3-alpha.1 起才存在,所以回滚等于换回了旧契约,并没有修复桥接本身。

第三方桥接该读 DeepSeek Harness 的 chunk 还是 assistant/message?

应该读 DeepSeek Harness 的 assistant/message.message.content,并只把它当作已结算的消息正文。按讨论给出的最小稳定面,通道插件订阅 session.event 就够了,不要自己解析会话日志,也不要依赖只存在于进程内的 assistant-stream 事件域。

飞书桥接连 DeepSeek Harness 的 provider 和 model 都读不到,是同一个原因吗?

DeepSeek Harness 正文为空和 provider/model 读不到不是同一个原因。正文为空是 session format 的投影契约变化,provider/model 读不到属于 composed profile tree 的配置解析问题,需要用 dump-config 看那一节是缺失还是被覆盖。两者要分开排查,不要用同一个补丁一起改。

相关术语

SESSION_FORMAT_VERSION
SESSION_FORMAT_VERSION 是 DSH 会话日志格式的版本号,决定事件类型与字段形状,由 core/session 的 types 统一声明,当前已发布的格式版本是 4。— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
assistant/message
assistant/message 是 format v2 起每次模型尝试的唯一持久结算事件,携带 turn、step、组装好的 message、原始 stream 与可选 usage,正文在 message.content 里。— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
assistant/attempt
assistant/attempt 是 format v2 新增的事件,记录失败、重试、取消或流错误的尝试,只带 turn、step 与 stream,不向模型可见历史添加消息。— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
session.event
session.event 是 SDK 协议里服务端到客户端的通知,载荷为 sessionId 与一条 SessionEvent,是通道类插件推荐消费的最小稳定面。— https://github.com/deepseek-ai/deepseek-harness/discussions/10074

来源