- Home
- Plugins
- Integrations & Remote
- dsh-status-chime
dsh-status-chime
lijiawei255/dsh-status-chime
Speaks a short status line when DSH changes state — turn done, turn error, job done, job failed, goal complete, goal blocked, an approval waiting, or the agent needs your answer — using eight pre-recorded clips in Chinese (default) and English. The audio is played by the host process through ffplay, or through the Windows PowerShell player that ships with the OS, so it does not depend on a Web Audio tab. Windows only.
Install
dsh plugin --profile web add github:lijiawei255/dsh-status-chimeREADME
dsh-status-chime
给 DeepSeek Harness 加上「会说话的状态提示」:任务跑完、出错、后台任务结束、需要你回答时,用声音告诉你,而不是只让任务栏图标闪一下。
适配状态(2026-10-09,v0.4.1):已在 DSH Desktop 官方版 0.2.0-rc.2(当前最新发布版)上逐接口核对并真实触发复核——8 个场景的触发与映射全部通过,其中 6 个直接观测到实际播报(含此前从未触发过的
job-failed)。@deepseek-ai/dsh0.1.5 起的版本都支持(0.1.7 的事件变更已兼容,0.1.6 及更早走回退分支)。逐条证据见 docs/verification.md §0。
为什么做这个
DSH 自带的通知是视觉的:任务栏图标闪烁 + 系统气泡。这类提示有一个隐含前提——你得正在看着屏幕。
但真正需要被通知的时刻,往往正是你不在屏幕前的时候:
- 一个跑了十几分钟的任务终于结束
- 后台任务失败,而你已经切去干别的了
- 代理停下来等你回答一个问题,却在原地等了半小时
- 目标被卡住,需要你介入才能继续
于是提示被错过,时间被浪费。换成声音之后,「必须盯着」就变成了「听得见就行」——你可以去泡杯茶、看会儿书、或者在另一台机器上干活。
装好即用,不需要配置任何东西。
八个场景
时长是刻意设计的:长 = 有事需要你,短 = 有事情结束了。
准确地说,它是三个带,而不是「每一条都比下一条长一截」:
| 带 | 时长范围 | 场景 |
|---|---|---|
| 失败 / 受阻 —— 最需要你 | 4.8 – 8.0s | turn-error、job-failed、goal-blocked |
| 在等你动手 | 3.0s | approval |
| 事情结束(或只是简单问一句) | 1.6 – 2.6s | job-done、goal-complete、needs-input、turn-done |
同一带内的两条刻意靠得很近,因为它们的紧急度本来就同级:goal-complete(2.16s)和 needs-input(2.09s)只差 0.072s,中文那套也一样(差 0.072s)。靠声音长短能分辨的是「带」,不是每一条。
| 场景 | 什么时候响 | 中文 | 英文 | 紧急度 |
|---|---|---|---|---|
turn-error | 本轮失败,或撞到 token 上限 | 7.97s | 7.90s | 失败/受阻 —— 最长 |
job-failed | 后台任务失败 | 5.95s | 6.46s | 失败/受阻 |
goal-blocked | 目标受阻,需要你介入 | 4.78s | 4.85s | 失败/受阻 |
approval | 有操作在等你批准 | 3.00s | 3.00s | 在等你动手 |
job-done | 后台任务完成 | 2.59s | 2.59s | 已结束 |
goal-complete | 目标整体完成(只响一次,不是每轮) | 2.16s | 2.16s | 已结束 |
needs-input | 代理停下来等你回答 | 2.09s | 2.09s | 简单问一句 |
turn-done | 你发起的那一轮正常结束 | 1.58s | 1.92s | 已结束 —— 最短 |
⚠️ 两套时长不一样,别混用。 中文和英文是各自合成的(不同音色、不同语言),所以顺序一致但数值不同:英文 job-failed 是 6.46s,中文是 5.95s。上表两列都给了,按你实际用的语言看那一列。
两条过滤规则值得单独说明,它们避免了这个插件变成噪音源:
turn-error不看是否由你发起。自动续跑的回合出错,同样需要你知道。turn-done只看你发起的回合。否则一个跑 20 轮的目标会响 20 次「任务完成」。自动推进的进度由goal-complete/goal-blocked在目标级别汇报,而不是每一轮。
approval 只在审批策略为 ask(需要人工确认)时才会响 —— 策略是 never 时没有任何东西在等你,自然也不该出声。它监听的是会话事件 approval/asked,这条在 ask 策略下必定派发;另外还挂了作用域瀑布 approval/request 作兜底,两条路径播的是同一条音。
音频试听(GitHub 的 Markdown 不支持内嵌播放器,所以放在 Release 里,点开即可播放):
见 v0.4.1 release 的 16 个音频附件——不带后缀的是中文(默认那套),带 .en 的是英文。
链接钉在具体 tag 上而不是 latest,并且每个 release 都重新附上这 16 个文件:音频自 0.3.0 起一个字节都没变(0.4.0 只改了代码),所以每个版本页都能直接试听,不必依赖"某个旧版本还留着附件"。
⚠️ 那些附件只是试听用的,不是安装产物:安装走 dsh plugin add,插件包里本来就带了全部 32 个文件(16 条 × mp3 + wav),不需要从 Release 下载任何东西。
两种语言:中文(默认)和英文
同一套安装包,八条场景各带中英两版音频,来回切换不需要重装。默认是中文。
/voice-alerts lang # 看当前语言
/voice-alerts lang en # 切到英文
/voice-alerts lang zh # 切回中文
切换会写回配置文件(language 字段),重启后仍然生效。也可以直接改配置:
{ "language": "en" }
英文不是翻译,是重写的
这一点值得说明,因为它关系到这个插件的核心设计。
中文原文直译成英文会变长、变平,而**「时长 = 紧急度」正是这个插件唯一的信息载体** —— 一旦英文长度失控,光凭声音长短判断要不要过去就失效了。所以英文八条是按英语语境重写的,不是逐句翻译:
| 场景 | 中文 | 英文 |
|---|---|---|
turn-done | 任务完成。 | Turn complete. |
needs-input | 需要你回答。 | Waiting for your answer. |
goal-complete | 目标已完成。 | The goal is complete. |
job-done | 后台任务完成。 | The background job has finished. |
approval | 有操作等待你批准。 | An action is waiting for your approval. |
goal-blocked | 目标受阻,需要你介入处理后才能继续。 | The goal is blocked. It needs you before it can continue. |
job-failed | 后台任务失败,请回到 DSH 查看详情。 | The background job failed, and it needs your attention. Check DSH for details. |
turn-error | 任务执行出错,本轮未能完成,请回到 DSH 查看错误详情。 | The turn failed, so this round did not finish. Open DSH to see the error details, then try again. |
英文版的时长梯度是 1.92s → 7.90s,与中文同样单调(同样按严重度递增)。真正拉开的是三个紧急度带:跨进「失败/受阻」带的那一步有 38%(3.00s → 4.85s)。同一带内刻意靠得近 —— goal-complete(2.16s)与 needs-input(2.09s)只差 0.072s,中文那套也一样。所以靠长短分辨的是带,不是每一条。
⚠️ 这里踩过一个坑,记下来:英文
turn-error最初只有 16 个词、6.55s,而job-failed是 6.46s —— 只差 0.09s,耳朵根本分不出来。中文那边两者差 2.02s(25%,相对较长者;本文件所有百分比都按这个口径)。后来把英文错误文案加长到 19 个词,才恢复到 7.90s / 1.44s 的差距。如果你改文案,记得重新量一遍时长,别只看中文字数。
英文用的音色是 loongmary(温暖英音),不是让中文音色去读英文。试听过三个候选并做了排序(scripts/qa.mjs rank,产物写到 qa/,该目录不进版本控制;重跑一次名次不变)。中文音色读英文得分最低,评语是「明显合成感、节奏不自然」——自然度 4/10、音色 5/10(这两个分数容易记混,naturalness 是 4,character 才是 5)。
/voice-alerts status 会同时列出两种语言各自的音频是否齐备:
Clip sets: zh (active): all clips present | en: all clips present
装之前先知道三件事
- 平台:Windows 10/11。 保底播放器用的是 Windows 自带的 PowerShell +
System.Media.SoundPlayer,没有 macOS / Linux 支持。装了 ffmpeg 会优先用ffplay,但那只是可选增强。 - 需要 DSH Desktop,以及能跑
dsh命令的终端。装插件走dsh plugin,它内部调用 pnpm(见下面的坑)。 - DSH 版本:
@deepseek-ai/dsh0.1.5 起都支持,当前最新版 0.2.0-rc.2 已实测复核(见文首「适配状态」)。装了之后插件会自动探测宿主用的是哪一代 jobs 事件接口,不需要你按版本挑安装包。 - 音频不用装任何东西 —— 中英两套 32 个音频文件都在包里。
⚠️ 一次性提醒:包名和运行时名字不一样
这个容易让人找错地方,先说清楚:
| 名字 | |
|---|---|
| npm 包 / GitHub 仓库 | dsh-status-chime |
| 斜杠命令 | /voice-alerts |
| 日志前缀 | [voice-alerts] |
| 配置文件 | $DSH_HOME/voice-alerts.config.json(主路径) |
| 音频目录 | $DSH_HOME/voice-alerts/clips/ |
| cordis id | voice-alerts |
配置还有一个兼容用的旧位置
$DSH_HOME/voice-alerts/voice-alerts.config.json。插件只读先找到的那一个(先查主路径),所以:如果主路径的文件已存在,你改旧位置那个文件会被静默忽略。写入是写回它实际读到的那一个文件:主路径存在就写主路径,否则写旧位置;两个都不存在时才新建主路径。所以在"只有旧位置文件"的安装上,
/voice-alerts lang和on/off更新的是旧位置那个文件,不是主路径。拿不准时看启动日志里的config <路径>那一行——它写的就是真正在用的文件。
包名在 0.3.0 从 dsh-voice-alerts 改成了 dsh-status-chime(原来的名字和社区目录里另一个插件只差一个字母,会被市场规则隐藏)。改名只动包名和仓库名:命令、配置路径、日志前缀全都还是 voice-alerts,所以老用户升级不会坏。
排查问题时去日志里 grep voice-alerts,不是 dsh-status-chime。
安装
方式一:交给你的 Agent(推荐)
把下面这句话发给你的 DSH:
把
https://github.com/lijiawei255/dsh-status-chime装进我的 DSH desktop profile,装完提醒我重启。
仓库里有一份 INSTALL.md,写清了每一步该做什么。它存在的意义是:让不同的 Agent 装出同样的结果,而不是各自发挥。
方式二:自己敲命令
把 <PROFILE> 换成你自己的 profile 名(通常是 desktop)。先看一眼 $DSH_HOME/profiles/ 下有哪些目录再决定,不要照抄别人的:
# 从 GitHub 直接装
dsh plugin --profile <PROFILE> add github:lijiawei255/dsh-status-chime
# 或者先 clone / 下载 ZIP,再指向本地目录
git clone https://github.com/lijiawei255/dsh-status-chime
dsh plugin --profile <PROFILE> add .\dsh-status-chime
这条命令会做两件事:把包装进 profile,并把包登记为一个 profile 层(前提是包里声明了 dsh.bundle,本仓库已经声明了)。
⚠️ 如果报 ERR_PNPM_ADDING_TO_ROOT:profile 自带一个声明了 packages: [.] 的 pnpm-workspace.yaml,pnpm 9 会因此拒绝裸 add。加一个 -w 即可:
dsh plugin --profile <PROFILE> add -w github:lijiawei255/dsh-status-chime
(实测过的是:pnpm 9 会报错、pnpm 11.8.0 正常;pnpm 10 没测过。所以最稳的是加 -w。)详见 TROUBLESHOOTING.md 第 11 条。
装完必须完全重启 DSH Desktop,否则插件不会加载。
重启之后
在聊天框里敲:
/voice-alerts status
看到 Voice alerts: on (v0.4.1)、Language: zh、Scenes (8) 就说明装好了。想听一遍全部八条:
/voice-alerts
卸载
dsh plugin --profile <PROFILE> remove dsh-status-chime
卸载不会删除你 $DSH_HOME 下的东西。想彻底清干净,删这两处:
# 音频、play.ps1、clips.json
Remove-Item -Recurse -Force "$env:USERPROFILE\.dsh\voice-alerts"
# 运行时配置(配置**不在**上面那个目录里,是同级的一个文件)
Remove-Item -Force "$env:USERPROFILE\.dsh\voice-alerts.config.json"
卸载后需要完全重启 DSH Desktop 才生效。
⚠️ 质检报告不在 $DSH_HOME 下,也不随包发布:qa/report.md 由开发脚本 scripts/qa.mjs 写进仓库检出目录里的 qa/。那个目录在 .gitignore 里、也不在 package.json 的 files 中,所以用 dsh plugin add 装出来的用户没有这个文件(要跑质检得先克隆仓库)。
它怎么工作
DSH 事件 ──▶ lib/index.js ──▶ 聚合 / 节流 / 优先级 ──▶ 播放器 ──▶ 音频文件
事件来源(都是宿主进程里的公开事件):
| 事件 | 用途 |
|---|---|
session/event → turn/start / user/message / turn/end | 判断这一轮是不是你发起的,以及它是怎么结束的 |
session/event → goal/change | 目标的 complete 与 block |
jobs.events.subscribe → settled | 后台任务结束;completed 播、failed 播、killed 不播,awaited(已有等待者收走结果)与 cause: 'teardown'(退出/归档)也都不播 |
tools/pre-execute | 工具名命中 waitingTools 时,说明代理要停下来等你 |
session/event → approval/asked | 主路径:审批策略为 ask 时必定派发 |
approval/request | 兜底:作用域瀑布,同样只在 ask 下可能派发;两条路播同一条音 |
播放规则:同一 400 毫秒窗口内的多个事件只播优先级最高的那条;同一场景 1.5 秒内不重复;新的提示会打断正在播的那条。
运行时零网络:插件只读本地音频文件、只启动本地播放器。不联网、不上传、不校验。
技术栈:用大模型给声音做「预审」
这部分是我觉得最值得分享的地方。
给提示音挑一个合适的音色,传统做法是一个个盲听:试 10 个音色、改 20 次风格描述、听到耳朵疲劳,最后凭模糊印象拍板。这个项目换了个做法——让大模型先做初筛和排序,人只在筛出来的少数里做最终决定。
具体是三件事:
1. 用全模态模型当评审(核心)
tools/qw_local_omni.py 把多段候选音频一次性交给 Qwen-Omni,让它横向比较并按维度打分:
python tools/qw_local_omni.py preview/flash-mary-en.mp3 preview/flash-eva-en.mp3 preview/flash-yuanfei-en.mp3 `
--message "横向比较这几段录音,按清晰度/自然度/音色/干净度打分并排序"
拿到的是一个排序而不是一堆孤立分数——「A 比 B 更贴目标」这种相对判断,比「A 得 7 分」有用得多。实测给出的排序和理由相当具体(下面是模型理由的概述):
A4 在保持系统提示所需的清晰稳重之外,音色最贴合目标;A2 辨识度不足,A5 偏平淡,A1、A3、A6 则因音色过亮或偏年轻而被排除。
2. 用 ASR 转写回读做客观校验
光听着顺耳不够——TTS 可能吞字、念错、把缩写拆开读。tools/qw_local_asr.py 把音频转回文字,与预期文案做字符级相似度比对:
python tools/qw_local_asr.py assets/clips/turn-error.mp3 --lang zh
这条是可以当硬门槛的客观指标:文案是已知的,转写对不对是机械可判的。本项目中英各 8 条、共 16 条音频的相似度都是 1.000。
3. 时长梯度作为信息编码
上面表格里的时长不是随手定的,而是把「严重程度」编码进了音频自身:长 = 需要你出手。这样即使手机在旁边、屏幕没看,也能靠声音长短判断该不该放下手里的事。
由此得到的完整流程(scripts/ 里两个脚本,可直接复现):
scripts/build.mjs audition 出多个音色候选
↓
scripts/qa.mjs rank 全模态模型横向排序(不做 ASR;回读在下一步)
↓
人听筛出来的前 2-3 个,拍板 ← 决策量被压缩到很少
↓
scripts/build.mjs build 批量生成 + 响度归一
↓
scripts/qa.mjs clips 全部质检过关
一点经验:不要把大模型的主观打分当硬门槛。实测同一段音频两次评分能从「自然 6 / 音色 4」跳到「自然 9 / 音色 7」,四段明显不同的音频甚至拿到过完全一样的分数。所以本项目把指标分了两层——客观项(ASR 相似度、有无削波、清晰度、干净度)当门槛,主观项(自然度、音色、成熟度)只作参考,最终由耳朵决定。这个分工是这套流程能稳定跑起来的关键。
后来补上的三条检查,都是先发现了具体漏洞才加的:
| 检查 | 性质 | 为什么加 |
|---|---|---|
| 静音下限(峰值 ≥ −30 dB、均值 ≥ −35 dB) | 硬门槛 | 原来的峰值检查只抓削波,所以「时长正常但内容全静音」这种经典 TTS 失效能全项通过。实测成品峰值 −4.2…−1.9 dB、均值 −20.6…−16.6 dB,而全静音是 −91 dB。两条线的余量不一样:峰值门槛离最差成品 25.8 dB(−4.2 vs −30),均值门槛 14.4 dB(−20.6 vs −35) |
| 净语速(去掉停顿后的单位/秒) | 参考项 | 中文刻意放慢(rate 0.95),套用人类播报那个 3.2–5.5 字/秒会把八条里的七条判失败。对这个项目真正有意义的是条与条之间的一致性(因为时长承载紧急度),所以跟同语言中位数比;少于 5 个单位的短句豁免,否则两个词的 clip 会假报警 |
| UTMOS(MOS 预测器) | 参考项 | 给自然度一个可复现的数字,补上「大模型打分不稳定」那一环。必须按语言分别比:它给英文八条的分全部高于中文八条(4.38–4.51 vs 3.75–4.28),跨语言比会读成中文那套有缺陷 |
这三条都有反向验证(scripts/qa-negative-control.mjs,离线运行、不花 API 额度):注入全静音、极安静、语速拉长三类缺陷,断言门槛确实报出来,同时确认健康音频不会被误伤。一个只会说「通过」的检查器没有价值。
顺带记一个坑:加静音下限之前,先用
ttsproof(一个现成的 TTS QA 工具)试过。它能抓削波和截断,但抓不到全静音 —— 它源码里的empty_audio只判断「文件不存在或 ≤44 字节」,不看音频内容。所以那条下限是自己加的,不是从工具里白拿的。
配置
配置文件:$DSH_HOME/voice-alerts.config.json($DSH_HOME 通常是 ~/.dsh)。
找不到这个文件时使用内置默认值,所以不配置也能正常用。文件按修改时间热读取,改完立即生效,不需要重启。
完整模板见 assets/voice-alerts.config.json。常用项:
| 配置项 | 默认 | 说明 |
|---|---|---|
enabled | true | 总开关 |
volume | 85 | 0-100,只对 ffplay 生效 |
minIntervalMs | 1500 | 同一场景的重复触发抑制窗口 |
coalesceMs | 400 | 事件聚合窗口,窗口内只播最高优先级那条 |
interrupt | true | 新提示是否打断正在播的 |
scenes.<场景>.enabled | true | 单独关掉某个场景 |
player | "auto" | auto / ffplay / powershell |
ffplayPath | null | 显式指定 ffplay 路径(不在 PATH 上时用) |
playPs1Path | null | 显式指定回退脚本 play.ps1 的路径 |
commandName | "voice-alerts" | 斜杠命令名。与别的插件重名时只丢命令,不影响出声 |
waitingTools | ["ask_user_question","exit_plan_mode"] | 命中即视为「在等你回答」;DSH 若改工具名可在此覆盖 |
language | "zh" | 语音语言:zh / en;无法识别的值回退到 zh(不会静默) |
watchApprovals | true | 审批请求是否出声(只在策略为 ask 时可能触发) |
clipsDir | null | 自定义音频目录,优先级最高 |
命令
| 命令 | 作用 |
|---|---|
/voice-alerts | 依次播放全部八条(当前语言) |
/voice-alerts on / off | 立即开关(会写回配置文件) |
/voice-alerts lang <zh|en> | 切换语音语言,写回配置 |
/voice-alerts status | 看播放器探测结果、各语言各场景音频是否齐备 |
/voice-alerts test <场景> | 只播一条,用来排查某类事件有没有触发 |
换成你自己的声音
音频不是必须用仓库里这 8 条(中英各 8 条)。完整流程:
# 1. 改 assets/clips.json:文案在 clips.<场景>.text,音色在 model/voice/instruction
# 2. 换音色时先出样试听(把候选写在 voiceCandidates 里)
node scripts/build.mjs audition
node scripts/qa.mjs rank # 大模型帮你排序,人只挑最终那条
# 3. 把选定的 model/voice/instruction 写回 clips.json,然后批量生成
node scripts/build.mjs build
node scripts/qa.mjs clips # 质检
生成音频需要:ffmpeg(响度归一与转码)、Python 3(质检脚本)、阿里云百炼 CLI(TTS/ASR/Omni)。这三样只有你想自己生成时才需要——用仓库自带音频的话,什么都不用装。
生成后会有 mp3 与 wav 两份。不要删掉 wav:PowerShell 回退播放器只认未压缩 PCM,它是「干净 Windows 上零依赖」的保证。
自己生成的音频放在 $DSH_HOME/voice-alerts/clips/ 会自动优先于包内的(逐文件覆盖,所以只想换一条也可以)。
文件名规则要记住(想只替换一条英文音频时必须知道):
| 语言 | 文件名 |
|---|---|
| 中文(默认) | <场景>.mp3 / .wav,例如 turn-done.mp3 |
| 英文 | <场景>.en.mp3 / .en.wav,例如 turn-done.en.mp3 |
默认语言用不带后缀的名字,其它语言加 .<语言码> 中缀。 所以只想换英文的 approval,就放一个 approval.en.mp3(以及 .wav,如果你用 PowerShell 回退播放器)。
装了没声音?
按这个顺序排查,能覆盖绝大多数情况。完整版见 TROUBLESHOOTING.md。
- 重启了吗? 插件文件不热加载,改完或装完必须完全重启 DSH Desktop。
/voice-alerts status看 Player 那一行。unavailable说明两条播放路径都没探测到。- Windows 音量合成器把
ffplay(或powershell.exe)单独静音了——这是最常见的原因。右键任务栏音量图标 → 打开音量合成器,检查对应条目。 - 输出设备选错了。
- 某个场景被关掉了,或
enabled是false。 - 日志里有
no <语言> audio for <场景>(例如no zh audio for turn-done)→ 该语言的音频文件缺失。 - 日志里有
throttled <场景>→ 被节流窗口挡住了,属于正常行为。 - 命令打了没反应? 如果
/voice-alerts单独打能播(试听 + 状态),但带上参数(lang en、on、test <场景>)就毫无反应、反而像发了一条普通消息,多半是中文输入法把/打成了全角/,或者命令名里混进了大写(必须是全小写voice-alerts)。这两种都会让整行不被认作命令。从自动补全列表里选中命令可以同时避开这两点。详见 TROUBLESHOOTING.md 第 14 节。
日志在哪:官方桌面版不写宿主日志文件(实测 0.2.0-rc.2:其 app.asar 里根本没有 logs/host 这个路径,userData 下的 logs/ 目录是空的,只有崩溃报告 %APPDATA%\@deepseek-ai\dsh-desktop\logs\crash-*.log)。所以看 [voice-alerts] 那一行有两个办法:
- 在终端里用 CLI 跑,日志直接打在 stdout;
- 或者在聊天框里敲
/voice-alerts status—— 它把探测到的播放器、配置路径、音频齐备情况全列出来,任何构建上都可用,也是排查时最该先看的那一条。
(%APPDATA%\DSH Desktop\logs\host\… 是已停用的第三方版路径,0.4.1 起本文不再引用。)
⚠️ 一件事先说明:日志里没有 ffplay 不是故障。ffplay 属于 ffmpeg,需要另外安装;它只是让启动快一点、音量可独立调节。没有它时插件会用 Windows 自带的 PowerShell 播放器,功能完全正常。
平台与依赖
支持:Windows 10 / 11。
干净 Windows 上不需要安装任何第三方依赖就能出声,因为保底播放器用的是系统自带的 Windows PowerShell 5.1 + .NET System.Media.SoundPlayer。
| 情况 | 需要什么 |
|---|---|
| 干净 Windows 10/11,只装了 DSH | 什么都不用装,用系统自带播放器,启动开销约 0.4 秒 |
| 装了 ffmpeg | 无需配置,自动改用 ffplay:启动更快、volume 配置生效 |
| 想自己生成音频 | 额外需要 ffmpeg + Python 3 + 阿里云百炼 CLI |
| 运行时网络 | 零网络 |
播放后端的选择顺序:
- ffplay(如果探测到)—— 直接播 mp3,支持独立音量
- Windows PowerShell + SoundPlayer —— 系统自带,只认 wav,音量跟随系统
先试
-File play.ps1(可审计的磁盘脚本);若被执行策略或 ACL 拦住,自动改用-EncodedCommand内联命令重试一次 - 两条都不通 → 静默降级,只在日志里记一行
宿主侧拿不到窗口是否聚焦(DSH 的 native 桥只暴露了 notifyAttention 等接口),所以这个插件任何时候都会出声,包括你正看着窗口的时候。如果觉得吵,用 /voice-alerts off 或调 volume。
验证状态
我一向觉得,把「写过」和「验证过」分开讲清楚,比含糊地说「功能完整」有用得多。所以逐条列出,并且明确写出验证的边界。
下面这张表是摘要。逐条的原始证据、测量值、以及「没验证到哪一步」都记在 docs/verification.md —— 想核对任何一个数字就去看那份。
验证边界(请先读这一段)
以下结论全部来自同一台机器上的验证:
| 项 | 值 |
|---|---|
| 操作系统 | Windows 11 |
| DSH | DSH Desktop(官方版),@deepseek-ai/dsh 0.2.0-rc.2 |
| 该机器上的额外软件 | 装有 ffmpeg、Python 3、阿里云百炼 CLI |
0.4.1 的复核(2026-10-09):0.4.0 之前这一格写的是 0.1.7-rc.2,而 0.2.0-rc.2(当前最新发布版,9/29 发布)之后仓库没有任何提交,所以整份记录都是拿两代之前的宿主写下的。0.4.1 做了两件事:把插件用到的每个宿主接口在官方 0.2.0-rc.2 的
app.asar里逐个核对(没有一个被删除或改名),然后在真实 0.2.0-rc.2 宿主上把 8 个场景全部重新触发了一遍——6 个直接观测到实际播报,其中job-failed是首次被真实触发。逐条证据见 docs/verification.md §0。上一版(0.3.0)验证的是 0.1.5-rc.2。0.1.7 移除了
jobs.onJobDone,后台任务两个场景因此静默失效;0.4.0 就是为这件事发的——它改用 0.1.7 的jobs.events.subscribe,同时保留对旧 API 的回退,所以 0.1.5/0.1.6 也照常可用。
这意味着:
- ✅ 代码正确性、事件映射、音频质量——与本机装了什么无关,结论可迁移到你的机器。
- ✅ 「干净 Windows 零依赖」——保底播放器用的是 Windows 自带的 PowerShell 5.1 + .NET
System.Media.SoundPlayer,两者都是 Win10/11 的操作系统组件;无 ffmpeg 的情形已通过模拟验证。 - ✅ DSH 0.2.0-rc.2(当前最新版)——接口逐个核对 + 8 个场景真实触发(见上)。
- ⚠️ 比 0.2.0-rc.2 更新的版本——
0.2.1-alpha.1已存在但未测。DSH 改事件接口时确实可能让某些场景静默失效(0.1.7 那次就是),所以这一条仍然是「未知」,只是未知的边界从 0.1.7 推到了 0.2.0-rc.2。 - ⚠️ 在一台完全干净的、别人的 Windows 上从 GitHub 安装——我没有第二台机器,没有实测过这一步。CI(见下)在 GitHub 提供的干净 Windows 运行器上覆盖了「安装 + 加载 + 后端探测」,但运行器没有声卡,无法验证声音真的到了扬声器。
如果你在别的 DSH 版本或别的机器上遇到问题,请提 Issue 并附上日志里的 [voice-alerts] active … 那一行(它会写明探测到的播放器和配置路径),那基本能一眼定位。
逐层状态
| 层级 | 状态 |
|---|---|
| 事件层 | 8 个场景全部在 0.2.0-rc.2 上重新触发过:其中 6 个观测到实际播报(needs-input、approval、goal-complete、goal-blocked、job-done、job-failed),turn-done 与 turn-error 观测到正确决策、正确音频与玩家进程已启动,但一次性宿主在 turn 结束即拆解插件,播放被卸载清理掐断(桌面宿主长驻,不存在这一条) |
| 音频层 | 中英各 8 条、共 16 条全部通过自动质检(ASR 相似度 1.000、清晰/干净 10/10、无削波)。其中中文那 8 条另做过逐条耳听确认;英文那 8 条没做过这一步,只过了自动质检 |
| 后端层 | 强制 PowerShell 会正确选 .wav;模拟「没装 ffmpeg」时自动回退且仍能播;显式指定不存在的 ffplay 会明确失败而不偷偷换后端 |
| 接口层 | 插件用到的每个服务、事件与字段名都在官方 0.2.0-rc.2 的 app.asar(8097 个 JS 文件)里核对过,无一被删除或改名;唯一消失的是 0.1.7 就已移除的 jobs.onJobDone(回退分支,不触发即按设计) |
| 代码层 | scripts/selftest.mjs 用模拟上下文驱动插件,76 项检查覆盖事件映射、过滤规则、优先级、节流、命令、重名冲突、资产解析顺序、语言切换与回退,以及新旧两代 jobs 事件 API 的探测与回退 |
| 语言层 | 默认中文、lang en 切换后确实改选英文文件(自测断言的是解析到的文件名,不只是状态文字);配置里写无法识别的语言会回退到中文而不是静默 |
| CI | .github/workflows/verify.yml 在干净的 windows-latest 上验证:真实安装并登记为 profile 层、清单无 BOM、只依赖 Node 内置模块、32 个音频齐备(8 场景 × 2 语言 × 2 格式)、无 ffplay 时 PowerShell 后端仍被探测到、76 项自检、隐私扫描。0.4.1 起钉的是 0.2.0-rc.2 |
关于 approval 的验证要说清楚边界:触发时审批策略必须是 ask。我本人是在 ask 策略下听到提示音的,但当时无法区分它走的是会话事件 approval/asked 还是兜底的作用域瀑布 approval/request —— 两条路径播同一条音。会话事件那条的行为由自检覆盖(含「子代理会话的审批不出声」),但它是否在生产环境中被派发,我没有单独取证过。
补充(0.4.1):这次在 0.2.0-rc.2 上把
approval真的触发了——宿主把「写工作区之外的路径」判成需要审批,插件随即播了approval.wav。两条路径仍然无法区分(它们播同一条音),但「在 0.2.0-rc.2 的ask策略下确实会响」这一点现在是实测的。另外,本机当前的审批策略是never——那种情况下不会响,这是按设计:没有任何东西在等你,自然不该出声。
CI 的详细「证明了什么 / 没证明什么」写在 workflow 文件头部——包括它不能证明声音到达扬声器这一点。
一个已知缺陷,必须说清楚
job-failed 对「后台 shell 命令非零退出」实际不可达。
实测:一个以后台方式运行、exit 7 结束的命令,DSH 把它的状态记为 completed,于是播的是「后台任务完成」。原因是 job 的状态只有 completed / killed / failed 三种终态,而 shell 后台任务的生产者只会产出前两种;failed 保留给「后台工具任务报告错误」或生产方合约违约的情况。这是框架行为,0.2.0-rc.2 里依然如此,不是本插件的实现问题。
0.1.7 新增了一个可观察点:job 详情里现在带 exit code: N,所以非零退出在日志里看得到了。但我故意不去解析它——那是写给人看的字符串,把它当契约,下次框架改措辞就会静默失效。想要「非零退出也响」的话,正确做法是让 DSH 把退出码变成状态字段,而不是在这里猜文本。
0.4.1 更新:这个场景的触发现在被真实观测到了。 触发它的是上面预测的那条路径——一个后台工具任务失败:那次后台 pwsh 作业因为沙箱没能给工作区套上写 ACL 而根本没能启动,生产者把它记成终态 failed,插件于是播了 job-failed.wav(复现两次);把沙箱切到 danger-full-access 后同一个作业正常完成,播的就是 job-done.wav——正是这个场景存在的意义。所以现在的口径是:「后台 shell 命令非零退出」仍然不可达(框架行为),但「后台工具任务失败」已实测可达。
0.4.0 修的是哪一处
0.1.7 删掉了 jobs.onJobDone,而 0.3.0 只认它——所以升级到 0.1.7 之后,job-done 与 job-failed 两个场景是静默失效的,日志里连一行报错都不会有(异常被宿主按 fiber 吞掉了)。0.4.0 改成订阅 0.1.7 的 jobs.events.subscribe({ owners: 'all' }) 提交流:
- 只认
settled;registered/progress/stopping/output/removed一律忽略。 awaited: true(已有等待者收走结果)与cause: 'teardown'(会话归档、宿主退出)都不出声——否则每次关程序都会响一声,job_wait也会重复播报。- 旧 API 仍然保留为回退分支:宿主没有
events.subscribe时自动走onJobDone,并在日志里说明用的是哪一条。两条分支都由自测覆盖。
0.4.1 做了什么
没有改插件代码——0.2.0-rc.2 没有破坏任何接口,所以这一版是「复核 + 标注」:
- 在官方 0.2.0-rc.2 上逐接口核对(docs/verification.md §0a),并真实触发 8 个场景(§0b/§0c)。
- 把文首的适配状态、徽章、安装章节的版本要求补上——此前版本口径只出现在本文中后段的「验证边界」表里,而且写的是 0.1.7。
- CI 的钉版从
0.1.7-rc.2升到0.2.0-rc.2,并修掉那段已经过期的注释(它还在说「npm 的latest是 0.1.5-rc.3」)。 - 修掉两份 README 里已停用的第三方版日志路径(见上面「装了没声音」第 9 条附近的说明)。
- GitHub 仓库描述补上版本信息。
一句话:0.4.1 是「确认 0.4.0 在 0.2.0-rc.2 上仍然成立,并且把这件事写在别人一眼能看到的地方」那一版。
本地与开源:镜像,以及唯一允许的差异
这个项目同时是两样东西:别人下载安装的仓库,和我本机正在跑的那份。两者一旦不一致,麻烦在于它不留痕迹——版本号还是同一个,代码已经变了,于是"我本地测过的"和"你下载到的"就不是一个东西。所以镜像被当成一条可执行的规则,而不是一个承诺。
逐字节镜像(node scripts/sync-profile.mjs --all --check / --apply):
| 本地 | 仓库 |
|---|---|
单文件安装的 <profile>/<name>.js | lib/index.js |
$DSH_HOME/voice-alerts/clips/** | assets/clips/**(32 个音频) |
$DSH_HOME/voice-alerts/play.ps1 | assets/play.ps1 |
这条规则不是摆设:这次就靠哈希抓出本地那份落后三笔提交、还差一次宿主 API 迁移,而两边版本号一模一样。
唯一允许的差异是措辞。 $DSH_HOME/voice-alerts/clips.json 是私密母本(措辞按我的原话写,不外发),仓库里的 assets/clips.json 是同一批音频的中性描述——音频不重新生成,32 个文件就是当初试听选中的那批。差异只允许落在:注释、试听候选清单(id / label / instruction)、chosenCandidate 名字、风格 instruction。
而且是机器可验的:sync-profile.mjs 会把每一个差异字段路径分类,功能性差异必须为 0(模型、音色、rate、pitch、volume、format、sampleRate、loudnorm、以及每一句中英文案),非 0 直接以 exit 3 报出来,因为那种差异不是"措辞",改不了也糊不过去。分类方向是保守的:没被归类的字段一律算功能性,所以新增字段不可能被当成"大概只是措辞"混过去。
禁用词表只存在本地:$DSH_HOME/voice-alerts.scan.json(在任何 clone 之外),由 node scripts/scan-sensitive.mjs . 执行。扫描器只报文件、行号、类别,绝不回显命中的内容,所以它的输出可以安全贴进 Issue、CI 日志或对话——检查本身不会变成被检查内容的第二份拷贝。
两点如实说明:
- CI 跑不了这道措辞闸门——GitHub 运行器没有我的
$DSH_HOME,所以 CI 只应用内置规则(凭据、密钥形态、个人路径)。措辞规则保护的是我提交前的那一步。 - 正向对照:扫私密目录(
node scripts/scan-sensitive.mjs "$DSH_HOME/voice-alerts")必须报命中;如果它变干净了,说明本地规则没加载,闸门实际上开着。
完整规则、逐项差异与"什么不算措辞差异"(maturity 是 qa.mjs 的评分维度名,保留)见 docs/mirror-policy.md。
目录结构
dsh-status-chime/
├── lib/index.js # 插件主体,唯一运行时代码
├── cordis.patch.yml # 声明这个包是 profile 层(dsh.bundle 指向它)
├── package.json # 包名、dsh.bundle 声明、engines
├── assets/
│ ├── clips/ # 8 场景 × 2 语言 = 16 条音频,每条 mp3 + wav(共 32 个文件)
│ ├── clips.json # 文案/音色/参数的唯一事实源
│ ├── voice-alerts.config.json # 配置模板
│ └── play.ps1 # PowerShell 回退播放器(纯 ASCII,原因见文件头)
├── tools/ # 百炼本地音频辅助脚本(ASR / Omni / UTMOS)
├── scripts/ # 生成 / 质检 / 自检 / 负向验证 / 隐私扫描
│ ├── build.mjs # 生成音频(支持 --lang)
│ ├── qa.mjs # 质检(支持 --lang)
│ ├── selftest.mjs # 离线自测,76 项
│ ├── qa-negative-control.mjs # 证明质检门槛真的会拦下坏音频
│ ├── verify-local-install.mjs # 验证已安装的单文件版本
│ ├── sync-profile.mjs # 让单文件本地安装与仓库保持一致(--check / --apply)
│ ├── scan-sensitive.mjs # 隐私/措辞扫描
│ └── scan-sensitive.verify.mjs # 证明扫描器真的抓得到
├── docs/verification.md # 逐条验证记录
├── docs/mirror-policy.md # 镜像规则:什么逐字节相同、什么只允许差措辞
├── .github/workflows/verify.yml # CI:干净 Windows 上跑安装 + 自测 + 扫描
├── INSTALL.md # 给 Agent 看的安装步骤
├── TROUBLESHOOTING.md # 没声音时的排查清单
├── README.en.md # 英文说明
├── CHANGELOG.md
└── LICENSE # MIT
qa/、preview/、tmp/ 是脚本跑出来的中间产物,已在 .gitignore 里,不随仓库发布。
收录
已被 Awesome DSH Plugin 收录,分类 notify:条目见通知与集成,提交记录 #5553(已合并)。
那个列表是人工 + CI 审核制,不是爬虫采集:要求仓库声明 dsh.bundle、含真实可运行的代码、仓库创建满 1 天,而且条目描述必须和代码对得上。
许可
代码与随包音频均以 MIT 发布。
随包音频是用阿里云百炼(Model Studio)的语音合成生成的,不是任何人的真人录音;如果你打算使用它们,请自行确认你的用法符合相应服务条款。仓库不授予任何底层声音模型的权利。你完全可以不用这套音频——scripts/build.mjs 可以生成你自己的,插件也会优先使用你放在 $DSH_HOME/voice-alerts/clips/ 的文件。
贡献
提交前请跑这两个脚本。第一个确认没有把个人路径或凭据带进来,第二个确认自检仍然全绿:
node scripts/scan-sensitive.mjs .
node scripts/selftest.mjs
scan-sensitive.mjs 只报告文件、行号与命中类别,绝不打印命中内容——所以你在 Issue 里贴它的输出是安全的。它自带的规则集可以用 $DSH_HOME/voice-alerts.scan.json(不提交)追加你自己机器上的私有字符串。想确认它真的能抓到东西,跑 node scripts/scan-sensitive.verify.mjs:它植入几段明显是伪造的凭据形态值并断言扫描器能报出来,同时断言报告不回显这些值。
欢迎提 Issue 与 PR。特别欢迎的:macOS / Linux 的播放后端(当前后端层是 Windows 专用的)。
Related plugins
dsh-web (dsh-ssh)
zhu1090093659/dsh-web
dsh-web (dsh-remote-web-ui)
zhu1090093659/dsh-web
dsh-web-ui (dsh-ssh)
zhu1090093659/dsh-web-ui
dsh-web-ui (dsh-remote-web-ui)
zhu1090093659/dsh-web-ui