dsh-notify-me 1.2.0 → 1.3.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 +30 -4
- package/README.en.md +23 -9
- package/README.md +23 -9
- package/lib/client.js +406 -31
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.3.0] — 2026-10-03
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **审批通知上直接裁决**(设置页新开关「审批通知上直接裁决」,默认开启):权限审批的系统通知带上「同意 / 拒绝」按钮,点一下就完成裁决,不用切回页面。按钮调的是审批卡片自己的 `PendingApproval.answer('allowed-once' | 'rejected')`——「同意」即「允许一次」,只放行这一次,与卡片上的按钮完全同义;点通知正文仍是 1.1.8 的「回到对应对话」。
|
|
9
|
+
- 绕开的是一条浏览器硬限制:`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,注册必挂。
|
|
10
|
+
- 按钮通知按交互 key 记账:一条等待一次只认领一张通知;页面里先答掉、或被新请求顶替之后,残留通知上的按钮再点不裁决任何东西,只把通知关掉。请求消失时残留通知一并关闭;带按钮的通知不设 15 秒自动消失,未裁决的审批留在通知中心等你。
|
|
11
|
+
- `window.__dshNotifyMe` 新增 `decide(key, outcome)`(按钮的程序化同款,`outcome` 取 `"allowed-once"` / `"rejected"`)与 `test("approval")`(设置页新增「测试『审批按钮』」按钮,一条通知端到端验证按钮链路);`debug()` 新增 `quickActions` / `bridge`(`active` = 按钮可用,否则给出不可用原因)/ `actionKeys`;`onEvent` 新增 `decision` 事件(`{key, outcome, sessionId}`,测试点击另带 `test: true`)。
|
|
12
|
+
|
|
13
|
+
### 变更
|
|
14
|
+
- 带按钮的审批通知改用 `dsh-notify-me-attention-<key>` 独立标签,多条待审批互不顶掉;提问 / 方案确认沿用原来的 `dsh-notify-me-attention` 标签行为。
|
|
15
|
+
- 不支持 Service Worker 的环境(`http://` 局域网地址等)自动退回 1.2.x 的无按钮通知,其余功能不受影响,`debug().bridge` 显示具体原因。
|
|
16
|
+
- 版本号 1.2.1 → 1.3.0;`window.__dshNotifyMe.version` 同步。
|
|
17
|
+
|
|
18
|
+
### 测试
|
|
19
|
+
- `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 链路。
|
|
20
|
+
- `smoke/cordis-host-test.mjs` 在 `sessionStatus` 宿主上补真 cordis 语义的裁决用例:`decide()` 经 `ctx.get("uiSession")` 找到活着的交互并调它的 `answer()`,重复裁决与未知 key 拒绝,标记随之释放。
|
|
21
|
+
|
|
22
|
+
### 已知限制
|
|
23
|
+
- 按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;DSH Desktop(Electron)里按钮能否渲染未实测,不渲染时按上述降级走无按钮通知。
|
|
24
|
+
|
|
25
|
+
## [1.2.1] — 2026-10-03
|
|
26
|
+
|
|
27
|
+
### 修复
|
|
28
|
+
- **提示音叠着响两声**:`new Notification(...)` 没带 `silent: true`,平台自带通知音与插件自己的 WebAudio 提示音各响一遍。现在通知一律静音,声音统一由插件承担(attention / done 两种音型、音量设置不变);设置页「提示音」关掉即完全安静。
|
|
29
|
+
|
|
30
|
+
### 变更
|
|
31
|
+
- `smoke/cordis-host-test.mjs` 借用 cordis 的探测路径补上 `~/.dsh/profiles/node_modules`(`healProfilesModuleFallback` 维护的平铺回退目录):此前只探测 `profiles/web/node_modules`,多数机器上最强的那套真 cordis 端到端用例会静默跳过。
|
|
32
|
+
|
|
5
33
|
## [1.2.0] — 2026-09-30
|
|
6
34
|
|
|
7
35
|
### 修复
|
|
@@ -9,11 +37,9 @@ All notable changes to **dsh-notify-me** are documented here.
|
|
|
9
37
|
- **`0.1.6` 及以后的宿主上认不出「当前会话」**:会话列表快照不再有 `current`(`SessionListState` 只剩 `ids / byId / phase / projectionsBySession`),于是当前会话面(face)订阅从未建立、`onListChanged()` 还把当前会话当成后台会话评估——后果是 1.1.7 的「当前对话不弹通知」规则失效(审批卡片就在屏幕上,Toast 仍会盖上去),标签页标记的归属也随之错位。现在按宿主自己的口径推导:有 `current` 用它,没有就取 `retainedBy.mainView > 0` 的那一行(与内置 `DocumentTitle` / `ui-workspace` / `ui-session` 的判定一致)。
|
|
10
38
|
- **`0.1.6` 及以后点通知切不回对应会话**:`sessions.open(id)` 已被移除,导航改由 `uiWorkspace.openSession(id)` 承担;插件现在两个都试(`open` 优先,旧宿主行为不变),会话已不在列表时依旧不调用。`debug()` 新增 `currentSession` 便于核对。
|
|
11
39
|
|
|
12
|
-
### 新增
|
|
13
|
-
- `sessionStatus` 适配完整:同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
|
|
14
|
-
|
|
15
40
|
### 变更
|
|
16
|
-
-
|
|
41
|
+
- 提醒语义在适配后原样保持(1.1.x 既有逻辑,不是新行为):同一会话里等待被替换成新 key 时旧的标题标记随之释放,等待消失(`pendingInteraction` 变 `null`)时按 key 释放标记,重复通知不重复提醒。
|
|
42
|
+
- `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` 的实测声明为准。
|
|
17
43
|
- 版本号 1.1.8 → 1.2.0(新宿主代际);`window.__dshNotifyMe.version` 同步。
|
|
18
44
|
- 设置页字典注册改为把 `locale.register()` 返回的 disposer 交给 `ctx.effect`(与官方 `ui-approval` 写法一致),客户端插件 HMR 重载时不会因「同名 namespace 已注册」而丢掉设置页。
|
|
19
45
|
- `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
|
[](https://www.npmjs.com/package/dsh-notify-me)
|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
10
10
|
[](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 Windows 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
|
|
|
@@ -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 |
|
|
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,15 @@ 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
|
+
- **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
56
|
- **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
|
|
56
|
-
- **Test buttons** — send one "needs you"
|
|
57
|
+
- **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
58
|
|
|
58
59
|
> Notification permission is required: click once on the page → **Allow** (or address-bar lock → Site settings → Notifications → Allow → reload).
|
|
59
60
|
|
|
60
61
|
## How it alerts
|
|
61
62
|
|
|
62
|
-
- **System notification** — native notification-center toast (clicking it brings the DSH window back to front)
|
|
63
|
+
- **System notification** — native notification-center toast (clicking it brings the DSH window back to front; approval toasts can be decided straight from the buttons)
|
|
63
64
|
- **Sound** — WebAudio beeps (distinct patterns for "needs you" vs "done")
|
|
64
65
|
- **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`
|
|
65
66
|
|
|
@@ -78,10 +79,12 @@ Restart `dsh web`, then hard-refresh the page (Ctrl+Shift+R).
|
|
|
78
79
|
|
|
79
80
|
| Host generation | "Needs you" interaction source | Current session | Toast-click navigation |
|
|
80
81
|
| --- | --- | --- | --- |
|
|
81
|
-
| `<= 0.1.
|
|
82
|
-
| `0.1.2-alpha.2 .. 0.1.
|
|
82
|
+
| `<= 0.1.2-alpha.1` | controller snapshot `pending[]` | snapshot `current` | `sessions.open()` |
|
|
83
|
+
| `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | snapshot `current` | `sessions.open()` |
|
|
83
84
|
| `>= 0.1.6-alpha.2` (incl. `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
84
85
|
|
|
86
|
+
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.
|
|
87
|
+
|
|
85
88
|
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
89
|
|
|
87
90
|
**Verify** — open the DevTools console and run:
|
|
@@ -89,6 +92,8 @@ Verified to actually activate on `0.1.2-rc.1`, `0.1.5-rc.2` and `0.2.0-rc.2` (ea
|
|
|
89
92
|
```js
|
|
90
93
|
window.__dshNotifyMe.test("done") // "reply finished" sample
|
|
91
94
|
window.__dshNotifyMe.test("attention") // "needs your input" sample
|
|
95
|
+
window.__dshNotifyMe.test("approval") // "approval buttons" sample: try the Approve / Reject buttons on the toast
|
|
96
|
+
window.__dshNotifyMe.debug() // the bridge field reports the button channel: "active" = buttons work
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
Nothing happened? 90% of the time it's one of:
|
|
@@ -111,9 +116,11 @@ window.__dshNotifyMe.setConfig({
|
|
|
111
116
|
toast: true, // system-notification toggle
|
|
112
117
|
sound: true, // sound toggle
|
|
113
118
|
volume: 0.5, // 0..1
|
|
114
|
-
autoFocus: true
|
|
119
|
+
autoFocus: true, // clicking the toast focuses the DSH window
|
|
120
|
+
quickActions: true // Approve / Reject buttons on approval toasts
|
|
115
121
|
})
|
|
116
122
|
window.__dshNotifyMe.resetConfig() // restore defaults
|
|
123
|
+
window.__dshNotifyMe.decide("approval:3", "allowed-once") // programmatic decision: "allowed-once" | "rejected"
|
|
117
124
|
```
|
|
118
125
|
|
|
119
126
|
## How it works
|
|
@@ -126,12 +133,19 @@ The reminder core picks its interaction source per host generation (see the tabl
|
|
|
126
133
|
|
|
127
134
|
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
135
|
|
|
136
|
+
### Quick decisions, and how they reach the agent
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
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.
|
|
141
|
+
|
|
129
142
|
## Known limitations
|
|
130
143
|
|
|
131
144
|
- 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
145
|
- 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.
|
|
133
146
|
- The first sound/toast of each page load needs one user click on the page first (browser autoplay + permission policy).
|
|
134
147
|
- Toasts are only produced once the notification permission is granted; declining means sound + title marker only.
|
|
148
|
+
- 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.
|
|
135
149
|
- Config lives in browser `localStorage`: switching browsers/devices or clearing site data returns to defaults (one-click restore in the Settings page).
|
|
136
150
|
- 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
151
|
- 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.
|
|
@@ -141,7 +155,7 @@ The reminder core stays dependency-free and self-contained: notification copy re
|
|
|
141
155
|
```powershell
|
|
142
156
|
node --check lib\client.js
|
|
143
157
|
node --check lib\index.js
|
|
144
|
-
node smoke\smoke-test.cjs # offline state-machine test: three host generations + master switch + zh/en cases
|
|
158
|
+
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
159
|
node smoke\cordis-host-test.mjs # real-cordis end-to-end (skips when no local DSH installation is found)
|
|
146
160
|
npm pack --dry-run # preview the published tarball
|
|
147
161
|
```
|
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/dsh-notify-me)
|
|
7
7
|

|
|
8
8
|

|
|
9
|
-

|
|
10
10
|
[](https://awesome-dsh-plugin.com)
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
**离开 DSH 页面也不错过任何动静。** 当模型停下来需要你操作(审批 / 方案待确认 / 提问)、或你在别的软件时回复正好在后台完成,dsh-notify-me 会用系统通知 + 提示音 +
|
|
14
|
+
**离开 DSH 页面也不错过任何动静。** 当模型停下来需要你操作(审批 / 方案待确认 / 提问)、或你在别的软件时回复正好在后台完成,dsh-notify-me 会用系统通知 + 提示音 + 标签页标题标记提醒你——权限审批的通知还带「同意 / 拒绝」按钮,点一下就完成裁决,不用切回页面。开关和通知语言都直接在 **DSH 设置 → 通知提醒** 里改。
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -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,15 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
52
52
|
- **启用提醒** 主开关:关闭后不再弹系统通知、不播放提示音、也不改标签页标题;
|
|
53
53
|
- **系统通知 / 提示音 / 音量**:Toast、WebAudio 提示音与音量滑块;
|
|
54
54
|
- **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
|
|
55
|
+
- **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」;
|
|
55
56
|
- **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
|
|
56
|
-
-
|
|
57
|
+
- **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失)。
|
|
57
58
|
|
|
58
59
|
> 需要浏览器通知权限:页面上点一下 → 允许;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新。
|
|
59
60
|
|
|
60
61
|
## 提醒方式
|
|
61
62
|
|
|
62
|
-
- **系统通知**:Windows 通知中心 Toast(点击可把 DSH
|
|
63
|
+
- **系统通知**:Windows 通知中心 Toast(点击可把 DSH 窗口切回前台;审批的通知点按钮直接裁决)
|
|
63
64
|
- **提示音**:WebAudio 合成音(「需要你」与「完成」使用不同音型)
|
|
64
65
|
- **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`
|
|
65
66
|
|
|
@@ -78,10 +79,12 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
78
79
|
|
|
79
80
|
| 宿主代际 | 「需要你」交互来源 | 判断当前会话 | 点通知切会话 |
|
|
80
81
|
| --- | --- | --- | --- |
|
|
81
|
-
| `≤ 0.1.
|
|
82
|
-
| `0.1.2-alpha.2 .. 0.1.
|
|
82
|
+
| `≤ 0.1.2-alpha.1` | 控制器快照的 `pending[]` | 快照 `current` | `sessions.open()` |
|
|
83
|
+
| `0.1.2-alpha.2 .. 0.1.6-alpha.1` | `uiSession.pendingInteractions` | 快照 `current` | `sessions.open()` |
|
|
83
84
|
| `≥ 0.1.6-alpha.2`(含 `0.2.0-rc.2`) | `uiSession.sessionStatus` | `retainedBy.mainView` | `uiWorkspace.openSession()` |
|
|
84
85
|
|
|
86
|
+
区间仅作示意:插件按 store 形状自动选源(`sessionStatus` 存在即走新路),版本号只是各形状的出现边界。
|
|
87
|
+
|
|
85
88
|
实测**真正激活**(各自独立 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
89
|
|
|
87
90
|
**验证** — F12 控制台执行:
|
|
@@ -89,6 +92,8 @@ dsh plugin --profile web add dsh-notify-me
|
|
|
89
92
|
```js
|
|
90
93
|
window.__dshNotifyMe.test("done") // 「完成」示例
|
|
91
94
|
window.__dshNotifyMe.test("attention") // 「需要你」示例
|
|
95
|
+
window.__dshNotifyMe.test("approval") // 「审批按钮」示例:通知上有「同意 / 拒绝」,点着试试
|
|
96
|
+
window.__dshNotifyMe.debug() // 桥接状态看 bridge 字段,active = 按钮可用
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
没反应?九成是:通知权限被拒绝(在页面上点一下 → 选「允许」;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新)、装完没重启 DSH、系统设置里浏览器通知被关。
|
|
@@ -107,9 +112,11 @@ window.__dshNotifyMe.setConfig({
|
|
|
107
112
|
toast: true, // 系统通知开关
|
|
108
113
|
sound: true, // 提示音开关
|
|
109
114
|
volume: 0.5, // 音量 0~1
|
|
110
|
-
autoFocus: true
|
|
115
|
+
autoFocus: true, // 点通知切回 DSH 窗口
|
|
116
|
+
quickActions: true // 审批通知上的「同意 / 拒绝」按钮
|
|
111
117
|
})
|
|
112
118
|
window.__dshNotifyMe.resetConfig() // 恢复默认
|
|
119
|
+
window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:"allowed-once" | "rejected"
|
|
113
120
|
```
|
|
114
121
|
|
|
115
122
|
## 工作原理
|
|
@@ -122,12 +129,19 @@ window.__dshNotifyMe.resetConfig() // 恢复默认
|
|
|
122
129
|
|
|
123
130
|
提醒核心仍然零第三方运行时依赖、完全自包含:通知文字按所选语言(跟随界面 / 中文 / English)即时解析。设置页是**可选**的 React 呈现层——当 DSH web profile 提供 `slots` / `locale` / `react` 时才注册进「设置 → 通知提醒」,缺任一能力时插件自动降级为纯提醒(无设置页),不影响功能。
|
|
124
131
|
|
|
132
|
+
### 快捷裁决怎么走通的
|
|
133
|
+
|
|
134
|
+
通知按钮(`actions`)只属于 Service Worker 弹的持久通知,往 `new Notification()` 里传 `actions` 会直接抛 `TypeError`——这是绕不开的浏览器限制。所以 bundle 一份文件干两件事:页面里照常注册提醒工厂,同一份字节再注册成一个只干转信的 Service Worker,把「点了哪个按钮」(同意 / 拒绝 / 点正文)带回页面。真正的裁决发生在页面里,调的是审批卡片自己用的 `PendingApproval.answer('allowed-once' | 'rejected')`,所以「同意」就是「允许一次」,不多放行任何东西。
|
|
135
|
+
|
|
136
|
+
每条按钮通知按交互 key 记账,一次只认领一条等待;请求被页面里答掉、或被新请求顶替之后,残留通知上的按钮再点也不会误裁,只是安静地关掉。带按钮的通知不设自动消失——审批没裁决就一直留在通知中心里等你。
|
|
137
|
+
|
|
125
138
|
## 已知限制
|
|
126
139
|
|
|
127
140
|
- 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
|
|
128
141
|
- 通知经由浏览器弹出,需允许浏览器通知权限,且勿扰模式不能屏蔽它。
|
|
129
142
|
- 每次页面加载后第一次出声/弹通知前,需在页面上点击过一次(浏览器自动播放与权限策略)。
|
|
130
143
|
- 未授权通知权限时只有提示音与标题标记。
|
|
144
|
+
- 快捷裁决按钮要 Service Worker:`http://127.0.0.1`、`https` 可用,`http://192.168.x.x` 这类局域网地址不行;浏览器或桌面宿主不渲染通知按钮时,`window.__dshNotifyMe.debug()` 的 `bridge` 字段会说明原因,通知自动退回无按钮样式,其余功能不受影响。
|
|
131
145
|
- 配置存在浏览器 `localStorage`:换浏览器/设备或清除站点数据后会回到默认值(设置页可一键恢复默认)。
|
|
132
146
|
- `≥ 0.1.6`(含 `0.2.0-rc.2`)的「回复完成」提醒正文只有会话名,不再附带回复摘要——宿主快照已不再提供 `nodes`;提醒本身照常触发。
|
|
133
147
|
- 标题标记由宿主与插件共同写 `document.title`:宿主重算标题时标记可能被覆盖,直到下一次提醒事件重新写成。
|
|
@@ -137,7 +151,7 @@ window.__dshNotifyMe.resetConfig() // 恢复默认
|
|
|
137
151
|
```powershell
|
|
138
152
|
node --check lib\client.js
|
|
139
153
|
node --check lib\index.js
|
|
140
|
-
node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例
|
|
154
|
+
node smoke\smoke-test.cjs # 离线状态机冒烟测试:三代宿主 + 主开关 + 中英语言用例 + 快捷裁决桥(Service Worker 半侧一并跑)
|
|
141
155
|
node smoke\cordis-host-test.mjs # 真 cordis 端到端(找不到本机 DSH 时自动跳过)
|
|
142
156
|
npm pack --dry-run # 预览发布包
|
|
143
157
|
```
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,13 +287,233 @@ window.__ModuleLoader__.load({
|
|
|
200
287
|
if (ws && typeof ws.openSession === "function") ws.openSession(sessionId);
|
|
201
288
|
} catch (e) {}
|
|
202
289
|
}
|
|
203
|
-
|
|
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:
|
|
510
|
+
tag: tag,
|
|
209
511
|
renotify: true,
|
|
512
|
+
// The plugin owns the audio: its WebAudio cues already differ per
|
|
513
|
+
// kind (attention vs done) and answer the Sound toggle and volume
|
|
514
|
+
// slider. Without this flag the platform plays its own chime on top
|
|
515
|
+
// of the beep — two sounds stacked on one alert.
|
|
516
|
+
silent: true,
|
|
210
517
|
icon: (location.origin || "") + "/favicon.svg"
|
|
211
518
|
});
|
|
212
519
|
if (config.autoFocus) {
|
|
@@ -322,6 +629,15 @@ window.__ModuleLoader__.load({
|
|
|
322
629
|
}
|
|
323
630
|
function unmarkAttention(key) {
|
|
324
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
|
+
}
|
|
325
641
|
if (!attentionKeysMarked[key]) return;
|
|
326
642
|
delete attentionKeysMarked[key];
|
|
327
643
|
releaseTitleMark();
|
|
@@ -342,21 +658,24 @@ window.__ModuleLoader__.load({
|
|
|
342
658
|
// buttons); respects the master switch and permission state. The title
|
|
343
659
|
// marker is NOT touched here — its ownership belongs to the watchers
|
|
344
660
|
// (markAttention / unmarkAttention), which keeps the counter balanced.
|
|
345
|
-
function
|
|
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) {
|
|
346
669
|
if (!config.enabled) return;
|
|
347
|
-
showToast(kind, title, body, sessionId);
|
|
670
|
+
showToast(kind, title, body, sessionId, quick);
|
|
348
671
|
if (kind === "attention") {
|
|
349
672
|
playAttentionSound();
|
|
350
673
|
} else {
|
|
351
674
|
playDoneSound();
|
|
352
675
|
}
|
|
353
|
-
|
|
354
|
-
if (window.__dshNotifyMe && window.__dshNotifyMe.onEvent) {
|
|
355
|
-
window.__dshNotifyMe.onEvent(kind, { title: title, body: body, sessionId: sessionId || null });
|
|
356
|
-
}
|
|
357
|
-
} catch (e) {}
|
|
676
|
+
notifyEvent(kind, { title: title, body: body, sessionId: sessionId || null });
|
|
358
677
|
}
|
|
359
|
-
function fire(kind, title, body, isCurrent, sessionId) {
|
|
678
|
+
function fire(kind, title, body, isCurrent, sessionId, quick) {
|
|
360
679
|
if (!config.enabled) return false;
|
|
361
680
|
if (kind === "attention" && config.attentionHiddenOnly && !hidden()) return false;
|
|
362
681
|
// The wait belongs to the conversation the user is looking at right now:
|
|
@@ -365,7 +684,7 @@ window.__ModuleLoader__.load({
|
|
|
365
684
|
// flushQuieted() re-delivers the alert the moment the page is backgrounded.
|
|
366
685
|
if (kind === "attention" && quietedByVisibleSession(isCurrent)) return false;
|
|
367
686
|
if (kind === "done" && config.doneHiddenOnly && !hidden()) return false;
|
|
368
|
-
deliver(kind, title, body, sessionId);
|
|
687
|
+
deliver(kind, title, body, sessionId, quick);
|
|
369
688
|
return true;
|
|
370
689
|
}
|
|
371
690
|
// True when this alert must stay silent purely because it is about the
|
|
@@ -385,7 +704,7 @@ window.__ModuleLoader__.load({
|
|
|
385
704
|
for (var i = 0; i < keys.length; i++) {
|
|
386
705
|
var copy = quietedAttention[keys[i]];
|
|
387
706
|
delete quietedAttention[keys[i]];
|
|
388
|
-
fire("attention", copy.title, copy.body, false, copy.sessionId);
|
|
707
|
+
fire("attention", copy.title, copy.body, false, copy.sessionId, copy.quick || null);
|
|
389
708
|
}
|
|
390
709
|
}
|
|
391
710
|
// A display label for a session row ("current" maps to the localized
|
|
@@ -431,16 +750,17 @@ window.__ModuleLoader__.load({
|
|
|
431
750
|
}
|
|
432
751
|
// Returns null once the alert went out, or the copy it would have shown
|
|
433
752
|
// when only the visible-session rule held it back — the caller queues that
|
|
434
|
-
// copy by interaction key so it can still fire from the background
|
|
435
|
-
|
|
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) {
|
|
436
756
|
var d = texts();
|
|
437
757
|
var label = d.kind[kind] || d.kindDefault;
|
|
438
758
|
var content = detail || d.bodyKind[kind] || d.bodyDefault;
|
|
439
759
|
var who = sessionLabel || "";
|
|
440
760
|
var title = "DSH · " + label;
|
|
441
761
|
var body = (who ? who + d.sep : "") + content;
|
|
442
|
-
if (quietedByVisibleSession(isCurrent)) return { title: title, body: body, sessionId: sessionId || null };
|
|
443
|
-
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);
|
|
444
764
|
return null;
|
|
445
765
|
}
|
|
446
766
|
function fireDone(sessionLabel, snippet, sessionId) {
|
|
@@ -459,6 +779,20 @@ window.__ModuleLoader__.load({
|
|
|
459
779
|
var TEST_MARK_KEY = "test:attention";
|
|
460
780
|
function testAlert(kind) {
|
|
461
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
|
+
}
|
|
462
796
|
if (kind === "attention") {
|
|
463
797
|
deliver("attention", "DSH · " + d.kind.question, d.testAttention);
|
|
464
798
|
markAttention(TEST_MARK_KEY);
|
|
@@ -573,7 +907,8 @@ window.__ModuleLoader__.load({
|
|
|
573
907
|
// the title marker itself (see the uiSession watcher below).
|
|
574
908
|
if (newKeys.length > 0) {
|
|
575
909
|
var it = newKeys[0];
|
|
576
|
-
var
|
|
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);
|
|
577
912
|
if (copy) queueQuieted(it.key, copy);
|
|
578
913
|
lastAttentionAt = Date.now();
|
|
579
914
|
markAttention(it.key);
|
|
@@ -806,7 +1141,8 @@ window.__ModuleLoader__.load({
|
|
|
806
1141
|
if (prev) unmarkAttention(prev.key);
|
|
807
1142
|
if (attentionKeysFired[key]) { markAttention(key); return; } // already alerted, still needs its mark
|
|
808
1143
|
attentionKeysFired[key] = true;
|
|
809
|
-
var
|
|
1144
|
+
var quick = quickFor(key, it, sid);
|
|
1145
|
+
var copy = fireAttention(it.kind, labelForSessionId(sid), textFromPayload(it), currentSessionId() === sid, sid, quick);
|
|
810
1146
|
if (copy) queueQuieted(key, copy);
|
|
811
1147
|
lastAttentionAt = Date.now();
|
|
812
1148
|
markAttention(key);
|
|
@@ -843,7 +1179,7 @@ window.__ModuleLoader__.load({
|
|
|
843
1179
|
});
|
|
844
1180
|
} catch (e) {}
|
|
845
1181
|
// keep in sync with package.json "version" on every release
|
|
846
|
-
api.version = "1.
|
|
1182
|
+
api.version = "1.3.0";
|
|
847
1183
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
848
1184
|
api.resetConfig = function () {
|
|
849
1185
|
try { localStorage.removeItem(LS_CONFIG); } catch (e) {}
|
|
@@ -853,10 +1189,15 @@ window.__ModuleLoader__.load({
|
|
|
853
1189
|
// manual test; run it after clicking anywhere so permission can be granted.
|
|
854
1190
|
// Test alerts fire regardless of the page-open visibility rules.
|
|
855
1191
|
api.test = function (kind) {
|
|
856
|
-
kind = kind === "attention"
|
|
1192
|
+
kind = (kind === "attention" || kind === "approval") ? kind : "done";
|
|
857
1193
|
unlockOnce();
|
|
858
1194
|
testAlert(kind);
|
|
859
|
-
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");
|
|
860
1201
|
};
|
|
861
1202
|
// Live diagnostics for the "why did nothing pop" case. The needs-input
|
|
862
1203
|
// channel has three possible owners on current hosts (uiSession.sessionStatus
|
|
@@ -873,6 +1214,11 @@ window.__ModuleLoader__.load({
|
|
|
873
1214
|
doneHiddenOnly: config.doneHiddenOnly,
|
|
874
1215
|
toast: config.toast,
|
|
875
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),
|
|
876
1222
|
permission: ("Notification" in window) ? Notification.permission : "n/a",
|
|
877
1223
|
visibility: hidden() ? "hidden" : "visible",
|
|
878
1224
|
uiSession: uiPending ? "bound" : "unbound",
|
|
@@ -910,6 +1256,11 @@ window.__ModuleLoader__.load({
|
|
|
910
1256
|
doneVisibleHint: "默认关闭:「完成」只在页面隐藏或切到后台时才提醒,避免打扰。",
|
|
911
1257
|
autoFocus: "点击通知回到对应对话",
|
|
912
1258
|
autoFocusHint: "默认开启:点系统通知会把 DSH 带到前台,并直接切到这条提醒所属的对话;关掉后点击通知不做任何事。",
|
|
1259
|
+
quickActions: "审批通知上直接裁决",
|
|
1260
|
+
quickActionsHint: "默认开启:权限审批的通知上带「同意 / 拒绝」按钮,点一下即按审批卡片的「允许一次 / 拒绝」直接完成裁决,不用切回页面。需要支持 Service Worker 的浏览器(http://127.0.0.1 或 https);用不了时自动退回普通通知。",
|
|
1261
|
+
quickApprovalNote: "「同意」等价于审批卡片上的「允许一次」,只放行这一次。",
|
|
1262
|
+
bridgeOff: "快捷裁决按钮暂不可用(",
|
|
1263
|
+
bridgeOffTail: "):审批通知会退回不带按钮的普通样式。",
|
|
913
1264
|
volume: "音量",
|
|
914
1265
|
language: "通知语言",
|
|
915
1266
|
langAuto: "跟随界面",
|
|
@@ -921,6 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
921
1272
|
testHint: "用当前设置各发一条测试提醒(不受上面「页面打开时也提醒」开关限制)。",
|
|
922
1273
|
testAttention: "测试「需要你」",
|
|
923
1274
|
testDone: "测试「回复完成」",
|
|
1275
|
+
testApprovalBtn: "测试「审批按钮」",
|
|
924
1276
|
reset: "恢复默认设置",
|
|
925
1277
|
resetDone: "已恢复默认设置。",
|
|
926
1278
|
sent: "已发送测试提醒 · 通知语言:",
|
|
@@ -944,6 +1296,11 @@ window.__ModuleLoader__.load({
|
|
|
944
1296
|
doneVisibleHint: "Off by default: \"done\" only alerts while the page is hidden or backgrounded, to avoid noise.",
|
|
945
1297
|
autoFocus: "Click a toast to open that conversation",
|
|
946
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.",
|
|
947
1304
|
volume: "Volume",
|
|
948
1305
|
language: "Notification language",
|
|
949
1306
|
langAuto: "Follow the interface",
|
|
@@ -955,6 +1312,7 @@ window.__ModuleLoader__.load({
|
|
|
955
1312
|
testHint: "Send one test alert with the current settings (not limited by the \"while the page is open\" toggles above).",
|
|
956
1313
|
testAttention: "Test \"needs you\"",
|
|
957
1314
|
testDone: "Test \"reply finished\"",
|
|
1315
|
+
testApprovalBtn: "Test approval buttons",
|
|
958
1316
|
reset: "Restore defaults",
|
|
959
1317
|
resetDone: "Defaults restored.",
|
|
960
1318
|
sent: "Test alert sent · notification language: ",
|
|
@@ -1042,7 +1400,9 @@ window.__ModuleLoader__.load({
|
|
|
1042
1400
|
// page is open, otherwise the button proves nothing.
|
|
1043
1401
|
testAlert(kind);
|
|
1044
1402
|
var langName = effectiveLang() === "en" ? t("langEn") : t("langZh");
|
|
1045
|
-
|
|
1403
|
+
var msg = t("sent") + langName;
|
|
1404
|
+
if (kind === "approval" && !bridgeActive()) msg += " · " + t("bridgeOff") + swState + t("bridgeOffTail");
|
|
1405
|
+
setFeedback(msg);
|
|
1046
1406
|
}
|
|
1047
1407
|
var permOK = ("Notification" in window) ? Notification.permission === "granted" : false;
|
|
1048
1408
|
var resolvedLang = effectiveLang();
|
|
@@ -1062,6 +1422,10 @@ window.__ModuleLoader__.load({
|
|
|
1062
1422
|
rowSwitch("currentQuiet", t("currentQuiet"), t("currentQuietHint"), cfg.currentHiddenOnly, function (v) { patch({ currentHiddenOnly: v }); }),
|
|
1063
1423
|
rowSwitch("doneVisible", t("doneVisible"), t("doneVisibleHint"), !cfg.doneHiddenOnly, function (v) { patch({ doneHiddenOnly: !v }); }),
|
|
1064
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
|
+
}),
|
|
1065
1429
|
h("div", { className: "dnm-row" }, [
|
|
1066
1430
|
h("div", { className: "dnm-rowText" }, [
|
|
1067
1431
|
h("div", { className: "dnm-rowLabel" }, t("volume")),
|
|
@@ -1101,9 +1465,15 @@ window.__ModuleLoader__.load({
|
|
|
1101
1465
|
]),
|
|
1102
1466
|
h("div", { className: "dnm-btnRow" }, [
|
|
1103
1467
|
h("button", { type: "button", className: "dnm-btn", disabled: !cfg.enabled, onClick: function () { runTest("attention"); } }, t("testAttention")),
|
|
1104
|
-
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"))
|
|
1105
1470
|
])
|
|
1106
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
|
+
]),
|
|
1107
1477
|
h("div", { className: "dnm-row" }, [
|
|
1108
1478
|
h("div", { className: "dnm-rowText" }, [
|
|
1109
1479
|
h("div", { className: "dnm-rowLabel" }, t("reset")),
|
|
@@ -1192,6 +1562,10 @@ window.__ModuleLoader__.load({
|
|
|
1192
1562
|
var onVisibility = function () { try { flushQuieted(); } catch (e) {} };
|
|
1193
1563
|
try { document.addEventListener("visibilitychange", onVisibility); } catch (e) {}
|
|
1194
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) {}
|
|
1195
1569
|
// Interaction source first: it owns the title marker once present, so
|
|
1196
1570
|
// binding it before the controller scan keeps a single mark per wait.
|
|
1197
1571
|
bindUiSession();
|
|
@@ -1220,5 +1594,6 @@ window.__ModuleLoader__.load({
|
|
|
1220
1594
|
exports.inject = inject;
|
|
1221
1595
|
exports.apply = apply;
|
|
1222
1596
|
return module.exports;
|
|
1223
|
-
|
|
1224
|
-
});
|
|
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.
|
|
4
|
-
"description": "DSH
|
|
3
|
+
"version": "1.3.0",
|
|
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
|
"回复完成提醒",
|