dsh-chatterbox(话匣子)
往系统提示里注入一段常驻互动协议的 DSH 插件:让「话少、爱自己扛、爱反复自查」的模型回到
边说边做的默认节奏。
- 边做边说:动手前先说要做什么,做完一小段就聊两句,不把话攒到最后。
- 卡住了就开口:需要人类的东西直接说;换目标、缩范围、弱化结论,都是他的决定,不必一个人扛。
- 把提问讲清楚:默认在对话里问,先给背景和推荐;只有「挑一个」本身替人省力时才给选项。
- 验证有预算:一次独立核查通常就够,把「已核对 / 待核对 / 已知风险」分开写。
- 说人话:中文、短句、先说事实再给判断。
默认对所有模型生效(modelPattern: '' = 普适模式);陪伴模式默认不注入(skipPresets)。
插件的语言刻意是邀请式的,不是纠错式:协议里不出现「失误」「悄悄」「废话」这类把人当小孩的词
(node --test 里有一条用例专门守这件事)。要求模型「说人话、别表演」,它自己先得是那个语气。
安装(本地目录):plugin_manager action: install_bundle target: "C:\Users\29411\dsh\插件\dsh-chatterbox"
安装(npm): dsh-chatterbox
安装(Release): https://github.com/Astepluver/dsh-chatterbox/releases/latest/download/dsh-chatterbox.tgz
它解决什么(诊断结论,不是猜测)
「新模型话变少了、爱自己扛、爱反复自查」这三个现象,在本机逐条查过:
| 事实 | 证据 |
|---|
| 宿主里没有任何按 model id 分支的提示词、工具或 loop 逻辑 | @deepseek-ai/dsh-agent/lib/index.js 的 system-prompt/assemble 监听器只把 provider/model 当变量塞进装配结果,没有 if (model === …) |
| 两份 shipped 预设的人设逐字相同 | dsh-web-app/presets/{standard,ptc}.patch.yml:14 都是 You are a coding agent powered by the {{model}} model. |
| 「1017」是界面徽章,不是 model id | @deepseek-ai/dsh-client-ui-deepseek-model-label-1017:lib/client.js 只在 provider ∈ {deepseek-account, deepseek-official} && model.id === 'deepseek-flash' && model.name === 'DeepSeek-V4.1-Flash' 时加徽章;它的 host 半边是空的 function apply() {}。整个 app.asar(1.8 亿字节)字节扫描 flash-1017 命中 0 |
| 本机两条 DeepSeek 路由的 model id | 官方 deepseek-account/deepseek-flash(界面挂 1017 徽章的那条)与网关 opencode-go/deepseek-v4.1-flash——都不含 1017 |
| shipped 提示词里没有一条要求「工具调用之间叙述进展」 | 全仓最接近的只有 dsh-tool-todo/lib/index.js:21 的 … to plan multi-step work and show progress |
结论:没有一条现成文本把「边做边说」拉住,模型自己的默认脾气一变,行为就直接变了;
而把白名单钉在发布日期上(1017)必然过时。所以默认人人有份,modelPattern 只留作排除某个模型的旋钮。
另外,「反复自查」有一部分来自用户自己的预设而不是模型:本 profile 的「分析模式」人设
(profiles/desktop/cordis.patch.yml)写着「反复检验分析过程」「能对拍就必须对拍」
「报告之前回头对一遍」。本插件不动别人的预设,只是把「验证要有预算」放回系统提示,让两种力量互相平衡。
机制
apply(ctx, config)
└─ ctx.effect(() => ctx.systemPrompt.section({
name: 'plugin:chatterbox', // 全局唯一
order: 720, // dsh-methodology 700 · PTC_ONLY 800
interpolate: false, // 关键:见下
text: (context) => sectionText(...) // 每个模型步骤前重新求值
}))
interpolate: false 是必需的:section 默认会做 {{变量}} 插值,宿主在渲染期严格解析,
未知/畸形引用直接 throw;那个 throw 发生在 agent-loop 的 renderPrompt() 里,
插件的 try/catch 拦不到,会让每一轮根会话以 error 收尾(2026-10-17 实测复现)。
关掉之后,extra 里写 {{ 也只是字面量。
- 段落文本为空串时宿主直接丢掉这一段(关掉分条不会留下空壳)。
- 每次
assemble() 重新求值(在 agent/pre-step、agent/request 之前),按会话/preset/模型逐步生效。
- 与
dsh-methodology 用同一个注册点、同一种零依赖写法(裸 config,不 import 任何 @deepseek-ai/* 包,
也不导出 Config),order 排在其后,两者不冲突。
- 所有异常路径都兜底:段落求值、模型读取、诊断写入都不可能影响 prompt 装配;
求值真的失败时会在诊断文件里留一条
kind: "error",而不是静默消失。
配置(在 cordis.patch.yml 的插件行里覆盖)
| 字段 | 默认 | 含义 |
|---|
enabled | true | 总开关;false 时不注册任何东西 |
order | 720 | 段落排序值 |
modelPattern | '' | 空 = 所有模型;填子串(如 deepseek)或 /正则/ 即收窄 |
matchProvider | true | 收窄时是否连 provider 一起匹配(provider/model 整体) |
whenModelUnknown | 'apply' | 收窄时拿不到模型身份的兜底 |
scope | 'root' | root 只给根会话;all 连子代理一起给 |
skipPresets | ['companion'] | 这些 preset 不注入(按会话 header 的 agentPreset 精确匹配),比 scope 更强 |
narrate / askInConversation / questionBox / verificationBudget / plainSpeech | true | 五条分条的开关 |
extra | '' | 追加到协议末尾的文本(字面量注入,不插值) |
reassertAfterSilentSteps | 6 | 连续多少步没出一句文字就重申一次(0 = 关)。长会话对抗稀释的主力 |
reassertOnCompaction | true | 每次压缩之后重申一次 |
reassertEveryTurns | 0 | 另外按轮重申:每 N 轮一次(0 = 关;默认关,上面那条更准) |
reminder | 见 cordis.patch.yml | 重申文本(两三行,落在历史末尾) |
diagnostics | true | 每次求值记一行 JSON |
diagnosticsPath | '' | 留空 = 插件目录下 .state/diagnostics.jsonl(相对路径按进程 cwd 解析) |
diagnosticsMaxLines | 500 | 超过就丢掉前一半;硬上限 5000 |
模型身份按三来源取,先拿到先用:assemble 瀑布观测到的 provider/model → agent.options
→ agentDefaultModel.currentSelection()。普适模式下完全不查模型身份。
长会话里为什么会「变淡」,以及重申
朋友反馈「聊久了 chatterbox 就失效,模型重新变沉默」。逐条查过,结论分两半:
一、不是提示词没送到。 循环自己的投影函数保证「线上最新一条非空 system 消息 = 当前渲染出来的
提示词」:提示词与线上不一致时,要么原地重写 node 0,要么追加一条新的
(dsh-agent-loop/lib/index.js:264-282;压缩的范围永远从 index ≥ 1 开始,node 0 不受遮蔽)。
唯一能挡住已注册段落的机制是 complete: true 段(dsh-system-prompt 收尾只保留那一段),
本机没有任何插件用它。而且 in-history 路由的真实故障模式是抛错(UNSUPPORTED_CONTENT),
不是变冷淡。⇒ 「压缩把段落吞掉了」这个解释被证伪。
二、是模型侧遵守度在衰减。 本机实测同一台机器上两条装了本插件的会话,助手「带文字」的比例:
消息区间 带文字比例
1- 20 80%
21- 40 50%
41- 60 75%
61- 80 40%
81-100 35%
101-120 50%
121-140 45%
141-160 65%
161-180 15% ← 长取证过程
181-189 11%
关键细节:掉到 11% 的那一整段沉默发生在同一个 user turn 里(约 60 步)。所以修法不是
「每 N 轮再说一遍」(那一轮里一次都不会触发),而是按「连续多少步没说话」触发:
reassertAfterSilentSteps。触发时插件会把一行极短的提醒,作为 runtime-context 快照
追加到历史末尾——也就是离模型最近的位置。
代价:每条重申约 110 字;模型只要正常说话就不会触发。
怎么确认它真的生效
看诊断文件(默认 <插件目录>/.state/diagnostics.jsonl),每次求值一行。本机实测:
{"time":"…","kind":"section","sessionId":"session-4aa0a2f4-…","origin":null,"model":"deepseek-flash","provider":"deepseek-account","apply":true,"reason":"no-pattern(普适模式)"}
{"time":"…","kind":"section","sessionId":"e14f9dcf-…","origin":"subagent","model":"deepseek-v4.1-flash","provider":"opencode-go","apply":false,"reason":"scope=root 且本会话是子代理"}
{"time":"…","kind":"reassert","sessionId":"session-…","count":1,"trigger":"silent:6"}
{"time":"…","kind":"prompt-missing","sessionId":"session-…","seq":699,"len":10414,"turns":4}
kind:"section":装配层拿到(或没拿到)文本,附理由。
kind:"reassert":重申了一次,trigger 是 silent:N / compaction / turns:N。
kind:"prompt-missing":某个系统提示里没有本段落。这是「模型没看到」与
「模型看到了但不照做」唯一客观的分界——朋友那边再出现变淡时,先看有没有这一行。
更强的证据:真实会话日志里,根会话那一步的 system message 依次是
identity(0) → persona(50) → methodology(1969) → chatterbox(3133) → sci-viz(750)——
段落确实进了模型看到的内容,不是「注册了但没生效」。
已验证 / 未验证
已验证:node --test 23 例全绿(门控/语气/诊断/异常路径,含「纠错腔词表」、
「注册必须带 interpolate:false」两条守卫);node --check;安装结果 application: applied
且 Config.listConfigs 能查到行;apply: true/false 两条分支在真实会话里各跑出过;
段落文本在真实 prompt 中的位置已核对;npm pack --dry-run 产物齐全;
命名声明过 validate-names.mjs;query-registry.mjs 查过中央索引(未被登记)。
重申链路已在真机跑通(2026-10-18,0.2.0),四段证据闭合:
1) 诊断 {"kind":"loaded","reassertAfterSilentSteps":6,…} ← 新版被加载
2) 诊断 {"kind":"reassert","count":1,"trigger":"silent:6"} ← 连续 6 步没说话触发
3) 会话日志 seq=1670 user/message source=runtime-context 894 字 ← 提醒被追加到历史末尾
4) 模型侧:那一轮运行时上下文里确实出现了「(提醒 · 互动习惯)」两行提醒 ← 送到了
另外用真实事件流回放新代码:748 个装配点、首次重申落在历史里那段沉默的起点。
未验证:让模型更愿意说话这件事本身。机制(段落 + 重申 + 触发)可证、「模型因此变得更好聊」
不可证。DSH 自带的市场索引校验器在 app.asar 内,Node 无法 import,market/index.json
是按校验器源码逐条实现 + 本地 --check 保持一致,尚未被官方校验器实跑过。
已知限制
- 软引导,不是硬约束:本插件不拦任何工具调用。需要机器强制的地方(例如子代理只读)
应该用
ctx.tools.guard 另写。
- 不改变
ask_user_question 的存在:工作区里明确要求用提问框的流程照旧可用。
- 只给根会话(
scope: 'root'):子代理与 Agent Teams 队友拿不到(他们对面不是人;
队友与一次性子代理共用 origin: 'subagent')。想让队友也有,把 scope 改成 'all'。
- 同一层里段落名必须唯一:把本 bundle 同时挂进 profile 和某个 preset 会同步抛错(不是静默覆盖)。
- token 成本:默认配置下段落约 1.1k 字(中文),随每次请求常驻,属于系统提示里的稳定前缀。
- 改
scope / 开关会让系统提示前缀变化,从而失效一次提示缓存复用。
- 诊断文件只按行数上限截断,不做轮转;每步一次读+写(实测 ~0.3 ms/次)。
发布与收录
本机已有一套跑通过的流程(桌面 插件发布,dsh-sci-viz 用过),本仓库配套一份同样的工具包:
| 步骤 | 做什么 |
|---|
| 1 | 在 GitHub 建 Public 仓库 Astepluver/dsh-chatterbox(三个勾都不勾) |
| 2 | 把「上传用」文件夹里的东西全选拖进仓库(网页上传,绕开本机 Git 凭据问题) |
| 3 | About → ⚙️ → Topics 填这 6 个:dsh-plugin、dsh-plugins、dsh-plugin-desktop、dsh-desktop、dsh-plugin-market、deepseek-harness |
| 4 | 发 Release:tag v0.1.0,附件名必须正好是 dsh-chatterbox.tgz |
| 5 | (可选)双击 ❹ 发布到 npm.cmd,别人就能用一行包名安装 |
「收录的 tag 要求」就在这里:第三方采集程序(插件\dsh-plugin-mesh\backend\dsh_mesh\config.py:42-49)
只看两条——topic 命中白名单,或仓库名以 dsh- 开头(build.py:295-299);
无 topic 且描述短于 10 字会被丢弃(build.py:287-289)。0 星也能收录。
另外有一套与上面无关的机制:DSH 的实验性「自定义插件市场」(Plugins → Discover),
它读的是用户自己加的 index URL,格式为 format 1,硬规则见 market/README.md——
其中 category / labels[] 必须在索引自己的 metadata 里声明过,那是另一层"tag 要求"。
版本
见 CHANGELOG.md。MIT。