<h1 align="center">DSH Notify</h1>

<p align="center">为 DeepSeek Harness 提供页内任务通知：清晰状态色、可折叠通知堆叠、提示音与后台标签页提醒。只做实时投递，不保存通知历史。</p>

<p align="center">
  <a href="https://github.com/idoall/dsh-notify/actions/workflows/ci.yml"><img src="https://github.com/idoall/dsh-notify/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://www.npmjs.com/package/@idoall/dsh-notify"><img src="https://img.shields.io/npm/v/@idoall/dsh-notify?label=npm&color=CB3837" alt="npm 版本"></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0F172A" alt="MIT"></a>
  <img src="https://img.shields.io/badge/DSH-0.2.2--rc.1-4B6BFB" alt="DSH 0.2.2-rc.1">
</p>

<p align="center"><a href="README.md">English</a> | 中文</p>

<p align="center">
  <a href="#能做什么">能做什么</a> ·
  <a href="#快速开始">快速开始</a> ·
  <a href="#设置">设置</a> ·
  <a href="#兼容性">兼容性</a> ·
  <a href="#卸载">卸载</a> ·
  <a href="CHANGELOG.md">更新记录</a>
</p>

> DSH Notify 是 DeepSeek Harness 社区插件，只注册设置区块与页内浮层，不修改 DSH 源码。

会话真正停下、运行失败、向你提问或请求审批时，当前页面右上角会出现通知卡片。绿色**任务完成**卡以 DSH 原生 `agent/status: idle` 为准——这与左侧会话树转圈停止是同一个状态——而不是以右侧文字看起来已经结束的时刻推测。goal 自动续跑和排队的下一轮保持安静。一切发生在你已打开的页面内：没有 Service Worker、Web Push、宿主机系统通知或通知历史。

<p align="center">
  <img src="./assets/toast.png" width="70%" alt="增强版通知堆叠：最新的失败通知完整显示，较早的蓝色、橙色和绿色通知等距露出下边缘">
</p>

## 能做什么

- **增强版或柔和版通知样式**：默认的**增强版**使用清晰状态色与四秒短暂提醒条；提醒条只用于吸引注意，不负责关闭通知。**柔和版**提供更克制的视觉效果。卡片会保留到你关闭、打开对应会话或完成卡片内回答为止。
- **可折叠的页内通知栈**：开启默认的**多条通知折叠显示**后，从第 2 条起会折叠：最新卡片完整可读，早期卡片仍保留完整内容并在下方等距露出 `18px` 边缘。鼠标移入或键盘焦点进入时会展开。有两条及以上时，堆叠上方会出现**全部清除**，一次收走本页所有卡片；清除始终只发生在本页，宿主里等待回答或审批的交互仍留在会话中。本页最多保存 50 条活跃卡片；待处理通知优先，其余按最新优先。
- **可追溯的会话跳转**：点通知会打开对应会话并定位到来源轮次。宿主选中会话后，左侧会话树会自动平滑滚动并展示已选行，即使它位于另一个屏幕外工作区。跳转失败时卡片会留在原地说明原因。
- **在卡片内回答**：待回复问题或审批会显示可兼容的输入框选项；执行审批与计划待审沿用 DSH 的橙色决策语义，普通需要回复保持蓝色；卡片内回答与输入框操作同一份宿主交互。
- **提示音与后台提醒**：通知成功进入已启用的可视 Toast 队列后，才会播放内置 WebAudio 或经过校验的自定义提示音；页面在后台时也适用。同一轮轮询的一批通知只播放一次。后台标签有未查看通知时，标题和 favicon 会短暂提示。
- **完成以原生 idle 为准，并且每项任务只通知一次**：绿色完成卡等待 DSH 报告所属 Agent 为 `idle` 才发送，和左侧会话树转圈停止一致；不再依据已渲染的回答或计时器猜测。计划待审和执行审批是控制门：它们短暂进入 idle 不会额外生成完成卡，批准后的后续工作轮才产生唯一一张。普通需要回复不同：如果你的回答直接结束任务，该轮仍会产生一张绿色完成卡；若之后又继续运行，旧候选会被取消，最终只通知真正结束的 native-idle 状态。
- **任务噪声控制**：子任务、后台任务与工作流完成通知默认关闭。
- **在线版本检测**：设置页会显示当前版本与 npm 上最新版本，并提供**检查更新**、GitHub、更新日志、反馈 Issue 入口。检测只读：宿主只查询 registry 并汇报，同时给出可复制的升级命令，绝不自动安装、也绝不重启 DSH。registry 不可达时显示「检查失败」，而不会把当前版本谎称为最新。
- **无历史设计**：通知仅保存在当前页面内存中；刷新或 Toast 浮层重新挂载后，不会回放已结算历史；若宿主仍能报告未处理交互，则会静默恢复。

## 快速开始

环境要求：

- 带 Web profile 的 DeepSeek Harness
- 当前 DSH 需要 Node.js 24.9.0 或更新（插件自身声明 Node.js 20+）
- 已验证的 DeepSeek Harness：`0.2.2-rc.1`

从 npm 安装：

```sh
dsh plugin --profile web add @idoall/dsh-notify@latest
```

从 GitHub 安装：

```sh
dsh plugin --profile web add "github:idoall/dsh-notify"
```

从本地克隆安装：

```sh
git clone https://github.com/idoall/dsh-notify.git
cd dsh-notify
npm install
dsh plugin --profile web add "link:$(pwd)"
```

若插件宿主没有热重载客户端，请重新加载 profile 或重启 DSH。客户端注册到 `settings.section` 与 `shell.overlay`，宿主通过 `cordis.patch.yml` 挂载。

> **命名提醒**：无 scope 的 npm 名 `dsh-notify` 属于另一位作者。请安装 **`@idoall/dsh-notify`**。内部 Cordis 名、挂载 id 和 `/plugins/dsh-notify/*` 路由仍保持 `dsh-notify`。

## 设置

**设置 → 通知**提供以预览为先的紧凑控制区：

<p align="center">
  <img src="./assets/settings.png" width="60%" alt="DSH 通知设置：增强版与柔和版选择、折叠通知开关、提示音与任务提醒">
</p>

| 设置项 | 默认 | 作用 |
| --- | --- | --- |
| Toast 位置 | 会话区右上 | 浮层贴在聊天区，并跟随左右侧栏布局变化。 |
| 通知样式 | **增强版** | 增强版使用清晰状态色和短提醒条；柔和版更克制。 |
| 多条通知折叠显示 | **开** | 从两条开始完整显示最新一条，旧卡叠在下方；悬停或聚焦即可展开。 |
| 页内提示音 | **开** | 一轮轮询只要有一条或多条新 Toast 成功进入可视队列，就播放一次提示音，即使 DSH 标签在后台。内置 chime / ping / alert / 静音，也可上传自定义声音。 |
| 子任务 / 后台任务完成时通知 | **关** | 开启子代理、后台任务与工作流的完成通知；默认避免噪声。 |
| 预览与自测 | — | 仅发送当前页面的完成、确认、失败、信息示例；也可重播四种状态、折叠当前示例或清空。不会发系统通知，也不影响真实任务。 |

自定义提示音保存在 profile 数据目录的 `<dataDir>/sounds/`，而非插件安装目录。上传限制 1 MB，支持 `mp3 / m4a / aac / wav / ogg / flac`，文件名必须是单段路径。

卡片上方是版本检测行：显示当前版本与 npm 上最新版本的关系——`v0.4.13 ✓ 最新`、`v0.4.12 ➔ v0.4.13` 或 `v0.4.13 · 检查失败`。**检查更新**会强制重新查询（`GET /plugins/dsh-notify/update?force=1`，需通过鉴权）；页面加载则复用宿主 6 小时缓存。检测到新版本时，行下方会展开 `dsh plugin --profile web add @idoall/dsh-notify@<版本>` 与复制按钮——是否安装、以及随后是否重启 DSH，都由你决定。

<p align="center">
  <img src="./assets/version-check.png" width="60%" alt="DSH 通知设置中的版本检测行：v0.4.0 已是最新、检查更新、GitHub、更新日志与反馈 Issue 入口">
</p>

## 兼容性

当前发布：插件 **`0.4.13`** 已在 DeepSeek Harness **`0.2.2-rc.1`** 上验证。

| 插件 | 已验证的 DeepSeek Harness | 这一版是什么 |
| --- | --- | --- |
| **`0.4.13`** | `0.2.2-rc.1` | 声明兼容新版 DSH RC，保留既有支持，并同步双语发布信息与当前 DSH 的 Node.js 要求。通知源码无改动。 |
| `0.4.12` | `0.2.2-alpha.3` | 修复客户端入口的 Cordis 生命周期处理（构造器识别与清理返回值），声明兼容当前 DSH alpha，同步锁文件的 DSH peer 范围、更新锁定的 Zod，并新增中英文发布检查。通知逻辑不变；验证边界见发布说明。 |
| `0.4.11` | `0.2.2-alpha.2` | 声明兼容 DSH `0.2.2-alpha.2`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：承载整个 `slots` 服务的 `dsh-client-ui-renderer` 逐字节相同，`dsh-client-ui-settings`、`dsh-jobs`、`dsh-client-ui-slots`、`dsh-tool-workflow` 以及 `dsh-client-connection` 的宿主半边也相同。有改动的六处都是叠加式、不经过通知调用路径——`dsh-host-webserver` 新增一种 `resource` 路由（`register(route)` 与 `{kind,path,handler}` 派发都未变）、`dsh-client-ui-session` 新增一个 `whenAccess()`、`dsh-client-ui-layout` 新增六份语言包、`dsh-client-modules` 新增 `require.asset` 与 `/plugins/<pkg>/assets` 路由、`dsh-client-ui-settings-general` 重排设置面板网格（挂载点的 `{close}` 参数未变）、`dsh-client-connection` 只新增一个浏览器传输类型 |
| `0.4.10` | `0.2.1-rc.4` | 声明兼容 DSH `0.2.1-rc.4`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：它绑定的每个包在 `0.2.1-rc.3` → `0.2.1-rc.4` 之间都逐字节相同，只有 `dsh-client-ui-settings-general` 变了，而那里只替换了界面显示的宿主版本号 |
| `0.4.9` | `0.2.1-rc.3` | 通知堆叠上方新增**全部清除**——只清本页、不替你回答待处理交互；宿主要求与 DSH 兼容声明保持不变 |
| `0.4.8` | `0.2.1-rc.3` | 声明兼容 DSH `0.2.1-rc.3`；通知行为与现有宿主版本范围保持不变 |
| `0.4.7` | `0.2.1-rc.2` | 声明兼容 DeepSeek Harness `0.2.1-rc.2`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：它绑定的包里只有 `dsh-client-ui-settings-general` 在 `0.2.1-alpha.5` → `0.2.1-rc.2` 之间有差异，且只差界面显示的宿主版本号。宿主侧 `cordis`、`schemastery` 同时转正为 `4.0.5` / `3.18.5`，与本插件此前运行的 alpha 版本逐字节相同 |
| `0.4.6` | `0.2.1-alpha.5` | 声明兼容 DeepSeek Harness `0.2.1-alpha.5`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：它绑定的每个包在 `0.2.1-alpha.4` → `0.2.1-alpha.5` 之间都逐字节相同，只有两个包的改动不在本插件路径上——`dsh-tools`（`run_code` 的工具描述）与 `dsh-client-ui-settings-general`（界面显示的宿主版本号）。另修正了中文 README 的 DSH 徽章，它此前落后了两个版本 |
| `0.4.5` | `0.2.1-alpha.4` | 声明兼容 DeepSeek Harness `0.2.1-alpha.4`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：它绑定的每个包在 `0.2.1-alpha.3` → `0.2.1-alpha.4` 之间都是逐字节相同的，只有 `dsh-client-ui-settings-general` 变了，而那里新增的只是宿主版本变更提示及其本地化文案 |
| `0.4.4` | `0.2.1-alpha.3` | 声明兼容 DeepSeek Harness `0.2.1-alpha.3`——只加一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它。无源码改动：它绑定的接口都没有变化，而且正在运行的 `0.4.3` 本来就已经在 `0.2.1-alpha.3` 上正常启动并投递通知 |
| `0.4.3` | `0.2.1-alpha.1` | 「通知位置」与「提示音」两个下拉框改为固定 13px，不再跟随宿主的正文字号而比自己的标签还大。只动客户端半边 |
| `0.4.2` | `0.2.1-alpha.1` | 走了审批的任务在完成时重新会通知（自 `0.3.2` 起这类完成通知被永久吞掉）；同时声明兼容 `0.2.0-rc.2` 与 `0.2.1-alpha.1`——各一行 `dshReleases`，因为 `>=0.1.7-rc.1 <0.3.0` 本来就接纳它们 |
| `0.4.1` | `0.2.0-rc.1` | 兼容性跟进：声明支持 DSH `0.2.0` 线（`>=0.1.7-rc.1 <0.3.0`），`0.2.0` 正式版发布时插件不会再在 profile 加载阶段被丢弃。无源码改动——它绑定的接口都没有变化 |
| `0.4.0` | `0.1.7-rc.2` | 设置 → 通知新增在线版本检测：当前版本 vs npm 最新版、手动**检查更新**（`?force=1`）、仓库/更新日志/Issue 入口，以及可复制的升级命令——只读、带缓存，绝不自动安装、绝不重启 |
| `0.3.4` | `0.1.7-rc.1` | 触发时序修复：失败在 `agent/error` 时即投递（与 `turn/end` 共用同一张卡）、同栈 `idle → running` 抖动不再误报完成、客户端在会话状态变化与标签页重新可见时立即拉取（保留 1.5s 兜底） |
| `0.3.3` | `0.1.7-rc.1` | 适配 DSH 0.1.7：后台任务完成走 `jobs.events.subscribe`，工具结果读取扁平化后的 tool-role 消息，卡片内回答读取 `uiSession.sessionStatus`，`@deepseek-ai/schemastery` 改为 peer |
| `0.3.2` | `0.1.6-alpha.1` | 完成通知跟随原生 `agent/status: idle`；连续任务只发一张绿色卡 |
| `0.3.1` | `0.1.6-alpha.1` | 提示音与可见投递对齐；浮层重挂载后恢复未处理交互 |
| `0.3.0` | `0.1.6-alpha.1` | 增强/柔和样式、折叠堆叠、页内自测 |
| `0.2.2` | `0.1.6-alpha.1` | 在 `0.3.3` 发布前仍是 npm `latest` |
| `0.2.1` | `0.1.6-alpha.1` | — |
| `0.2.0` | `0.1.6-alpha.1` | — |
| `0.1.2` | `0.1.6-alpha.1` | — |

- **`0.3.3` 只支持 DSH `0.1.7` 线。** DSH `0.1.7` 移除了 `jobs.onJobDone`，把工具结果身份扁平化到 tool-role 消息上，并用统一的 `sessionStatus` 快照替换了 `uiSession.pendingInteractions`。更早的 DSH（含 `0.1.6-alpha.1`）请继续用插件 **`0.3.2`**。
- 两处声明决定插件能否加载：`dsh.engines.dsh` 与每一个 `@deepseek-ai/dsh-*` peer 都写同一条要求，自 `0.4.1` 起为 `>=0.1.7-rc.1 <0.3.0`。DSH 会在 profile 加载时拒绝不兼容的 bundle，范围写错就会静默丢掉插件——旧的 `<0.2.0` 上限在 `0.2.0` 正式版上正是如此。
- **交互门禁只在「还没等到人回答」时压住完成通知。** DSH 的执行审批与计划审批都是在**提问的那个回合内**解决的（`approval.request()` 必须处于打开的回合中并原地等待结果；`exit_plan_mode` 把结论交回同一回合），所以接着干活的回合就是提问的那个回合，它的结束就是用户任务的完成。`0.4.2` 以此为规则；`0.3.2`–`0.4.1` 压掉了整个回合，于是每个带审批的任务都静默丢失完成通知。
- `@deepseek-ai/schemastery` 是 **peer**，不是普通依赖：DSH 只会把 **link 形态**插件的 peer 解析到当前安装；若仍写成普通依赖，`link:` 安装的 Host 半会导入失败。
- **声明的是「实测验证过的版本」，不是语义化猜测。** `dsh.compatibility.dshReleases` 列出真正启动验证过的每个版本（截至 `0.4.13`：`0.1.7-rc.1`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`、`0.2.1-alpha.3`、`0.2.1-alpha.4`、`0.2.1-alpha.5`、`0.2.1-rc.2`、`0.2.1-rc.3`、`0.2.1-rc.4`、`0.2.2-alpha.2`、`0.2.2-alpha.3`、`0.2.2-rc.1`），一旦其中某个落在声明范围之外，`npm run pack:check` 会直接失败。

以下截图来自此前的浏览器验证，并非本次 RC 的新视觉测试；本次验证覆盖接口兼容性、自动化测试与实时浮层注册，不包含新的视觉或声音实测。此前在 390px 手机视口检查：设置控件会重排，Toast 会满宽显示且不会被移动端侧栏遮挡。

<p align="center">
  <img src="./assets/mobile.png" width="34%" alt="390px 手机视口：增强版通知栈满宽显示，最新通知下方有等距状态色边缘">
</p>

本次发布时 npm DSH 的 `next` 指向当前运行的 `0.2.2-rc.1`，`alpha` 指向 `0.2.2-alpha.3`，`latest` 指向 `0.2.1-rc.5`。后者仍位于声明范围内，但未加入明确启动验证的列表。

## 卸载

```sh
dsh plugin --profile web remove @idoall/dsh-notify
```

卸载不会删除 profile 数据目录里的自定义声音或 `settings.json`。通知记录从不写入磁盘。

## 开发

```sh
npm install
npm run verify     # typecheck + 测试 + 构建 + 打包检查
npm run test       # node --test test/*.test.js
npm run build      # dist/index.js、dist/client.js
npm run pack:check # 发布前置条件 + 客户端注册校验
```

## 发版

发版由 tag 驱动。更新 `package.json` 与锁文件根版本、同步中英文 README 和 DSH 声明、将对应 CHANGELOG 段落移出 `Unreleased`、编写 `release-notes/v<版本>.md` 后，推送发版提交与 tag：

```sh
git tag v0.4.13
git push origin v0.4.13
```

发版工作流会运行 `npm run verify`、打包插件、通过 npm trusted publishing（OIDC）发布，并创建附带 tarball 与 sha256 的 GitHub Release。

## 许可证

MIT，见 [LICENSE](LICENSE)。
