@tanzerfe/page-use 0.0.4 → 0.0.6
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 +41 -153
- package/dist/{index-CD2K3C_Q.d.mts → index-D4_hId31.d.mts} +203 -14
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-B9wNTD0L.mjs → src-CHB0xWWP.mjs} +1427 -239
- package/dist/vue.d.mts +2 -2
- package/dist/vue.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,75 @@
|
|
|
1
1
|
import { PageAgent } from "page-agent";
|
|
2
|
-
import { LitElement, css, html, nothing } from "lit";
|
|
2
|
+
import { LitElement, css, html, nothing, svg } from "lit";
|
|
3
3
|
import { keyed } from "lit/directives/keyed.js";
|
|
4
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
4
5
|
|
|
6
|
+
//#region src/voice/browserEngine.ts
|
|
7
|
+
/** 内置浏览器引擎的保留 id;注入引擎使用该 id 会被忽略 */
|
|
8
|
+
const BROWSER_ENGINE_ID = "browser";
|
|
9
|
+
/** 播报语言 */
|
|
10
|
+
const SPEECH_LANG = "zh-CN";
|
|
11
|
+
/** 超时兜底的语速估算:中文语音在 rate=1 时约每秒 4 字 */
|
|
12
|
+
const CHARS_PER_SECOND = 4;
|
|
13
|
+
/** 超时兜底的余量(毫秒):覆盖语音引擎启动延迟;onend 不触发时按估算时长 + 余量视为播完 */
|
|
14
|
+
const TIMEOUT_MARGIN_MS = 2e3;
|
|
15
|
+
/** 当前环境是否支持语音合成 */
|
|
16
|
+
function isSpeechSupported() {
|
|
17
|
+
return typeof globalThis.speechSynthesis !== "undefined" && typeof globalThis.SpeechSynthesisUtterance !== "undefined";
|
|
18
|
+
}
|
|
19
|
+
/** 可用的中文语音(lang 以 zh 开头,兼容 zh-CN / zh_CN / zh-HK 等写法);Chrome 首次调用可能为空,需监听 voiceschanged */
|
|
20
|
+
function getChineseVoices() {
|
|
21
|
+
if (!isSpeechSupported()) return [];
|
|
22
|
+
return globalThis.speechSynthesis.getVoices().filter((voice) => /^zh(?:$|[-_])/i.test(voice.lang));
|
|
23
|
+
}
|
|
24
|
+
/** 语音选择:指定名称 → 第一个中文语音 → undefined(浏览器默认语音) */
|
|
25
|
+
function resolveVoice(voiceName) {
|
|
26
|
+
if (!isSpeechSupported()) return void 0;
|
|
27
|
+
const voices = globalThis.speechSynthesis.getVoices();
|
|
28
|
+
return voiceName && voices.find((voice) => voice.name === voiceName) || getChineseVoices()[0];
|
|
29
|
+
}
|
|
30
|
+
const browserTtsEngine = {
|
|
31
|
+
id: BROWSER_ENGINE_ID,
|
|
32
|
+
label: "本地语音(浏览器)",
|
|
33
|
+
splitSentences: true,
|
|
34
|
+
isAvailable: isSpeechSupported,
|
|
35
|
+
listVoices: () => getChineseVoices().map((voice) => ({
|
|
36
|
+
value: voice.name,
|
|
37
|
+
label: voice.name
|
|
38
|
+
})),
|
|
39
|
+
speak(text, { rate, voice: voiceName, signal }) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
resolve();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
46
|
+
utterance.lang = SPEECH_LANG;
|
|
47
|
+
utterance.rate = rate > 0 ? rate : 1;
|
|
48
|
+
const voice = resolveVoice(voiceName);
|
|
49
|
+
if (voice) utterance.voice = voice;
|
|
50
|
+
let settled = false;
|
|
51
|
+
let timer;
|
|
52
|
+
const finish = () => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
signal.removeEventListener("abort", onAbort);
|
|
57
|
+
resolve();
|
|
58
|
+
};
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
globalThis.speechSynthesis.cancel();
|
|
61
|
+
finish();
|
|
62
|
+
};
|
|
63
|
+
utterance.onend = finish;
|
|
64
|
+
utterance.onerror = finish;
|
|
65
|
+
signal.addEventListener("abort", onAbort);
|
|
66
|
+
timer = setTimeout(finish, text.length / (CHARS_PER_SECOND * utterance.rate) * 1e3 + TIMEOUT_MARGIN_MS);
|
|
67
|
+
globalThis.speechSynthesis.speak(utterance);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
5
73
|
//#region src/core/maskTheme.ts
|
|
6
74
|
/**
|
|
7
75
|
* 运行期蒙层与模拟光标的定制主题(覆盖 page-agent SimulatorMask 的默认视觉)。
|
|
@@ -93,6 +161,38 @@ ${CURSOR}[class*='clicking'] > :nth-child(1)::before {
|
|
|
93
161
|
}
|
|
94
162
|
`;
|
|
95
163
|
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/voice/engines.ts
|
|
166
|
+
/**
|
|
167
|
+
* 语音合成引擎注册表:合并宿主注入的引擎与内置浏览器引擎,并按设置解析当前生效引擎。
|
|
168
|
+
* narration(实际播报)与调试面板(设置展示)共用这里的结论,避免两处各自回退。
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
171
|
+
* 可选引擎列表:浏览器引擎(本地语音)在最前,注入引擎按注入顺序、去重排在其后(即下拉框顺序)。
|
|
172
|
+
* 宿主注入 id 为 'browser' 的引擎会被忽略;重复 id 保留先出现者。
|
|
173
|
+
*/
|
|
174
|
+
function listTtsEngines(injected) {
|
|
175
|
+
const list = injected === void 0 ? [] : Array.isArray(injected) ? injected : [injected];
|
|
176
|
+
const seen = /* @__PURE__ */ new Set([BROWSER_ENGINE_ID]);
|
|
177
|
+
const engines = [browserTtsEngine];
|
|
178
|
+
for (const engine of list) {
|
|
179
|
+
if (seen.has(engine.id)) {
|
|
180
|
+
console.warn(`[PageUse] 语音合成引擎 id 重复,已忽略:${engine.id}(${engine.label})`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
seen.add(engine.id);
|
|
184
|
+
engines.push(engine);
|
|
185
|
+
}
|
|
186
|
+
return engines;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 当前生效引擎:保存的 id 命中则用之;否则为默认引擎,即浏览器引擎(本地语音),与是否注入引擎无关。
|
|
190
|
+
* 回退只影响本次解析,不改写设置,宿主恢复该引擎后仍按原选择播报。
|
|
191
|
+
*/
|
|
192
|
+
function resolveTtsEngine(engines, savedId) {
|
|
193
|
+
return savedId && engines.find((engine) => engine.id === savedId) || browserTtsEngine;
|
|
194
|
+
}
|
|
195
|
+
|
|
96
196
|
//#endregion
|
|
97
197
|
//#region src/core/options.ts
|
|
98
198
|
/** 默认模型,仅为示例取值;接入其它端点时应通过 llm.model 显式指定支持 function calling 的模型 */
|
|
@@ -147,12 +247,14 @@ function normalizeOptions(options) {
|
|
|
147
247
|
beforeTaskLabel: options.beforeTaskLabel || "任务开始前执行宿主预处理",
|
|
148
248
|
storageKeyPrefix: options.storageKeyPrefix || "tz-page-use",
|
|
149
249
|
pageAgentOptions: options.pageAgentOptions ?? {},
|
|
150
|
-
exposeGlobal: options.exposeGlobal !== false
|
|
250
|
+
exposeGlobal: options.exposeGlobal !== false,
|
|
251
|
+
ttsEngines: listTtsEngines(options.tts)
|
|
151
252
|
};
|
|
152
253
|
}
|
|
153
254
|
/** URL 查询参数:?pageUseDebug=1 打开调试面板,?pageUseDebug=0 关闭;结果记入 sessionStorage */
|
|
154
255
|
const LAUNCHER_QUERY_KEY = "pageUseDebug";
|
|
155
256
|
/** 从 location 读取调试开关(hash 路由的查询参数在 # 之后,两处都查) */
|
|
257
|
+
/** 读取 URL 中的 pageUseDebug 参数(search 或 hash 内的查询串);未携带时为 null。供 launcher 判定显示状态 */
|
|
156
258
|
function readLauncherQuery() {
|
|
157
259
|
const fromSearch = new URLSearchParams(window.location.search).get(LAUNCHER_QUERY_KEY);
|
|
158
260
|
if (fromSearch !== null) return fromSearch;
|
|
@@ -220,7 +322,7 @@ function createStore(initial) {
|
|
|
220
322
|
//#region src/core/settings.ts
|
|
221
323
|
/**
|
|
222
324
|
* Page Use 本地设置(localStorage 持久化,单例)。
|
|
223
|
-
* 含 page-agent
|
|
325
|
+
* 含 page-agent 内置面板/元素编号高亮开关、蒙层与光晕、演示播报、调试入口拖拽位置与显示状态。
|
|
224
326
|
* 存储键为 `${storageKeyPrefix}-settings`。
|
|
225
327
|
*/
|
|
226
328
|
/** page-agent 元素高亮容器 id(page-controller 内部常量,随版本可能变化) */
|
|
@@ -239,7 +341,9 @@ const DEFAULT_PAGE_USE_SETTINGS = {
|
|
|
239
341
|
customMaskEffect: true,
|
|
240
342
|
voiceEnabled: false,
|
|
241
343
|
voiceRate: 1.1,
|
|
242
|
-
|
|
344
|
+
voiceEngineId: "",
|
|
345
|
+
voiceNames: {},
|
|
346
|
+
launcherVisible: null,
|
|
243
347
|
fabPos: null
|
|
244
348
|
};
|
|
245
349
|
/**
|
|
@@ -251,16 +355,25 @@ const LOCKED_SETTINGS = {
|
|
|
251
355
|
showBuiltinPanel: false,
|
|
252
356
|
customMaskEffect: true
|
|
253
357
|
};
|
|
254
|
-
function storageKey$
|
|
358
|
+
function storageKey$2(prefix) {
|
|
255
359
|
return `${prefix}-settings`;
|
|
256
360
|
}
|
|
257
|
-
|
|
361
|
+
/** 旧字段 voiceName 迁移为浏览器引擎的音色;已有 voiceNames 时以新结构为准,旧字段一律丢弃 */
|
|
362
|
+
function migrate(stored) {
|
|
363
|
+
const { voiceName, ...rest } = stored;
|
|
364
|
+
if (voiceName === void 0 || rest.voiceNames) return rest;
|
|
365
|
+
return {
|
|
366
|
+
...rest,
|
|
367
|
+
voiceNames: voiceName ? { [BROWSER_ENGINE_ID]: voiceName } : {}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function load$2(prefix) {
|
|
258
371
|
try {
|
|
259
|
-
const raw = window.localStorage.getItem(storageKey$
|
|
372
|
+
const raw = window.localStorage.getItem(storageKey$2(prefix));
|
|
260
373
|
if (!raw) return { ...DEFAULT_PAGE_USE_SETTINGS };
|
|
261
374
|
return {
|
|
262
375
|
...DEFAULT_PAGE_USE_SETTINGS,
|
|
263
|
-
...JSON.parse(raw),
|
|
376
|
+
...migrate(JSON.parse(raw)),
|
|
264
377
|
...LOCKED_SETTINGS
|
|
265
378
|
};
|
|
266
379
|
} catch {
|
|
@@ -269,7 +382,7 @@ function load$1(prefix) {
|
|
|
269
382
|
}
|
|
270
383
|
function save(prefix, settings) {
|
|
271
384
|
try {
|
|
272
|
-
window.localStorage.setItem(storageKey$
|
|
385
|
+
window.localStorage.setItem(storageKey$2(prefix), JSON.stringify(settings));
|
|
273
386
|
} catch {}
|
|
274
387
|
}
|
|
275
388
|
/** 按开关注入/移除一段覆盖样式(用于压制 page-agent 自带的 DOM,这些节点位于宿主 document 中) */
|
|
@@ -297,12 +410,12 @@ function applyToDocument(settings, prev) {
|
|
|
297
410
|
if (!prev || prev.showBuiltinPanel !== settings.showBuiltinPanel) toggleOverrideStyle(BUILTIN_PANEL_STYLE_ID, `#${BUILTIN_PANEL_ID}{display:none !important;}`, settings.showBuiltinPanel);
|
|
298
411
|
if (!prev || prev.customMaskEffect !== settings.customMaskEffect) toggleOverrideStyle(MASK_THEME_STYLE_ID, PAGE_USE_MASK_THEME_CSS, !settings.customMaskEffect);
|
|
299
412
|
}
|
|
300
|
-
let prefix$
|
|
301
|
-
let store$
|
|
413
|
+
let prefix$2 = DEFAULT_STORAGE_KEY_PREFIX;
|
|
414
|
+
let store$4 = null;
|
|
302
415
|
/** 懒创建:首次访问时读取存储并把设置应用到页面 */
|
|
303
|
-
function ensureStore$
|
|
304
|
-
if (store$
|
|
305
|
-
const created = createStore(load$
|
|
416
|
+
function ensureStore$2() {
|
|
417
|
+
if (store$4) return store$4;
|
|
418
|
+
const created = createStore(load$2(prefix$2));
|
|
306
419
|
let prev = null;
|
|
307
420
|
const sync = (next) => {
|
|
308
421
|
applyToDocument(next, prev);
|
|
@@ -310,94 +423,123 @@ function ensureStore$1() {
|
|
|
310
423
|
};
|
|
311
424
|
sync(created.get());
|
|
312
425
|
created.subscribe((next) => {
|
|
313
|
-
save(prefix$
|
|
426
|
+
save(prefix$2, next);
|
|
314
427
|
sync(next);
|
|
315
428
|
});
|
|
316
|
-
store$
|
|
429
|
+
store$4 = created;
|
|
317
430
|
return created;
|
|
318
431
|
}
|
|
319
432
|
/** 切换存储键前缀(createPageUse 时调用);前缀变化时重新读取 */
|
|
320
433
|
function setSettingsStoragePrefix(next) {
|
|
321
|
-
if (next === prefix$
|
|
322
|
-
prefix$
|
|
323
|
-
if (store$
|
|
324
|
-
else ensureStore$
|
|
434
|
+
if (next === prefix$2 && store$4) return;
|
|
435
|
+
prefix$2 = next;
|
|
436
|
+
if (store$4) store$4.replace(load$2(prefix$2));
|
|
437
|
+
else ensureStore$2();
|
|
325
438
|
}
|
|
326
439
|
const pageUseSettings = {
|
|
327
440
|
store: {
|
|
328
|
-
get: () => ensureStore$
|
|
329
|
-
subscribe: (listener) => ensureStore$
|
|
441
|
+
get: () => ensureStore$2().get(),
|
|
442
|
+
subscribe: (listener) => ensureStore$2().subscribe(listener)
|
|
330
443
|
},
|
|
331
444
|
get() {
|
|
332
|
-
return ensureStore$
|
|
445
|
+
return ensureStore$2().get();
|
|
333
446
|
},
|
|
334
447
|
/** 固定项(LOCKED_SETTINGS)的修改被忽略 */
|
|
335
448
|
update(patch) {
|
|
336
|
-
ensureStore$
|
|
449
|
+
ensureStore$2().set({
|
|
337
450
|
...patch,
|
|
338
451
|
...LOCKED_SETTINGS
|
|
339
452
|
});
|
|
340
453
|
},
|
|
341
454
|
/** 把浮动入口(及随之定位的面板)恢复到默认角落位置 */
|
|
342
455
|
resetPositions() {
|
|
343
|
-
ensureStore$
|
|
456
|
+
ensureStore$2().set({ fabPos: null });
|
|
344
457
|
}
|
|
345
458
|
};
|
|
346
459
|
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/voice/preview.ts
|
|
462
|
+
/**
|
|
463
|
+
* 音色试听:用当前引擎、音色与语速播报一句固定文本,供设置页选择音色时对比。
|
|
464
|
+
* 不经 speaker 队列:试听是单次、可中止的调用,不参与待播与抢占,也不受本次任务静音影响。
|
|
465
|
+
* 与任务播报互斥:launcher 在任务执行中禁用试听,narration 在任务开始时调用 stopPreview()。
|
|
466
|
+
*/
|
|
467
|
+
/** 试听文本 */
|
|
468
|
+
const PREVIEW_TEXT = "您好,我是页面智能体。";
|
|
469
|
+
const store$3 = createStore({ playing: false });
|
|
470
|
+
/** 试听状态(playing:是否正在试听),供设置页切换「试听 / 停止」按钮 */
|
|
471
|
+
const previewStore = store$3;
|
|
472
|
+
/** 当前试听的中止控制;空闲时为 null */
|
|
473
|
+
let controller = null;
|
|
474
|
+
/** 开始试听:先中止上一次试听;引擎 speak 结束(播完、失败或被中止)后复位状态 */
|
|
475
|
+
function startPreview(engine, options) {
|
|
476
|
+
stopPreview();
|
|
477
|
+
if (!engine.isAvailable()) return;
|
|
478
|
+
const current = new AbortController();
|
|
479
|
+
controller = current;
|
|
480
|
+
store$3.set({ playing: true });
|
|
481
|
+
let speaking;
|
|
482
|
+
try {
|
|
483
|
+
speaking = engine.speak(PREVIEW_TEXT, {
|
|
484
|
+
...options,
|
|
485
|
+
signal: current.signal
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
speaking = Promise.reject(error);
|
|
489
|
+
}
|
|
490
|
+
speaking.catch((error) => console.warn("[PageUse] 试听失败", error)).then(() => {
|
|
491
|
+
if (controller !== current) return;
|
|
492
|
+
controller = null;
|
|
493
|
+
store$3.set({ playing: false });
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/** 中止试听;空闲时不触碰引擎(浏览器引擎中止会调用 speechSynthesis.cancel()) */
|
|
497
|
+
function stopPreview() {
|
|
498
|
+
if (!controller) return;
|
|
499
|
+
controller.abort();
|
|
500
|
+
controller = null;
|
|
501
|
+
store$3.set({ playing: false });
|
|
502
|
+
}
|
|
503
|
+
|
|
347
504
|
//#endregion
|
|
348
505
|
//#region src/voice/speaker.ts
|
|
349
506
|
/**
|
|
350
|
-
*
|
|
507
|
+
* 演示播报的播放队列,发声委托给当前语音合成引擎(由 getEngine 提供,可在播报间切换)。
|
|
351
508
|
*
|
|
352
|
-
*
|
|
509
|
+
* 队列只有两部分:当前正在播报的一组文本,以及一条待播的步骤文本。
|
|
353
510
|
* - 步骤播报不打断当前播报,只覆盖待播槽(语音跟不上执行时丢弃中间步骤);
|
|
354
511
|
* - 提问 / 结论先清空再播报;
|
|
355
|
-
* -
|
|
512
|
+
* - 每组开始时读取一次引擎并在组内固定使用(splitSentences 与中止对象保持一致);
|
|
513
|
+
* - 每组持有一个 AbortController,清空时中止,引擎据此停止自身的合成与播放;
|
|
514
|
+
* - 每次清空递增代次,旧代次 speak() 的迟到 resolve 一律忽略,避免中止后误触发下一条。
|
|
356
515
|
*
|
|
357
|
-
*
|
|
516
|
+
* 当前引擎不可用时 enqueue / interrupt 静默返回。
|
|
358
517
|
*/
|
|
359
|
-
/**
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const TIMEOUT_MARGIN_MS = 2e3;
|
|
365
|
-
/** 当前环境是否支持语音合成 */
|
|
366
|
-
function isSpeechSupported() {
|
|
367
|
-
return typeof globalThis.speechSynthesis !== "undefined" && typeof globalThis.SpeechSynthesisUtterance !== "undefined";
|
|
368
|
-
}
|
|
369
|
-
/** 可用的中文语音(lang 以 zh 开头,兼容 zh-CN / zh_CN / zh-HK 等写法);Chrome 首次调用可能为空,需监听 voiceschanged */
|
|
370
|
-
function getChineseVoices() {
|
|
371
|
-
if (!isSpeechSupported()) return [];
|
|
372
|
-
return globalThis.speechSynthesis.getVoices().filter((voice) => /^zh(?:$|[-_])/i.test(voice.lang));
|
|
373
|
-
}
|
|
374
|
-
/** 语音选择:指定名称 → 第一个中文语音 → undefined(浏览器默认语音) */
|
|
375
|
-
function resolveVoice(voiceName) {
|
|
376
|
-
if (!isSpeechSupported()) return void 0;
|
|
377
|
-
const voices = globalThis.speechSynthesis.getVoices();
|
|
378
|
-
return voiceName && voices.find((voice) => voice.name === voiceName) || getChineseVoices()[0];
|
|
379
|
-
}
|
|
380
|
-
function createSpeaker(getSettings) {
|
|
518
|
+
/**
|
|
519
|
+
* @param getSettings 每次调用引擎前读取;参数为当前组使用的引擎,调用方据此取该引擎的音色
|
|
520
|
+
* @param getEngine 每组开始时读取当前生效引擎
|
|
521
|
+
*/
|
|
522
|
+
function createSpeaker(getSettings, getEngine = () => browserTtsEngine) {
|
|
381
523
|
let generation = 0;
|
|
382
|
-
/**
|
|
524
|
+
/** 当前组剩余的待调用文本;null 表示空闲 */
|
|
383
525
|
let current = null;
|
|
384
526
|
let pending = null;
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
timer = null;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
527
|
+
/** 当前组的中止控制;空闲时为 null,clear 据此判断是否需要中止引擎 */
|
|
528
|
+
let controller = null;
|
|
529
|
+
/** 当前组使用的引擎;组内不随设置切换 */
|
|
530
|
+
let engine = getEngine();
|
|
392
531
|
function playGroup(sentences) {
|
|
393
|
-
|
|
532
|
+
engine = getEngine();
|
|
533
|
+
if (!engine.isAvailable()) return;
|
|
534
|
+
current = engine.splitSentences ? [...sentences] : [sentences.join("")];
|
|
535
|
+
controller = new AbortController();
|
|
394
536
|
playNext();
|
|
395
537
|
}
|
|
396
538
|
function playNext() {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (sentence === void 0) {
|
|
539
|
+
const text = current?.shift();
|
|
540
|
+
if (text === void 0 || !controller) {
|
|
400
541
|
current = null;
|
|
542
|
+
controller = null;
|
|
401
543
|
if (pending) {
|
|
402
544
|
const next = pending;
|
|
403
545
|
pending = null;
|
|
@@ -405,35 +547,32 @@ function createSpeaker(getSettings) {
|
|
|
405
547
|
}
|
|
406
548
|
return;
|
|
407
549
|
}
|
|
408
|
-
const { rate, voiceName } = getSettings();
|
|
409
|
-
const utterance = new SpeechSynthesisUtterance(sentence);
|
|
410
|
-
utterance.lang = SPEECH_LANG;
|
|
411
|
-
utterance.rate = rate > 0 ? rate : 1;
|
|
412
|
-
const voice = resolveVoice(voiceName);
|
|
413
|
-
if (voice) utterance.voice = voice;
|
|
414
550
|
const owner = generation;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
551
|
+
const { rate, voiceName } = getSettings(engine);
|
|
552
|
+
let speaking;
|
|
553
|
+
try {
|
|
554
|
+
speaking = engine.speak(text, {
|
|
555
|
+
rate,
|
|
556
|
+
voice: voiceName,
|
|
557
|
+
signal: controller.signal
|
|
558
|
+
});
|
|
559
|
+
} catch (error) {
|
|
560
|
+
speaking = Promise.reject(error);
|
|
561
|
+
}
|
|
562
|
+
speaking.catch((error) => console.warn("[PageUse] 语音播报失败", error)).then(() => {
|
|
563
|
+
if (owner === generation) playNext();
|
|
564
|
+
});
|
|
425
565
|
}
|
|
426
566
|
function clear() {
|
|
427
|
-
const active = current !== null;
|
|
428
567
|
generation += 1;
|
|
568
|
+
controller?.abort();
|
|
569
|
+
controller = null;
|
|
429
570
|
current = null;
|
|
430
571
|
pending = null;
|
|
431
|
-
clearTimer();
|
|
432
|
-
if (active && isSpeechSupported()) globalThis.speechSynthesis.cancel();
|
|
433
572
|
}
|
|
434
573
|
return {
|
|
435
574
|
enqueue(sentences) {
|
|
436
|
-
if (!
|
|
575
|
+
if (!sentences.length) return;
|
|
437
576
|
if (current !== null) {
|
|
438
577
|
pending = sentences;
|
|
439
578
|
return;
|
|
@@ -441,7 +580,6 @@ function createSpeaker(getSettings) {
|
|
|
441
580
|
playGroup(sentences);
|
|
442
581
|
},
|
|
443
582
|
interrupt(sentences) {
|
|
444
|
-
if (!isSpeechSupported()) return;
|
|
445
583
|
clear();
|
|
446
584
|
if (sentences.length) playGroup(sentences);
|
|
447
585
|
},
|
|
@@ -455,10 +593,15 @@ function createSpeaker(getSettings) {
|
|
|
455
593
|
* 播报文本清洗(纯函数,无 DOM 依赖):把模型输出的步骤描述 / 结论转成适合语音合成的句子数组。
|
|
456
594
|
* 结果只用于播报,不回写任务结果与面板展示内容。
|
|
457
595
|
*/
|
|
458
|
-
/** 单条播报清洗后的字数上限:中文语音约 4 字/秒,
|
|
459
|
-
const SPEECH_MAX_CHARS =
|
|
460
|
-
/**
|
|
596
|
+
/** 单条播报清洗后的字数上限:中文语音约 4 字/秒,240 字约 1 分钟(语速 1 时) */
|
|
597
|
+
const SPEECH_MAX_CHARS = 240;
|
|
598
|
+
/** 超过上限截断时追加的提示语(未挂载调试面板、不知道面板名称时使用) */
|
|
461
599
|
const SPEECH_TRUNCATED_HINT = "详细内容请查看面板";
|
|
600
|
+
/** 截断提示语:有面板名称(调试面板标题)时指明名称,如「详细内容请查看「页面智能体」」 */
|
|
601
|
+
function buildTruncatedHint(panelName) {
|
|
602
|
+
const name = panelName?.trim();
|
|
603
|
+
return name ? `详细内容请查看「${name}」` : SPEECH_TRUNCATED_HINT;
|
|
604
|
+
}
|
|
462
605
|
/**
|
|
463
606
|
* 单次合成的字数上限:Chrome 的在线语音单条超过约 15 秒会被截断,
|
|
464
607
|
* 超过该长度的句子再按逗号等停顿切分(约 4 字/秒 × 12 秒)
|
|
@@ -526,7 +669,7 @@ function joinLines(lines) {
|
|
|
526
669
|
return result.replace(/([。!?!?;;,,、::])[。!?!?;;,,、::\s]+/g, "$1").replace(/^[。!?!?;;,,、::\s]+/, "").trim();
|
|
527
670
|
}
|
|
528
671
|
/** 超过上限时在上限内最后一个句末标点处截断,找不到则硬截断,并追加提示语 */
|
|
529
|
-
function truncate(text, maxChars) {
|
|
672
|
+
function truncate(text, maxChars, hint) {
|
|
530
673
|
if (text.length <= maxChars) return text;
|
|
531
674
|
const head = text.slice(0, maxChars);
|
|
532
675
|
let cut = -1;
|
|
@@ -537,7 +680,7 @@ function truncate(text, maxChars) {
|
|
|
537
680
|
let kept = cut > 0 ? head.slice(0, cut + 1) : head;
|
|
538
681
|
kept = kept.replace(/[;;,,、::]$/, "");
|
|
539
682
|
if (!/[。!?!?]$/.test(kept)) kept += "。";
|
|
540
|
-
return `${kept}${
|
|
683
|
+
return `${kept}${hint}`;
|
|
541
684
|
}
|
|
542
685
|
/** 按句末标点切句;过长的句子再按逗号等停顿切分 */
|
|
543
686
|
function splitSentences(text) {
|
|
@@ -566,10 +709,10 @@ function splitSentences(text) {
|
|
|
566
709
|
* 处理顺序:代码块 / HTML → 表格 → 行内 Markdown 与符号 → 拼行 → 截断 → 切句。
|
|
567
710
|
*/
|
|
568
711
|
function toSpeechText(text, options = {}) {
|
|
569
|
-
const maxChars = options.maxChars ??
|
|
712
|
+
const maxChars = options.maxChars ?? 240;
|
|
570
713
|
const joined = joinLines(toLines((text || "").replace(/```[\s\S]*?(?:```|$)/g, "\n").replace(/<\/?[a-zA-Z][^>]*>/g, "")));
|
|
571
714
|
if (!joined) return [];
|
|
572
|
-
return splitSentences(truncate(joined, maxChars));
|
|
715
|
+
return splitSentences(truncate(joined, maxChars, options.truncatedHint ?? "详细内容请查看面板"));
|
|
573
716
|
}
|
|
574
717
|
|
|
575
718
|
//#endregion
|
|
@@ -577,6 +720,7 @@ function toSpeechText(text, options = {}) {
|
|
|
577
720
|
/**
|
|
578
721
|
* 演示播报:订阅进度事件,按设置把步骤描述、提问与结论交给 speaker 播报。
|
|
579
722
|
* 只订阅事件,不影响任务执行节奏;对所有任务来源生效。
|
|
723
|
+
* 是否播报 = 设置 voiceEnabled(总开关,持久化)且未处于本次任务静音(narrationMute,运行期)。
|
|
580
724
|
*/
|
|
581
725
|
/**
|
|
582
726
|
* 不播报的步骤:滚动、等待没有演示价值;done 由结论播报替代;
|
|
@@ -591,40 +735,65 @@ const SILENT_STEP_TOOLS = /* @__PURE__ */ new Set([
|
|
|
591
735
|
"error",
|
|
592
736
|
"retry"
|
|
593
737
|
]);
|
|
738
|
+
/**
|
|
739
|
+
* 调试面板名称(PageUseLauncherConfig.title),用于截断提示语「详细内容请查看「名称」」。
|
|
740
|
+
* 由 launcher 挂载时登记、卸载时撤销;未挂载面板时为 null,提示语回退为「详细内容请查看面板」。
|
|
741
|
+
*/
|
|
742
|
+
let panelName = null;
|
|
743
|
+
/** launcher 登记面板名称,返回撤销函数(仅撤销自己登记的值) */
|
|
744
|
+
function registerNarrationPanelName(name) {
|
|
745
|
+
panelName = name;
|
|
746
|
+
return () => {
|
|
747
|
+
if (panelName === name) panelName = null;
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* 本次任务静音(调试面板标题栏按钮):仅运行期,不持久化,不修改 voiceEnabled。
|
|
752
|
+
* 新任务 started 时重置为 false;静音期间跳过的内容在取消静音后不补播。
|
|
753
|
+
*/
|
|
754
|
+
const narrationMute = createStore({ muted: false });
|
|
594
755
|
/** 启动播报,返回停止函数(取消订阅并停止播报) */
|
|
595
|
-
function startNarration({ subscribe, speaker: injected }) {
|
|
596
|
-
const
|
|
597
|
-
|
|
756
|
+
function startNarration({ subscribe, engines, speaker: injected }) {
|
|
757
|
+
const available = engines ?? listTtsEngines(void 0);
|
|
758
|
+
const speaker = injected || createSpeaker((engine) => {
|
|
759
|
+
const { voiceRate, voiceNames } = pageUseSettings.get();
|
|
598
760
|
return {
|
|
599
761
|
rate: voiceRate,
|
|
600
|
-
voiceName
|
|
762
|
+
voiceName: voiceNames[engine.id] ?? ""
|
|
601
763
|
};
|
|
602
|
-
});
|
|
603
|
-
const enabled = () => pageUseSettings.get().voiceEnabled;
|
|
764
|
+
}, () => resolveTtsEngine(available, pageUseSettings.get().voiceEngineId));
|
|
765
|
+
const enabled = () => pageUseSettings.get().voiceEnabled && !narrationMute.get().muted;
|
|
766
|
+
const speech = (text) => toSpeechText(text, { truncatedHint: buildTruncatedHint(panelName ?? void 0) });
|
|
604
767
|
const offEvents = subscribe((event) => {
|
|
605
768
|
switch (event.type) {
|
|
606
769
|
case "started":
|
|
770
|
+
narrationMute.set({ muted: false });
|
|
771
|
+
stopPreview();
|
|
607
772
|
speaker.clear();
|
|
608
773
|
break;
|
|
609
774
|
case "step":
|
|
610
775
|
if (!enabled() || SILENT_STEP_TOOLS.has(event.step.tool)) return;
|
|
611
|
-
speaker.enqueue(
|
|
776
|
+
speaker.enqueue(speech(event.step.goal));
|
|
612
777
|
break;
|
|
613
778
|
case "question":
|
|
614
|
-
if (enabled()) speaker.interrupt(
|
|
779
|
+
if (enabled()) speaker.interrupt(speech(event.question));
|
|
615
780
|
break;
|
|
616
781
|
case "finished": if (event.result.status === "stopped" || !enabled()) speaker.clear();
|
|
617
|
-
else speaker.interrupt(
|
|
782
|
+
else speaker.interrupt(speech(event.result.summary));
|
|
618
783
|
}
|
|
619
784
|
});
|
|
620
|
-
let
|
|
785
|
+
let prev = pageUseSettings.get();
|
|
621
786
|
const offSettings = pageUseSettings.store.subscribe((settings) => {
|
|
622
|
-
if (
|
|
623
|
-
|
|
787
|
+
if (prev.voiceEnabled && !settings.voiceEnabled || prev.voiceEngineId !== settings.voiceEngineId) speaker.clear();
|
|
788
|
+
prev = settings;
|
|
789
|
+
});
|
|
790
|
+
const offMute = narrationMute.subscribe(({ muted }) => {
|
|
791
|
+
if (muted) speaker.clear();
|
|
624
792
|
});
|
|
625
793
|
return () => {
|
|
626
794
|
offEvents();
|
|
627
795
|
offSettings();
|
|
796
|
+
offMute();
|
|
628
797
|
speaker.clear();
|
|
629
798
|
};
|
|
630
799
|
}
|
|
@@ -819,56 +988,186 @@ function createPageUseAgent({ options, enableMask, narration = false, shouldRunB
|
|
|
819
988
|
* 用于快速重放上次指令,以及查看任务结果。存储键为 `${storageKeyPrefix}-history`。
|
|
820
989
|
*/
|
|
821
990
|
const MAX_ENTRIES = 30;
|
|
822
|
-
function storageKey(prefix) {
|
|
991
|
+
function storageKey$1(prefix) {
|
|
823
992
|
return `${prefix}-history`;
|
|
824
993
|
}
|
|
825
|
-
function load(prefix) {
|
|
994
|
+
function load$1(prefix) {
|
|
826
995
|
try {
|
|
827
|
-
const raw = window.localStorage.getItem(storageKey(prefix));
|
|
996
|
+
const raw = window.localStorage.getItem(storageKey$1(prefix));
|
|
828
997
|
const parsed = raw ? JSON.parse(raw) : [];
|
|
829
998
|
return Array.isArray(parsed) ? parsed : [];
|
|
830
999
|
} catch {
|
|
831
1000
|
return [];
|
|
832
1001
|
}
|
|
833
1002
|
}
|
|
834
|
-
let prefix = DEFAULT_STORAGE_KEY_PREFIX;
|
|
1003
|
+
let prefix$1 = DEFAULT_STORAGE_KEY_PREFIX;
|
|
835
1004
|
/** 懒创建:避免模块加载即访问 window(SSR / 测试环境) */
|
|
836
|
-
let store$
|
|
837
|
-
function ensureStore() {
|
|
838
|
-
if (!store$
|
|
839
|
-
return store$
|
|
1005
|
+
let store$2 = null;
|
|
1006
|
+
function ensureStore$1() {
|
|
1007
|
+
if (!store$2) store$2 = createStore({ entries: load$1(prefix$1) });
|
|
1008
|
+
return store$2;
|
|
840
1009
|
}
|
|
841
|
-
function persist(entries) {
|
|
842
|
-
ensureStore().set({ entries });
|
|
1010
|
+
function persist$1(entries) {
|
|
1011
|
+
ensureStore$1().set({ entries });
|
|
843
1012
|
try {
|
|
844
|
-
window.localStorage.setItem(storageKey(prefix), JSON.stringify(entries));
|
|
1013
|
+
window.localStorage.setItem(storageKey$1(prefix$1), JSON.stringify(entries));
|
|
845
1014
|
} catch {}
|
|
846
1015
|
}
|
|
847
1016
|
/** 切换存储键前缀(createPageUse 时调用);前缀变化时重新读取 */
|
|
848
1017
|
function setHistoryStoragePrefix(next) {
|
|
849
|
-
if (next === prefix && store$
|
|
850
|
-
prefix = next;
|
|
851
|
-
ensureStore().replace({ entries: load(prefix) });
|
|
1018
|
+
if (next === prefix$1 && store$2) return;
|
|
1019
|
+
prefix$1 = next;
|
|
1020
|
+
ensureStore$1().replace({ entries: load$1(prefix$1) });
|
|
852
1021
|
}
|
|
853
1022
|
const pageUseHistory = {
|
|
854
1023
|
store: {
|
|
855
|
-
get: () => ensureStore().get(),
|
|
856
|
-
subscribe: (listener) => ensureStore().subscribe(listener)
|
|
1024
|
+
get: () => ensureStore$1().get(),
|
|
1025
|
+
subscribe: (listener) => ensureStore$1().subscribe(listener)
|
|
857
1026
|
},
|
|
858
1027
|
entries() {
|
|
859
|
-
return ensureStore().get().entries;
|
|
1028
|
+
return ensureStore$1().get().entries;
|
|
860
1029
|
},
|
|
861
1030
|
add(entry) {
|
|
862
|
-
persist([{
|
|
1031
|
+
persist$1([{
|
|
863
1032
|
id: `${entry.at}-${Math.random().toString(36).slice(2, 8)}`,
|
|
864
1033
|
...entry
|
|
865
|
-
}, ...ensureStore().get().entries].slice(0, MAX_ENTRIES));
|
|
1034
|
+
}, ...ensureStore$1().get().entries].slice(0, MAX_ENTRIES));
|
|
866
1035
|
},
|
|
867
1036
|
remove(id) {
|
|
868
|
-
persist(ensureStore().get().entries.filter((item) => item.id !== id));
|
|
1037
|
+
persist$1(ensureStore$1().get().entries.filter((item) => item.id !== id));
|
|
869
1038
|
},
|
|
870
1039
|
clear() {
|
|
871
|
-
persist([]);
|
|
1040
|
+
persist$1([]);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/core/presetStore.ts
|
|
1046
|
+
/**
|
|
1047
|
+
* Page Use 快捷指令的用户维护数据(localStorage,懒创建单例)。
|
|
1048
|
+
* - custom:用户自定义指令,新增排在最前
|
|
1049
|
+
* - hidden:被用户删除(隐藏)的来源指令(菜单生成 / 内置 / 宿主 presets / scenarios)的 task 文本
|
|
1050
|
+
* 来源指令本身不落盘;宿主修改 task 后旧的隐藏记录自然失效,不主动清理。
|
|
1051
|
+
* 存储键为 `${storageKeyPrefix}-presets`。
|
|
1052
|
+
*/
|
|
1053
|
+
const EMPTY_STATE = {
|
|
1054
|
+
custom: [],
|
|
1055
|
+
hidden: []
|
|
1056
|
+
};
|
|
1057
|
+
function storageKey(prefix) {
|
|
1058
|
+
return `${prefix}-presets`;
|
|
1059
|
+
}
|
|
1060
|
+
function createId() {
|
|
1061
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1062
|
+
}
|
|
1063
|
+
/** 结构校验:丢弃缺少字符串 label / task 的项,缺 id 时补生成;hidden 只保留字符串并去重 */
|
|
1064
|
+
function sanitize(raw) {
|
|
1065
|
+
if (!raw || typeof raw !== "object") return { ...EMPTY_STATE };
|
|
1066
|
+
const { custom, hidden } = raw;
|
|
1067
|
+
return {
|
|
1068
|
+
custom: (Array.isArray(custom) ? custom : []).flatMap((item) => {
|
|
1069
|
+
if (!item || typeof item !== "object") return [];
|
|
1070
|
+
const { id, label, task } = item;
|
|
1071
|
+
if (typeof label !== "string" || typeof task !== "string") return [];
|
|
1072
|
+
return [{
|
|
1073
|
+
id: typeof id === "string" && id ? id : createId(),
|
|
1074
|
+
label,
|
|
1075
|
+
task
|
|
1076
|
+
}];
|
|
1077
|
+
}),
|
|
1078
|
+
hidden: Array.isArray(hidden) ? [...new Set(hidden.filter((item) => typeof item === "string"))] : []
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function load(prefix) {
|
|
1082
|
+
try {
|
|
1083
|
+
const raw = window.localStorage.getItem(storageKey(prefix));
|
|
1084
|
+
return raw ? sanitize(JSON.parse(raw)) : { ...EMPTY_STATE };
|
|
1085
|
+
} catch {
|
|
1086
|
+
return { ...EMPTY_STATE };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
let prefix = DEFAULT_STORAGE_KEY_PREFIX;
|
|
1090
|
+
let store$1 = null;
|
|
1091
|
+
function ensureStore() {
|
|
1092
|
+
if (!store$1) store$1 = createStore(load(prefix));
|
|
1093
|
+
return store$1;
|
|
1094
|
+
}
|
|
1095
|
+
function persist(patch) {
|
|
1096
|
+
const current = ensureStore();
|
|
1097
|
+
current.set(patch);
|
|
1098
|
+
try {
|
|
1099
|
+
window.localStorage.setItem(storageKey(prefix), JSON.stringify(current.get()));
|
|
1100
|
+
} catch {}
|
|
1101
|
+
}
|
|
1102
|
+
/** 必填与重复校验;excludeId 为编辑中的自身,与自身相同不算重复 */
|
|
1103
|
+
function validate(preset, excludeId) {
|
|
1104
|
+
if (!preset.label.trim()) return {
|
|
1105
|
+
ok: false,
|
|
1106
|
+
reason: "label-empty"
|
|
1107
|
+
};
|
|
1108
|
+
const task = preset.task.trim();
|
|
1109
|
+
if (!task) return {
|
|
1110
|
+
ok: false,
|
|
1111
|
+
reason: "task-empty"
|
|
1112
|
+
};
|
|
1113
|
+
return ensureStore().get().custom.some((item) => item.id !== excludeId && item.task.trim() === task) ? {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
reason: "duplicate"
|
|
1116
|
+
} : { ok: true };
|
|
1117
|
+
}
|
|
1118
|
+
/** 切换存储键前缀(createPageUse 时调用);前缀变化时重新读取 */
|
|
1119
|
+
function setPresetsStoragePrefix(next) {
|
|
1120
|
+
if (next === prefix && store$1) return;
|
|
1121
|
+
prefix = next;
|
|
1122
|
+
ensureStore().replace(load(prefix));
|
|
1123
|
+
}
|
|
1124
|
+
const pageUsePresets = {
|
|
1125
|
+
store: {
|
|
1126
|
+
get: () => ensureStore().get(),
|
|
1127
|
+
subscribe: (listener) => ensureStore().subscribe(listener)
|
|
1128
|
+
},
|
|
1129
|
+
get() {
|
|
1130
|
+
return ensureStore().get();
|
|
1131
|
+
},
|
|
1132
|
+
/** 新增自定义指令,排在最前;label / task 去除首尾空白后保存 */
|
|
1133
|
+
add(preset) {
|
|
1134
|
+
const result = validate(preset);
|
|
1135
|
+
if (!result.ok) return result;
|
|
1136
|
+
persist({ custom: [{
|
|
1137
|
+
id: createId(),
|
|
1138
|
+
label: preset.label.trim(),
|
|
1139
|
+
task: preset.task.trim()
|
|
1140
|
+
}, ...ensureStore().get().custom] });
|
|
1141
|
+
return result;
|
|
1142
|
+
},
|
|
1143
|
+
/** 编辑自定义指令,位置不变 */
|
|
1144
|
+
update(id, preset) {
|
|
1145
|
+
const { custom } = ensureStore().get();
|
|
1146
|
+
if (!custom.some((item) => item.id === id)) return {
|
|
1147
|
+
ok: false,
|
|
1148
|
+
reason: "not-found"
|
|
1149
|
+
};
|
|
1150
|
+
const result = validate(preset, id);
|
|
1151
|
+
if (!result.ok) return result;
|
|
1152
|
+
persist({ custom: custom.map((item) => item.id === id ? {
|
|
1153
|
+
id,
|
|
1154
|
+
label: preset.label.trim(),
|
|
1155
|
+
task: preset.task.trim()
|
|
1156
|
+
} : item) });
|
|
1157
|
+
return result;
|
|
1158
|
+
},
|
|
1159
|
+
remove(id) {
|
|
1160
|
+
persist({ custom: ensureStore().get().custom.filter((item) => item.id !== id) });
|
|
1161
|
+
},
|
|
1162
|
+
/** 隐藏一条来源指令(以 task 文本为标识);已隐藏时不重复记录 */
|
|
1163
|
+
hide(task) {
|
|
1164
|
+
const { hidden } = ensureStore().get();
|
|
1165
|
+
if (hidden.includes(task)) return;
|
|
1166
|
+
persist({ hidden: [...hidden, task] });
|
|
1167
|
+
},
|
|
1168
|
+
/** 恢复全部被隐藏的来源指令(含已失效的记录);不影响自定义指令 */
|
|
1169
|
+
restoreHidden() {
|
|
1170
|
+
persist({ hidden: [] });
|
|
872
1171
|
}
|
|
873
1172
|
};
|
|
874
1173
|
|
|
@@ -1124,9 +1423,13 @@ function configurePageUse(options) {
|
|
|
1124
1423
|
everConfigured = true;
|
|
1125
1424
|
setSettingsStoragePrefix(options.storageKeyPrefix);
|
|
1126
1425
|
setHistoryStoragePrefix(options.storageKeyPrefix);
|
|
1426
|
+
setPresetsStoragePrefix(options.storageKeyPrefix);
|
|
1127
1427
|
store.set({ ready: Boolean(options.llm.baseURL) });
|
|
1128
1428
|
stopNarration?.();
|
|
1129
|
-
stopNarration = startNarration({
|
|
1429
|
+
stopNarration = startNarration({
|
|
1430
|
+
subscribe: subscribePageUse,
|
|
1431
|
+
engines: options.ttsEngines
|
|
1432
|
+
});
|
|
1130
1433
|
return () => {
|
|
1131
1434
|
if (config !== options) return;
|
|
1132
1435
|
config = null;
|
|
@@ -1324,6 +1627,39 @@ const pageUseCapability = {
|
|
|
1324
1627
|
subscribe: subscribePageUse
|
|
1325
1628
|
};
|
|
1326
1629
|
|
|
1630
|
+
//#endregion
|
|
1631
|
+
//#region src/ui/launcherControl.ts
|
|
1632
|
+
/**
|
|
1633
|
+
* 调试入口的显示控制:供业务系统通过模块导出或全局对象调出 / 隐藏调试面板。
|
|
1634
|
+
* 显示状态写入设置 launcherVisible(localStorage 持久化),刷新后保持;
|
|
1635
|
+
* 不依赖 lit,全局桥接引用本模块不会引入 UI 组件。
|
|
1636
|
+
*/
|
|
1637
|
+
/** 已挂载 launcher 登记的「展开面板」回调;未挂载时为空 */
|
|
1638
|
+
const openers = /* @__PURE__ */ new Set();
|
|
1639
|
+
/** launcher 挂载时登记展开回调,返回注销函数 */
|
|
1640
|
+
function registerLauncherOpener(open) {
|
|
1641
|
+
openers.add(open);
|
|
1642
|
+
return () => {
|
|
1643
|
+
openers.delete(open);
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* 显示调试入口并展开面板,状态持久化。
|
|
1648
|
+
* 宿主未挂载调试面板时只保存状态并警告,之后挂载的面板按该状态显示。
|
|
1649
|
+
*/
|
|
1650
|
+
function showPageUseLauncher() {
|
|
1651
|
+
pageUseSettings.update({ launcherVisible: true });
|
|
1652
|
+
if (!openers.size) {
|
|
1653
|
+
console.warn("[PageUse] 调试面板未挂载,显示状态已保存,挂载后生效(mountPageUseUI 需传入 launcher)");
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
openers.forEach((open) => open());
|
|
1657
|
+
}
|
|
1658
|
+
/** 隐藏调试入口(面板与浮动入口),状态持久化;效果与面板标题栏的关闭按钮相同 */
|
|
1659
|
+
function hidePageUseLauncher() {
|
|
1660
|
+
pageUseSettings.update({ launcherVisible: false });
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1327
1663
|
//#endregion
|
|
1328
1664
|
//#region src/capability/tool.ts
|
|
1329
1665
|
/** 工具名;对接方在 system 提示中引用时保持一致 */
|
|
@@ -1416,7 +1752,9 @@ function installPageUseGlobal() {
|
|
|
1416
1752
|
const api = {
|
|
1417
1753
|
...pageUseCapability,
|
|
1418
1754
|
tool: PAGE_USE_TOOL,
|
|
1419
|
-
runTool: runPageUseTool
|
|
1755
|
+
runTool: runPageUseTool,
|
|
1756
|
+
showLauncher: showPageUseLauncher,
|
|
1757
|
+
hideLauncher: hidePageUseLauncher
|
|
1420
1758
|
};
|
|
1421
1759
|
window[PAGE_USE_GLOBAL_KEY] = api;
|
|
1422
1760
|
return () => {
|
|
@@ -1718,6 +2056,10 @@ var PageUseGlowElement = class extends LitElement {
|
|
|
1718
2056
|
}
|
|
1719
2057
|
};
|
|
1720
2058
|
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region package.json
|
|
2061
|
+
var version = "0.0.6";
|
|
2062
|
+
|
|
1721
2063
|
//#endregion
|
|
1722
2064
|
//#region src/ui/drag.ts
|
|
1723
2065
|
/** 判定为拖拽而非点击的最小位移(px) */
|
|
@@ -1742,6 +2084,7 @@ function createDrag(options) {
|
|
|
1742
2084
|
if (options.skipInteractive !== false) {
|
|
1743
2085
|
if (event.composedPath()[0]?.closest?.("button, input, textarea, select, a, label, [data-no-drag]")) return;
|
|
1744
2086
|
}
|
|
2087
|
+
const tapTarget = event.composedPath()[0];
|
|
1745
2088
|
const handle = event.currentTarget;
|
|
1746
2089
|
const rect = el.getBoundingClientRect();
|
|
1747
2090
|
const startX = event.clientX;
|
|
@@ -1767,7 +2110,7 @@ function createDrag(options) {
|
|
|
1767
2110
|
try {
|
|
1768
2111
|
handle.releasePointerCapture(event.pointerId);
|
|
1769
2112
|
} catch {}
|
|
1770
|
-
if (!moved) options.onTap?.();
|
|
2113
|
+
if (!moved) options.onTap?.(tapTarget);
|
|
1771
2114
|
};
|
|
1772
2115
|
handle.setPointerCapture(event.pointerId);
|
|
1773
2116
|
handle.addEventListener("pointermove", onMove);
|
|
@@ -1787,6 +2130,89 @@ function createDrag(options) {
|
|
|
1787
2130
|
};
|
|
1788
2131
|
}
|
|
1789
2132
|
|
|
2133
|
+
//#endregion
|
|
2134
|
+
//#region src/ui/formatTokens.ts
|
|
2135
|
+
/**
|
|
2136
|
+
* 标题栏 token 数格式化:< 1000 显示原值;否则以 k(千)/ M(百万)为单位,
|
|
2137
|
+
* 保留 1 位小数并去掉末尾的 .0,例如 1234 → 1.2k、1500000 → 1.5M。
|
|
2138
|
+
*/
|
|
2139
|
+
/** 单位由小到大;四舍五入后达到下一档的倍数时进位到下一档(避免出现 1000k) */
|
|
2140
|
+
const UNITS = [{
|
|
2141
|
+
value: 1e3,
|
|
2142
|
+
suffix: "k"
|
|
2143
|
+
}, {
|
|
2144
|
+
value: 1e6,
|
|
2145
|
+
suffix: "M"
|
|
2146
|
+
}];
|
|
2147
|
+
function formatTokens(n) {
|
|
2148
|
+
if (n < 1e3) return String(n);
|
|
2149
|
+
for (let i = 0; i < UNITS.length; i++) {
|
|
2150
|
+
const unit = UNITS[i];
|
|
2151
|
+
const scaled = Number((n / unit.value).toFixed(1));
|
|
2152
|
+
if (scaled >= 1e3 && i < UNITS.length - 1) continue;
|
|
2153
|
+
return `${scaled}${unit.suffix}`;
|
|
2154
|
+
}
|
|
2155
|
+
return String(n);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
//#endregion
|
|
2159
|
+
//#region src/ui/icons.ts
|
|
2160
|
+
/**
|
|
2161
|
+
* 调试面板标题栏图标:24×24 线条图标,颜色跟随 currentColor,尺寸由 .icon-btn svg 控制。
|
|
2162
|
+
* 路径取自 Lucide(ISC 许可):settings、arrow-left、minimize-2、x、volume-2、volume-x;
|
|
2163
|
+
* robotIcon 为本包自绘的页面智能体形象,其 SVG 字符串以 PAGE_USE_ROBOT_ICON_SVG 对外导出。
|
|
2164
|
+
*/
|
|
2165
|
+
/** 外层 <svg> 须在 html 模板中创建(lit 的 svg 模板只用于 <svg> 内部的片段) */
|
|
2166
|
+
function icon(content) {
|
|
2167
|
+
return html`<svg
|
|
2168
|
+
viewBox="0 0 24 24"
|
|
2169
|
+
fill="none"
|
|
2170
|
+
stroke="currentColor"
|
|
2171
|
+
stroke-width="2"
|
|
2172
|
+
stroke-linecap="round"
|
|
2173
|
+
stroke-linejoin="round"
|
|
2174
|
+
aria-hidden="true"
|
|
2175
|
+
>${content}</svg>`;
|
|
2176
|
+
}
|
|
2177
|
+
const settingsIcon = icon(svg`
|
|
2178
|
+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
|
2179
|
+
<circle cx="12" cy="12" r="3" />
|
|
2180
|
+
`);
|
|
2181
|
+
/** 设置 / 元信息视图中返回执行记录 */
|
|
2182
|
+
const backIcon = icon(svg`<path d="M19 12H5" /><path d="m12 19-7-7 7-7" />`);
|
|
2183
|
+
/** 收起为浮动入口 */
|
|
2184
|
+
const collapseIcon = icon(svg`
|
|
2185
|
+
<path d="M4 14h6v6" /><path d="M20 10h-6V4" /><path d="m14 10 7-7" /><path d="m3 21 7-7" />
|
|
2186
|
+
`);
|
|
2187
|
+
/** 关闭调试入口 */
|
|
2188
|
+
const closeIcon = icon(svg`<path d="M18 6 6 18" /><path d="m6 6 12 12" />`);
|
|
2189
|
+
/** 播报中(点击静音) */
|
|
2190
|
+
const volumeIcon = icon(svg`
|
|
2191
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
2192
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" /><path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
2193
|
+
`);
|
|
2194
|
+
/** 已静音(点击恢复) */
|
|
2195
|
+
const mutedIcon = icon(svg`
|
|
2196
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
2197
|
+
<line x1="22" x2="16" y1="9" y2="15" /><line x1="16" x2="22" y1="9" y2="15" />
|
|
2198
|
+
`);
|
|
2199
|
+
/**
|
|
2200
|
+
* 页面智能体机器人图标(天线 + 屏幕式头部 + 双眼)的 SVG 字符串,供宿主在自身界面中复用(如智能体列表卡片)。
|
|
2201
|
+
* 颜色全部跟随 currentColor,不含 width / height,尺寸由宿主 CSS 控制;viewBox 为 24×24、线宽 2。
|
|
2202
|
+
* robot-antenna / robot-eye 类名供面板在执行中做闪烁与眨眼动画,宿主不定义对应样式时无影响。
|
|
2203
|
+
*/
|
|
2204
|
+
const PAGE_USE_ROBOT_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
2205
|
+
<circle class="robot-antenna" cx="12" cy="3" r="1.5" fill="currentColor" stroke="none" />
|
|
2206
|
+
<path d="M12 4.5V7" />
|
|
2207
|
+
<rect x="4" y="7" width="16" height="12.5" rx="4" />
|
|
2208
|
+
<path d="M2 12v3.5" /><path d="M22 12v3.5" />
|
|
2209
|
+
<circle class="robot-eye" cx="9" cy="12.5" r="1.6" fill="currentColor" stroke="none" />
|
|
2210
|
+
<circle class="robot-eye" cx="15" cy="12.5" r="1.6" fill="currentColor" stroke="none" />
|
|
2211
|
+
<path d="M9.5 16.3h5" />
|
|
2212
|
+
</svg>`;
|
|
2213
|
+
/** 收起时的入口按钮图标:与导出的 PAGE_USE_ROBOT_ICON_SVG 同源,内容为包内常量,无注入风险 */
|
|
2214
|
+
const robotIcon = html`${unsafeHTML(PAGE_USE_ROBOT_ICON_SVG)}`;
|
|
2215
|
+
|
|
1790
2216
|
//#endregion
|
|
1791
2217
|
//#region src/ui/presets.ts
|
|
1792
2218
|
/** 通用示例:不依赖具体站点,覆盖详情、标签、返回等典型操作 */
|
|
@@ -1804,8 +2230,11 @@ const PAGE_USE_STATIC_PRESETS = [
|
|
|
1804
2230
|
task: "概括当前页面展示的主要内容与可执行的操作"
|
|
1805
2231
|
}
|
|
1806
2232
|
];
|
|
1807
|
-
/**
|
|
1808
|
-
|
|
2233
|
+
/**
|
|
2234
|
+
* 取前 N 个可见叶子菜单,生成「打开 XX」快捷指令。
|
|
2235
|
+
* exclude 中的 task 被跳过且不占名额,由后续菜单补位(用户隐藏菜单指令时使用)。
|
|
2236
|
+
*/
|
|
2237
|
+
function buildMenuPresets(menus, limit = 8, exclude) {
|
|
1809
2238
|
const out = [];
|
|
1810
2239
|
const walk = (list) => {
|
|
1811
2240
|
for (const node of list) {
|
|
@@ -1813,15 +2242,51 @@ function buildMenuPresets(menus, limit = 8) {
|
|
|
1813
2242
|
if (node.hidden) continue;
|
|
1814
2243
|
const children = (node.children || []).filter((child) => !child.hidden);
|
|
1815
2244
|
if (children.length) walk(children);
|
|
1816
|
-
else
|
|
1817
|
-
|
|
1818
|
-
task
|
|
1819
|
-
|
|
2245
|
+
else {
|
|
2246
|
+
const task = `打开「${node.label}」页面`;
|
|
2247
|
+
if (!exclude?.has(task)) out.push({
|
|
2248
|
+
label: node.label,
|
|
2249
|
+
task
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
1820
2252
|
}
|
|
1821
2253
|
};
|
|
1822
2254
|
walk(menus || []);
|
|
1823
2255
|
return out;
|
|
1824
2256
|
}
|
|
2257
|
+
/** 组合快捷指令分组:展示区与编辑视图共用,保证两处结果一致 */
|
|
2258
|
+
function resolvePresetGroups(input) {
|
|
2259
|
+
const hidden = new Set(input.state.hidden);
|
|
2260
|
+
const visible = (list) => (list || []).filter((preset) => !hidden.has(preset.task));
|
|
2261
|
+
const limit = input.menuPresetLimit;
|
|
2262
|
+
const menu = limit > 0 ? buildMenuPresets(input.menus, limit, hidden) : [];
|
|
2263
|
+
const builtin = visible(PAGE_USE_STATIC_PRESETS);
|
|
2264
|
+
const host = visible(input.presets);
|
|
2265
|
+
const scenarios = visible(input.scenarios);
|
|
2266
|
+
const allSourceTasks = new Set([
|
|
2267
|
+
...limit > 0 ? buildMenuPresets(input.menus, Number.POSITIVE_INFINITY) : [],
|
|
2268
|
+
...PAGE_USE_STATIC_PRESETS,
|
|
2269
|
+
...input.presets || [],
|
|
2270
|
+
...input.scenarios || []
|
|
2271
|
+
].map((preset) => preset.task));
|
|
2272
|
+
const hiddenCount = [...hidden].filter((task) => allSourceTasks.has(task)).length;
|
|
2273
|
+
return {
|
|
2274
|
+
mine: input.state.custom,
|
|
2275
|
+
common: [
|
|
2276
|
+
...menu,
|
|
2277
|
+
...builtin,
|
|
2278
|
+
...host
|
|
2279
|
+
],
|
|
2280
|
+
scenarios,
|
|
2281
|
+
sources: {
|
|
2282
|
+
menu,
|
|
2283
|
+
builtin,
|
|
2284
|
+
host,
|
|
2285
|
+
scenarios
|
|
2286
|
+
},
|
|
2287
|
+
hiddenCount
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
1825
2290
|
|
|
1826
2291
|
//#endregion
|
|
1827
2292
|
//#region src/ui/styles.ts
|
|
@@ -1840,6 +2305,11 @@ const launcherStyles = css`
|
|
|
1840
2305
|
--text-3: #8a94a6;
|
|
1841
2306
|
--border: #e6eaf0;
|
|
1842
2307
|
--bg-soft: #f5f7fa;
|
|
2308
|
+
/* 入口按钮背景与面板边框共用的流光渐变;宿主可通过 --tz-page-use-flow-gradient 覆盖 */
|
|
2309
|
+
--flow-gradient: var(
|
|
2310
|
+
--tz-page-use-flow-gradient,
|
|
2311
|
+
radial-gradient(#fa6400 0%, #fa6400 10%, #fcf26e 40%, #00e754 60%, #00eef4 70%, #d519d2 100%)
|
|
2312
|
+
);
|
|
1843
2313
|
font-family: system-ui, -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
1844
2314
|
font-size: 13px;
|
|
1845
2315
|
line-height: 1.5;
|
|
@@ -1896,59 +2366,181 @@ const launcherStyles = css`
|
|
|
1896
2366
|
* 运行期蒙层 z-index 为 2147483641、内置面板为 2147483642;
|
|
1897
2367
|
* 入口与面板须高于两者,否则任务执行中无法点「终止」或回答模型提问。
|
|
1898
2368
|
*/
|
|
2369
|
+
/*
|
|
2370
|
+
* 入口按钮:多色径向渐变放大到 300%,循环移动 background-position 形成流光;
|
|
2371
|
+
* ::before 在内缩 3px 处叠一层 14% 黑:压暗文字背后的高亮色段(黄、青)以保证白字清晰,
|
|
2372
|
+
* 外圈 3px 保持原色,形成渐变描边质感。
|
|
2373
|
+
* 渐变见 :host 的 --flow-gradient。
|
|
2374
|
+
*/
|
|
1899
2375
|
.fab {
|
|
1900
2376
|
position: fixed;
|
|
1901
2377
|
z-index: 2147483643;
|
|
1902
2378
|
display: flex;
|
|
1903
2379
|
align-items: center;
|
|
1904
2380
|
gap: 6px;
|
|
1905
|
-
padding: 8px
|
|
2381
|
+
padding: 8px 16px;
|
|
1906
2382
|
border: none;
|
|
1907
|
-
border-radius:
|
|
1908
|
-
background: var(--
|
|
2383
|
+
border-radius: 999px;
|
|
2384
|
+
background: var(--flow-gradient) 0 0 / 300% 300%;
|
|
1909
2385
|
color: #fff;
|
|
1910
2386
|
font: inherit;
|
|
1911
2387
|
font-weight: 600;
|
|
2388
|
+
/* 贴边的深色阴影形成细描边,配合内层压暗保证白字在高亮色段上可读 */
|
|
2389
|
+
text-shadow:
|
|
2390
|
+
0 0 1px rgba(0, 0, 0, 0.35),
|
|
2391
|
+
0 1px 2px rgba(0, 0, 0, 0.35);
|
|
1912
2392
|
cursor: grab;
|
|
1913
2393
|
touch-action: none;
|
|
1914
|
-
box-shadow:
|
|
2394
|
+
box-shadow: -1px 2px 6px 1px rgba(0, 0, 0, 0.12);
|
|
2395
|
+
animation: fab-shining 15s linear infinite;
|
|
2396
|
+
will-change: background-position;
|
|
2397
|
+
/* 「执行中」与标题文案切换时的宽度过渡(由 launcher 设置起止宽度);过渡中不换行、裁掉溢出文字 */
|
|
2398
|
+
overflow: hidden;
|
|
2399
|
+
white-space: nowrap;
|
|
2400
|
+
transition: width 0.3s ease;
|
|
2401
|
+
}
|
|
2402
|
+
.fab::before {
|
|
2403
|
+
content: '';
|
|
2404
|
+
position: absolute;
|
|
2405
|
+
inset: 3px;
|
|
2406
|
+
border-radius: inherit;
|
|
2407
|
+
background: rgba(0, 0, 0, 0.14);
|
|
2408
|
+
pointer-events: none;
|
|
2409
|
+
}
|
|
2410
|
+
.fab > * {
|
|
2411
|
+
position: relative;
|
|
1915
2412
|
}
|
|
1916
2413
|
.fab:active {
|
|
1917
2414
|
cursor: grabbing;
|
|
1918
2415
|
}
|
|
2416
|
+
/* 执行中:流速加快(15s → 10s)并外发光 */
|
|
1919
2417
|
.fab.running {
|
|
1920
|
-
|
|
1921
|
-
box-shadow:
|
|
2418
|
+
animation-duration: 10s;
|
|
2419
|
+
box-shadow:
|
|
2420
|
+
-1px 2px 6px 1px rgba(0, 0, 0, 0.12),
|
|
2421
|
+
0 0 14px 2px rgba(250, 100, 0, 0.45);
|
|
1922
2422
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2423
|
+
@keyframes fab-shining {
|
|
2424
|
+
0% {
|
|
2425
|
+
background-position: -20% -20%;
|
|
2426
|
+
}
|
|
2427
|
+
25% {
|
|
2428
|
+
background-position: 30% 80%;
|
|
2429
|
+
}
|
|
2430
|
+
50% {
|
|
2431
|
+
background-position: 110% 110%;
|
|
2432
|
+
}
|
|
2433
|
+
75% {
|
|
2434
|
+
background-position: 80% 30%;
|
|
2435
|
+
}
|
|
2436
|
+
100% {
|
|
2437
|
+
background-position: -20% -20%;
|
|
2438
|
+
}
|
|
1928
2439
|
}
|
|
1929
|
-
|
|
2440
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2441
|
+
.fab {
|
|
2442
|
+
transition: none;
|
|
2443
|
+
}
|
|
2444
|
+
.fab,
|
|
2445
|
+
.panel::before,
|
|
2446
|
+
.fab.running .robot-antenna,
|
|
2447
|
+
.fab.running .robot-eye {
|
|
2448
|
+
animation: none;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
/* 入口机器人图标:执行中天线闪烁、双眼眨动 */
|
|
2452
|
+
.fab-icon {
|
|
2453
|
+
display: inline-flex;
|
|
2454
|
+
flex: none;
|
|
2455
|
+
}
|
|
2456
|
+
/* 图标 24px,上下 -2px 抵消超出 20px 的部分,按钮高度保持不变 */
|
|
2457
|
+
.fab-icon svg {
|
|
2458
|
+
width: 24px;
|
|
2459
|
+
height: 24px;
|
|
2460
|
+
margin: -2px 0;
|
|
2461
|
+
}
|
|
2462
|
+
.fab.running .robot-antenna {
|
|
1930
2463
|
animation: blink 1s infinite;
|
|
1931
2464
|
}
|
|
1932
|
-
|
|
2465
|
+
.robot-eye {
|
|
2466
|
+
transform-box: fill-box;
|
|
2467
|
+
transform-origin: center;
|
|
2468
|
+
}
|
|
2469
|
+
.fab.running .robot-eye {
|
|
2470
|
+
animation: robot-blink 2.4s infinite;
|
|
2471
|
+
}
|
|
2472
|
+
@keyframes robot-blink {
|
|
2473
|
+
0%,
|
|
2474
|
+
90%,
|
|
2475
|
+
100% {
|
|
2476
|
+
transform: scaleY(1);
|
|
2477
|
+
}
|
|
2478
|
+
95% {
|
|
2479
|
+
transform: scaleY(0.15);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
/*
|
|
2483
|
+
* 面板:高透毛玻璃 + 2px 流光边框。
|
|
2484
|
+
* 面板本体为半透明白底并对背后页面做模糊(backdrop-filter);
|
|
2485
|
+
* 边框由 ::before 绘制:整块铺流光渐变,再用遮罩挖掉 content-box,只留外圈 2px,
|
|
2486
|
+
* 因此内部保持透明,不会透出渐变。边框与入口按钮使用同一套渐变与移动轨迹(fab-shining)。
|
|
2487
|
+
*/
|
|
1933
2488
|
.panel {
|
|
1934
2489
|
position: fixed;
|
|
1935
2490
|
z-index: 2147483643;
|
|
1936
|
-
/* width / height 由 panelStyle
|
|
2491
|
+
/* width / height 由 panelStyle 按入口位置计算(border-box,内边距计入宽高) */
|
|
1937
2492
|
display: flex;
|
|
1938
2493
|
flex-direction: column;
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2494
|
+
/* 为流光边框留出 2px,内容不被边框覆盖 */
|
|
2495
|
+
padding: 2px;
|
|
2496
|
+
border-radius: 12px;
|
|
2497
|
+
background: rgba(255, 255, 255, 0.72);
|
|
2498
|
+
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
|
2499
|
+
backdrop-filter: blur(18px) saturate(160%);
|
|
2500
|
+
box-shadow:
|
|
2501
|
+
0 8px 28px rgba(0, 0, 0, 0.18),
|
|
2502
|
+
0 0 18px rgba(0, 238, 244, 0.18);
|
|
1942
2503
|
overflow: hidden;
|
|
1943
2504
|
}
|
|
2505
|
+
/* 不支持 backdrop-filter 时回退为接近不透明的白底,保证内容可读 */
|
|
2506
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
2507
|
+
.panel {
|
|
2508
|
+
background: rgba(255, 255, 255, 0.96);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
.panel::before {
|
|
2512
|
+
content: '';
|
|
2513
|
+
position: absolute;
|
|
2514
|
+
inset: 0;
|
|
2515
|
+
z-index: 1;
|
|
2516
|
+
padding: 2px;
|
|
2517
|
+
border-radius: inherit;
|
|
2518
|
+
background: var(--flow-gradient) 0 0 / 300% 300%;
|
|
2519
|
+
/* 两层遮罩相减:整块减去 content-box,只保留 padding 区域的外圈 */
|
|
2520
|
+
-webkit-mask:
|
|
2521
|
+
linear-gradient(#000 0 0) content-box,
|
|
2522
|
+
linear-gradient(#000 0 0);
|
|
2523
|
+
-webkit-mask-composite: xor;
|
|
2524
|
+
mask:
|
|
2525
|
+
linear-gradient(#000 0 0) content-box exclude,
|
|
2526
|
+
linear-gradient(#000 0 0);
|
|
2527
|
+
animation: fab-shining 15s linear infinite;
|
|
2528
|
+
pointer-events: none;
|
|
2529
|
+
}
|
|
2530
|
+
/* 执行中:边框流速加快(15s → 10s) */
|
|
2531
|
+
.panel.running::before {
|
|
2532
|
+
animation-duration: 10s;
|
|
2533
|
+
}
|
|
1944
2534
|
|
|
1945
2535
|
.head {
|
|
1946
2536
|
display: flex;
|
|
1947
2537
|
align-items: center;
|
|
1948
2538
|
gap: 8px;
|
|
1949
2539
|
padding: 10px 12px;
|
|
1950
|
-
|
|
1951
|
-
border-
|
|
2540
|
+
/* 毛玻璃面板内的半透明色块;上圆角与面板内圈(12px - 2px 边框)一致,避免直角露出 */
|
|
2541
|
+
border-radius: 10px 10px 0 0;
|
|
2542
|
+
background: rgba(226, 232, 240, 0.75);
|
|
2543
|
+
border-bottom: 1px solid rgba(208, 215, 226, 0.8);
|
|
1952
2544
|
cursor: grab;
|
|
1953
2545
|
touch-action: none;
|
|
1954
2546
|
user-select: none;
|
|
@@ -1980,6 +2572,8 @@ const launcherStyles = css`
|
|
|
1980
2572
|
.title {
|
|
1981
2573
|
flex: 1 0 auto;
|
|
1982
2574
|
font-weight: 600;
|
|
2575
|
+
/* 连续点击标题打开元信息面板,避免选中文字 */
|
|
2576
|
+
user-select: none;
|
|
1983
2577
|
}
|
|
1984
2578
|
.status {
|
|
1985
2579
|
min-width: 0;
|
|
@@ -1990,6 +2584,48 @@ const launcherStyles = css`
|
|
|
1990
2584
|
white-space: nowrap;
|
|
1991
2585
|
}
|
|
1992
2586
|
|
|
2587
|
+
/* 标题栏图标按钮:常态为弱化的线条图标,悬停显示浅底色;关闭按钮悬停为红底白图标 */
|
|
2588
|
+
.head-actions {
|
|
2589
|
+
display: flex;
|
|
2590
|
+
flex: none;
|
|
2591
|
+
align-items: center;
|
|
2592
|
+
gap: 6px;
|
|
2593
|
+
}
|
|
2594
|
+
.icon-btn {
|
|
2595
|
+
display: inline-flex;
|
|
2596
|
+
align-items: center;
|
|
2597
|
+
justify-content: center;
|
|
2598
|
+
width: 26px;
|
|
2599
|
+
height: 26px;
|
|
2600
|
+
padding: 0;
|
|
2601
|
+
border: none;
|
|
2602
|
+
border-radius: 6px;
|
|
2603
|
+
background: transparent;
|
|
2604
|
+
color: var(--text-3);
|
|
2605
|
+
cursor: pointer;
|
|
2606
|
+
transition: background 0.15s, color 0.15s;
|
|
2607
|
+
}
|
|
2608
|
+
.icon-btn svg {
|
|
2609
|
+
width: 16px;
|
|
2610
|
+
height: 16px;
|
|
2611
|
+
}
|
|
2612
|
+
.icon-btn:hover {
|
|
2613
|
+
background: #e6eaf0;
|
|
2614
|
+
color: var(--text);
|
|
2615
|
+
}
|
|
2616
|
+
.icon-btn:focus-visible {
|
|
2617
|
+
outline: 2px solid color-mix(in srgb, var(--primary) 45%, transparent);
|
|
2618
|
+
outline-offset: 1px;
|
|
2619
|
+
}
|
|
2620
|
+
.icon-btn.active {
|
|
2621
|
+
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
|
2622
|
+
color: var(--primary);
|
|
2623
|
+
}
|
|
2624
|
+
.icon-btn.close:hover {
|
|
2625
|
+
background: var(--error);
|
|
2626
|
+
color: #fff;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
1993
2629
|
/* 占满头部与输入区之间的剩余高度(min-height:0 允许在 flex 列中收缩) */
|
|
1994
2630
|
.fill {
|
|
1995
2631
|
flex: 1;
|
|
@@ -2013,8 +2649,28 @@ const launcherStyles = css`
|
|
|
2013
2649
|
color: var(--text-3);
|
|
2014
2650
|
font-size: 12px;
|
|
2015
2651
|
}
|
|
2016
|
-
.
|
|
2652
|
+
.settings-status {
|
|
2653
|
+
color: var(--text-3);
|
|
2654
|
+
font-size: 12px;
|
|
2655
|
+
}
|
|
2656
|
+
/* 元信息面板:标签 / 值两列 */
|
|
2657
|
+
.meta-list {
|
|
2658
|
+
display: grid;
|
|
2659
|
+
grid-template-columns: auto 1fr;
|
|
2660
|
+
gap: 6px 12px;
|
|
2661
|
+
margin: 0;
|
|
2662
|
+
font-size: 12px;
|
|
2663
|
+
}
|
|
2664
|
+
.meta-list dt {
|
|
2017
2665
|
color: var(--text-3);
|
|
2666
|
+
}
|
|
2667
|
+
.meta-list dd {
|
|
2668
|
+
margin: 0;
|
|
2669
|
+
word-break: break-all;
|
|
2670
|
+
}
|
|
2671
|
+
.meta-engines {
|
|
2672
|
+
margin: 0;
|
|
2673
|
+
padding-left: 18px;
|
|
2018
2674
|
font-size: 12px;
|
|
2019
2675
|
word-break: break-all;
|
|
2020
2676
|
}
|
|
@@ -2263,7 +2919,8 @@ const launcherStyles = css`
|
|
|
2263
2919
|
gap: 8px;
|
|
2264
2920
|
padding: 10px 12px 12px;
|
|
2265
2921
|
border-top: 1px solid var(--border);
|
|
2266
|
-
|
|
2922
|
+
border-radius: 0 0 10px 10px;
|
|
2923
|
+
background: rgba(250, 251, 252, 0.5);
|
|
2267
2924
|
}
|
|
2268
2925
|
/* 悬浮在输入区上沿(以 .composer 为定位参照) */
|
|
2269
2926
|
.jump-latest {
|
|
@@ -2302,7 +2959,6 @@ const launcherStyles = css`
|
|
|
2302
2959
|
}
|
|
2303
2960
|
/* ---------- 演示播报设置 ---------- */
|
|
2304
2961
|
.voice-warning {
|
|
2305
|
-
margin-left: 21px;
|
|
2306
2962
|
font-size: 12px;
|
|
2307
2963
|
}
|
|
2308
2964
|
/* 与复选框文字左对齐(复选框宽 13px + 间距 8px) */
|
|
@@ -2312,6 +2968,99 @@ const launcherStyles = css`
|
|
|
2312
2968
|
gap: 8px;
|
|
2313
2969
|
margin-left: 21px;
|
|
2314
2970
|
}
|
|
2971
|
+
/* 设置页底部操作按钮:与复选框文字、播报选项输入框左对齐(同 21px 缩进),与上方拉开间距 */
|
|
2972
|
+
.settings-actions {
|
|
2973
|
+
margin-top: 8px;
|
|
2974
|
+
margin-left: 21px;
|
|
2975
|
+
}
|
|
2976
|
+
/* 面板内轻提示:相对面板(.panel 为 fixed 定位)底部居中,不拦截点击 */
|
|
2977
|
+
.toast {
|
|
2978
|
+
position: absolute;
|
|
2979
|
+
left: 50%;
|
|
2980
|
+
bottom: 24px;
|
|
2981
|
+
z-index: 1;
|
|
2982
|
+
max-width: calc(100% - 48px);
|
|
2983
|
+
padding: 6px 14px;
|
|
2984
|
+
border-radius: 6px;
|
|
2985
|
+
background: rgba(31, 45, 61, 0.88);
|
|
2986
|
+
color: #fff;
|
|
2987
|
+
font-size: 12px;
|
|
2988
|
+
white-space: nowrap;
|
|
2989
|
+
overflow: hidden;
|
|
2990
|
+
text-overflow: ellipsis;
|
|
2991
|
+
pointer-events: none;
|
|
2992
|
+
transform: translateX(-50%);
|
|
2993
|
+
animation: toast-in 0.2s ease-out;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
/* ---------- 快捷指令编辑 ---------- */
|
|
2997
|
+
/*
|
|
2998
|
+
* 滚动区与面板底边留出间距,避免列表滚动时贴住底边与圆角。
|
|
2999
|
+
* 用 margin 而非 padding:滚动内容会滚入滚动容器自身的 padding 区域,仍然贴边。
|
|
3000
|
+
*/
|
|
3001
|
+
.fill.preset-scroll {
|
|
3002
|
+
margin-bottom: 10px;
|
|
3003
|
+
}
|
|
3004
|
+
/* 分组标题行:标题左、操作按钮右 */
|
|
3005
|
+
.preset-group-head {
|
|
3006
|
+
display: flex;
|
|
3007
|
+
align-items: center;
|
|
3008
|
+
justify-content: space-between;
|
|
3009
|
+
margin-top: 4px;
|
|
3010
|
+
}
|
|
3011
|
+
.preset-list {
|
|
3012
|
+
display: flex;
|
|
3013
|
+
flex-direction: column;
|
|
3014
|
+
gap: 4px;
|
|
3015
|
+
}
|
|
3016
|
+
.preset-item {
|
|
3017
|
+
display: flex;
|
|
3018
|
+
align-items: center;
|
|
3019
|
+
gap: 4px;
|
|
3020
|
+
padding: 4px 4px 4px 8px;
|
|
3021
|
+
border: 1px solid var(--border);
|
|
3022
|
+
border-radius: 6px;
|
|
3023
|
+
}
|
|
3024
|
+
/* min-width:0 使长文本在 flex 中可收缩,否则省略号不生效、按钮被挤出 */
|
|
3025
|
+
.preset-text {
|
|
3026
|
+
flex: 1;
|
|
3027
|
+
min-width: 0;
|
|
3028
|
+
display: flex;
|
|
3029
|
+
flex-direction: column;
|
|
3030
|
+
}
|
|
3031
|
+
.preset-label,
|
|
3032
|
+
.preset-task {
|
|
3033
|
+
overflow: hidden;
|
|
3034
|
+
text-overflow: ellipsis;
|
|
3035
|
+
white-space: nowrap;
|
|
3036
|
+
}
|
|
3037
|
+
/* 指令内容只显示一行,完整内容通过 title 悬停查看或进入编辑表单查看 */
|
|
3038
|
+
.preset-task {
|
|
3039
|
+
color: var(--text-3);
|
|
3040
|
+
font-size: 12px;
|
|
3041
|
+
}
|
|
3042
|
+
.preset-form {
|
|
3043
|
+
display: flex;
|
|
3044
|
+
flex-direction: column;
|
|
3045
|
+
gap: 6px;
|
|
3046
|
+
padding: 8px;
|
|
3047
|
+
border: 1px solid var(--primary);
|
|
3048
|
+
border-radius: 6px;
|
|
3049
|
+
}
|
|
3050
|
+
.preset-form .input {
|
|
3051
|
+
height: 28px;
|
|
3052
|
+
}
|
|
3053
|
+
.preset-error {
|
|
3054
|
+
font-size: 12px;
|
|
3055
|
+
}
|
|
3056
|
+
.preset-form-actions {
|
|
3057
|
+
justify-content: flex-end;
|
|
3058
|
+
}
|
|
3059
|
+
.preset-restore {
|
|
3060
|
+
align-items: center;
|
|
3061
|
+
margin-top: 8px;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
2315
3064
|
.field {
|
|
2316
3065
|
display: flex;
|
|
2317
3066
|
flex-direction: column;
|
|
@@ -2329,6 +3078,19 @@ const launcherStyles = css`
|
|
|
2329
3078
|
select.input {
|
|
2330
3079
|
height: 28px;
|
|
2331
3080
|
}
|
|
3081
|
+
/* 音色下拉框与试听按钮同一行:下拉框占满剩余宽度,长音色名省略显示 */
|
|
3082
|
+
.voice-name-row {
|
|
3083
|
+
display: flex;
|
|
3084
|
+
gap: 6px;
|
|
3085
|
+
}
|
|
3086
|
+
.voice-name-row .input {
|
|
3087
|
+
flex: 1;
|
|
3088
|
+
min-width: 0;
|
|
3089
|
+
text-overflow: ellipsis;
|
|
3090
|
+
}
|
|
3091
|
+
.voice-name-row .preview-btn {
|
|
3092
|
+
flex: none;
|
|
3093
|
+
}
|
|
2332
3094
|
|
|
2333
3095
|
.input-group {
|
|
2334
3096
|
display: flex;
|
|
@@ -2351,7 +3113,10 @@ const launcherStyles = css`
|
|
|
2351
3113
|
overflow-y: auto;
|
|
2352
3114
|
border: 1px solid var(--border);
|
|
2353
3115
|
border-radius: 8px;
|
|
2354
|
-
|
|
3116
|
+
/* 浮在执行记录上方,透明度低于面板以保证列表可读 */
|
|
3117
|
+
background: rgba(255, 255, 255, 0.88);
|
|
3118
|
+
-webkit-backdrop-filter: blur(12px);
|
|
3119
|
+
backdrop-filter: blur(12px);
|
|
2355
3120
|
}
|
|
2356
3121
|
.history-item {
|
|
2357
3122
|
display: flex;
|
|
@@ -2395,6 +3160,12 @@ const launcherStyles = css`
|
|
|
2395
3160
|
opacity: 0.25;
|
|
2396
3161
|
}
|
|
2397
3162
|
}
|
|
3163
|
+
@keyframes toast-in {
|
|
3164
|
+
from {
|
|
3165
|
+
opacity: 0;
|
|
3166
|
+
transform: translate(-50%, 6px);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
2398
3169
|
@keyframes spin {
|
|
2399
3170
|
to {
|
|
2400
3171
|
transform: rotate(360deg);
|
|
@@ -2424,9 +3195,9 @@ const STATUS_LABELS = {
|
|
|
2424
3195
|
error: "出错",
|
|
2425
3196
|
stopped: "已终止"
|
|
2426
3197
|
};
|
|
2427
|
-
/** 入口默认位置:距视口右边/下边的距离(px
|
|
2428
|
-
const FAB_DEFAULT_RIGHT =
|
|
2429
|
-
const FAB_DEFAULT_BOTTOM =
|
|
3198
|
+
/** 入口默认位置:距视口右边/下边的距离(px);面板展开时与入口对齐于同一角,即面板默认贴右下角 */
|
|
3199
|
+
const FAB_DEFAULT_RIGHT = 24;
|
|
3200
|
+
const FAB_DEFAULT_BOTTOM = 24;
|
|
2430
3201
|
/** 面板宽度(px),与入口对齐及视口夹取计算共用 */
|
|
2431
3202
|
const PANEL_WIDTH = 380;
|
|
2432
3203
|
/** 面板期望高度(px),受视口 72vh 与锚点剩余空间约束 */
|
|
@@ -2434,7 +3205,25 @@ const PANEL_HEIGHT = 560;
|
|
|
2434
3205
|
/** 日志区距底部小于该值(px)视为贴底 */
|
|
2435
3206
|
const STICK_THRESHOLD = 24;
|
|
2436
3207
|
const TEXTAREA_MIN_ROWS = 2;
|
|
2437
|
-
/**
|
|
3208
|
+
/** 元信息面板解锁:任意连续 META_UNLOCK_WINDOW_MS 毫秒内点击标题达到 META_UNLOCK_CLICKS 次(滑动窗口) */
|
|
3209
|
+
const META_UNLOCK_WINDOW_MS = 5e3;
|
|
3210
|
+
const META_UNLOCK_CLICKS = 10;
|
|
3211
|
+
/** 面板内轻提示(toast)的显示时长(毫秒) */
|
|
3212
|
+
const TOAST_DURATION_MS = 2e3;
|
|
3213
|
+
/** 标题栏返回按钮的目标视图:按层级返回;main 无上级,此时按钮用于进入设置页 */
|
|
3214
|
+
const PARENT_VIEW = {
|
|
3215
|
+
settings: "main",
|
|
3216
|
+
meta: "main",
|
|
3217
|
+
presets: "settings"
|
|
3218
|
+
};
|
|
3219
|
+
/** 自定义指令保存失败原因 → 提示文字 */
|
|
3220
|
+
const PRESET_SAVE_ERRORS = {
|
|
3221
|
+
"label-empty": "请填写名称",
|
|
3222
|
+
"task-empty": "请填写指令内容",
|
|
3223
|
+
duplicate: "已存在指令内容相同的自定义指令",
|
|
3224
|
+
"not-found": "该指令已被删除,请重新新增"
|
|
3225
|
+
};
|
|
3226
|
+
/** 播报语速倍率的调节范围与步长(1 为正常语速,过快或过慢时中文语音难以听清;注入引擎自行换算为服务参数) */
|
|
2438
3227
|
const VOICE_RATE_MIN = .5;
|
|
2439
3228
|
const VOICE_RATE_MAX = 2;
|
|
2440
3229
|
const VOICE_RATE_STEP = .05;
|
|
@@ -2460,13 +3249,28 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2460
3249
|
}
|
|
2461
3250
|
set config(value) {
|
|
2462
3251
|
this._config = value || {};
|
|
3252
|
+
if (this.isConnected) this.syncPanelName();
|
|
2463
3253
|
this.requestUpdate();
|
|
2464
3254
|
}
|
|
3255
|
+
/** 撤销本面板登记的播报用面板名称 */
|
|
3256
|
+
unregisterPanelName = null;
|
|
3257
|
+
/** 把面板标题登记为播报截断提示中的面板名称(「详细内容请查看「标题」」) */
|
|
3258
|
+
syncPanelName() {
|
|
3259
|
+
this.unregisterPanelName?.();
|
|
3260
|
+
this.unregisterPanelName = registerNarrationPanelName(this._config.title || "Page Use");
|
|
3261
|
+
}
|
|
2465
3262
|
open = false;
|
|
2466
|
-
|
|
3263
|
+
view = "main";
|
|
3264
|
+
/** 标题点击时间戳(毫秒),用于元信息面板解锁的滑动窗口计数;不持久化 */
|
|
3265
|
+
titleTaps = [];
|
|
2467
3266
|
task = "";
|
|
2468
3267
|
answer = "";
|
|
2469
3268
|
tray = null;
|
|
3269
|
+
/** 快捷指令编辑视图中正在编辑的表单;同一时刻只有一个,离开编辑视图时清空 */
|
|
3270
|
+
presetDraft = null;
|
|
3271
|
+
/** 面板内轻提示文字;空串为不显示 */
|
|
3272
|
+
toastText = "";
|
|
3273
|
+
toastTimer = null;
|
|
2470
3274
|
/**
|
|
2471
3275
|
* 日志区跟随滚动:默认贴底跟随新内容;用户上翻后停止跟随,
|
|
2472
3276
|
* 回到底部附近(阈值内)或点击「最新」后恢复。
|
|
@@ -2481,8 +3285,8 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2481
3285
|
* 因此在可见时测量并保留最近一次结果;初值为「Page Use」文案下的近似尺寸。
|
|
2482
3286
|
*/
|
|
2483
3287
|
fabSize = {
|
|
2484
|
-
width:
|
|
2485
|
-
height:
|
|
3288
|
+
width: 122,
|
|
3289
|
+
height: 36
|
|
2486
3290
|
};
|
|
2487
3291
|
viewport = {
|
|
2488
3292
|
width: window.innerWidth,
|
|
@@ -2498,6 +3302,12 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2498
3302
|
alignBottom: true
|
|
2499
3303
|
};
|
|
2500
3304
|
wasRunning = false;
|
|
3305
|
+
/** 入口按钮上次渲染时是否为「执行中」文案;文案切换时用于触发宽度过渡 */
|
|
3306
|
+
fabShowsRunning = false;
|
|
3307
|
+
/** 文案切换前的入口宽度(px);null 表示本次更新不需要宽度过渡 */
|
|
3308
|
+
fabFromWidth = null;
|
|
3309
|
+
/** 进行中宽度过渡的结束回调注销函数;过渡中再次切换时先注销,避免旧回调提前清除宽度 */
|
|
3310
|
+
fabWidthCleanup = null;
|
|
2501
3311
|
cleanups = [];
|
|
2502
3312
|
get fabEl() {
|
|
2503
3313
|
return this.renderRoot.querySelector(".fab");
|
|
@@ -2509,7 +3319,7 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2509
3319
|
return this.renderRoot.querySelector(".log");
|
|
2510
3320
|
}
|
|
2511
3321
|
get textareaEl() {
|
|
2512
|
-
return this.renderRoot.querySelector(".textarea");
|
|
3322
|
+
return this.renderRoot.querySelector(".composer .textarea");
|
|
2513
3323
|
}
|
|
2514
3324
|
fabDrag = createDrag({
|
|
2515
3325
|
getEl: () => this.fabEl,
|
|
@@ -2527,6 +3337,9 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2527
3337
|
panelDrag = createDrag({
|
|
2528
3338
|
getEl: () => this.panelEl,
|
|
2529
3339
|
getPos: () => null,
|
|
3340
|
+
onTap: (target) => {
|
|
3341
|
+
if (target?.closest?.(".title")) this.onTitleTap();
|
|
3342
|
+
},
|
|
2530
3343
|
setPos: ({ x, y }) => {
|
|
2531
3344
|
const el = this.panelEl;
|
|
2532
3345
|
if (!el) return;
|
|
@@ -2541,22 +3354,46 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2541
3354
|
connectedCallback() {
|
|
2542
3355
|
super.connectedCallback();
|
|
2543
3356
|
this.wasRunning = isRunning();
|
|
3357
|
+
const query = readLauncherQuery();
|
|
3358
|
+
if (query !== null) pageUseSettings.update({ launcherVisible: query === "1" || query === "true" });
|
|
2544
3359
|
this.cleanups = [
|
|
2545
3360
|
pageUseStore.subscribe((state) => this.onStateChange(state)),
|
|
2546
3361
|
pageUseSettings.store.subscribe(() => this.requestUpdate()),
|
|
2547
3362
|
pageUseHistory.store.subscribe(() => this.requestUpdate()),
|
|
3363
|
+
pageUsePresets.store.subscribe(() => this.requestUpdate()),
|
|
3364
|
+
narrationMute.subscribe(() => this.requestUpdate()),
|
|
3365
|
+
previewStore.subscribe(() => this.requestUpdate()),
|
|
3366
|
+
registerLauncherOpener(() => void this.expand()),
|
|
2548
3367
|
registerQuestionUi(),
|
|
2549
3368
|
this.watchVoices()
|
|
2550
3369
|
];
|
|
3370
|
+
this.syncPanelName();
|
|
2551
3371
|
window.addEventListener("resize", this.onResize);
|
|
2552
3372
|
}
|
|
2553
3373
|
disconnectedCallback() {
|
|
2554
3374
|
super.disconnectedCallback();
|
|
2555
3375
|
this.cleanups.forEach((off) => off());
|
|
2556
3376
|
this.cleanups = [];
|
|
3377
|
+
this.unregisterPanelName?.();
|
|
3378
|
+
this.unregisterPanelName = null;
|
|
3379
|
+
stopPreview();
|
|
2557
3380
|
window.removeEventListener("resize", this.onResize);
|
|
3381
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3382
|
+
this.toastTimer = null;
|
|
3383
|
+
this.toastText = "";
|
|
3384
|
+
}
|
|
3385
|
+
/** 可选播报引擎:宿主注入的引擎 + 浏览器引擎;尚未 createPageUse 时只有浏览器引擎 */
|
|
3386
|
+
ttsEngines() {
|
|
3387
|
+
return getPageUseConfig()?.ttsEngines ?? listTtsEngines(void 0);
|
|
3388
|
+
}
|
|
3389
|
+
/** 当前生效的播报引擎,与 narration 使用同一解析规则 */
|
|
3390
|
+
ttsEngine() {
|
|
3391
|
+
return resolveTtsEngine(this.ttsEngines(), pageUseSettings.get().voiceEngineId);
|
|
2558
3392
|
}
|
|
2559
|
-
/**
|
|
3393
|
+
/**
|
|
3394
|
+
* Chrome 的语音列表异步加载,首次读取可能为空:加载完成后刷新语音下拉框。
|
|
3395
|
+
* 挂载时宿主可能尚未注入引擎,因此不按引擎判断;注入引擎时该事件只触发一次无害的重绘。
|
|
3396
|
+
*/
|
|
2560
3397
|
watchVoices() {
|
|
2561
3398
|
if (!isSpeechSupported()) return () => {};
|
|
2562
3399
|
const synth = globalThis.speechSynthesis;
|
|
@@ -2564,11 +3401,55 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2564
3401
|
synth.addEventListener?.("voiceschanged", onChange);
|
|
2565
3402
|
return () => synth.removeEventListener?.("voiceschanged", onChange);
|
|
2566
3403
|
}
|
|
3404
|
+
/**
|
|
3405
|
+
* 入口文案在「执行中」与标题之间切换时宽度会变化;CSS 无法对 width:auto 过渡,
|
|
3406
|
+
* 因此在渲染新文案前记录旧宽度,渲染后由 animateFabWidth 从旧宽度过渡到新宽度。
|
|
3407
|
+
*/
|
|
3408
|
+
willUpdate() {
|
|
3409
|
+
const running = isRunning(pageUseStore.get());
|
|
3410
|
+
if (running === this.fabShowsRunning) return;
|
|
3411
|
+
this.fabShowsRunning = running;
|
|
3412
|
+
const width = this.open ? 0 : this.fabEl?.offsetWidth ?? 0;
|
|
3413
|
+
this.fabFromWidth = width > 0 ? width : null;
|
|
3414
|
+
}
|
|
3415
|
+
/** 从记录的旧宽度过渡到新文案的自然宽度,结束后恢复自动宽度并重新测量入口尺寸 */
|
|
3416
|
+
animateFabWidth() {
|
|
3417
|
+
const from = this.fabFromWidth;
|
|
3418
|
+
this.fabFromWidth = null;
|
|
3419
|
+
const el = this.fabEl;
|
|
3420
|
+
if (from === null || !el) return;
|
|
3421
|
+
this.fabWidthCleanup?.();
|
|
3422
|
+
this.fabWidthCleanup = null;
|
|
3423
|
+
el.style.width = "";
|
|
3424
|
+
const to = el.offsetWidth;
|
|
3425
|
+
if (!to || to === from || parseFloat(getComputedStyle(el).transitionDuration) === 0) {
|
|
3426
|
+
this.measureFab();
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
el.style.width = `${from}px`;
|
|
3430
|
+
el.offsetWidth;
|
|
3431
|
+
el.style.width = `${to}px`;
|
|
3432
|
+
const finish = () => {
|
|
3433
|
+
el.removeEventListener("transitionend", onEnd);
|
|
3434
|
+
el.removeEventListener("transitioncancel", onEnd);
|
|
3435
|
+
this.fabWidthCleanup = null;
|
|
3436
|
+
};
|
|
3437
|
+
const onEnd = (event) => {
|
|
3438
|
+
if (event.propertyName !== "width") return;
|
|
3439
|
+
finish();
|
|
3440
|
+
el.style.width = "";
|
|
3441
|
+
this.measureFab();
|
|
3442
|
+
};
|
|
3443
|
+
el.addEventListener("transitionend", onEnd);
|
|
3444
|
+
el.addEventListener("transitioncancel", onEnd);
|
|
3445
|
+
this.fabWidthCleanup = finish;
|
|
3446
|
+
}
|
|
2567
3447
|
firstUpdated() {
|
|
2568
3448
|
this.measureFab();
|
|
2569
3449
|
}
|
|
2570
3450
|
updated(_changed) {
|
|
2571
3451
|
this.autosizeTextarea();
|
|
3452
|
+
this.animateFabWidth();
|
|
2572
3453
|
const state = pageUseStore.get();
|
|
2573
3454
|
const signature = [
|
|
2574
3455
|
state.currentTask,
|
|
@@ -2656,7 +3537,61 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2656
3537
|
style.height = `min(${PANEL_HEIGHT}px, 72vh, ${Math.max(240, vh - offsetY - 8)}px)`;
|
|
2657
3538
|
return toStyle(style);
|
|
2658
3539
|
}
|
|
3540
|
+
/** 调试入口是否显示:持久化状态优先,未操作过时跟随宿主 defaultHidden */
|
|
3541
|
+
isVisible(settings) {
|
|
3542
|
+
return settings.launcherVisible ?? !this._config.defaultHidden;
|
|
3543
|
+
}
|
|
3544
|
+
/** 展开面板(showPageUseLauncher 调用):等入口按新的显示状态渲染后再测量定位 */
|
|
3545
|
+
async expand() {
|
|
3546
|
+
await this.updateComplete;
|
|
3547
|
+
this.measureFab();
|
|
3548
|
+
this.resolvePanelAnchor();
|
|
3549
|
+
this.open = true;
|
|
3550
|
+
this.requestUpdate();
|
|
3551
|
+
}
|
|
3552
|
+
/** 关闭调试入口:收起面板并持久化隐藏状态,之后需调用显示方法或 URL 参数调出 */
|
|
3553
|
+
close() {
|
|
3554
|
+
stopPreview();
|
|
3555
|
+
this.open = false;
|
|
3556
|
+
this.view = "main";
|
|
3557
|
+
this.presetDraft = null;
|
|
3558
|
+
hidePageUseLauncher();
|
|
3559
|
+
}
|
|
3560
|
+
/** 标题点击计数:滑动窗口内达到次数时打开元信息面板并重新计数 */
|
|
3561
|
+
onTitleTap() {
|
|
3562
|
+
const now = Date.now();
|
|
3563
|
+
this.titleTaps = this.titleTaps.filter((time) => now - time < META_UNLOCK_WINDOW_MS);
|
|
3564
|
+
this.titleTaps.push(now);
|
|
3565
|
+
if (this.titleTaps.length < META_UNLOCK_CLICKS) return;
|
|
3566
|
+
this.titleTaps = [];
|
|
3567
|
+
this.view = "meta";
|
|
3568
|
+
this.requestUpdate();
|
|
3569
|
+
}
|
|
3570
|
+
/** 切换面板视图:离开设置页时中止音色试听,离开快捷指令编辑时丢弃未保存的草稿 */
|
|
3571
|
+
switchView(next) {
|
|
3572
|
+
if (this.view === "settings" && next !== "settings") stopPreview();
|
|
3573
|
+
if (this.view === "presets" && next !== "presets") this.presetDraft = null;
|
|
3574
|
+
this.view = next;
|
|
3575
|
+
this.requestUpdate();
|
|
3576
|
+
}
|
|
3577
|
+
/** 在面板内显示轻提示,TOAST_DURATION_MS 后自动消失;连续触发时以最后一次为准并重新计时 */
|
|
3578
|
+
showToast(text) {
|
|
3579
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3580
|
+
this.toastText = text;
|
|
3581
|
+
this.toastTimer = setTimeout(() => {
|
|
3582
|
+
this.toastText = "";
|
|
3583
|
+
this.toastTimer = null;
|
|
3584
|
+
this.requestUpdate();
|
|
3585
|
+
}, TOAST_DURATION_MS);
|
|
3586
|
+
this.requestUpdate();
|
|
3587
|
+
}
|
|
3588
|
+
clearHistory() {
|
|
3589
|
+
const count = pageUseHistory.entries().length;
|
|
3590
|
+
pageUseHistory.clear();
|
|
3591
|
+
this.showToast(count ? `已清空 ${count} 条历史指令` : "暂无历史指令");
|
|
3592
|
+
}
|
|
2659
3593
|
async collapse() {
|
|
3594
|
+
stopPreview();
|
|
2660
3595
|
this.open = false;
|
|
2661
3596
|
this.requestUpdate();
|
|
2662
3597
|
await this.updateComplete;
|
|
@@ -2745,23 +3680,15 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2745
3680
|
...this._config.sourceLabels
|
|
2746
3681
|
}[source] || source;
|
|
2747
3682
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
endpointLabel() {
|
|
2758
|
-
const baseURL = getPageUseConfig()?.llm.baseURL;
|
|
2759
|
-
if (!baseURL) return "未配置";
|
|
2760
|
-
try {
|
|
2761
|
-
return new URL(baseURL).host;
|
|
2762
|
-
} catch {
|
|
2763
|
-
return baseURL;
|
|
2764
|
-
}
|
|
3683
|
+
/** 快捷指令分组:来源指令排除用户隐藏项,外加用户自定义指令;展示区与编辑视图共用 */
|
|
3684
|
+
presetGroups() {
|
|
3685
|
+
return resolvePresetGroups({
|
|
3686
|
+
menus: getPageUseConfig()?.getMenus(),
|
|
3687
|
+
menuPresetLimit: this._config.menuPresetLimit ?? 6,
|
|
3688
|
+
presets: this._config.presets,
|
|
3689
|
+
scenarios: this._config.scenarios,
|
|
3690
|
+
state: pageUsePresets.get()
|
|
3691
|
+
});
|
|
2765
3692
|
}
|
|
2766
3693
|
renderChips(presets, accent = false) {
|
|
2767
3694
|
return presets.map((preset) => html`<button
|
|
@@ -2773,18 +3700,31 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2773
3700
|
${preset.label}
|
|
2774
3701
|
</button>`);
|
|
2775
3702
|
}
|
|
3703
|
+
/** 分组顺序:我的 → 常用 → 复杂场景;空分组不显示,全部为空时提示去设置中维护 */
|
|
2776
3704
|
renderPresetGroups() {
|
|
2777
|
-
const scenarios = this.
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
3705
|
+
const { mine, common, scenarios } = this.presetGroups();
|
|
3706
|
+
const visible = [
|
|
3707
|
+
[
|
|
3708
|
+
"我的",
|
|
3709
|
+
mine,
|
|
3710
|
+
false
|
|
3711
|
+
],
|
|
3712
|
+
[
|
|
3713
|
+
"常用",
|
|
3714
|
+
common,
|
|
3715
|
+
false
|
|
3716
|
+
],
|
|
3717
|
+
[
|
|
3718
|
+
"复杂场景",
|
|
3719
|
+
scenarios,
|
|
3720
|
+
true
|
|
3721
|
+
]
|
|
3722
|
+
].filter(([, presets]) => presets.length);
|
|
3723
|
+
if (!visible.length) return html`<span class="hint">暂无快捷指令,可在设置中维护</span>`;
|
|
3724
|
+
return visible.map(([caption, presets, accent]) => html`<div class="chips">
|
|
3725
|
+
<span class="chips-caption">${caption}</span>
|
|
3726
|
+
${this.renderChips(presets, accent)}
|
|
3727
|
+
</div>`);
|
|
2788
3728
|
}
|
|
2789
3729
|
renderCheckbox(key, label, hint, disabled = false) {
|
|
2790
3730
|
const settings = pageUseSettings.get();
|
|
@@ -2801,33 +3741,220 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2801
3741
|
renderSettings() {
|
|
2802
3742
|
const settings = pageUseSettings.get();
|
|
2803
3743
|
const config = getPageUseConfig();
|
|
3744
|
+
const status = pageUseStore.get().status;
|
|
2804
3745
|
return html`<div class="fill">
|
|
2805
3746
|
<div class="settings-body">
|
|
3747
|
+
<div class="settings-status">当前状态:${STATUS_LABELS[status] || status}</div>
|
|
2806
3748
|
${this.renderCheckbox("showHighlights", "显示元素编号高亮", "运行时页面上带数字的彩色框")}
|
|
2807
3749
|
${this.renderCheckbox("enableMask", "运行时蒙层防误触", "下次任务开始生效")}
|
|
2808
3750
|
${config?.beforeTask ? this.renderCheckbox("enableBeforeTask", config.beforeTaskLabel) : nothing}
|
|
2809
3751
|
${this.renderCheckbox("showStepDetails", "步骤显示模型思考与返回")}
|
|
2810
3752
|
${this.renderVoiceSettings(settings)}
|
|
2811
|
-
<div class="row">
|
|
3753
|
+
<div class="row settings-actions">
|
|
2812
3754
|
<button type="button" class="btn" @click=${() => pageUseSettings.resetPositions()}>
|
|
2813
3755
|
复位窗口
|
|
2814
3756
|
</button>
|
|
2815
|
-
<button type="button" class="btn" @click=${() =>
|
|
3757
|
+
<button type="button" class="btn clear-history" @click=${() => this.clearHistory()}>
|
|
3758
|
+
清空历史
|
|
3759
|
+
</button>
|
|
3760
|
+
<button type="button" class="btn preset-entry" @click=${() => this.switchView("presets")}>
|
|
3761
|
+
快捷指令设置
|
|
3762
|
+
</button>
|
|
2816
3763
|
</div>
|
|
2817
|
-
<span class="meta">端点 ${this.endpointLabel()} · 模型 ${config?.llm.model ?? "-"}</span>
|
|
2818
3764
|
</div>
|
|
2819
3765
|
</div>`;
|
|
2820
3766
|
}
|
|
2821
|
-
/**
|
|
3767
|
+
/** 打开表单:id 为 null 时新增,否则编辑该条;替换当前未保存的草稿 */
|
|
3768
|
+
editPreset(id) {
|
|
3769
|
+
const item = id === null ? null : pageUsePresets.get().custom.find((preset) => preset.id === id);
|
|
3770
|
+
this.presetDraft = {
|
|
3771
|
+
id,
|
|
3772
|
+
label: item?.label ?? "",
|
|
3773
|
+
task: item?.task ?? "",
|
|
3774
|
+
error: ""
|
|
3775
|
+
};
|
|
3776
|
+
this.requestUpdate();
|
|
3777
|
+
}
|
|
3778
|
+
savePreset() {
|
|
3779
|
+
const draft = this.presetDraft;
|
|
3780
|
+
if (!draft) return;
|
|
3781
|
+
const preset = {
|
|
3782
|
+
label: draft.label,
|
|
3783
|
+
task: draft.task
|
|
3784
|
+
};
|
|
3785
|
+
const result = draft.id === null ? pageUsePresets.add(preset) : pageUsePresets.update(draft.id, preset);
|
|
3786
|
+
this.presetDraft = result.ok ? null : {
|
|
3787
|
+
...draft,
|
|
3788
|
+
error: PRESET_SAVE_ERRORS[result.reason]
|
|
3789
|
+
};
|
|
3790
|
+
this.requestUpdate();
|
|
3791
|
+
}
|
|
3792
|
+
renderPresetForm(draft) {
|
|
3793
|
+
return html`<div class="preset-form">
|
|
3794
|
+
<input
|
|
3795
|
+
class="input preset-label-input"
|
|
3796
|
+
placeholder="名称(按钮上显示的文字)"
|
|
3797
|
+
.value=${draft.label}
|
|
3798
|
+
@input=${(e) => {
|
|
3799
|
+
draft.label = e.target.value;
|
|
3800
|
+
}}
|
|
3801
|
+
/>
|
|
3802
|
+
<textarea
|
|
3803
|
+
class="textarea preset-task-input"
|
|
3804
|
+
rows="3"
|
|
3805
|
+
placeholder="指令内容(点击后下达的完整指令)"
|
|
3806
|
+
.value=${draft.task}
|
|
3807
|
+
@input=${(e) => {
|
|
3808
|
+
draft.task = e.target.value;
|
|
3809
|
+
}}
|
|
3810
|
+
></textarea>
|
|
3811
|
+
${draft.error ? html`<div class="alert error preset-error">${draft.error}</div>` : nothing}
|
|
3812
|
+
<div class="row preset-form-actions">
|
|
3813
|
+
<button
|
|
3814
|
+
type="button"
|
|
3815
|
+
class="btn tiny"
|
|
3816
|
+
@click=${() => {
|
|
3817
|
+
this.presetDraft = null;
|
|
3818
|
+
this.requestUpdate();
|
|
3819
|
+
}}
|
|
3820
|
+
>
|
|
3821
|
+
取消
|
|
3822
|
+
</button>
|
|
3823
|
+
<button type="button" class="btn tiny primary preset-save" @click=${() => this.savePreset()}>
|
|
3824
|
+
保存
|
|
3825
|
+
</button>
|
|
3826
|
+
</div>
|
|
3827
|
+
</div>`;
|
|
3828
|
+
}
|
|
3829
|
+
/** 列表项:名称一行,指令内容灰色单行省略(悬停查看全文),右侧为操作按钮 */
|
|
3830
|
+
renderPresetItem(preset, actions) {
|
|
3831
|
+
return html`<div class="preset-item">
|
|
3832
|
+
<div class="preset-text">
|
|
3833
|
+
<span class="preset-label">${preset.label}</span>
|
|
3834
|
+
<span class="preset-task" title=${preset.task}>${preset.task}</span>
|
|
3835
|
+
</div>
|
|
3836
|
+
${actions}
|
|
3837
|
+
</div>`;
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* 快捷指令编辑:我的指令(新增 / 编辑 / 删除)、按来源分组的来源指令(删除即隐藏)、
|
|
3841
|
+
* 已隐藏条数与恢复按钮。任务执行中同样可编辑,只影响后续展示。
|
|
3842
|
+
*/
|
|
3843
|
+
renderPresetEditor() {
|
|
3844
|
+
const { mine, sources, hiddenCount } = this.presetGroups();
|
|
3845
|
+
const draft = this.presetDraft;
|
|
3846
|
+
const removeButton = (onClick) => html`<button
|
|
3847
|
+
type="button"
|
|
3848
|
+
class="btn ghost tiny circle preset-remove"
|
|
3849
|
+
title="删除"
|
|
3850
|
+
@click=${onClick}
|
|
3851
|
+
>
|
|
3852
|
+
×
|
|
3853
|
+
</button>`;
|
|
3854
|
+
const sourceGroups = [
|
|
3855
|
+
["菜单页面", sources.menu],
|
|
3856
|
+
["通用示例", sources.builtin],
|
|
3857
|
+
["站点常用", sources.host],
|
|
3858
|
+
["复杂场景", sources.scenarios]
|
|
3859
|
+
];
|
|
3860
|
+
return html`<div class="fill preset-scroll">
|
|
3861
|
+
<div class="settings-body preset-editor">
|
|
3862
|
+
<div class="preset-group-head">
|
|
3863
|
+
<span class="field-label">我的指令</span>
|
|
3864
|
+
<button
|
|
3865
|
+
type="button"
|
|
3866
|
+
class="btn tiny preset-add"
|
|
3867
|
+
?disabled=${draft?.id === null}
|
|
3868
|
+
@click=${() => this.editPreset(null)}
|
|
3869
|
+
>
|
|
3870
|
+
+ 新增
|
|
3871
|
+
</button>
|
|
3872
|
+
</div>
|
|
3873
|
+
<div class="preset-list preset-mine">
|
|
3874
|
+
${draft?.id === null ? this.renderPresetForm(draft) : nothing}
|
|
3875
|
+
${mine.map((item) => draft?.id === item.id ? this.renderPresetForm(draft) : this.renderPresetItem(item, html`<button
|
|
3876
|
+
type="button"
|
|
3877
|
+
class="btn ghost tiny preset-edit"
|
|
3878
|
+
@click=${() => this.editPreset(item.id)}
|
|
3879
|
+
>
|
|
3880
|
+
编辑
|
|
3881
|
+
</button>
|
|
3882
|
+
${removeButton(() => {
|
|
3883
|
+
pageUsePresets.remove(item.id);
|
|
3884
|
+
})}`))}
|
|
3885
|
+
${!mine.length && !draft ? html`<span class="hint">暂无自定义指令</span>` : nothing}
|
|
3886
|
+
</div>
|
|
3887
|
+
${sourceGroups.filter(([, list]) => list.length).map(([caption, list]) => html`<div class="field-label preset-group-head">${caption}</div>
|
|
3888
|
+
<div class="preset-list preset-source">
|
|
3889
|
+
${list.map((preset) => this.renderPresetItem(preset, removeButton(() => pageUsePresets.hide(preset.task))))}
|
|
3890
|
+
</div>`)}
|
|
3891
|
+
<div class="row preset-restore">
|
|
3892
|
+
<span class="hint preset-hidden-count">已隐藏 ${hiddenCount} 条</span>
|
|
3893
|
+
<button
|
|
3894
|
+
type="button"
|
|
3895
|
+
class="btn tiny push-right preset-restore-btn"
|
|
3896
|
+
?disabled=${hiddenCount === 0}
|
|
3897
|
+
@click=${() => pageUsePresets.restoreHidden()}
|
|
3898
|
+
>
|
|
3899
|
+
恢复默认指令
|
|
3900
|
+
</button>
|
|
3901
|
+
</div>
|
|
3902
|
+
</div>
|
|
3903
|
+
</div>`;
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* 演示播报:总开关关闭时只显示开关;开启后依次为引擎下拉(可选引擎多于一个时)、
|
|
3907
|
+
* 语速、音色下拉与试听按钮(无音色列表时只有试听按钮)、不可用 / 无中文语音提示。
|
|
3908
|
+
*/
|
|
2822
3909
|
renderVoiceSettings(settings) {
|
|
2823
|
-
const
|
|
2824
|
-
|
|
2825
|
-
const
|
|
3910
|
+
const toggle = this.renderCheckbox("voiceEnabled", "语音播报", "演示时播报执行步骤、提问与结论");
|
|
3911
|
+
if (!settings.voiceEnabled) return toggle;
|
|
3912
|
+
const engines = this.ttsEngines();
|
|
3913
|
+
const engine = this.ttsEngine();
|
|
3914
|
+
const isBrowser = engine.id === BROWSER_ENGINE_ID;
|
|
3915
|
+
const available = engine.isAvailable();
|
|
3916
|
+
const voices = available ? engine.listVoices?.() ?? [] : [];
|
|
3917
|
+
const voiceName = settings.voiceNames[engine.id] ?? "";
|
|
3918
|
+
const { playing } = previewStore.get();
|
|
3919
|
+
const running = isRunning(pageUseStore.get());
|
|
3920
|
+
const previewButton = html`<button
|
|
3921
|
+
type="button"
|
|
3922
|
+
class="btn preview-btn"
|
|
3923
|
+
?disabled=${!available || running}
|
|
3924
|
+
title=${running ? "任务执行中不可试听" : "按当前设置试听"}
|
|
3925
|
+
@click=${() => playing ? stopPreview() : startPreview(engine, {
|
|
3926
|
+
rate: settings.voiceRate,
|
|
3927
|
+
voice: voiceName
|
|
3928
|
+
})}
|
|
3929
|
+
>
|
|
3930
|
+
${playing ? "停止" : "试听"}
|
|
3931
|
+
</button>`;
|
|
3932
|
+
const warning = !available ? isBrowser ? "当前浏览器不支持语音合成,不会播报" : `当前环境不支持${engine.label},不会播报` : isBrowser && !voices.length ? "未检测到中文语音,将使用浏览器默认语音播报" : "";
|
|
2826
3933
|
return html`
|
|
2827
|
-
${
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3934
|
+
${toggle}
|
|
3935
|
+
<div class="voice-options">
|
|
3936
|
+
${engines.length > 1 ? html`<label class="field">
|
|
3937
|
+
<span class="field-label">播报引擎</span>
|
|
3938
|
+
<select
|
|
3939
|
+
class="input voice-engine-select"
|
|
3940
|
+
@change=${(e) => {
|
|
3941
|
+
stopPreview();
|
|
3942
|
+
pageUseSettings.update({ voiceEngineId: e.target.value });
|
|
3943
|
+
}}
|
|
3944
|
+
>
|
|
3945
|
+
${engines.map((item) => {
|
|
3946
|
+
const usable = item.isAvailable();
|
|
3947
|
+
return html`<option
|
|
3948
|
+
value=${item.id}
|
|
3949
|
+
?selected=${item.id === engine.id}
|
|
3950
|
+
?disabled=${!usable}
|
|
3951
|
+
>
|
|
3952
|
+
${item.label}${usable ? "" : "(不可用)"}
|
|
3953
|
+
</option>`;
|
|
3954
|
+
})}
|
|
3955
|
+
</select>
|
|
3956
|
+
</label>` : nothing}
|
|
3957
|
+
${available ? html`<label class="field">
|
|
2831
3958
|
<span class="field-label">语速 ${settings.voiceRate.toFixed(2)}</span>
|
|
2832
3959
|
<input
|
|
2833
3960
|
class="range"
|
|
@@ -2838,21 +3965,60 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2838
3965
|
.value=${String(settings.voiceRate)}
|
|
2839
3966
|
@input=${(e) => pageUseSettings.update({ voiceRate: Number(e.target.value) })}
|
|
2840
3967
|
/>
|
|
2841
|
-
</label
|
|
2842
|
-
|
|
2843
|
-
<span class="field-label"
|
|
2844
|
-
<
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3968
|
+
</label>` : nothing}
|
|
3969
|
+
${voices.length ? html`<div class="field">
|
|
3970
|
+
<span class="field-label">音色</span>
|
|
3971
|
+
<div class="voice-name-row">
|
|
3972
|
+
<select
|
|
3973
|
+
class="input voice-name-select"
|
|
3974
|
+
title=${voices.find((voice) => voice.value === voiceName)?.label ?? ""}
|
|
3975
|
+
@change=${(e) => {
|
|
3976
|
+
stopPreview();
|
|
3977
|
+
pageUseSettings.update({ voiceNames: {
|
|
3978
|
+
...pageUseSettings.get().voiceNames,
|
|
3979
|
+
[engine.id]: e.target.value
|
|
3980
|
+
} });
|
|
3981
|
+
}}
|
|
3982
|
+
>
|
|
3983
|
+
<option value="" ?selected=${!voiceName}>
|
|
3984
|
+
${isBrowser ? "自动(第一个中文语音)" : "自动"}
|
|
3985
|
+
</option>
|
|
3986
|
+
${voices.map((voice) => html`<option value=${voice.value} ?selected=${voice.value === voiceName}>
|
|
3987
|
+
${voice.label}
|
|
3988
|
+
</option>`)}
|
|
3989
|
+
</select>
|
|
3990
|
+
${previewButton}
|
|
3991
|
+
</div>
|
|
3992
|
+
</div>` : html`<div class="voice-name-row">${previewButton}</div>`}
|
|
3993
|
+
${warning ? html`<div class="alert warning voice-warning">${warning}</div>` : nothing}
|
|
3994
|
+
</div>
|
|
2855
3995
|
`;
|
|
3996
|
+
}
|
|
3997
|
+
/** 元信息:与业务无关的配置与运行环境信息,仅供开发排查;API Key 只显示是否已配置 */
|
|
3998
|
+
renderMeta() {
|
|
3999
|
+
const config = getPageUseConfig();
|
|
4000
|
+
const settings = pageUseSettings.get();
|
|
4001
|
+
const engine = this.ttsEngine();
|
|
4002
|
+
const voiceName = settings.voiceNames[engine.id] || "自动";
|
|
4003
|
+
const rows = [
|
|
4004
|
+
["版本", `v${version}`],
|
|
4005
|
+
["端点", config?.llm.baseURL || "未配置"],
|
|
4006
|
+
["模型", config?.llm.model || "未配置"],
|
|
4007
|
+
["API Key", config?.llm.apiKey ? "已配置" : "未配置"],
|
|
4008
|
+
["存储键前缀", config?.storageKeyPrefix || "未配置"],
|
|
4009
|
+
["当前引擎", `${engine.label}(${engine.id}) · 音色 ${voiceName}`]
|
|
4010
|
+
];
|
|
4011
|
+
return html`<div class="fill">
|
|
4012
|
+
<div class="settings-body">
|
|
4013
|
+
<dl class="meta-list">
|
|
4014
|
+
${rows.map(([label, value]) => html`<dt>${label}</dt><dd>${value}</dd>`)}
|
|
4015
|
+
</dl>
|
|
4016
|
+
<div class="field-label">可选引擎</div>
|
|
4017
|
+
<ul class="meta-engines">
|
|
4018
|
+
${this.ttsEngines().map((item) => html`<li>${item.id} · ${item.label} · ${item.isAvailable() ? "可用" : "不可用"}</li>`)}
|
|
4019
|
+
</ul>
|
|
4020
|
+
</div>
|
|
4021
|
+
</div>`;
|
|
2856
4022
|
}
|
|
2857
4023
|
renderLog(state, isLogEmpty) {
|
|
2858
4024
|
const running = isRunning(state);
|
|
@@ -3012,9 +4178,11 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
3012
4178
|
render() {
|
|
3013
4179
|
const state = pageUseStore.get();
|
|
3014
4180
|
const settings = pageUseSettings.get();
|
|
4181
|
+
if (!this.isVisible(settings)) return nothing;
|
|
3015
4182
|
const running = isRunning(state);
|
|
3016
4183
|
const title = this._config.title || "Page Use";
|
|
3017
4184
|
const elapsed = getElapsedSeconds(state);
|
|
4185
|
+
const { muted } = narrationMute.get();
|
|
3018
4186
|
const isLogEmpty = !running && !state.currentTask && !state.steps.length && !state.errorMessage;
|
|
3019
4187
|
return html`
|
|
3020
4188
|
<button
|
|
@@ -3022,36 +4190,56 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
3022
4190
|
class="fab ${running ? "running" : ""}"
|
|
3023
4191
|
?hidden=${this.open}
|
|
3024
4192
|
style=${this.fabStyle(settings)}
|
|
3025
|
-
title=${running ? `执行中:${state.activityText || state.currentTask}` : `打开 ${title}
|
|
4193
|
+
title=${running ? `执行中:${state.activityText || state.currentTask}` : `打开 ${title}`}
|
|
3026
4194
|
@pointerdown=${this.fabDrag.onPointerDown}
|
|
3027
4195
|
>
|
|
3028
|
-
<span class="fab-
|
|
4196
|
+
<span class="fab-icon">${robotIcon}</span>
|
|
3029
4197
|
<span>${running ? "执行中" : title}</span>
|
|
3030
4198
|
</button>
|
|
3031
4199
|
|
|
3032
|
-
<section
|
|
4200
|
+
<section
|
|
4201
|
+
class="panel ${running ? "running" : ""}"
|
|
4202
|
+
?hidden=${!this.open}
|
|
4203
|
+
style=${this.open ? this.panelStyle(settings) : ""}
|
|
4204
|
+
>
|
|
3033
4205
|
<header class="head" @pointerdown=${this.panelDrag.onPointerDown}>
|
|
3034
|
-
<span class="dot ${state.status}"></span>
|
|
4206
|
+
<span class="dot ${state.status}" title=${STATUS_LABELS[state.status] || state.status}></span>
|
|
3035
4207
|
<span class="title">${title}</span>
|
|
3036
4208
|
<span class="status">
|
|
3037
|
-
${STATUS_LABELS[state.status] || state.status}${state.steps.length ? ` · ${state.steps.length} 步` : ""}${state.totalTokens ? ` · ${state.totalTokens} tok` : ""}${elapsed ? ` · ${elapsed}s` : ""}
|
|
4209
|
+
${state.status === "idle" ? "" : `${STATUS_LABELS[state.status] || state.status}${state.steps.length ? ` · ${state.steps.length} 步` : ""}${state.totalTokens ? ` · ${formatTokens(state.totalTokens)} tok` : ""}${elapsed ? ` · ${elapsed}s` : ""}`}
|
|
3038
4210
|
</span>
|
|
3039
|
-
<
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
4211
|
+
<div class="head-actions">
|
|
4212
|
+
${running && settings.voiceEnabled ? html`<button
|
|
4213
|
+
type="button"
|
|
4214
|
+
class="icon-btn mute ${muted ? "active" : ""}"
|
|
4215
|
+
title=${muted ? "恢复播报" : "本次任务静音"}
|
|
4216
|
+
@click=${() => narrationMute.set({ muted: !muted })}
|
|
4217
|
+
>
|
|
4218
|
+
${muted ? mutedIcon : volumeIcon}
|
|
4219
|
+
</button>` : nothing}
|
|
4220
|
+
<button
|
|
4221
|
+
type="button"
|
|
4222
|
+
class="icon-btn settings-toggle ${this.view === "settings" ? "active" : ""}"
|
|
4223
|
+
title=${this.view === "main" ? "设置" : "返回"}
|
|
4224
|
+
@click=${() => this.switchView(this.view === "main" ? "settings" : PARENT_VIEW[this.view])}
|
|
4225
|
+
>
|
|
4226
|
+
${this.view === "main" ? settingsIcon : backIcon}
|
|
4227
|
+
</button>
|
|
4228
|
+
<button type="button" class="icon-btn" title="收起" @click=${() => void this.collapse()}>
|
|
4229
|
+
${collapseIcon}
|
|
4230
|
+
</button>
|
|
4231
|
+
<button
|
|
4232
|
+
type="button"
|
|
4233
|
+
class="icon-btn close"
|
|
4234
|
+
title="关闭"
|
|
4235
|
+
@click=${() => this.close()}
|
|
4236
|
+
>
|
|
4237
|
+
${closeIcon}
|
|
4238
|
+
</button>
|
|
4239
|
+
</div>
|
|
3053
4240
|
</header>
|
|
3054
|
-
${this.
|
|
4241
|
+
${this.view === "settings" ? this.renderSettings() : this.view === "presets" ? this.renderPresetEditor() : this.view === "meta" ? this.renderMeta() : html`${this.renderLog(state, isLogEmpty)}${this.renderComposer(state, isLogEmpty)}`}
|
|
4242
|
+
${this.toastText ? html`<div class="toast" role="status">${this.toastText}</div>` : nothing}
|
|
3055
4243
|
</section>
|
|
3056
4244
|
`;
|
|
3057
4245
|
}
|
|
@@ -3094,4 +4282,4 @@ function mountPageUseUI(options = {}) {
|
|
|
3094
4282
|
}
|
|
3095
4283
|
|
|
3096
4284
|
//#endregion
|
|
3097
|
-
export {
|
|
4285
|
+
export { pageUseSettings as A, browserTtsEngine as B, getPageUseConfig as C, pageUsePresets as D, pageUseStore as E, normalizeOptions as F, readLauncherSwitch as I, PAGE_USE_MASK_COLORS as L, DEFAULT_MODEL as M, DEFAULT_SESSION_HEADER as N, pageUseHistory as O, DEFAULT_STORAGE_KEY_PREFIX as P, SIMULATOR_MASK_ID as R, getElapsedSeconds as S, isRunning as T, runPageUseTool as _, PAGE_USE_STATIC_PRESETS as a, pageUseCapability as b, PAGE_USE_GLOW_TAG as c, PAGE_USE_GLOBAL_KEY as d, PAGE_USE_COMMAND_TYPE as f, isPageUseCommand as g, formatPageUseResultForModel as h, PageUseLauncherElement as i, DEFAULT_MAX_STEPS as j, DEFAULT_PAGE_USE_SETTINGS as k, PageUseGlowElement as l, PAGE_USE_TOOL_NAME as m, mountPageUseUI as n, buildMenuPresets as o, PAGE_USE_TOOL as p, PAGE_USE_LAUNCHER_TAG as r, PAGE_USE_ROBOT_ICON_SVG as s, definePageUseElements as t, createPageUse as u, hidePageUseLauncher as v, isPageUseConfigured as w, clearPageUseLog as x, showPageUseLauncher as y, BROWSER_ENGINE_ID as z };
|