dsh-turn-chime
DSH Web plugin: play a chime when the agent finishes a turn, with a settings page to enable it, pick the volume and upload your own audio file. Host-stored configuration, no core changes.
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:AGImentu/dsh-turn-chime说明文档
阅读完整 README ↗dsh-turn-chime
任务做完了,喊你一声。
一个 DSH(DeepSeek Harness)Web 插件:每次智能体把一个任务做完,浏览器里播一段提示音—— 你可以随时关掉它、调音量,也可以上传自己的 MP3 当提示音。默认自带一段两音铃声。
不读凭据、不访问外网、不改 DSH 内核,只用官方公开的两个插槽。
✨ 它怎么知道"任务做完了"
这一点决定了插件非常小:DSH 的 conversation.chat.assistant-actions 插槽有个天然特性——
它只在回合收尾之后才渲染(回合进行中,官方根本不渲染那一行)。
所以本插件注册一个渲染 null 的隐形条目,「我被挂载」就等于「任务做完了」。
为了不吵到你,又加了三道保险:
| 保险 | 作用 |
|---|---|
| 新鲜度窗口(60 秒) | 只有"刚结束"的回合才响。你翻历史、切会话时挂载的旧回合不会响 |
| 页面稳定窗口(3 秒) | 刚打开页面时渲染出来的最后一个回合不算"完成",不响 |
| 每回合只响一次 | 同一回合无论重渲染多少次,最多响一次(带 200 条上限的记忆) |
📦 安装
前置:DSH 已能正常运行(dsh web 起得来);Node.js ≥ 20,pnpm ≥ 10。
支持的 DSH 版本:在 DSH 0.1.5-rc.2 上真机验证(只用公开插槽与平台种子模块,不 import 内部包)。
方式一:本机 tarball 安装(推荐,现在就能用)
git clone https://github.com/AGImentu/dsh-turn-chime && cd dsh-turn-chime
pnpm install && pnpm build && pnpm pack # 产出 dsh-turn-chime-.tgz
dsh plugin --profile web add ./dsh-turn-chime-0.1.0.tgz # 文件名按上一步输出替换
方式二:源码 link: 安装(改代码即时生效)
git clone https://github.com/AGImentu/dsh-turn-chime && cd dsh-turn-chime
pnpm install && pnpm build
dsh plugin --profile web add "link:$PWD" # Windows PowerShell: "link:$($PWD.Path)"
或用仓库里的一键脚本(等价的两步,并在 CLI 不在 PATH 时兜底):
node scripts/install-local.mjs --profile web
方式三:从 npm 安装(包发布后可用)
dsh plugin --profile web add dsh-turn-chime@latest
方式四:交给 DSH 自己装
把下面这段原样发给任意一个 DSH 会话:
帮我装 dsh-turn-chime 插件(DSH 任务完成提示音),步骤:
1. git clone https://github.com/AGImentu/dsh-turn-chime 到 ~/Code/dsh-turn-chime
2. 在该目录执行 pnpm install && pnpm build && pnpm pack(记下产出的 .tgz 文件名)
3. 执行 dsh plugin --profile web add ./
4. 完成后提醒我:重启 dsh web,然后硬刷新浏览器(Ctrl/Cmd+Shift+R)
遇到报错先查 https://github.com/AGImentu/dsh-turn-chime 的 README「常见问题」表。
装完必须做的一步
重启 dsh web,然后硬刷新浏览器(Ctrl/Cmd+Shift+R)。
规则:lib/client.js(浏览器半边)改动,硬刷新即可;lib/index.js(宿主半边)改动必须重启。
本插件的设置与音频存在宿主半边,所以第一次安装请重启一次。
更新
cd ~/Code/dsh-turn-chime && git pull && pnpm install && pnpm build && pnpm pack
dsh plugin --profile web add ./ # tarball 方式
# 或 link: 方式:重新 pnpm run build 即可,profile 里已经是符号链接
卸载
dsh plugin --profile web remove dsh-turn-chime
remove 会按已安装状态对账 dsh.profile.bundles,自动摘掉本包;之后重启 dsh web。
已上传的音频与设置仍留在 ~/.dsh/turn-chime/,想彻底清掉就删掉这个目录。
常见问题
| 现象 | 原因与解决 |
|---|---|
| 设置里点「试听」提示被浏览器拦截 | 浏览器要求页面先有一次点击或按键才允许出声。在页面上随便点一下再试听即可(插件会自动解锁,之后任务完成时就能正常响) |
| 任务做完了但没响 | ① 插件开关是关的;② 你切到了别的会话(提示音只对当前打开的会话生效);③ 页面刚打开不到 3 秒;④ 浏览器还没被解锁(见上一条) |
| 上传音频报"只支持 mp3 / wav / ogg..." | 该文件既没有受支持的扩展名,浏览器也没报出音频 MIME 类型。换成 mp3/wav/ogg/m4a 再试 |
| 上传报"内容超过上限" | 上限 8 MB。长音频请先剪辑或用码率更低的格式 |
| 设置页显示「读取设置失败」 | 宿主半边没加载:必须重启 dsh web(只刷新页面不够) |
| 页面出现两个「提示音」页 | 双挂载:profile 的 cordis.patch.yml 里还留着手写挂载行,删掉那段 |
| 换浏览器后设置没了 | 设置与音频存在 ~/.dsh/turn-chime/(本机),所以换浏览器应该还在;若不在,检查 DSH_HOME 是否变了 |
提示 dsh: command not found | 用 npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-turn-chime@latest,或走方式一/二 |
🔊 设置页(设置 → 提示音)
| 控件 | 说明 |
|---|---|
| 开启提示音 | 关掉后不再播放任何声音(设置保留) |
| 音量 | 0–100%,试听与正式播放共用 |
| 试听 | 立刻按当前音量播一次,用来确认效果 |
| 上传音频 | 选一个本地音频文件,上传即启用;上限 8 MB |
| 恢复内置 | 回到自带的两音铃声 |
补充说明(页面底部也写着):
- 提示音只对当前打开的会话生效;后台任务、其它会话的任务不会响(要跨会话提醒需要监听全局会话列表,属于后续可做项)。
- 音频与设置在
~/.dsh/turn-chime/(config.json+sound.),所以换浏览器、清缓存都还在,且只在本机。 - 任务失败/中断也能响,但目前不区分成功与失败的声音(可作为后续项)。
🎵 内置提示音
assets/default-chime.wav 是用脚本合成的(仓库里不带任何第三方音频):
pnpm run make-chime # → assets/default-chime.wav
一段 0.9 秒的两音铃声(E6 → A6,上行四度),每个音由基频 + 两个非谐波分音叠加、
指数衰减包络,归一化后留 15% 余量——像铃铛而不像"滴"声。合成参数见
scripts/make-default-chime.mjs,想换音色改那里的
NOTES 即可重新生成。
🛠️ 开发
pnpm install
pnpm run typecheck # tsc --noEmit
pnpm test # 26 项单测:触发时机(三道保险/去重/上限)、设置校验、磁盘状态
pnpm run build # lib/index.js(host 半边:路由 + 存储) + lib/client.js(浏览器半边)
pnpm run smoke # 产物契约冒烟:注册 id / 插件形状 / 两个插槽 / 抛错隔离 / 渲染为空 / 取设置
pnpm run verify # typecheck + test + build + smoke
pnpm run watch # 开发时增量重建(配合 dsh 的 client HMR;host 改动仍需重启)
目录结构
src/
shared.ts 两半边共享的 wire 类型与上限(设置形状 / 支持格式 / 8 MB)
routes.ts 路由常量(避免两半边字符串漂移)
index.ts host 半边:注册 /turn-chime/* 路由,并作为一行 live Loader row
host/
config.ts 设置校验(纯函数):补默认值、夹取音量、清洗文件名
store.ts 磁盘状态:$DSH_HOME/turn-chime/config.json + sound.(原子写)
routes.ts 四个端点:读/写设置、上传/下发/删除音频(边读边限流)
contract.ts host 侧契约镜像(webServer 的 req/res)
client/
index.tsx 浏览器半边:注入样式、注册字典、解锁音频、注册两个插槽条目
Trigger.tsx 隐形触发器(渲染 null):挂载即"任务完成"
decide.ts 触发时机(纯函数 + 去重日志):三道保险都在这里
play.ts 播放与"首次手势解锁"(浏览器自动播放策略)
config-store.ts 设置缓存(30 秒 TTL、并发合并)+ 上传/恢复
SettingsSection.tsx 设置页:开关 / 音量 / 试听 / 上传 / 恢复
select.ts 从聊天快照取"本回合结束时刻"的选择器(必须返回存储引用)
locales.ts 中英字典 + 内置中文兜底
styles.ts 插件自有样式(仅消费 DSH 设计令牌)
Boundary.tsx 错误边界:渲染失败只影响自己那一块
contract.ts 本插件读取的 DSH 浏览器契约的镜像类型
assets/default-chime.wav 内置提示音(脚本合成)
cordis.patch.yml bundle patch:把本包挂成 profile 的一层
tsdown.config.ts 产出 host 半边(ESM)与浏览器半边(CJS 闭包工厂)
scripts/
make-default-chime.mjs 合成内置提示音
install-local.mjs 本地 link 安装 + bundles 对账
smoke-client-bundle.mjs 产物契约冒烟(无浏览器)
🧩 为什么这么设计
| 取舍 | 理由 |
|---|---|
| 用插槽挂载判断"任务完成",不监听事件 | 官方插槽的渲染时机本身就是"回合已收尾"(回合进行中 closing === null),这是公开且稳定的信号;去猜事件流反而脆弱 |
| 三道保险(新鲜度 / 页面稳定 / 去重) | 每一条都对应一种"会很烦"的场景:翻历史响、开页面响、一次任务响好几遍 |
| 音频与设置放宿主而不是浏览器 | "上传一次,换浏览器也还在"是用户对声音偏好的预期;浏览器侧还要处理 IndexedDB 与容量限制 |
| 上传边读边限流而不是先收完再判断 | 8 MB 上限是保护进程的,必须在流里生效;一个同源页面不该能用超大请求把宿主打爆 |
| 只消费 DSH 设计令牌画界面 | 设置列是官方界面的一部分,插件是客人:跟随主题与字号,不引入自己的色板与字体 |
| 条目包在错误边界里 | 触发器就渲染在官方复制/分支/用量那一行里;插件抛错必须只让自己消失 |
| 上传格式白名单,并拒绝未知 | 服务端要给出正确的 content-type,白名单是唯一能保证这件事的方式;不做魔数嗅探,是因为代价大于收益(播放失败会明确回报) |
⚠️ 已知边界
- 只对当前打开的会话生效:后台/其它会话的任务完成不会响。
- 需要一次页面交互解锁音频(浏览器策略),之后自动生效。
- 只在浏览器页面里响:DSH 窗口最小化时仍会响;若浏览器把标签页静音(整站静音),则不响。
- 不区分成功与失败:任务失败/中断也会响(后续可加第二种声音)。
- 上传上限 8 MB,格式白名单见上;超大音频请先压缩。