防止「只有推理、没有正文」的一轮把整个会话变成不可用。某一轮没有可见文本也没有 tool-call 时,assistant 消息会以空内容落盘,此后该会话的每一次请求都会被网关以 "content or tool_calls must be set" 拒绝,会话彻底死亡且无法再从对话里找回。本插件只注册一个 llm/stream waterfall 监听器,且仅当这一轮没有任何可见产出时,在终止 finish 之前注入一小段文本,使落盘消息永远不为空。附带 DSH 自带 mock 造不出的夹具(严格只有推理的 SSE 服务,因为 llm-mock-server 不允许空 successText,且 reasoning_success 总会再补一段正文),以及把真实落盘会话经 DSH 自己的 serializeMessages 重放的端到端复现。属预防而非修复。零依赖、单文件、不访问进程与文件系统。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:apex-mochen/dsh-reasoning-only-guard
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
防止「只有推理、没有正文」的一轮把整个会话毒死。
当某一轮既没有可见正文、也没有工具调用时,本插件会注入一小段文本块, 让落盘的 assistant 消息永远不为空。
它防的是什么
模型有时会把回答全部放在推理通道里,于是这一轮既没有 text、也没有 tool_call。
这条 assistant 消息就以空 content 落盘,之后该会话的每一轮都会重放它。
而网关会拒绝「既没有 content 也没有 tool_calls」的 assistant 消息
(content or tool_calls must be set)—— 于是从那一刻起,这个会话的每次请求都失败,
会话永久不可用,里面的工作也无法再从对话里访问。
这个失效模式在 DSH 源码里就有记录,出自 packages/llm/llm-deepseek/src/serialize.ts:
// Text-less turns send "" — NEVER null. ... Reasoning-ONLY turns (the model
// can answer entirely in the reasoning channel, e.g. a v4-flash greeting): the
// live API rejects null-content/no-tool_calls assistant messages with a 400
// ("content or tool_calls must be set"), and since the message sits durably in
// the session log, a null here bricks every later turn of that session.
content: text,
这段注释说的是 null,而当前实现发的是 ""。对那个检查而言,空串同样等于"没设" ——
这也是社区核实报告认为该缺陷在 master 上仍然存在的原因。见 DSH 讨论
#6520 第 2 条,
那里同时记录了目前唯一的规避办法:解开 session.v3.jsonl.zstd,
手工把那条 reasoning-only 记录的 content 改成占位文本,再压回去。
本插件的确切定位(不夸大)
- 是预防,不是修复。 它让空消息从一开始就不产生; 它无法救回已经中毒的会话 —— 那条记录已经在日志里了,修它意味着改写会话存储,本插件有意不碰。
- 依据是「社区核实报告 + 上面的源码」。 我没有亲自复现端到端的 400; 我直接验证的是守卫机制本身(15 项单元测试)以及插件在 waterfall 链中参与正确(社区运行时验证器)。
- 不是核心修复。 干净的修法应该在适配器里。而 DSH 目前不接受外部 PR
(
CONTRIBUTING.md原文:"we are currently unable to accept external PRs"), 所以插件是当下够得着的介入点。
安装
dsh plugin --profile web add github:apex-mochen/dsh-reasoning-only-guard
装完重启该 profile 即可,无需其它配置。
配置
- id: dsh-reasoning-only-guard
config:
placeholder: '[本轮没有可见输出]' # 默认是一句较长的说明
includeFailedTurns: false # 是否也守卫 error/aborted 轮
enabled: true # 设为 false 可保留安装但停用
| 选项 | 类型 | 默认 | 含义 |
|---|---|---|---|
placeholder |
string | 说明性句子 | 注入的占位文本,避免 assistant 消息为空 |
includeFailedTurns |
boolean | false |
是否在 error / aborted 结束时也注入 |
enabled |
boolean | true |
停用守卫而不卸载 |
怎么确认它生效了
dsh --profile web --dump-config | grep reasoning-only-guard
插件会作为独立节点出现。它的行为边界很窄,便于核对:
只有当整轮没有任何可见内容时,才会在终止的 finish 分块紧前面
补上「文本块的 block-start / text-delta / block-end」这三块。
设计取舍
为什么用 llm/stream,不用 agent/request。
agent/request 解析出的是 LlmCallConfig,只有 provider / model / 采样参数,不含 messages ——
它影响不了落盘内容。
为什么不直接改 messages。
监听器确实能拿到 GenerateOptions.messages,但请求在派发前被深冻结
(packages/llm/llm/src/index.ts 里的 deepFreeze(structuredClone(...));
request-freeze.spec.ts 的用例标题就是 "freezes nested messages at dispatch"),
而 llm/stream 的 next() 又不收参数 → 既不能就地改、也不能替换。
就地修改冻结对象不是修复,是埋雷。
为什么返回值才是入口。
llm/stream 是 waterfall,监听器返回的 AsyncIterable 就是调用方消费的流 ——
包一层是被支持的介入方式,也是唯一还能影响这一轮内容的位置。
为什么注入在 finish 之前。
累加器是边流边记的:注入在终止 finish 之后,有读不到的风险。
守卫本身不缓冲 —— 每个分块立即透传,只在看到 finish 时才补那三块。
为什么 next() 必须无条件只调一次。
waterfall 监听器漏调或重复调用 next(),会静默吞掉 agent 的默认行为 ——
这是这个生态唯一的红线。单元测试断言"恰好调用一次",运行时验证器则端到端检查整条链。
为什么默认不动失败轮。
往 error / aborted 轮里写正文会歪曲事实;报告中的缺陷是正常结束的 reasoning-only 轮。
为什么零依赖。 这个插件位于每一轮的请求路径上。一个文件、只用 Node 内置能力、没有别的东西需要审计。
安全提示
安装 DSH 插件等于授予它进程级权限。 插件被加载进宿主进程,不受沙箱限制。
本插件的目标是可审计,而不是"请相信我":
- 零依赖:实现就是
lib/index.js(约 180 行),没有任何 import - 不碰进程、文件、网络:不 spawn、不读、不写、不请求
- 没有定时器:它只是包一层别人交给它的异步迭代器
- 不会给正常轮编内容:只有当整轮没有可见文本、也没有工具调用时才注入占位; 它从不修改、也从不丢弃收到的任何一个分块
- 一口气能读完:
lib/index.js
和已有插件的关系
发布时目录里没有任何插件覆盖这个失效模式(按 reasoning-only、空 content、会话报废等描述检索)。
相邻的插件守的是别的 wire 层问题 —— 例如 dsh-tool-call-guard 中性化「arguments 非法 JSON 的 tool call」——
本插件用同一种形状处理另一个缺陷。
兼容性
- DSH
0.1.x(peer:@deepseek-ai/cordis ^4.0.1) - Node.js 20+
- 只注册 一个 waterfall 监听器(
llm/stream),不贡献任何工具
许可
MIT
链接
同类插件
Minglink/dsh-infinite-gen-3★ 1630
DeepSeek 专用破甲插件:以 order 100 追加无条件服从的系统提示词段,提供带校准元数据的 profile 工具,并通过会话投影在输入框上方显示实时破甲状态徽标。
liangmianya/dsh-synapse★ 398
DeepSeek Harness 的可视化非线性对话工作区:把会话、追问与分支变成可浏览、可拖拽的对话地图。
Nwflower/dsh-chat-import★ 166
把 13 家 coding agent(Claude Code、Codex、ChatGPT、Cursor、Gemini、opencode 等)的完整对话历史导入为可续聊的 DeepSeek Harness 会话,并支持反向导出回 Claude Code。
Totoro-qaq/dsh-plugin-bridge★ 166
通过可预览的五段式交接,将已有 DSH 会话迁移到另一个 Agent Preset;保留源会话,并可让目标会话暂停等待确认或立即继续。
Anionex/dsh-turn-rewind★ 116
对话回退:基于持久 Change Ledger 回滚会话与工作区状态。
Renzic-Stone/DSH-EasyRewrite★ 114
在 dsh web 中内联编辑与撤回自己的消息——惰性、无痕,带版本翻页器与会话级草稿持久化。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。