# dsh-task-chime [English](README.md) | 中文 一个用于 **DeepSeek Harness (DSH)** 的插件:**每次对话任务完成时播放真正的操作系统提示音**,让你在长任务期间可以放心离开屏幕,任务一结束立刻知道。 它不是浏览器里的提示音——声音由 Host 进程经系统音频设备发出,所以 DSH 窗口最小化、切到后台、甚至在另一个虚拟桌面时,你照样听得见。 ``` 一轮对话结束 ──► agent/status: idle ──► ctx.shell ──► 🔔 C:\Windows\Media\notify.wav ``` ## 功能 | | | |---|---| | **真·系统提示音** | 由 Host 经 `ctx.shell` 播放(Windows 上是 PowerShell 的 `System.Media.SoundPlayer` / `[console]::Beep`),而非网页播放,后台也能听见 | | **两个时刻分开提醒** | *任务完成* 与 **「Agent 卡在等你」**(提问或等待授权)各有独立音效与强度,一听就能分辨 | | **13 种音效** | 系统音效(`notify`、`ding`、`chimes`、`tada`、`chord`、`calendar`、`messaging`、`exclamation`、`alarm`、`ring`)、两种不依赖音频文件的纯蜂鸣,以及**任意自定义音频路径** | | **4 级提醒强度** | L1 轻提示(响 1 次)· L2 标准(响 2 次)· L3 强提醒(升调前奏 + 响 3 次)· L4 闹钟级(双向前奏 + 响 5 次) | | **长任务自动升级** | 任务超过 2 分钟自动 +1 级,超过 10 分钟再 +1 级(上限 L4) | | **触发范围** | 默认仅主会话,可选包含子代理会话 | | **时长门槛** | 低于 N 秒的快速回答不响铃 | | **不会误响** | 完成提醒必须观察到 `running → idle` 完整跃迁;授权提醒要过了宽限期仍未落定才响 —— 所以打开会话、或被策略自动应答的授权,都是安静的 | | **`task_chime` 工具** | 可选的模型工具,你直接说"响一下",Agent 就能按需触发 | ### 为什么第二个时刻需要单独的触发点 Agent 在等你的时候 —— `ask_user_question`、`exit_plan_mode`、授权弹窗 —— 它的状态**仍然是 `running`**。`agent/status` 永远到不了 `idle`,所以完成提醒**根本不会响**。没有这个单独触发点,最需要你出现的那一刻恰恰是最安静的一刻。 ## 两种运行方式 仓库同时提供**同一功能的两个半边**,按你希望它活多久来选。 | | **A · Profile Bundle** | **B · 动态插件** | |---|---|---| | 入口 | `lib/index.js` + `cordis.patch.yml` | `src/host.js` + `src/client.js` | | 安装 | `dsh plugin add` 后重启 | `cordis_define` + `cordis_run`,无需安装 | | 重启后仍生效 | **是** | 否 | | 对所有会话/项目生效 | **是** | 仅当前会话 | | 配置 | **设置 → 插件 里的卡片**、`settings.yaml`(命名空间 `task-chime`)或 composition 条目 | 内存态,重启即失 | | 界面 | **设置 → 插件** 里的设置卡片 | 设置页 + `cordis_run` 卡片内的面板,另有试听、临时静音、提醒日志 | | 需要授权 | 否 | 是(含 Client 半必须授权) | 要长期用就装 A。B 仍然有用:改一行立刻生效,而且面板更丰富。 ## A · 作为 Profile Bundle 安装(永久生效) ```sh dsh plugin --profile web add github:ruazero/dsh-task-chime ``` 然后**重启 Harness**(退出并重开 DSH Desktop,或重启你的 `dsh web` 进程)。就这样:包内的 `dsh.bundle.patch` 声明会让它的 composition 行自动生效,**你不需要手工编辑任何 composition 文件**。 重启前可以先离线校验(不启动任何东西): ```sh dsh --profile web --dump-config | grep -A2 task-chime ``` 卸载: ```sh dsh plugin --profile web remove dsh-task-chime ``` ### 配置 三层,按优先级从高到低: 1. **设置卡片** —— *设置 → 插件 → 任务完成提示音*。每个控件即改即写;被改过的字段会显示「已自定义」徽标,其**恢复默认**是删除覆盖值,而不是把默认值写进去。 2. **`settings.yaml` 的 `task-chime` 命名空间** —— 卡片写的就是这份持久化文档,且被实时监听:手工改完下一轮就生效,无需重启。 3. **`cordis.patch.yml` 里的 composition 条目** —— 基础层,也是没有 settings 服务时的兜底。 ```yaml # settings.yaml task-chime: enabled: true # 总开关,管住所有提醒 sound: notify # notify | ding | chimes | tada | chord | calendar | # messaging | exclamation | alarm | ring | # beep-triad | beep-low | custom customPath: '' # 仅当 sound 为 custom 时使用的音频绝对路径 level: 2 # 1 轻提示 · 2 标准 · 3 强提醒 · 4 闹钟级 autoEscalate: true # 超 2 分钟 +1 级,超 10 分钟 +2 级 minDurationSec: 0 # 低于该时长不响 scope: roots # roots = 仅主会话 · all = 含子代理 registerTool: true # 是否暴露按需触发的 task_chime 工具 # Agent 卡在等你时提醒 notifyOnInput: true # 提问与待授权 inputSound: messaging # 同一音效表;建议与 sound 不同 inputLevel: 3 # 独立强度 inputTools: # 这些工具一被调用就会等你回答 - ask_user_question - exit_plan_mode approvalDelayMs: 1200 # 宽限期;被策略自动应答的授权不会响 ``` 若想固定写进 composition,在你 profile 自己的 `cordis.patch.yml` 里按 id 覆盖: ```yaml - id: task-chime config: level: 3 sound: chimes ``` ### 强度对照 | 等级 | 前奏 | 播放次数 | 间隔 | |---|---|---|---| | L1 轻提示 | — | 1 | — | | L2 标准 | — | 2 | 0.22s | | L3 强提醒 | 升调三音(C6–E6–G6) | 3 | 0.4s | | L4 闹钟级 | 升调 + 降调三音 | 5 | 0.65s | ## B · 作为动态插件运行(仅当前会话) ```sh git clone https://github.com/ruazero/dsh-task-chime.git ``` 然后在具备 Cordis 工具的 DSH 会话里(自带的 `cordis` preset 即可)粘贴: > 读取 `<克隆目录>` 下的 `src/host.js` 和 `src/client.js`,然后调用 `cordis_define`:`plugin.kind: "new"`、`idPrefix: "chime"`、`code.host` 为 `src/host.js` 全文、`code.client` 为 `src/client.js` 全文。随后用 `cordis_run` 的 `run` 模式激活。 出现卡片时点允许。这种方式额外提供一个**自定义控制面板**——音效选择与试听、四个强度按钮、触发范围、时长门槛、临时静音(10/30/60 分钟)、最近 12 次提醒日志——位于**设置 → 任务提示音**以及 `cordis_run` 卡片内。 两个 `src/*.js` 都是动态求值器所需的**函数体**纯 JavaScript —— 以 `return { apply(ctx) { … } }` 结尾。不要包成模块,也不要加 `import`/`require`:那个沙箱里没有这些东西。细节与排错见 [INSTALL.md](INSTALL.md)。 ## 工作原理 1. Host 订阅 `agent/status` 事件。该事件在 `running` ⇄ `idle` 间切换;`idle` 表示已无 driver 在排队或运行 —— 这一轮真的结束了,而不是步骤之间的间隙。 2. `running` 时记录起始时间戳。`idle` 时算出任务用时,并依次判断:总开关、触发范围、时长门槛、长任务升级。**必须先观察到起始事件才会响**,因此恢复会话不会自己响。 3. 拼出一小段 shell 脚本(`SoundPlayer.Load()` + 多次 `PlaySync()`,或 `[console]::Beep` 音型),经 `ctx.shell.resolve()` + `ctx.shell.run()` 以 fire-and-forget 方式执行:Agent 绝不等待声音播完。 4. 沙箱策略取自完成的那个会话。由于该命令不写任何文件,仅当会话为 `read-only` 时提升为 `workspace-write`,唯一目的是让 PowerShell 保持 FullLanguage;绝不申请更宽的权限。 5. 所有能力都是探测而非假定:缺 `shell`、缺 `agents`、缺 `settings`、缺 `tools`,都只降级一个特性,而不会让插件行加载失败。 ### 卡在等你 6. **提问**在 `tools/pre-execute` 瀑布上捕获:待执行调用的名字命中 `inputTools` 就播放操作提醒音,然后原样把决策交给下游 —— 插件绝不干预某个调用是否被允许。 7. **待授权**在 `approval/request` 瀑布上捕获。立刻响会把被策略自动应答的授权也一起响掉,所以插件先武装一个 `approvalDelayMs` 定时器,并在 `finally` 里清除:只有过了宽限期仍然挂着(即真的在等你)的请求才会发声。已武装的定时器在 fiber 拆卸时统一清除,不会有残留。 ### 设置卡片是怎么出现的 内置的「插件」设置分区会枚举 Host 供给的 settings 命名空间,并以**每个命名空间为 key** 派发 `settings.plugin.item`,渲染认领该 key 的那张卡片 —— 一个被供给但没有卡片认领的命名空间,什么都不渲染。Host 半注册了 `task-chime` 命名空间,因此 `lib/client.js` 只要认领这个 key,卡片就会出现在内置的 shell、agent-loop 卡片旁边。 `lib/client.js` 是直接按客户端 wire 格式(`window.__ModuleLoader__.load({ id, factory })`)手写的浏览器模块,没有经过打包器:它除了 `react` 什么都不 require,引入构建链只会增加工具负担而不增加任何行为。写入走绑定的 settings scope(`ctx.settingsScope.bind({ namespace: 'task-chime' })` → `set` / `unset`),也就是 Host 半已经在监听的那份持久化文档 —— 因此两个半边之间不需要私有 RPC,也不存在第二份"真相"。 ## 平台支持 | | 状态 | |---|---| | **Windows 10/11** | **已验证。** PowerShell `System.Media.SoundPlayer` + `[console]::Beep`,音效取自 `C:\Windows\Media\` | | macOS | 尽力实现、未经验证:`afplay` + `/System/Library/Sounds/*.aiff`、`osascript -e beep` | | Linux | 尽力实现、未经验证:`paplay`(回退 `aplay`)+ `/usr/share/sounds/freedesktop/stereo/*.oga` | macOS / Linux 上的问题请当 bug 提 issue,而不是"已知限制"。 ## 开发 ```sh npm test # 25 项检查,全程静音,不会真的播放 ``` `test/smoke.mjs`(17 项)用 fake Cordis 上下文驱动 Host 的 `apply()`,逐项断言:各等级的命令形状、自定义路径的引号转义、"未观察到起始就不响"的守卫、触发范围过滤、时长门槛、沙箱策略处理、settings 的实时优先级,以及各条降级路径。 `test/client.mjs`(8 项)按模块加载器的方式加载浏览器模块,断言:插槽注册合同、每个控件是否只写自己那个字段、恢复默认是否走 `unset` 而不是写值、只读/加载中时是否锁死全部控件,以及在真实 React 下能否渲染。 两个半边的依赖都来自 host profile。本地跑测试时链接进 `./node_modules`(已被 git 忽略): ```powershell # Windows 示例;请指向你自己 profile 的 node_modules $profile = "$env:APPDATA\dsh-desktop\harness\profiles\node_modules" New-Item -ItemType Directory node_modules\@deepseek-ai -Force foreach ($m in 'schemastery','dsh-tools','dsh-settings') { New-Item -ItemType Junction "node_modules\@deepseek-ai\$m" -Target "$profile\@deepseek-ai\$m" } foreach ($m in 'react','react-dom','scheduler') { New-Item -ItemType Junction "node_modules\$m" -Target "$profile\$m" } ``` ## 限制 - **方式 B 是临时的。** 动态插件及其配置只存活于当前 DSH 进程的内存中;方式 A 才是永久形态。 - **卡片里没有试听。** 试听需要从浏览器调 Host,而组合插件只能通过发布 Remote 服务做到;想试听就让 Agent 调用 `task_chime`。 - **`registerTool` 需要重启。** 它在插件行激活时只读一次,所以在卡片里改它要下次启动 Harness 才生效;其余字段都是下一轮即生效。 - **`PlaySync` 会阻塞它自己的子进程**直到播放结束 —— 这是刻意的,重复播放的节奏正是这样计时的。它不会阻塞 Agent。 - **所有结果共用一个音效。** 成功、报错、等待你输入,目前响法相同。 ## 路线图 - 经过验证的 macOS / Linux 后端 - 按结果区分音效(成功 / 出错 / 等待输入) - 可选的 Windows 通知气泡(与声音并存) - 组合模式下的临时静音与试听(方式 B 两者都有) ## 许可 [MIT](LICENSE)