Skip to content
dsh.fish
Bundle

dsh-turn-chime

DSH Web plugin: play a chime when the agent finishes a turn, with a settings page to enable it, pick the volume and upload your own audio file. Host-stored configuration, no core changes.

Source
AGImentu
License
MIT
Updated
Updated 10 hours ago

Readme

# dsh-turn-chime

**任务做完了,喊你一声。**

一个 DSH(DeepSeek Harness)Web 插件:每次智能体把一个任务做完,浏览器里播一段提示音——
你可以随时关掉它、调音量,也可以上传自己的 MP3 当提示音。默认自带一段两音铃声。

不读凭据、不访问外网、不改 DSH 内核,只用官方公开的两个插槽。

---

## ✨ 它怎么知道"任务做完了"

这一点决定了插件非常小:DSH 的 `conversation.chat.assistant-actions` 插槽有个天然特性——
**它只在回合收尾之后才渲染**(回合进行中,官方根本不渲染那一行)。

所以本插件注册一个**渲染 `null` 的隐形条目**,「我被挂载」就等于「任务做完了」。

为了不吵到你,又加了三道保险:

| 保险 | 作用 |
|---|---|
| **新鲜度窗口(60 秒)** | 只有"刚结束"的回合才响。你翻历史、切会话时挂载的旧回合不会响 |
| **页面稳定窗口(3 秒)** | 刚打开页面时渲染出来的最后一个回合不算"完成",不响 |
| **每回合只响一次** | 同一回合无论重渲染多少次,最多响一次(带 200 条上限的记忆) |

---

## 📦 安装

**前置**:DSH 已能正常运行(`dsh web` 起得来);Node.js ≥ 20,pnpm ≥ 10。

**支持的 DSH 版本**:在 **DSH `0.1.5-rc.2`** 上真机验证(只用公开插槽与平台种子模块,不 import 内部包)。

### 方式一:本机 tarball 安装(推荐,现在就能用)

```sh
git clone https://github.com/AGImentu/dsh-turn-chime && cd dsh-turn-chime
pnpm install && pnpm build && pnpm pack          # 产出 dsh-turn-chime-<版本>.tgz
dsh plugin --profile web add ./dsh-turn-chime-0.1.0.tgz   # 文件名按上一步输出替换
```

### 方式二:源码 `link:` 安装(改代码即时生效)

```sh
git clone https://github.com/AGImentu/dsh-turn-chime && cd dsh-turn-chime
pnpm install && pnpm build
dsh plugin --profile web add "link:$PWD"        # Windows PowerShell: "link:$($PWD.Path)"
```

或用仓库里的一键脚本(等价的两步,并在 CLI 不在 PATH 时兜底):

```sh
node scripts/install-local.mjs --profile web
```

### 方式三:从 npm 安装(包发布后可用)

```sh
dsh plugin --profile web add dsh-turn-chime@latest
```

### 方式四:交给 DSH 自己装

把下面这段原样发给任意一个 DSH 会话:

```text
帮我装 dsh-turn-chime 插件(DSH 任务完成提示音),步骤:
1. git clone https://github.com/AGImentu/dsh-turn-chime 到 ~/Code/dsh-turn-chime
2. 在该目录执行 pnpm install && pnpm build && pnpm pack(记下产出的 .tgz 文件名)
3. 执行 dsh plugin --profile web add ./<上一步的 .tgz 文件名>
4. 完成后提醒我:重启 dsh web,然后硬刷新浏览器(Ctrl/Cmd+Shift+R)
遇到报错先查 https://github.com/AGImentu/dsh-turn-chime 的 README「常见问题」表。
```

### 装完必须做的一步

**重启 `dsh web`,然后硬刷新浏览器(Ctrl/Cmd+Shift+R)。**

规则:**`lib/client.js`(浏览器半边)改动,硬刷新即可;`lib/index.js`(宿主半边)改动必须重启。**
本插件的设置与音频存在宿主半边,所以第一次安装请重启一次。

<details>
<summary><b>更新</b></summary>

```sh
cd ~/Code/dsh-turn-chime && git pull && pnpm install && pnpm build && pnpm pack
dsh plugin --profile web add ./<上一步产出的 .tgz>     # tarball 方式
# 或 link: 方式:重新 pnpm run build 即可,profile 里已经是符号链接
```

</details>

<details>
<summary><b>卸载</b></summary>

```sh
dsh plugin --profile web remove dsh-turn-chime
```

`remove` 会按已安装状态对账 `dsh.profile.bundles`,自动摘掉本包;之后重启 `dsh web`。
已上传的音频与设置仍留在 `~/.dsh/turn-chime/`,想彻底清掉就删掉这个目录。

</details>

<details>
<summary><b>常见问题</b></summary>

| 现象 | 原因与解决 |
|---|---|
| 设置里点「试听」提示被浏览器拦截 | 浏览器要求页面**先有一次点击或按键**才允许出声。在页面上随便点一下再试听即可(插件会自动解锁,之后任务完成时就能正常响) |
| 任务做完了但没响 | ① 插件开关是关的;② 你切到了别的会话(提示音只对**当前打开的会话**生效);③ 页面刚打开不到 3 秒;④ 浏览器还没被解锁(见上一条) |
| 上传音频报"只支持 mp3 / wav / ogg..." | 该文件既没有受支持的扩展名,浏览器也没报出音频 MIME 类型。换成 mp3/wav/ogg/m4a 再试 |
| 上传报"内容超过上限" | 上限 **8 MB**。长音频请先剪辑或用码率更低的格式 |
| 设置页显示「读取设置失败」 | 宿主半边没加载:必须重启 `dsh web`(只刷新页面不够) |
| 页面出现两个「提示音」页 | 双挂载:profile 的 `cordis.patch.yml` 里还留着手写挂载行,删掉那段 |
| 换浏览器后设置没了 | 设置与音频存在 `~/.dsh/turn-chime/`(本机),所以换浏览器**应该**还在;若不在,检查 DSH_HOME 是否变了 |
| 提示 `dsh: command not found` | 用 `npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-turn-chime@latest`,或走方式一/二 |

</details>

---

## 🔊 设置页(设置 → 提示音)

| 控件 | 说明 |
|---|---|
| **开启提示音** | 关掉后不再播放任何声音(设置保留) |
| **音量** | 0–100%,试听与正式播放共用 |
| **试听** | 立刻按当前音量播一次,用来确认效果 |
| **上传音频** | 选一个本地音频文件,**上传即启用**;上限 8 MB |
| **恢复内置** | 回到自带的两音铃声 |

补充说明(页面底部也写着):

- 提示音只对**当前打开的会话**生效;后台任务、其它会话的任务不会响(要跨会话提醒需要监听全局会话列表,属于后续可做项)。
- 音频与设置在 `~/.dsh/turn-chime/`(`config.json` + `sound.<ext>`),所以**换浏览器、清缓存都还在**,且只在本机。
- 任务失败/中断也能响,但**目前不区分**成功与失败的声音(可作为后续项)。

---

## 🎵 内置提示音

`assets/default-chime.wav` 是**用脚本合成**的(仓库里不带任何第三方音频):

```sh
pnpm run make-chime     # → assets/default-chime.wav
```

一段 0.9 秒的两音铃声(E6 → A6,上行四度),每个音由基频 + 两个非谐波分音叠加、
指数衰减包络,归一化后留 15% 余量——像铃铛而不像"滴"声。合成参数见
[`scripts/make-default-chime.mjs`](scripts/make-default-chime.mjs),想换音色改那里的
`NOTES` 即可重新生成。

---

## 🛠️ 开发

```sh
pnpm install
pnpm run typecheck   # tsc --noEmit
pnpm test            # 26 项单测:触发时机(三道保险/去重/上限)、设置校验、磁盘状态
pnpm run build       # lib/index.js(host 半边:路由 + 存储) + lib/client.js(浏览器半边)
pnpm run smoke       # 产物契约冒烟:注册 id / 插件形状 / 两个插槽 / 抛错隔离 / 渲染为空 / 取设置
pnpm run verify      # typecheck + test + build + smoke
pnpm run watch       # 开发时增量重建(配合 dsh 的 client HMR;host 改动仍需重启)
```

### 目录结构

```
src/
  shared.ts              两半边共享的 wire 类型与上限(设置形状 / 支持格式 / 8 MB)
  routes.ts              路由常量(避免两半边字符串漂移)
  index.ts               host 半边:注册 /turn-chime/* 路由,并作为一行 live Loader row
  host/
    config.ts            设置校验(纯函数):补默认值、夹取音量、清洗文件名
    store.ts             磁盘状态:$DSH_HOME/turn-chime/config.json + sound.<ext>(原子写)
    routes.ts            四个端点:读/写设置、上传/下发/删除音频(边读边限流)
    contract.ts          host 侧契约镜像(webServer 的 req/res)
  client/
    index.tsx            浏览器半边:注入样式、注册字典、解锁音频、注册两个插槽条目
    Trigger.tsx          隐形触发器(渲染 null):挂载即"任务完成"
    decide.ts            触发时机(纯函数 + 去重日志):三道保险都在这里
    play.ts              播放与"首次手势解锁"(浏览器自动播放策略)
    config-store.ts      设置缓存(30 秒 TTL、并发合并)+ 上传/恢复
    SettingsSection.tsx  设置页:开关 / 音量 / 试听 / 上传 / 恢复
    select.ts            从聊天快照取"本回合结束时刻"的选择器(必须返回存储引用)
    locales.ts           中英字典 + 内置中文兜底
    styles.ts            插件自有样式(仅消费 DSH 设计令牌)
    Boundary.tsx         错误边界:渲染失败只影响自己那一块
    contract.ts          本插件读取的 DSH 浏览器契约的镜像类型
assets/default-chime.wav 内置提示音(脚本合成)
cordis.patch.yml         bundle patch:把本包挂成 profile 的一层
tsdown.config.ts         产出 host 半边(ESM)与浏览器半边(CJS 闭包工厂)
scripts/
  make-default-chime.mjs 合成内置提示音
  install-local.mjs      本地 link 安装 + bundles 对账
  smoke-client-bundle.mjs 产物契约冒烟(无浏览器)
```

---

## 🧩 为什么这么设计

| 取舍 | 理由 |
|---|---|
| 用**插槽挂载**判断"任务完成",不监听事件 | 官方插槽的渲染时机本身就是"回合已收尾"(回合进行中 `closing === null`),这是公开且稳定的信号;去猜事件流反而脆弱 |
| 三道保险(新鲜度 / 页面稳定 / 去重) | 每一条都对应一种"会很烦"的场景:翻历史响、开页面响、一次任务响好几遍 |
| 音频与设置放**宿主**而不是浏览器 | "上传一次,换浏览器也还在"是用户对声音偏好的预期;浏览器侧还要处理 IndexedDB 与容量限制 |
| 上传**边读边限流**而不是先收完再判断 | 8 MB 上限是保护进程的,必须在流里生效;一个同源页面不该能用超大请求把宿主打爆 |
| 只消费 DSH 设计令牌画界面 | 设置列是官方界面的一部分,插件是客人:跟随主题与字号,不引入自己的色板与字体 |
| 条目包在**错误边界**里 | 触发器就渲染在官方复制/分支/用量那一行里;插件抛错必须只让自己消失 |
| 上传格式**白名单**,并拒绝未知 | 服务端要给出正确的 `content-type`,白名单是唯一能保证这件事的方式;不做魔数嗅探,是因为代价大于收益(播放失败会明确回报) |

---

## ⚠️ 已知边界

- **只对当前打开的会话生效**:后台/其它会话的任务完成不会响。
- **需要一次页面交互**解锁音频(浏览器策略),之后自动生效。
- **只在浏览器页面里响**:DSH 窗口最小化时仍会响;若浏览器把标签页静音(整站静音),则不响。
- **不区分成功与失败**:任务失败/中断也会响(后续可加第二种声音)。
- 上传上限 8 MB,格式白名单见上;超大音频请先压缩。

## 📄 License

[MIT](LICENSE)

Install

dsh plugin --profile web add github:AGImentu/dsh-turn-chime

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source