@webskill/sdk 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +3 -3
- package/dist/agent.js +2842 -3
- package/dist/{dist-Bfga1TmS.js → approval-BnLLCOG0.js} +235 -2932
- package/dist/browser.d.ts +242 -8
- package/dist/browser.js +687 -40
- package/dist/{openUiLibrary-BKXW7Iwx-DjvEnMlJ.js → dist-CTsxblSD.js} +496 -28
- package/dist/{echarts-De78wXqV.js → echarts-BE7oV_Dl.js} +1 -1
- package/dist/{env-8cY40DXB-CGnEVZby.js → env-Bj1MI2Ww.js} +1 -1
- package/dist/errors-BDZNpC13.js +22 -0
- package/dist/{eventTypes-FllCrX-Z-DNDeHWoG.js → eventTypes-CwciaqCU.js} +37 -2
- package/dist/external-_ZRQe-V9.js +53 -0
- package/dist/governance.d.ts +38 -3
- package/dist/governance.js +86 -4
- package/dist/{index-DQwGvHAI.d.ts → index-BLfnO7sz.d.ts} +406 -19
- package/dist/{index-CWoKOFBP.d.ts → index-DbVqg2V3.d.ts} +159 -3
- package/dist/{index-sd-gVKey.d.ts → index-DeXgdtHa.d.ts} +26 -18
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1001 -5
- package/dist/kind-DxgS8LM-.js +77 -0
- package/dist/linkedDocument-xNXF-z8n.js +94 -0
- package/dist/llm-B7lLH0ZI.js +1767 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +41 -4
- package/dist/memoryArtifactStore-D6dLeY55.js +48 -0
- package/dist/miniChart-lauNtZu8.js +528 -0
- package/dist/node.d.ts +3 -3
- package/dist/node.js +106 -9
- package/dist/openUiLibrary-BWEBU9P-.js +30 -0
- package/dist/openUiSpecLang-BIK6CWlW.js +1849 -0
- package/dist/pathSecurity-B1owvJAF.js +65 -0
- package/dist/processSandboxEntry.js +5 -1
- package/dist/renderResult-D9Q-Vu2x.js +143 -0
- package/dist/{rolldown-runtime-BOF7iYI8.js → rolldown-runtime-BHkdefai.js} +1 -2
- package/dist/sandboxWorkerEntry.js +5 -1
- package/dist/{dist-D5YhlCdY.js → skill-CAJMsLod.js} +44 -372
- package/dist/{skillVersionStore-D-qHk9ZE-C0OFDTQY.d.ts → skillVersionStore-B24Jjjry-CsbzF7Oe.d.ts} +14 -2
- package/dist/surface-DVGiCmwq.js +145 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +64 -3
- package/dist/types-BuacBov_.js +89 -0
- package/dist/{types-C3V6lAeO-BgeOvc1Y.d.ts → types-C3V6lAeO-BUpcRLER.d.ts} +28 -1
- package/dist/ui-react.d.ts +18 -5
- package/dist/ui-react.js +130781 -6951
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +12 -3
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +1254 -3
- package/dist/urlSafety-CiSuCJvX.js +65 -0
- package/dist/webSkillApi-DOOyz2G5.js +106 -0
- package/dist/{webskillLitCatalog-D_zCqeQF-swXsWoAe.js → webskillLitCatalog-poL1qhBp.js} +11 -3
- package/package.json +1 -1
- package/dist/catalogComponents-BgAJN0p8-Cr55ouOg.js +0 -124384
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +0 -7787
- package/dist/dist-BnqAkSS6.js +0 -2384
- package/dist/dist-CiaDIqkV.js +0 -3509
- package/dist/dist-DxyxmXPU-DGC-dpXf.js +0 -5621
- package/dist/memoryArtifactStore-52Zn9npI-BxtUkrgV.js +0 -86
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +0 -31
- package/dist/testing-Cm8MseLF.js +0 -142
- package/dist/types-WovEf4ED-CZSDiiBU.js +0 -6215
package/dist/browser.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
2
|
+
import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest, O as MANIFEST_EXCLUDED_FILES, P as verifyManifest, b as parseSkillMarkdown, c as unzipWithLimits, f as SKILL_PACK_FILE, i as buildCatalog, k as SKILLS_LOCKFILE, m as parseSkillPackManifest, n as SkillDiscovery, o as readResponseWithLimit, p as exportSkills, t as validateSkills, u as detectSkillArchiveShapeFromFs, w as readSkillSignature, y as isValidSkillName } from "./skill-CAJMsLod.js";
|
|
3
|
+
import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot } from "./pathSecurity-B1owvJAF.js";
|
|
4
|
+
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
5
|
+
import { a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, g as FsRunSnapshotStore, l as parseBridgeRequest, nt as ProgressiveRouter, p as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-BnLLCOG0.js";
|
|
6
|
+
import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-B7lLH0ZI.js";
|
|
7
|
+
import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
|
|
8
|
+
import { t as createWebSkillApi } from "./webSkillApi-DOOyz2G5.js";
|
|
9
|
+
import { a as frameLabel, i as toFrameScopes, o as frameSteps, r as toActionFrameScopes } from "./types-BuacBov_.js";
|
|
5
10
|
|
|
6
|
-
//#region ../browser/
|
|
11
|
+
//#region ../browser/src/fs/featureDetection.ts
|
|
7
12
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
8
13
|
function isOpfsAvailable(globalScope) {
|
|
9
14
|
return typeof (globalScope ?? globalThis).navigator?.storage?.getDirectory === "function";
|
|
10
15
|
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region ../browser/src/fs/opfsProvider.ts
|
|
11
19
|
const isDomException = (e, name) => e instanceof DOMException && e.name === name;
|
|
12
20
|
const mapError = (e, path) => {
|
|
13
21
|
if (isDomException(e, "NotFoundError")) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${path}`);
|
|
@@ -174,6 +182,9 @@ var OpfsProvider = class {
|
|
|
174
182
|
await this.remove(from);
|
|
175
183
|
}
|
|
176
184
|
};
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region ../browser/src/executor/workerBootstrap.ts
|
|
177
188
|
/**
|
|
178
189
|
* Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
|
|
179
190
|
* 注意:内容为纯 JS,不能含反引号与 ${ 占位(外层用模板字符串内嵌);
|
|
@@ -183,7 +194,7 @@ var OpfsProvider = class {
|
|
|
183
194
|
*
|
|
184
195
|
* 协议(宿主 ↔ Worker):
|
|
185
196
|
* - 宿主 → Worker:{type:'load', id, source, networkPolicy?} 请求加载脚本定义
|
|
186
|
-
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?} 请求执行
|
|
197
|
+
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?, documentSurface?} 请求执行
|
|
187
198
|
* - Worker → 宿主:{type:'load-result', id, ok, definition?, error?}
|
|
188
199
|
* - Worker → 宿主:{type:'execute-result', id, ok, value?, stdout, stderr, error?}
|
|
189
200
|
* - Worker → 宿主:{type:'bridge', request} 能力桥 RPC(readReference/writeArtifact/confirm)
|
|
@@ -299,7 +310,7 @@ function callCapability(kind, payload) {
|
|
|
299
310
|
}
|
|
300
311
|
|
|
301
312
|
function makeContext(msg) {
|
|
302
|
-
|
|
313
|
+
var ctx = {
|
|
303
314
|
skillName: msg.skillName,
|
|
304
315
|
runId: msg.runId,
|
|
305
316
|
readReference: function (path) {
|
|
@@ -329,6 +340,21 @@ function makeContext(msg) {
|
|
|
329
340
|
return callCapability('confirm', { message: message });
|
|
330
341
|
},
|
|
331
342
|
};
|
|
343
|
+
// 能力位而非能力函数:其余能力无条件挂上去(拿不到时桥对面报错),
|
|
344
|
+
// documentSurface 是个供探测的布尔值,无条件挂上去就变成恒真谎言。
|
|
345
|
+
if (msg.documentSurface) ctx.documentSurface = true;
|
|
346
|
+
// 上传文件同理:宿主没接就不能给出「存在但必然失败」的假出口(分册 17 FR-17.1)
|
|
347
|
+
if (msg.uploadFiles) {
|
|
348
|
+
ctx.listUploadFiles = function () {
|
|
349
|
+
return callCapability('listUploadFiles', {});
|
|
350
|
+
};
|
|
351
|
+
ctx.readUploadFile = function (fileId) {
|
|
352
|
+
return callCapability('readUploadFile', { fileId: fileId }).then(function (bytes) {
|
|
353
|
+
return new Uint8Array(bytes);
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return ctx;
|
|
332
358
|
}
|
|
333
359
|
|
|
334
360
|
self.onmessage = async function (event) {
|
|
@@ -404,6 +430,9 @@ self.onmessage = async function (event) {
|
|
|
404
430
|
}
|
|
405
431
|
};
|
|
406
432
|
`;
|
|
433
|
+
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region ../browser/src/executor/tsTranspiler.ts
|
|
407
436
|
/** 按资源地址缓存,而不是全局单例:不同地址必须各自加载,否则先到的实例会顶替后来的配置 */
|
|
408
437
|
const esbuildCache = /* @__PURE__ */ new Map();
|
|
409
438
|
async function loadEsbuild(options) {
|
|
@@ -466,12 +495,18 @@ var TsTranspiler = class {
|
|
|
466
495
|
return code;
|
|
467
496
|
}
|
|
468
497
|
};
|
|
498
|
+
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region ../browser/src/skillManagement/checksumWeb.ts
|
|
469
501
|
/** WebCrypto sha256(浏览器/Worker 通用;Node 20+ 全局 crypto 同样可用) */
|
|
470
502
|
async function sha256HexWeb(data) {
|
|
471
503
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
472
504
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
473
505
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
474
506
|
}
|
|
507
|
+
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region ../browser/src/skillManagement/fsUtils.ts
|
|
475
510
|
/** 递归拷贝目录(含子目录与二进制文件;原子写中断残留的 `.tmp-*` 不拷贝) */
|
|
476
511
|
async function copyDir(fs, from, to) {
|
|
477
512
|
await fs.mkdir(to);
|
|
@@ -498,6 +533,9 @@ async function removeDirQuiet(fs, dir) {
|
|
|
498
533
|
if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
|
|
499
534
|
} catch {}
|
|
500
535
|
}
|
|
536
|
+
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region ../browser/src/skillManagement/zipExtract.ts
|
|
501
539
|
/**
|
|
502
540
|
* zip 解包(fflate 流式,单条目/总解压体积上限,浏览器无依赖)。
|
|
503
541
|
* 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
|
|
@@ -509,6 +547,9 @@ async function extractZipWeb(fs, data, destRoot, limits) {
|
|
|
509
547
|
else await fs.writeBinary(target, content);
|
|
510
548
|
}
|
|
511
549
|
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region ../browser/src/skillManagement/browserSkillManager.ts
|
|
512
553
|
/**
|
|
513
554
|
* 验签失败不能被折叠成 INSTALL_FAILED:四种失败的宿主处置完全不同
|
|
514
555
|
* (引导选策略 / 提示重下 / 引导添加密钥 / 安全事件),与 Node 侧同口径。
|
|
@@ -885,6 +926,9 @@ var BrowserSkillManager = class {
|
|
|
885
926
|
await atomicWriteText(this.#fs, lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
|
|
886
927
|
}
|
|
887
928
|
};
|
|
929
|
+
|
|
930
|
+
//#endregion
|
|
931
|
+
//#region ../browser/src/executor/iframeSandbox.ts
|
|
888
932
|
const ENVELOPE = "__webskill_sandbox__";
|
|
889
933
|
let channelSeq = 0;
|
|
890
934
|
/** 每实例 channelId(防同页伪造消息注入;crypto.randomUUID 优先,无则随机回退) */
|
|
@@ -1098,8 +1142,9 @@ function createIframeWorker(bootstrapSource, options = {}) {
|
|
|
1098
1142
|
doc.body.appendChild(iframe);
|
|
1099
1143
|
return worker;
|
|
1100
1144
|
}
|
|
1145
|
+
const readyTimeoutMs = resolved.readyTimeoutMs ?? 1e4;
|
|
1101
1146
|
const worker = new IframeWorkerLike(iframe, hostWindow, channelId, {
|
|
1102
|
-
ms:
|
|
1147
|
+
ms: readyTimeoutMs,
|
|
1103
1148
|
documentUrl
|
|
1104
1149
|
});
|
|
1105
1150
|
iframe.addEventListener("load", () => {
|
|
@@ -1116,6 +1161,9 @@ function createIframeWorker(bootstrapSource, options = {}) {
|
|
|
1116
1161
|
doc.body.appendChild(iframe);
|
|
1117
1162
|
return worker;
|
|
1118
1163
|
}
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region ../browser/src/executor/workerScriptExecutor.ts
|
|
1119
1167
|
const blobWorkerFactory = () => {
|
|
1120
1168
|
const blob = new Blob([WORKER_BOOTSTRAP_SOURCE], { type: "text/javascript" });
|
|
1121
1169
|
const url = URL.createObjectURL(blob);
|
|
@@ -1228,7 +1276,9 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1228
1276
|
runId: context.runId,
|
|
1229
1277
|
source: script.source,
|
|
1230
1278
|
args,
|
|
1231
|
-
networkPolicy: this.#networkPolicy
|
|
1279
|
+
networkPolicy: this.#networkPolicy,
|
|
1280
|
+
...context.documentSurface ? { documentSurface: true } : {},
|
|
1281
|
+
...context.readUploadFile ? { uploadFiles: true } : {}
|
|
1232
1282
|
}, timeoutMs, (bridgeRequest) => this.#handleBridge(bridgeRequest, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
1233
1283
|
if (!response.ok) {
|
|
1234
1284
|
const stderrSummary = response.stderr?.length ? ` | stderr: ${response.stderr.join(" | ").slice(0, 500)}` : "";
|
|
@@ -1377,12 +1427,29 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
1377
1427
|
value: await context.confirm(request.message)
|
|
1378
1428
|
};
|
|
1379
1429
|
}
|
|
1430
|
+
case "listUploadFiles":
|
|
1431
|
+
if (!context.listUploadFiles) return bridgeError(request.id, "TOOL_UNSUPPORTED", UPLOAD_FILES_UNAVAILABLE);
|
|
1432
|
+
return {
|
|
1433
|
+
id: request.id,
|
|
1434
|
+
ok: true,
|
|
1435
|
+
value: await context.listUploadFiles()
|
|
1436
|
+
};
|
|
1437
|
+
case "readUploadFile":
|
|
1438
|
+
if (!context.readUploadFile) return bridgeError(request.id, "TOOL_UNSUPPORTED", UPLOAD_FILES_UNAVAILABLE);
|
|
1439
|
+
return {
|
|
1440
|
+
id: request.id,
|
|
1441
|
+
ok: true,
|
|
1442
|
+
value: Array.from(await context.readUploadFile(request.fileId))
|
|
1443
|
+
};
|
|
1380
1444
|
}
|
|
1381
1445
|
} catch (e) {
|
|
1382
1446
|
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
|
|
1383
1447
|
}
|
|
1384
1448
|
}
|
|
1385
1449
|
};
|
|
1450
|
+
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region ../browser/src/navigator.ts
|
|
1386
1453
|
/**
|
|
1387
1454
|
* navigator.webskill 显式装配:OPFS 默认存储 + BrowserWorkerScriptExecutor +
|
|
1388
1455
|
* BrowserSkillManager → createWebSkillApi;target 提供时挂 target.navigator
|
|
@@ -1412,6 +1479,9 @@ function installWebSkillNavigator(config) {
|
|
|
1412
1479
|
nav["webskill"] = api;
|
|
1413
1480
|
return api;
|
|
1414
1481
|
}
|
|
1482
|
+
|
|
1483
|
+
//#endregion
|
|
1484
|
+
//#region ../browser/src/llm/chromeBuiltinClient.ts
|
|
1415
1485
|
function languageModel() {
|
|
1416
1486
|
const candidate = globalThis.LanguageModel;
|
|
1417
1487
|
return typeof candidate?.availability === "function" && typeof candidate.create === "function" ? candidate : void 0;
|
|
@@ -1547,6 +1617,9 @@ var ChromeBuiltinLlmClient = class {
|
|
|
1547
1617
|
return (await probeChromeBuiltinAvailability()).available;
|
|
1548
1618
|
}
|
|
1549
1619
|
};
|
|
1620
|
+
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region ../browser/src/llm/createLlmClient.ts
|
|
1550
1623
|
/**
|
|
1551
1624
|
* 配置 → `LlmClient`。收在 SDK 里是为了让下游只跟 `RuntimeConfigStore` 打交道,
|
|
1552
1625
|
* 不必自己维护「provider → client 类」的映射(设计 04 §5)。
|
|
@@ -1578,6 +1651,9 @@ function createLlmClient(config) {
|
|
|
1578
1651
|
model: config.model
|
|
1579
1652
|
});
|
|
1580
1653
|
}
|
|
1654
|
+
|
|
1655
|
+
//#endregion
|
|
1656
|
+
//#region ../browser/src/media/dictation.ts
|
|
1581
1657
|
function recognitionCtor() {
|
|
1582
1658
|
const scope = globalThis;
|
|
1583
1659
|
return scope.SpeechRecognition ?? scope.webkitSpeechRecognition;
|
|
@@ -1653,6 +1729,9 @@ function startDictation(options) {
|
|
|
1653
1729
|
recognition.stop();
|
|
1654
1730
|
} };
|
|
1655
1731
|
}
|
|
1732
|
+
|
|
1733
|
+
//#endregion
|
|
1734
|
+
//#region ../browser/src/media/compressImage.ts
|
|
1656
1735
|
/** 动图无法在不丢帧的前提下走单帧编码管线,超限直接拒绝(裁决 D-2) */
|
|
1657
1736
|
const UNCOMPRESSABLE_MIME_TYPES = /* @__PURE__ */ new Set(["image/gif"]);
|
|
1658
1737
|
const QUALITY_STEPS = [
|
|
@@ -1720,6 +1799,9 @@ async function encode(bitmap, width, height, quality) {
|
|
|
1720
1799
|
function formatBytes(bytes) {
|
|
1721
1800
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
1722
1801
|
}
|
|
1802
|
+
|
|
1803
|
+
//#endregion
|
|
1804
|
+
//#region ../browser/src/dom/imageCapture.ts
|
|
1723
1805
|
/** @experimental */
|
|
1724
1806
|
function isCaptureFailure(result) {
|
|
1725
1807
|
return "reason" in result;
|
|
@@ -2049,6 +2131,9 @@ function describeError(e) {
|
|
|
2049
2131
|
if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
|
|
2050
2132
|
return e instanceof Error ? e.message : String(e);
|
|
2051
2133
|
}
|
|
2134
|
+
|
|
2135
|
+
//#endregion
|
|
2136
|
+
//#region ../browser/src/dom/promotion.ts
|
|
2052
2137
|
/**
|
|
2053
2138
|
* 非标可交互元素的识别与角色提升(0.14.0 分册 15)。
|
|
2054
2139
|
*
|
|
@@ -2191,9 +2276,12 @@ function promoteRole(input) {
|
|
|
2191
2276
|
href
|
|
2192
2277
|
} : { role: "button" };
|
|
2193
2278
|
}
|
|
2279
|
+
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region ../browser/src/dom/perception.ts
|
|
2194
2282
|
/** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
|
|
2195
2283
|
const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
|
|
2196
|
-
/**
|
|
2284
|
+
/** 可被操作的角色:只有这些节点的句柄能拿去写页面 */
|
|
2197
2285
|
const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
|
|
2198
2286
|
"button",
|
|
2199
2287
|
"checkbox",
|
|
@@ -2205,9 +2293,34 @@ const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
|
|
|
2205
2293
|
"switch",
|
|
2206
2294
|
"textbox"
|
|
2207
2295
|
]);
|
|
2296
|
+
/**
|
|
2297
|
+
* 只能做子树锚点的容器角色(分册 14 §6.1)。
|
|
2298
|
+
*
|
|
2299
|
+
* 这些节点拿到的是“读”句柄:`perceive_page(ref)` 能以它为根续读,
|
|
2300
|
+
* `act_on_page` 拿到它则一律被拒。不取 generic:那等于给每个 div 发句柄。
|
|
2301
|
+
*/
|
|
2302
|
+
const ANCHOR_ROLES = /* @__PURE__ */ new Set([
|
|
2303
|
+
"article",
|
|
2304
|
+
"dialog",
|
|
2305
|
+
"feed",
|
|
2306
|
+
"grid",
|
|
2307
|
+
"group",
|
|
2308
|
+
"list",
|
|
2309
|
+
"listitem",
|
|
2310
|
+
"main",
|
|
2311
|
+
"navigation",
|
|
2312
|
+
"region",
|
|
2313
|
+
"row",
|
|
2314
|
+
"rowgroup",
|
|
2315
|
+
"table",
|
|
2316
|
+
"tabpanel",
|
|
2317
|
+
"tree",
|
|
2318
|
+
"treeitem"
|
|
2319
|
+
]);
|
|
2208
2320
|
/** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
|
|
2209
2321
|
const ROLE_BY_TAG = {
|
|
2210
2322
|
A: "link",
|
|
2323
|
+
ARTICLE: "article",
|
|
2211
2324
|
BUTTON: "button",
|
|
2212
2325
|
CANVAS: "img",
|
|
2213
2326
|
H1: "heading",
|
|
@@ -2381,7 +2494,7 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
|
|
|
2381
2494
|
element,
|
|
2382
2495
|
role,
|
|
2383
2496
|
doc,
|
|
2384
|
-
hasActionableDescendant: children.some((child) => child.ref !== void 0),
|
|
2497
|
+
hasActionableDescendant: children.some((child) => child.ref !== void 0 && handles?.isAction(child.ref) === true),
|
|
2385
2498
|
context: promotion
|
|
2386
2499
|
});
|
|
2387
2500
|
if (promoted !== void 0) {
|
|
@@ -2514,11 +2627,13 @@ var HandleIssuer = class {
|
|
|
2514
2627
|
}
|
|
2515
2628
|
issue(element, role, frame) {
|
|
2516
2629
|
if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
|
|
2517
|
-
|
|
2630
|
+
const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
|
|
2631
|
+
if (kind === void 0) return void 0;
|
|
2518
2632
|
const known = this.#refByElement.get(element);
|
|
2519
2633
|
if (known !== void 0 && this.#table.has(known)) {
|
|
2520
2634
|
this.#table.set(known, {
|
|
2521
2635
|
element,
|
|
2636
|
+
kind,
|
|
2522
2637
|
...frame !== void 0 ? { frame } : {}
|
|
2523
2638
|
});
|
|
2524
2639
|
return known;
|
|
@@ -2528,11 +2643,16 @@ var HandleIssuer = class {
|
|
|
2528
2643
|
const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2529
2644
|
this.#table.set(ref, {
|
|
2530
2645
|
element,
|
|
2646
|
+
kind,
|
|
2531
2647
|
...frame !== void 0 ? { frame } : {}
|
|
2532
2648
|
});
|
|
2533
2649
|
this.#refByElement.set(element, ref);
|
|
2534
2650
|
return ref;
|
|
2535
2651
|
}
|
|
2652
|
+
/** 角色提升要的是「有可操作后代」,容器锚点不算;混进来会把可点容器判成已有出口 */
|
|
2653
|
+
isAction(ref) {
|
|
2654
|
+
return this.#table.get(ref)?.kind === "action";
|
|
2655
|
+
}
|
|
2536
2656
|
get table() {
|
|
2537
2657
|
return this.#table;
|
|
2538
2658
|
}
|
|
@@ -2633,12 +2753,20 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2633
2753
|
};
|
|
2634
2754
|
/** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
|
|
2635
2755
|
const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
|
|
2636
|
-
|
|
2756
|
+
/**
|
|
2757
|
+
* @param subtree 不为 `undefined` 时只读这一棵子树(分册 14)。
|
|
2758
|
+
* 它**不是一个新的范围来源**:子树必须先落在某条帧授权的 include 根下、
|
|
2759
|
+
* 且未被 exclude 命中,否则遍历**根本不开始**(`anchored: false`)。
|
|
2760
|
+
* 命中时也只是把那一帧的 `roots` 换成这一个元素,遍历器与 exclude 判定一字未改——
|
|
2761
|
+
* 「带 `ref` 的结果是不带它时的子集」因此是个结构事实(FR-14.2)。
|
|
2762
|
+
*/
|
|
2763
|
+
function collect(scope, capture, subtree) {
|
|
2637
2764
|
const root = rootDocument();
|
|
2638
2765
|
if (root === void 0) return {
|
|
2639
2766
|
nodes: [],
|
|
2640
2767
|
frameNotes: [],
|
|
2641
|
-
nestedFrames: []
|
|
2768
|
+
nestedFrames: [],
|
|
2769
|
+
anchored: false
|
|
2642
2770
|
};
|
|
2643
2771
|
handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
|
|
2644
2772
|
handles.prune();
|
|
@@ -2730,6 +2858,7 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2730
2858
|
issuer.useSets(actionable, actionExcluded);
|
|
2731
2859
|
return node !== void 0;
|
|
2732
2860
|
}
|
|
2861
|
+
let anchored = subtree === void 0;
|
|
2733
2862
|
for (const { scope: frameScope, label, doc } of resolvedScopes) {
|
|
2734
2863
|
const view = doc.defaultView;
|
|
2735
2864
|
const tag = label === "self" ? void 0 : label;
|
|
@@ -2737,14 +2866,27 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2737
2866
|
issuer.useSets(sets.actionable, sets.excluded);
|
|
2738
2867
|
const excluded = /* @__PURE__ */ new Set();
|
|
2739
2868
|
for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
|
|
2740
|
-
|
|
2869
|
+
let roots = [];
|
|
2741
2870
|
for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
|
|
2871
|
+
if (subtree !== void 0) {
|
|
2872
|
+
const inScope = roots.some((element) => element === subtree || element.contains(subtree));
|
|
2873
|
+
const cut = [...excluded].some((element) => element === subtree || element.contains(subtree));
|
|
2874
|
+
if (!inScope || cut) continue;
|
|
2875
|
+
roots = [subtree];
|
|
2876
|
+
anchored = true;
|
|
2877
|
+
}
|
|
2742
2878
|
for (const element of roots) {
|
|
2743
2879
|
const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
|
|
2744
2880
|
if (node !== void 0) nodes.push(node);
|
|
2745
2881
|
}
|
|
2746
2882
|
descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
|
|
2747
2883
|
}
|
|
2884
|
+
if (subtree !== void 0) return {
|
|
2885
|
+
nodes,
|
|
2886
|
+
frameNotes,
|
|
2887
|
+
nestedFrames,
|
|
2888
|
+
anchored
|
|
2889
|
+
};
|
|
2748
2890
|
pruneElevated();
|
|
2749
2891
|
const view = root.defaultView;
|
|
2750
2892
|
const sets = actionSets(root, actionScopeFor("self"));
|
|
@@ -2764,15 +2906,16 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2764
2906
|
return {
|
|
2765
2907
|
nodes,
|
|
2766
2908
|
frameNotes,
|
|
2767
|
-
nestedFrames
|
|
2909
|
+
nestedFrames,
|
|
2910
|
+
anchored
|
|
2768
2911
|
};
|
|
2769
2912
|
}
|
|
2770
|
-
async function readWithImages(scope, capture) {
|
|
2913
|
+
async function readWithImages(scope, capture, subtree) {
|
|
2771
2914
|
const targets = [];
|
|
2772
|
-
const { nodes, frameNotes, nestedFrames } = collect(scope, {
|
|
2915
|
+
const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, {
|
|
2773
2916
|
targets,
|
|
2774
2917
|
minImageArea: capture.minImageArea ?? 0
|
|
2775
|
-
});
|
|
2918
|
+
}, subtree);
|
|
2776
2919
|
const selected = targets.slice(0, capture.maxImages);
|
|
2777
2920
|
const images = [];
|
|
2778
2921
|
let imageFailures = 0;
|
|
@@ -2800,6 +2943,7 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2800
2943
|
images,
|
|
2801
2944
|
imagesOmitted: targets.length - selected.length,
|
|
2802
2945
|
imageFailures,
|
|
2946
|
+
anchored,
|
|
2803
2947
|
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
2804
2948
|
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
2805
2949
|
};
|
|
@@ -2816,6 +2960,28 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2816
2960
|
}
|
|
2817
2961
|
return readWithImages(scope, capture);
|
|
2818
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* 子树续读(分册 14 FR-14.1)。三条判定全在遍历开始**之前**:
|
|
2965
|
+
* 句柄不认识 → `unknown-ref`;解析得到的元素已离开文档、
|
|
2966
|
+
* 或不在任何一条帧授权的 include 根下、或被 exclude 连根剪掉 → `out-of-scope`。
|
|
2967
|
+
*
|
|
2968
|
+
* 拒绝走返回值而不是抛错:跨上下文的 reject 到本地就是一个没有错误码的 `Error`。
|
|
2969
|
+
*/
|
|
2970
|
+
async function readSubtree(scope, ref, capture) {
|
|
2971
|
+
const element = handles?.table.get(ref)?.element;
|
|
2972
|
+
if (element === void 0 || !element.isConnected) return { rejected: "unknown-ref" };
|
|
2973
|
+
if (capture?.images === true && capture.maxImages > 0) {
|
|
2974
|
+
const { anchored, ...result } = await readWithImages(scope, capture, element);
|
|
2975
|
+
return anchored ? result : { rejected: "out-of-scope" };
|
|
2976
|
+
}
|
|
2977
|
+
const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, void 0, element);
|
|
2978
|
+
if (!anchored) return { rejected: "out-of-scope" };
|
|
2979
|
+
return {
|
|
2980
|
+
nodes,
|
|
2981
|
+
...frameNotes.length > 0 ? { frameNotes } : {},
|
|
2982
|
+
...nestedFrames.length > 0 ? { nestedFrames } : {}
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2819
2985
|
/** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
|
|
2820
2986
|
function modalSnapshot() {
|
|
2821
2987
|
const root = rootDocument();
|
|
@@ -2828,7 +2994,9 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2828
2994
|
}
|
|
2829
2995
|
return {
|
|
2830
2996
|
read,
|
|
2997
|
+
readSubtree,
|
|
2831
2998
|
resolve: (ref) => handles?.table.get(ref)?.element,
|
|
2999
|
+
handleKindOf: (ref) => handles?.table.get(ref)?.kind,
|
|
2832
3000
|
frameOf: (ref) => handles?.table.get(ref)?.frame,
|
|
2833
3001
|
modalSnapshot,
|
|
2834
3002
|
elevateNewModals: (before) => {
|
|
@@ -2871,6 +3039,38 @@ function createDomPerceptionReader(options = {}) {
|
|
|
2871
3039
|
}
|
|
2872
3040
|
}
|
|
2873
3041
|
}
|
|
3042
|
+
|
|
3043
|
+
//#endregion
|
|
3044
|
+
//#region ../browser/src/remote/documentIdentity.ts
|
|
3045
|
+
/**
|
|
3046
|
+
* 文档标识(0.16.0 分册 11 · FR-11.1)。
|
|
3047
|
+
*
|
|
3048
|
+
* 它有且只有两个消费者,且**必须是同一个函数**:
|
|
3049
|
+
* ① 感知回复里的 `document.key`,决定这批句柄进哪个分片;
|
|
3050
|
+
* ② `PageActionOutcome.navigated`,执行前后各取一次。
|
|
3051
|
+
* 两处各写一套判定,就会出现「操作说导航了、分片说没换」的不一致(AC-11.4 / AC-13.4 验的就是这个)。
|
|
3052
|
+
*/
|
|
3053
|
+
/** 每个文档实例一次。页面重载即换,因此真导航天然换 key */
|
|
3054
|
+
const instance = (() => {
|
|
3055
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
3056
|
+
crypto.getRandomValues(bytes);
|
|
3057
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3058
|
+
})();
|
|
3059
|
+
/**
|
|
3060
|
+
* `instance` + 当前地址。真导航换 `instance`,帧内路由(SPA)换 `href`,
|
|
3061
|
+
* 两类都被这一个串覆盖——靠 origin 比对识别不出后者。
|
|
3062
|
+
*
|
|
3063
|
+
* `href` 含 hash 是有意的:纯 hash 跳转在很多站点上会换掉整块内容,
|
|
3064
|
+
* 判成换文档是安全侧的选择(代价是多感知一次;漏判没有便宜的补救)。
|
|
3065
|
+
* @experimental
|
|
3066
|
+
*/
|
|
3067
|
+
function documentKey(view) {
|
|
3068
|
+
const href = view?.location.href;
|
|
3069
|
+
return href === void 0 ? void 0 : `${instance}\u0000${href}`;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
//#endregion
|
|
3073
|
+
//#region ../browser/src/dom/pageAction.ts
|
|
2874
3074
|
const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
|
|
2875
3075
|
/** 只看显式声明,不看尺寸:jsdom 有 getComputedStyle 但没有布局 */
|
|
2876
3076
|
function hidden(element) {
|
|
@@ -2953,6 +3153,54 @@ function fillValue(element, value) {
|
|
|
2953
3153
|
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2954
3154
|
}
|
|
2955
3155
|
const norm = (text) => (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
3156
|
+
/**
|
|
3157
|
+
* 一次「像人一样」的点击。
|
|
3158
|
+
*
|
|
3159
|
+
* `HTMLElement.click()` **只**派发 `click`,不派发指针/鼠标序列,也不移动焦点。
|
|
3160
|
+
* 大量自研下拉、弹层、菜单是在 `pointerdown` / `mousedown` 上开的(这样拖拽和
|
|
3161
|
+
* 「点外面关闭」才好写),于是模型「点了但什么都没发生」,人手点却正常——
|
|
3162
|
+
* 差别不在权限,在事件序列。真实用户点击本来就会先后产生 down 与 click 两拨事件,
|
|
3163
|
+
* 因此补齐序列不会造成「点两下」。
|
|
3164
|
+
*
|
|
3165
|
+
* 激活行为仍交给 `.click()`:链接跳转、复选框翻转这些默认动作由它保证。
|
|
3166
|
+
*/
|
|
3167
|
+
function press(element) {
|
|
3168
|
+
const view = element.ownerDocument.defaultView;
|
|
3169
|
+
const rect = element.getBoundingClientRect();
|
|
3170
|
+
const base = {
|
|
3171
|
+
bubbles: true,
|
|
3172
|
+
cancelable: true,
|
|
3173
|
+
composed: true,
|
|
3174
|
+
detail: 1,
|
|
3175
|
+
button: 0,
|
|
3176
|
+
clientX: rect.left + rect.width / 2,
|
|
3177
|
+
clientY: rect.top + rect.height / 2
|
|
3178
|
+
};
|
|
3179
|
+
const pointer = {
|
|
3180
|
+
...base,
|
|
3181
|
+
pointerId: 1,
|
|
3182
|
+
pointerType: "mouse",
|
|
3183
|
+
isPrimary: true
|
|
3184
|
+
};
|
|
3185
|
+
const Pointer = view?.PointerEvent;
|
|
3186
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerdown", {
|
|
3187
|
+
...pointer,
|
|
3188
|
+
buttons: 1
|
|
3189
|
+
}));
|
|
3190
|
+
if (element.dispatchEvent(new MouseEvent("mousedown", {
|
|
3191
|
+
...base,
|
|
3192
|
+
buttons: 1
|
|
3193
|
+
}))) element.focus?.();
|
|
3194
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerup", {
|
|
3195
|
+
...pointer,
|
|
3196
|
+
buttons: 0
|
|
3197
|
+
}));
|
|
3198
|
+
element.dispatchEvent(new MouseEvent("mouseup", {
|
|
3199
|
+
...base,
|
|
3200
|
+
buttons: 0
|
|
3201
|
+
}));
|
|
3202
|
+
element.click();
|
|
3203
|
+
}
|
|
2956
3204
|
/** 轮询等待,上限固定:等不到就失败,不能无限挂着一次工具调用 */
|
|
2957
3205
|
async function waitFor(probe, timeoutMs = 2e3) {
|
|
2958
3206
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2991,7 +3239,7 @@ async function selectOption(element, value) {
|
|
|
2991
3239
|
} : fail(`The control did not accept "${value}".`);
|
|
2992
3240
|
}
|
|
2993
3241
|
const doc = element.ownerDocument;
|
|
2994
|
-
element
|
|
3242
|
+
press(element);
|
|
2995
3243
|
const listbox = await waitFor(() => doc.querySelector("[role=listbox], [role=grid], [role=menu]") ?? void 0);
|
|
2996
3244
|
if (listbox === null || listbox === void 0) return fail("The options panel did not open.");
|
|
2997
3245
|
const option = [...listbox.querySelectorAll("[role=option], [role=gridcell], [role=menuitem], option")].find((candidate) => norm(accessibleName(candidate)) === norm(value));
|
|
@@ -3029,7 +3277,7 @@ function setToggle(element, value, target) {
|
|
|
3029
3277
|
target,
|
|
3030
3278
|
noop: true
|
|
3031
3279
|
};
|
|
3032
|
-
element
|
|
3280
|
+
press(element);
|
|
3033
3281
|
return {
|
|
3034
3282
|
ok: true,
|
|
3035
3283
|
target
|
|
@@ -3072,7 +3320,44 @@ function createDomPageActionExecutor(options) {
|
|
|
3072
3320
|
elevated: true
|
|
3073
3321
|
} : described;
|
|
3074
3322
|
};
|
|
3323
|
+
/**
|
|
3324
|
+
* 后退(0.16.0 分册 13 / FR-13.6)。
|
|
3325
|
+
*
|
|
3326
|
+
* 两种「没退成」都返回 `ok: true, noop: true` 而不是失败:
|
|
3327
|
+
* 返回失败模型会重试,而重试同样退不动——它需要的是「这条路走不通」的事实,
|
|
3328
|
+
* 不是一个可以再试一次的错误(与 `set` 的 noop 同惯例)。
|
|
3329
|
+
*/
|
|
3330
|
+
const goBack = async () => {
|
|
3331
|
+
const view = (options.document ?? globalThis.document)?.defaultView;
|
|
3332
|
+
const target = { role: "document" };
|
|
3333
|
+
if (view === void 0 || view === null) return {
|
|
3334
|
+
ok: false,
|
|
3335
|
+
target,
|
|
3336
|
+
reason: "No browsing context is available."
|
|
3337
|
+
};
|
|
3338
|
+
if (view.history.length <= 1) return {
|
|
3339
|
+
ok: true,
|
|
3340
|
+
target,
|
|
3341
|
+
noop: true
|
|
3342
|
+
};
|
|
3343
|
+
const keyBefore = documentKey(view);
|
|
3344
|
+
view.history.back();
|
|
3345
|
+
return await waitFor(() => {
|
|
3346
|
+
const key = documentKey(view);
|
|
3347
|
+
return key !== void 0 && key !== keyBefore ? key : void 0;
|
|
3348
|
+
}, 500) === void 0 ? {
|
|
3349
|
+
ok: true,
|
|
3350
|
+
target,
|
|
3351
|
+
noop: true
|
|
3352
|
+
} : {
|
|
3353
|
+
ok: true,
|
|
3354
|
+
target,
|
|
3355
|
+
navigated: true,
|
|
3356
|
+
documentUrl: view.location.href
|
|
3357
|
+
};
|
|
3358
|
+
};
|
|
3075
3359
|
const execute = async (request) => {
|
|
3360
|
+
if (request.action === "back") return await goBack();
|
|
3076
3361
|
const element = reader.resolve(request.ref);
|
|
3077
3362
|
if (element === void 0) return {
|
|
3078
3363
|
ok: false,
|
|
@@ -3085,36 +3370,37 @@ function createDomPageActionExecutor(options) {
|
|
|
3085
3370
|
target,
|
|
3086
3371
|
reason
|
|
3087
3372
|
});
|
|
3373
|
+
if (reader.handleKindOf(request.ref) === "anchor") return fail("That reference points at a container, which cannot be acted on. Use it with perceive_page to read inside it, then act on an element found there.");
|
|
3088
3374
|
if (reader.modalStateOf(element) === "unelevated") return fail("That dialog is not in the authorized scope because it was opened manually. Ask me to open it, or add it to the host allowlist.");
|
|
3089
3375
|
if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
|
|
3090
3376
|
if (hidden(element)) return fail("The element is not visible.");
|
|
3091
3377
|
if (element.hasAttribute("disabled")) return fail("The element is disabled.");
|
|
3092
3378
|
const modalsBefore = reader.modalSnapshot();
|
|
3093
3379
|
const frameView = element.ownerDocument.defaultView;
|
|
3094
|
-
const
|
|
3380
|
+
const keyBefore = documentKey(frameView);
|
|
3095
3381
|
const done = async (outcome) => {
|
|
3096
3382
|
if (!outcome.ok) return outcome;
|
|
3097
3383
|
const settled = await waitFor(() => {
|
|
3098
3384
|
const modal = reader.elevateNewModals(modalsBefore);
|
|
3099
3385
|
if (modal !== void 0) return { modal };
|
|
3100
|
-
const
|
|
3101
|
-
return
|
|
3386
|
+
const key = documentKey(frameView);
|
|
3387
|
+
return key !== void 0 && key !== keyBefore ? { key } : void 0;
|
|
3102
3388
|
}, 500);
|
|
3103
3389
|
const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
|
|
3104
3390
|
const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
|
|
3105
|
-
const
|
|
3106
|
-
const navigated =
|
|
3391
|
+
const keyAfter = documentKey(frameView);
|
|
3392
|
+
const navigated = keyAfter !== void 0 && keyBefore !== void 0 && keyAfter !== keyBefore;
|
|
3107
3393
|
return {
|
|
3108
3394
|
...outcome,
|
|
3109
3395
|
...name !== void 0 ? { elevatedModal: name } : {},
|
|
3110
3396
|
...navigated ? {
|
|
3111
3397
|
navigated: true,
|
|
3112
|
-
documentUrl:
|
|
3398
|
+
documentUrl: frameView?.location.href
|
|
3113
3399
|
} : {}
|
|
3114
3400
|
};
|
|
3115
3401
|
};
|
|
3116
3402
|
if (request.action === "click") {
|
|
3117
|
-
element
|
|
3403
|
+
press(element);
|
|
3118
3404
|
return await done({
|
|
3119
3405
|
ok: true,
|
|
3120
3406
|
target
|
|
@@ -3168,38 +3454,121 @@ function createDomPageActionExecutor(options) {
|
|
|
3168
3454
|
describe
|
|
3169
3455
|
};
|
|
3170
3456
|
}
|
|
3457
|
+
|
|
3458
|
+
//#endregion
|
|
3459
|
+
//#region ../browser/src/remote/registry.ts
|
|
3460
|
+
/**
|
|
3461
|
+
* 分片数的防泄漏软上限(分册 11 / DV-2)。
|
|
3462
|
+
*
|
|
3463
|
+
* 它**不是** FR-11.5 那个容量闸门:那一条管的是「同时有几个**标签页**可操作」,
|
|
3464
|
+
* 触顶硬拒绝,落在 `TabWorksetPolicy`。这里管的是同一个标签页里连续跳页攒下的历史分片,
|
|
3465
|
+
* 那些本来就该失效,按最久未使用逐出即可,逐出不构成对模型的拒绝。
|
|
3466
|
+
*/
|
|
3467
|
+
const SHARD_SOFT_LIMIT = 24;
|
|
3468
|
+
/** 不给 `shard` 时用的分片键;单分片宿主全程待在这一个分片里 */
|
|
3469
|
+
const DEFAULT_SHARD = "";
|
|
3470
|
+
/**
|
|
3471
|
+
* 把解析结果翻译成给模型看的补充说明(分册 11 / FR-11.6);
|
|
3472
|
+
* 接到 `PageActionPolicyOptions.diagnose` 上即可:
|
|
3473
|
+
*
|
|
3474
|
+
* ```ts
|
|
3475
|
+
* diagnose: (ref) => explainResolution(targets.resolve(ref))
|
|
3476
|
+
* ```
|
|
3477
|
+
*
|
|
3478
|
+
* `unknown` 返回 `undefined` 而不是自己拼一句:让 policy 落回它的缺省文案,
|
|
3479
|
+
* 于是 AC-11.3 要求的「与 0.15.0 逐字相同」是**构造性**成立的,
|
|
3480
|
+
* 而不是靠两个文件里的字符串抄得一样。
|
|
3481
|
+
* @experimental
|
|
3482
|
+
*/
|
|
3483
|
+
function explainResolution(resolution) {
|
|
3484
|
+
if (resolution.status === "unfocused") return "This reference belongs to a different page. Switch to that page first, then retry.";
|
|
3485
|
+
if (resolution.status === "expired") return "The page this reference belongs to has changed or was closed. Perceive the page again and retry.";
|
|
3486
|
+
}
|
|
3171
3487
|
/** @experimental */
|
|
3172
|
-
function createRemoteTargetRegistry() {
|
|
3173
|
-
|
|
3488
|
+
function createRemoteTargetRegistry(options = {}) {
|
|
3489
|
+
const maxShards = Math.max(1, Math.trunc(options.maxShards ?? 24));
|
|
3490
|
+
let shards = /* @__PURE__ */ new Map();
|
|
3491
|
+
let refIndex = /* @__PURE__ */ new Map();
|
|
3492
|
+
let focused = DEFAULT_SHARD;
|
|
3493
|
+
const touch = (key, shard) => {
|
|
3494
|
+
shards.delete(key);
|
|
3495
|
+
shards.set(key, shard);
|
|
3496
|
+
while (shards.size > maxShards) {
|
|
3497
|
+
const oldest = shards.keys().next();
|
|
3498
|
+
if (oldest.done === true) break;
|
|
3499
|
+
shards.delete(oldest.value);
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3174
3502
|
return {
|
|
3175
3503
|
/**
|
|
3176
3504
|
* 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
|
|
3177
3505
|
* 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
|
|
3178
3506
|
* (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
|
|
3179
3507
|
* 否则远端比本地更松。
|
|
3508
|
+
*
|
|
3509
|
+
* 0.16.0 起替换的是**该分片**的表,别的分片不受影响——这是相对 0.15.0 的唯一语义变化。
|
|
3180
3510
|
*/
|
|
3181
|
-
replace(entries) {
|
|
3182
|
-
const
|
|
3511
|
+
replace(entries, document) {
|
|
3512
|
+
const shard = document?.key ?? DEFAULT_SHARD;
|
|
3513
|
+
const table = /* @__PURE__ */ new Map();
|
|
3183
3514
|
for (const entry of entries) {
|
|
3184
3515
|
const { ref, role, name, secret, frame, elevated } = entry;
|
|
3185
|
-
|
|
3516
|
+
table.set(ref, {
|
|
3186
3517
|
role,
|
|
3187
3518
|
...name !== void 0 ? { name } : {},
|
|
3188
3519
|
...secret === true ? { secret: true } : {},
|
|
3189
3520
|
...frame !== void 0 ? { frame } : {},
|
|
3190
3521
|
...elevated === true ? { elevated: true } : {}
|
|
3191
3522
|
});
|
|
3523
|
+
refIndex.set(ref, shard);
|
|
3192
3524
|
}
|
|
3193
|
-
|
|
3525
|
+
touch(shard, {
|
|
3526
|
+
table,
|
|
3527
|
+
...document !== void 0 ? { document } : {}
|
|
3528
|
+
});
|
|
3529
|
+
focused = shard;
|
|
3194
3530
|
},
|
|
3195
3531
|
get(ref) {
|
|
3196
|
-
return table.get(ref);
|
|
3532
|
+
return shards.get(focused)?.table.get(ref);
|
|
3533
|
+
},
|
|
3534
|
+
resolve(ref) {
|
|
3535
|
+
const target = shards.get(focused)?.table.get(ref);
|
|
3536
|
+
if (target !== void 0) return {
|
|
3537
|
+
status: "ok",
|
|
3538
|
+
target
|
|
3539
|
+
};
|
|
3540
|
+
const owner = refIndex.get(ref);
|
|
3541
|
+
if (owner === void 0) return { status: "unknown" };
|
|
3542
|
+
return shards.get(owner)?.table.has(ref) === true ? { status: "unfocused" } : { status: "expired" };
|
|
3543
|
+
},
|
|
3544
|
+
focus(shard) {
|
|
3545
|
+
focused = shard;
|
|
3546
|
+
},
|
|
3547
|
+
get focused() {
|
|
3548
|
+
return focused === DEFAULT_SHARD ? void 0 : focused;
|
|
3549
|
+
},
|
|
3550
|
+
describePage() {
|
|
3551
|
+
return shards.get(focused)?.document;
|
|
3552
|
+
},
|
|
3553
|
+
drop(shard) {
|
|
3554
|
+
shards.delete(shard);
|
|
3555
|
+
},
|
|
3556
|
+
clear() {
|
|
3557
|
+
shards = /* @__PURE__ */ new Map();
|
|
3558
|
+
refIndex = /* @__PURE__ */ new Map();
|
|
3559
|
+
focused = DEFAULT_SHARD;
|
|
3197
3560
|
},
|
|
3198
3561
|
get size() {
|
|
3199
|
-
return table.size;
|
|
3562
|
+
return shards.get(focused)?.table.size ?? 0;
|
|
3563
|
+
},
|
|
3564
|
+
get shardCount() {
|
|
3565
|
+
return shards.size;
|
|
3200
3566
|
}
|
|
3201
3567
|
};
|
|
3202
3568
|
}
|
|
3569
|
+
|
|
3570
|
+
//#endregion
|
|
3571
|
+
//#region ../browser/src/remote/transport.ts
|
|
3203
3572
|
/** 缺省单次往返上限(FR-13.2 / FR-13.3) */
|
|
3204
3573
|
const DEFAULT_PAGE_AGENT_TIMEOUT_MS = 1e4;
|
|
3205
3574
|
/**
|
|
@@ -3230,6 +3599,9 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
|
|
|
3230
3599
|
if (reply.type !== expect) throw new WebSkillError(code, `Page agent replied with "${reply.type}" to a "${request.type}" request; expected "${expect}".`);
|
|
3231
3600
|
return reply;
|
|
3232
3601
|
}
|
|
3602
|
+
|
|
3603
|
+
//#endregion
|
|
3604
|
+
//#region ../browser/src/remote/reader.ts
|
|
3233
3605
|
/**
|
|
3234
3606
|
* 跨上下文的页面感知(分册 13 / FR-13.2)。
|
|
3235
3607
|
*
|
|
@@ -3238,17 +3610,21 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
|
|
|
3238
3610
|
* @experimental
|
|
3239
3611
|
*/
|
|
3240
3612
|
function createRemotePerceptionReader(options) {
|
|
3241
|
-
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
|
|
3613
|
+
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
|
|
3242
3614
|
return { async read(scope, capture) {
|
|
3243
3615
|
const reply = await roundTrip(transport, {
|
|
3244
3616
|
type: "perceive",
|
|
3245
3617
|
scope,
|
|
3246
3618
|
...capture !== void 0 ? { capture } : {}
|
|
3247
3619
|
}, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
|
|
3248
|
-
targets.replace(reply.targets);
|
|
3620
|
+
targets.replace(reply.targets, reply.document);
|
|
3621
|
+
if (reply.document !== void 0) onDocument?.(reply.document);
|
|
3249
3622
|
return reply.result;
|
|
3250
3623
|
} };
|
|
3251
3624
|
}
|
|
3625
|
+
|
|
3626
|
+
//#endregion
|
|
3627
|
+
//#region ../browser/src/remote/executor.ts
|
|
3252
3628
|
/**
|
|
3253
3629
|
* 跨上下文的页面操作执行器(分册 13 / FR-13.3)。
|
|
3254
3630
|
* @experimental
|
|
@@ -3280,6 +3656,9 @@ function createRemotePageActionExecutor(options) {
|
|
|
3280
3656
|
}
|
|
3281
3657
|
};
|
|
3282
3658
|
}
|
|
3659
|
+
|
|
3660
|
+
//#endregion
|
|
3661
|
+
//#region ../browser/src/remote/handler.ts
|
|
3283
3662
|
/** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
|
|
3284
3663
|
function collectRefs(nodes, out = []) {
|
|
3285
3664
|
for (const node of nodes) {
|
|
@@ -3310,8 +3689,25 @@ function createPageAgentHandler(options = {}) {
|
|
|
3310
3689
|
});
|
|
3311
3690
|
const executor = createDomPageActionExecutor({
|
|
3312
3691
|
reader,
|
|
3692
|
+
...options.document !== void 0 ? { document: options.document } : {},
|
|
3313
3693
|
...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
|
|
3314
3694
|
});
|
|
3695
|
+
/**
|
|
3696
|
+
* 这批句柄属于哪个文档(FR-11.1)。`key` 与 `PageActionOutcome.navigated`
|
|
3697
|
+
* 出自同一个 `documentKey()` —— 这是「操作说导航了、分片说没换」不可能发生的构造性保证。
|
|
3698
|
+
*/
|
|
3699
|
+
const describeDocument = () => {
|
|
3700
|
+
const doc = options.document ?? globalThis.document;
|
|
3701
|
+
const key = documentKey(doc?.defaultView);
|
|
3702
|
+
if (key === void 0) return void 0;
|
|
3703
|
+
const url = doc.defaultView?.location.href;
|
|
3704
|
+
const title = doc.title;
|
|
3705
|
+
return {
|
|
3706
|
+
key,
|
|
3707
|
+
...url !== void 0 ? { url } : {},
|
|
3708
|
+
...title !== void 0 && title !== "" ? { title } : {}
|
|
3709
|
+
};
|
|
3710
|
+
};
|
|
3315
3711
|
return { async handle(request) {
|
|
3316
3712
|
try {
|
|
3317
3713
|
if (request.type === "perceive") {
|
|
@@ -3325,10 +3721,12 @@ function createPageAgentHandler(options = {}) {
|
|
|
3325
3721
|
...target
|
|
3326
3722
|
});
|
|
3327
3723
|
}
|
|
3724
|
+
const identity = describeDocument();
|
|
3328
3725
|
return {
|
|
3329
3726
|
type: "perceive-result",
|
|
3330
3727
|
result,
|
|
3331
|
-
targets
|
|
3728
|
+
targets,
|
|
3729
|
+
...identity !== void 0 ? { document: identity } : {}
|
|
3332
3730
|
};
|
|
3333
3731
|
}
|
|
3334
3732
|
return {
|
|
@@ -3344,6 +3742,206 @@ function createPageAgentHandler(options = {}) {
|
|
|
3344
3742
|
}
|
|
3345
3743
|
} };
|
|
3346
3744
|
}
|
|
3745
|
+
|
|
3746
|
+
//#endregion
|
|
3747
|
+
//#region ../browser/src/remote/frameRouter.ts
|
|
3748
|
+
/** @experimental */
|
|
3749
|
+
const DEFAULT_FRAME_BUDGET = {
|
|
3750
|
+
maxFrames: 12,
|
|
3751
|
+
maxNodes: 1500
|
|
3752
|
+
};
|
|
3753
|
+
function countNodes(nodes) {
|
|
3754
|
+
let total = 0;
|
|
3755
|
+
for (const node of nodes) total += 1 + countNodes(node.children ?? []);
|
|
3756
|
+
return total;
|
|
3757
|
+
}
|
|
3758
|
+
function note(name) {
|
|
3759
|
+
return {
|
|
3760
|
+
role: "note",
|
|
3761
|
+
name
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
/**
|
|
3765
|
+
* 地址归一化:只比较 origin + path。
|
|
3766
|
+
*
|
|
3767
|
+
* 父页面写的 `src` 常带 `?t=123` 之类的缓存参数,甚至被框架改写过 hash。
|
|
3768
|
+
* 带上 query/hash 比对会把明明是同一帧的判成换了页,
|
|
3769
|
+
* 于是本该读到的内容变成一句「导航走了」——宁可粗一点也不能漏。
|
|
3770
|
+
*/
|
|
3771
|
+
function frameKey(url) {
|
|
3772
|
+
try {
|
|
3773
|
+
const parsed = new URL(url);
|
|
3774
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
3775
|
+
} catch {
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* 确认卡与模型上下文里指称一帧的说法(分册 16 FR-16.6)。
|
|
3781
|
+
*
|
|
3782
|
+
* 带上 host 而不只用父页面写的 `hint`:用户要能一眼分清「页面主体里的那个按钮」
|
|
3783
|
+
* 和「某个第三方 iframe 里的按钮」,而 `hint` 来自被嵌一方决定不了、
|
|
3784
|
+
* 嵌入方随便写的属性——`title="账户设置"` 的 iframe 完全可以指向别人的域。
|
|
3785
|
+
*/
|
|
3786
|
+
function frameLabelOf(nested) {
|
|
3787
|
+
try {
|
|
3788
|
+
return `${nested.hint} (${new URL(nested.url).host})`;
|
|
3789
|
+
} catch {
|
|
3790
|
+
return nested.hint;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
/** 与 SDK 单帧取像超限时的文案同源;两边改要一起改 */
|
|
3794
|
+
const IMAGE_LIMIT_NOTE = "Not captured: the per-message image limit was reached";
|
|
3795
|
+
/**
|
|
3796
|
+
* 深度重写节点:打帧标签,并把 `imageId` 换成汇总后的全局 id。
|
|
3797
|
+
*
|
|
3798
|
+
* 每帧的取像 id 都是自己从 `img-1` 数起的,直接合并会让不同帧的图撞成同一个 id。
|
|
3799
|
+
* 落选(超整页预算)的图必须**同时**抹掉节点上的 `imageId`,否则节点指向一张
|
|
3800
|
+
* 根本没下发的图,模型会以为自己看过了。
|
|
3801
|
+
*/
|
|
3802
|
+
function relabel(nodes, frame, imageIds) {
|
|
3803
|
+
return nodes.map((node) => {
|
|
3804
|
+
const next = { ...node };
|
|
3805
|
+
if (frame !== void 0) next.frame = node.frame ?? frame;
|
|
3806
|
+
if (node.imageId !== void 0) {
|
|
3807
|
+
const mapped = imageIds.get(node.imageId);
|
|
3808
|
+
if (mapped === void 0) {
|
|
3809
|
+
delete next.imageId;
|
|
3810
|
+
next.imageNote = node.imageNote ?? IMAGE_LIMIT_NOTE;
|
|
3811
|
+
} else next.imageId = mapped;
|
|
3812
|
+
}
|
|
3813
|
+
if (node.children !== void 0) next.children = relabel(node.children, frame, imageIds);
|
|
3814
|
+
return next;
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* 逐帧下钻并汇总(分册 16 FR-16.2 / FR-16.3 / FR-16.4,分册 11 FR-11.2)。
|
|
3819
|
+
*
|
|
3820
|
+
* 编排本身与浏览器 API 无关:它只认 `PageAgentRequest` / `PageAgentReply`,
|
|
3821
|
+
* 寻址全压在宿主实现的 `FrameDispatcher` 里。因此这个模块可以在 Service Worker
|
|
3822
|
+
* 里实例化,也可以服务不按帧号寻址的宿主。
|
|
3823
|
+
* @experimental
|
|
3824
|
+
*/
|
|
3825
|
+
function createFrameRouter(options) {
|
|
3826
|
+
const budget = {
|
|
3827
|
+
...DEFAULT_FRAME_BUDGET,
|
|
3828
|
+
...options.budget
|
|
3829
|
+
};
|
|
3830
|
+
/** ref → 发放它的那一帧。`execute` 必须投回原帧,投给别的帧要么查无此 ref 要么误点同名控件 */
|
|
3831
|
+
const refFrames = /* @__PURE__ */ new Map();
|
|
3832
|
+
async function perceive(request) {
|
|
3833
|
+
const mainAddress = await options.dispatcher.mainFrame();
|
|
3834
|
+
const nodes = [];
|
|
3835
|
+
const targets = [];
|
|
3836
|
+
const frameNotes = [];
|
|
3837
|
+
const main = await options.dispatcher.send(mainAddress, request);
|
|
3838
|
+
if (main.reply.type !== "perceive-result") return main.reply;
|
|
3839
|
+
const images = [];
|
|
3840
|
+
let imagesOmitted = 0;
|
|
3841
|
+
let imageFailures = 0;
|
|
3842
|
+
const imageBudget = request.capture?.images === true ? request.capture.maxImages : 0;
|
|
3843
|
+
const queue = [];
|
|
3844
|
+
const visited = [mainAddress];
|
|
3845
|
+
let nodeCount = 0;
|
|
3846
|
+
let framesRead = 0;
|
|
3847
|
+
let descended = 0;
|
|
3848
|
+
const skipped = [];
|
|
3849
|
+
const absorb = (reply, address, frame, imagePrefix) => {
|
|
3850
|
+
framesRead += 1;
|
|
3851
|
+
nodeCount += countNodes(reply.result.nodes);
|
|
3852
|
+
const imageIds = /* @__PURE__ */ new Map();
|
|
3853
|
+
for (const image of reply.result.images ?? []) {
|
|
3854
|
+
if (images.length >= imageBudget) {
|
|
3855
|
+
imagesOmitted += 1;
|
|
3856
|
+
continue;
|
|
3857
|
+
}
|
|
3858
|
+
const id = `${imagePrefix}${image.id}`;
|
|
3859
|
+
imageIds.set(image.id, id);
|
|
3860
|
+
images.push({
|
|
3861
|
+
...image,
|
|
3862
|
+
id
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
imagesOmitted += reply.result.imagesOmitted ?? 0;
|
|
3866
|
+
imageFailures += reply.result.imageFailures ?? 0;
|
|
3867
|
+
nodes.push(...relabel(reply.result.nodes, frame, imageIds));
|
|
3868
|
+
for (const target of reply.targets) {
|
|
3869
|
+
targets.push(frame === void 0 ? target : {
|
|
3870
|
+
...target,
|
|
3871
|
+
frame: target.frame ?? frame
|
|
3872
|
+
});
|
|
3873
|
+
refFrames.set(target.ref, address);
|
|
3874
|
+
}
|
|
3875
|
+
if (reply.result.frameNotes !== void 0) frameNotes.push(...reply.result.frameNotes);
|
|
3876
|
+
for (const nested of reply.result.nestedFrames ?? []) queue.push({
|
|
3877
|
+
...nested,
|
|
3878
|
+
parent: address
|
|
3879
|
+
});
|
|
3880
|
+
};
|
|
3881
|
+
absorb(main.reply, mainAddress, options.mainFrameLabel, "");
|
|
3882
|
+
while (queue.length > 0) {
|
|
3883
|
+
const next = queue.shift();
|
|
3884
|
+
if (framesRead >= budget.maxFrames || nodeCount >= budget.maxNodes) {
|
|
3885
|
+
skipped.push(next.hint);
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
const address = await options.dispatcher.resolveNestedFrame(next, {
|
|
3889
|
+
parent: next.parent,
|
|
3890
|
+
visited
|
|
3891
|
+
});
|
|
3892
|
+
if (address === void 0) {
|
|
3893
|
+
nodes.push(note(`A nested frame "${next.hint}" (${next.url}) could not be addressed and was not read. It may have been removed, replaced, or it is out of reach for this host.`));
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
visited.push(address);
|
|
3897
|
+
const answer = await options.dispatcher.send(address, request).catch((e) => {
|
|
3898
|
+
return {
|
|
3899
|
+
reply: {
|
|
3900
|
+
type: "error",
|
|
3901
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
3902
|
+
message: e instanceof Error ? e.message : String(e)
|
|
3903
|
+
},
|
|
3904
|
+
documentUrl: ""
|
|
3905
|
+
};
|
|
3906
|
+
});
|
|
3907
|
+
if (answer.reply.type !== "perceive-result") {
|
|
3908
|
+
const reason = answer.reply.type === "error" ? answer.reply.message : "it returned an unexpected reply";
|
|
3909
|
+
nodes.push(note(`A nested frame "${next.hint}" (${next.url}) was not read: ${reason}`));
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
if (frameKey(answer.documentUrl) !== frameKey(next.url)) {
|
|
3913
|
+
nodes.push(note(`A nested frame "${next.hint}" (${next.url}) navigated away while it was being read (it answered from ${answer.documentUrl}); its content was discarded.`));
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
descended += 1;
|
|
3917
|
+
absorb(answer.reply, address, frameLabelOf(next), `f${descended}-`);
|
|
3918
|
+
}
|
|
3919
|
+
if (skipped.length > 0) nodes.push(note(`${skipped.length} nested frame(s) were not read because this page exceeds the per-perception budget (${budget.maxFrames} frames / ${budget.maxNodes} nodes): ${skipped.join(", ")}. The content above is complete for the frames that were read.`));
|
|
3920
|
+
return {
|
|
3921
|
+
type: "perceive-result",
|
|
3922
|
+
result: {
|
|
3923
|
+
nodes,
|
|
3924
|
+
...images.length > 0 ? { images } : {},
|
|
3925
|
+
...imagesOmitted > 0 ? { imagesOmitted } : {},
|
|
3926
|
+
...imageFailures > 0 ? { imageFailures } : {},
|
|
3927
|
+
...frameNotes.length > 0 ? { frameNotes } : {}
|
|
3928
|
+
},
|
|
3929
|
+
targets,
|
|
3930
|
+
...main.reply.document !== void 0 ? { document: main.reply.document } : {}
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
async function execute(request) {
|
|
3934
|
+
const address = refFrames.get(request.request.ref) ?? await options.dispatcher.mainFrame();
|
|
3935
|
+
return (await options.dispatcher.send(address, request)).reply;
|
|
3936
|
+
}
|
|
3937
|
+
return {
|
|
3938
|
+
perceive,
|
|
3939
|
+
execute
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
//#endregion
|
|
3944
|
+
//#region ../browser/src/host/protocol.ts
|
|
3347
3945
|
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
3348
3946
|
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
3349
3947
|
/** Worker 侧入站校验:非法消息返回 undefined(忽略) */
|
|
@@ -3431,6 +4029,9 @@ function parseWorkerEvent(data) {
|
|
|
3431
4029
|
default: return;
|
|
3432
4030
|
}
|
|
3433
4031
|
}
|
|
4032
|
+
|
|
4033
|
+
//#endregion
|
|
4034
|
+
//#region ../browser/src/ui/workerUiBridge.ts
|
|
3434
4035
|
/**
|
|
3435
4036
|
* Worker 内 UiBridge:request → 'interaction' 消息到主线程,
|
|
3436
4037
|
* 挂起等待 'interaction-response' → resolve(行内 await 跨线程成立)。
|
|
@@ -3485,6 +4086,9 @@ var WorkerUiBridge = class {
|
|
|
3485
4086
|
});
|
|
3486
4087
|
}
|
|
3487
4088
|
};
|
|
4089
|
+
|
|
4090
|
+
//#endregion
|
|
4091
|
+
//#region ../browser/src/host/workerRuntimeHost.ts
|
|
3488
4092
|
const serializeError = (e) => ({
|
|
3489
4093
|
code: e instanceof WebSkillError ? e.code : "RUN_FAILED",
|
|
3490
4094
|
message: e instanceof Error ? e.message : String(e)
|
|
@@ -3712,6 +4316,9 @@ function startWorkerRuntimeHost(scope, overrides = {}) {
|
|
|
3712
4316
|
}
|
|
3713
4317
|
});
|
|
3714
4318
|
}
|
|
4319
|
+
|
|
4320
|
+
//#endregion
|
|
4321
|
+
//#region ../browser/src/host/workerClient.ts
|
|
3715
4322
|
/** 主线程客户端:init/run/cancel/status/dispose 的 Promise API + 交互转发 */
|
|
3716
4323
|
var WorkerRuntimeClient = class {
|
|
3717
4324
|
#worker;
|
|
@@ -3857,6 +4464,9 @@ var WorkerRuntimeClient = class {
|
|
|
3857
4464
|
}
|
|
3858
4465
|
}
|
|
3859
4466
|
};
|
|
4467
|
+
|
|
4468
|
+
//#endregion
|
|
4469
|
+
//#region ../browser/src/memory/encryptedMemoryStore.ts
|
|
3860
4470
|
/**
|
|
3861
4471
|
* 静态加密的 MemoryStore 包装(FR-19.5)。落盘形态:
|
|
3862
4472
|
* `{ v: 1, iv: base64, ct: base64 }`;每次写入生成新的 12 字节随机 IV
|
|
@@ -4024,6 +4634,9 @@ async function deleteMemoryEncryptionKey(name = "user-profile") {
|
|
|
4024
4634
|
db.close();
|
|
4025
4635
|
}
|
|
4026
4636
|
}
|
|
4637
|
+
|
|
4638
|
+
//#endregion
|
|
4639
|
+
//#region ../browser/src/host/portMatrix.ts
|
|
4027
4640
|
/**
|
|
4028
4641
|
* 端口 × 宿主类型的**唯一定义**(AC-17.4)。
|
|
4029
4642
|
* About 页与装配工厂都读这一份;护栏 `port-matrix-single-source` 盯着不许再抄。
|
|
@@ -4173,6 +4786,9 @@ function inspectHostWiring(input) {
|
|
|
4173
4786
|
function missingPorts(reports) {
|
|
4174
4787
|
return reports.filter((report) => report.status === "missing");
|
|
4175
4788
|
}
|
|
4789
|
+
|
|
4790
|
+
//#endregion
|
|
4791
|
+
//#region ../browser/src/host/createBrowserChatbotHost.ts
|
|
4176
4792
|
function createBrowserChatbotHost(options) {
|
|
4177
4793
|
const { fs, managedRoot, skillRoots, navigation, overrides } = options;
|
|
4178
4794
|
const chatbotAdapter = {
|
|
@@ -4197,6 +4813,9 @@ function createBrowserChatbotHost(options) {
|
|
|
4197
4813
|
connectFacade: overrides?.connectFacade ?? connectFacade
|
|
4198
4814
|
};
|
|
4199
4815
|
}
|
|
4816
|
+
|
|
4817
|
+
//#endregion
|
|
4818
|
+
//#region ../browser/src/net/fetchLinkedDocument.ts
|
|
4200
4819
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
4201
4820
|
/**
|
|
4202
4821
|
* 默认的链接文档读取实现(分册 22 §2.2)。
|
|
@@ -4227,6 +4846,9 @@ function createFetchLinkedDocumentReader(options = {}) {
|
|
|
4227
4846
|
}
|
|
4228
4847
|
};
|
|
4229
4848
|
}
|
|
4849
|
+
|
|
4850
|
+
//#endregion
|
|
4851
|
+
//#region ../browser/src/document/docx.ts
|
|
4230
4852
|
/**
|
|
4231
4853
|
* docx 正文抽取(分册 22 §2.4 / FR-23.7)。
|
|
4232
4854
|
*
|
|
@@ -4304,6 +4926,9 @@ async function extractDocxText(bytes) {
|
|
|
4304
4926
|
}
|
|
4305
4927
|
return blocks.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
4306
4928
|
}
|
|
4929
|
+
|
|
4930
|
+
//#endregion
|
|
4931
|
+
//#region ../browser/src/document/xlsx.ts
|
|
4307
4932
|
/**
|
|
4308
4933
|
* xlsx 文本抽取(0.13.0 分册 12)。
|
|
4309
4934
|
*
|
|
@@ -4553,6 +5178,9 @@ async function extractXlsxText(bytes) {
|
|
|
4553
5178
|
}
|
|
4554
5179
|
return sections.join("\n\n");
|
|
4555
5180
|
}
|
|
5181
|
+
|
|
5182
|
+
//#endregion
|
|
5183
|
+
//#region ../browser/src/viewer/viewerCsp.ts
|
|
4556
5184
|
/**
|
|
4557
5185
|
* 文档投放面的 CSP 单一来源(0.11.0 分册 18 FR-18.3 / AC-G21)。
|
|
4558
5186
|
*
|
|
@@ -4603,6 +5231,9 @@ function viewerCspHeader(options) {
|
|
|
4603
5231
|
}
|
|
4604
5232
|
/** 本版固定的 sandbox token(供判据与装配文档引用,避免第二份清单) */
|
|
4605
5233
|
const VIEWER_SANDBOX_TOKENS = SANDBOX_TOKENS;
|
|
5234
|
+
|
|
5235
|
+
//#endregion
|
|
5236
|
+
//#region ../browser/src/viewer/documentSurface.ts
|
|
4606
5237
|
const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 2e6;
|
|
4607
5238
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
4608
5239
|
/** hello 的重发间隔:viewer 脚本加载完之前投出去的消息全部丢失 */
|
|
@@ -4674,6 +5305,16 @@ async function openDocumentSurface(input, options) {
|
|
|
4674
5305
|
function defaultOpen(url, target) {
|
|
4675
5306
|
return globalThis.open?.(url, target) ?? null;
|
|
4676
5307
|
}
|
|
5308
|
+
/**
|
|
5309
|
+
* 把 `openDocumentSurface` 柯里化成 `DocumentSurfaceHost`(0.15.0 分册 13 FR-13.3)。
|
|
5310
|
+
*
|
|
5311
|
+
* **只绑定选项,不新增任何逻辑**:体积上限、握手超时、`DOCUMENT_SURFACE_DECLINED`
|
|
5312
|
+
* 与审计事件全部沿用上面那一份实现(FR-13.5「逐条不变」的实现基础)。
|
|
5313
|
+
* @experimental
|
|
5314
|
+
*/
|
|
5315
|
+
function createDocumentSurfaceHost(options) {
|
|
5316
|
+
return { open: async (input) => void await openDocumentSurface(input, options) };
|
|
5317
|
+
}
|
|
4677
5318
|
/** viewer 是 opaque origin,`postMessage` 的 targetOrigin 只能是 `*` —— 用 channelId 做配对校验 */
|
|
4678
5319
|
function viewerOrigin(_options) {
|
|
4679
5320
|
return "*";
|
|
@@ -4732,6 +5373,9 @@ async function audit(options, input, data) {
|
|
|
4732
5373
|
}
|
|
4733
5374
|
}).catch(() => void 0);
|
|
4734
5375
|
}
|
|
5376
|
+
|
|
5377
|
+
//#endregion
|
|
5378
|
+
//#region ../browser/src/viewer/shell.ts
|
|
4735
5379
|
/**
|
|
4736
5380
|
* 启动外壳:等主应用的 hello → 回报 ready → 收文档 → 写进 DOM。
|
|
4737
5381
|
*
|
|
@@ -4764,6 +5408,9 @@ function startViewerShell(host, mount, onData) {
|
|
|
4764
5408
|
host.removeEventListener("message", onMessage);
|
|
4765
5409
|
} };
|
|
4766
5410
|
}
|
|
5411
|
+
|
|
5412
|
+
//#endregion
|
|
5413
|
+
//#region ../browser/src/viewer/blockedResources.ts
|
|
4767
5414
|
/**
|
|
4768
5415
|
* 监听 CSP 违规并把被拦资源汇总成一句话。
|
|
4769
5416
|
*
|
|
@@ -4797,4 +5444,4 @@ function originOf(blockedURI) {
|
|
|
4797
5444
|
}
|
|
4798
5445
|
|
|
4799
5446
|
//#endregion
|
|
4800
|
-
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, SANDBOX_PAGE_SCRIPT_SOURCE, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
|
|
5447
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_FRAME_BUDGET, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, SANDBOX_PAGE_SCRIPT_SOURCE, SHARD_SOFT_LIMIT, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDocumentSurfaceHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, deleteMemoryEncryptionKey, documentKey, explainResolution, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
|