@kesike/reminder-dsh 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KeS1Ke
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 CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # reminder-dsh
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Windows reminders for DeepSeek Harness, with two independently configurable modules:
4
+
5
+ - **Taskbar flashing:** flash the existing DSH window's taskbar icon.
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
+
8
+ [完整中文说明](README.zh-CN.md)
9
+
10
+ ## Install
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. Taskbar flashing requires PowerShell 7 (`pwsh.exe` on PATH).
13
+
14
+ ```powershell
15
+ dsh plugin --profile web add github:KeS1Ke/reminder-dsh
16
+ dsh --profile web
17
+ ```
18
+
19
+ 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
+
21
+ ### Upgrading from older names
22
+
23
+ Versions before 0.2.0 shipped as package `@kesike/dsh-taskbar-reminder` with the profile entry `dsh-taskbar-reminder`. To migrate, remove the old package, rename that entry in your profile's `cordis.patch.yml` to `dsh-reminder` (keep its `config`), then install the current version and restart DSH. Remove any separately installed `dsh-winreminder` package to avoid duplicate reminders.
24
+
25
+ ## Settings
26
+
27
+ Open **Settings → Notifications** from the settings sidebar. Windows system notifications default on. Taskbar flashing has its own switch and defaults off.
28
+
29
+ The notification interval defaults to **10 seconds**, configurable from **1–300 seconds**. It controls the minimum time between consecutive submissions. Windows controls banner duration and delivery through its notification settings for **DSH**. Disabling notifications cancels pending sends.
30
+
31
+ 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
+
33
+ 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
+
35
+ Taskbar flashing identifies 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 do not flash. Flashing ends on focus or after about ten seconds; further completions refresh it.
36
+
37
+ 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. No model request is made for taskbar-only mode.
38
+
39
+ ## Modules and configuration
40
+
41
+ - `src/modules/taskbar/`: native taskbar flashing, target selection and process lifecycle.
42
+ - `src/modules/popup/`: summaries, submission queue and native Windows toast delivery.
43
+ - `src/reminder.js`: shared main-session completion detection.
44
+ - `lib/client.js`: settings for both modules, using DSH ConfigForms.
45
+
46
+ See [example.patch.yml](example.patch.yml) for the nested `taskbar` / `popup` configuration. Existing `popup` settings carry over. `popup.durationSeconds` now controls the submission interval. Switches, target window title and interval apply immediately and persist through DSH's profile configuration.
47
+
48
+ ## Validation
49
+
50
+ ```sh
51
+ npm ci --ignore-scripts
52
+ npm test
53
+ npm run test:settings-ui
54
+ npm pack --dry-run
55
+ ```
56
+
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 flashing. Set `CHROMIUM_PATH` for the settings-component browser test if necessary.
58
+
59
+ Automated checks include Cordis Loader hot updates, independent module selection, queue scheduling, model-failure handling and subprocess control. Windows-only checks cover the native toast payload and Win32 helper. Notifications are delivered on the Windows backend machine.
60
+
61
+ ## License
62
+
63
+ [MIT](LICENSE). The unchanged official whale icon includes its [upstream license and source](assets/README.md). This is a third-party plugin.
@@ -0,0 +1,138 @@
1
+ # reminder-dsh
2
+
3
+ DeepSeek Harness 的 Windows 提醒插件提供**原生系统通知**和**任务栏图标闪烁**。在 DSH 的「设置 → 通知」中可分别开关,修改即时生效。
4
+
5
+ [English](README.md)
6
+
7
+ ## 要求与安装
8
+
9
+ - Windows 10/11 本机交互式桌面会话。
10
+ - 系统通知使用 Windows 自带的 `powershell.exe`。任务栏闪烁需要 PowerShell 7,`pwsh.exe` 须在 PATH 中。
11
+ - Node.js **24.9.0+**,DSH **`>=0.2.2-rc.1 <0.3.0`**。
12
+
13
+ 原仓库骨架提到的 DSH `0.1.1-rc.2` 不支持本版采用的动态配置接口,需要升级。实现依据官方源码 [`fcc0ad4`](https://github.com/deepseek-ai/deepseek-harness/tree/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159)。
14
+
15
+ ```powershell
16
+ winget install --id Microsoft.PowerShell --source winget
17
+ pwsh --version
18
+
19
+ dsh plugin --profile web add github:KeS1Ke/reminder-dsh
20
+ dsh --profile web
21
+ ```
22
+
23
+ 将 `web` 替换成你实际使用的 profile。首次安装后重启对应 DSH;之后调整提醒开关和间隔无需重启。如果安装 PowerShell 后仍找不到 pwsh,请重启终端和 DSH,使 PATH 更新。
24
+
25
+ 包名 `@kesike/reminder-dsh`,配置条目 ID `dsh-reminder`,与 GitHub 仓库名一致。在当前 profile 安装一次即可,不要在每个 agent 里重复安装。
26
+
27
+ ### 从旧版本升级
28
+
29
+ 0.2.0 之前包名为 `@kesike/dsh-taskbar-reminder`、配置条目 ID 为 `dsh-taskbar-reminder`。升级时先卸载旧包,把 profile 的 `cordis.patch.yml` 中该条目改名为 `dsh-reminder`(`config` 内容保留),再安装新包并重启 DSH。如果安装过旧版独立的 `dsh-winreminder`,也要一并移除,避免重复提醒。系统通知配置位于 `popup` 下,任务栏配置位于 `taskbar` 下。
30
+
31
+ ## 在设置中选择提醒功能
32
+
33
+ 打开设置,在左侧选择 **通知**。
34
+
35
+ | 模块 / 字段 | 默认值 | 行为 |
36
+ | --- | --- | --- |
37
+ | Windows 任务栏图标闪烁 | 关闭 | 主会话完成后闪烁 DSH 窗口的任务栏图标。 |
38
+ | 目标窗口标题包含 | 空 | 自动识别 DSH 窗口;可填写更具体的标题文字。 |
39
+ | Windows 系统通知 | 开启 | 通过原生通知横幅和通知中心显示任务总结。 |
40
+ | 通知发送间隔 | 10 秒 | 控制连续发送的最短间隔,可设为 1–300 秒整数。 |
41
+
42
+ 设置使用 DSH 原生 ConfigForm 和 `.volatile()` 动态引用,保存到当前 profile 的配置覆盖层,不重新挂载插件。只读配置、上层覆盖或网络错误导致保存被拒绝时,会显示失败反馈。
43
+
44
+ 两个模块独立运行。关闭任务栏闪烁会停止闪烁,关闭系统通知会取消尚未发送的提醒。重新开启后仅处理新的任务完成事件。
45
+
46
+ ## 模块一:任务栏闪烁
47
+
48
+ 通过 `pwsh` 调用 Windows `FlashWindowEx`,只闪烁任务栏,不抢占焦点。已在前台的目标窗口不闪烁;切回目标窗口或约 10 秒后停止。同时完成的多个主会话会合并为一次闪烁,后续完成会刷新闪烁时间。
49
+
50
+ 默认识别标题中的 `DeepSeek Harness`、`DSH` 或 `DSH Local Build` 产品名,优先选择属于当前 DSH 后端进程祖先的匹配窗口。如果只能通过标题找到多个窗口,不会任意选择,而是记录提示,要求设置更具体的目标窗口标题。
51
+
52
+ 浏览器使用场景中,DSH 标签页需要是该浏览器窗口当前显示的标签页,窗口本身可以在后台或最小化。若浏览器已经切到别的标签,自动识别通常找不到 DSH;可填写目标窗口标题,但请保证该文字能唯一定位你要提醒的窗口。该模块不能可靠地通过窗口标题区分同名的多个独立 DSH 实例。
53
+
54
+ 找不到窗口、匹配不唯一、缺少 pwsh 或系统策略阻止原生调用时,原因会记录在 DSH 日志中。关闭任务栏模块会请求子进程停止闪烁并退出;进程异常时有超时和有限闪烁次数作为上限。
55
+
56
+ ## 模块二:Windows 系统通知
57
+
58
+ 主会话正常结束并进入空闲后,插件以 **DSH** 为发送来源提交原生 Toast。顶部显示应用图标和名称,正文显示单行标题、最多四行任务总结和快速回复框。横幅外观、显示时间和通知中心收录由 Windows 管理,受系统通知与勿扰设置控制。
59
+
60
+ 在通知中输入内容并点击「发送」,Windows 回调会通过本机命名管道将回复交给触发该通知的 DSH 会话。每条通知可回复一次,回复入口在发送后 24 小时内有效,并要求原会话和 DSH 后端仍在运行。遇到回复未确认的提示时,请回到原会话核对后再发送。
61
+
62
+ 多个主会话同时完成时,按完成事件顺序提交。发送间隔从上一条提交成功后开始计算,修改后立即重算等待时间。原会话关闭会取消该会话尚未提交的提醒。已提交的通知由 Windows 通知中心管理。
63
+
64
+ ### AI 任务总结
65
+
66
+ 取本次运行最后一轮的最终文字回复,用完成时的会话模型生成简短中文总结,要求保留失败、未完成事项和验证限制。不会使用其他轮次、子会话、思考过程或工具参数。
67
+
68
+ 每次有最终文字回复的正常完成会多一次模型调用,使用 DSH 已配置的提供商和凭据,可能产生额外用量;仅开启任务栏模块时不会发生这次调用。摘要是辅助请求,不追加原会话的用户消息,也不启动新 agent 轮次。
69
+
70
+ 默认等待最多 15 秒、总结最多 180 字符;超长回复只取首尾合计 12000 字符。超时或失败时明确显示“AI 总结暂不可用”并附最终回复摘录;没有最终文字回复时说明原因并使用备用文案。模型摘要不能替代对实际完成情况的检查。
71
+
72
+ ## 提醒触发条件
73
+
74
+ 两个模块共用主会话完成检测:观察到 `running`,最后一个 `turn/end` 为 `completed`,再进入 `idle`。子代理(`origin: subagent` 或 `delegationDepth > 0`)、取消、报错、阻止执行、输出长度达到上限、历史恢复和重复空闲事件不提醒。普通会话分叉仍算主会话。
75
+
76
+ 这里只表示本轮正常结束并空闲,不保证任务业务目标全部完成。后台工作随后唤醒主会话并再次正常完成时,会产生新的完成事件。
77
+
78
+ ## 文件配置
79
+
80
+ 在当前 profile 的 `cordis.patch.yml` 添加覆盖项:
81
+
82
+ ```yaml
83
+ - id: dsh-reminder
84
+ config:
85
+ taskbar:
86
+ enabled: false
87
+ windowTitle: ''
88
+ popup:
89
+ enabled: true
90
+ durationSeconds: 10
91
+ title: DSH 任务完成
92
+ message: 主会话已完成本轮任务。
93
+ summaryMaxChars: 180
94
+ summaryTimeoutSeconds: 15
95
+ ```
96
+
97
+ `taskbar.enabled`、`taskbar.windowTitle`、`popup.enabled`、`popup.durationSeconds` 支持热更新。`popup.durationSeconds` 现在表示连续通知的发送间隔。其余为普通配置,修改时遵循插件重载。`summaryMaxChars` 为 60–1000,`summaryTimeoutSeconds` 为 1–60。
98
+
99
+ ## 模块目录
100
+
101
+ ```text
102
+ src/index.js 组合两个模块、注册热更新
103
+ src/reminder.js 共用主会话完成检测
104
+ src/modules/taskbar/ 原生任务栏闪烁、窗口定位、子进程管理
105
+ src/modules/popup/ AI 总结、发送队列、Windows 原生通知
106
+ lib/index.js 兼容原包入口
107
+ lib/client.js DSH 设置界面的两个模块配置
108
+ ```
109
+
110
+ ## 验证与开发
111
+
112
+ ```powershell
113
+ git clone https://github.com/KeS1Ke/reminder-dsh.git
114
+ cd reminder-dsh
115
+ npm ci --ignore-scripts
116
+ npm test
117
+ npm run test:settings-ui
118
+ npm run test:popup
119
+ npm run test:taskbar
120
+ ```
121
+
122
+ `test:popup` 发送一条示例系统通知,并将测试回复打印到终端。`test:taskbar` 需要有可匹配的 DSH 窗口,可通过 `npm run test:taskbar -- "具体窗口标题"` 指定匹配文字。
123
+
124
+ 自动测试覆盖真实 Cordis Loader 动态更新、模块独立开关、排队和间隔重算、摘要回退及子进程行为。Chromium 测试通过测试夹具验证「通知」注册和设置保存,可用 `CHROMIUM_PATH` 指定浏览器。Windows 专用测试验证原生通知载荷和 Win32 辅助代码。
125
+
126
+ 提醒发生在 **DSH 后端所在 Windows 机器**。WSL/Linux 后端、远程 Windows 浏览器连接到别处后端、Windows 服务或无人登录会话不保证有可见提醒。
127
+
128
+ ## 卸载
129
+
130
+ ```powershell
131
+ dsh plugin --profile web remove @kesike/reminder-dsh
132
+ ```
133
+
134
+ 删除手工添加的 `id: dsh-reminder` 覆盖项并重启 DSH。
135
+
136
+ ## 许可证
137
+
138
+ 插件代码采用 [MIT](LICENSE)。官方图标来源和 DeepSeek 许可见 [assets/README.md](assets/README.md)。这是第三方插件,不是 DeepSeek 官方产品。
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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.
@@ -0,0 +1,9 @@
1
+ # DeepSeek Harness icon
2
+
3
+ `deepseek-harness.png` is an unchanged copy of the official Windows desktop icon:
4
+
5
+ https://github.com/deepseek-ai/deepseek-harness/blob/fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159/apps/desktop/resources/icon-windows.png
6
+
7
+ Copyright (c) 2026 DeepSeek. The upstream MIT license is included in `DEEPSEEK-LICENSE.txt`.
8
+
9
+ This third-party reminder plugin is not an official DeepSeek product.
Binary file
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-reminder
3
+ name: '@kesike/reminder-dsh'
@@ -0,0 +1,8 @@
1
+ - id: dsh-reminder
2
+ config:
3
+ taskbar:
4
+ enabled: false
5
+ windowTitle: ''
6
+ popup:
7
+ enabled: true
8
+ durationSeconds: 10
package/lib/client.js ADDED
@@ -0,0 +1,138 @@
1
+ /** Browser entry using DSH's published module registration protocol; no build step required. */
2
+ window.__ModuleLoader__.load({
3
+ id: '@kesike/reminder-dsh',
4
+ factory(require) {
5
+ const React = require('react')
6
+ const { Switch } = require('@deepseek-ai/dsh-client-ui-primitives')
7
+ const h = React.createElement
8
+ const dictionaries = {
9
+ zh: {
10
+ nav: '通知',
11
+ title: 'Windows 系统通知',
12
+ taskbar: 'Windows 任务栏图标闪烁',
13
+ taskbarDescription: '主会话完成时闪烁 DSH 窗口的任务栏图标。切回目标窗口后停止闪烁。此开关独立于 Windows 系统通知。',
14
+ windowTitle: '目标窗口标题包含(可选)', saveTitle: '保存目标窗口',
15
+ windowHelp: '留空自动识别 DeepSeek Harness / DSH 窗口。浏览器需显示 DSH 标签页;匹配多个窗口时,请填写更具体的标题文本。',
16
+ description: '主会话完成后通过 Windows 通知显示任务总结。可在通知中直接回复原会话。',
17
+ interval: '通知发送间隔(秒)', intervalHelp: '默认 10 秒,可设为 1–300 秒。横幅显示时间由 Windows 管理。', save: '保存间隔',
18
+ hot: '修改立即生效并自动保存。关闭后取消尚未发送的通知。',
19
+ saved: '已保存,立即生效。',
20
+ error: '设置未保存,请检查连接或配置覆盖后重试。',
21
+ unavailable: '此配置暂不可编辑,请确认插件已加载且当前配置可写。',
22
+ },
23
+ en: {
24
+ nav: 'Notifications',
25
+ title: 'Windows system notifications',
26
+ taskbar: 'Windows taskbar flashing',
27
+ taskbarDescription: 'Flash the DSH window taskbar icon when a main session finishes. Focusing the target window stops flashing. This switch is independent of Windows notifications.',
28
+ windowTitle: 'Target window title contains (optional)', saveTitle: 'Save target window',
29
+ 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 a task summary in Windows notifications when a main session finishes. Reply to the original session from the notification.',
31
+ interval: 'Notification interval (seconds)', intervalHelp: 'Default: 10 seconds. Range: 1–300. Windows controls how long banners stay visible.', save: 'Save interval',
32
+ hot: 'Changes save and apply immediately. Turning off notifications cancels pending sends.',
33
+ saved: 'Saved and applied immediately.',
34
+ error: 'Could not save. Check the connection or configuration overrides and retry.',
35
+ unavailable: 'Settings are unavailable. Check that the plugin is loaded and its configuration is writable.',
36
+ },
37
+ }
38
+
39
+ function NotificationSettings({ usePreferences, setEnabled, setDurationSeconds, setTaskbarEnabled, setWindowTitle, t }) {
40
+ const snapshot = usePreferences(value => value)
41
+ const [busy, setBusy] = React.useState(false)
42
+ const [feedback, setFeedback] = React.useState('')
43
+ const [interval, setInterval] = React.useState(String(snapshot.value?.popup?.durationSeconds ?? 10))
44
+ const intervalId = React.useId()
45
+ const titleId = React.useId()
46
+ const [windowTitle, changeWindowTitle] = React.useState(snapshot.value?.taskbar?.windowTitle ?? '')
47
+ React.useEffect(() => { changeWindowTitle(snapshot.value?.taskbar?.windowTitle ?? '') }, [snapshot.value?.taskbar?.windowTitle])
48
+ React.useEffect(() => { setInterval(String(snapshot.value?.popup?.durationSeconds ?? 10)) }, [snapshot.value?.popup?.durationSeconds])
49
+ const seconds = Number(interval)
50
+ const validInterval = interval.trim() !== '' && Number.isInteger(seconds) && seconds >= 1 && seconds <= 300
51
+ const available = snapshot.status === 'ready' && snapshot.writable
52
+ const enabled = snapshot.value?.popup?.enabled === true
53
+ return h('div', { style: { padding: '12px 0' }, 'data-reminder-dsh-settings': '' },
54
+ h('section', { 'data-reminder-module': 'taskbar', style: { marginBottom: 24, borderBottom: '1px solid #8885', paddingBottom: 20 } },
55
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
56
+ h('h3', { style: { fontSize: 14, margin: 0 } }, t('taskbar')),
57
+ h(Switch, { checked: snapshot.value?.taskbar?.enabled === true, disabled: busy || !available, label: t('taskbar'),
58
+ onChange: async value => {
59
+ setBusy(true); setFeedback('')
60
+ try { setFeedback(await setTaskbarEnabled(value) ? 'saved' : 'error') }
61
+ catch { setFeedback('error') }
62
+ finally { setBusy(false) }
63
+ },
64
+ })),
65
+ h('p', { style: { opacity: 0.75, fontSize: 13 } }, t('taskbarDescription')),
66
+ h('form', { onSubmit: async event => {
67
+ event.preventDefault()
68
+ if (busy || !available || windowTitle.length > 200) return
69
+ setBusy(true); setFeedback('')
70
+ try { setFeedback(await setWindowTitle(windowTitle.trim()) ? 'saved' : 'error') }
71
+ catch { setFeedback('error') }
72
+ finally { setBusy(false) }
73
+ } },
74
+ h('label', { htmlFor: titleId }, t('windowTitle')),
75
+ h('div', { style: { display: 'flex', gap: 12, margin: '8px 0' } },
76
+ h('input', { id: titleId, type: 'text', maxLength: 200, value: windowTitle, disabled: busy || !available,
77
+ onChange: event => changeWindowTitle(event.target.value),
78
+ style: { flex: 1, minWidth: 100, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
79
+ 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
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24 } },
82
+ h('div', { style: { fontWeight: 600 } }, t('title')),
83
+ h(Switch, { checked: enabled, disabled: busy || !available, label: t('title'),
84
+ onChange: async value => {
85
+ setBusy(true)
86
+ setFeedback('')
87
+ try { setFeedback(await setEnabled(value) ? 'saved' : 'error') }
88
+ catch { setFeedback('error') }
89
+ finally { setBusy(false) }
90
+ },
91
+ })),
92
+ h('p', { style: { opacity: 0.75, fontSize: 13, margin: '8px 0' } }, t('description')),
93
+ h('p', { style: { opacity: 0.75, fontSize: 12, margin: '8px 0' } }, t('hot')),
94
+ h('form', { onSubmit: async event => {
95
+ event.preventDefault()
96
+ if (!validInterval || busy || !available) return
97
+ setBusy(true)
98
+ setFeedback('')
99
+ try { setFeedback(await setDurationSeconds(seconds) ? 'saved' : 'error') }
100
+ catch { setFeedback('error') }
101
+ finally { setBusy(false) }
102
+ } },
103
+ h('label', { htmlFor: intervalId }, t('interval')),
104
+ h('div', { style: { display: 'flex', gap: 12, alignItems: 'center', margin: '8px 0' } },
105
+ h('input', { id: intervalId, type: 'number', min: 1, max: 300, step: 1, value: interval,
106
+ disabled: busy || !available, onChange: event => { setInterval(event.target.value); setFeedback('') },
107
+ style: { width: 90, padding: '6px 8px', border: '1px solid currentColor', borderRadius: 6, background: 'transparent', color: 'inherit' } }),
108
+ h('button', { type: 'submit', disabled: busy || !available || !validInterval,
109
+ style: { padding: '6px 12px', borderRadius: 6, border: '1px solid currentColor', background: 'transparent', color: 'inherit', cursor: 'pointer' } }, t('save'))),
110
+ h('p', { style: { opacity: 0.75, fontSize: 12 } }, t('intervalHelp'))),
111
+ !available && h('p', { role: 'status' }, t('unavailable')),
112
+ feedback && h('p', { role: feedback === 'error' ? 'alert' : 'status' }, t(feedback)))
113
+ }
114
+
115
+ return {
116
+ name: 'reminder-dsh-settings',
117
+ inject: ['slots', 'locale', 'configForms'],
118
+ apply(ctx) {
119
+ ctx.effect(() => ctx.locale.register('reminder-dsh', dictionaries))
120
+ const t = ctx.locale.bind('reminder-dsh')
121
+ const form = ctx.configForms.get('dsh-reminder')
122
+ const inject = () => ({
123
+ hooks: { preferences: form },
124
+ setEnabled: value => form.mutate([{ op: 'set', path: ['popup', 'enabled'], value }]),
125
+ setDurationSeconds: value => form.mutate([{ op: 'set', path: ['popup', 'durationSeconds'], value }]),
126
+ setTaskbarEnabled: value => form.mutate([{ op: 'set', path: ['taskbar', 'enabled'], value }]),
127
+ setWindowTitle: value => form.mutate([{ op: 'set', path: ['taskbar', 'windowTitle'], value }]),
128
+ })
129
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
130
+ name: 'settings.section', id: 'dsh-reminder', order: 50, label: () => t('nav'), locale: 'reminder-dsh', inject,
131
+ }, NotificationSettings))
132
+ ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
133
+ name: 'plugins.bundle.config', key: '@kesike/reminder-dsh', locale: 'reminder-dsh', inject,
134
+ }, NotificationSettings))
135
+ },
136
+ }
137
+ },
138
+ })
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from '../src/index.js'
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@kesike/reminder-dsh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "DeepSeek Harness Windows notifications with quick reply and taskbar flashing",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json",
11
+ "./cordis.patch.yml": "./cordis.patch.yml"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "assets",
16
+ "scripts",
17
+ "cordis.patch.yml",
18
+ "README.md",
19
+ "LICENSE",
20
+ "lib",
21
+ "README.zh-CN.md",
22
+ "example.patch.yml"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test test/*.test.js",
26
+ "test:popup": "node scripts/test-popup.js",
27
+ "test:settings-ui": "node test/settings.browser.js",
28
+ "test:taskbar": "node scripts/test-taskbar.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=24.9.0",
32
+ "dsh": ">=0.2.2-rc.1 <0.3.0"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "3.19.0"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "client": {
42
+ "platform": "web",
43
+ "inject": [
44
+ "@deepseek-ai/dsh-client-ui-settings",
45
+ "@deepseek-ai/dsh-client-ui-settings-general",
46
+ "@deepseek-ai/dsh-client-locale"
47
+ ]
48
+ }
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/KeS1Ke/reminder-dsh.git"
53
+ },
54
+ "keywords": [
55
+ "dsh-plugin",
56
+ "deepseek-harness",
57
+ "windows",
58
+ "notification"
59
+ ],
60
+ "license": "MIT",
61
+ "devDependencies": {
62
+ "@deepseek-ai/cordis": "4.1.0",
63
+ "@deepseek-ai/cordis-plugin-loader": "1.1.0",
64
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
65
+ "playwright-core": "1.65.0",
66
+ "react": "18.3.1",
67
+ "react-dom": "18.3.1"
68
+ },
69
+ "peerDependencies": {
70
+ "@deepseek-ai/dsh-llm": ">=0.2.2-rc.1 <0.3.0"
71
+ },
72
+ "displayName": "DSH reminders",
73
+ "homepage": "https://github.com/KeS1Ke/reminder-dsh#readme",
74
+ "bugs": {
75
+ "url": "https://github.com/KeS1Ke/reminder-dsh/issues"
76
+ }
77
+ }
@@ -0,0 +1,19 @@
1
+ import { createWindowsToast } from '../src/modules/popup/toast.js'
2
+
3
+ if (process.platform !== 'win32') {
4
+ console.error('请在 Windows 桌面会话中运行 npm run test:popup。')
5
+ process.exitCode = 1
6
+ } else {
7
+ const notifier = createWindowsToast({
8
+ title: 'DSH 通知测试', message: '任务总结(演示):Windows 系统通知已就绪。',
9
+ })
10
+ let done, timeout
11
+ const reply = new Promise(resolve => { done = resolve })
12
+ try {
13
+ await notifier.notify({ onReply: text => { console.log('收到测试回复', text); done() } })
14
+ await Promise.race([reply, new Promise(resolve => { timeout = setTimeout(resolve, 60_000) })])
15
+ } finally {
16
+ clearTimeout(timeout)
17
+ notifier.dispose()
18
+ }
19
+ }
@@ -0,0 +1,11 @@
1
+ import { createTaskbarNotifier } from '../src/modules/taskbar/windows.js'
2
+
3
+ if (process.platform !== 'win32') {
4
+ console.error('请在 Windows 桌面会话中运行任务栏测试。')
5
+ process.exitCode = 1
6
+ } else {
7
+ const notifier = createTaskbarNotifier({ isEnabled: () => true, getWindowTitle: () => process.argv[2] ?? '' })
8
+ try { await notifier.notify() }
9
+ catch (error) { console.error(error.message); process.exitCode = 1 }
10
+ finally { notifier.dispose() }
11
+ }
package/src/index.js ADDED
@@ -0,0 +1,38 @@
1
+ import Schema from '@deepseek-ai/schemastery'
2
+ import { attachReminder } from './reminder.js'
3
+ import { Config as PopupConfig, createPopupModule } from './modules/popup/index.js'
4
+ import { Config as TaskbarConfig, createTaskbarModule } from './modules/taskbar/index.js'
5
+
6
+ export const name = 'dsh-reminder'
7
+ export const inject = ['llm']
8
+ export const Config = Schema.object({
9
+ taskbar: TaskbarConfig.default({}).description('任务栏闪烁模块'),
10
+ popup: PopupConfig.default({}).description('Windows 系统通知模块'),
11
+ })
12
+
13
+ /** Mount both independently controlled modules behind one main-session completion observer. */
14
+ export function apply(ctx, config = Config({})) {
15
+ ctx.inject(['settings'], child => {
16
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber))
17
+ })
18
+ if (process.platform !== 'win32') {
19
+ ctx.logger(name).warn('Windows 提醒仅在 Windows 本机后端生效;设置入口仍可使用。')
20
+ return
21
+ }
22
+ startReminderRuntime(ctx, config)
23
+ }
24
+
25
+ /** Share only completion detection; each module keeps its own configuration and lifecycle. */
26
+ export function startReminderRuntime(ctx, config, modules = {
27
+ taskbar: createTaskbarModule(config.taskbar), popup: createPopupModule(ctx, config.popup),
28
+ }) {
29
+ const logger = ctx.logger(name)
30
+ ctx.effect(() => () => { for (const module of Object.values(modules)) module.dispose() })
31
+ ctx.on('loader/volatile-update', () => { for (const module of Object.values(modules)) module.update() })
32
+ attachReminder(ctx, options => Promise.all(Object.entries(modules).map(async ([name, module]) => {
33
+ try { await module.notify(options) }
34
+ catch (error) {
35
+ if (!options.signal.aborted) logger.warn(`${name} 提醒失败:${error instanceof Error ? error.message : String(error)}`)
36
+ }
37
+ })), error => logger.warn(`会话提醒失败:${error instanceof Error ? error.message : String(error)}`))
38
+ }