<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 秒自动消失，点击关闭。 |

> [!NOTE]
> 后两条通道**按窗口聚焦状态分流**：设置面板中「失焦时」「聚焦时」各有一个下拉，可选 `双通道` / `仅系统通知` / `仅页内提示` / `不通知`。判定失焦的条件是 `document.visibilityState === 'hidden'` 或 `!document.hasFocus()`，也就是标签页被切走、窗口被最小化或点到别处时，走「失焦时」那一路。

### 后台会话全覆盖

- 宿主为所有会话（含后台、未打开窗口的）维护「最近一条通知正文」的会话投影单元（key = `session-complete-notify`），推送正文跨会话一致，不依赖你恰好开着那个窗口。
- 客户端从会话列表快照观测所有会话的 `running` 位，`true → false` 边沿即触发推送，与官方 sidebar 提醒同策略（首次观测只记录基线，已在 idle 的会话不补发）。

### 提问即时提醒

- AI 调用 `ask_user_question` 向你提问时，宿主立刻把「提问标题 + 正文」写入独立投影单元（key = `session-complete-notify-question`），客户端实时轮询并弹窗提醒——**即使你正看着别的页面，也不会错过提问**。
- 提问文案完全可定制：标题走「按原因定制标题 → 全局标题 → 默认标题」链路，正文支持 `{question}` 占位符（注入 AI 的实际提问），媒体开关 `{image}` / `{icon}` / `{audio}` 同样生效。

### 审批即时提醒

- 会话请求权限审批时（`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。
- **零依赖自检**：`npm run selftest` 跑 `scripts/selftest.mjs`——宿主 schema 与 sanitize、客户端纯逻辑与接线、设置面板渲染冒烟、提示音播放链路、README × 5 一致性，全部为可执行断言，发布前必跑。

---

## 环境要求

| 依赖 | 要求 |
| --- | --- |
| DSH（DeepSeek Harness） | **0.1.x 与 0.2.0-rc.2 起**（当前实测 0.2.0-rc.2）。Web profile 部署；官方 base bundle 默认包含 `@deepseek-ai/dsh-settings` 与会话投影，两代设置契约由插件自动适配，无需配置 |
| cordis | `>=4.0.0-rc <5`（peer dependency，由宿主提供） |
| Node.js | `>=22`（宿主侧） |
| 浏览器 | 支持 Web Notification 则有系统通知；不支持、权限拒绝或被静默时由 toast 兜底 |

两代宿主的**设置契约不同，插件自动适配，无需任何配置**：

| 环境 | 设置命名空间 | 设置面板入口 |
| --- | --- | --- |
| dsh 0.1.x | 插件自选：`session-complete-notify`（`ctx.settings.register`） | 宿主 `settingsScope` → `settings.plugin.item` 槽位 |
| dsh 0.2.0-rc.2 起 | profile entry id：`dsh-session-notify` | 宿主 `configForms` → `plugins.bundle.config` / `plugins.row.config` / `settings.plugins.tab` 槽位 |

> 0.1.x 的代码路径保持不变，但最近一轮改动只在 0.2.0-rc.2 上做过端到端回归；旧宿主上 `schemastery` < 3.18.3 时自动跳过 `volatile()`（0.1.x 不需要它）。

---

## 安装

> [!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 种语言单选，切换即时重渲染整个面板 |
| 失焦时 | 四选一：**不通知**（该时机完全静默）/ 双通道（系统通知 + 页内提示，默认）/ 仅系统通知 / 仅页内提示 —— 窗口失焦（切走标签页、最小化、点到别处）时生效 |
| 聚焦时 | 四选一：同上（默认双通道）—— 窗口聚焦时生效。两路互相独立，可自由组合成「失焦系统通知 + 聚焦不通知」等搭配 |
| 音频 | 折叠区（**默认展开**，点折叠头的 ▾ 可收起）：提示音的音量与时长。展开后是**「音量」+ 滑块同一行**（**「音量」二字在滑块前面**，滑块右侧显示百分比与「试听 / 停止」按钮：0~100%，**内存 0~1**，默认 60%；**试听按当前音量播放**——拖完滑块点一下就能听出效果，**播放中按钮变「停止」，再点一次即停**，未设置任何提示音时按钮禁用）与**「时长」行**（同一行顺序：**标签 → 滑块 → 数值框 → 秒**；滑块量程 **0~15 秒**、步长 **0.1 秒**，拖动即改，也可直接在数值框里输入任意秒数；**默认 0 = 不限制**，设为 N 秒后音频到点仍未播完 → **0.5 秒音量淡出后停止**，音频本身比上限短时自然播完；**只影响播放，上传的音频原样保存、不截断**）。两项写入设置文档的 `volume` / `maxDuration`；第三行是**「全局提示音」**（未设置显示「添加音频」按钮，已设置显示 `WAV · 5 KB` 这样的类型/体积摘要 + 「替换 / 试听 / 清除」），写入 `globalAudio`——**内容没为该状态指定音频时播放它**（优先级：该状态指定的音频 > 全局提示音；都没有则不播放） |
| 通知图片 | 大图两种来源：**按原因上传**——在模板中通过「＋ 插入信息 → 图片」插入 `{image}` 标签并选择本地图片（编辑器内显示为带缩略图的标签，**自动压缩至 512px 宽、按通知显示比例 16:9 居中裁切**，随各原因独立保存）；**全局大图/图标**——两张上传卡片并排一行（**图标在前**，空态 = 圆角矩形 + 号，点击上传；**大图 512×288（16:9 居中裁切）、图标 128×128（1:1 方形居中裁切）**；已上传则卡片显示缩略图，**点击缩略图可全屏查看完整原图（等比未裁切）**，右上角 × 删除）。图标留空用站点默认图标，也可在模板中插入 `{icon}` 标签**按原因指定图标**（优先于全局）。仅系统通知通道生效（页内 toast 为文字卡片），「发送」测试按钮同样生效 |
| 标题 | 折叠区（**默认收起**，点击展开）：**全局推送标题**（所有原因共用，Chip 编辑器——点「＋ 插入信息」插入的信息以**胶囊标签**形式显示，点击胶囊移除；占位提示「通用推送标题，留空时则使用默认标题，优先级低于下方自定义标题」（不可选中/删除）；**通知发送时标题里的信息位（用时/消耗/错误/缓存命中/速度）会替换为实际值，不再显示代码**；留空时各原因用默认标题——完成=任务已完成、出错=任务出错、中止=任务已中止、阻塞=任务被阻塞、上限=任务达到上限、提问=AI 正在向你提问）+ **按原因定制标题**（6 条原因各自输入，每行带「+」插入按钮——可插入信息标签（含「提问」，不含图片/图标），插入到光标处；**优先于全局标题**，留空 = 用全局或语言默认标题） |
| 内容 | 折叠区（**默认收起**，点击展开）；展开后每条结束原因（完成、出错、中止、阻塞、上限、提问）**一行式布局**（原因标签 + Chip 编辑器 + 「+」插入按钮——菜单展开时变「−」+ **发送箭头按钮**，按钮为矩形、垂直居中）：**空模板（默认预设）时编辑器显示默认文案「会话「{title}」已xx，请点击查看。」**，文字 + 内联信息胶囊，光标处插入；`{image}`/`{icon}` 标签**点击缩略图可预览大图、点 × 才删除**（防误删），`{audio}` 标签**点播放图标试听、播放中变成暂停图标、再点即停，点 × 才删除**（同样防误删；删除标签只清掉该原因的音频数据，正文原样保留），其他标签点击移除；**编辑后删空则显示「留空则使用默认文案」占位（不可选中/删除）**；提问行的默认文案为「AI 向你提问：{question}」，`{question}` 会在发送时替换为 AI 的实际提问（插入菜单同样提供「提问」标签，与其他标签同款交互） |
| 跳过子代理会话 | 复选框（保存时一并写入设置文档） |
| 通知权限 | 状态实时显示：已授权（绿）/ 尚未授权（附「请求授权」按钮）/ 已被浏览器屏蔽（附地址栏操作指引）/ 环境不支持 |
| 按原因定制标题 | 折叠区（默认收起）：每个结束原因一个独立标题输入框，留空 = 用全局模板或语言默认标题 |
| 保存 | 写入宿主设置文档（`language` / `templates` / `titleTemplate` / `titleTemplates` / `pushModeBlur` / `pushModeFocus` / `skipSubagents` / `audios` / `globalAudio` / `volume` / `maxDuration`）；**只写真正变化的字段**（其余一律不动），保存期间按钮禁用；保存后显示「点击刷新」链接 |
| 重置 | 一键还原默认值（**语言保留当前选择**，标题/模板/失焦聚焦通道/提示音/时长恢复默认）并立即保存 |

> [!NOTE]
> 「失焦时」「聚焦时」的通道取舍：`dual`（默认）同时弹 Windows 系统通知与页内 toast，toast 是保底通道，防止系统通知被平台静默（专注助手、通知横幅关闭）。两路互相独立，可组合成「失焦时系统通知、聚焦时不打扰」。**老配置不受影响**——设置文档里没有 `pushModeBlur` / `pushModeFocus` 时，两路都沿用旧版单一 `pushMode` 的值。但 **QQ 浏览器等国产 Chromium 壳浏览器会把 `Notification` 渲染成「浏览器内置的页内推送弹窗」**（页面顶部/角落的横幅，不经 Windows 通知中心）——此时 `dual` 会造成页内两个提示（浏览器内置弹窗 + 插件 toast）。这类浏览器请选「仅页内提示」（不再调用 `Notification`，浏览器内置弹窗不会出现，页内只有插件自己的小 toast）；「仅系统通知」模式在 QQ 浏览器无效（它永远渲染为页内弹窗）。设置面板每个原因的「发送」测试按钮同样受此影响——测试通知优先走「聚焦时」那一路，该路为「不通知」时改用「失焦时」那一路，两路都关则回落 `dual`，保证预览总有反馈。

> [!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 方形），按原因独立；正文渲染时剥除，不进会话日志；优先于全局「通知图标」；删除标签时该原因图标数据一并清除 | — |
| `{audio}` | **自定义提示音开关**：从「＋ 插入信息 → 音频」插入并选择本地音频文件（mp3/wav/ogg… **原样读取**，不做压缩、不做截断；>512 KB 会先确认），按原因独立；**默认为空（不设置就没有提示音）**，音量与「时长」由面板「音频」折叠区里的「音量」滑块 / 「时长」行（**滑块 0~15 秒、步长 0.1** + 数值框，**默认 0 = 不限制**，超时 0.5 秒淡出）统一控制，**试听即按当前音量**；**点标签上的播放图标试听、播放中变成暂停图标再点即停**，**点标签上的 × 才删除**（删除只清掉该原因的音频数据与标签，正文不受影响）；正文渲染时剥除，不进会话日志；**同个状态只允许一个**（已插入时菜单按钮置灰，重复标签在保存时自动剥除）。**是否播放由「已保存」的模板决定**（点保存后刷新生效，与面板其他项一致）——插了标签**且为该状态选了音频**才用它；否则播放「音频」区里的**全局提示音**（两者都没有才静音） | — |
| `{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 语法校验
│   ├── selftest.mjs            # 零依赖自检（发布前必跑）：schema / 纯逻辑 / 渲染冒烟 / 播放链路 / README 一致性
│   ├── 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
```

自检（发布前必跑）：宿主 schema / 客户端纯逻辑与接线 / 设置面板渲染冒烟 / 提示音播放链路 / README × 5 一致性：

```bash
npm run selftest
```

发布（发布前自动执行 `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 字） |
| `window.__dsch_notify_debug.alertAudio(action, src, volume, maxSeconds)` | 提示音探针：`play` / `stop` / `toggle` / `state`，返回当前播放音源（'' = 未播放） |

---

## 常见问题

<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}` / `{audio}` 媒体开关同样生效。回答后（`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}` 标签为该原因单独指定图标（优先于全局）。**声音也可以自定义**：在某个状态的「内容」行点「＋ 插入信息 → 音频」，插入 `{audio}` 标签并选择本地音频文件（每个状态各一个，默认为空——不设置就没有提示音），音量与「时长」在面板「音频」折叠区里统一设置（「音量」标签就在滑块前，拖完可直接点「试听」——**试听即按当前音量**；「时长」= 滑块 **0~15 秒**、步长 0.1 + 数值框，**默认 0 = 不限制**，超时 0.5 秒淡出；**上传的音频原样保存、不截断**），通知与页内提示发出时按该音量播放一次；音频标签上的播放图标可随时试听（播放中变成暂停图标，再点即停）。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.23** | 2026-10-04 | **适配 dsh 0.2.0 设置契约 + 全局提示音 + 侧边栏铃铛**：修好 0.2.0 下设置面板整体消失（schema 补 `.volatile()`、槽位改用 `plugins.bundle.config` / `plugins.row.config` / `settings.plugins.tab`、命名空间改用 profile entry id），同时保住 0.1.x 兼容（`volatile()` 探测降级）；「音频」区新增**全局提示音**（内容未为该状态指定音频时兜底，优先级：该状态音频 > 全局 > 静音）；通知大图与图标收进**「图片」折叠区**；侧边栏「设置」右侧新增**铃铛入口**，点开自绘浮层（同一份设置面板，Esc / 遮罩 / × 关闭） |
| **0.1.22** | 2026-09-21 | **自定义提示音**：结束原因模板可插入 `{audio}` 标签并上传本地音频（**原样保存**，不压缩、不截断）；新增「音频」折叠区（**默认展开**）统一设置**音量**（「音量」在滑块前，0~100%、默认 60%、试听 ↔ 停止开关，**试听按当前音量**）与**时长**（标签 → 滑块 → 数值框 → 秒，滑块 **0~15 秒 / 步长 0.1**；**0 = 不限制**，到点未播完 → **0.5 秒音量淡出**后停止）；标签自带**播放 / 暂停图标**（点即试听、再点即停）与**删除 ×**（只清该原因音频数据，正文原样保留）；与 `{image}`/`{icon}` 同口径——正文/标题渲染时剥除、不进会话日志、同状态唯一；**保存只写真正变化的字段**（此前固定 18 次全量写入，大音频下被放大 18 倍；现典型 2~3 次），保存期间按钮禁用 |
| **0.1.21** | 2026-09-14 | **推送通道按失焦/聚焦分流**（[PR #3](https://github.com/TelosmaYLX/dsh-session-notify/pull/3) 由 [@YiHui-Liu](https://github.com/YiHui-Liu) 贡献）：新增「失焦时」「聚焦时」两个独立下拉，各自可选 `不通知` / `双通道` / `仅系统通知` / `仅页内提示`；移除旧的单一「推送方式」设置项，未设置新项时两路自动沿用旧 `pushMode` 值（老配置行为不变）；某时机设为「不通知」时该时机完全静默（提问与审批不记去重，页面切到另一时机后同一次事件仍会提醒；完成属边沿事件，静默即不补发）；「发送」测试通知优先走聚焦那一路，其为「不通知」时改用失焦那一路 |
| **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` 投影 + 客户端三路信号兜底）；[PR #3](https://github.com/TelosmaYLX/dsh-session-notify/pull/3)——推送通道按失焦 / 聚焦分流，两路各含「不通知」。

---

## 贡献

欢迎 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
