dsh-notify-me 1.12.0 → 1.14.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/CHANGELOG.md +54 -0
- package/README.en.md +36 -22
- package/README.md +37 -24
- package/lib/client.js +616 -121
- package/lib/focus-window.ps1 +349 -0
- package/lib/index.js +292 -18
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.14.0] — 2026-10-23
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **桌面端点通知真的能抬窗了**:插件在宿主侧注册一条 exact 路由 `POST /__dsh-notify-me/focus-window`,页面点通知时 POST 过去,宿主 spawn 一次 `powershell.exe` 跑 `lib/focus-window.ps1`,按 pid 找到 Electron 主窗口后做原生激活,把 `FOCUSED=1|0` 回成 `200` / `202`。此前那条 `dsh://open` 深链在 0.2.1-rc.2 上隔离复测 3/3 无效,1.14.0 起整条删掉。
|
|
10
|
+
- **`debug()` 多一个 `raiseLast`**:`focused` / `not-focused:<原因>` / `http-<状态码>` / `error` / `browser` / `no-fetch`,报障时能分清是「路由没到」「脚本没抬起来」还是「浏览器里本来就只有 `window.focus()`」。`raiseChannel` 现在桌面端是 `host-route`,浏览器端仍是 `window-focus`。
|
|
11
|
+
- **路由自带三条自证**:`GET` 返回 `{ok, route, enabled, desktop, platform}`,装机后可以先用浏览器打开确认路由在不在;非桌面宿主(Web 版、macOS / Linux 桌面、纯 Node)一律 `501 unsupported-host`,不会去 spawn 一个注定失败的进程。
|
|
12
|
+
|
|
13
|
+
### 移除
|
|
14
|
+
|
|
15
|
+
- **`dsh://open` 隐藏 iframe 通道**:子框架导航绕过 `will-navigate` 那条路依旧在,但应用的单实例让位每次都先于聚焦发生(带参第二实例约 130 ms 后退出码 1,`IsIconic` 不变),留着只是白起一个进程。
|
|
16
|
+
|
|
17
|
+
### 说明
|
|
18
|
+
|
|
19
|
+
- **为什么走 PowerShell 而不是原生模块**:插件宿主是普通 Node 进程(壳以 `ELECTRON_RUN_AS_NODE=1` 拉起),拿不到 Electron API;带原生依赖还得跟宿主 ABI 对齐。`powershell.exe` 在 Windows 10/11 上必在,脚本用 `Add-Type` 现编译 P/Invoke,零安装。
|
|
20
|
+
- **抬窗顺序是实的,不是拍脑袋**:已在前台就什么都不做;最小化了先 `SW_RESTORE`;然后合成 ALT 抖动给本线程前台权限再 `SetForegroundWindow`;都不行才走 `SW_MINIMIZE` + `SW_RESTORE` 兜底。单独调 `SetForegroundWindow` 被前台锁拒,退到 ALT 抖动那一步就过——这条来自 [discussions #10446 的实测](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913),也解释了此前「时灵时不灵」的观感。
|
|
21
|
+
- **等待上限 8 秒、400 毫秒内合并**:脚本自身最坏 0.4 秒,超时按 `FOCUSED=0` 收口并 kill 掉;连点两条通知只跑一次。
|
|
22
|
+
- **只 Windows**:`process.platform === "win32"` 且宿主跑在 Electron 里(`process.versions.electron`)才启用,其余平台一律 `501`,页面的抬窗行为跟以前一样。
|
|
23
|
+
|
|
24
|
+
### 测试
|
|
25
|
+
|
|
26
|
+
- 冒烟新增 6 个场景:桌面页面断言 POST 到路由、带 `x-dsh-notify-me-focus` 头、点击后切会话、`raiseLast` 为 `focused`、不再插 iframe;浏览器断点零 `fetch`、`raiseLast` 为 `browser`;`hostEnv` 两个方向都能覆盖;`501` 与 `fetch` 抛错时都只记状态、不挡切会话。
|
|
27
|
+
- cordis 冒烟新增路由段:真 `Context` + 假 `webServer`,断言只注册一条 exact 路由、`GET` 探测形状、非桌面 `501`、`DELETE` 405、缺 header 403,以及 `FOCUSED=1` / `FOCUSED=0` 两个桩脚本分别回 `200` / `202`。
|
|
28
|
+
- 真实窗口实测(`powershell.exe` 5.1,同一轮):窗口在前台 ×3、最小化 + 记事本在前台 ×3、可见被盖住 ×3 全部 `FOCUSED=1` 且前台比对一致;记事本最大化遮挡、最小化 + 最大化遮挡、`-ProcessId 0` 自动探测同样成功;脚本内部耗时最坏 434 ms。`pwsh` 7.6 对照一轮行为一致,仅 `Add-Type` 编译慢约 100 ms。不存在的 pid 与存在但无窗口的 pid 都稳定 `FOCUSED=0` 且退出码 0。
|
|
29
|
+
|
|
30
|
+
### 文档
|
|
31
|
+
|
|
32
|
+
- README 中英重写[桌面端怎么抬窗的](README.md#桌面端怎么抬窗的):删掉 1.5.2 深链那套叙述,换成宿主路由 + 原生激活的链路、实测数据与失败时的回退行为;已知限制里「桌面端不一定上前台」换成「提权窗口 / 虚拟桌面 / 前台被连续抢占时仍可能抬不起来,此时返回 `202` 但对话照常切」。
|
|
33
|
+
- 致谢加上 [@xinming-chenniang](https://github.com/xinming-chenniang):[discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913) 里给的五种手法实测表与「宿主自定义路由 + 同源校验 + PowerShell 兜底链」的思路,是这一版落地的直接依据。
|
|
34
|
+
|
|
35
|
+
## [1.13.0] — 2026-10-23
|
|
36
|
+
|
|
37
|
+
### 新增
|
|
38
|
+
|
|
39
|
+
- **外部推送新增三种国内常用目标**(来自 [#17](https://github.com/chromoany/dsh-notify-me/issues/17) 的安卓端诉求):`feishu`(飞书群机器人)、`serverchan`(Server酱 Turbo 与 Server酱³ 同一套 `…/send/<SendKey>.send` 地址 + `title`/`desp` 表单)、`wxpusher`(`appToken` + 接收者 UID,端点固定,没有可填错的 URL)。三种都走官方接口,浏览器页面能直连确认送达——飞书返回 `Access-Control-Allow-Origin: *`,Server酱与 WxPusher 回显页面 Origin。
|
|
40
|
+
- **失败自动重试与失败记录**:明显临时的失败(网络中断、HTTP 5xx、429)按 `externalRetryDelays`(默认 2 秒 / 6 秒,带 ±25% 抖动)退避重试;其它错误立即停。重试仍失败的投递写进「失败记录」(最多 10 条,只留标题与正文摘要、事件类型与主机名,不留完整端点),设置页可查看与清空。
|
|
41
|
+
- **每个目标可单独启停、并显示最近一次投递状态**:目标行右侧显示「上次成功 / 上次已送出(不可确认)/ 上次失败 + 原因 + 时间」,旁边一个独立开关(`enabled: false`)——某个端点临时不可用就单独关掉它,不必关掉整类事件。
|
|
42
|
+
- **出站签名**:自定义目标填了签名密钥时,请求带 `X-DSH-Notify-Signature`(对请求体做 HMAC-SHA256,`sha256=<hex>`)与 `X-DSH-Notify-Timestamp`;飞书目标填了密钥时按官方算法在请求体里带 `timestamp`(秒)/ `sign`(`base64(HMAC(key = timestamp + "\n" + secret, data = ""))`)。
|
|
43
|
+
- **消息模板**:飞书 / Server酱 / WxPusher / 自定义目标可填模板,占位符 `{{title}}` `{{body}}` `{{event}}` `{{time}}` `{{session}}`;留空是「标题 + 换行 + 正文」。
|
|
44
|
+
- 服务端返回的失败原因如实写进状态:飞书/Server酱/WxPusher 的 `code` 判成功(WxPusher 为 `1000`),失败带上 `error` / `message` / `info` / `msg`。
|
|
45
|
+
|
|
46
|
+
### 说明
|
|
47
|
+
|
|
48
|
+
- **钉钉群机器人没有做成目标类型**:实测其接口不返回任何 CORS 头,且只接受 `application/json`——浏览器页面既读不到响应,也无法在 no-cors 下发出合规请求。文档与设置页提示:自建一个中转,用「自定义 Webhook」指向它。
|
|
49
|
+
- **PushDeer / ntfy 同样没有做**:PushDeer 官方安卓客户端已停止维护、接口无 CORS 头;ntfy 自建在安卓上依赖常连保活、默认 `priority` 低于 4 不弹横幅。需要时仍可用自定义目标接自建 ntfy。
|
|
50
|
+
|
|
51
|
+
### 测试
|
|
52
|
+
|
|
53
|
+
- 冒烟新增:4 种目标各发一次并逐个校验报文形状(飞书 text + 签名、Server酱 form 编码、WxPusher token/uids、自定义模板与 HMAC 头);网络错误/5xx 重试到耗尽进失败记录、4xx 不重试、第二次成功只记 `sent`;目标级 `enabled:false` 被裁剪;设置页的类型专属字段、目标启停开关、状态文案、失败记录展示与清空。
|
|
54
|
+
|
|
55
|
+
### 文档
|
|
56
|
+
|
|
57
|
+
- README 中英:外部推送整节重写(目标类型、可靠性、状态、签名、模板、钉钉为何不支持),控制台示例补 `externalRetryDelays` 与目标字段形状,已知限制口径同步;致谢加上 [@yuexian7](https://github.com/yuexian7) 与 #17。
|
|
58
|
+
|
|
5
59
|
## [1.12.0] — 2026-10-21
|
|
6
60
|
|
|
7
61
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -26,11 +26,11 @@ A notification plugin for DSH Web / Desktop, so you can keep full access off wit
|
|
|
26
26
|
- **When input is needed:** approval, plan review, and questions trigger notifications, sound, and a title marker.
|
|
27
27
|
- **When a background reply finishes:** return to the result; click the notification to open its conversation. Pick "background only", "quiet for the open one", or "always".
|
|
28
28
|
- **When you walk off but the window stays in front:** over 3 minutes without input counts as away, and the alert still fires; a toast you have not seen is not closed on a timer before you return.
|
|
29
|
-
- **When you leave your computer:** optionally forward alerts to Discord, Bark, Gotify, or your own webhook.
|
|
29
|
+
- **When you leave your computer:** optionally forward alerts to Discord, Bark, Gotify, Feishu group bots, ServerChan, WxPusher, or your own webhook.
|
|
30
30
|
|
|
31
31
|
> Keep the page or desktop app running; backgrounding / minimizing is fine. System notifications require permission. Approval buttons need a Service Worker: Web (`http://127.0.0.1` or `https`) has one, the Desktop `dsh-app://` pages cannot register one, so their notifications carry no buttons — click the notification to decide in the conversation.
|
|
32
32
|
|
|
33
|
-
> 🔎 Looking for it? Search `dsh-notify-me`, **message alerts**, **notification**, **desktop notification**, **reminder**, **approval alert**, **external notifications**, **Discord / Bark / Gotify push** or **可操作提醒 / 消息提醒 / 桌面通知 / 需要你操作** (npm keywords include both English and Chinese terms).
|
|
33
|
+
> 🔎 Looking for it? Search `dsh-notify-me`, **message alerts**, **notification**, **desktop notification**, **reminder**, **approval alert**, **external notifications**, **Android push**, **Discord / Bark / Gotify / Feishu / ServerChan / WxPusher push** or **可操作提醒 / 消息提醒 / 桌面通知 / 需要你操作** (npm keywords include both English and Chinese terms).
|
|
34
34
|
|
|
35
35
|
## UI Preview
|
|
36
36
|
|
|
@@ -118,7 +118,7 @@ Open **Settings → Notify me**. The page runs as how you get told, when to aler
|
|
|
118
118
|
|
|
119
119
|
- **Enable reminders**: master switch — off means no toasts, no sounds, and the tab title is left alone;
|
|
120
120
|
- **Sound / Volume**: the switch is direct; the slider folds into that row — click the title (▸ turns into ▾) to reveal it. What it plays comes from Advanced → Alert sound;
|
|
121
|
-
- **Decide approvals from the toast** (on by default): approvals carry Approve / Reject that settle the request in place. On Web that is a system notification with buttons; on Desktop `dsh-app://` cannot register a Service Worker, so the notification carries no buttons — click it to decide in the conversation. The line
|
|
121
|
+
- **Decide approvals from the toast** (on by default): approvals carry Approve / Reject that settle the request in place. On Web that is a system notification with buttons; on Desktop `dsh-app://` cannot register a Service Worker, so the notification carries no buttons — click it to decide in the conversation. The line under the Try-it block states whether buttons are possible here.
|
|
122
122
|
|
|
123
123
|
### When to alert
|
|
124
124
|
|
|
@@ -127,7 +127,7 @@ Open **Settings → Notify me**. The page runs as how you get told, when to aler
|
|
|
127
127
|
- **When "reply finished" alerts** (default "background only"): three choices. "Background only" is the old rule — alert when you are not watching the page; "quiet for the open one" stays silent only for the conversation on screen while any other conversation still alerts (useful with several open); "always" never suppresses. An old profile's "also alert reply finished while in front" switch migrates to this setting;
|
|
128
128
|
- **Dismiss the toast automatically** (default "keep while away"): three choices. "Keep while away" only closes a toast a few seconds later while you are at the page, so one that arrived while you were away is still in the notification centre when you come back; "always close" is the old tidy-up; "never close" leaves plain toasts up until you dismiss one. Approvals keep their 12 s rule and are likewise not withdrawn while you are away;
|
|
129
129
|
- **Ignore subagent sessions** (on): skip background sessions spawned by a subagent — the parent conversation covers them; the one you have open still alerts;
|
|
130
|
-
- **Click a toast to open that conversation** (on): the click switches to the conversation that raised it; in a browser it also brings the window forward, while on the Desktop shell
|
|
130
|
+
- **Click a toast to open that conversation** (on): the click switches to the conversation that raised it; in a browser it also brings the window forward, while on the Desktop shell the host route raises the native window (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)) and a failure only leaves the conversation switch in place, with the reason in `debug().raiseLast`; with several tabs open only one is raised, and off means a click does nothing.
|
|
131
131
|
|
|
132
132
|
### Try it
|
|
133
133
|
|
|
@@ -152,24 +152,29 @@ Choose a built-in cue, custom audio file, or system notification sound independe
|
|
|
152
152
|
|
|
153
153
|
### External push
|
|
154
154
|
|
|
155
|
-
Off by default. Supports Discord Webhook / Bark / Gotify / custom webhooks. Alert text is sent to the addresses you enter; use trusted endpoints.
|
|
155
|
+
Off by default. Supports Discord Webhook / Bark / Gotify / Feishu group bots / ServerChan (Turbo and ServerChan³) / WxPusher / custom webhooks. Alert text is sent to the addresses you enter; use trusted endpoints.
|
|
156
156
|
|
|
157
157
|
<details>
|
|
158
|
-
<summary>Target URLs, forwarding switches, and
|
|
158
|
+
<summary>Target URLs, forwarding switches, test messages, and the failure log</summary>
|
|
159
159
|
|
|
160
|
-
- **External notifications** (default off) — forward each alert to your own push endpoints as well, so it reaches you away from the machine: **Discord Webhook** / **Bark** / **Gotify** / **custom webhook**, any number of targets, each
|
|
160
|
+
- **External notifications** (default off) — forward each alert to your own push endpoints as well, so it reaches you away from the machine: **Discord Webhook** / **Bark** / **Gotify** / **Feishu group bot** / **ServerChan** / **WxPusher** / **custom webhook**, any number of targets, each of which can be enabled or disabled on its own (Discord takes the webhook URL; Bark `https://api.day.app/<device key>`; Gotify `https://<server>/message?token=<app token>`; Feishu the group-bot webhook plus an optional verification secret; ServerChan the full `…/send/<SendKey>.send` URL; WxPusher the appToken and the receiver UID; a custom endpoint receives one POST of JSON: `{source, event, title, body, text, sessionId, time}`). "Forward 'needs you'" and "Forward 'reply finished'" switch independently; **Send test notification** sends one test message to every target (not limited by those two switches, and never pretending to be a real alert). Forwarding follows exactly the same trigger rules as local alerts ("stay quiet for the conversation on screen" and friends apply to it too); the alert text is sent as-is to the addresses you fill in, so **only use endpoints you trust**;
|
|
161
|
+
- **Reliable delivery** — failures that look transient (network error, 5xx, 429) are retried after 2 s and 6 s; anything else stops at once. A delivery that still fails is recorded in the failure log (10 entries at most — title and body snippet plus host only, never the full endpoint), which the settings page shows and can clear;
|
|
162
|
+
- **Per-target state** — the right end of each target row shows how its last delivery went (last delivery ok / sent-unverified / failed with the reason) next to a switch of its own, so an endpoint that is temporarily down can be turned off alone instead of muting a whole alert kind;
|
|
163
|
+
- **Signing** — a custom target with a secret sends `X-DSH-Notify-Signature` (HMAC-SHA256 over the body, `sha256=<hex>`); a Feishu target with a secret carries `timestamp` / `sign` in the body per Feishu's own algorithm;
|
|
164
|
+
- **Message template** (optional) — Feishu / ServerChan / WxPusher / custom targets accept a template with `{{title}}` `{{body}}` `{{event}}` `{{time}}` `{{session}}`; empty means "title, newline, body";
|
|
165
|
+
- **DingTalk is not supported** — its robot endpoint answers no CORS header at all and only accepts `application/json`, so a page can neither read the reply nor send a conforming request (under no-cors the content type is restricted to the safelisted ones). To reach DingTalk, relay it through a machine you control and point a custom webhook at that.
|
|
161
166
|
|
|
162
167
|
</details>
|
|
163
168
|
|
|
164
169
|
### Runtime & diagnostics
|
|
165
170
|
|
|
166
|
-
The last block inside Advanced. The page protocol is detected automatically to tell Web from Desktop (`dsh-app://`); override it by hand if detection is wrong — Desktop clicks
|
|
171
|
+
The last block inside Advanced. The page protocol is detected automatically to tell Web from Desktop (`dsh-app://`); override it by hand if detection is wrong — Desktop clicks go through the host route for native activation (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)) and carry no quick-decision buttons, while Web uses `window.focus()` plus the Service Worker bridge. When the override disagrees with detection the page says so.
|
|
167
172
|
|
|
168
173
|
<details>
|
|
169
174
|
<summary>Alert language, Desktop window raising, reset</summary>
|
|
170
175
|
|
|
171
176
|
- **Alert language**: follow the UI / Simplified Chinese / English — governs alert copy and the `🔔 …` title marker;
|
|
172
|
-
- **How the Desktop window is raised**: `window.focus()` cannot lift a native Electron window, so since 1.
|
|
177
|
+
- **How the Desktop window is raised**: `window.focus()` cannot lift a native Electron window, so since 1.14.0 the page POSTs to the host route `/__dsh-notify-me/focus-window` and the host runs one PowerShell pass to activate the window natively — measured raising it back from both minimized and covered (see the next section);
|
|
173
178
|
- **Reset settings**: restores every switch and cue setting to the factory defaults (webhook targets and chosen audio files are kept).
|
|
174
179
|
|
|
175
180
|
</details>
|
|
@@ -183,7 +188,7 @@ The last block inside Advanced. The page protocol is detected automatically to t
|
|
|
183
188
|
- **System notification** — a toast through the browser into the Windows action center / macOS Notification Center (clicking it opens the conversation it came from; supported Web environments offer approval buttons, Desktop does not)
|
|
184
189
|
- **Sound** — WebAudio beeps by default (distinct patterns for "needs you" vs "done"); each kind can instead play a picked audio file, or hand the sound over to the operating system's own notification sound
|
|
185
190
|
- **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`; while a background session is **finished but unread**, with `✅ Reply finished · …` / `✅ 回复完成 · …`, gone once you open that session (or it starts running again). Both kinds can be up together, "needs you" first; a host title change is rebased under the markers
|
|
186
|
-
- **External notifications** (optional, default off) — the same alert is also forwarded to the push targets you configure: Discord Webhook / Bark / Gotify / custom webhook — so the push channel on your phone picks it up while you step away from the machine
|
|
191
|
+
- **External notifications** (optional, default off) — the same alert is also forwarded to the push targets you configure: Discord Webhook / Bark / Gotify / Feishu group bot / ServerChan / WxPusher / custom webhook — so the push channel on your phone picks it up while you step away from the machine
|
|
187
192
|
|
|
188
193
|
This is a **browser-layer** plugin: the DSH page must stay open (minimized or backgrounded is fine — that's exactly the "away" state it watches for).
|
|
189
194
|
|
|
@@ -241,8 +246,14 @@ window.__dshNotifyMe.setConfig({
|
|
|
241
246
|
externalEnabled: false, // external-notification forwarding master switch (default off: no endpoint, nothing is sent)
|
|
242
247
|
externalAttention: true, // forward "needs you" (exactly the same trigger rules as local alerts)
|
|
243
248
|
externalDone: true, // forward "reply finished"
|
|
244
|
-
|
|
249
|
+
externalRetryDelays: [2000, 6000], // backoff for retryable failures (ms, with jitter; [] = send once)
|
|
250
|
+
externalTargets: [] // push targets, see the shape below
|
|
245
251
|
})
|
|
252
|
+
// Target shape: {id, type, url, secret?, token?, uid?, template?, enabled?}
|
|
253
|
+
// type: 'discord' | 'bark' | 'gotify' | 'feishu' | 'serverchan' | 'wxpusher' | 'custom'
|
|
254
|
+
// token/uid only for wxpusher (its URL is fixed to the official endpoint); secret signs Feishu bodies
|
|
255
|
+
// and custom HMAC headers; template is optional ({{title}} {{body}} {{event}} {{time}} {{session}});
|
|
256
|
+
// enabled:false disables that one target
|
|
246
257
|
window.__dshNotifyMe.resetConfig() // restore defaults (also drops any picked audio files)
|
|
247
258
|
window.__dshNotifyMe.decide("approval:3", "allowed-once") // programmatic decision: "allowed-once" | "rejected"
|
|
248
259
|
window.__dshNotifyMe.testExternal() // explicit outbound test to valid targets; resolves {sent, unverified, failed, statuses}
|
|
@@ -258,7 +269,7 @@ window.__dshNotifyMe.testExternal() // explicit outbound test to v
|
|
|
258
269
|
|
|
259
270
|
## How it works
|
|
260
271
|
|
|
261
|
-
The reminder core watches session state and pending interactions. The page handles approval decisions; Desktop window activation uses
|
|
272
|
+
The reminder core watches session state and pending interactions. The page handles approval decisions; Desktop window activation uses a host route plus native Win32 calls.
|
|
262
273
|
|
|
263
274
|
<details>
|
|
264
275
|
<summary>Event sources, approval bridge, and Desktop window activation</summary>
|
|
@@ -280,17 +291,17 @@ Every button toast is booked against its interaction key, so it can only decide
|
|
|
280
291
|
|
|
281
292
|
### How the Desktop window gets raised
|
|
282
293
|
|
|
283
|
-
DSH Desktop is a thin Electron shell and its page lives under `dsh-app://app`: DOM `window.focus()` only touches the browsing-context window, never the native one, and the shell's `will-navigate`
|
|
294
|
+
DSH Desktop is a thin Electron shell and its page lives under `dsh-app://app`: DOM `window.focus()` only touches the browsing-context window, never the native one, and the shell's `will-navigate` handler blocks the page from firing a `dsh:` URL. From 1.5.2 through 1.13.0 the plugin sidestepped that with a hidden iframe navigating to `dsh://open`, relying on the app's single-instance yield to focus the window second-hand. Isolated retests on 0.2.1-rc.2 failed 3/3, so 1.14.0 removes that path and goes through the plugin's own host route instead.
|
|
284
295
|
|
|
285
|
-
|
|
296
|
+
For every non-asset path under `dsh-app://app` the shell forwards the request to the host HTTP server (with an origin check and the host cookie injected), so a page POST to `/__dsh-notify-me/focus-window` is a same-origin request with no CORS involved. The host half (`lib/index.js`) already runs as a child of the Electron main process, and its `process.ppid` is that of the window's browser process. On a toast click the page POSTs, the host spawns one `powershell.exe` running `lib/focus-window.ps1 -ProcessId <ppid>`, the script enumerates the visible, owner-less, non-toolwindow top-level windows of that pid (preferring class `Chrome_WidgetWin_1`, then the largest area from `GetWindowRect`), raises it, compares `GetForegroundWindow()` and prints `FOCUSED=1|0`, and the host answers `200` / `202`.
|
|
286
297
|
|
|
287
|
-
|
|
298
|
+
The raise order is adaptive and comes from the five-method table measured in [discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913): already in front means do nothing (the common case, no flicker); minimized means `SW_RESTORE` first; then a synthetic Alt tap (`keybd_event` down/up) grants the calling thread foreground permission before `SetForegroundWindow`; only if all of that fails does it fall back to `SW_MINIMIZE` + `SW_RESTORE`. `SetForegroundWindow` on its own gets refused by the foreground lock — per call, unrelated to `ForegroundLockTimeout`. That also re-reads the old "sometimes it works" behaviour: the times the window did come forward were most likely the OS activating the app on the click itself.
|
|
288
299
|
|
|
289
|
-
|
|
300
|
+
Measured with `powershell.exe` 5.1 in one round: window in front ×3, minimized with Notepad in front ×3, visible but covered ×3 — all `FOCUSED=1` with the foreground comparison matching and every `IsIconic` flipped back to `false`; Notepad maximized, minimized plus maximized, and `-ProcessId 0` auto-detection all succeeded too. Script-internal time peaked at 434 ms, process wall time at 782 ms, well inside the 8 s cap; `pwsh` 7.6 behaved identically, only slower by ~100 ms compiling the `Add-Type` helpers (the script avoids PowerShell 7 syntax on purpose). A pid that does not exist and a pid with no visible window both settle on `FOCUSED=0` and `exit 0`.
|
|
290
301
|
|
|
291
|
-
|
|
302
|
+
The route is a single exact path and says no by default: non-POST answers `405`, a non-Desktop host (Web, macOS / Linux, plain Node) `501`, a missing `x-dsh-notify-me-focus: focus` header `403`. A cross-origin page would have to pass a preflight to send that header and the route allows no CORS at all, so other sites in the browser cannot drive it. A `GET` returns `{ok, route, enabled, desktop, platform}` so you can check the route from a browser after installing. Calls within 400 ms coalesce into one, and a script that has not answered after 8 s is killed and counted as a failure.
|
|
292
303
|
|
|
293
|
-
|
|
304
|
+
A few cases still will not raise, and they answer `202` with the conversation switch intact: a target window owned by a higher integrity level (elevated / UAC), a window on another virtual desktop, or a foreground that keeps getting stolen. A tray-hidden window (`SW_HIDE` rather than minimized) is filtered out as well; DSH does not do that today.
|
|
294
305
|
|
|
295
306
|
## Known limitations
|
|
296
307
|
|
|
@@ -298,15 +309,15 @@ There is no page-side fallback either: the shell preload only exposes `dshDeskto
|
|
|
298
309
|
- Notifications appear through the browser, so the browser needs "show notifications" permission in the OS notification settings, and notification-center "Do Not Disturb" must not suppress them.
|
|
299
310
|
- The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
|
|
300
311
|
- Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
|
|
301
|
-
- On the Desktop shell (DSH Desktop
|
|
312
|
+
- On the Desktop shell (DSH Desktop, Windows) a toast click can still fail to raise the window: a target owned by a higher integrity level (elevated / UAC), a window on another virtual desktop, a foreground that keeps getting stolen, or a window hidden with `SW_HIDE` to the tray all end in `FOCUSED=0` and a `202` from the route, with the conversation switch intact (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)).
|
|
302
313
|
- System notification banners are drawn on the primary display only: Windows has no setting and Chromium no API to aim one at another monitor, so on a multi-monitor setup the second screen never receives them. Forwarding to your phone with external notifications is the only route across screens (since 1.12.0 the page no longer draws an approval card).
|
|
303
|
-
- Quick-decision buttons need a Service Worker: they work on `http://127.0.0.1` and `https`, not on plain `http://192.168.x.x`-style LAN addresses, and some browsers or desktop hosts never render notification buttons. **The Desktop (`dsh-app://`) is tested and cannot register a Service Worker
|
|
314
|
+
- Quick-decision buttons need a Service Worker: they work on `http://127.0.0.1` and `https`, not on plain `http://192.168.x.x`-style LAN addresses, and some browsers or desktop hosts never render notification buttons. **The Desktop (`dsh-app://`) is tested and cannot register a Service Worker** (the shell does not grant the `allowServiceWorkers` privilege), so buttons never render there and approval toasts fall back to their buttonless form. The `bridge` field of `window.__dshNotifyMe.debug()` says why; everything else keeps working.
|
|
304
315
|
- macOS: in the browser, alerts, sounds, the title marker and toast-click navigation carry no OS assumptions and work as on Windows. Two caveats: DSH Desktop (Electron) notifications require the app to be code-signed — unsigned builds simply emit no notification — and whether the official macOS build satisfies that is untested; and notification buttons on the macOS desktop host are likewise untested (the Windows desktop host is tested and renders none, see the line above), falling back to the buttonless form above.
|
|
305
316
|
- Config lives in browser `localStorage`: switching browsers/devices or clearing site data returns to defaults (one-click restore in the Settings page).
|
|
306
317
|
- On `>= 0.1.6` (including `0.2.0-rc.2`, `0.2.1-rc.2`) the "reply finished" toast body carries only the session label, no assistant snippet: host snapshots no longer expose `nodes`. The alert itself still fires.
|
|
307
318
|
- The title marker is written into `document.title` alongside the host: when the host recomputes the title it can overwrite the marker until the next alert event rewrites it.
|
|
308
|
-
- "Stay quiet for the conversation on screen" is judged by
|
|
309
|
-
- External notification forwarding is subject to browser CORS rules: an endpoint that does not allow cross-origin requests falls back to a "sent but unverifiable" delivery (no-cors) and is reported honestly as such; Discord
|
|
319
|
+
- "Stay quiet for the conversation on screen" is judged by whether the window has focus: a window covered by another app, a switched-away app, or a minimized one all count as background, so waits in the current conversation alert there too; once the window is back in front, or you touch the keyboard / mouse again (no longer idle), it goes quiet again.
|
|
320
|
+
- External notification forwarding is subject to browser CORS rules: an endpoint that does not allow cross-origin requests falls back to a "sent but unverifiable" delivery (no-cors) and is reported honestly as such; Discord, Feishu, ServerChan, and WxPusher all allow cross-origin, so delivery to them is confirmed. DingTalk allows no CORS header at all and only accepts `application/json`, so a page cannot post to it — hence no DingTalk target type (relay it through your own box and use a custom webhook). A failure that looks transient (network error, 5xx, 429) is retried on the backoff; a delivery that still fails lands in the failure log, and anything else stops at once. Forwarding runs only after the local alert has decided to fire, so anything the rules silence is never sent out.
|
|
310
321
|
|
|
311
322
|
- **The settings sidebar glyph relies on a DOM patch**: the shell hard-codes icons per section id and `settings.section` exposes no icon field, so the plugin can only swap the `<svg>` in its own row after the panel renders. It depends on the current panel structure (`data-shortcut-modal="settings"` plus a label `<span>` inside each nav button); if DSH changes that markup the patch silently stops working and the default gear shows instead — nothing else is affected.
|
|
312
323
|
|
|
@@ -317,6 +328,7 @@ node --check lib\client.js
|
|
|
317
328
|
node --check lib\index.js
|
|
318
329
|
node smoke\smoke-test.cjs # offline state-machine test: three host generations + master switch + zh/en cases + the quick-decision bridge (worker half included) + idle detection and toast self-dismiss
|
|
319
330
|
node smoke\cordis-host-test.mjs # real-cordis end-to-end (skips when no local DSH installation is found)
|
|
331
|
+
powershell -NoProfile -File lib\focus-window.ps1 -ProcessId 0 # manual raise test, the last line should be FOCUSED=1
|
|
320
332
|
npm pack --dry-run # preview the published tarball
|
|
321
333
|
```
|
|
322
334
|
|
|
@@ -332,8 +344,10 @@ Contributors (by handle; the link points at the PR / issue they worked on):
|
|
|
332
344
|
- [@Wh1stle05](https://github.com/Wh1stle05) — [#10](https://github.com/chromoany/dsh-notify-me/pull/10): the per-kind alert sound source (built-in synth / picked audio file / system notification sound, chosen independently for "needs you" and "reply done"), with its settings UI, fallback chain, tests and docs; the two refinements — a "system" preview sending a real test notification instead of doing nothing, and the fake React holding hook state by index across renders — come from the same PR.
|
|
333
345
|
- [@yanzhaohui1999](https://github.com/yanzhaohui1999) — [#13](https://github.com/chromoany/dsh-notify-me/issues/13) / [#14](https://github.com/chromoany/dsh-notify-me/pull/14): the 482-session idle-page CPU report (pinning `currentSessionId()` rescanning the whole table per row, 5.9%~6.5% of CPU) and the fix (derive the on-screen session once per list change), with a red-then-green regression assertion — a 4-row and a 40-row list change must cost the same (upstream reported 7 vs 43, 5 vs 5 after).
|
|
334
346
|
- [@RiZhiZhaoYi](https://github.com/RiZhiZhaoYi) — [#12](https://github.com/chromoany/dsh-notify-me/issues/12): the external-notification forwarding suggestion (Discord Webhook / Bark / Gotify / custom webhook) and its use case (1.7.0).
|
|
347
|
+
- [@yuexian7](https://github.com/yuexian7) — [#17](https://github.com/chromoany/dsh-notify-me/issues/17): the ask for alerts on an Android phone — Discord is awkward in mainland China and Bark has no Android app, so a self-hosted relay was the only option; 1.13.0's Feishu / ServerChan / WxPusher targets, retries, and failure log came out of it.
|
|
335
348
|
- [@shrekcg](https://github.com/shrekcg) — [#16](https://github.com/chromoany/dsh-notify-me/pull/16): counting "in front but walked away" as away (`idleMinutes`, 3 minutes by default), deciding "reply finished" alerts by which conversation finished (`donePolicy`, three choices), the three-way toast self-dismiss policy (`autoDismiss`), with matching smoke cases and old-config migration; when it landed in 1.12.0 the maintainer added the idle-expiry replay for suppressed approvals, removed the leftover in-page approval card, and brought docs and version up to date.
|
|
336
349
|
|
|
350
|
+
- [@xinming-chenniang](https://github.com/xinming-chenniang) — [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913): the five-method Win32 raise comparison (`SetForegroundWindow` alone refused by the foreground lock, accepted after a synthetic Alt tap, minimize→restore working in both scenarios), plus a working proof of the "host-registered same-origin route + PowerShell fallback chain" detour; the direct basis for 1.14.0's host route and `lib/focus-window.ps1`.
|
|
337
351
|
Attribution policy: external PRs are landed as maintainer commits — the original commit is not reused and no Co-authored-by trailer is added, so GitHub's contributors graph lists the maintainer only and contributors are credited above.
|
|
338
352
|
|
|
339
353
|
## License
|
package/README.md
CHANGED
|
@@ -26,11 +26,11 @@ DSH Web / Desktop 的通知插件,让你敢不开完整访问,也不必一
|
|
|
26
26
|
- **需要你操作时**:审批、方案确认、提问,用通知、提示音和标题标记提醒。
|
|
27
27
|
- **后台回复完成时**:及时回来查看结果,点击通知可回到对应对话;提醒时机可选「仅后台 / 仅当前对话静默 / 总是提醒」。
|
|
28
28
|
- **人走开但窗口没走**:窗口留在最前台、人却离开座位,超过 3 分钟没有输入也算离开,该提醒的照常提醒;收到的那条通知不会在你回来之前自己消失。
|
|
29
|
-
- **离开电脑时**:可选转发到 Discord、Bark、Gotify 或自己的 Webhook。
|
|
29
|
+
- **离开电脑时**:可选转发到 Discord、Bark、Gotify、飞书群机器人、Server酱、WxPusher 或自己的 Webhook。
|
|
30
30
|
|
|
31
31
|
> 页面或桌面应用需保持运行,后台 / 最小化可以。系统通知需要通知权限。审批按钮靠 Service Worker 才能进通知:Web 端(`http://127.0.0.1` 或 `https`)有,桌面端 `dsh-app://` 页面注册不了,通知上没有按钮,点通知回到对话里裁决。
|
|
32
32
|
|
|
33
|
-
> 🔎 想找这个插件?可以搜:`dsh-notify-me
|
|
33
|
+
> 🔎 想找这个插件?可以搜:`dsh-notify-me`、**消息提醒**、**可操作提醒**、**桌面通知**、**需要你操作**、**回复完成提醒**、**后台完成提醒**、**外部通知**、**安卓推送**、**Discord / Bark / Gotify / 飞书 / Server酱 / WxPusher 推送**(npm 关键词已含以上中英文词)。
|
|
34
34
|
|
|
35
35
|
## 效果预览
|
|
36
36
|
|
|
@@ -73,14 +73,14 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
73
73
|
2. 打开 **设置 → 通知提醒**,确认出现该分区及底部插件版本号,保持「启用提醒」「提示音」开启。
|
|
74
74
|
3. 点击设置页的「测试『回复完成』」按钮:应出现一条测试通知,并按当前音效设置播放声音。这个本地测试不会转发到外部通知端点,也不受「窗口在前台时也提醒」开关限制。
|
|
75
75
|
|
|
76
|
-
Web
|
|
76
|
+
Web 用户可再测审批按钮,通知上会带「同意 / 拒绝」。桌面端拿不到 Service Worker,这条测试只发一条普通通知;真实审批的提醒同理——只弹通知,点它回到对话里裁决。
|
|
77
77
|
|
|
78
78
|
**控制台验证(可选)** — 支持 DevTools 的环境也可执行:
|
|
79
79
|
|
|
80
80
|
```js
|
|
81
81
|
window.__dshNotifyMe.test("done") // 「完成」示例
|
|
82
82
|
window.__dshNotifyMe.test("attention") // 「需要你」示例
|
|
83
|
-
window.__dshNotifyMe.test("approval") // 桥可用时带「同意 /
|
|
83
|
+
window.__dshNotifyMe.test("approval") // 桥可用时带「同意 / 拒绝」按钮;否则只发普通通知(桌面端即如此)
|
|
84
84
|
window.__dshNotifyMe.debug() // 桥接状态看 bridge 字段,active = 按钮可用
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -114,7 +114,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
114
114
|
|
|
115
115
|
- **启用提醒** 总开关:关闭后不弹通知、不响提示音,也不改标签页标题;
|
|
116
116
|
- **提示音 / 音量**:开关直接切;音量滑块收在该行里,点一下标题(▸ 变成 ▾)展开。具体播什么由「高级设置 → 音效来源」决定;
|
|
117
|
-
- **审批通知上直接裁决**(默认开):审批带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。Web 走带按钮的系统通知;桌面端 `dsh-app://` 注册不了 Service Worker
|
|
117
|
+
- **审批通知上直接裁决**(默认开):审批带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。Web 走带按钮的系统通知;桌面端 `dsh-app://` 注册不了 Service Worker,通知上没有按钮,点通知回到对话里裁决。设置页「试一试」下面那行会如实说明当前环境能不能给按钮。
|
|
118
118
|
|
|
119
119
|
### 什么时候提醒
|
|
120
120
|
|
|
@@ -123,7 +123,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
123
123
|
- **「回复完成」什么时候提醒**(默认「仅后台」):三选一。「仅后台」=原来的规则,不在看页面才提醒;「仅当前对话静默」=屏幕上那条静默,别的对话完成照常提醒,多开对话时有用;「总是提醒」=从不静默。老配置里的「窗口在前台时也提醒『回复完成』」开关自动迁移到这一项;
|
|
124
124
|
- **通知自动消失**(默认「离开时保留」):三选一。「离开时保留」=只有你在看页面时才几秒后关闭,人不在时收到的那条留在通知中心等你回来;「总是关闭」=旧行为;「从不关闭」=普通通知一直留着,等你点掉。带「同意 / 拒绝」的审批通知仍按 12 秒走,同样不会在人不在时被撤掉;
|
|
125
125
|
- **子代理会话不提醒**(默认开):跳过后台子代理(subagent)会话——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;
|
|
126
|
-
-
|
|
126
|
+
- **点击通知回到对应对话**(默认开):点通知切到提醒所属的对话;浏览器里同时把窗口带到前台;桌面端交给宿主路由做原生激活(见[桌面端怎么抬窗的](#桌面端怎么抬窗的)),抬不起来时只切对话,原因记在 `debug().raiseLast` 里;多个标签页同时开着时只调起一个(优先当前可见的那个);关掉后点通知不做任何事。
|
|
127
127
|
|
|
128
128
|
### 试一试
|
|
129
129
|
|
|
@@ -148,24 +148,29 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
148
148
|
|
|
149
149
|
### 外部推送
|
|
150
150
|
|
|
151
|
-
默认关闭。支持 Discord Webhook / Bark / Gotify / 自定义 Webhook;提醒正文会发往你填写的地址,只使用可信端点。
|
|
151
|
+
默认关闭。支持 Discord Webhook / Bark / Gotify / 飞书群机器人 / Server酱(Turbo 与 Server酱³)/ WxPusher / 自定义 Webhook;提醒正文会发往你填写的地址,只使用可信端点。
|
|
152
152
|
|
|
153
153
|
<details>
|
|
154
|
-
<summary
|
|
154
|
+
<summary>目标地址、转发开关、发送测试与失败记录</summary>
|
|
155
155
|
|
|
156
|
-
- **外部通知**(默认关):把提醒同时转发到你自己的推送端点,人不在电脑前也能收到——支持 **Discord Webhook** / **Bark** / **Gotify** / **自定义 Webhook
|
|
156
|
+
- **外部通知**(默认关):把提醒同时转发到你自己的推送端点,人不在电脑前也能收到——支持 **Discord Webhook** / **Bark** / **Gotify** / **飞书群机器人** / **Server酱** / **WxPusher** / **自定义 Webhook**,可加多个目标,每个目标可单独启用/停用(Discord 填 Webhook 地址;Bark 填 `https://api.day.app/<设备密钥>`;Gotify 填 `https://<服务器>/message?token=<应用令牌>`;飞书填群机器人 Webhook,可选填签名校验密钥;Server酱 填 `…/send/<SendKey>.send` 完整地址;WxPusher 填 appToken 与接收者 UID;自定义端点收一条 POST JSON:`{source, event, title, body, text, sessionId, time}`);「转发「需要你」」「转发「回复完成」」各自独立开关;**发送测试通知**往所有目标发一条测试消息(不受这两类开关限制,也不会冒充真提醒)。转发跟本地提醒走同一套触发规则(「当前对话不弹通知」等开关同样管它);提醒正文会原样发往你填的地址,**只填你自己信任的端点**;
|
|
157
|
+
- **可靠投递**:明显是临时故障的失败(网络中断、5xx、429)按 2 秒 / 6 秒自动重试两次,其它错误立即停;重试仍失败会把这一条记进「失败记录」(最多 10 条,只留标题与正文摘要和主机名,不留完整端点),设置页能一眼看到并清空;
|
|
158
|
+
- **每个目标的状态**:目标行右侧显示它最近一次投递的结果(上次成功 / 上次已送出但不可确认 / 上次失败 + 原因),并各自带一个启停开关——某个端点临时不可用就单独关掉它,不用关掉整类事件;
|
|
159
|
+
- **签名**:自定义目标填了签名密钥时,请求带 `X-DSH-Notify-Signature`(对请求体做 HMAC-SHA256,`sha256=<hex>`);飞书填了签名密钥时按官方算法在请求体里带 `timestamp` / `sign`;
|
|
160
|
+
- **消息模板**(可选):飞书 / Server酱 / WxPusher / 自定义目标可填模板,占位符 `{{title}}` `{{body}}` `{{event}}` `{{time}}` `{{session}}`,留空默认是「标题 + 换行 + 正文」;
|
|
161
|
+
- **钉钉不在支持列表里**:钉钉群机器人接口不返回任何 CORS 头,且只接受 `application/json`——浏览器页面既读不到响应,也无法发出符合要求的请求(no-cors 下 Content-Type 会被限制成 safelisted 类型)。要用钉钉,请在自己能控制的机器上搭一个中转,再把「自定义 Webhook」指向它。
|
|
157
162
|
|
|
158
163
|
</details>
|
|
159
164
|
|
|
160
165
|
### 运行环境与诊断
|
|
161
166
|
|
|
162
|
-
在「高级设置」最后一块。默认自动识别当前是 Web 端还是桌面端(DSH Desktop,`dsh-app
|
|
167
|
+
在「高级设置」最后一块。默认自动识别当前是 Web 端还是桌面端(DSH Desktop,`dsh-app://`);识别不准时可手动指定——桌面端点通知走宿主路由做原生抬窗(见[桌面端怎么抬窗的](#桌面端怎么抬窗的))、通知上不带快捷裁决按钮;Web 端走 `window.focus()` 与 Service Worker 桥。手动覆盖与自动识别不一致时,设置页会明示检测结果。
|
|
163
168
|
|
|
164
169
|
<details>
|
|
165
170
|
<summary>通知语言、桌面端抬窗与恢复默认</summary>
|
|
166
171
|
|
|
167
172
|
- **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
|
|
168
|
-
- **桌面端怎么抬窗**:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.
|
|
173
|
+
- **桌面端怎么抬窗**:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.14.0 起由页面 POST 到宿主路由 `/__dsh-notify-me/focus-window`、宿主跑一次 PowerShell 做原生激活——窗口最小化、被别的窗口盖住都实测能抬回来(详见下一节);
|
|
169
174
|
- **恢复默认设置**:把所有开关与音效设置还原成出厂值(不动外部推送目标和已选的自定义音频文件)。
|
|
170
175
|
|
|
171
176
|
</details>
|
|
@@ -179,7 +184,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
179
184
|
- **系统通知**:经浏览器弹出的系统通知,进 Windows 通知中心或 macOS 通知中心(点击回到提醒所属的对话;支持快捷裁决的 Web 环境可点审批按钮直接裁决,桌面端没有按钮)
|
|
180
185
|
- **提示音**:默认是 WebAudio 合成音(「需要你」与「完成」使用不同音型);可在设置里为两类提醒分别换成自定义音频文件,或改成让操作系统播放它自己的通知音
|
|
181
186
|
- **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`;后台会话**完成但还没看**时出现 `✅ 回复完成 · …` / `✅ Reply finished · …`,打开对应会话(或它再次开跑)即消失。两种标记可同时出现,「需要你」在前;宿主改写标题后标记基于新标题重建
|
|
182
|
-
- **外部通知**(可选,默认关):按设置里配的推送目标,把同一条提醒转发到 Discord Webhook / Bark / Gotify / 自定义 Webhook——临时离开电脑时,手机上的推送渠道照样收到
|
|
187
|
+
- **外部通知**(可选,默认关):按设置里配的推送目标,把同一条提醒转发到 Discord Webhook / Bark / Gotify / 飞书群机器人 / Server酱 / WxPusher / 自定义 Webhook——临时离开电脑时,手机上的推送渠道照样收到
|
|
183
188
|
|
|
184
189
|
本插件是**浏览器层**实现:DSH 页面需保持打开(最小化/后台即可——那正是它监听的「离开」状态)。
|
|
185
190
|
|
|
@@ -237,8 +242,13 @@ window.__dshNotifyMe.setConfig({
|
|
|
237
242
|
externalEnabled: false, // 外部通知转发主开关(默认关:不填端点就什么都不发)
|
|
238
243
|
externalAttention: true, // 转发「需要你」(触发规则跟本地提醒完全一致)
|
|
239
244
|
externalDone: true, // 转发「回复完成」
|
|
240
|
-
|
|
245
|
+
externalRetryDelays: [2000, 6000], // 可重试失败的退避间隔(毫秒,带抖动;[] = 只发一次)
|
|
246
|
+
externalTargets: [] // 推送目标,见下方字段说明
|
|
241
247
|
})
|
|
248
|
+
// 目标的形状:{id, type, url, secret?, token?, uid?, template?, enabled?}
|
|
249
|
+
// type: 'discord' | 'bark' | 'gotify' | 'feishu' | 'serverchan' | 'wxpusher' | 'custom'
|
|
250
|
+
// token/uid 只用于 wxpusher(url 固定为官方端点);secret 用于飞书签名与自定义 HMAC 签名;
|
|
251
|
+
// template 可选,占位符 {{title}} {{body}} {{event}} {{time}} {{session}};enabled:false 单独停用该目标
|
|
242
252
|
window.__dshNotifyMe.resetConfig() // 恢复默认(同时清掉已选的自定义音效文件)
|
|
243
253
|
window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:"allowed-once" | "rejected"
|
|
244
254
|
window.__dshNotifyMe.testExternal() // 显式外发测试:向有效目标发送,返回 {sent, unverified, failed, statuses}
|
|
@@ -254,7 +264,7 @@ window.__dshNotifyMe.testExternal() // 显式外发测试:向有
|
|
|
254
264
|
|
|
255
265
|
## 工作原理
|
|
256
266
|
|
|
257
|
-
|
|
267
|
+
提醒核心监听会话状态与待处理交互;审批裁决由页面侧完成,桌面抬窗走宿主路由加 Win32 原生激活。
|
|
258
268
|
|
|
259
269
|
<details>
|
|
260
270
|
<summary>展开事件来源、审批桥接与桌面抬窗实现</summary>
|
|
@@ -276,33 +286,33 @@ window.__dshNotifyMe.testExternal() // 显式外发测试:向有
|
|
|
276
286
|
|
|
277
287
|
### 桌面端怎么抬窗的
|
|
278
288
|
|
|
279
|
-
DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的 `window.focus()` 只动浏览器窗口对象、碰不到原生窗口,壳的 `will-navigate`
|
|
289
|
+
DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的 `window.focus()` 只动浏览器窗口对象、碰不到原生窗口,壳的 `will-navigate` 又把页面里发 `dsh:` 的常规出口堵上了。1.5.2 到 1.13.0 靠一条隐藏 iframe 导航到 `dsh://open` 绕过去,借应用的单实例让位间接聚焦——在 0.2.1-rc.2 上隔离复测 3/3 无效,1.14.0 起整条删掉。
|
|
280
290
|
|
|
281
|
-
|
|
291
|
+
现在走插件自己的宿主路由。壳把 `dsh-app://app` 下除静态资源以外的所有路径原样转发给宿主 HTTP server(带 origin 校验、注入宿主 cookie),所以页面对 `/__dsh-notify-me/focus-window` 发 POST 是同源请求,没有 CORS 问题;宿主半(`lib/index.js`)本来就跑在 Electron 主进程的子进程里,`process.ppid` 就是主窗口所属的浏览器进程。点通知时:页面 POST → 宿主起一次 `powershell.exe` 跑 `lib/focus-window.ps1 -ProcessId <ppid>` → 脚本枚举该 pid 下可见、无 owner、非 toolwindow 的顶层窗口(优先类名 `Chrome_WidgetWin_1`,再按 `GetWindowRect` 面积取最大)→ 自适应激活 → 比对 `GetForegroundWindow()` 打印 `FOCUSED=1|0` → 宿主回 `200` / `202`。
|
|
282
292
|
|
|
283
|
-
|
|
293
|
+
激活顺序是实的,来自 [discussions #10446 那份五种手法的实测](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913):已经在前台就什么都不做(最常见的情况,零闪烁);最小化了先 `SW_RESTORE`;然后合成一次 ALT 按键(`keybd_event` 按下抬起)给本线程前台权限,再 `SetForegroundWindow`;这一串都不行才退到 `SW_MINIMIZE` + `SW_RESTORE` 兜底。单独调 `SetForegroundWindow` 会被前台锁直接拒,按调用算、跟 `ForegroundLockTimeout` 无关;这也重新解释了 1.5.2 那套深链「时灵时不灵」——能抬起来的那些次,多半是系统在通知被点击时自己激活了应用。
|
|
284
294
|
|
|
285
|
-
|
|
295
|
+
实测(`powershell.exe` 5.1,同一轮跑完):窗口在前台 ×3、最小化 + 记事本在前台 ×3、可见但被盖住 ×3,全部 `FOCUSED=1` 且前台窗口比对一致,最小化那几个的 `IsIconic` 都从 `true` 变回 `false`;记事本最大化完全遮挡、最小化 + 完全遮挡、`-ProcessId 0` 自动探测同样成功。脚本内部耗时最坏 434 ms,进程启动到退出最坏 782 ms,都在 8 秒上限内;`pwsh` 7.6 对照一轮行为一致,只是 `Add-Type` 编译慢约 100 ms(脚本刻意不用 7 的语法)。不存在的 pid、存在但没有可见窗口的 pid 都稳定 `FOCUSED=0` 并 `exit 0`。
|
|
286
296
|
|
|
287
|
-
|
|
297
|
+
安全上只开一条 exact 路由:非 POST 回 `405`,非桌面宿主(Web 版、macOS / Linux、纯 Node)回 `501`,缺了 `x-dsh-notify-me-focus: focus` 头回 `403`——跨源页面要带这个头得先过 preflight,而路由不放行任何 CORS 头,所以浏览器里别的站点调不动它。`GET` 只回 `{ok, route, enabled, desktop, platform}`,装完可以先用浏览器打开确认路由在不在。400 毫秒内的重复调用合并成一次,脚本超过 8 秒没回话就 kill 掉按失败处理。
|
|
288
298
|
|
|
289
|
-
|
|
299
|
+
抬不起来的场合还剩几种,都会如实回 `202`、对话照常切:目标窗口属于更高完整性级别(提权 / UAC)、窗口在另一个虚拟桌面、前台被别的进程反复抢回去。托盘隐藏(`SW_HIDE` 而非最小化)的窗口也选不到,DSH 当前不会这样。
|
|
290
300
|
|
|
291
301
|
## 已知限制
|
|
292
302
|
|
|
293
303
|
- 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
|
|
294
304
|
- 通知经由浏览器弹出,需允许浏览器通知权限,且勿扰模式不能屏蔽它。
|
|
295
|
-
- 桌面端(DSH Desktop
|
|
305
|
+
- 桌面端(DSH Desktop,Windows)点通知后窗口仍可能抬不起来:目标窗口属于更高完整性级别(提权 / UAC)、窗口在另一个虚拟桌面、前台被别的进程反复抢回去、或窗口被 `SW_HIDE` 隐藏到托盘时,宿主脚本回 `FOCUSED=0`、路由回 `202`,对话照常切(详见[桌面端怎么抬窗的](#桌面端怎么抬窗的))。
|
|
296
306
|
- 系统通知横幅只画在主屏:Windows 没有设置项、Chromium 没有 API 能把它指到别的显示器(多屏时副屏收不到系统通知)。要跨屏提醒只能靠外部推送转到手机(1.12.0 起页面里不再画审批卡片)。
|
|
297
307
|
- 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
|
|
298
308
|
- 未授权通知权限时只有提示音与标题标记。
|
|
299
|
-
- 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;**桌面端(`dsh-app://`)已实测无法注册 Service Worker
|
|
309
|
+
- 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;**桌面端(`dsh-app://`)已实测无法注册 Service Worker**(宿主未开放 `allowServiceWorkers` 特权),按钮渲染不了,审批通知退回无按钮样式。浏览器或桌面宿主不渲染通知按钮时,`window.__dshNotifyMe.debug()` 的 `bridge` 字段会说明原因,其余功能不受影响。
|
|
300
310
|
- macOS:浏览器里提醒、提示音、标题标记、点通知跳转都没有平台假设,照常工作;但 DSH Desktop(Electron)的系统通知要求应用已签名,未签名的通知直接发不出去,官方 macOS 版是否满足这一前提未实测;审批按钮在 macOS 桌面宿主上的渲染同样未实测(Windows 桌面端已实测不渲染,见上条),不渲染时自动退回无按钮通知。
|
|
301
311
|
- 配置存在浏览器 `localStorage`:换浏览器/设备或清除站点数据后会回到默认值(设置页可一键恢复默认)。
|
|
302
312
|
- `≥ 0.1.6`(含 `0.2.0-rc.2`、`0.2.1-rc.2`)的「回复完成」提醒正文只有会话名,不再附带回复摘要——宿主快照已不再提供 `nodes`;提醒本身照常触发。
|
|
303
313
|
- 标题标记由宿主与插件共同写 `document.title`:宿主重算标题时标记可能被覆盖,直到下一次提醒事件重新写成。
|
|
304
|
-
-
|
|
305
|
-
- 外部通知转发受浏览器跨域(CORS)限制:端点不放行时自动降级为「送出但不可确认」(no-cors),如实标为「已送出不可确认」;Discord
|
|
314
|
+
- 「当前对话不弹通知」按「窗口是否在前台」判定:窗口被别的应用盖住、切到别的应用、最小化都算后台,此时当前对话的等待照常提醒;窗口回到前台、或重新有键鼠输入(不再闲置)之后,再恢复静默。
|
|
315
|
+
- 外部通知转发受浏览器跨域(CORS)限制:端点不放行时自动降级为「送出但不可确认」(no-cors),如实标为「已送出不可确认」;Discord、飞书、Server酱、WxPusher 都放行跨域,可直接确认送达。钉钉不放行任何 CORS 头、又只收 `application/json`,页面发不出去,所以没有做成目标类型(自建中转 + 自定义目标才行)。明显临时的失败(网络中断、5xx、429)会按退避重试,重试仍失败进失败记录;其余错误立即停、不重试。转发在本地提醒决定要发之后才触发,所以被「当前对话不弹通知」等规则静音的提醒也不会外发。
|
|
306
316
|
|
|
307
317
|
- **设置左侧导航行图标靠 DOM 补丁**:外壳按分区 id 硬编码图标且 `settings.section` 没有图标字段,插件只能在面板渲染后把那一行的 `<svg>` 换成铃铛。命中依赖宿主当前的面板结构(`data-shortcut-modal="settings"` + 导航按钮里的标签 `<span>`);如果 DSH 改了这套结构,补丁会静默失效、显示回默认齿轮,其他功能不受影响。
|
|
308
318
|
|
|
@@ -313,6 +323,7 @@ node --check lib\client.js
|
|
|
313
323
|
node --check lib\index.js
|
|
314
324
|
node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例 + 快捷裁决桥(Service Worker 半侧一并跑)+ 闲置判定与通知自收
|
|
315
325
|
node smoke\cordis-host-test.mjs # 真 cordis 端到端(找不到本机 DSH 时自动跳过)
|
|
326
|
+
powershell -NoProfile -File lib\focus-window.ps1 -ProcessId 0 # 手动验证抬窗脚本,末行应是 FOCUSED=1
|
|
316
327
|
npm pack --dry-run # 预览发布包
|
|
317
328
|
```
|
|
318
329
|
|
|
@@ -328,8 +339,10 @@ npm pack --dry-run # 预览发布包
|
|
|
328
339
|
- [@Wh1stle05](https://github.com/Wh1stle05) — [#10](https://github.com/chromoany/dsh-notify-me/pull/10):「音效来源」功能(内置合成音 / 自定义音频文件 / 系统通知音,「需要你」与「回复完成」各自独立选),含设置页交互、回落链与测试、文档;「系统通知音」试听改发测试通知、假 React 按索引跨渲染保持 hook 状态这两处修正同样出自该 PR。
|
|
329
340
|
- [@yanzhaohui1999](https://github.com/yanzhaohui1999) — [#13](https://github.com/chromoany/dsh-notify-me/issues/13) / [#14](https://github.com/chromoany/dsh-notify-me/pull/14):482 个会话下空闲页面烧 CPU 的性能报告(定位到 `currentSessionId()` 每行重扫全表,实测占 CPU 5.9%~6.5%)与修复(一次列表变化只推导一次「当前会话」),并带先红后绿的回归断言——4 行与 40 行的列表变化代价必须相等(上游报 7 vs 43,修后 5 vs 5)。
|
|
330
341
|
- [@RiZhiZhaoYi](https://github.com/RiZhiZhaoYi) — [#12](https://github.com/chromoany/dsh-notify-me/issues/12):外部通知转发(Discord Webhook / Bark / Gotify / 自定义 Webhook)的功能建议与使用场景(1.7.0)。
|
|
342
|
+
- [@yuexian7](https://github.com/yuexian7) — [#17](https://github.com/chromoany/dsh-notify-me/issues/17):安卓端收消息的诉求——Discord 在国内不便、Bark 没有安卓端,只能自己搭后台中转;1.13.0 的飞书 / Server酱 / WxPusher 三种目标、自动重试与失败记录都是据此做的。
|
|
331
343
|
- [@shrekcg](https://github.com/shrekcg) — [#16](https://github.com/chromoany/dsh-notify-me/pull/16):把「人在前台但离开了」并入离开判定(`idleMinutes`,默认 3 分钟)、「回复完成」按哪个对话完成决定是否提醒(`donePolicy` 三选一)、通知自动消失的三档策略(`autoDismiss`),并带上对应冒烟用例与旧配置迁移;1.12.0 落地时维护者补了闲置到期补发被压下的审批、删掉残留的页内审批卡片、补齐文档与版本。
|
|
332
344
|
|
|
345
|
+
- [@xinming-chenniang](https://github.com/xinming-chenniang) — [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446#discussioncomment-19143913):五种 Win32 抬窗手法的实测对照(单独 `SetForegroundWindow` 被前台锁拒、合成 ALT 键后通过、最小化→恢复两种场景都有效),以及「宿主注册同源路由 + PowerShell 兜底链」这条绕法的可运行验证;1.14.0 的宿主路由与 `lib/focus-window.ps1` 的直接依据。
|
|
333
346
|
署名口径:本仓库收编外部 PR 时以维护者提交落地——不沿用原 commit、不挂 Co-authored-by,GitHub 的 contributors 图只显示维护者,贡献者以上面的致谢署名。
|
|
334
347
|
|
|
335
348
|
## License
|