dsh-task-chime
English | 中文
一个用于 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 安装(永久生效)
dsh plugin --profile web add github:ruazero/dsh-task-chime然后重启 Harness(退出并重开 DSH Desktop,或重启你的 dsh web 进程)。就这样:包内的 dsh.bundle.patch 声明会让它的 composition 行自动生效,你不需要手工编辑任何 composition 文件。
重启前可以先离线校验(不启动任何东西):
dsh --profile web --dump-config | grep -A2 task-chime卸载:
dsh plugin --profile web remove dsh-task-chime配置
三层,按优先级从高到低:
1. 设置卡片 —— *设置 → 插件 → 任务完成提示音*。每个控件即改即写;被改过的字段会显示「已自定义」徽标,其恢复默认是删除覆盖值,而不是把默认值写进去。 2. settings.yaml 的 task-chime 命名空间 —— 卡片写的就是这份持久化文档,且被实时监听:手工改完下一轮就生效,无需重启。 3. cordis.patch.yml 里的 composition 条目 —— 基础层,也是没有 settings 服务时的兜底。
# 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 覆盖:
- 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 · 作为动态插件运行(仅当前会话)
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,而不是"已知限制"。
开发
npm test # 25 项检查,全程静音,不会真的播放test/smoke.mjs(17 项)用 fake Cordis 上下文驱动 Host 的 apply(),逐项断言:各等级的命令形状、自定义路径的引号转义、"未观察到起始就不响"的守卫、触发范围过滤、时长门槛、沙箱策略处理、settings 的实时优先级,以及各条降级路径。
test/client.mjs(8 项)按模块加载器的方式加载浏览器模块,断言:插槽注册合同、每个控件是否只写自己那个字段、恢复默认是否走 unset 而不是写值、只读/加载中时是否锁死全部控件,以及在真实 React 下能否渲染。
两个半边的依赖都来自 host profile。本地跑测试时链接进 ./node_modules(已被 git 忽略):
# 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)