dsh-narrator
Manifest validDeepSeek Harness plugin: When the model thinks for a long time without vocalizing, inject a prompt that has it clearly state in the main text what it's doing and how far it's gotten, without interrupting the task; a floating window in the bottom-left corner of the conversation body synchronously displays this original text. Includes a minute-level graphical settings card, with zero runtime dependencies.
dsh-narrator
模型想得太久时,先替它说一句话,然后让它接着干。
DeepSeek Harness 插件。解决一个很具体的问题:模型长时间只思考、不出声时,界面上什么都没有,你不知道它是卡住了还是在干活。
装上之后,模型静默超过 N 分钟,它会自己用一段正文说清「在做什么、做到哪了、接下来干什么」,任务不中断;对话区左下角浮窗同步显示这段原文。

为什么这件事不简单
DSH 的代理循环里有这么一条(宿主源码 dsh-agent-loop):
const stepEnd = await this.step(decision);
if (turnEnds && decision.messages.length === 0) break;
if (turnEnds && this.inbox.nextStep.length === 0) {
await this.dispatch.serial("agent/turn-stopping", { turn, signal });
}
if (turnEnds && this.inbox.nextStep.length === 0) break;
一条不带任何工具调用的纯文本消息,会被判定为「回合结束」。 所以在提示词里要求模型「中途说一句」是不管用的——它真说了,回合就停了。这是宿主语义层的限制,不是插件能绕的小聪明。
本插件的做法:静默超阈值时只记账,什么都不做;等下一个 step 的边界,在 agent/pre-step 这个 waterfall 钩子里往请求多带一条消息。decision.messages.length 不为 0,回合就不关。
模型只思考、不出声
│
▼ 静默超过阈值(默认 60 秒)
只记一笔「该汇报了」 ← 界面上什么都还没动
│
▼ 下一个 step 边界
注入一段提示词
│
▼
模型用正文说一段
│
├─→ 对话区左下角浮窗显示这段原文
└─→ 正文继续走,回合不关闭
特点
- 话是模型自己说的。 插件只给四个要点(正在做什么 / 已完成什么 / 接下来做什么 / 卡在哪),具体内容由模型按当下情况写。模板只能拿到工具名和路径,模型知道自己刚才想明白了什么。
- 注入的消息用户看不见。
source: { kind: 'narrator', form: 'notice' }会被客户端归为 context 节点,界面隐藏,但模型照样读得到。想让原文可见,设置卡里一个开关。 - 按「多久没出文字」计时,不是按「某一步跑了多久」。 工具调用、思考都不算文字输出。模型连续调二十次工具却一句话不说,恰恰最该提示。
- 等子智能体时也说话。 主窗口派出子代理后那一步可能挂着几分钟。浮窗分三段:还在等 → 正在整理汇报 → 正在汇报,会告诉你它在等什么(等子任务 / 查资料 / 跑命令 / 读文件)。
- 图形设置卡。 分钟级滑杆、可编辑提示词、浮窗透明度与停留时长,全部即时生效不用重启。
- 浅色深色通用。 全部颜色走宿主的
--dsw-alias-*令牌,深色主题下浮窗自己切一套底色与描边。

安装
前置:DeepSeek Harness 0.2.1+(dsh.client 与 shell.overlay 槽位依赖它)、Node.js 18+。零运行时依赖,不需要 npm install。
面向普通用户的图文版说明见 安装与功能说明.md(安装 + 配置 + 排错)和 功能介绍.md(不读源码也能看懂它解决什么问题)。下面这段是给开发者的速览。
1. 拷贝到 profile 的 node_modules
git clone https://github.com/Asza-other/dsh-narrator.git
cp -r dsh-narrator ~/.dsh/profiles/desktop/node_modules/dsh-narrator
# Windows: %USERPROFILE%\.dsh\profiles\desktop\node_modules\dsh-narrator
desktop 换成你自己的 profile 名(DSH 设置里能看到)。
2. 改 profile 的 package.json,两处都要加
{
"dependencies": {
"dsh-narrator": "file:../../path/to/dsh-narrator"
},
"dsh": {
"profile": {
"bundles": ["dsh-narrator"]
}
}
}
⚠️
dependencies与dsh.profile.bundles缺一不可。 只加bundles,pnpm 重装时会被清掉;只加dependencies,插件根本不加载。改完如果 DSH 自动跑了一次pnpm install,回头复查bundles数组还在不在——插件管理器重写这个文件时有过把条目清掉的先例。
3. 重启 DSH
agent/pre-step 是后端钩子,刷新页面不够,必须重启进程。
4. 验证
设置里出现 设置 → 进度旁白 就是装好了。
curl http://127.0.0.1:<端口>/api/narrator/status
# {"ok":true,"version":"0.12.5",...}
端口看 DSH 窗口地址栏。这个路由只在 127.0.0.1 上响应,外部访问不到。
配置
设置页 → 左侧「进度旁白」。主要几项:

| 项目 | 默认 | 说明 |
|---|---|---|
| 让模型定期汇报 | 开 | 总开关 |
| 多久没有文字输出就让它说一句 | 1 分钟 | 1~60 分钟,线性滑杆或直接填数字 |
| 两次汇报至少间隔 | 1 分钟 | 防止刷屏。设得越小越频繁 |
| 汇报内容怎么给模型说 | 内置四点式 | 可整段替换;留空即用默认 |
| 在对话里显示这条提示词 | 关 | 开了之后 source.kind 换成 user,对话流里能看到注入原文 |
| 同时把汇报内容弹到对话区左下角浮窗 | 开 | 关掉后仍然注入,只是不显示浮窗 |
| 子任务里也照样汇报 | 关 | 子代理会话界面上看不到,注入纯属浪费一次模型调用 |
| 浮窗透明度 | 100% | 35%~100%,只影响浮窗底色 |
| 浮窗停留多久后自动消失 | 90 秒 | 5 秒~600 秒 |
保存和「恢复默认」固定在卡片下沿(position: sticky),滚到哪里都点得到。底栏还向下多涂了一截底色:宿主的滚动容器自己在滚动区里留了 24px 内边距,光靠 bottom: 0 盖不住那一段,滚过去的文字会从按钮底下露出来。

底部的「测试」区块可以把阈值临时压到几秒,方便当场看效果;关掉后恢复真实配置,真实配置一个字节都没被改过。
实测单步耗时通常 5~7 秒。建议:想经常看到选 1 分钟,只在明显卡住时提示选 3~5 分钟。设成 30 分钟以上基本不会触发。
配置写到 ~/.dsh/narrator/config.json,优先级高于部署时在 cordis.patch.yml 里预置的值。旧版配置里的键会被静默忽略,不会报错。
已知限制
- 注入不是硬保证。 机制能保证回合不关闭,但模型自己写完就停下来,那还是会停。默认提示词末尾的「说完必须继续执行,不要结束任务」就是在防这个。
- 注入有延迟。 到点只是记账,真正注入要等下一个 step 边界。如果那一步在等子智能体,可能要多等几分钟——浮窗这期间显示「还在等」,所以不会看起来像卡死。
- 只支持 web 端。 没有做桌面外壳的原生 UI。
- 前端改动要刷新页面。 后端配置即时生效,但
client.js的改动需要刷新页面重新拉 bundle。
开发
npm test # 67 项单测
node test/probe.mjs # 25 项端到端探针,模拟宿主真实事件时序
node test/demo.mjs # 四幕演示,不需要装 DSH
node test/check-status.mjs <端口> # 读运行中插件的自检数据,给人话诊断
test/ 下的探针是复刻宿主真实事件契约写的(agent/assistant-stream 的 frame 结构、agent/pre-step 的 waterfall 语义),不是 mock 出来的假时序。
深入:四条工程约束
这几条都是踩过的坑,改代码时别踩回去。
agent/pre-step是 waterfall。 签名是(payload, next) => decision,必须await next()拿到下游决策,再把消息并进去。别在next()之前 return。- 只有
decision.kind === 'enter'才能往里塞消息。reject表示宿主已经决定不进入这一步。 - 别用
session.append硬插 assistant 消息。 除了语义错误(会被同 step 的真实回复整体覆盖),还有三条暗礁:必须带reasoningblock、必须带stream: []、绝不能传sourceEventSeqs(宿主SessionPersistence对assistant/message有硬校验:raw !== void 0就抛)。v0.11 起已经不写这条路了。 - 判「是不是子代理」只能看
session.header。 宿主ReactLoopAgent的字段只有loopCtx / id / options / session / inbox / phase / activityDone / scope / ctx / dispatch,没有parent、parentSessionId、kind、role。真正的判据是两个会话头字段:session.header.origin === 'subagent'与session.header.parentSession。曾按那批宽松字段判断,结果includeSubagents: false在真实宿主上永远不生效。
与宿主原生格式的对照
| 本插件的做法 | 宿主原生对应 | 说明 |
|---|---|---|
agent/pre-step 注入 | 宿主自己的 runtime context | 同一个 waterfall,排在后面追加 |
source.kind: 'narrator' + form: 'notice' | webhook / subagent-settled 等 | 都走「归为 context、界面隐藏」这条路 |
source.kind: 'user'(显示原文时) | 用户真实发言 | 只为了让 isVisibleChatNode 放行 |
静默计时的语义
触发基准是「距最后一次正文输出的时长」,不是「某一步跑了多久」。这张表是定稿语义:
| 事件 | 重置计时器? | 说明 |
|---|---|---|
text-delta(非空) | 重置 | 这是「文字输出」,用户能看见 |
block-start(blockType: 'text') | 重置 | 同上 |
block-start(blockType: 'tool-call') | 不重置 | 调工具不是说话 |
tool-call-delta | 不重置 | 密集调工具恰恰是最该提示的场景 |
reasoning-delta | 不重置 | 想多久都不算开口 |
step start | 同回合内沿用,不归零 | 跨 step 累计,不因换步清零 |
step end | 只记时长,不清状态 | 清状态会让定时器在换步时被取消 |
turn/end | 清零 | 新回合从用户发言重新开始 |
注入成功之后同时把计时器推到现在——汇报本身算一次可见输出,否则定时器一醒就判定「仍然静默」而连发。
浮窗停在哪
贴在对话正文的左下角,与正文左对齐:纵向悬在输入框上方 16px 处,横向取消息列的左边界,读起来像一条跟着对话走的批注。
那个位置不是写死的,每次布局变化都重量一次(ResizeObserver + requestAnimationFrame 合并,拖侧边栏时不会掉帧):
left ← [data-chat-flow] 的左边界(扣掉 overlayLayer 的偏移)
bottom ← overlayLayer 底边 − [data-composer-seat] 顶边 + 16px
⚠ 这里有个必须知道的坑:[data-chat-flow] 在宿主 DOM 里不止一处(主聊天列一份,ChatGroupSeat 的工作步骤面板里还有一份)。document.querySelector 只返回文档序第一个,很容易命中侧栏那份——它的左边界贴近窗口边缘,算出来的 left 只有几个像素,浮窗就贴到整个窗口最左边、压住侧边栏,看起来像「跑到左下角了」。
所以这里按宿主自己的做法筛选:枚举全部候选 → 排掉 [data-sidebar-right-session] / [data-step-process-body] 内的、排掉不可见的 → 取宽度最大的那个。主对话列一定是同屏里最宽的那块。lib/client.js 的 measureSeat() 里有完整注释,test/client-render.test.mjs 里有对应的回归用例(喂一份「窄的在前面、宽的在后面」的假 DOM,断言取到宽的那个)。
浮窗的三个阶段
静默到点后,真正的注入要等下一个 step 边界。主窗口调子代理时这个 step 可能挂着好几分钟,等它结束再亮卡,用户早就以为卡死了。所以浮窗分三段:
| 阶段 | 标题 | 正文 |
|---|---|---|
| 到点,还没到 step 边界 | 等这一步跑完再汇总 / 等这一步结束再汇总 | 上一段原文(如果有)+ 一行「正在等子任务返回,已经等了 42 秒」 |
| 已注入,模型在憋那段话 | 正在整理汇报 | 同上,等待行换成「模型正在整理这次汇报」 |
| 模型开口了 | 正在汇报 | 模型的原话,替换掉整张卡 |
标题随「此刻在不在等工具返回」二选一:有工具在跑说「等这一步跑完再汇总」,没工具(等 step 边界)说「等这一步结束再汇总」。两句话说的是同一件事——step 没结束,模型还没机会开口,差别只在于要不要顺带点出是哪个工具在拖。
两条容易踩的细节:
- 上一段原文必须留着。 早期实现到点后直接写一条
text: ''的新记录,结果是模型刚汇报完、静默重新起算,到点那张卡「啪」地变成空白——用户眼前的信息反而变少了。现在只翻开pending标记,原文原地保留。 phase不能被降级。 模型已经拿到「请汇报」的提示、正憋那段话时,这段静默恰恰是它在整理汇报,界面该说「正在整理汇报」;无条件写回working会让用户以为提示词没送到。
浮窗怎么渲染正文
正文按空行切段、段内换行原样保留,同时认四种 markdown 结构:
| 写法 | 渲染成 |
|---|---|
# / ## / ### + 空格 | 三级标题,13 / 12.5 / 12px |
- / * / + 或 1. + 空格 | 列表,标记独立成列 |
| 甲 | 乙 | 配一行 |---|---| | 真表格(GFM) |
`code` / **粗** / *斜* | 行内代码 / 粗体 / 斜体 |
模型很爱在汇报里用表格对比数据、用标题分段、用粗体点重点。原样渲染的话竖线、---、井号、星号会直接糊在正文里,比不渲染还难读。
每条判据都朝「宁可不认」的方向压,因为认错的代价不对称——不认最多多出几个字面量符号,认错会把正文吞掉或劈碎:
| 判据 | 防的是什么 |
|---|---|
| 表格必须从段首开始 | 正文中途冒出两行带竖线的句子(比如讲管道运算符),不该被拆成格子 |
表格第二行必须是分割行(|---|:--:|) | 只靠「连续多行都有竖线」判表格,会把普通句子硬拆 |
| 表头与分割行列数一致 | 模型手滑多写一列时,宁可当纯文本显示,也不能丢字 |
| 标题、列表的标记后必须跟空格 | -3 度、#话题 是正文,不是列表和标题 |
| 斜体开头不能是空白 | 1 * 2 和 3 * 4 是乘法,不能把中间那段配成斜体 |
| 下划线不认 | 正文里到处是 snake_case,foo_bar_baz 会被劈成 foo + 斜体 bar + baz |
反引号里的竖线不算分隔符(`a|b` 是一个单元格,不是两个),否则讲管道运算符的表格必错。列数不齐时补齐/截断,避免 React 渲染出缺格的行。
围栏代码块整块原样显示:```js 到 ``` 之间的内容逐字照排,内部一个标记都不解析——代码被星号、反引号拆碎比不渲染糟得多。围栏本身不显示,~~~ 同样认。
围栏没闭合时整段退回普通文字,不吞后续内容。一个 ``` 打错就把后面几百字全收进代码块的话,用户看到的是「汇报后半段不见了」。
这条是改了方向的结果。早先的打算是「代码块不解析」,理由听起来也成立:汇报里常有「我刚才跑了
npm test」这类句子,整段缩进成灰色块反而看不清。真跑起来发现,``` 三个反引号白纸黑字印在正文里,而块里的星号、反引号照样被当行内标记拆碎,代码片段七零八落,比两种极端都难看。
浮窗什么时候走
出现容易,消失的语义反而更难定。三条规则:
- 模型重新开口,浮窗立刻撤。 浮窗是「模型长期不出声」时的信息补位;它一开口,你就有新东西可看。判据来自服务端的
assistant-stream事件流(text-delta上的时间戳),不靠 DOM——虚拟滚动、代码高亮、折叠面板都会让 DOM 乱动。 - 汇报原文本身不算「新正文」。 写浮窗记录时会把「当时的最后一次正文时刻」冻结进记录(
textAt字段),前端比的是「记录之后又说过话没有」。所以不会出现「模型刚写完汇报,浮窗一闪就消失」。 - 叉掉就是叉掉。 关闭状态按「哪一条汇报」记(记录的
at):切走再切回同一条汇报不再弹出,等下一条新汇报才重新出现,不会变成永久失聪。子代理的输出也进不了这个判据——等子任务那几分钟恰恰最需要这张卡。
第 1 条的判据在服务端,所以升级到 v0.12.5 后必须重启 DSH 进程(不只是刷新页面)才会生效。前端会回落到 at 兜底,功能不坏,但对「模型中途说了句话」不如新版敏感。
运行期自检
设置卡填不出效果时,先看统计:
GET /api/narrator/status
{
"ok": true,
"version": "0.12.5",
"config": { "silentSeconds": 60, "cooldownSeconds": 60, "lingerSeconds": 90 },
"stats": {
"startFrames": 41, "chunkFrames": 22793, "endFrames": 41,
"stepsSeen": 41, "preSteps": 40, "injected": 3, "captured": 2,
"skipped": {
"disabled": 0, "subagent": 0, "cooldown": 12,
"maxPerTurn": 0, "notSilent": 24, "notDue": 1,
"badShape": 0, "overlayOff": 0
},
"lastError": null
}
}
| 现象 | 看哪个字段 | 含义 |
|---|---|---|
| 一次都没触发 | stepsSeen / startFrames 是否在涨 | 为 0 说明事件没派发到插件 |
涨了但 injected: 0 | skipped.notSilent | 步骤都太短,没到阈值。把阈值调小或开测试模式 |
skipped.notDue | 到点了但还没等到 step 边界(正常状态) | |
skipped.cooldown | 被冷却挡住 | |
injected 涨了但浮窗空 | captured | 为 0 说明模型被提醒后没输出正文(或直接调了工具) |
| 全都没动 | stats === undefined | 宿主跑的是旧代码,重启 |
也可以直接跑 node test/check-status.mjs <端口>,它会打印人话诊断。
测试的五条铁律
- 必须隔离配置。 所有测试都要在
import插件之前设process.env.DSH_NARRATOR_CONFIG,否则用户真实配置会污染断言(曾因此 12 项假失败)。 node --test test/会失败,必须显式列出文件。- 断言不许用
?.包住核心判断。 曾写ctx._latestBySession?.set?.()这种不存在的对象,断言静默跳过,测试绿着但什么都没验。 - 浮层测试必须清定时器。 组件里有
setInterval,不清理 node 永不退出。 - CSS 修复要能拦住「改回去」。 修完 bug 顺手把变异试一遍——删掉关键声明、把数值调小、换成被否决的写法,测试都必须失败。设置卡底栏那条缝就是这么验的(见
test/client-render.test.mjs里.narr-foot的 box-shadow 断言)。
和其它插件的边界
| 插件 | 关系 |
|---|---|
dsh-step-pause | 触发机制参考了它(agent/pre-step 注入)。差别:它按步数触发,本插件按静默时长触发,且把模型说的那段抽出来显示在浮窗 |
dsh-call-guard | 互补。它管「工具调用被烧成文字导致回合假完成」,本插件管「长时间不说话」 |
dsh-thinking-guard | 反向。它在纯思考超时后 agent.cancel() 中止回合,本插件是让任务继续 |
dsh-gap-feed | 同床异梦。它也往对话流插卡片,但内容是外部新闻 |
License
MIT
Comments
Loading…
Similar plugins
by a179-sanae
DeepSeek Harness Web 客户端插件:把会话里的工具卡片与 Think 推理块折叠成一行,折叠态实时显示当前正在进行的工作(工具名 + 正在执行的命令/参数、或思考内容),运行中带流光动画;点击展开/收起。让界面只保留模型说的话。
★ 64
MIT
JavaScript
Sep 14, 2026
dsh plugin --profile web add dsh-auto-collapseby tianyaojiudi-prog
DSH(DeepSeek Harness) 插件:把一段可在设置页随时改写的文字,作为全局系统提示词段注入到所有会话,含子代理与工作流内部子代理;文本与开关即时生效,无需重启。
★ 9
MIT
JavaScript
Oct 4, 2026
dsh plugin --profile web add dsh-djy-xttscby xiaoso456
DeepSeek Harness 的软工作流插件:在画布上设计流程,编译成计划提示词,由模型按计划执行;运行状态由模型自己记,没有执行引擎
★ 11
↓ 357/wk
NOASSERTION
TypeScript
Oct 20, 2026
dsh plugin --profile web add @xiaoso/dsh-workflow-liteby Fishsb
DeepSeek Harness (DSH) 插件:✨ 提示词一键增强 + 💬 语音识别(说完自动停·云端/本地双引擎)· 附 🔁 服务异常一键重启
★ 104
Unknown
JavaScript
Oct 20, 2026
dsh plugin --profile web add dsh-prompt-enhancerby zzy2210
DeepSeek Harness 常驻插件:在会话标题栏显示主代理与每个子代理实际使用的模型和思考档位 | Persistent DSH web plugin: shows the model & reasoning effort actually used by every subagent, right in the conversation header
★ 0
JavaScript
Oct 8, 2026
dsh plugin --profile web add y1n-agent-rosterby Autodog875
常驻任务进度条 — DeepSeek Harness 插件:显示当前对话与所有对话的实时任务进度
★ 0
MIT
JavaScript
Oct 21, 2026
dsh plugin --profile web add dsh-agent-progress