@kesike/reminder-dsh 0.2.0 → 0.2.1
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 +23 -13
- package/README.zh-CN.md +36 -22
- package/example.patch.yml +1 -0
- package/lib/client.js +165 -57
- package/package.json +2 -2
- package/src/index.js +5 -1
- package/src/modules/popup/ToastActivator.cs +33 -5
- package/src/modules/popup/ToastIdentity.cs +71 -0
- package/src/modules/popup/approval.js +60 -0
- package/src/modules/popup/index.js +77 -8
- package/src/modules/popup/open.js +33 -0
- package/src/modules/popup/reply.js +49 -18
- package/src/modules/popup/summary.js +16 -3
- package/src/modules/popup/toast.js +30 -10
- package/src/modules/popup/toast.ps1 +77 -18
- package/src/modules/taskbar/badge.ps1 +237 -0
- package/src/modules/taskbar/index.js +2 -2
- package/src/modules/taskbar/windows.js +16 -13
- package/src/reminder.js +13 -1
- package/src/modules/taskbar/flash.ps1 +0 -107
package/README.md
CHANGED
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Windows reminders for DeepSeek Harness, with two independently configurable modules:
|
|
4
4
|
|
|
5
|
-
- **Taskbar
|
|
5
|
+
- **Taskbar completion badge:** show the number of unread completed sessions on the lower-right corner of the DSH taskbar icon.
|
|
6
6
|
- **Windows system notifications:** show an AI task summary in a native notification banner and Notification Center. The reply field sends a message to the original DSH session.
|
|
7
7
|
|
|
8
8
|
[完整中文说明](README.zh-CN.md)
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
|
-
Requires Windows 10/11, Node.js 24.9.0+, and DSH `>=0.2.2-rc.1 <0.3.0`. System notifications use the built-in Windows PowerShell.
|
|
12
|
+
Requires Windows 10/11, Node.js 24.9.0+, and DSH `>=0.2.2-rc.1 <0.3.0`. System notifications use the built-in Windows PowerShell. The taskbar badge requires PowerShell 7 (`pwsh.exe` on PATH).
|
|
13
13
|
|
|
14
14
|
```powershell
|
|
15
|
-
dsh plugin --profile web add
|
|
15
|
+
dsh plugin --profile web add @kesike/reminder-dsh
|
|
16
16
|
dsh --profile web
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`@kesike/reminder-dsh` is published on [npm](https://www.npmjs.com/package/@kesike/reminder-dsh). The same version installs straight from GitHub with `dsh plugin --profile web add github:KeS1Ke/reminder-dsh`.
|
|
20
|
+
|
|
19
21
|
Replace `web` with your actual profile. Restart after installation; settings changes then apply without restarting. The plugin installs as package `@kesike/reminder-dsh` and registers the profile entry `dsh-reminder`.
|
|
20
22
|
|
|
21
23
|
### Upgrading from older names
|
|
@@ -24,26 +26,32 @@ Versions before 0.2.0 shipped as package `@kesike/dsh-taskbar-reminder` with the
|
|
|
24
26
|
|
|
25
27
|
## Settings
|
|
26
28
|
|
|
27
|
-
Open **Settings → Notifications** from the settings sidebar. Windows system notifications default on.
|
|
29
|
+
Open **Settings → Notifications** from the settings sidebar. Windows system notifications default on. The taskbar completion badge has its own switch and defaults off.
|
|
30
|
+
|
|
31
|
+
The notification interval defaults to **10 seconds**, configurable from **1–300 seconds**. It controls the minimum time between consecutive completion notifications. Windows controls banner duration and delivery through its notification settings for **DeepSeek Harness**.
|
|
32
|
+
|
|
33
|
+
Clicking the notification body switches back to DSH **and opens the session that notification belongs to**, instead of whichever session is currently shown. The click is a local background activation: the plugin posts the session id to the backend, the GUI picks it up through a host long poll and calls `uiWorkspace.openSession`, then the activator launches `dsh://open` to focus the desktop window. The jump therefore requires the reminder backend to be running (same as the reply and approval buttons); when the backend has exited, clicking does not open DSH.
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
**Show task summary in notifications** defaults on: the notification shows the session title plus a task summary compressed to three lines. Turning it off shows only the session title and skips the summary model request. The session title comes from DSH's session-title service and heads every notification, approvals included; when that service has no title for the session the configured title is used.
|
|
30
36
|
|
|
31
37
|
Both modules only observe successful main-session completion. Subagents, cancelled or failed turns, restored history and duplicate idle events do not notify. Notifications are submitted in completion order.
|
|
32
38
|
|
|
33
39
|
Quick replies use a Windows activation callback and a local named pipe. Each notification accepts one reply within 24 hours while its original session and backend are running. If DSH cannot confirm delivery, check the original conversation before resending.
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
Approval notifications use the same session title as their heading (falling back to `Needs approval · <tool>`), show the requesting tool and reason in the body, and offer **Allow** and **Reject** buttons. Allow grants only the current request. The notification and the DSH approval card share the same pending decision, so the buttons expire once the request is handled or withdrawn.
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
Taskbar badges identify DSH by its window title, preferring windows in the backend's ancestor process tree. A browser must have its DSH tab selected. Set a specific title substring if automatic matching fails or multiple unrelated windows match. Foreground windows show no badge. Each completed main session increments the unread count; focusing the target window clears the badge and the count. Counts above 99 display as **99+**. The helper process clears the badge after two hours as a safety net.
|
|
44
|
+
|
|
45
|
+
Notification summaries make one auxiliary model request using the session's existing route. They summarize only its final answer and may incur model usage. Failure is labeled and falls back to an excerpt. The summary is compressed to a three-line budget (about 60 characters); some Windows 11 builds ignore `hint-maxLines`, so the text itself is truncated to that budget. No model request is made for taskbar-only mode or when the summary switch is off.
|
|
38
46
|
|
|
39
47
|
## Modules and configuration
|
|
40
48
|
|
|
41
|
-
- `src/modules/taskbar/`: native taskbar
|
|
42
|
-
- `src/modules/popup/`: summaries, submission queue and native Windows toast delivery.
|
|
43
|
-
- `src/reminder.js`: shared main-session completion detection.
|
|
49
|
+
- `src/modules/taskbar/`: native taskbar completion badge, target selection and process lifecycle.
|
|
50
|
+
- `src/modules/popup/`: summaries, submission queue, session-open mailbox and native Windows toast delivery.
|
|
51
|
+
- `src/reminder.js`: shared main-session completion detection and session-title lookup.
|
|
44
52
|
- `lib/client.js`: settings for both modules, using DSH ConfigForms.
|
|
45
53
|
|
|
46
|
-
See [example.patch.yml](example.patch.yml) for the nested `taskbar` / `popup` configuration. Existing `popup` settings carry over
|
|
54
|
+
See [example.patch.yml](example.patch.yml) for the nested `taskbar` / `popup` configuration. Existing `popup` settings carry over, and `popup.durationSeconds` now controls the submission interval. The three switches only change the values shown on the page. Click **Save** at the bottom of the page to write them (every pending switch is submitted as one atomic change), or **Cancel** to discard them. A successful save briefly confirms itself; a refused save keeps the pending values and shows the error so you can retry or cancel. The target window title and the notification interval keep their own save buttons. All writes persist through DSH's profile configuration without remounting the plugin.
|
|
47
55
|
|
|
48
56
|
## Validation
|
|
49
57
|
|
|
@@ -54,9 +62,11 @@ npm run test:settings-ui
|
|
|
54
62
|
npm pack --dry-run
|
|
55
63
|
```
|
|
56
64
|
|
|
57
|
-
On Windows, `npm run test:popup` sends a sample system notification and prints a test reply to the terminal. `npm run test:taskbar -- "specific window title"` checks
|
|
65
|
+
On Windows, `npm run test:popup` sends a sample system notification and prints a test reply to the terminal. `npm run test:taskbar -- "specific window title"` checks the taskbar badge; switch away from DSH to see it. Set `CHROMIUM_PATH` for the settings-component browser test if necessary.
|
|
66
|
+
|
|
67
|
+
Automated checks include Cordis Loader hot updates, independent module selection, queue scheduling, notification title selection, model-failure handling and subprocess control. The browser check covers the staged switch save/cancel bar, its atomic write and refused writes. Windows-only checks cover the native toast payload and Win32 helper. Notifications are delivered on the Windows backend machine.
|
|
58
68
|
|
|
59
|
-
|
|
69
|
+
Publish a release with `npm publish --access public --registry=https://registry.npmjs.org`; the explicit registry is required whenever the configured default is a mirror.
|
|
60
70
|
|
|
61
71
|
## License
|
|
62
72
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# reminder-dsh
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness 的 Windows
|
|
3
|
+
DeepSeek Harness 的 Windows 提醒插件提供**原生系统通知**和**任务栏完成角标**。在 DSH 的「设置 → 通知」中可分别开关,修改即时生效。
|
|
4
4
|
|
|
5
5
|
[English](README.md)
|
|
6
6
|
|
|
7
7
|
## 要求与安装
|
|
8
8
|
|
|
9
9
|
- Windows 10/11 本机交互式桌面会话。
|
|
10
|
-
- 系统通知使用 Windows 自带的 `powershell.exe
|
|
10
|
+
- 系统通知使用 Windows 自带的 `powershell.exe`。任务栏角标需要 PowerShell 7,`pwsh.exe` 须在 PATH 中。
|
|
11
11
|
- Node.js **24.9.0+**,DSH **`>=0.2.2-rc.1 <0.3.0`**。
|
|
12
12
|
|
|
13
13
|
原仓库骨架提到的 DSH `0.1.1-rc.2` 不支持本版采用的动态配置接口,需要升级。实现依据官方源码 [`fcc0ad4`](https://github.com/deepseek-ai/deepseek-harness/tree/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159)。
|
|
@@ -16,10 +16,12 @@ DeepSeek Harness 的 Windows 提醒插件提供**原生系统通知**和**任务
|
|
|
16
16
|
winget install --id Microsoft.PowerShell --source winget
|
|
17
17
|
pwsh --version
|
|
18
18
|
|
|
19
|
-
dsh plugin --profile web add
|
|
19
|
+
dsh plugin --profile web add @kesike/reminder-dsh
|
|
20
20
|
dsh --profile web
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
`@kesike/reminder-dsh` 已发布到 [npm](https://www.npmjs.com/package/@kesike/reminder-dsh);同一版本也可直接从 GitHub 安装:`dsh plugin --profile web add github:KeS1Ke/reminder-dsh`。
|
|
24
|
+
|
|
23
25
|
将 `web` 替换成你实际使用的 profile。首次安装后重启对应 DSH;之后调整提醒开关和间隔无需重启。如果安装 PowerShell 后仍找不到 pwsh,请重启终端和 DSH,使 PATH 更新。
|
|
24
26
|
|
|
25
27
|
包名 `@kesike/reminder-dsh`,配置条目 ID `dsh-reminder`,与 GitHub 仓库名一致。在当前 profile 安装一次即可,不要在每个 agent 里重复安装。
|
|
@@ -34,40 +36,49 @@ dsh --profile web
|
|
|
34
36
|
|
|
35
37
|
| 模块 / 字段 | 默认值 | 行为 |
|
|
36
38
|
| --- | --- | --- |
|
|
37
|
-
| Windows
|
|
39
|
+
| Windows 任务栏完成角标 | 关闭 | 主会话完成后在 DSH 任务栏图标角上显示未查看的完成数量(深色圆底白色数字,同 ChatGPT 角标),切回目标窗口后清除。 |
|
|
38
40
|
| 目标窗口标题包含 | 空 | 自动识别 DSH 窗口;可填写更具体的标题文字。 |
|
|
39
|
-
| Windows 系统通知 | 开启 |
|
|
41
|
+
| Windows 系统通知 | 开启 | 通过原生通知显示会话标题、任务总结和待处理审批。 |
|
|
42
|
+
| 在通知中显示任务总结 | 开启 | 通知正文显示压缩在 3 行内的任务总结;关闭后只显示会话标题,并不再调用模型总结。 |
|
|
40
43
|
| 通知发送间隔 | 10 秒 | 控制连续发送的最短间隔,可设为 1–300 秒整数。 |
|
|
41
44
|
|
|
42
|
-
设置使用 DSH 原生 ConfigForm 和 `.volatile()` 动态引用,保存到当前 profile
|
|
45
|
+
设置使用 DSH 原生 ConfigForm 和 `.volatile()` 动态引用,保存到当前 profile 的配置覆盖层,不重新挂载插件。三个开关只改动页面上显示的值,需点击页面底部的「保存」才写入(所有未保存开关合并为一次原子提交),「取消」丢弃这些改动;保存成功后页面底部短暂显示「已保存」,失败时保留未保存的值并显示错误,可重试或取消。目标窗口标题和通知间隔仍由各自输入框旁的按钮保存。只读配置、上层覆盖或网络错误导致保存被拒绝时,同样显示失败反馈。
|
|
46
|
+
|
|
47
|
+
两个模块独立运行。关闭任务栏角标会立即清除角标并退出后台进程,关闭系统通知会取消尚未发送的提醒和通知上的待决审批操作。重新开启后处理新的完成和审批事件。
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
## 模块一:任务栏完成角标
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
通过 `pwsh` 调用 Windows 任务栏角标接口,在 DSH 窗口任务栏图标的角上显示未查看的完成数量,不抢占焦点。角标与 ChatGPT/Codex 的任务栏角标同款:深色圆底(`#26252D`)+ 白色数字,图标填满 Windows 的任务栏角标槽位(该槽位固定为 16px,ChatGPT 用系统原生角标约 18px,已尽量接近),两位数及以上自动扩展为同高胶囊以放得下数字;位置由 Windows 按任务栏方向决定,横向任务栏在图标右下角,竖排任务栏在图标右上角。
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
每次主会话完成时未查看数加一;多个主会话完成会累计到同一个角标。切回目标窗口(目标窗口成为前台)后角标自动清除,数字同时归零。超过 99 显示 `99+`。已在前台的目标窗口不显示角标。
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
角标后台进程会轮询前台窗口,切回后退出;为防止异常残留,最长保留 2 小时。默认识别标题中的 `DeepSeek Harness`、`DSH` 或 `DSH Local Build` 产品名,优先选择属于当前 DSH 后端进程祖先的匹配窗口。如果只能通过标题找到多个窗口,不会任意选择,而是记录提示,要求设置更具体的目标窗口标题。
|
|
51
56
|
|
|
52
57
|
浏览器使用场景中,DSH 标签页需要是该浏览器窗口当前显示的标签页,窗口本身可以在后台或最小化。若浏览器已经切到别的标签,自动识别通常找不到 DSH;可填写目标窗口标题,但请保证该文字能唯一定位你要提醒的窗口。该模块不能可靠地通过窗口标题区分同名的多个独立 DSH 实例。
|
|
53
58
|
|
|
54
|
-
找不到窗口、匹配不唯一、缺少 pwsh 或系统策略阻止原生调用时,原因会记录在 DSH
|
|
59
|
+
找不到窗口、匹配不唯一、缺少 pwsh 或系统策略阻止原生调用时,原因会记录在 DSH 日志中。关闭任务栏模块会请求子进程清除角标并退出;进程异常时由超时上限兜底。
|
|
55
60
|
|
|
56
61
|
## 模块二:Windows 系统通知
|
|
57
62
|
|
|
58
|
-
主会话正常结束并进入空闲后,插件以 **
|
|
63
|
+
主会话正常结束并进入空闲后,插件以 **DeepSeek Harness** 为发送来源提交原生 Toast。顶部显示应用图标和名称,标题显示会话标题(取不到时使用配置的固定标题);开启任务总结时正文最多显示三行任务总结和快速回复框,关闭后只显示会话标题。
|
|
64
|
+
|
|
65
|
+
点击通知正文会切回 DSH,并打开该通知对应的会话(不是当前停留的会话)。实现方式是通知正文触发本机后台激活:插件先通知后端,GUI 通过长轮询取到会话 ID 并由 `uiWorkspace.openSession` 导航,再启动 `dsh://open` 切回桌面窗口。因此点击跳转要求插件后端仍在运行(与通知上的回复、审批按钮一致);后端已退出时点击不会打开 DSH。横幅外观、显示时间和通知中心收录由 Windows 管理,受系统通知与勿扰设置控制。
|
|
59
66
|
|
|
60
67
|
在通知中输入内容并点击「发送」,Windows 回调会通过本机命名管道将回复交给触发该通知的 DSH 会话。每条通知可回复一次,回复入口在发送后 24 小时内有效,并要求原会话和 DSH 后端仍在运行。遇到回复未确认的提示时,请回到原会话核对后再发送。
|
|
61
68
|
|
|
69
|
+
当 DSH 请求人工审批时,通知的标题同样是该会话的标题(取不到会话标题时显示「需要审批 · 工具名」),正文首句为「需要审批(工具名):申请原因」,并带「允许」「拒绝」两个按钮。允许仅授予当前请求一次。通知与会话内审批卡片共用 DSH 的待决请求,任一处处理或取消后,旧通知上的按钮便失效。需要查看完整操作时,点击通知正文回到 DSH。
|
|
70
|
+
|
|
71
|
+
桌面版首次发送通知时,插件在当前用户的开始菜单中注册 `DeepSeek Harness Notifications/DeepSeek Harness.lnk`,供 Windows 读取通知顶部的应用名称和图标。快捷方式也可用于打开 DeepSeek Harness。
|
|
72
|
+
|
|
62
73
|
多个主会话同时完成时,按完成事件顺序提交。发送间隔从上一条提交成功后开始计算,修改后立即重算等待时间。原会话关闭会取消该会话尚未提交的提醒。已提交的通知由 Windows 通知中心管理。
|
|
63
74
|
|
|
64
75
|
### AI 任务总结
|
|
65
76
|
|
|
66
|
-
|
|
77
|
+
取本次运行最后一轮的最终文字回复,用完成时的会话模型生成简短中文总结,要求压缩在 3 行以内(约 60 字,且不超过 `summaryMaxChars`),保留失败、未完成事项和验证限制。部分 Windows 11 版本会忽略 `hint-maxLines`,因此正文按约 60 字的 3 行预算直接截断。不会使用其他轮次、子会话、思考过程或工具参数。
|
|
67
78
|
|
|
68
|
-
每次有最终文字回复的正常完成会多一次模型调用,使用 DSH
|
|
79
|
+
每次有最终文字回复的正常完成会多一次模型调用,使用 DSH 已配置的提供商和凭据,可能产生额外用量;仅开启任务栏模块时、或关闭「在通知中显示任务总结」时不会发生这次调用。摘要是辅助请求,不追加原会话的用户消息,也不启动新 agent 轮次。
|
|
69
80
|
|
|
70
|
-
默认等待最多 15 秒、总结最多
|
|
81
|
+
默认等待最多 15 秒、总结最多 60 字符(约 3 行);超长回复只取首尾合计 12000 字符。超时或失败时明确显示“AI 总结暂不可用”并附最终回复摘录;没有最终文字回复时说明原因并使用备用文案。模型摘要不能替代对实际完成情况的检查。
|
|
71
82
|
|
|
72
83
|
## 提醒触发条件
|
|
73
84
|
|
|
@@ -88,21 +99,22 @@ dsh --profile web
|
|
|
88
99
|
popup:
|
|
89
100
|
enabled: true
|
|
90
101
|
durationSeconds: 10
|
|
102
|
+
showSummary: true
|
|
91
103
|
title: DSH 任务完成
|
|
92
104
|
message: 主会话已完成本轮任务。
|
|
93
|
-
summaryMaxChars:
|
|
105
|
+
summaryMaxChars: 60
|
|
94
106
|
summaryTimeoutSeconds: 15
|
|
95
107
|
```
|
|
96
108
|
|
|
97
|
-
`taskbar.enabled`、`taskbar.windowTitle`、`popup.enabled`、`popup.durationSeconds` 支持热更新。`popup.durationSeconds` 现在表示连续通知的发送间隔。其余为普通配置,修改时遵循插件重载。`summaryMaxChars` 为
|
|
109
|
+
`taskbar.enabled`、`taskbar.windowTitle`、`popup.enabled`、`popup.durationSeconds`、`popup.showSummary` 支持热更新。`popup.durationSeconds` 现在表示连续通知的发送间隔。其余为普通配置,修改时遵循插件重载。`summaryMaxChars` 为 20–1000(默认 60,超过 60 的部分仍按 3 行预算截断),`summaryTimeoutSeconds` 为 1–60。
|
|
98
110
|
|
|
99
111
|
## 模块目录
|
|
100
112
|
|
|
101
113
|
```text
|
|
102
114
|
src/index.js 组合两个模块、注册热更新
|
|
103
|
-
src/reminder.js
|
|
104
|
-
src/modules/taskbar/
|
|
105
|
-
src/modules/popup/ AI
|
|
115
|
+
src/reminder.js 共用主会话完成检测、读取会话标题
|
|
116
|
+
src/modules/taskbar/ 原生任务栏完成角标、窗口定位、子进程管理
|
|
117
|
+
src/modules/popup/ AI 总结、发送队列、会话跳转、Windows 原生通知
|
|
106
118
|
lib/index.js 兼容原包入口
|
|
107
119
|
lib/client.js DSH 设置界面的两个模块配置
|
|
108
120
|
```
|
|
@@ -119,12 +131,14 @@ npm run test:popup
|
|
|
119
131
|
npm run test:taskbar
|
|
120
132
|
```
|
|
121
133
|
|
|
122
|
-
`test:popup` 发送一条示例系统通知,并将测试回复打印到终端。`test:taskbar` 需要有可匹配的 DSH 窗口,可通过 `npm run test:taskbar -- "具体窗口标题"`
|
|
134
|
+
`test:popup` 发送一条示例系统通知,并将测试回复打印到终端。`test:taskbar` 需要有可匹配的 DSH 窗口,可通过 `npm run test:taskbar -- "具体窗口标题"` 指定匹配文字;运行后请切到别的窗口查看角标。
|
|
123
135
|
|
|
124
|
-
自动测试覆盖真实 Cordis Loader
|
|
136
|
+
自动测试覆盖真实 Cordis Loader 动态更新、模块独立开关、排队和间隔重算、摘要回退、通知标题取值及子进程行为。chromium 测试通过测试夹具验证「通知」注册、铃铛图标、开关的底部保存/取消与原子写入、输入框反馈和保存失败处理,可用 `CHROMIUM_PATH` 指定浏览器。Windows 专用测试验证原生通知载荷和 Win32 辅助代码。
|
|
125
137
|
|
|
126
138
|
提醒发生在 **DSH 后端所在 Windows 机器**。WSL/Linux 后端、远程 Windows 浏览器连接到别处后端、Windows 服务或无人登录会话不保证有可见提醒。
|
|
127
139
|
|
|
140
|
+
发布:`npm publish --access public --registry=https://registry.npmjs.org`。若本机默认 registry 是镜像,必须显式指定官方源。
|
|
141
|
+
|
|
128
142
|
## 卸载
|
|
129
143
|
|
|
130
144
|
```powershell
|
package/example.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -9,13 +9,17 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
zh: {
|
|
10
10
|
nav: '通知',
|
|
11
11
|
title: 'Windows 系统通知',
|
|
12
|
-
taskbar: 'Windows
|
|
13
|
-
taskbarDescription: '
|
|
12
|
+
taskbar: 'Windows 任务栏完成角标',
|
|
13
|
+
taskbarDescription: '主会话完成时在 DSH 任务栏图标的角上显示未查看的完成数量(深色圆底白色数字,同 ChatGPT 角标)。切回目标窗口后角标清除。此开关独立于 Windows 系统通知。',
|
|
14
|
+
summary: '在通知中显示任务总结',
|
|
15
|
+
summaryDescription: '开启后通知正文最多显示 3 行任务总结;关闭后通知只显示会话标题,并省去一次 AI 总结调用。',
|
|
14
16
|
windowTitle: '目标窗口标题包含(可选)', saveTitle: '保存目标窗口',
|
|
15
17
|
windowHelp: '留空自动识别 DeepSeek Harness / DSH 窗口。浏览器需显示 DSH 标签页;匹配多个窗口时,请填写更具体的标题文本。',
|
|
16
|
-
description: '
|
|
18
|
+
description: '通过 Windows 通知显示任务总结和待处理审批。可直接回复原会话或处理本次审批。',
|
|
17
19
|
interval: '通知发送间隔(秒)', intervalHelp: '默认 10 秒,可设为 1–300 秒。横幅显示时间由 Windows 管理。', save: '保存间隔',
|
|
18
|
-
hot: '
|
|
20
|
+
hot: '开关更改需点击本页底部的保存才生效;关闭 Windows 系统通知会取消尚未发送的提醒。',
|
|
21
|
+
saveChanges: '保存', cancelChanges: '取消',
|
|
22
|
+
switchUnsaved: '开关更改尚未保存,保存后立即生效。',
|
|
19
23
|
saved: '已保存,立即生效。',
|
|
20
24
|
error: '设置未保存,请检查连接或配置覆盖后重试。',
|
|
21
25
|
unavailable: '此配置暂不可编辑,请确认插件已加载且当前配置可写。',
|
|
@@ -23,24 +27,80 @@ window.__ModuleLoader__.load({
|
|
|
23
27
|
en: {
|
|
24
28
|
nav: 'Notifications',
|
|
25
29
|
title: 'Windows system notifications',
|
|
26
|
-
taskbar: 'Windows taskbar
|
|
27
|
-
taskbarDescription: '
|
|
30
|
+
taskbar: 'Windows taskbar completion badge',
|
|
31
|
+
taskbarDescription: 'Show the number of unread completed sessions on the DSH taskbar icon (dark chip with white digits, like the ChatGPT badge) when a main session finishes. Focusing the target window clears the badge. This switch is independent of Windows notifications.',
|
|
32
|
+
summary: 'Show task summary in notifications',
|
|
33
|
+
summaryDescription: 'When on, the notification body shows the task summary in up to three lines. When off, only the session title is shown and no summary model call is made.',
|
|
28
34
|
windowTitle: 'Target window title contains (optional)', saveTitle: 'Save target window',
|
|
29
35
|
windowHelp: 'Leave blank to detect DeepSeek Harness / DSH windows. A browser must show its DSH tab. If multiple windows match, enter a more specific title.',
|
|
30
|
-
description: 'Show
|
|
36
|
+
description: 'Show task summaries and pending approvals in Windows notifications. Reply to the original session or decide the current approval from the notification.',
|
|
31
37
|
interval: 'Notification interval (seconds)', intervalHelp: 'Default: 10 seconds. Range: 1–300. Windows controls how long banners stay visible.', save: 'Save interval',
|
|
32
|
-
hot: '
|
|
38
|
+
hot: 'Switch changes apply only after you save them at the bottom of this page. Turning notifications off cancels pending sends.',
|
|
39
|
+
saveChanges: 'Save', cancelChanges: 'Cancel',
|
|
40
|
+
switchUnsaved: 'Switch changes are not saved yet. Saving applies them immediately.',
|
|
33
41
|
saved: 'Saved and applied immediately.',
|
|
34
42
|
error: 'Could not save. Check the connection or configuration overrides and retry.',
|
|
35
43
|
unavailable: 'Settings are unavailable. Check that the plugin is loaded and its configuration is writable.',
|
|
36
44
|
},
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
/** 16 px outline bell matching the DSH medium icon weight (1.3 px stroke, currentColor). */
|
|
48
|
+
function BellIcon({ size = 16 }) {
|
|
49
|
+
return h('svg', {
|
|
50
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor',
|
|
51
|
+
strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false',
|
|
52
|
+
},
|
|
53
|
+
h('path', { d: 'M12 5.33A4 4 0 0 0 4 5.33c0 4.67-2 6-2 6h12s-2-1.33-2-6' }),
|
|
54
|
+
h('path', { d: 'M9.15 14a1.33 1.33 0 0 1-2.3 0' }))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolve one of the plugin's host routes under the page base (prefix-safe for the desktop shell). */
|
|
58
|
+
function hostRoute(path) {
|
|
59
|
+
try {
|
|
60
|
+
const base = typeof document !== 'undefined' && typeof document.baseURI === 'string' ? document.baseURI : ''
|
|
61
|
+
if (!base) return undefined
|
|
62
|
+
const url = new URL(base)
|
|
63
|
+
url.search = ''
|
|
64
|
+
url.hash = ''
|
|
65
|
+
if (!url.pathname.endsWith('/')) url.pathname += '/'
|
|
66
|
+
return new URL(path.replace(/^\/+/, ''), url.href).href
|
|
67
|
+
} catch { return undefined }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Follow the host's session-open mailbox so a clicked toast navigates to its own session. */
|
|
71
|
+
async function watchSessionOpen(workspace, endpoint, signal) {
|
|
72
|
+
let failures = 0
|
|
73
|
+
while (!signal.aborted) {
|
|
74
|
+
let delay = 0
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(endpoint, { signal, credentials: 'same-origin', cache: 'no-store' })
|
|
77
|
+
if (!response.ok) throw new Error(`session-open route ${response.status}`)
|
|
78
|
+
failures = 0
|
|
79
|
+
if (response.status === 200) {
|
|
80
|
+
const value = await response.json()
|
|
81
|
+
if (typeof value?.sessionId === 'string' && value.sessionId !== '') {
|
|
82
|
+
try { workspace.openSession(value.sessionId) } catch {}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
if (signal.aborted) return
|
|
87
|
+
failures += 1
|
|
88
|
+
delay = Math.min(30_000, 1000 * 2 ** Math.min(failures, 5))
|
|
89
|
+
}
|
|
90
|
+
if (delay) await new Promise(resolve => setTimeout(resolve, delay))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const buttonStyle = { padding: '6px 12px', borderRadius: 6, border: '1px solid currentColor', background: 'transparent', color: 'inherit', cursor: 'pointer' }
|
|
95
|
+
|
|
96
|
+
function NotificationSettings({ usePreferences, saveOptions, setDurationSeconds, setWindowTitle, t }) {
|
|
40
97
|
const snapshot = usePreferences(value => value)
|
|
41
98
|
const [busy, setBusy] = React.useState(false)
|
|
42
|
-
const [
|
|
99
|
+
const [switchFeedback, setSwitchFeedback] = React.useState('')
|
|
100
|
+
const [windowFeedback, setWindowFeedback] = React.useState('')
|
|
101
|
+
const [intervalFeedback, setIntervalFeedback] = React.useState('')
|
|
43
102
|
const [interval, setInterval] = React.useState(String(snapshot.value?.popup?.durationSeconds ?? 10))
|
|
103
|
+
const [drafts, setDrafts] = React.useState({})
|
|
44
104
|
const intervalId = React.useId()
|
|
45
105
|
const titleId = React.useId()
|
|
46
106
|
const [windowTitle, changeWindowTitle] = React.useState(snapshot.value?.taskbar?.windowTitle ?? '')
|
|
@@ -49,67 +109,99 @@ window.__ModuleLoader__.load({
|
|
|
49
109
|
const seconds = Number(interval)
|
|
50
110
|
const validInterval = interval.trim() !== '' && Number.isInteger(seconds) && seconds >= 1 && seconds <= 300
|
|
51
111
|
const available = snapshot.status === 'ready' && snapshot.writable
|
|
52
|
-
|
|
112
|
+
// Switches only edit drafts; the bar at the bottom writes every pending switch as one mutation.
|
|
113
|
+
const stored = {
|
|
114
|
+
taskbarEnabled: snapshot.value?.taskbar?.enabled === true,
|
|
115
|
+
popupEnabled: snapshot.value?.popup?.enabled === true,
|
|
116
|
+
showSummary: snapshot.value?.popup?.showSummary !== false,
|
|
117
|
+
}
|
|
118
|
+
const shown = name => drafts[name] ?? stored[name]
|
|
119
|
+
const changes = {}
|
|
120
|
+
for (const name of Object.keys(stored)) if (shown(name) !== stored[name]) changes[name] = shown(name)
|
|
121
|
+
const dirty = Object.keys(changes).length > 0
|
|
122
|
+
const stage = (name, value) => {
|
|
123
|
+
if (busy || !available) return
|
|
124
|
+
setSwitchFeedback('')
|
|
125
|
+
setDrafts(current => ({ ...current, [name]: value }))
|
|
126
|
+
}
|
|
127
|
+
const saveSwitches = async () => {
|
|
128
|
+
if (busy || !available || !dirty) return
|
|
129
|
+
setBusy(true)
|
|
130
|
+
setSwitchFeedback('')
|
|
131
|
+
try {
|
|
132
|
+
if (await saveOptions(changes)) { setDrafts({}); setSwitchFeedback('saved') }
|
|
133
|
+
else setSwitchFeedback('error')
|
|
134
|
+
} catch { setSwitchFeedback('error') } finally { setBusy(false) }
|
|
135
|
+
}
|
|
136
|
+
const cancelSwitches = () => {
|
|
137
|
+
if (busy) return
|
|
138
|
+
setDrafts({})
|
|
139
|
+
setSwitchFeedback('')
|
|
140
|
+
}
|
|
141
|
+
// A successful switch save is a transient confirmation; the bar itself disappears with the changes.
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
if (switchFeedback !== 'saved') return undefined
|
|
144
|
+
const timer = setTimeout(() => setSwitchFeedback(''), 5000)
|
|
145
|
+
return () => clearTimeout(timer)
|
|
146
|
+
}, [switchFeedback])
|
|
53
147
|
return h('div', { style: { padding: '12px 0' }, 'data-reminder-dsh-settings': '' },
|
|
54
148
|
h('section', { 'data-reminder-module': 'taskbar', style: { marginBottom: 24, borderBottom: '1px solid #8885', paddingBottom: 20 } },
|
|
55
149
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
|
|
56
150
|
h('h3', { style: { fontSize: 14, margin: 0 } }, t('taskbar')),
|
|
57
|
-
h(Switch, { checked:
|
|
58
|
-
onChange:
|
|
59
|
-
setBusy(true); setFeedback('')
|
|
60
|
-
try { setFeedback(await setTaskbarEnabled(value) ? 'saved' : 'error') }
|
|
61
|
-
catch { setFeedback('error') }
|
|
62
|
-
finally { setBusy(false) }
|
|
63
|
-
},
|
|
64
|
-
})),
|
|
151
|
+
h(Switch, { checked: shown('taskbarEnabled'), disabled: busy || !available, label: t('taskbar'),
|
|
152
|
+
onChange: value => stage('taskbarEnabled', value) })),
|
|
65
153
|
h('p', { style: { opacity: 0.75, fontSize: 13 } }, t('taskbarDescription')),
|
|
66
154
|
h('form', { onSubmit: async event => {
|
|
67
155
|
event.preventDefault()
|
|
68
156
|
if (busy || !available || windowTitle.length > 200) return
|
|
69
|
-
setBusy(true);
|
|
70
|
-
try {
|
|
71
|
-
catch {
|
|
157
|
+
setBusy(true); setWindowFeedback('')
|
|
158
|
+
try { setWindowFeedback(await setWindowTitle(windowTitle.trim()) ? 'saved' : 'error') }
|
|
159
|
+
catch { setWindowFeedback('error') }
|
|
72
160
|
finally { setBusy(false) }
|
|
73
161
|
} },
|
|
74
162
|
h('label', { htmlFor: titleId }, t('windowTitle')),
|
|
75
163
|
h('div', { style: { display: 'flex', gap: 12, margin: '8px 0' } },
|
|
76
164
|
h('input', { id: titleId, type: 'text', maxLength: 200, value: windowTitle, disabled: busy || !available,
|
|
77
|
-
onChange: event => changeWindowTitle(event.target.value),
|
|
165
|
+
onChange: event => { changeWindowTitle(event.target.value); setWindowFeedback('') },
|
|
78
166
|
style: { flex: 1, minWidth: 100, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
|
|
79
167
|
h('button', { type: 'submit', disabled: busy || !available, style: { padding: '6px 12px' } }, t('saveTitle'))),
|
|
80
|
-
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('windowHelp'))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
h(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('windowHelp')),
|
|
169
|
+
windowFeedback && h('p', { role: windowFeedback === 'error' ? 'alert' : 'status' }, t(windowFeedback)))),
|
|
170
|
+
h('section', { 'data-reminder-module': 'popup' },
|
|
171
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
|
|
172
|
+
h('div', { style: { fontWeight: 600 } }, t('title')),
|
|
173
|
+
h(Switch, { checked: shown('popupEnabled'), disabled: busy || !available, label: t('title'),
|
|
174
|
+
onChange: value => stage('popupEnabled', value) })),
|
|
175
|
+
h('p', { style: { opacity: 0.75, fontSize: 13, margin: '8px 0' } }, t('description')),
|
|
176
|
+
h('p', { style: { opacity: 0.75, fontSize: 12, margin: '8px 0' } }, t('hot')),
|
|
177
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, marginTop: 12 } },
|
|
178
|
+
h('div', { style: { fontWeight: 600 } }, t('summary')),
|
|
179
|
+
h(Switch, { checked: shown('showSummary'), disabled: busy || !available, label: t('summary'),
|
|
180
|
+
onChange: value => stage('showSummary', value) })),
|
|
181
|
+
h('p', { style: { opacity: 0.75, fontSize: 13, margin: '8px 0' } }, t('summaryDescription')),
|
|
182
|
+
h('form', { onSubmit: async event => {
|
|
183
|
+
event.preventDefault()
|
|
184
|
+
if (!validInterval || busy || !available) return
|
|
185
|
+
setBusy(true)
|
|
186
|
+
setIntervalFeedback('')
|
|
187
|
+
try { setIntervalFeedback(await setDurationSeconds(seconds) ? 'saved' : 'error') }
|
|
188
|
+
catch { setIntervalFeedback('error') }
|
|
189
|
+
finally { setBusy(false) }
|
|
190
|
+
} },
|
|
191
|
+
h('label', { htmlFor: intervalId }, t('interval')),
|
|
192
|
+
h('div', { style: { display: 'flex', gap: 12, alignItems: 'center', margin: '8px 0' } },
|
|
193
|
+
h('input', { id: intervalId, type: 'number', min: 1, max: 300, step: 1, value: interval,
|
|
194
|
+
disabled: busy || !available, onChange: event => { setInterval(event.target.value); setIntervalFeedback('') },
|
|
195
|
+
style: { width: 90, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
|
|
196
|
+
h('button', { type: 'submit', disabled: busy || !available || !validInterval, style: buttonStyle }, t('save'))),
|
|
197
|
+
h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('intervalHelp')),
|
|
198
|
+
intervalFeedback && h('p', { role: intervalFeedback === 'error' ? 'alert' : 'status' }, t(intervalFeedback)))),
|
|
111
199
|
!available && h('p', { role: 'status' }, t('unavailable')),
|
|
112
|
-
|
|
200
|
+
(dirty || switchFeedback) && h('section', { 'data-reminder-switch-changes': '', style: { marginTop: 24, paddingTop: 20, borderTop: '1px solid #8885', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' } },
|
|
201
|
+
dirty && h('button', { type: 'button', disabled: busy || !available, onClick: saveSwitches, style: buttonStyle }, t('saveChanges')),
|
|
202
|
+
dirty && h('button', { type: 'button', disabled: busy || !available, onClick: cancelSwitches, style: buttonStyle }, t('cancelChanges')),
|
|
203
|
+
h('p', { role: switchFeedback === 'error' ? 'alert' : 'status', style: { margin: 0, opacity: 0.75, fontSize: 12 } },
|
|
204
|
+
switchFeedback === 'error' ? t('error') : dirty ? t('switchUnsaved') : t('saved'))))
|
|
113
205
|
}
|
|
114
206
|
|
|
115
207
|
return {
|
|
@@ -119,13 +211,29 @@ window.__ModuleLoader__.load({
|
|
|
119
211
|
ctx.effect(() => ctx.locale.register('reminder-dsh', dictionaries))
|
|
120
212
|
const t = ctx.locale.bind('reminder-dsh')
|
|
121
213
|
const form = ctx.configForms.get('dsh-reminder')
|
|
214
|
+
ctx.inject(['uiWorkspace'], scope => {
|
|
215
|
+
const workspace = scope.get('uiWorkspace')
|
|
216
|
+
if (!workspace || typeof workspace.openSession !== 'function') return
|
|
217
|
+
const endpoint = hostRoute('reminder-dsh/session-open')
|
|
218
|
+
if (!endpoint) return
|
|
219
|
+
const controller = new AbortController()
|
|
220
|
+
void watchSessionOpen(workspace, endpoint, controller.signal)
|
|
221
|
+
return () => controller.abort()
|
|
222
|
+
})
|
|
122
223
|
const inject = () => ({
|
|
123
224
|
hooks: { preferences: form },
|
|
124
|
-
|
|
225
|
+
// One atomic write for every pending switch so the bar cannot leave half-applied settings.
|
|
226
|
+
saveOptions: changes => form.mutate([
|
|
227
|
+
...changes.taskbarEnabled === undefined ? [] : [{ op: 'set', path: ['taskbar', 'enabled'], value: changes.taskbarEnabled }],
|
|
228
|
+
...changes.popupEnabled === undefined ? [] : [{ op: 'set', path: ['popup', 'enabled'], value: changes.popupEnabled }],
|
|
229
|
+
...changes.showSummary === undefined ? [] : [{ op: 'set', path: ['popup', 'showSummary'], value: changes.showSummary }],
|
|
230
|
+
]),
|
|
125
231
|
setDurationSeconds: value => form.mutate([{ op: 'set', path: ['popup', 'durationSeconds'], value }]),
|
|
126
|
-
setTaskbarEnabled: value => form.mutate([{ op: 'set', path: ['taskbar', 'enabled'], value }]),
|
|
127
232
|
setWindowTitle: value => form.mutate([{ op: 'set', path: ['taskbar', 'windowTitle'], value }]),
|
|
128
233
|
})
|
|
234
|
+
ctx.slots.inject('settings.section.icon', () => ctx.slots.register({
|
|
235
|
+
name: 'settings.section.icon', id: 'dsh-reminder', order: 50, locale: 'reminder-dsh',
|
|
236
|
+
}, BellIcon))
|
|
129
237
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
130
238
|
name: 'settings.section', id: 'dsh-reminder', order: 50, label: () => t('nav'), locale: 'reminder-dsh', inject,
|
|
131
239
|
}, NotificationSettings))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kesike/reminder-dsh",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "DeepSeek Harness
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Windows native notifications and taskbar badge for DeepSeek Harness: session-titled toasts with quick reply and approvals · DSH 的 Windows 原生通知与任务栏角标",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery'
|
|
2
2
|
import { attachReminder } from './reminder.js'
|
|
3
3
|
import { Config as PopupConfig, createPopupModule } from './modules/popup/index.js'
|
|
4
|
+
import { attachApprovalNotifications } from './modules/popup/approval.js'
|
|
4
5
|
import { Config as TaskbarConfig, createTaskbarModule } from './modules/taskbar/index.js'
|
|
5
6
|
|
|
6
7
|
export const name = 'dsh-reminder'
|
|
7
8
|
export const inject = ['llm']
|
|
8
9
|
export const Config = Schema.object({
|
|
9
|
-
taskbar: TaskbarConfig.default({}).description('
|
|
10
|
+
taskbar: TaskbarConfig.default({}).description('任务栏完成角标模块'),
|
|
10
11
|
popup: PopupConfig.default({}).description('Windows 系统通知模块'),
|
|
11
12
|
})
|
|
12
13
|
|
|
@@ -29,6 +30,9 @@ export function startReminderRuntime(ctx, config, modules = {
|
|
|
29
30
|
const logger = ctx.logger(name)
|
|
30
31
|
ctx.effect(() => () => { for (const module of Object.values(modules)) module.dispose() })
|
|
31
32
|
ctx.on('loader/volatile-update', () => { for (const module of Object.values(modules)) module.update() })
|
|
33
|
+
if (modules.popup.notifyApproval) {
|
|
34
|
+
ctx.inject(['approval', 'typertGateway'], scope => attachApprovalNotifications(scope, modules.popup.notifyApproval))
|
|
35
|
+
}
|
|
32
36
|
attachReminder(ctx, options => Promise.all(Object.entries(modules).map(async ([name, module]) => {
|
|
33
37
|
try { await module.notify(options) }
|
|
34
38
|
catch (error) {
|