dsh-notify-tone 0.1.0 → 0.2.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/README.md +16 -12
- package/lib/client.js +484 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
# dsh-notify-tone
|
|
1
|
+
# dsh-notify-tone(声音 + 视觉 + 系统通知提醒插件)
|
|
2
2
|
|
|
3
|
-
dsh web GUI
|
|
3
|
+
dsh web GUI 的提醒插件:
|
|
4
4
|
|
|
5
|
-
- **需要操作提醒**:AI 执行任务中遇到需要用户授权或选择时(沙箱权限授权弹窗、`ask_user_question`
|
|
6
|
-
- **回答完成提醒**:AI
|
|
5
|
+
- **需要操作提醒**:AI 执行任务中遇到需要用户授权或选择时(沙箱权限授权弹窗、`ask_user_question` 提问、计划审批等),同时触发提示音、屏幕光效与系统通知,提醒你回到窗口处理。
|
|
6
|
+
- **回答完成提醒**:AI 每轮回答结束时,同样触发三重提醒,提醒你回复已生成。
|
|
7
7
|
|
|
8
8
|
## 特性
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **不侵入**:不改动 dsh 任何原有组件与第三方插件;插件只做「监听会话状态 + 发声 + 一个悬浮按钮/菜单」。
|
|
10
|
+
- **声音提醒**:Web Audio 合成(基频 + 泛音,无音频文件),内置 **5 套音色**——经典双音、清脆叮咚(叮——)、明亮三连、柔和轻音、**尖锐警示**(方波高频,最醒目),点击即可试听并选中。
|
|
11
|
+
- **视觉提醒**:屏幕四周大圆弧光带 + 泡泡式弹性向内挤压脉冲(柔和、不遮挡、不拦截鼠标);「需要操作」「回答完成」**各自独立配色**(8 种预设 + 🌈 自定义色相),选色即时预览。
|
|
12
|
+
- **系统通知**:触发时弹出**操作系统级桌面通知**(含系统声音),**在浏览器外、其他窗口、浏览器最小化时也能看到**;点击通知可跳回 dsh 窗口。首次使用需在浏览器中授权(打开开关时自动请求)。
|
|
13
|
+
- **分别开关**:总开关、声音开关、视觉开关、系统通知开关**四者独立**,互不影响。
|
|
14
|
+
- **持久生效**:所有开关、音色、颜色都存 `localStorage`,重启后保持;旧版设置(`dsh.notify-sound.*`)自动迁移。
|
|
15
|
+
- **悬浮设置菜单**:鼠标悬停页面右下角 🔔 按钮自动弹出:总开关、声音/视觉/系统通知分别开关、音色选择(点击试听)、颜色选择(展开面板,点击预览)。
|
|
16
|
+
- **不侵入**:不改动 dsh 任何原有组件与第三方插件;插件只做「监听会话状态 + 发声 + 视觉层 + 系统通知 + 一个悬浮按钮/菜单」。
|
|
17
|
+

|
|
18
|
+

|
|
19
|
+

|
|
17
20
|
|
|
18
21
|
## 安装
|
|
19
22
|
|
|
20
|
-
### 方式一:npm
|
|
23
|
+
### 方式一:npm 安装(已发布到npm)
|
|
21
24
|
|
|
22
25
|
```bash
|
|
23
26
|
dsh plugin --profile web add dsh-notify-tone
|
|
@@ -54,4 +57,5 @@ dsh plugin --profile web add link:/绝对路径/dsh-notify-tone
|
|
|
54
57
|
## 故障排查
|
|
55
58
|
|
|
56
59
|
- 听不到声音:浏览器自动播放策略要求页面至少被点击过一次;点击页面任意位置后再试。
|
|
60
|
+
- 收不到系统通知:① 打开 🔔 菜单「系统通知」开关并允许浏览器的授权询问;② 若显示"权限已被禁用",需到浏览器站点设置(地址栏左侧锁图标 → 网站设置)中把「通知」改为允许;③ Windows 还需确认系统"通知和操作"未对浏览器静音。
|
|
57
61
|
- 若之后运行过 `pnpm install` 导致手动放置的包被清理,请重新复制本目录到 `~/.dsh/profiles/web/node_modules/`(或在 profile 目录执行 `dsh plugin --profile web add link:<本目录绝对路径>`)。
|
package/lib/client.js
CHANGED
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 功能:
|
|
4
4
|
// 1. AI 执行任务中需要用户授权/选择时(sandbox 授权弹窗、ask_user_question、
|
|
5
|
-
// plan review 等,即会话出现 pendingInteraction
|
|
6
|
-
// 2. AI 每轮回答结束时(会话 running -> false
|
|
7
|
-
// 3.
|
|
8
|
-
//
|
|
5
|
+
// plan review 等,即会话出现 pendingInteraction)触发「需要操作」提醒;
|
|
6
|
+
// 2. AI 每轮回答结束时(会话 running -> false)触发「回答完成」提醒。
|
|
7
|
+
// 3. 提醒 = 声音 + 视觉,可分别开关:
|
|
8
|
+
// - 声音:多套内置音色(经典双音 / 清脆叮咚 / 明亮三连 / 柔和轻音),
|
|
9
|
+
// 用 Web Audio 合成(基频 + 泛音,无需音频文件);
|
|
10
|
+
// - 视觉:屏幕四周大圆弧光带 + 泡泡式弹性向内挤压脉冲,
|
|
11
|
+
// 「需要操作」「回答完成」各自独立配色(预设 8 色 + 自定义色相)。
|
|
12
|
+
// 4. 鼠标悬停 🔔 按钮弹出设置菜单:总开关、声音/视觉分别开关、
|
|
13
|
+
// 音色选择(点击试听)、颜色选择(点击预览)。
|
|
9
14
|
//
|
|
10
15
|
// 实现要点:
|
|
11
|
-
// - 零依赖:不 require 任何 dsh 客户端模块,只用注入的 ctx.sessions
|
|
12
|
-
//
|
|
13
|
-
// -
|
|
14
|
-
// - 开关与所选提示音存 localStorage,持久生效。
|
|
15
|
-
// - 不改动 dsh 任何原有组件:只做「监听 + 发声 + 一个悬浮按钮/菜单」。
|
|
16
|
+
// - 零依赖:不 require 任何 dsh 客户端模块,只用注入的 ctx.sessions 服务。
|
|
17
|
+
// - 所有设置存 localStorage,持久生效;旧版 key(dsh.notify-sound.*)自动迁移。
|
|
18
|
+
// - 不改动 dsh 任何原有组件:只做「监听 + 发声 + 视觉层 + 一个悬浮按钮/菜单」。
|
|
16
19
|
|
|
17
20
|
window.__ModuleLoader__.load({
|
|
18
21
|
id: "dsh-notify-tone",
|
|
@@ -22,15 +25,21 @@ window.__ModuleLoader__.load({
|
|
|
22
25
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
23
26
|
|
|
24
27
|
// ===================== 常量 =====================
|
|
25
|
-
const KEY_ENABLED = "dsh.notify-
|
|
26
|
-
const
|
|
28
|
+
const KEY_ENABLED = "dsh.notify-tone.enabled";
|
|
29
|
+
const KEY_SOUND_ENABLED = "dsh.notify-tone.sound.enabled";
|
|
30
|
+
const KEY_PRESET = "dsh.notify-tone.sound.preset";
|
|
31
|
+
const KEY_VISUAL_ENABLED = "dsh.notify-tone.visual.enabled";
|
|
32
|
+
const KEY_HUE_INTERACT = "dsh.notify-tone.visual.interactHue";
|
|
33
|
+
const KEY_HUE_DONE = "dsh.notify-tone.visual.doneHue";
|
|
34
|
+
const KEY_NOTIFICATION_ENABLED = "dsh.notify-tone.notification.enabled";
|
|
35
|
+
|
|
27
36
|
const ROOT_ID = "dsh-notify-tone-root";
|
|
28
37
|
const TOGGLE_ID = "dsh-notify-tone-toggle";
|
|
29
38
|
const MENU_ID = "dsh-notify-tone-menu";
|
|
39
|
+
const FX_ID = "dsh-notify-tone-fx";
|
|
40
|
+
const FX_STYLE_ID = "dsh-notify-tone-fx-style";
|
|
30
41
|
|
|
31
42
|
// ===================== 提示音预设 =====================
|
|
32
|
-
// 每个预设含 interact(需要授权/选择)与 done(回答完成)两组音符。
|
|
33
|
-
// 音符格式:[频率Hz, 时长s];gap 为音符间隔(秒)。
|
|
34
43
|
const PRESETS = {
|
|
35
44
|
classic: {
|
|
36
45
|
label: "经典双音",
|
|
@@ -52,11 +61,43 @@ window.__ModuleLoader__.load({
|
|
|
52
61
|
interact: { tones: [[523, 0.2], [659, 0.25]], gap: 0.09 },
|
|
53
62
|
done: { tones: [[659, 0.28]], gap: 0 },
|
|
54
63
|
},
|
|
64
|
+
sharp: {
|
|
65
|
+
label: "尖锐警示",
|
|
66
|
+
wave: "square",
|
|
67
|
+
volume: 0.26,
|
|
68
|
+
interact: { tones: [[1760, 0.09], [2093, 0.09], [2637, 0.16]], gap: 0.06 },
|
|
69
|
+
done: { tones: [[2093, 0.28]], gap: 0 },
|
|
70
|
+
},
|
|
55
71
|
};
|
|
56
72
|
const PRESET_IDS = Object.keys(PRESETS);
|
|
57
|
-
/** 默认音色:清脆叮咚(比经典双音更明显,如「叮——」)。 */
|
|
58
73
|
const DEFAULT_PRESET = "ding";
|
|
59
74
|
|
|
75
|
+
// 预设颜色(色相值),与 demo 一致
|
|
76
|
+
const COLOR_PRESETS = [35, 15, 0, 320, 270, 215, 180, 145];
|
|
77
|
+
const DEFAULT_HUE_INTERACT = 35;
|
|
78
|
+
const DEFAULT_HUE_DONE = 145;
|
|
79
|
+
|
|
80
|
+
// 视觉固定参数(demo 确认过的默认值;暂不提供菜单滑块,保持简洁)
|
|
81
|
+
const VISUAL = { amp: "26px", glow: "0.55", dur: "1.3s", radius: "34px" };
|
|
82
|
+
|
|
83
|
+
// ===================== 旧版设置迁移(dsh.notify-sound.* → dsh.notify-tone.*) =====================
|
|
84
|
+
(function migrateOldKeys() {
|
|
85
|
+
try {
|
|
86
|
+
const map = {
|
|
87
|
+
"dsh.notify-sound.enabled": KEY_ENABLED,
|
|
88
|
+
"dsh.notify-sound.preset": KEY_PRESET,
|
|
89
|
+
};
|
|
90
|
+
for (const [oldKey, newKey] of Object.entries(map)) {
|
|
91
|
+
if (localStorage.getItem(oldKey) !== null && localStorage.getItem(newKey) === null) {
|
|
92
|
+
localStorage.setItem(newKey, localStorage.getItem(oldKey));
|
|
93
|
+
localStorage.removeItem(oldKey);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
/* 迁移失败不影响使用 */
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
|
|
60
101
|
// ===================== 声音引擎(Web Audio,无音频文件) =====================
|
|
61
102
|
let audioCtx = null;
|
|
62
103
|
|
|
@@ -79,13 +120,13 @@ window.__ModuleLoader__.load({
|
|
|
79
120
|
|
|
80
121
|
/**
|
|
81
122
|
* 播放一个音符:基频 + 2 倍频泛音(更明亮、更明显),指数衰减包络。
|
|
123
|
+
* @param wave - 波形(sine/square/triangle/sawtooth),尖锐音用 square。
|
|
82
124
|
*/
|
|
83
|
-
function playTone(ctx, freq, dur, start, volume) {
|
|
125
|
+
function playTone(ctx, freq, dur, start, volume, wave) {
|
|
84
126
|
const stop = start + dur + 0.05;
|
|
85
|
-
// 基频
|
|
86
127
|
const osc = ctx.createOscillator();
|
|
87
128
|
const gain = ctx.createGain();
|
|
88
|
-
osc.type = "sine";
|
|
129
|
+
osc.type = wave ?? "sine";
|
|
89
130
|
osc.frequency.setValueAtTime(freq, start);
|
|
90
131
|
gain.gain.setValueAtTime(0.0001, start);
|
|
91
132
|
gain.gain.exponentialRampToValueAtTime(volume, start + 0.02);
|
|
@@ -97,7 +138,7 @@ window.__ModuleLoader__.load({
|
|
|
97
138
|
// 泛音(2 倍频,音量更低,增强穿透力)
|
|
98
139
|
const osc2 = ctx.createOscillator();
|
|
99
140
|
const gain2 = ctx.createGain();
|
|
100
|
-
osc2.type = "sine";
|
|
141
|
+
osc2.type = wave ?? "sine";
|
|
101
142
|
osc2.frequency.setValueAtTime(freq * 2, start);
|
|
102
143
|
gain2.gain.setValueAtTime(0.0001, start);
|
|
103
144
|
gain2.gain.exponentialRampToValueAtTime(volume * 0.35, start + 0.02);
|
|
@@ -108,18 +149,105 @@ window.__ModuleLoader__.load({
|
|
|
108
149
|
osc2.stop(stop);
|
|
109
150
|
}
|
|
110
151
|
|
|
111
|
-
/**
|
|
112
|
-
function playPreset(tones, gap) {
|
|
152
|
+
/** 按音符序列播放一组提示音(支持预设波形与音量)。 */
|
|
153
|
+
function playPreset(tones, gap, wave, volume) {
|
|
113
154
|
const ctx = ensureAudio();
|
|
114
155
|
if (ctx === null) return;
|
|
115
|
-
const
|
|
156
|
+
const vol = volume ?? 0.22;
|
|
116
157
|
let t = ctx.currentTime + 0.02;
|
|
117
158
|
for (const [freq, dur] of tones) {
|
|
118
|
-
playTone(ctx, freq, dur, t,
|
|
159
|
+
playTone(ctx, freq, dur, t, vol, wave);
|
|
119
160
|
t += dur + (gap ?? 0);
|
|
120
161
|
}
|
|
121
162
|
}
|
|
122
163
|
|
|
164
|
+
// ===================== 视觉层 CSS(注入 <style>) =====================
|
|
165
|
+
const FX_CSS = `
|
|
166
|
+
#${FX_ID} {
|
|
167
|
+
position: fixed; inset: 0;
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
z-index: 2147483000;
|
|
170
|
+
--hue: 35;
|
|
171
|
+
--amp: ${VISUAL.amp};
|
|
172
|
+
--glow: ${VISUAL.glow};
|
|
173
|
+
--dur: ${VISUAL.dur};
|
|
174
|
+
--radius: ${VISUAL.radius};
|
|
175
|
+
}
|
|
176
|
+
#${FX_ID} .halo {
|
|
177
|
+
position: absolute; inset: 0;
|
|
178
|
+
border-radius: var(--radius);
|
|
179
|
+
opacity: 0;
|
|
180
|
+
box-shadow:
|
|
181
|
+
inset 0 0 calc(150px * var(--glow)) calc(22px * var(--glow)) hsla(var(--hue), 90%, 62%, calc(var(--glow) * 0.5)),
|
|
182
|
+
inset 0 0 calc(50px * var(--glow)) calc(10px * var(--glow)) hsla(var(--hue), 95%, 70%, calc(var(--glow) * 0.4));
|
|
183
|
+
}
|
|
184
|
+
#${FX_ID} .arc {
|
|
185
|
+
position: absolute;
|
|
186
|
+
opacity: 0;
|
|
187
|
+
filter: blur(3px);
|
|
188
|
+
}
|
|
189
|
+
#${FX_ID} .arc.top {
|
|
190
|
+
top: -40px; left: -8%; right: -8%; height: 280px;
|
|
191
|
+
background: radial-gradient(150% 100% at 50% 0%, hsla(var(--hue), 95%, 74%, calc(var(--glow) * 0.95)), transparent 60%);
|
|
192
|
+
}
|
|
193
|
+
#${FX_ID} .arc.bottom {
|
|
194
|
+
bottom: -40px; left: -8%; right: -8%; height: 280px;
|
|
195
|
+
background: radial-gradient(150% 100% at 50% 100%, hsla(var(--hue), 95%, 74%, calc(var(--glow) * 0.95)), transparent 60%);
|
|
196
|
+
}
|
|
197
|
+
#${FX_ID} .arc.left {
|
|
198
|
+
left: -40px; top: -8%; bottom: -8%; width: 280px;
|
|
199
|
+
background: radial-gradient(100% 150% at 0% 50%, hsla(var(--hue), 95%, 74%, calc(var(--glow) * 0.95)), transparent 60%);
|
|
200
|
+
}
|
|
201
|
+
#${FX_ID} .arc.right {
|
|
202
|
+
right: -40px; top: -8%; bottom: -8%; width: 280px;
|
|
203
|
+
background: radial-gradient(100% 150% at 100% 50%, hsla(var(--hue), 95%, 74%, calc(var(--glow) * 0.95)), transparent 60%);
|
|
204
|
+
}
|
|
205
|
+
#${FX_ID}.play .halo { animation: dshnt-halo var(--dur) ease-out forwards; }
|
|
206
|
+
#${FX_ID}.play .arc.top { animation: dshnt-pop-top var(--dur) cubic-bezier(.22, 1.5, .36, 1) forwards; }
|
|
207
|
+
#${FX_ID}.play .arc.bottom { animation: dshnt-pop-bottom var(--dur) cubic-bezier(.22, 1.5, .36, 1) forwards; }
|
|
208
|
+
#${FX_ID}.play .arc.left { animation: dshnt-pop-left var(--dur) cubic-bezier(.22, 1.5, .36, 1) forwards; }
|
|
209
|
+
#${FX_ID}.play .arc.right { animation: dshnt-pop-right var(--dur) cubic-bezier(.22, 1.5, .36, 1) forwards; }
|
|
210
|
+
@keyframes dshnt-halo {
|
|
211
|
+
0% { opacity: 0; }
|
|
212
|
+
12% { opacity: calc(var(--glow)); }
|
|
213
|
+
40% { opacity: calc(var(--glow) * 1.0); }
|
|
214
|
+
72% { opacity: calc(var(--glow) * 0.5); }
|
|
215
|
+
100% { opacity: 0; }
|
|
216
|
+
}
|
|
217
|
+
@keyframes dshnt-pop-top {
|
|
218
|
+
0% { opacity: 0; transform: translateY(0); }
|
|
219
|
+
10% { opacity: 1; }
|
|
220
|
+
38% { opacity: 1; transform: translateY(var(--amp)); }
|
|
221
|
+
62% { transform: translateY(calc(var(--amp) * -0.12)); }
|
|
222
|
+
82% { transform: translateY(0); }
|
|
223
|
+
100% { opacity: 0; }
|
|
224
|
+
}
|
|
225
|
+
@keyframes dshnt-pop-bottom {
|
|
226
|
+
0% { opacity: 0; transform: translateY(0); }
|
|
227
|
+
10% { opacity: 1; }
|
|
228
|
+
38% { opacity: 1; transform: translateY(calc(var(--amp) * -1)); }
|
|
229
|
+
62% { transform: translateY(calc(var(--amp) * 0.12)); }
|
|
230
|
+
82% { transform: translateY(0); }
|
|
231
|
+
100% { opacity: 0; }
|
|
232
|
+
}
|
|
233
|
+
@keyframes dshnt-pop-left {
|
|
234
|
+
0% { opacity: 0; transform: translateX(0); }
|
|
235
|
+
10% { opacity: 1; }
|
|
236
|
+
38% { opacity: 1; transform: translateX(var(--amp)); }
|
|
237
|
+
62% { transform: translateX(calc(var(--amp) * -0.12)); }
|
|
238
|
+
82% { transform: translateX(0); }
|
|
239
|
+
100% { opacity: 0; }
|
|
240
|
+
}
|
|
241
|
+
@keyframes dshnt-pop-right {
|
|
242
|
+
0% { opacity: 0; transform: translateX(0); }
|
|
243
|
+
10% { opacity: 1; }
|
|
244
|
+
38% { opacity: 1; transform: translateX(calc(var(--amp) * -1)); }
|
|
245
|
+
62% { transform: translateX(calc(var(--amp) * 0.12)); }
|
|
246
|
+
82% { transform: translateX(0); }
|
|
247
|
+
100% { opacity: 0; }
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
|
|
123
251
|
// ===================== 状态(localStorage 持久化) =====================
|
|
124
252
|
function storageGet(key, fallback) {
|
|
125
253
|
try {
|
|
@@ -137,6 +265,7 @@ window.__ModuleLoader__.load({
|
|
|
137
265
|
}
|
|
138
266
|
}
|
|
139
267
|
|
|
268
|
+
// 总开关
|
|
140
269
|
function isEnabled() {
|
|
141
270
|
return storageGet(KEY_ENABLED, "true") !== "false";
|
|
142
271
|
}
|
|
@@ -144,6 +273,23 @@ window.__ModuleLoader__.load({
|
|
|
144
273
|
storageSet(KEY_ENABLED, value ? "true" : "false");
|
|
145
274
|
}
|
|
146
275
|
|
|
276
|
+
// 声音开关
|
|
277
|
+
function isSoundEnabled() {
|
|
278
|
+
return storageGet(KEY_SOUND_ENABLED, "true") !== "false";
|
|
279
|
+
}
|
|
280
|
+
function setSoundEnabled(value) {
|
|
281
|
+
storageSet(KEY_SOUND_ENABLED, value ? "true" : "false");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 视觉开关
|
|
285
|
+
function isVisualEnabled() {
|
|
286
|
+
return storageGet(KEY_VISUAL_ENABLED, "true") !== "false";
|
|
287
|
+
}
|
|
288
|
+
function setVisualEnabled(value) {
|
|
289
|
+
storageSet(KEY_VISUAL_ENABLED, value ? "true" : "false");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 音色预设
|
|
147
293
|
function currentPresetId() {
|
|
148
294
|
const id = storageGet(KEY_PRESET, DEFAULT_PRESET);
|
|
149
295
|
return PRESETS[id] ? id : DEFAULT_PRESET;
|
|
@@ -152,6 +298,65 @@ window.__ModuleLoader__.load({
|
|
|
152
298
|
if (PRESETS[id]) storageSet(KEY_PRESET, id);
|
|
153
299
|
}
|
|
154
300
|
|
|
301
|
+
// 各功能颜色(色相)
|
|
302
|
+
function interactHue() {
|
|
303
|
+
const v = Number(storageGet(KEY_HUE_INTERACT, String(DEFAULT_HUE_INTERACT)));
|
|
304
|
+
return Number.isFinite(v) ? v : DEFAULT_HUE_INTERACT;
|
|
305
|
+
}
|
|
306
|
+
function doneHue() {
|
|
307
|
+
const v = Number(storageGet(KEY_HUE_DONE, String(DEFAULT_HUE_DONE)));
|
|
308
|
+
return Number.isFinite(v) ? v : DEFAULT_HUE_DONE;
|
|
309
|
+
}
|
|
310
|
+
function setInteractHue(h) {
|
|
311
|
+
if (Number.isFinite(h)) storageSet(KEY_HUE_INTERACT, String(Math.round(h)));
|
|
312
|
+
}
|
|
313
|
+
function setDoneHue(h) {
|
|
314
|
+
if (Number.isFinite(h)) storageSet(KEY_HUE_DONE, String(Math.round(h)));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 系统通知开关
|
|
318
|
+
function isNotificationEnabled() {
|
|
319
|
+
return storageGet(KEY_NOTIFICATION_ENABLED, "true") !== "false";
|
|
320
|
+
}
|
|
321
|
+
function setNotificationEnabled(value) {
|
|
322
|
+
storageSet(KEY_NOTIFICATION_ENABLED, value ? "true" : "false");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ===================== 系统通知(桌面通知,浏览器外可见) =====================
|
|
326
|
+
/** 若权限未定,请求一次通知权限(需在用户交互后调用才有效)。 */
|
|
327
|
+
function ensureNotificationPermission() {
|
|
328
|
+
if (typeof Notification === "undefined") return;
|
|
329
|
+
if (Notification.permission === "default" && typeof Notification.requestPermission === "function") {
|
|
330
|
+
Notification.requestPermission().catch(() => {});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 弹桌面系统通知(任何窗口下都能看到;随系统声音提醒)。 */
|
|
335
|
+
function notifyUser(kind) {
|
|
336
|
+
if (typeof Notification === "undefined") return;
|
|
337
|
+
if (Notification.permission !== "granted") return;
|
|
338
|
+
const isInteract = kind === "interact";
|
|
339
|
+
try {
|
|
340
|
+
const n = new Notification(
|
|
341
|
+
isInteract ? "dsh:AI 需要你的操作" : "dsh:AI 回答完成",
|
|
342
|
+
{
|
|
343
|
+
body: isInteract
|
|
344
|
+
? "有授权或选择等待处理,请回到 dsh 窗口"
|
|
345
|
+
: "本轮回答已生成,可以回来查看了",
|
|
346
|
+
tag: "dsh-notify-tone",
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
n.onclick = () => {
|
|
350
|
+
try {
|
|
351
|
+
if (typeof window !== "undefined" && typeof window.focus === "function") window.focus();
|
|
352
|
+
} catch { /* 忽略 */ }
|
|
353
|
+
n.close();
|
|
354
|
+
};
|
|
355
|
+
} catch {
|
|
356
|
+
/* 通知被浏览器拦截时静默 */
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
155
360
|
// ===================== 事件节流(防连发) =====================
|
|
156
361
|
const lastPlayed = { interact: 0, done: 0 };
|
|
157
362
|
function throttledPlay(kind, minGapMs) {
|
|
@@ -159,13 +364,21 @@ window.__ModuleLoader__.load({
|
|
|
159
364
|
const now = Date.now();
|
|
160
365
|
if (now - lastPlayed[kind] < minGapMs) return;
|
|
161
366
|
lastPlayed[kind] = now;
|
|
162
|
-
|
|
163
|
-
|
|
367
|
+
// 声音 + 视觉 + 系统通知同时触发(各自可独立开关)
|
|
368
|
+
if (isSoundEnabled()) {
|
|
369
|
+
const preset = PRESETS[currentPresetId()];
|
|
370
|
+
const spec = preset[kind];
|
|
371
|
+
playPreset(spec.tones, spec.gap, preset.wave, preset.volume);
|
|
372
|
+
}
|
|
373
|
+
if (isVisualEnabled()) {
|
|
374
|
+
playFx(kind);
|
|
375
|
+
}
|
|
376
|
+
if (isNotificationEnabled()) {
|
|
377
|
+
notifyUser(kind);
|
|
378
|
+
}
|
|
164
379
|
}
|
|
165
380
|
|
|
166
381
|
// ===================== 会话状态订阅 =====================
|
|
167
|
-
// 基线:id -> { running, pending }。首次快照只建基线不发声,
|
|
168
|
-
// 避免页面刷新时对「正在运行」的会话误报。
|
|
169
382
|
let prevBySession = new Map();
|
|
170
383
|
|
|
171
384
|
function handleListChange(ctx) {
|
|
@@ -184,22 +397,54 @@ window.__ModuleLoader__.load({
|
|
|
184
397
|
for (const [id, cur] of now) {
|
|
185
398
|
const prev = prevBySession.get(id);
|
|
186
399
|
if (prev === undefined) continue; // 新出现的会话:只入基线
|
|
187
|
-
// 需要用户授权/选择:pendingInteraction 从无到有
|
|
188
400
|
if (prev.pending === undefined && cur.pending !== undefined) {
|
|
189
401
|
throttledPlay("interact", 1200);
|
|
190
402
|
}
|
|
191
|
-
// 每轮回答结束:running true -> false
|
|
192
403
|
if (prev.running && !cur.running) {
|
|
193
404
|
throttledPlay("done", 2000);
|
|
194
405
|
}
|
|
195
406
|
}
|
|
196
|
-
// 已被移除的会话从基线中清理
|
|
197
407
|
for (const id of prevBySession.keys()) {
|
|
198
408
|
if (!now.has(id)) prevBySession.delete(id);
|
|
199
409
|
}
|
|
200
410
|
prevBySession = now;
|
|
201
411
|
}
|
|
202
412
|
|
|
413
|
+
// ===================== 视觉层(挂载 + 播放) =====================
|
|
414
|
+
function mountFx() {
|
|
415
|
+
if (typeof document === "undefined") return;
|
|
416
|
+
if (document.getElementById(FX_ID)) return;
|
|
417
|
+
if (!document.body || !document.head) {
|
|
418
|
+
document.addEventListener("DOMContentLoaded", mountFx, { once: true });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!document.getElementById(FX_STYLE_ID)) {
|
|
422
|
+
const style = document.createElement("style");
|
|
423
|
+
style.id = FX_STYLE_ID;
|
|
424
|
+
style.textContent = FX_CSS;
|
|
425
|
+
document.head.appendChild(style);
|
|
426
|
+
}
|
|
427
|
+
const root = document.createElement("div");
|
|
428
|
+
root.id = FX_ID;
|
|
429
|
+
root.innerHTML =
|
|
430
|
+
'<div class="halo"></div>' +
|
|
431
|
+
'<div class="arc top"></div>' +
|
|
432
|
+
'<div class="arc bottom"></div>' +
|
|
433
|
+
'<div class="arc left"></div>' +
|
|
434
|
+
'<div class="arc right"></div>';
|
|
435
|
+
document.body.appendChild(root);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function playFx(kind) {
|
|
439
|
+
if (typeof document === "undefined") return;
|
|
440
|
+
const root = document.getElementById(FX_ID);
|
|
441
|
+
if (!root) return;
|
|
442
|
+
root.style.setProperty("--hue", kind === "interact" ? interactHue() : doneHue());
|
|
443
|
+
root.classList.remove("play");
|
|
444
|
+
void root.offsetWidth; // 强制 reflow 重启动画
|
|
445
|
+
root.classList.add("play");
|
|
446
|
+
}
|
|
447
|
+
|
|
203
448
|
// ===================== 悬浮按钮 + hover 设置菜单 =====================
|
|
204
449
|
function el(tag, text, css) {
|
|
205
450
|
const node = document.createElement(tag);
|
|
@@ -212,79 +457,209 @@ window.__ModuleLoader__.load({
|
|
|
212
457
|
const on = isEnabled();
|
|
213
458
|
btn.textContent = on ? "🔔" : "🔕";
|
|
214
459
|
btn.title = on
|
|
215
|
-
? "
|
|
216
|
-
: "
|
|
217
|
-
btn.setAttribute("aria-label", on ? "
|
|
460
|
+
? "提醒已开启。鼠标悬停可设置声音/视觉/颜色"
|
|
461
|
+
: "提醒已关闭。鼠标悬停可设置";
|
|
462
|
+
btn.setAttribute("aria-label", on ? "提醒已开启" : "提醒已关闭");
|
|
218
463
|
btn.style.opacity = on ? "0.9" : "0.55";
|
|
219
464
|
}
|
|
220
465
|
|
|
221
|
-
|
|
466
|
+
// 菜单内通用小组件
|
|
467
|
+
function switchButton(current, onToggle) {
|
|
468
|
+
const btn = el("button", current ? "已开启" : "已关闭",
|
|
469
|
+
"font-family:system-ui,sans-serif;border:1px solid rgba(128,128,128,0.4);background:rgba(255,255,255,0.08);color:#e8e8ea;border-radius:6px;padding:1px 9px;cursor:pointer;font-size:12px;");
|
|
470
|
+
btn.addEventListener("click", (event) => {
|
|
471
|
+
event.stopPropagation();
|
|
472
|
+
onToggle(!current);
|
|
473
|
+
});
|
|
474
|
+
return btn;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function presetButtons(onPick) {
|
|
478
|
+
const wrap = el("div", "", "display:flex;flex-wrap:wrap;gap:5px;margin-top:4px;");
|
|
479
|
+
for (const id of PRESET_IDS) {
|
|
480
|
+
const p = PRESETS[id];
|
|
481
|
+
const sel = id === currentPresetId();
|
|
482
|
+
const b = el("button", p.label,
|
|
483
|
+
"font-family:system-ui,sans-serif;font-size:11px;border-radius:6px;padding:3px 8px;cursor:pointer;border:1px solid " +
|
|
484
|
+
(sel ? "rgba(122,184,255,0.9)" : "rgba(128,128,128,0.35)") + ";" +
|
|
485
|
+
(sel ? "background:rgba(64,128,255,0.28);color:#dbe9ff;" : "background:rgba(255,255,255,0.06);color:#e8e8ea;"));
|
|
486
|
+
b.addEventListener("click", (event) => {
|
|
487
|
+
event.stopPropagation();
|
|
488
|
+
onPick(id);
|
|
489
|
+
});
|
|
490
|
+
wrap.appendChild(b);
|
|
491
|
+
}
|
|
492
|
+
return wrap;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 颜色选择器:预设色块 + 自定义色相滑块。
|
|
497
|
+
* @param currentHue - 当前色相。
|
|
498
|
+
* @param onPick - 颜色变化回调(只改值 + 预览,不重建菜单,保证滑块可连续拖动)。
|
|
499
|
+
* @param onCommitted - 提交回调(重建菜单刷新选中态;色块点击与滑块松手时调用)。
|
|
500
|
+
*/
|
|
501
|
+
function colorPicker(currentHue, onPick, onCommitted) {
|
|
502
|
+
const wrap = el("div", "", "display:flex;flex-direction:column;gap:4px;margin-top:4px;");
|
|
503
|
+
const dots = el("div", "", "display:flex;gap:4px;flex-wrap:wrap;");
|
|
504
|
+
for (const h of COLOR_PRESETS) {
|
|
505
|
+
const sel = Math.abs(h - currentHue) < 2;
|
|
506
|
+
const d = el("div", "", "width:16px;height:16px;border-radius:50%;cursor:pointer;flex:none;background:hsl(" + h + " 85% 60%);border:1px solid rgba(255,255,255,0.35);box-sizing:border-box;" + (sel ? "outline:2px solid #fff;outline-offset:1px;" : ""));
|
|
507
|
+
d.addEventListener("click", (event) => {
|
|
508
|
+
event.stopPropagation();
|
|
509
|
+
onPick(h);
|
|
510
|
+
if (typeof onCommitted === "function") onCommitted();
|
|
511
|
+
});
|
|
512
|
+
dots.appendChild(d);
|
|
513
|
+
}
|
|
514
|
+
wrap.appendChild(dots);
|
|
515
|
+
const hueRow = el("div", "", "display:flex;align-items:center;gap:6px;");
|
|
516
|
+
hueRow.appendChild(el("span", "🌈", "font-size:12px;"));
|
|
517
|
+
const slider = document.createElement("input");
|
|
518
|
+
slider.type = "range";
|
|
519
|
+
slider.min = "0";
|
|
520
|
+
slider.max = "360";
|
|
521
|
+
slider.step = "1";
|
|
522
|
+
slider.value = String(Math.round(currentHue));
|
|
523
|
+
slider.style.cssText = "flex:1;accent-color:#58a6ff;height:18px;";
|
|
524
|
+
slider.addEventListener("input", () => {
|
|
525
|
+
onPick(Number(slider.value)); // 拖动中只改色+预览,不重建菜单
|
|
526
|
+
});
|
|
527
|
+
slider.addEventListener("change", () => {
|
|
528
|
+
if (typeof onCommitted === "function") onCommitted(); // 松手后重建刷新 UI
|
|
529
|
+
});
|
|
530
|
+
hueRow.appendChild(slider);
|
|
531
|
+
wrap.appendChild(hueRow);
|
|
532
|
+
return wrap;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let expandedColor = null; // "interact" | "done" | null(菜单中当前展开的颜色选择器)
|
|
536
|
+
|
|
222
537
|
function renderMenu(menu, btn) {
|
|
223
538
|
updateToggle(btn);
|
|
224
539
|
menu.textContent = "";
|
|
225
540
|
const base = "font-family:system-ui,-apple-system,sans-serif;";
|
|
226
541
|
|
|
227
|
-
menu.appendChild(el("div", "🔔
|
|
542
|
+
menu.appendChild(el("div", "🔔 提醒设置", base + "font-weight:600;font-size:13px;margin-bottom:8px;"));
|
|
228
543
|
|
|
229
|
-
// ——
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
sw.addEventListener("click", (event) => {
|
|
234
|
-
event.stopPropagation();
|
|
235
|
-
const next = !isEnabled();
|
|
544
|
+
// —— 总开关 ——
|
|
545
|
+
const masterRow = el("div", "", base + "display:flex;justify-content:space-between;align-items:center;padding:4px 2px;");
|
|
546
|
+
masterRow.appendChild(el("span", "功能开关"));
|
|
547
|
+
masterRow.appendChild(switchButton(isEnabled(), (next) => {
|
|
236
548
|
setEnabled(next);
|
|
237
549
|
renderMenu(menu, btn);
|
|
550
|
+
if (next && isSoundEnabled()) {
|
|
551
|
+
const preset = PRESETS[currentPresetId()];
|
|
552
|
+
const spec = preset.done;
|
|
553
|
+
playPreset(spec.tones, spec.gap, preset.wave, preset.volume);
|
|
554
|
+
}
|
|
555
|
+
}));
|
|
556
|
+
menu.appendChild(masterRow);
|
|
557
|
+
|
|
558
|
+
menu.appendChild(el("div", "", "border-top:1px solid rgba(128,128,128,0.22);margin:7px 0;"));
|
|
559
|
+
|
|
560
|
+
// —— 声音提醒 ——
|
|
561
|
+
const soundRow = el("div", "", base + "display:flex;justify-content:space-between;align-items:center;padding:4px 2px;");
|
|
562
|
+
soundRow.appendChild(el("span", "声音提醒"));
|
|
563
|
+
soundRow.appendChild(switchButton(isSoundEnabled(), (next) => {
|
|
564
|
+
setSoundEnabled(next);
|
|
565
|
+
renderMenu(menu, btn);
|
|
238
566
|
if (next) {
|
|
239
|
-
const
|
|
240
|
-
|
|
567
|
+
const preset = PRESETS[currentPresetId()];
|
|
568
|
+
const spec = preset.done;
|
|
569
|
+
playPreset(spec.tones, spec.gap, preset.wave, preset.volume);
|
|
241
570
|
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
|
|
571
|
+
}));
|
|
572
|
+
menu.appendChild(soundRow);
|
|
573
|
+
if (isSoundEnabled()) {
|
|
574
|
+
menu.appendChild(presetButtons((id) => {
|
|
575
|
+
setPresetId(id);
|
|
576
|
+
const preset = PRESETS[id];
|
|
577
|
+
const spec = preset.interact;
|
|
578
|
+
playPreset(spec.tones, spec.gap, preset.wave, preset.volume); // 点击音色试听
|
|
579
|
+
renderMenu(menu, btn);
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
245
582
|
|
|
246
|
-
menu.appendChild(el("div", "", "border-top:1px solid rgba(128,128,128,0.22);margin:
|
|
583
|
+
menu.appendChild(el("div", "", "border-top:1px solid rgba(128,128,128,0.22);margin:7px 0;"));
|
|
247
584
|
|
|
248
|
-
// ——
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
585
|
+
// —— 视觉提醒 ——
|
|
586
|
+
const visualRow = el("div", "", base + "display:flex;justify-content:space-between;align-items:center;padding:4px 2px;");
|
|
587
|
+
visualRow.appendChild(el("span", "视觉提醒"));
|
|
588
|
+
visualRow.appendChild(switchButton(isVisualEnabled(), (next) => {
|
|
589
|
+
setVisualEnabled(next);
|
|
590
|
+
renderMenu(menu, btn);
|
|
591
|
+
if (next) playFx("done");
|
|
592
|
+
}));
|
|
593
|
+
menu.appendChild(visualRow);
|
|
594
|
+
|
|
595
|
+
if (isVisualEnabled()) {
|
|
596
|
+
// 「需要操作」颜色
|
|
597
|
+
const iRow = el("div", "", base + "display:flex;align-items:center;gap:6px;padding:3px 2px;cursor:pointer;");
|
|
598
|
+
const iDot = el("span", "", "width:13px;height:13px;border-radius:50%;flex:none;background:hsl(" + interactHue() + " 85% 60%);");
|
|
599
|
+
iRow.appendChild(iDot);
|
|
600
|
+
iRow.appendChild(el("span", "需要操作", base + "flex:1;font-size:12px;"));
|
|
601
|
+
const iBtn = el("button", expandedColor === "interact" ? "▴" : "▾", base + "border:none;background:transparent;color:#7ab8ff;cursor:pointer;font-size:12px;");
|
|
602
|
+
iRow.appendChild(iBtn);
|
|
603
|
+
iRow.addEventListener("click", (event) => {
|
|
604
|
+
// 必须阻止冒泡:renderMenu 会移除本元素,事件若冒泡到 document
|
|
605
|
+
// 的「点击外部关闭」监听,contains(已移除元素) 为 false 会误关菜单
|
|
264
606
|
event.stopPropagation();
|
|
265
|
-
|
|
607
|
+
expandedColor = expandedColor === "interact" ? null : "interact";
|
|
608
|
+
renderMenu(menu, btn);
|
|
266
609
|
});
|
|
267
|
-
|
|
268
|
-
|
|
610
|
+
menu.appendChild(iRow);
|
|
611
|
+
if (expandedColor === "interact") {
|
|
612
|
+
menu.appendChild(colorPicker(interactHue(),
|
|
613
|
+
(h) => { // onPick:改色 + 预览,不重建(滑块可连续拖动)
|
|
614
|
+
setInteractHue(h);
|
|
615
|
+
playFx("interact");
|
|
616
|
+
},
|
|
617
|
+
() => renderMenu(menu, btn) // onCommitted:提交后重建刷新选中态
|
|
618
|
+
));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 「回答完成」颜色
|
|
622
|
+
const dRow = el("div", "", base + "display:flex;align-items:center;gap:6px;padding:3px 2px;cursor:pointer;");
|
|
623
|
+
const dDot = el("span", "", "width:13px;height:13px;border-radius:50%;flex:none;background:hsl(" + doneHue() + " 85% 60%);");
|
|
624
|
+
dRow.appendChild(dDot);
|
|
625
|
+
dRow.appendChild(el("span", "回答完成", base + "flex:1;font-size:12px;"));
|
|
626
|
+
const dBtn = el("button", expandedColor === "done" ? "▴" : "▾", base + "border:none;background:transparent;color:#7ab8ff;cursor:pointer;font-size:12px;");
|
|
627
|
+
dRow.appendChild(dBtn);
|
|
628
|
+
dRow.addEventListener("click", (event) => {
|
|
629
|
+
event.stopPropagation(); // 同上:防止重建后误关菜单
|
|
630
|
+
expandedColor = expandedColor === "done" ? null : "done";
|
|
269
631
|
renderMenu(menu, btn);
|
|
270
632
|
});
|
|
271
|
-
|
|
272
|
-
|
|
633
|
+
menu.appendChild(dRow);
|
|
634
|
+
if (expandedColor === "done") {
|
|
635
|
+
menu.appendChild(colorPicker(doneHue(),
|
|
636
|
+
(h) => {
|
|
637
|
+
setDoneHue(h);
|
|
638
|
+
playFx("done");
|
|
639
|
+
},
|
|
640
|
+
() => renderMenu(menu, btn)
|
|
641
|
+
));
|
|
642
|
+
}
|
|
273
643
|
}
|
|
274
644
|
|
|
275
|
-
menu.appendChild(el("div", "
|
|
276
|
-
}
|
|
645
|
+
menu.appendChild(el("div", "", "border-top:1px solid rgba(128,128,128,0.22);margin:7px 0;"));
|
|
277
646
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
647
|
+
// —— 系统通知 ——
|
|
648
|
+
const notifRow = el("div", "", base + "display:flex;justify-content:space-between;align-items:center;padding:4px 2px;");
|
|
649
|
+
notifRow.appendChild(el("span", "系统通知"));
|
|
650
|
+
notifRow.appendChild(switchButton(isNotificationEnabled(), (next) => {
|
|
651
|
+
setNotificationEnabled(next);
|
|
652
|
+
if (next) ensureNotificationPermission(); // 打开时若未授权则请求权限
|
|
653
|
+
renderMenu(menu, btn);
|
|
654
|
+
}));
|
|
655
|
+
menu.appendChild(notifRow);
|
|
656
|
+
if (typeof Notification !== "undefined" && Notification.permission === "denied") {
|
|
657
|
+
menu.appendChild(el("div", "⚠ 通知权限已在浏览器设置中禁用,请到浏览器站点设置中开启", base + "color:rgba(255,160,120,0.9);font-size:11px;padding:1px 2px 4px;"));
|
|
658
|
+
} else if (typeof Notification !== "undefined" && Notification.permission === "default" && isNotificationEnabled()) {
|
|
659
|
+
menu.appendChild(el("div", "ℹ 尚未授权:打开开关或点击上方任意提醒会请求通知权限", base + "color:rgba(255,255,255,0.45);font-size:11px;padding:1px 2px 4px;"));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
menu.appendChild(el("div", "声音 / 视觉 / 系统通知 各自独立开关;颜色各功能独立", base + "color:rgba(255,255,255,0.45);font-size:11px;margin-top:8px;"));
|
|
288
663
|
}
|
|
289
664
|
|
|
290
665
|
function mountToggle() {
|
|
@@ -295,9 +670,9 @@ window.__ModuleLoader__.load({
|
|
|
295
670
|
return;
|
|
296
671
|
}
|
|
297
672
|
|
|
298
|
-
const root = el("div", "", "position:fixed;right:18px;bottom:18px;z-index:
|
|
673
|
+
const root = el("div", "", "position:fixed;right:18px;bottom:18px;z-index:2147483001;display:flex;flex-direction:column;align-items:flex-end;");
|
|
299
674
|
|
|
300
|
-
const menu = el("div", "", "display:none;margin-bottom:8px;min-width:
|
|
675
|
+
const menu = el("div", "", "display:none;margin-bottom:8px;min-width:240px;max-height:64vh;overflow-y:auto;background:rgba(24,24,30,0.96);border:1px solid rgba(128,128,128,0.3);border-radius:10px;padding:10px 12px;box-shadow:0 6px 24px rgba(0,0,0,0.45);backdrop-filter:blur(6px);color:#e8e8ea;");
|
|
301
676
|
root.appendChild(menu);
|
|
302
677
|
|
|
303
678
|
const btn = el("button", "🔔", "width:38px;height:38px;border-radius:50%;border:1px solid rgba(128,128,128,0.35);background:rgba(28,28,34,0.75);color:#fff;font-size:18px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 10px rgba(0,0,0,0.35);backdrop-filter:blur(4px);transition:opacity .15s ease;");
|
|
@@ -309,16 +684,16 @@ window.__ModuleLoader__.load({
|
|
|
309
684
|
setEnabled(next);
|
|
310
685
|
updateToggle(btn);
|
|
311
686
|
renderMenu(menu, btn);
|
|
312
|
-
if (next) {
|
|
313
|
-
const
|
|
314
|
-
|
|
687
|
+
if (next && isSoundEnabled()) {
|
|
688
|
+
const preset = PRESETS[currentPresetId()];
|
|
689
|
+
const spec = preset.done;
|
|
690
|
+
playPreset(spec.tones, spec.gap, preset.wave, preset.volume);
|
|
315
691
|
}
|
|
316
692
|
});
|
|
317
693
|
root.appendChild(btn);
|
|
318
694
|
|
|
319
695
|
document.body.appendChild(root);
|
|
320
696
|
|
|
321
|
-
// hover 显示菜单(延迟 120ms),移出后延迟 300ms 关闭(留出移到菜单的时间)
|
|
322
697
|
let hideTimer = null;
|
|
323
698
|
root.addEventListener("mouseenter", () => {
|
|
324
699
|
clearTimeout(hideTimer);
|
|
@@ -329,16 +704,18 @@ window.__ModuleLoader__.load({
|
|
|
329
704
|
clearTimeout(hideTimer);
|
|
330
705
|
hideTimer = setTimeout(() => {
|
|
331
706
|
menu.style.display = "none";
|
|
707
|
+
expandedColor = null;
|
|
332
708
|
}, 300);
|
|
333
709
|
});
|
|
334
|
-
// 点击菜单外部区域时关闭菜单
|
|
335
710
|
document.addEventListener("click", (event) => {
|
|
336
|
-
if (!root.contains(event.target))
|
|
711
|
+
if (!root.contains(event.target)) {
|
|
712
|
+
menu.style.display = "none";
|
|
713
|
+
expandedColor = null;
|
|
714
|
+
}
|
|
337
715
|
});
|
|
338
716
|
}
|
|
339
717
|
|
|
340
718
|
// 浏览器自动播放策略:需要用户先与页面交互过,AudioContext 才会出声。
|
|
341
|
-
// 捕获任意一次用户点击来预热音频上下文。
|
|
342
719
|
function warmupAudio() {
|
|
343
720
|
if (typeof document === "undefined") return;
|
|
344
721
|
const unlock = () => ensureAudio();
|
|
@@ -347,17 +724,14 @@ window.__ModuleLoader__.load({
|
|
|
347
724
|
}
|
|
348
725
|
|
|
349
726
|
// ===================== 插件主体 =====================
|
|
350
|
-
//
|
|
351
|
-
// 不是包名/模块 id。sessions 服务由 @deepseek-ai/dsh-client-runtime
|
|
352
|
-
// (rootCtx.reflect.provide("sessions", ...))。
|
|
727
|
+
// inject 是 cordis「服务名」列表(fiber 等待服务可用才激活),
|
|
728
|
+
// 不是包名/模块 id。sessions 服务由 @deepseek-ai/dsh-client-runtime 提供。
|
|
353
729
|
const inject = ["sessions"];
|
|
354
730
|
|
|
355
731
|
function apply(ctx) {
|
|
356
|
-
// 订阅会话列表状态变化(running / pendingInteraction)
|
|
357
732
|
const list = ctx && ctx.sessions && ctx.sessions.list;
|
|
358
733
|
if (list && typeof list.subscribe === "function") {
|
|
359
734
|
ctx.effect(() => {
|
|
360
|
-
// 先建立基线快照,再订阅后续变化
|
|
361
735
|
const bootstrap = () => {
|
|
362
736
|
const snap = list.getSnapshot();
|
|
363
737
|
prevBySession = new Map();
|
|
@@ -375,9 +749,10 @@ window.__ModuleLoader__.load({
|
|
|
375
749
|
};
|
|
376
750
|
}, "dsh-notify-tone: sessions subscription");
|
|
377
751
|
} else {
|
|
378
|
-
console.warn("[dsh-notify-tone] ctx.sessions.list
|
|
752
|
+
console.warn("[dsh-notify-tone] ctx.sessions.list 不可用,提醒功能未启用");
|
|
379
753
|
}
|
|
380
754
|
|
|
755
|
+
mountFx();
|
|
381
756
|
mountToggle();
|
|
382
757
|
warmupAudio();
|
|
383
758
|
}
|
|
@@ -391,9 +766,23 @@ window.__ModuleLoader__.load({
|
|
|
391
766
|
setPresetId,
|
|
392
767
|
isEnabled,
|
|
393
768
|
setEnabled,
|
|
769
|
+
isSoundEnabled,
|
|
770
|
+
setSoundEnabled,
|
|
771
|
+
isVisualEnabled,
|
|
772
|
+
setVisualEnabled,
|
|
773
|
+
isNotificationEnabled,
|
|
774
|
+
setNotificationEnabled,
|
|
775
|
+
notifyUser,
|
|
776
|
+
ensureNotificationPermission,
|
|
777
|
+
interactHue,
|
|
778
|
+
doneHue,
|
|
779
|
+
setInteractHue,
|
|
780
|
+
setDoneHue,
|
|
394
781
|
playPreset,
|
|
395
782
|
playTone,
|
|
396
783
|
ensureAudio,
|
|
784
|
+
playFx,
|
|
785
|
+
FX_ID,
|
|
397
786
|
resetThrottle() {
|
|
398
787
|
lastPlayed.interact = 0;
|
|
399
788
|
lastPlayed.done = 0;
|
package/package.json
CHANGED