Skip to content
dsh.fish
Bundle

@telosmaylx/dsh-session-notify

DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).

Source
TelosmaYLX
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

<div align="center">

# dsh-session-notify

**简体中文** · [English](README.en.md) · [繁體中文](README.zh-TW.md) · [日本語](README.ja.md) · [한국어](README.ko.md)

**DSH(DeepSeek Harness)会话完成提醒插件 —— 每一轮结束,让完成状态主动找你,而不是你盯着屏幕等。**

[![npm version](https://img.shields.io/npm/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![npm downloads](https://img.shields.io/npm/dm/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![license](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](./LICENSE)
[![node](https://img.shields.io/node/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![DSH](https://img.shields.io/badge/DSH-Web%20Profile-4D6BFE)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/TelosmaYLX/dsh-session-notify/pulls)

每轮对话结束时,把「已完成 / 出错 / 被阻塞 / 达到上限」连同用时、token 消耗写入会话日志,并推送浏览器系统通知与页内 toast;**AI 向你提问时同样立即弹窗提醒**,不必守着会话页面。内置 5 种语言、4 套风格预设(颜文字 / 艾露猫 / 猫娘 / DeepSeek 娘)、可视化文案模板编辑器、自定义预设库,缓存命中率与生成速度取自官方投影,与状态栏同口径。

</div>

---

## 目录

- [功能特性](#功能特性)
- [环境要求](#环境要求)
- [安装](#安装)
- [卸载](#卸载)
- [快速开始](#快速开始)
- [通知行为](#通知行为)
  - [触发条件](#触发条件)
  - [推送正文从哪来](#推送正文从哪来)
  - [通知示例](#通知示例)
  - [通知权限](#通知权限)
- [配置](#配置)
  - [设置面板](#设置面板)
  - [文案模板与占位符](#文案模板与占位符)
  - [预设系统](#预设系统)
  - [宿主配置项](#宿主配置项)
- [工作原理](#工作原理)
- [项目结构](#项目结构)
- [开发与调试](#开发与调试)
- [常见问题](#常见问题)
- [更新日志](#更新日志)
- [致谢](#致谢)
- [贡献](#贡献)
- [相关链接](#相关链接)
- [许可证](#许可证)

---

## 功能特性

<div align="center">

<img src="screenshot/screenshot1.png" width="220" alt="标题编辑器">
<img src="screenshot/ScreenShot2png.png" width="220" alt="内容编辑器">
<img src="screenshot/ScreenShot3.png" width="220" alt="任务完成通知">
<img src="screenshot/ScreenShot4.png" width="220" alt="AI 提问通知">
<img src="screenshot/ScreenShot5.png" width="220" alt="任务出错通知">

</div>

### 三通道提醒,一条不漏

| 通道 | 形式 | 说明 |
| --- | --- | --- |
| 会话内系统消息 | 可折叠提示行 | 每轮结束把结束原因与用时、消耗作为插件来源的系统消息追加进会话日志,随 JSONL 落盘,恢复或回放会话后依然可见。 |
| 浏览器系统通知 | Web Notification | 原生弹窗。每次完成事件使用独立 `tag`(`dsh-session-notify:<timestamp>`),不与前一次互相替换,也不被折叠成一个分组条目;点击通知聚焦回窗口。 |
| 页内 toast | 右下角浮动弹窗 | 永远展示的保底通道:系统通知被平台静默、权限拒绝或环境不支持时仍有可见反馈。同屏最多 3 条(超出移除最旧),10 秒自动消失,点击关闭。 |

### 后台会话全覆盖

- 宿主为所有会话(含后台、未打开窗口的)维护「最近一条通知正文」的会话投影单元(key = `session-complete-notify`),推送正文跨会话一致,不依赖你恰好开着那个窗口。
- 客户端从会话列表快照观测所有会话的 `running` 位,`true → false` 边沿即触发推送,与官方 sidebar 提醒同策略(首次观测只记录基线,已在 idle 的会话不补发)。

### 提问即时提醒

- AI 调用 `ask_user_question` 向你提问时,宿主立刻把「提问标题 + 正文」写入独立投影单元(key = `session-complete-notify-question`),客户端实时轮询并弹窗提醒——**即使你正看着别的页面,也不会错过提问**。
- 提问文案完全可定制:标题走「按原因定制标题 → 全局标题 → 默认标题」链路,正文支持 `{question}` 占位符(注入 AI 的实际提问),媒体开关 `{image}` / `{icon}` 同样生效。

### 审批即时提醒

- 会话请求权限审批时(`approval/asked`)立刻提醒,`approval/decided` 后失效——切到别的标签页也不会漏掉审批。
- 三路信号兜底:harness 原生 `pendingInteractions`(宿主提供时最准)→ 宿主审批投影(key = `session-complete-notify-approval`,标题与正文由宿主按当前语言渲染)→ 会话列表快照的 `pendingInteraction === 'approval'`。
- 文案只含工具名与可选原因(如「会话请求使用 Bash,请前往审批。(原因:…)」),**绝不含命令参数等敏感内容**;推送方式与媒体设置同样生效。

### 可定制到每一句话

- **5 种语言**:简体中文、繁體中文、English、日本語、한국어 —— 通知文案、时长与用量措辞、设置面板界面全部随语言切换(切换即时重渲染)。
- **可视化模板编辑器**(Chip 胶囊编辑器):动态信息渲染为内联胶囊(占位符代码不露出),「+ 插入信息」在光标处插入(可插到文字中间),点击胶囊移除,每栏带实时预览(信息以示例值流入正文)。
- **预设系统**:内置「默认」基线 + 4 套一键风格预设(颜文字 / 艾露猫 / 猫娘 / DeepSeek 娘——标题与 5 结束原因 + 提问正文整套风格化文案);当前配置可另存为自定义预设(`localStorage` 持久化),支持自动编号的未命名预设(`未命名`、`未命名 2`…)、「来自:xxx · 已修改」来源指示、删除预设。
- **推送标题模板**:留空时各原因用默认标题(完成=任务已完成 / 出错=任务出错 / … / 提问=AI 正在向你提问);`{title}` 引用会话标题。

### 与官方口径同源

- **缓存命中率**取自官方 `tokenUsage` 投影:缓存读 /(未缓存输入 + 缓存读 + 缓存写)。
- **生成速度**取自官方 `sessionStats` 投影:输出 token ÷ 解码耗时。
- 两者与 dsh-web-ui 状态栏完全同口径,不含排队、准备、工具时间;投影不可用或数据未就绪时自动退回本地用量聚合估算。

> [!NOTE]
> 缓存命中率与速度只在自定义模板中通过 `{cache}`、`{tps}` 占位符插入时才显示。使用内置默认文案时,正文不含用时与消耗(要显示数据需在自定义模板中插入对应占位符)。

### 工程质量

- **只响应实时事件**:resume、replay 不重放旧通知,加载会话不刷屏。
- **自免疫循环**:插件追加的消息类型(`user/message`)与自身监听目标(`turn/*`)不相交。
- **零外部依赖**:宿主平面零裸 import,UserMessage 按 `dsh-llm` 的 `createUserMessage` 契约手工构造;纯逻辑层(`lib/core.js`)零依赖,可独立测试。
- **Cordis effect 纪律**:重试定时器包装在 `ctx.effect()` 中并返回 `clearTimeout` disposer,注册随 fiber 卸载自动撤销,HMR 热重载安全。
- **安装即挂载**:声明官方 `dsh.bundle` manifest,`dsh plugin add` 一条命令装完即用,无需手写 patch。

---

## 环境要求

| 依赖 | 要求 |
| --- | --- |
| DSH(DeepSeek Harness) | Web profile 部署。官方 base bundle 默认包含 `@deepseek-ai/dsh-settings`(设置命名空间)与会话投影,无需额外配置 |
| cordis | `>=4.0.0-rc <5`(peer dependency,由宿主提供) |
| Node.js | `>=22`(宿主侧) |
| 浏览器 | 支持 Web Notification 则有系统通知;不支持、权限拒绝或被静默时由 toast 兜底 |

---

## 安装

> [!WARNING]
> 裸 `npm install` 只会把包装进依赖树,**不会注册插件** —— 这是 DSH 官方设计(`npm install only adds the dependency; it does not register the plugin`)。自动挂载的唯一官方途径是 `dsh plugin add`:它读取包内 `dsh.bundle` manifest(本插件自 0.1.3 起声明,指向仓库根 `cordis.patch.yml`)并自动应用。

### 方式一:dsh plugin add(推荐)

安装包的同时自动应用 `cordis.patch.yml`,把插件挂载进 profile 装配(host 事件订阅 + client 启动图注入)。

```bash
dsh plugin --profile web add @telosmaylx/dsh-session-notify
```

### 方式二:从 GitHub 仓库安装

```bash
dsh plugin add github:TelosmaYLX/dsh-session-notify
```

也可以在 DSH Web GUI 会话内执行:

```bash
dev_install_package github=TelosmaYLX/dsh-session-notify
```

### 方式三:本地目录热装配(开发用)

把路径换成你的克隆目录,在 DSH Web GUI 会话内执行:

```bash
dev_install_package dir=/你的/克隆目录/dsh-session-notify
```

### 方式四:npm 包手动安装

先打包:

```bash
npm pack @telosmaylx/dsh-session-notify
```

解压后指定目录安装(在 DSH Web GUI 会话内执行):

```bash
dev_install_package dir=/解压/目录/package
```

### 方式五:手动 cordis patch(不依赖安装器)

在 `~/.dsh/profiles/web/cordis.patch.yml` 追加:

```yaml
- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config: {}
```

> [!IMPORTANT]
> 无论用哪种方式,装完都需要**刷新一次浏览器页面** —— 客户端 bundle 通过 `__DSH_BOOT__` 启动图注入。

## 卸载

一条命令移除插件及其挂载(自动从 `cordis.patch.yml` 移除 insert 条目):

```bash
dsh plugin --profile web remove @telosmaylx/dsh-session-notify
```

> [!NOTE]
> 手动安装(方式四/五)的用户,需同步从 `~/.dsh/profiles/web/cordis.patch.yml` 删除对应 insert 条目,再刷新页面。

### 卸载时自动清理的内容

插件实现了完整的生命周期收尾(Cordis effect 纪律),卸载/禁用/HMR 热重载时:

| 平面 | 自动释放的资源 |
| --- | --- |
| host | `session/event` 事件订阅、settings 命名空间、会话投影单元、设置注册重试定时器(`ctx.effect` 包装);置卸载标志抑制已调度的微任务追加 |
| client | 会话列表订阅、完成推送正文的轮询定时器、`window.__dsch_notify_debug` 调试钩子(按引用删除,防闭包泄漏)、页内 toast 容器 DOM |

### 卸载后保留的数据

- **设置配置**(语言、文案模板)留在 settings 文档,重装后自动恢复;
- **自定义预设**存于浏览器 `localStorage`(`dsh-scn-custom-presets`),重装后仍在;
- 历史会话中已追加的系统消息与 JSONL 日志**不会**被回滚(它们是会话数据的一部分,与官方侧边栏提示同语义)。

---

## 快速开始

1. 按上面任一方式安装并刷新页面。
2. 发起任意一轮对话,等它结束 —— 右下角弹出 toast、浏览器弹系统通知、会话日志里出现可折叠的系统提示行。
3. 首次收到完成事件时,浏览器会请求通知权限(每页只问一次),允许后后续完成都有系统通知。
4. 打开 **设置 → 插件 → 会话完成提醒**,切换语言、编辑文案模板、另存预设。保存后点「点击刷新」让宿主与客户端两侧重新读取,新配置即生效。

刚装好时,会话日志里会出现这样一行可折叠提示:

```text
会话「重构登录模块」已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。
```

> 默认文案在「会话」后内嵌会话标题标签(`{title}`);会话无标题时自动退回「会话已完成」。

---

## 通知行为

### 触发条件

每轮对话结束(`turn/end`)时按结束原因判断,命中白名单即提醒:

| 结束原因 | 含义 | 默认 |
| --- | --- | --- |
| `completed` | 会话正常完成 | 提醒 |
| `aborted` | 会话中止 | 提醒 |
| `blocked` | 会话被阻塞 | 提醒 |
| `error` | 会话出错(附错误详情,超长截断) | 提醒 |
| `max-tokens` | 达到输出 token 上限 | 提醒 |
| `interrupted` | 中断(崩溃恢复后由持久化后端补写的孤儿轮次关闭标记) | 不提醒(可配置加入) |

**子代理会话默认跳过**(`header.origin === 'subagent'` 或 `delegationDepth > 0`)—— 子代理由父会话编排,逐轮提醒是噪音;可在宿主配置关闭跳过。

**提问是独立通道,不走上面的白名单**:AI 调用 `ask_user_question` 等待你回答时(`tool/call` 事件)立即提醒,`tool/result` 返回后提醒失效。提问不写会话日志,只弹通知。

**审批也是独立通道**:会话请求权限审批时(`approval/asked`)立即提醒,`approval/decided` 后失效。同样不写会话日志,只弹通知;标题与正文只含工具名与可选原因,不含命令参数。

### 推送正文从哪来

客户端在会话列表观测到 `running: true → false` 边沿时推送,正文按以下优先级获取(最长轮询 6 秒,400ms 间隔):

1. **宿主投影**(key = `session-complete-notify`)—— 每个会话都有,后台会话同样拿到全文;
2. **会话事件窗口里的 notice 节点**(`kind=context` + `form=notice`)—— 正在查看的会话,落盘后立即可用;
3. **降级** —— 「详情见会话内系统消息」+ 工作区信息(`cwd` 最后一段)。

提问提醒的正文同样优先取宿主投影(key = `session-complete-notify-question`,宿主已渲染好标题与正文),老宿主无该投影时客户端自行拼接标题与 `{question}` 文本兜底。

审批提醒按可用性三路取源:harness 原生 `pendingInteractions` → 宿主审批投影(key = `session-complete-notify-approval`)→ 会话列表快照的 `pendingInteraction` 字段;取到即提醒,同一审批只推一次。

### 通知示例

以下均由 `lib/core.js` 的 `buildNotice` 实际生成。默认文案统一为「会话「{title}」已xx,请点击查看。」句式(按结束原因差异用词;**不含用时与消耗**):

简体中文默认文案:

```text
会话「重构登录模块」已完成,请点击查看。   ← 完成
会话「重构登录模块」已中止,请点击查看。   ← 中止
会话「重构登录模块」被阻塞,请点击查看。   ← 阻塞
会话「重构登录模块」达到上限,请点击查看。 ← 上限
会话「重构登录模块」出错,请点击查看。     ← 出错
```

> 会话无标题(`titleValue` 为空)时自动回退「会话已完成,请点击查看。」;用时/消耗/缓存命中/速度等数据只在自定义模板中通过 `{duration}` `{usage}` `{cache}` `{tps}` 占位符插入时显示。

自定义模板(在设置面板编辑,本例用到全部信息位):

```text
{title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}
```

渲染结果:

```text
重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s
```

五种语言的同一事件:

```text
会话「重构登录模块」已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
會話「重構登入模組」已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
Session "重构登录模块" completed (took 3m25s, used 1,240 in / 3,560 out).
セッション「重构登录模块」完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
세션「重构登录模块」 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。
```

### 通知权限

| 权限状态 | 行为 |
| --- | --- |
| `default`(未决定) | 完成事件只发 toast;设置面板「通知权限」区提供「请求授权」按钮(**用户手势内请求**——Chromium 会忽略非手势的自动请求,因此插件不再自动请求) |
| `granted` | 按「推送方式」发系统通知(独立 tag,互不覆盖) |
| `denied`(被浏览器屏蔽) | 仅 toast;设置面板显示地址栏操作指引(权限图标 → 网站设置 → 通知 → 允许) |
| `undefined`(非安全上下文 / 不支持) | 仅 toast;建议改用「仅页内提示」 |

---

## 配置

绝大多数配置在 **DSH Web UI → 设置 → 插件 → 会话完成提醒** 面板完成(保存后点「点击刷新」生效)。仅「触发原因白名单」在宿主 `cordis.patch.yml` 的 `config` 中配置(跳过子代理在面板中以复选框控制)。

### 设置面板

面板在官方「设置 → 插件」面板中注册(`settings.plugin.item` keyed slot,key = `session-complete-notify`),样式逐值复刻原生插件卡片(12px 圆角、展开收起、旋转 chevron、footer 状态位 + 弃置 ghost + 主色保存按钮):

| 区域 | 内容 |
| --- | --- |
| 预设 | 下拉选择内置或自定义预设;「新增」把当前配置另存为自定义预设;当前预设可「删除」 |
| 语言 | 5 种语言单选,切换即时重渲染整个面板 |
| 推送方式 | 三选一:双通道(系统通知 + 页内提示,默认)/ 仅系统通知 / 仅页内提示 |
| 通知图片 | 大图两种来源:**按原因上传**——在模板中通过「+ 插入信息 → 图片」插入 `{image}` 标签并选择本地图片(编辑器内显示为带缩略图的标签,**自动压缩至 512px 宽、按通知显示比例 16:9 居中裁切**,随各原因独立保存);**全局大图/图标**——两张上传卡片并排一行(**图标在前**,空态 = 圆角矩形 + 号,点击上传;**大图 512×288(16:9 居中裁切)、图标 128×128(1:1 方形居中裁切)**;已上传则卡片显示缩略图,**点击缩略图可全屏查看完整原图(等比未裁切)**,右上角 × 删除)。图标留空用站点默认图标,也可在模板中插入 `{icon}` 标签**按原因指定图标**(优先于全局)。仅系统通知通道生效(页内 toast 为文字卡片),「发送」测试按钮同样生效 |
| 标题 | 折叠区(**默认收起**,点击展开):**全局推送标题**(所有原因共用,Chip 编辑器——点「+ 插入信息」插入的信息以**胶囊标签**形式显示,点击胶囊移除;占位提示「通用推送标题,留空时则使用默认标题,优先级低于下方自定义标题」(不可选中/删除);**通知发送时标题里的信息位(用时/消耗/错误/缓存命中/速度)会替换为实际值,不再显示代码**;留空时各原因用默认标题——完成=任务已完成、出错=任务出错、中止=任务已中止、阻塞=任务被阻塞、上限=任务达到上限、提问=AI 正在向你提问)+ **按原因定制标题**(6 条原因各自输入,每行带「+」插入按钮——可插入信息标签(含「提问」,不含图片/图标),插入到光标处;**优先于全局标题**,留空 = 用全局或语言默认标题) |
| 内容 | 折叠区(**默认收起**,点击展开);展开后每条结束原因(完成、出错、中止、阻塞、上限、提问)**一行式布局**(原因标签 + Chip 编辑器 + 「+」插入按钮——菜单展开时变「−」+ **发送箭头按钮**,按钮为矩形、垂直居中):**空模板(默认预设)时编辑器显示默认文案「会话「{title}」已xx,请点击查看。」**,文字 + 内联信息胶囊,光标处插入;`{image}`/`{icon}` 标签**点击缩略图可预览大图、点 × 才删除**(防误删),其他标签点击移除;**编辑后删空则显示「留空则使用默认文案」占位(不可选中/删除)**;提问行的默认文案为「AI 向你提问:{question}」,`{question}` 会在发送时替换为 AI 的实际提问(插入菜单同样提供「提问」标签,与其他标签同款交互) |
| 跳过子代理会话 | 复选框(保存时一并写入设置文档) |
| 通知权限 | 状态实时显示:已授权(绿)/ 尚未授权(附「请求授权」按钮)/ 已被浏览器屏蔽(附地址栏操作指引)/ 环境不支持 |
| 按原因定制标题 | 折叠区(默认收起):每个结束原因一个独立标题输入框,留空 = 用全局模板或语言默认标题 |
| 保存 | 写入宿主设置文档(`language` / `templates` / `titleTemplate` / `titleTemplates` / `pushMode` / `skipSubagents`);保存后显示「点击刷新」链接 |
| 重置 | 一键还原默认值(**语言保留当前选择**,标题/模板/推送方式恢复默认)并立即保存 |

> [!NOTE]
> 「推送方式」的取舍:`dual`(默认)同时弹 Windows 系统通知与页内 toast,toast 是保底通道,防止系统通知被平台静默(专注助手、通知横幅关闭)。但 **QQ 浏览器等国产 Chromium 壳浏览器会把 `Notification` 渲染成「浏览器内置的页内推送弹窗」**(页面顶部/角落的横幅,不经 Windows 通知中心)——此时 `dual` 会造成页内两个提示(浏览器内置弹窗 + 插件 toast)。这类浏览器请选「仅页内提示」(不再调用 `Notification`,浏览器内置弹窗不会出现,页内只有插件自己的小 toast);「仅系统通知」模式在 QQ 浏览器无效(它永远渲染为页内弹窗)。设置面板每个原因的「发送」测试按钮同样受此影响。

> [!NOTE]
> 系统通知(`Notification` API)能否弹出由**浏览器与站点访问方式**共同决定:Edge/Chrome 对"不熟悉"的站点会**自动屏蔽通知**(地址栏出现「通知已屏蔽」)——点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许即可恢复;`http://IP` 这类非安全上下文访问时 `Notification` 根本不存在,请改用「仅页内提示」。设置面板「通知权限」区域会实时显示当前状态并给出对应操作指引(可一键请求授权)。Firefox 窗口聚焦时通知显示为页内横幅、失焦才进系统通知中心。

> [!NOTE]
> 面板中「跳过子代理会话」保存的是设置文档里的布尔值;宿主 `cordis.patch.yml` 的 `config.skipSubagents` 是其启动默认值,两者任一为真即跳过。

### 文案模板与占位符

每条结束原因独立一个模板输入框,**标签即开关** —— 在模板里插入对应信息标签,该项数据才会显示:

| 占位符 | 含义 | 示例值 |
| --- | --- | --- |
| `{title}` | 会话标题(推送标题模板也可用) | `重构登录模块` |
| `{duration}` | 本轮用时(`turn/start` 起表 → `turn/end` 结束) | `3 分 25 秒` / `3m25s` |
| `{usage}` | token 消耗(输入 = 未缓存 + 缓存读 + 缓存写) | `1,240 输入 / 3,560 输出` |
| `{error}` | 错误信息(无错误时显示 `none`;单行化,80 字符截断) | `connection timeout` |
| `{cache}` | 缓存命中率(官方投影口径,无数据为空) | `96.5%` |
| `{tps}` | 生成速度(官方投影口径,无数据为空) | `92 tok/s` |
| `{image}` | 自定义通知大图开关:从「+ 插入信息」插入并选择本地图片(自动压缩至 512px),按原因独立;正文渲染时剥除,不进会话日志;删除标签时该原因图片数据一并清除 | — |
| `{icon}` | 自定义通知图标开关:从「+ 插入信息」插入并选择本地图片(自动压缩至 128×128 方形),按原因独立;正文渲染时剥除,不进会话日志;优先于全局「通知图标」;删除标签时该原因图标数据一并清除 | — |
| `{question}` | **提问行的专属占位符**:发送时替换为 AI 的实际提问文本;与「+ 插入信息」菜单联动(可直接选「提问」标签,手输 `{question}` 同样识别为胶囊),仅提问通道可用,其他原因行插了也会被替换为空(防字面量泄漏) | `要继续生成报告吗?` |
| `{label}` | 已废弃 —— 渲染时自动剥除,旧模板仍兼容(插入菜单已移除该选项) | — |

模板留空即使用内置默认文案(「会话「{title}」已xx,请点击查看。」句式,不含用时与消耗)。折叠行 `summary` 与正文同源(渲染结果截断至 120 字符)—— 只看折叠行的用户也能看到真实标题与用时、消耗。

### 预设系统

- **内置预设**:仅「默认」,作为基线。
- **自定义预设**:保存在 `localStorage`(key = `dsh-scn-custom-presets`):
  - 「新增」命名后保存为自定义预设;保存后可「修改」自动同步、「删除」移除;
  - **自动编号的未命名预设**:从「默认 / 空白」直接保存时,自动生成 `未命名`、`未命名 2`、`未命名 3`…(编号取当前最大值 + 1);
  - 表单显示「来自:xxx · 已修改」来源指示(来自预设但内容已改动时)。
- **保存即同步**:保存时若表单来源是自定义预设则更新该预设,否则新建或继续编号未命名预设。

### 宿主配置项

```yaml
- insert:
    - id: dsh-session-notify
      name: '@telosmaylx/dsh-session-notify'
      config:
        reasons: [completed, aborted, blocked, error, max-tokens]
        skipSubagents: true
```

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `reasons` | `string[]` | `[completed, aborted, blocked, error, max-tokens]` | 触发提醒的 `turn/end` 原因白名单 |
| `skipSubagents` | `boolean` | `true` | 跳过子代理会话(`origin=subagent` 或 `delegationDepth>0`) |

---

## 工作原理

插件分**宿主平面**(Node)与**客户端平面**(浏览器),中间靠会话日志(JSONL)与官方会话投影衔接:

```text
┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
│                                                                     │
│  session/event 火线                                                 │
│   ├─ turn/start        → tracker 起表(key: sessionId:turn)        │
│   ├─ assistant/message → 累加该轮 token 用量                        │
│   ├─ tool/call         → ask_user_question?写提问投影(标题+正文) │
│   └─ turn/end          → reason.kind ∈ reasons ?                   │
│                            ├─ 子代理会话?跳过                       │
│                            ├─ 读官方投影:cache / tps / title        │
│                            ├─ 按语言+模板构建通知(summary ≤120 字) │
│                            └─ queueMicrotask 追加系统消息            │
│                                 (避开 append 重入窗口)             │
│                                                                     │
│  settings.register   → 官方「设置 → 插件」命名空间(失败退避重试)   │
│  sessionProjections  → 注册投影单元(key=session-complete-notify)  │
│                        + 提问投影(key=session-complete-notify-     │
│                          question,等待回答期间持续推送)            │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ user/message (source: plugin, form: notice)
                               ▼  JSONL 持久化 + 投影推送
┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
│                                                                     │
│  会话列表订阅:running true → false 边沿 → pushCompletion            │
│   ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s)               │
│   ├─ Web Notification(独立 tag,点击聚焦)                          │
│   └─ 页内 toast(永远展示,≤3 条,10s 自动消失)                     │
│  提问投影轮询(key=session-complete-notify-question):              │
│   有值 → 立即弹提醒(标题+正文),无值清空                            │
│                                                                     │
│  slots.inject('settings.plugin.item') → 设置卡片(预设/语言/模板)   │
└─────────────────────────────────────────────────────────────────────┘
```

### 关键设计决策

- **不重放**:只处理实时事件,resume、replay 不会补发历史通知。
- **无自我循环**:插件追加 `user/message`,自身只监听 `turn/*`,事件类型不相交。
- **零外部 import**:插件从仓库目录以 realpath 加载,`@deepseek-ai/*` 无法裸解析 —— 宿主平面用 `createRequire` 锚定 profile 共享依赖枢纽(`.dsh/profiles/node_modules`)取 `schemastery`(设置 schema)与 `zod`(投影 schema);UserMessage 按 `dsh-llm` 契约手工构造(`id = crypto.randomUUID()`,deep-freeze 由 `session.append` 的 adopt 快照阶段完成)。
- **append 重入规避**:`session/event` 观察者回调运行在 `turn/end` 那次 append 的发布边界之内(dsh-session 在 dispatch 前置 `entry.appending`、`finally` 复位),同步 append 会被拒绝 —— 因此推迟到 `queueMicrotask`(微任务在本次同步栈含 `finally` 复位之后才执行)。
- **effect 纪律**:设置注册的退避重试定时器包装在 `ctx.effect()` 中并返回 `clearTimeout` disposer —— 插件在重试窗口内被卸载或热重载时定时器随 fiber 拆除,不会对已释放的 ctx 触发注册(极老环境无 `ctx.effect` API 时退化为裸定时器 + ctx 已拆除兜底捕获)。
- **HMR 安全**:`core.js` 导入带 `?v=1` 缓存破坏(HMR 重载按 URL 键控);设置注册遇到热重载竞态(duplicate)时自动退避重试(最多 8 次,间隔 `400ms × attempts`)。
- **投影注册双轨**:优先 `ctx.root.get('sessionProjections')`(最靠近宿主根的一份),拿不到时回退注入实例;只注册进注入实例时客户端可能读不到投影单元,推送正文走降级路径 —— 属尽力而为,不影响会话内系统消息。

---

## 项目结构

```text
dsh-session-notify/
├── lib/
│   ├── index.js      # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
│   │                 #   settings 命名空间注册(schemastery schema,退避重试);
│   │                 #   sessionProjections 投影单元(后台会话推送正文)
│   ├── core.js       # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
│   │                 #   5 语言文案表、时长/用量/缓存/速度格式化、
│   │                 #   模板渲染({title}{duration}{usage}{error}{cache}{tps})、
│   │                 #   提问正文构建(buildQuestionBody,{question} + 媒体剥除)
│   └── client.js     # 浏览器平面:完成推送(系统通知 + toast)、
│                     #   设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
├── scripts/
│   ├── build.sh                # 零构建:仅 node --check 语法校验
│   ├── verify-notice.mjs       # 校验会话日志落盘证据(zstd 多帧逐帧解压)
│   ├── probe-client.mjs        # 探针:客户端装配
│   ├── probe-client-e2e.mjs    # 探针:客户端端到端
│   ├── probe-card-render.mjs   # 探针:设置卡片渲染
│   ├── probe-settings-card.mjs # 探针:设置面板卡片
│   ├── probe-settings-check.mjs# 探针:设置面板检查
│   └── probe-diag-settings.mjs # 探针:settings 诊断
├── cordis.patch.yml  # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
├── package.json      # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
│                     #   exports: "." / "./client" / "./core"
├── LICENSE           # MIT
└── README.md         # 本文档
```

---

## 开发与调试

语法校验(零构建,`prepublishOnly` 同款检查):

```bash
npm run build
```

发布(发布前自动执行 `prepublishOnly` 语法校验):

```bash
npm publish --registry=https://registry.npmjs.org --access public
```

离线校验:解出会话日志中所有 plugin-source 事件与 `turn/end` 尾部序列(不传路径则自动选 `~/.dsh/sessions` 下最新会话):

```bash
node scripts/verify-notice.mjs <session.jsonl.zstd>
```

### 调试入口

| 入口 | 内容 |
| --- | --- |
| `~/.dsh/session-complete-notify.log` | 宿主诊断日志:设置注册、重试与失败、投影注册、追加失败堆栈 |
| 浏览器 console `[dsh-session-notify-client]` | 客户端日志:权限状态、通知展示、设置保存 |
| `window.__dsch_notify_debug.readNotice(id)` | 手动读取指定会话的最新通知正文 |
| `window.__dsch_notify_debug.snapshotDebug(id)` | 会话尾部节点类型 + notice 数量 + 最近正文(前 200 字) |

---

## 常见问题

<details>
<summary><b>npm install 之后为什么不自动挂载?</b></summary>

这是 DSH 官方设计:`npm install` 只把包装进依赖树,不注册插件。自动挂载的唯一途径是 `dsh plugin add` —— 它读取包内 `dsh.bundle` manifest(本插件自 0.1.3 起声明)并自动应用 `cordis.patch.yml`。参见[安装](#安装)。

</details>

<details>
<summary><b>AI 向我提问时也会弹窗提醒吗?</b></summary>

会。AI 调用 `ask_user_question` 等待你回答时,宿主立刻把「提问标题 + 正文」写入独立投影(key = `session-complete-notify-question`),客户端轮询到后立即弹提醒——即使你正看着别的页面也不会错过。提问文案与完成通知一样完全可定制:设置面板的「标题 / 内容」折叠区各有「提问」一行,正文支持 `{question}` 占位符(注入 AI 的实际提问),`{image}` / `{icon}` 媒体开关同样生效。回答后(`tool/result`)提醒失效,不会残留。

</details>

<details>
<summary><b>为什么「中断」(interrupted)不提醒?</b></summary>

`interrupted` 是崩溃恢复后由持久化后端补写的孤儿轮次关闭标记,用户视角的「完成」不包含它(否则恢复会话会刷一屏误报)。确有需要可在宿主配置的 `reasons` 中加入。

</details>

<details>
<summary><b>后台会话(没打开窗口的)也会推送吗?</b></summary>

会。客户端从会话列表快照观测所有会话的 `running` 边沿;正文优先取宿主投影 —— 宿主为所有会话(含后台)维护投影单元,因此推送正文跨会话一致。投影不可用时降级为事件窗口或工作区信息。

</details>

<details>
<summary><b>保存设置后为什么提示刷新页面?</b></summary>

宿主在注册命名空间时读取一次设置,客户端 bundle 在页面加载时装配。保存后点「点击刷新」让两侧重新读取,新语言、模板即生效。

</details>

<details>
<summary><b>缓存命中率、速度数据从哪来?为什么有时是空的?</b></summary>

来自官方 `sessionProjections`(`tokenUsage`、`sessionStats`),与 dsh-web-ui 状态栏同口径。宿主读取投影快照失败或数据尚未就绪时,退回本地用量聚合估算,仍无数据则该项留空(标签插了也不显示)。另外,这两项只在自定义模板中通过 `{cache}`、`{tps}` 插入时才出现,默认文案不含。

</details>

<details>
<summary><b>通知正文里的错误信息太长、有换行怎么办?</b></summary>

摘要行(折叠行)与错误详情都会单行化并截断:摘要 120 字符、模板 `{error}` 80 字符、默认文案的错误详情 40 字符,超长以省略号结尾。

</details>

<details>
<summary><b>可以自定义系统通知的图标或声音吗?</b></summary>

图标**可以自定义**:设置面板「通知图片」区可上传**通知大图**与**通知图标**(全局),也可在各原因模板中插入 `{icon}` 标签为该原因单独指定图标(优先于全局);**声音**暂不支持自定义(沿用系统/浏览器默认),toast 为固定深色卡片。如有其他需求欢迎提 Issue 或 PR。

</details>

<details>
<summary><b>为什么 Edge 推不了系统通知?QQ 浏览器为什么只有页内横幅(内置推送弹窗)?</b></summary>

两者都是浏览器行为,插件无法强制:

- **Edge / Chrome**:对"不熟悉"的站点会**自动屏蔽通知**(地址栏出现「通知已屏蔽」)。点击地址栏左侧权限图标 → 网站设置 → 通知 → 允许即可恢复,之后正常弹 Windows 通知中心。也可在浏览器通知设置中关闭「自动屏蔽」。
- **QQ 浏览器等国产 Chromium 壳**:把 `Notification` 固定渲染为**浏览器内置的页内推送弹窗**(页面顶部/角落横幅,不经 Windows 通知中心),且无系统通知选项。三种推送方式的实际表现:
  - `双通道` → 浏览器内置弹窗 + 插件 toast,页内两个提示;
  - `仅系统通知` → 无效(QQ 浏览器永远渲染为页内弹窗);
  - `仅页内提示` → 浏览器内置弹窗不出现,页内只有插件自带的小 toast(推荐)。
  设置面板每个原因的「发送」测试按钮同样按此规则渲染。
- **Firefox**:窗口聚焦时通知显示为页内横幅,失焦/最小化才进系统通知中心;权限需在地址栏手动允许。
- 另注意:`http://IP` 访问(非安全上下文)时 `Notification` 不存在,任何浏览器都弹不了系统通知。

设置面板「通知权限」区域会实时显示当前状态与对应操作指引。

</details>

---

## 更新日志

| 版本 | 日期 | 变更 |
| --- | --- | --- |
| **0.1.20** | 2026-09-09 | **审批即时提醒 + 历史会话加载修复**:新增权限审批提醒([PR #2](https://github.com/TelosmaYLX/dsh-session-notify/pull/2) 由 [@YiHui-Liu](https://github.com/YiHui-Liu) 贡献——`approval/asked` 投影 + 客户端三路信号兜底);修复 0.1.19 回归:投影注册契约改双代并存(`schema`/`view` 与 `stateSchema`/`wire` 同时注册),旧宿主打开历史会话不再因 `undefined.parse` 失败;客户端 `uiSession` 移出 `inject` 改 `ctx.get` 可选查找,避免服务缺席时通知与设置面板整体失效 |
| **0.1.19** | 2026-09-07 | **修复提问弹窗(宿主投影断链)**:投影单元注册迁移到 `stateSchema` + `wire: { viewSchema, view }` 契约——旧形状(顶层 `schema`/`view`)在新宿主(dsh-session-projection)下是 host-only 单元,值永不送达客户端,完成/提问投影均失效;`tool/call` 的 `callId` 为空串时(部分 OpenAI 兼容代理路由)回退 `turn:step` 作提问 id,`tool/result` 同步按 turn/step 匹配清除;顺带修复 `stateSchema` 缺失在投影 checkpoint restore 路径的潜在崩溃 |
| **0.1.18** | 2026-09-01 | **修复提问弹窗失效**:部分 dsh 版本(0.1.2)宿主投影未送达客户端导致提问不弹窗;客户端提问推送新增 harness 原生「待提问」标记兜底触发,宿主投影缺失时仍提醒;完成推送/设置面板行为不变 |
| **0.1.17** | 2026-08-30 | **提问即时提醒(可定制)**:AI 提问立即弹窗;提问文案支持 `{question}` 占位符与媒体开关;4 套预设补齐 5 语言提问文案;旧宿主自动兜底 |
| **0.1.16** | 2026-08-30 | **交互修复**:连按 Backspace 不再误删标签(仅当光标与标签间无文字时才删标签) |
| **0.1.15** | 2026-08-30 | **交互优化**:「内容」折叠区默认展开;删除标签后光标直达真实内容,可连贯删除 |
| **0.1.14** | 2026-08-30 | **代码审查修复**:删除预设确认、预设恢复为已保存配置、媒体「×」清除预览、按原因图片参与预设匹配、同名预设提示、重置仅在有修改时可用、调试日志自动截断 |
| **0.1.13** | 2026-08-29 | 新增 4 套一键风格预设(颜文字/艾露猫/猫娘/DeepSeek 娘),支持 5 语言 |
| **0.1.12** | 2026-08-29 | 发布包清理 |
| **0.1.11** | 2026-08-29 | **自定义通知媒体**:模板可插 `{image}`/`{icon}` 并上传图片/图标(自动裁切);推送标题支持信息占位符;「正文模板 × 5」改折叠区,布局交互全面优化 |
| **0.1.10** | 2026-08-29 | 推送标题改原生输入框;新增多语言 README(English/繁體/日本語/한국어) |
| **0.1.9** | 2026-08-29 | 推送标题按原因定制;投影升级为对象;重置保留语言;每原因加「发送」测试按钮 |
| **0.1.8** | 2026-08-29 | 默认标题「任务已完成」;默认文案按结束原因差异化;新增重置按钮 |
| **0.1.7** | 2026-08-29 | 修复设置卡片崩溃(通知权限行作用域问题) |
| **0.1.6** | 2026-08-29 | 新增通知权限状态区;权限改为用户手势内请求 |
| **0.1.5** | 2026-08-29 | 新增推送方式(双通道/仅系统/仅页内),解决 QQ 浏览器双提示 |
| **0.1.4** | 2026-08-28 | 完整卸载支持(dispose 生命周期收尾) |
| **0.1.3** | 2026-08-28 | 声明 dsh.bundle manifest;settings 重试定时器改 ctx.effect() |
| 0.1.2 | 2026-08-27 | 包更名至 `@telosmaylx` scope |
| 0.1.1 | 2026-08-27 | GitHub、npm 安装方式文档化 |
| 0.1.0 | 2026-08-26 | 初始版本:会话内系统消息 + 浏览器推送 + 官方设置面板 |

---

## 致谢

感谢 [@YiHui-Liu](https://github.com/YiHui-Liu) 的 [PR #2](https://github.com/TelosmaYLX/dsh-session-notify/pull/2)——权限审批即时提醒(`approval/asked` 投影 + 客户端三路信号兜底)。

---

## 贡献

欢迎 Issue 与 PR:

1. Fork 仓库并新建分支(`feat/xxx`)
2. 改动后运行 `npm run build` 做语法校验
3. 提交 PR,说明动机与验证方式

提交前请遵守 [Cordis 开发教程](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) 纪律:

- Cordis 之外的资源(定时器、订阅、watcher)必须包装在 `ctx.effect()` 中并返回 disposer;
- 配置项显式 `id` 防止编辑漂移;
- 插件须声明 `dsh.bundle` manifest 才能被 `dsh plugin add` 识别安装。

---

## 相关链接

- [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) —— DSH 插件精选列表(投稿规范:`dsh.bundle` 是安装唯一凭证)
- [Cordis 开发教程](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) —— 插件开发全流程(01-07 章)
- [npm 包主页](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
- [GitHub 仓库](https://github.com/TelosmaYLX/dsh-session-notify)

---

## 许可证

[MIT](./LICENSE) © dsh-session-notify contributors

Install

dsh plugin --profile web add github:TelosmaYLX/dsh-session-notify

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