dsh-notify-me 1.2.1 → 1.3.1

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.3.1] — 2026-10-03
6
+
7
+ ### 文档
8
+ - README 中英补齐 1.1.7 / 1.1.8 起就有、但一直没写进文档的两个设置项:「当前对话不弹通知」(`currentHiddenOnly`)与「点击通知回到对应对话」(`autoFocus`),控制台配置示例同步补 `currentHiddenOnly`;「提醒时机」表的默认口径改为「页面可见也提醒(当前对话除外:默认只留标题标记)」——原表述把当前对话的静默规则漏掉了。
9
+ - 措辞去掉 Windows 专属口径:系统通知按「经浏览器弹出,进 Windows 通知中心或 macOS 通知中心」描述,权限提示补 macOS 的 系统设置 → 通知;已知限制新增 macOS 一条——浏览器路径无平台假设,但 DSH Desktop(Electron)的系统通知要求应用已签名(官方 macOS 版是否满足未实测),审批按钮在 macOS 与桌面宿主上的渲染同样未实测,不渲染时自动退回无按钮通知。
10
+ - 已知限制补「当前对话不弹通知」按 `document.hidden` 判定的边界(窗口被别的应用完全盖住仍算可见);开发自检一节补 `cordis-host-test.mjs` 的 cordis 探测路径(含 `~/.dsh/profiles/node_modules` 平铺回退目录)。
11
+
12
+ ### 变更
13
+ - 版本号 1.3.0 → 1.3.1;`window.__dshNotifyMe.version` 同步。功能零改动。
14
+
15
+ ## [1.3.0] — 2026-10-03
16
+
17
+ ### 新增
18
+ - **审批通知上直接裁决**(设置页新开关「审批通知上直接裁决」,默认开启):权限审批的系统通知带上「同意 / 拒绝」按钮,点一下就完成裁决,不用切回页面。按钮调的是审批卡片自己的 `PendingApproval.answer('allowed-once' | 'rejected')`——「同意」即「允许一次」,只放行这一次,与卡片上的按钮完全同义;点通知正文仍是 1.1.8 的「回到对应对话」。
19
+ - 绕开的是一条浏览器硬限制:`actions` 只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 直接抛 `TypeError`。所以 bundle 一份文件两用:页面里照常注册提醒工厂,同一份字节再注册成 Service Worker,只负责把「点了哪个按钮」(同意 / 拒绝 / 点正文)转告页面(BroadcastChannel 与客户端 postMessage 双通道),裁决本身仍发生在页面里。注册只用本插件自己的单条目 combo URL(`/plugins/??dsh-notify-me/client.js&rev=…`,取自 `__DSH_BOOT__.entries`)——多插件 combo 会在 worker 里执行别家的 factory,注册必挂。
20
+ - 按钮通知按交互 key 记账:一条等待一次只认领一张通知;页面里先答掉、或被新请求顶替之后,残留通知上的按钮再点不裁决任何东西,只把通知关掉。请求消失时残留通知一并关闭;带按钮的通知不设 15 秒自动消失,未裁决的审批留在通知中心等你。
21
+ - `window.__dshNotifyMe` 新增 `decide(key, outcome)`(按钮的程序化同款,`outcome` 取 `"allowed-once"` / `"rejected"`)与 `test("approval")`(设置页新增「测试『审批按钮』」按钮,一条通知端到端验证按钮链路);`debug()` 新增 `quickActions` / `bridge`(`active` = 按钮可用,否则给出不可用原因)/ `actionKeys`;`onEvent` 新增 `decision` 事件(`{key, outcome, sessionId}`,测试点击另带 `test: true`)。
22
+
23
+ ### 变更
24
+ - 带按钮的审批通知改用 `dsh-notify-me-attention-<key>` 独立标签,多条待审批互不顶掉;提问 / 方案确认沿用原来的 `dsh-notify-me-attention` 标签行为。
25
+ - 不支持 Service Worker 的环境(`http://` 局域网地址等)自动退回 1.2.x 的无按钮通知,其余功能不受影响,`debug().bridge` 显示具体原因。
26
+ - 版本号 1.2.1 → 1.3.0;`window.__dshNotifyMe.version` 同步。
27
+
28
+ ### 测试
29
+ - `smoke/smoke-test.cjs` 新增快捷裁决桥用例,把 bundle 的两半都在同一套件里跑起来(worker 半侧执行在 worker 形状的 vm 上下文,页面半侧走常规桩,两边用同一个 BroadcastChannel 桩对接):单条目 combo URL 注册、按钮文案与 `data` 携带 key/会话、点「同意」→ `answer('allowed-once')`、点「拒绝」→ `answer('rejected')`、陈旧 key 与未知 key 一律不裁决、页面里先答掉后残留按钮失效、`quickActions` 关闭或无 Service Worker 时退回普通通知且不带 `actions`(构造函数带它会抛 `TypeError`)、测试按钮走完整 worker 链路。
30
+ - `smoke/cordis-host-test.mjs` 在 `sessionStatus` 宿主上补真 cordis 语义的裁决用例:`decide()` 经 `ctx.get("uiSession")` 找到活着的交互并调它的 `answer()`,重复裁决与未知 key 拒绝,标记随之释放。
31
+
32
+ ### 已知限制
33
+ - 按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;DSH Desktop(Electron)里按钮能否渲染未实测,不渲染时按上述降级走无按钮通知。
34
+
5
35
  ## [1.2.1] — 2026-10-03
6
36
 
7
37
  ### 修复
@@ -17,11 +47,9 @@ All notable changes to **dsh-notify-me** are documented here.
17
47
  - **`0.1.6` 及以后的宿主上认不出「当前会话」**:会话列表快照不再有 `current`(`SessionListState` 只剩 `ids / byId / phase / projectionsBySession`),于是当前会话面(face)订阅从未建立、`onListChanged()` 还把当前会话当成后台会话评估——后果是 1.1.7 的「当前对话不弹通知」规则失效(审批卡片就在屏幕上,Toast 仍会盖上去),标签页标记的归属也随之错位。现在按宿主自己的口径推导:有 `current` 用它,没有就取 `retainedBy.mainView > 0` 的那一行(与内置 `DocumentTitle` / `ui-workspace` / `ui-session` 的判定一致)。
18
48
  - **`0.1.6` 及以后点通知切不回对应会话**:`sessions.open(id)` 已被移除,导航改由 `uiWorkspace.openSession(id)` 承担;插件现在两个都试(`open` 优先,旧宿主行为不变),会话已不在列表时依旧不调用。`debug()` 新增 `currentSession` 便于核对。
19
49
 
20
- ### 新增
21
- - `sessionStatus` 适配完整:同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
22
-
23
50
  ### 变更
24
- - `peerDependencies` 放宽为 `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || >=0.1.6-alpha.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`:原范围在 `0.2.0-rc.2` 上被 `evaluatePluginCompatibility` 判为不兼容,`dsh plugin add` 会拒绝并回滚安装(`0.3` 线仍被拒)。`dsh.compatibility.dshReleases` 增加 `0.2.0-rc.2: compatible`。
51
+ - 提醒语义在适配后原样保持(1.1.x 既有逻辑,不是新行为):同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
52
+ - `peerDependencies` 放宽为 `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-rc.1 || ^0.1.5-rc.1 || >=0.1.6-alpha.2 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`:原范围在 `0.2.0-rc.2` 上被 `evaluatePluginCompatibility` 判为不兼容,`dsh plugin add` 会拒绝并回滚安装(`0.3` 线仍被拒)。`dsh.compatibility.dshReleases` 增加 `0.2.0-rc.2: compatible`。范围对未实测的 `0.2.x` 正式版同样放行,是否兼容以 `dshReleases` 的实测声明为准。
25
53
  - 版本号 1.1.8 → 1.2.0(新宿主代际);`window.__dshNotifyMe.version` 同步。
26
54
  - 设置页字典注册改为把 `locale.register()` 返回的 disposer 交给 `ctx.effect`(与官方 `ui-approval` 写法一致),客户端插件 HMR 重载时不会因「同名 namespace 已注册」而丢掉设置页。
27
55
  - `uiSession` 绑定的诊断降噪:真实 `0.2.0-rc.2` 页面上每次加载都会出现的那条 `uiSession is provided but not reachable` 警告其实是**启动竞态**(本插件的客户端条目常早于 `ui-session` 条目激活,此时 `ctx.get()` 按 cordis 的严格语义拿不到尚未激活的服务),并非缺陷。现在只有**能拿到服务实例、但它既没有 `sessionStatus` 也没有 `pendingInteractions`** 时才打一条警告(并提示附上 `debug()` 输出);「还没可见」保持安静,由 1.5s 重试定时器接管,`debug().uiSessionNote` 仍可查。
package/README.en.md CHANGED
@@ -6,12 +6,12 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/dsh-notify-me?style=flat-square&label=downloads&color=1F883D)](https://www.npmjs.com/package/dsh-notify-me)
7
7
  ![License](https://img.shields.io/badge/license-MIT-blue)
8
8
  ![Platform](https://img.shields.io/badge/platform-browser-blue)
9
- ![Size](https://img.shields.io/badge/bundle-%7E37KB-green)
9
+ ![Size](https://img.shields.io/badge/bundle-%7E79KB-green)
10
10
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
11
11
 
12
12
  ---
13
13
 
14
- **Step away from the DeepSeek Harness web UI — and still know what's going on.** When the agent stops and needs your input, or when a reply finishes in the background while you're in another app, dsh-notify-me alerts you with a Windows desktop notification, a sound, and a tab-title marker. Toggle it and pick the notification language right from **Settings → Notify me**.
14
+ **Step away from the DeepSeek Harness web UI — and still know what's going on.** When the agent stops and needs your input, or when a reply finishes in the background while you're in another app, dsh-notify-me alerts you with a desktop notification, a sound, and a tab-title marker. Approval notifications even carry Approve / Reject buttons, so you can settle the request without switching back. Toggle it and pick the notification language right from **Settings → Notify me**.
15
15
 
16
16
  ---
17
17
 
@@ -32,7 +32,7 @@ dsh-notify-me decouples **supervision** from **sitting in front of the screen**:
32
32
 
33
33
  ## UI Preview
34
34
 
35
- | Windows system notification |
35
+ | System notification (screenshot from Windows) |
36
36
  | --- |
37
37
  | ![DSH notification](https://raw.githubusercontent.com/chromoany/dsh-notify-me/main/docs/screenshots/notify-toast.png) |
38
38
 
@@ -42,7 +42,7 @@ dsh-notify-me decouples **supervision** from **sitting in front of the screen**:
42
42
 
43
43
  | When | What you get | Default |
44
44
  | --- | --- | --- |
45
- | 🔔 **The agent needs your input** — sandbox approval / plan review / a question (`ask_user_question`) | Toast + sound + `🔔 Action needed ·` title marker | Alerts even while the page is visible |
45
+ | 🔔 **The agent needs your input** — sandbox approval / plan review / a question (`ask_user_question`) | Toast + sound + `🔔 Action needed ·` title marker; approval toasts also carry Approve / Reject buttons | Alerts even while the page is visible (the conversation on screen is the exception: tab marker only by default) |
46
46
  | ✅ **A reply finishes** — a turn completes; background sessions finishing are also reported | Toast + sound | Alerts only while the page is hidden / backgrounded |
47
47
 
48
48
  ## Configure it from DSH Settings
@@ -52,14 +52,17 @@ Open **Settings → Notify me** (refresh the page once after installing):
52
52
  - **Enable reminders** — master switch; when off, no toasts, no sounds and the tab title is never touched;
53
53
  - **System notifications / Sound / Volume** — toast toggle, WebAudio beep toggle and a volume slider;
54
54
  - **"Needs you" alerts while the page is open** (default on) and **"Reply finished" alerts while the page is open** (default off);
55
+ - **Stay quiet for the conversation on screen** (default on): a wait inside the conversation you are looking at keeps only the tab marker — no toast or sound landing on the approval card. Backgrounding the page puts that alert back at once, unless you already handled it. Waits in other background sessions are unaffected;
56
+ - **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from; when off, clicking a toast does nothing;
57
+ - **Decide approvals from the toast** (default on): approval notifications carry Approve / Reject buttons, and one click settles the request exactly like the Allow once / Reject buttons on the approval card. Clicking the toast body still takes you back to the conversation;
55
58
  - **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
56
- - **Test buttons** — send one "needs you" or "reply finished" test alert with the current settings (**not** limited by the "while the page is open" toggles; the "needs you" test's title marker clears itself after ~6s).
59
+ - **Test buttons** — send one "needs you", "reply finished" or "approval buttons" test alert with the current settings (**not** limited by the "while the page is open" toggles; the "needs you" test's title marker clears itself after ~6s).
57
60
 
58
- > Notification permission is required: click once on the page → **Allow** (or address-bar lock → Site settings → Notifications → Allow → reload).
61
+ > Notification permission is required: click once on the page → **Allow** (or address-bar lock → Site settings → Notifications → Allow → reload). On macOS, also allow the browser under System Settings → Notifications.
59
62
 
60
63
  ## How it alerts
61
64
 
62
- - **System notification** — native notification-center toast (clicking it brings the DSH window back to front)
65
+ - **System notification** — a toast through the browser into the Windows action center / macOS Notification Center (clicking it opens the conversation it came from; approval toasts can be decided straight from the buttons)
63
66
  - **Sound** — WebAudio beeps (distinct patterns for "needs you" vs "done")
64
67
  - **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`
65
68
 
@@ -78,10 +81,12 @@ Restart `dsh web`, then hard-refresh the page (Ctrl+Shift+R).
78
81
 
79
82
  | Host generation | "Needs you" interaction source | Current session | Toast-click navigation |
80
83
  | --- | --- | --- | --- |
81
- | `<= 0.1.1-rc.2` | controller snapshot `pending[]` | snapshot `current` | `sessions.open()` |
82
- | `0.1.2-alpha.2 .. 0.1.5-rc.3` | `uiSession.pendingInteractions` | snapshot `current` | `sessions.open()` |
84
+ | `<= 0.1.2-alpha.1` | controller snapshot `pending[]` | snapshot `current` | `sessions.open()` |
85
+ | `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | snapshot `current` | `sessions.open()` |
83
86
  | `>= 0.1.6-alpha.2` (incl. `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
84
87
 
88
+ The ranges are illustrative: the plugin picks its source by store shape (whenever `sessionStatus` exists it takes the new path); the version numbers only mark where each shape appeared.
89
+
85
90
  Verified to actually activate on `0.1.2-rc.1`, `0.1.5-rc.2` and `0.2.0-rc.2` (each booted in its own profile: the Settings → Notifications section registers, i.e. the plugin's `apply` really runs), and previously on `0.1.1-rc.2`. Two traps are worth remembering: a package listed in `dsh.client.inject` that a newer runtime no longer ships parks the client entry at `pending (waiting for services: …)` forever — which is what broke 1.1.3 on `0.1.2-rc.1` and later; and when the host renames a store or a field nothing throws, alerts just go **silent** — `0.2.0-rc.2` moved `pendingInteractions`→`sessionStatus`, `list.current`→`retainedBy.mainView` and `sessions.open()`→`uiWorkspace.openSession()`. So every adaptation must confirm `window.__dshNotifyMe.debug()` reports `bound` and which source answered.
86
91
 
87
92
  **Verify** — open the DevTools console and run:
@@ -89,6 +94,8 @@ Verified to actually activate on `0.1.2-rc.1`, `0.1.5-rc.2` and `0.2.0-rc.2` (ea
89
94
  ```js
90
95
  window.__dshNotifyMe.test("done") // "reply finished" sample
91
96
  window.__dshNotifyMe.test("attention") // "needs your input" sample
97
+ window.__dshNotifyMe.test("approval") // "approval buttons" sample: try the Approve / Reject buttons on the toast
98
+ window.__dshNotifyMe.debug() // the bridge field reports the button channel: "active" = buttons work
92
99
  ```
93
100
 
94
101
  Nothing happened? 90% of the time it's one of:
@@ -107,13 +114,16 @@ window.__dshNotifyMe.setConfig({
107
114
  enabled: true, // false = disable all reminders (master switch)
108
115
  language: "auto", // 'auto' follow the UI | 'zh' | 'en'
109
116
  attentionHiddenOnly: false, // true = don't alert "needs you" while the page is visible
117
+ currentHiddenOnly: true, // true = waits in the conversation on screen keep only the tab marker (replayed once backgrounded)
110
118
  doneHiddenOnly: true, // false = also alert "reply finished" while visible
111
119
  toast: true, // system-notification toggle
112
120
  sound: true, // sound toggle
113
121
  volume: 0.5, // 0..1
114
- autoFocus: true // clicking the toast focuses the DSH window
122
+ autoFocus: true, // clicking a toast opens the conversation it came from
123
+ quickActions: true // Approve / Reject buttons on approval toasts
115
124
  })
116
125
  window.__dshNotifyMe.resetConfig() // restore defaults
126
+ window.__dshNotifyMe.decide("approval:3", "allowed-once") // programmatic decision: "allowed-once" | "rejected"
117
127
  ```
118
128
 
119
129
  ## How it works
@@ -126,27 +136,36 @@ The reminder core picks its interaction source per host generation (see the tabl
126
136
 
127
137
  The reminder core stays dependency-free and self-contained: notification copy resolves at alert time from the chosen language (follow-interface / Simplified Chinese / English). The Settings page is an **optional** React surface — it only registers into Settings when the web profile provides the `slots` / `locale` services and `react`; without them the plugin degrades gracefully to alerts-only (no Settings page).
128
138
 
139
+ ### Quick decisions, and how they reach the agent
140
+
141
+ Action buttons (`actions`) belong to persistent notifications shown through `ServiceWorkerRegistration.showNotification()`; passing `actions` to the `new Notification()` constructor throws a `TypeError`. That is a browser rule no plugin can route around, so the bundle holds two jobs in the same bytes: the page half registers the reminder factory as it always did, and the very same file is registered as a Service Worker whose only task is relaying which button was clicked back to the page. The decision itself happens in the page, through the same `PendingApproval.answer('allowed-once' | 'rejected')` the approval card uses — which is why "Approve" is exactly "Allow once" and nothing broader.
142
+
143
+ Every button toast is booked against its interaction key, so it can only decide the request it was raised for. Once the page answers that request or a newer one replaces it, clicking the leftover buttons decides nothing and just closes the notification. Button toasts also skip the auto-dismiss timer: an undecided approval stays in the notification center until you settle it.
144
+
129
145
  ## Known limitations
130
146
 
131
147
  - The page must be open for alerts to fire (background tab / minimized is fine; closing the tab stops it — that's inherent to a browser-layer plugin).
132
- - Notifications appear through the browser, so the browser needs "show notifications" permission in Windows settings, and notification-center "Do Not Disturb" must not suppress them.
148
+ - 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.
133
149
  - The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
134
150
  - Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
151
+ - 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 `bridge` field of `window.__dshNotifyMe.debug()` says why; toasts then fall back to their buttonless form and everything else keeps working.
152
+ - 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 macOS / desktop hosts are likewise unrendered in some cases, falling back to the buttonless form above.
135
153
  - Config lives in browser `localStorage`: switching browsers/devices or clearing site data returns to defaults (one-click restore in the Settings page).
136
154
  - On `>= 0.1.6` (including `0.2.0-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.
137
155
  - 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.
156
+ - "Stay quiet for the conversation on screen" is judged by `document.hidden`: a window fully covered by other apps still counts as visible, so waits in the current conversation stay quiet there too.
138
157
 
139
158
  ## Development
140
159
 
141
160
  ```powershell
142
161
  node --check lib\client.js
143
162
  node --check lib\index.js
144
- node smoke\smoke-test.cjs # offline state-machine test: three host generations + master switch + zh/en cases
163
+ node smoke\smoke-test.cjs # offline state-machine test: three host generations + master switch + zh/en cases + the quick-decision bridge (worker half included)
145
164
  node smoke\cordis-host-test.mjs # real-cordis end-to-end (skips when no local DSH installation is found)
146
165
  npm pack --dry-run # preview the published tarball
147
166
  ```
148
167
 
149
- `cordis-host-test.mjs` borrows `@deepseek-ai/cordis` from a local DSH installation; in a source checkout with no profile dependencies, point it there with `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js`.
168
+ `cordis-host-test.mjs` borrows `@deepseek-ai/cordis` from a local DSH installation (it probes the global npm directory, `~/.dsh/profiles/web/node_modules` and the flat fallback `~/.dsh/profiles/node_modules` in turn); in a source checkout with no profile dependencies, point it there with `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js`.
150
169
 
151
170
  ## License
152
171
 
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/dsh-notify-me?style=flat-square&label=downloads&color=1F883D)](https://www.npmjs.com/package/dsh-notify-me)
7
7
  ![License](https://img.shields.io/badge/license-MIT-blue)
8
8
  ![Platform](https://img.shields.io/badge/platform-browser-blue)
9
- ![Size](https://img.shields.io/badge/bundle-%7E37KB-green)
9
+ ![Size](https://img.shields.io/badge/bundle-%7E79KB-green)
10
10
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
11
11
 
12
12
  ---
13
13
 
14
- **离开 DSH 页面也不错过任何动静。** 当模型停下来需要你操作(审批 / 方案待确认 / 提问)、或你在别的软件时回复正好在后台完成,dsh-notify-me 会用系统通知 + 提示音 + 标签页标题标记提醒你——开关和通知语言都直接在 **DSH 设置 → 通知提醒** 里改。
14
+ **离开 DSH 页面也不错过任何动静。** 当模型停下来需要你操作(审批 / 方案待确认 / 提问)、或你在别的软件时回复正好在后台完成,dsh-notify-me 会用系统通知 + 提示音 + 标签页标题标记提醒你——权限审批的通知还带「同意 / 拒绝」按钮,点一下就完成裁决,不用切回页面。开关和通知语言都直接在 **DSH 设置 → 通知提醒** 里改。
15
15
 
16
16
  ---
17
17
 
@@ -32,7 +32,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
32
32
 
33
33
  ## 效果预览
34
34
 
35
- | Windows 系统通知效果 |
35
+ | 系统通知效果(截图来自 Windows) |
36
36
  | --- |
37
37
  | ![DSH 通知效果](https://raw.githubusercontent.com/chromoany/dsh-notify-me/main/docs/screenshots/notify-toast.png) |
38
38
 
@@ -42,7 +42,7 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
42
42
 
43
43
  | 时机 | 提醒内容 | 默认 |
44
44
  | --- | --- | --- |
45
- | 🔔 **模型需要你操作** — 审批请求 / 方案待确认(plan review)/ 提问(`ask_user_question`) | 通知 + 提示音 + `🔔 需要你 ·` 标题标记 | 页面可见也提醒 |
45
+ | 🔔 **模型需要你操作** — 审批请求 / 方案待确认(plan review)/ 提问(`ask_user_question`) | 通知 + 提示音 + `🔔 需要你 ·` 标题标记;审批的通知上直接带「同意 / 拒绝」按钮 | 页面可见也提醒(当前对话除外:默认只留标题标记) |
46
46
  | ✅ **回复完成** — 一轮回复跑完;后台会话完成也会报 | 通知 + 提示音 | 仅页面隐藏/后台时提醒 |
47
47
 
48
48
  ## 在 DSH 设置里改配置
@@ -52,14 +52,17 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
52
52
  - **启用提醒** 主开关:关闭后不再弹系统通知、不播放提示音、也不改标签页标题;
53
53
  - **系统通知 / 提示音 / 音量**:Toast、WebAudio 提示音与音量滑块;
54
54
  - **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
55
+ - **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标题标记,不弹通知也不响音(免得盖住审批卡片);页面切到后台立刻补发,若你已经处理掉就作废。其它后台会话不受影响;
56
+ - **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;关掉后点通知不做任何事;
57
+ - **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」;
55
58
  - **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
56
- - **测试按钮**:用当前设置立即发一条「需要你」或「回复完成」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
59
+ - **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
57
60
 
58
- > 需要浏览器通知权限:页面上点一下 → 允许;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新。
61
+ > 需要浏览器通知权限:页面上点一下 → 允许;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新。macOS 还要在 系统设置 → 通知 里允许该浏览器。
59
62
 
60
63
  ## 提醒方式
61
64
 
62
- - **系统通知**:Windows 通知中心 Toast(点击可把 DSH 窗口切回前台)
65
+ - **系统通知**:经浏览器弹出的系统通知,进 Windows 通知中心或 macOS 通知中心(点击回到提醒所属的对话;审批的通知点按钮直接裁决)
63
66
  - **提示音**:WebAudio 合成音(「需要你」与「完成」使用不同音型)
64
67
  - **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`
65
68
 
@@ -78,10 +81,12 @@ dsh plugin --profile web add dsh-notify-me
78
81
 
79
82
  | 宿主代际 | 「需要你」交互来源 | 判断当前会话 | 点通知切会话 |
80
83
  | --- | --- | --- | --- |
81
- | `≤ 0.1.1-rc.2` | 控制器快照的 `pending[]` | 快照 `current` | `sessions.open()` |
82
- | `0.1.2-alpha.2 .. 0.1.5-rc.3` | `uiSession.pendingInteractions` | 快照 `current` | `sessions.open()` |
84
+ | `≤ 0.1.2-alpha.1` | 控制器快照的 `pending[]` | 快照 `current` | `sessions.open()` |
85
+ | `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | 快照 `current` | `sessions.open()` |
83
86
  | `≥ 0.1.6-alpha.2`(含 `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
84
87
 
88
+ 区间仅作示意:插件按 store 形状自动选源(`sessionStatus` 存在即走新路),版本号只是各形状的出现边界。
89
+
85
90
  实测**真正激活**(各自独立 profile 启动:设置里出现「通知提醒」分区,即插件的 `apply` 确实执行)于 `0.1.2-rc.1`、`0.1.5-rc.2` 与 `0.2.0-rc.2`;更早的 `0.1.1-rc.2` 亦验证过。两个坑值得记住:一是 `dsh.client.inject` 里列了新版运行时已不再提供的包,客户端条目会停在 `pending (waiting for services: …)` 而永不执行——1.1.3 在 `0.1.2-rc.1` 及以后正是这样失效的;二是宿主换存储/字段名时**不会报错**,提醒只会静默失效——`0.2.0-rc.2` 上 `pendingInteractions`→`sessionStatus`、`list.current`→`retainedBy.mainView`、`sessions.open()`→`uiWorkspace.openSession()` 都是这一类,所以每次适配都要用 `window.__dshNotifyMe.debug()` 确认真的是 `bound`、以及用的是哪个来源。
86
91
 
87
92
  **验证** — F12 控制台执行:
@@ -89,6 +94,8 @@ dsh plugin --profile web add dsh-notify-me
89
94
  ```js
90
95
  window.__dshNotifyMe.test("done") // 「完成」示例
91
96
  window.__dshNotifyMe.test("attention") // 「需要你」示例
97
+ window.__dshNotifyMe.test("approval") // 「审批按钮」示例:通知上有「同意 / 拒绝」,点着试试
98
+ window.__dshNotifyMe.debug() // 桥接状态看 bridge 字段,active = 按钮可用
92
99
  ```
93
100
 
94
101
  没反应?九成是:通知权限被拒绝(在页面上点一下 → 选「允许」;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新)、装完没重启 DSH、系统设置里浏览器通知被关。
@@ -103,13 +110,16 @@ window.__dshNotifyMe.setConfig({
103
110
  enabled: true, // false = 关闭所有提醒(主开关)
104
111
  language: "auto", // 'auto' 跟随界面 | 'zh' 简体中文 | 'en' English
105
112
  attentionHiddenOnly: false, // true = 页面可见时「需要你」不提醒
113
+ currentHiddenOnly: true, // true = 当前对话的等待只留标题标记(转后台补发)
106
114
  doneHiddenOnly: true, // false = 页面可见时「完成」也提醒
107
115
  toast: true, // 系统通知开关
108
116
  sound: true, // 提示音开关
109
117
  volume: 0.5, // 音量 0~1
110
- autoFocus: true // 点通知切回 DSH 窗口
118
+ autoFocus: true, // 点通知回到提醒所属的对话
119
+ quickActions: true // 审批通知上的「同意 / 拒绝」按钮
111
120
  })
112
121
  window.__dshNotifyMe.resetConfig() // 恢复默认
122
+ window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:"allowed-once" | "rejected"
113
123
  ```
114
124
 
115
125
  ## 工作原理
@@ -122,27 +132,36 @@ window.__dshNotifyMe.resetConfig() // 恢复默认
122
132
 
123
133
  提醒核心仍然零第三方运行时依赖、完全自包含:通知文字按所选语言(跟随界面 / 中文 / English)即时解析。设置页是**可选**的 React 呈现层——当 DSH web profile 提供 `slots` / `locale` / `react` 时才注册进「设置 → 通知提醒」,缺任一能力时插件自动降级为纯提醒(无设置页),不影响功能。
124
134
 
135
+ ### 快捷裁决怎么走通的
136
+
137
+ 通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')`,所以「同意」就是「允许一次」,不多放行任何东西。
138
+
139
+ 每条按钮通知按交互 key 记账,一次只认领一条等待;请求被页面里答掉、或被新请求顶替之后,残留通知上的按钮再点也不会误裁,只是安静地关掉。带按钮的通知不设自动消失——审批没裁决就一直留在通知中心里等你。
140
+
125
141
  ## 已知限制
126
142
 
127
143
  - 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
128
144
  - 通知经由浏览器弹出,需允许浏览器通知权限,且勿扰模式不能屏蔽它。
129
145
  - 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
130
146
  - 未授权通知权限时只有提示音与标题标记。
147
+ - 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;浏览器或桌面宿主不渲染通知按钮时,`window.__dshNotifyMe.debug()` 的 `bridge` 字段会说明原因,通知自动退回无按钮样式,其余功能不受影响。
148
+ - macOS:浏览器里提醒、提示音、标题标记、点通知跳转都没有平台假设,照常工作;但 DSH Desktop(Electron)的系统通知要求应用已签名,未签名的通知直接发不出去,官方 macOS 版是否满足这一前提未实测;审批按钮在 macOS 与桌面宿主上的渲染同样未实测,不渲染时自动退回无按钮通知。
131
149
  - 配置存在浏览器 `localStorage`:换浏览器/设备或清除站点数据后会回到默认值(设置页可一键恢复默认)。
132
150
  - `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文只有会话名,不再附带回复摘要——宿主快照已不再提供 `nodes`;提醒本身照常触发。
133
151
  - 标题标记由宿主与插件共同写 `document.title`:宿主重算标题时标记可能被覆盖,直到下一次提醒事件重新写成。
152
+ - 「当前对话不弹通知」按 `document.hidden` 判定:窗口被别的应用完全盖住时仍算「页面可见」,此时当前对话的等待照样静默。
134
153
 
135
154
  ## 开发自检
136
155
 
137
156
  ```powershell
138
157
  node --check lib\client.js
139
158
  node --check lib\index.js
140
- node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例
159
+ node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例 + 快捷裁决桥(Service Worker 半侧一并跑)
141
160
  node smoke\cordis-host-test.mjs # 真 cordis 端到端(找不到本机 DSH 时自动跳过)
142
161
  npm pack --dry-run # 预览发布包
143
162
  ```
144
163
 
145
- `cordis-host-test.mjs` 会从本机 DSH 安装借 `@deepseek-ai/cordis`;源码 checkout 里没有 profile 依赖时,用 `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js` 指定即可。
164
+ `cordis-host-test.mjs` 会从本机 DSH 安装借 `@deepseek-ai/cordis`(依次探测 npm 全局目录、`~/.dsh/profiles/web/node_modules` 与 `~/.dsh/profiles/node_modules` 平铺回退目录);源码 checkout 里没有 profile 依赖时,用 `DSH_CORDIS_PATH=<checkout>/vendor/cordis/lib/index.js` 指定即可。
146
165
 
147
166
  ## License
148
167
 
package/lib/client.js CHANGED
@@ -35,6 +35,15 @@
35
35
  // - fires a Windows desktop Notification (+ optional sound) when the agent
36
36
  // needs the user's input — pending interaction kinds 'approval',
37
37
  // 'plan-review' or 'question' — and when a reply/turn finishes;
38
+ // - quick-decides approvals from the toast itself (option, on by default):
39
+ // approval notifications carry 同意/拒绝 action buttons and one click on a
40
+ // button settles the request through the interaction's own
41
+ // answer('allowed-once' | 'rejected') — the exact verb the in-page approval
42
+ // card uses. Action buttons only exist on persistent notifications, so this
43
+ // file is dual-context: registered as a Service Worker as well, whose
44
+ // notificationclick relays the chosen button back to this page (broadcast +
45
+ // client postMessage). Where a worker cannot run (insecure context, no
46
+ // bundle URL) the plugin degrades to today's plain toast;
38
47
  // - registers a "通知提醒 / Notify me" page into DSH Settings
39
48
  // (settings.section slot) exposing an on/off master switch, per-channel
40
49
  // toggles, volume and the notification language (follow-interface /
@@ -42,9 +51,71 @@
42
51
  // - preferences persist in localStorage (origin-scoped, like other
43
52
  // third-party client plugins) and are tweakable on window.__dshNotifyMe.
44
53
 
45
- window.__ModuleLoader__.load({
46
- id: "dsh-notify-me",
47
- factory: (require) => {
54
+ // One file, two jobs. The DSH module loader evaluates this script in the page
55
+ // (registering the factory below), and the very same served bytes are also
56
+ // registered as the plugin's Service Worker — see the bridge at the bottom of
57
+ // this wrapper.
58
+ (function (root) {
59
+ if (!root) return;
60
+
61
+ // ──────────────── service-worker half (notification bridge) ────────────
62
+ var BRIDGE_CHANNEL = "dsh-notify-me:bridge";
63
+ // A real Service Worker global is the only context with a registration and
64
+ // no document; the page (and any test harness) never matches.
65
+ function isServiceWorkerScope(g) {
66
+ return !!g && typeof g.registration === "object" && g.registration !== null &&
67
+ typeof g.addEventListener === "function" && typeof g.document === "undefined";
68
+ }
69
+ // The worker never decides anything: it only relays which button was
70
+ // clicked to the page that owns the pending interaction. Both a
71
+ // BroadcastChannel and a direct client postMessage are used — the channel
72
+ // covers every same-origin page, the postMessage covers environments where
73
+ // the worker cannot open one.
74
+ function installNotificationBridge(g) {
75
+ function relay(msg) {
76
+ try {
77
+ if (typeof g.BroadcastChannel === "function") {
78
+ var ch = new g.BroadcastChannel(BRIDGE_CHANNEL);
79
+ ch.postMessage(msg);
80
+ try { ch.close(); } catch (e) {}
81
+ }
82
+ } catch (e) {}
83
+ try {
84
+ if (g.clients && typeof g.clients.matchAll === "function") {
85
+ g.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (list) {
86
+ for (var i = 0; i < list.length; i++) {
87
+ try { list[i].postMessage(msg); } catch (e) {}
88
+ }
89
+ }, function () {});
90
+ }
91
+ } catch (e) {}
92
+ }
93
+ g.addEventListener("notificationclick", function (event) {
94
+ try {
95
+ var n = event.notification;
96
+ var data = (n && n.data) || {};
97
+ var msg = {
98
+ source: "dsh-notify-me",
99
+ type: "notification",
100
+ action: event.action || "",
101
+ key: data.key || null,
102
+ sessionId: data.sessionId || null
103
+ };
104
+ try { if (n && typeof n.close === "function") n.close(); } catch (e) {}
105
+ if (typeof event.waitUntil === "function") {
106
+ event.waitUntil(Promise.resolve().then(function () { relay(msg); }));
107
+ } else {
108
+ relay(msg);
109
+ }
110
+ } catch (e) {}
111
+ });
112
+ }
113
+ if (isServiceWorkerScope(root)) { installNotificationBridge(root); return; }
114
+ if (!root.__ModuleLoader__ || typeof root.__ModuleLoader__.load !== "function") return;
115
+
116
+ root.__ModuleLoader__.load({
117
+ id: "dsh-notify-me",
118
+ factory: (require) => {
48
119
  var module = { exports: {} };
49
120
  var exports = module.exports;
50
121
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
@@ -72,7 +143,13 @@ window.__ModuleLoader__.load({
72
143
  toast: true,
73
144
  sound: true,
74
145
  volume: 0.5,
75
- autoFocus: true // clicking the toast focuses the DSH window
146
+ autoFocus: true, // clicking the toast focuses the DSH window
147
+ // Approval toasts carry 同意/拒绝 buttons and settle the request on one
148
+ // click (the same answer('allowed-once' | 'rejected') the in-page
149
+ // approval card sends). Buttons need persistent notifications, served
150
+ // through the Service Worker half of this same file; where that cannot
151
+ // run, the toast simply falls back to its plain form.
152
+ quickActions: true
76
153
  };
77
154
  function readConfig() {
78
155
  var cfg = {};
@@ -114,8 +191,13 @@ window.__ModuleLoader__.load({
114
191
  background: "后台会话",
115
192
  marker: "🔔 需要你 · ",
116
193
  sep: ":",
194
+ approve: "同意",
195
+ reject: "拒绝",
117
196
  testAttention: "这是一条测试提醒:DSH 需要你的操作",
118
- testDone: "测试提醒 · 回复完成示例"
197
+ testDone: "测试提醒 · 回复完成示例",
198
+ testApproval: "这是一条测试审批:点通知上的「同意 / 拒绝」按钮试试快捷裁决",
199
+ testApproved: "测试提醒 · 已模拟「同意」(仅测试,未改动任何会话)",
200
+ testRejected: "测试提醒 · 已模拟「拒绝」(仅测试,未改动任何会话)"
119
201
  },
120
202
  en: {
121
203
  kind: { approval: "Approval request", "plan-review": "Plan review", question: "Question" },
@@ -132,8 +214,13 @@ window.__ModuleLoader__.load({
132
214
  background: "Background session",
133
215
  marker: "🔔 Action needed · ",
134
216
  sep: ": ",
217
+ approve: "Approve",
218
+ reject: "Reject",
135
219
  testAttention: "This is a test alert: DSH is waiting for your input",
136
- testDone: "Test alert · reply-finished sample"
220
+ testDone: "Test alert · reply-finished sample",
221
+ testApproval: "This is a test approval: try quick decisions with the Approve / Reject buttons on the toast",
222
+ testApproved: "Test alert · simulated \"Approve\" (test only, nothing changed)",
223
+ testRejected: "Test alert · simulated \"Reject\" (test only, nothing changed)"
137
224
  }
138
225
  };
139
226
  // Locale service handle (registered on the client context by dsh-client-locale).
@@ -200,12 +287,227 @@ window.__ModuleLoader__.load({
200
287
  if (ws && typeof ws.openSession === "function") ws.openSession(sessionId);
201
288
  } catch (e) {}
202
289
  }
203
- function showToast(kind, title, body, sessionId) {
290
+ // ─────────────── approval quick-decision bridge ───────────────
291
+ // Action buttons exist only on persistent notifications
292
+ // (ServiceWorkerRegistration.showNotification); the Notification()
293
+ // constructor throws on an `actions` option. The worker half lives at the
294
+ // bottom of this file's wrapper and only relays clicks back here, where the
295
+ // pending interaction's own answer() settles the request.
296
+ var BRIDGE_CHANNEL = "dsh-notify-me:bridge";
297
+ var swRegistration = null;
298
+ var swState = "idle"; // idle | disabled | registering | active | unsupported:* | failed:*
299
+ var bridgeListening = false;
300
+ var actionChannel = null;
301
+ // Keyed by interaction key: every toast that carries buttons has a record
302
+ // here, so a click can only ever decide the exact request it was raised
303
+ // for. { sessionId, interaction, tag, test }
304
+ var pendingActions = Object.create(null);
305
+ // The single-record combo URL of THIS bundle — the only URL that may be
306
+ // registered as a worker, since a multi-plugin combo would evaluate every
307
+ // other plugin's factory in the worker and fail the registration. The boot
308
+ // graph publishes it per entry ({id, url, rev}).
309
+ function selfBundleUrl() {
310
+ try {
311
+ var boot = window.__DSH_BOOT__;
312
+ var entries = boot && boot.entries;
313
+ if (Array.isArray(entries)) {
314
+ for (var i = 0; i < entries.length; i++) {
315
+ var e = entries[i];
316
+ if (e && e.id === "dsh-notify-me" && typeof e.url === "string" && e.url) return e.url;
317
+ }
318
+ }
319
+ } catch (e) {}
320
+ return null;
321
+ }
322
+ function ensureActionBridge() {
323
+ if (!config.quickActions) { swState = "disabled"; return; }
324
+ if (swState === "active" || swState === "registering") return;
325
+ listenBridge();
326
+ try {
327
+ if (typeof navigator === "undefined" || !navigator.serviceWorker) {
328
+ swState = "unsupported: no service worker";
329
+ return;
330
+ }
331
+ if (window.isSecureContext === false) {
332
+ swState = "unsupported: insecure context";
333
+ return;
334
+ }
335
+ var url = selfBundleUrl();
336
+ if (!url) {
337
+ swState = "unsupported: bundle url unknown";
338
+ return;
339
+ }
340
+ swState = "registering";
341
+ navigator.serviceWorker.register(url).then(function (reg) {
342
+ try {
343
+ swRegistration = reg || null;
344
+ if (!reg) { swState = "failed: no registration"; return; }
345
+ if (reg.active) { swState = "active"; return; }
346
+ var w = reg.installing || reg.waiting;
347
+ if (!w) { swState = "active"; return; }
348
+ var settle = function () {
349
+ try {
350
+ if (reg.active || w.state === "activated") swState = "active";
351
+ else if (w.state === "redundant") swState = "failed: worker redundant";
352
+ } catch (e) {}
353
+ };
354
+ try { w.addEventListener("statechange", settle); } catch (e) {}
355
+ settle();
356
+ } catch (e) {
357
+ swState = "failed: " + ((e && e.message) || e);
358
+ }
359
+ }, function (err) {
360
+ swState = "failed: " + ((err && err.message) || err);
361
+ });
362
+ } catch (e) {
363
+ swState = "failed: " + ((e && e.message) || e);
364
+ }
365
+ }
366
+ function bridgeActive() {
367
+ return !!(swRegistration && swState === "active");
368
+ }
369
+ // Both relay paths end here; the worker's broadcast goes to every
370
+ // same-origin page and the postMessage to every window client, so the
371
+ // handler must be idempotent (decidePending() is a one-shot per key).
372
+ function listenBridge() {
373
+ if (bridgeListening) return;
374
+ bridgeListening = true;
375
+ try {
376
+ if (typeof navigator !== "undefined" && navigator.serviceWorker &&
377
+ typeof navigator.serviceWorker.addEventListener === "function") {
378
+ navigator.serviceWorker.addEventListener("message", function (ev) {
379
+ try { onBridgeMessage(ev && ev.data); } catch (e) {}
380
+ });
381
+ }
382
+ } catch (e) {}
383
+ try {
384
+ if (typeof BroadcastChannel === "function") {
385
+ actionChannel = new BroadcastChannel(BRIDGE_CHANNEL);
386
+ actionChannel.onmessage = function (ev) {
387
+ try { onBridgeMessage(ev && ev.data); } catch (e) {}
388
+ };
389
+ }
390
+ } catch (e) {}
391
+ }
392
+ function onBridgeMessage(msg) {
393
+ if (!msg || msg.source !== "dsh-notify-me" || msg.type !== "notification") return;
394
+ if (msg.action === "approve") { decidePending(msg.key, "allowed-once"); return; }
395
+ if (msg.action === "reject") { decidePending(msg.key, "rejected"); return; }
396
+ if (config.autoFocus) focusSession(msg.sessionId || null);
397
+ }
398
+ // The live interaction wins over the reference captured at alert time (the
399
+ // wait may have been replaced), and a key with no live record is stale —
400
+ // the notification outlived its request, so the click must decide nothing.
401
+ function findPendingByKey(key) {
402
+ try {
403
+ var svc = lookupService("uiSession");
404
+ var found = interactionStore(svc);
405
+ if (!found) return null;
406
+ var snap = found.store.getSnapshot();
407
+ if (!snap || typeof snap.forEach !== "function") return null;
408
+ var hit = null;
409
+ snap.forEach(function (value) {
410
+ if (hit) return;
411
+ var it = pendingOf(value, found.source);
412
+ if (it && it.key === key) hit = it;
413
+ });
414
+ return hit;
415
+ } catch (e) { return null; }
416
+ }
417
+ function closeNotificationTag(tag) {
418
+ if (!swRegistration || typeof swRegistration.getNotifications !== "function") return;
419
+ try {
420
+ var done = function (list) {
421
+ for (var i = 0; i < (list || []).length; i++) {
422
+ try { list[i].close(); } catch (e) {}
423
+ }
424
+ };
425
+ var r = swRegistration.getNotifications({ tag: tag });
426
+ if (r && typeof r.then === "function") r.then(done, function () {});
427
+ else done(r);
428
+ } catch (e) {}
429
+ }
430
+ // One toast button press. outcome: 'allowed-once' | 'rejected' — exactly
431
+ // the two decisions the in-page approval card exposes.
432
+ function decidePending(key, outcome) {
433
+ var rec = key ? pendingActions[key] : null;
434
+ if (!rec) return false; // already answered elsewhere, or a stale toast
435
+ delete pendingActions[key];
436
+ if (!rec.test) closeNotificationTag(rec.tag);
437
+ if (rec.test) {
438
+ // Test toasts settle nothing real; the feedback toast IS the proof that
439
+ // the button clicked all the way through the worker bridge.
440
+ deliver("done", texts().doneTitle, outcome === "allowed-once" ? texts().testApproved : texts().testRejected, null, null);
441
+ notifyEvent("decision", { key: key, outcome: outcome, sessionId: rec.sessionId || null, test: true });
442
+ return true;
443
+ }
444
+ var target = findPendingByKey(key) || rec.interaction;
445
+ if (!target || target.key !== key || typeof target.answer !== "function") return false;
446
+ try {
447
+ var p = target.answer(outcome);
448
+ if (p && typeof p.catch === "function") p.catch(function () {});
449
+ } catch (e) { return false; }
450
+ unmarkAttention(key);
451
+ notifyEvent("decision", { key: key, outcome: outcome, sessionId: rec.sessionId || null });
452
+ return true;
453
+ }
454
+ // Attach buttons to one alert. Only real approval interactions qualify:
455
+ // they are the only kind whose decision is a plain binary, and the only one
456
+ // whose object answers answer('allowed-once' | 'rejected').
457
+ function quickFor(key, it, sessionId) {
458
+ if (!config.quickActions || !config.toast) return null;
459
+ if (!it || it.kind !== "approval" || typeof it.answer !== "function") return null;
460
+ var q = {
461
+ key: key,
462
+ sessionId: sessionId || null,
463
+ interaction: it,
464
+ tag: "dsh-notify-me-attention-" + key,
465
+ test: false
466
+ };
467
+ pendingActions[key] = q;
468
+ return q;
469
+ }
470
+ function quickForTest(key) {
471
+ var q = {
472
+ key: key,
473
+ sessionId: null,
474
+ interaction: null,
475
+ tag: "dsh-notify-me-attention-" + key,
476
+ test: true
477
+ };
478
+ pendingActions[key] = q;
479
+ return q;
480
+ }
481
+
482
+ function showToast(kind, title, body, sessionId, quick) {
204
483
  if (!canToast()) return;
205
484
  try {
485
+ var tag = quick ? quick.tag : "dsh-notify-me-" + kind;
486
+ if (quick && bridgeActive()) {
487
+ var d = texts();
488
+ try {
489
+ swRegistration.showNotification(title, {
490
+ body: body || "",
491
+ tag: tag,
492
+ renotify: true,
493
+ // The plugin owns the audio (see showToast's silent note below).
494
+ silent: true,
495
+ icon: (location.origin || "") + "/favicon.svg",
496
+ data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test },
497
+ actions: [
498
+ { action: "approve", title: d.approve },
499
+ { action: "reject", title: d.reject }
500
+ ]
501
+ });
502
+ // No auto-close on purpose: an unanswered approval stays clickable
503
+ // (toast, then Action Center) until the request is settled — the
504
+ // wait-teardown path closes it through closeNotificationTag().
505
+ return;
506
+ } catch (e) { /* worker notifications unavailable -> plain toast */ }
507
+ }
206
508
  var n = new Notification(title, {
207
509
  body: body || "",
208
- tag: "dsh-notify-me-" + kind,
510
+ tag: tag,
209
511
  renotify: true,
210
512
  // The plugin owns the audio: its WebAudio cues already differ per
211
513
  // kind (attention vs done) and answer the Sound toggle and volume
@@ -327,6 +629,15 @@ window.__ModuleLoader__.load({
327
629
  }
328
630
  function unmarkAttention(key) {
329
631
  delete quietedAttention[key]; // the wait is over; a queued alert dies with it
632
+ // A toast that still carries buttons for this wait dies with it too —
633
+ // otherwise a settled request would leave a clickable (but stale)
634
+ // approval toast behind. Test toasts are exempt: their click is the
635
+ // proof, so they outlive the short title mark on purpose.
636
+ var rec = pendingActions[key];
637
+ if (rec) {
638
+ delete pendingActions[key];
639
+ if (!rec.test) closeNotificationTag(rec.tag);
640
+ }
330
641
  if (!attentionKeysMarked[key]) return;
331
642
  delete attentionKeysMarked[key];
332
643
  releaseTitleMark();
@@ -347,21 +658,24 @@ window.__ModuleLoader__.load({
347
658
  // buttons); respects the master switch and permission state. The title
348
659
  // marker is NOT touched here — its ownership belongs to the watchers
349
660
  // (markAttention / unmarkAttention), which keeps the counter balanced.
350
- function deliver(kind, title, body, sessionId) {
661
+ function notifyEvent(kind, payload) {
662
+ try {
663
+ if (window.__dshNotifyMe && window.__dshNotifyMe.onEvent) {
664
+ window.__dshNotifyMe.onEvent(kind, payload);
665
+ }
666
+ } catch (e) {}
667
+ }
668
+ function deliver(kind, title, body, sessionId, quick) {
351
669
  if (!config.enabled) return;
352
- showToast(kind, title, body, sessionId);
670
+ showToast(kind, title, body, sessionId, quick);
353
671
  if (kind === "attention") {
354
672
  playAttentionSound();
355
673
  } else {
356
674
  playDoneSound();
357
675
  }
358
- try {
359
- if (window.__dshNotifyMe && window.__dshNotifyMe.onEvent) {
360
- window.__dshNotifyMe.onEvent(kind, { title: title, body: body, sessionId: sessionId || null });
361
- }
362
- } catch (e) {}
676
+ notifyEvent(kind, { title: title, body: body, sessionId: sessionId || null });
363
677
  }
364
- function fire(kind, title, body, isCurrent, sessionId) {
678
+ function fire(kind, title, body, isCurrent, sessionId, quick) {
365
679
  if (!config.enabled) return false;
366
680
  if (kind === "attention" && config.attentionHiddenOnly && !hidden()) return false;
367
681
  // The wait belongs to the conversation the user is looking at right now:
@@ -370,7 +684,7 @@ window.__ModuleLoader__.load({
370
684
  // flushQuieted() re-delivers the alert the moment the page is backgrounded.
371
685
  if (kind === "attention" && quietedByVisibleSession(isCurrent)) return false;
372
686
  if (kind === "done" && config.doneHiddenOnly && !hidden()) return false;
373
- deliver(kind, title, body, sessionId);
687
+ deliver(kind, title, body, sessionId, quick);
374
688
  return true;
375
689
  }
376
690
  // True when this alert must stay silent purely because it is about the
@@ -390,7 +704,7 @@ window.__ModuleLoader__.load({
390
704
  for (var i = 0; i < keys.length; i++) {
391
705
  var copy = quietedAttention[keys[i]];
392
706
  delete quietedAttention[keys[i]];
393
- fire("attention", copy.title, copy.body, false, copy.sessionId);
707
+ fire("attention", copy.title, copy.body, false, copy.sessionId, copy.quick || null);
394
708
  }
395
709
  }
396
710
  // A display label for a session row ("current" maps to the localized
@@ -436,16 +750,17 @@ window.__ModuleLoader__.load({
436
750
  }
437
751
  // Returns null once the alert went out, or the copy it would have shown
438
752
  // when only the visible-session rule held it back — the caller queues that
439
- // copy by interaction key so it can still fire from the background.
440
- function fireAttention(kind, sessionLabel, detail, isCurrent, sessionId) {
753
+ // copy by interaction key so it can still fire from the background (the
754
+ // quick-decision payload rides along with the copy).
755
+ function fireAttention(kind, sessionLabel, detail, isCurrent, sessionId, quick) {
441
756
  var d = texts();
442
757
  var label = d.kind[kind] || d.kindDefault;
443
758
  var content = detail || d.bodyKind[kind] || d.bodyDefault;
444
759
  var who = sessionLabel || "";
445
760
  var title = "DSH · " + label;
446
761
  var body = (who ? who + d.sep : "") + content;
447
- if (quietedByVisibleSession(isCurrent)) return { title: title, body: body, sessionId: sessionId || null };
448
- fire("attention", title, body, isCurrent, sessionId);
762
+ if (quietedByVisibleSession(isCurrent)) return { title: title, body: body, sessionId: sessionId || null, quick: quick || null };
763
+ fire("attention", title, body, isCurrent, sessionId, quick);
449
764
  return null;
450
765
  }
451
766
  function fireDone(sessionLabel, snippet, sessionId) {
@@ -464,6 +779,20 @@ window.__ModuleLoader__.load({
464
779
  var TEST_MARK_KEY = "test:attention";
465
780
  function testAlert(kind) {
466
781
  var d = texts();
782
+ if (kind === "approval") {
783
+ // End-to-end proof of the quick-decision path: this toast carries the
784
+ // same 同意/拒绝 buttons as a real approval one, and clicking them runs
785
+ // the whole worker-bridge -> decidePending() chain (settling nothing
786
+ // real — the feedback toast says so).
787
+ var key = "test:approval:" + String(Date.now());
788
+ var quick = quickForTest(key);
789
+ deliver("attention", "DSH · " + d.kind.approval, d.testApproval, null, quick);
790
+ markAttention(key);
791
+ // The title mark drops like every other test's, but the toast is NOT
792
+ // closed with it — its buttons are the very thing being tested.
793
+ setTimeout(function () { unmarkAttention(key); }, TEST_MARK_RELEASE_MS);
794
+ return;
795
+ }
467
796
  if (kind === "attention") {
468
797
  deliver("attention", "DSH · " + d.kind.question, d.testAttention);
469
798
  markAttention(TEST_MARK_KEY);
@@ -578,7 +907,8 @@ window.__ModuleLoader__.load({
578
907
  // the title marker itself (see the uiSession watcher below).
579
908
  if (newKeys.length > 0) {
580
909
  var it = newKeys[0];
581
- var copy = fireAttention(it.kind, sessionLabelText(row), textFromPayload(it.payload), !!row.face, row.id);
910
+ var quick = quickFor(it.key, it, row.id);
911
+ var copy = fireAttention(it.kind, sessionLabelText(row), textFromPayload(it.payload), !!row.face, row.id, quick);
582
912
  if (copy) queueQuieted(it.key, copy);
583
913
  lastAttentionAt = Date.now();
584
914
  markAttention(it.key);
@@ -811,7 +1141,8 @@ window.__ModuleLoader__.load({
811
1141
  if (prev) unmarkAttention(prev.key);
812
1142
  if (attentionKeysFired[key]) { markAttention(key); return; } // already alerted, still needs its mark
813
1143
  attentionKeysFired[key] = true;
814
- var copy = fireAttention(it.kind, labelForSessionId(sid), textFromPayload(it), currentSessionId() === sid, sid);
1144
+ var quick = quickFor(key, it, sid);
1145
+ var copy = fireAttention(it.kind, labelForSessionId(sid), textFromPayload(it), currentSessionId() === sid, sid, quick);
815
1146
  if (copy) queueQuieted(key, copy);
816
1147
  lastAttentionAt = Date.now();
817
1148
  markAttention(key);
@@ -848,7 +1179,7 @@ window.__ModuleLoader__.load({
848
1179
  });
849
1180
  } catch (e) {}
850
1181
  // keep in sync with package.json "version" on every release
851
- api.version = "1.2.1";
1182
+ api.version = "1.3.1";
852
1183
  api.setConfig = function (patch) { return setConfigLocal(patch); };
853
1184
  api.resetConfig = function () {
854
1185
  try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
@@ -858,10 +1189,15 @@ window.__ModuleLoader__.load({
858
1189
  // manual test; run it after clicking anywhere so permission can be granted.
859
1190
  // Test alerts fire regardless of the page-open visibility rules.
860
1191
  api.test = function (kind) {
861
- kind = kind === "attention" ? "attention" : "done";
1192
+ kind = (kind === "attention" || kind === "approval") ? kind : "done";
862
1193
  unlockOnce();
863
1194
  testAlert(kind);
864
- return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ")";
1195
+ return "sent " + kind + " (lang=" + effectiveLang() + ", permission=" + ("Notification" in window ? Notification.permission : "n/a") + ", bridge=" + swState + ")";
1196
+ };
1197
+ // Programmatic twin of a toast button press: settles the pending approval
1198
+ // behind `key` the same way the 同意/拒绝 buttons do.
1199
+ api.decide = function (key, outcome) {
1200
+ return decidePending(key, outcome === "rejected" ? "rejected" : "allowed-once");
865
1201
  };
866
1202
  // Live diagnostics for the "why did nothing pop" case. The needs-input
867
1203
  // channel has three possible owners on current hosts (uiSession.sessionStatus
@@ -878,6 +1214,11 @@ window.__ModuleLoader__.load({
878
1214
  doneHiddenOnly: config.doneHiddenOnly,
879
1215
  toast: config.toast,
880
1216
  sound: config.sound,
1217
+ quickActions: config.quickActions,
1218
+ // quick-decision bridge: 'active' is the only state in which approval
1219
+ // toasts carry working 同意/拒绝 buttons.
1220
+ bridge: swState,
1221
+ actionKeys: Object.keys(pendingActions),
881
1222
  permission: ("Notification" in window) ? Notification.permission : "n/a",
882
1223
  visibility: hidden() ? "hidden" : "visible",
883
1224
  uiSession: uiPending ? "bound" : "unbound",
@@ -915,6 +1256,11 @@ window.__ModuleLoader__.load({
915
1256
  doneVisibleHint: "默认关闭:「完成」只在页面隐藏或切到后台时才提醒,避免打扰。",
916
1257
  autoFocus: "点击通知回到对应对话",
917
1258
  autoFocusHint: "默认开启:点系统通知会把 DSH 带到前台,并直接切到这条提醒所属的对话;关掉后点击通知不做任何事。",
1259
+ quickActions: "审批通知上直接裁决",
1260
+ quickActionsHint: "默认开启:权限审批的通知上带「同意 / 拒绝」按钮,点一下即按审批卡片的「允许一次 / 拒绝」直接完成裁决,不用切回页面。需要支持 Service Worker 的浏览器(http://127.0.0.1 或 https);用不了时自动退回普通通知。",
1261
+ quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
1262
+ bridgeOff: "快捷裁决按钮暂不可用(",
1263
+ bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
918
1264
  volume: "音量",
919
1265
  language: "通知语言",
920
1266
  langAuto: "跟随界面",
@@ -926,6 +1272,7 @@ window.__ModuleLoader__.load({
926
1272
  testHint: "用当前设置各发一条测试提醒(不受上面「页面打开时也提醒」开关限制)。",
927
1273
  testAttention: "测试「需要你」",
928
1274
  testDone: "测试「回复完成」",
1275
+ testApprovalBtn: "测试「审批按钮」",
929
1276
  reset: "恢复默认设置",
930
1277
  resetDone: "已恢复默认设置。",
931
1278
  sent: "已发送测试提醒 · 通知语言:",
@@ -949,6 +1296,11 @@ window.__ModuleLoader__.load({
949
1296
  doneVisibleHint: "Off by default: \"done\" only alerts while the page is hidden or backgrounded, to avoid noise.",
950
1297
  autoFocus: "Click a toast to open that conversation",
951
1298
  autoFocusHint: "On by default: clicking a toast brings DSH forward and switches to the conversation the alert came from. When off, clicking a toast does nothing.",
1299
+ quickActions: "Decide approvals from the toast",
1300
+ quickActionsHint: "On by default: approval notifications carry Approve / Reject buttons, and one click settles the request exactly like the Allow once / Reject buttons on the approval card. Needs a Service Worker capable browser (http://127.0.0.1 or https); falls back to plain toasts where unavailable.",
1301
+ quickApprovalNote: "\"Approve\" equals \"Allow once\" on the approval card — it allows that one call only.",
1302
+ bridgeOff: "Quick-decision buttons are unavailable right now (",
1303
+ bridgeOffTail: "); approval toasts fall back to their plain form.",
952
1304
  volume: "Volume",
953
1305
  language: "Notification language",
954
1306
  langAuto: "Follow the interface",
@@ -960,6 +1312,7 @@ window.__ModuleLoader__.load({
960
1312
  testHint: "Send one test alert with the current settings (not limited by the \"while the page is open\" toggles above).",
961
1313
  testAttention: "Test \"needs you\"",
962
1314
  testDone: "Test \"reply finished\"",
1315
+ testApprovalBtn: "Test approval buttons",
963
1316
  reset: "Restore defaults",
964
1317
  resetDone: "Defaults restored.",
965
1318
  sent: "Test alert sent · notification language: ",
@@ -1047,7 +1400,9 @@ window.__ModuleLoader__.load({
1047
1400
  // page is open, otherwise the button proves nothing.
1048
1401
  testAlert(kind);
1049
1402
  var langName = effectiveLang() === "en" ? t("langEn") : t("langZh");
1050
- setFeedback(t("sent") + langName);
1403
+ var msg = t("sent") + langName;
1404
+ if (kind === "approval" && !bridgeActive()) msg += " · " + t("bridgeOff") + swState + t("bridgeOffTail");
1405
+ setFeedback(msg);
1051
1406
  }
1052
1407
  var permOK = ("Notification" in window) ? Notification.permission === "granted" : false;
1053
1408
  var resolvedLang = effectiveLang();
@@ -1067,6 +1422,10 @@ window.__ModuleLoader__.load({
1067
1422
  rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
1068
1423
  rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
1069
1424
  rowSwitch("autoFocus", t("autoFocus"), t("autoFocusHint"), cfg.autoFocus, function (v) { patch({ autoFocus: v }); }),
1425
+ rowSwitch("quickActions", t("quickActions"), t("quickActionsHint"), cfg.quickActions, function (v) {
1426
+ patch({ quickActions: v });
1427
+ if (v) ensureActionBridge();
1428
+ }),
1070
1429
  h("div", { className: "dnm-row" }, [
1071
1430
  h("div", { className: "dnm-rowText" }, [
1072
1431
  h("div", { className: "dnm-rowLabel" }, t("volume")),
@@ -1106,9 +1465,15 @@ window.__ModuleLoader__.load({
1106
1465
  ]),
1107
1466
  h("div", { className: "dnm-btnRow" }, [
1108
1467
  h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
1109
- h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone"))
1468
+ h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("done"); } }, t("testDone")),
1469
+ h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled || !cfg.quickActions, onClick: function () { runTest("approval"); } }, t("testApprovalBtn"))
1110
1470
  ])
1111
1471
  ]),
1472
+ cfg.quickActions && cfg.toast && !bridgeActive() ? h("div", { className: "dnm-row" }, [
1473
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("bridgeOff") + swState + t("bridgeOffTail"))])
1474
+ ]) : h("div", { className: "dnm-row" }, [
1475
+ h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("quickApprovalNote"))])
1476
+ ]),
1112
1477
  h("div", { className: "dnm-row" }, [
1113
1478
  h("div", { className: "dnm-rowText" }, [
1114
1479
  h("div", { className: "dnm-rowLabel" }, t("reset")),
@@ -1197,6 +1562,10 @@ window.__ModuleLoader__.load({
1197
1562
  var onVisibility = function () { try { flushQuieted(); } catch (e) {} };
1198
1563
  try { document.addEventListener("visibilitychange", onVisibility); } catch (e) {}
1199
1564
  unsubs.push(function () { try { document.removeEventListener("visibilitychange", onVisibility); } catch (e) {} });
1565
+ // Quick-decision bridge: register the worker half of this bundle (a
1566
+ // no-op where quickActions is off or a worker cannot run) and start
1567
+ // listening for toast-button relays.
1568
+ try { ensureActionBridge(); } catch (e) {}
1200
1569
  // Interaction source first: it owns the title marker once present, so
1201
1570
  // binding it before the controller scan keeps a single mark per wait.
1202
1571
  bindUiSession();
@@ -1225,5 +1594,6 @@ window.__ModuleLoader__.load({
1225
1594
  exports.inject = inject;
1226
1595
  exports.apply = apply;
1227
1596
  return module.exports;
1228
- }
1229
- });
1597
+ }
1598
+ });
1599
+ })(typeof window !== "undefined" ? window : typeof self !== "undefined" ? self : null);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-notify-me",
3
- "version": "1.2.1",
4
- "description": "DSH 桌面消息提醒插件:模型需要你操作(审批/方案确认/提问的可操作提醒)或回复在后台完成时,系统通知+提示音+标签页标题提醒你;可在设置页开关并切换通知语言。Desktop notification & message alerts for DeepSeek Harness web — toasts, sounds and a tab-title marker when the agent needs your input (approval / plan review / question) or a reply finishes in the background; toggleable from Settings with zh/en notification language.",
3
+ "version": "1.3.1",
4
+ "description": "DSH 桌面消息提醒插件:模型需要你操作(审批/方案确认/提问的可操作提醒)或回复在后台完成时,系统通知+提示音+标签页标题提醒你;权限审批的通知上可直接点「同意/拒绝」完成裁决;可在设置页开关并切换通知语言。Desktop notification & message alerts for DeepSeek Harness web — toasts, sounds and a tab-title marker when the agent needs your input (approval / plan review / question) or a reply finishes in the background, Approve/Reject buttons on approval toasts to decide right from the notification; toggleable from Settings with zh/en notification language.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",
@@ -34,10 +34,12 @@
34
34
  "message-alert",
35
35
  "needs-your-input",
36
36
  "approval-alert",
37
+ "approval-actions",
37
38
  "background-alert",
38
39
  "桌面通知",
39
40
  "消息提醒",
40
41
  "可操作提醒",
42
+ "审批提醒",
41
43
  "提醒插件",
42
44
  "需要你操作",
43
45
  "回复完成提醒",