@tanzerfe/page-use 0.0.12 → 0.0.14

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.
@@ -355,6 +355,8 @@ function normalizeOptions(options) {
355
355
  model: (llm.model || "").trim() || "deepseek-chat",
356
356
  maxSteps: llm.maxSteps && llm.maxSteps > 0 ? llm.maxSteps : 20,
357
357
  disableNamedToolChoice: Boolean(llm.disableNamedToolChoice),
358
+ stream: llm.stream === true,
359
+ streamOptions: typeof llm.streamOptions?.includeUsage === "boolean" ? { includeUsage: llm.streamOptions.includeUsage } : {},
358
360
  extraBody: llm.extraBody && typeof llm.extraBody === "object" ? llm.extraBody : {},
359
361
  sessionHeader: resolveSessionHeader(llm.sessionHeader)
360
362
  },
@@ -366,6 +368,7 @@ function normalizeOptions(options) {
366
368
  routeHints: options.routeHints ?? {},
367
369
  layoutHint: options.layoutHint,
368
370
  beforeTask: options.beforeTask,
371
+ onUserSubmit: options.onUserSubmit,
369
372
  beforeTaskLabel: options.beforeTaskLabel || "任务开始前执行宿主预处理",
370
373
  storageKeyPrefix: options.storageKeyPrefix || "tz-page-use",
371
374
  pageAgentOptions: options.pageAgentOptions ?? {},
@@ -605,19 +608,17 @@ const PRESENT_PAGE_TOOL_NAME = "present_page";
605
608
  //#endregion
606
609
  //#region src/voice/preview.ts
607
610
  /**
608
- * 音色试听:用当前引擎、音色与语速播报一句固定文本,供设置页选择音色时对比。
611
+ * 音色试听:用当前引擎、音色与语速播报业务应用名,供设置页选择音色时对比。
609
612
  * 不经 speaker 队列:试听是单次、可中止的调用,不参与待播与抢占,也不受本次任务静音影响。
610
613
  * 与任务播报互斥:launcher 在任务执行中禁用试听,narration 在任务开始时调用 stopPreview()。
611
614
  */
612
- /** 试听文本 */
613
- const PREVIEW_TEXT = "您好,我是页面智能体。";
614
615
  const store$3 = createStore({ playing: false });
615
616
  /** 试听状态(playing:是否正在试听),供设置页切换「试听 / 停止」按钮 */
616
617
  const previewStore = store$3;
617
618
  /** 当前试听的中止控制;空闲时为 null */
618
619
  let controller = null;
619
620
  /** 开始试听:先中止上一次试听;引擎 speak 结束(播完、失败或被中止)后复位状态 */
620
- function startPreview(engine, options) {
621
+ function startPreview(engine, options, appName) {
621
622
  stopPreview();
622
623
  if (!engine.isAvailable()) return;
623
624
  const current = new AbortController();
@@ -625,7 +626,7 @@ function startPreview(engine, options) {
625
626
  store$3.set({ playing: true });
626
627
  let speaking;
627
628
  try {
628
- speaking = engine.speak(PREVIEW_TEXT, {
629
+ speaking = engine.speak(`您好,我是${appName}。`, {
629
630
  ...options,
630
631
  signal: current.signal
631
632
  });
@@ -913,7 +914,7 @@ const SILENT_DWELL_MS = 2500;
913
914
  /** 讲解词的播报字数上限:高于讲解工具的讲解词上限,正常不截断 */
914
915
  const TOUR_SPEECH_MAX_CHARS$1 = 400;
915
916
  /** 当前运行的播报(startNarration 期间有效),供页面讲解逐段播报 */
916
- let active$1 = null;
917
+ let active$2 = null;
917
918
  /**
918
919
  * 调试面板名称(PageUseLauncherConfig.title),用于截断提示语「详细内容请查看「名称」」。
919
920
  * 由 launcher 挂载时登记、卸载时撤销;未挂载面板时为 null,提示语回退为「详细内容请查看面板」。
@@ -946,7 +947,7 @@ function startNarration({ subscribe, engines, speaker: injected }) {
946
947
  speaker,
947
948
  enabled
948
949
  };
949
- active$1 = current;
950
+ active$2 = current;
950
951
  const offEvents = subscribe((event) => {
951
952
  if (event.silent) return;
952
953
  switch (event.type) {
@@ -975,7 +976,7 @@ function startNarration({ subscribe, engines, speaker: injected }) {
975
976
  if (muted) speaker.clear();
976
977
  });
977
978
  return () => {
978
- if (active$1 === current) active$1 = null;
979
+ if (active$2 === current) active$2 = null;
979
980
  offEvents();
980
981
  offSettings();
981
982
  offMute();
@@ -1006,7 +1007,7 @@ function dwell(ms, signal) {
1006
1007
  async function narrate(text, signal) {
1007
1008
  if (signal?.aborted) return;
1008
1009
  const startedAt = Date.now();
1009
- const narration = active$1;
1010
+ const narration = active$2;
1010
1011
  if (narration?.enabled()) {
1011
1012
  const sentences = toSpeechText(text, { maxChars: TOUR_SPEECH_MAX_CHARS$1 });
1012
1013
  await narration.speaker.speakAndWait(sentences, signal);
@@ -1874,6 +1875,281 @@ function buildPageInstruction(options, outlineText = "", openedUrls = []) {
1874
1875
  return parts.length ? parts.join("\n") : null;
1875
1876
  }
1876
1877
 
1878
+ //#endregion
1879
+ //#region src/core/llmTransport.ts
1880
+ const MAX_STREAM_BYTES = 8388608;
1881
+ const MAX_STREAM_ITEMS = 32;
1882
+ function record(value) {
1883
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1884
+ }
1885
+ function invalid(message) {
1886
+ return /* @__PURE__ */ new Error(`[PageUse SSE] ${message}`);
1887
+ }
1888
+ /** 正式流式配置最后写入,extraBody/宿主变换不能单独开启无解析器的 SSE。 */
1889
+ function completionRequestBody(body, llm) {
1890
+ const { stream: _stream, stream_options: _options, ...rest } = body;
1891
+ return {
1892
+ ...rest,
1893
+ stream: llm.stream,
1894
+ ...llm.stream && typeof llm.streamOptions.includeUsage === "boolean" ? { stream_options: { include_usage: llm.streamOptions.includeUsage } } : {}
1895
+ };
1896
+ }
1897
+ function index(value) {
1898
+ if (!Number.isSafeInteger(value) || value < 0 || value >= MAX_STREAM_ITEMS) throw invalid("无效的 choice/tool_calls index");
1899
+ return value;
1900
+ }
1901
+ function appendFunction(target, delta) {
1902
+ if (!record(delta)) throw invalid("无效的工具 function");
1903
+ for (const field of ["name", "arguments"]) {
1904
+ const part = delta[field];
1905
+ if (part !== void 0) {
1906
+ if (typeof part !== "string") throw invalid(`工具 ${field} 不是字符串`);
1907
+ target[field] += part;
1908
+ }
1909
+ }
1910
+ }
1911
+ function validateFunction(call) {
1912
+ if (!call.name || !call.arguments) throw invalid("工具名称或参数不完整");
1913
+ try {
1914
+ JSON.parse(call.arguments);
1915
+ } catch {
1916
+ throw invalid("工具参数不是完整 JSON");
1917
+ }
1918
+ }
1919
+ /** 只有 [DONE] 与各 choice 的正常 finish_reason 均已收到,才交付完整响应。 */
1920
+ async function readEventStream(response, signal) {
1921
+ signal?.throwIfAborted();
1922
+ if (!response.body) throw invalid("响应没有可读流");
1923
+ const reader = response.body.getReader();
1924
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1925
+ const choices = /* @__PURE__ */ new Map();
1926
+ const metadata = { object: "chat.completion" };
1927
+ let usage;
1928
+ let buffer = "";
1929
+ let data = [];
1930
+ let eventName = "";
1931
+ let ended = false;
1932
+ let bytes = 0;
1933
+ const cancel = () => {
1934
+ reader.cancel().catch(() => {});
1935
+ };
1936
+ signal?.addEventListener("abort", cancel, { once: true });
1937
+ function acceptChunk(chunk) {
1938
+ if (!record(chunk)) throw invalid("事件不是 JSON 对象");
1939
+ if (chunk.error) throw invalid(record(chunk.error) && typeof chunk.error.message === "string" ? chunk.error.message.slice(0, 500) : "网关返回错误事件");
1940
+ for (const key of [
1941
+ "id",
1942
+ "model",
1943
+ "created",
1944
+ "system_fingerprint",
1945
+ "service_tier"
1946
+ ]) if (chunk[key] !== void 0) metadata[key] = chunk[key];
1947
+ if (chunk.usage != null) {
1948
+ if (!record(chunk.usage)) throw invalid("无效的 usage");
1949
+ usage = chunk.usage;
1950
+ }
1951
+ if (!Array.isArray(chunk.choices)) throw invalid("事件缺少 choices");
1952
+ for (const item of chunk.choices) {
1953
+ if (!record(item)) throw invalid("无效的 choice");
1954
+ const id = index(item.index);
1955
+ let pending = choices.get(id);
1956
+ if (!pending) {
1957
+ pending = {
1958
+ choice: {
1959
+ index: id,
1960
+ message: {
1961
+ role: "assistant",
1962
+ content: null
1963
+ },
1964
+ finish_reason: null
1965
+ },
1966
+ tools: /* @__PURE__ */ new Map()
1967
+ };
1968
+ choices.set(id, pending);
1969
+ }
1970
+ if (pending.choice.finish_reason !== null) throw invalid("choice 结束后仍收到增量");
1971
+ const delta = item.delta;
1972
+ if (!record(delta)) throw invalid("choice 缺少 delta");
1973
+ const message = pending.choice.message;
1974
+ if (delta.role != null && delta.role !== "assistant") throw invalid("无效的响应 role");
1975
+ if (delta.content != null) {
1976
+ if (typeof delta.content !== "string") throw invalid("content 不是字符串");
1977
+ message.content = (message.content ?? "") + delta.content;
1978
+ }
1979
+ if (delta.refusal) throw invalid("模型拒绝了请求");
1980
+ if (delta.function_call != null) {
1981
+ message.function_call ??= {
1982
+ name: "",
1983
+ arguments: ""
1984
+ };
1985
+ appendFunction(message.function_call, delta.function_call);
1986
+ }
1987
+ if (delta.tool_calls != null) {
1988
+ if (!Array.isArray(delta.tool_calls)) throw invalid("无效的 tool_calls");
1989
+ for (const tool of delta.tool_calls) {
1990
+ if (!record(tool)) throw invalid("无效的工具增量");
1991
+ const toolIndex = index(tool.index);
1992
+ let call = pending.tools.get(toolIndex);
1993
+ if (!call) {
1994
+ call = { function: {
1995
+ name: "",
1996
+ arguments: ""
1997
+ } };
1998
+ pending.tools.set(toolIndex, call);
1999
+ }
2000
+ for (const field of ["id", "type"]) if (tool[field] !== void 0) {
2001
+ if (typeof tool[field] !== "string" || call[field] !== void 0 && call[field] !== tool[field]) throw invalid(`工具 ${field} 不一致`);
2002
+ call[field] = tool[field];
2003
+ }
2004
+ if (tool.function !== void 0) appendFunction(call.function, tool.function);
2005
+ }
2006
+ }
2007
+ if (item.finish_reason != null) {
2008
+ if (![
2009
+ "stop",
2010
+ "tool_calls",
2011
+ "function_call",
2012
+ "length",
2013
+ "content_filter"
2014
+ ].includes(String(item.finish_reason))) throw invalid(`异常结束:${item.finish_reason}`);
2015
+ pending.choice.finish_reason = String(item.finish_reason);
2016
+ }
2017
+ }
2018
+ }
2019
+ function line(text) {
2020
+ if (text === "") {
2021
+ if (eventName === "error") throw invalid("网关返回 error 事件");
2022
+ if (data.length) {
2023
+ const payload = data.join("\n");
2024
+ if (payload.trim() === "[DONE]") ended = true;
2025
+ else {
2026
+ let chunk;
2027
+ try {
2028
+ chunk = JSON.parse(payload);
2029
+ } catch {
2030
+ throw invalid("事件不是完整 JSON");
2031
+ }
2032
+ acceptChunk(chunk);
2033
+ }
2034
+ }
2035
+ data = [];
2036
+ eventName = "";
2037
+ return;
2038
+ }
2039
+ if (text.startsWith(":")) return;
2040
+ const colon = text.indexOf(":");
2041
+ const field = colon < 0 ? text : text.slice(0, colon);
2042
+ const value = colon < 0 ? "" : text.slice(colon + 1).replace(/^ /, "");
2043
+ if (field === "data") data.push(value);
2044
+ if (field === "event") eventName = value;
2045
+ }
2046
+ function consume(text, final = false) {
2047
+ buffer += text;
2048
+ while (!ended) {
2049
+ const end = buffer.search(/[\r\n]/);
2050
+ if (end < 0 || !final && buffer[end] === "\r" && end === buffer.length - 1) break;
2051
+ const size = buffer[end] === "\r" && buffer[end + 1] === "\n" ? 2 : 1;
2052
+ const current = buffer.slice(0, end);
2053
+ buffer = buffer.slice(end + size);
2054
+ line(current);
2055
+ }
2056
+ }
2057
+ try {
2058
+ while (!ended) {
2059
+ signal?.throwIfAborted();
2060
+ const next = await reader.read();
2061
+ signal?.throwIfAborted();
2062
+ if (next.done) {
2063
+ consume(decoder.decode(), true);
2064
+ break;
2065
+ }
2066
+ bytes += next.value.byteLength;
2067
+ if (bytes > MAX_STREAM_BYTES) throw invalid("响应超过 8 MiB 限制");
2068
+ consume(decoder.decode(next.value, { stream: true }));
2069
+ }
2070
+ signal?.throwIfAborted();
2071
+ if (!ended || !choices.size || !choices.has(0)) throw invalid("流在完整结束标记前中断");
2072
+ const result = [];
2073
+ for (const [id, pending] of [...choices].sort(([a], [b]) => a - b)) {
2074
+ if (id !== result.length || !pending.choice.finish_reason) throw invalid("choice 不完整");
2075
+ const failed = ["length", "content_filter"].includes(pending.choice.finish_reason);
2076
+ if (pending.tools.size) pending.choice.message.tool_calls = [...pending.tools].sort(([a], [b]) => a - b).map(([id, call], position) => {
2077
+ if (id !== position || !call.id || call.type !== "function") throw invalid("工具调用不完整");
2078
+ if (!failed) validateFunction(call.function);
2079
+ return call;
2080
+ });
2081
+ if (!failed && pending.choice.message.function_call) validateFunction(pending.choice.message.function_call);
2082
+ result.push(pending.choice);
2083
+ }
2084
+ return {
2085
+ ...metadata,
2086
+ choices: result,
2087
+ ...usage ? { usage } : {}
2088
+ };
2089
+ } finally {
2090
+ signal?.removeEventListener("abort", cancel);
2091
+ cancel();
2092
+ reader.releaseLock();
2093
+ }
2094
+ }
2095
+ /** JSON 回退保持兼容;SSE 必须完整读取,取消后绝不返回聚合结果。 */
2096
+ async function readCompletionResponse(response, signal, forAgent = false) {
2097
+ if (signal?.aborted) {
2098
+ response.body?.cancel().catch(() => {});
2099
+ signal.throwIfAborted();
2100
+ }
2101
+ const result = response.headers?.get("content-type")?.split(";")[0].trim().toLowerCase() === "text/event-stream" ? await readEventStream(response, signal) : await response.json();
2102
+ signal?.throwIfAborted();
2103
+ if (record(result) && Array.isArray(result.choices)) {
2104
+ for (const choice of result.choices) if (!forAgent && record(choice) && choice.finish_reason != null && ![
2105
+ "stop",
2106
+ "tool_calls",
2107
+ "function_call"
2108
+ ].includes(String(choice.finish_reason))) throw invalid(`异常结束:${choice.finish_reason}`);
2109
+ }
2110
+ return result;
2111
+ }
2112
+ /** 仅包装当前 agent 的 Response;不修改全局 fetch,也不改上游依赖。 */
2113
+ function agentCompletionResponse(response, signal) {
2114
+ if (!response.ok) return response;
2115
+ const headers = new Headers(response.headers);
2116
+ headers.set("content-type", "application/json");
2117
+ headers.delete("content-length");
2118
+ headers.delete("content-encoding");
2119
+ const aggregation = new AbortController();
2120
+ const abort = () => aggregation.abort(signal?.reason);
2121
+ signal?.addEventListener("abort", abort, { once: true });
2122
+ if (signal?.aborted) abort();
2123
+ const body = new ReadableStream({
2124
+ async start(controller) {
2125
+ try {
2126
+ const completion = await readCompletionResponse(response, aggregation.signal, true);
2127
+ const message = completion.choices[0]?.message;
2128
+ if (completion.choices.length !== 1 || (message?.tool_calls?.length ?? 0) > 1 || message?.function_call && message.tool_calls?.length) throw invalid("agent 每轮只允许一个 choice 和一个工具调用");
2129
+ if (!["length", "content_filter"].includes(completion.choices[0]?.finish_reason ?? "")) {
2130
+ for (const call of message?.tool_calls ?? []) validateFunction(call.function);
2131
+ if (message?.function_call) validateFunction(message.function_call);
2132
+ }
2133
+ aggregation.signal.throwIfAborted();
2134
+ controller.enqueue(new TextEncoder().encode(JSON.stringify(completion)));
2135
+ controller.close();
2136
+ } catch (error) {
2137
+ controller.error(error);
2138
+ } finally {
2139
+ signal?.removeEventListener("abort", abort);
2140
+ }
2141
+ },
2142
+ cancel() {
2143
+ aggregation.abort(new DOMException("响应读取已取消", "AbortError"));
2144
+ }
2145
+ });
2146
+ return new Response(body, {
2147
+ status: response.status,
2148
+ statusText: response.statusText,
2149
+ headers
2150
+ });
2151
+ }
2152
+
1877
2153
  //#endregion
1878
2154
  //#region src/core/createPageUseAgent.ts
1879
2155
  /**
@@ -1914,21 +2190,19 @@ function createPageUseAgent({ options, enableMask, narration = false, shouldRunB
1914
2190
  const { llm } = options;
1915
2191
  if (!llm.baseURL) throw new Error("[PageUse] 未配置 llm.baseURL");
1916
2192
  let sessionId = newSessionId();
1917
- /**
1918
- * 注入会话头(OpenCode Go 缺少 x-opencode-session 会返回 400)。
1919
- * User-Agent 属浏览器禁止修改的头,需要时由代理补充。
1920
- */
1921
- const customFetch = llm.sessionHeader ? (input, init) => {
2193
+ const extraBody = llm.extraBody;
2194
+ const hostBeforeTask = options.beforeTask;
2195
+ const hostPageAgent = options.pageAgentOptions;
2196
+ /** 注入任务会话头;浏览器禁止修改 User-Agent,需要时仍由代理补充。 */
2197
+ const customFetch = async (input, init) => {
1922
2198
  const headers = new Headers(init?.headers);
1923
- headers.set(llm.sessionHeader, sessionId);
1924
- return fetch(input, {
2199
+ if (llm.sessionHeader) headers.set(llm.sessionHeader, sessionId);
2200
+ const response = await (hostPageAgent.customFetch ?? fetch)(input, {
1925
2201
  ...init,
1926
2202
  headers
1927
2203
  });
1928
- } : void 0;
1929
- const extraBody = llm.extraBody;
1930
- const hostBeforeTask = options.beforeTask;
1931
- const hostPageAgent = options.pageAgentOptions;
2204
+ return llm.stream ? agentCompletionResponse(response, init?.signal) : response;
2205
+ };
1932
2206
  const tour = options.pageTour.enabled ? createPageTour({
1933
2207
  getContentRoot: options.getContentRoot,
1934
2208
  maxTabSwitches: options.pageTour.maxTabSwitches,
@@ -1944,11 +2218,6 @@ function createPageUseAgent({ options, enableMask, narration = false, shouldRunB
1944
2218
  language: "zh-CN",
1945
2219
  maxSteps: llm.maxSteps,
1946
2220
  disableNamedToolChoice: llm.disableNamedToolChoice,
1947
- customFetch,
1948
- transformRequestBody: Object.keys(extraBody).length ? (body) => ({
1949
- ...body,
1950
- ...extraBody
1951
- }) : void 0,
1952
2221
  stepDelay: STEP_DELAY_SECONDS,
1953
2222
  promptForNextTask: false,
1954
2223
  enableMask,
@@ -1962,6 +2231,15 @@ function createPageUseAgent({ options, enableMask, narration = false, shouldRunB
1962
2231
  return `${content.slice(0, PAGE_CONTENT_MAX_CHARS)}\n...(页面内容过长已截断,可滚动后再观察)`;
1963
2232
  },
1964
2233
  ...hostPageAgent,
2234
+ customFetch,
2235
+ transformRequestBody: (body) => {
2236
+ const merged = {
2237
+ ...body,
2238
+ ...extraBody
2239
+ };
2240
+ const transformed = hostPageAgent.transformRequestBody?.(merged) ?? merged;
2241
+ return completionRequestBody(transformed, llm);
2242
+ },
1965
2243
  customTools: {
1966
2244
  [CLICK_TOOL_NAME]: createClickTool(newWindows),
1967
2245
  ...tour ? { [PRESENT_PAGE_TOOL_NAME]: tour.tool } : {},
@@ -2273,6 +2551,64 @@ function describeActivity(activity) {
2273
2551
  }
2274
2552
  }
2275
2553
 
2554
+ //#endregion
2555
+ //#region src/core/userSubmit.ts
2556
+ /** 用户输入接管;程序化 run 和 ask_user 回答不经过这里。 */
2557
+ const state = createStore({ pending: false });
2558
+ const pageUseSubmitStore = {
2559
+ get: state.get,
2560
+ subscribe: state.subscribe
2561
+ };
2562
+ let active$1;
2563
+ function cancelPageUseSubmit() {
2564
+ const previous = active$1;
2565
+ active$1 = void 0;
2566
+ state.set({ pending: false });
2567
+ previous?.abort();
2568
+ }
2569
+ async function decidePageUseSubmit(text, handler) {
2570
+ if (active$1) return { action: "busy" };
2571
+ if (!handler) return { action: "continue" };
2572
+ const controller = new AbortController();
2573
+ active$1 = controller;
2574
+ state.set({ pending: true });
2575
+ let onAbort = () => {};
2576
+ try {
2577
+ const cancelled = new Promise((resolve) => {
2578
+ onAbort = () => resolve({ action: "cancelled" });
2579
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2580
+ });
2581
+ const decision = Promise.resolve().then(async () => controller.signal.aborted ? { action: "cancelled" } : handler({
2582
+ text,
2583
+ requestId: crypto.randomUUID(),
2584
+ signal: controller.signal
2585
+ }));
2586
+ const result = await Promise.race([decision, cancelled]);
2587
+ if (controller.signal.aborted || active$1 !== controller) return { action: "cancelled" };
2588
+ if (!result || ![
2589
+ "continue",
2590
+ "handled",
2591
+ "reject"
2592
+ ].includes(result.action)) return {
2593
+ action: "reject",
2594
+ message: "输入处理返回无效结果,请重试"
2595
+ };
2596
+ return result;
2597
+ } catch (error) {
2598
+ if (controller.signal.aborted) return { action: "cancelled" };
2599
+ return {
2600
+ action: "reject",
2601
+ message: error instanceof Error ? error.message : "输入处理失败,请重试"
2602
+ };
2603
+ } finally {
2604
+ controller.signal.removeEventListener("abort", onAbort);
2605
+ if (active$1 === controller) {
2606
+ active$1 = void 0;
2607
+ state.set({ pending: false });
2608
+ }
2609
+ }
2610
+ }
2611
+
2276
2612
  //#endregion
2277
2613
  //#region src/core/service.ts
2278
2614
  /** 无人可答时回给模型的固定答复,引导其基于已有信息决策或以未达成结束 */
@@ -2322,6 +2658,8 @@ let stopNarration = null;
2322
2658
  let questionUiCount = 0;
2323
2659
  /** 当前任务的调用方选项(提问回调、进度回调) */
2324
2660
  let activeOptions = null;
2661
+ let activeCancellation = null;
2662
+ let cancelQuestion = null;
2325
2663
  /** 整场流程持有执行权,包括暂停与人工等待;内部动作通过 owner 进入。 */
2326
2664
  let executionLease = null;
2327
2665
  let flowMaskOwner;
@@ -2389,28 +2727,54 @@ function bindAgentEvents(target) {
2389
2727
  });
2390
2728
  }
2391
2729
  /** 模型提问的分发:调用方回调 → 调试面板 → 固定答复 */
2392
- function handleAskUser(question, signal) {
2393
- emit({
2394
- type: "question",
2395
- taskId: store.get().currentTaskId,
2396
- question
2397
- });
2730
+ async function handleAskUser(question, signal) {
2731
+ const taskId = store.get().currentTaskId;
2398
2732
  const callerHandler = activeOptions?.onAskUser;
2399
- if (callerHandler) return callerHandler(question);
2400
- if (questionUiCount <= 0) return Promise.resolve(NO_ANSWER_REPLY);
2401
- return new Promise((resolve, reject) => {
2402
- store.set({ pendingQuestion: {
2403
- question,
2404
- resolve: (answer) => {
2405
- store.set({ pendingQuestion: null });
2406
- resolve(answer);
2407
- }
2408
- } });
2409
- signal?.addEventListener("abort", () => {
2410
- store.set({ pendingQuestion: null });
2411
- reject(signal.reason);
2412
- }, { once: true });
2413
- });
2733
+ cancelQuestion?.();
2734
+ const controller = new AbortController();
2735
+ const cancel = () => controller.abort(new DOMException("任务已取消", "AbortError"));
2736
+ const signals = [signal, activeCancellation?.signal].filter((value) => Boolean(value));
2737
+ for (const upstream of signals) {
2738
+ upstream.addEventListener("abort", cancel, { once: true });
2739
+ if (upstream.aborted) cancel();
2740
+ }
2741
+ cancelQuestion = cancel;
2742
+ let questionUi;
2743
+ let onAbort = () => {};
2744
+ try {
2745
+ if (controller.signal.aborted) throw controller.signal.reason;
2746
+ const cancelled = new Promise((_, reject) => {
2747
+ onAbort = () => reject(controller.signal.reason);
2748
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2749
+ });
2750
+ const answer = Promise.resolve().then(() => {
2751
+ if (controller.signal.aborted) throw controller.signal.reason;
2752
+ emit({
2753
+ type: "question",
2754
+ taskId,
2755
+ question
2756
+ });
2757
+ if (controller.signal.aborted) throw controller.signal.reason;
2758
+ if (callerHandler) return callerHandler(question, {
2759
+ taskId,
2760
+ signal: controller.signal
2761
+ });
2762
+ if (questionUiCount <= 0) return NO_ANSWER_REPLY;
2763
+ return new Promise((resolve) => {
2764
+ questionUi = {
2765
+ question,
2766
+ resolve
2767
+ };
2768
+ store.set({ pendingQuestion: questionUi });
2769
+ });
2770
+ });
2771
+ return await Promise.race([answer, cancelled]);
2772
+ } finally {
2773
+ for (const upstream of signals) upstream.removeEventListener("abort", cancel);
2774
+ controller.signal.removeEventListener("abort", onAbort);
2775
+ if (questionUi && store.get().pendingQuestion === questionUi) store.set({ pendingQuestion: null });
2776
+ if (cancelQuestion === cancel) cancelQuestion = null;
2777
+ }
2414
2778
  }
2415
2779
  /**
2416
2780
  * 懒创建单例;蒙层或播报设置变化时重建(enableMask 属构造期选项,系统提示在构造时生成)。
@@ -2529,6 +2893,8 @@ async function runTaskInternal(task, options, owner) {
2529
2893
  const taskId = newTaskId();
2530
2894
  const selectedConfig = config;
2531
2895
  const startedAt = Date.now();
2896
+ const cancellation = new AbortController();
2897
+ activeCancellation = cancellation;
2532
2898
  activeOptions = options;
2533
2899
  store.set({
2534
2900
  errorMessage: "",
@@ -2554,9 +2920,10 @@ async function runTaskInternal(task, options, owner) {
2554
2920
  options.signal?.addEventListener("abort", onAbort, { once: true });
2555
2921
  let result;
2556
2922
  try {
2923
+ if (cancellation.signal.aborted) throw cancellation.signal.reason;
2557
2924
  const execution = await ensureAgent(config).execute(normalized);
2558
2925
  if (config !== selectedConfig || activeOptions !== options) return rejectedResult(normalized, source, "宿主配置或执行会话已变化", void 0, "stopped");
2559
- const final = resolveFinalStatus(execution.success);
2926
+ const final = cancellation.signal.aborted ? "stopped" : resolveFinalStatus(execution.success);
2560
2927
  const { steps, totalTokens } = store.get();
2561
2928
  result = {
2562
2929
  taskId,
@@ -2573,24 +2940,26 @@ async function runTaskInternal(task, options, owner) {
2573
2940
  if (config !== selectedConfig || activeOptions !== options) return rejectedResult(normalized, source, "宿主配置或执行会话已变化", void 0, "stopped");
2574
2941
  const message = err instanceof Error ? err.message : String(err);
2575
2942
  const { steps, totalTokens } = store.get();
2943
+ const stopped = cancellation.signal.aborted;
2576
2944
  store.set({
2577
- errorMessage: message,
2578
- status: "error"
2945
+ errorMessage: stopped ? "" : message,
2946
+ status: stopped ? "stopped" : "error"
2579
2947
  });
2580
2948
  result = {
2581
2949
  taskId,
2582
2950
  task: normalized,
2583
2951
  source,
2584
- status: "error",
2952
+ status: stopped ? "stopped" : "error",
2585
2953
  success: false,
2586
- summary: message,
2587
- errorCode: "AGENT_ERROR",
2954
+ summary: stopped ? "任务已取消" : message,
2955
+ errorCode: stopped ? void 0 : "AGENT_ERROR",
2588
2956
  steps,
2589
2957
  tokens: totalTokens,
2590
2958
  durationMs: Date.now() - startedAt
2591
2959
  };
2592
2960
  } finally {
2593
2961
  options.signal?.removeEventListener("abort", onAbort);
2962
+ if (activeCancellation === cancellation) activeCancellation = null;
2594
2963
  if (config === selectedConfig && activeOptions === options) {
2595
2964
  store.set({ finishedAt: Date.now() });
2596
2965
  if (!executionLease) stopTick();
@@ -2615,11 +2984,18 @@ async function runTaskInternal(task, options, owner) {
2615
2984
  }
2616
2985
  /** 终止当前任务(无任务时忽略);等待 agent 完全停下后 resolve */
2617
2986
  async function stopTask() {
2987
+ cancelPageUseSubmit();
2988
+ activeCancellation?.abort();
2989
+ cancelQuestion?.();
2618
2990
  if (executionLease) return executionLease.stop();
2619
2991
  await agent?.stop();
2620
2992
  }
2621
2993
  async function stopTaskForFlow(owner) {
2622
- if (executionLease?.owner === owner) await agent?.stop();
2994
+ if (executionLease?.owner === owner) {
2995
+ activeCancellation?.abort();
2996
+ cancelQuestion?.();
2997
+ await agent?.stop();
2998
+ }
2623
2999
  }
2624
3000
  function acquireFlowExecution(owner, stop) {
2625
3001
  if (isRunning() || activeOptions) throw new FlowError("BUSY", "已有任务正在操作页面");
@@ -2720,6 +3096,9 @@ function clearPageUseLog() {
2720
3096
  }
2721
3097
  /** 释放 agent(清掉高亮与蒙层);宿主销毁(如登出)时调用 */
2722
3098
  function disposeAgent() {
3099
+ cancelPageUseSubmit();
3100
+ activeCancellation?.abort();
3101
+ cancelQuestion?.();
2723
3102
  const lease = executionLease;
2724
3103
  executionLease = null;
2725
3104
  if (lease) lease.stop().catch((error) => console.warn("[PageUse] 取消流程失败", error));
@@ -4933,10 +5312,9 @@ function createRegionSelector(llm) {
4933
5312
  method: "POST",
4934
5313
  headers,
4935
5314
  signal,
4936
- body: JSON.stringify({
5315
+ body: JSON.stringify(completionRequestBody({
4937
5316
  ...extra,
4938
5317
  model: llm.model,
4939
- stream: false,
4940
5318
  temperature: 0,
4941
5319
  messages: [{
4942
5320
  role: "system",
@@ -4950,10 +5328,10 @@ function createRegionSelector(llm) {
4950
5328
  }],
4951
5329
  tools: [],
4952
5330
  tool_choice: "none"
4953
- })
5331
+ }, llm))
4954
5332
  });
4955
5333
  if (!response.ok) throw new FlowError("REGION_SELECTION_INVALID", `区域模型请求失败(HTTP ${response.status})`);
4956
- const message = (await response.json()).choices?.[0]?.message;
5334
+ const message = (await readCompletionResponse(response, signal)).choices?.[0]?.message;
4957
5335
  if (message?.tool_calls?.length || message?.function_call) throw new FlowError("REGION_SELECTION_INVALID", "区域模型返回了工具调用");
4958
5336
  try {
4959
5337
  const value = JSON.parse(message?.content ?? "");
@@ -5017,10 +5395,9 @@ function createCompletionEvaluator(llm) {
5017
5395
  method: "POST",
5018
5396
  headers,
5019
5397
  signal,
5020
- body: JSON.stringify({
5398
+ body: JSON.stringify(completionRequestBody({
5021
5399
  ...extra,
5022
5400
  model: llm.model,
5023
- stream: false,
5024
5401
  temperature: 0,
5025
5402
  tools: [],
5026
5403
  tool_choice: "none",
@@ -5036,10 +5413,10 @@ function createCompletionEvaluator(llm) {
5036
5413
  record: request.record
5037
5414
  })
5038
5415
  }]
5039
- })
5416
+ }, llm))
5040
5417
  });
5041
5418
  if (!response.ok) throw new FlowError("CHECK_UNCERTAIN", `完成检查模型请求失败(HTTP ${response.status})`);
5042
- const message = (await response.json()).choices?.[0]?.message;
5419
+ const message = (await readCompletionResponse(response, signal)).choices?.[0]?.message;
5043
5420
  if (message?.tool_calls?.length || message?.function_call) throw new FlowError("CHECK_UNCERTAIN", "完成检查模型返回了工具调用");
5044
5421
  try {
5045
5422
  const value = JSON.parse(message?.content ?? "");
@@ -8393,4 +8770,4 @@ async function runPageUseFlowTool(flow, args, options = {}) {
8393
8770
  }
8394
8771
 
8395
8772
  //#endregion
8396
- export { showFlowMask as $, parseFlowBundle as A, browserTtsEngine as At, acquireFlowExecution as B, createIndexedDbFlowRepository as C, readLauncherQuery as Ct, flowSceneDraftSchema as D, PAGE_USE_MASK_COLORS as Dt, flowBundleSchema as E, resolveTtsEngine as Et, sceneKey as F, cancelRegionPicker as Ft, getPageUseSnapshot as G, configurePageUse as H, scopeKey as I, hasRegionPicker as It, isRunning as J, hideFlowMask as K, validateFlowInputs as L, FLOW_BUNDLE_VERSION as Lt, parseFlowRegion as M, DEFAULT_TOUR_STYLES as Mt, parseFlowScene as N, TOUR_STYLE_TRAINER as Nt, flowSceneSchema as O, SIMULATOR_MASK_ID as Ot, parseFlowSceneDraft as P, TOUR_STYLE_USER as Pt, runTaskForFlow as Q, validateRegionPreviewInputs as R, FLOW_SCHEMA_VERSION as Rt, previewFlowImport as S, normalizeOptions as St, cloneJson as T, listTtsEngines as Tt, getElapsedSeconds as U, clearPageUseLog as V, getPageUseConfig as W, registerQuestionUi as X, pageUseStore as Y, runTask as Z, createFlowApiAdapter as _, DEFAULT_MAX_STEPS as _t, mountPageUseManager as a, pageUseHistory as at, exportFlowBundle as b, DEFAULT_STORAGE_KEY_PREFIX as bt, createFlowRegionPicker as c, narrate as ct, evaluateFlowCompletion as d, previewStore as dt, stopTask as et, composedParent as f, startPreview as ft, validateRegionPages as g, createStore as gt, regionAbortable as h, pageUseSettings as ht, definePageUseManagerElement as i, pageUsePresets as it, parseFlowKnowledge as j, isSpeechSupported as jt, newRevision as k, BROWSER_ENGINE_ID as kt, createFlowRunner as l, narrationMute as lt, locateFlowRegion as m, DEFAULT_PAGE_USE_SETTINGS as mt, getPageUseFlowCatalog as n, subscribePageUse as nt, PAGE_USE_MANAGER_TAG as o, hideTourHighlight as ot, createRegionSelector as p, stopPreview as pt, isPageUseConfigured as q, runPageUseFlowTool as r, updateFlowExecution as rt, PageUseManagerElement as s, showTourHighlight as st, PAGE_USE_FLOW_TOOL as t, stopTaskForFlow as tt, createCompletionEvaluator as u, registerNarrationPanelName as ut, validateFlowApiOptions as v, DEFAULT_MODEL as vt, FLOW_SEGMENT_MAX_CHARS as w, readLauncherSwitch as wt, importFlowBundle as x, DEFAULT_TOUR_MAX_TAB_SWITCHES as xt, createFlowCloud as y, DEFAULT_SESSION_HEADER as yt, validateSceneBindings as z, FlowError as zt };
8773
+ export { showFlowMask as $, parseFlowBundle as A, listTtsEngines as At, acquireFlowExecution as B, cancelRegionPicker as Bt, createIndexedDbFlowRepository as C, DEFAULT_MODEL as Ct, flowSceneDraftSchema as D, normalizeOptions as Dt, flowBundleSchema as E, DEFAULT_TOUR_MAX_TAB_SWITCHES as Et, sceneKey as F, browserTtsEngine as Ft, getPageUseSnapshot as G, configurePageUse as H, FLOW_BUNDLE_VERSION as Ht, scopeKey as I, isSpeechSupported as It, isRunning as J, hideFlowMask as K, validateFlowInputs as L, DEFAULT_TOUR_STYLES as Lt, parseFlowRegion as M, PAGE_USE_MASK_COLORS as Mt, parseFlowScene as N, SIMULATOR_MASK_ID as Nt, flowSceneSchema as O, readLauncherQuery as Ot, parseFlowSceneDraft as P, BROWSER_ENGINE_ID as Pt, runTaskForFlow as Q, validateRegionPreviewInputs as R, TOUR_STYLE_TRAINER as Rt, previewFlowImport as S, DEFAULT_MAX_STEPS as St, cloneJson as T, DEFAULT_STORAGE_KEY_PREFIX as Tt, getElapsedSeconds as U, FLOW_SCHEMA_VERSION as Ut, clearPageUseLog as V, hasRegionPicker as Vt, getPageUseConfig as W, FlowError as Wt, registerQuestionUi as X, pageUseStore as Y, runTask as Z, createFlowApiAdapter as _, startPreview as _t, mountPageUseManager as a, decidePageUseSubmit as at, exportFlowBundle as b, pageUseSettings as bt, createFlowRegionPicker as c, pageUseHistory as ct, evaluateFlowCompletion as d, hideTourHighlight as dt, stopTask as et, composedParent as f, showTourHighlight as ft, validateRegionPages as g, previewStore as gt, regionAbortable as h, registerNarrationPanelName as ht, definePageUseManagerElement as i, cancelPageUseSubmit as it, parseFlowKnowledge as j, resolveTtsEngine as jt, newRevision as k, readLauncherSwitch as kt, createFlowRunner as l, completionRequestBody as lt, locateFlowRegion as m, narrationMute as mt, getPageUseFlowCatalog as n, subscribePageUse as nt, PAGE_USE_MANAGER_TAG as o, pageUseSubmitStore as ot, createRegionSelector as p, narrate as pt, isPageUseConfigured as q, runPageUseFlowTool as r, updateFlowExecution as rt, PageUseManagerElement as s, pageUsePresets as st, PAGE_USE_FLOW_TOOL as t, stopTaskForFlow as tt, createCompletionEvaluator as u, readCompletionResponse as ut, validateFlowApiOptions as v, stopPreview as vt, FLOW_SEGMENT_MAX_CHARS as w, DEFAULT_SESSION_HEADER as wt, importFlowBundle as x, createStore as xt, createFlowCloud as y, DEFAULT_PAGE_USE_SETTINGS as yt, validateSceneBindings as z, TOUR_STYLE_USER as zt };