@vanadium-23/dsh-ping 0.1.0
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/DESIGN.md +168 -0
- package/LICENSE +21 -0
- package/README.md +161 -0
- package/bin/dsh-ping.mjs +8 -0
- package/cordis.patch.yml +8 -0
- package/lib/channels-CpGxdbPv.js +426 -0
- package/lib/index.js +421 -0
- package/lib/smoke.js +65 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/channels.d.ts +109 -0
- package/lib/types/channels.d.ts.map +1 -0
- package/lib/types/channels.js +204 -0
- package/lib/types/channels.js.map +1 -0
- package/lib/types/decide.d.ts +110 -0
- package/lib/types/decide.d.ts.map +1 -0
- package/lib/types/decide.js +145 -0
- package/lib/types/decide.js.map +1 -0
- package/lib/types/defaults.d.ts +64 -0
- package/lib/types/defaults.d.ts.map +1 -0
- package/lib/types/defaults.js +51 -0
- package/lib/types/defaults.js.map +1 -0
- package/lib/types/index.d.ts +36 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +330 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/protocol.d.ts +122 -0
- package/lib/types/protocol.d.ts.map +1 -0
- package/lib/types/protocol.js +107 -0
- package/lib/types/protocol.js.map +1 -0
- package/lib/types/smoke.d.ts +17 -0
- package/lib/types/smoke.d.ts.map +1 -0
- package/lib/types/smoke.js +66 -0
- package/lib/types/smoke.js.map +1 -0
- package/package.json +66 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# dsh-ping 设计说明
|
|
2
|
+
|
|
3
|
+
记录"为什么这么写",尤其是三个只有真实运行才会暴露的问题。想快速上手看 [README.md](README.md)。
|
|
4
|
+
|
|
5
|
+
## 1. 约束:不能再被内部包改名弄坏
|
|
6
|
+
|
|
7
|
+
调研第三方候选时发现的硬事实(见 README 的对比表):`dsh-notify-me`、`dsh-notify-xc`、
|
|
8
|
+
`dsh-notify-sound` 的**客户端 bundle** 都引用了 `@deepseek-ai/dsh-client-runtime`,而这个包在
|
|
9
|
+
0.1.5 已经被改名移除。用户 profile 里那个被禁用的 `dsh-notification` 是同一个死因——它的浏览器端
|
|
10
|
+
`require` 了一个不存在的模块,导致整个 Web 客户端加载失败。
|
|
11
|
+
|
|
12
|
+
于是这个插件立了三条规矩:
|
|
13
|
+
|
|
14
|
+
1. **没有浏览器端**(`dsh.client` 字段根本不声明)→ 不可能影响 Web 客户端加载。
|
|
15
|
+
2. **不 import 任何 `dsh-*` 内部包**。宿主事件、服务、工具定义都用本地声明的结构类型描述
|
|
16
|
+
(`src/protocol.ts`),字段一律当作可选。唯一运行时依赖是 `@deepseek-ai/schemastery`,
|
|
17
|
+
它是独立版本线的 vendor 包,不随 `dsh-*` 改名而变。
|
|
18
|
+
3. **工具不用 `defineTool`**,直接注册普通对象(`{name, description, parameters, output, execute}`)。
|
|
19
|
+
modlens 就是这么做的,证明注册表接受裸定义。
|
|
20
|
+
|
|
21
|
+
代价是事件负载没有编译期类型。这被测试补回来了:负载形状是照着
|
|
22
|
+
`packages/interaction/user-approval/src/types.ts` 等宿主源码写的,并在真实 launcher 里跑过。
|
|
23
|
+
|
|
24
|
+
## 2. 事件选择
|
|
25
|
+
|
|
26
|
+
| 用途 | 事件 | 为什么是它 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| 回合结束 | `agent/status`(`running`→`idle`) | 宿主唯一的生命周期状态机,`AgentStatus` 就是 `'idle' \| 'running'`。持久事实在 `session/event` 里,但那是给回放用的,实时控制面在 `agent/*`。 |
|
|
29
|
+
| 出错 | `agent/error` | 与状态对里,但错误不该等到回合结束才说。 |
|
|
30
|
+
| 等批准 | `approval/request` | 宿主问"谁能批"的瀑布。 |
|
|
31
|
+
| 等回答 | `user-questions/request` | 同上,`ask_user_question` 工具最终走到这里。 |
|
|
32
|
+
| 回答摘要 | `session/event` 的 `assistant/message` | 通知正文里最有用的是模型最后说了什么。 |
|
|
33
|
+
|
|
34
|
+
**为什么不去 hook 审批的持久事件**:`approval/asked` 之类是落盘后的事实,而"现在有人卡在等你"
|
|
35
|
+
是实时状态。瀑布是在**问**的那一刻触发的,正好是用户需要被打断的那一秒。
|
|
36
|
+
|
|
37
|
+
### 一次回合只通知一次
|
|
38
|
+
|
|
39
|
+
`agent/status` → `running` 时记下开始时间;`agent/error` 时标记 `sawError` 并**立刻**发错误通知;
|
|
40
|
+
`idle` 时如果这一轮已经报过错就闭嘴,否则发「任务完成」。这样"先报错再结束"不会变成两条通知。
|
|
41
|
+
|
|
42
|
+
开始时间同时提供了时长,用来(a)显示"用时 2 分 13 秒",(b)支持 `minTurnDurationMs` 这个闸。
|
|
43
|
+
|
|
44
|
+
## 3. 噪音模型:什么时候**不**通知
|
|
45
|
+
|
|
46
|
+
第一版把 `minTurnDurationMs` 默认设成 `0`,理由是"不要偷偷替用户过滤掉东西"。这是错的。一个正常
|
|
47
|
+
会话每问一句就是一次回合结束,于是**每问一句弹一次通知**。用户第一反应就是"为什么在不需要通知的
|
|
48
|
+
时候频繁弹出"——这个反馈是对的,默认值本身就是设计缺陷。
|
|
49
|
+
|
|
50
|
+
通知的价值完全建立在"你可能已经走开"这个前提上,而**回合时长是宿主侧唯一拿得到、还说得通的代理
|
|
51
|
+
指标**:
|
|
52
|
+
|
|
53
|
+
| 情况 | 判定 | 理由 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| 回合 < `minTurnDurationMs`(默认 20 秒) | 不弹 | 你还在键盘前,答案你自己会看到 |
|
|
56
|
+
| 回合 >= 20 秒 | 弹 | 你可能去干别的了 |
|
|
57
|
+
| 出错 / 待批准 / 待回答 | 一定弹(只受冷却约束) | 与"你走没走开"无关,是"需要你介入" |
|
|
58
|
+
|
|
59
|
+
所以时长闸**只作用于 `done`**。这不是图省事,是语义:闸门回答的是"用户是否可能不在",而只有
|
|
60
|
+
"任务完成"这一类才需要问这个问题——出错和待决定,用户不在场恰恰是最需要叫他的时候。
|
|
61
|
+
|
|
62
|
+
顺带确认了宿主侧没有更好的信号,也没有别的噪音源:
|
|
63
|
+
|
|
64
|
+
- `agent/status` 把 `maintenance` 阶段也映射成 `idle`,但 `runMaintenance` 要求进入时已是 idle,
|
|
65
|
+
状态串走的是 idle→idle,`setPhase` 只在状态**串**变化时才 emit,所以维护阶段不会多发一条。
|
|
66
|
+
- 子代理由 `parentAgent` + `options.origin === 'subagent'` 双重标记,`rootsOnly` 能可靠过滤;
|
|
67
|
+
workflow 里各分支的完成不会刷屏。
|
|
68
|
+
- `agent/error` 只在终点失败边界 `throwError` 里 emit 一次;可重试的失败走
|
|
69
|
+
`agent/request-error` 瀑布,不会每次重试都弹。
|
|
70
|
+
- 冷却按「会话 + 类型」计,所以"批准"和"提问"互不压制,连续同类事件才会合并。
|
|
71
|
+
|
|
72
|
+
## 4. 瀑布:必须 prepend,且必须 next()
|
|
73
|
+
|
|
74
|
+
`approval/request` 和 `user-questions/request` 都是 **waterfall**:监听器要么认领请求
|
|
75
|
+
(返回结果),要么 `next()` 交给下一个。这里有两个陷阱:
|
|
76
|
+
|
|
77
|
+
1. **不 prepend 就可能永远看不到请求。** 如果真正的应答者(Web UI 那条链路)先注册,它会直接
|
|
78
|
+
认领,我的监听器根本不会被调用——插件会"装上了但从来不通知"。所以两个都用
|
|
79
|
+
`{ prepend: true }`。
|
|
80
|
+
2. **必须原样转发。** 监听器写成 `(req, next) => { 通知(req); return next() }`:先做自己的事,
|
|
81
|
+
再把 `next()` 的 Promise 原样返回。返回值不能被吞、不能被改写,否则"谁能批准"这件事就变了。
|
|
82
|
+
一个通知插件把审批搞坏,比不通知严重得多。
|
|
83
|
+
|
|
84
|
+
所有观察代码都包在 `guard()` 里。测试里专门构造了一个 getter 会抛异常的负载,验证:异常被吞掉、
|
|
85
|
+
日志里有记录、`next()` 仍然被调用。
|
|
86
|
+
|
|
87
|
+
## 5. 三个只有真实运行才会暴露的问题
|
|
88
|
+
|
|
89
|
+
### 5.1 `detached: true` 让通知静默消失(最严重)
|
|
90
|
+
|
|
91
|
+
通知用 `spawn` 异步发出、不阻塞会话。最初加了 `detached: true`,理由是"万一在 DSH 退出瞬间发的
|
|
92
|
+
通知也别被带走"。结果:**通知一条都没弹出来,而且 PowerShell 还是退出码 0**。
|
|
93
|
+
|
|
94
|
+
在隔离 profile 里跑真实事件总线时发现:控制台那行 `[dsh-ping] DSH · 任务完成 — …` 打出来了,
|
|
95
|
+
但 Windows 通知中心里没有对应记录。用四种 spawn 组合做了对照实验:
|
|
96
|
+
|
|
97
|
+
| 组合 | 退出码 | 进通知中心 |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `detached=false`, `stdio=ignore` | 0 | **是** |
|
|
100
|
+
| `detached=false`, `stdio=pipe` | 0 | **是** |
|
|
101
|
+
| `detached=true`, `stdio=ignore` | 0 | 否 |
|
|
102
|
+
| `detached=true`, `stdio=pipe` | 0 | 否 |
|
|
103
|
+
|
|
104
|
+
结论:detached 的 Windows 进程没有控制台,Windows PowerShell 5.1 在这个状态下加载 WinRT 类型、
|
|
105
|
+
调用 `Show()`、然后正常退出,**什么都没发生**。去掉 `detached`,靠 `unref()` 保持非阻塞。
|
|
106
|
+
|
|
107
|
+
这个坑能被抓到,唯一原因是验证做在了"通知中心里有没有这条记录"这一层,而不是"进程退出码是不是 0"。
|
|
108
|
+
`tests/toast.e2e.mjs` 现在同时覆盖同步诊断路径和**真实投递路径**——两者 spawn 参数不同,
|
|
109
|
+
而只有后者在会话里跑。
|
|
110
|
+
|
|
111
|
+
### 5.2 手搓的 `session/event` 能把整棵树打崩
|
|
112
|
+
|
|
113
|
+
真实 launcher 的驱动脚本最初 `ctx.emit('session/event', session, {type:'assistant/message', …})`
|
|
114
|
+
来喂回答摘要。结果:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
dsh: fatal load failure: TypeError: SessionLogOffset must be a non-negative safe integer, got undefined
|
|
118
|
+
at SessionProjectionRegistry.drive
|
|
119
|
+
at ping-driver.mjs:31:9
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
伪造的事件缺了 `seq` 之类的持久信封字段,session-projection 的监听器直接抛异常,整棵树倒了。
|
|
123
|
+
|
|
124
|
+
两件事:其一,这**不是** dsh-ping 的问题——它只读事件、从不 `emit`,而且自己的监听器都有 guard;
|
|
125
|
+
其二,它说明**测试夹具也必须遵守宿主契约**。夹具改成只发安全的 `agent/status` 和两个瀑布,
|
|
126
|
+
回答摘要的提取逻辑改由 `tests/plugin.test.mjs` 对着 stub context 覆盖。测试因此分成三层:
|
|
127
|
+
|
|
128
|
+
| 层 | 覆盖什么 | 需要什么 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `decide.test.mjs` | 判定规则、文本组装、XML 转义与注入抵抗 | 无 |
|
|
131
|
+
| `plugin.test.mjs` | 装配、去重、瀑布转发、异常隔离 | 假 context |
|
|
132
|
+
| `toast.e2e.mjs` | 通知真的进了 Windows 通知平台 | Windows + 真实弹窗 |
|
|
133
|
+
| 隔离 profile 手工验证 | 真实 launcher 挂载 + 真实事件总线 | 一个 lab profile |
|
|
134
|
+
|
|
135
|
+
### 5.3 正文里工作区名重复
|
|
136
|
+
|
|
137
|
+
`buildNotice` 在会话没有标题时用工作区名兜底当主语,但附加信息行里又拼了一次工作区,
|
|
138
|
+
于是出现 `… · api · api`。单元测试抓到后改成:附加行里的工作区与主语相同时不再重复。
|
|
139
|
+
|
|
140
|
+
## 6. 安全:通知内容不碰 PowerShell 源码
|
|
141
|
+
|
|
142
|
+
设计目标:**会话里的任何文本都不可能被当作 PowerShell 求值**。做法是让 PowerShell 脚本成为一个
|
|
143
|
+
不含任何插值的常量:
|
|
144
|
+
|
|
145
|
+
- 脚本经 `-EncodedCommand`(base64 UTF-16LE)传入,命令行上不出现文本;
|
|
146
|
+
- 通知内容在 **JS 里**组装成 XML,XML 特殊字符先转义;
|
|
147
|
+
- XML 再 base64 编码,经环境变量 `DSH_PING_XML` 送达;脚本只做
|
|
148
|
+
`$xml.LoadXml([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($env:DSH_PING_XML)))`。
|
|
149
|
+
|
|
150
|
+
`LoadXml` 的参数是一个 .NET 字符串**值**,不是 PowerShell 表达式,所以文本里的 `$(...)`、反引号、
|
|
151
|
+
`'@` 都只是数据。文本里的换行还会被 `flatten()` 压平,进一步保证不会提前终止任何宿主语法。
|
|
152
|
+
|
|
153
|
+
测试里用一段同时包含 `"$(Start-Process calc.exe)"`、`'@`、`</text></binding>…` 的恶意标题,
|
|
154
|
+
断言:注入的文本没有多出任何元素(结构里 `<toast>`/`<binding>` 各只出现一次)、属性不能被提前
|
|
155
|
+
闭合、脚本里不含注入文本。*(这条断言最初写错了——把"文本节点里的 `$()`"当成了注入。它确实会
|
|
156
|
+
出现在 XML 里,但那是数据。断言改成了真正成立的性质:文本无法离开自己的节点。)*
|
|
157
|
+
|
|
158
|
+
另外子进程环境是**白名单重建**的(`minimalEnv()`),通知助手只拿到 Windows 必需的那几个变量,
|
|
159
|
+
不会顺带继承 API Key。
|
|
160
|
+
|
|
161
|
+
## 7. 不做的事
|
|
162
|
+
|
|
163
|
+
- **不做浏览器端通知**:那是踩雷最多的路径(`Notification` API + `dsh-client-*` 内部包)。
|
|
164
|
+
系统通知在窗口切走时反而更可靠。
|
|
165
|
+
- **不去重到"完全静音"**:宁可让用户用 `debug: true` 看到每条被抑制的通知和原因,
|
|
166
|
+
也不引入一套猜不透的抑制规则。
|
|
167
|
+
- **不做"窗口聚焦检测"**:宿主侧拿不到浏览器焦点,只能用时长的近似。假装知道反而更烦人。
|
|
168
|
+
- **不感知前台焦点**:宿主侧拿不到浏览器焦点,硬猜反而更烦人。交给 `minTurnDurationMs`/`cooldownMs`。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niobium-41-nb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# dsh-ping
|
|
2
|
+
|
|
3
|
+
给 DeepSeek Harness 用的**桌面通知插件**:任务跑完、出错、等你批准、等你回答的时候,弹一个
|
|
4
|
+
Windows 原生通知,顺便在终端留一行。跑长任务时可以切走窗口,不用一直盯着页面。
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
DSH · 任务完成
|
|
8
|
+
重构完成,91 项断言全过。
|
|
9
|
+
重构 dsh-ping · proj · 2 分 13 秒
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 为什么不直接用现成的
|
|
13
|
+
|
|
14
|
+
GitHub / npm 上 dsh 通知插件不少,但**能装在你这套 DSH(0.1.5-rc.1)上的不多**。我把候选包拉下来
|
|
15
|
+
逐个查了依赖面,结果:
|
|
16
|
+
|
|
17
|
+
| 包 | 周下载量 | 结论 |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `dsh-notify-me` | 5039 | ✗ 客户端 bundle 声明依赖 `@deepseek-ai/dsh-client-runtime`(该包在 0.1.5 已被移除),并且只声明兼容到 0.1.2-rc.1 |
|
|
20
|
+
| `dsh-notify-xc` | 257 | ✗ `lib/client.js` 里直接 `require` 了同一个已移除的包 |
|
|
21
|
+
| `dsh-notify-sound` | — | ✗ 同样引用已移除的包 |
|
|
22
|
+
| `dsh-notify`(Pasumao) | 112 | ✓ 可用,宿主侧 Windows Toast + 托盘图标 |
|
|
23
|
+
| `dsh-notify-windows`(SeverusZh) | 236 | ✓ 可用,宿主侧 Windows Toast |
|
|
24
|
+
|
|
25
|
+
你机器上那个**已经装了但被禁用**的 `dsh-notification` 就是同一个死因:它的浏览器端还
|
|
26
|
+
`require("@deepseek-ai/dsh-client-runtime/client")`,装上会让整个 Web 客户端卡在
|
|
27
|
+
"Failed to load plugins"。
|
|
28
|
+
|
|
29
|
+
所以这个插件按同一个约束从零写:**宿主侧、没有浏览器端、运行时只 import 一个 `schemastery`**
|
|
30
|
+
(配置校验库,独立于 `dsh-*` 内部包),不去碰任何会改名的内部模块。这也是它比上面那些"能用"
|
|
31
|
+
的候选更抗版本漂移的地方。
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 在 E:\deepseek-harness 下执行
|
|
37
|
+
node --import tsx/esm apps/cli/src/bin.ts plugin --profile web add file:E:/dsh-plugin/dsh-ping
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`dsh-ping` 的 package.json 声明了 `dsh.bundle.patch`,`dsh plugin add` 会自动把它追加进
|
|
41
|
+
`dsh.profile.bundles`,**不需要**再往 `cordis.patch.yml` 里手写 insert 行(两处都写会
|
|
42
|
+
`duplicate loader entry id`)。装完重启一次 DSH 生效。
|
|
43
|
+
|
|
44
|
+
卸载:`... plugin --profile web remove dsh-ping`
|
|
45
|
+
|
|
46
|
+
## 什么时候会通知
|
|
47
|
+
|
|
48
|
+
| 时机 | 判定依据 | 标题 | 声音 |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| 一轮任务跑完 | `agent/status` 由 `running` 变 `idle` | `DSH · 任务完成` | Default |
|
|
51
|
+
| 出错 | `agent/error` | `DSH · 出错了` | Default |
|
|
52
|
+
| 等你批准 | `approval/request` 瀑布 | `DSH · 等你批准` | Reminder |
|
|
53
|
+
| 等你回答 | `user-questions/request` 瀑布 | `DSH · 等你回答` | Reminder |
|
|
54
|
+
|
|
55
|
+
后两个是**长通知**(`duration="long"`)并且用不同的提示音,所以在通知中心一眼能分辨
|
|
56
|
+
"跑完了"和"卡在等你"。
|
|
57
|
+
|
|
58
|
+
一轮里如果先出错、再结束,只会有一条「出错了」,不会重复。
|
|
59
|
+
|
|
60
|
+
## 不打扰你的几道闸
|
|
61
|
+
|
|
62
|
+
通知只在"你可能已经走开"时才值得打断你。判定规则:
|
|
63
|
+
|
|
64
|
+
- **`minTurnDurationMs`(默认 20 秒)**:**只作用于「任务完成」**。短于 20 秒的回合说明你人还在键盘前,
|
|
65
|
+
响一声纯属打扰 —— 你正看着屏幕上的答案。想让每轮都提醒就设 `0`。
|
|
66
|
+
- **`cooldownMs`(默认 30 秒)**:同一会话同一类型 30 秒内只提醒一次,用来吸收连续回合和抖动。
|
|
67
|
+
- **`rootsOnly`(默认开)**:只通知根会话。子代理(subagent)、workflow 里各分支的完成都不刷屏。
|
|
68
|
+
- **出错 / 等你批准 / 等你回答不受时长闸限制**:这三类是"需要你介入",哪怕只跑了 2 秒也该叫你
|
|
69
|
+
(仍然受 30 秒冷却约束)。
|
|
70
|
+
- 每个时机都能单独关:`notifyOn.done / error / approval / question`;整个插件也能关:`enabled: false`。
|
|
71
|
+
|
|
72
|
+
一句话版本:**长时间任务的完成会叫你,短对话的完成不会;出错和等你决定,任何时候都叫你。**
|
|
73
|
+
|
|
74
|
+
> 想确认某条通知为什么没弹(或为什么弹了),把 `debug: true` 打开,每条被抑制的通知和原因都会打到
|
|
75
|
+
> stderr,例如 `suppressed done (too-short)` / `suppressed done (cooldown)`。
|
|
76
|
+
|
|
77
|
+
## 先自检,再依赖它
|
|
78
|
+
|
|
79
|
+
**命令行**(不用启动 DSH,会真弹一条通知):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node E:/dsh-plugin/dsh-ping/lib/smoke.js 随便一句正文
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**会话里**:让模型调用 `dsh_ping_test` 工具,它会弹一条自检通知并回报走通了哪些通道。
|
|
86
|
+
|
|
87
|
+
**没弹出来?** 按顺序查:`设置 → 系统 → 通知` 里 **Windows PowerShell** 是否被允许;
|
|
88
|
+
「专注助手 / 免打扰」是否开着;通知是否进了通知中心但没弹横幅。
|
|
89
|
+
(Windows 通知的发送者是 Windows PowerShell 的 AUMID,这是脚本弹 Toast 的标准做法。)
|
|
90
|
+
|
|
91
|
+
## 配置
|
|
92
|
+
|
|
93
|
+
在 profile 的 `cordis.patch.yml` 里按行 id 覆盖:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
- id: dsh-ping
|
|
97
|
+
config:
|
|
98
|
+
rootsOnly: true # 子代理不通知
|
|
99
|
+
cooldownMs: 30000 # 同会话同类最小间隔
|
|
100
|
+
minTurnDurationMs: 20000 # 「任务完成」短于这个时长不通知(只作用于完成,0 = 每轮都通知)
|
|
101
|
+
notifyOn:
|
|
102
|
+
done: true
|
|
103
|
+
error: true
|
|
104
|
+
approval: true
|
|
105
|
+
question: true
|
|
106
|
+
channels:
|
|
107
|
+
toast: true # Windows 原生通知
|
|
108
|
+
console: true # 终端里打一行
|
|
109
|
+
webhook: false # POST 到你自己的地址
|
|
110
|
+
webhookUrl: '' # 例如 https://example.com/hook
|
|
111
|
+
url: '' # 点击通知跳转的地址;留空自动用 Web GUI 的回环地址
|
|
112
|
+
maxBodyChars: 180 # 正文摘要截断长度
|
|
113
|
+
titles: # 想改文案就改这里
|
|
114
|
+
done: 'DSH · 任务完成'
|
|
115
|
+
error: 'DSH · 出错了'
|
|
116
|
+
approval: 'DSH · 等你批准'
|
|
117
|
+
question: 'DSH · 等你回答'
|
|
118
|
+
debug: false # 打开后会把每条被抑制的通知和原因打到 stderr
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 安全
|
|
122
|
+
|
|
123
|
+
- **通知正文永远不进 PowerShell 源码。** 脚本是一个常量,经 `-EncodedCommand` 传入;通知内容
|
|
124
|
+
以 XML 形式经环境变量送到脚本里,直接交给 WinRT 的 XML 解析器。会话标题、报错信息、工具名里
|
|
125
|
+
写什么都不可能被当作 PowerShell 求值。
|
|
126
|
+
- **XML 值全部转义**,文本不可能跳出自己的节点;文本里的换行会被压平,因此也不可能提前终止任何
|
|
127
|
+
宿主语法。
|
|
128
|
+
- **子进程环境是白名单重建的**,只保留 `SystemRoot`/`PATH`/`TEMP` 之类 Windows 必需项 ——
|
|
129
|
+
通知助手不会顺带继承你的 API Key、代理凭据或 token。
|
|
130
|
+
- 插件只**读**宿主事件,从不 `emit`;两个瀑布监听器一律 `return next()`,所以它不可能改变
|
|
131
|
+
"谁来批准"。
|
|
132
|
+
|
|
133
|
+
## 已知限制
|
|
134
|
+
|
|
135
|
+
- Windows Toast 依赖 Windows PowerShell 5.1(`pwsh` 不行,WinRT 类型投影只在 5.1 里)。
|
|
136
|
+
非 Windows 主机上 toast 通道自动跳过,console / webhook 照常工作。
|
|
137
|
+
- 通知无法感知"浏览器窗口是不是在前台",只能用回合时长近似判断你有没有走开。所以一个跑了
|
|
138
|
+
25 秒的任务,即使你一直盯着屏幕也会弹一次。调大 `minTurnDurationMs` 可以更安静;
|
|
139
|
+
只想要"需要我介入"的提醒,就把 `notifyOn.done` 关掉。
|
|
140
|
+
- 点击通知只能打开 Web GUI 根地址(DSH 的 Web 端目前没有会话级深链路由)。
|
|
141
|
+
|
|
142
|
+
## 开发
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit # 类型检查
|
|
146
|
+
node node_modules/typescript/bin/tsc -b tsconfig.json # 编译
|
|
147
|
+
node node_modules/tsdown/dist/run.mjs # 打包
|
|
148
|
+
|
|
149
|
+
node --experimental-strip-types tests/decide.test.mjs # 59 项:判定规则与文本/XML 组装
|
|
150
|
+
node --experimental-strip-types tests/plugin.test.mjs # 43 项:假上下文里的装配与瀑布行为
|
|
151
|
+
node --experimental-strip-types tests/toast.e2e.mjs # 10 项:真弹通知并回读通知中心
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`node_modules` 里的 `@deepseek-ai/*` 是指向 `E:\deepseek-harness` 的 junction,只用于编译期类型。
|
|
155
|
+
|
|
156
|
+
**改完代码要让 profile 用上新构建**:pnpm 对 `file:` 依赖是按内容快照装的,直接再跑 `add` 会说
|
|
157
|
+
"Already up to date"。要 `remove` 再 `add`,然后重启 DSH。
|
|
158
|
+
|
|
159
|
+
接下来要做什么,见 [ROADMAP.md](ROADMAP.md);工作区级的硬规则与验证手册见 `../AGENTS.md`。
|
|
160
|
+
|
|
161
|
+
详见 [DESIGN.md](DESIGN.md)。
|
package/bin/dsh-ping.mjs
ADDED
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# dsh-ping bundle layer: mount the desktop-notification plugin.
|
|
2
|
+
#
|
|
3
|
+
# The plugin watches host events only (turn end, agent error, pending approval,
|
|
4
|
+
# pending question) and has no browser half, so it cannot affect what the Web
|
|
5
|
+
# client loads.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: dsh-ping
|
|
8
|
+
name: 'dsh-ping'
|