dsh-notify-ding 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 +15 -9
- package/cordis.patch.yml +1 -1
- package/docs/design.md +7 -15
- package/lib/client.js +22 -28
- package/lib/index.d.ts +10 -12
- package/lib/index.js +7 -105
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh·通知叮咚插件
|
|
2
2
|
|
|
3
|
-
在 DSH
|
|
3
|
+
在 DSH 需要你人工回答或一轮对话跑完时,弹出系统通知。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
纯浏览器端的插件,不替换任何原生界面。
|
|
6
6
|
|
|
7
7
|
## 引言
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
- 某个会话出现新的待人工回答交互时叮咚一次,question、plan-review、approval 都算。
|
|
15
15
|
- 某个会话从执行中落到空闲、也就是一轮对话跑完时叮咚一次。
|
|
16
|
-
-
|
|
16
|
+
- 两种触发都会弹一条浏览器系统通知,提示音由 Windows 通知系统自带。
|
|
17
17
|
- 不判断页面是否聚焦,也不区分是否为当前会话,任何会话有事都会响。
|
|
18
18
|
- 通知不设自动关闭时间,会一直留在系统通知里,直到对应会话发生操作:会话被选中、该会话开始新一轮、待回答被解决,或会话从列表移除。
|
|
19
19
|
- 每个会话最多保留一条通知,新通知会顶掉同会话的旧通知;点击通知会聚焦窗口并切到该会话。
|
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
|
|
26
26
|
浏览器系统通知需要页面授权。插件在加载时会尝试请求一次,若被浏览器拒绝,会在你下一次点击或按键时再请求一次。所以在第一次看到页面后点一下任意位置,授权弹窗就会出来。
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### 提示音从哪来
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
插件不自行发声,提示音由 Windows 的通知系统在弹通知时自带。提示音的音量与静音都跟着系统通知设置走,和插件本身无关。
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### 全屏时通知会被系统静默
|
|
33
|
+
|
|
34
|
+
Windows 11 的「专注」有一条自动规则「当我在全屏使用应用时」,全屏看视频或玩游戏时会自动进入专注状态,通知横幅不显示、通知音被静音,通知只落进通知中心,看起来就像插件根本没响。这是系统行为,插件感知不到也绕不开。
|
|
35
|
+
|
|
36
|
+
想在全屏时也看到横幅,去 设置 → 系统 → 通知 → 专注,关闭「自动开启专注会话」里的「当我在全屏使用应用时」。代价是全屏时其他应用的通知也会冒出来。
|
|
37
|
+
|
|
38
|
+
此时插件没有任何声音提示,因为它不自己发声,提示音就是被一并静音的通知音。
|
|
33
39
|
|
|
34
40
|
## 安装
|
|
35
41
|
|
|
@@ -59,13 +65,13 @@ dsh plugin --profile web remove dsh-notify-ding
|
|
|
59
65
|
|
|
60
66
|
浏览器半身订阅 `ctx.sessions.list` 与 `ctx.uiSession.pendingInteractions` 两个只读快照,用边沿检测判断该不该响:运行态从真落到假算一轮跑完,出现新 key 的待回答交互算需要人工介入。首帧只建基线不发声,同一待回答请求只响一次,两次响铃之间留至少 800 毫秒并把该时刻写入 localStorage 跨标签页共享。
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
判定通过时它弹出一条浏览器系统通知。通知按会话记录且不设自动关闭,同一会话只留最新一条;会话被选中、开始新一轮、待回答被解决或从列表移除时会关掉对应通知,点击通知则聚焦窗口、切换过去并关掉该条。完整设计见 [设计说明](docs/design.md)。
|
|
63
69
|
|
|
64
70
|
## 要求与开发
|
|
65
71
|
|
|
66
72
|
- 是标准形态的 dsh client 插件,声明 `dsh.client`,导出 `./client`。
|
|
67
73
|
- 同时声明了 `dsh.bundle`,因此也是一个自挂载的 bundle 层插件:用 `dsh plugin --profile <name> add` 从 GitHub 安装后会被自动识别为 profile layer 并挂载,无需手工写组合 entry。
|
|
68
|
-
-
|
|
74
|
+
- 浏览器半身依赖 `uiSession` 与 `sessions`;宿主入口没有运行时行为,只为让包能被 Loader 加载。
|
|
69
75
|
- 构建型插件:`src/` 是 TypeScript 源码,`lib/` 是构建产物且不入库,安装或发布前由 `prepare` 构建。
|
|
70
76
|
|
|
71
77
|
## 开发
|
|
@@ -76,7 +82,7 @@ pnpm run typecheck # tsc --noEmit 严格类型检查
|
|
|
76
82
|
pnpm run build # tsdown,产出 lib/index.js 与 lib/client.js
|
|
77
83
|
```
|
|
78
84
|
|
|
79
|
-
- `src/index.ts
|
|
85
|
+
- `src/index.ts`:宿主入口,无运行时行为,只用于让包被 Loader 加载。
|
|
80
86
|
- `src/client/index.ts`:浏览器半身,边沿检测与通知。
|
|
81
87
|
- 构建用 tsdown,client 产物是 `window.__ModuleLoader__.load({ id, factory })` 的注册式模块,`@deepseek-ai/cordis` 保持外部依赖。
|
|
82
88
|
|
package/cordis.patch.yml
CHANGED
package/docs/design.md
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
# 设计说明
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 为什么只剩浏览器半身
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
插件原先让宿主进程用 PowerShell 播放 `C:\Windows\Media` 下的 wav。实测下来,实际听到的提示音是系统通知自带的音效,宿主那一侧没有可感知的效果,因此整条宿主播音链路已删除,插件只保留浏览器半身。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **浏览器半身** 只负责判断:盯住会话状态,判断什么时刻该响,然后同时弹系统通知并请求宿主播音。
|
|
9
|
-
|
|
10
|
-
宿主半身不参与任何触发判断,浏览器半身不碰任何文件路径,职责边界清楚。
|
|
7
|
+
宿主入口文件仍然存在,但没有任何运行时行为。客户端模块系统按 Loader entry 扫描 `dsh.client` 声明,entry 指向的宿主模块必须存在,浏览器半身才会被加载,因此 `src/index.ts` 只导出插件名和一个空的 `apply`。
|
|
11
8
|
|
|
12
9
|
## 触发信号
|
|
13
10
|
|
|
@@ -45,18 +42,13 @@
|
|
|
45
42
|
|
|
46
43
|
只识别这四种状态信号,因此通知只有在 DSH 里真的对那个会话动了手才会消失;单纯回到浏览器窗口不会关掉它。
|
|
47
44
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
两者互相独立:浏览器通知弹不出来时声音照响,宿主播音失败时通知照弹。任一侧失败只记一条 `console.warn`,不抛错、不互相阻塞。
|
|
51
|
-
|
|
52
|
-
## 认证
|
|
45
|
+
## 全屏静默
|
|
53
46
|
|
|
54
|
-
|
|
47
|
+
Windows 11 的「专注」有一条自动规则「当我在全屏使用应用时」,全屏看视频或玩游戏时会自动进入专注状态,通知横幅不显示、通知音被静音,通知只落进通知中心。插件不自己发声,提示音就是系统通知自带的音效,所以这种场合没有任何声音提示。
|
|
55
48
|
|
|
56
49
|
## 已知边界
|
|
57
50
|
|
|
58
|
-
-
|
|
59
|
-
- 播音走 PowerShell 进程,单次开销约是一百毫秒级,慢但不阻塞响应。
|
|
51
|
+
- 浏览器半身依赖 `uiSession` 与 `sessions`,缺任一则不激活。
|
|
60
52
|
- 「一轮跑完」只覆盖正常结束与被打断这两种空闲落点,插件不区分具体结束原因。
|
|
61
|
-
-
|
|
53
|
+
- 系统通知被静默时横幅和声音一起消失,插件察觉不到,也没有补偿手段。
|
|
62
54
|
- 页面被关掉后,浏览器通知可能仍留在系统通知中心,此时插件已不在运行,无法再收回,只能手动清掉。
|
package/lib/client.js
CHANGED
|
@@ -8,12 +8,30 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
[Symbol.toStringTag]: { value: "Module" }
|
|
9
9
|
});
|
|
10
10
|
//#region src/client/index.ts
|
|
11
|
+
/**
|
|
12
|
+
* dsh-notify-ding 的浏览器半身。
|
|
13
|
+
*
|
|
14
|
+
* 只做两件事:判断「该响了」,然后弹一条浏览器系统通知。
|
|
15
|
+
* 提示音由系统通知自带,插件不另外发声,全屏专注时通知音会被系统一并静音。
|
|
16
|
+
*
|
|
17
|
+
* 触发条件有两个:
|
|
18
|
+
* 1. 某个会话出现了新的待人工回答交互,question / plan-review / approval 都算;
|
|
19
|
+
* 2. 某个会话从「执行中」落到「空闲」,即一轮对话跑完。
|
|
20
|
+
*
|
|
21
|
+
* 按用户选择,两种都响,且不判断页面是否聚焦、不区分是否为当前会话。
|
|
22
|
+
* 重复靠两道闸门挡:同一待回答请求的 key 只响一次,以及跨标签页共享
|
|
23
|
+
* 的最小间隔,避免多开标签页时同一件事响好几轮。
|
|
24
|
+
*
|
|
25
|
+
* 通知不设自动关闭时间,会一直挂在系统通知里,直到对应会话发生操作:
|
|
26
|
+
* 会话被选中、该会话开始新一轮、待回答被解决,或会话从列表移除。每个会话
|
|
27
|
+
* 最多只保留最新一条,新通知会顶掉旧的;点击通知会聚焦窗口并切到该会话。
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-notify-ding/client
|
|
30
|
+
*/
|
|
11
31
|
/** 插件名,同时也是配置项 id。 */
|
|
12
32
|
const name = "dsh-notify-ding";
|
|
13
33
|
/** 本插件需要的客户端服务。 */
|
|
14
34
|
const inject = ["uiSession", "sessions"];
|
|
15
|
-
/** 宿主播音路由,与宿主半身注册的路径一致。 */
|
|
16
|
-
const DING_PATH = "/dsh-notify-ding/ding";
|
|
17
35
|
/** 两次响铃之间的最小间隔,窗口内的合并成一次。 */
|
|
18
36
|
const MIN_INTERVAL_MS = 800;
|
|
19
37
|
/** 跨标签页共享「上次响铃时刻」的 localStorage 键。 */
|
|
@@ -60,20 +78,6 @@ window.__ModuleLoader__.load({
|
|
|
60
78
|
console.warn("[dsh-notify-ding] 写入共享响铃时刻失败:" + String(error));
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
|
-
/**
|
|
64
|
-
* 请求宿主播放提示音。
|
|
65
|
-
*
|
|
66
|
-
* 声音本身与系统通知互相独立:通知弹不出来时声音照样响,反之亦然。
|
|
67
|
-
* 这里不阻塞调用方,播音失败只记一条警告。
|
|
68
|
-
*/
|
|
69
|
-
function requestHostDing() {
|
|
70
|
-
fetch(DING_PATH, {
|
|
71
|
-
method: "POST",
|
|
72
|
-
credentials: "same-origin"
|
|
73
|
-
}).catch((error) => {
|
|
74
|
-
console.warn("[dsh-notify-ding] 请求宿主播音失败:" + String(error));
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
81
|
/** 关掉某会话的通知;没有或已关就当无操作。 */
|
|
78
82
|
function closeNotification(sessionId) {
|
|
79
83
|
const notification = liveNotifications.get(sessionId);
|
|
@@ -171,16 +175,6 @@ window.__ModuleLoader__.load({
|
|
|
171
175
|
window.addEventListener("keydown", onGesture, true);
|
|
172
176
|
}
|
|
173
177
|
/**
|
|
174
|
-
* 发一次「叮咚」:弹系统通知并请宿主播音。
|
|
175
|
-
* @param sessionId 这条通知所属的会话。
|
|
176
|
-
* @param title 通知标题。
|
|
177
|
-
* @param body 通知正文。
|
|
178
|
-
*/
|
|
179
|
-
function emit(sessionId, title, body) {
|
|
180
|
-
showSystemNotification(sessionId, title, body);
|
|
181
|
-
requestHostDing();
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
178
|
* 发一次「叮咚」,并做节流。
|
|
185
179
|
*
|
|
186
180
|
* 节流时刻写进 localStorage,多标签页共享,避免多开时同一件事响好几轮。
|
|
@@ -197,7 +191,7 @@ window.__ModuleLoader__.load({
|
|
|
197
191
|
if (since < 0 || since >= MIN_INTERVAL_MS) {
|
|
198
192
|
lastDingAt = now;
|
|
199
193
|
writeSharedLastDing(now);
|
|
200
|
-
|
|
194
|
+
showSystemNotification(sessionId, title, body);
|
|
201
195
|
return;
|
|
202
196
|
}
|
|
203
197
|
deferred = {
|
|
@@ -214,7 +208,7 @@ window.__ModuleLoader__.load({
|
|
|
214
208
|
const stamp = Date.now();
|
|
215
209
|
lastDingAt = stamp;
|
|
216
210
|
writeSharedLastDing(stamp);
|
|
217
|
-
|
|
211
|
+
showSystemNotification(pending.sessionId, pending.title, pending.body);
|
|
218
212
|
}, MIN_INTERVAL_MS - since);
|
|
219
213
|
}
|
|
220
214
|
/** 会话标题,缺失时退回会话 id,保证通知里总有个能认的名字。 */
|
package/lib/index.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
2
1
|
//#region src/index.d.ts
|
|
3
|
-
/** 插件名,同时也是配置项 id。 */
|
|
4
|
-
export declare const name = "dsh-notify-ding";
|
|
5
|
-
/** 依赖宿主 web 载体的具名路由注册能力。没有 web 载体时本插件不激活。 */
|
|
6
|
-
export declare const inject: string[];
|
|
7
|
-
/** Windows 通知音的绝对路径,本家音色。 */
|
|
8
|
-
export declare const SOUND_FILE = "C:\\Windows\\Media\\Windows Notify System Generic.wav";
|
|
9
|
-
/** 一次「叮咚」播放的次数,一声即可,重复反而显得吵。 */
|
|
10
|
-
export declare const BEEP_COUNT = 1;
|
|
11
2
|
/**
|
|
12
|
-
*
|
|
3
|
+
* dsh-notify-ding 的宿主入口。
|
|
4
|
+
*
|
|
5
|
+
* 提示音交给系统通知自带,插件不再自行发声,因此本入口没有运行时行为。
|
|
6
|
+
* 之所以还留一个入口文件,是因为客户端模块系统按 Loader entry 扫描
|
|
7
|
+
* dsh.client 声明:entry 指向的宿主模块必须存在,浏览器半身才会被加载。
|
|
13
8
|
*
|
|
14
|
-
* @
|
|
9
|
+
* @module dsh-notify-ding
|
|
15
10
|
*/
|
|
16
|
-
|
|
11
|
+
/** 插件名,同时也是配置项 id。 */
|
|
12
|
+
export declare const name = "dsh-notify-ding";
|
|
13
|
+
/** 无宿主行为,仅作为可挂载的宿主入口存在。 */
|
|
14
|
+
export declare function apply(): void;
|
|
17
15
|
//#endregion
|
package/lib/index.js
CHANGED
|
@@ -1,114 +1,16 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
//#region src/index.ts
|
|
3
2
|
/**
|
|
4
|
-
* dsh-notify-ding
|
|
3
|
+
* dsh-notify-ding 的宿主入口。
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 而 Windows 本家音色只有读得到那些 wav 的宿主进程才播得出来。
|
|
10
|
-
*
|
|
11
|
-
* 浏览器半身负责判断「该响了」;本半身不参与任何触发判断,只负责发声。
|
|
5
|
+
* 提示音交给系统通知自带,插件不再自行发声,因此本入口没有运行时行为。
|
|
6
|
+
* 之所以还留一个入口文件,是因为客户端模块系统按 Loader entry 扫描
|
|
7
|
+
* dsh.client 声明:entry 指向的宿主模块必须存在,浏览器半身才会被加载。
|
|
12
8
|
*
|
|
13
9
|
* @module dsh-notify-ding
|
|
14
10
|
*/
|
|
15
11
|
/** 插件名,同时也是配置项 id。 */
|
|
16
12
|
const name = "dsh-notify-ding";
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
/** 播音路由的绝对路径。 */
|
|
20
|
-
const ROUTE_PATH = "/dsh-notify-ding/ding";
|
|
21
|
-
/** Windows 通知音的绝对路径,本家音色。 */
|
|
22
|
-
const SOUND_FILE = "C:\\Windows\\Media\\Windows Notify System Generic.wav";
|
|
23
|
-
/** 一次「叮咚」播放的次数,一声即可,重复反而显得吵。 */
|
|
24
|
-
const BEEP_COUNT = 1;
|
|
25
|
-
/**
|
|
26
|
-
* 组装播音用的 PowerShell 脚本。
|
|
27
|
-
*
|
|
28
|
-
* 路径经单引号转义后拼进脚本字面量,避免文件名里的特殊字符被当成命令解析。
|
|
29
|
-
* 播放次数写进脚本,因此一次进程启动就能响完整段声音,不必每声各起一个进程。
|
|
30
|
-
*
|
|
31
|
-
* @returns 一段自包含的 PowerShell 脚本。
|
|
32
|
-
*/
|
|
33
|
-
function buildPlayScript() {
|
|
34
|
-
return [
|
|
35
|
-
"$ErrorActionPreference = 'Stop'",
|
|
36
|
-
"$ProgressPreference = 'SilentlyContinue'",
|
|
37
|
-
"$player = New-Object System.Media.SoundPlayer '" + SOUND_FILE.replace(/'/g, "''") + "'",
|
|
38
|
-
"for ($i = 0; $i -lt 1; $i++) {",
|
|
39
|
-
" $player.PlaySync()",
|
|
40
|
-
"}"
|
|
41
|
-
].join("\n");
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 把播音请求交给一个脱离宿主事件循环的 PowerShell 进程。
|
|
45
|
-
*
|
|
46
|
-
* 用 detached 启动,因此响铃不会被 dsh 的生命周期或响应结束打断;
|
|
47
|
-
* stderr 仍要接住,否则播放失败时宿主会留下无人消费的管道数据。
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
function spawnPlayer() {
|
|
51
|
-
const script = buildPlayScript();
|
|
52
|
-
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
53
|
-
const child = spawn("powershell.exe", [
|
|
54
|
-
"-NoLogo",
|
|
55
|
-
"-NoProfile",
|
|
56
|
-
"-NonInteractive",
|
|
57
|
-
"-EncodedCommand",
|
|
58
|
-
encoded
|
|
59
|
-
], {
|
|
60
|
-
detached: true,
|
|
61
|
-
stdio: [
|
|
62
|
-
"ignore",
|
|
63
|
-
"ignore",
|
|
64
|
-
"pipe"
|
|
65
|
-
],
|
|
66
|
-
windowsHide: true
|
|
67
|
-
});
|
|
68
|
-
child.stderr?.on("data", (chunk) => {
|
|
69
|
-
const text = chunk.toString("utf8").trim();
|
|
70
|
-
if (text) console.warn("[dsh-notify-ding] 播音进程报错:" + text);
|
|
71
|
-
});
|
|
72
|
-
child.on("error", (error) => {
|
|
73
|
-
console.warn("[dsh-notify-ding] 播音进程启动失败:" + error.message);
|
|
74
|
-
});
|
|
75
|
-
child.on("close", (code) => {
|
|
76
|
-
if (code !== 0 && code !== null) console.warn("[dsh-notify-ding] 播音进程退出码:" + String(code));
|
|
77
|
-
});
|
|
78
|
-
child.unref();
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* 挂载播音路由。
|
|
82
|
-
*
|
|
83
|
-
* @param ctx 宿主上下文,webServer 服务已就绪。
|
|
84
|
-
*/
|
|
85
|
-
function apply(ctx) {
|
|
86
|
-
const webServer = ctx.get("webServer");
|
|
87
|
-
if (!webServer) {
|
|
88
|
-
console.warn("[dsh-notify-ding] 未找到 webServer 服务,播音路由不注册。");
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
webServer.register({
|
|
92
|
-
kind: "exact",
|
|
93
|
-
path: ROUTE_PATH,
|
|
94
|
-
handler: (req, res) => {
|
|
95
|
-
if (req.method !== "POST") {
|
|
96
|
-
res.statusCode = 405;
|
|
97
|
-
res.end("method not allowed");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
spawnPlayer();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.warn("[dsh-notify-ding] 播音调用失败:" + error.message);
|
|
104
|
-
res.statusCode = 500;
|
|
105
|
-
res.end("spawn failed");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
res.statusCode = 200;
|
|
109
|
-
res.end("ding");
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
13
|
+
/** 无宿主行为,仅作为可挂载的宿主入口存在。 */
|
|
14
|
+
function apply() {}
|
|
113
15
|
//#endregion
|
|
114
|
-
export {
|
|
16
|
+
export { apply, name };
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-ding",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "在 DSH
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "在 DSH 需要人工回答或一轮对话跑完时弹出系统通知。标准可安装的 dsh 客户端插件。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
8
8
|
"dsh-plugin",
|
|
9
9
|
"notification",
|
|
10
|
-
"sound",
|
|
11
10
|
"notify"
|
|
12
11
|
],
|
|
13
12
|
"repository": {
|