任务完成提示音(Task Completion Sound)
一个 DeepSeek Harness 组合包(bundle)插件:任务结束时播放一声提示音,让你在别的窗口忙别的事时也能注意到任务已经完成。
icon
- 五种合成音色,可在「设置 → 通用」里逐个试听并切换
- 配置走官方
Config:提示音开关、默认音色、是否包含子代理、最小间隔、总音量都是受校验的配置字段,写入 profile 的设置文档(不再是浏览器本地存储)
- 零构建、无安装脚本:唯一的运行时依赖是与宿主同版本的
@deepseek-ai/schemastery(配置 schema),不需要任何构建步骤或构建授权
- 不打包任何音频文件:所有音效都由 Web Audio 实时合成
- 自动过滤子代理:默认只有顶层任务真正结束才响,子代理先结束不会误报(可用配置改为包含)
- 无界面侵入:监视器不渲染任何 DOM;只有浏览器拦截音频时才出现一次性解锁提示
目录
安装
本包是一个标准组合包:package.json 声明 dsh.bundle.patch,指向 cordis.patch.yml,该 patch 按包名插入一行插件。
方式一:本地目录(开发/自用)
先在插件目录里装好它自己的依赖(link: 安装不会替它装):
cd /绝对路径/dsh-task-complete-sound
pnpm install
再把它装进 profile:
dsh plugin --profile desktop add /绝对路径/dsh-task-complete-sound
dsh plugin 会在 profile 目录里转发给 pnpm,因此依赖与组合包列表会自动更新:
{
"dependencies": { "dsh-task-complete-sound": "link:/绝对路径/dsh-task-complete-sound" },
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-task-complete-sound"] } }
}
方式二:直接从 GitHub 安装
本包没有构建脚本,也没有 prepare 脚本(交付的是可直接运行的纯 JS,不经过 TypeScript 构建),因此 git 安装不需要任何构建授权:
dsh plugin --profile desktop add github:Chatbot-zhou/dsh-task-complete-sound
官方文档提到 git 安装会因 prepare 脚本而要求用户配置 allowBuilds 授权。本包不存在这一步——这也是本包选择「源码即可运行」而不是「发布构建产物」的原因。
方式三:tarball 或注册表
pnpm pack # 产出 dsh-task-complete-sound-2.0.0.tgz
dsh plugin --profile desktop add ./dsh-task-complete-sound-2.0.0.tgz
dsh plugin --profile desktop add dsh-task-complete-sound # 已发布到 npm 时
桌面端(DeepSeek Harness 应用)
桌面端没有终端时:
- 在对话里让 Harness 安装该包(内部使用插件管理器的
install_bundle,等价于上面的 add);
- 之后在 设置 → 插件 里可以看到该组合包的卡片、开关与卸载按钮。
生效范围与卸载
- 安装进 profile 后,该 profile 下所有会话都生效,并且跨重启保持。
- 卸载:
dsh plugin --profile desktop remove <包名>,或在 设置 → 插件 里卸载。
- 只想临时静音:在 设置 → 插件 里关掉这条组合包即可,不必卸载。
本包没有 scripts(因此安装时不需要任何构建授权),唯一的运行时依赖是 @deepseek-ai/schemastery,声明在 dependencies 里由包管理器正常安装。
仓库与发布
发布到 npm(可选)
pnpm publish # 无作用域包不需要 --access public
如果该名字在 npm 上已被占用,可改用你自己的作用域(例如 @chatbot-zhou/dsh-task-complete-sound),并同步修改 cordis.patch.yml 里插件行的 name: —— 包名与 patch 行必须始终一致,否则那一行解析不到模块。
打包前自检
pnpm pack # 应为 10 个文件
package.json cordis.patch.yml index.js client.js icon.svg
locale/en.json locale/zh.json README.md CHANGELOG.md LICENSE
node_modules/、pnpm-lock.yaml、.gitignore 都不会被打进去。
五种音色
| 按钮 | id | 音色性格 | 合成配方概要 |
|---|
| 清脆 | chime | 默认。明亮的两声上行 | 880 Hz → 1318.5 Hz |
| 木鱼 | wood | 干燥的木质敲击,不刺耳 | 260 Hz 腔体 + 520 Hz 主体 + 1040 Hz 只留 45 ms 当"敲击沿" |
| 柔和 | soft | 温暖的低音和弦,最不打扰 | C4 261.63 + G4 392,C5 523.25 延迟 30 ms 且只给 0.10 音量,衰减 900 ms |
| 厚重 | deep | 低沉厚实,有"份量" | G3 196 正弦 + 同音三角波增密 + D4 293.66 五度 + G4 392 八度,22–34 ms 慢起音 |
| 水滴 | drop | 短促下滑 | 1300 Hz → 900 Hz |
选择存放在 profile 的设置文档里(见插件配置),因此清浏览器数据也不会丢。如果存的值对应的音色已经被删除(例如旧版本的 bell/pluck),会自动回落到 chime,不会出现"没有选中项"的状态。
旧版本把选择存在 localStorage 的 dsh.task-complete-sound.preset。升级后该值仍会被读取作为回退,并会在配置仍是默认值时自动迁移一次,所以不会被打回默认音色。另外桌面窗口(dsh-app://)与浏览器页(http://127.0.0.1:19387)是两个不同的源,旧版路径下的 localStorage 互不相通。
修改音效
所有音效都定义在 client.js 顶部的 PRESETS 数组里,没有任何音频文件。改完保存即生效(见文末"修改后如何生效")。
音调参数完整参考
每个音色是一个对象 { id, tones },其中 tones 是若干"音调"的数组。每个音调是定长数组,按位置取值:
[频率, 起始偏移, 时长, 峰值, 波形, 终止频率, 起音]
| 位置 | 名称 | 单位 | 说明 |
|---|
| 1 | frequency | Hz | 起始频率。人耳可听范围约 20–20000;低音区 80–300 更"沉",中音区 400–800 更"实",1200 Hz 以上偏"尖" |
| 2 | offsetMs | ms | 相对音色起点的延迟。0 = 与其它音同时开始;给它 100–300 就得到"两声"或琶音效果 |
| 3 | durationMs | ms | 该音从起音到完全衰减为 0 的总时长。越长越有余韵 |
| 4 | peak | 0–1 | 峰值音量。同一音色里各音调的峰值之和建议 ≤ 0.9,否则会在输出端削顶(听起来发糊) |
| 5 | type | — | 波形:'sine'(最纯净)、'triangle'(圆润,多奇次谐波)、'square'(洪亮但偏硬)、'sawtooth'(最亮最硬) |
| 6 | endFrequency | Hz | 可选,0 = 不滑音。给一个 > 0 的值,音高会从起始频率指数滑到该值(下滑如 [440, 0, 260, 0.45, 'triangle', 392]) |
| 7 | attackMs | ms | 可选,起音时间,省略时默认 5 ms。调大它 = 把"咔哒"换成"份量",是做出"厚重"感最有效的一个参数;内部会自动限制在该音时长的 1/2 以内,所以填超大值也不会出错 |
还有一组音色级常量:
| 名称 | 位置 | 说明 |
|---|
id | 每个 PRESETS 条目 | 音色标识。决定 localStorage 里存的值,也决定文案键名 preset.<id> |
DEFAULT_PRESET | client.js 顶部 | 默认音色(当前 'chime'),也是存储值失效时的回落目标 |
RAMP_SECONDS | client.js 顶部 | 默认起音时长,0.005 秒;被音调第 7 位的 attackMs 覆盖 |
三步新增一个音色
第 1 步:在 PRESETS 里加一个条目(放在你希望它出现的顺序位置;设置行按钮的排列顺序 = 数组顺序)。
{
id: 'ping',
tones: [
[1046.5, 0, 150, 0.4, 'sine', 0, 8],
[1568, 90, 260, 0.3, 'sine', 0, 8],
],
},
第 2 步:在 zh 与 en 两个字典里各加一条同名键。两个字典的键必须完全一致,否则切换语言时该按钮会显示原始键名。
'preset.ping': '叮咚', // zh 字典
'preset.ping': 'Ping', // en 字典
第 3 步:刷新页面。设置行会多出一个「叮咚」按钮,点一下即可试听。
提醒:id 一旦发布过就不要再改,因为用户 localStorage 里存的是它——改 id 等于让老用户的选择失效(会静默回落到 chime)。
三种常见调整配方
想更沉、更厚重
- 把主频降到 150–260 Hz;
- 叠加一个同频率的
'triangle' 波形(增加谐波密度,比单纯加大音量更"厚");
- 补一个五度(×1.5)或八度(×2)在上面,音量给主频的 1/3–1/2,保留清晰度;
- 把第 7 位
attackMs 调到 20–35,去掉"啪"的那一下;
- 峰值总和控制在 0.9 以内。
想更亮、更醒神
- 主频提到 900–1400 Hz;
- 换成
'triangle' 或 'square';
- 起音保持默认 5 ms 甚至更短 —— 亮感就来自快起音;
- 想做成"叮-咚"两声,用第 2 位
offsetMs 让第二个音延后 120–200 ms 并适当降调。
想更长、更有余韵
- 加大第 3 位
durationMs(800–1200 ms 就很绵长);
- 同时把峰值调低一点(例如 0.3),否则长音听起来会吵;
- 高音层给更短的时长、低音层给更长的时长,就得到"高音先消失、低音在回荡"的自然衰减。
修改后如何生效
编辑 client.js 并保存后:
- 刷新当前页面(桌面窗口
Ctrl+R,浏览器 F5)。客户端 bundle 通常会被模块系统自动换新,刷新是确保拿到最新代码的可靠手段;
- 如果刷新后仍是旧音效,重启一次应用(profile 重新加载时会重新读取包);
- 改的是
package.json(例如包名、exports、icon)时,必须重启应用才能重新解析包清单。
改坏了怎么排查:打开开发者工具(F12)看 Console,插槽渲染失败会明确打印 slot entry crashed in 'settings.general.item' 并附上错误原文——本插件的设置行一旦渲染抛错就会被静默清空,控制台是唯一的线索。
插件配置(Config)
Host 半边按官方要求导出一个 Schemastery Config:所有部署可调的参数都在这里,默认值直接写在 schema 上。
| 字段 | 类型 | 默认值 | 作用 |
|---|
enabled | boolean | true | 完成提示音总开关 |
preset | 'chime' | 'wood' | 'soft' | 'deep' | 'drop' | 'chime' | 默认音色,也是存储值失效时的回落目标 |
includeSubagents | boolean | false | 子代理会话结束时是否也提示 |
minGapMs | number(0–10000,步长 1) | 500 | 两次完成提示之间的最小间隔 |
volume | number(0–1,步长 0.05) | 1 | 总音量系数,叠乘在每个音色自身峰值之上 |
全部字段都标了 .volatile()(运行时可改的偏好):Host 侧用 config.<字段>.get() 读取,浏览器半边通过设置镜像 ctx.configForms 读同一份值,并把你选的音色写回去。因此选择写在 profile 的设置文档里,不再依赖浏览器存储。
在 profile 里覆盖
直接改 profile 的 cordis.patch.yml:
- id: task-complete-sound
name: 'dsh-task-complete-sound'
config:
preset: deep
volume: 0.6
includeSubagents: false
改完保存即可 —— 配置变更会触发热替换,不需要重启。
⚠️ patch 会整体替换该行的 config,不是逐键深合并。因此一旦你手写了 config,它就是那一行的完整配置:没写的字段回到 schema 默认值,而不会保留用户在设置里的选择。要么把字段写全,要么只用设置界面。
设置行与镜像的关系
设置行只在 Host 正在服务这个命名空间时注册(ctx.configForms.whileServed([NS], …))。这是官方约定:没有组合出设置文档的部署看不到这一行,也就不会出现一个按不动的开关;同时提示音仍按 schema 默认值正常工作。
内部常量与插槽
| 常量 | 默认值 | 作用 |
|---|
NS / LOCALE_NS | 'task-complete-sound' | 行 id、设置命名空间、语言命名空间三者同名 |
PRESET_STORAGE_KEY | 'dsh.task-complete-sound.preset' | 旧版浏览器存储键,现在只作为回退读取并镜像写入 |
RAMP_SECONDS | 0.005 | 默认起音时长;被音调第 7 位 attackMs 覆盖 |
四条插槽注册(改 order 可调整位置):
| 插槽 | id | order | 作用 |
|---|
shell.overlay | task-complete-sound | 100 | 全窗口监视器,不渲染 DOM |
shell.overlay | task-complete-sound-unlock | 101 | 音频解锁提示(仅在浏览器拦截音频时出现) |
conversation.composer.dock | task-complete-sound | 5 | 会话级监视器,不渲染 DOM |
settings.general.item | task-complete-sound | 21 | 设置行的音色选择按钮组(仅命名空间被服务时注册) |
关于 @deepseek-ai/schemastery 依赖
-
官方要求 Config 必须实现 Standard Schema 接口,不能导出普通对象,因此必须引入 Schemastery。
-
它是构建产物里的副本,对 profile 安装的插件不可见(宿主只从 app.asar 内部解析自己的包),所以本插件把它声明为 dependencies 并精确锁定 3.18.4 —— 与当前安装附带的版本一致,避免两份实现漂移。
-
开发时(把插件目录 link: 进 profile)需要先在插件目录里装一次:
pnpm install
从 npm 安装的正式包则由 pnpm 自动装好,使用者无需任何额外操作。
工作原理
package.json dsh.bundle.patch → cordis.patch.yml(按包名插入一行插件)
dsh.client → 浏览器半边;exports 暴露 client/locale/package.json
index.js Host 半边:导出 Schemastery `Config`(全部偏好字段),
apply 为空——它只为占一条 Loader 行,让客户端模块系统
把浏览器半边送出去,并持有受校验的配置
client.js 浏览器半边:监视器 ×2 + 解锁提示 + 设置行 + 合成器
locale/en.json 插件展示名与描述(英文)
locale/zh.json 同上(中文)
icon.svg 插件卡片图标(自包含 SVG,渲染为图片而非内联 HTML)
判定规则
任务"完成"=顶层会话的运行标记从 true 变为 false,这对应 Host 的 agent/status → idle:整轮任务(含所有工具调用与排队输入)真正结束。
- 全窗口监视器:从
shell.overlay 拿 useSessions,把所有非子代理会话压成一个 "<会话id>:<0|1>" 字符串做比较——字符串能被默认的选择器相等性判断命中,因此不会因为每次生成新数组/对象而反复重渲染。
- 会话级监视器:从
conversation.composer.dock 拿当前会话的 useSession,独立判断同一件事。
- 两条通路共用同一个 500 ms 节流,所以同时命中只会响一声,而任意一条可用就足够——这是刻意的冗余。
- 转换判定:只有当某个会话 id 在前后两次快照里都存在、且由
true 变 false 时才算完成。因此"新出现的会话""被归档/删除的会话""页面刚加载的第一次快照"都不会响。
- 子代理被跳过:
session.origin === 'subagent' 的会话不参与判定,避免子代理结束、主任务还在跑时误报。
- 基线放在模块作用域:客户端条目可能被销毁重建(模块热替换、profile 重载),基线若放在组件内的
useRef 里就会随重建丢失,从而静默吞掉那次完成。
音频解锁
Chromium 规定页面在首次用户交互前不允许出声。所以插件会在激活时探测一次音频上下文状态,并在此后每次鼠标按下/按键时重试;如果探测到仍被拦截,就在左下角显示一次性提示「提示音需要一次点击才能播放 · 点此试听」。Electron 桌面窗口通常不需要手势,因此很少看到它。
音量与削顶
每个音调独立走 振荡器 → 增益包络 → 输出,包络是"5 ms(或指定起音)线性升到峰值 → 线性降到 0"。同一音色里各音调的峰值之和是本插件刻意控制在 0.9 以下的上限——因为叠加的多个振荡器初相位一致,峰值会直接相加,超过 1.0 就会被输出端削平。
显示名与图标
插件卡片上的名称与描述来自 locale/en.json 与 locale/zh.json 的 meta 字段:
{
"meta": {
"title": "任务完成提示音",
"description": "任务结束时播放提示音,五种合成音色可选,可在设置中逐个试听。"
}
}
en.json 是发现入口,必须存在;
- 字段缺失时逐级回落:标题 →
package.json 的 name → 完整插件名;描述 → package.json 的 description → 无描述;
package.json 里需要导出这两个资源,并把图标放进 files:
{
"icon": "./icon.svg",
"exports": {
"./package.json": "./package.json",
"./locale/*.json": "./locale/*.json"
},
"files": ["locale/*.json", "icon.svg"]
}
icon 路径相对于声明它的清单文件;支持 SVG/PNG/JPEG/WebP,上限 256 KiB;绝对路径、URL、目录外路径、指向目录外的符号链接都会被拒绝;SVG 以图片方式渲染(不是内联 HTML),所以不能依赖外部 CSS 或脚本。
已知限制与延期工作
- 增删
Config 字段需要重启一次。.volatile() 字段运行时可改、改完热替换即生效;但改动 schema 本身(新增/删除字段)需要重启应用,让 Host 重新加载模块 —— 浏览器半边会自行换新。
- 必须有一个打开的页面。提示音由页面发声,因此应用完全退出、或页面关闭时不会有任何系统级通知。
- 首次交互前可能被浏览器拦截。这是 Chromium 的自动播放策略,不是本插件的缺陷;此时会显示一次性解锁提示。
- 音效是实时合成的,不是采样。因此音色偏"电子",做不出真实乐器或录音的质感;好处是零资产、零体积、任意调参。
- 没有"仅本会话静音"。静音粒度是:整包开关(设置 → 插件)、
enabled 配置字段,其余是 5 个音色本身。没有按会话、按时段或按任务类型的静音。
模型体验
无。本包是纯浏览器侧的界面与提示插件,不注册任何面向模型的内容,也不参与提示词组装。
KV 缓存影响
无;本包既不组装也不发送提供方请求。
故障排查
| 现象 | 原因与处理 |
|---|
| 设置 → 通用里没有「任务完成提示音」这一行 | ① 先确认组合包已启用(设置 → 插件);② 客户端 bundle 没换新,刷新页面后重启应用;③ 该行渲染时抛错会被静默清空——按 F12 看 Console 里的 slot entry crashed in 'settings.general.item' |
| 任务结束了但没响 | ① 确认页面处于打开状态;② 按 F12 看 Console 有无音频相关告警;③ 确认系统音量与 Windows 音量合成器里本应用的音量;④ 该会话是否被判定为子代理(子代理不响) |
| 第一次不响,之后正常 | 浏览器自动播放限制,点一下左下角的解锁提示即可;Electron 桌面窗口一般不会遇到 |
| 声音偏轻 / 听不清 | 加大 PRESETS 里的第 4 位峰值(注意总和 ≤ 0.9),或改用 'triangle'/'square' 波形 |
| 声音发糊、像破音 | 同一音色的峰值之和超过了 1.0,调低各音峰值 |
| 选了某个音色后重启又变回"清脆" | 该 id 对应的音色已被删除(例如旧版本的 bell/pluck),插件回落到 Config.preset 的默认值——这是预期行为 |
patch 里写了 config 之后,设置界面里的选择被重置了 | patch 的 config 是整体替换而非逐键合并,没写的字段会回到 schema 默认值;把字段写全,或只用设置界面 |
目录结构
dsh-task-complete-sound/
├── package.json 组合包清单:dsh.bundle.patch / dsh.client / exports / files / icon / dependencies
├── cordis.patch.yml 配置层:按包名插入一行插件
├── index.js Host 半边:导出 Schemastery `Config`,空 `apply`
├── client.js 浏览器半边:预设、合成器、监视器、设置行、解锁提示
├── locale/
│ ├── en.json 展示元数据(发现入口)
│ └── zh.json 展示元数据(中文)
├── icon.svg 插件卡片图标
├── README.md 本文档
├── CHANGELOG.md 版本历史
├── LICENSE MIT
├── .gitignore 忽略 node_modules
└── node_modules/ 本地开发用(pnpm install 产出,不发布)
node_modules/ 只在「把插件目录 link 进 profile」这种开发方式下需要 pnpm install;从 npm 安装的正式包由 pnpm 自动处理。
许可证
MIT