dsh-notify-me 1.5.2 → 1.6.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 +39 -0
- package/README.en.md +19 -7
- package/README.md +19 -7
- package/lib/client.js +317 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-notify-me** are documented here.
|
|
4
4
|
|
|
5
|
+
## [1.6.0] — 2026-10-15
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- **设置页可选音效来源**(`soundDone` / `soundAttention`,「需要你」与「回复完成」各自独立):
|
|
9
|
+
- `'synth'`(默认)——原有 WebAudio 合成音,行为不变;
|
|
10
|
+
- `'custom'`——自选一个本地音频文件(上限 1 MiB,可试听、可移除),提醒时播放该文件;
|
|
11
|
+
- `'system'`——完全不播插件音效,改为把通知以 `silent: false` 发出,交由操作系统播放它自己的通知音(Windows 通知中心/macOS 通知中心),音色跟着系统通知设置走。
|
|
12
|
+
- 设置页新增「音效来源」卡片:两类提醒各一行来源下拉 + 「试听」,选「自定义音频文件」时展开文件选择、已选文件名与体积、「试听」「移除」。
|
|
13
|
+
- `window.__dshNotifyMe.debug()` 新增 `soundDone` / `soundAttention`(当前生效来源)与 `customSoundDone` / `customSoundAttention`(该类别是否已存自选文件)。
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
- **通知的 `silent` 不再硬编码为 `true`**:改为 `!systemCue(kind)`。默认两种模式仍是 `silent: true`(插件自己出声,避免与平台提示音叠成两声),只有 `'system'` 模式主动让平台出声。主开关 `sound: false` 仍会连系统通知音一起静音——关掉声音就该是安静的。
|
|
17
|
+
- 自选音频存在**独立的 localStorage 键**(`dshNotifyMe.sound.done` / `dshNotifyMe.sound.attention`,值为 `{name, size, data}`),不塞进 `dshNotifyMe.config`:data URL 会膨胀约 1.37 倍,进配置会拖慢每次 `readConfig()`(每次提醒都会调用)。
|
|
18
|
+
- `resetConfig()` 与设置页「恢复默认设置」现在**一并清掉**这两个音频键,不再只清配置——否则文件会残留占用 origin 配额,而 UI 上已经没有入口能删它。
|
|
19
|
+
- **任何来源取不到声音都回落到内置合成音**(文件缺失、解码/自动播放被拒、超出配额),提醒不会因为音效配置坏掉而变成哑的。
|
|
20
|
+
- 版本号 1.5.3 → 1.6.0;`window.__dshNotifyMe.version` 与 `package.json` 同步。
|
|
21
|
+
- README(中英)同步:设置清单加「音效来源」,控制台配置块补 `soundDone` / `soundAttention` 与两个存储键的说明。
|
|
22
|
+
|
|
23
|
+
### 修复
|
|
24
|
+
- **「系统通知音」模式下点「试听」不再静默无反应**:该模式本就没有插件可播的音(声音是随通知由系统播放的),而初版实现让试听照样走插件音效通道、在 `'system'` 分支直接返回,按钮看起来就是坏的。现在改为按该类别**发一条测试通知**——你听到的就是它,并在按钮下方写明原因;`sound` 总开关关闭时同样给一句说明而不是静默。
|
|
25
|
+
|
|
26
|
+
### 测试
|
|
27
|
+
- `smoke/smoke-test.cjs` 新增音效来源用例段(**此前音效路径零覆盖**——harness 的 `AudioContext` 是 `undefined`,所有既有用例都静默走了「无音频」分支):注入可捕获的 `AudioContext` 后,覆盖内置合成音的两种音型(done `659,988`、attention `880,1174,1568`)、`'system'` 不产生振荡器且通知 `silent === false`、`sound: false` 在 `'system'` 模式下重新静音、`'custom'` 播放所存 data URL 且音量跟随滑块、两类来源互不影响、文件缺失或浏览器拒绝播放(`play()` 异步拒绝)时回落内置音、`resetConfig()` 清空两个音频键。
|
|
28
|
+
- 同一文件新增设置页渲染用例:`bootBundle` 支持注入 `require`(此前固定返回 `{}`,设置页永不注册、无法被测试),用例断言 `settings.section` 注册成功、「音效来源」两张卡片渲染、`custom` 时才出现文件选择行并显示文件名与体积,以及**按下 `'system'` 行的「试听」确实发出一条 `silent === false` 的测试通知**(为此假 React 改为按索引跨渲染保持 hook 状态,能读到点击后的反馈文案);默认路径(不注入 react)行为不变。
|
|
29
|
+
- `npm test` 两套件均通过;`smoke/cordis-host-test.mjs` 用 `DSH_NODE_MODULES` 指向本机 DSH 安装后**真跑**(此前在多数机器上静默 SKIP),真 cordis 下的 `inject` 语义守卫一并复核通过。
|
|
30
|
+
|
|
31
|
+
## [1.5.3] — 2026-10-04
|
|
32
|
+
|
|
33
|
+
### 新增
|
|
34
|
+
- **设置页底部常显插件版本号**(`dsh-notify-me v…`,中英跟随界面语言):报障、核对是否装上新版本时直接照抄这一行,不必再开控制台查 `window.__dshNotifyMe.version`(该字段保留)。
|
|
35
|
+
|
|
36
|
+
### 变更
|
|
37
|
+
- 版本号 1.5.2 → 1.5.3;`window.__dshNotifyMe.version` 同步。
|
|
38
|
+
- README(中英):「桌面端怎么抬窗的 / How the Desktop window gets raised」补真实壳复测结论与触发时机口径——深链必须在带用户手势的处理器里发(通知 `onclick` 自带手势,产品路径通);无手势的脚本化注入会被 Chromium 静默丢弃、外部协议不回流,复现时看似「通道被拦死」。致谢名单新增 [@EliteOtaku](https://github.com/EliteOtaku)(issue #9 的逐层排查与三轮真实壳复测)。
|
|
39
|
+
|
|
40
|
+
### 核实
|
|
41
|
+
- issue #9 真实壳三轮复测(DSH Desktop 0.2.0-rc.2 / Windows 11):点通知 = 抬窗 + 切会话一步到位,同会话、跨会话、窗口在后台均稳定 ✅,1.5.2 的抬窗修复确认生效。
|
|
42
|
+
- 修正 1.5.2 发布当天「CDP 注入 iframe 导航 0 新进程 ⇒ 沙箱 renderer 拦截外部协议」的判断:从发行包 `lib/main.js` 逐处核对,壳对主窗口既没有 `will-frame-navigate` 拦截、页面无 CSP,非 media 权限(含 `openExternal`)一律放行;真实差异在**用户手势上下文**,与渲染进程的 OS 沙箱无关(OS 沙箱 ≠ iframe 的 `sandbox` 属性,GHSA-p2rr-rvmm-c5fp 只影响后者)。
|
|
43
|
+
|
|
5
44
|
## [1.5.2] — 2026-10-04
|
|
6
45
|
|
|
7
46
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -53,21 +53,23 @@ Open **Settings → Notify me** (refresh the page once after installing):
|
|
|
53
53
|
|
|
54
54
|
- **Enable reminders** — master switch; when off, no toasts, no sounds and the tab title is never touched;
|
|
55
55
|
- **Environment** (auto-detect by default) — identifies Web vs Desktop (DSH Desktop, `dsh-app://`) from the page protocol, with a manual override for hosts where detection comes out wrong: on the Desktop a toast click rides the `dsh://` deep link to raise the window and quick-decision buttons are unavailable; on the Web it is `window.focus()` plus the Service Worker bridge. Whenever the override disagrees with detection, the settings page says what detection saw;
|
|
56
|
-
- **System notifications / Sound / Volume** — toast toggle,
|
|
56
|
+
- **System notifications / Sound / Volume** — toast toggle, cue toggle and a volume slider;
|
|
57
|
+
- **Alert sound** (picked separately for "needs you" and "reply finished") — built-in cue (default) / custom audio file / system notification sound. "Custom audio file" takes one local audio file in the settings page (1 MiB limit, stored in the browser, with preview and remove); "system notification sound" plays no plugin cue at all — the toast still appears and the operating system supplies the sound, so its timbre follows your system notification settings; in that mode "Preview" sends a test alert instead, because the sound only exists as part of a toast and that is the sound you hear. Any source that has no file, cannot be read, or does not fit falls back to the built-in cue, so an alert never goes silent;
|
|
57
58
|
- **"Needs you" alerts while the page is open** (default on) and **"Reply finished" alerts while the page is open** (default off);
|
|
58
59
|
- **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;
|
|
59
60
|
- **Ignore subagent sessions** (default on): background subagent child sessions raise neither "needs you" nor "reply finished" — those are steps of the parent conversation's turn, and the parent's own alert already covers them. The subagent session you have open alerts as usual. Turn it off to alert for every session;
|
|
60
61
|
- **Click a toast to open that conversation** (default on): clicking brings DSH to the front and switches to the conversation the alert came from. With several DSH tabs open, one click raises exactly one window (a visible one first) and switches only that one — the others stay put; when off, clicking a toast does nothing. On the Desktop (DSH Desktop) the click likewise raises the window and switches conversations: page-side `window.focus()` cannot raise a native Electron window, so since 1.5.2 the plugin rides the app's own `dsh://open` deep link instead (a window hidden to the tray comes back too);
|
|
61
62
|
- **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. **Buttons are unavailable on the Desktop** (`dsh-app://` pages cannot register a Service Worker): approval toasts fall back to their buttonless form there, and the settings page says so;
|
|
62
63
|
- **Notification language** — follow the interface / 简体中文 / English: controls the language of the alert text and the `🔔 …` title marker;
|
|
63
|
-
- **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)
|
|
64
|
+
- **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);
|
|
65
|
+
- **Version info** — the settings page always shows the installed plugin version at its foot (`dsh-notify-me v…`); quote that line in bug reports or to check which build you are on.
|
|
64
66
|
|
|
65
67
|
> 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.
|
|
66
68
|
|
|
67
69
|
## How it alerts
|
|
68
70
|
|
|
69
71
|
- **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)
|
|
70
|
-
- **Sound** — WebAudio beeps (distinct patterns for "needs you" vs "done")
|
|
72
|
+
- **Sound** — WebAudio beeps by default (distinct patterns for "needs you" vs "done"); each kind can instead play a picked audio file, or hand the sound over to the operating system's own notification sound
|
|
71
73
|
- **Tab title marker** — while something is waiting on you, the tab title is prefixed with `🔔 Action needed · …` / `🔔 需要你 · …`; while a background session is **finished but unread**, with `✅ Reply finished · …` / `✅ 回复完成 · …`, gone once you open that session (or it starts running again). Both kinds can be up together, "needs you" first; a host title change is rebased under the markers
|
|
72
74
|
|
|
73
75
|
This is a **browser-layer** plugin: the DSH page must stay open (minimized or backgrounded is fine — that's exactly the "away" state it watches for).
|
|
@@ -122,15 +124,21 @@ window.__dshNotifyMe.setConfig({
|
|
|
122
124
|
doneHiddenOnly: true, // false = also alert "reply finished" while visible
|
|
123
125
|
ignoreSubagent: true, // true = never alert for background subagent sessions (the open one is exempt)
|
|
124
126
|
toast: true, // system-notification toggle
|
|
125
|
-
sound: true, // sound
|
|
127
|
+
sound: true, // cue toggle (also governs the "system notification sound" mode)
|
|
128
|
+
soundDone: "synth", // "reply finished" cue source: 'synth' built-in | 'custom' picked file | 'system' OS sound
|
|
129
|
+
soundAttention: "synth", // "needs you" cue source, same values
|
|
126
130
|
volume: 0.5, // 0..1
|
|
127
131
|
autoFocus: true, // clicking a toast opens the conversation it came from
|
|
128
132
|
quickActions: true // Approve / Reject buttons on approval toasts
|
|
129
133
|
})
|
|
130
|
-
window.__dshNotifyMe.resetConfig() // restore defaults
|
|
134
|
+
window.__dshNotifyMe.resetConfig() // restore defaults (also drops any picked audio files)
|
|
131
135
|
window.__dshNotifyMe.decide("approval:3", "allowed-once") // programmatic decision: "allowed-once" | "rejected"
|
|
132
136
|
```
|
|
133
137
|
|
|
138
|
+
> Picked audio files do not live in `dshNotifyMe.config`: each kind gets its own key (`dshNotifyMe.sound.done` / `dshNotifyMe.sound.attention`, holding `{name, size, data}` with a data URL), because a file inflates by ~1.37x once base64-encoded and would slow down every config read. `resetConfig()` clears both keys along with the config.
|
|
139
|
+
>
|
|
140
|
+
> To see which source is live, check `soundDone` / `soundAttention` and `customSoundDone` / `customSoundAttention` in `window.__dshNotifyMe.debug()`.
|
|
141
|
+
|
|
134
142
|
## How it works
|
|
135
143
|
|
|
136
144
|
The reminder core picks its interaction source per host generation (see the table above); the current session always comes from the client `sessions` service:
|
|
@@ -152,6 +160,8 @@ Every button toast is booked against its interaction key, so it can only decide
|
|
|
152
160
|
|
|
153
161
|
DSH Desktop is a thin Electron shell and its page lives under `dsh-app://app`: DOM `window.focus()` only touches the browsing-context window, never the native one, and the shell's `will-navigate` / window-open handlers block every ordinary way for the page to fire a `dsh://` URL — so through 1.5.1 a toast click switched the conversation in the background but never brought the window forward. Since 1.5.2 a body click navigates a hidden iframe to `dsh://open`: subframe navigations pass the shell's `will-navigate` interception, the URL reaches the OS protocol registration, and the app's single-instance owner runs its own `focusPrimaryWindow()` (restore + show + focus) — the same path the login completion page uses to bring the client forward. A window hidden to the tray, buried under others, or minimized all come back. Browsers are unaffected and still use `window.focus()` / the worker's `client.focus()`.
|
|
154
162
|
|
|
163
|
+
Verified on the real shell (DSH Desktop 0.2.0-rc.2, three rounds of retesting in issue #9): a toast click raises the window and switches the conversation in one step, stable across sessions and with the window in the background. Timing of the fire is what makes the chain work: the deep link must be launched from a **user-gesture handler** — the notification's `onclick` carries one, which is why the product path works — while gesture-less scripted injection (an iframe appended from a CDP evaluate, say) is silently dropped by Chromium and the external protocol never launches, which looks exactly like the channel being blocked.
|
|
164
|
+
|
|
155
165
|
## Known limitations
|
|
156
166
|
|
|
157
167
|
- 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).
|
|
@@ -179,10 +189,12 @@ npm pack --dry-run # preview the published tarball
|
|
|
179
189
|
|
|
180
190
|
## Credits & how contributions land
|
|
181
191
|
|
|
182
|
-
Contributors (by handle; the
|
|
192
|
+
Contributors (by handle; the link points at the PR / issue they worked on):
|
|
183
193
|
|
|
184
194
|
- [@AgMahone](https://github.com/AgMahone) — [#7](https://github.com/chromoany/dsh-notify-me/pull/7): the 0.1.6+ / 0.2.0 host-generation support, plus the double-chime and cordis-lookup reports and retesting;
|
|
185
|
-
- [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8): muting subagent sessions (`ignoreSubagent`) with its tests and docs; the 1.4.0 refinement — mute only background subagents, alert as usual for the one you have open — is his suggestion from the #8 discussion, landed by the maintainer
|
|
195
|
+
- [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8): muting subagent sessions (`ignoreSubagent`) with its tests and docs; the 1.4.0 refinement — mute only background subagents, alert as usual for the one you have open — is his suggestion from the #8 discussion, landed by the maintainer;
|
|
196
|
+
- [@EliteOtaku](https://github.com/EliteOtaku) — [#9](https://github.com/chromoany/dsh-notify-me/issues/9): the layer-by-layer investigation and three rounds of real-shell retesting of the desktop raise path (1.5.2) — confirming that a toast click raises the window and switches conversations reliably, and pinning down the reproduction difference ("gesture-less scripted injection is silently dropped by Chromium"), which corrected the initial "the sandbox blocks it" reading;
|
|
197
|
+
- [@Wh1stle05](https://github.com/Wh1stle05) — [#10](https://github.com/chromoany/dsh-notify-me/pull/10): the per-kind alert sound source (built-in synth / picked audio file / system notification sound, chosen independently for "needs you" and "reply done"), with its settings UI, fallback chain, tests and docs; the two refinements — a "system" preview sending a real test notification instead of doing nothing, and the fake React holding hook state by index across renders — come from the same PR.
|
|
186
198
|
|
|
187
199
|
Attribution policy: external PRs are landed as maintainer commits — the original commit is not reused and no Co-authored-by trailer is added, so GitHub's contributors graph lists the maintainer only and contributors are credited above.
|
|
188
200
|
|
package/README.md
CHANGED
|
@@ -53,21 +53,23 @@ dsh-notify-me 把「监督」和「守在屏幕前」解耦:只在真的出现
|
|
|
53
53
|
|
|
54
54
|
- **启用提醒** 主开关:关闭后不再弹系统通知、不播放提示音、也不改标签页标题;
|
|
55
55
|
- **运行环境**(默认自动识别):按页面协议识别 Web 端 / 桌面端(DSH Desktop,`dsh-app://`),识别不准时可手动指定——桌面端点通知走 `dsh://` 深链抬窗、快捷裁决按钮不可用;Web 端走 `window.focus()` 与 Service Worker 桥。手动覆盖与自动识别不一致时,设置页会明示检测结果;
|
|
56
|
-
- **系统通知 / 提示音 / 音量**:Toast
|
|
56
|
+
- **系统通知 / 提示音 / 音量**:Toast、提示音开关与音量滑块;
|
|
57
|
+
- **音效来源**(「需要你」与「回复完成」各自独立选):内置合成音(默认)/ 自定义音频文件 / 系统通知音。选「自定义音频文件」可在设置页挑一个本地音频(上限 1 MiB,存在浏览器本地,可试听与移除);选「系统通知音」则完全不播插件音效,通知照发,声音交给操作系统(音色跟着系统通知设置走),此时「试听」会改发一条测试通知——该模式没有插件可播的音,你听到的就是它。任一来源缺文件、读不出来或存不下时自动回落到内置合成音,提醒不会因此变成哑的;
|
|
57
58
|
- **页面打开时也提醒「需要你」**(默认开)与 **页面打开时也提醒「回复完成」**(默认关);
|
|
58
59
|
- **当前对话不弹通知**(默认开):等待就发生在你正看着的对话里时只留标题标记,不弹通知也不响音(免得盖住审批卡片);页面切到后台立刻补发,若你已经处理掉就作废。其它后台会话不受影响;
|
|
59
60
|
- **子代理会话不提醒**(默认开):后台子代理(subagent)子会话的「需要你」与「回复完成」都不提醒——它们是主对话那一轮的步骤,完成由主对话自己的提醒覆盖;你当前打开的那个子代理会话照常提醒;关掉后恢复逐会话提醒;
|
|
60
61
|
- **点击通知回到对应对话**(默认开):点通知把 DSH 带到前台并切到提醒所属的对话;多个 DSH 标签页同时开着时,一次点击只调起一个窗口(优先当前可见的那个)并只切它的对话,其余标签页不动;关掉后点通知不做任何事。桌面端(DSH Desktop)同样会把窗口带到前台并切对话:页面侧的 `window.focus()` 在 Electron 里抬不动原生窗口,1.5.2 起改走应用自己的 `dsh://open` 深链调窗(窗口隐藏在托盘里也能被抬回来);
|
|
61
62
|
- **审批通知上直接裁决**(默认开):权限审批的通知上带「同意 / 拒绝」按钮,点一下就按审批卡片的「允许一次 / 拒绝」直接裁决,不用切回页面;点通知正文仍是原来的「切回对应对话」。**桌面端暂不支持按钮**(`dsh-app://` 页面无法注册 Service Worker),审批通知自动退回无按钮样式,设置页会明示这一限制;
|
|
62
63
|
- **通知语言**:跟随界面 / 简体中文 / English——决定通知文字与 `🔔 …` 标题标记使用的语言;
|
|
63
|
-
- **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6
|
|
64
|
+
- **测试按钮**:用当前设置立即发一条「需要你」「回复完成」或「审批按钮」测试提醒(**不受**上面「页面打开时也提醒」开关限制;「需要你」测试的标题标记约 6 秒后自动消失);
|
|
65
|
+
- **版本信息**:设置页底部常显当前插件版本号(`dsh-notify-me v…`)——报障、核对是否装上新版本时直接照抄这一行。
|
|
64
66
|
|
|
65
67
|
> 需要浏览器通知权限:页面上点一下 → 允许;或地址栏锁 → 站点设置 → 通知 → 允许 → 刷新。macOS 还要在 系统设置 → 通知 里允许该浏览器。
|
|
66
68
|
|
|
67
69
|
## 提醒方式
|
|
68
70
|
|
|
69
71
|
- **系统通知**:经浏览器弹出的系统通知,进 Windows 通知中心或 macOS 通知中心(点击回到提醒所属的对话;审批的通知点按钮直接裁决)
|
|
70
|
-
-
|
|
72
|
+
- **提示音**:默认是 WebAudio 合成音(「需要你」与「完成」使用不同音型);可在设置里为两类提醒分别换成自定义音频文件,或改成让操作系统播放它自己的通知音
|
|
71
73
|
- **标签页标题标记**:有待处理事项时,标题前出现 `🔔 需要你 · …` / `🔔 Action needed · …`;后台会话**完成但还没看**时出现 `✅ 回复完成 · …` / `✅ Reply finished · …`,打开对应会话(或它再次开跑)即消失。两种标记可同时出现,「需要你」在前;宿主改写标题后标记基于新标题重建
|
|
72
74
|
|
|
73
75
|
本插件是**浏览器层**实现:DSH 页面需保持打开(最小化/后台即可——那正是它监听的「离开」状态)。
|
|
@@ -118,15 +120,21 @@ window.__dshNotifyMe.setConfig({
|
|
|
118
120
|
doneHiddenOnly: true, // false = 页面可见时「完成」也提醒
|
|
119
121
|
ignoreSubagent: true, // true = 后台子代理(subagent)会话不提醒(当前打开的照常)
|
|
120
122
|
toast: true, // 系统通知开关
|
|
121
|
-
sound: true, //
|
|
123
|
+
sound: true, // 提示音开关(含「系统通知音」模式)
|
|
124
|
+
soundDone: "synth", // 「完成」音效来源:'synth' 内置 | 'custom' 自定义文件 | 'system' 系统通知音
|
|
125
|
+
soundAttention: "synth", // 「需要你」音效来源,取值同上
|
|
122
126
|
volume: 0.5, // 音量 0~1
|
|
123
127
|
autoFocus: true, // 点通知回到提醒所属的对话
|
|
124
128
|
quickActions: true // 审批通知上的「同意 / 拒绝」按钮
|
|
125
129
|
})
|
|
126
|
-
window.__dshNotifyMe.resetConfig() //
|
|
130
|
+
window.__dshNotifyMe.resetConfig() // 恢复默认(同时清掉已选的自定义音效文件)
|
|
127
131
|
window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:"allowed-once" | "rejected"
|
|
128
132
|
```
|
|
129
133
|
|
|
134
|
+
> 自定义音效文件不在 `dshNotifyMe.config` 里,而是按类别各存一个键(`dshNotifyMe.sound.done` / `dshNotifyMe.sound.attention`,内容是 `{name, size, data}` 的 data URL)——一个文件转成 data URL 会膨胀约 1.37 倍,塞进配置会拖慢每次读取。`resetConfig()` 会连这两个键一起清掉。
|
|
135
|
+
>
|
|
136
|
+
> 想确认当前生效的来源,看 `window.__dshNotifyMe.debug()` 的 `soundDone` / `soundAttention` 与 `customSoundDone` / `customSoundAttention`。
|
|
137
|
+
|
|
130
138
|
## 工作原理
|
|
131
139
|
|
|
132
140
|
提醒核心按宿主代际取源(见上表),当前会话一律来自客户端 `sessions` 服务:
|
|
@@ -148,6 +156,8 @@ window.__dshNotifyMe.decide("approval:3", "allowed-once") // 程序化裁决:
|
|
|
148
156
|
|
|
149
157
|
DSH Desktop 是一层 Electron 壳,页面跑在 `dsh-app://app` 下:DOM 的 `window.focus()` 只动浏览器窗口对象、碰不到原生窗口,壳的 `will-navigate` / `window-open` 又把页面里发 `dsh://` 的常规出口全堵上了——所以 1.5.1 及以前点通知只在后台切好对话,窗口不上前台。1.5.2 起点通知正文时页面发一条隐藏 iframe 导航到 `dsh://open`:子框架导航不经过壳的 `will-navigate` 拦截,URL 交给系统协议注册,应用的单实例锁收到第二次启动后走 `focusPrimaryWindow()`(restore + show + focus)——与登录完成页把客户端置前是同一条路。窗口隐藏在托盘、被别的窗口压住、最小化,都能被抬回来。浏览器端不受影响,仍走 `window.focus()` / worker `client.focus()`。
|
|
150
158
|
|
|
159
|
+
真实壳已实测(DSH Desktop 0.2.0-rc.2,issue #9 三轮复测):点通知 = 抬窗 + 切会话一步到位,跨会话、窗口在后台时同样稳定。触发时机是这条链路的关键:深链必须在**带用户手势的处理器**里发——通知的 `onclick` 自带手势,所以产品路径通;而无手势的脚本化注入(如 CDP evaluate 手工塞 iframe)会被 Chromium 静默丢弃、外部协议根本不回流,复现时看起来就像「通道被拦死」。
|
|
160
|
+
|
|
151
161
|
## 已知限制
|
|
152
162
|
|
|
153
163
|
- 页面必须开着才会提醒(后台标签/最小化可以;关标签页即失效——浏览器层方案固有限制)。
|
|
@@ -175,10 +185,12 @@ npm pack --dry-run # 预览发布包
|
|
|
175
185
|
|
|
176
186
|
## 致谢与贡献须知
|
|
177
187
|
|
|
178
|
-
贡献者(按 handle
|
|
188
|
+
贡献者(按 handle 列出,链接指向他们参与的 PR / issue):
|
|
179
189
|
|
|
180
190
|
- [@AgMahone](https://github.com/AgMahone) — [#7](https://github.com/chromoany/dsh-notify-me/pull/7):0.1.6+ / 0.2.0 宿主代际支持,以及通知双响、cordis 查找路径的报障与复测;
|
|
181
|
-
- [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8):子代理会话静音(`ignoreSubagent`)与配套测试、文档;1.4.0 里「只静音后台子代理、当前打开的照常提醒」的口径同样出自他在 #8
|
|
191
|
+
- [@d0ublecl1ck](https://github.com/d0ublecl1ck) — [#8](https://github.com/chromoany/dsh-notify-me/pull/8):子代理会话静音(`ignoreSubagent`)与配套测试、文档;1.4.0 里「只静音后台子代理、当前打开的照常提醒」的口径同样出自他在 #8 讨论里的建议,落地由维护者完成;
|
|
192
|
+
- [@EliteOtaku](https://github.com/EliteOtaku) — [#9](https://github.com/chromoany/dsh-notify-me/issues/9):桌面端抬窗链路(1.5.2)的逐层排查与真实壳三轮复测——确认点通知抬窗 + 切会话稳定生效,并定位出「无用户手势的脚本化注入被 Chromium 静默丢弃」这一复现差异,纠正了最初「沙箱拦死」的判断;
|
|
193
|
+
- [@Wh1stle05](https://github.com/Wh1stle05) — [#10](https://github.com/chromoany/dsh-notify-me/pull/10):「音效来源」功能(内置合成音 / 自定义音频文件 / 系统通知音,「需要你」与「回复完成」各自独立选),含设置页交互、回落链与测试、文档;「系统通知音」试听改发测试通知、假 React 按索引跨渲染保持 hook 状态这两处修正同样出自该 PR。
|
|
182
194
|
|
|
183
195
|
署名口径:本仓库收编外部 PR 时以维护者提交落地——不沿用原 commit、不挂 Co-authored-by,GitHub 的 contributors 图只显示维护者,贡献者以上面的致谢署名。
|
|
184
196
|
|
package/lib/client.js
CHANGED
|
@@ -239,6 +239,15 @@
|
|
|
239
239
|
ignoreSubagent: true,
|
|
240
240
|
toast: true,
|
|
241
241
|
sound: true,
|
|
242
|
+
// Where each alert's cue comes from (per kind, so "needs you" and "reply
|
|
243
|
+
// finished" stay independent):
|
|
244
|
+
// 'synth' — the built-in WebAudio cue (default, needs no asset)
|
|
245
|
+
// 'custom' — an audio file the user picked, kept in its own
|
|
246
|
+
// localStorage key and played through an <audio> element
|
|
247
|
+
// 'system' — no plugin cue at all: the toast is raised un-silenced so
|
|
248
|
+
// the operating system plays its own notification sound
|
|
249
|
+
soundDone: "synth",
|
|
250
|
+
soundAttention: "synth",
|
|
242
251
|
volume: 0.5,
|
|
243
252
|
autoFocus: true, // clicking the toast raises DSH and switches to the alerted conversation
|
|
244
253
|
// Approval toasts carry 同意/拒绝 buttons and settle the request on one
|
|
@@ -272,6 +281,38 @@
|
|
|
272
281
|
return config;
|
|
273
282
|
}
|
|
274
283
|
|
|
284
|
+
// Picked cue files live in their own localStorage keys, never inside the
|
|
285
|
+
// config JSON: a data URL is ~1.37x the file size, and the config is parsed
|
|
286
|
+
// on every readConfig() — which happens on every alert.
|
|
287
|
+
var LS_SOUND = { done: "dshNotifyMe.sound.done", attention: "dshNotifyMe.sound.attention" };
|
|
288
|
+
// 1 MiB of source audio is ~1.37 MiB once base64-encoded; localStorage
|
|
289
|
+
// grants ~5 MiB per origin, and the other kind plus the config must fit too.
|
|
290
|
+
var SOUND_MAX_BYTES = 1048576;
|
|
291
|
+
function soundMode(kind) {
|
|
292
|
+
var v = kind === "attention" ? config.soundAttention : config.soundDone;
|
|
293
|
+
return (v === "custom" || v === "system") ? v : "synth";
|
|
294
|
+
}
|
|
295
|
+
// True when the platform, not this plugin, should make the noise: only then
|
|
296
|
+
// is the toast raised un-silenced. The master Sound switch still mutes it.
|
|
297
|
+
function systemCue(kind) { return config.sound && soundMode(kind) === "system"; }
|
|
298
|
+
function readSound(kind) {
|
|
299
|
+
try {
|
|
300
|
+
var raw = localStorage.getItem(LS_SOUND[kind]);
|
|
301
|
+
if (!raw) return null;
|
|
302
|
+
var rec = JSON.parse(raw);
|
|
303
|
+
return (rec && typeof rec.data === "string" && rec.data) ? rec : null;
|
|
304
|
+
} catch (e) { return null; }
|
|
305
|
+
}
|
|
306
|
+
// false = the record did not fit (quota) or storage is unavailable; the
|
|
307
|
+
// caller reports it and the previous record stays in place.
|
|
308
|
+
function writeSound(kind, rec) {
|
|
309
|
+
try {
|
|
310
|
+
if (rec) localStorage.setItem(LS_SOUND[kind], JSON.stringify(rec));
|
|
311
|
+
else localStorage.removeItem(LS_SOUND[kind]);
|
|
312
|
+
return true;
|
|
313
|
+
} catch (e) { return false; }
|
|
314
|
+
}
|
|
315
|
+
|
|
275
316
|
// ───────────────────────── copy tables ─────────────────────────
|
|
276
317
|
// Notification copy, keyed by language id. Selected through
|
|
277
318
|
// effectiveLang() so a pinned language stays pinned even while the DSH UI
|
|
@@ -389,6 +430,15 @@
|
|
|
389
430
|
// the protocol registration, and the single-instance owner restores,
|
|
390
431
|
// shows and focuses the window — the same path the login completion page
|
|
391
432
|
// uses to bring DSH forward.
|
|
433
|
+
//
|
|
434
|
+
// Verified on the real shell (DSH Desktop 0.2.0-rc.2, issue #9): a click
|
|
435
|
+
// on the notification raises the window and switches the conversation in
|
|
436
|
+
// one step, reproducibly. The fire must sit in a user-gesture context —
|
|
437
|
+
// the page-side Notification onclick carries one, and Chromium drops
|
|
438
|
+
// external-protocol navigations launched without it. That is also why
|
|
439
|
+
// gesture-less injections (CDP evaluate, scripted iframe appends) silently
|
|
440
|
+
// produce no protocol launch at all, which read like the channel being
|
|
441
|
+
// blocked when the gesture was the only difference.
|
|
392
442
|
function raiseWindow() {
|
|
393
443
|
try { window.focus(); } catch (e) {}
|
|
394
444
|
if (!isDesktopShell()) return;
|
|
@@ -632,8 +682,10 @@
|
|
|
632
682
|
body: body || "",
|
|
633
683
|
tag: tag,
|
|
634
684
|
renotify: true,
|
|
635
|
-
// The plugin owns the audio (see showToast's silent note below)
|
|
636
|
-
|
|
685
|
+
// The plugin owns the audio (see showToast's silent note below),
|
|
686
|
+
// except in the 'system' cue mode, which hands the sound back to
|
|
687
|
+
// the platform on purpose.
|
|
688
|
+
silent: !systemCue(kind),
|
|
637
689
|
icon: (location.origin || "") + "/favicon.svg",
|
|
638
690
|
data: { key: quick.key, sessionId: sessionId || null, kind: kind, test: !!quick.test, focus: config.autoFocus !== false },
|
|
639
691
|
actions: [
|
|
@@ -654,8 +706,10 @@
|
|
|
654
706
|
// The plugin owns the audio: its WebAudio cues already differ per
|
|
655
707
|
// kind (attention vs done) and answer the Sound toggle and volume
|
|
656
708
|
// slider. Without this flag the platform plays its own chime on top
|
|
657
|
-
// of the beep — two sounds stacked on one alert.
|
|
658
|
-
|
|
709
|
+
// of the beep — two sounds stacked on one alert. The single exception
|
|
710
|
+
// is the 'system' cue mode, which is precisely "let the platform play
|
|
711
|
+
// it"; the master Sound switch still mutes that too.
|
|
712
|
+
silent: !systemCue(kind),
|
|
659
713
|
icon: (location.origin || "") + "/favicon.svg"
|
|
660
714
|
});
|
|
661
715
|
if (config.autoFocus) {
|
|
@@ -696,18 +750,52 @@
|
|
|
696
750
|
osc.stop(t0 + durMs / 1000 + 0.05);
|
|
697
751
|
} catch (e) {}
|
|
698
752
|
}
|
|
699
|
-
|
|
753
|
+
// 'custom' plays the file the user picked. Anything short of a started
|
|
754
|
+
// element — no file stored, a browser without the element, a constructor or
|
|
755
|
+
// play() that throws — falls through to the built-in cue, so an alert is
|
|
756
|
+
// never silent merely because a stored file went missing or unreadable.
|
|
757
|
+
// The last failure mode is different in kind: a decode or autoplay refusal
|
|
758
|
+
// rejects play()'s promise instead of throwing, so it gets the built-in cue
|
|
759
|
+
// handed to it as a fallback rather than falling through.
|
|
760
|
+
function playCustomSound(kind, fallback) {
|
|
761
|
+
var rec = readSound(kind);
|
|
762
|
+
if (!rec) return false;
|
|
763
|
+
try {
|
|
764
|
+
var el = new Audio(rec.data);
|
|
765
|
+
el.volume = Math.max(0, Math.min(1, config.volume));
|
|
766
|
+
var started = el.play();
|
|
767
|
+
if (started && typeof started.catch === "function") started.catch(fallback);
|
|
768
|
+
return true;
|
|
769
|
+
} catch (e) { return false; }
|
|
770
|
+
}
|
|
771
|
+
// 'system' returns silently on purpose: showToast raises the toast
|
|
772
|
+
// un-silenced for exactly this mode, so the OS plays its own notification
|
|
773
|
+
// sound and the plugin must not stack a second cue on top of it.
|
|
774
|
+
function playCue(kind, build) {
|
|
700
775
|
if (!config.sound) return;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
776
|
+
var mode = soundMode(kind);
|
|
777
|
+
if (mode === "system") return;
|
|
778
|
+
function synth() {
|
|
779
|
+
// An async rejection can land after the user muted everything.
|
|
780
|
+
if (!config.sound) return;
|
|
781
|
+
unlockAudio();
|
|
782
|
+
build();
|
|
783
|
+
}
|
|
784
|
+
if (mode === "custom" && playCustomSound(kind, synth)) return;
|
|
785
|
+
synth();
|
|
786
|
+
}
|
|
787
|
+
function playAttentionSound() {
|
|
788
|
+
playCue("attention", function () {
|
|
789
|
+
tone(880, 0.0, 140, config.volume);
|
|
790
|
+
tone(1174, 0.16, 140, config.volume);
|
|
791
|
+
tone(1568, 0.32, 200, config.volume);
|
|
792
|
+
});
|
|
705
793
|
}
|
|
706
794
|
function playDoneSound() {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
795
|
+
playCue("done", function () {
|
|
796
|
+
tone(659, 0.0, 150, config.volume);
|
|
797
|
+
tone(988, 0.18, 260, config.volume);
|
|
798
|
+
});
|
|
711
799
|
}
|
|
712
800
|
function unlockOnce() {
|
|
713
801
|
unlockAudio();
|
|
@@ -1479,10 +1567,17 @@
|
|
|
1479
1567
|
});
|
|
1480
1568
|
} catch (e) {}
|
|
1481
1569
|
// keep in sync with package.json "version" on every release
|
|
1482
|
-
api.version = "1.
|
|
1570
|
+
api.version = "1.6.0";
|
|
1483
1571
|
api.setConfig = function (patch) { return setConfigLocal(patch); };
|
|
1484
1572
|
api.resetConfig = function () {
|
|
1485
|
-
try {
|
|
1573
|
+
try {
|
|
1574
|
+
localStorage.removeItem(LS_CONFIG);
|
|
1575
|
+
// The picked cue files are per-kind keys of their own; dropping the
|
|
1576
|
+
// config alone would leave them orphaned, still eating the origin's
|
|
1577
|
+
// quota with no UI left to remove them from.
|
|
1578
|
+
localStorage.removeItem(LS_SOUND.done);
|
|
1579
|
+
localStorage.removeItem(LS_SOUND.attention);
|
|
1580
|
+
} catch (e) {}
|
|
1486
1581
|
config = readConfig();
|
|
1487
1582
|
return config;
|
|
1488
1583
|
};
|
|
@@ -1515,6 +1610,10 @@
|
|
|
1515
1610
|
ignoreSubagent: config.ignoreSubagent,
|
|
1516
1611
|
toast: config.toast,
|
|
1517
1612
|
sound: config.sound,
|
|
1613
|
+
soundDone: soundMode("done"),
|
|
1614
|
+
soundAttention: soundMode("attention"),
|
|
1615
|
+
customSoundDone: !!readSound("done"),
|
|
1616
|
+
customSoundAttention: !!readSound("attention"),
|
|
1518
1617
|
quickActions: config.quickActions,
|
|
1519
1618
|
// quick-decision bridge: 'active' is the only state in which approval
|
|
1520
1619
|
// toasts carry working 同意/拒绝 buttons.
|
|
@@ -1562,7 +1661,30 @@
|
|
|
1562
1661
|
toast: "系统通知",
|
|
1563
1662
|
toastHint: "经浏览器弹出系统 Toast(Windows 通知中心);需要已授予通知权限。",
|
|
1564
1663
|
sound: "提示音",
|
|
1565
|
-
soundHint: "
|
|
1664
|
+
soundHint: "提示音总开关;具体播什么由下面的「音效来源」决定。",
|
|
1665
|
+
soundSource: "音效来源",
|
|
1666
|
+
soundSourceHint: "两类提醒各自选择声音来源,互不影响。默认是插件自带的内置合成音。",
|
|
1667
|
+
soundDoneLabel: "「回复完成」的声音",
|
|
1668
|
+
soundAttentionLabel: "「需要你」的声音",
|
|
1669
|
+
soundSynth: "内置合成音(默认)",
|
|
1670
|
+
soundCustom: "自定义音频文件",
|
|
1671
|
+
soundSystem: "系统通知音(系统播放)",
|
|
1672
|
+
soundSynthNote: "插件合成音,「需要你」与「回复完成」音型不同。",
|
|
1673
|
+
soundSystemNote: "不播插件音效:通知照发,声音交给操作系统——音色跟着系统通知设置走,开关仍听这里的「提示音」。",
|
|
1674
|
+
soundNone: "尚未选择文件",
|
|
1675
|
+
soundPick: "选择音频文件",
|
|
1676
|
+
soundPreview: "试听",
|
|
1677
|
+
soundClear: "移除",
|
|
1678
|
+
soundTooBig: "文件太大,上限 ",
|
|
1679
|
+
soundNotAudio: "这不是浏览器能播放的音频文件。",
|
|
1680
|
+
soundSaved: "已保存自定义音效:",
|
|
1681
|
+
soundCleared: "已移除自定义音效,该提醒回落到内置合成音。",
|
|
1682
|
+
soundReadFail: "读取文件失败。",
|
|
1683
|
+
soundQuota: "浏览器存储空间不足,存不下这个文件——先移除另一类提醒的自定义音效,或换一个更小的文件。",
|
|
1684
|
+
soundPreviewDone: "试听「回复完成」音效",
|
|
1685
|
+
soundPreviewAttention: "试听「需要你」音效",
|
|
1686
|
+
soundOffHint: "「提示音」开关是关的:先打开它才能试听(关掉时提醒本身也确实不出声)。",
|
|
1687
|
+
soundPreviewSystem: "「系统通知音」是随通知由系统播放的,插件这边没有可播的音;已发一条测试提醒,声音就是它。",
|
|
1566
1688
|
attentionVisible: "页面打开时也提醒「需要你」",
|
|
1567
1689
|
attentionVisibleHint: "默认开启:模型在等你审批 / 确认方案 / 回答问题(含提问弹窗)时,即使页面在最前也会提醒。",
|
|
1568
1690
|
currentQuiet: "当前对话不弹通知",
|
|
@@ -1597,7 +1719,8 @@
|
|
|
1597
1719
|
resetDone: "已恢复默认设置。",
|
|
1598
1720
|
sent: "已发送测试提醒 · 通知语言:",
|
|
1599
1721
|
permHint: "通知权限尚未授予:先在页面上点击一次并允许通知,系统通知才能弹出(提示音不受影响)。",
|
|
1600
|
-
offHint: "已关闭提醒:先打开「启用提醒」再测试。"
|
|
1722
|
+
offHint: "已关闭提醒:先打开「启用提醒」再测试。",
|
|
1723
|
+
versionLine: "插件版本:dsh-notify-me v"
|
|
1601
1724
|
};
|
|
1602
1725
|
var UI_en = {
|
|
1603
1726
|
nav: "Notify me",
|
|
@@ -1615,7 +1738,30 @@
|
|
|
1615
1738
|
toast: "System notifications",
|
|
1616
1739
|
toastHint: "Native toasts via the browser (Windows notification center); requires notification permission.",
|
|
1617
1740
|
sound: "Sound",
|
|
1618
|
-
soundHint: "
|
|
1741
|
+
soundHint: "Master cue switch; what it actually plays comes from \"Alert sound\" below.",
|
|
1742
|
+
soundSource: "Alert sound",
|
|
1743
|
+
soundSourceHint: "Each alert kind picks its own sound source; the two never affect each other. The default is the plugin's built-in cue.",
|
|
1744
|
+
soundDoneLabel: "\"Reply finished\" sound",
|
|
1745
|
+
soundAttentionLabel: "\"Needs you\" sound",
|
|
1746
|
+
soundSynth: "Built-in cue (default)",
|
|
1747
|
+
soundCustom: "Custom audio file",
|
|
1748
|
+
soundSystem: "System notification sound (OS plays it)",
|
|
1749
|
+
soundSynthNote: "The plugin's synthesized cue — \"needs you\" and \"reply finished\" use different patterns.",
|
|
1750
|
+
soundSystemNote: "No plugin cue: the toast still appears, but the sound is left to the operating system, so its timbre follows your system notification settings. The Sound switch above still mutes it.",
|
|
1751
|
+
soundNone: "No file chosen yet",
|
|
1752
|
+
soundPick: "Choose an audio file",
|
|
1753
|
+
soundPreview: "Preview",
|
|
1754
|
+
soundClear: "Remove",
|
|
1755
|
+
soundTooBig: "File is too large; the limit is ",
|
|
1756
|
+
soundNotAudio: "That is not an audio file this browser can play.",
|
|
1757
|
+
soundSaved: "Custom sound saved: ",
|
|
1758
|
+
soundCleared: "Custom sound removed; this alert falls back to the built-in cue.",
|
|
1759
|
+
soundReadFail: "Could not read the file.",
|
|
1760
|
+
soundQuota: "Not enough browser storage for this file — remove the other kind's custom sound first, or pick a smaller file.",
|
|
1761
|
+
soundPreviewDone: "Preview the \"reply finished\" sound",
|
|
1762
|
+
soundPreviewAttention: "Preview the \"needs you\" sound",
|
|
1763
|
+
soundOffHint: "The Sound switch is off: turn it on to preview (with it off the alerts themselves stay silent too).",
|
|
1764
|
+
soundPreviewSystem: "The system sound plays with a toast, not from the plugin — a test alert was sent instead; that sound is it.",
|
|
1619
1765
|
attentionVisible: "Also alert \"needs you\" while the page is open",
|
|
1620
1766
|
attentionVisibleHint: "On by default: alert even in the foreground when the model waits for approval / plan review / a question.",
|
|
1621
1767
|
currentQuiet: "Stay quiet for the conversation on screen",
|
|
@@ -1650,7 +1796,8 @@
|
|
|
1650
1796
|
resetDone: "Defaults restored.",
|
|
1651
1797
|
sent: "Test alert sent · notification language: ",
|
|
1652
1798
|
permHint: "Notification permission is not granted yet — click once on the page and allow notifications so toasts can appear (sound is unaffected).",
|
|
1653
|
-
offHint: "Reminders are off: enable them first, then test."
|
|
1799
|
+
offHint: "Reminders are off: enable them first, then test.",
|
|
1800
|
+
versionLine: "Plugin version: dsh-notify-me v"
|
|
1654
1801
|
};
|
|
1655
1802
|
// Small element helper to keep the hand-written page readable.
|
|
1656
1803
|
function h(tag, props) {
|
|
@@ -1678,6 +1825,7 @@
|
|
|
1678
1825
|
".dnm-btn:hover{background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,.1))}" +
|
|
1679
1826
|
".dnm-btn[disabled]{opacity:.45;cursor:default}" +
|
|
1680
1827
|
".dnm-btnRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}" +
|
|
1828
|
+
".dnm-file{font-size:12px;font-family:inherit;color:var(--dsw-alias-label-primary,rgba(20,20,20,.9));max-width:200px}" +
|
|
1681
1829
|
".dnm-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9))}" +
|
|
1682
1830
|
".dnm-feedback{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,rgba(120,120,120,.9));min-height:18px}";
|
|
1683
1831
|
(function injectStyle() {
|
|
@@ -1723,9 +1871,137 @@
|
|
|
1723
1871
|
var feedbackState = React.useState("");
|
|
1724
1872
|
var feedback = feedbackState[0];
|
|
1725
1873
|
var setFeedback = feedbackState[1];
|
|
1874
|
+
// Picked cue files bypass the config object (own localStorage keys), so
|
|
1875
|
+
// the page needs its own nudge to re-read them after a change.
|
|
1876
|
+
var soundBumpState = React.useState(0);
|
|
1877
|
+
var soundBump = soundBumpState[0];
|
|
1878
|
+
var setSoundBump = soundBumpState[1];
|
|
1726
1879
|
function patch(p) {
|
|
1727
1880
|
setCfg(setConfigLocal(p));
|
|
1728
1881
|
}
|
|
1882
|
+
function previewSound(kind) {
|
|
1883
|
+
// The master switch is authoritative here too, so a preview with sound
|
|
1884
|
+
// off has to say why it stayed quiet instead of looking broken.
|
|
1885
|
+
if (!config.sound) { setFeedback(t("soundOffHint")); return; }
|
|
1886
|
+
// 'system' has no cue to play from here: the sound only exists as part
|
|
1887
|
+
// of a toast, which the OS then chimes for. The honest preview is
|
|
1888
|
+
// therefore a real test alert — otherwise the button is silent and
|
|
1889
|
+
// reads as broken.
|
|
1890
|
+
if (soundMode(kind) === "system") {
|
|
1891
|
+
unlockOnce();
|
|
1892
|
+
testAlert(kind);
|
|
1893
|
+
setFeedback(t("soundPreviewSystem"));
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
// Otherwise: the same cue the alert itself will play, volume included.
|
|
1897
|
+
unlockOnce();
|
|
1898
|
+
if (kind === "attention") playAttentionSound();
|
|
1899
|
+
else playDoneSound();
|
|
1900
|
+
}
|
|
1901
|
+
// One row per alert kind. The source select is always there; the file
|
|
1902
|
+
// picker only appears for 'custom', so the page stays short for anyone
|
|
1903
|
+
// who never touches it.
|
|
1904
|
+
function soundRows(kind, labelText) {
|
|
1905
|
+
var modeKey = kind === "attention" ? "soundAttention" : "soundDone";
|
|
1906
|
+
var previewLabel = t(kind === "attention" ? "soundPreviewAttention" : "soundPreviewDone");
|
|
1907
|
+
var mode = soundMode(kind);
|
|
1908
|
+
var rec = readSound(kind);
|
|
1909
|
+
// The hint states what will actually be heard, so a stored file is
|
|
1910
|
+
// named right where the choice is made.
|
|
1911
|
+
var note = mode === "system" ? t("soundSystemNote")
|
|
1912
|
+
: mode === "custom" ? (rec ? rec.name : t("soundNone"))
|
|
1913
|
+
: t("soundSynthNote");
|
|
1914
|
+
var rows = [
|
|
1915
|
+
h("div", { className: "dnm-row" }, [
|
|
1916
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1917
|
+
h("div", { className: "dnm-rowLabel" }, labelText),
|
|
1918
|
+
h("div", { className: "dnm-rowHint" }, note)
|
|
1919
|
+
]),
|
|
1920
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
1921
|
+
h("select", {
|
|
1922
|
+
className: "dnm-select",
|
|
1923
|
+
value: mode,
|
|
1924
|
+
"aria-label": labelText,
|
|
1925
|
+
onChange: function (e) {
|
|
1926
|
+
var p = {};
|
|
1927
|
+
p[modeKey] = e.currentTarget.value;
|
|
1928
|
+
patch(p);
|
|
1929
|
+
}
|
|
1930
|
+
}, [
|
|
1931
|
+
h("option", { value: "synth" }, t("soundSynth")),
|
|
1932
|
+
h("option", { value: "custom" }, t("soundCustom")),
|
|
1933
|
+
h("option", { value: "system" }, t("soundSystem"))
|
|
1934
|
+
]),
|
|
1935
|
+
h("button", {
|
|
1936
|
+
type: "button",
|
|
1937
|
+
className: "dnm-btn",
|
|
1938
|
+
disabled: !cfg.enabled,
|
|
1939
|
+
"aria-label": previewLabel,
|
|
1940
|
+
onClick: function () { previewSound(kind); }
|
|
1941
|
+
}, t("soundPreview"))
|
|
1942
|
+
])
|
|
1943
|
+
])
|
|
1944
|
+
];
|
|
1945
|
+
if (mode !== "custom") return rows;
|
|
1946
|
+
rows.push(h("div", { className: "dnm-row" }, [
|
|
1947
|
+
h("div", { className: "dnm-rowText" }, [
|
|
1948
|
+
h("div", { className: "dnm-rowLabel" }, t("soundPick")),
|
|
1949
|
+
h("div", { className: "dnm-rowHint" }, (rec ? Math.round((rec.size || 0) / 1024) + " KB · " : t("soundNone") + " · ")
|
|
1950
|
+
+ "≤" + Math.round(SOUND_MAX_BYTES / 1024) + " KB")
|
|
1951
|
+
]),
|
|
1952
|
+
h("div", { className: "dnm-btnRow" }, [
|
|
1953
|
+
h("input", {
|
|
1954
|
+
type: "file",
|
|
1955
|
+
accept: "audio/*",
|
|
1956
|
+
className: "dnm-file",
|
|
1957
|
+
"aria-label": t("soundPick"),
|
|
1958
|
+
onChange: function (e) {
|
|
1959
|
+
var input = e.currentTarget;
|
|
1960
|
+
var f = input.files && input.files[0];
|
|
1961
|
+
if (!f) return;
|
|
1962
|
+
if (f.size > SOUND_MAX_BYTES) {
|
|
1963
|
+
// Clear on every exit so re-picking the same file still fires.
|
|
1964
|
+
input.value = "";
|
|
1965
|
+
setFeedback(t("soundTooBig") + Math.round(SOUND_MAX_BYTES / 1024) + " KB");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
var fr = new FileReader();
|
|
1969
|
+
fr.onload = function () {
|
|
1970
|
+
input.value = "";
|
|
1971
|
+
var data = String(fr.result || "");
|
|
1972
|
+
if (data.indexOf("data:audio/") !== 0) { setFeedback(t("soundNotAudio")); return; }
|
|
1973
|
+
if (!writeSound(kind, { name: f.name, size: f.size, data: data })) {
|
|
1974
|
+
setFeedback(t("soundQuota"));
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
setSoundBump(soundBump + 1);
|
|
1978
|
+
setFeedback(t("soundSaved") + f.name);
|
|
1979
|
+
};
|
|
1980
|
+
fr.onerror = function () { input.value = ""; setFeedback(t("soundReadFail")); };
|
|
1981
|
+
fr.readAsDataURL(f);
|
|
1982
|
+
}
|
|
1983
|
+
}),
|
|
1984
|
+
h("button", {
|
|
1985
|
+
type: "button",
|
|
1986
|
+
className: "dnm-btn",
|
|
1987
|
+
disabled: !rec,
|
|
1988
|
+
"aria-label": previewLabel,
|
|
1989
|
+
onClick: function () { previewSound(kind); }
|
|
1990
|
+
}, t("soundPreview")),
|
|
1991
|
+
h("button", {
|
|
1992
|
+
type: "button",
|
|
1993
|
+
className: "dnm-btn",
|
|
1994
|
+
disabled: !rec,
|
|
1995
|
+
onClick: function () {
|
|
1996
|
+
writeSound(kind, null);
|
|
1997
|
+
setSoundBump(soundBump + 1);
|
|
1998
|
+
setFeedback(t("soundCleared"));
|
|
1999
|
+
}
|
|
2000
|
+
}, t("soundClear"))
|
|
2001
|
+
])
|
|
2002
|
+
]));
|
|
2003
|
+
return rows;
|
|
2004
|
+
}
|
|
1729
2005
|
function runTest(kind) {
|
|
1730
2006
|
if (!config.enabled) { setFeedback(t("offHint")); return; }
|
|
1731
2007
|
unlockOnce();
|
|
@@ -1801,6 +2077,20 @@
|
|
|
1801
2077
|
})
|
|
1802
2078
|
])
|
|
1803
2079
|
]),
|
|
2080
|
+
// Alert sound source: the built-in cue, a picked file, or the platform's
|
|
2081
|
+
// own notification sound. Kept next to the Sound/volume card so the
|
|
2082
|
+
// sound-related knobs read together, and re-read through soundBump
|
|
2083
|
+
// because picked files live outside the config object.
|
|
2084
|
+
card([].concat(
|
|
2085
|
+
[h("div", { className: "dnm-row" }, [
|
|
2086
|
+
h("div", { className: "dnm-rowText" }, [
|
|
2087
|
+
h("div", { className: "dnm-rowLabel" }, t("soundSource")),
|
|
2088
|
+
h("div", { className: "dnm-rowHint" }, t("soundSourceHint"))
|
|
2089
|
+
])
|
|
2090
|
+
])],
|
|
2091
|
+
soundRows("done", t("soundDoneLabel")),
|
|
2092
|
+
soundRows("attention", t("soundAttentionLabel"))
|
|
2093
|
+
)),
|
|
1804
2094
|
card([
|
|
1805
2095
|
h("div", { className: "dnm-row" }, [
|
|
1806
2096
|
h("div", { className: "dnm-rowText" }, [
|
|
@@ -1843,17 +2133,21 @@
|
|
|
1843
2133
|
h("div", { className: "dnm-rowHint" }, " ")
|
|
1844
2134
|
]),
|
|
1845
2135
|
h("button", { type: "button", className: "dnm-btn", onClick: function () {
|
|
1846
|
-
|
|
1847
|
-
|
|
2136
|
+
// api.resetConfig() also drops the picked cue files, which are
|
|
2137
|
+
// stored outside the config.
|
|
2138
|
+
setCfg(api.resetConfig());
|
|
1848
2139
|
applyTitle();
|
|
1849
|
-
|
|
2140
|
+
setSoundBump(soundBump + 1);
|
|
1850
2141
|
setFeedback(t("resetDone"));
|
|
1851
2142
|
} }, t("reset"))
|
|
1852
2143
|
]),
|
|
1853
2144
|
!permOK ? h("div", { className: "dnm-row" }, [
|
|
1854
2145
|
h("div", { className: "dnm-rowText" }, [h("div", { className: "dnm-rowHint" }, t("permHint"))])
|
|
1855
2146
|
]) : null
|
|
1856
|
-
])
|
|
2147
|
+
]),
|
|
2148
|
+
// Always-visible version line at the page foot: bug reports need it,
|
|
2149
|
+
// and "which build am I on" should not require the console.
|
|
2150
|
+
h("p", { className: "dnm-hint" }, t("versionLine") + api.version)
|
|
1857
2151
|
]);
|
|
1858
2152
|
}
|
|
1859
2153
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notify-me",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
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",
|