@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.
- package/README.md +2 -0
- package/dist/flow.d.mts +1 -1
- package/dist/flow.mjs +1 -1
- package/dist/{index-zDjmWzFh.d.mts → index-DUS8tBFn.d.mts} +25 -7
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/{options-BhKU0oJ5.d.mts → options-BIwnIeed.d.mts} +45 -2
- package/dist/{src-DAbH8ij6.mjs → src-D27GErmQ.mjs} +134 -38
- package/dist/{tool-CvEvIsN9.mjs → tool-DBeaEt3g.mjs} +439 -62
- package/dist/vue.d.mts +3 -3
- package/dist/vue.mjs +3 -3
- package/package.json +2 -1
|
@@ -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(
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
*/
|
|
1921
|
-
const customFetch =
|
|
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
|
-
|
|
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
|
-
|
|
1929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
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)
|
|
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
|
|
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
|
|
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,
|
|
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 };
|