DeepSeek Harness 飞书桥接回复为空:session format v2 排查与兼容
飞书侧回复突然全空,但会话日志里明明有文本,这是桥接的投影契约过期了,不是模型或网络的问题。 一个提交把 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):
- 0.1.5 代际:从
0.1.2-rc.1升到0.1.5-rc.1后,飞书回复 100% 静默丢失,turn 记为completed,日志与工具通道都正常。预期判据:会话事件里已经没有assistant/chunk。 - 0.2.0 代际:桥接在
0.2.0-rc.2上入站与工具卡片正常,正文恒为(No content returned)。预期判据:assistant/message里有文本,桥接只投影了usage。 - 版本边界:该提交不在
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都带。预期结论:升级跨过这条线就会中。 - 回滚验证:退回
0.1.2-rc.1立即恢复。预期结论:旧版本仍写 format 1,旧渲染路径还有数据可读。
注意别把另一个问题混进来:同版本族里 provider/model 读不到属于 composed profile tree 的配置解析,和正文为空无关。
DeepSeek Harness session format v2:两代症状与正文位置
v2 之后 core 只保证「结算」,把结算投影成卡片正文是消费方的责任。 事件形状如下:
assistant/message携带{ turn, step, message, stream, usage?, interrupted? },其中message.content是组装好的内容块,stream是产生这些块的带时序流。assistant/attempt携带{ turn, step, stream },用于失败、重试、取消、流错误等没有提交可见消息的尝试。- 逐 token 的
assistant/chunk已被删除;agent/assistant-stream是进程内事件域,唯一远程消费者是 Web 的会话跟随适配器,不是给外部通道用的投递面。 - 空回复的判定因此很直接:
assistant/message存在、message.content里有文本块,而桥接输出为空,就是投影漏了。
据讨论,官方给出的低成本核对法是比对两个 tag 之间的 docs/persistence-catalog.md,里面为每种持久化类型钉了 SHA-256,事件形状一变哈希就变,不用读代码。
DeepSeek Harness 升级前四步判断会不会中招
判断依据是格式版本与订阅的事件,不是桥接自己的版本号。 四步可以在一台机器上做完:
- 确认目标宿主 tag 是否包含提交
f99b06eaed。预期结果:0.1.3-alpha.1及之后的 tag 都包含,0.1.2-rc.1不包含。 - 在桥接源码里搜
assistant/chunk。预期结果:有命中就说明它还在按旧契约渲染,升级后大概率空回复。 - 打开一个已有会话的事件日志,搜
assistant/message并看message.content。预期结果:v2 之后这里应有完整文本块,若为空才是模型侧问题。 - 对照桥接的
COMPATIBILITY.md与实际依赖的 SDK 版本。预期结果:声明只覆盖旧代际时,先按路径一改投影,再升级宿主。
四步里第 2 步最关键:只要桥接还在等 chunk,正文就永远不会出现,日志再完整也没用。
DSH插件路径一:桥接改读 assistant/message.message.content
这是唯一能让桥接继续跟进新版本的做法,改动集中在一个投影函数。 步骤:
- 在桥接里找到处理会话事件的分支,确认它订阅了
assistant/chunk。预期结果:能看到按 chunk 累积文本的旧逻辑。 - 增加对
assistant/message的处理:取event.message.content,过滤出文本块并按顺序拼接。预期结果:非流式运行时也能拿到完整正文。 - 保留
usage投影,但不要再把它当成正文来源。预期结果:计费统计不变,正文不再依赖usage。 - 把未知事件类型改为忽略而不是报错。预期结果:后续 format 升级时桥接不会因为多出的事件类型直接崩。
- 用一条普通消息验证。预期结果:飞书侧收到完整正文,
(No content returned)消失。
如果桥接还想保留流式打字效果,可以从 assistant/message.stream 里回放,而不是回头去等已经删除的 chunk 事件。
DeepSeek Harness 路径二:回滚到 0.1.2-rc.1
这条路只适合救急,代价是把整个宿主钉在旧代际。 操作顺序:
- 确认当前桥接依赖的 SDK 版本,以及它的兼容声明覆盖到哪一代。预期结果:知道回滚到哪个宿主版本能同时满足桥接与现有插件。
- 把宿主降到
0.1.2-rc.1并重启。预期结果:飞书回复恢复,日志里重新出现assistant/chunk。 - 记录回滚原因与时间,避免下次升级又踩同一个坑。预期结果:团队知道这是一条临时路径,不是长期方案。
- 评估副作用:0.2.x 的能力与要求新代际的周边插件都用不了。预期结果:能明确回滚的业务代价。
回滚不会修好桥接,只是把契约换回旧的。只要还想升到 0.2.x,路径一或路径三迟早要做。
回滚期间不要再顺手升级桥接或 SDK,否则两个变量一起变,下次定位会更难。把宿主版本、SDK 版本、桥接版本三个号记在同一行,出问题能一眼看出是谁跨了代际。
DeepSeek Harness 路径三:改用 session.event 事件流
通道插件最稳的姿势是不解析会话日志,只消费 SDK 的稳定事件面;现成桥接插件的兼容声明可以在 DSH Plugin Hub 对照。 讨论给出的最小面很小:
- 订阅通知:
session.event({ sessionId, event })、session.status(idle/running)、subagent.started、subagent.finished。预期结果:入站消息、状态指示、子代理进度都能覆盖。 - 使用请求:
initialize、session/prompt、session/resume、session/wait、session/working-directory/get、session/working-directory/set、shutdown。预期结果:一个聊天通道所需的收发闭环齐全。 - 把
session.event里的assistant/message投影成卡片正文,session.status驱动「正在输入」。预期结果:不再依赖任何进程内事件。 - 按格式版本而不是客户端版本做兼容验证。预期结果:升级前先看
SESSION_FORMAT_VERSION有没有变,而不是等用户报空回复。 - 用临时
DSH_HOME跑一次真实收发。预期结果:新代际下正文正常,旧代际下也不报错。
据讨论,ACP 是自动化用途,不是通道 API,不建议拿它搭桥接。
DeepSeek Harness 迁移后的验收清单
验收只看四件事:正文、失败尝试、未知事件、工具块。 依次检查:
- 发一条纯文本消息。预期结果:正文完整,没有
(No content returned)。 - 触发一次模型失败或重试。预期结果:桥接看到
assistant/attempt,不会把它误当正文,也不会重复发送。 - 人为注入一个未知事件类型。预期结果:桥接忽略并继续工作,不抛致命错误。
- 回放一段带工具调用的历史。预期结果:工具卡片与正文各自呈现,正文不会被工具块覆盖。
- 用旧版宿主再跑一次同一组用例。预期结果:桥接能明确报出版本不匹配,而不是静默返回空正文。
据讨论,这个缺口需要第三方桥接自己改投影,宿主侧没有可回滚的开关。把「按 settlement 渲染」写进桥接的契约测试,比每次升级后人工试一条消息可靠。
如果桥接已有自己的测试目录,把上面四条固化成用例,升级宿主前先跑一遍,成本比在生产上等用户报空回复低得多。测试里记得把事件载荷当外部数据,不要依赖进程内状态。
这四个用例也可以直接当作升级 SDK 前的准入检查。
来源:Discussion #10074、deepseek-ai/deepseek-harness。飞书机器人不回复的其它原因另见 DSH 机器人不回复。
常见问题
不是模型没输出,是 DeepSeek Harness 桥接读错了事件。session format v2 删除了 assistant/chunk,正文唯一的持久载体变成 assistant/message.message.content;桥接如果仍从 chunk 渲染、只从 message 里投影 usage,就会在 turn 正常完成的同时打印空内容。
因为 DeepSeek Harness 0.1.2-rc.1 仍然写 format 1,日志里还有 assistant/chunk,旧桥接的渲染路径能拿到逐段文本。这个提交从 dsh-v0.1.3-alpha.1 起才存在,所以回滚等于换回了旧契约,并没有修复桥接本身。
应该读 DeepSeek Harness 的 assistant/message.message.content,并只把它当作已结算的消息正文。按讨论给出的最小稳定面,通道插件订阅 session.event 就够了,不要自己解析会话日志,也不要依赖只存在于进程内的 assistant-stream 事件域。
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
来源
- deepseek-harness Discussion #10074:飞书/Lark 通道的跨代际兼容缺口,0.1.5+ 回复丢失/正文为空· deepseek-ai(GitHub Discussions)
- deepseek-ai/deepseek-harness 官方仓库· GitHub