dsh-notify-me 1.9.4 → 1.10.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 CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to **dsh-notify-me** are documented here.
4
4
 
5
+ ## [1.10.0] — 2026-10-21
6
+
7
+ ### 新增
8
+
9
+ - **提醒方式改成二选一**:「系统通知」和「页面浮层」从两个独立开关变成一组单选。两个都开时同一条提醒会弹两遍、浮层还会被系统横幅压住,现在选一个另一个自动关掉。老配置里开着浮层的,升级后自动落到浮层(`readConfig` 迁移,`overlay`/`toast` 两个旧字段继续作为镜像同步,控制台 `setConfig({ overlay: true })` 照旧生效)。
10
+ - 设置页里这两个选项改成行首带单选的列表,读起来是一组互斥选项,而不是两个可以同时开的开关。
11
+
12
+ ### 变更
13
+
14
+ - **停留时间收紧**:普通系统通知 10 秒自动收(带按钮的 20 秒),原来统一 15 秒;浮层卡片「需要你」12 秒、「回复完成」6 秒、带按钮的审批卡片 18 秒——原来带按钮的卡永不自动消失,留着占屏。
15
+ - 说明文案同步:系统通知那行直说「桌面端这类通知没有按钮」,浮层那行直说「与上面二选一」;`bridgeOffDesktop` 改成指路——桌面端要按钮就切到浮层,并点明宿主缺 `allowServiceWorkers`。
16
+ - `api.debug()` 新增 `channel` 字段。
17
+
18
+ ### 测试
19
+
20
+ - 冒烟:两个单选互斥的持久化、老配置迁移、切频道时图层清理、两种渠道各自只发一条、带按钮卡片 18 秒自收(用可控 `setTimeout` 捕获定时器)。
21
+ - `npm test` 两套件(`smoke-test.cjs` / `cordis-host-test.mjs`)全过。
22
+
23
+ ## [1.9.5] — 2026-10-20
24
+
25
+ ### 说明
26
+
27
+ - **修正 1.9.4 的口径过重**:实测补充了「有时能上前台」的反馈后重测——1.9.4 说的「0.2.1-rc.2 上失效」不准确,实际是**不稳定**:有时窗口会上前,有时只切对话。
28
+ - 复测方法:把 `dsh://open` 深链单独拿出来,三种走法(直接带参启动 exe、走系统协议激活、`WM_SYSCOMMAND` 真实最小化后再发)各 3 次,**全部失败**——带参第二实例每次约 130 ms 后退出码 1(不带参数约 260 ms 退出码 0,正常单实例让位),最小化窗口的 `IsIconic` 每次保持 `true`。⇒ 能抬起来的那些次大概率是系统/Chromium 在通知点击时自己激活了窗口,不是深链。
29
+ - README(中英)与设置页提示改为「不稳定」,并写明隔离复测结论;上游 [discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446) 待补充这条「间歇」信息。
30
+
31
+ ### 测试
32
+
33
+ - `npm test` 两套件通过(`ALL SMOKE TESTS PASSED ✔` / `cordis-host-test passed`)。
34
+
5
35
  ## [1.9.4] — 2026-10-20
6
36
 
7
37
  ### 说明
package/README.en.md CHANGED
@@ -13,7 +13,7 @@
13
13
  [![npm total downloads](https://img.shields.io/npm/dt/dsh-notify-me?style=flat-square&label=total%20downloads&color=1F883D)](https://www.npmjs.com/package/dsh-notify-me)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
15
15
  [![DSH Web and Desktop](https://img.shields.io/badge/DSH-Web%20%7C%20Desktop-blue?style=flat-square)](#installation)
16
- [![Client bundle size](https://img.shields.io/badge/client-%7E166%20KiB-green?style=flat-square)](lib/client.js)
16
+ [![Client bundle size](https://img.shields.io/badge/client-%7E168%20KiB-green?style=flat-square)](lib/client.js)
17
17
  [![DSH Compatibility](https://img.shields.io/badge/DSH-tested%200.2.1--rc.2-6366f1.svg?style=flat-square)](#compatibility)
18
18
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
19
19
 
@@ -26,7 +26,7 @@ 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.
28
28
  - **When you leave your computer:** optionally forward alerts to Discord, Bark, Gotify, or your own webhook.
29
- - **When you do not watch the primary display:** an optional in-page overlay follows the DSH window onto whichever screen you are looking at (system notifications only ever appear on the primary display).
29
+ - **When you do not watch the primary display:** switch the alert channel to the in-page overlay — it follows the DSH window onto whichever screen you are looking at (system notifications only ever appear on the primary display).
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 capable Web environment (such as `http://127.0.0.1` or `https`); Desktop notifications have no buttons — turn on the in-page overlay to decide from there instead (see [Secondary displays and the overlay](#secondary-displays-and-the-overlay)).
32
32
 
@@ -47,7 +47,7 @@ A notification plugin for DSH Web / Desktop, so you can keep full access off wit
47
47
 
48
48
  ![The Notify me settings page: header, how you get told, when to alert, try it, and the collapsed Advanced block](https://raw.githubusercontent.com/chromoany/dsh-notify-me/main/docs/screenshots/settings-en.png)
49
49
 
50
- The real settings component (v1.9.2 defaults) rendered as the content column in a standalone demo shell. Row explanations now live in a hover tooltip that appears when the pointer rests on a title, which is why only the titles show here.
50
+ The real settings component (v1.10.0 defaults) rendered in a standalone demo shell. The alert channel is a single choice, and row explanations live in a hover tooltip that appears when the pointer rests on a title, which is why only the titles show here.
51
51
  </details>
52
52
 
53
53
  ## Installation
@@ -121,10 +121,11 @@ Open **Settings → Notify me**. The page runs as how you get told, when to aler
121
121
  ### How you get told
122
122
 
123
123
  - **Enable reminders**: master switch — off means no toasts, no sounds, and the tab title is left alone;
124
- - **System notifications** (on by default): native toasts through the browser, landing in the Windows / macOS notification center;
125
- - **In-page overlay** (off by default): cards drawn in the page corner, on whichever display the window sits on — system notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. Runs alongside toasts; pick one of four corners when expanded. See [Secondary displays and the overlay](#secondary-displays-and-the-overlay);
124
+ - **How you get told (pick one)**: system notifications or the in-page overlay — a single choice, so turning one on turns the other off;
125
+ - **System notifications** (default): native toasts landing in the Windows / macOS notification center; needs notification permission;
126
+ - **In-page overlay**: cards drawn in the page corner, on whichever display the window sits on — system notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. Pick one of four corners when it is the chosen channel. See [Secondary displays and the overlay](#secondary-displays-and-the-overlay);
126
127
  - **Sound / Volume**: master cue switch and a volume slider; what it plays comes from Advanced → Alert sound;
127
- - **Decide approvals from the toast** (on by default): approval toasts carry Approve / Reject that settle the request in place. **Desktop notifications have no buttons** (`dsh-app://` cannot register a Service Worker) — leave the overlay on instead; the page states plainly which channel can carry buttons right now.
128
+ - **Decide approvals from the toast** (on by default): approval toasts carry Approve / Reject that settle the request in place. **Desktop notifications have no buttons** (`dsh-app://` cannot register a Service Worker) — pick the in-page overlay as the channel instead; the page states plainly which channel can carry buttons right now.
128
129
 
129
130
  ### Secondary displays and the overlay
130
131
 
@@ -135,8 +136,8 @@ The **in-page overlay** skips the notification center and draws the card inside
135
136
  - **Position** — top right / bottom right / top left / bottom left, default bottom right; changes apply immediately;
136
137
  - **No notification permission needed** — it works when permission was never granted, and on the Desktop shell where buttonless toasts are all the OS channel can do;
137
138
  - **Approval cards carry Approve / Reject** — the same decision as Allow once / Reject on the approval card, and they work on the Desktop too (no Service Worker involved). Clicking the card body opens its conversation;
138
- - **Lifetime** — "needs you" cards retire after 20s, "reply finished" after 8s; an approval card with buttons stays until it is decided or closed. At most 4 cards stack at once;
139
- - **Relation to system notifications** — the two switches are independent. With both on, the primary display gets toasts and the second screen gets the overlay; the overlay alone is also a valid setup.
139
+ - **Lifetime** — "needs you" cards retire after 12s, "reply finished" after 6s, and an approval card with buttons after 18s; a card or its close button retires one earlier. At most 4 cards stack at once;
140
+ - **Relation to system notifications** — it is one or the other. Both used to be switchable at once, which meant every alert arrived twice and the pair covered each other; 1.10.0 makes it a single choice, and an old config with the overlay on migrates to it.
140
141
 
141
142
  The overlay only knows which screen the window is on, not which screen you are looking at. If the window stays on the primary display while you watch another one, it stays invisible — same as a toast.
142
143
 
@@ -146,7 +147,7 @@ The overlay only knows which screen the window is on, not which screen you are l
146
147
  - **Stay quiet for the conversation on screen** (on): a wait belonging to the conversation you are looking at keeps only the tab marker so the approval card stays readable; backgrounding restores alerts;
147
148
  - **Also alert "reply finished" while the page is open** (off): by default only background finishes alert;
148
149
  - **Ignore subagent sessions** (on): skip background sessions spawned by a subagent — the parent conversation covers them; the one you have open still alerts;
149
- - **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 whether the native window comes forward depends on the shell version (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)); with several tabs open only one is raised, and off means a click does nothing.
150
+ - **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 it is flaky — sometimes the window comes forward, sometimes only the conversation switches (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)); with several tabs open only one is raised, and off means a click does nothing.
150
151
 
151
152
  ### Try it
152
153
 
@@ -182,7 +183,7 @@ Off by default. Supports Discord Webhook / Bark / Gotify / custom webhooks. Aler
182
183
 
183
184
  ### Runtime & diagnostics
184
185
 
185
- 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 fire the `dsh://` deep link (whether that actually raises the window depends on the shell version — 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.
186
+ 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 fire the `dsh://` deep link (the effect is unreliable — 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.
186
187
 
187
188
  <details>
188
189
  <summary>Alert language, Desktop window raising, reset</summary>
@@ -200,7 +201,7 @@ The last block inside Advanced. The page protocol is detected automatically to t
200
201
  ## How it alerts
201
202
 
202
203
  - **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)
203
- - **In-page overlay** (optional, default off) — an alert card drawn in the corner of the DSH page, on whatever display the window is on, optionally with Approve / Reject buttons; see [Secondary displays and the overlay](#secondary-displays-and-the-overlay)
204
+ - **In-page overlay** — an alert card drawn in the corner of the DSH page, on whatever display the window is on, optionally with Approve / Reject buttons; see [Secondary displays and the overlay](#secondary-displays-and-the-overlay)
204
205
  - **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
205
206
  - **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
206
207
  - **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
@@ -250,14 +251,14 @@ window.__dshNotifyMe.setConfig({
250
251
  currentHiddenOnly: true, // true = waits in the conversation on screen keep only the tab marker (replayed once backgrounded)
251
252
  doneHiddenOnly: true, // false = also alert "reply finished" while visible
252
253
  ignoreSubagent: true, // true = never alert for background subagent sessions (the open one is exempt)
253
- toast: true, // system-notification toggle
254
+ channel: "toast", // alert channel: 'toast' system notification | 'overlay' in-page card, pick one
254
255
  sound: true, // cue toggle (also governs the "system notification sound" mode)
255
256
  soundDone: "synth", // "reply finished" cue source: 'synth' built-in | 'custom' picked file | 'system' OS sound
256
257
  soundAttention: "synth", // "needs you" cue source, same values
257
258
  volume: 0.5, // 0..1
258
259
  autoFocus: true, // clicking a toast opens the conversation it came from
259
260
  quickActions: true, // Approve / Reject buttons on approval toasts
260
- overlay: false, // in-page overlay alert (follows the DSH window to its display; no notification permission needed)
261
+ overlay: false, // legacy mirror kept in sync with channel (old configs migrate through it)
261
262
  overlayCorner: "bottom-right", // overlay anchor: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'
262
263
  externalEnabled: false, // external-notification forwarding master switch (default off: no endpoint, nothing is sent)
263
264
  externalAttention: true, // forward "needs you" (exactly the same trigger rules as local alerts)
@@ -305,7 +306,11 @@ DSH Desktop is a thin Electron shell and its page lives under `dsh-app://app`: D
305
306
 
306
307
  Verified on the real shell (DSH Desktop 0.2.0-rc.2, three rounds of retesting in issue #9): a toast click raises the window and switches the conversation in one step, stable across sessions and with the window in the background. Timing of the fire is what makes the chain work: the deep link must be launched from a **user-gesture handler** — the notification's `onclick` carries one, which is why the product path works — while gesture-less scripted injection (an iframe appended from a CDP evaluate, say) is silently dropped by Chromium and the external protocol never launches, which looks exactly like the channel being blocked.
307
308
 
308
- **Regression on 0.2.1-rc.2 / Windows** (measured 2026-10-20): the toast click handler still runs (`navigator.userActivation.isActive === true`, `visibilityState` is `hidden`), but the window no longer comes forward. Shell-side evidence: launching a second instance with the `dsh://open` argument exits with code 1 after 133 ms, while launching it without arguments exits with code 0 after 258 ms (the normal single-instance yield), so the URL branch is what breaks. Minimizing the window for real via `WM_SYSCOMMAND` and then firing the deep link — or relaunching with no arguments — leaves `IsIconic` at `true`, so `focusPrimaryWindow()`'s `restore()` never takes effect. There is no page-side fallback either: the shell preload only exposes `dshDesktop`, the directory picker and locale, and `DESKTOP_IPC` has no focus/raise channel. The repro and code locations are filed upstream in [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446). The plugin's part ends there: it still fires the deep link; raising the native window is the shell's call.
309
+ **Flaky on 0.2.1-rc.2 / Windows** (measured 2026-10-20): the toast click handler still runs (`navigator.userActivation.isActive === true`, `visibilityState` is `hidden`), but whether the window actually comes forward is inconsistent — sometimes it does, sometimes only the conversation switches while the window stays put.
310
+
311
+ Do not count on the plugin's own path: isolating the `dsh://open` deep link (three ways, three runs each — launching the exe with the argument, OS protocol activation, and a real `WM_SYSCOMMAND` minimize followed by the deep link) failed **every time**. The second instance with the argument exits with code 1 after ~130 ms (without arguments it exits with code 0 after ~260 ms, the normal single-instance yield), and after a real minimize `IsIconic` stays `true`. So the times it does come forward are most likely the OS/Chromium activating the app on the notification click itself, not the deep link.
312
+
313
+ There is no page-side fallback either: the shell preload only exposes `dshDesktop`, the directory picker and locale, `DESKTOP_IPC` has no focus/raise channel, and the main window's `will-navigate` rejects `dsh:` URLs outright. The repro and code locations are filed upstream in [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446).
309
314
 
310
315
  </details>
311
316
 
@@ -315,7 +320,7 @@ Verified on the real shell (DSH Desktop 0.2.0-rc.2, three rounds of retesting in
315
320
  - 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.
316
321
  - The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
317
322
  - Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
318
- - On the Desktop shell (DSH Desktop 0.2.1-rc.2 / Windows) a toast click no longer brings the window forward by itself: the plugin fires the `dsh://open` deep link, but the shell does not raise the window ([discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446)). Switching to the conversation still works.
323
+ - On the Desktop shell (DSH Desktop 0.2.1-rc.2 / Windows) a toast click does not reliably bring the window forward: sometimes it does, sometimes only the conversation switches. The plugin's `dsh://open` deep link on its own failed 3/3 in isolation (see [How the Desktop window gets raised](#how-the-desktop-window-gets-raised)), so when it does come forward it is most likely the OS/Chromium activating the app on the notification click itself ([discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446)).
319
324
  - 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. Use the in-page overlay for that — it follows the DSH window to its display.
320
325
  - 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**, 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.
321
326
  - 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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  [![npm total downloads](https://img.shields.io/npm/dt/dsh-notify-me?style=flat-square&label=total%20downloads&color=1F883D)](https://www.npmjs.com/package/dsh-notify-me)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
15
15
  [![DSH Web and Desktop](https://img.shields.io/badge/DSH-Web%20%7C%20Desktop-blue?style=flat-square)](#安装)
16
- [![Client bundle size](https://img.shields.io/badge/client-%7E166%20KiB-green?style=flat-square)](lib/client.js)
16
+ [![Client bundle size](https://img.shields.io/badge/client-%7E168%20KiB-green?style=flat-square)](lib/client.js)
17
17
  [![DSH Compatibility](https://img.shields.io/badge/DSH-%E5%B7%B2%E9%80%82%E9%85%8D%200.2.1--rc.2-6366f1.svg?style=flat-square)](#兼容性)
18
18
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
19
19
 
@@ -26,9 +26,9 @@ DSH Web / Desktop 的通知插件,让你敢不开完整访问,也不必一
26
26
  - **需要你操作时**:审批、方案确认、提问,用通知、提示音和标题标记提醒。
27
27
  - **后台回复完成时**:及时回来查看结果,点击通知可回到对应对话。
28
28
  - **离开电脑时**:可选转发到 Discord、Bark、Gotify 或自己的 Webhook。
29
- - **不看主屏时**:可选页面内浮层提醒,跟着 DSH 窗口出现在你正看着的那块屏(系统通知只会出现在主屏)。
29
+ - **不看主屏时**:提醒方式可以切成页面内浮层,跟着 DSH 窗口出现在你正看着的那块屏(系统通知只会出现在主屏)。
30
30
 
31
- > 页面或桌面应用需保持运行,后台 / 最小化可以。系统通知需要通知权限;审批按钮在支持 Service Worker 的 Web 环境(如 `http://127.0.0.1` 或 `https`)由通知提供,桌面端的通知没有按钮——打开「页面浮层提醒」后可在浮层上直接裁决(见[副屏与浮层提醒](#副屏与浮层提醒))。
31
+ > 页面或桌面应用需保持运行,后台 / 最小化可以。系统通知需要通知权限;审批按钮在支持 Service Worker 的 Web 环境(如 `http://127.0.0.1` 或 `https`)由通知提供,桌面端的通知没有按钮——把「提醒方式」切到页面浮层「页面浮层提醒」后可在浮层上直接裁决(见[副屏与浮层提醒](#副屏与浮层提醒))。
32
32
 
33
33
  > 🔎 想找这个插件?可以搜:`dsh-notify-me`、**消息提醒**、**可操作提醒**、**桌面通知**、**需要你操作**、**回复完成提醒**、**后台完成提醒**、**外部通知**、**Discord / Bark / Gotify 推送**(npm 关键词已含以上中英文词)。
34
34
 
@@ -47,7 +47,7 @@ DSH Web / Desktop 的通知插件,让你敢不开完整访问,也不必一
47
47
 
48
48
  ![通知提醒设置页:页眉、提醒方式、什么时候提醒、试一试与折叠的高级设置](https://raw.githubusercontent.com/chromoany/dsh-notify-me/main/docs/screenshots/settings-zh.png)
49
49
 
50
- 真实插件设置组件(v1.9.2 默认配置)在独立演示环境里按内容列整页渲染;每行的说明改成鼠标停在标题文字上才出现的小方框,所以这里只看到标题。
50
+ 真实插件设置组件(v1.10.0 默认配置)在独立演示环境里渲染;提醒方式是一组单选,每行的说明在鼠标停在标题文字上时才出现,所以这里只看到标题。
51
51
  </details>
52
52
 
53
53
  ## 安装
@@ -74,7 +74,7 @@ dsh plugin --profile web add dsh-notify-me
74
74
  ## 首次验证
75
75
 
76
76
  1. 在 DSH 页面上点击一次,按提示允许通知;若此前拒绝过,在浏览器站点设置中允许通知。操作系统也需要允许对应浏览器 / DSH Desktop 显示通知。
77
- 2. 打开 **设置 → 通知提醒**,确认出现该分区及底部插件版本号,保持「启用提醒」「系统通知」「提示音」开启。
77
+ 2. 打开 **设置 → 通知提醒**,确认出现该分区及底部插件版本号,保持「启用提醒」「提示音」开启,提醒方式先留「系统通知」。
78
78
  3. 点击设置页的「测试『回复完成』」按钮:应出现一条测试通知,并按当前音效设置播放声音。这个本地测试不会转发到外部通知端点,也不受「页面打开时也提醒」开关限制。
79
79
 
80
80
  Web 用户可再测试审批按钮;桌面端该测试只发普通通知,**没有「同意 / 拒绝」按钮属于预期行为**。
@@ -117,8 +117,9 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
117
117
  ### 提醒方式
118
118
 
119
119
  - **启用提醒** 总开关:关闭后不弹通知、不响提示音,也不改标签页标题;
120
- - **系统通知**(默认开):经浏览器弹系统通知,进 Windows / macOS 通知中心;
121
- - **页面浮层提醒**(默认关):在 DSH 页面角落画提醒卡片,窗口在哪块屏它就在哪块屏——系统通知只会画在主屏,主屏不看时用它;不需要通知权限,桌面端也能在卡片上直接「同意 / 拒绝」,可与系统通知同开。展开后可选四个角落的位置,见[副屏与浮层提醒](#副屏与浮层提醒);
120
+ - **提醒方式(二选一)**:系统通知 / 页面浮层,单选,开了这个就不再弹那个;
121
+ - **系统通知**(默认):进 Windows / macOS 通知中心,需要通知权限;
122
+ - **页面浮层**:在 DSH 页面角落画提醒卡片,窗口在哪块屏它就在哪块屏——系统通知只会画在主屏,主屏不看时用它;不需要通知权限,桌面端也能在卡片上直接「同意 / 拒绝」。选中后可选四个角落的位置,见[副屏与浮层提醒](#副屏与浮层提醒);
122
123
  - **提示音 / 音量**:提示音开关与音量滑块;具体播什么由「高级设置 → 音效来源」决定;
123
124
  - **审批通知上直接裁决**(默认开):审批通知带「同意 / 拒绝」,点一下即完成裁决,不用切回页面。**桌面端的通知没有按钮**(`dsh-app://` 页面无法注册 Service Worker),需要按钮就打开「页面浮层提醒」,改在浮层上裁决;设置页会如实说明当前哪种渠道能给按钮。
124
125
 
@@ -131,8 +132,8 @@ Windows 的系统通知横幅固定画在主屏上,系统没有设置项、浏
131
132
  - **位置**:设置里选右上 / 右下 / 左上 / 左下,默认右下;改完立刻生效;
132
133
  - **不需要通知权限**:通知权限没给、或桌面端弹不出带按钮的通知时,浮层照常工作;
133
134
  - **审批卡片带「同意 / 拒绝」**:与审批卡片的「允许一次 / 拒绝」等价,桌面端也能用(不依赖 Service Worker);点卡片正文则切回对应对话;
134
- - **停留时间**:「需要你」20 秒后自动收起,「回复完成」8 秒;带按钮的审批卡片不自动收,直到裁决或手动关掉。最多同时叠 4 张;
135
- - **与系统通知的关系**:两个开关互不替代。都开着时主屏看系统通知、副屏看浮层,也可以只留浮层。
135
+ - **停留时间**:「需要你」12 秒,「回复完成」6 秒,带按钮的审批卡片 18 秒,都会自动收起;也可以点卡片或右上角叉提前关掉。最多同时叠 4 张;
136
+ - **与系统通知的关系**:二选一。以前两个开关可以同开,结果同一条提醒弹两遍、还互相挡住,1.10.0 起改成单选。老配置里开着浮层的,升级后自动落到浮层。
136
137
 
137
138
  浮层只知道窗口在哪块屏,不知道你人在看哪块屏。窗口留在主屏、人却盯着副屏时,它同样看不见。
138
139
 
@@ -142,7 +143,7 @@ Windows 的系统通知横幅固定画在主屏上,系统没有设置项、浏
142
143
  - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标签页标记,免得挡住审批卡片;切到后台后照常提醒,若你已处理掉就作废;
143
144
  - **页面打开时也提醒「回复完成」**(默认关):默认只在切到后台时提醒,避免打扰;
144
145
  - **子代理会话不提醒**(默认开):跳过后台子代理(subagent)会话——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;
145
- - **点击通知回到对应对话**(默认开):点通知切到提醒所属的对话;浏览器里同时把窗口带到前台,桌面端能不能上前台看壳的版本(见[桌面端怎么抬窗的](#桌面端怎么抬窗的));多个标签页同时开着时只调起一个(优先当前可见的那个);关掉后点通知不做任何事。
146
+ - **点击通知回到对应对话**(默认开):点通知切到提醒所属的对话;浏览器里同时把窗口带到前台;桌面端不稳定——有时窗口会上前,有时只切对话(见[桌面端怎么抬窗的](#桌面端怎么抬窗的));多个标签页同时开着时只调起一个(优先当前可见的那个);关掉后点通知不做任何事。
146
147
 
147
148
  ### 试一试
148
149
 
@@ -178,13 +179,13 @@ Windows 的系统通知横幅固定画在主屏上,系统没有设置项、浏
178
179
 
179
180
  ### 运行环境与诊断
180
181
 
181
- 在「高级设置」最后一块。默认自动识别当前是 Web 端还是桌面端(DSH Desktop,`dsh-app://`);识别不准时可手动指定——桌面端点通知走 `dsh://` 深链调窗(能否真的上前台看壳的版本,见[桌面端怎么抬窗的](#桌面端怎么抬窗的))、通知上不带快捷裁决按钮;Web 端走 `window.focus()` 与 Service Worker 桥。手动覆盖与自动识别不一致时,设置页会明示检测结果。
182
+ 在「高级设置」最后一块。默认自动识别当前是 Web 端还是桌面端(DSH Desktop,`dsh-app://`);识别不准时可手动指定——桌面端点通知走 `dsh://` 深链调窗(效果不稳定,见[桌面端怎么抬窗的](#桌面端怎么抬窗的))、通知上不带快捷裁决按钮;Web 端走 `window.focus()` 与 Service Worker 桥。手动覆盖与自动识别不一致时,设置页会明示检测结果。
182
183
 
183
184
  <details>
184
185
  <summary>通知语言、桌面端抬窗与恢复默认</summary>
185
186
 
186
187
  - **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
187
- - **桌面端怎么抬窗**:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.5.2 起改走应用自己的 `dsh://open` 深链调窗——在 DSH Desktop 0.2.0-rc.2 上实测有效,0.2.1-rc.2 在 Windows 上失效(详见下一节);
188
+ - **桌面端怎么抬窗**:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.5.2 起改走应用自己的 `dsh://open` 深链调窗——在 DSH Desktop 0.2.0-rc.2 上实测有效,0.2.1-rc.2 在 Windows 上不稳定(详见下一节);
188
189
  - **恢复默认设置**:把所有开关与音效设置还原成出厂值(不动外部推送目标和已选的自定义音频文件)。
189
190
 
190
191
  </details>
@@ -196,7 +197,9 @@ Windows 的系统通知横幅固定画在主屏上,系统没有设置项、浏
196
197
  ## 五种提醒渠道
197
198
 
198
199
  - **系统通知**:经浏览器弹出的系统通知,进 Windows 通知中心或 macOS 通知中心(点击回到提醒所属的对话;支持快捷裁决的 Web 环境可点审批按钮直接裁决,桌面端无按钮)
199
- - **页面浮层**(可选,默认关):画在 DSH 页面角落的提醒卡片,落在窗口所在的那块屏,可带「同意 / 拒绝」按钮;见[副屏与浮层提醒](#副屏与浮层提醒)
200
+ - **页面浮层**:画在 DSH 页面角落的提醒卡片,落在窗口所在的那块屏,可带「同意 / 拒绝」按钮;见[副屏与浮层提醒](#副屏与浮层提醒)
201
+
202
+ 两种渠道在设置里二选一,不会同时出现。
200
203
  - **提示音**:默认是 WebAudio 合成音(「需要你」与「完成」使用不同音型);可在设置里为两类提醒分别换成自定义音频文件,或改成让操作系统播放它自己的通知音
201
204
  - **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`;后台会话**完成但还没看**时出现 `✅ 回复完成 · …` / `✅ Reply finished · …`,打开对应会话(或它再次开跑)即消失。两种标记可同时出现,「需要你」在前;宿主改写标题后标记基于新标题重建
202
205
  - **外部通知**(可选,默认关):按设置里配的推送目标,把同一条提醒转发到 Discord Webhook / Bark / Gotify / 自定义 Webhook——临时离开电脑时,手机上的推送渠道照样收到
@@ -246,14 +249,14 @@ window.__dshNotifyMe.setConfig({
246
249
  currentHiddenOnly: true, // true = 当前对话的等待只留标题标记(转后台补发)
247
250
  doneHiddenOnly: true, // false = 页面可见时「完成」也提醒
248
251
  ignoreSubagent: true, // true = 后台子代理(subagent)会话不提醒(当前打开的照常)
249
- toast: true, // 系统通知开关
252
+ channel: "toast", // 提醒方式:'toast' 系统通知 | 'overlay' 页面浮层,二选一
250
253
  sound: true, // 提示音开关(含「系统通知音」模式)
251
254
  soundDone: "synth", // 「完成」音效来源:'synth' 内置 | 'custom' 自定义文件 | 'system' 系统通知音
252
255
  soundAttention: "synth", // 「需要你」音效来源,取值同上
253
256
  volume: 0.5, // 音量 0~1
254
257
  autoFocus: true, // 点通知回到提醒所属的对话
255
258
  quickActions: true, // 审批通知上的「同意 / 拒绝」按钮
256
- overlay: false, // 页面浮层提醒(跟着 DSH 窗口落在哪块屏;不需要通知权限)
259
+ overlay: false, // 兼容字段,跟随 channel 同步(老配置靠它迁移)
257
260
  overlayCorner: "bottom-right", // 浮层位置:'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'
258
261
  externalEnabled: false, // 外部通知转发主开关(默认关:不填端点就什么都不发)
259
262
  externalAttention: true, // 转发「需要你」(触发规则跟本地提醒完全一致)
@@ -301,7 +304,11 @@ DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的
301
304
 
302
305
  真实壳已实测(DSH Desktop 0.2.0-rc.2,issue #9 三轮复测):点通知 = 抬窗 + 切会话一步到位,跨会话、窗口在后台时同样稳定。触发时机是这条链路的关键:深链必须在**带用户手势的处理器**里发——通知的 `onclick` 自带手势,所以产品路径通;而无手势的脚本化注入(如 CDP evaluate 手工塞 iframe)会被 Chromium 静默丢弃、外部协议根本不回流,复现时看起来就像「通道被拦死」。
303
306
 
304
- **0.2.1-rc.2 在 Windows 上失效**(2026-10-20 实测):点通知的回调照常执行(`navigator.userActivation.isActive === true`、`visibilityState` 为 `hidden`),但窗口不再上前台。壳侧可复现的证据:带 `dsh://open` 参数启动第二实例,133 ms 后以退出码 1 结束;不带参数时 258 ms 退出码 0(正常的单实例让位),说明问题出在 URL 参数那条分支。把窗口用 `WM_SYSCOMMAND` 真实最小化后,无论发深链还是无参重启,`IsIconic` 都保持 `true`,`focusPrimaryWindow()` 的 `restore()` 没有实际生效。页面侧也没有替代通道:壳的 preload 只暴露 `dshDesktop`、目录选择、本地化这些,`DESKTOP_IPC` 里没有 focus/raise 类通道。已把复现步骤与代码位置提交到 [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446)。插件侧能做的到此为止:深链照发,原生窗口抬不抬由壳决定。
307
+ **0.2.1-rc.2 在 Windows 上不稳定**(2026-10-20):点通知的回调照常执行(`navigator.userActivation.isActive === true`、`visibilityState` 为 `hidden`),但窗口是否上前台时灵时不灵——有时会,有时只切对话、窗口不动。
308
+
309
+ 插件这一侧为什么不能指望:把 `dsh://open` 深链单独拿出来复测(三种走法各 3 次:直接带参启动 exe、走系统协议激活、`WM_SYSCOMMAND` 真实最小化后再发),**全部失败**——带参第二实例每次都在约 130 ms 后以退出码 1 结束(不带参数则约 260 ms 退出码 0,属正常单实例让位),最小化窗口的 `IsIconic` 每次都保持 `true`。也就是说,能抬起来的那些次,多半是系统/Chromium 在通知被点击时自己激活了应用窗口,不是插件发的深链起了作用。
310
+
311
+ 页面侧也没有替代通道:壳的 preload 只暴露 `dshDesktop`、目录选择、本地化这些,`DESKTOP_IPC` 里没有 focus/raise 类通道;主窗口的 `will-navigate` 对 `dsh:` 一律 `preventDefault()`。复现步骤与代码位置已提交到 [deepseek-harness discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446)。
305
312
 
306
313
  </details>
307
314
 
@@ -309,7 +316,7 @@ DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的
309
316
 
310
317
  - 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
311
318
  - 通知经由浏览器弹出,需允许浏览器通知权限,且勿扰模式不能屏蔽它。
312
- - 桌面端(DSH Desktop 0.2.1-rc.2 / Windows)点通知不再自动把窗口带到前台:插件能发 `dsh://open` 深链,壳侧却没把窗口抬起来([discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446));切到对应对话不受影响。
319
+ - 桌面端(DSH Desktop 0.2.1-rc.2 / Windows)点通知后窗口不一定上前台:有时会,有时只切对话。插件发的 `dsh://open` 深链单独复测 3/3 无效(详见[桌面端怎么抬窗的](#桌面端怎么抬窗的)),能抬的时候多半是系统/Chromium 在通知激活时自己把窗口带起来了([discussions #10446](https://github.com/deepseek-ai/deepseek-harness/discussions/10446))。
313
320
  - 系统通知横幅只画在主屏:Windows 没有设置项、Chromium 没有 API 能把它指到别的显示器(多屏时副屏收不到系统通知)。要用副屏提醒,开「页面浮层提醒」,它会跟着 DSH 窗口走。
314
321
  - 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
315
322
  - 未授权通知权限时只有提示音与标题标记。
package/lib/client.js CHANGED
@@ -256,15 +256,19 @@
256
256
  // through the Service Worker half of this same file; where that cannot
257
257
  // run, the toast simply falls back to its plain form.
258
258
  quickActions: true,
259
- // Where the alert is drawn. The OS toast always lands on the primary
260
- // display — Windows offers no setting (and Chromium no API) to aim a
261
- // banner at another monitor — so a user whose DSH window lives on a
262
- // second screen never sees one. This option draws the same alert inside
263
- // the page instead: it appears on whatever screen the window is on, needs
264
- // no notification permission, and can carry the approval buttons even on
265
- // the desktop shell (whose dsh-app:// page cannot run the Service Worker
266
- // that OS action buttons need). Independent of `toast`; neither replaces
267
- // the other, so "both" is a valid setup.
259
+ // The alert channel: exactly one of them, never both.
260
+ // 'toast' — the OS notification center. Always drawn on the primary
261
+ // display (Windows offers no setting and Chromium no API to
262
+ // aim a banner at another monitor), needs notification
263
+ // permission, and cannot carry action buttons on the desktop
264
+ // shell.
265
+ // 'overlay' — a card inside the page, so it follows the DSH window to
266
+ // whatever display it sits on, needs no permission, and can
267
+ // carry the approval buttons even on the desktop shell.
268
+ channel: "toast",
269
+ // Legacy mirrors of `channel`, kept in sync on every read/write so that a
270
+ // config written by an older build, or a console poke at one of them,
271
+ // still means what it used to.
268
272
  overlay: false,
269
273
  // Which corner of this window the overlay cards stack in. The screen
270
274
  // follows the window — the page cannot aim it anywhere else either.
@@ -285,12 +289,31 @@
285
289
  var raw = localStorage.getItem(LS_CONFIG);
286
290
  if (raw) cfg = JSON.parse(raw);
287
291
  } catch (e) { /* corrupt storage -> defaults */ }
292
+ // Older builds stored two independent switches; the channel is now a
293
+ // single choice, and a config that had the overlay on keeps using it.
294
+ // Checked before the defaults fill in `channel`, or the fresh default
295
+ // would shadow the legacy keys.
296
+ if (typeof cfg.channel !== "string") {
297
+ cfg.channel = cfg.overlay === true ? "overlay" : "toast";
298
+ }
288
299
  for (var k in DEFAULTS) if (!(k in cfg)) cfg[k] = DEFAULTS[k];
300
+
301
+ cfg.toast = cfg.channel === "toast";
302
+ cfg.overlay = cfg.channel === "overlay";
289
303
  return cfg;
290
304
  }
291
305
  function writeConfig(patch) {
292
306
  var cfg = readConfig();
293
307
  for (var k in patch) cfg[k] = patch[k];
308
+ if (!("channel" in patch)) {
309
+ if (patch.overlay === true) cfg.channel = "overlay";
310
+ else if (patch.overlay === false && cfg.channel === "overlay") cfg.channel = "toast";
311
+ if (patch.toast === true) cfg.channel = "toast";
312
+ else if (patch.toast === false && cfg.channel === "toast") cfg.channel = "overlay";
313
+ }
314
+ cfg.channel = cfg.channel === "overlay" ? "overlay" : "toast";
315
+ cfg.toast = cfg.channel === "toast";
316
+ cfg.overlay = cfg.channel === "overlay";
294
317
  try { localStorage.setItem(LS_CONFIG, JSON.stringify(cfg)); } catch (e) {}
295
318
  return cfg;
296
319
  }
@@ -303,7 +326,7 @@
303
326
  // Overlay re-apply: switching it off takes the cards down at once and a
304
327
  // new corner moves the layer without waiting for the next alert.
305
328
  try {
306
- if (!config.overlay) clearOverlays();
329
+ if (config.channel !== "overlay") clearOverlays();
307
330
  else if (overlayLayer) overlayLayer.className = "dnm-ov-layer dnm-ov-" + overlayCorner();
308
331
  } catch (e) {}
309
332
  return config;
@@ -436,7 +459,7 @@
436
459
  } catch (e) {}
437
460
  }
438
461
  function canToast() {
439
- if (!config.toast) return false;
462
+ if (config.channel !== "toast") return false;
440
463
  try {
441
464
  return "Notification" in window && Notification.permission === "granted";
442
465
  } catch (e) { return false; } // a policy-disabled Notification must not break delivery
@@ -682,10 +705,11 @@
682
705
  // they are the only kind whose decision is a plain binary, and the only one
683
706
  // whose object answers answer('allowed-once' | 'rejected').
684
707
  function quickFor(key, it, sessionId) {
685
- // The overlay can settle approvals without a worker, so it counts as a
686
- // button channel on its own — the desktop shell, where OS action buttons
687
- // cannot render, gets them this way.
688
- if (!config.quickActions || !(config.toast || config.overlay)) return null;
708
+ // The buttons themselves are a rendering concern: the overlay always
709
+ // draws them, and a toast needs a live worker bridge for OS actions. The
710
+ // pending record is kept either way, so a click still settles the same
711
+ // decision (and debug().actionKeys stays honest about what is waiting).
712
+ if (!config.quickActions) return null;
689
713
  if (!it || it.kind !== "approval" || typeof it.answer !== "function") return null;
690
714
  var q = {
691
715
  key: key,
@@ -756,7 +780,9 @@
756
780
  n.close();
757
781
  };
758
782
  }
759
- setTimeout(function () { n.close(); }, 15000);
783
+ // Longer for an actionable toast so there is time to click it, shorter
784
+ // for a plain one: a notification that just sits there is noise.
785
+ setTimeout(function () { n.close(); }, quick ? 20000 : 10000);
760
786
  } catch (e) {}
761
787
  }
762
788
 
@@ -772,9 +798,10 @@
772
798
  var OVERLAY_STYLE_ID = "dsh-notify-me/overlay";
773
799
  var OVERLAY_ID = "dsh-notify-me-overlay";
774
800
  var OVERLAY_MAX_CARDS = 4;
775
- // An approval card is actionable and stays until it is answered or closed;
776
- // the other kinds fade on the toast's own schedule (a toast closes at 15s).
777
- var OVERLAY_TTL = { attention: 20000, done: 8000 };
801
+ // How long a card stays. "needs you" deserves a longer look than a finished
802
+ // reply; actionable cards get the longest window but still fade, because a
803
+ // card that never leaves is just clutter once the user has moved on.
804
+ var OVERLAY_TTL = { attention: 12000, done: 6000, action: 18000 };
778
805
  var OVERLAY_CORNERS = ["bottom-right", "bottom-left", "top-right", "top-left"];
779
806
  var OVERLAY_CSS = "" +
780
807
  ".dnm-ov-layer{position:fixed;z-index:2147483000;display:flex;flex-direction:column;gap:10px;pointer-events:none;max-width:calc(100vw - 24px)}" +
@@ -871,7 +898,7 @@
871
898
  // Same inputs as showToast: `quick` is the approval record built by
872
899
  // quickFor(), and it is what lets an overlay settle the request in place.
873
900
  function showOverlay(kind, title, body, sessionId, quick) {
874
- if (!config.overlay) return false;
901
+ if (config.channel !== "overlay") return false;
875
902
  var layer = ensureOverlayLayer();
876
903
  if (!layer) return false;
877
904
  var d = texts();
@@ -937,10 +964,10 @@
937
964
  overlayCards[id] = rec;
938
965
  var ids = Object.keys(overlayCards);
939
966
  for (var i = 0; i < ids.length - OVERLAY_MAX_CARDS; i++) dismissOverlay(ids[i]);
940
- if (!(quick && typeof quick.key === "string")) {
941
- var ttl = OVERLAY_TTL[kind === "attention" ? "attention" : "done"];
942
- if (ttl) rec.timer = setTimeout(function () { dismissOverlay(id); }, ttl);
943
- }
967
+ var ttl = (quick && typeof quick.key === "string")
968
+ ? OVERLAY_TTL.action
969
+ : OVERLAY_TTL[kind === "attention" ? "attention" : "done"];
970
+ if (ttl) rec.timer = setTimeout(function () { dismissOverlay(id); }, ttl);
944
971
  return true;
945
972
  } catch (e) { return false; }
946
973
  }
@@ -1937,7 +1964,7 @@
1937
1964
  });
1938
1965
  } catch (e) {}
1939
1966
  // keep in sync with package.json "version" on every release
1940
- api.version = "1.9.4";
1967
+ api.version = "1.10.0";
1941
1968
  api.setConfig = function (patch) { return setConfigLocal(patch); };
1942
1969
  api.resetConfig = function () {
1943
1970
  try {
@@ -1998,6 +2025,7 @@
1998
2025
  currentHiddenOnly: config.currentHiddenOnly,
1999
2026
  doneHiddenOnly: config.doneHiddenOnly,
2000
2027
  ignoreSubagent: config.ignoreSubagent,
2028
+ channel: config.channel,
2001
2029
  toast: config.toast,
2002
2030
  sound: config.sound,
2003
2031
  soundDone: soundMode("done"),
@@ -2073,7 +2101,7 @@
2073
2101
  enable: "启用提醒",
2074
2102
  enableHint: "关闭后不弹通知、不响提示音,也不改标签页标题。",
2075
2103
  hostEnv: "运行环境",
2076
- hostEnvHint: "默认自动识别:页面协议是 dsh-app:// 即桌面端(DSH Desktop),其余为 Web 端。识别不准时手动指定:桌面端走 dsh:// 深链调窗(能否真的上前台看壳的版本)、快捷裁决按钮不可用;Web 端走 window.focus() 与 Service Worker 桥。",
2104
+ hostEnvHint: "默认自动识别:页面协议是 dsh-app:// 即桌面端(DSH Desktop),其余为 Web 端。识别不准时手动指定:桌面端走 dsh:// 深链调窗(效果不稳定),系统通知不带按钮、浮层才带;Web 端走 window.focus() 与 Service Worker 桥。",
2077
2105
  hostEnvAuto: "自动识别(推荐)",
2078
2106
  hostEnvDesktop: "桌面端(DSH Desktop)",
2079
2107
  hostEnvWeb: "Web 端(浏览器)",
@@ -2081,7 +2109,7 @@
2081
2109
  hostEnvMismatchTail: "」,手动选择会覆盖它)",
2082
2110
  hostEnvDesktopForcedNote: "(当前页面不是 dsh-app://:点通知会尝试唤起 dsh:// 协议,浏览器可能弹「打开应用」确认框)",
2083
2111
  toast: "系统通知",
2084
- toastHint: "经浏览器弹系统通知(Windows 通知中心),需要通知权限。",
2112
+ toastHint: "走系统通知中心,需要通知权限;桌面端这类通知没有「同意 / 拒绝」按钮。",
2085
2113
  sound: "提示音",
2086
2114
  soundHint: "提示音总开关;播什么由「高级设置 → 音效来源」决定。",
2087
2115
  soundSource: "音效来源",
@@ -2153,9 +2181,9 @@
2153
2181
  quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
2154
2182
  bridgeOff: "快捷裁决按钮暂不可用(",
2155
2183
  bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
2156
- bridgeOffDesktop: "桌面端暂不支持快捷裁决按钮:dsh-app:// 页面无法注册 Service Worker,审批通知只能是不带按钮的普通样式,测试按钮同样只发普通通知。这是宿主限制,不是设置坏了。",
2184
+ bridgeOffDesktop: "系统通知没有「同意 / 拒绝」按钮:桌面端页面(dsh-app://)注册不了 Service Worker(宿主少给 allowServiceWorkers 特权),按钮发不出来。想一键裁决就把「提醒方式」切到「页面浮层」。",
2157
2185
  overlay: "页面浮层提醒",
2158
- overlayHint: "在页面角落画提醒卡片:窗口在哪块屏,它就在哪块屏。系统通知只会出现在主屏,主屏不看时用它。不需要通知权限,桌面端也能直接「同意 / 拒绝」,可与系统通知同开。",
2186
+ overlayHint: "在窗口角落画提醒卡片:窗口在哪块屏,它就在哪块屏;系统通知只会出现在主屏,主屏不看时用它。不需要通知权限,卡片上能直接「同意 / 拒绝」。与上面二选一,选它就不再弹系统通知。",
2159
2187
  overlayCorner: "浮层位置",
2160
2188
  overlayCornerHint: "贴在 DSH 窗口的哪个角;窗口在哪块屏,浮层就出现在哪块屏。",
2161
2189
  overlayPosTR: "右上",
@@ -2200,7 +2228,7 @@
2200
2228
  enable: "Enable reminders",
2201
2229
  enableHint: "When off: no toasts, no sounds, and the tab title is left alone.",
2202
2230
  hostEnv: "Environment",
2203
- hostEnvHint: "Auto-detect by default: a dsh-app:// page is the Desktop (DSH Desktop) shell, anything else is the Web. Override it if detection comes out wrong: on the Desktop a toast click fires the dsh:// deep link (whether the window actually comes forward depends on the shell version) and quick-decision buttons are unavailable; on the Web it is window.focus() plus the Service Worker bridge.",
2231
+ hostEnvHint: "Auto-detect by default: a dsh-app:// page is the Desktop (DSH Desktop) shell, anything else is the Web. Override it if detection comes out wrong: on the Desktop a toast click fires the dsh:// deep link (the effect is unreliable) and system notifications carry no buttons (the overlay does); on the Web it is window.focus() plus the Service Worker bridge.",
2204
2232
  hostEnvAuto: "Auto-detect (recommended)",
2205
2233
  hostEnvDesktop: "Desktop (DSH Desktop)",
2206
2234
  hostEnvWeb: "Web (browser)",
@@ -2208,7 +2236,7 @@
2208
2236
  hostEnvMismatchTail: "\"; the manual choice overrides it)",
2209
2237
  hostEnvDesktopForcedNote: " (this page is not dsh-app://: a toast click will try to launch the dsh:// protocol and the browser may show an \"open app?\" prompt)",
2210
2238
  toast: "System notifications",
2211
- toastHint: "Native toasts through the browser (Windows notification center); needs notification permission.",
2239
+ toastHint: "Through the system notification center; needs notification permission, and on the Desktop these carry no Approve / Reject buttons.",
2212
2240
  sound: "Sound",
2213
2241
  soundHint: "Master cue switch; what it plays comes from Advanced → Alert sound.",
2214
2242
  soundSource: "Alert sound",
@@ -2280,9 +2308,9 @@
2280
2308
  quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
2281
2309
  bridgeOff: "Quick-decision buttons are unavailable right now (",
2282
2310
  bridgeOffTail: "); approval toasts fall back to their plain form.",
2283
- bridgeOffDesktop: "Quick-decision buttons are unavailable on Desktop: dsh-app:// pages cannot register a Service Worker, so approval toasts fall back to their plain, button-less form and the test button only sends a plain notification. This is a host limitation, not a broken setting.",
2311
+ bridgeOffDesktop: "System notifications carry no Approve / Reject buttons: dsh-app:// pages cannot register a Service Worker (the shell does not grant the allowServiceWorkers privilege), so the buttons cannot be attached. Switch the alert channel to the in-page overlay to settle approvals in one click.",
2284
2312
  overlay: "In-page overlay",
2285
- overlayHint: "Draw a card in the page corner, on whatever display the window sits on. System notifications only land on the primary display. No permission needed, and the Desktop shell can Approve / Reject right on the card. Can run with toasts on.",
2313
+ overlayHint: "Draw a card in the window corner, on whatever display the window sits on. System notifications only land on the primary display. No permission needed, and the card carries Approve / Reject directly. Pick this or the system notification above: choosing it stops system notifications.",
2286
2314
  overlayCorner: "Overlay position",
2287
2315
  overlayCornerHint: "Which corner of the DSH window the cards stack in; they follow the window to whatever display it is on.",
2288
2316
  overlayPosTR: "Top right",
@@ -2419,6 +2447,9 @@
2419
2447
  ".dnm-switch.on{background:var(--dsw-alias-state-business-primary,#3b82f6);border-color:transparent}" +
2420
2448
  ".dnm-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .15s ease}" +
2421
2449
  ".dnm-switch.on .dnm-knob{left:16px}" +
2450
+ ".dnm-radio{flex:none;width:16px;height:16px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#3b82f6);cursor:pointer}" +
2451
+ ".dnm-rowRadio{justify-content:flex-start;gap:10px}" +
2452
+ ".dnm-rowRadio .dnm-rowText{flex:1}" +
2422
2453
  ".dnm-range{width:150px;accent-color:var(--dsw-alias-state-business-primary,#3b82f6)}" +
2423
2454
  ".dnm-select{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 8px;font-size:13px;font-family:inherit}" +
2424
2455
  ".dnm-btn{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:transparent;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));border-radius:8px;padding:5px 12px;font-size:13px;cursor:pointer;font-family:inherit}" +
@@ -2876,6 +2907,27 @@
2876
2907
  function secTitle(labelText) {
2877
2908
  return h("div", { className: "dnm-secTitle" }, labelText);
2878
2909
  }
2910
+ // One channel at a time, so the pair renders as radios, not switches. The
2911
+ // radio sits ahead of the label: two rows sharing one choice read as a
2912
+ // list of options, which a lone dot on the far right does not.
2913
+ function rowRadio(labelText, hintText, checked, onChange) {
2914
+ var pick = function () { if (!checked) onChange(); };
2915
+ return tipRow("dnm-row dnm-rowRadio", hintText, [
2916
+ h("input", {
2917
+ type: "radio",
2918
+ className: "dnm-radio",
2919
+ name: "dnm-alert-channel",
2920
+ checked: !!checked,
2921
+ "aria-label": labelText,
2922
+ onChange: pick,
2923
+ onClick: pick
2924
+ }),
2925
+ h("div", { className: "dnm-rowText" }, [
2926
+ h("div", { className: "dnm-rowLabel" }, labelText),
2927
+ srOnly(hintText)
2928
+ ])
2929
+ ]);
2930
+ }
2879
2931
  // The permission state belongs on the System-notifications row it affects
2880
2932
  // rather than in a footer note the user has to connect to it.
2881
2933
  function toastHintText() {
@@ -2911,9 +2963,9 @@
2911
2963
  patch({ enabled: v });
2912
2964
  if (!v && attentionActive > 0) { attentionActive = 0; applyTitle(); }
2913
2965
  }),
2914
- rowSwitch("toast", t("toast"), toastHintText(), cfg.toast, function (v) { patch({ toast: v }); }),
2915
- rowSwitch("overlay", t("overlay"), t("overlayHint"), cfg.overlay, function (v) { patch({ overlay: v }); }),
2916
- cfg.overlay ? tipRow("dnm-row dnm-rowSub", t("overlayCornerHint"), [
2966
+ rowRadio(t("toast"), toastHintText(), cfg.channel !== "overlay", function () { patch({ channel: "toast" }); }),
2967
+ rowRadio(t("overlay"), t("overlayHint"), cfg.channel === "overlay", function () { patch({ channel: "overlay" }); }),
2968
+ cfg.channel === "overlay" ? tipRow("dnm-row dnm-rowSub", t("overlayCornerHint"), [
2917
2969
  h("div", { className: "dnm-rowText" }, [
2918
2970
  h("div", { className: "dnm-rowLabel" }, t("overlayCorner")),
2919
2971
  srOnly(t("overlayCornerHint"))
@@ -2966,7 +3018,7 @@
2966
3018
  !cfg.quickActions ? h("div", { className: "dnm-row" }, [
2967
3019
  h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("testNeedsQuick"))])
2968
3020
  ]) : h("div", { className: "dnm-row" }, [
2969
- h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.overlay ? t("overlayQuickNote") : (cfg.toast && !bridgeActive() ? (isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail")) : t("quickApprovalNote")))])
3021
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, cfg.channel === "overlay" ? t("overlayQuickNote") : (!bridgeActive() ? (isDesktopShell() ? t("bridgeOffDesktop") : t("bridgeOff") + swState + t("bridgeOffTail")) : t("quickApprovalNote")))])
2970
3022
  ]),
2971
3023
  feedback ? h("div", { className: "dnm-row" }, [
2972
3024
  h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-feedback" }, feedback)])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-me",
3
- "version": "1.9.4",
3
+ "version": "1.10.0",
4
4
  "description": "离开 DSH 页面也能收到提醒:需要你操作 / 后台回复完成时发系统通知、标题标记与可选页面浮层(副屏也能看见),支持审批快捷裁决与外部推送。Brand icon and a reworked settings page in 1.9.0.",
5
5
  "license": "MIT",
6
6
  "type": "module",