# dsh-work-done-bell [English](README.md) | **简体中文** 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件: 当一次会话**真正把所有活干完**时,响一声 **Windows 系统提示音**,并在**右下角 弹出 toast** 写明是哪次会话完成了。右下角还有一个常驻开关可以同时关掉两者,这个 偏好写进真实的 settings 命名空间,所以重启后依然有效。 ## 为什么 "idle" 不等于 "干完了" `agent/status -> idle` 只说明 agent 的驱动手上没有排到事情。活经常比它活得久: - `run_in_background` 启动的后台作业还在跑,等它落地会**再开一轮**; - 被委派出去的子代理还在跑,而它的父代理已经 idle 了。 所以对着裸的 `idle` 直接响铃,一个任务会响两三次。本插件等的是**彻底静默**: `idle` *且* 该会话名下没有处于 `running`/`stopping` 的作业 *且* 名下没有在跑的 agent。这个等待有 90 秒的截止时间兜底,长构建吞不掉这声提示;一旦在截止时间响过, 同一轮里随后那次真正的静默就不再出声。 ## 各种结束原因对应什么声音 原因取自持久化的会话日志(`turn/end`),不是猜出来的: | `turn/end` 原因 | 声音 | Toast | | --- | --- | --- | | `completed` | `Asterisk`(默认通知音) | 有 | | `blocked`(某个插件驳回了这一步) | `Asterisk` | 有 | | `max-tokens`(撞到输出上限) | `Asterisk` | 有 | | `error` | `Hand`(严重错误音) | 有 | | `aborted` —— 包括你自己按的 Esc/停止 | 静音 | 无 | | `interrupted`(崩溃后遗留的孤儿轮次) | 静音 | 无 | 2.5 秒内同时完成的会话会合并成一声,所以五个会话一起结束不会叠出五个播放器。 ## 安装 先克隆仓库,再从克隆下来的目录安装: ```sh git clone https://github.com/FunctionHookTJU/dsh-work-done-bell dsh plugin --profile web add file:/absolute/path/to/dsh-work-done-bell ``` 然后把包名加进 profile 的 bundle 列表,也就是 `/profiles/web/package.json`: ```json { "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "...", "dsh-work-done-bell"] } } } ``` (安装的包只要声明了 `dsh.bundle`,`dsh plugin add` 就会自己把这份列表对上。之后 重启 DSH,组合里其它地方都不用动。) > **用 `file:`,别用 `link:`。** 插件是靠从自己所在目录向上走进 profile 的 > `node_modules` 才解析到 `@deepseek-ai/schemastery` 的。`link:` 安装会把真实 > 路径留在 profile 之外,于是 import 在加载时直接失败。 ## 卸载 ```sh dsh plugin --profile web remove dsh-work-done-bell ``` 把它从 `dsh.profile.bundles` 里去掉(`dsh plugin remove` 也会顺带对上这份列表), 然后重启。插件没了之后 `settings.yaml` 里的 `work-done-bell` 命名空间就只是躺着不 起作用,可以手动删掉。 ## 右下角那个开关 右下角常驻可见:*完成提示已开 / 完成提示已关*。它通过 settings 服务写 `work-done-bell.enabled`,两半都读它 —— 宿主那一半停掉声音,浏览器那一半停掉 toast。状态存在 `/settings.yaml`,所以重启后依然有效。 ## 自省(Introspection) 故意没有面向模型的工具:这个 profile 里没有 `@deepseek-ai/dsh-tools`,而插件只能 import profile 本身已经带的包(ESM 解析不看 `NODE_PATH`)。同样的信息改由 state 路由承载: ```sh curl "http://127.0.0.1:3080/dsh-work-done-bell/state?verbose=1" ``` 它返回被跟踪的状态、每个会话最后一次 `turn/end` 的原因、还开着的静默观察周期及其 截止时间、上一次响铃的退出码,以及插件吞掉的任何错误。不带 `?verbose=1` 时只返回 浏览器那一半需要的东西。 ## 环境要求与已知限制 - **声音仅限 Windows**:它通过组合出来的 `shell` 服务执行 `[System.Media.SystemSounds]` 来播放。在其他平台上插件能加载、能弹 toast,但不 出声。移植的办法就是把 `play()` 里的命令换成 `afplay` / `paplay`。 - **响铃命令带着一份解析过的 sandbox policy。** 有约束的 shell 服务会拒绝一份没带 policy 的命令,而部署默认的那个模式可能宿主根本执行不了 —— 在构建这个插件的机器 上,`workspace-write` 就是跑不起来的:Windows ACL 运行器要求临时根目录位于工作区 之外,而工作区是用户主目录,于是受约束的命令**一条都跑不了**,不带 policy 的命令 则被直接拒绝。因此 policy 是每次响铃时针对被通报的那个会话解析的 (`sandboxPolicy.resolve({ session })`),用的是那个会话自己的权限,绝不放大它。 在这样的宿主上,被约束在 `workspace-write` 的会话会保持静音;`?verbose=1` 会在 `lastError` 里给出原因,在 `lastPolicy` 里给出解析出的模式。 - Toast 需要页面开着;声音不需要。 - 浏览器那一半每秒通过 `/dsh-work-done-bell/state` 轮询宿主。没有推送通道:包之间 被认可的方向是 浏览器 -> 宿主。 - Toast 用短 id 标注会话,不是标题。 ## 开发 四套测试,都不需要 DSH 在跑: ```sh npm test # 四套全跑 node test/state-machine.test.mjs # 决策表,跑在手动时钟上 node test/integration.test.mjs # 插件装在一个真实的 cordis 应用里 node test/client-bundle.test.mjs # 浏览器 bundle 的契约 node test/docs-parity.test.mjs # 两份 README 共用的骨架 ``` `integration.test.mjs` 是最要紧的一套:它用一个真实的 `@deepseek-ai/cordis` 应用加桩 服务启动,断言 `apply` 跑起来了、`ctx.inject` 触发了、两个 HTTP 路由都注册了。这个 插件的早期版本在 apply 时读一次 `ctx.get('webServer')`,又不加保护地调用 `jobs.onJobsChanged`;它看起来一切正常,生产里却什么都没注册 —— 因为宿主组合的一行 是在*启动过程中*挂载的,那时候它的服务还不存在。 测试需要能从仓库解析到 `@deepseek-ai/cordis`、`@deepseek-ai/cordis-plugin-timer`、 `@deepseek-ai/schemastery` 和 `@deepseek-ai/cosmokit`。检出的仓库把它们从 harness 安装里链接过来;`node_modules` 已 gitignore,所以这些链接只留在本地: ```powershell $repo = (Get-Location).Path # 当前检出的仓库 $dsh = "$env:APPDATA\npm\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai" $web = "$env:USERPROFILE\.dsh\profiles\web\node_modules\@deepseek-ai" New-Item -ItemType Directory -Force -Path "$repo\node_modules\@deepseek-ai" | Out-Null foreach ($p in 'cordis','cordis-plugin-timer') { New-Item -ItemType Junction -Path "$repo\node_modules\@deepseek-ai\$p" -Target "$dsh\$p" } foreach ($p in 'schemastery','cosmokit') { New-Item -ItemType Junction -Path "$repo\node_modules\@deepseek-ai\$p" -Target "$web\$p" } ``` `docs-parity.test.mjs` 守住两份 README 不漂移:标题骨架、代码块、表格、行内技术 名词都必须一致。它判断不了行文,但只要一种语言多出一节而另一种没有,它立刻失败。 ## 目录结构 ``` lib/index.js 宿主一半 —— 静默状态机、声音、HTTP 路由 client/client.js 浏览器一半 —— shell.overlay 的 toast 栈与开关 cordis.patch.yml 这个包贡献的那一行组合 test/ 决策表、真实 cordis 集成、客户端与 README 契约 ``` ## 许可证 MIT