@game25011/dsh-notify-toast 0.0.0-stage → 0.4.2
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/AI-DISCLOSURE.md +29 -0
- package/CHANGELOG.md +87 -0
- package/LICENSE +22 -0
- package/README.en.md +290 -0
- package/README.md +308 -3
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/assets/sounds/bubble.ogg +0 -0
- package/cordis.patch.yml +5 -0
- package/docs/INTERFACE.md +180 -0
- package/docs/RELEASING.md +282 -0
- package/docs/TEST-REPORT.md +183 -0
- package/docs/assets/native-toast.png +0 -0
- package/dsh-plugin.json +14 -0
- package/icon.svg +17 -0
- package/lib/all-done.js +126 -0
- package/lib/client.js +3486 -0
- package/lib/config.js +420 -0
- package/lib/goal-summary.js +438 -0
- package/lib/index.js +1411 -0
- package/lib/native-focus.js +350 -0
- package/lib/native-toast-helper.cs +206 -0
- package/lib/native-toast.js +372 -0
- package/lib/native-toast.ps1 +72 -0
- package/lib/taskbar-badge-helper.cs +126 -0
- package/lib/taskbar-badge-main.cs +43 -0
- package/lib/taskbar-badge.js +347 -0
- package/lib/taskbar-badge.ps1 +59 -0
- package/lib/text.js +174 -0
- package/locale/en.json +17 -0
- package/locale/zh.json +17 -0
- package/package.json +76 -6
package/README.md
CHANGED
|
@@ -1,3 +1,308 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# 轻量原生通知(Windows)
|
|
2
|
+
|
|
3
|
+
**轻量 · 原生 · 可扩展 · 不打扰**
|
|
4
|
+
|
|
5
|
+
中文 | [English](./README.en.md)
|
|
6
|
+
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://github.com/game25011/dsh-notify-toast/actions/workflows/ci.yml)
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
> dsh-notify-toast 是一款适用于 Windows 10/11 的非官方 DSH 插件(MIT 协议)。它提供聚焦不打扰的后台提醒,通过 Windows 原生通知提示提问、授权与根会话完成;任务栏未读角标按不同根会话计数,同一会话多次完成算一个,1–3 显示数字,超过 3 显示红点。主要代码由 AI 编程助手生成,人类维护者负责需求、调试、测试与发布。
|
|
13
|
+
|
|
14
|
+
- 源码/本地包名:`dsh-notify-toast` · 版本:`0.4.2` · 许可:MIT · 仓库:https://github.com/game25011/dsh-notify-toast
|
|
15
|
+
- npm 包:`@game25011/dsh-notify-toast@0.4.2`(scoped 分发;源码包名仍为 `dsh-notify-toast`)
|
|
16
|
+
- 适用:DeepSeek Harness Desktop `0.2.2-rc.1`(Electron 壳,profile `desktop`),Windows 10/11
|
|
17
|
+
- 形态:DSH 官方 bundle(Host + Web Client 两半),通过官方插件管理器安装/卸载
|
|
18
|
+
- 状态:0.4.2 更新 DSH 0.2.2-rc.1 兼容声明;0.4.1 修复审批通知漏发;0.4.0 新增任务栏未读角标(方案 B:不同根会话计数);0.3.0 原生进度 toast、bar-only 样式、自动收回、点击回会话与 Goal 汇总已在本机简单实测;Goal 边界场景仍待补测;「全部完成后改写标题」为可选功能(默认关)
|
|
19
|
+
- 截图:
|
|
20
|
+
|
|
21
|
+
## 0.4.2 更新
|
|
22
|
+
|
|
23
|
+
v0.4.2 是元数据与兼容声明补丁,未变更通知核心逻辑。针对 DSH Desktop 0.2.2-rc.1,将 `engines.dsh` 显式覆盖至 `0.2.2-rc.x` 范围以避免 semver 误判,并同步更新 README 徽标。94 项单测、26 项 smoke 及 Host probe 均通过;上游处于预发布期,不作长效兼容承诺。
|
|
24
|
+
|
|
25
|
+
## 0.4.1 更新
|
|
26
|
+
|
|
27
|
+
修复审批通知可能不出现的问题:DSH 的 `approval/request` 是 waterfall,UI 审批应答器可能先处理并短路;插件观察者已改为 `prepend + global`,发布通知后始终 `next()` 委托,不改变审批结果。
|
|
28
|
+
|
|
29
|
+
## 维护方向
|
|
30
|
+
|
|
31
|
+
目标是把插件做成**轻量、原生、可自我拓展、不打扰**的 Windows 通知层,并长期保持更新。
|
|
32
|
+
|
|
33
|
+
- **轻量**:不常驻托盘或后台服务,helper 按需启动。
|
|
34
|
+
- **原生**:使用 Windows 原生 toast 和任务栏 overlay API。
|
|
35
|
+
- **可自我拓展**:MIT 许可,Host/Client/原生 helper 分层,配置和 `docs/INTERFACE.md` 开放;使用者可以调整设置、自行 fork,或按接口增加通知类型、声音和路由。当前没有在线脚本或第三方运行时扩展 API,深度定制需要改代码。
|
|
36
|
+
- **持续更新**:优先兼容性、安装可靠性和真实反馈,不追求功能堆叠。
|
|
37
|
+
|
|
38
|
+
## 特色
|
|
39
|
+
|
|
40
|
+
轻量级 DSH 通知插件,无常驻后台,helper 按需启动。联动提示音、Windows 原生通知与任务栏未读角标,依实际使用经验覆盖约 90% 日常提示场景(经验估测,非绝对保证)。采用 MIT 协议与 Host/Client/原生 helper 分层架构,支持修改配置与 fork 扩展(暂无在线脚本 API,深度定制需修改代码)。主体代码由 AI 辅助生成,人类维护者负责需求把控、调试、测试与发布维护,并以持续跟进 DSH 兼容为目标。
|
|
41
|
+
## AI 参与说明
|
|
42
|
+
|
|
43
|
+
本项目的主要代码(Host、Web Client、PowerShell/C# 原生 helper 及多数测试/文档)由 AI 编程助手生成;人类维护者负责需求、调试、测试、发布和维护。AI 生成内容可能存在错误,欢迎提交 issue 或 pull request。本项目采用 MIT License,AI 工具不是作者、版权持有者或责任主体,本说明不构成法律意见。详见 [AI-DISCLOSURE.md](AI-DISCLOSURE.md)。
|
|
44
|
+
|
|
45
|
+
> 命名说明:中文显示名仅用于界面展示;npm 包名、目录、Host 行 id、settings 命名空间与 RPC 前缀保留 ASCII `dsh-notify-toast`;插件清单公共 id 为 `dsh.notify-toast`。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 功能
|
|
50
|
+
|
|
51
|
+
| 能力 | 行为 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| 需要回答/确认 | `user-questions/request` 触发;标题「DeepSeek 想问你一个问题」,正文为问题原文前 N 字 |
|
|
54
|
+
| 需要授权(审批) | `approval/request` 触发;标题「DeepSeek 需要你的授权」,正文为「工具名 · 简短目的」 |
|
|
55
|
+
| 本轮回复结束 | `session/event` 的 `turn/end(completed)` 触发;仅根会话,子代理默认不触发;单步标题「DeepSeek 搞定了一小步」,2–4 步「DeepSeek 一口气推进了 N 步」,5 步及以上「DeepSeek 火力全开,连推了 N 步」;正文取最后一条助手回复首段有效文字,取不到写「点击查看」 |
|
|
56
|
+
| 目标模式汇总 | goal `active + armed` 期间不逐轮提醒;在 `complete` / `paused` / `blocked` / Round 上限 / `disarmed` 后按 `roundsStarted` 增量只发一条;暂停/受阻使用中性标题;提问/授权仍即时 |
|
|
57
|
+
| 只在未聚焦时提醒 | 事件到达时实时判断 `document.hasFocus()` / `visibilityState`;聚焦时不弹、不响 |
|
|
58
|
+
| 任务栏未读角标 | DSH 在后台时,在 DSH 任务栏图标上显示有完成结果的**不同根会话数**(方案 B);同一会话完成多次只计 1 个。1–3 显示深灰底、白字、蓝色细环的数字角标,超过 3 个显示红点;回到 DSH 后自动清除;设置页可关闭 |
|
|
59
|
+
| Windows 原生进度 toast | Host 通过 PowerShell/C#/WinRT 创建 Windows 系统通知;通知内含动态 `<progress>` 进度条;到达近似时间后停止更新,短暂缓冲后收回横幅;通知中心保留由 ExpirationTime 约束,不写自绘窗口 |
|
|
60
|
+
| 兼容降级 | 原生 helper 不可用或启动失败时自动退回 Web Notification;两条路径都 `silent`,声音由插件统一播放一次 |
|
|
61
|
+
| 正文长度 | 默认 40 字(Unicode 码点),可配 20–60;超出截断加「…」 |
|
|
62
|
+
| 隐私模式 | 只显示标题,正文留空 |
|
|
63
|
+
| 提示音 | Web Audio 合成 4 种(叮咚/风铃/清铃/轻点)+ 1 个 CC0 开源音效「弹一下」,音量 0–100,可关闭、可试听;首次用户手势后解锁;失败静默降级 |
|
|
64
|
+
| 点击回窗 | 先由 Host 用 Windows `SetForegroundWindow` 把 DSH 窗口置前,再尽力打开对应会话;koffi 不可用时降级 `window.focus()` |
|
|
65
|
+
| 合并 | 默认 2.5 s 内的多个「完成」合并为一条,避免连环弹 |
|
|
66
|
+
| 全部完成后改写标题 | 可选(默认关):多个会话同时运行时,等全部会话都成功结束,把最后结束的会话标题改为指定文案;只有一个会话运行时不生效 |
|
|
67
|
+
| 设置页 | DSH 设置 →「通知」:启用/提问/授权/本轮回复/任务栏未读角标/目标模式结束后汇总开关、通知显示方式(原生进度/兼容)、自动移除时间、正文长度、隐私模式、提示音(可试听)、音量、合并间隔、测试通知/测试系统通知、状态诊断 |
|
|
68
|
+
| 持久化 | 官方 `ctx.settings`(插件 Config 的 volatile 字段 → profile patch),重启 DSH 后保留;不写自建配置文件 |
|
|
69
|
+
| 降级 | 通知权限拒绝 / 环境不支持 / Host 不可用 / `settings` 缺失 / koffi 加载失败:不崩溃,设置页显示状态 |
|
|
70
|
+
|
|
71
|
+
### 明确不做
|
|
72
|
+
|
|
73
|
+
常驻托盘/后台服务、任务栏进度条/闪烁动画;任务栏角标只在任务栏 helper 的一次性调用中更新,不增加常驻进程。不访问外部网络、不修改 DSH 核心、不遥测、不调用模型。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 安装
|
|
78
|
+
|
|
79
|
+
> 本插件只通过 **官方插件管理器** 或 GitHub Release 安装;不要手改 profile 的 `package.json` 或 `cordis.patch.yml`。
|
|
80
|
+
>
|
|
81
|
+
> npm 包:`@game25011/dsh-notify-toast@0.4.2`;可用 `npm pack @game25011/dsh-notify-toast` 获取 tgz,再按插件管理器安装。
|
|
82
|
+
|
|
83
|
+
> 插件清单说明:`dsh-plugin.json` 只声明 Host facet(`facets.host`);Web Client 半由 `package.json` 的 `dsh.client` 声明(同类第三方插件也采用该结构),因此未声明 `facets.client`。DSH 0.2.2-rc.1 的官方插件加载以 `package.json` 的 `dsh.bundle` / `dsh.client` 为准。
|
|
84
|
+
|
|
85
|
+
### 方式 0:npm 包(可选)
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
npm pack @game25011/dsh-notify-toast --registry=https://registry.npmjs.org/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
然后按下面的 tgz / 插件管理器方式安装;DSH Desktop 仍推荐应用内插件管理器。
|
|
92
|
+
|
|
93
|
+
### 方式 A:应用内插件管理器(推荐)
|
|
94
|
+
|
|
95
|
+
1. 把交付的 `dsh-notify-toast-0.4.2.tgz` 放到本机任意目录(路径不要含空格)。
|
|
96
|
+
2. 在 DSH 里让 Agent 用官方插件管理器安装,或使用插件管理界面:
|
|
97
|
+
- Agent 工具:`plugin_manager` → `action: install_bundle`,`target: <tgz 绝对路径>`
|
|
98
|
+
- 命令行:**仅适用于自管 profile(web/headless)**;官方 Desktop 的 desktop profile 由 Electron 应用的插件管理器管理,请用上面的界面或 Agent 方式。
|
|
99
|
+
3. 安装完成后在 设置 → 插件 的列表里确认 `dsh-notify-toast` 已启用。
|
|
100
|
+
- **首次安装/升级后需要重启一次 DSH**:官方插件管理器可热插入 Host 行,但已加载的 Host JavaScript 模块要重启才会换成新代码(客户端 bundle 会热更新)。本插件不会自行重启应用。
|
|
101
|
+
4. 打开 设置 →「通知」,点「发送测试通知」验证;「运行状态」块会显示 Host 连接、transport、设置持久化与最近提醒计数。
|
|
102
|
+
|
|
103
|
+
### 方式 B:开发期目录安装(link)
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
plugin_manager → install_bundle → target: <插件目录绝对路径>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
本地目录安装为 link,代码改动可被 DSH 的 HMR 感知;Host 侧 JavaScript 变更若未热更新,需要重启 DSH。
|
|
110
|
+
|
|
111
|
+
### 方式 C:插件市场索引
|
|
112
|
+
|
|
113
|
+
本仓库自带 `market-index.json`。在 DSH 的插件市场/发现页添加自定义索引:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
https://raw.githubusercontent.com/game25011/dsh-notify-toast/4a3c83e5634cf9dcac005ec2e9bbd4c4dd828596/market-index.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
添加后搜索 `dsh-notify-toast` 即可看到本插件,并从市场安装固定版本的 Git 源。
|
|
120
|
+
这是**自托管索引**;DSH 官方默认索引不会自动收录本插件,其他用户需要添加同一索引或由官方目录收录。
|
|
121
|
+
|
|
122
|
+
### 升级
|
|
123
|
+
|
|
124
|
+
- 新版本 tgz:再次 `install_bundle`(应用内会提示重启以加载新的 JS 模块)。
|
|
125
|
+
- 开发目录:直接更新代码;必要时重启 DSH。
|
|
126
|
+
|
|
127
|
+
### 卸载
|
|
128
|
+
|
|
129
|
+
- 应用内:插件管理界面禁用/移除 `dsh-notify-toast`;
|
|
130
|
+
- Agent 工具:`plugin_manager` → `action: remove_bundle`,`target: dsh-notify-toast`;
|
|
131
|
+
- 卸载后:Host 路由/事件监听/计时器随插件 fiber 释放,客户端 bundle 不再加载;设置页条目消失。
|
|
132
|
+
已写入 profile patch 的配置项会随插件移除而不再生效(不会留下后台进程或临时文件)。
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 配置
|
|
137
|
+
|
|
138
|
+
设置位置:**设置 →「通知」**(`settings.section`,id `dsh-notify-toast`)。
|
|
139
|
+
|
|
140
|
+
| 设置 | 范围/选项 | 默认 | 说明 |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| 启用通知 | 开/关 | 开 | 总开关;关闭后不弹也不响(测试通知除外) |
|
|
143
|
+
| 提问与方案确认 | 开/关 | 开 | `user-questions/request` |
|
|
144
|
+
| 授权(审批) | 开/关 | 开 | `approval/request` |
|
|
145
|
+
| 本轮回复结束 | 开/关 | 开 | 子代理完成始终不提醒;只表示这一轮回复结束,不代表整个任务完成 |
|
|
146
|
+
| 任务栏未读角标 | 开/关 | 开 | DSH 后台时显示有完成结果的不同根会话数;同一会话多次完成只计 1;1–3 显示深灰底、白字、蓝色细环的数字角标,超过 3 显示红点;聚焦/点击任务栏后清除;仅 Windows |
|
|
147
|
+
| 目标模式结束后汇总 | 开/关 | 开 | goal 自动推进期间不逐轮提醒;结束/暂停/受阻/Round 上限后按 Goal Round 增量汇总为一条;提问/授权仍即时 |
|
|
148
|
+
| 正文长度 | 20–60 字 | 40 | 按 Unicode 码点截断,超出加「…」 |
|
|
149
|
+
| 隐私模式(只显示标题) | 开/关 | 关 | 正文留空 |
|
|
150
|
+
| 提示音 | 叮咚 / 风铃 / 清铃 / 轻点 / 弹一下(开源音效) / 关闭 | 叮咚 | 前 4 种由 Web Audio 合成;「弹一下」是 uisfx CC0 音频(内嵌,不联网);设置页有「试听」按钮 |
|
|
151
|
+
| 音量 | 0–100 | 60 | 0 等同静音 |
|
|
152
|
+
| 合并间隔 | 0–10000 ms | 2500 | 该窗口内的多个「完成」合并为一条 |
|
|
153
|
+
| 全部完成后改写标题 | 开/关 | 关 | 至少两个根会话同时运行、且全部成功结束后才生效;只有一个会话运行时不触发 |
|
|
154
|
+
| 标题文案 | 1–96 字节 | 所有任务均已完成 | 最后结束的会话将改成该标题(仅「全部完成后改写标题」功能) |
|
|
155
|
+
| 通知显示方式 | Windows 原生进度通知 / Windows 兼容通知 | 原生进度 | 原生模式显示系统通知进度条;helper 不可用时自动回退兼容通知 |
|
|
156
|
+
| 通知自动移除时间(近似) | 3–30 秒 | 8 秒 | 控制进度更新停止时间与通知中心保留窗口;实际退场由 Windows 控制,可能提前或延后 |
|
|
157
|
+
|
|
158
|
+
所有改动即时生效;持久化走 DSH 官方 settings(写入当前 profile 的 patch 层),重启后保留。
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 工作原理
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
Host (lib/index.js) Client (lib/client.js)
|
|
166
|
+
───────────────────────────────────────────── ─────────────────────────────────────
|
|
167
|
+
approval/request ───────────┐ settings.section 设置页(React,纯 HTML)
|
|
168
|
+
user-questions/request ─────┼─ 清洗/截断/合并 ──┐ ├─ 焦点判断(hasFocus + visibilityState)
|
|
169
|
+
session/event turn/end ─────┘ │ ├─ native toast RPC / Web Notification 回退 + 点击回窗
|
|
170
|
+
▼ ├─ Web Audio 合成提示音 + 内嵌 CC0「弹一下」音效
|
|
171
|
+
事件环形缓冲 + 长轮询 ┘
|
|
172
|
+
Host 浏览器路由 /dsh-notify-toast(官方 RPC 信封 + 同源栅栏)
|
|
173
|
+
config / save / poll / health / test / focus / taskbar-badge
|
|
174
|
+
Host: native-toast.ps1 → WinRT Toast;taskbar-badge EXE → ITaskbarList3
|
|
175
|
+
Host: ctx.settings(volatile Config)← save
|
|
176
|
+
Host: koffi user32 SetForegroundWindow ← focus
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- **Host 检测**:直接监听 DSH 的权威事件(审批、提问、`turn/end`),只读会话日志提取正文,不调用模型、不访问外部网络。
|
|
180
|
+
- **传输**:走 `/dsh-notify-toast` 前缀长轮询。优先使用官方 `connection.rpc.handle`;在 DSH 0.2.1-rc.2 上该 API 有 bug
|
|
181
|
+
(内部读 `webServer` 抛 `cannot get property "webServer" without inject`),插件自动回退为同路径 `webServer` 路由,
|
|
182
|
+
并在入口调用官方 `connection.requestRejection` 复用 Host/Origin 同源栅栏 + 浏览器签名 cookie 认证;
|
|
183
|
+
请求/响应用官方一元 RPC 信封,所以客户端 `connection.rpc.call` 代码不变。设置页「Host 连接 / transport」可看到当前链路。
|
|
184
|
+
- **客户端只负责展示**:事件到达时才判断窗口是否聚焦;聚焦则直接丢弃(不弹不响)。
|
|
185
|
+
- **根会话判定**:`SessionHeader.parentSession === undefined && origin !== 'subagent'`。
|
|
186
|
+
- **正文提取**:
|
|
187
|
+
- 提问:`request.questions[0].question` 原文;
|
|
188
|
+
- 授权:工具名 + `displayReason`/`reason` 简短目的;
|
|
189
|
+
- 完成:从 `turn/end` 事件往前找最近一条 `assistant/message`,取文本块,清洗 Markdown/代码围栏/行内代码/链接/标题符号等,压缩空白,取第一段;无有效文字 → 「点击查看」。
|
|
190
|
+
- **点击回窗**:客户端 `POST`(RPC)`focus` → Host 用 DSH 自带 koffi(`resources/app.asar.unpacked/.../koffi.node`)调用 user32 `ShowWindow/SetForegroundWindow/AttachThreadInput` 把 Electron 主窗口置前;随后客户端 `window.focus()` 并打开对应会话。
|
|
191
|
+
- **配置持久化**:`Config` 全字段 `.volatile()`;Host 从 profile 解析 `@deepseek-ai/schemastery`(优先 `DSH_PROFILE_DIR`,其次 Host 进程 cwd 即 profile 目录,再次 DSH 安装包内副本),
|
|
192
|
+
通过 `ctx.settings.describe()/update()` 读写当前 profile patch。设置页「设置持久化 / configSource」可见状态。
|
|
193
|
+
|
|
194
|
+
### 实现与资源占用
|
|
195
|
+
|
|
196
|
+
- **Host** 在 DSH 进程内运行,只监听事件、维护 200 条内存环形缓冲、长轮询和 settings;空闲时没有额外常驻进程。
|
|
197
|
+
- **客户端** 是约 159 KB 的 Web bundle;空闲时只有一条 25 秒长轮询和 5 秒一次的 localStorage leader 心跳。
|
|
198
|
+
- **Windows toast** 由一次性 PowerShell/C# helper 调 WinRT 创建;一个 toast 对应一个 helper,toast 结束后退出,同时最多 8 个。
|
|
199
|
+
- **任务栏角标** 优先使用首次编译并缓存的小型独立 C# EXE,直接调用 \`ITaskbarList3::SetOverlayIcon\`;热路径实测约 55–85 ms、约 14 MB 瞬时内存。首次使用需要编译,之后命中缓存;编译或 EXE 不可用时回退 PowerShell helper。
|
|
200
|
+
- **缓存** 位于 \`%LOCALAPPDATA%\\dsh-notify-toast\\native-cache\`;任务栏 helper 缓存只保留最近 8 个,避免旧 hash 文件无限积累。
|
|
201
|
+
- 不访问外部网络、不调用模型、不采集;点击回调只使用本机 loopback。
|
|
202
|
+
|
|
203
|
+
### 隐私
|
|
204
|
+
|
|
205
|
+
- 不访问外部网络、不上报、不采集;仅本机 loopback 激活回调;无模型调用;通知正文只来自本机会话日志的已有内容。
|
|
206
|
+
- 不写自建配置文件;配置由 DSH 官方 settings 持久化到 profile patch。
|
|
207
|
+
- 通知内容可能包含回复摘要:介意时开启「隐私模式(只显示标题)」。
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 兼容性
|
|
212
|
+
|
|
213
|
+
| 项目 | 说明 |
|
|
214
|
+
|---|---|
|
|
215
|
+
| DSH | `0.2.2-rc.1`(`engines.dsh: >=0.2.1-rc.2 <0.2.2-0 || >=0.2.2-rc.0 <0.3.0`);Host 事件/RPC/settings 契约在本版本实测 |
|
|
216
|
+
| 操作系统 | Windows 10/11(原生 toast + koffi user32 置前);macOS/Linux 未验证(本插件目标为 Windows) |
|
|
217
|
+
| 运行形态 | DSH Desktop(Electron,页面 origin `dsh-app://app`)。普通浏览器 Web profile 下 toast 可用,但"置前 Windows 窗口"会降级为浏览器自身聚焦 |
|
|
218
|
+
| 通知权限 | Windows 通知中心需允许应用通知;Electron Desktop 默认放行非媒体权限,若被系统/组策略禁用则只降级不崩溃 |
|
|
219
|
+
| 聚焦助手 | Windows 专注助手/勿扰模式可能静默 toast(系统行为,插件无法绕过) |
|
|
220
|
+
| 插件格式 | `dsh-plugin.json`(Host facet)+ `package.json` 的 `dsh.bundle` / `dsh.client`;未使用 `facets.client`,理由见「安装」小节的插件清单说明 |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 已知限制
|
|
225
|
+
|
|
226
|
+
1. 页面必须处于打开状态(DSH 窗口最小化可以,但页面进程不能退出);浏览器关闭则没有通知。
|
|
227
|
+
2. 通知权限被拒绝 / 系统不支持 Web Notification 时:设置页显示状态,原生 toast 不可用(保留声音与点击回窗能力),不崩溃。
|
|
228
|
+
3. Windows 关闭了通知横幅、或开启了专注助手时,原生 toast 可能只进通知中心不弹横幅;设置页「系统通知」计数与「测试系统通知」可用于确认,请在 Windows 设置中允许该应用通知并关闭专注助手。
|
|
229
|
+
4. 首次提示音需要页面上发生过一次用户手势(Chromium 自动播放策略);未解锁前声音静默,通知照常。
|
|
230
|
+
5. 点击回窗的置前依赖 DSH 自带 koffi 原生模块;模块路径变化/加载失败时降级为 `window.focus()`,可能不置前但仍会打开会话。
|
|
231
|
+
6. 多窗口/多标签页时用 localStorage 租约(5 s 心跳、15 s 过期)只让一个窗口弹 toast/出声;租约竞争极端情况下可能短暂重复。
|
|
232
|
+
7. 子代理完成始终不提醒;对子代理的提问/审批仍会提醒(它们同样阻塞任务)。
|
|
233
|
+
8. 「完成」正文取最后一条助手回复的首段;若回复只有代码/工具调用,正文回退为「点击查看」。
|
|
234
|
+
9. 合并只针对「完成」;提问/授权不合并(需要你尽快处理)。
|
|
235
|
+
10. **Windows 原生 toast 的退场动画和横幅停留时间由 Windows 控制**(Windows 设置/专注助手);「通知自动移除时间(近似)」控制进度更新停止与通知中心保留窗口,不保证横幅精确停留 N 秒。到期后插件会短暂缓冲再调用 `Hide()` 收回横幅;`Hide()` 的视觉过渡仍由 Windows 决定。
|
|
236
|
+
11. 原生进度通知的点击回会话依赖 C# helper 的本地激活回调;回调失败时可能只关闭通知、无法定位会话。提示条不再使用 `dsh://open` 协议唤起,避免点击时另起 DSH 实例/会话。
|
|
237
|
+
12. 原生 helper 首次运行会在 `%LOCALAPPDATA%\dsh-notify-toast\native-cache` 编译/缓存一个 C# DLL;编译失败或 PowerShell/C# 不可用时自动回退兼容通知。
|
|
238
|
+
13. 插件不修改 DSH 核心;DSH 升级若改动上述事件/RPC/settings 契约,插件可能需要同步升级。
|
|
239
|
+
14. DSH 0.2.1-rc.2 的官方 `connection.rpc.handle` 自身有 bug(`cannot get property "webServer" without inject`),
|
|
240
|
+
插件已自动回退为同路径 + `connection.requestRejection` 的官方栅栏路由;这不改变客户端行为,但 DSH 修复该 API 后插件会优先走官方 channel。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 故障排查
|
|
245
|
+
|
|
246
|
+
| 现象 | 检查 |
|
|
247
|
+
|---|---|
|
|
248
|
+
| 完全没有 toast | 设置 →「通知」看「通知权限 / Host 连接 / 系统通知」状态;点「测试系统通知」:若显示「已创建」却无横幅,去 Windows 设置 → 系统 → 通知 打开横幅/关闭专注助手;也检查 Win+N 通知中心里是否有该通知 |
|
|
249
|
+
| 有声音但没弹窗 | 同上;另外确认系统未把该应用通知设为「仅通知中心」 |
|
|
250
|
+
| 聚焦时也不弹 | 设计如此(聚焦不打扰);把焦点切到别的应用或用「发送测试通知」验证 |
|
|
251
|
+
| 有 toast 没声音 | 设置 →「通知」点「试听」;看「提示音引擎」是否为「已就绪」(「待解锁」时先点一下页面);检查 Windows 音量合成器中本应用的音量/静音、系统输出设备;音量是否 0/提示音是否关闭 |
|
|
252
|
+
| 点击通知没回到 DSH | 看设置页「窗口置前」是否为可用;不可用时为降级模式 |
|
|
253
|
+
| 重启后设置丢失 | 看设置页「设置持久化」是否为「官方设置」;若显示降级,说明 `ctx.settings` 不可用(插件仍按默认值工作) |
|
|
254
|
+
| 插件列表里没有条目 | 用官方插件管理器确认 bundle 已安装并启用;必要时重启 DSH |
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 开发与测试
|
|
259
|
+
|
|
260
|
+
```text
|
|
261
|
+
命令 说明
|
|
262
|
+
npm test 文本清洗/截断、配置、合并、goal 汇总与原生生命周期的单元测试(94 项)
|
|
263
|
+
npm run smoke 模拟 __ModuleLoader__ 加载 client.js 的冒烟测试(26 项)
|
|
264
|
+
npm run probe RPC 探针:--self-check 纯函数校验;--url 探测宿主 RPC 路由(无 cookie 预期 401/403)
|
|
265
|
+
node scripts/host-probe.mjs <profileDir> Host 集成探针(真实 profile cwd)
|
|
266
|
+
npm pack 生成交付用 tgz
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- 源码结构:
|
|
270
|
+
- `lib/index.js` Host 半(事件、浏览器路由、settings、合并、清理)
|
|
271
|
+
- `lib/text.js` 文本清洗/截断纯函数(有单元测试)
|
|
272
|
+
- `lib/config.js` 默认值/校验/合并/volatile Config schema
|
|
273
|
+
- `lib/native-focus.js` koffi user32 窗口置前
|
|
274
|
+
- `lib/client.js` Web 客户端半(焦点、toast、声音、设置页、长轮询)
|
|
275
|
+
- `locale/zh.json`、`locale/en.json` 插件市场/设置页显示元数据
|
|
276
|
+
- `scripts/client-smoke.mjs`、`scripts/probe-rpc.mjs` 冒烟/探针
|
|
277
|
+
- `dsh-plugin.json` 插件清单(Host facet,公共 id `dsh.notify-toast`);`package.json` 的 `dsh.client` 声明 Web Client 半;`cordis.patch.yml` 为 bundle patch
|
|
278
|
+
- `docs/PLAN.md` 历史开工方案(不进入 npm 包);`docs/INTERFACE.md` Host/Client 契约;`docs/TEST-REPORT.md` 验收记录;`docs/RELEASING.md` 发布手册
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 开源与贡献
|
|
283
|
+
|
|
284
|
+
- 仓库:https://github.com/game25011/dsh-notify-toast
|
|
285
|
+
- 英文文档:[README.en.md](./README.en.md)
|
|
286
|
+
- 贡献指引:[CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
287
|
+
- 安全问题:[SECURITY.md](./SECURITY.md)(请走私密 GitHub Security Advisories,不要公开 issue)
|
|
288
|
+
- 行为准则:[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
|
289
|
+
- 发布流程:[docs/RELEASING.md](./docs/RELEASING.md)
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 致谢
|
|
294
|
+
|
|
295
|
+
实现思路参考了以下 MIT 开源插件(代码为原创,未整段照抄):
|
|
296
|
+
|
|
297
|
+
- [chromoany/dsh-notify-me](https://github.com/chromoany/dsh-notify-me) — 焦点判断、通知降级、设置页、多窗口 leader
|
|
298
|
+
- [BillCx330/dsh-plugin-notify-sounds](https://github.com/BillCx330/dsh-plugin-notify-sounds) — koffi user32 窗口置前、Connection 路由栅栏
|
|
299
|
+
- [Laplace-bit/dsh-bell-notify](https://github.com/Laplace-bit/dsh-bell-notify) — Web Audio 合成音效
|
|
300
|
+
- [uisfx](https://github.com/romainsimon/uisfx) — 「弹一下」使用的 CC0 音效来源,详见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT,见 [LICENSE](./LICENSE)。
|
|
305
|
+
|
|
306
|
+
## 非官方声明
|
|
307
|
+
|
|
308
|
+
本插件是社区第三方插件,与 DeepSeek 官方无隶属关系,未获其背书或赞助(**Not affiliated with DeepSeek**)。DeepSeek、DeepSeek Harness 等名称与商标归其各自所有者。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## uisfx — bubble notification sound
|
|
4
|
+
|
|
5
|
+
- Project: uisfx
|
|
6
|
+
- Repository: https://github.com/romainsimon/uisfx
|
|
7
|
+
- Package/version: `uisfx@0.4.0`
|
|
8
|
+
- Original file: `sounds/rubber/notification.ogg`
|
|
9
|
+
- Vendored file: `assets/sounds/bubble.ogg` (unmodified OGG; also embedded as base64 in `lib/client.js` for zero-network playback)
|
|
10
|
+
- Audio license: **CC0 1.0 Universal (public domain dedication)**. No attribution is required; this notice records provenance.
|
|
11
|
+
- Code license of uisfx: MIT. This plugin vendors only the audio asset, not the uisfx runtime.
|
|
12
|
+
|
|
13
|
+
Source package was audited from `uisfx-0.4.0.tgz`; see the upstream repository for the complete license texts and sound-pack manifest.
|
|
14
|
+
|
|
15
|
+
## Community design/API references
|
|
16
|
+
|
|
17
|
+
This plugin was informed by the following MIT-licensed community plugins
|
|
18
|
+
(ideas and API usage patterns only; all code here is original):
|
|
19
|
+
|
|
20
|
+
- https://github.com/chromoany/dsh-notify-me
|
|
21
|
+
- https://github.com/BillCx330/dsh-plugin-notify-sounds
|
|
22
|
+
- https://github.com/Laplace-bit/dsh-bell-notify
|
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# dsh-notify-toast — Host/Client 接口契约(开发用)
|
|
2
|
+
|
|
3
|
+
> 实现时严格遵守本契约;改动契约先改本文件并在提交说明里写明。
|
|
4
|
+
> 单位:字符数按 Unicode 码点(`Array.from(text).length`)计。
|
|
5
|
+
|
|
6
|
+
## 0. 常量
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
PKG_ID = 'dsh-notify-toast' // package.json name / 客户端 ModuleLoader id / 行 id / settings ns
|
|
10
|
+
VERSION = '0.4.2'
|
|
11
|
+
RPC_CHANNEL = '/dsh-notify-toast' // 官方 Connection RPC 频道(Host rpc.handle)
|
|
12
|
+
LOCALE_NS = 'dsh-notify-toast' // 客户端 locale 命名空间
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
传输层结论(已核实 DSH 安装包内 `@deepseek-ai/dsh-client-connection` 源码与运行日志):
|
|
16
|
+
|
|
17
|
+
- 首选官方 `ctx.connection.rpc.handle('/dsh-notify-toast', handler)`:由 Connection 包负责注册前缀路由、Host/Origin 栅栏与签名 cookie 认证。
|
|
18
|
+
- **DSH 0.2.1-rc.2 实测该 API 有 bug**:其内部 `HostConnectionService.rpc.handle` 在自己的 fiber 上读取 `webServer`,抛
|
|
19
|
+
`cannot get property "webServer" without inject`(`dsh-client-connection/lib/index.js:871`),路由未注册。
|
|
20
|
+
- 因此插件实现**同路径回退路由**:用 `ctx.webServer.register({kind:'prefix', path:'/dsh-notify-toast', handler})` 注册同样的前缀路径,
|
|
21
|
+
handler 内先调 `ctx.connection.requestRejection(req)` 复用官方 Host/Origin + 浏览器签名 cookie 栅栏,再按官方一元 RPC 信封
|
|
22
|
+
`{type:'client-request', rpcId, method, payload}` → `{type:'server-response', rpcId, result}` 应答。
|
|
23
|
+
这条回退链路与官方客户端 `connection.rpc.call` 完全兼容,客户端代码不变。
|
|
24
|
+
- Client:`ctx.connection.rpc.call('/dsh-notify-toast', endpoint, payload, signal)`(相对 URL,经页面同源/`dsh-app://app` 代理到 Host),
|
|
25
|
+
返回 `{ok:true,value} | {ok:false,error}`(transport 信封)→ Host 业务值再见一层 `{ok:true,...}` / `{ok:false,error}`。
|
|
26
|
+
- 桌面壳与普通浏览器页面都适用,无需知道端口/令牌,也无需 EventSource。
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## 1. 文件所有权(并行开发,勿越界写)
|
|
30
|
+
|
|
31
|
+
| 文件 | 负责人 | 说明 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `lib/index.js`, `lib/text.js`, `lib/config.js`, `lib/native-focus.js`, `lib/native-toast.js`, `lib/taskbar-badge.js` | host-dev | Host 半 |
|
|
34
|
+
| `lib/client.js` | client-dev | Client 半(单文件,CJS 包装,无 import) |
|
|
35
|
+
| `test/config.test.mjs`, `test/merge.test.mjs`, `test/text.test.mjs`, `test/rpc-route.test.mjs`, `test/goal-summary.test.mjs` | host-dev/lead | 单元/路由/goal 状态机探针 |
|
|
36
|
+
| `scripts/client-smoke.mjs` | client-dev | 用 Node 模拟 `window/__ModuleLoader__` 做加载冒烟 |
|
|
37
|
+
| `scripts/host-probe.mjs` | lead | 假 Cordis ctx + 真实 profile cwd 的 Host 集成探针 |
|
|
38
|
+
| `package.json`, `cordis.patch.yml`, `locale/*`, `icon.svg`, `README.md`, `docs/*`, `CHANGELOG.md`, `LICENSE` | lead | 不动 |
|
|
39
|
+
|
|
40
|
+
## 2. Host 半(lib/index.js)
|
|
41
|
+
|
|
42
|
+
### 2.1 导出
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
export const name = 'dsh-notify-toast'
|
|
46
|
+
export const inject = [] // 全部用 ctx.inject([...]) 可选注入
|
|
47
|
+
export const Config = <schemastery object> // 见 §2.4;解析失败时导出 undefined
|
|
48
|
+
export function apply(ctx, config) { ... } // 注册事件/路由/清理;无全局副作用
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2.2 依赖注入(都用可选注入,缺失时降级而不是让插件不挂载)
|
|
52
|
+
|
|
53
|
+
- `ctx.inject(['connection', 'settings', 'sessions'], (child) => {...})` 里注册 RPC 频道与 settings 策略。
|
|
54
|
+
- 事件监听 `ctx.on('approval/request', ...)`、`ctx.on('user-questions/request', ...)`、`ctx.on('session/event', ...)` 挂在 apply 自己的 ctx 上(插件卸载自动清理)。
|
|
55
|
+
- 所有计时器必须用 `ctx.effect`/`ctx.timeout` 或显式清理;卸载后不得留 listener/timer。
|
|
56
|
+
|
|
57
|
+
### 2.3 浏览器传输(官方 channel 优先,同路径 webServer 路由回退)
|
|
58
|
+
|
|
59
|
+
- 路径:`/dsh-notify-toast`(前缀)。官方 `connection.rpc.handle` 可用时由它注册并认证;0.2.1-rc.2 上它会抛
|
|
60
|
+
`cannot get property "webServer" without inject`,此时插件用 `webServer.register` 注册同一路径,并在 handler 最前面调用
|
|
61
|
+
`connection.requestRejection(req)`(官方 Host/Origin + 签名 cookie 栅栏)——即同源校验仍由官方 Connection 层完成。
|
|
62
|
+
- 请求/响应信封(与官方一元 RPC 一致):
|
|
63
|
+
`{type:'client-request', rpcId, method, payload}` →
|
|
64
|
+
`{type:'server-response', rpcId, result:{ok:true,value}|{ok:false,error:{code,message,details}}}`。
|
|
65
|
+
- 端点(endpoint 字符集 `[A-Za-z0-9_$.-]+`,仅 POST + `application/json`):
|
|
66
|
+
|
|
67
|
+
| endpoint | payload | 返回 value |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `config` | `{}` | `{ok:true, version, ns, config, status:{settingsAvailable, nativeFocus, platform, clients, transport, configSource, stats, lastEvent}}` |
|
|
70
|
+
| `save` | `{patch:{...}}` | `{ok:true, config, status}`;非法字段/越界 `{ok:false,error:{code,message}}` |
|
|
71
|
+
| `poll` | `{after:number, wait:number}` | `{ok:true, events:[EventPayload...], next:number, serverTime:number}`;无事件时最多挂起 wait ms(夹 1000–25000) |
|
|
72
|
+
| `health` | `{}` | `{ok:true, version, ns, settingsAvailable, platform, nativeFocus, clients, status}` |
|
|
73
|
+
| `test` | `{}` | `{ok:true, queued:boolean}`;向事件队列发布 `kind:'test'` |
|
|
74
|
+
| `focus` | `{sessionId?:string}` | `{ok:true, native:boolean}`;尽力置前,绝不抛错 |
|
|
75
|
+
| `taskbar-badge` | `{count:number}`(0–99) | `{ok:true, count}`;0 清除,1–3 数字,>3 红点;未启用时返回 `{count:0,disabled:true}` |
|
|
76
|
+
|
|
77
|
+
- 事件队列:环形缓冲(最多 200 条);`poll` 用 `after` 游标增量取;等待者由新事件立即唤醒;客户端断开由 AbortSignal 清理;插件卸载时全部 resolve 空结果并清理计时器。
|
|
78
|
+
|
|
79
|
+
`EventPayload`(JSON,Host 只发送**未截断的清洗后原文**,截断由 Client 按配置执行):
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
{
|
|
83
|
+
id: number, // 单调递增,供 after 游标
|
|
84
|
+
kind: 'question'|'approval'|'complete'|'test',
|
|
85
|
+
at: number, // Date.now()
|
|
86
|
+
sessionId?: string,
|
|
87
|
+
sessionTitle?: string, // 能取到就带(完成通知的备选正文)
|
|
88
|
+
text?: string|null, // question: 首个问题原文;complete: 最后回复首段(清洗后,≤400 字);test: 固定文案
|
|
89
|
+
toolName?: string, // approval
|
|
90
|
+
purpose?: string, // approval: displayReason/ reason 的简短目的(已选语言,≤120 字)
|
|
91
|
+
count?: number, // complete 合并条数,默认 1
|
|
92
|
+
sessionIds?: string[], // count>1 且来自多个根会话时去重后的会话列表
|
|
93
|
+
goalOutcome?: 'complete'|'paused'|'blocked' // goal 汇总结果
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2.4 Config(schemastery,全部 `.volatile()`)
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
{
|
|
101
|
+
enabled: true,
|
|
102
|
+
questionEnabled: true,
|
|
103
|
+
approvalEnabled: true,
|
|
104
|
+
completionEnabled: true,
|
|
105
|
+
taskbarBadgeEnabled: true, // 任务栏未读角标;方案 B,默认开
|
|
106
|
+
titleOnly: false,
|
|
107
|
+
bodyLength: 40, // 20..60
|
|
108
|
+
sound: 'ding', // ding|chime|bell|soft|bubble|none
|
|
109
|
+
volume: 60, // 0..100
|
|
110
|
+
mergeIntervalMs: 2500, // 0..10000
|
|
111
|
+
allDoneTitleEnabled: false,
|
|
112
|
+
allDoneTitle: '所有任务均已完成', // 1..96 bytes
|
|
113
|
+
goalSummaryEnabled: true // goal 结束后汇总,默认开
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- 用 `createRequire(join(process.env.DSH_PROFILE_DIR,'package.json'))('@deepseek-ai/schemastery')` 解析(顶层 await,失败则 `Config=undefined`)。
|
|
118
|
+
- `ctx.inject(['settings'], c => c.effect(() => c.settings.configure({auto:false}, ctx.fiber)))`。
|
|
119
|
+
- 运行期读配置:优先 `ctx.settings.describe().find(d=>d.ns===NS)?.value` 与默认值合并(缓存 ≤500ms + `settings/document-updated` 失效);无 settings 时用 apply 收到的 config 合并默认值。
|
|
120
|
+
- 写配置:白名单 key + 类型/范围校验 → `await settings.update(NS, patch)` → 失效缓存 → `describe()` 回读;失败返回 400。
|
|
121
|
+
|
|
122
|
+
### 2.5 事件检测
|
|
123
|
+
|
|
124
|
+
1. `approval/request`:`(req, next) => { try{ publishApproval(req) }catch{} ; return next() }`(必须调用 next)。
|
|
125
|
+
2. `user-questions/request`:同上,`publishQuestion(request)`。
|
|
126
|
+
3. `session/event`:`goal/change` 更新 goal 状态;`user/message(source.kind=goal)` 记录已准入的 Goal Round;`turn/start` 跟踪 all-done;`turn/end` 在 goal active+armed 时只缓存,不逐轮发布;`goal/change` 的 complete/pause/block/clear、Round 上限或 `goal/activation-changed(disarmed)` 后按 `roundsStarted` 增量汇总一条。`goalSummaryEnabled` 默认开。
|
|
127
|
+
- 全部完成标题(可选,默认关):根会话 `turn/start` 进入 running 集合;同一时刻 ≥2 个根会话运行才 arm;`turn/end` 全部结束且无 error/aborted 时,用 `ctx.get('sessionTitle').rename(lastSession, allDoneTitle)` 改最后一个结束会话的标题;只有一个会话运行时不触发。
|
|
128
|
+
- `isRoot`:`!session.header.parentSession && session.header.origin !== 'subagent'`。
|
|
129
|
+
- 完成正文:从 `event.seq` 往前至多 200 个事件,找最近 `assistant/message`;取其中 `content[*].type==='text'` 的文本拼接,交给 `lib/text.js` 的 `firstParagraph()`;取不到 → `text:null`。
|
|
130
|
+
- 合并:把完成放进 pending 批(记录最新 text/sessionId/count++),`mergeIntervalMs` 后发布一条 `kind:'complete'`(count>1 时带 count)。计时器必须可清理。
|
|
131
|
+
4. 去重:`kind+agentId+callId/toolName` 5 秒内重复同请求忽略;完成按 `sessionId+turn` 去重。
|
|
132
|
+
5. 发布到事件队列:环形缓冲最多 200 条;所有 `/poll` 等待者立即收到;无客户端时也缓存(页面稍后连上可取到,但客户端可自行决定不展示)。
|
|
133
|
+
|
|
134
|
+
### 2.6 窗口置前(lib/native-focus.js)
|
|
135
|
+
|
|
136
|
+
- 仅 win32;按参考实现从 `app.asar.unpacked` 多候选路径加载 koffi 原生 `.node`(`createRequire(join(dirname(modulePath),'index.cjs'))(modulePath)`)。
|
|
137
|
+
- user32:EnumWindows / IsWindowVisible / GetWindowTextW / ShowWindow(SW_RESTORE) / BringWindowToTop / SetForegroundWindow / AttachThreadInput / GetForegroundWindow / GetWindowThreadProcessId / keybd_event / kernel32.GetCurrentThreadId。
|
|
138
|
+
- 窗口匹配:优先用 `process.ppid`(Electron 主进程 PID)匹配窗口 PID;否则标题包含 `DeepSeek Harness`。
|
|
139
|
+
- 加载失败/调用失败返回 `false`,绝不抛错;导出 `focusDesktopWindow(): boolean` 与 `nativeFocusAvailable(): boolean`。
|
|
140
|
+
|
|
141
|
+
### 2.7 原生 toast helper(lib/native-toast.js / native-toast.ps1 / native-toast-helper.cs)
|
|
142
|
+
|
|
143
|
+
- `createNativeToastManager()` 启动 per-Host loopback activation server(随机 token;`/activate` 校验 token、notificationId;点击后发布 `activate`)。
|
|
144
|
+
- `show()` 通过 PowerShell 以 stdin 传 base64 JSON;helper 用 WinRT `ToastNotification` 显示 `<progress>`,同 kind 使用唯一 tag,旧 helper 不立即 kill,允许自然退场。
|
|
145
|
+
- helper 启动时后台 `warm()` 预编译 source-hash 缓存 DLL;首条通知不显示 toast;预热失败不影响正常展示/Web Notification 回退。
|
|
146
|
+
- 到达近似 duration 后停止进度更新,短暂缓冲后调用 `Hide()` 收回横幅;`ExpirationTime` 约束通知中心保留,`History.Remove` 不在到期时立即调用。
|
|
147
|
+
|
|
148
|
+
## 3. Client 半(lib/client.js)
|
|
149
|
+
|
|
150
|
+
- 单文件、CJS 包装、无 `import`/`require` 除 `require('react')`;`window.__ModuleLoader__.load({ id:'dsh-notify-toast', factory(require){ ... return {inject, apply} } })`。
|
|
151
|
+
- `inject: ['slots','locale','connection']`;`uiWorkspace` 用 `ctx.get('uiWorkspace')` 可选。
|
|
152
|
+
- `apply(ctx)`:
|
|
153
|
+
1. `ctx.effect(() => ctx.locale.register(LOCALE_NS, {zh, en}))`;`const t = ctx.locale.bind(LOCALE_NS)`。
|
|
154
|
+
2. 焦点状态:`focus/blur/visibilitychange` 监听(`ctx.on` 或 `ctx.effect` 清理),`isFocused()` 现算:`document.hasFocus() && document.visibilityState !== 'hidden'`。
|
|
155
|
+
3. `call('config')` 拉配置/状态,失败重试;随后启动长轮询循环:`call('poll',{after,wait:25000}, signal)`,`after` 游标,指数退避 1s→15s,`ctx.effect` 清理 `AbortController`。
|
|
156
|
+
4. 处理事件:`config.enabled` 为假则忽略(test 例外);`kind==='test'` 强制展示+出声;其余 `isFocused()` 为真则跳过(不弹不响)。
|
|
157
|
+
5. 展示:本地化标题(question/approval/complete/test 4 条)+ 正文;`titleOnly` → body='';正文截断到 `bodyLength` 码点,超长加 '…';approval 正文 = `toolName + ' · ' + purpose`;complete 无 text → `t('fallbackBody')`。
|
|
158
|
+
6. 通知:`Notification.permission` 检查;`new Notification(...)` 兜异常;`silent:true`;不向平台传 `tag`(自管 map 关闭上一条);不主动自动关闭(Windows 横幅自动收起,条目留在通知中心供稍后点击);`onclick` → `call('focus',{sessionId})`(不 await 失败)→ `window.focus()` → `uiWorkspace.openSession(sessionId)`(有 sessionId 时)→ `close()`。
|
|
159
|
+
7. 声音:4 个 Web Audio 合成预设(ding / chime / bell / soft)+ 内嵌的 uisfx CC0「弹一下」+ none 关闭;`volume` 映射 master gain;AudioContext 懒建、播放前 `resume()`、首次 `pointerdown`/`keydown` 解锁;资源解码失败回退合成音;`ctx.effect` 卸载时 `close()`;任何异常静默。
|
|
160
|
+
8. 设置页:`settings.section` 注册(id=`dsh-notify-toast`, order=42, label thunk);组件用 React hooks + 纯 HTML(`role="switch"` 自绘开关、`input[type=range]`、`select`、`button`),主题 token 用 `var(--dsw-alias-*, fallback)`;改动 250ms 防抖 `call('save',{patch})`;显示保存状态、通知权限、Host 连接、nativeFocus、settingsAvailable 状态;「发送测试通知」按钮 → `call('test')`(Host 无响应时本地直接展示并标注)。
|
|
161
|
+
9. 权限:页面首次 `pointerdown`/`keydown` 且 `Notification.permission==='default'` 时 `Notification.requestPermission()`(一次);测试按钮点击也可触发。
|
|
162
|
+
10. 多窗口:用 `localStorage` 租约(`dsh-notify-toast:leader`,心跳 5s,15s 过期)+ `storage` 事件竞争,仅 leader 弹 toast/出声(settings 页仍各自可开)。
|
|
163
|
+
11. 任务栏角标:仅 leader 在未聚焦的 `complete` 事件上维护本窗口的未读根会话 `Set`;`taskbarBadgeEnabled` 为真时按不同 `sessionId`(合并事件用 `sessionIds`)计数,`call('taskbar-badge',{count})`;1–3 深灰底白字蓝环数字、>3 红点;窗口聚焦/点击任务栏、关闭设置或卸载时 `call('taskbar-badge',{count:0})`。Host helper 一次性调用 `ITaskbarList3::SetOverlayIcon`,不常驻。
|
|
164
|
+
- 卸载清理:所有 `ctx.effect` 返回 disposer;长轮询 Abort、AudioContext close、监听移除、任务栏角标清零。
|
|
165
|
+
|
|
166
|
+
## 4. 验收命令
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
npm test # text/config/merge/rpc-route/goal-summary/native-toast 单元测试
|
|
170
|
+
npm run smoke # scripts/client-smoke.mjs 客户端加载/行为冒烟
|
|
171
|
+
npm run probe # scripts/probe-rpc.mjs RPC 探针(--self-check / --url)
|
|
172
|
+
node scripts/host-probe.mjs <profileDir> # Host 集成探针(真实 profile cwd)
|
|
173
|
+
npm pack # 交付 tgz
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 5. 参考实现(MIT,学习思路,不整段抄)
|
|
177
|
+
|
|
178
|
+
- chromoany/dsh-notify-me:焦点/权限/设置页/多窗口 leader/通知降级
|
|
179
|
+
- BillCx330/dsh-plugin-notify-sounds:koffi user32 置前、`connection.requestRejection` 路由栅栏、volatile Config + configForms
|
|
180
|
+
- Laplace-bit/dsh-bell-notify:Web Audio 合成、settings 传输降级
|