dsh-notify-windows 0.4.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 +21 -0
- package/README.en.md +75 -0
- package/README.md +93 -0
- package/lib/index.js +211 -0
- package/lib/notify.ps1 +35 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SeverusZh
|
|
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.en.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# dsh-notify
|
|
2
|
+
|
|
3
|
+
A DeepSeek Harness (DSH) plugin that sends Windows toast notifications whenever
|
|
4
|
+
the agent needs your attention:
|
|
5
|
+
|
|
6
|
+
1. Task finished (turn/end) — a turn completed, errored, or hit the token cap;
|
|
7
|
+
2. Approval pending (approval/asked) — a permission decision is waiting for you
|
|
8
|
+
(skipped when the session's approval policy is never, since nothing waits);
|
|
9
|
+
3. Question pending — the agent asked you a question via ask_user_question,
|
|
10
|
+
including calls nested inside run_code programs (in this deployment every
|
|
11
|
+
tool call goes through run_code, so the plugin scans run_code source for
|
|
12
|
+
tools.ask_user_question( calls).
|
|
13
|
+
|
|
14
|
+
Zero runtime dependencies: toasts are sent through Windows PowerShell 5.1's
|
|
15
|
+
WinRT toast API (lib/notify.ps1), with the AppUserModelId registered under HKCU
|
|
16
|
+
on first use (no admin required).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```powershell
|
|
21
|
+
pwsh scripts\install-profile.ps1 -Profile web # or: dsh plugin --profile web add dsh-notify-windows
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The script deploys the package into the profile's hoisted node_modules as
|
|
25
|
+
dsh-notify-<version> (the versioned directory lets a running host hot-load a
|
|
26
|
+
redeploy without a restart) and adds this entry to the profile's
|
|
27
|
+
cordis.patch.yml, which the running host hot-applies:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
- insert:
|
|
31
|
+
- id: dsh-notify
|
|
32
|
+
name: 'dsh-notify-0.4.0'
|
|
33
|
+
config:
|
|
34
|
+
enabled: true
|
|
35
|
+
reasons: [completed, error, max-tokens]
|
|
36
|
+
includeSubagents: false
|
|
37
|
+
notifyOnStart: true
|
|
38
|
+
notifyOnApproval: true
|
|
39
|
+
notifyOnAskUser: true
|
|
40
|
+
appName: 'DeepSeek Harness'
|
|
41
|
+
aumid: 'DeepSeekHarness.Notify'
|
|
42
|
+
log: true
|
|
43
|
+
debug: false
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
| Key | Default | Meaning |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| enabled | true | master switch |
|
|
51
|
+
| reasons | [completed, error, max-tokens] | turn-end reasons to notify |
|
|
52
|
+
| includeSubagents | false | also notify subagent sessions |
|
|
53
|
+
| notifyOnStart | false | toast when the plugin loads |
|
|
54
|
+
| notifyOnApproval | true | toast on approval/asked (policy-aware) |
|
|
55
|
+
| notifyOnAskUser | true | toast when the agent asks a question |
|
|
56
|
+
| appName | DeepSeek Harness | toast source display name |
|
|
57
|
+
| aumid | DeepSeekHarness.Notify | toast AppUserModelId |
|
|
58
|
+
| log | true | write %TEMP%\dsh-notify\notify.log |
|
|
59
|
+
| debug | false | log every session event (diagnostics, heavy) |
|
|
60
|
+
|
|
61
|
+
## Verify
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
node scripts\smoke-test.mjs
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Runs the plugin on a bare cordis Context with synthetic events and asserts the
|
|
68
|
+
log output (task, approval, and question toasts fire; subagent / never-policy /
|
|
69
|
+
unrelated events are filtered).
|
|
70
|
+
|
|
71
|
+
## Limitations
|
|
72
|
+
|
|
73
|
+
- Windows only (requires Windows PowerShell 5.1, shipped with Windows 10/11);
|
|
74
|
+
- toast delivery depends on the system notification / Focus Assist settings;
|
|
75
|
+
- the run_code question detection is heuristic and may miss or misreport.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# dsh-notify
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness(DSH)插件:Agent 需要你关注时,向 Windows 发送系统桌面通知。
|
|
4
|
+
|
|
5
|
+
触发时机(三类,均可在配置中开关):
|
|
6
|
+
|
|
7
|
+
1. 任务完成(turn/end):Agent 每完成一轮回复,或任务出错/输出超限时弹窗提醒;
|
|
8
|
+
2. 权限审批(approval/asked):有操作在等待你的审批时立即提醒(会话审批策略为
|
|
9
|
+
never 时自动跳过,因为那时没有东西在等你);
|
|
10
|
+
3. 提问确认(ask_user_question):Agent 向你提问、等待回答时提醒——包括通过
|
|
11
|
+
run_code 内部调用 ask_user_question 的情形(本部署下所有工具都经 run_code
|
|
12
|
+
调用,插件会扫描 run_code 程序源码里的 tools.ask_user_question 调用)。
|
|
13
|
+
|
|
14
|
+
让你无需盯着网页界面也能知道任务结束、需要审批或等待你回答。
|
|
15
|
+
|
|
16
|
+
## 功能
|
|
17
|
+
|
|
18
|
+
- 监听会话事件流:turn/end、approval/asked、tool/call(含 run_code 源码启发式检测);
|
|
19
|
+
- turn/end 支持按结束原因过滤:completed、error、max-tokens、aborted、interrupted、blocked;
|
|
20
|
+
- 审批提醒按会话生效策略折叠:策略为 never 时不提醒;
|
|
21
|
+
- 默认忽略子代理(subagent)会话,只提醒主会话,避免通知轰炸;
|
|
22
|
+
- 通知标题自动使用会话标题(session/title),正文显示原因与轮次;
|
|
23
|
+
- 零运行时依赖:通知通过 Windows PowerShell 5.1 的 WinRT Toast API 发送,并自动注册通知用的 AppUserModelId;
|
|
24
|
+
- 可选的日志文件(%TEMP%\dsh-notify\notify.log)与 debug 事件日志便于排查。
|
|
25
|
+
|
|
26
|
+
## 工作原理
|
|
27
|
+
|
|
28
|
+
turn/end、approval/asked、tool/call (session/event) -> lib/index.js -> spawn powershell.exe -> lib/notify.ps1 -> WinRT Toast
|
|
29
|
+
|
|
30
|
+
1. 插件在根上下文监听 session/event;
|
|
31
|
+
2. 匹配到关注的结束原因/审批请求/提问调用后生成标题与正文;
|
|
32
|
+
3. 启动 powershell.exe 执行 lib/notify.ps1:先按需在 HKCU\SOFTWARE\Classes\AppUserModelId 下注册 AUMID(无需管理员权限),再发送 Toast。
|
|
33
|
+
|
|
34
|
+
## 安装
|
|
35
|
+
|
|
36
|
+
把本包放进 DSH profile 可解析的位置,然后在 profile 的 cordis.patch.yml 里追加条目。
|
|
37
|
+
|
|
38
|
+
### 方式一:自动安装脚本(Windows)
|
|
39
|
+
|
|
40
|
+
pwsh scripts\install-profile.ps1 -Profile web
|
|
41
|
+
|
|
42
|
+
脚本会把插件复制到 profile 的 hoisted node_modules(目录名为 dsh-notify-版本号,
|
|
43
|
+
重新部署时目录名变化可使运行中的宿主热加载新代码而无需重启),并幂等地写入/更新
|
|
44
|
+
$HOME\.dsh\profiles\web\cordis.patch.yml(该文件被运行中的 DSH 热监视):
|
|
45
|
+
|
|
46
|
+
- insert:
|
|
47
|
+
- id: dsh-notify
|
|
48
|
+
name: 'dsh-notify-0.4.0'
|
|
49
|
+
config:
|
|
50
|
+
enabled: true
|
|
51
|
+
reasons: [completed, error, max-tokens]
|
|
52
|
+
includeSubagents: false
|
|
53
|
+
notifyOnStart: true
|
|
54
|
+
notifyOnApproval: true
|
|
55
|
+
notifyOnAskUser: true
|
|
56
|
+
appName: 'DeepSeek Harness'
|
|
57
|
+
aumid: 'DeepSeekHarness.Notify'
|
|
58
|
+
log: true
|
|
59
|
+
debug: false
|
|
60
|
+
|
|
61
|
+
### 方式二:dsh plugin 命令
|
|
62
|
+
|
|
63
|
+
dsh plugin --profile web add 本仓库路径
|
|
64
|
+
|
|
65
|
+
再手动把上面的 insert 条目加入 cordis.patch.yml(注意:修改插件代码后若想被运行中的宿主立即加载,请用安装脚本,它会更换部署目录名以绕过模块缓存)。
|
|
66
|
+
|
|
67
|
+
## 配置项
|
|
68
|
+
|
|
69
|
+
enabled true 总开关
|
|
70
|
+
reasons [completed, error, max-tokens] 需要提醒的回合结束原因
|
|
71
|
+
includeSubagents false 是否也提醒子代理会话
|
|
72
|
+
notifyOnStart false 插件加载时发一条「已激活」通知
|
|
73
|
+
notifyOnApproval true 审批请求提醒(策略 never 时自动跳过)
|
|
74
|
+
notifyOnAskUser true Agent 提问等待回答时提醒
|
|
75
|
+
appName DeepSeek Harness 通知来源显示名与兜底标题
|
|
76
|
+
aumid DeepSeekHarness.Notify 通知 AppUserModelId
|
|
77
|
+
log true 写日志到 %TEMP%\dsh-notify\notify.log
|
|
78
|
+
debug false 把所有收到的会话事件写入日志(排查用,量大会产生大量磁盘写入)
|
|
79
|
+
|
|
80
|
+
## 验证
|
|
81
|
+
|
|
82
|
+
node scripts\smoke-test.mjs
|
|
83
|
+
|
|
84
|
+
独立进程内用 cordis Context 模拟会话事件,通过后会弹出多条测试通知(任务完成、
|
|
85
|
+
审批、提问),并断言日志记录与过滤行为。
|
|
86
|
+
|
|
87
|
+
## 限制与说明
|
|
88
|
+
|
|
89
|
+
- 仅支持 Windows(依赖 Windows PowerShell 5.1,Windows 10/11 自带);
|
|
90
|
+
- 通知是否弹出受系统「专注助手 / 通知中心」设置影响;
|
|
91
|
+
- Toast 显示为 appName 名称;首次发送会自动注册 AUMID(HKCU,无需管理员);
|
|
92
|
+
- 审批提醒只在会话审批策略为 ask(默认)时有意义;策略为 never 的会话不提醒;
|
|
93
|
+
- 提问检测依赖 run_code 程序源码中出现 tools.ask_user_question( 调用,属启发式,可能漏检或误报。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// dsh-notify — DeepSeek Harness plugin: send a Windows toast notification
|
|
2
|
+
// whenever the agent needs the user's attention:
|
|
3
|
+
// - turn/end — a task completed, failed, or was cut off;
|
|
4
|
+
// - approval/asked — a permission approval is waiting (only when the
|
|
5
|
+
// session's effective approval policy is "ask");
|
|
6
|
+
// - tool/call — the agent called the ask_user_question tool and is
|
|
7
|
+
// waiting for an answer.
|
|
8
|
+
//
|
|
9
|
+
// Zero runtime dependencies: notifications go through the toast sender
|
|
10
|
+
// script (./notify.ps1) executed by Windows PowerShell 5.1, whose .NET
|
|
11
|
+
// Framework runtime supports the WinRT toast API.
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const NOTIFY_SCRIPT = fileURLToPath(new URL("./notify.ps1", import.meta.url));
|
|
19
|
+
const VERSION = "0.4.0";
|
|
20
|
+
|
|
21
|
+
const REASON_TEXT = {
|
|
22
|
+
completed: "任务已完成",
|
|
23
|
+
error: "任务出错",
|
|
24
|
+
"max-tokens": "输出达到 token 上限",
|
|
25
|
+
aborted: "任务已取消",
|
|
26
|
+
interrupted: "任务中断",
|
|
27
|
+
blocked: "任务未开始",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const name = "dsh-notify";
|
|
31
|
+
|
|
32
|
+
function truncate(text, max) {
|
|
33
|
+
if (typeof text !== "string") return "";
|
|
34
|
+
return text.length > max ? text.slice(0, max) + "…" : text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** First question text of an ask_user_question tool call, if parseable. */
|
|
38
|
+
function extractAskQuestion(argumentsJson) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(argumentsJson);
|
|
41
|
+
const first = Array.isArray(parsed?.questions) ? parsed.questions[0] : undefined;
|
|
42
|
+
return typeof first?.question === "string" ? first.question : undefined;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This deployment invokes every tool through run_code programs, so a real
|
|
50
|
+
* question never appears as a model-level ask_user_question tool call.
|
|
51
|
+
* Detect the nested call inside a run_code program's code and pull out the
|
|
52
|
+
* first question text.
|
|
53
|
+
*/
|
|
54
|
+
function extractQuestionFromCode(argumentsJson) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(argumentsJson);
|
|
57
|
+
const code = typeof parsed?.code === "string" ? parsed.code : "";
|
|
58
|
+
const callIndex = code.search(/tools\.ask_user_question\s*\(/);
|
|
59
|
+
if (callIndex < 0) return undefined;
|
|
60
|
+
const region = code.slice(callIndex, callIndex + 8000);
|
|
61
|
+
const matches = [...region.matchAll(/question\s*:\s*["']([^"']{2,200})["']/g)];
|
|
62
|
+
return matches.length > 0 ? matches[0][1] : undefined;
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function dshNotify(ctx, config = {}) {
|
|
69
|
+
const cfg = {
|
|
70
|
+
enabled: true,
|
|
71
|
+
reasons: ["completed", "error", "max-tokens"],
|
|
72
|
+
includeSubagents: false,
|
|
73
|
+
notifyOnStart: false,
|
|
74
|
+
notifyOnApproval: true,
|
|
75
|
+
notifyOnAskUser: true,
|
|
76
|
+
appName: "DeepSeek Harness",
|
|
77
|
+
aumid: "DeepSeekHarness.Notify",
|
|
78
|
+
log: true,
|
|
79
|
+
debug: false,
|
|
80
|
+
...config,
|
|
81
|
+
};
|
|
82
|
+
const reasons = new Set(cfg.reasons);
|
|
83
|
+
const logPath = join(tmpdir(), "dsh-notify", "notify.log");
|
|
84
|
+
|
|
85
|
+
const log = (entry) => {
|
|
86
|
+
if (!cfg.log) return;
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(join(tmpdir(), "dsh-notify"), { recursive: true });
|
|
89
|
+
appendFileSync(logPath, JSON.stringify({ time: Date.now(), ...entry }) + "\n");
|
|
90
|
+
} catch {
|
|
91
|
+
// diagnostics must never break the agent loop
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const sendToast = (title, body) => {
|
|
96
|
+
const child = spawn(
|
|
97
|
+
"powershell.exe",
|
|
98
|
+
[
|
|
99
|
+
"-NoProfile",
|
|
100
|
+
"-NonInteractive",
|
|
101
|
+
"-ExecutionPolicy",
|
|
102
|
+
"Bypass",
|
|
103
|
+
"-File",
|
|
104
|
+
NOTIFY_SCRIPT,
|
|
105
|
+
"-Title",
|
|
106
|
+
title,
|
|
107
|
+
"-Body",
|
|
108
|
+
body,
|
|
109
|
+
"-Aumid",
|
|
110
|
+
cfg.aumid,
|
|
111
|
+
"-AppName",
|
|
112
|
+
cfg.appName,
|
|
113
|
+
],
|
|
114
|
+
{ stdio: "ignore", windowsHide: true },
|
|
115
|
+
);
|
|
116
|
+
child.on("error", (error) => {
|
|
117
|
+
log({ event: "error", message: String(error?.message ?? error) });
|
|
118
|
+
ctx.logger?.warn?.("dsh-notify: 启动通知进程失败", error);
|
|
119
|
+
});
|
|
120
|
+
child.on("exit", (code) => {
|
|
121
|
+
if (code !== 0) {
|
|
122
|
+
log({ event: "error", code });
|
|
123
|
+
ctx.logger?.warn?.("dsh-notify: 通知进程退出码 " + code);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const titleFor = (session) => {
|
|
129
|
+
try {
|
|
130
|
+
return ctx.sessionTitle?.get?.(session)?.title;
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** True for subagent sessions when subagents are excluded. */
|
|
137
|
+
const isFilteredSession = (session) => {
|
|
138
|
+
if (cfg.includeSubagents) return false;
|
|
139
|
+
return session?.header?.origin === "subagent" || session?.header?.depth !== undefined;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Last approval/policy event of the session, or undefined (defaults to ask). */
|
|
143
|
+
const effectivePolicy = (session) => {
|
|
144
|
+
const events = session?.events ?? [];
|
|
145
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
146
|
+
const event = events[index];
|
|
147
|
+
if (event?.type === "approval/policy") return event.data?.policy;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
ctx.on("session/event", (session, event) => {
|
|
153
|
+
if (!cfg.enabled) return;
|
|
154
|
+
if (cfg.debug) {
|
|
155
|
+
log({ event: "debug", type: event.type, name: event.data?.name ?? null, sessionId: session?.id });
|
|
156
|
+
}
|
|
157
|
+
if (isFilteredSession(session)) return;
|
|
158
|
+
|
|
159
|
+
if (event.type === "turn/end") {
|
|
160
|
+
const reason = event.data?.reason;
|
|
161
|
+
if (typeof reason?.kind !== "string" || !reasons.has(reason.kind)) return;
|
|
162
|
+
const title = titleFor(session) || cfg.appName;
|
|
163
|
+
const body = REASON_TEXT[reason.kind] ?? "任务已结束";
|
|
164
|
+
sendToast(title, body + (Number.isInteger(event.data.turn) ? "(第 " + event.data.turn + " 轮)" : ""));
|
|
165
|
+
log({
|
|
166
|
+
event: "notify",
|
|
167
|
+
sessionId: session?.id,
|
|
168
|
+
reason: reason.kind,
|
|
169
|
+
turn: event.data?.turn,
|
|
170
|
+
title,
|
|
171
|
+
body,
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (event.type === "approval/asked" && cfg.notifyOnApproval) {
|
|
177
|
+
// Never notify when the session policy auto-rejects: nothing waits for
|
|
178
|
+
// the user, so a toast would be noise.
|
|
179
|
+
if (effectivePolicy(session) === "never") return;
|
|
180
|
+
const data = event.data ?? {};
|
|
181
|
+
const body = typeof data.toolName === "string"
|
|
182
|
+
? (typeof data.reason === "string" && data.reason.length > 0
|
|
183
|
+
? "工具 " + data.toolName + ":" + truncate(data.reason, 100)
|
|
184
|
+
: "工具 " + data.toolName)
|
|
185
|
+
: "有一项操作等待你的批准";
|
|
186
|
+
sendToast(cfg.appName + " · 需要审批", body);
|
|
187
|
+
log({ event: "approval", sessionId: session?.id, id: data.id, toolName: data.toolName, reason: data.reason });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.type === "tool/call" && cfg.notifyOnAskUser) {
|
|
192
|
+
const data = event.data ?? {};
|
|
193
|
+
let question;
|
|
194
|
+
if (data.name === "ask_user_question") {
|
|
195
|
+
question = extractAskQuestion(data.arguments);
|
|
196
|
+
} else if (data.name === "run_code" && typeof data.arguments === "string" && data.arguments.includes("ask_user_question")) {
|
|
197
|
+
question = extractQuestionFromCode(data.arguments);
|
|
198
|
+
}
|
|
199
|
+
if (question !== undefined || data.name === "ask_user_question") {
|
|
200
|
+
sendToast(cfg.appName + " · 需要回答", question ? truncate(question, 100) : "Agent 正在等待你的确认");
|
|
201
|
+
log({ event: "ask-user", sessionId: session?.id, question: question ?? null });
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
log({ event: "start", version: VERSION, enabled: cfg.enabled, reasons: [...reasons], sessionId: null });
|
|
208
|
+
if (cfg.enabled && cfg.notifyOnStart) {
|
|
209
|
+
sendToast(cfg.appName, "任务完成提醒已激活(v" + VERSION + ")");
|
|
210
|
+
}
|
|
211
|
+
}
|
package/lib/notify.ps1
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# dsh-notify toast sender (Windows PowerShell 5.1 required: WinRT projection).
|
|
2
|
+
# Parameters are bound by PowerShell from the process argument vector, so this
|
|
3
|
+
# file stays ASCII-only and needs no BOM.
|
|
4
|
+
param(
|
|
5
|
+
[Parameter(Mandatory = $true)][string]$Title,
|
|
6
|
+
[Parameter(Mandatory = $true)][string]$Body,
|
|
7
|
+
[string]$Aumid = "DeepSeekHarness.Notify",
|
|
8
|
+
[string]$AppName = "DeepSeek Harness"
|
|
9
|
+
)
|
|
10
|
+
$ErrorActionPreference = "Stop"
|
|
11
|
+
|
|
12
|
+
# Register the AppUserModelId once under HKCU so Win32 toasts are allowed.
|
|
13
|
+
# Best effort: if the write is denied we still try to show the toast below.
|
|
14
|
+
$key = "HKCU:\SOFTWARE\Classes\AppUserModelId\$Aumid"
|
|
15
|
+
try {
|
|
16
|
+
if (-not (Test-Path $key)) {
|
|
17
|
+
New-Item -Path $key -Force | Out-Null
|
|
18
|
+
New-ItemProperty -Path $key -Name DisplayName -Value $AppName -PropertyType String -Force | Out-Null
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
Write-Warning ("AUMID registration failed: " + $_.Exception.Message)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Project the WinRT toast API (supported natively by .NET Framework).
|
|
25
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
26
|
+
|
|
27
|
+
$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
|
28
|
+
$texts = $xml.GetElementsByTagName("text")
|
|
29
|
+
$texts.Item(0).AppendChild($xml.CreateTextNode($Title)) | Out-Null
|
|
30
|
+
$texts.Item(1).AppendChild($xml.CreateTextNode($Body)) | Out-Null
|
|
31
|
+
|
|
32
|
+
$toast = New-Object Windows.UI.Notifications.ToastNotification($xml)
|
|
33
|
+
$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($Aumid)
|
|
34
|
+
$notifier.Show($toast)
|
|
35
|
+
Write-Output "toast shown"
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-notify-windows",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DeepSeek Harness 插件:任务完成时发送 Windows 系统通知",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/SeverusZh/dsh-notify-windows.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"deepseek-harness",
|
|
23
|
+
"dsh",
|
|
24
|
+
"plugin",
|
|
25
|
+
"notify",
|
|
26
|
+
"toast",
|
|
27
|
+
"windows"
|
|
28
|
+
]
|
|
29
|
+
}
|