dsh-sound-notifier
v1.2.0
Published
DSH 提示音插件:需要你决策(审批/提问)时播放提示音,可在设置页里配置,自带原创音效
Maintainers
Readme
dsh-sound-notifier
DSH 提示音插件:AI 需要你做决定时,播放一声提示音。
当 AI 需要你决策时(请求你批准某个操作,或向你提问并等待回答),它会停下来等你。 如果你正在做别的事,很容易错过——这个插件在这些时刻播放提示音,你就不必一直盯着屏幕。
特性
- 🔔 两种决策场景:审批确认、用户提问,可分别开关
- 🎛 在设置页配置:DSH「设置 → 插件 → 插件配置」里的一张卡片,默认收起
- ▶️ 一键试听:改完声音当场听效果,不用等下次提问
- ⚡ 改动立即生效:无需保存、无需重启
- 🌏 中英双语:跟随 DSH 的界面语言
- 🎵 自带原创音效:包内附 4 个原创提示音(Ding / Chime / Alert / Tick),装上即用
- 🎚 也可自定义:支持 macOS 系统音效,或任意音频文件的绝对路径
- 🔀 双半侧插件:host 半侧监听事件,浏览器半侧渲染配置卡片
安装
从 npm 安装(推荐)
dsh plugin --profile web add dsh-sound-notifier本包声明了 dsh.bundle.patch,把包名加入 profile 的 dsh.profile.bundles 即可挂载:
{
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-sound-notifier"
]
}
}
}手动挂载(本地开发)
把包放到 profile 的 plugins/ 目录,然后在 cordis.patch.yml 里按相对路径挂载:
- insert:
- id: sound-notifier
name: "./plugins/dsh-sound-notifier/lib/index.js"
config:
enabled: true
sound: /System/Library/Sounds/Funk.aiff
onApproval: true
onQuestion: true配置
设置 → 插件 → 插件配置 → 展开 提示音 卡片。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| enabled | boolean | true | 总开关,关闭后完全不发声 |
| sound | string | ding | 内置音效名,或音频文件绝对路径 |
| onApproval | boolean | true | 审批确认时是否响 |
| onQuestion | boolean | true | 用户提问时是否响 |
- 被改过的字段会显示 已自定义,可点 重置 恢复默认。
- 改动立即生效,不需要保存按钮。
- 声音字段旁的 试听 按钮会立即播放一遍,可当场确认路径是否有效。
声音的两种写法
1)内置音效名(推荐) —— 直接填名字,host 半侧解析到包内 assets/sounds/<name>.wav:
| 名字 | 音色 |
|---|---|
| ding | 温和双音铃(A5 → E6),默认 |
| chime | 上行三音铃(C6 → E6 → G6) |
| alert | 下行双音(G5 → D5),更有紧迫感 |
| tick | 单音轻点(C6),最不打扰 |
名字大小写不敏感,.wav 后缀可省略;只允许 [a-z0-9-],因此不可能被用来做路径穿越。
2)绝对路径 —— 填任意 .aiff / .mp3 / .wav 文件的绝对路径,例如 macOS 自带音效位于 /System/Library/Sounds/:
Basso Blow Bottle Frog Funk Glass Hero Morse
Ping Pop Purr Sosumi Submarine Tink版权说明:包内自带的 4 个音效是本项目原创合成的(生成脚本见提交历史),以 MIT 随包分发。 macOS 系统音效(Funk / Glass 等)只作为路径被引用,不随包分发——它们的版权属于 Apple。 请勿把这些系统文件打包进你发布的分支。
跨平台播放
内置音效是标准 16-bit PCM WAV,任何系统都能播。插件会自动探测本机可用的播放器:
| 平台 | 使用的播放器 |
|---|---|
| macOS | afplay(系统自带) |
| Linux | 按序探测 paplay → aplay → pw-play → play → ffplay → mpv → cvlc,取第一个可用的 |
| Windows | PowerShell 的 Media.SoundPlayer(仅支持 WAV) |
探测只做一次并缓存。若系统上一个播放器都没有,「试听」会返回明确提示 (提示安装 paplay / aplay / ffplay / mpv),而不是静默失败。
Windows 注意:
SoundPlayer只支持 WAV,所以自定义的 mp3/m4a 在 Windows 上 无法播放;内置的 4 个音效都是 WAV,不受影响。
工作原理
host 半侧 — lib/index.js
inject = ["shell"],通过 shell 服务执行afplay。- 监听两个 Cordis waterfall 事件:
approval/request—— 审批请求user-questions/request—— 用户提问
- 监听器必须调用并返回
next()透传,否则会 veto 掉真正的 UI answerer。本插件严格透传,绝不干扰决策流程。 - 通过
settings.installSection(...)把部署配置注册成用户可覆盖的 settings 命名空间, 并用setSource拿到的取值函数在每次触发时读取当前配置。
试听路由
host 半侧另外注册了一个仅回环可达的接口,供卡片上的「试听」按钮调用:
POST /api/dsh-sound-notifier/preview
body: { "sound": "/absolute/path.aiff" } # 可省略,省略则播放当前配置
→ 200 { "ok": true, "sound": "..." }
→ 400 { "error": "invalid sound: expected an absolute file path" }
→ 403 非回环 / 跨站请求与事件播放的 fire-and-forget 不同,试听会 await 命令结果,把失败如实回报给界面。
浏览器半侧 — lib/client.js
手写的 module-loader 模块(不需要构建工具链):
window.__ModuleLoader__.load({
id: 'dsh-sound-notifier',
factory: function (require) {
var React = require('react'); // 外部依赖,用 createElement 而非 JSX
// ...
}
});在 settings.plugin.item 这个 keyed slot 上注册卡片,key 即 settings 命名空间:
ctx.slots.inject('settings.plugin.item', function () {
return ctx.slots.register(
{ name: 'settings.plugin.item', key: 'sound-notifier' },
SoundCard
);
});读写配置用 ctx.settingsScope.bind({ namespace }):
getSnapshot() / subscribe() / set(field, value) / unset(field)。
开发要点(踩过的坑)
- 设置页渲染的是「交集」:宿主服务的 settings 命名空间 ∩ 注册在这些 key 上的卡片。 只有 host 半侧时命名空间无人认领 → 什么都不渲染。两个半侧的命名空间名必须完全一致。
- 跨插件 value import 被禁止:官方卡片的
PluginCard外壳不能直接复用, 第三方插件必须自绘卡片 chrome(这也是本插件自带样式的原因)。 - waterfall 事件必须透传:忘了
return next()会直接破坏决策弹窗。 - schemastery 没有
.optional():可选字段用.default()表达,0~1 的数值用z.percent()。 - 插件代码改动需要重启
dsh web:cordis.patch.yml是 live 热重载的, 但 Node 的 ESM 模块按 URL 缓存,已加载的模块不会因文件变化自动重新求值。 - 路径插进 shell 命令必须转义:声音路径是可配置字符串,直接拼进
afplay "..."会形成命令注入面。本插件把路径整体包进单引号并转义内部单引号(shQuote), 已用真实 bash 验证恶意载荷只会被当成 1 个普通参数。 - 路由只对回环 + 同源开放:试听路由复用了 DSH 其他插件的
isLoopbackRequest约定, 校验远端地址、Host 头、sec-fetch-site与Origin。 ctx.shell.run必须收resolve()后的 spec:直接run({ command })会跳过 执行器对workdir/stdoutMaxBytes/sandboxPolicy的补全,沙箱执行器随即在const { mode } = policy处抛Cannot destructure property 'mode' of 'policy' as it is undefined。 正确写法:
(const result = await ctx.shell.run(ctx.shell.resolve({ command }));resolve()是同步的,会补上request.sandboxPolicy ?? ctx.sandboxPolicy.resolve()。)- 别用
.catch(() => {})吞掉播放错误:本项目就因此让「声音其实一直没响」被掩盖了很久。 失败要经ctx.logger.warn留痕。 - CSS 变量名要对:用
--dsw-alias-border-l2这类真实存在的主题变量 (--dsw-alias-border-secondary不存在),才能跟随明暗主题。
License
MIT
