dsh-notify 0.1.1 → 0.1.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/README.md CHANGED
@@ -1,126 +1,40 @@
1
- # dsh-plugin-notify
1
+ # dsh-notify
2
2
 
3
- > ⚠️ **AI 生成项目声明**
4
- >
5
- > 本项目(代码、脚本、文档)**由 AI 辅助生成**,仅用于学习和参考。
6
- > 使用前请自行阅读、审查并充分测试代码,按需修改后再部署到你的环境。
3
+ > ⚠️ **AI 生成项目**:代码与文档由 AI 辅助生成,仅供学习参考,使用前请自行审查。
7
4
 
8
- dsh Web GUI 的 Windows 通知插件:当 agent **不再运行**时(任务完成 / 被停止 / 出错 / 停下来等你选择 / 会话被关闭),弹一条 Windows 原生 Toast 提醒你回来看结果。适合发起绘画、生成等长任务后离开电脑的场景。
9
-
10
- - **纯 host 端插件**:零运行时依赖、零构建步骤,普通 ESM;
11
- - **通知常驻**:`scenario="reminder"`,直到你手动关闭;
12
- - **位置标注**:通知正文显示 `工作区「…」 · 会话「…」`,一眼看出是哪个工作区的哪个会话;
13
- - **系统托盘**:任务栏右下角常驻 dsh 鲸鱼图标,双击/右键「打开 dsh」、右键「关闭进程」终止 dsh 后台,dsh 退出后图标自动消失。
14
-
15
- ## 目录
16
-
17
- - [触发条件](#触发条件)
18
- - [安装](#安装)
19
- - [配置](#配置config均有默认值)
20
- - [测试](#测试)
21
- - [排障](#排障)
22
- - [相关文档](#相关文档)
23
-
24
- ## 触发条件
25
-
26
- | 场景 | 信号 | 通知 |
27
- |---|---|---|
28
- | 任务正常完成 | `agent/status` running→idle + `turn/end: completed` | 「dsh · 任务完成」 |
29
- | 你点了停止 / 被中止 | `turn/end: aborted` | 「dsh · 任务已停止」 |
30
- | 执行出错 | `turn/end: error` | 「dsh · 任务出错」 |
31
- | 达到输出上限 | `turn/end: max-tokens` | 「dsh · 达到输出上限」 |
32
- | agent 停下来让你选择 | 未配对的 `ask_user_question` 工具调用 | 「dsh · 在等你选择」 |
33
- | 运行中的会话被关闭 | `agent/disposed` | 「dsh · 会话已关闭」 |
34
-
35
- > **位置标注说明**:每条通知正文带一行 `工作区「…」 · 会话「…」`。
36
- > 工作区取 `workspaceRegistry` 中该会话的归属标题(兜底用会话 cwd 的目录名),
37
- > 会话取 sidebar 显示的会话标题(无标题时兜底用会话 id 短号,如 `#a1b2c3d4`)。
38
- > 两个服务都不可用时该行自动省略,不影响通知。
5
+ dsh 的 Windows 通知插件:agent 不再运行时(完成 / 停止 / 出错 / 等你选择 / 会话关闭)弹原生 Toast 提醒,正文标注「工作区 · 会话」,托盘常驻鲸鱼图标。
39
6
 
40
7
  ## 安装
41
8
 
42
- 本插件是标准的 DSH **bundle**(`package.json` 声明 `dsh.bundle.patch`,包内自带
43
- `cordis.patch.yml`),安装后会自动加入 profile 的 bundle 层,无需手动写补丁。
44
-
45
- ### 方式一:npm(推荐)
46
-
47
9
  ```powershell
48
10
  dsh plugin --profile web add dsh-notify
49
11
  ```
50
12
 
51
- ### 方式二:GitHub 直接安装(无需 npm)
52
-
53
- ```powershell
54
- dsh plugin --profile web add github:Pasumao/dsh-plugin-notify
55
- ```
56
-
57
- > git 源插件若带构建脚本,pnpm 会要求先在 `profiles\web\pnpm-workspace.yaml` 的
58
- > `allowBuilds` 里放行对应包名;本项目零构建(无 prepare 脚本),一般无需配置。
13
+ GitHub 安装:`dsh plugin --profile web add github:Pasumao/dsh-plugin-notify`
59
14
 
60
- ### 方式三:本地路径(开发 / 私有测试)
15
+ 装完重启 `dsh web`,任务栏出现鲸鱼图标即生效。
61
16
 
62
- ```powershell
63
- dsh plugin --profile web add C:/Users/18303/Desktop/dsh-plugin-notify
64
- ```
65
-
66
- ### 方式四:手动 link(可选,等同旧版流程)
67
-
68
- 1. 在 `C:\Users\18303\.dsh\profiles\web\package.json` 的 dependencies 加:
17
+ ## 触发时机
69
18
 
70
- ```jsonc
71
- "dependencies": {
72
- // ...已有依赖...
73
- "dsh-notify": "link:C:/Users/18303/Desktop/dsh-plugin-notify"
74
- }
75
- ```
19
+ 任务完成、被中止、执行出错、达到输出上限、停下来等你选择、运行中的会话被关闭。
76
20
 
77
- 2. profile 目录执行安装:
21
+ ## 配置
78
22
 
79
- ```powershell
80
- cd C:\Users\18303\.dsh\profiles\web
81
- pnpm install
82
- ```
83
-
84
- 安装完成后 **重启 `dsh web`**:任务栏右下角出现 dsh 鲸鱼小图标即加载成功。
85
- 如需调整通知行为,在 profile 的 `cordis.patch.yml` 按 id 覆盖 `config` 即可
86
- (例如 `- id: dsh-plugin-notify` + `config: { cooldownMs: 5000 }`)。
87
-
88
- ## 配置(`config`,均有默认值)
23
+ 在 profile 的 `cordis.patch.yml` 按 id `dsh-plugin-notify` 覆盖 config。常用项:
89
24
 
90
25
  | key | 默认 | 说明 |
91
26
  |---|---|---|
92
- | `enabled` | `true` | 总开关 |
93
- | `tray` | `true` | 系统托盘图标开关(`false` 关闭) |
94
- | `rootsOnly` | `true` | 仅根会话通知;子代理不刷屏 |
95
- | `notifyFinished` / `notifyAborted` / `notifyError` / `notifyWaiting` / `notifyDisposed` | `true` | 分类型开关 |
96
- | `cooldownMs` | `10000` | 同一会话、同一类型两次通知最小间隔(毫秒) |
97
- | `titlePrefix` | `'dsh'` | 通知标题前缀(形如 `dsh · 任务完成`) |
98
- | `iconPath` | 内置 `assets/dsh.png` | 通知 logo PNG 路径;设为空串 `''` 则不显示自定义图标 |
99
- | `webUrl` | `http://127.0.0.1:3080` | dsh 页面地址(点击通知/托盘「打开 dsh」跳转的地址) |
100
- | `aumid` | PowerShell 5.1 的 AUMID | Toast 归属的 AppUserModelID(一般无需改) |
101
- | `summaryMaxChars` | `40` | 任务摘要截断长度 |
102
- | `powershellPath` | 自动 | 覆盖 PowerShell 5.1 路径 |
27
+ | `cooldownMs` | `10000` | 同会话同类型两次通知的最小间隔(毫秒) |
28
+ | `rootsOnly` | `true` | 仅根会话通知,子代理不刷屏 |
29
+ | `tray` | `true` | 托盘图标开关 |
30
+ | `titlePrefix` | `'dsh'` | 通知标题前缀 |
103
31
 
104
32
  ## 测试
105
33
 
106
- 通知管道单测(英文 Toast):
107
-
108
34
  ```powershell
109
- powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/smoke-notice.ps1
110
- ```
111
-
112
- 插件逻辑端到端自测(无需 dsh、无需重启,弹「等待选择 / 任务完成 / 任务已停止」三条中文 Toast):
113
-
114
- ```powershell
115
- node scripts/test-harness.mjs
35
+ node scripts/test-harness.mjs # 弹三条真实 Toast 自测
116
36
  ```
117
37
 
118
38
  ## 排障
119
39
 
120
- - **Toast 不出现**:确认 Windows「通知与操作」里允许 PowerShell 显示通知;首次加载 WinRT 模块稍慢属正常。
121
- - **图标不显示**:确认 `assets/dsh.png`(通知)与 `assets/dsh.ico`(托盘)随插件目录一起存在。
122
- - **中文乱码**:经环境变量(UTF-16)与 `-EncodedCommand`(UTF-16LE base64)传参,不应乱码。
123
-
124
- ## 相关文档
125
-
126
- - `设计文稿.md`:设计思路、对 dsh 源码的调研结论、实现细节与边界情况。
40
+ Toast 不出现:检查 Windows「通知与操作」是否允许 PowerShell 显示通知。
package/lib/index.js CHANGED
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * 通知正文除了任务摘要,还会标注「工作区 · 会话」位置行:工作区标题取自
9
9
  * `ctx.workspaceRegistry`(按会话归属匹配,兜底用会话 cwd 的目录名),会话
10
- * 显示名取自 `ctx.sessionTitle`(sidebar 标题,兜底用会话 id 短号)。两个服务
11
- * 都不可用时自动省略,不影响通知本身。
10
+ * 显示名优先官方 `ctx.sessionTitle` 服务、再直接折会话日志的 `session/title`
11
+ * 事件(与 UI sidebar 一致),兜底用会话 id 短号。服务都不可用时自动省略,
12
+ * 不影响通知本身。
12
13
  *
13
14
  * 触发信号(均为主机端 Cordis 事件):
14
15
  * - `agent/status`(idle ⇄ running):running→idle 即「不再运行」。
@@ -185,14 +186,32 @@ function resolveWorkspaceLabel(ctx, session) {
185
186
  return truncate(pathBasename(session?.header?.cwd), 40)
186
187
  }
187
188
 
188
- /** 解析会话显示名:优先 sidebar 标题(sessionTitle),兜底用会话 id 短号。 */
189
+ /**
190
+ * 解析会话显示名:优先 sidebar 标题,兜底用会话 id 短号。
191
+ * 标题先走官方 `sessionTitle` 服务(存在时),再直接从会话日志折
192
+ * `session/title` 事件(与 `foldSessionTitle` 同款)——插件 ctx 上服务不可见时
193
+ * 也能取到标题,与 UI 显示一致。
194
+ */
189
195
  function resolveSessionLabel(ctx, session) {
190
196
  try {
191
197
  const snapshot = ctx.sessionTitle?.get?.(session)
192
198
  const title = snapshot?.title
193
199
  if (typeof title === 'string' && title.trim()) return truncate(title.trim(), 40)
194
200
  } catch {
195
- // sessionTitle 服务不可用 / 会话已关闭时忽略,走 id 兜底。
201
+ // sessionTitle 服务不可用 / 会话已关闭时忽略,继续走日志折叠。
202
+ }
203
+ try {
204
+ const events = session?.events
205
+ if (Array.isArray(events)) {
206
+ for (let index = events.length - 1; index >= 0; index--) {
207
+ const event = events[index]
208
+ if (event?.type === 'session/title' && typeof event.data?.title === 'string' && event.data.title.trim()) {
209
+ return truncate(event.data.title.trim(), 40)
210
+ }
211
+ }
212
+ }
213
+ } catch {
214
+ // 会话已分离等异常时忽略,走 id 兜底。
196
215
  }
197
216
  return typeof session?.id === 'string' && session.id ? `#${session.id.slice(0, 8)}` : ''
198
217
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-notify",
3
3
  "description": "DeepSeek Harness (dsh) Windows notification plugin: a native toast when the agent stops running (finished / aborted / error / waiting for your choice / session closed), plus a system-tray icon. Pure host-side, zero dependencies, zero build.",
4
- "version": "0.1.1",
4
+ "version": "0.1.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -0,0 +1,30 @@
1
+ import { zstdDecompressSync } from 'node:zlib'
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ const file = process.argv[2]
5
+ const buf = readFileSync(file)
6
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
7
+ const frames = []
8
+ let i = 0
9
+ while (i < buf.length) {
10
+ const idx = buf.indexOf(MAGIC, i)
11
+ if (idx < 0) break
12
+ let next = buf.indexOf(MAGIC, idx + 4)
13
+ if (next < 0) next = buf.length
14
+ frames.push(zstdDecompressSync(buf.subarray(idx, next)))
15
+ i = next
16
+ }
17
+ const text = Buffer.concat(frames).toString('utf8')
18
+ const lines = text.split('\n').filter(Boolean)
19
+ console.log('total events:', lines.length)
20
+ const titles = lines.filter((l) => l.includes('session/title'))
21
+ console.log('session/title events:', titles.length)
22
+ for (const t of titles.slice(-3)) console.log('TITLE:', t.slice(0, 500))
23
+ console.log('--- user/message + turn/end (last 12) ---')
24
+ let shown = 0
25
+ for (const l of lines) {
26
+ if (l.includes('user/message') || l.includes('turn/end')) {
27
+ console.log(l.slice(0, 300))
28
+ if (++shown >= 12) break
29
+ }
30
+ }
@@ -30,10 +30,7 @@ const ctx = {
30
30
  },
31
31
  ],
32
32
  },
33
- /** mock sessionTitle:返回 sidebar 标题。 */
34
- sessionTitle: {
35
- get: (s) => (s?.id === 'test-session' ? { title: '画一只赛博朋克猫' } : undefined),
36
- },
33
+ // 故意不提供 sessionTitle 服务:验证「直接折会话日志 session/title」路径。
37
34
  on(name, cb) {
38
35
  if (!handlers.has(name)) handlers.set(name, [])
39
36
  handlers.get(name).push(cb)
@@ -59,6 +56,8 @@ const emit = (name, ...args) => {
59
56
  const session = {
60
57
  id: 'test-session',
61
58
  header: { cwd: 'C:\\Users\\18303\\Desktop\\dsh-plugin-notify' }, // 无 delegationDepth = 根会话
59
+ // 会话日志(与真实 dsh Session.events 同形):含 session/title 事件。
60
+ events: [{ type: 'session/title', data: { title: '画一只赛博朋克猫' } }],
62
61
  }
63
62
  const agent = { id: 'test-session', status: 'running', session }
64
63
 
@@ -72,7 +72,7 @@ agent 循环 ──agent/status──▶ 状态机(每会话) ──running→id
72
72
 
73
73
  `TOAST_SCRIPT` 用 `ToastNotificationManager` 发 `<toast scenario="reminder" activationType="protocol" launch="<webUrl>">`(整条常驻且点击打开 dsh 页面),`<image placement="appLogoOverride" src="file:///…dsh.png">` 显示鲸鱼 logo。脚本经 `-EncodedCommand` 传入、数据经 `env` 传入;进程失败只 `ctx.logger.warn`,不影响 dsh。
74
74
 
75
- 正文由三个 `<text>` 组成:标题、任务摘要/等待文案、位置行(`工作区「…」 · 会话「…」`,经 `DSH_NOTICE_LOCATION` 环境变量传入,为空则省略该元素)。位置行在 `notify()` 时现取:`resolveWorkspaceLabel` 先按 `workspaceRegistry.list()` 的 `sessionIds` 匹配归属标题、兜底 cwd 目录名;`resolveSessionLabel` `sessionTitle.get(session).title`、兜底会话 id 短号;两者均含长度截断,服务缺失时自动降级(无服务 → 无位置行)。
75
+ 正文由三个 `<text>` 组成:标题、任务摘要/等待文案、位置行(`工作区「…」 · 会话「…」`,经 `DSH_NOTICE_LOCATION` 环境变量传入,为空则省略该元素)。位置行在 `notify()` 时现取:`resolveWorkspaceLabel` 先按 `workspaceRegistry.list()` 的 `sessionIds` 匹配归属标题、兜底 cwd 目录名;`resolveSessionLabel` 优先 `sessionTitle.get(session).title`,再直接折会话日志 `session/title` 事件(与 `foldSessionTitle` 同款,插件 ctx 上服务不可见时仍能取到标题)、兜底会话 id 短号;两者均含长度截断,服务缺失时自动降级(无服务 → 无位置行)。
76
76
 
77
77
  ### 5.2 系统托盘图标
78
78