dsh-completion-alert
Verifieddsh-completion-alert · v1.8.4 · MIT · Web UI
DSH 工作完成提示:一轮工作结束播放「冰冰冰」提示音,并在右下角弹出可点击跳转的通知条;声音、音量、提示范围与自定义音源都在设置页里
Install
dsh plugin add dsh-completion-alert Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-completion-alert
中文 | English
一个 dsh(DeepSeek Harness)客户端插件:一轮工作结束时告诉你。
- 播放提示音 —— 就是那段循环的「冰冰冰」梗音效,裁成一轮 1.06 秒的三连音,会话从"忙"变"闲"的那一刻响一次;
- 右下角弹出通知卡,写明哪个会话完成了、跑了多久;
- 点一下卡片就切到那个会话;
- 一切都能在设置里改:总开关、提示范围、是否出声、音量、试听,以及上传自定义提示音。
提示音内嵌在客户端 bundle 里,所以插件不需要宿主路由、不需要磁盘资源、也不联网。通知层和设置页全部使用 dsh 自己的主题变量与 slot 体系,和桌面端观感一致。
一轮工作结束(会话 running: true -> false)
│
├─ 按设置播放一次提示音
└─ 右下角通知卡:「压缩图标」已完成 · 用时 1 分 12 秒
└─ 点击 -> uiWorkspace.openSession(id)
安装
从 registry 装(插件管理器的做法)
dsh-completion-alert
把这个名字填进 设置 → 内置插件 → 安装。插件管理器会在 profile 里跑 pnpm add,记下依赖,并把这个包列进 dsh.profile.bundles。
从本仓库安装(本地检出,不走包管理器)
git clone https://github.com/yimengqingfeng3-debug/dsh-completion-alert.git
cd dsh-completion-alert
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 # desktop profile
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1 -Profile web
安装脚本是幂等的,改动前会备份:
- 把包复制到
<DSH_HOME>/profiles/<profile>/node_modules/dsh-completion-alert; - 向该 profile 的
cordis.patch.yml追加completion-alert行(如果 profile 已在本包的dsh.profile.bundles里列出,则由本包自带的 bundle patch 插入,脚本不会重复插入——重复的行 id 会直接导致启动失败); - 缺少
dsh.profile.patchReload时写入live,之后改插件不必重启应用。
装完刷新一次 dsh 窗口(Ctrl+R),让浏览器加载新的客户端 bundle。
手动安装
把 lib/、assets/、package.json、cordis.patch.yml 复制到 profile 的 node_modules/dsh-completion-alert,再往 profile 的 cordis.patch.yml 加:
- insert:
- id: completion-alert
name: 'dsh-completion-alert'
卸载
三条路都实测过,都不需要插件本身配合,也都不碰其他插件。
1. 插件面板自带的卸载按钮
设置 → 内置插件 → dsh-completion-alert → 卸载。 它会摘掉 bundle 登记与 patch 行(插件立刻不再加载),然后让 pnpm 删包。
最后那一步有个已知问题:插件管理器调用的是 app 自带的 pnpm(11.7.0),而这个版本在包发布不满 24 小时时会忽略 profile 里的 minimumReleaseAgeExclude 豁免名单,于是 pnpm remove 可能报 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION,界面显示"卸载失败" —— 尽管插件其实已经卸载了。PATH 上的 pnpm 12 认这份名单,同一条命令手跑就能过。
无论哪种结果,界面自身状态是对的;可能留下的是 node_modules 里的那份拷贝和 package.json / pnpm-lock.yaml 里的条目 —— 下面两条路专门清这个。
2. 彻底清理脚本(界面报错时推荐)
关掉 dsh,然后:
powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall-all.ps1 # desktop profile
powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall-all.ps1 -Profile web
powershell -NoProfile -ExecutionPolicy Bypass -File .\uninstall-all.ps1 -WhatIf # 只打印计划,不改任何文件
它删除并验证以下每一处痕迹:
| 位置 | 清掉什么 |
|---|---|
node_modules/dsh-completion-alert |
包目录 |
cordis.patch.yml |
所有 completion-alert insert 行(安装失败可能留下不止一份) |
package.json |
依赖条目(如果插件管理器加过) |
pnpm-lock.yaml |
importer 条目 + packages: / snapshots: 块 |
pnpm-workspace.yaml |
本包的 minimumReleaseAgeExclude 那一行 |
每个被改写的文件都会备份到 <profile>\.completion-alert-backup\;它会逐项打印故意没动的东西(余额插件的目录、依赖、豁免、lockfile 条目都报告为 kept),最后再跑一次"应为空"的扫描,列出任何没清掉的引用。务必在 dsh 关闭时运行:应用运行期间占着 profile,退出时还可能把同样的文件写回去。
3. 手动删
删掉 node_modules/dsh-completion-alert,从 cordis.patch.yml 移除它的 - insert: 行,从 package.json 里去掉 "dsh-completion-alert"(dsh.profile.bundles 与 dependencies 两处),再清掉 pnpm-lock.yaml 里的条目。留着 lockfile 条目不会致命(pnpm 会报"lockfile 不是最新",而不是做错事),上面那个脚本存在的意义就是让你不必手动做这些。
确认是否挂载
宿主半边提供一个诊断路由,浏览器半边会把自身的激活结果 POST 上去(只含挂载事实,没有会话内容、没有偏好数据):
GET http://127.0.0.1:<端口>/api/completion-alert.diag
-> { "report": { "facts": { "watcher": true, "overlay": true, "settings": true } } }
watcher: true 表示完成检测接上了,settings: true 表示偏好已经绑到宿主设置文档。回环端口带鉴权,所以请在应用自己的控制台 / DevTools Network 里看,而不是用 curl。
设置项
设置 → 工作完成提示(与「通用设置」「内置插件」并列的独立一页):
| 设置项 | 说明 |
|---|---|
| 启用工作完成提示 | 总开关。关掉后不出声也不弹通知 |
| 提示范围 | 全部会话:任何会话完成都提示;仅后台会话:当前正在看的会话完成后不打扰 |
| 后台时全部提示 | 只在选了「仅后台会话」时有意义:应用被切到后台或最小化后,屏幕上其实什么都没在看,于是任何会话完成都提示。这一行会显示当前状态;选「全部会话」时它是灰的 |
| 播放提示音 | 只关声音,通知照常弹 |
| 音量 | 0–100%,试听和正式提示音同时生效 |
| 重复次数 | 每次提示连播几遍,1~4 遍。重复播放的是你裁好的那一段;点「停止」会取消还没播的遍数 |
| 提示音 | ‹ 当前音效 › 左右箭头切换(切换即试听),点名字重播;右侧下拉箭头打开全部音效 |
| 全部音效 | 覆盖页列出所有内置音效,每行带独立试听键;下方是自定义音效入口 |
| 自定义音效 | 选择本地音频 → 在波形上拖选范围 → 试听这段 → 保存并使用 |
偏好存在插件自己的设置命名空间 completion-alert(写进 profile 的设置文档),所以重启保留、多窗口同步。宿主不提供设置服务时插件照常工作,选择只在当前页面生命周期内有效。
实现要点
1. 完成检测:订阅 uiSession.sessionStatus
不抓 DOM、不轮询。插件订阅客户端自己的会话状态投影 —— 侧边栏状态点、Stop 快捷键的守卫用的是同一份:
status.subscribe(() => {
// running: true -> false 就是一轮工作结束
});
这份投影的数据源是宿主的 api-session/status 事件(agent/status → status === "running"),所以主会话、后台会话、子代理会话都会上报。
两个刻意的规则:
- 首个快照只当基线。窗口打开时已经有会话在跑,那不是"完成",不会响。
- 只认 true → false 的边沿。等审批结束的一轮、被用户 Stop 的一轮,同样是"结束了",一样提示。
2. 提示音:内嵌 Ogg + Web Audio
bundle 里带一段 base64 的 Ogg(//#region embedded-tone 标记块),首次播放时 decodeAudioData 解一次并缓存;同一时刻只允许一个音源,第二次完成不会叠音。
Chromium 在页面收到用户手势前不允许启动 AudioContext,而这正是刚打开窗口的状态。插件不丢弃这一声,而是注册一次性手势解锁并在手势到来后补播,同时在角落显示"点击窗口任意位置即可开启提示音"的小胶囊。窗口被点过之后就不会再出现。
3. 通知与跳转
通知层注册在 shell.overlay(框架自带的浮层槽,position: absolute; inset: 0),卡片自己 fixed 钉在右下角,pointer-events 只开在卡片本身,不挡界面。
点卡片调用 uiWorkspace.openSession(sessionId) —— 会话浏览器、fork 会话用的是同一个入口。悬停会暂停自动消失计时,右上角 × 可以手动关掉。
4. 卡片配色
设置页整体跟随主题的 --dsw-alias-* 变量。右下角卡片额外读一次页面自身的背景色(这些变量在浮层里取不到值),深浅模式各解析一次,让卡片和当前皮肤融合,而不是一块贴上去的白块。
提示音
插件自带三个音效。其中两个是从零做加法合成的 —— 一声干净的钟式「叮」,也就是系统通知音的那种质感 —— 不是从任何产品里抓的采样,所以可以合法随包分发:
| 音效 | 素材 | 说明 |
|---|---|---|
冰冰冰 (bingbingbing) |
assets/bingbingbing.ogg |
梗音效,裁成一轮 1.06 秒的三连音,12 642 字节 |
清脆提示 (crisp-a) |
assets/crisp-a.ogg,合成 |
两音上行(F#6 → F#7),付款确认那种干脆感,0.50 秒,6 477 字节 |
清脆短音 (crisp-b) |
assets/crisp-b.ogg,合成 |
三音上行马林巴(D4 → A4 → D5),短信提示那种,0.58 秒,7 068 字节 |
怎么加一个音效
tools/tones.json 是声明音效的唯一地方:
- 把 Ogg 放进
assets/; - 在
tools/tones.json加一行 ——id、label、hint、source,以及kind(自己合成的写synth,第三方素材写recording,后者的来源必须写进 NOTICE); powershell -NoProfile -ExecutionPolicy Bypass -File tools\embed-tones.ps1;- 升版本号并刷新窗口。
lib/client.js 的音效列表由生成块里的 TONE_DEFINITIONS 构建,所以不用改代码;两个漂移检查
(tools/check-embedded-tone.mjs 与 PowerShell 的 -Check)读的都是这份注册表。
合成脚本是 tools/synthesize_tones.py:每个音是"一叠衰减分音 + 起音处一个极短的带限噪声爆发"(后者就是它让铃声"脆"起来的原因),按手放的起音位置叠进轨道,再混音。改写时有两个坑值得记住:
- 敲击音要在 dB 上衰减,不是线性幅度。 线性的
exp(-t / tau)在前一个tau内几乎不降,短音听起来就是"从无声渐强"而不是"敲一下"。这里的每个音都按10 ** (-3 * t / tau)下落,峰值就落在起音上 —— 开发过程中正是这个错误让最后一个音出现了明显的"渐强",而且渲染包络一看就露馅。 - 马林巴的高次分音比基频衰减更快,所以
crisp-b的敲击瞬间听感高五度,再落回基频。第 3、5 分音是整叠里最响的。
重新生成:
python tools/synthesize_tones.py assets # 生成 WAV 母版
python tools/synthesize_tones.py assets <ffmpeg> # 再生成插件内嵌用的 Ogg
重新内嵌进 bundle 里那段 //#region embedded-tones 标记块:
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\embed-tones.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\embed-tones.ps1 -Check # 素材与生成物不一致时失败
powershell -NoProfile -ExecutionPolicy Bypass -File .\install.ps1
脚本写入前会逐个校验 OggS magic。必须是 Ogg:Chromium 的 decodeAudioData 不解码 mp3,内嵌 mp3 也没有意义。跨平台等价命令:node tools/check-embedded-tone.mjs。
自定义音效:就地裁切
设置里有一项自定义音效。选中本地文件后会解码、画出波形,并打开裁切对话框:拖动首尾两个把手、试听选中的那一段、保存。只有选中的片段会被编码(16-bit PCM WAV —— 浏览器不借助库能写出的唯一容器)并存入设置文档;建议 3 秒以内,超过约 2 MB 会被直接拒绝而不是悄悄截断。
梗音效的来源与再分发注意事项见 NOTICE;两个合成音效没有这个问题,终端用户也随时可以上传自己的。
目录结构
dsh-completion-alert/
├─ package.json dsh.pluginType=client、dsh.client.inject、bundle patch
├─ cordis.patch.yml 把 completion-alert 行插进 profile
├─ install.ps1 安装(备份、patchReload=live、幂等),-Uninstall 亦可
├─ uninstall-all.ps1 把插件痕迹从 profile 里彻底清掉,不碰别的插件
├─ assets/ 内嵌工具读取的音源
│ ├─ bingbingbing.ogg 梗音效(来源见 NOTICE)
│ ├─ crisp-a.ogg/.wav 合成:两音上行付款提示(F#6 → F#7)
│ └─ crisp-b.ogg/.wav 合成:三音上行短信提示(D4 → A4 → D5)
├─ lib/
│ ├─ index.js 宿主半边:volatile 设置 schema + 诊断路由
│ ├─ client.js 浏览器半边:完成检测 / 播放 / 通知层 / 设置页
├─ tools/
│ ├─ synthesize_tones.py 从零合成清脆音效(numpy)
│ ├─ embed-tones.ps1 把 assets/ 重新嵌进 lib/client.js(-Check 查漂移)
│ ├─ embed-audio.ps1 转发到 embed-tones.ps1 的兼容壳
│ └─ check-embedded-tone.mjs 漂移 + Ogg magic 检查(跨平台,CI 用)
└─ test/
├─ host.test.mjs schema 表面、volatile 标记、诊断路由
├─ client.test.mjs 音效库、设置清洗、完成边沿、提示范围、持久化、跳转
└─ loader.mjs / -hooks 给测试解析 schemastery 这个 peer 依赖
开发
npm install # 拉取宿主半边 import 的 schemastery peer 依赖
npm test # 45 项测试
node tools/check-embedded-tone.mjs
测试是行为测试而不是结构快照:客户端测试把真实 bundle 载入 vm 沙箱,配一个 React 桩、生成好的音效模块和伪造的 dsh 客户端上下文,然后直接驱动 store 去断言真正决定行为的那些点 —— 音效库与打包素材逐字节一致、首个快照基线、忙→闲边沿、仅后台会话 范围、去抖写入自己的命名空间、经 uiWorkspace 跳转、以及通知队列。宿主测试校验 schema 表面(包括 volatile 节点位于固定路径、其内部不再套 volatile —— 这是应用会直接拒绝的形状)和诊断路由的往返。
CI(.github/workflows/test.yml)在 Node 24 上跑测试与漂移检查。
已知限制
- 自定义音效存成 WAV。裁切对话框写 16-bit PCM,因为那是浏览器不借助库能编码的唯一容器;3 秒以内可以保证设置文档不会太大。
- 只有内嵌那一条路必须是 Ogg。自定义上传 mp3/wav/ogg 都能解码(Chromium 解 mp3 没问题),但构建期内嵌的 payload 必须是 Ogg。
- 同时只响一声。上一声还没放完时来的新完成会替换它,而不是混在一起。
- 不走系统级通知。通知是 dsh 自己的浮层卡片,因此不需要 Electron 通知权限,Web 与桌面端表现一致。
- 提示音有三个,其中一个是梗素材。两个清脆音是我自己合成的,没有版权顾虑;「冰冰冰」按 NOTICE 里的说明对待。