给 DeepSeek Harness Desktop 用的通知插件:任务跑完会说话。
36.2/100
综合分
3
GitHub 星标
1,991
npm 周下载
—/5
用户评分
数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-notify-saynpm 源 · dsh-notify-say 的发布者账号已校验与本仓库一致
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
是什么
dsh 原生插件 · desktop
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 9 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✓npm 包dsh-notify-say @ 0.5.3
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/11/1 22:52:13
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 81 处,展示前 69 条)
- 读取本地文件lib/index.js:140
return JSON.parse(readFileSync(join(PLUGIN_ROOT, 'package.json'), 'utf8')).version || 'unknown'; - 读取本地文件lib/index.js:250
if (!existsSync(p)) return; - 读取本地文件lib/index.js:251
if (statSync(p).size < LOG_MAX_BYTES) return; - 写入本地文件lib/index.js:252
renameSync(p, p + '.1'); - 写入本地文件lib/index.js:269
mkdirSync(dirname(p), { recursive: true }); - 写入本地文件lib/index.js:275
appendFileSync(p, `${ts} ${message}\n`, 'utf8'); - 执行外部命令来自配置/变量lib/index.js:1153
child = spawn(POWERSHELL_EXE, args, { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true }); - 执行外部命令固定命令lib/index.js:1202
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }); - 执行外部命令来自配置/变量lib/index.js:1249
const c = spawn(POWERSHELL_EXE, [ - 执行外部命令来自配置/变量lib/index.js:1697
const out = execFileSync(POWERSHELL_EXE, [ - 读取本地文件lib/index.js:1811
const useVbs = existsSync(vbsHandler); - 读取本地文件lib/index.js:1813
if (!existsSync(handler)) { jumpProtoState = 'failed'; return false; } - 执行外部命令来自配置/变量lib/index.js:1835
execFileSync(POWERSHELL_EXE, [ - 执行外部命令来自配置/变量lib/index.js:1876
execFileSync(POWERSHELL_EXE, [ - 写入本地文件lib/index.js:3017
try { mkdirSync(dir, { recursive: true }); } catch { } - 写入本地文件lib/index.js:3018
writeFileSync(join(dir, 'port.txt'), String(detectedPort), 'utf8'); - 发起网络请求lib/client.js:98
return fetch(cfgUrl('pending-jump'), { - 发起网络请求lib/client.js:117
return fetch(apiUrl('consume'), { - 发起网络请求lib/client.js:156
return fetch(apiUrl('voicepacks/index'), { cache: 'no-store', credentials: 'same-origin' }) - 发起网络请求lib/client.js:162
return fetch(apiUrl('voicepacks/install'), { - 发起网络请求lib/client.js:170
return fetch(apiUrl('voicepacks/uninstall'), { - 发起网络请求lib/client.js:179
return fetch(apiUrl('diagnostics'), { - 写入剪贴板lib/client.js:1131
navigator.clipboard.writeText(cmd).then(function () { done(true) }).catch(function () { done(false) }) - 读取本地文件lib/config-store.js:298
try { exists = existsSync(path); } catch { exists = false; } - 读取本地文件lib/config-store.js:348
if (!existsSync(p)) return { ok: true, value: {} }; - 读取本地文件lib/config-store.js:349
text = readFileSync(p, 'utf8'); - 写入本地文件lib/config-store.js:401
mkdirSync(dirname(p), { recursive: true }); - 写入本地文件lib/config-store.js:402
writeFileSync(tmp, JSON.stringify(doc, null, 2) + '\n', 'utf8'); - 写入本地文件lib/config-store.js:403
renameSync(tmp, p); - 读取本地文件lib/config-store.js:405
try { if (existsSync(tmp)) unlinkSync(tmp); } catch { } - 删除本地文件lib/config-store.js:405
try { if (existsSync(tmp)) unlinkSync(tmp); } catch { } - 读取本地文件lib/config-schema.js:103
if (!existsSync(anchor)) continue; - 读取本地文件lib/config-route.js:140
if (existsSync(p)) unlinkSync(p); - 删除本地文件lib/config-route.js:140
if (existsSync(p)) unlinkSync(p); - 读取本地文件lib/voice-pack-store.js:116
if (!existsSync(dir)) return []; - 读取本地文件lib/voice-pack-store.js:117
if (!statSync(dir).isDirectory()) return []; - 读取本地文件lib/voice-pack-store.js:118
return readdirSync(dir).filter((f) => f.toLowerCase().endsWith('.wav')); - 读取本地文件lib/voice-pack-store.js:132
if (!existsSync(root)) return []; - 读取本地文件lib/voice-pack-store.js:133
return readdirSync(root, { withFileTypes: true }) - 读取本地文件lib/voice-pack-store.js:195
if (existsSync(userKeyDir)) { - 读取本地文件lib/voice-regions.js:79
return !!p && existsSync(p) && statSync(p).isDirectory(); - 读取本地文件lib/voice-regions.js:79
return !!p && existsSync(p) && statSync(p).isDirectory(); - 读取本地文件lib/voice-regions.js:116
const flat = readdirSync(userRoot).some((f) => f.toLowerCase().endsWith('.wav')); - 读取本地文件lib/update-check.js:71
const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8')); - 读取本地文件lib/update-check.js:158
readFileSync(join(dir, '.git', 'HEAD')); - 读取本地文件lib/diagnostics.js:70
if (existsSync(rotated)) { - 读取本地文件lib/diagnostics.js:72
parts.push(readFileSync(rotated, 'utf8')); - 读取本地文件lib/diagnostics.js:74
if (existsSync(main)) { - 读取本地文件lib/diagnostics.js:76
parts.push(readFileSync(main, 'utf8')); - 读取本地文件lib/diagnostics.js:89
const files = readdirSync(dir) - 读取本地文件lib/diagnostics.js:91
.map((f) => ({ f, t: statSync(join(dir, f)).mtimeMs })) - 删除本地文件lib/diagnostics.js:94
try { unlinkSync(join(dir, f)); } catch { } - 写入本地文件lib/diagnostics.js:136
mkdirSync(dir, { recursive: true }); - 写入本地文件lib/diagnostics.js:139
writeFileSync(out, content, 'utf8'); - 删除本地文件递归lib/voice-pack-download.js:426
rmSync(tmpDir, { recursive: true, force: true }); - 写入本地文件lib/voice-pack-download.js:427
mkdirSync(tmpDir, { recursive: true }); - 写入本地文件lib/voice-pack-download.js:453
mkdirSync(join(target, '..'), { recursive: true }); - 写入本地文件lib/voice-pack-download.js:454
writeFileSync(target, r.data); - 删除本地文件递归lib/voice-pack-download.js:458
try { rmSync(tmpDir, { recursive: true, force: true }); } catch { } - 读取本地文件lib/voice-pack-download.js:475
if (existsSync(finalDir)) { - 写入本地文件lib/voice-pack-download.js:476
renameSync(finalDir, backupDir); - 写入本地文件lib/voice-pack-download.js:480
renameSync(tmpDir, finalDir); - 写入本地文件lib/voice-pack-download.js:483
try { renameSync(backupDir, finalDir); } catch { } - 删除本地文件递归lib/voice-pack-download.js:488
try { rmSync(backupDir, { recursive: true, force: true }); } catch { } - 删除本地文件递归lib/voice-pack-download.js:491
try { rmSync(tmpDir, { recursive: true, force: true }); } catch { } - 读取本地文件lib/voice-pack-download.js:512
if (!existsSync(dir)) return { ok: false, error: 'not-installed' }; - 删除本地文件递归lib/voice-pack-download.js:514
rmSync(dir, { recursive: true, force: true }); - 读取本地文件lib/voice-pack-download.js:525
if (!existsSync(p)) return null; - 读取本地文件lib/voice-pack-download.js:526
return JSON.parse(readFileSync(p, 'utf8'));
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
DSH Desktop 的桌面通知真的会说话:Windows TTS 开箱即用,VOICEVOX 语音包可选,还有空闲门控,会等子代理跑完再说「done」。
最近更新
在 GitHub 查看全部 →voice-packs2026-10-18
· 语音包索引(固定 tag)
这个 Release 专用于语音包分发,不随插件版本变化。
· 为什么单独一个 tag
插件原来的索引地址是 /releases/latest/download/packs.json。
latest 指向最新的 Release —— 但那个 Release 不一定带 packs.json。
哪天为了修插件 bug 发一个只有插件的版本,latest 就跳过去,
它没有 packs.json → 404 → 旧版下载器的音色列表整个失效。
也就是说:发一个无关的插件版本能把语音包功能弄坏。
所以索引固定在这个 tag 下,与插件版本解耦。
插件侧的地址是:
另保留了 latest 作为回退(万一这个 tag 出问题)。
· 内容
13 个音色 zip + packs.json 索引。每个 zip 内附 TERMS.md(署名与官方条款链接)。
音色来自 VOICEVOX,各有各的使用条款。下载前请在插件设置页查看对应条款。
v0.4.302026-10-18
· 语音包(可选素材)
13 个通过许可审核的 VOICEVOX 音色,每个音色一个独立 zip。
按需下载,不需要全下。
· 怎么装
推荐用插件 GUI 的「语音包管理」下载(会自动校验 SHA-256 并解压到正确位置)。
手动安装:解压到 %LOCALAPPDATA%\dsh-notify-say\voice-lines\\。
· 每个包里有
/done/*.wav 完成
/question/*.wav 提问
/approval/*.wav 待批准
/fail/*.wav 失败
/TERMS.md 该音色的官方条款与署名要求
/MANIFEST.json 文件清单 + SHA-256
· 授权说明
这些音频由 VOICEVOX 生成,不适用本项目的 MIT 许可。
每个包内有 TERMS.md,列明署名格式与官方条款链接。
再分发时必须把条款一并传递。
· 未包含的音色
以下三个有意不提供(见 docs/audio-audit/README.md):
· もち子さん —— 官方规约禁止「音声素材を無償配布」
· 春日部つむぎ —— 官方规约禁止事项含「二次配布」
· ユーレイちゃん —— 角色身份未确认(与「夜語タバリ」是不同权利人)
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。
README
由 DeepSeek 最新模型翻译生成dsh-notify-say
给 DeepSeek Harness Desktop 用的通知插件:任务跑完会说话。
别的通知插件「叮」一声;这个会喊你「任务完成了」。
而且子代理还在跑的时候,它不会骗你说完成了。
只支持 Windows。
设置页
设置页实拍(声音来源 = 系统语音合成)。语音包、提醒规则、高级区见下文。
目录
- 它解决什么问题
- 特性
- 安装
- 快速开始
- 配置
- 语音包
- 关于「跳回会话」
- 兼容性与降级
- 已知限制
- 常见问题
- 开发
- 许可
它解决什么问题
你让 AI 跑一个较长任务,然后切去干别的。常见两种通知插件的毛病:
1. 该响的时候不响 —— 主会话一轮结束就报「完成」,但其实它派出的子代理还在跑,你回来发现没干完。
2. 不该响的时候一直响 —— 每个子代理结束都弹一次,连环轰炸。
本插件的做法:
- 空闲门(idle gate):主会话这轮结束、但仍有子代理或后台任务在跑时,先不播;等全部结束再播一次。超时兜底(默认 10 分钟),防子任务卡死导致永不播报。
- 按会话去抖:同一会话默认 60 秒内只响一次。
- 失败单独提醒:回合出错时播日语道歉语音(可选)—— 比「任务完成」更该让你立刻知道。
特性
| 特性 | 说明 |
| 语音播报 | 播放本地 wav,不调用云端 TTS。
展开完整文档(README 剩余 1 节)
dsh-notify-say
给 DeepSeek Harness Desktop 用的通知插件:任务跑完会说话。
别的通知插件「叮」一声;这个会喊你「任务完成了」。
而且子代理还在跑的时候,它不会骗你说完成了。
只支持 Windows。
设置页
设置页实拍(声音来源 = 系统语音合成)。语音包、提醒规则、高级区见下文。
目录
- 它解决什么问题
- 特性
- 安装
- 快速开始
- 配置
- 语音包
- 关于「跳回会话」
- 兼容性与降级
- 已知限制
- 常见问题
- 开发
- 许可
它解决什么问题
你让 AI 跑一个较长任务,然后切去干别的。常见两种通知插件的毛病:
1. 该响的时候不响 —— 主会话一轮结束就报「完成」,但其实它派出的子代理还在跑,你回来发现没干完。
2. 不该响的时候一直响 —— 每个子代理结束都弹一次,连环轰炸。
本插件的做法:
- 空闲门(idle gate):主会话这轮结束、但仍有子代理或后台任务在跑时,先不播;等全部结束再播一次。超时兜底(默认 10 分钟),防子任务卡死导致永不播报。
- 按会话去抖:同一会话默认 60 秒内只响一次。
- 失败单独提醒:回合出错时播日语道歉语音(可选)—— 比「任务完成」更该让你立刻知道。
特性
| 特性 | 说明 |
| 语音播报 | 播放本地 wav,不调用云端 TTS。发布版自带 2 个音色(1 日语 + 1 中文),另 15 个可在设置页按需下载 |
| 空闲门 | 有在飞工作时不误报「完成」,全部结束才播一次 |
| 五类事件 | 完成 / 出错 / 被拦截 / 待批准 / 要提问,各自独立开关 |
| 设置页 GUI | 走官方 Config schema,在 DSH 设置页里直接改,改完立即生效 |
| 命令行 | tools/notify-config.ps1,适合脚本化与排查 |
| 切回窗口时跳会话 | 通知时你不在前台 → 你切回来时自动跳到那个会话(见下文) |
| 点哪条通知就跳哪条 | 多会话同时完成时,点不同的 Toast 会落到各自的会话(可选开关,见下文) |
| 不装语音也能用 | 没有语音包 = Toast + 系统提示音,这是正常配置 |
| 许可先告知 | 下载音色前弹出条款确认,写明署名要求与官方条款链接 |
| 诊断导出 | 一键导出脱敏日志(用户名/路径已替换),方便提 Issue |
| 日志隐私分级 | 默认只记元数据,不记台词正文;要详细日志需手动开 debug |
| 检查更新 | 显示当前/最新版本,有新版时给出升级命令(不自动更新,见下文) |
联网说明:通知、播放、TTS 完全离线。
会联网的只有两件事: ① 下载语音包(访问 GitHub,只有你点「下载」时才发生) ② 检查更新(访问 npm registry 与 GitHub,每 24 小时最多一次)
不想让它检查更新?不点「检查更新」就不会主动查;已缓存的 24 小时内也不会再查。
网络不可用时这两件事都会失败,但不影响通知和语音。
安装
前置
- DSH Desktop 0.2.1-rc.2 ~ 0.3.0(>=0.2.1-rc.2 <0.3.0)
- 实测环境是 0.2.1-rc.2。更早版本未测过,所以没有声明支持
- 设了上界 0.3.0:DSH 若有不兼容改动,市场会拦住而不是让你装了才发现
- Windows(Toast 用 WinRT,播放用 MCI,调用 Windows PowerShell 5.1)
方式一:插件页(推荐,不用命令行)
1. 打开 DSH 的 设置 → 插件
2. 找到「包名或地址」输入框(不是上面那个搜索框 —— 那个只过滤已安装列表)
3. 填进去:
dsh-notify-say
4. 点「安装」,装完重启 DSH
装的时候卡住或报网络错误?在同一个面板里把「安装源」切到「中国大陆镜像源」再试 —— 官方源在国内经常拉不动。
填 dsh-notify-say 是包名(不是搜索词)。它也能接受 GitHub 仓库地址或本机目录路径,但包名最简单。
方式二:npm
npm install dsh-notify-say
方式三:手工 link(本地开发)
把本仓库目录链接进 profile:
D:\Program Files (x86)\DSH\profiles\desktop\package.json
dependencies 里加: "dsh-notify-say": "link:<本仓库路径>" dsh.profile.bundles 里加: "dsh-notify-say"
然后在该 profile 目录执行 pnpm install,重启 DSH。
快速开始
装好之后什么都不用配就能用:任务完成时会弹 Toast + 播自带音色。
想做更多,进 DSH 设置 → 通知:
| 想做的事 | 去哪 |
| 换音色 / 调音量 | 设置页「音色」「音量」 |
| 下载更多音色(15 个可选) | 设置页「语音包」→ 点「加载列表」→ 选一个点「下载」 |
| 只保留提示音、不要语音 | 音源改成「系统提示音」 |
| 出错时也要语音提醒 | 「出错」开关(默认已开) |
| 让子代理结束也提醒 | 「子代理」开关(默认关) |
| 少响一点 | 「同会话去抖」调大 |
| 看看有没有新版 | 高级区「版本与更新」→ 检查更新 |
配置
四种来源,优先级由低到高(后者覆盖前者):
1. schema 默认值
2. cordis.patch.yml 的 config
3. DSH 设置页里改的值
4. tools/notify-config.ps1 写入的 storages/config.json
⚠️ 注意语义:只要你在设置页改过某项,再改 cordis.patch.yml 里同一项不再生效。
这是 DSH 官方设计(用户层优先级更高),不是本插件的 bug。
设置页
DSH 设置 → 通知。包含音源选择、语音语言、音色、音量、增益、TTS 参数、五个事件开关、去抖与冷却、音源按事件覆盖、自定义音频、语音包管理、日志级别、诊断导出等。
分常规区与「高级」折叠区:日常只用常规区,排查时才展开高级区。
语音语言:日语音色和中文音色不混播。选「日本語」时随机模式只从日语音色里抽,选「中文」时只从中文音色里抽,音色下拉也只列该语言的音色。切换语言会自动把固定音色换到该语言的默认音色(中文 → 温柔女声,日语 → ずんだもん),避免出现「语言=中文、固定音色=ずんだもん」这种会直接落空的组合。
从旧版本升级、且之前已经把音色设成中文的用户,不需要手动改语言:插件会按你现有的音色自动归到「中文」(依据是音色名以 zh- 开头)。这条只在你的配置里没有语音语言这个键时生效 —— 自己显式选过的语言永远不会被覆盖。
命令行
cd <插件目录>\tools
.\notify-config.ps1 -Show # 看当前生效值 .\notify-config.ps1 -Voice off # 静音(Toast 仍弹) .\notify-config.ps1 -Volume 40 .\notify-config.ps1 -VoiceMode random .\notify-config.ps1 -VoiceName ankomon .\notify-config.ps1 -Disable aborted -Enable subagents .\notify-config.ps1 -Clear # 交还控制权给设置页
改完立即生效,不需要重启 DSH。
语音包
核心包自带两个音色:一个日语(ずんだもん)+ 一个中文(温柔女声),安装包约 3.7 MB。
为什么带两个而不是只带一个:中日两种台词都在包里(中文台词是给系统语音合成用的),只带日语音色的话,中文用户听到的是日语腔念中文。带一个中文的,中文用户开箱就有可用声音;想要别的音色再下载。
另有 15 个音色可单独下载(12 个日语 + 3 个中文,每个约 1.0–3.0 MB,合计约 27.6 MB), 在设置页里点几下就能装,不需要命令行。
中文音色
| 音色 | 说明 |
| 温柔女声 · 中文 | 自带,声音最宽厚 |
| 清亮女声 · 中文 | 可下载 |
| 平稳男声 · 中文 | 可下载 |
| 低沉男声 · 中文 | 可下载 |
中文音色由 EmotiVoice 合成(网易有道开源,Apache-2.0),与日语的 VOICEVOX 音色在分发机制上完全一样:打包、下载、卸载、条款确认走同一套代码。
每个音色有 12 条台词,覆盖完成、出错、等待回答、等待批准四种场景,每条按场景配了不同的语气(出错时会放低,完成时轻快)。
怎么装
1. DSH 设置 → 通知 → 语音包 → 点「加载列表」
2. 列表里会显示每个音色的名称、体积、署名要求
3. 点「下载」→ 先弹出条款确认弹窗(见下)→ 确认后开始下载
4. 装好后在「音色」下拉里就能选到
下载前会告知什么
点下载会弹一个居中弹窗,写明:
- 署名要求(例如 VOICEVOX:ずんだもん)—— 你用这些语音做的内容需要保留这个署名
- 官方条款链接(可点开原文)
- 提示"下载后 zip 内也带 TERMS.md;你用它做的内容(视频、直播等)仍需守上述条款"
这不是走形式:日语 13 个音色里有 2 个因为条款禁止再分发被我们排除在外, 1 个因为身份存疑(同名角色有多个)暂不收录。审核依据是官方条款原文,留档在 docs/audio-audit/。
需要说清楚:我们的审核结论是依据官方通用条款得出的,不等于各权利人针对"独立 WAV 素材 ZIP 再分发"出具过专项授权。
卸载
设置页里点「卸载」即可,会删掉该音色整个目录。
卸载不依赖网络 —— 离线时也能卸。
已装的音色放哪
%LOCALAPPDATA%\dsh-notify-say\voice-lines\<音色key>\
每个音色一个目录,卸载 = 删这一个目录。
不装语音包也完全可用
通知会退回 Windows Toast + 系统提示音。
想自己做音色
装 VOICEVOX(本地免费 TTS),启动引擎(默认 http://127.0.0.1:50021),然后按命名规则把合成好的 wav 放进对应目录:
sounds/voice-lines/<key>~1.wav 完成 sounds/voice-lines/question/<key>~1.wav 提问 sounds/voice-lines/approval/<key>~1.wav 审批 sounds/voice-lines/fail/<key>~1.wav 出错
(合成脚本不随本包发布 —— 它依赖本地 VOICEVOX 引擎与特定音色表,放进发布包只会误导。)
关于"跳回会话":能做到什么,做不到什么
这一节把边界写清楚 —— 有些事是宿主没给接口,不是本插件偷懒。
结论先给:能在你切回来时跳到「你离开期间最早完成」的那个会话;不能做到「点哪条通知就跳哪条」。
✅ 能做到:切回窗口时跳到那个会话
通知发出时你不在前台 → 插件记下那个会话你切回 DSH 窗口 → 插件自动跳到它
所以体验是:你在别处干活 → 听到"任务完成了" → 切回 DSH → 它已经帮你跳过去了。
相关开关(默认值偏保守):
| 开关 | 默认 | 说明 |
| 后台完成时跳转 | 开 | 通知时你不在前台 → 切回来时跳 |
| 前台完成时也跳 | 关 | 你正看着屏幕时也带你过去(v0.4.63 起真正生效,见下) |
| 多个会话完成时 | 跳到最早错过的 | 见下节 |
配置读不到时两个开关都当作关(宁可少跳一次,也不擅自动你的界面)。
前台完成怎么做到「立刻跳」
打开「前台完成时也跳」后,插件用长轮询盯着新信号:
窗口可见 → 客户端挂一个 GET /wait?after=<已知最新> → 服务端没新信号就挂着不返回(不占 CPU、不发新请求) → 一旦有完成信号,立刻返回 → 客户端立刻跳
所以是信号一到就跳,不存在「声音响了一会儿才跳过去」的延迟。
窗口一隐藏,长轮询立刻停止,改由下面的「切回来时跳」接手 —— 两者不会重复跳。
实现细节:为什么不用定时轮询 —— 定时轮询要么延迟明显(体验割裂),要么空转(每秒几次请求)。长轮询两头都不占。
多个会话都完成时跳哪个
这是个真问题:你离开期间 A、B 先后完成,回来只跳一个,另一个就只能靠侧栏绿点。
默认规则是跳到最早错过的那个。理由是:你回来时界面通常仍停在它上面,对它 openSession 等于无操作 —— 于是你正在看的会话不被打断,而你最先错过的那个正好被带过去。其余的留着绿点,你一个个看。
| 选项 | 行为 |
| 跳到最早错过的(默认) | 见上。多为你仍停在它上面,等于不打断 |
| 跳到最后一个 | 只在最后完成的那条落脚 |
| 都不跳 | 多个完成时不跳,侧栏绿点一个个自己点(单会话时照常跳) |
✅ 能做到:点哪条 Toast 就跳哪条(v0.4.70 起,v0.4.83 稳定)
这一节曾经写的是「做不到」。 2026-10-20 我们做出了可靠实现,旧结论已推翻,下面说明它是怎么做到的、以及怎么开启。
先说限制:DSH 桌面壳层对 dsh://open 做精确匹配 ——
app.on("open-url", (event, url) => {
event.preventDefault();
if (url === "dsh://open" || url === "dsh://open/") focusPrimaryWindow();
});
只有这两个精确字符串会唤窗,带任何参数连窗口都唤不起来。
所以这条限制到今天依然成立:不能靠 DSH 自己的协议传递会话 id。
绕开它的办法:注册我们自己的协议 dshnotify-say://。
你点 Toast → Windows 按 HKCU 注册表调起处理器(wscript + jump-handler.vbs) → 处理器把 session id POST 给插件的本地端点 /pending-jump → DSH 前端读到后调用 uiWorkspace.openSession() → 落到你点的那条通知对应的会话
三个关键点:
| 关注点 | 做法 |
| 不弹黑窗口 | 处理器用 wscript.exe(GUI 子系统,不创建控制台),不是 powershell.exe |
| 端口每次都会变 | 插件把当前端口写进 %LOCALAPPDATA%\dsh-notify-say\port.txt,处理器读它 |
| 只有点了才跳 | 开启此功能后,旧的「切回窗口自动跳」会被关掉,避免抢跳 |
怎么开启:设置页 →「通知」→ 打开 「点击通知跳转到对应会话」。
开启时会注册 Windows 自定义协议(仅 HKCU,不需要管理员,随时可关)。
不开启时:点 Toast 仍然是原来的行为 —— 把 DSH 窗口唤到前台,不跳会话。
不存在「装了就自动具备」这回事,这是一个显式开关。
相关代价(如实说明):
- 首次点击会比平时多几十毫秒(要拉起处理器进程)
- 处理器若被安全软件拦截,会退化成「只唤窗不跳转」,不会更糟
- 开关关闭时会删掉注册表项,不留垃圾
卸载插件后它会怎么清理?
Windows 自定义协议只能在插件被加载时注册/清理,而 DSH 官方不提供「插件被卸载」的独立事件 —— 升级、卸载、热重载走的是同一条移除路径(源码 dsh-plugin-manager 明确写着这一点)。
所以不能在插件退出时删注册表,否则会把热重载后的注册误删。
改用一条只清失效项的规则:插件每次启动时检查旧注册 ——
| 旧注册指向 | 判定 | 行为 |
| 带本插件身份标记(DSHNotifyOwner),文件还在 | 正常 | 不动它(不重写注册表) |
| 带本插件标记,文件已不存在 | 失效 | 清掉(这就是卸载后的尾巴) |
| 没有标记、也没有本插件的处理器文件名 | 别人的 | 绝不动,自己降级成只唤窗 |
判据是「身份标记 + 文件不存在」,热重载/升级/正常运行时它永远为假, 所以不会误删。实测四种情形均符合预期(有测试锁定)。
为什么用「身份标记」而不是「插件目录」
早期版本靠比对插件目录判断归属。这在插件改名/换路径之后会永久坏掉:旧注册的路径与新目录对不上 → 判成「别人的」→ 既不清理也不重建 → 精准跳转悄无声息地退化为「只唤窗」,用户关掉再打开开关也修不好。
现在注册时会在注册表键里写一个 DSHNotifyOwner = dsh-notify-say 标记,目录怎么搬都认得出是自己写的(v0.5.0 起)。
如果你卸载了插件、想立刻清掉注册表(不等下次启动 DSH):
Remove-Item 'HKCU:\Software\Classes\dshnotify-say' -Recurse -Force
只删这一个键,不影响任何其他程序。v0.5.0 之前的旧键 dshnotify 会在插件下次启动时自动清理。
❌ 做不到:知道「你现在正看着哪个会话」
插件拿不到这个信息,四条路都堵着:
| 想用的手段 | 实际情况 |
| uiWorkspace 服务 | 只提供 openSession / startSession / openWorkspace —— 只写不读 |
| 会话切换事件 | 插件能订阅的是 session/event / agent/status / approval/request,没有「切换/激活」 |
| URL 路由 | DSH 不用 URL 承载会话 id |
| 宿主内部状态 | 宿主确实知道(SessionUpdateRouter.activeSessions 记录着哪些会话正被订阅),但那是 client 传输层内部结构,没有对插件开放 |
所以插件无法判断"要跳的就是你正看着的那个"。
上面那条「跳到最早错过的」规则,正是因为缺这个信息才采用的间接办法 ——它靠「跳到同一个会话等于没跳」来达到相近效果。
如果将来 DSH 开放「当前会话」读取,这里可以换成精确判断。
❌ 做不到:把绿点加回去
宿主没有「标记未读」的接口(整个 app.asar 里搜 unread 只命中一个 SVG 图标文件名)。
插件只能观察会话状态,不能写回。
所以「跳走之后把原来那条重新标成未读」做不到 —— 这也是默认规则选「跳到最早错过的」的原因:从源头上避免把你从该看的会话上带走。
✅ 能做到:Toast 告诉你是哪个会话
既然跳不过去,至少让标题说清是谁。Toast 标题用会话标题(DSH 侧栏显示的那个,通常是你第一句话的摘要),例如:
┌────────────────────────────────────┐
│ 优化 Deepseek Harness 配置与插件 │ │ ✅ 任务完成 · 回来看结果吧 │ │ 0Deepseek │ └────────────────────────────────────┘
- 标题来源三级:sessionTitle 服务 → sessionProjections 的 title 投影 → 会话 id 前 12 位。
前两级都拿不到时退回通用文案(不会把会话 id 当标题显示)。
- 第三行是项目名(工作目录最后一段),只在标题里没包含它时才显示,避免单项目用户看到重复信息。
为什么拿不到「会话内容摘要」:插件订阅的 turn/end 事件只带 { turn, reason },没有消息正文;投影缓存里也只有标题、没有正文预览。标题本身已是首句摘要,已经是现有接口下最有信息量的选择。
兼容性与降级
本插件的首要目标是稳定,所以每个依赖外部能力的地方都有降级路径:
| 能力 | 拿不到时怎么办 |
| @deepseek-ai/schemastery(生成设置页) | 不导出 Config → 设置页不出现,插件照常工作 |
| uiWorkspace.openSession(跳会话) | 不跳转,Toast 仍能唤出窗口(退回旧行为) |
| 语音包目录 | 回退系统提示音 |
| 网络(下载语音包时) | 显示失败原因并可重试;已装音色不受影响 |
| $DSH_HOME 未设置 | 不是降级:按官方同款规则回落到 ~/.dsh(见下) |
| 连 ~/.dsh 都取不到(homedir() 失败等极端情况) | 配置走 patch 默认值;不写任何状态文件 |
关于 $DSH_HOME(v0.4.88 修正)
$DSH_HOME 是可选覆盖项,官方 DSH 的解析优先级是显式配置 > $DSH_HOME > ~/.dsh。绝大多数用户机器上并没有设置这个环境变量。
v0.4.87 及更早版本只读 process.env.DSH_HOME,没值即返回空串,于是设置页任何保存都返回 保存失败:no-home(连「声音来源」都切不了)——这是真 bug,不是降级。v0.4.88 起改为与官方同一套规则: $DSH_HOME(空白视为未设置)→ ~/.dsh,并优先尝试直接调用官方 @deepseek-ai/dsh-home-paths 模块(拿不到时才复刻规则)。
注意:如果你的机器显式设置了 $DSH_HOME(本机开发环境就是如此, D:\Program Files (x86)\DSH),配置文件仍落在 $DSH_HOME 下,行为不变。
不会出现「静默失效」:每个降级都有明确行为,并有测试锁定。
已知限制
- 仅 Windows(Toast = WinRT,播放 = MCI,调用 Windows PowerShell 5.1)
- 跳会话依赖未文档化 API(uiWorkspace),官方随时可能改;坏了会自动降级为「只唤起窗口」
- blocked(回合被拦截)只用系统蜂鸣,不播失败语音 —— 语义是「被拦下」而非「失败」
- 失败语音只覆盖部分音色:选到未覆盖的音色时,会在同池内换一个音色播,不会哑掉、也不会播错语义
- 自重载是实验性功能,而且只有「点一次」:设置页「高级设置 → 开发者选项」里有个「重载现在生效」按钮,点一下尝试一次;没有开关、不会自动监听文件。
为什么改成这样:2026-10-18 实测「监听文件自动重载」会把宿主拖崩 —— 一次改 UI 期间触发了 40 多次失败重载,每次销毁实例都会释放宿主仍在用的会话引用,最后渲染进程崩溃。
危险的不是「重载失败」,是「反复重载」
- 自重载不能替代重启:它只能重新导入「同一路径上的」代码。用 dsh plugin update 升级后,新版本装在 .pnpm/<name>@<ver>/ 另一个路径,自重载做不到 —— 必须重启 DSH
- 日语 13 个、中文 4 个音色的许可结论是条件性判断(依据官方通用条款),不代表各权利人出具过专项授权
常见问题
装了没声音? 按顺序查:① 设置页「音源」是不是「不发声」;② 「语音」开关是否关着; ③ 「音量」是否拉到 0;④ 播放依赖 Windows PowerShell 5.1,被安全软件拦了会静音。
设置页「诊断」区可以导出脱敏诊断文件,里面能看出每次播报的决策路径,还会写明 DSH 目录是从哪来的($DSH_HOME 还是 ~/.dsh)以及配置文件当前在不在 —— 「设置改了保存不了」几乎都是这一类问题。
会不会打扰我? 默认同会话 60 秒只响一次;「子代理」默认关(子代理结束不单独提醒)。
觉得吵可以把「同会话去抖」调大,或直接关掉某类事件。
会上传我的会话内容吗? 不会。通知文案是插件内置的固定句子(可自定义),播放也全在本地。
默认日志只记「播了什么、成功与否」,不记台词正文。
会联网的只有两处:下载语音包、以及每 24 小时最多一次的检查更新。二者失败都不影响通知与语音。
提示「未能获取可下载列表」? 那是 GitHub 访问不通。已装的音色照常能用,卸载也能用(卸载不依赖网络)。
网络恢复后点「重试」即可。
为什么只有这么几个音色? 日语方面 VOICEVOX 有几十个角色,但每个的条款都要单独读原文核对。
我们只收录条款明确允许本插件这样分发的,宁可少也不冒险。
被排除的例子:有 2 个明确禁止再分发,1 个身份存疑。
中文音色是另一条线:由 EmotiVoice(Apache-2.0)合成,训练语料 LibriTTS / HiFiTTS 都是 CC BY 4.0。我们只做了 4 个,因为再多就得逐个试听、逐句核对发音 ——中文口语里的多音字、儿化音比日语更容易念错。
有新版本了怎么办?
设置页 → 高级 → 版本与更新 → 点「检查更新」。有新版时那里会显示版本号、 CHANGELOG.md 链接,以及一条升级命令(可一键复制)。
插件不会自己更新自己。 原因很实在:
- 装上你的机器后,插件不知道自己是通过 npm、GitHub 还是 link: 装的
- 正在运行的程序替换自己的文件,失败后很难恢复
- DSH 官方命令行已经能升级插件(dsh plugin --profile <名字> update dsh-notify-say)—— 重复实现没意义
所以分工是:插件负责告诉你有新版;你执行命令;DSH 完成升级。 更新后需要重启对应的 DSH 配置档。
如果你是从源码 link: 安装的(开发模式),插件会认出来并不给命令 ——那种情况请直接更新源码目录,用插件命令会覆盖你的源码。
能加我喜欢的音色吗? 可以提 Issue。但要走审核流程(读官方条款原文、留档),不是加个下载链接的事。
开发
跑测试
npm test # 主套件(纯 Node,不需要 DSH 宿主)
npm run test:all # 全部(含 route 注入的两组)
npm run test:route # 需要 --import loader 的接线测试
npm run test:audio-wire
611 条测试通过(test:all 两段实测:主套件 594 = 567 passed + 27 skipped 需真实宿主; route 注入段 45 = 44 passed + 1 skipped)。
绝大多数是纯逻辑测试,克隆仓库后 npm test 即可跑。
少数用例需要真实的 DSH 宿主(用于验证 Cordis 上下文行为)。
找不到宿主时它们会自动 skip —— 注意 skip 不等于通过。
$env:DSH_CORDIS_ENTRY = "<DSH 安装目录>\.runtime\node_modules\@deepseek-ai\cordis\lib\index.js"
npm test
测试分层
| 层次 | 内容 |
| 单元 | 音源选择、空闲门、配置校验、TTS 文案、http-guard 判定 |
| 集成 | 真实 HTTP 服务器 + 真实 handler,覆盖请求/响应全链路 |
| 行为 | 构造真实输入验证行为(如真实 ZIP 字节、真实目录) |
| 实机 | 真实 Cordis 上下文(需宿主,无则 skip) |
| 发布 | 构建发布包并断言被禁音色/第三方素材未进包 |
为什么有这么多「防复发」测试
这个项目踩过的坑多数是同一类:代码看着对、测试也绿,但两者一起错。所以后来改成:
- 构造真实输入而不是检查源码里有没有某个字符串(典型:ZIP 解压防护查了不存在的字段 entry.size,测试也在断言同一个错字段 → 一起绿)
- 状态更新全量扫描而不是逐个手查(典型:组件状态漏字段已发生 4 次,现在有 C43 静态扫描锁住)
- 变异验证:故意改坏代码,确认测试真的会红
CI
.github/workflows/ci.yml 在每次 push / PR 时跑:
- 单元测试(Node 20 / 22 / 24)
- PowerShell 语法与 UTF-8 BOM 检查
- 关键文件齐全 + npm 打包内容检查(含「被禁音色不得进包」)
- 版本一致性
发版
.\tools\release.ps1 -Bump patch -PublishNpm
流程:升版本 → 跑测试 → 重建发布包 → 在发布包里再跑一次测试 (这一步是必要的:开发目录与发布包的文件集不同,只在开发目录跑会「本地绿、线上红」) → 提交推送 → 可选发 npm。
CHANGELOG.md 由 release-build.ps1 自动生成(从开发提交提取标题,过滤掉内部资料与 hash),随包发布。请勿手工编辑。
许可
代码以 MIT 发布。
语音文件不适用 MIT:日语语音由 VOICEVOX 生成、中文语音由 EmotiVoice 生成,各音色有其自身使用条款,详见 THIRD-PARTY-NOTICES.md。
每个音色包内另有该音色的 TERMS.md。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
unifilitence/dsh-notify-say
给 DeepSeek Harness Desktop 用的通知插件:任务跑完会说话。
相关插件 · 桌面端
Codex 任务面板chuspeeism/dashi-taskboard
现代化可灵活嵌入的任务面板,支持 Codex、DeepSeek Harness
★ 3,409已汉化DSH
71.7
多智能体自进化开发平台Prism-Shadow/penguin-harness
统一、稳定的 Agent 自进化框架全自动优化:任意模型 · 任意算法 · 任意 Benchmark
★ 3,111已汉化DSH
71.1
桌面宠物PC2005-cloud/dsh-pet
DSH 桌面宠物:一行命令装好即用的透明动画小桌宠,支持多开、大小位置随心配置;还内置 DIY 素材链,能用 AI…
★ 5,341已汉化DSH
71
多端 Agent 工作台GCWing/OpenBitFun
OpenBitFun 是一个高度可定制的开源 Agent 工作台,让任务按你的方式持续推进。
★ 3,449已汉化DSH
70.8
DSH Desktopanywhere-labs/dsh-desktop
为 DeepSeek Harness DSH 插件生态打造的现代化桌面端解决方案。万物皆「插件」,桌面本身也是「插件」。
★ 34,532已汉化DSH
70.7
桌面宠物伴侣OpenPetsHQ/openpets
桌面伴侣平台,带宠物、插件,还能选配本地智能体集成。
★ 1,353已汉化DSH
70.2