@telosmaylx/dsh-session-notify 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,78 +1,150 @@
1
+ <div align="center">
2
+
1
3
  # dsh-session-notify
2
4
 
5
+ **DSH(DeepSeek Harness)会话完成提醒插件 —— 每一轮结束,让完成状态主动找你,而不是你盯着屏幕等。**
6
+
3
7
  [![npm version](https://img.shields.io/npm/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
4
8
  [![npm downloads](https://img.shields.io/npm/dm/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
5
- [![license MIT](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](LICENSE)
9
+ [![license](https://img.shields.io/npm/l/@telosmaylx/dsh-session-notify)](./LICENSE)
10
+ [![node](https://img.shields.io/node/v/@telosmaylx/dsh-session-notify)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
11
+ [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-4D6BFE)](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/TelosmaYLX/dsh-session-notify/pulls)
6
13
 
7
- **DeepSeek Harness plugin that notifies you when a session finishes** — a durable system message is appended into the session log (plugin-source notice, persisted to JSONL), and the browser gets a real push (Web Notification + toast). Which session, how long it took, tokens, cache-hit rate and generation speed — all customizable from the official settings panel.
14
+ 每轮对话结束时,把「已完成 / 出错 / 被阻塞 / 达到上限」连同用时、token 消耗写入会话日志,并推送浏览器系统通知与页内 toast。内置 5 种语言、可视化文案模板编辑器、自定义预设库,缓存命中率与生成速度取自官方投影,与状态栏同口径。
8
15
 
9
- ```bash
10
- dsh plugin --profile web add @telosmaylx/dsh-session-notify
11
- ```
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## 目录
21
+
22
+ - [功能特性](#功能特性)
23
+ - [环境要求](#环境要求)
24
+ - [安装](#安装)
25
+ - [卸载](#卸载)
26
+ - [快速开始](#快速开始)
27
+ - [通知行为](#通知行为)
28
+ - [触发条件](#触发条件)
29
+ - [推送正文从哪来](#推送正文从哪来)
30
+ - [通知示例](#通知示例)
31
+ - [通知权限](#通知权限)
32
+ - [配置](#配置)
33
+ - [设置面板](#设置面板)
34
+ - [文案模板与占位符](#文案模板与占位符)
35
+ - [预设系统](#预设系统)
36
+ - [宿主配置项](#宿主配置项)
37
+ - [工作原理](#工作原理)
38
+ - [项目结构](#项目结构)
39
+ - [开发与调试](#开发与调试)
40
+ - [常见问题](#常见问题)
41
+ - [更新日志](#更新日志)
42
+ - [贡献](#贡献)
43
+ - [相关链接](#相关链接)
44
+ - [许可证](#许可证)
45
+
46
+ ---
12
47
 
13
- - **Dual channel**: host system message + browser push (each notification has its own tag; toast always shows as a safety net)
14
- - **Official settings panel** (`设置 → 插件配置 → 会话完成提醒`): preset library, title template, per-reason message templates with insertable tags, live preview
15
- - **5 languages** (zh / zh-tw / en / ja / ko) for both the panel and the generated messages
16
- - **Real metrics** from official projections: cache hit rate (`tokenUsage`), tok/s decode speed (`sessionStats`), session title (`title`) — same source as dsh-web-ui
17
- - **Zero build deps**: hand-written ESM host + official `window.__ModuleLoader__` client bundle; verified end-to-end with headless Chrome probes included in `scripts/`
48
+ ## 功能特性
49
+
50
+ ### 三通道提醒,一条不漏
51
+
52
+ | 通道 | 形式 | 说明 |
53
+ | --- | --- | --- |
54
+ | 会话内系统消息 | 可折叠提示行 | 每轮结束把结束原因与用时、消耗作为插件来源的系统消息追加进会话日志,随 JSONL 落盘,恢复或回放会话后依然可见。 |
55
+ | 浏览器系统通知 | Web Notification | 原生弹窗。每次完成事件使用独立 `tag`(`dsh-session-notify:<timestamp>`),不与前一次互相替换,也不被折叠成一个分组条目;点击通知聚焦回窗口。 |
56
+ | 页内 toast | 右下角浮动弹窗 | 永远展示的保底通道:系统通知被平台静默、权限拒绝或环境不支持时仍有可见反馈。同屏最多 3 条(超出移除最旧),10 秒自动消失,点击关闭。 |
57
+
58
+ ### 后台会话全覆盖
59
+
60
+ - 宿主为所有会话(含后台、未打开窗口的)维护「最近一条通知正文」的会话投影单元(key = `session-complete-notify`),推送正文跨会话一致,不依赖你恰好开着那个窗口。
61
+ - 客户端从会话列表快照观测所有会话的 `running` 位,`true → false` 边沿即触发推送,与官方 sidebar 提醒同策略(首次观测只记录基线,已在 idle 的会话不补发)。
18
62
 
19
- > 中文完整文档见下文。An English section follows the Chinese README below — see 功能特性/安装/使用/标签 tables for details.
63
+ ### 可定制到每一句话
20
64
 
21
- > DeepSeek Harness 会话完成提醒插件:**会话完成时,在会话内写一条系统消息,并向浏览器推送通知**(哪个会话、耗时、token、缓存命中率、生成速度,全部可自定义)。
65
+ - **5 种语言**:简体中文、繁體中文、English、日本語、한국어 —— 通知文案、时长与用量措辞、设置面板界面全部随语言切换(切换即时重渲染)。
66
+ - **可视化模板编辑器**(Chip 胶囊编辑器):动态信息渲染为内联胶囊(占位符代码不露出),「+ 插入信息」在光标处插入(可插到文字中间),点击胶囊移除,每栏带实时预览(信息以示例值流入正文)。
67
+ - **预设系统**:内置「默认」预设作为基线;当前配置可另存为自定义预设(`localStorage` 持久化),支持自动编号的未命名预设(`未命名`、`未命名 2`…)、「来自:xxx · 已修改」来源指示、删除预设。
68
+ - **推送标题模板**:`{title}` 引用会话标题,留空则直接使用会话标题。
22
69
 
23
- - ✅ 双通道提醒:**宿主系统消息**(写入会话日志,随 JSONL 持久化)+ **浏览器推送**(Web Notification + 页内 toast)
24
- - ✅ 官方设置面板:**设置 → 插件配置 → 会话完成提醒**(预设 / 语言 / 标题模板 / 消息模板 / 标签插入)
25
- - 5 语言界面与文案:简体中文 / 繁體中文 / English / 日本語 / 한국어
26
- - 零外部依赖打包:宿主插件为手写 ESM(无构建步骤),客户端 bundle 为官方 `window.__ModuleLoader__` 契约
27
- - 全链路实测:无头 Chrome 端到端验证(落盘 / 渲染 / 推送),仓库附带探针脚本
70
+ ### 与官方口径同源
71
+
72
+ - **缓存命中率**取自官方 `tokenUsage` 投影:缓存读 /(未缓存输入 + 缓存读 + 缓存写)。
73
+ - **生成速度**取自官方 `sessionStats` 投影:输出 token ÷ 解码耗时。
74
+ - 两者与 dsh-web-ui 状态栏完全同口径,不含排队、准备、工具时间;投影不可用或数据未就绪时自动退回本地用量聚合估算。
75
+
76
+ > [!NOTE]
77
+ > 缓存命中率与速度只在自定义模板中通过 `{cache}`、`{tps}` 占位符插入时才显示。使用内置默认文案时,正文只含用时与消耗。
78
+
79
+ ### 工程质量
80
+
81
+ - **只响应实时事件**:resume、replay 不重放旧通知,加载会话不刷屏。
82
+ - **自免疫循环**:插件追加的消息类型(`user/message`)与自身监听目标(`turn/*`)不相交。
83
+ - **零外部依赖**:宿主平面零裸 import,UserMessage 按 `dsh-llm` 的 `createUserMessage` 契约手工构造;纯逻辑层(`lib/core.js`)零依赖,可独立测试。
84
+ - **Cordis effect 纪律**:重试定时器包装在 `ctx.effect()` 中并返回 `clearTimeout` disposer,注册随 fiber 卸载自动撤销,HMR 热重载安全。
85
+ - **安装即挂载**:声明官方 `dsh.bundle` manifest,`dsh plugin add` 一条命令装完即用,无需手写 patch。
28
86
 
29
87
  ---
30
88
 
31
- ## 功能特性
89
+ ## 环境要求
32
90
 
33
- | 能力 | 说明 |
91
+ | 依赖 | 要求 |
34
92
  | --- | --- |
35
- | 完成提醒 | 监听 `session/event` 火线,`turn/end`(`completed/aborted/blocked/error/max-tokens`)时触发 |
36
- | 系统消息 | 以 plugin-source `user/message`(`form: 'notice'`)写入会话日志:UI 渲染为可折叠系统行(折叠态即显示正文渲染结果,含 `{title}`/用时/消耗,截断至 120 字符),随 JSONL 持久化,resume/replay 可见 |
37
- | 浏览器推送 | `running → idle` 边沿检测:Web Notification(每次独立 tag,不折叠)+ **toast 永远展示**(保底,同屏最多 3 条) |
38
- | 详情指标 | 用时 / token 用量 / **缓存命中率** / **速度 tok/s** —— 数据来自官方投影(`tokenUsage`、`sessionStats`),与 dsh-web-ui 状态栏同源 |
39
- | 会话标题 | `{title}` 标签:推送标题与消息正文都可插入会话标题(官方 `title` 投影) |
40
- | 设置面板 | 预设库(默认 + 自定义预设自动管理 / 命名预设)、标题模板、5 字段消息模板、5 语言、标签插入(光标处)、实时预览 |
41
- | 多语言 | 消息文案(标签/时长/用量/默认文案)+ 面板 UI 全量 i18n:`zh` `zh-tw` `en` `ja` `ko` |
42
- | 子代理过滤 | 默认跳过子代理会话(子代理由父会话编排) |
43
- | 投影同步 | 注册 `session-complete-notify` 投影单元:每个会话的最新通知全文推送到客户端,**后台会话同样有完整正文** |
93
+ | DSH(DeepSeek Harness) | Web profile 部署。官方 base bundle 默认包含 `@deepseek-ai/dsh-settings`(设置命名空间)与会话投影,无需额外配置 |
94
+ | cordis | `>=4.0.0-rc <5`(peer dependency,由宿主提供) |
95
+ | Node.js | `>=22`(宿主侧) |
96
+ | 浏览器 | 支持 Web Notification 则有系统通知;不支持、权限拒绝或被静默时由 toast 兜底 |
44
97
 
45
98
  ---
46
99
 
47
100
  ## 安装
48
101
 
49
- ### 方式一:npm 包(推荐,一条命令安装即自动挂载)
102
+ > [!WARNING]
103
+ > 裸 `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`)并自动应用。
104
+
105
+ ### 方式一:dsh plugin add(推荐)
106
+
107
+ 安装包的同时自动应用 `cordis.patch.yml`,把插件挂载进 profile 装配(host 事件订阅 + client 启动图注入)。
50
108
 
51
109
  ```bash
52
110
  dsh plugin --profile web add @telosmaylx/dsh-session-notify
53
111
  ```
54
112
 
55
- 本插件按官方协议在 `package.json` 声明了 **`dsh.bundle` manifest**(`dsh.bundle.patch` → 仓库根目录 `cordis.patch.yml`):`dsh plugin add` 在安装包的同时自动执行该 patch,把插件挂载进 profile 装配(host 事件订阅 + client 启动图注入)——**装完刷新浏览器即可,无需手写 cordis.patch.yml,无需 dev_install_package**。
113
+ ### 方式二:从 GitHub 仓库安装
56
114
 
57
- > ⚠️ 裸 `npm install @telosmaylx/dsh-session-notify` 只是把包装进 node_modules(当作普通依赖),**不会**向 DSH 注册插件——这是 DSH 生态的官方约定。挂载必须经由 `dsh plugin add`(或下方方式二/三/四)。
115
+ ```bash
116
+ dsh plugin add github:TelosmaYLX/dsh-session-notify
117
+ ```
58
118
 
59
- ### 方式二:GitHub 仓库
119
+ 也可以在 DSH Web GUI 会话内执行:
60
120
 
61
121
  ```bash
62
- # 在 Web GUI 会话中执行
63
122
  dev_install_package github=TelosmaYLX/dsh-session-notify
64
- # 或
65
- dsh plugin --profile web add git+https://github.com/TelosmaYLX/dsh-session-notify.git
66
123
  ```
67
124
 
68
- ### 方式三:bundle 热装配(本地开发,dsh-super-injector)
125
+ ### 方式三:本地目录热装配(开发用)
126
+
127
+ 把路径换成你的克隆目录,在 DSH Web GUI 会话内执行:
128
+
129
+ ```bash
130
+ dev_install_package dir=/你的/克隆目录/dsh-session-notify
131
+ ```
132
+
133
+ ### 方式四:npm 包手动安装
134
+
135
+ 先打包:
69
136
 
70
137
  ```bash
71
- # Web GUI 会话中执行(dir 换成你的克隆目录)
72
- dev_install_package dir=</你的插件目录>
138
+ npm pack @telosmaylx/dsh-session-notify
73
139
  ```
74
140
 
75
- ### 方式四:cordis patch(手动装配,重启后由 bundles 装配,与热装配双路径一致)
141
+ 解压后指定目录安装(在 DSH Web GUI 会话内执行):
142
+
143
+ ```bash
144
+ dev_install_package dir=/解压/目录/package
145
+ ```
146
+
147
+ ### 方式五:手动 cordis patch(不依赖安装器)
76
148
 
77
149
  在 `~/.dsh/profiles/web/cordis.patch.yml` 追加:
78
150
 
@@ -83,120 +155,391 @@ dev_install_package dir=</你的插件目录>
83
155
  config: {}
84
156
  ```
85
157
 
86
- > 装配要求:`@deepseek-ai/dsh-settings`(设置命名空间)、`@deepseek-ai/dsh-session-projection`(投影)在部署中启用(官方 base bundle 默认包含)。
158
+ > [!IMPORTANT]
159
+ > 无论用哪种方式,装完都需要**刷新一次浏览器页面** —— 客户端 bundle 通过 `__DSH_BOOT__` 启动图注入。
87
160
 
88
- 安装后**刷新浏览器页面**一次(客户端 bundle 通过 `__DSH_BOOT__` 启动图注入)。
161
+ ## 卸载
89
162
 
90
- ---
163
+ 一条命令移除插件及其挂载(自动从 `cordis.patch.yml` 移除 insert 条目):
91
164
 
92
- ## 使用
165
+ ```bash
166
+ dsh plugin --profile web remove @telosmaylx/dsh-session-notify
167
+ ```
93
168
 
94
- 1. 打开 **设置 → 插件配置 → 会话完成提醒**
95
- 2. 配置(见下)→ **保存** 状态位提示 `已保存 ✓ · 刷新页面后生效` → 点击 **刷新**
96
- 3. 任一会话(顶部会话)完成时:
97
- - 会话尾部出现系统消息(文案 = 你的模板)
98
- - 右下角 toast 弹出(标题 = 你的标题模板 + 会话标题)
99
- - 授权后系统通知同步弹出
169
+ > [!NOTE]
170
+ > 手动安装(方式四/五)的用户,需同步从 `~/.dsh/profiles/web/cordis.patch.yml` 删除对应 insert 条目,再刷新页面。
100
171
 
101
- ### 设置面板字段
172
+ ### 卸载时自动清理的内容
102
173
 
103
- | 字段 | 说明 |
174
+ 插件实现了完整的生命周期收尾(Cordis effect 纪律),卸载/禁用/HMR 热重载时:
175
+
176
+ | 平面 | 自动释放的资源 |
104
177
  | --- | --- |
105
- | **预设(载入)** | 下拉显示当前应用的预设;选预设 = 载入其全部配置(语言 + 标题 + 5 模板)。内置仅「**默认**」 |
106
- | **新增** | 将当前配置另存为**命名**自定义预设 |
107
- | **修改 / 删除** | 选中自定义预设时出现:修改 = 当前表单写回该预设;删除 = 移除 |
108
- | **语言** | 简体中文 / 繁體中文 / English / 日本語 / 한국어 —— 面板与消息文案即时切换 |
109
- | **推送标题** | 占位符提示「留空则使用会话标题」;`{title}` 标签经「+ 会话标题」插入,如 `【完成】{title}` |
110
- | **模板 × 5** | 完成 / 出错 / 中止 / 阻塞 / 输出上限:文字 + 内联标签胶囊(点击 × 删除、+ 光标处插入、全选可删);空态直接展示默认文案(始终可编辑) |
111
- | 预览 | 纯文本最终效果(示例值直出,无标签样式) |
178
+ | host | `session/event` 事件订阅、settings 命名空间、会话投影单元、设置注册重试定时器(`ctx.effect` 包装);置卸载标志抑制已调度的微任务追加 |
179
+ | client | 会话列表订阅、完成推送正文的轮询定时器、`window.__dsch_notify_debug` 调试钩子(按引用删除,防闭包泄漏)、页内 toast 容器 DOM |
112
180
 
113
- ### 标签(可插入信息)
181
+ ### 卸载后保留的数据
114
182
 
115
- | 标签 | 胶囊文字 | 内容 | 示例 |
116
- | --- | --- | --- | --- |
117
- | `{title}` | 会话标题 | 会话标题(官方 title 投影) | DeepSeek Harness 插件开发 |
118
- | `{duration}` | 用时 | 本轮耗时(语言化格式) | 1 分 23 秒 / 1m23s |
119
- | `{usage}` | 消耗 | 输入 / 输出 token | 1,600 输入 / 3,560 输出 |
120
- | `{error}` | 错误 | 错误详情;无错误显示 `none` | 连接超时 |
121
- | `{cache}` | 缓存命中 | 缓存命中率(官方 tokenUsage 四桶口径) | 96.5% |
122
- | `{tps}` | 速度 TPS | 输出 token ÷ 解码耗时(官方 sessionStats 口径) | 115 tok/s |
123
-
124
- ### 预设模型(自动生命周期)
125
-
126
- - **默认预设**:空模板 + 空标题(= 内置默认文案)
127
- - **保存 = 自动落入一个预设**:
128
- - 来自自定义预设 → 更新它(修改即自动,无需手动按钮)
129
- - 来自默认/空白 → 新建 **未命名预设**;已存在则递增编号(未命名预设 2 / 3 …),从不覆盖旧的
130
- - 从未命名预设本身载入再改 → 更新该未命名预设
131
- - 预设库持久化在浏览器 localStorage(`dsh-scn-custom-presets`)
183
+ - **设置配置**(语言、文案模板)留在 settings 文档,重装后自动恢复;
184
+ - **自定义预设**存于浏览器 `localStorage`(`dsh-scn-custom-presets`),重装后仍在;
185
+ - 历史会话中已追加的系统消息与 JSONL 日志**不会**被回滚(它们是会话数据的一部分,与官方侧边栏提示同语义)。
132
186
 
133
187
  ---
134
188
 
135
- ## 工作原理
189
+ ## 快速开始
190
+
191
+ 1. 按上面任一方式安装并刷新页面。
192
+ 2. 发起任意一轮对话,等它结束 —— 右下角弹出 toast、浏览器弹系统通知、会话日志里出现可折叠的系统提示行。
193
+ 3. 首次收到完成事件时,浏览器会请求通知权限(每页只问一次),允许后后续完成都有系统通知。
194
+ 4. 打开 **设置 → 插件 → 会话完成提醒**,切换语言、编辑文案模板、另存预设。保存后点「点击刷新」让宿主与客户端两侧重新读取,新配置即生效。
136
195
 
137
- ### 生命周期
196
+ 刚装好时,会话日志里会出现这样一行可折叠提示:
138
197
 
198
+ ```text
199
+ 会话已完成(用时 1 分 12 秒,消耗 1,240 输入 / 3,560 输出)。
139
200
  ```
140
- 用户会话 turn/end
141
- └─ host lib/index.js(session/event 监听)
142
- ├─ tracker:turn/start 起表,assistant/message 累计用量
143
- ├─ 官方投影快照:tokenUsage → 缓存命中率;sessionStats → tok/s;title → 会话标题
144
- ├─ buildNotice(kind, reason, 数据, {language, templates}) → 多语言/模板渲染
145
- ├─ queueMicrotask → session.append('user/message', plugin-notice, {surfaceOp:'append'})
146
- └─(投影单元自动更新:每个会话的最新通知全文)
147
- └─ client lib/client.js(sessions.list running→idle 边沿)
148
- ├─ 正文:投影(每个会话都有全文)→ 事件窗口 notice 节点 → 降级
149
- ├─ 标题:用户标题模板({title} = 会话标题;留空 = 会话标题)
150
- └─ 通知:独立 tag(不折叠)+ toast 永远展示(保底)
201
+
202
+ ---
203
+
204
+ ## 通知行为
205
+
206
+ ### 触发条件
207
+
208
+ 每轮对话结束(`turn/end`)时按结束原因判断,命中白名单即提醒:
209
+
210
+ | 结束原因 | 含义 | 默认 |
211
+ | --- | --- | --- |
212
+ | `completed` | 会话正常完成 | 提醒 |
213
+ | `aborted` | 会话中止 | 提醒 |
214
+ | `blocked` | 会话被阻塞 | 提醒 |
215
+ | `error` | 会话出错(附错误详情,超长截断) | 提醒 |
216
+ | `max-tokens` | 达到输出 token 上限 | 提醒 |
217
+ | `interrupted` | 中断(崩溃恢复后由持久化后端补写的孤儿轮次关闭标记) | 不提醒(可配置加入) |
218
+
219
+ **子代理会话默认跳过**(`header.origin === 'subagent'` 或 `delegationDepth > 0`)—— 子代理由父会话编排,逐轮提醒是噪音;可在宿主配置关闭跳过。
220
+
221
+ ### 推送正文从哪来
222
+
223
+ 客户端在会话列表观测到 `running: true → false` 边沿时推送,正文按以下优先级获取(最长轮询 6 秒,400ms 间隔):
224
+
225
+ 1. **宿主投影**(key = `session-complete-notify`)—— 每个会话都有,后台会话同样拿到全文;
226
+ 2. **会话事件窗口里的 notice 节点**(`kind=context` + `form=notice`)—— 正在查看的会话,落盘后立即可用;
227
+ 3. **降级** —— 「详情见会话内系统消息」+ 工作区信息(`cwd` 最后一段)。
228
+
229
+ ### 通知示例
230
+
231
+ 以下均由 `lib/core.js` 的 `buildNotice` 实际生成。
232
+
233
+ 简体中文默认文案:
234
+
235
+ ```text
236
+ 会话已完成(用时 3 分 25 秒,消耗 12,400 输入 / 35,600 输出)。
151
237
  ```
152
238
 
153
- ### 关键设计取舍
239
+ 出错时附带错误详情(单行化,超过 40 字符截断):
154
240
 
155
- | 取舍 | 说明 |
156
- | --- | --- |
157
- | 追加必须微任务延迟 | `session/event` 观察者回调运行在 `turn/end` 那次 append 的发布边界内,同步重入会被拒绝(`"session append cannot reenter…"`)——`queueMicrotask` 在边界复位后执行 |
158
- | 指标不存插件内存 | 缓存命中/速度/标题取自官方投影(宿主维护、无内存状态,热重载不丢)——与 dsh-web-ui 状态栏同源 |
159
- | 客户端零构建 | 手写 ESM;宿主经 `createRequire` 锚定 profile 共享依赖枢纽取 schemastery/zod(从仓库目录以 realpath 加载时裸导入不可用) |
160
- | 双通道解耦 | 系统消息(宿主,持久)与 push(客户端,即时)各自独立;正文以投影通知全文为准保证跨会话一致 |
161
- | 标签即开关 | 模板里插了标签就显示、不插就不显示(无独立开关) |
162
- | 注册竞态兜底 | 设置命名空间注册遇 duplicate(热重载下的旧 fiber 注销竞态)自动退避重试 8 次并落盘日志 |
241
+ ```text
242
+ 会话出错:connection timeout(用时 12 秒)。
243
+ ```
244
+
245
+ English 默认文案:
163
246
 
164
- ### 多语言
247
+ ```text
248
+ Session completed (took 3m25s, used 12,400 in / 35,600 out).
249
+ ```
250
+
251
+ 自定义模板(在设置面板编辑,本例用到全部信息位):
252
+
253
+ ```text
254
+ {title} 干完了!用时 {duration},消耗 {usage},缓存命中 {cache},速度 {tps}
255
+ ```
256
+
257
+ 渲染结果:
258
+
259
+ ```text
260
+ 重构登录模块 干完了!用时 3 分 25 秒,消耗 103,600 输入 / 35,600 输出,缓存命中 96.5%,速度 92 tok/s
261
+ ```
262
+
263
+ 五种语言的同一事件:
264
+
265
+ ```text
266
+ 会话已完成(用时 3 分 25 秒,消耗 1,240 输入 / 3,560 输出)。
267
+ 會話已完成(用時 3 分 25 秒,消耗 1,240 輸入 / 3,560 輸出)。
268
+ Session completed (took 3m25s, used 1,240 in / 3,560 out).
269
+ セッション完了(所要 3 分 25 秒、消費 1,240 入力 / 3,560 出力)。
270
+ 세션 완료(소요 3분 25초, 소모 1,240 입력 / 3,560 출력)。
271
+ ```
165
272
 
166
- - **宿主消息**:标签、时长(`1 分 23 秒` / `1m23s` / `1分 23초`…)、用量措辞、默认文案骨架、错误分隔符随语言
167
- - **面板 UI**:标题/字段/预设/按钮/提示/标签名/预览示例全量 i18n(约 45 串 × 5)
273
+ ### 通知权限
274
+
275
+ | 权限状态 | 行为 |
276
+ | --- | --- |
277
+ | `default`(未决定) | 每页加载只发起一次授权请求,后续完成事件不再触发询问 |
278
+ | `granted` | 每次完成都发系统通知(独立 tag,互不覆盖) |
279
+ | `denied` 或环境不支持 | 仅 toast(永远展示,保底可见) |
168
280
 
169
281
  ---
170
282
 
171
- ## 脚本(验证工具)
283
+ ## 配置
284
+
285
+ 绝大多数配置在 **DSH Web UI → 设置 → 插件 → 会话完成提醒** 面板完成(保存后点「点击刷新」生效)。仅「触发原因白名单」与「跳过子代理」两项在宿主 `cordis.patch.yml` 的 `config` 中配置。
286
+
287
+ ### 设置面板
288
+
289
+ 面板在官方「设置 → 插件」面板中注册(`settings.plugin.item` keyed slot,key = `session-complete-notify`),样式逐值复刻原生插件卡片(12px 圆角、展开收起、旋转 chevron、footer 状态位 + 弃置 ghost + 主色保存按钮):
172
290
 
173
- | 脚本 | 用途 |
291
+ | 区域 | 内容 |
174
292
  | --- | --- |
175
- | `scripts/verify-notice.mjs` | 解压多帧 zstd 会话日志,核验系统消息落盘(plugin-source notice)+ turn/end 触发点 |
176
- | `scripts/probe-client.mjs` | 客户端激活冒烟(boot graph / 模块加载 / 报错) |
177
- | `scripts/probe-client-e2e.mjs` | 端到端推送实测:无头 Chrome 等待真实完成事件,抓取 toast 内容(可钉会话窗口:`node scripts/probe-client-e2e.mjs 900000 <sessionId>`) |
178
- | `scripts/probe-card-render.mjs` | 无头完整流程:设置 插件 卡片渲染 + 控制台错误 |
179
- | `scripts/probe-settings-card.mjs` | 设置面板卡片检测(DOM 文本) |
293
+ | 预设 | 下拉选择内置或自定义预设;「新增」把当前配置另存为自定义预设;当前预设可「删除」 |
294
+ | 语言 | 5 种语言单选,切换即时重渲染整个面板 |
295
+ | 推送标题 | `{title}` 引用会话标题,留空则直接用会话标题 |
296
+ | 模板 × 5 | 每条结束原因(完成、出错、中止、阻塞、输出上限)独立一个 Chip 编辑器:文字 + 内联信息胶囊,光标处插入、点击移除、实时预览 |
297
+ | 跳过子代理会话 | 复选框(保存时一并写入设置文档) |
298
+ | 保存 | 写入宿主设置文档(`language` / `templates` / `titleTemplate`);保存后显示「点击刷新」链接 |
299
+
300
+ > [!NOTE]
301
+ > 面板中「跳过子代理会话」保存的是设置文档里的布尔值;宿主 `cordis.patch.yml` 的 `config.skipSubagents` 是其启动默认值,两者任一为真即跳过。
302
+
303
+ ### 文案模板与占位符
304
+
305
+ 每条结束原因独立一个模板输入框,**标签即开关** —— 在模板里插入对应信息标签,该项数据才会显示:
306
+
307
+ | 占位符 | 含义 | 示例值 |
308
+ | --- | --- | --- |
309
+ | `{title}` | 会话标题(推送标题模板也可用) | `重构登录模块` |
310
+ | `{duration}` | 本轮用时(`turn/start` 起表 → `turn/end` 结束) | `3 分 25 秒` / `3m25s` |
311
+ | `{usage}` | token 消耗(输入 = 未缓存 + 缓存读 + 缓存写) | `1,240 输入 / 3,560 输出` |
312
+ | `{error}` | 错误信息(无错误时显示 `none`;单行化,80 字符截断) | `connection timeout` |
313
+ | `{cache}` | 缓存命中率(官方投影口径,无数据为空) | `96.5%` |
314
+ | `{tps}` | 生成速度(官方投影口径,无数据为空) | `92 tok/s` |
315
+ | `{label}` | 已废弃 —— 渲染时自动剥除,旧模板仍兼容(插入菜单已移除该选项) | — |
316
+
317
+ 模板留空即使用内置默认文案(自动带用时与消耗)。折叠行 `summary` 与正文同源(渲染结果截断至 120 字符)—— 只看折叠行的用户也能看到真实标题与用时、消耗。
318
+
319
+ ### 预设系统
320
+
321
+ - **内置预设**:仅「默认」,作为基线。
322
+ - **自定义预设**:保存在 `localStorage`(key = `dsh-scn-custom-presets`):
323
+ - 「新增」命名后保存为自定义预设;保存后可「修改」自动同步、「删除」移除;
324
+ - **自动编号的未命名预设**:从「默认 / 空白」直接保存时,自动生成 `未命名`、`未命名 2`、`未命名 3`…(编号取当前最大值 + 1);
325
+ - 表单显示「来自:xxx · 已修改」来源指示(来自预设但内容已改动时)。
326
+ - **保存即同步**:保存时若表单来源是自定义预设则更新该预设,否则新建或继续编号未命名预设。
327
+
328
+ ### 宿主配置项
329
+
330
+ ```yaml
331
+ - insert:
332
+ - id: dsh-session-notify
333
+ name: '@telosmaylx/dsh-session-notify'
334
+ config:
335
+ reasons: [completed, aborted, blocked, error, max-tokens]
336
+ skipSubagents: true
337
+ ```
338
+
339
+ | 字段 | 类型 | 默认值 | 说明 |
340
+ | --- | --- | --- | --- |
341
+ | `reasons` | `string[]` | `[completed, aborted, blocked, error, max-tokens]` | 触发提醒的 `turn/end` 原因白名单 |
342
+ | `skipSubagents` | `boolean` | `true` | 跳过子代理会话(`origin=subagent` 或 `delegationDepth>0`) |
343
+
344
+ ---
345
+
346
+ ## 工作原理
347
+
348
+ 插件分**宿主平面**(Node)与**客户端平面**(浏览器),中间靠会话日志(JSONL)与官方会话投影衔接:
349
+
350
+ ```text
351
+ ┌─────────────────── 宿主平面(lib/index.js,Node)──────────────────┐
352
+ │ │
353
+ │ session/event 火线 │
354
+ │ ├─ turn/start → tracker 起表(key: sessionId:turn) │
355
+ │ ├─ assistant/message → 累加该轮 token 用量 │
356
+ │ └─ turn/end → reason.kind ∈ reasons ? │
357
+ │ ├─ 子代理会话?跳过 │
358
+ │ ├─ 读官方投影:cache / tps / title │
359
+ │ ├─ 按语言+模板构建通知(summary ≤120 字) │
360
+ │ └─ queueMicrotask 追加系统消息 │
361
+ │ (避开 append 重入窗口) │
362
+ │ │
363
+ │ settings.register → 官方「设置 → 插件」命名空间(失败退避重试) │
364
+ │ sessionProjections → 注册投影单元(key=session-complete-notify) │
365
+ └──────────────────────────────┬──────────────────────────────────────┘
366
+ │ user/message (source: plugin, form: notice)
367
+ ▼ JSONL 持久化 + 投影推送
368
+ ┌─────────────────── 客户端平面(lib/client.js,浏览器)──────────────┐
369
+ │ │
370
+ │ 会话列表订阅:running true → false 边沿 → pushCompletion │
371
+ │ ├─ 取正文:投影 → 事件窗口 notice → 降级(轮询 ≤6s) │
372
+ │ ├─ Web Notification(独立 tag,点击聚焦) │
373
+ │ └─ 页内 toast(永远展示,≤3 条,10s 自动消失) │
374
+ │ │
375
+ │ slots.inject('settings.plugin.item') → 设置卡片(预设/语言/模板) │
376
+ └─────────────────────────────────────────────────────────────────────┘
377
+ ```
378
+
379
+ ### 关键设计决策
380
+
381
+ - **不重放**:只处理实时事件,resume、replay 不会补发历史通知。
382
+ - **无自我循环**:插件追加 `user/message`,自身只监听 `turn/*`,事件类型不相交。
383
+ - **零外部 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 快照阶段完成)。
384
+ - **append 重入规避**:`session/event` 观察者回调运行在 `turn/end` 那次 append 的发布边界之内(dsh-session 在 dispatch 前置 `entry.appending`、`finally` 复位),同步 append 会被拒绝 —— 因此推迟到 `queueMicrotask`(微任务在本次同步栈含 `finally` 复位之后才执行)。
385
+ - **effect 纪律**:设置注册的退避重试定时器包装在 `ctx.effect()` 中并返回 `clearTimeout` disposer —— 插件在重试窗口内被卸载或热重载时定时器随 fiber 拆除,不会对已释放的 ctx 触发注册(极老环境无 `ctx.effect` API 时退化为裸定时器 + ctx 已拆除兜底捕获)。
386
+ - **HMR 安全**:`core.js` 导入带 `?v=1` 缓存破坏(HMR 重载按 URL 键控);设置注册遇到热重载竞态(duplicate)时自动退避重试(最多 8 次,间隔 `400ms × attempts`)。
387
+ - **投影注册双轨**:优先 `ctx.root.get('sessionProjections')`(最靠近宿主根的一份),拿不到时回退注入实例;只注册进注入实例时客户端可能读不到投影单元,推送正文走降级路径 —— 属尽力而为,不影响会话内系统消息。
180
388
 
181
389
  ---
182
390
 
183
391
  ## 项目结构
184
392
 
393
+ ```text
394
+ dsh-session-notify/
395
+ ├── lib/
396
+ │ ├── index.js # 宿主平面(Node):session/event 订阅 → 系统消息落盘;
397
+ │ │ # settings 命名空间注册(schemastery schema,退避重试);
398
+ │ │ # sessionProjections 投影单元(后台会话推送正文)
399
+ │ ├── core.js # 纯逻辑层(零依赖,可独立测试):轮次计时与用量聚合、
400
+ │ │ # 5 语言文案表、时长/用量/缓存/速度格式化、
401
+ │ │ # 模板渲染({title}{duration}{usage}{error}{cache}{tps})
402
+ │ └── client.js # 浏览器平面:完成推送(系统通知 + toast)、
403
+ │ # 设置卡片(Chip 模板编辑器 + 预设系统 + 实时预览)
404
+ ├── scripts/
405
+ │ ├── build.sh # 零构建:仅 node --check 语法校验
406
+ │ ├── verify-notice.mjs # 校验会话日志落盘证据(zstd 多帧逐帧解压)
407
+ │ ├── probe-client.mjs # 探针:客户端装配
408
+ │ ├── probe-client-e2e.mjs # 探针:客户端端到端
409
+ │ ├── probe-card-render.mjs # 探针:设置卡片渲染
410
+ │ ├── probe-settings-card.mjs # 探针:设置面板卡片
411
+ │ ├── probe-settings-check.mjs# 探针:设置面板检查
412
+ │ └── probe-diag-settings.mjs # 探针:settings 诊断
413
+ ├── cordis.patch.yml # dsh.bundle manifest —— dsh plugin add 自动挂载的凭证
414
+ ├── package.json # dsh.bundle(patch)+ dsh.client(web 注入)双 manifest;
415
+ │ # exports: "." / "./client" / "./core"
416
+ ├── LICENSE # MIT
417
+ └── README.md # 本文档
185
418
  ```
186
- lib/index.js # 宿主:事件订阅 / 计时 / 设置命名空间(5 语言 schema)/ 投影注册 / 通知构建 + 追加
187
- lib/core.js # 纯逻辑:计时/用量聚合/命中率/tps/多语言标签/模板渲染(可独立单测)
188
- lib/client.js # 浏览器:推送(通知 + toast)/ 设置卡片(ChipEditor + 预设库 + 标题模板)
189
- scripts/build.sh # 构建(node --check 校验手写 ESM)
190
- scripts/*.mjs # 验证/探针脚本(见上表)
419
+
420
+ ---
421
+
422
+ ## 开发与调试
423
+
424
+ 语法校验(零构建,`prepublishOnly` 同款检查):
425
+
426
+ ```bash
427
+ npm run build
191
428
  ```
192
429
 
193
- ## 开发提示
430
+ 发布(发布前自动执行 `prepublishOnly` 语法校验):
431
+
432
+ ```bash
433
+ npm publish --registry=https://registry.npmjs.org --access public
434
+ ```
435
+
436
+ 离线校验:解出会话日志中所有 plugin-source 事件与 `turn/end` 尾部序列(不传路径则自动选 `~/.dsh/sessions` 下最新会话):
437
+
438
+ ```bash
439
+ node scripts/verify-notice.mjs <session.jsonl.zstd>
440
+ ```
441
+
442
+ ### 调试入口
443
+
444
+ | 入口 | 内容 |
445
+ | --- | --- |
446
+ | `~/.dsh/session-complete-notify.log` | 宿主诊断日志:设置注册、重试与失败、投影注册、追加失败堆栈 |
447
+ | 浏览器 console `[dsh-session-notify-client]` | 客户端日志:权限状态、通知展示、设置保存 |
448
+ | `window.__dsch_notify_debug.readNotice(id)` | 手动读取指定会话的最新通知正文 |
449
+ | `window.__dsch_notify_debug.snapshotDebug(id)` | 会话尾部节点类型 + notice 数量 + 最近正文(前 200 字) |
450
+
451
+ ---
452
+
453
+ ## 常见问题
454
+
455
+ <details>
456
+ <summary><b>npm install 之后为什么不自动挂载?</b></summary>
457
+
458
+ 这是 DSH 官方设计:`npm install` 只把包装进依赖树,不注册插件。自动挂载的唯一途径是 `dsh plugin add` —— 它读取包内 `dsh.bundle` manifest(本插件自 0.1.3 起声明)并自动应用 `cordis.patch.yml`。参见[安装](#安装)。
459
+
460
+ </details>
461
+
462
+ <details>
463
+ <summary><b>为什么「中断」(interrupted)不提醒?</b></summary>
464
+
465
+ `interrupted` 是崩溃恢复后由持久化后端补写的孤儿轮次关闭标记,用户视角的「完成」不包含它(否则恢复会话会刷一屏误报)。确有需要可在宿主配置的 `reasons` 中加入。
466
+
467
+ </details>
468
+
469
+ <details>
470
+ <summary><b>后台会话(没打开窗口的)也会推送吗?</b></summary>
471
+
472
+ 会。客户端从会话列表快照观测所有会话的 `running` 边沿;正文优先取宿主投影 —— 宿主为所有会话(含后台)维护投影单元,因此推送正文跨会话一致。投影不可用时降级为事件窗口或工作区信息。
473
+
474
+ </details>
475
+
476
+ <details>
477
+ <summary><b>保存设置后为什么提示刷新页面?</b></summary>
478
+
479
+ 宿主在注册命名空间时读取一次设置,客户端 bundle 在页面加载时装配。保存后点「点击刷新」让两侧重新读取,新语言、模板即生效。
480
+
481
+ </details>
482
+
483
+ <details>
484
+ <summary><b>缓存命中率、速度数据从哪来?为什么有时是空的?</b></summary>
485
+
486
+ 来自官方 `sessionProjections`(`tokenUsage`、`sessionStats`),与 dsh-web-ui 状态栏同口径。宿主读取投影快照失败或数据尚未就绪时,退回本地用量聚合估算,仍无数据则该项留空(标签插了也不显示)。另外,这两项只在自定义模板中通过 `{cache}`、`{tps}` 插入时才出现,默认文案不含。
487
+
488
+ </details>
489
+
490
+ <details>
491
+ <summary><b>通知正文里的错误信息太长、有换行怎么办?</b></summary>
492
+
493
+ 摘要行(折叠行)与错误详情都会单行化并截断:摘要 120 字符、模板 `{error}` 80 字符、默认文案的错误详情 40 字符,超长以省略号结尾。
494
+
495
+ </details>
194
496
 
195
- - 宿主热重载后会出现一次「用时 0 秒」:内存计时器随重载重置(下轮恢复;缓存命中/速度/标题来自投影不受影响)
196
- - 设置保存后需刷新页面:宿主实时生效,但客户端 bundle 需整页加载
197
- - 通知权限:首次完成时浏览器会请求(权限 `default` 只发起一次);拒绝不影响 toast
198
- - 发布包:`npm run build`(node --check)→ `npm pack` → `telosmaylx-dsh-session-notify-*.tgz`
497
+ <details>
498
+ <summary><b>可以自定义系统通知的图标或声音吗?</b></summary>
499
+
500
+ 当前版本使用浏览器默认通知样式,不注入自定义图标或声音,toast 为固定深色卡片。如需这些能力欢迎提 Issue PR。
501
+
502
+ </details>
503
+
504
+ ---
505
+
506
+ ## 更新日志
507
+
508
+ | 版本 | 日期 | 变更 |
509
+ | --- | --- | --- |
510
+ | **0.1.4** | 2026-08-28 | 补充完整卸载支持:`dispose` 生命周期收尾(host 置卸载标志抑制待追加微任务;client 清理正文轮询定时器、`__dsch_notify_debug` 钩子、toast 容器);卸载文档与 FAQ 同步 |
511
+ | **0.1.3** | 2026-08-28 | 声明官方 `dsh.bundle` manifest(`dsh plugin add` 一条命令自动挂载);settings 重试定时器改为 `ctx.effect()` 包装(Cordis effect 纪律);安装文档重排 |
512
+ | 0.1.2 | 2026-08-27 | 包更名至 `@telosmaylx` scope(npm 用户名作用域) |
513
+ | 0.1.1 | 2026-08-27 | GitHub、npm 安装方式文档化 |
514
+ | 0.1.0 | 2026-08-26 | 初始版本:会话内系统消息 + 浏览器推送 + 官方设置面板 |
515
+
516
+ ---
517
+
518
+ ## 贡献
519
+
520
+ 欢迎 Issue 与 PR:
521
+
522
+ 1. Fork 仓库并新建分支(`feat/xxx`)
523
+ 2. 改动后运行 `npm run build` 做语法校验
524
+ 3. 提交 PR,说明动机与验证方式
525
+
526
+ 提交前请遵守 [Cordis 开发教程](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) 纪律:
527
+
528
+ - Cordis 之外的资源(定时器、订阅、watcher)必须包装在 `ctx.effect()` 中并返回 disposer;
529
+ - 配置项显式 `id` 防止编辑漂移;
530
+ - 插件须声明 `dsh.bundle` manifest 才能被 `dsh plugin add` 识别安装。
531
+
532
+ ---
533
+
534
+ ## 相关链接
535
+
536
+ - [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) —— DSH 插件精选列表(投稿规范:`dsh.bundle` 是安装唯一凭证)
537
+ - [Cordis 开发教程](https://deepseek-harness.github.io/deepseek-harness/develop/cordis-tutorial) —— 插件开发全流程(01-07 章)
538
+ - [npm 包主页](https://www.npmjs.com/package/@telosmaylx/dsh-session-notify)
539
+ - [GitHub 仓库](https://github.com/TelosmaYLX/dsh-session-notify)
540
+
541
+ ---
199
542
 
200
- ## License
543
+ ## 许可证
201
544
 
202
- MIT
545
+ [MIT](./LICENSE) © dsh-session-notify contributors
package/lib/client.js CHANGED
@@ -142,6 +142,8 @@ window.__ModuleLoader__.load({
142
142
  try { notifyScope = ctx.settingsScope.bind({ namespace: SETTINGS_NS }) } catch (e) { /* 无设置作用域时用默认 */ }
143
143
  var prevRunning = new Map() // sessionId -> 上次观测的 running 位
144
144
  var primed = false // 首次观测只记录基线(已在 idle 的会话不补发),与官方 sidebar 提醒同策略
145
+ var retryTimer = null // 完成推送正文轮询定时器(卸载时清除:不在已拆 fiber 上继续弹通知)
146
+ var debugHook = null // window.__dsch_notify_debug 引用(卸载时按引用删除,防闭包泄漏)
145
147
 
146
148
  try {
147
149
  console.log('[' + PLUGIN_TAG + '-client] active, watching session activity for completion pushes')
@@ -180,10 +182,11 @@ window.__ModuleLoader__.load({
180
182
  function attempt() {
181
183
  var notice = readNoticeAny(id)
182
184
  if (notice || Date.now() - start > 6000) {
185
+ retryTimer = null
183
186
  notifyUser(pushTitle(name, notice ? '' : ' 已完成'), notice || (cwdLine ? cwdLine + ' · ' : '') + '详情见会话内系统消息')
184
187
  return
185
188
  }
186
- setTimeout(attempt, 400)
189
+ retryTimer = setTimeout(attempt, 400)
187
190
  }
188
191
  attempt()
189
192
  }
@@ -263,7 +266,7 @@ window.__ModuleLoader__.load({
263
266
  // 调试钩子(仅探针/排障用):window.__dsch_notify_debug
264
267
  try {
265
268
  if (typeof window !== 'undefined') {
266
- window.__dsch_notify_debug = {
269
+ debugHook = {
267
270
  readNotice: readNotice,
268
271
  snapshotDebug: function (id) {
269
272
  try {
@@ -280,17 +283,42 @@ window.__ModuleLoader__.load({
280
283
  }
281
284
  },
282
285
  }
286
+ window.__dsch_notify_debug = debugHook
283
287
  }
284
288
  } catch (e) { /* noop */ }
285
289
 
290
+ // 卸载清理(Cordis 教程第 2 章生命周期纪律):取消会话订阅、清除正文轮询
291
+ // 定时器(不在已拆 fiber 上继续弹通知)、按引用删除调试钩子(防闭包持有
292
+ // ctx/list 泄漏)、移除页内 toast 容器。settings 卡片为 slot 托管 effect,
293
+ // 自定义预设存于 localStorage(用户数据),两者卸载时均保留。
294
+ function cleanup() {
295
+ try { dispose() } catch (e) { /* 订阅已取消则忽略 */ }
296
+ if (retryTimer !== null) {
297
+ clearTimeout(retryTimer)
298
+ retryTimer = null
299
+ }
300
+ try {
301
+ if (typeof window !== 'undefined' && window.__dsch_notify_debug === debugHook) {
302
+ delete window.__dsch_notify_debug
303
+ }
304
+ } catch (e) { /* noop */ }
305
+ try {
306
+ if (typeof document !== 'undefined') {
307
+ var toastRoot = document.querySelector('[data-dsh-notify-root]')
308
+ if (toastRoot) toastRoot.remove()
309
+ }
310
+ } catch (e) { /* noop */ }
311
+ log('unloaded: subscription, retry timer, debug hook, toast root released')
312
+ }
313
+
286
314
  // 与宿主插件生命周期一致:ctx.effect 登记清理,fiber 卸载时释放。
287
315
  if (ctx.effect && typeof ctx.effect === 'function') {
288
316
  ctx.effect(function () {
289
- return dispose
317
+ return cleanup
290
318
  })
291
319
  } else {
292
320
  try {
293
- var oc = ((ctx.onDispose && ctx.onDispose(dispose)) || undefined)
321
+ var oc = ((ctx.onDispose && ctx.onDispose(cleanup)) || undefined)
294
322
  void oc
295
323
  } catch (e) {
296
324
  /* 老接口缺失时仅内存泄漏,不影响功能 */
package/lib/index.js CHANGED
@@ -110,6 +110,7 @@ export function apply(ctx, config = {}) {
110
110
  const tracker = createTurnTracker()
111
111
  let settings = DEFAULT_SETTINGS
112
112
  let projRegistry = null // sessionProjections 服务(非注入可选依赖,经 ctx.inject 捕获)
113
+ let disposed = false // 卸载/HMR 拆除标记:置位后不再追加通知
113
114
 
114
115
  // 官方设置命名空间:设置面板(设置 → 插件)可编辑;user 层持久化在 settings 文档。
115
116
  // 重试兜底:热重载时旧 fiber 注销与新 fiber 注册存在竞态,register 可能因
@@ -258,13 +259,30 @@ export function apply(ctx, config = {}) {
258
259
  // finally 中复位),此时同步 append 会被拒绝:
259
260
  // "session append cannot reenter while another append is being published"
260
261
  // 推迟到微任务——微任务队列在本次同步栈(含 finally 复位)之后才跑。
261
- queueMicrotask(() => appendNotice(ctx, session, notice))
262
+ queueMicrotask(() => {
263
+ if (disposed) return // 卸载窗口内已调度的微任务:fiber 已拆除,跳过追加
264
+ appendNotice(ctx, session, notice)
265
+ })
262
266
  return
263
267
  }
264
268
  default:
265
269
  return
266
270
  }
267
271
  })
272
+
273
+ // 卸载清理(Cordis 教程第 2 章生命周期纪律):事件订阅 / settings scope /
274
+ // 投影注入均为 Cordis 托管 effect,卸载时自动释放;这里只做插件自有状态的
275
+ // 收尾——置 disposed 标志(抑制已调度的微任务追加)并落一条卸载日志。
276
+ try {
277
+ if (typeof ctx.on === 'function') {
278
+ ctx.on('dispose', () => {
279
+ disposed = true
280
+ fileLog('plugin unloaded (host): pending appends suppressed')
281
+ })
282
+ }
283
+ } catch {
284
+ /* 极老环境无 dispose 事件:由 fiber 自然回收 */
285
+ }
268
286
  }
269
287
 
270
288
  /** 把系统消息追加进会话日志(失败只记日志,绝不抛出破坏 event 火线)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telosmaylx/dsh-session-notify",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "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).",
5
5
  "private": false,
6
6
  "type": "module",