@tanzerfe/page-use 0.0.2 → 0.0.5
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 +146 -2
- package/dist/{index-DtJ3mvL2.d.mts → index-D4_hId31.d.mts} +208 -8
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-C8EWDZVz.mjs → src-tOb9oqjb.mjs} +1995 -403
- package/dist/vue.d.mts +2 -2
- package/dist/vue.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,804 @@
|
|
|
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
|
|
73
|
+
//#region src/core/maskTheme.ts
|
|
74
|
+
/**
|
|
75
|
+
* 运行期蒙层与模拟光标的定制主题(覆盖 page-agent SimulatorMask 的默认视觉)。
|
|
76
|
+
*
|
|
77
|
+
* page-agent 的 CSS Module 类名带哈希(如 `_cursor_1dgwb_2`),随版本变化,
|
|
78
|
+
* 因此这里只依赖稳定的结构:
|
|
79
|
+
* - 蒙层根节点 id:`page-agent-runtime_simulator-mask`
|
|
80
|
+
* - 子节点顺序:ai-motion 光晕层在前(初始化失败时不存在),光标节点恒为最后一个子节点
|
|
81
|
+
* - 光标子节点顺序:1 点击波纹 / 2 箭头填充 / 3 箭头描边
|
|
82
|
+
* - 点击态类名包含 `clicking` 子串
|
|
83
|
+
* 升级 page-agent 后若光标样式失效,先核对 SimulatorMask 的 #createCursor 结构。
|
|
84
|
+
*/
|
|
85
|
+
/** 蒙层根节点 id(@page-agent/page-controller 内部常量) */
|
|
86
|
+
const SIMULATOR_MASK_ID = "page-agent-runtime_simulator-mask";
|
|
87
|
+
/** 主题色:主色 #0879ff 同系的蓝 → 青 → 靛渐变 */
|
|
88
|
+
const PAGE_USE_MASK_COLORS = {
|
|
89
|
+
primary: "#0879ff",
|
|
90
|
+
cyan: "#36cfc9",
|
|
91
|
+
indigo: "#597ef7",
|
|
92
|
+
/** 误触拦截时的警示色 */
|
|
93
|
+
warning: "#fa8c16"
|
|
94
|
+
};
|
|
95
|
+
const CURSOR_ARROW_MASK = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='M 15 42 L 15 36.99 Q 15 31.99 23.7 31.99 L 28.05 31.99 Q 32.41 31.99 32.41 21.99 L 32.41 17 Q 32.41 12 41.09 16.95 L 76.31 37.05 Q 85 42 76.31 46.95 L 41.09 67.05 Q 32.41 72 32.41 62.01 L 32.41 57.01 Q 32.41 52.01 23.7 52.01 L 19.35 52.01 Q 15 52.01 15 47.01 Z' fill='#000'/></svg>`)}")`;
|
|
96
|
+
const MASK = `#${SIMULATOR_MASK_ID}`;
|
|
97
|
+
/** 光标节点:蒙层的最后一个子节点 */
|
|
98
|
+
const CURSOR = `${MASK} > :last-child`;
|
|
99
|
+
const { primary, cyan, indigo } = PAGE_USE_MASK_COLORS;
|
|
100
|
+
const PAGE_USE_MASK_THEME_CSS = `
|
|
101
|
+
/* 隐藏 ai-motion 默认光晕(光标之外的子节点),光晕改由 <tz-page-use-glow> 渲染 */
|
|
102
|
+
${MASK} > :not(:last-child) { display: none !important; }
|
|
103
|
+
/* 蒙层本身保持透明,仅负责拦截点击;真实鼠标由 wait 转圈改为禁止符号,明确「此时不可操作」而非「页面卡死」 */
|
|
104
|
+
${MASK} { cursor: not-allowed !important; }
|
|
105
|
+
|
|
106
|
+
/* 光标整体外发光 */
|
|
107
|
+
${CURSOR} {
|
|
108
|
+
filter: drop-shadow(0 0 6px ${primary}8c) drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25));
|
|
109
|
+
}
|
|
110
|
+
/* 光标尖端(节点原点)的呼吸光圈 */
|
|
111
|
+
${CURSOR}::before {
|
|
112
|
+
content: '';
|
|
113
|
+
position: absolute;
|
|
114
|
+
left: -16px;
|
|
115
|
+
top: -16px;
|
|
116
|
+
width: 32px;
|
|
117
|
+
height: 32px;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
background: radial-gradient(circle, ${cyan}66 0%, ${primary}26 55%, transparent 72%);
|
|
120
|
+
animation: page-use-cursor-halo 1.6s ease-in-out infinite;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
}
|
|
123
|
+
/* 箭头描边:主题渐变 */
|
|
124
|
+
${CURSOR} > :nth-child(3) {
|
|
125
|
+
background: linear-gradient(45deg, ${primary}, ${cyan}) !important;
|
|
126
|
+
}
|
|
127
|
+
/* 箭头填充:用同形遮罩替换原白色 SVG 背景,改为浅色渐变 */
|
|
128
|
+
${CURSOR} > :nth-child(2) {
|
|
129
|
+
background: linear-gradient(135deg, #ffffff 30%, #e6f4ff) !important;
|
|
130
|
+
-webkit-mask: ${CURSOR_ARROW_MASK} center / 100% 100% no-repeat;
|
|
131
|
+
mask: ${CURSOR_ARROW_MASK} center / 100% 100% no-repeat;
|
|
132
|
+
}
|
|
133
|
+
/* 点击波纹:外环改主题色 */
|
|
134
|
+
${CURSOR} > :nth-child(1)::after {
|
|
135
|
+
border-color: ${cyan} !important;
|
|
136
|
+
border-width: 3px !important;
|
|
137
|
+
}
|
|
138
|
+
/* 点击波纹:追加一层实心扩散,增强点击反馈 */
|
|
139
|
+
${CURSOR} > :nth-child(1)::before {
|
|
140
|
+
content: '';
|
|
141
|
+
position: absolute;
|
|
142
|
+
inset: 25%;
|
|
143
|
+
border-radius: 50%;
|
|
144
|
+
background: radial-gradient(circle, ${indigo}80, transparent 70%);
|
|
145
|
+
opacity: 0;
|
|
146
|
+
}
|
|
147
|
+
${CURSOR}[class*='clicking'] > :nth-child(1)::before {
|
|
148
|
+
animation: page-use-cursor-press 420ms ease-out forwards;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@keyframes page-use-cursor-halo {
|
|
152
|
+
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
153
|
+
50% { transform: scale(1.25); opacity: 1; }
|
|
154
|
+
}
|
|
155
|
+
@keyframes page-use-cursor-press {
|
|
156
|
+
0% { transform: scale(0.4); opacity: 1; }
|
|
157
|
+
100% { transform: scale(2.4); opacity: 0; }
|
|
158
|
+
}
|
|
159
|
+
@media (prefers-reduced-motion: reduce) {
|
|
160
|
+
${CURSOR}::before { animation: none; }
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
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
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/core/options.ts
|
|
198
|
+
/** 默认模型,仅为示例取值;接入其它端点时应通过 llm.model 显式指定支持 function calling 的模型 */
|
|
199
|
+
const DEFAULT_MODEL = "deepseek-chat";
|
|
200
|
+
/** 单次任务最大步数;每步一次 LLM 调用,防止死循环消耗 token */
|
|
201
|
+
const DEFAULT_MAX_STEPS = 20;
|
|
202
|
+
/** 默认会话头:OpenCode Go 要求每个会话带稳定的 x-opencode-session 用于路由与提示缓存 */
|
|
203
|
+
const DEFAULT_SESSION_HEADER = "x-opencode-session";
|
|
204
|
+
/** localStorage / sessionStorage 键前缀,可通过 storageKeyPrefix 覆盖 */
|
|
205
|
+
const DEFAULT_STORAGE_KEY_PREFIX = "tz-page-use";
|
|
206
|
+
/** 相对路径补齐为当前 origin 的绝对地址并去掉末尾 /;无法解析时返回空串 */
|
|
207
|
+
function toAbsoluteUrl(input) {
|
|
208
|
+
const base = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
209
|
+
try {
|
|
210
|
+
return new URL(input, base).toString().replace(/\/+$/, "");
|
|
211
|
+
} catch {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** 按路由模式从 location 读取当前路径 */
|
|
216
|
+
function readLocationPath(mode) {
|
|
217
|
+
if (typeof window === "undefined") return "/";
|
|
218
|
+
if (mode === "hash") return window.location.hash.replace(/^#/, "") || "/";
|
|
219
|
+
return `${window.location.pathname}${window.location.search}`;
|
|
220
|
+
}
|
|
221
|
+
function resolveSessionHeader(value) {
|
|
222
|
+
if (value === false) return "";
|
|
223
|
+
return (value ?? "").trim() || "x-opencode-session";
|
|
224
|
+
}
|
|
225
|
+
function normalizeOptions(options) {
|
|
226
|
+
const { llm } = options;
|
|
227
|
+
const rawBase = (llm.baseURL || "").trim();
|
|
228
|
+
const routerMode = options.routerMode ?? "hash";
|
|
229
|
+
return {
|
|
230
|
+
llm: {
|
|
231
|
+
baseURL: rawBase ? toAbsoluteUrl(rawBase) : "",
|
|
232
|
+
apiKey: (llm.apiKey || "").trim(),
|
|
233
|
+
model: (llm.model || "").trim() || "deepseek-chat",
|
|
234
|
+
maxSteps: llm.maxSteps && llm.maxSteps > 0 ? llm.maxSteps : 20,
|
|
235
|
+
disableNamedToolChoice: Boolean(llm.disableNamedToolChoice),
|
|
236
|
+
extraBody: llm.extraBody && typeof llm.extraBody === "object" ? llm.extraBody : {},
|
|
237
|
+
sessionHeader: resolveSessionHeader(llm.sessionHeader)
|
|
238
|
+
},
|
|
239
|
+
appName: options.appName,
|
|
240
|
+
getMenus: options.getMenus ?? (() => void 0),
|
|
241
|
+
routerMode,
|
|
242
|
+
getCurrentPath: options.getCurrentPath ?? (() => readLocationPath(routerMode)),
|
|
243
|
+
resolveRouteName: options.resolveRouteName,
|
|
244
|
+
routeHints: options.routeHints ?? {},
|
|
245
|
+
layoutHint: options.layoutHint,
|
|
246
|
+
beforeTask: options.beforeTask,
|
|
247
|
+
beforeTaskLabel: options.beforeTaskLabel || "任务开始前执行宿主预处理",
|
|
248
|
+
storageKeyPrefix: options.storageKeyPrefix || "tz-page-use",
|
|
249
|
+
pageAgentOptions: options.pageAgentOptions ?? {},
|
|
250
|
+
exposeGlobal: options.exposeGlobal !== false,
|
|
251
|
+
ttsEngines: listTtsEngines(options.tts)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** URL 查询参数:?pageUseDebug=1 打开调试面板,?pageUseDebug=0 关闭;结果记入 sessionStorage */
|
|
255
|
+
const LAUNCHER_QUERY_KEY = "pageUseDebug";
|
|
256
|
+
/** 从 location 读取调试开关(hash 路由的查询参数在 # 之后,两处都查) */
|
|
257
|
+
/** 读取 URL 中的 pageUseDebug 参数(search 或 hash 内的查询串);未携带时为 null。供 launcher 判定显示状态 */
|
|
258
|
+
function readLauncherQuery() {
|
|
259
|
+
const fromSearch = new URLSearchParams(window.location.search).get(LAUNCHER_QUERY_KEY);
|
|
260
|
+
if (fromSearch !== null) return fromSearch;
|
|
261
|
+
const hashQuery = window.location.hash.split("?")[1];
|
|
262
|
+
return hashQuery ? new URLSearchParams(hashQuery).get(LAUNCHER_QUERY_KEY) : null;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 调试面板是否显示。优先级:URL 参数(并记入本会话)> 会话记录 > fallback。
|
|
266
|
+
* fallback 由宿主决定(例如「开发环境显示,生产环境隐藏」)。
|
|
267
|
+
*/
|
|
268
|
+
function readLauncherSwitch(fallback, storageKeyPrefix = DEFAULT_STORAGE_KEY_PREFIX) {
|
|
269
|
+
if (typeof window === "undefined") return fallback;
|
|
270
|
+
const sessionKey = `${storageKeyPrefix}-debug`;
|
|
271
|
+
try {
|
|
272
|
+
const query = readLauncherQuery();
|
|
273
|
+
if (query !== null) {
|
|
274
|
+
const enabled = query === "1" || query === "true";
|
|
275
|
+
window.sessionStorage.setItem(sessionKey, enabled ? "1" : "0");
|
|
276
|
+
return enabled;
|
|
277
|
+
}
|
|
278
|
+
const session = window.sessionStorage.getItem(sessionKey);
|
|
279
|
+
if (session !== null) return session === "1";
|
|
280
|
+
} catch {}
|
|
281
|
+
return fallback;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/core/store.ts
|
|
286
|
+
function createStore(initial) {
|
|
287
|
+
let state = initial;
|
|
288
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
289
|
+
const notify = () => {
|
|
290
|
+
for (const listener of listeners) try {
|
|
291
|
+
listener(state);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.warn("[PageUse] store 订阅回调异常", err);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
get: () => state,
|
|
298
|
+
set(patch) {
|
|
299
|
+
const partial = typeof patch === "function" ? patch(state) : patch;
|
|
300
|
+
if (Object.keys(partial).every((key) => Object.is(state[key], partial[key]))) return;
|
|
301
|
+
state = {
|
|
302
|
+
...state,
|
|
303
|
+
...partial
|
|
304
|
+
};
|
|
305
|
+
notify();
|
|
306
|
+
},
|
|
307
|
+
replace(next) {
|
|
308
|
+
if (Object.is(next, state)) return;
|
|
309
|
+
state = next;
|
|
310
|
+
notify();
|
|
311
|
+
},
|
|
312
|
+
subscribe(listener) {
|
|
313
|
+
listeners.add(listener);
|
|
314
|
+
return () => {
|
|
315
|
+
listeners.delete(listener);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/core/settings.ts
|
|
323
|
+
/**
|
|
324
|
+
* Page Use 本地设置(localStorage 持久化,单例)。
|
|
325
|
+
* 含 page-agent 内置面板/元素编号高亮开关、蒙层与光晕、演示播报、调试入口拖拽位置与显示状态。
|
|
326
|
+
* 存储键为 `${storageKeyPrefix}-settings`。
|
|
327
|
+
*/
|
|
328
|
+
/** page-agent 元素高亮容器 id(page-controller 内部常量,随版本可能变化) */
|
|
329
|
+
const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
|
|
330
|
+
const HIGHLIGHT_STYLE_ID = "page-use-hide-highlights";
|
|
331
|
+
/** page-agent 内置面板根节点 id(@page-agent/ui 内部常量) */
|
|
332
|
+
const BUILTIN_PANEL_ID = "page-agent-runtime_agent-panel";
|
|
333
|
+
const BUILTIN_PANEL_STYLE_ID = "page-use-hide-builtin-panel";
|
|
334
|
+
const MASK_THEME_STYLE_ID = "page-use-mask-theme";
|
|
335
|
+
const DEFAULT_PAGE_USE_SETTINGS = {
|
|
336
|
+
showBuiltinPanel: false,
|
|
337
|
+
showHighlights: false,
|
|
338
|
+
showStepDetails: true,
|
|
339
|
+
enableBeforeTask: true,
|
|
340
|
+
enableMask: true,
|
|
341
|
+
customMaskEffect: true,
|
|
342
|
+
voiceEnabled: false,
|
|
343
|
+
voiceRate: 1.1,
|
|
344
|
+
voiceEngineId: "",
|
|
345
|
+
voiceNames: {},
|
|
346
|
+
launcherVisible: null,
|
|
347
|
+
fabPos: null
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* 调试面板不再提供开关的设置项及其固定值。
|
|
351
|
+
* 复选框已移除,本地存储中曾经改过的值无法在界面上改回,因此读取与更新时一律覆盖为固定值;
|
|
352
|
+
* PageUseSettings 保留这两个字段,避免破坏调用方的类型。
|
|
353
|
+
*/
|
|
354
|
+
const LOCKED_SETTINGS = {
|
|
355
|
+
showBuiltinPanel: false,
|
|
356
|
+
customMaskEffect: true
|
|
357
|
+
};
|
|
358
|
+
function storageKey$2(prefix) {
|
|
359
|
+
return `${prefix}-settings`;
|
|
360
|
+
}
|
|
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) {
|
|
371
|
+
try {
|
|
372
|
+
const raw = window.localStorage.getItem(storageKey$2(prefix));
|
|
373
|
+
if (!raw) return { ...DEFAULT_PAGE_USE_SETTINGS };
|
|
374
|
+
return {
|
|
375
|
+
...DEFAULT_PAGE_USE_SETTINGS,
|
|
376
|
+
...migrate(JSON.parse(raw)),
|
|
377
|
+
...LOCKED_SETTINGS
|
|
378
|
+
};
|
|
379
|
+
} catch {
|
|
380
|
+
return { ...DEFAULT_PAGE_USE_SETTINGS };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function save(prefix, settings) {
|
|
384
|
+
try {
|
|
385
|
+
window.localStorage.setItem(storageKey$2(prefix), JSON.stringify(settings));
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
/** 按开关注入/移除一段覆盖样式(用于压制 page-agent 自带的 DOM,这些节点位于宿主 document 中) */
|
|
389
|
+
function toggleOverrideStyle(styleId, css, show) {
|
|
390
|
+
const existing = document.getElementById(styleId);
|
|
391
|
+
if (show) {
|
|
392
|
+
existing?.remove();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (existing) return;
|
|
396
|
+
const style = document.createElement("style");
|
|
397
|
+
style.id = styleId;
|
|
398
|
+
style.textContent = css;
|
|
399
|
+
document.head.appendChild(style);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 把设置同步到页面:
|
|
403
|
+
* - 高亮:page-controller 的 doHighlightElements 内部写死为 true,只能用样式覆盖其高亮容器
|
|
404
|
+
* - 内置面板:Panel 在 statuschange 到 running 时会自行 show(),因此用 !important 样式压制
|
|
405
|
+
* - 蒙层主题:注入覆盖样式;关闭时移除,恢复 page-agent 默认光晕与光标
|
|
406
|
+
*/
|
|
407
|
+
function applyToDocument(settings, prev) {
|
|
408
|
+
if (typeof document === "undefined") return;
|
|
409
|
+
if (!prev || prev.showHighlights !== settings.showHighlights) toggleOverrideStyle(HIGHLIGHT_STYLE_ID, `#${HIGHLIGHT_CONTAINER_ID}{display:none !important;}`, settings.showHighlights);
|
|
410
|
+
if (!prev || prev.showBuiltinPanel !== settings.showBuiltinPanel) toggleOverrideStyle(BUILTIN_PANEL_STYLE_ID, `#${BUILTIN_PANEL_ID}{display:none !important;}`, settings.showBuiltinPanel);
|
|
411
|
+
if (!prev || prev.customMaskEffect !== settings.customMaskEffect) toggleOverrideStyle(MASK_THEME_STYLE_ID, PAGE_USE_MASK_THEME_CSS, !settings.customMaskEffect);
|
|
412
|
+
}
|
|
413
|
+
let prefix$2 = DEFAULT_STORAGE_KEY_PREFIX;
|
|
414
|
+
let store$4 = null;
|
|
415
|
+
/** 懒创建:首次访问时读取存储并把设置应用到页面 */
|
|
416
|
+
function ensureStore$2() {
|
|
417
|
+
if (store$4) return store$4;
|
|
418
|
+
const created = createStore(load$2(prefix$2));
|
|
419
|
+
let prev = null;
|
|
420
|
+
const sync = (next) => {
|
|
421
|
+
applyToDocument(next, prev);
|
|
422
|
+
prev = next;
|
|
423
|
+
};
|
|
424
|
+
sync(created.get());
|
|
425
|
+
created.subscribe((next) => {
|
|
426
|
+
save(prefix$2, next);
|
|
427
|
+
sync(next);
|
|
428
|
+
});
|
|
429
|
+
store$4 = created;
|
|
430
|
+
return created;
|
|
431
|
+
}
|
|
432
|
+
/** 切换存储键前缀(createPageUse 时调用);前缀变化时重新读取 */
|
|
433
|
+
function setSettingsStoragePrefix(next) {
|
|
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();
|
|
438
|
+
}
|
|
439
|
+
const pageUseSettings = {
|
|
440
|
+
store: {
|
|
441
|
+
get: () => ensureStore$2().get(),
|
|
442
|
+
subscribe: (listener) => ensureStore$2().subscribe(listener)
|
|
443
|
+
},
|
|
444
|
+
get() {
|
|
445
|
+
return ensureStore$2().get();
|
|
446
|
+
},
|
|
447
|
+
/** 固定项(LOCKED_SETTINGS)的修改被忽略 */
|
|
448
|
+
update(patch) {
|
|
449
|
+
ensureStore$2().set({
|
|
450
|
+
...patch,
|
|
451
|
+
...LOCKED_SETTINGS
|
|
452
|
+
});
|
|
453
|
+
},
|
|
454
|
+
/** 把浮动入口(及随之定位的面板)恢复到默认角落位置 */
|
|
455
|
+
resetPositions() {
|
|
456
|
+
ensureStore$2().set({ fabPos: null });
|
|
457
|
+
}
|
|
458
|
+
};
|
|
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
|
+
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/voice/speaker.ts
|
|
506
|
+
/**
|
|
507
|
+
* 演示播报的播放队列,发声委托给当前语音合成引擎(由 getEngine 提供,可在播报间切换)。
|
|
508
|
+
*
|
|
509
|
+
* 队列只有两部分:当前正在播报的一组文本,以及一条待播的步骤文本。
|
|
510
|
+
* - 步骤播报不打断当前播报,只覆盖待播槽(语音跟不上执行时丢弃中间步骤);
|
|
511
|
+
* - 提问 / 结论先清空再播报;
|
|
512
|
+
* - 每组开始时读取一次引擎并在组内固定使用(splitSentences 与中止对象保持一致);
|
|
513
|
+
* - 每组持有一个 AbortController,清空时中止,引擎据此停止自身的合成与播放;
|
|
514
|
+
* - 每次清空递增代次,旧代次 speak() 的迟到 resolve 一律忽略,避免中止后误触发下一条。
|
|
515
|
+
*
|
|
516
|
+
* 当前引擎不可用时 enqueue / interrupt 静默返回。
|
|
517
|
+
*/
|
|
518
|
+
/**
|
|
519
|
+
* @param getSettings 每次调用引擎前读取;参数为当前组使用的引擎,调用方据此取该引擎的音色
|
|
520
|
+
* @param getEngine 每组开始时读取当前生效引擎
|
|
521
|
+
*/
|
|
522
|
+
function createSpeaker(getSettings, getEngine = () => browserTtsEngine) {
|
|
523
|
+
let generation = 0;
|
|
524
|
+
/** 当前组剩余的待调用文本;null 表示空闲 */
|
|
525
|
+
let current = null;
|
|
526
|
+
let pending = null;
|
|
527
|
+
/** 当前组的中止控制;空闲时为 null,clear 据此判断是否需要中止引擎 */
|
|
528
|
+
let controller = null;
|
|
529
|
+
/** 当前组使用的引擎;组内不随设置切换 */
|
|
530
|
+
let engine = getEngine();
|
|
531
|
+
function playGroup(sentences) {
|
|
532
|
+
engine = getEngine();
|
|
533
|
+
if (!engine.isAvailable()) return;
|
|
534
|
+
current = engine.splitSentences ? [...sentences] : [sentences.join("")];
|
|
535
|
+
controller = new AbortController();
|
|
536
|
+
playNext();
|
|
537
|
+
}
|
|
538
|
+
function playNext() {
|
|
539
|
+
const text = current?.shift();
|
|
540
|
+
if (text === void 0 || !controller) {
|
|
541
|
+
current = null;
|
|
542
|
+
controller = null;
|
|
543
|
+
if (pending) {
|
|
544
|
+
const next = pending;
|
|
545
|
+
pending = null;
|
|
546
|
+
playGroup(next);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const owner = generation;
|
|
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
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function clear() {
|
|
567
|
+
generation += 1;
|
|
568
|
+
controller?.abort();
|
|
569
|
+
controller = null;
|
|
570
|
+
current = null;
|
|
571
|
+
pending = null;
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
enqueue(sentences) {
|
|
575
|
+
if (!sentences.length) return;
|
|
576
|
+
if (current !== null) {
|
|
577
|
+
pending = sentences;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
playGroup(sentences);
|
|
581
|
+
},
|
|
582
|
+
interrupt(sentences) {
|
|
583
|
+
clear();
|
|
584
|
+
if (sentences.length) playGroup(sentences);
|
|
585
|
+
},
|
|
586
|
+
clear
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/voice/speechText.ts
|
|
592
|
+
/**
|
|
593
|
+
* 播报文本清洗(纯函数,无 DOM 依赖):把模型输出的步骤描述 / 结论转成适合语音合成的句子数组。
|
|
594
|
+
* 结果只用于播报,不回写任务结果与面板展示内容。
|
|
595
|
+
*/
|
|
596
|
+
/** 单条播报清洗后的字数上限:中文语音约 4 字/秒,240 字约 1 分钟(语速 1 时) */
|
|
597
|
+
const SPEECH_MAX_CHARS = 240;
|
|
598
|
+
/** 超过上限截断时追加的提示语(未挂载调试面板、不知道面板名称时使用) */
|
|
599
|
+
const SPEECH_TRUNCATED_HINT = "详细内容请查看面板";
|
|
600
|
+
/** 截断提示语:有面板名称(调试面板标题)时指明名称,如「详细内容请查看「页面智能体」」 */
|
|
601
|
+
function buildTruncatedHint(panelName) {
|
|
602
|
+
const name = panelName?.trim();
|
|
603
|
+
return name ? `详细内容请查看「${name}」` : SPEECH_TRUNCATED_HINT;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 单次合成的字数上限:Chrome 的在线语音单条超过约 15 秒会被截断,
|
|
607
|
+
* 超过该长度的句子再按逗号等停顿切分(约 4 字/秒 × 12 秒)
|
|
608
|
+
*/
|
|
609
|
+
const MAX_UTTERANCE_CHARS = 50;
|
|
610
|
+
/** 句末标点:截断与切句的边界(分号视为句间边界,列表项以分号连接) */
|
|
611
|
+
const SENTENCE_END = "。!?!?;;";
|
|
612
|
+
/** 句内停顿:长句二次切分的边界 */
|
|
613
|
+
const CLAUSE_END = ",、:,:";
|
|
614
|
+
/** 行尾已有这些标点时,拼接下一行不再补分隔符 */
|
|
615
|
+
const TRAILING_PUNCT = /[。!?!?;;,,、::]$/;
|
|
616
|
+
/**
|
|
617
|
+
* 元素编号:page-agent 以 [index] 标注可交互元素,模型常把编号写进步骤描述,
|
|
618
|
+
* 如「点击索引为 23 的菜单」「点击 [23]」「点击元素 23」。
|
|
619
|
+
* 不匹配「编号 / 序号」:结论中的「编号 20260809」等可能是业务数据。
|
|
620
|
+
*/
|
|
621
|
+
const ELEMENT_INDEX_PATTERNS = [
|
|
622
|
+
/索引\s*(?:为|是)?\s*\[?\d+\]?\s*(?:号)?\s*(?:的)?/g,
|
|
623
|
+
/元素\s*\[\d+\]\s*(?:的)?/g,
|
|
624
|
+
/元素\s*\d+\s*(?:号)?\s*(?:的)?/g,
|
|
625
|
+
/\[\d+\]/g
|
|
626
|
+
];
|
|
627
|
+
/** 表格分隔行:| --- | :---: | */
|
|
628
|
+
const TABLE_SEPARATOR = /^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)*\|?\s*$/;
|
|
629
|
+
/** 表格数据行:首尾为竖线 */
|
|
630
|
+
const TABLE_ROW = /^\s*\|.*\|\s*$/;
|
|
631
|
+
/** 列表项行首:- * + 或 1. 1) 1、(序号后紧跟数字时视为小数,如「2.5 吨」,不当作列表) */
|
|
632
|
+
const LIST_MARKER = /^\s*(?:[-*+]\s+|\d+[.)](?!\d)\s*|\d+、\s*)/;
|
|
633
|
+
/** 行内 Markdown、网址、emoji、元素编号等清理 */
|
|
634
|
+
function cleanInline(text) {
|
|
635
|
+
let s = text;
|
|
636
|
+
for (const pattern of ELEMENT_INDEX_PATTERNS) s = s.replace(pattern, "");
|
|
637
|
+
s = s.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/https?:\/\/[^\s))]+/g, "").replace(/(\*\*|__)(.+?)\1/g, "$2").replace(/\*([^*\n]+)\*/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/[*`~]/g, "").replace(/[\p{Extended_Pictographic}\u{FE0F}\u{200D}\u{2713}\u{2717}\u{2718}]/gu, "").replace(/[「」『』【】]/g, "");
|
|
638
|
+
return s.replace(/\s+/g, " ").trim();
|
|
639
|
+
}
|
|
640
|
+
/** 按行处理块级结构:代码块与 HTML 已在调用前移除 */
|
|
641
|
+
function toLines(text) {
|
|
642
|
+
const lines = [];
|
|
643
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
644
|
+
if (TABLE_SEPARATOR.test(raw)) continue;
|
|
645
|
+
if (TABLE_ROW.test(raw)) {
|
|
646
|
+
const cells = raw.split("|").map((cell) => cleanInline(cell)).filter(Boolean);
|
|
647
|
+
if (cells.length) lines.push({
|
|
648
|
+
text: cells.join(","),
|
|
649
|
+
item: true
|
|
650
|
+
});
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
const item = LIST_MARKER.test(raw);
|
|
654
|
+
const cleaned = cleanInline(raw.replace(/^\s*#{1,6}\s+/, "").replace(/^\s*(?:>\s?)+/, "").replace(LIST_MARKER, ""));
|
|
655
|
+
if (cleaned) lines.push({
|
|
656
|
+
text: cleaned,
|
|
657
|
+
item
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
return lines;
|
|
661
|
+
}
|
|
662
|
+
/** 拼接各行:列表项之间用分号,其余行之间补句号;行尾已有标点时不补 */
|
|
663
|
+
function joinLines(lines) {
|
|
664
|
+
let result = "";
|
|
665
|
+
lines.forEach((line, index) => {
|
|
666
|
+
if (index > 0 && !TRAILING_PUNCT.test(result)) result += lines[index - 1].item && line.item ? ";" : "。";
|
|
667
|
+
result += line.text;
|
|
668
|
+
});
|
|
669
|
+
return result.replace(/([。!?!?;;,,、::])[。!?!?;;,,、::\s]+/g, "$1").replace(/^[。!?!?;;,,、::\s]+/, "").trim();
|
|
670
|
+
}
|
|
671
|
+
/** 超过上限时在上限内最后一个句末标点处截断,找不到则硬截断,并追加提示语 */
|
|
672
|
+
function truncate(text, maxChars, hint) {
|
|
673
|
+
if (text.length <= maxChars) return text;
|
|
674
|
+
const head = text.slice(0, maxChars);
|
|
675
|
+
let cut = -1;
|
|
676
|
+
for (let i = head.length - 1; i >= 0; i -= 1) if (SENTENCE_END.includes(head[i])) {
|
|
677
|
+
cut = i;
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
let kept = cut > 0 ? head.slice(0, cut + 1) : head;
|
|
681
|
+
kept = kept.replace(/[;;,,、::]$/, "");
|
|
682
|
+
if (!/[。!?!?]$/.test(kept)) kept += "。";
|
|
683
|
+
return `${kept}${hint}`;
|
|
684
|
+
}
|
|
685
|
+
/** 按句末标点切句;过长的句子再按逗号等停顿切分 */
|
|
686
|
+
function splitSentences(text) {
|
|
687
|
+
const sentences = text.match(new RegExp(`[^${SENTENCE_END}]+[${SENTENCE_END}]*`, "g")) || [];
|
|
688
|
+
const result = [];
|
|
689
|
+
for (const sentence of sentences) {
|
|
690
|
+
if (sentence.length <= MAX_UTTERANCE_CHARS) {
|
|
691
|
+
result.push(sentence);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const clauses = sentence.match(new RegExp(`[^${CLAUSE_END}]+[${CLAUSE_END}]*`, "g")) || [sentence];
|
|
695
|
+
let buffer = "";
|
|
696
|
+
for (const clause of clauses) {
|
|
697
|
+
if (buffer && buffer.length + clause.length > MAX_UTTERANCE_CHARS) {
|
|
698
|
+
result.push(buffer);
|
|
699
|
+
buffer = "";
|
|
700
|
+
}
|
|
701
|
+
buffer += clause;
|
|
702
|
+
}
|
|
703
|
+
if (buffer) result.push(buffer);
|
|
704
|
+
}
|
|
705
|
+
return result.map((s) => s.trim()).filter((s) => /[\p{L}\p{N}]/u.test(s));
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 模型文本 → 播报句子数组。清洗后为空时返回空数组(调用方据此跳过播报)。
|
|
709
|
+
* 处理顺序:代码块 / HTML → 表格 → 行内 Markdown 与符号 → 拼行 → 截断 → 切句。
|
|
710
|
+
*/
|
|
711
|
+
function toSpeechText(text, options = {}) {
|
|
712
|
+
const maxChars = options.maxChars ?? 240;
|
|
713
|
+
const joined = joinLines(toLines((text || "").replace(/```[\s\S]*?(?:```|$)/g, "\n").replace(/<\/?[a-zA-Z][^>]*>/g, "")));
|
|
714
|
+
if (!joined) return [];
|
|
715
|
+
return splitSentences(truncate(joined, maxChars, options.truncatedHint ?? "详细内容请查看面板"));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/voice/narration.ts
|
|
720
|
+
/**
|
|
721
|
+
* 演示播报:订阅进度事件,按设置把步骤描述、提问与结论交给 speaker 播报。
|
|
722
|
+
* 只订阅事件,不影响任务执行节奏;对所有任务来源生效。
|
|
723
|
+
* 是否播报 = 设置 voiceEnabled(总开关,持久化)且未处于本次任务静音(narrationMute,运行期)。
|
|
724
|
+
*/
|
|
725
|
+
/**
|
|
726
|
+
* 不播报的步骤:滚动、等待没有演示价值;done 由结论播报替代;
|
|
727
|
+
* ask_user 的问题由 question 事件播报;error / retry 为执行出错与重试的伪工具名。
|
|
728
|
+
*/
|
|
729
|
+
const SILENT_STEP_TOOLS = /* @__PURE__ */ new Set([
|
|
730
|
+
"scroll",
|
|
731
|
+
"scroll_horizontally",
|
|
732
|
+
"wait",
|
|
733
|
+
"done",
|
|
734
|
+
"ask_user",
|
|
735
|
+
"error",
|
|
736
|
+
"retry"
|
|
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 });
|
|
755
|
+
/** 启动播报,返回停止函数(取消订阅并停止播报) */
|
|
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();
|
|
760
|
+
return {
|
|
761
|
+
rate: voiceRate,
|
|
762
|
+
voiceName: voiceNames[engine.id] ?? ""
|
|
763
|
+
};
|
|
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) });
|
|
767
|
+
const offEvents = subscribe((event) => {
|
|
768
|
+
switch (event.type) {
|
|
769
|
+
case "started":
|
|
770
|
+
narrationMute.set({ muted: false });
|
|
771
|
+
stopPreview();
|
|
772
|
+
speaker.clear();
|
|
773
|
+
break;
|
|
774
|
+
case "step":
|
|
775
|
+
if (!enabled() || SILENT_STEP_TOOLS.has(event.step.tool)) return;
|
|
776
|
+
speaker.enqueue(speech(event.step.goal));
|
|
777
|
+
break;
|
|
778
|
+
case "question":
|
|
779
|
+
if (enabled()) speaker.interrupt(speech(event.question));
|
|
780
|
+
break;
|
|
781
|
+
case "finished": if (event.result.status === "stopped" || !enabled()) speaker.clear();
|
|
782
|
+
else speaker.interrupt(speech(event.result.summary));
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
let prev = pageUseSettings.get();
|
|
786
|
+
const offSettings = pageUseSettings.store.subscribe((settings) => {
|
|
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();
|
|
792
|
+
});
|
|
793
|
+
return () => {
|
|
794
|
+
offEvents();
|
|
795
|
+
offSettings();
|
|
796
|
+
offMute();
|
|
797
|
+
speaker.clear();
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
//#endregion
|
|
5
802
|
//#region src/core/siteMap.ts
|
|
6
803
|
/** 去掉 `#`、前导 `/` 与查询串,得到用于比较的路径 */
|
|
7
804
|
function normalizePath(path) {
|
|
@@ -52,8 +849,17 @@ function findMenuByPath(path, menus) {
|
|
|
52
849
|
//#region src/core/pageInstructions.ts
|
|
53
850
|
/** 未提供 layoutHint 时的通用页面结构描述 */
|
|
54
851
|
const DEFAULT_LAYOUT_HINT = ["- 页面通常由导航菜单与内容区组成;内容区顶部若有面包屑或标题,可用来确认当前位置。", "- 分组菜单点击后展开/收起下级;叶子菜单点击后进入页面。"].join("\n");
|
|
55
|
-
/**
|
|
56
|
-
|
|
852
|
+
/**
|
|
853
|
+
* 演示播报开启时追加的约束:page-agent 默认要求 next_goal 写「目标 + 动作」,模型常带元素编号,
|
|
854
|
+
* 念出来生硬;这里只约束步骤描述,不改变 done 的 text(结论会回传给调用方)。
|
|
855
|
+
*/
|
|
856
|
+
const NARRATION_RULES = [
|
|
857
|
+
"## 语音播报",
|
|
858
|
+
"- 每一步的 next_goal 会被语音播报给观众:写成一句简短的中文,描述这一步要做什么,如「打开隐患台账页面」;不要包含元素编号、索引或方括号数字。",
|
|
859
|
+
"- 该要求只针对 next_goal;done 的 text 仍按原要求书写。"
|
|
860
|
+
].join("\n");
|
|
861
|
+
/** 全局系统提示:站点结构 + 交互约定 + 安全边界(+ 播报约束) */
|
|
862
|
+
function buildSystemInstruction(options, flags = {}) {
|
|
57
863
|
const urlLabel = options.routerMode === "hash" ? "URL hash" : "URL 路径";
|
|
58
864
|
const siteMap = buildSiteMapText(options.getMenus(), options.routerMode);
|
|
59
865
|
return [
|
|
@@ -76,7 +882,8 @@ function buildSystemInstruction(options) {
|
|
|
76
882
|
"",
|
|
77
883
|
"## 安全边界",
|
|
78
884
|
"- 未经用户明确要求,不得执行删除、提交、审批、发布、批量修改等不可逆操作;不确定时用 ask_user 询问。",
|
|
79
|
-
"- 任务完成后调用 done,用一句话说明当前所在页面及已完成的动作;无法完成时说明卡在哪一步。"
|
|
885
|
+
"- 任务完成后调用 done,用一句话说明当前所在页面及已完成的动作;无法完成时说明卡在哪一步。",
|
|
886
|
+
...flags.narration ? ["", NARRATION_RULES] : []
|
|
80
887
|
].join("\n");
|
|
81
888
|
}
|
|
82
889
|
/**
|
|
@@ -115,7 +922,7 @@ const STEP_DELAY_SECONDS = .6;
|
|
|
115
922
|
function newSessionId() {
|
|
116
923
|
return typeof crypto !== "undefined" && crypto.randomUUID?.() || `page-use-${Date.now()}`;
|
|
117
924
|
}
|
|
118
|
-
function createPageUseAgent({ options, enableMask, shouldRunBeforeTask, onTaskFinished }) {
|
|
925
|
+
function createPageUseAgent({ options, enableMask, narration = false, shouldRunBeforeTask, onTaskFinished }) {
|
|
119
926
|
const { llm } = options;
|
|
120
927
|
if (!llm.baseURL) throw new Error("[PageUse] 未配置 llm.baseURL");
|
|
121
928
|
let sessionId = newSessionId();
|
|
@@ -150,7 +957,7 @@ function createPageUseAgent({ options, enableMask, shouldRunBeforeTask, onTaskFi
|
|
|
150
957
|
enableMask,
|
|
151
958
|
experimentalScriptExecutionTool: false,
|
|
152
959
|
instructions: {
|
|
153
|
-
system: buildSystemInstruction(options),
|
|
960
|
+
system: buildSystemInstruction(options, { narration }),
|
|
154
961
|
getPageInstructions: () => buildPageInstruction(options)
|
|
155
962
|
},
|
|
156
963
|
transformPageContent: (content) => {
|
|
@@ -167,134 +974,11 @@ function createPageUseAgent({ options, enableMask, shouldRunBeforeTask, onTaskFi
|
|
|
167
974
|
}
|
|
168
975
|
await options.pageAgentOptions.onBeforeTask?.(...args);
|
|
169
976
|
},
|
|
170
|
-
onAfterTask: async (...args) => {
|
|
171
|
-
onTaskFinished?.(args[1]);
|
|
172
|
-
await options.pageAgentOptions.onAfterTask?.(...args);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
//#endregion
|
|
178
|
-
//#region src/core/options.ts
|
|
179
|
-
/** 默认模型,仅为示例取值;接入其它端点时应通过 llm.model 显式指定支持 function calling 的模型 */
|
|
180
|
-
const DEFAULT_MODEL = "deepseek-chat";
|
|
181
|
-
/** 单次任务最大步数;每步一次 LLM 调用,防止死循环消耗 token */
|
|
182
|
-
const DEFAULT_MAX_STEPS = 20;
|
|
183
|
-
/** 默认会话头:OpenCode Go 要求每个会话带稳定的 x-opencode-session 用于路由与提示缓存 */
|
|
184
|
-
const DEFAULT_SESSION_HEADER = "x-opencode-session";
|
|
185
|
-
/** localStorage / sessionStorage 键前缀,可通过 storageKeyPrefix 覆盖 */
|
|
186
|
-
const DEFAULT_STORAGE_KEY_PREFIX = "tz-page-use";
|
|
187
|
-
/** 相对路径补齐为当前 origin 的绝对地址并去掉末尾 /;无法解析时返回空串 */
|
|
188
|
-
function toAbsoluteUrl(input) {
|
|
189
|
-
const base = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
190
|
-
try {
|
|
191
|
-
return new URL(input, base).toString().replace(/\/+$/, "");
|
|
192
|
-
} catch {
|
|
193
|
-
return "";
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/** 按路由模式从 location 读取当前路径 */
|
|
197
|
-
function readLocationPath(mode) {
|
|
198
|
-
if (typeof window === "undefined") return "/";
|
|
199
|
-
if (mode === "hash") return window.location.hash.replace(/^#/, "") || "/";
|
|
200
|
-
return `${window.location.pathname}${window.location.search}`;
|
|
201
|
-
}
|
|
202
|
-
function resolveSessionHeader(value) {
|
|
203
|
-
if (value === false) return "";
|
|
204
|
-
return (value ?? "").trim() || "x-opencode-session";
|
|
205
|
-
}
|
|
206
|
-
function normalizeOptions(options) {
|
|
207
|
-
const { llm } = options;
|
|
208
|
-
const rawBase = (llm.baseURL || "").trim();
|
|
209
|
-
const routerMode = options.routerMode ?? "hash";
|
|
210
|
-
return {
|
|
211
|
-
llm: {
|
|
212
|
-
baseURL: rawBase ? toAbsoluteUrl(rawBase) : "",
|
|
213
|
-
apiKey: (llm.apiKey || "").trim(),
|
|
214
|
-
model: (llm.model || "").trim() || "deepseek-chat",
|
|
215
|
-
maxSteps: llm.maxSteps && llm.maxSteps > 0 ? llm.maxSteps : 20,
|
|
216
|
-
disableNamedToolChoice: Boolean(llm.disableNamedToolChoice),
|
|
217
|
-
extraBody: llm.extraBody && typeof llm.extraBody === "object" ? llm.extraBody : {},
|
|
218
|
-
sessionHeader: resolveSessionHeader(llm.sessionHeader)
|
|
219
|
-
},
|
|
220
|
-
appName: options.appName,
|
|
221
|
-
getMenus: options.getMenus ?? (() => void 0),
|
|
222
|
-
routerMode,
|
|
223
|
-
getCurrentPath: options.getCurrentPath ?? (() => readLocationPath(routerMode)),
|
|
224
|
-
resolveRouteName: options.resolveRouteName,
|
|
225
|
-
routeHints: options.routeHints ?? {},
|
|
226
|
-
layoutHint: options.layoutHint,
|
|
227
|
-
beforeTask: options.beforeTask,
|
|
228
|
-
beforeTaskLabel: options.beforeTaskLabel || "任务开始前执行宿主预处理",
|
|
229
|
-
storageKeyPrefix: options.storageKeyPrefix || "tz-page-use",
|
|
230
|
-
pageAgentOptions: options.pageAgentOptions ?? {},
|
|
231
|
-
exposeGlobal: options.exposeGlobal !== false
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/** URL 查询参数:?pageUseDebug=1 打开调试面板,?pageUseDebug=0 关闭;结果记入 sessionStorage */
|
|
235
|
-
const LAUNCHER_QUERY_KEY = "pageUseDebug";
|
|
236
|
-
/** 从 location 读取调试开关(hash 路由的查询参数在 # 之后,两处都查) */
|
|
237
|
-
function readLauncherQuery() {
|
|
238
|
-
const fromSearch = new URLSearchParams(window.location.search).get(LAUNCHER_QUERY_KEY);
|
|
239
|
-
if (fromSearch !== null) return fromSearch;
|
|
240
|
-
const hashQuery = window.location.hash.split("?")[1];
|
|
241
|
-
return hashQuery ? new URLSearchParams(hashQuery).get(LAUNCHER_QUERY_KEY) : null;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* 调试面板是否显示。优先级:URL 参数(并记入本会话)> 会话记录 > fallback。
|
|
245
|
-
* fallback 由宿主决定(例如「开发环境显示,生产环境隐藏」)。
|
|
246
|
-
*/
|
|
247
|
-
function readLauncherSwitch(fallback, storageKeyPrefix = DEFAULT_STORAGE_KEY_PREFIX) {
|
|
248
|
-
if (typeof window === "undefined") return fallback;
|
|
249
|
-
const sessionKey = `${storageKeyPrefix}-debug`;
|
|
250
|
-
try {
|
|
251
|
-
const query = readLauncherQuery();
|
|
252
|
-
if (query !== null) {
|
|
253
|
-
const enabled = query === "1" || query === "true";
|
|
254
|
-
window.sessionStorage.setItem(sessionKey, enabled ? "1" : "0");
|
|
255
|
-
return enabled;
|
|
256
|
-
}
|
|
257
|
-
const session = window.sessionStorage.getItem(sessionKey);
|
|
258
|
-
if (session !== null) return session === "1";
|
|
259
|
-
} catch {}
|
|
260
|
-
return fallback;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
//#endregion
|
|
264
|
-
//#region src/core/store.ts
|
|
265
|
-
function createStore(initial) {
|
|
266
|
-
let state = initial;
|
|
267
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
268
|
-
const notify = () => {
|
|
269
|
-
for (const listener of listeners) try {
|
|
270
|
-
listener(state);
|
|
271
|
-
} catch (err) {
|
|
272
|
-
console.warn("[PageUse] store 订阅回调异常", err);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
return {
|
|
276
|
-
get: () => state,
|
|
277
|
-
set(patch) {
|
|
278
|
-
const partial = typeof patch === "function" ? patch(state) : patch;
|
|
279
|
-
if (Object.keys(partial).every((key) => Object.is(state[key], partial[key]))) return;
|
|
280
|
-
state = {
|
|
281
|
-
...state,
|
|
282
|
-
...partial
|
|
283
|
-
};
|
|
284
|
-
notify();
|
|
285
|
-
},
|
|
286
|
-
replace(next) {
|
|
287
|
-
if (Object.is(next, state)) return;
|
|
288
|
-
state = next;
|
|
289
|
-
notify();
|
|
290
|
-
},
|
|
291
|
-
subscribe(listener) {
|
|
292
|
-
listeners.add(listener);
|
|
293
|
-
return () => {
|
|
294
|
-
listeners.delete(listener);
|
|
295
|
-
};
|
|
977
|
+
onAfterTask: async (...args) => {
|
|
978
|
+
onTaskFinished?.(args[1]);
|
|
979
|
+
await options.pageAgentOptions.onAfterTask?.(...args);
|
|
296
980
|
}
|
|
297
|
-
};
|
|
981
|
+
});
|
|
298
982
|
}
|
|
299
983
|
|
|
300
984
|
//#endregion
|
|
@@ -323,7 +1007,7 @@ function ensureStore$1() {
|
|
|
323
1007
|
if (!store$2) store$2 = createStore({ entries: load$1(prefix$1) });
|
|
324
1008
|
return store$2;
|
|
325
1009
|
}
|
|
326
|
-
function persist(entries) {
|
|
1010
|
+
function persist$1(entries) {
|
|
327
1011
|
ensureStore$1().set({ entries });
|
|
328
1012
|
try {
|
|
329
1013
|
window.localStorage.setItem(storageKey$1(prefix$1), JSON.stringify(entries));
|
|
@@ -344,206 +1028,100 @@ const pageUseHistory = {
|
|
|
344
1028
|
return ensureStore$1().get().entries;
|
|
345
1029
|
},
|
|
346
1030
|
add(entry) {
|
|
347
|
-
persist([{
|
|
1031
|
+
persist$1([{
|
|
348
1032
|
id: `${entry.at}-${Math.random().toString(36).slice(2, 8)}`,
|
|
349
1033
|
...entry
|
|
350
1034
|
}, ...ensureStore$1().get().entries].slice(0, MAX_ENTRIES));
|
|
351
1035
|
},
|
|
352
1036
|
remove(id) {
|
|
353
|
-
persist(ensureStore$1().get().entries.filter((item) => item.id !== id));
|
|
1037
|
+
persist$1(ensureStore$1().get().entries.filter((item) => item.id !== id));
|
|
354
1038
|
},
|
|
355
1039
|
clear() {
|
|
356
|
-
persist([]);
|
|
1040
|
+
persist$1([]);
|
|
357
1041
|
}
|
|
358
1042
|
};
|
|
359
1043
|
|
|
360
1044
|
//#endregion
|
|
361
|
-
//#region src/core/
|
|
1045
|
+
//#region src/core/presetStore.ts
|
|
362
1046
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* - 子节点顺序:ai-motion 光晕层在前(初始化失败时不存在),光标节点恒为最后一个子节点
|
|
369
|
-
* - 光标子节点顺序:1 点击波纹 / 2 箭头填充 / 3 箭头描边
|
|
370
|
-
* - 点击态类名包含 `clicking` 子串
|
|
371
|
-
* 升级 page-agent 后若光标样式失效,先核对 SimulatorMask 的 #createCursor 结构。
|
|
1047
|
+
* Page Use 快捷指令的用户维护数据(localStorage,懒创建单例)。
|
|
1048
|
+
* - custom:用户自定义指令,新增排在最前
|
|
1049
|
+
* - hidden:被用户删除(隐藏)的来源指令(菜单生成 / 内置 / 宿主 presets / scenarios)的 task 文本
|
|
1050
|
+
* 来源指令本身不落盘;宿主修改 task 后旧的隐藏记录自然失效,不主动清理。
|
|
1051
|
+
* 存储键为 `${storageKeyPrefix}-presets`。
|
|
372
1052
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const PAGE_USE_MASK_COLORS = {
|
|
377
|
-
primary: "#0879ff",
|
|
378
|
-
cyan: "#36cfc9",
|
|
379
|
-
indigo: "#597ef7",
|
|
380
|
-
/** 误触拦截时的警示色 */
|
|
381
|
-
warning: "#fa8c16"
|
|
1053
|
+
const EMPTY_STATE = {
|
|
1054
|
+
custom: [],
|
|
1055
|
+
hidden: []
|
|
382
1056
|
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
/** 光标节点:蒙层的最后一个子节点 */
|
|
386
|
-
const CURSOR = `${MASK} > :last-child`;
|
|
387
|
-
const { primary, cyan, indigo } = PAGE_USE_MASK_COLORS;
|
|
388
|
-
const PAGE_USE_MASK_THEME_CSS = `
|
|
389
|
-
/* 隐藏 ai-motion 默认光晕(光标之外的子节点),光晕改由 <tz-page-use-glow> 渲染 */
|
|
390
|
-
${MASK} > :not(:last-child) { display: none !important; }
|
|
391
|
-
/* 蒙层本身保持透明,仅负责拦截点击;真实鼠标由 wait 转圈改为禁止符号,明确「此时不可操作」而非「页面卡死」 */
|
|
392
|
-
${MASK} { cursor: not-allowed !important; }
|
|
393
|
-
|
|
394
|
-
/* 光标整体外发光 */
|
|
395
|
-
${CURSOR} {
|
|
396
|
-
filter: drop-shadow(0 0 6px ${primary}8c) drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25));
|
|
397
|
-
}
|
|
398
|
-
/* 光标尖端(节点原点)的呼吸光圈 */
|
|
399
|
-
${CURSOR}::before {
|
|
400
|
-
content: '';
|
|
401
|
-
position: absolute;
|
|
402
|
-
left: -16px;
|
|
403
|
-
top: -16px;
|
|
404
|
-
width: 32px;
|
|
405
|
-
height: 32px;
|
|
406
|
-
border-radius: 50%;
|
|
407
|
-
background: radial-gradient(circle, ${cyan}66 0%, ${primary}26 55%, transparent 72%);
|
|
408
|
-
animation: page-use-cursor-halo 1.6s ease-in-out infinite;
|
|
409
|
-
pointer-events: none;
|
|
410
|
-
}
|
|
411
|
-
/* 箭头描边:主题渐变 */
|
|
412
|
-
${CURSOR} > :nth-child(3) {
|
|
413
|
-
background: linear-gradient(45deg, ${primary}, ${cyan}) !important;
|
|
414
|
-
}
|
|
415
|
-
/* 箭头填充:用同形遮罩替换原白色 SVG 背景,改为浅色渐变 */
|
|
416
|
-
${CURSOR} > :nth-child(2) {
|
|
417
|
-
background: linear-gradient(135deg, #ffffff 30%, #e6f4ff) !important;
|
|
418
|
-
-webkit-mask: ${CURSOR_ARROW_MASK} center / 100% 100% no-repeat;
|
|
419
|
-
mask: ${CURSOR_ARROW_MASK} center / 100% 100% no-repeat;
|
|
420
|
-
}
|
|
421
|
-
/* 点击波纹:外环改主题色 */
|
|
422
|
-
${CURSOR} > :nth-child(1)::after {
|
|
423
|
-
border-color: ${cyan} !important;
|
|
424
|
-
border-width: 3px !important;
|
|
425
|
-
}
|
|
426
|
-
/* 点击波纹:追加一层实心扩散,增强点击反馈 */
|
|
427
|
-
${CURSOR} > :nth-child(1)::before {
|
|
428
|
-
content: '';
|
|
429
|
-
position: absolute;
|
|
430
|
-
inset: 25%;
|
|
431
|
-
border-radius: 50%;
|
|
432
|
-
background: radial-gradient(circle, ${indigo}80, transparent 70%);
|
|
433
|
-
opacity: 0;
|
|
434
|
-
}
|
|
435
|
-
${CURSOR}[class*='clicking'] > :nth-child(1)::before {
|
|
436
|
-
animation: page-use-cursor-press 420ms ease-out forwards;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
@keyframes page-use-cursor-halo {
|
|
440
|
-
0%, 100% { transform: scale(0.85); opacity: 0.7; }
|
|
441
|
-
50% { transform: scale(1.25); opacity: 1; }
|
|
442
|
-
}
|
|
443
|
-
@keyframes page-use-cursor-press {
|
|
444
|
-
0% { transform: scale(0.4); opacity: 1; }
|
|
445
|
-
100% { transform: scale(2.4); opacity: 0; }
|
|
1057
|
+
function storageKey(prefix) {
|
|
1058
|
+
return `${prefix}-presets`;
|
|
446
1059
|
}
|
|
447
|
-
|
|
448
|
-
|
|
1060
|
+
function createId() {
|
|
1061
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
449
1062
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
showBuiltinPanel: false,
|
|
468
|
-
showHighlights: false,
|
|
469
|
-
showStepDetails: true,
|
|
470
|
-
enableBeforeTask: true,
|
|
471
|
-
enableMask: true,
|
|
472
|
-
customMaskEffect: true,
|
|
473
|
-
fabPos: null
|
|
474
|
-
};
|
|
475
|
-
function storageKey(prefix) {
|
|
476
|
-
return `${prefix}-settings`;
|
|
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
|
+
};
|
|
477
1080
|
}
|
|
478
1081
|
function load(prefix) {
|
|
479
1082
|
try {
|
|
480
1083
|
const raw = window.localStorage.getItem(storageKey(prefix));
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
...DEFAULT_PAGE_USE_SETTINGS,
|
|
484
|
-
...JSON.parse(raw)
|
|
485
|
-
};
|
|
1084
|
+
return raw ? sanitize(JSON.parse(raw)) : { ...EMPTY_STATE };
|
|
486
1085
|
} catch {
|
|
487
|
-
return { ...
|
|
1086
|
+
return { ...EMPTY_STATE };
|
|
488
1087
|
}
|
|
489
1088
|
}
|
|
490
|
-
function save(prefix, settings) {
|
|
491
|
-
try {
|
|
492
|
-
window.localStorage.setItem(storageKey(prefix), JSON.stringify(settings));
|
|
493
|
-
} catch {}
|
|
494
|
-
}
|
|
495
|
-
/** 按开关注入/移除一段覆盖样式(用于压制 page-agent 自带的 DOM,这些节点位于宿主 document 中) */
|
|
496
|
-
function toggleOverrideStyle(styleId, css, show) {
|
|
497
|
-
const existing = document.getElementById(styleId);
|
|
498
|
-
if (show) {
|
|
499
|
-
existing?.remove();
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (existing) return;
|
|
503
|
-
const style = document.createElement("style");
|
|
504
|
-
style.id = styleId;
|
|
505
|
-
style.textContent = css;
|
|
506
|
-
document.head.appendChild(style);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* 把设置同步到页面:
|
|
510
|
-
* - 高亮:page-controller 的 doHighlightElements 内部写死为 true,只能用样式覆盖其高亮容器
|
|
511
|
-
* - 内置面板:Panel 在 statuschange 到 running 时会自行 show(),因此用 !important 样式压制
|
|
512
|
-
* - 蒙层主题:注入覆盖样式;关闭时移除,恢复 page-agent 默认光晕与光标
|
|
513
|
-
*/
|
|
514
|
-
function applyToDocument(settings, prev) {
|
|
515
|
-
if (typeof document === "undefined") return;
|
|
516
|
-
if (!prev || prev.showHighlights !== settings.showHighlights) toggleOverrideStyle(HIGHLIGHT_STYLE_ID, `#${HIGHLIGHT_CONTAINER_ID}{display:none !important;}`, settings.showHighlights);
|
|
517
|
-
if (!prev || prev.showBuiltinPanel !== settings.showBuiltinPanel) toggleOverrideStyle(BUILTIN_PANEL_STYLE_ID, `#${BUILTIN_PANEL_ID}{display:none !important;}`, settings.showBuiltinPanel);
|
|
518
|
-
if (!prev || prev.customMaskEffect !== settings.customMaskEffect) toggleOverrideStyle(MASK_THEME_STYLE_ID, PAGE_USE_MASK_THEME_CSS, !settings.customMaskEffect);
|
|
519
|
-
}
|
|
520
1089
|
let prefix = DEFAULT_STORAGE_KEY_PREFIX;
|
|
521
1090
|
let store$1 = null;
|
|
522
|
-
/** 懒创建:首次访问时读取存储并把设置应用到页面 */
|
|
523
1091
|
function ensureStore() {
|
|
524
|
-
if (store$1)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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"
|
|
530
1107
|
};
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
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 };
|
|
538
1117
|
}
|
|
539
1118
|
/** 切换存储键前缀(createPageUse 时调用);前缀变化时重新读取 */
|
|
540
|
-
function
|
|
1119
|
+
function setPresetsStoragePrefix(next) {
|
|
541
1120
|
if (next === prefix && store$1) return;
|
|
542
1121
|
prefix = next;
|
|
543
|
-
|
|
544
|
-
else ensureStore();
|
|
1122
|
+
ensureStore().replace(load(prefix));
|
|
545
1123
|
}
|
|
546
|
-
const
|
|
1124
|
+
const pageUsePresets = {
|
|
547
1125
|
store: {
|
|
548
1126
|
get: () => ensureStore().get(),
|
|
549
1127
|
subscribe: (listener) => ensureStore().subscribe(listener)
|
|
@@ -551,12 +1129,45 @@ const pageUseSettings = {
|
|
|
551
1129
|
get() {
|
|
552
1130
|
return ensureStore().get();
|
|
553
1131
|
},
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
556
1142
|
},
|
|
557
|
-
/**
|
|
558
|
-
|
|
559
|
-
ensureStore().
|
|
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: [] });
|
|
560
1171
|
}
|
|
561
1172
|
};
|
|
562
1173
|
|
|
@@ -668,6 +1279,10 @@ let everConfigured = false;
|
|
|
668
1279
|
let agent = null;
|
|
669
1280
|
/** 创建实例时的蒙层设置;与当前设置不一致时须重建 */
|
|
670
1281
|
let agentMaskFlag = null;
|
|
1282
|
+
/** 创建实例时的播报设置:系统提示在构造时生成,播报开关变化后须重建才能追加 / 移除步骤描述约束 */
|
|
1283
|
+
let agentVoiceFlag = null;
|
|
1284
|
+
/** 演示播报的停止函数;配置期间运行,解绑 / 重新配置时停止 */
|
|
1285
|
+
let stopNarration = null;
|
|
671
1286
|
/** 已挂载且可作答的调试面板数量;>0 时未指定 onAskUser 的提问交给面板 */
|
|
672
1287
|
let questionUiCount = 0;
|
|
673
1288
|
/** 当前任务的调用方选项(提问回调、进度回调) */
|
|
@@ -752,20 +1367,25 @@ function handleAskUser(question, signal) {
|
|
|
752
1367
|
}, { once: true });
|
|
753
1368
|
});
|
|
754
1369
|
}
|
|
755
|
-
/**
|
|
1370
|
+
/**
|
|
1371
|
+
* 懒创建单例;蒙层或播报设置变化时重建(enableMask 属构造期选项,系统提示在构造时生成)。
|
|
1372
|
+
* 在任务开始时调用,因此执行中切换开关只影响下一次任务。
|
|
1373
|
+
*/
|
|
756
1374
|
function ensureAgent(options) {
|
|
757
1375
|
const settings = pageUseSettings.get();
|
|
758
|
-
if (agent && !agent.disposed && agentMaskFlag === settings.enableMask) return agent;
|
|
1376
|
+
if (agent && !agent.disposed && agentMaskFlag === settings.enableMask && agentVoiceFlag === settings.voiceEnabled) return agent;
|
|
759
1377
|
if (agent && !agent.disposed) agent.dispose();
|
|
760
1378
|
const created = createPageUseAgent({
|
|
761
1379
|
options,
|
|
762
1380
|
enableMask: settings.enableMask,
|
|
1381
|
+
narration: settings.voiceEnabled,
|
|
763
1382
|
shouldRunBeforeTask: () => pageUseSettings.get().enableBeforeTask
|
|
764
1383
|
});
|
|
765
1384
|
bindAgentEvents(created);
|
|
766
1385
|
created.onAskUser = (question, askOptions) => handleAskUser(question, askOptions?.signal);
|
|
767
1386
|
agent = created;
|
|
768
1387
|
agentMaskFlag = settings.enableMask;
|
|
1388
|
+
agentVoiceFlag = settings.voiceEnabled;
|
|
769
1389
|
store.set({ maskEnabled: settings.enableMask });
|
|
770
1390
|
return created;
|
|
771
1391
|
}
|
|
@@ -803,10 +1423,18 @@ function configurePageUse(options) {
|
|
|
803
1423
|
everConfigured = true;
|
|
804
1424
|
setSettingsStoragePrefix(options.storageKeyPrefix);
|
|
805
1425
|
setHistoryStoragePrefix(options.storageKeyPrefix);
|
|
1426
|
+
setPresetsStoragePrefix(options.storageKeyPrefix);
|
|
806
1427
|
store.set({ ready: Boolean(options.llm.baseURL) });
|
|
1428
|
+
stopNarration?.();
|
|
1429
|
+
stopNarration = startNarration({
|
|
1430
|
+
subscribe: subscribePageUse,
|
|
1431
|
+
engines: options.ttsEngines
|
|
1432
|
+
});
|
|
807
1433
|
return () => {
|
|
808
1434
|
if (config !== options) return;
|
|
809
1435
|
config = null;
|
|
1436
|
+
stopNarration?.();
|
|
1437
|
+
stopNarration = null;
|
|
810
1438
|
disposeAgent();
|
|
811
1439
|
store.set({ ready: false });
|
|
812
1440
|
};
|
|
@@ -969,6 +1597,7 @@ function disposeAgent() {
|
|
|
969
1597
|
agent?.dispose();
|
|
970
1598
|
agent = null;
|
|
971
1599
|
agentMaskFlag = null;
|
|
1600
|
+
agentVoiceFlag = null;
|
|
972
1601
|
activeOptions = null;
|
|
973
1602
|
store.set({
|
|
974
1603
|
maskEnabled: false,
|
|
@@ -998,6 +1627,39 @@ const pageUseCapability = {
|
|
|
998
1627
|
subscribe: subscribePageUse
|
|
999
1628
|
};
|
|
1000
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
|
+
|
|
1001
1663
|
//#endregion
|
|
1002
1664
|
//#region src/capability/tool.ts
|
|
1003
1665
|
/** 工具名;对接方在 system 提示中引用时保持一致 */
|
|
@@ -1090,7 +1752,9 @@ function installPageUseGlobal() {
|
|
|
1090
1752
|
const api = {
|
|
1091
1753
|
...pageUseCapability,
|
|
1092
1754
|
tool: PAGE_USE_TOOL,
|
|
1093
|
-
runTool: runPageUseTool
|
|
1755
|
+
runTool: runPageUseTool,
|
|
1756
|
+
showLauncher: showPageUseLauncher,
|
|
1757
|
+
hideLauncher: hidePageUseLauncher
|
|
1094
1758
|
};
|
|
1095
1759
|
window[PAGE_USE_GLOBAL_KEY] = api;
|
|
1096
1760
|
return () => {
|
|
@@ -1392,6 +2056,10 @@ var PageUseGlowElement = class extends LitElement {
|
|
|
1392
2056
|
}
|
|
1393
2057
|
};
|
|
1394
2058
|
|
|
2059
|
+
//#endregion
|
|
2060
|
+
//#region package.json
|
|
2061
|
+
var version = "0.0.5";
|
|
2062
|
+
|
|
1395
2063
|
//#endregion
|
|
1396
2064
|
//#region src/ui/drag.ts
|
|
1397
2065
|
/** 判定为拖拽而非点击的最小位移(px) */
|
|
@@ -1416,6 +2084,7 @@ function createDrag(options) {
|
|
|
1416
2084
|
if (options.skipInteractive !== false) {
|
|
1417
2085
|
if (event.composedPath()[0]?.closest?.("button, input, textarea, select, a, label, [data-no-drag]")) return;
|
|
1418
2086
|
}
|
|
2087
|
+
const tapTarget = event.composedPath()[0];
|
|
1419
2088
|
const handle = event.currentTarget;
|
|
1420
2089
|
const rect = el.getBoundingClientRect();
|
|
1421
2090
|
const startX = event.clientX;
|
|
@@ -1441,7 +2110,7 @@ function createDrag(options) {
|
|
|
1441
2110
|
try {
|
|
1442
2111
|
handle.releasePointerCapture(event.pointerId);
|
|
1443
2112
|
} catch {}
|
|
1444
|
-
if (!moved) options.onTap?.();
|
|
2113
|
+
if (!moved) options.onTap?.(tapTarget);
|
|
1445
2114
|
};
|
|
1446
2115
|
handle.setPointerCapture(event.pointerId);
|
|
1447
2116
|
handle.addEventListener("pointermove", onMove);
|
|
@@ -1461,6 +2130,89 @@ function createDrag(options) {
|
|
|
1461
2130
|
};
|
|
1462
2131
|
}
|
|
1463
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
|
+
|
|
1464
2216
|
//#endregion
|
|
1465
2217
|
//#region src/ui/presets.ts
|
|
1466
2218
|
/** 通用示例:不依赖具体站点,覆盖详情、标签、返回等典型操作 */
|
|
@@ -1478,8 +2230,11 @@ const PAGE_USE_STATIC_PRESETS = [
|
|
|
1478
2230
|
task: "概括当前页面展示的主要内容与可执行的操作"
|
|
1479
2231
|
}
|
|
1480
2232
|
];
|
|
1481
|
-
/**
|
|
1482
|
-
|
|
2233
|
+
/**
|
|
2234
|
+
* 取前 N 个可见叶子菜单,生成「打开 XX」快捷指令。
|
|
2235
|
+
* exclude 中的 task 被跳过且不占名额,由后续菜单补位(用户隐藏菜单指令时使用)。
|
|
2236
|
+
*/
|
|
2237
|
+
function buildMenuPresets(menus, limit = 8, exclude) {
|
|
1483
2238
|
const out = [];
|
|
1484
2239
|
const walk = (list) => {
|
|
1485
2240
|
for (const node of list) {
|
|
@@ -1487,15 +2242,51 @@ function buildMenuPresets(menus, limit = 8) {
|
|
|
1487
2242
|
if (node.hidden) continue;
|
|
1488
2243
|
const children = (node.children || []).filter((child) => !child.hidden);
|
|
1489
2244
|
if (children.length) walk(children);
|
|
1490
|
-
else
|
|
1491
|
-
|
|
1492
|
-
task
|
|
1493
|
-
|
|
2245
|
+
else {
|
|
2246
|
+
const task = `打开「${node.label}」页面`;
|
|
2247
|
+
if (!exclude?.has(task)) out.push({
|
|
2248
|
+
label: node.label,
|
|
2249
|
+
task
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
1494
2252
|
}
|
|
1495
2253
|
};
|
|
1496
2254
|
walk(menus || []);
|
|
1497
2255
|
return out;
|
|
1498
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
|
+
}
|
|
1499
2290
|
|
|
1500
2291
|
//#endregion
|
|
1501
2292
|
//#region src/ui/styles.ts
|
|
@@ -1514,6 +2305,11 @@ const launcherStyles = css`
|
|
|
1514
2305
|
--text-3: #8a94a6;
|
|
1515
2306
|
--border: #e6eaf0;
|
|
1516
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
|
+
);
|
|
1517
2313
|
font-family: system-ui, -apple-system, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
1518
2314
|
font-size: 13px;
|
|
1519
2315
|
line-height: 1.5;
|
|
@@ -1570,59 +2366,181 @@ const launcherStyles = css`
|
|
|
1570
2366
|
* 运行期蒙层 z-index 为 2147483641、内置面板为 2147483642;
|
|
1571
2367
|
* 入口与面板须高于两者,否则任务执行中无法点「终止」或回答模型提问。
|
|
1572
2368
|
*/
|
|
2369
|
+
/*
|
|
2370
|
+
* 入口按钮:多色径向渐变放大到 300%,循环移动 background-position 形成流光;
|
|
2371
|
+
* ::before 在内缩 3px 处叠一层 14% 黑:压暗文字背后的高亮色段(黄、青)以保证白字清晰,
|
|
2372
|
+
* 外圈 3px 保持原色,形成渐变描边质感。
|
|
2373
|
+
* 渐变见 :host 的 --flow-gradient。
|
|
2374
|
+
*/
|
|
1573
2375
|
.fab {
|
|
1574
2376
|
position: fixed;
|
|
1575
2377
|
z-index: 2147483643;
|
|
1576
2378
|
display: flex;
|
|
1577
2379
|
align-items: center;
|
|
1578
2380
|
gap: 6px;
|
|
1579
|
-
padding: 8px
|
|
2381
|
+
padding: 8px 16px;
|
|
1580
2382
|
border: none;
|
|
1581
|
-
border-radius:
|
|
1582
|
-
background: var(--
|
|
2383
|
+
border-radius: 999px;
|
|
2384
|
+
background: var(--flow-gradient) 0 0 / 300% 300%;
|
|
1583
2385
|
color: #fff;
|
|
1584
2386
|
font: inherit;
|
|
1585
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);
|
|
1586
2392
|
cursor: grab;
|
|
1587
2393
|
touch-action: none;
|
|
1588
|
-
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;
|
|
1589
2412
|
}
|
|
1590
2413
|
.fab:active {
|
|
1591
2414
|
cursor: grabbing;
|
|
1592
2415
|
}
|
|
2416
|
+
/* 执行中:流速加快(15s → 10s)并外发光 */
|
|
1593
2417
|
.fab.running {
|
|
1594
|
-
|
|
1595
|
-
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);
|
|
1596
2422
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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
|
+
}
|
|
2439
|
+
}
|
|
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 {
|
|
2463
|
+
animation: blink 1s infinite;
|
|
2464
|
+
}
|
|
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;
|
|
1602
2471
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
2472
|
+
@keyframes robot-blink {
|
|
2473
|
+
0%,
|
|
2474
|
+
90%,
|
|
2475
|
+
100% {
|
|
2476
|
+
transform: scaleY(1);
|
|
2477
|
+
}
|
|
2478
|
+
95% {
|
|
2479
|
+
transform: scaleY(0.15);
|
|
2480
|
+
}
|
|
1605
2481
|
}
|
|
1606
|
-
|
|
2482
|
+
/*
|
|
2483
|
+
* 面板:高透毛玻璃 + 2px 流光边框。
|
|
2484
|
+
* 面板本体为半透明白底并对背后页面做模糊(backdrop-filter);
|
|
2485
|
+
* 边框由 ::before 绘制:整块铺流光渐变,再用遮罩挖掉 content-box,只留外圈 2px,
|
|
2486
|
+
* 因此内部保持透明,不会透出渐变。边框与入口按钮使用同一套渐变与移动轨迹(fab-shining)。
|
|
2487
|
+
*/
|
|
1607
2488
|
.panel {
|
|
1608
2489
|
position: fixed;
|
|
1609
2490
|
z-index: 2147483643;
|
|
1610
|
-
/* width / height 由 panelStyle
|
|
2491
|
+
/* width / height 由 panelStyle 按入口位置计算(border-box,内边距计入宽高) */
|
|
1611
2492
|
display: flex;
|
|
1612
2493
|
flex-direction: column;
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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);
|
|
1616
2503
|
overflow: hidden;
|
|
1617
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
|
+
}
|
|
1618
2534
|
|
|
1619
2535
|
.head {
|
|
1620
2536
|
display: flex;
|
|
1621
2537
|
align-items: center;
|
|
1622
2538
|
gap: 8px;
|
|
1623
2539
|
padding: 10px 12px;
|
|
1624
|
-
|
|
1625
|
-
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);
|
|
1626
2544
|
cursor: grab;
|
|
1627
2545
|
touch-action: none;
|
|
1628
2546
|
user-select: none;
|
|
@@ -1654,6 +2572,8 @@ const launcherStyles = css`
|
|
|
1654
2572
|
.title {
|
|
1655
2573
|
flex: 1 0 auto;
|
|
1656
2574
|
font-weight: 600;
|
|
2575
|
+
/* 连续点击标题打开元信息面板,避免选中文字 */
|
|
2576
|
+
user-select: none;
|
|
1657
2577
|
}
|
|
1658
2578
|
.status {
|
|
1659
2579
|
min-width: 0;
|
|
@@ -1664,6 +2584,48 @@ const launcherStyles = css`
|
|
|
1664
2584
|
white-space: nowrap;
|
|
1665
2585
|
}
|
|
1666
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
|
+
|
|
1667
2629
|
/* 占满头部与输入区之间的剩余高度(min-height:0 允许在 flex 列中收缩) */
|
|
1668
2630
|
.fill {
|
|
1669
2631
|
flex: 1;
|
|
@@ -1687,8 +2649,28 @@ const launcherStyles = css`
|
|
|
1687
2649
|
color: var(--text-3);
|
|
1688
2650
|
font-size: 12px;
|
|
1689
2651
|
}
|
|
1690
|
-
.
|
|
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 {
|
|
1691
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;
|
|
1692
2674
|
font-size: 12px;
|
|
1693
2675
|
word-break: break-all;
|
|
1694
2676
|
}
|
|
@@ -1937,7 +2919,8 @@ const launcherStyles = css`
|
|
|
1937
2919
|
gap: 8px;
|
|
1938
2920
|
padding: 10px 12px 12px;
|
|
1939
2921
|
border-top: 1px solid var(--border);
|
|
1940
|
-
|
|
2922
|
+
border-radius: 0 0 10px 10px;
|
|
2923
|
+
background: rgba(250, 251, 252, 0.5);
|
|
1941
2924
|
}
|
|
1942
2925
|
/* 悬浮在输入区上沿(以 .composer 为定位参照) */
|
|
1943
2926
|
.jump-latest {
|
|
@@ -1974,6 +2957,141 @@ const launcherStyles = css`
|
|
|
1974
2957
|
background: var(--bg-soft);
|
|
1975
2958
|
cursor: not-allowed;
|
|
1976
2959
|
}
|
|
2960
|
+
/* ---------- 演示播报设置 ---------- */
|
|
2961
|
+
.voice-warning {
|
|
2962
|
+
font-size: 12px;
|
|
2963
|
+
}
|
|
2964
|
+
/* 与复选框文字左对齐(复选框宽 13px + 间距 8px) */
|
|
2965
|
+
.voice-options {
|
|
2966
|
+
display: flex;
|
|
2967
|
+
flex-direction: column;
|
|
2968
|
+
gap: 8px;
|
|
2969
|
+
margin-left: 21px;
|
|
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
|
+
|
|
3064
|
+
.field {
|
|
3065
|
+
display: flex;
|
|
3066
|
+
flex-direction: column;
|
|
3067
|
+
gap: 4px;
|
|
3068
|
+
}
|
|
3069
|
+
.field-label {
|
|
3070
|
+
color: var(--text-3);
|
|
3071
|
+
font-size: 12px;
|
|
3072
|
+
}
|
|
3073
|
+
.range {
|
|
3074
|
+
width: 100%;
|
|
3075
|
+
margin: 0;
|
|
3076
|
+
accent-color: var(--primary);
|
|
3077
|
+
}
|
|
3078
|
+
select.input {
|
|
3079
|
+
height: 28px;
|
|
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
|
+
}
|
|
3094
|
+
|
|
1977
3095
|
.input-group {
|
|
1978
3096
|
display: flex;
|
|
1979
3097
|
gap: 6px;
|
|
@@ -1995,7 +3113,10 @@ const launcherStyles = css`
|
|
|
1995
3113
|
overflow-y: auto;
|
|
1996
3114
|
border: 1px solid var(--border);
|
|
1997
3115
|
border-radius: 8px;
|
|
1998
|
-
|
|
3116
|
+
/* 浮在执行记录上方,透明度低于面板以保证列表可读 */
|
|
3117
|
+
background: rgba(255, 255, 255, 0.88);
|
|
3118
|
+
-webkit-backdrop-filter: blur(12px);
|
|
3119
|
+
backdrop-filter: blur(12px);
|
|
1999
3120
|
}
|
|
2000
3121
|
.history-item {
|
|
2001
3122
|
display: flex;
|
|
@@ -2039,6 +3160,12 @@ const launcherStyles = css`
|
|
|
2039
3160
|
opacity: 0.25;
|
|
2040
3161
|
}
|
|
2041
3162
|
}
|
|
3163
|
+
@keyframes toast-in {
|
|
3164
|
+
from {
|
|
3165
|
+
opacity: 0;
|
|
3166
|
+
transform: translate(-50%, 6px);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
2042
3169
|
@keyframes spin {
|
|
2043
3170
|
to {
|
|
2044
3171
|
transform: rotate(360deg);
|
|
@@ -2051,7 +3178,7 @@ const launcherStyles = css`
|
|
|
2051
3178
|
/**
|
|
2052
3179
|
* <tz-page-use-launcher>:调试入口(可拖拽浮动按钮 + 指令面板)。
|
|
2053
3180
|
* 与其它智能体共用 core/service 的单例状态:其它智能体下达的任务同样在此展示,可随时终止。
|
|
2054
|
-
*
|
|
3181
|
+
* page-agent 自带面板固定隐藏、元素编号高亮默认隐藏(设置里可开回),进度与提问改由本面板承接。
|
|
2055
3182
|
*
|
|
2056
3183
|
* 宿主元素须带 data-page-agent-ignore="true"(由 mountPageUseUI 设置):
|
|
2057
3184
|
* page-controller 在该属性上直接跳过整棵子树(含 shadow root),面板不会被模型当作页面元素。
|
|
@@ -2068,9 +3195,9 @@ const STATUS_LABELS = {
|
|
|
2068
3195
|
error: "出错",
|
|
2069
3196
|
stopped: "已终止"
|
|
2070
3197
|
};
|
|
2071
|
-
/** 入口默认位置:距视口右边/下边的距离(px
|
|
2072
|
-
const FAB_DEFAULT_RIGHT =
|
|
2073
|
-
const FAB_DEFAULT_BOTTOM =
|
|
3198
|
+
/** 入口默认位置:距视口右边/下边的距离(px);面板展开时与入口对齐于同一角,即面板默认贴右下角 */
|
|
3199
|
+
const FAB_DEFAULT_RIGHT = 24;
|
|
3200
|
+
const FAB_DEFAULT_BOTTOM = 24;
|
|
2074
3201
|
/** 面板宽度(px),与入口对齐及视口夹取计算共用 */
|
|
2075
3202
|
const PANEL_WIDTH = 380;
|
|
2076
3203
|
/** 面板期望高度(px),受视口 72vh 与锚点剩余空间约束 */
|
|
@@ -2078,6 +3205,28 @@ const PANEL_HEIGHT = 560;
|
|
|
2078
3205
|
/** 日志区距底部小于该值(px)视为贴底 */
|
|
2079
3206
|
const STICK_THRESHOLD = 24;
|
|
2080
3207
|
const TEXTAREA_MIN_ROWS = 2;
|
|
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 为正常语速,过快或过慢时中文语音难以听清;注入引擎自行换算为服务参数) */
|
|
3227
|
+
const VOICE_RATE_MIN = .5;
|
|
3228
|
+
const VOICE_RATE_MAX = 2;
|
|
3229
|
+
const VOICE_RATE_STEP = .05;
|
|
2081
3230
|
/**
|
|
2082
3231
|
* 步骤 → 时间线节点颜色:失败/报错为 error,完成为 success,
|
|
2083
3232
|
* 向用户提问与重试为 warning,其余普通操作为 info(主色)。
|
|
@@ -2100,13 +3249,28 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2100
3249
|
}
|
|
2101
3250
|
set config(value) {
|
|
2102
3251
|
this._config = value || {};
|
|
3252
|
+
if (this.isConnected) this.syncPanelName();
|
|
2103
3253
|
this.requestUpdate();
|
|
2104
3254
|
}
|
|
3255
|
+
/** 撤销本面板登记的播报用面板名称 */
|
|
3256
|
+
unregisterPanelName = null;
|
|
3257
|
+
/** 把面板标题登记为播报截断提示中的面板名称(「详细内容请查看「标题」」) */
|
|
3258
|
+
syncPanelName() {
|
|
3259
|
+
this.unregisterPanelName?.();
|
|
3260
|
+
this.unregisterPanelName = registerNarrationPanelName(this._config.title || "Page Use");
|
|
3261
|
+
}
|
|
2105
3262
|
open = false;
|
|
2106
|
-
|
|
3263
|
+
view = "main";
|
|
3264
|
+
/** 标题点击时间戳(毫秒),用于元信息面板解锁的滑动窗口计数;不持久化 */
|
|
3265
|
+
titleTaps = [];
|
|
2107
3266
|
task = "";
|
|
2108
3267
|
answer = "";
|
|
2109
3268
|
tray = null;
|
|
3269
|
+
/** 快捷指令编辑视图中正在编辑的表单;同一时刻只有一个,离开编辑视图时清空 */
|
|
3270
|
+
presetDraft = null;
|
|
3271
|
+
/** 面板内轻提示文字;空串为不显示 */
|
|
3272
|
+
toastText = "";
|
|
3273
|
+
toastTimer = null;
|
|
2110
3274
|
/**
|
|
2111
3275
|
* 日志区跟随滚动:默认贴底跟随新内容;用户上翻后停止跟随,
|
|
2112
3276
|
* 回到底部附近(阈值内)或点击「最新」后恢复。
|
|
@@ -2121,8 +3285,8 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2121
3285
|
* 因此在可见时测量并保留最近一次结果;初值为「Page Use」文案下的近似尺寸。
|
|
2122
3286
|
*/
|
|
2123
3287
|
fabSize = {
|
|
2124
|
-
width:
|
|
2125
|
-
height:
|
|
3288
|
+
width: 122,
|
|
3289
|
+
height: 36
|
|
2126
3290
|
};
|
|
2127
3291
|
viewport = {
|
|
2128
3292
|
width: window.innerWidth,
|
|
@@ -2138,6 +3302,12 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2138
3302
|
alignBottom: true
|
|
2139
3303
|
};
|
|
2140
3304
|
wasRunning = false;
|
|
3305
|
+
/** 入口按钮上次渲染时是否为「执行中」文案;文案切换时用于触发宽度过渡 */
|
|
3306
|
+
fabShowsRunning = false;
|
|
3307
|
+
/** 文案切换前的入口宽度(px);null 表示本次更新不需要宽度过渡 */
|
|
3308
|
+
fabFromWidth = null;
|
|
3309
|
+
/** 进行中宽度过渡的结束回调注销函数;过渡中再次切换时先注销,避免旧回调提前清除宽度 */
|
|
3310
|
+
fabWidthCleanup = null;
|
|
2141
3311
|
cleanups = [];
|
|
2142
3312
|
get fabEl() {
|
|
2143
3313
|
return this.renderRoot.querySelector(".fab");
|
|
@@ -2149,7 +3319,7 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2149
3319
|
return this.renderRoot.querySelector(".log");
|
|
2150
3320
|
}
|
|
2151
3321
|
get textareaEl() {
|
|
2152
|
-
return this.renderRoot.querySelector(".textarea");
|
|
3322
|
+
return this.renderRoot.querySelector(".composer .textarea");
|
|
2153
3323
|
}
|
|
2154
3324
|
fabDrag = createDrag({
|
|
2155
3325
|
getEl: () => this.fabEl,
|
|
@@ -2167,6 +3337,9 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2167
3337
|
panelDrag = createDrag({
|
|
2168
3338
|
getEl: () => this.panelEl,
|
|
2169
3339
|
getPos: () => null,
|
|
3340
|
+
onTap: (target) => {
|
|
3341
|
+
if (target?.closest?.(".title")) this.onTitleTap();
|
|
3342
|
+
},
|
|
2170
3343
|
setPos: ({ x, y }) => {
|
|
2171
3344
|
const el = this.panelEl;
|
|
2172
3345
|
if (!el) return;
|
|
@@ -2181,25 +3354,102 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2181
3354
|
connectedCallback() {
|
|
2182
3355
|
super.connectedCallback();
|
|
2183
3356
|
this.wasRunning = isRunning();
|
|
3357
|
+
const query = readLauncherQuery();
|
|
3358
|
+
if (query !== null) pageUseSettings.update({ launcherVisible: query === "1" || query === "true" });
|
|
2184
3359
|
this.cleanups = [
|
|
2185
3360
|
pageUseStore.subscribe((state) => this.onStateChange(state)),
|
|
2186
3361
|
pageUseSettings.store.subscribe(() => this.requestUpdate()),
|
|
2187
3362
|
pageUseHistory.store.subscribe(() => this.requestUpdate()),
|
|
2188
|
-
|
|
3363
|
+
pageUsePresets.store.subscribe(() => this.requestUpdate()),
|
|
3364
|
+
narrationMute.subscribe(() => this.requestUpdate()),
|
|
3365
|
+
previewStore.subscribe(() => this.requestUpdate()),
|
|
3366
|
+
registerLauncherOpener(() => void this.expand()),
|
|
3367
|
+
registerQuestionUi(),
|
|
3368
|
+
this.watchVoices()
|
|
2189
3369
|
];
|
|
3370
|
+
this.syncPanelName();
|
|
2190
3371
|
window.addEventListener("resize", this.onResize);
|
|
2191
3372
|
}
|
|
2192
3373
|
disconnectedCallback() {
|
|
2193
3374
|
super.disconnectedCallback();
|
|
2194
3375
|
this.cleanups.forEach((off) => off());
|
|
2195
3376
|
this.cleanups = [];
|
|
3377
|
+
this.unregisterPanelName?.();
|
|
3378
|
+
this.unregisterPanelName = null;
|
|
3379
|
+
stopPreview();
|
|
2196
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);
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Chrome 的语音列表异步加载,首次读取可能为空:加载完成后刷新语音下拉框。
|
|
3395
|
+
* 挂载时宿主可能尚未注入引擎,因此不按引擎判断;注入引擎时该事件只触发一次无害的重绘。
|
|
3396
|
+
*/
|
|
3397
|
+
watchVoices() {
|
|
3398
|
+
if (!isSpeechSupported()) return () => {};
|
|
3399
|
+
const synth = globalThis.speechSynthesis;
|
|
3400
|
+
const onChange = () => this.requestUpdate();
|
|
3401
|
+
synth.addEventListener?.("voiceschanged", onChange);
|
|
3402
|
+
return () => synth.removeEventListener?.("voiceschanged", onChange);
|
|
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;
|
|
2197
3446
|
}
|
|
2198
3447
|
firstUpdated() {
|
|
2199
3448
|
this.measureFab();
|
|
2200
3449
|
}
|
|
2201
3450
|
updated(_changed) {
|
|
2202
3451
|
this.autosizeTextarea();
|
|
3452
|
+
this.animateFabWidth();
|
|
2203
3453
|
const state = pageUseStore.get();
|
|
2204
3454
|
const signature = [
|
|
2205
3455
|
state.currentTask,
|
|
@@ -2287,7 +3537,61 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2287
3537
|
style.height = `min(${PANEL_HEIGHT}px, 72vh, ${Math.max(240, vh - offsetY - 8)}px)`;
|
|
2288
3538
|
return toStyle(style);
|
|
2289
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
|
+
}
|
|
2290
3593
|
async collapse() {
|
|
3594
|
+
stopPreview();
|
|
2291
3595
|
this.open = false;
|
|
2292
3596
|
this.requestUpdate();
|
|
2293
3597
|
await this.updateComplete;
|
|
@@ -2376,23 +3680,15 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2376
3680
|
...this._config.sourceLabels
|
|
2377
3681
|
}[source] || source;
|
|
2378
3682
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
endpointLabel() {
|
|
2389
|
-
const baseURL = getPageUseConfig()?.llm.baseURL;
|
|
2390
|
-
if (!baseURL) return "未配置";
|
|
2391
|
-
try {
|
|
2392
|
-
return new URL(baseURL).host;
|
|
2393
|
-
} catch {
|
|
2394
|
-
return baseURL;
|
|
2395
|
-
}
|
|
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
|
+
});
|
|
2396
3692
|
}
|
|
2397
3693
|
renderChips(presets, accent = false) {
|
|
2398
3694
|
return presets.map((preset) => html`<button
|
|
@@ -2404,18 +3700,31 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2404
3700
|
${preset.label}
|
|
2405
3701
|
</button>`);
|
|
2406
3702
|
}
|
|
3703
|
+
/** 分组顺序:我的 → 常用 → 复杂场景;空分组不显示,全部为空时提示去设置中维护 */
|
|
2407
3704
|
renderPresetGroups() {
|
|
2408
|
-
const scenarios = this.
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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>`);
|
|
2419
3728
|
}
|
|
2420
3729
|
renderCheckbox(key, label, hint, disabled = false) {
|
|
2421
3730
|
const settings = pageUseSettings.get();
|
|
@@ -2432,21 +3741,282 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2432
3741
|
renderSettings() {
|
|
2433
3742
|
const settings = pageUseSettings.get();
|
|
2434
3743
|
const config = getPageUseConfig();
|
|
3744
|
+
const status = pageUseStore.get().status;
|
|
2435
3745
|
return html`<div class="fill">
|
|
2436
3746
|
<div class="settings-body">
|
|
2437
|
-
|
|
3747
|
+
<div class="settings-status">当前状态:${STATUS_LABELS[status] || status}</div>
|
|
2438
3748
|
${this.renderCheckbox("showHighlights", "显示元素编号高亮", "运行时页面上带数字的彩色框")}
|
|
2439
3749
|
${this.renderCheckbox("enableMask", "运行时蒙层防误触", "下次任务开始生效")}
|
|
2440
|
-
${this.renderCheckbox("customMaskEffect", "定制光晕与光标配色", "关闭后恢复 page-agent 默认效果", !settings.enableMask)}
|
|
2441
3750
|
${config?.beforeTask ? this.renderCheckbox("enableBeforeTask", config.beforeTaskLabel) : nothing}
|
|
2442
3751
|
${this.renderCheckbox("showStepDetails", "步骤显示模型思考与返回")}
|
|
2443
|
-
|
|
3752
|
+
${this.renderVoiceSettings(settings)}
|
|
3753
|
+
<div class="row settings-actions">
|
|
2444
3754
|
<button type="button" class="btn" @click=${() => pageUseSettings.resetPositions()}>
|
|
2445
3755
|
复位窗口
|
|
2446
3756
|
</button>
|
|
2447
|
-
<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>
|
|
3763
|
+
</div>
|
|
3764
|
+
</div>
|
|
3765
|
+
</div>`;
|
|
3766
|
+
}
|
|
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>
|
|
2448
3901
|
</div>
|
|
2449
|
-
|
|
3902
|
+
</div>
|
|
3903
|
+
</div>`;
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* 演示播报:总开关关闭时只显示开关;开启后依次为引擎下拉(可选引擎多于一个时)、
|
|
3907
|
+
* 语速、音色下拉与试听按钮(无音色列表时只有试听按钮)、不可用 / 无中文语音提示。
|
|
3908
|
+
*/
|
|
3909
|
+
renderVoiceSettings(settings) {
|
|
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 ? "未检测到中文语音,将使用浏览器默认语音播报" : "";
|
|
3933
|
+
return html`
|
|
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">
|
|
3958
|
+
<span class="field-label">语速 ${settings.voiceRate.toFixed(2)}</span>
|
|
3959
|
+
<input
|
|
3960
|
+
class="range"
|
|
3961
|
+
type="range"
|
|
3962
|
+
min=${VOICE_RATE_MIN}
|
|
3963
|
+
max=${VOICE_RATE_MAX}
|
|
3964
|
+
step=${VOICE_RATE_STEP}
|
|
3965
|
+
.value=${String(settings.voiceRate)}
|
|
3966
|
+
@input=${(e) => pageUseSettings.update({ voiceRate: Number(e.target.value) })}
|
|
3967
|
+
/>
|
|
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>
|
|
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>
|
|
2450
4020
|
</div>
|
|
2451
4021
|
</div>`;
|
|
2452
4022
|
}
|
|
@@ -2608,9 +4178,11 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2608
4178
|
render() {
|
|
2609
4179
|
const state = pageUseStore.get();
|
|
2610
4180
|
const settings = pageUseSettings.get();
|
|
4181
|
+
if (!this.isVisible(settings)) return nothing;
|
|
2611
4182
|
const running = isRunning(state);
|
|
2612
4183
|
const title = this._config.title || "Page Use";
|
|
2613
4184
|
const elapsed = getElapsedSeconds(state);
|
|
4185
|
+
const { muted } = narrationMute.get();
|
|
2614
4186
|
const isLogEmpty = !running && !state.currentTask && !state.steps.length && !state.errorMessage;
|
|
2615
4187
|
return html`
|
|
2616
4188
|
<button
|
|
@@ -2618,36 +4190,56 @@ var PageUseLauncherElement = class extends LitElement {
|
|
|
2618
4190
|
class="fab ${running ? "running" : ""}"
|
|
2619
4191
|
?hidden=${this.open}
|
|
2620
4192
|
style=${this.fabStyle(settings)}
|
|
2621
|
-
title=${running ? `执行中:${state.activityText || state.currentTask}` : `打开 ${title}
|
|
4193
|
+
title=${running ? `执行中:${state.activityText || state.currentTask}` : `打开 ${title}`}
|
|
2622
4194
|
@pointerdown=${this.fabDrag.onPointerDown}
|
|
2623
4195
|
>
|
|
2624
|
-
<span class="fab-
|
|
4196
|
+
<span class="fab-icon">${robotIcon}</span>
|
|
2625
4197
|
<span>${running ? "执行中" : title}</span>
|
|
2626
4198
|
</button>
|
|
2627
4199
|
|
|
2628
|
-
<section
|
|
4200
|
+
<section
|
|
4201
|
+
class="panel ${running ? "running" : ""}"
|
|
4202
|
+
?hidden=${!this.open}
|
|
4203
|
+
style=${this.open ? this.panelStyle(settings) : ""}
|
|
4204
|
+
>
|
|
2629
4205
|
<header class="head" @pointerdown=${this.panelDrag.onPointerDown}>
|
|
2630
|
-
<span class="dot ${state.status}"></span>
|
|
4206
|
+
<span class="dot ${state.status}" title=${STATUS_LABELS[state.status] || state.status}></span>
|
|
2631
4207
|
<span class="title">${title}</span>
|
|
2632
4208
|
<span class="status">
|
|
2633
|
-
${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` : ""}`}
|
|
2634
4210
|
</span>
|
|
2635
|
-
<
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
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="关闭(可调用 showPageUseLauncher 或加 URL 参数 pageUseDebug=1 重新打开)"
|
|
4235
|
+
@click=${() => this.close()}
|
|
4236
|
+
>
|
|
4237
|
+
${closeIcon}
|
|
4238
|
+
</button>
|
|
4239
|
+
</div>
|
|
2649
4240
|
</header>
|
|
2650
|
-
${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}
|
|
2651
4243
|
</section>
|
|
2652
4244
|
`;
|
|
2653
4245
|
}
|
|
@@ -2690,4 +4282,4 @@ function mountPageUseUI(options = {}) {
|
|
|
2690
4282
|
}
|
|
2691
4283
|
|
|
2692
4284
|
//#endregion
|
|
2693
|
-
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 };
|