@webskill/sdk 0.12.0 → 0.14.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.
Files changed (35) hide show
  1. package/dist/agent.d.ts +3 -3
  2. package/dist/agent.js +3 -3
  3. package/dist/browser.d.ts +250 -9
  4. package/dist/browser.js +1257 -95
  5. package/dist/{catalogComponents-BgAJN0p8-C3K8klJd.js → catalogComponents-BgAJN0p8-Cr55ouOg.js} +348 -14
  6. package/dist/{dist-sdKFgERo.js → dist-Bfga1TmS.js} +368 -34
  7. package/dist/{dist-DU9KDAuR.js → dist-BnqAkSS6.js} +965 -49
  8. package/dist/{dist-DqcL6jKO.js → dist-CiaDIqkV.js} +2 -2
  9. package/dist/{dist-Bev6i6Ip.js → dist-D5YhlCdY.js} +59 -1
  10. package/dist/governance.d.ts +3 -3
  11. package/dist/governance.js +2 -2
  12. package/dist/{index-C9pzXLKy.d.ts → index-CWoKOFBP.d.ts} +86 -5
  13. package/dist/{index-DFhU1uks.d.ts → index-DQwGvHAI.d.ts} +447 -22
  14. package/dist/{index-3fCHc1mQ.d.ts → index-sd-gVKey.d.ts} +2 -2
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +5 -5
  17. package/dist/mcp.d.ts +77 -3
  18. package/dist/mcp.js +78 -4
  19. package/dist/{memoryArtifactStore-52Zn9npI-LbCQaqyx.js → memoryArtifactStore-52Zn9npI-BxtUkrgV.js} +1 -1
  20. package/dist/node.d.ts +3 -3
  21. package/dist/node.js +6 -6
  22. package/dist/{openUiLibrary-BKXW7Iwx-DaymVubt.js → openUiLibrary-BKXW7Iwx-DjvEnMlJ.js} +2 -2
  23. package/dist/{skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts → skillVersionStore-D-qHk9ZE-C0OFDTQY.d.ts} +1 -1
  24. package/dist/{testing-WPTyXQYt.js → testing-Cm8MseLF.js} +2 -2
  25. package/dist/testing.d.ts +1 -1
  26. package/dist/testing.js +2 -2
  27. package/dist/{types-DLctJep_-B5G4uk2u.d.ts → types-C3V6lAeO-BgeOvc1Y.d.ts} +63 -4
  28. package/dist/ui-react.d.ts +2 -2
  29. package/dist/ui-react.js +13 -11
  30. package/dist/ui-vue.d.ts +1 -1
  31. package/dist/ui-vue.js +1 -1
  32. package/dist/ui.d.ts +3 -3
  33. package/dist/ui.js +2 -2
  34. package/dist/{webskillLitCatalog-D_zCqeQF-C9lrvvMr.js → webskillLitCatalog-D_zCqeQF-swXsWoAe.js} +1 -1
  35. package/package.json +2 -1
package/dist/browser.js CHANGED
@@ -1,7 +1,7 @@
1
- import { D as exportSkills, F as parseSkillPackManifest, G as unzipWithLimits, I as readResponseWithLimit, J as verifySkillSignature, K as validateSkills, L as readSkillSignature, M as messageOf, O as isAtomicTempPath, P as parseSkillMarkdown, T as detectSkillArchiveShapeFromFs, V as resolveInsideRoot, b as buildManifest, c as SKILL_MANIFEST_FILE, d as SKILL_PACK_FILE, g as assertRemoteUrlAllowed, h as WebSkillError, i as MANIFEST_EXCLUDED_FILES, k as isValidSkillName, p as SkillDiscovery, q as verifyManifest, s as SKILLS_LOCKFILE, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
2
- import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-sdKFgERo.js";
3
- import { T as toFrameScopes, w as toActionFrameScopes } from "./dist-DU9KDAuR.js";
4
- import { n as MockLlmClient } from "./testing-WPTyXQYt.js";
1
+ import { $ as unzipWithLimits, B as messageOf, E as buildManifest, G as readSkillSignature, H as parseSkillMarkdown, I as isAtomicTempPath, L as isValidSkillName, M as detectSkillArchiveShapeFromFs, P as exportSkills, S as assertRemoteUrlAllowed, T as buildCatalog, U as parseSkillPackManifest, W as readResponseWithLimit, Y as resolveInsideRoot, _ as SkillDiscovery, b as WebSkillError, c as MANIFEST_EXCLUDED_FILES, d as SKILLS_LOCKFILE, et as validateSkills, f as SKILL_MANIFEST_FILE, h as SKILL_PACK_FILE, nt as verifySkillSignature, tt as verifyManifest, w as atomicWriteText } from "./dist-D5YhlCdY.js";
2
+ import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-Bfga1TmS.js";
3
+ import { A as frameLabel, M as toActionFrameScopes, N as toFrameScopes, j as frameSteps } from "./dist-BnqAkSS6.js";
4
+ import { n as MockLlmClient } from "./testing-Cm8MseLF.js";
5
5
 
6
6
  //#region ../browser/dist/index.js
7
7
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -905,12 +905,16 @@ var IframeWorkerLike = class {
905
905
  #channelId;
906
906
  #hostWindow;
907
907
  #hostListener;
908
- constructor(iframe, hostWindow, channelId) {
908
+ #readyTimer;
909
+ constructor(iframe, hostWindow, channelId, readyTimeout) {
909
910
  this.#iframe = iframe;
910
911
  this.#hostWindow = hostWindow;
911
912
  this.#channelId = channelId;
912
913
  this.#hostListener = (event) => this.#onMessage(event);
913
914
  this.#hostWindow.addEventListener("message", this.#hostListener);
915
+ if (readyTimeout) this.#readyTimer = setTimeout(() => {
916
+ this.#failQueue(`Sandbox document did not become ready within ${readyTimeout.ms}ms: ${readyTimeout.documentUrl}. Ensure it is listed in both manifest sandbox.pages and web_accessible_resources.`);
917
+ }, readyTimeout.ms);
914
918
  }
915
919
  get ready() {
916
920
  return this.#ready;
@@ -919,10 +923,38 @@ var IframeWorkerLike = class {
919
923
  return this.#channelId;
920
924
  }
921
925
  markReady() {
926
+ this.#clearReadyTimer();
922
927
  this.#ready = true;
923
928
  for (const payload of this.#queue) this.#post(payload);
924
929
  this.#queue = [];
925
930
  }
931
+ #clearReadyTimer() {
932
+ if (this.#readyTimer !== void 0) {
933
+ clearTimeout(this.#readyTimer);
934
+ this.#readyTimer = void 0;
935
+ }
936
+ }
937
+ /**
938
+ * 就绪超时(FR-12.4):`createIframeWorker` 同步返回,超时无处可抛,只能走回包通道。
939
+ * 而 `#request` 按 `data.id` 匹配回包,id 对不上的消息被静默丢弃——
940
+ * 所以必须**逐条**回复队列里每一条消息的 id,否则调用方一直挂着。
941
+ */
942
+ #failQueue(message) {
943
+ const queued = this.#queue;
944
+ this.#queue = [];
945
+ for (const payload of queued) {
946
+ const id = payload.id;
947
+ if (typeof id === "string") this.emit({
948
+ id,
949
+ ok: false,
950
+ error: {
951
+ code: "TOOL_EXECUTION_FAILED",
952
+ message
953
+ }
954
+ });
955
+ }
956
+ this.terminate();
957
+ }
926
958
  #post(payload) {
927
959
  this.#iframe.contentWindow?.postMessage({
928
960
  [ENVELOPE]: true,
@@ -953,11 +985,18 @@ var IframeWorkerLike = class {
953
985
  for (const listener of this.#listeners) listener({ data });
954
986
  }
955
987
  terminate() {
988
+ this.#clearReadyTimer();
956
989
  this.#hostWindow.removeEventListener("message", this.#hostListener);
957
990
  this.#iframe.remove();
958
991
  }
959
992
  };
960
- /** srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断) */
993
+ /**
994
+ * srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断)。
995
+ *
996
+ * **这段文本被 FR-12.5 / AC-12.1 冻结在 0.13.0 基线上**:既有网页宿主不给 `documentUrl`,
997
+ * 必须逐字节拿到与升级前相同的沙箱文档。因此它不与 `SANDBOX_PAGE_SCRIPT_SOURCE` 共用模板——
998
+ * 抽模板必然改动生成文本。两份中继的一致性改由行为对拍守卫(见 `sandboxRelayParity` 用例)。
999
+ */
961
1000
  function sandboxDocument(bootstrapSource, channelId) {
962
1001
  const bootstrapJson = JSON.stringify(bootstrapSource).replace(/<\//g, "<\\/");
963
1002
  return `<!doctype html>
@@ -984,16 +1023,96 @@ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: { type: '
984
1023
  <\/script></body></html>`;
985
1024
  }
986
1025
  /**
987
- * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 classic Worker。
1026
+ * 静态沙箱页的中继脚本源码(分册 12 / FR-12.3)。
1027
+ *
1028
+ * 宿主把它落成一个独立文件并用 `<script src>` 引入——**不得内联**:
1029
+ * sandbox 页虽允许 `unsafe-inline`,但外链能让「沙箱页脚本 = SDK 原样产物」在审计时可核对。
1030
+ *
1031
+ * 与 srcdoc 形态的两点区别,都源于「静态页在加载完时还不知道要跑什么」:
1032
+ * - **引导源码**随宿主的 `sandbox-init` 消息到达,而不是内嵌在文档里;
1033
+ * - **channelId** 由第一条来自 `parent` 的消息 latch。SDK 导出的是一份与宿主无关的源码,
1034
+ * 不可能预知宿主这次生成的 id。latch 前先认 `event.source === parent`:少了这句,
1035
+ * 任何把本页嵌进去的第三方 frame 都能抢先 latch 一个自己的 id,
1036
+ * 让真正的宿主消息全部被丢弃——一个纯粹的拒绝服务面。
1037
+ *
1038
+ * 本源码不含任何扩展 API:sandbox 页拿不到它们(实证)。
1039
+ * `ENVELOPE` 与宿主侧同源(同一个模块常量),两边各写一份字面量会在改名时静默失配。
1040
+ * @experimental
1041
+ */
1042
+ const SANDBOX_PAGE_SCRIPT_SOURCE = `const ENVELOPE = ${JSON.stringify(ENVELOPE)};
1043
+ let CHANNEL_ID = null;
1044
+ let worker = null;
1045
+ function post(payload) {
1046
+ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload }, '*');
1047
+ }
1048
+ function startWorker(bootstrapSource) {
1049
+ if (worker || typeof bootstrapSource !== 'string') return;
1050
+ const blob = new Blob([bootstrapSource], { type: 'text/javascript' });
1051
+ const url = URL.createObjectURL(blob);
1052
+ // 注意:opaque origin 下 module Worker 无法加载(实证),classic Worker + data: URL 动态导入可用
1053
+ worker = new Worker(url);
1054
+ URL.revokeObjectURL(url);
1055
+ worker.addEventListener('message', (event) => post(event.data));
1056
+ worker.addEventListener('error', (event) => post({ type: 'sandbox-worker-error', message: event.message }));
1057
+ post({ type: 'sandbox-ready' });
1058
+ }
1059
+ window.addEventListener('message', (event) => {
1060
+ const data = event.data;
1061
+ if (!data || data[ENVELOPE] !== true) return;
1062
+ if (event.source !== parent) return;
1063
+ if (CHANNEL_ID === null) {
1064
+ if (typeof data.channelId !== 'string') return;
1065
+ CHANNEL_ID = data.channelId;
1066
+ } else if (data.channelId !== CHANNEL_ID) return;
1067
+ const payload = data.payload;
1068
+ if (payload && payload.type === 'sandbox-init') {
1069
+ startWorker(payload.bootstrapSource);
1070
+ return;
1071
+ }
1072
+ if (worker) worker.postMessage(payload);
1073
+ });
1074
+ `;
1075
+ /**
1076
+ * `Document` 判别式。不用 `'documentUrl' in v`:`Document` 上没有该属性,
1077
+ * 但一个只给了 `doc` 的选项对象上也没有,两者会被判到同一侧。
1078
+ */
1079
+ function isDocument(value) {
1080
+ return value.nodeType === 9;
1081
+ }
1082
+ /**
1083
+ * 创建 opaque iframe 沙箱 Worker:srcdoc(默认)或宿主指定的静态沙箱页承载 classic Worker。
988
1084
  * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
989
1085
  */
990
- function createIframeWorker(bootstrapSource, doc = document) {
1086
+ function createIframeWorker(bootstrapSource, options = {}) {
1087
+ const resolved = isDocument(options) ? { doc: options } : options;
1088
+ const doc = resolved.doc ?? document;
991
1089
  const channelId = nextChannelId();
992
1090
  const iframe = doc.createElement("iframe");
993
- iframe.setAttribute("sandbox", "allow-scripts");
994
1091
  iframe.style.display = "none";
995
- iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
996
- const worker = new IframeWorkerLike(iframe, doc.defaultView ?? window, channelId);
1092
+ const hostWindow = doc.defaultView ?? window;
1093
+ const { documentUrl } = resolved;
1094
+ if (documentUrl === void 0) {
1095
+ iframe.setAttribute("sandbox", "allow-scripts");
1096
+ iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
1097
+ const worker = new IframeWorkerLike(iframe, hostWindow, channelId);
1098
+ doc.body.appendChild(iframe);
1099
+ return worker;
1100
+ }
1101
+ const worker = new IframeWorkerLike(iframe, hostWindow, channelId, {
1102
+ ms: resolved.readyTimeoutMs ?? 1e4,
1103
+ documentUrl
1104
+ });
1105
+ iframe.addEventListener("load", () => {
1106
+ iframe.contentWindow?.postMessage({
1107
+ [ENVELOPE]: true,
1108
+ channelId,
1109
+ payload: {
1110
+ type: "sandbox-init",
1111
+ bootstrapSource
1112
+ }
1113
+ }, "*");
1114
+ });
1115
+ iframe.src = documentUrl;
997
1116
  doc.body.appendChild(iframe);
998
1117
  return worker;
999
1118
  }
@@ -1195,7 +1314,7 @@ var BrowserWorkerScriptExecutor = class {
1195
1314
  ...details !== void 0 ? { details } : {}
1196
1315
  });
1197
1316
  if (decision === "allowed") return void 0;
1198
- return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
1317
+ return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled by the sandbox capability settings` : `Capability "${capability}" was denied by the user`);
1199
1318
  };
1200
1319
  try {
1201
1320
  switch (request.kind) {
@@ -1222,7 +1341,7 @@ var BrowserWorkerScriptExecutor = class {
1222
1341
  };
1223
1342
  }
1224
1343
  case "fetchData": {
1225
- if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is disabled");
1344
+ if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is unavailable: this host wired no data source access at all. This is not the capability switch being off — a data source has to be declared in the host settings first.");
1226
1345
  const denied = await gate("fetchData", `Script "${context.skillName}" wants to fetch data source "${request.sourceId}"`, { sourceId: request.sourceId });
1227
1346
  if (denied) return denied;
1228
1347
  return {
@@ -1633,10 +1752,12 @@ function splitDataUrl(dataUrl) {
1633
1752
  /** 压到预算内;压不下去时把失败原因原样上抛给调用方转成 `CaptureFailure` */
1634
1753
  async function toBudget(id, level, blob, maxBytes) {
1635
1754
  const compressed = await compressImageToBudget(blob, maxBytes);
1755
+ const mimeType = compressed.blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
1756
+ if (!DELIVERABLE_IMAGE_MIME_TYPES.has(mimeType)) throw new Error(`the bytes normalized to "${mimeType === "" ? "no content type" : mimeType}", which is not one of ${[...DELIVERABLE_IMAGE_MIME_TYPES].join(", ")}`);
1636
1757
  const buffer = await compressed.blob.arrayBuffer();
1637
1758
  return {
1638
1759
  id,
1639
- mimeType: compressed.blob.type === "" ? "application/octet-stream" : compressed.blob.type,
1760
+ mimeType,
1640
1761
  data: base64Of(new Uint8Array(buffer)),
1641
1762
  level,
1642
1763
  originalBytes: compressed.originalBytes,
@@ -1647,15 +1768,16 @@ async function toBudget(id, level, blob, maxBytes) {
1647
1768
  async function captureImg(element, id, maxBytes) {
1648
1769
  const src = element.currentSrc !== "" ? element.currentSrc : element.src;
1649
1770
  if (src === "") throw new Error("the <img> element has no resolved source");
1771
+ const oversized = Math.max(element.naturalWidth, element.naturalHeight) > MAX_RASTER_EDGE;
1650
1772
  const inline = splitDataUrl(src);
1651
1773
  if (inline) {
1652
1774
  const bytes = Uint8Array.from(atob(inline.data), (char) => char.charCodeAt(0));
1653
- return toBudget(id, "src", new Blob([bytes], { type: inline.mimeType }), maxBytes);
1775
+ return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType }), oversized), maxBytes);
1654
1776
  }
1655
1777
  const response = await fetch(src, { mode: "cors" });
1656
1778
  if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
1657
1779
  if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
1658
- return toBudget(id, "src", await response.blob(), maxBytes);
1780
+ return toBudget(id, "src", await toDeliverableBlob(await response.blob(), oversized), maxBytes);
1659
1781
  }
1660
1782
  /** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
1661
1783
  async function captureCanvas(element, id, maxBytes) {
@@ -1664,29 +1786,229 @@ async function captureCanvas(element, id, maxBytes) {
1664
1786
  const bytes = Uint8Array.from(atob(parsed.data), (char) => char.charCodeAt(0));
1665
1787
  return toBudget(id, "canvas", new Blob([bytes], { type: parsed.mimeType }), maxBytes);
1666
1788
  }
1667
- /** L2:内联 `<svg>` 序列化。SVG 是文本,不进压缩管线——重编码成位图反而丢信息 */
1668
- function captureSvg(element, id, maxBytes) {
1669
- const markup = new XMLSerializer().serializeToString(element);
1670
- const data = base64Of(new TextEncoder().encode(markup));
1671
- const bytes = data.length;
1672
- if (bytes > maxBytes) throw new Error(`the serialized SVG is ${bytes} bytes, which exceeds the budget`);
1789
+ const SVG_NS = "http://www.w3.org/2000/svg";
1790
+ /** 无固有尺寸的 SVG 在浏览器里的默认渲染尺寸,照抄规范而不是自创 */
1791
+ const SVG_FALLBACK_SIZE = {
1792
+ width: 300,
1793
+ height: 150
1794
+ };
1795
+ /** 栅格画布的长边上限;再大也会被 `compressImageToBudget` 缩回去,只是白占内存 */
1796
+ const MAX_RASTER_EDGE = 2048;
1797
+ function numberAttr(element, name) {
1798
+ const raw = element.getAttribute?.(name);
1799
+ if (raw === null || raw === void 0) return void 0;
1800
+ const value = Number.parseFloat(raw);
1801
+ return Number.isFinite(value) && value > 0 ? value : void 0;
1802
+ }
1803
+ /**
1804
+ * 元素的显示尺寸,取不到返回 `undefined`(0.13.0 FR-20.3)。
1805
+ *
1806
+ * 逐级回退而不是只看 `getBoundingClientRect()`:没有布局的环境里它恒为 0,
1807
+ * 只认它会把所有图都判成 0 面积。**取不到就是取不到**,由调用方决定不过滤,
1808
+ * 不在这里编一个尺寸出来。
1809
+ */
1810
+ function measuredSizeOf(element) {
1811
+ const rect = element.getBoundingClientRect?.();
1812
+ if (rect !== void 0 && rect.width > 0 && rect.height > 0) return {
1813
+ width: rect.width,
1814
+ height: rect.height
1815
+ };
1816
+ const { naturalWidth, naturalHeight } = element;
1817
+ if (typeof naturalWidth === "number" && typeof naturalHeight === "number" && naturalWidth > 0 && naturalHeight > 0) return {
1818
+ width: naturalWidth,
1819
+ height: naturalHeight
1820
+ };
1821
+ const { width: propWidth, height: propHeight } = element;
1822
+ if (typeof propWidth === "number" && typeof propHeight === "number" && propWidth > 0 && propHeight > 0) return {
1823
+ width: propWidth,
1824
+ height: propHeight
1825
+ };
1826
+ const attrWidth = numberAttr(element, "width");
1827
+ const attrHeight = numberAttr(element, "height");
1828
+ if (attrWidth !== void 0 && attrHeight !== void 0) return {
1829
+ width: attrWidth,
1830
+ height: attrHeight
1831
+ };
1832
+ const box = element.getAttribute?.("viewBox")?.trim().split(/[\s,]+/);
1833
+ if (box?.length === 4) {
1834
+ const width = Number.parseFloat(box[2] ?? "");
1835
+ const height = Number.parseFloat(box[3] ?? "");
1836
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) return {
1837
+ width,
1838
+ height
1839
+ };
1840
+ }
1841
+ }
1842
+ function rasterSizeOf(element) {
1843
+ const measured = measuredSizeOf(element) ?? SVG_FALLBACK_SIZE;
1844
+ const scale = Math.min(1, MAX_RASTER_EDGE / Math.max(measured.width, measured.height));
1673
1845
  return {
1674
- id,
1675
- mimeType: "image/svg+xml",
1676
- data,
1677
- level: "canvas",
1678
- originalBytes: bytes,
1679
- bytes
1846
+ width: Math.max(1, Math.round(measured.width * scale)),
1847
+ height: Math.max(1, Math.round(measured.height * scale))
1680
1848
  };
1681
1849
  }
1682
1850
  /**
1851
+ * 解码超时。真实浏览器里 data URL 的 SVG 解码是瞬时的(不加载任何外部资源),
1852
+ * 但环境不支持图像管线时 load / error 可能一个都不来——没有这道闸门整次感知会永远挂着。
1853
+ */
1854
+ const DECODE_TIMEOUT_MS = 2e3;
1855
+ async function loadImage(url) {
1856
+ const image = new Image();
1857
+ image.src = url;
1858
+ const decoded = typeof image.decode === "function" ? image.decode() : new Promise((resolve, reject) => {
1859
+ image.onload = () => resolve();
1860
+ image.onerror = () => reject(/* @__PURE__ */ new Error("the browser could not decode the serialized SVG"));
1861
+ });
1862
+ let timer;
1863
+ try {
1864
+ await Promise.race([decoded, new Promise((_, reject) => {
1865
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("decoding the serialized SVG timed out")), DECODE_TIMEOUT_MS);
1866
+ })]);
1867
+ } finally {
1868
+ if (timer !== void 0) clearTimeout(timer);
1869
+ }
1870
+ return image;
1871
+ }
1872
+ /** 逐像素查 alpha,命中一个不透明点立刻返回;只有真正的空图才会扫完全图 */
1873
+ function isFullyTransparent(ctx, width, height) {
1874
+ const { data } = ctx.getImageData(0, 0, width, height);
1875
+ for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
1876
+ return true;
1877
+ }
1878
+ async function toPngBlob(canvas) {
1879
+ return await new Promise((resolve, reject) => {
1880
+ canvas.toBlob((blob) => {
1881
+ if (blob === null) reject(/* @__PURE__ */ new Error("the canvas could not be encoded as PNG"));
1882
+ else resolve(blob);
1883
+ }, "image/png");
1884
+ });
1885
+ }
1886
+ /**
1887
+ * L2:内联 `<svg>` 栅格化成 PNG(0.13.0 FR-20.1)。
1888
+ *
1889
+ * 三家提供方的图片通道都不收 `image/svg+xml`(分册 20 §0.1),原样序列化下发等于取不到图。
1890
+ * 经 `<img>` 加载的 SVG 被浏览器强制资源隔离:不执行脚本,不加载外部图片 / 样式 / 字体。
1891
+ * 因此这里不做污染处理**不是漏了**——画布根本不会被跨源内容污染,也不会因为取像发起外部请求。
1892
+ * 代价是页面 CSS 类与外部字体不生效,由下面的空图检测兜底(FR-20.2)。
1893
+ */
1894
+ async function captureSvg(element, id, maxBytes) {
1895
+ if (typeof Image !== "function" || typeof document === "undefined") throw new Error("this environment has no DOM image pipeline, so SVG cannot be rasterized");
1896
+ const { width, height } = rasterSizeOf(element);
1897
+ const clone = element.cloneNode(true);
1898
+ clone.setAttribute("xmlns", SVG_NS);
1899
+ clone.setAttribute("width", String(width));
1900
+ clone.setAttribute("height", String(height));
1901
+ const markup = new XMLSerializer().serializeToString(clone);
1902
+ const image = await loadImage(`data:image/svg+xml;base64,${base64Of(new TextEncoder().encode(markup))}`);
1903
+ const canvas = document.createElement("canvas");
1904
+ canvas.width = width;
1905
+ canvas.height = height;
1906
+ const ctx = canvas.getContext("2d");
1907
+ if (ctx === null) throw new Error("a 2D canvas context is unavailable");
1908
+ ctx.drawImage(image, 0, 0, width, height);
1909
+ if (isFullyTransparent(ctx, width, height)) throw new Error("the SVG rendered blank once detached from the page, so its appearance most likely comes from page CSS or external assets that do not apply inside an <img>");
1910
+ return toBudget(id, "canvas", await toPngBlob(canvas), maxBytes);
1911
+ }
1912
+ /** 模型图片通道只收这几种位图(与 runtime 的 `MODEL_IMAGE_MIME_TYPES` 同一组) */
1913
+ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
1914
+ "image/png",
1915
+ "image/jpeg",
1916
+ "image/gif",
1917
+ "image/webp"
1918
+ ]);
1919
+ /** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
1920
+ const SVG_SNIFF_BYTES = 1024;
1921
+ /**
1922
+ * WebP 动图:扩展头 `VP8X` 的 flags 字节第 1 位。位置固定在文件前 24 字节内,
1923
+ * 不必解码整图就能判出来。
1924
+ */
1925
+ function isAnimatedWebp(bytes) {
1926
+ return [
1927
+ 86,
1928
+ 80,
1929
+ 56,
1930
+ 88
1931
+ ].every((byte, index) => bytes[12 + index] === byte) && ((bytes[20] ?? 0) & 2) !== 0;
1932
+ }
1933
+ /**
1934
+ * 按魔数认图片类型(服务端声明只作兜底)。
1935
+ *
1936
+ * 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
1937
+ * 而模型侧只按 MIME 判能不能投递——照抄服务端的声明,整张图会在最后一步被丢掉,
1938
+ * 模型只收到一句「这个格式发不了」。
1939
+ */
1940
+ function sniffImageMime(bytes) {
1941
+ const at = (offset, ...signature) => signature.every((byte, index) => bytes[offset + index] === byte);
1942
+ if (at(0, 137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
1943
+ if (at(0, 255, 216, 255)) return "image/jpeg";
1944
+ if (at(0, 71, 73, 70, 56)) return "image/gif";
1945
+ if (at(0, 82, 73, 70, 70) && at(8, 87, 69, 66, 80)) return "image/webp";
1946
+ if (at(0, 66, 77)) return "image/bmp";
1947
+ const head = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, SVG_SNIFF_BYTES));
1948
+ if (/<svg[\s>]/i.test(head)) return "image/svg+xml";
1949
+ }
1950
+ /** 把任意 blob 重新编码成 PNG:先让浏览器解码,再画进画布 */
1951
+ async function rasterizeToPng(blob) {
1952
+ if (typeof document === "undefined" || typeof Image !== "function" || typeof URL?.createObjectURL !== "function") throw new Error("this environment has no DOM image pipeline, so the image cannot be re-encoded");
1953
+ const url = URL.createObjectURL(blob);
1954
+ try {
1955
+ const image = await loadImage(url);
1956
+ const natural = {
1957
+ width: image.naturalWidth,
1958
+ height: image.naturalHeight
1959
+ };
1960
+ const measured = natural.width > 0 && natural.height > 0 ? natural : SVG_FALLBACK_SIZE;
1961
+ const scale = Math.min(1, MAX_RASTER_EDGE / Math.max(measured.width, measured.height));
1962
+ const width = Math.max(1, Math.round(measured.width * scale));
1963
+ const height = Math.max(1, Math.round(measured.height * scale));
1964
+ const canvas = document.createElement("canvas");
1965
+ canvas.width = width;
1966
+ canvas.height = height;
1967
+ const ctx = canvas.getContext("2d");
1968
+ if (ctx === null) throw new Error("a 2D canvas context is unavailable");
1969
+ ctx.drawImage(image, 0, 0, width, height);
1970
+ return await toPngBlob(canvas);
1971
+ } finally {
1972
+ URL.revokeObjectURL(url);
1973
+ }
1974
+ }
1975
+ /**
1976
+ * 把取到的字节归一成模型收得下的位图(0.13.0)。
1977
+ *
1978
+ * 两类真实站点上很常见的图会在最后一步被模型侧丢掉:服务端把图报成 `application/octet-stream`,
1979
+ * 以及矢量图(三家提供方的图片通道都不收 `image/svg+xml`,分册 20 §0.1)。
1980
+ * 前者只是声明错了,按魔数改回来即可;后者必须就地栅格化,否则等于没取到图。
1981
+ *
1982
+ * **声明一律不采信**:大站 CDN 会按 `Accept` 头改发 AVIF 却仍旧报 `image/webp`,
1983
+ * 照抄声明会让端点解码失败并连累整次请求(一张图 400,37 张全废)。
1984
+ *
1985
+ * 魔数对得上也不等于发得出去,还有两类会被端点整次拒收,必须重编码成单帧位图:
1986
+ * - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位):图片通道普遍只收静态图;
1987
+ * - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
1988
+ * 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
1989
+ *
1990
+ * @param oversized 调用方已知原图长边超限(`<img>` 能直接读到 `naturalWidth`,不必为此解码)
1991
+ */
1992
+ async function toDeliverableBlob(blob, oversized = false) {
1993
+ const declared = blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
1994
+ const head = new Uint8Array(await blob.slice(0, SVG_SNIFF_BYTES).arrayBuffer());
1995
+ const sniffed = sniffImageMime(head);
1996
+ if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
1997
+ const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
1998
+ const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head);
1999
+ return oversized || animated ? await rasterizeToPng(typed) : typed;
2000
+ }
2001
+ const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
2002
+ if (mimeType === void 0) throw new Error(`the response is not a recognizable image (the server described it as "${blob.type === "" ? "no content type" : blob.type}")`);
2003
+ return await rasterizeToPng(new Blob([blob], { type: mimeType }));
2004
+ }
2005
+ /**
1683
2006
  * 取一个元素的图像(0.6.0 FR-12.1)。
1684
2007
  *
1685
2008
  * 按元素类型分派,不做无意义的尝试:`<img>` 只走 L1,`<canvas>` / `<svg>` 只走 L2,
1686
2009
  * 两者互不兜底。失败返回 `CaptureFailure` 而不是抛错——一张图抓不到不该让整次感知失败。
1687
2010
  * @experimental
1688
- */
1689
- async function captureElementImage(element, options) {
2011
+ */ async function captureElementImage(element, options) {
1690
2012
  const { id, maxBytes } = options;
1691
2013
  const tag = element.tagName.toUpperCase();
1692
2014
  if (tag === "IMG") try {
@@ -1708,11 +2030,11 @@ async function captureElementImage(element, options) {
1708
2030
  };
1709
2031
  }
1710
2032
  if (tag === "SVG") try {
1711
- return captureSvg(element, id, maxBytes);
2033
+ return await captureSvg(element, id, maxBytes);
1712
2034
  } catch (e) {
1713
2035
  return {
1714
2036
  id,
1715
- reason: `L2 could not serialize the SVG: ${describeError(e)}`,
2037
+ reason: `L2 could not rasterize the SVG: ${describeError(e)}`,
1716
2038
  triedLevels: ["canvas"]
1717
2039
  };
1718
2040
  }
@@ -1727,6 +2049,148 @@ function describeError(e) {
1727
2049
  if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
1728
2050
  return e instanceof Error ? e.message : String(e);
1729
2051
  }
2052
+ /**
2053
+ * 非标可交互元素的识别与角色提升(0.14.0 分册 15)。
2054
+ *
2055
+ * 现代前端用 `div` / `span` 做按钮是常态,它们在无障碍树里全落到 `generic`,
2056
+ * 于是拿不到句柄——模型读得到那几个字,却点不动。本模块补的就是这一段判定。
2057
+ *
2058
+ * 整套逻辑默认关闭,由 `createDomPerceptionReader` 的 `promoteRoles` 开启(D-15-1)。
2059
+ */
2060
+ /**
2061
+ * class / id 里的交互语义词根(FR-15.2)。
2062
+ *
2063
+ * 带词边界匹配:`table` 含 `tab`、`container` 含 `nav`,子串匹配会把它们全误伤(D-15-8)。
2064
+ * 图标类单列一支:`icon-view` / `el-icon-download` / `anticon-*` 这类命名带着具体动作语义。
2065
+ */
2066
+ const INTERACTIVE_TOKEN = /(?:^|[^a-z])(btn|button|actions?|click(?:able)?|link|menu|nav|tabs?|trigger|operate|operations?|handle|entry|toolbar|[a-z]*icons?(?:[-_][a-z0-9]+)?)(?:[^a-z]|$)/i;
2067
+ /** 从图标类名反推语义:`el-icon-view` → `view`、`icon-download` → `download`(FR-15.4 第 2 级) */
2068
+ const ICON_CLASS = /[a-z]*icons?[-_]([a-z0-9][a-z0-9-_]*)$/i;
2069
+ /** 框架路由属性:值非空即视为确凿的导航线索(FR-15.3 第 2 条) */
2070
+ const ROUTE_ATTRIBUTES = [
2071
+ "to",
2072
+ "routerlink",
2073
+ "data-href",
2074
+ "data-url"
2075
+ ];
2076
+ /** 只有这三种角色会被提升:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3) */
2077
+ const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
2078
+ "generic",
2079
+ "img",
2080
+ "cell"
2081
+ ]);
2082
+ /** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
2083
+ const SYNTHETIC_NAME_PREFIX = "unlabeled control";
2084
+ /** 往上找具名祖先的层数上限:再往上就是整页容器,拿它当限定语反而更含糊 */
2085
+ const SYNTHETIC_NAME_LOOKUP_DEPTH = 5;
2086
+ const classAndId = (element) => `${element.getAttribute("class") ?? ""} ${element.getAttribute("id") ?? ""}`;
2087
+ /** `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后 */
2088
+ function looksClickable(element, view) {
2089
+ if (view === null) return false;
2090
+ try {
2091
+ return view.getComputedStyle(element).cursor === "pointer";
2092
+ } catch {
2093
+ return false;
2094
+ }
2095
+ }
2096
+ /**
2097
+ * 五级信号,命中任一即为真(FR-15.1)。
2098
+ *
2099
+ * 求值顺序按**成本**排而不是按强度排:前面的廉价信号命中就短路,
2100
+ * 把 `getComputedStyle` 这条唯一会触发样式重算的路径留到最后。
2101
+ */
2102
+ function looksInteractive(element, context) {
2103
+ if (context.interactiveHint?.(element) === true) return true;
2104
+ const tabindex = element.getAttribute("tabindex");
2105
+ if (tabindex !== null && tabindex.trim() !== "" && tabindex.trim() !== "-1") return true;
2106
+ const title = element.getAttribute("title");
2107
+ if (title !== null && title.trim() !== "") return true;
2108
+ if (INTERACTIVE_TOKEN.test(classAndId(element))) return true;
2109
+ return looksClickable(element, context.view);
2110
+ }
2111
+ /** 图标类名反推出的语义词;没有图标类则 undefined */
2112
+ function iconHintOf(element) {
2113
+ for (const cls of (element.getAttribute("class") ?? "").split(/\s+/)) {
2114
+ const match = ICON_CLASS.exec(cls);
2115
+ if (match === null) continue;
2116
+ const semantic = match[1]?.split("__")[0];
2117
+ if (semantic !== void 0 && semantic !== "") return semantic;
2118
+ }
2119
+ }
2120
+ /** 宿主逃生舱的角色;命中即无条件提升成声明的角色(FR-15.6) */
2121
+ function hintedRoleOf(element, hints) {
2122
+ if (hints === void 0) return void 0;
2123
+ for (const hint of hints) for (const selector of hint.selectors) try {
2124
+ if (element.matches(selector)) return hint.role;
2125
+ } catch {}
2126
+ }
2127
+ /**
2128
+ * 确凿的导航线索(FR-15.3):拿得到就判 `link`,拿不到一律 `button`。
2129
+ *
2130
+ * **不看** `cursor` / class 词根 / `onclick` 这类间接迹象——猜错会让模型以为「点了会跳走」
2131
+ * 而放弃先把页内信息读完,代价比多标一个 `button` 大。
2132
+ */
2133
+ function navigationTargetOf(element, doc) {
2134
+ const own = resolvedHref(element, doc);
2135
+ if (own !== void 0) return own;
2136
+ for (const attribute of ROUTE_ATTRIBUTES) {
2137
+ const value = element.getAttribute(attribute);
2138
+ if (value !== null && value.trim() !== "") return value.trim();
2139
+ }
2140
+ const anchor = element.closest("a[href]");
2141
+ if (anchor !== null && anchor.getAttribute("role") === null) return resolvedHref(anchor, doc);
2142
+ }
2143
+ /** `href` 解析成绝对地址;不导航的形态(`#`、`javascript:`、空值)一律不算线索 */
2144
+ function resolvedHref(element, doc) {
2145
+ const raw = element.getAttribute("href") ?? element.getAttribute("xlink:href");
2146
+ if (raw === null) return void 0;
2147
+ const trimmed = raw.trim();
2148
+ if (trimmed === "" || trimmed === "#" || /^javascript:/i.test(trimmed)) return void 0;
2149
+ try {
2150
+ const url = new URL(trimmed, doc.baseURI);
2151
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
2152
+ } catch {
2153
+ return;
2154
+ }
2155
+ }
2156
+ /**
2157
+ * 算不出名字时合成一个可指称的称呼(FR-15.4 第 3 级)。
2158
+ *
2159
+ * 用「最近具名祖先 + 同级序位」而不是坐标:坐标随滚动和窗口尺寸变,
2160
+ * 用户在屏幕上对不上号,模型跨两次感知也认不出是同一个东西。
2161
+ */
2162
+ function synthesizeName(element, nameOf) {
2163
+ const siblings = element.parentElement?.children;
2164
+ let position = 0;
2165
+ if (siblings !== void 0) {
2166
+ for (const [index, sibling] of [...siblings].entries()) if (sibling === element) {
2167
+ position = index + 1;
2168
+ break;
2169
+ }
2170
+ }
2171
+ const ordinal = position > 0 ? `${SYNTHETIC_NAME_PREFIX} #${position}` : SYNTHETIC_NAME_PREFIX;
2172
+ let ancestor = element.parentElement;
2173
+ for (let depth = 0; depth < SYNTHETIC_NAME_LOOKUP_DEPTH && ancestor !== null; depth += 1) {
2174
+ const label = nameOf(ancestor);
2175
+ if (label !== void 0 && label !== "") return `${ordinal} in "${label}"`;
2176
+ ancestor = ancestor.parentElement;
2177
+ }
2178
+ return ordinal;
2179
+ }
2180
+ /** 提升后的角色与导航目标;未提升时返回 undefined */
2181
+ function promoteRole(input) {
2182
+ const { element, role, doc, hasActionableDescendant, context } = input;
2183
+ const hinted = hintedRoleOf(element, context.roleHints);
2184
+ if (hinted !== void 0) return { role: hinted };
2185
+ if (!PROMOTABLE_ROLES.has(role)) return void 0;
2186
+ if (hasActionableDescendant && role !== "img") return void 0;
2187
+ if (!looksInteractive(element, context)) return void 0;
2188
+ const href = navigationTargetOf(element, doc);
2189
+ return href !== void 0 ? {
2190
+ role: "link",
2191
+ href
2192
+ } : { role: "button" };
2193
+ }
1730
2194
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
1731
2195
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1732
2196
  /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
@@ -1804,6 +2268,33 @@ function ownText(element) {
1804
2268
  for (const node of element.childNodes) if (node.nodeType === 3) text += node.nodeValue ?? "";
1805
2269
  return clip(text);
1806
2270
  }
2271
+ /** 名字全在元素外面的那几种控件 */
2272
+ const FORM_CONTROL_TAGS = /* @__PURE__ */ new Set([
2273
+ "INPUT",
2274
+ "TEXTAREA",
2275
+ "SELECT"
2276
+ ]);
2277
+ /**
2278
+ * 表单控件的名字来源:`<label>`(关联式与包裹式)、`placeholder`、`title`。
2279
+ *
2280
+ * 缺了这几级,一个填着「基小建」的输入框在模型眼里就是无名的 textbox——
2281
+ * 旁边的 `<label>` 文字它读得到,却接不到这个框上,于是「值明明在结果里,
2282
+ * 模型却说不出这是谁」。操作侧的 `accessibleName` 一直取这几级,两边不一致
2283
+ * 还会让「感知时叫不出名字、点下去审计里却有名字」。
2284
+ */
2285
+ function externalNameOf(element) {
2286
+ const labels = element.labels;
2287
+ if (labels && labels.length > 0) {
2288
+ const joined = [...labels].map((node) => node.textContent ?? "").filter((part) => part.trim() !== "").join(" ");
2289
+ if (joined.trim() !== "") return clip(joined);
2290
+ }
2291
+ const wrapping = element.closest("label")?.textContent ?? "";
2292
+ if (wrapping.trim() !== "") return clip(wrapping);
2293
+ for (const attribute of ["placeholder", "title"]) {
2294
+ const value = element.getAttribute(attribute);
2295
+ if (value !== null && value.trim() !== "") return clip(value);
2296
+ }
2297
+ }
1807
2298
  function nameOf(element, doc) {
1808
2299
  const label = element.getAttribute("aria-label");
1809
2300
  if (label !== null && label.trim() !== "") return clip(label);
@@ -1816,6 +2307,7 @@ function nameOf(element, doc) {
1816
2307
  const alt = element.getAttribute("alt");
1817
2308
  if (alt !== null && alt.trim() !== "") return clip(alt);
1818
2309
  }
2310
+ if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
1819
2311
  const own = ownText(element);
1820
2312
  return own === "" ? void 0 : own;
1821
2313
  }
@@ -1859,20 +2351,50 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1859
2351
  ]);
1860
2352
  /** 活动模态的三种形态;Radix 之流用 aria-modal,原生用 <dialog open> */
1861
2353
  const MODAL_SELECTOR = "[role=dialog][open], dialog[open], [aria-modal=true]";
1862
- function describe(element, excluded, doc, view, targets, handles, frame, provenance) {
2354
+ /** 按尺寸落选取像的说明(FR-20.3);与「超名额」「取像失败」必须可区分,三者排查方向不同 */
2355
+ const ICON_SKIPPED_NOTE = "Not captured: this image is smaller than the icon threshold";
2356
+ /**
2357
+ * 元素是否小到该当图标看待(FR-20.3)。
2358
+ * 测不到尺寸就**不过滤**:没有布局的环境里宁可多取一张,也不能把图全吞了。
2359
+ */
2360
+ function isBelowMinArea(element, minImageArea) {
2361
+ if (minImageArea <= 0) return false;
2362
+ const size = measuredSizeOf(element);
2363
+ if (size === void 0) return false;
2364
+ return size.width * size.height < minImageArea;
2365
+ }
2366
+ function describe(element, excluded, doc, view, capture, handles, frame, provenance, promotion) {
1863
2367
  if (excluded.has(element)) return void 0;
1864
2368
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1865
2369
  if (hidden$1(element, view)) return void 0;
1866
2370
  const children = [];
1867
2371
  for (const child of element.children) {
1868
- const node = describe(child, excluded, doc, view, targets, handles, frame, provenance);
2372
+ const node = describe(child, excluded, doc, view, capture, handles, frame, provenance, promotion);
1869
2373
  if (node !== void 0) children.push(node);
1870
2374
  }
1871
- const name = nameOf(element, doc);
2375
+ let name = nameOf(element, doc);
1872
2376
  const value = valueOf(element);
1873
- const role = roleOf$1(element);
1874
- const href = role === "link" ? hrefOf(element) : void 0;
2377
+ let role = roleOf$1(element);
2378
+ let promotedHref;
2379
+ if (promotion !== void 0) {
2380
+ const promoted = promoteRole({
2381
+ element,
2382
+ role,
2383
+ doc,
2384
+ hasActionableDescendant: children.some((child) => child.ref !== void 0),
2385
+ context: promotion
2386
+ });
2387
+ if (promoted !== void 0) {
2388
+ role = promoted.role;
2389
+ promotedHref = promoted.href;
2390
+ if (name === void 0) name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
2391
+ }
2392
+ }
2393
+ const href = promotedHref ?? (role === "link" ? hrefOf(element) : void 0);
1875
2394
  if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
2395
+ if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0) {
2396
+ if (children.length === 1) return children[0];
2397
+ }
1876
2398
  const node = {
1877
2399
  role,
1878
2400
  ...name !== void 0 ? { name } : {},
@@ -1884,18 +2406,94 @@ function describe(element, excluded, doc, view, targets, handles, frame, provena
1884
2406
  };
1885
2407
  const ref = handles?.issue(element, role, frame);
1886
2408
  if (ref !== void 0) node.ref = ref;
1887
- if (targets !== void 0 && isCapturableElement(element)) targets.push({
2409
+ if (capture !== void 0 && isCapturableElement(element)) if (isBelowMinArea(element, capture.minImageArea)) node.imageNote = ICON_SKIPPED_NOTE;
2410
+ else capture.targets.push({
1888
2411
  element,
1889
2412
  node
1890
2413
  });
1891
2414
  return node;
1892
2415
  }
1893
2416
  /**
1894
- * 句柄发放与解析。表在 reader 实例内、每次 `read()` 整体替换:
1895
- * 全局表是第二套状态且会跨会话泄漏;不替换则页面变了还能用旧句柄点到别的东西。
2417
+ * 子树里的 `<iframe>`(含根自身),跳过 exclude 命中的整棵子树。
2418
+ * 供 FR-18.4 判断「范围里有帧但没被授权」;本函数**不读**这些帧的内容。
2419
+ */
2420
+ function nestedFramesIn(root, excluded) {
2421
+ const inExcluded = (element) => {
2422
+ for (let node = element; node !== null; node = node.parentElement) if (excluded.has(node)) return true;
2423
+ return false;
2424
+ };
2425
+ const found = [];
2426
+ if (root.tagName === "IFRAME" && !inExcluded(root)) found.push(root);
2427
+ for (const iframe of root.querySelectorAll("iframe")) if (!inExcluded(iframe)) found.push(iframe);
2428
+ return found;
2429
+ }
2430
+ /**
2431
+ * 未授权嵌套帧的指称。`<iframe>` 的可访问名来自 `title`,而 `nameOf` 走的是
2432
+ * aria/文本那一套(帧没有自身文本),所以这里单独取——**只用于说明文本**,
2433
+ * 不进任何可操作路径。
2434
+ */
2435
+ function frameHint(iframe) {
2436
+ for (const attribute of [
2437
+ "aria-label",
2438
+ "title",
2439
+ "name",
2440
+ "id"
2441
+ ]) {
2442
+ const value = iframe.getAttribute(attribute);
2443
+ if (value !== null && value.trim() !== "") return clip(value);
2444
+ }
2445
+ return "unnamed";
2446
+ }
2447
+ /**
2448
+ * 可路由的绝对地址(分册 16 FR-16.1)。
2449
+ *
2450
+ * 只认 http(s):`about:blank` / `srcdoc` / `data:` 这些外壳寻不到址,
2451
+ * 交出去只会让它拿一个匹配不上任何 frameId 的字符串去猜。
2452
+ */
2453
+ function httpUrlOf(value, base) {
2454
+ try {
2455
+ const url = new URL(value, base);
2456
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
2457
+ } catch {
2458
+ return;
2459
+ }
2460
+ }
2461
+ function routableUrlOf(iframe, doc) {
2462
+ try {
2463
+ const live = iframe.contentWindow?.location?.href;
2464
+ if (live !== void 0 && live !== "about:blank") {
2465
+ const routable = httpUrlOf(live, doc.baseURI);
2466
+ if (routable !== void 0) return routable;
2467
+ }
2468
+ } catch {}
2469
+ const src = iframe.getAttribute("src");
2470
+ if (src === null || src.trim() === "") return void 0;
2471
+ return httpUrlOf(src, doc.baseURI);
2472
+ }
2473
+ /**
2474
+ * 同源嵌套帧的文档;跨源与尚未就绪时返回 `undefined`。
2475
+ *
2476
+ * 跨源时 `contentDocument` 返回 `null` 而**不抛异常**,某些引擎在别的属性上才抛——
2477
+ * 两种形态都要接住,只写 try/catch 会漏掉前一种。
2478
+ */
2479
+ function sameOriginDocumentOf(iframe) {
2480
+ try {
2481
+ const inner = iframe.contentDocument;
2482
+ return inner === null || inner.body === null ? void 0 : inner;
2483
+ } catch {
2484
+ return;
2485
+ }
2486
+ }
2487
+ /**
2488
+ * 句柄发放与解析。表在 reader 实例内、**跨感知保留**(分册 18 FR-18.5):
2489
+ * 每次 read() 作废全部旧句柄会让多步任务陷入「读—点—失效—再读」的空转,
2490
+ * 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
2491
+ * 可见性 / 可用性四道校验才是闸门,它们按**执行那一刻**的 DOM 实时判定。
1896
2492
  */
1897
2493
  var HandleIssuer = class {
1898
2494
  #table = /* @__PURE__ */ new Map();
2495
+ /** 同一元素复用同一个 ref:模型重读页面后手里的句柄仍然指向同一个东西 */
2496
+ #refByElement = /* @__PURE__ */ new WeakMap();
1899
2497
  #actionable;
1900
2498
  #excluded;
1901
2499
  constructor(actionable, excluded) {
@@ -1907,9 +2505,24 @@ var HandleIssuer = class {
1907
2505
  this.#actionable = actionable;
1908
2506
  this.#excluded = excluded;
1909
2507
  }
2508
+ /**
2509
+ * 丢弃已离开文档的条目。**只防表随长会话无限膨胀,不是安全判据**——
2510
+ * 「元素还在但已挪出授权范围」这一类由执行前的第二道闸门拦(FR-18.5)。
2511
+ */
2512
+ prune() {
2513
+ for (const [ref, entry] of this.#table) if (!entry.element.isConnected) this.#table.delete(ref);
2514
+ }
1910
2515
  issue(element, role, frame) {
1911
2516
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
1912
2517
  if (!ACTIONABLE_ROLES.has(role)) return void 0;
2518
+ const known = this.#refByElement.get(element);
2519
+ if (known !== void 0 && this.#table.has(known)) {
2520
+ this.#table.set(known, {
2521
+ element,
2522
+ ...frame !== void 0 ? { frame } : {}
2523
+ });
2524
+ return known;
2525
+ }
1913
2526
  const bytes = /* @__PURE__ */ new Uint8Array(8);
1914
2527
  crypto.getRandomValues(bytes);
1915
2528
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -1917,6 +2530,7 @@ var HandleIssuer = class {
1917
2530
  element,
1918
2531
  ...frame !== void 0 ? { frame } : {}
1919
2532
  });
2533
+ this.#refByElement.set(element, ref);
1920
2534
  return ref;
1921
2535
  }
1922
2536
  get table() {
@@ -1941,41 +2555,60 @@ function createDomPerceptionReader(options = {}) {
1941
2555
  */
1942
2556
  const elevatedModals = /* @__PURE__ */ new Set();
1943
2557
  const rootDocument = () => options.document ?? globalThis.document;
2558
+ /** 提升未开启时返回 undefined,遍历侧据此整段跳过(AC-G10) */
2559
+ const promotionOf = (view) => options.promoteRoles === true ? {
2560
+ view,
2561
+ ...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
2562
+ ...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {}
2563
+ } : void 0;
1944
2564
  /**
1945
- * 把一条帧授权解析成 Document
2565
+ * 把一条帧授权解析成 Document,逐层下钻(分册 18 FR-18.3)。
1946
2566
  *
1947
2567
  * 跨源时 `contentDocument` 在多数浏览器返回 `null` 而**不抛异常**,
1948
2568
  * 只有部分属性访问才抛 —— 所以两种形态都要处理,只写 try/catch 会漏。
1949
2569
  */
1950
2570
  function resolveFrame(frame, root) {
1951
- if (frame === "self") return { doc: root };
1952
- const element = root.querySelector(frame);
1953
- if (element === null || element.tagName !== "IFRAME") return { note: {
1954
- frame,
1955
- reason: "not-found",
1956
- message: `Frame "${frame}" was not found in the page.`
1957
- } };
1958
- let doc;
1959
- let origin;
1960
- try {
1961
- doc = element.contentDocument;
1962
- origin = element.contentWindow?.location.origin;
1963
- } catch {
1964
- doc = null;
1965
- }
1966
- if (doc === null || origin === void 0) return { note: {
1967
- frame,
1968
- reason: "cross-origin",
1969
- message: `Frame "${frame}" is cross-origin and was not read.`
1970
- } };
1971
- const known = frameOrigins.get(frame);
1972
- if (known === void 0) frameOrigins.set(frame, origin);
1973
- else if (known !== origin) return { note: {
1974
- frame,
1975
- reason: "origin-changed",
1976
- message: `Frame "${frame}" now points at ${origin} instead of the authorized ${known}; the grant was revoked.`
1977
- } };
1978
- return { doc };
2571
+ const steps = frameSteps(frame);
2572
+ const label = frameLabel(frame);
2573
+ let doc = root;
2574
+ const frames = [];
2575
+ for (const [index, selector] of steps.entries()) {
2576
+ const hop = `step ${index + 1} ("${selector}") of frame path "${label}"`;
2577
+ const element = doc.querySelector(selector);
2578
+ if (element === null || element.tagName !== "IFRAME") return { note: {
2579
+ frame: label,
2580
+ reason: "not-found",
2581
+ message: `Frame ${hop} was not found in the page.`
2582
+ } };
2583
+ const iframe = element;
2584
+ let next;
2585
+ let origin;
2586
+ try {
2587
+ next = iframe.contentDocument;
2588
+ origin = iframe.contentWindow?.location.origin;
2589
+ } catch {
2590
+ next = null;
2591
+ }
2592
+ if (next === null || origin === void 0) return { note: {
2593
+ frame: label,
2594
+ reason: "cross-origin",
2595
+ message: `Frame ${hop} is cross-origin and was not read.`
2596
+ } };
2597
+ const key = frameLabel(steps.slice(0, index + 1));
2598
+ const known = frameOrigins.get(key);
2599
+ if (known === void 0) frameOrigins.set(key, origin);
2600
+ else if (known !== origin) return { note: {
2601
+ frame: label,
2602
+ reason: "origin-changed",
2603
+ message: `Frame ${hop} now points at ${origin} instead of the authorized ${known}; the grant was revoked.`
2604
+ } };
2605
+ doc = next;
2606
+ frames.push(iframe);
2607
+ }
2608
+ return {
2609
+ doc,
2610
+ frames
2611
+ };
1979
2612
  }
1980
2613
  /** 按当前 DOM 重算一次可操作集合;发句柄与执行前各算一次 */
1981
2614
  const actionSets = (doc, scope) => {
@@ -1998,49 +2631,126 @@ function createDomPerceptionReader(options = {}) {
1998
2631
  excluded
1999
2632
  };
2000
2633
  };
2001
- /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」 */
2002
- const actionScopeFor = (frame) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => entry.frame === frame);
2003
- function collect(scope, targets) {
2634
+ /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
2635
+ const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
2636
+ function collect(scope, capture) {
2004
2637
  const root = rootDocument();
2005
2638
  if (root === void 0) return {
2006
2639
  nodes: [],
2007
- frameNotes: []
2640
+ frameNotes: [],
2641
+ nestedFrames: []
2008
2642
  };
2009
- handles = new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2643
+ handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2644
+ handles.prune();
2645
+ const issuer = handles;
2010
2646
  const nodes = [];
2011
2647
  const frameNotes = [];
2648
+ const nestedFrames = [];
2649
+ /** 同一个未授权 iframe 至多报一次(它可能同时落在多个 include 根的子树里) */
2650
+ const reportedUnauthorized = /* @__PURE__ */ new Set();
2651
+ const resolvedScopes = [];
2652
+ const authorizedFrames = /* @__PURE__ */ new Set();
2012
2653
  for (const frameScope of toFrameScopes(scope)) {
2013
2654
  if (frameScope.include.length === 0) continue;
2655
+ const label = frameLabel(frameScope.frame);
2014
2656
  const resolved = resolveFrame(frameScope.frame, root);
2015
2657
  if ("note" in resolved) {
2016
2658
  frameNotes.push(resolved.note);
2017
2659
  nodes.push({
2018
2660
  role: "note",
2019
2661
  name: resolved.note.message,
2020
- frame: frameScope.frame
2662
+ frame: label
2021
2663
  });
2022
2664
  continue;
2023
2665
  }
2024
- const doc = resolved.doc;
2666
+ for (const iframe of resolved.frames) authorizedFrames.add(iframe);
2667
+ resolvedScopes.push({
2668
+ scope: frameScope,
2669
+ label,
2670
+ doc: resolved.doc
2671
+ });
2672
+ }
2673
+ /**
2674
+ * 范围内未授权嵌套帧的处置(分册 16 FR-16.1)。
2675
+ *
2676
+ * 递归而不是循环固定层数:0.13.0 D-18-8 用「路径是宿主写死的数组」论证深度上限,
2677
+ * 自动发现一进来这个前提就没了。兜底改由**帧数与节点总预算**承担,
2678
+ * 层数是错的度量(见分册 16 §0.2)。
2679
+ */
2680
+ function descendFrames(roots, doc, excluded, frameScope, label, tag, actionable, actionExcluded) {
2681
+ for (const element of roots) for (const iframe of nestedFramesIn(element, excluded)) {
2682
+ if (authorizedFrames.has(iframe)) continue;
2683
+ if (reportedUnauthorized.has(iframe)) continue;
2684
+ reportedUnauthorized.add(iframe);
2685
+ const hint = frameHint(iframe);
2686
+ if (options.discoverNestedFrames === true) {
2687
+ const url = routableUrlOf(iframe, doc);
2688
+ if (url !== void 0) {
2689
+ nestedFrames.push({
2690
+ url,
2691
+ hint,
2692
+ ...tag !== void 0 ? { frame: tag } : {}
2693
+ });
2694
+ continue;
2695
+ }
2696
+ const inner = sameOriginDocumentOf(iframe);
2697
+ if (inner !== void 0 && inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded)) continue;
2698
+ }
2699
+ nodes.push({
2700
+ role: "note",
2701
+ name: `A nested frame "${hint}" inside "${label}" is not part of the authorized scope and was not read. Ask the user to authorize it if its content is needed.`,
2702
+ ...tag !== void 0 ? { frame: tag } : {}
2703
+ });
2704
+ }
2705
+ }
2706
+ /**
2707
+ * 无地址同源帧的就地内联;成功返回 `true`。
2708
+ *
2709
+ * `srcdoc` / `about:blank` 没有可路由地址,外壳寻不到址,只能由父帧代读。
2710
+ *
2711
+ * 可操作性**继承自那个 `<iframe>` 元素本身**:它由父页面写入内容,视觉上就是父页面的一部分,
2712
+ * 父帧的 action scope 把它圈进来了就等于圈进了它的内容;没圈进来则帧内一个句柄都不发。
2713
+ * exclude 在子文档里重新求值——敏感控件在每一层都要剪掉,不因为它在帧里就放宽(§0.1 边界 3)。
2714
+ */
2715
+ function inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded) {
2716
+ const innerExcluded = /* @__PURE__ */ new Set();
2717
+ for (const selector of frameScope.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerExcluded.add(target);
2718
+ const innerActionable = /* @__PURE__ */ new Set();
2719
+ const innerActionExcluded = /* @__PURE__ */ new Set();
2720
+ if (actionable.has(iframe)) {
2721
+ innerActionable.add(inner.body);
2722
+ for (const descendant of inner.body.querySelectorAll("*")) innerActionable.add(descendant);
2723
+ for (const selector of actionScopeFor(label)?.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerActionExcluded.add(target);
2724
+ }
2725
+ issuer.useSets(innerActionable, innerActionExcluded);
2726
+ const innerView = inner.defaultView;
2727
+ const node = describe(inner.body, innerExcluded, inner, innerView, capture, issuer, tag, "inline-frame", promotionOf(innerView));
2728
+ if (node !== void 0) nodes.push(node);
2729
+ descendFrames([inner.body], inner, innerExcluded, frameScope, label, tag, innerActionable, innerActionExcluded);
2730
+ issuer.useSets(actionable, actionExcluded);
2731
+ return node !== void 0;
2732
+ }
2733
+ for (const { scope: frameScope, label, doc } of resolvedScopes) {
2025
2734
  const view = doc.defaultView;
2026
- const tag = frameScope.frame === "self" ? void 0 : frameScope.frame;
2027
- const sets = actionSets(doc, actionScopeFor(frameScope.frame));
2028
- handles.useSets(sets.actionable, sets.excluded);
2735
+ const tag = label === "self" ? void 0 : label;
2736
+ const sets = actionSets(doc, actionScopeFor(label));
2737
+ issuer.useSets(sets.actionable, sets.excluded);
2029
2738
  const excluded = /* @__PURE__ */ new Set();
2030
2739
  for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
2031
2740
  const roots = [];
2032
2741
  for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2033
2742
  for (const element of roots) {
2034
- const node = describe(element, excluded, doc, view, targets, handles, tag, void 0);
2743
+ const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
2035
2744
  if (node !== void 0) nodes.push(node);
2036
2745
  }
2746
+ descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
2037
2747
  }
2038
2748
  pruneElevated();
2039
2749
  const view = root.defaultView;
2040
2750
  const sets = actionSets(root, actionScopeFor("self"));
2041
2751
  for (const modal of elevatedModals) {
2042
- handles.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2043
- const node = describe(modal, /* @__PURE__ */ new Set(), root, view, targets, handles, void 0, "modal-elevated");
2752
+ issuer.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2753
+ const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated", promotionOf(view));
2044
2754
  if (node !== void 0) nodes.push(node);
2045
2755
  }
2046
2756
  for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
@@ -2053,12 +2763,16 @@ function createDomPerceptionReader(options = {}) {
2053
2763
  }
2054
2764
  return {
2055
2765
  nodes,
2056
- frameNotes
2766
+ frameNotes,
2767
+ nestedFrames
2057
2768
  };
2058
2769
  }
2059
2770
  async function readWithImages(scope, capture) {
2060
2771
  const targets = [];
2061
- const { nodes, frameNotes } = collect(scope, targets);
2772
+ const { nodes, frameNotes, nestedFrames } = collect(scope, {
2773
+ targets,
2774
+ minImageArea: capture.minImageArea ?? 0
2775
+ });
2062
2776
  const selected = targets.slice(0, capture.maxImages);
2063
2777
  const images = [];
2064
2778
  let imageFailures = 0;
@@ -2086,16 +2800,18 @@ function createDomPerceptionReader(options = {}) {
2086
2800
  images,
2087
2801
  imagesOmitted: targets.length - selected.length,
2088
2802
  imageFailures,
2089
- ...frameNotes.length > 0 ? { frameNotes } : {}
2803
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2804
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2090
2805
  };
2091
2806
  }
2092
2807
  function read(scope, capture) {
2093
2808
  if (capture === void 0) return collect(scope, void 0).nodes;
2094
2809
  if (!capture.images || capture.maxImages <= 0) {
2095
- const { nodes, frameNotes } = collect(scope, void 0);
2810
+ const { nodes, frameNotes, nestedFrames } = collect(scope, void 0);
2096
2811
  return Promise.resolve({
2097
2812
  nodes,
2098
- ...frameNotes.length > 0 ? { frameNotes } : {}
2813
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2814
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2099
2815
  });
2100
2816
  }
2101
2817
  return readWithImages(scope, capture);
@@ -2137,14 +2853,21 @@ function createDomPerceptionReader(options = {}) {
2137
2853
  return sets.actionable.has(element) && !sets.excluded.has(element);
2138
2854
  }
2139
2855
  };
2140
- /** 反查一个 Document 属于哪条帧授权;不在授权列表里的帧一律判不可操作 */
2856
+ /**
2857
+ * 反查一个 Document 属于哪条帧授权,返回它的展示串;不在授权列表里的帧一律判不可操作。
2858
+ * 没有反向解析展示串(那是不成立的,D-18-1),而是把每条授权路径重新走一遍后比对
2859
+ * 文档对象:路径中任一层被换掉 / origin 变了,这里就对不上,旧句柄因此失效。
2860
+ */
2141
2861
  function frameOfDocument(doc) {
2142
2862
  const root = rootDocument();
2143
2863
  if (root === void 0) return void 0;
2144
2864
  if (doc === root) return "self";
2145
- for (const frame of frameOrigins.keys()) {
2146
- const element = root.querySelector(frame);
2147
- if (element?.tagName === "IFRAME" && element.contentDocument === doc) return frame;
2865
+ if (options.actionScope === void 0) return void 0;
2866
+ for (const entry of toActionFrameScopes(options.actionScope)) {
2867
+ if (frameSteps(entry.frame).length === 0) continue;
2868
+ const resolved = resolveFrame(entry.frame, root);
2869
+ if ("note" in resolved) continue;
2870
+ if (resolved.doc === doc) return frameLabel(entry.frame);
2148
2871
  }
2149
2872
  }
2150
2873
  }
@@ -2367,13 +3090,27 @@ function createDomPageActionExecutor(options) {
2367
3090
  if (hidden(element)) return fail("The element is not visible.");
2368
3091
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
2369
3092
  const modalsBefore = reader.modalSnapshot();
3093
+ const frameView = element.ownerDocument.defaultView;
3094
+ const urlBefore = frameView?.location.href;
2370
3095
  const done = async (outcome) => {
2371
3096
  if (!outcome.ok) return outcome;
2372
- const elevated = await waitFor(() => reader.elevateNewModals(modalsBefore), 500) ?? reader.elevateNewModals(modalsBefore);
3097
+ const settled = await waitFor(() => {
3098
+ const modal = reader.elevateNewModals(modalsBefore);
3099
+ if (modal !== void 0) return { modal };
3100
+ const url = frameView?.location.href;
3101
+ return url !== void 0 && url !== urlBefore ? { url } : void 0;
3102
+ }, 500);
3103
+ const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
2373
3104
  const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
2374
- return name === void 0 ? outcome : {
3105
+ const urlAfter = frameView?.location.href;
3106
+ const navigated = urlAfter !== void 0 && urlBefore !== void 0 && urlAfter !== urlBefore;
3107
+ return {
2375
3108
  ...outcome,
2376
- elevatedModal: name
3109
+ ...name !== void 0 ? { elevatedModal: name } : {},
3110
+ ...navigated ? {
3111
+ navigated: true,
3112
+ documentUrl: urlAfter
3113
+ } : {}
2377
3114
  };
2378
3115
  };
2379
3116
  if (request.action === "click") {
@@ -2431,6 +3168,182 @@ function createDomPageActionExecutor(options) {
2431
3168
  describe
2432
3169
  };
2433
3170
  }
3171
+ /** @experimental */
3172
+ function createRemoteTargetRegistry() {
3173
+ let table = /* @__PURE__ */ new Map();
3174
+ return {
3175
+ /**
3176
+ * 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
3177
+ * 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
3178
+ * (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
3179
+ * 否则远端比本地更松。
3180
+ */
3181
+ replace(entries) {
3182
+ const next = /* @__PURE__ */ new Map();
3183
+ for (const entry of entries) {
3184
+ const { ref, role, name, secret, frame, elevated } = entry;
3185
+ next.set(ref, {
3186
+ role,
3187
+ ...name !== void 0 ? { name } : {},
3188
+ ...secret === true ? { secret: true } : {},
3189
+ ...frame !== void 0 ? { frame } : {},
3190
+ ...elevated === true ? { elevated: true } : {}
3191
+ });
3192
+ }
3193
+ table = next;
3194
+ },
3195
+ get(ref) {
3196
+ return table.get(ref);
3197
+ },
3198
+ get size() {
3199
+ return table.size;
3200
+ }
3201
+ };
3202
+ }
3203
+ /** 缺省单次往返上限(FR-13.2 / FR-13.3) */
3204
+ const DEFAULT_PAGE_AGENT_TIMEOUT_MS = 1e4;
3205
+ /**
3206
+ * 一次带超时的往返,并把三类失败统一成 `WebSkillError`(FR-13.6)。
3207
+ *
3208
+ * 三类都必须显式失败,**绝不降级成空结果**:
3209
+ * 空节点数组的含义是「页面上什么都没有」,模型收到它会据此作答,
3210
+ * 比报错危险得多——报错至少不会产出一个自信的错误回答。
3211
+ *
3212
+ * @param expect 期望的应答类型;对不上通常意味着装错了 agent 版本,
3213
+ * 静默容忍会把问题推到很远的地方才暴露。
3214
+ */
3215
+ async function roundTrip(transport, request, expect, code, timeoutMs) {
3216
+ let timer;
3217
+ let reply;
3218
+ try {
3219
+ reply = await Promise.race([transport.send(request), new Promise((_resolve, reject) => {
3220
+ timer = setTimeout(() => reject(new WebSkillError(code, `Page agent request "${request.type}" timed out after ${timeoutMs}ms with no reply from the page.`)), timeoutMs);
3221
+ })]);
3222
+ } catch (e) {
3223
+ if (e instanceof WebSkillError) throw e;
3224
+ const message = e instanceof Error ? e.message : String(e);
3225
+ throw new WebSkillError(code, `Page agent request "${request.type}" failed: ${message}`);
3226
+ } finally {
3227
+ if (timer !== void 0) clearTimeout(timer);
3228
+ }
3229
+ if (reply.type === "error") throw new WebSkillError(reply.code, reply.message);
3230
+ if (reply.type !== expect) throw new WebSkillError(code, `Page agent replied with "${reply.type}" to a "${request.type}" request; expected "${expect}".`);
3231
+ return reply;
3232
+ }
3233
+ /**
3234
+ * 跨上下文的页面感知(分册 13 / FR-13.2)。
3235
+ *
3236
+ * 把句柄描述表写进 registry,**只把 `result` 返回给调用方**——
3237
+ * 返回值就是标准 `PerceptionResult`,不夹带任何新字段(AC-13.2)。
3238
+ * @experimental
3239
+ */
3240
+ function createRemotePerceptionReader(options) {
3241
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3242
+ return { async read(scope, capture) {
3243
+ const reply = await roundTrip(transport, {
3244
+ type: "perceive",
3245
+ scope,
3246
+ ...capture !== void 0 ? { capture } : {}
3247
+ }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
3248
+ targets.replace(reply.targets);
3249
+ return reply.result;
3250
+ } };
3251
+ }
3252
+ /**
3253
+ * 跨上下文的页面操作执行器(分册 13 / FR-13.3)。
3254
+ * @experimental
3255
+ */
3256
+ function createRemotePageActionExecutor(options) {
3257
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3258
+ return {
3259
+ /**
3260
+ * **同步**查表。`PageActionPolicy.act()` 在弹确认卡之前调它,
3261
+ * 拿到的 target 决定卡上写什么、是否 `secret`、是否 `elevated`——
3262
+ * 改成异步就是改 `@webskill/agent` 的公开契约。
3263
+ *
3264
+ * 表里没有即 `undefined`,于是 `act()` 抛 `PAGE_ACTION_STALE_REF`:
3265
+ * 没感知过就执行 = 拒绝,宁可让模型多感知一次(fail-closed)。
3266
+ */
3267
+ describe(ref) {
3268
+ return targets.get(ref);
3269
+ },
3270
+ /**
3271
+ * 原样返回远端**执行那一刻**回填的 outcome(FR-13.5):
3272
+ * 不用 registry 里的快照拼装,也不补默认值——
3273
+ * 补出来的 `false` 与「远端没报」在审计上不是一回事。
3274
+ */
3275
+ async execute(request) {
3276
+ return (await roundTrip(transport, {
3277
+ type: "execute",
3278
+ request
3279
+ }, "execute-result", "TOOL_EXECUTION_FAILED", timeoutMs)).outcome;
3280
+ }
3281
+ };
3282
+ }
3283
+ /** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
3284
+ function collectRefs(nodes, out = []) {
3285
+ for (const node of nodes) {
3286
+ if (node.ref !== void 0) out.push(node.ref);
3287
+ if (node.children !== void 0) collectRefs(node.children, out);
3288
+ }
3289
+ return out;
3290
+ }
3291
+ /**
3292
+ * 页面侧处理器(分册 13 / FR-13.7)。
3293
+ *
3294
+ * 必须是工厂而不是裸函数:`DomPerceptionReader` **是有状态的**——
3295
+ * `resolve(ref)` 依赖上一次感知发出的句柄表,模态提升状态也跨调用累积。
3296
+ * 每次请求新建 reader,`execute` 就永远 resolve 不到 `perceive` 发出的 ref。
3297
+ *
3298
+ * 它**不自己收发消息**:只做「请求进、应答出」。监听消息通道、判断消息是不是给自己的、
3299
+ * 回不回 `true`——那些全是宿主的事,本包不得出现任何扩展 API。
3300
+ * @experimental
3301
+ */
3302
+ function createPageAgentHandler(options = {}) {
3303
+ const reader = createDomPerceptionReader({
3304
+ ...options.document !== void 0 ? { document: options.document } : {},
3305
+ ...options.actionScope !== void 0 ? { actionScope: options.actionScope } : {},
3306
+ ...options.promoteRoles !== void 0 ? { promoteRoles: options.promoteRoles } : {},
3307
+ ...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
3308
+ ...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {},
3309
+ ...options.discoverNestedFrames !== void 0 ? { discoverNestedFrames: options.discoverNestedFrames } : {}
3310
+ });
3311
+ const executor = createDomPageActionExecutor({
3312
+ reader,
3313
+ ...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
3314
+ });
3315
+ return { async handle(request) {
3316
+ try {
3317
+ if (request.type === "perceive") {
3318
+ const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
3319
+ const result = Array.isArray(raw) ? { nodes: raw } : raw;
3320
+ const targets = [];
3321
+ for (const ref of collectRefs(result.nodes)) {
3322
+ const target = executor.describe(ref);
3323
+ if (target !== void 0) targets.push({
3324
+ ref,
3325
+ ...target
3326
+ });
3327
+ }
3328
+ return {
3329
+ type: "perceive-result",
3330
+ result,
3331
+ targets
3332
+ };
3333
+ }
3334
+ return {
3335
+ type: "execute-result",
3336
+ outcome: await executor.execute(request.request)
3337
+ };
3338
+ } catch (e) {
3339
+ return {
3340
+ type: "error",
3341
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
3342
+ message: e instanceof Error ? e.message : String(e)
3343
+ };
3344
+ }
3345
+ } };
3346
+ }
2434
3347
  const isRecord = (v) => typeof v === "object" && v !== null;
2435
3348
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
2436
3349
  /** Worker 侧入站校验:非法消息返回 undefined(忽略) */
@@ -3392,6 +4305,255 @@ async function extractDocxText(bytes) {
3392
4305
  return blocks.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3393
4306
  }
3394
4307
  /**
4308
+ * xlsx 文本抽取(0.13.0 分册 12)。
4309
+ *
4310
+ * **零新增依赖**:xlsx 和 docx 是同一种容器(OPC = zip + XML),
4311
+ * 所以沿用 docx 那条路 —— zip 走仓内既有的 `unzipWithLimits`(fflate,
4312
+ * 自带解压体积上限,顺带挡住 zip 炸弹),XML 走浏览器原生 `DOMParser`。
4313
+ *
4314
+ * ⚠️ `DOMParser` 是浏览器 API,Node 没有 —— 所以这个模块只在 `@webskill/browser`。
4315
+ * Node 宿主没有 xlsx 能力,与 docx 同一条**显式声明**的限制,不是静默降级(备案 D55)。
4316
+ */
4317
+ /**
4318
+ * **不抽取**的内容,逐条写明而不是笼统说「尽力而为」(FR-12.4)。
4319
+ *
4320
+ * 公式只取 Excel 自己写进文件的缓存值:零依赖求值等于自写一个表达式引擎。
4321
+ */
4322
+ const XLSX_UNEXTRACTED = [
4323
+ "styles",
4324
+ "conditional formatting",
4325
+ "merged cell reconstruction",
4326
+ "charts",
4327
+ "images",
4328
+ "comments",
4329
+ "pivot tables",
4330
+ "macros",
4331
+ "hidden row and column marking",
4332
+ "formula expressions (only cached values are read)"
4333
+ ];
4334
+ const WORKBOOK = "xl/workbook.xml";
4335
+ const WORKBOOK_RELS = "xl/_rels/workbook.xml.rels";
4336
+ const SHARED_STRINGS = "xl/sharedStrings.xml";
4337
+ const STYLES = "xl/styles.xml";
4338
+ /** OLE2 复合文档签名:旧版 .xls 的开头,它不是 zip,走不到解压那一步 */
4339
+ const OLE2_SIGNATURE = [
4340
+ 208,
4341
+ 207,
4342
+ 17,
4343
+ 224,
4344
+ 161,
4345
+ 177,
4346
+ 26,
4347
+ 225
4348
+ ];
4349
+ /** 内建的日期/时间数字格式 id(ECMA-376 §18.8.30) */
4350
+ const BUILTIN_DATE_FORMATS = /* @__PURE__ */ new Set([
4351
+ 14,
4352
+ 15,
4353
+ 16,
4354
+ 17,
4355
+ 18,
4356
+ 19,
4357
+ 20,
4358
+ 21,
4359
+ 22,
4360
+ 45,
4361
+ 46,
4362
+ 47
4363
+ ]);
4364
+ function fail(message) {
4365
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", message);
4366
+ }
4367
+ /** 按 localName 取子孙元素,忽略命名空间前缀:不同生成器的前缀写法并不统一 */
4368
+ const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
4369
+ function parseXml(bytes, what) {
4370
+ const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
4371
+ if (doc.getElementsByTagName("parsererror").length > 0) fail(`The workbook ${what} could not be parsed as XML.`);
4372
+ return doc;
4373
+ }
4374
+ /** `B` → 1、`AA` → 26;拿不到列号时返回 undefined,由调用方顺位补上 */
4375
+ function columnIndex(ref) {
4376
+ if (ref === null) return void 0;
4377
+ let index = 0;
4378
+ let seen = false;
4379
+ for (const ch of ref) {
4380
+ const code = ch.charCodeAt(0);
4381
+ if (code < 65 || code > 90) break;
4382
+ index = index * 26 + (code - 64);
4383
+ seen = true;
4384
+ }
4385
+ return seen ? index - 1 : void 0;
4386
+ }
4387
+ /**
4388
+ * 一个 `<si>` 的文本。富文本会被切成多个 `<r>`,须拼接;
4389
+ * `<rPh>` 是日文注音,拼进去会在正文里插入读音噪声,跳过。
4390
+ */
4391
+ function sharedStringText(si) {
4392
+ let out = "";
4393
+ for (const child of si.children) {
4394
+ const tag = child.localName;
4395
+ if (tag === "t") out += child.textContent ?? "";
4396
+ else if (tag === "r") {
4397
+ for (const run of child.children) if (run.localName === "t") out += run.textContent ?? "";
4398
+ }
4399
+ }
4400
+ return out;
4401
+ }
4402
+ /**
4403
+ * 数字格式是否表示日期/时间。
4404
+ *
4405
+ * `[h]:mm:ss` 这类方括号包住时分秒的是**经过时长**(可以是 25:30),不是时刻,
4406
+ * 按日期换算会得出荒谬结果,先排掉。
4407
+ */
4408
+ function isDateFormatCode(code) {
4409
+ if (/\[[hms]+\]/i.test(code)) return false;
4410
+ const stripped = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "").replace(/\\./g, "");
4411
+ return /[ymdhs]/i.test(stripped);
4412
+ }
4413
+ /** 单元格样式索引 → 是否日期格式 */
4414
+ function readDateStyles(bytes) {
4415
+ if (bytes === void 0) return [];
4416
+ const doc = parseXml(bytes, "styles");
4417
+ const custom = /* @__PURE__ */ new Map();
4418
+ for (const fmt of byTag(doc, "numFmt")) {
4419
+ const id = Number(fmt.getAttribute("numFmtId"));
4420
+ const code = fmt.getAttribute("formatCode");
4421
+ if (Number.isFinite(id) && code !== null) custom.set(id, code);
4422
+ }
4423
+ const cellXfs = byTag(doc, "cellXfs")[0];
4424
+ if (cellXfs === void 0) return [];
4425
+ return [...cellXfs.children].filter((xf) => xf.localName === "xf").map((xf) => {
4426
+ const id = Number(xf.getAttribute("numFmtId") ?? "0");
4427
+ if (!Number.isFinite(id)) return false;
4428
+ const code = custom.get(id);
4429
+ return code !== void 0 ? isDateFormatCode(code) : BUILTIN_DATE_FORMATS.has(id);
4430
+ });
4431
+ }
4432
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
4433
+ /**
4434
+ * Excel 日期序列号 → ISO 8601。
4435
+ *
4436
+ * 1900 历制里序列号 60 是**不存在的 1900-02-29**:Excel 为兼容 Lotus 1-2-3
4437
+ * 保留了这个错误。不特判它,1900-03-01 之后的全部日期都会差一天。
4438
+ */
4439
+ function serialToIso(serial, date1904) {
4440
+ const whole = Math.floor(serial);
4441
+ const fraction = serial - whole;
4442
+ let seconds = Math.round(fraction * 86400);
4443
+ let days = whole;
4444
+ if (seconds >= 86400) {
4445
+ seconds -= 86400;
4446
+ days += 1;
4447
+ }
4448
+ let iso;
4449
+ if (!date1904 && days === 60) iso = "1900-02-29";
4450
+ else {
4451
+ const at = new Date((date1904 ? Date.UTC(1904, 0, 1) : days < 60 ? Date.UTC(1899, 11, 31) : Date.UTC(1899, 11, 30)) + days * 864e5);
4452
+ iso = `${pad(at.getUTCFullYear(), 4)}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())}`;
4453
+ }
4454
+ if (seconds === 0) return iso;
4455
+ const h = Math.floor(seconds / 3600);
4456
+ const m = Math.floor(seconds % 3600 / 60);
4457
+ const s = seconds % 60;
4458
+ return `${iso}T${pad(h)}:${pad(m)}:${pad(s)}`;
4459
+ }
4460
+ /** 一个 `<c>` 的呈现文本(FR-12.3) */
4461
+ function cellText(cell, shared, dateStyles, date1904) {
4462
+ const type = cell.getAttribute("t");
4463
+ if (type === "inlineStr") {
4464
+ const is = [...cell.children].find((child) => child.localName === "is");
4465
+ return is !== void 0 ? sharedStringText(is) : "";
4466
+ }
4467
+ const raw = [...cell.children].find((child) => child.localName === "v")?.textContent ?? "";
4468
+ if (raw === "") return "";
4469
+ if (type === "s") {
4470
+ const index = Number(raw);
4471
+ return Number.isInteger(index) ? shared[index] ?? "" : "";
4472
+ }
4473
+ if (type === "str" || type === "e") return raw;
4474
+ if (type === "b") return raw === "1" ? "TRUE" : "FALSE";
4475
+ const numeric = Number(raw);
4476
+ if (!Number.isFinite(numeric)) return raw;
4477
+ const styleIndex = Number(cell.getAttribute("s") ?? "0");
4478
+ if (Number.isInteger(styleIndex) && dateStyles[styleIndex] === true) return serialToIso(numeric, date1904);
4479
+ return raw;
4480
+ }
4481
+ /**
4482
+ * 一张工作表的文本:一行一行,单元格用制表符分隔(与 docx 的表格同口径)。
4483
+ * 空行与空列**照原样留着** —— 位置本身是语义,压掉之后「第 3 列」就对不上了。
4484
+ */
4485
+ function sheetText(doc, shared, dateStyles, date1904) {
4486
+ const lines = [];
4487
+ let previousRow = 0;
4488
+ for (const row of byTag(doc, "row")) {
4489
+ const declared = Number(row.getAttribute("r"));
4490
+ const rowNumber = Number.isInteger(declared) && declared > 0 ? declared : previousRow + 1;
4491
+ for (let gap = previousRow + 1; gap < rowNumber; gap++) lines.push("");
4492
+ previousRow = rowNumber;
4493
+ const cells = [];
4494
+ let nextColumn = 0;
4495
+ for (const cell of row.children) {
4496
+ if (cell.localName !== "c") continue;
4497
+ const column = columnIndex(cell.getAttribute("r")) ?? nextColumn;
4498
+ for (let gap = cells.length; gap < column; gap++) cells.push("");
4499
+ const text = cellText(cell, shared, dateStyles, date1904);
4500
+ if (cells.length > column) cells[column] = text;
4501
+ else cells.push(text);
4502
+ nextColumn = column + 1;
4503
+ }
4504
+ lines.push(cells.join(" ").replace(/\t+$/, ""));
4505
+ }
4506
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
4507
+ return lines.join("\n");
4508
+ }
4509
+ const looksLikeOle2 = (bytes) => bytes.length >= OLE2_SIGNATURE.length && OLE2_SIGNATURE.every((byte, i) => bytes[i] === byte);
4510
+ /** rels 里的 Target 可能是相对 `xl/` 的,也可能是包根绝对路径 */
4511
+ function resolveSheetPath(target) {
4512
+ const clean = target.replace(/^\.\//, "");
4513
+ return clean.startsWith("/") ? clean.slice(1) : `xl/${clean}`;
4514
+ }
4515
+ /**
4516
+ * 从 xlsx 字节里抽出文本,每个工作表一段、段首是表名。
4517
+ *
4518
+ * @throws WebSkillError 归档损坏、旧版 `.xls`、缺 `xl/workbook.xml`、工作表关系解不开、XML 解析失败
4519
+ */
4520
+ async function extractXlsxText(bytes) {
4521
+ if (looksLikeOle2(bytes)) fail("This is a legacy .xls workbook, which is not supported. Save it as .xlsx and try again.");
4522
+ let entries;
4523
+ try {
4524
+ entries = await unzipWithLimits(bytes);
4525
+ } catch {
4526
+ fail("This file could not be read as an Excel workbook: the archive is damaged or not a .xlsx file.");
4527
+ }
4528
+ const parts = new Map(entries);
4529
+ const workbookBytes = parts.get(WORKBOOK);
4530
+ if (workbookBytes === void 0) fail(`This file is not an Excel workbook: it has no ${WORKBOOK} entry.`);
4531
+ const workbook = parseXml(workbookBytes, "index");
4532
+ const workbookPr = byTag(workbook, "workbookPr")[0];
4533
+ const date1904 = workbookPr?.getAttribute("date1904") === "1" || workbookPr?.getAttribute("date1904") === "true";
4534
+ const relsBytes = parts.get(WORKBOOK_RELS);
4535
+ const targets = /* @__PURE__ */ new Map();
4536
+ if (relsBytes !== void 0) for (const rel of byTag(parseXml(relsBytes, "relationships"), "Relationship")) {
4537
+ const id = rel.getAttribute("Id");
4538
+ const target = rel.getAttribute("Target");
4539
+ if (id !== null && target !== null) targets.set(id, resolveSheetPath(target));
4540
+ }
4541
+ const sharedBytes = parts.get(SHARED_STRINGS);
4542
+ const shared = sharedBytes === void 0 ? [] : byTag(parseXml(sharedBytes, "shared strings"), "si").map(sharedStringText);
4543
+ const dateStyles = readDateStyles(parts.get(STYLES));
4544
+ const sections = [];
4545
+ for (const [ordinal, sheet] of byTag(workbook, "sheet").entries()) {
4546
+ const name = sheet.getAttribute("name") ?? `Sheet${ordinal + 1}`;
4547
+ const relId = sheet.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? sheet.getAttribute("r:id");
4548
+ const path = relId !== null ? targets.get(relId) : void 0;
4549
+ const sheetBytes = path !== void 0 ? parts.get(path) : void 0;
4550
+ if (sheetBytes === void 0) fail(`The worksheet "${name}" could not be located inside the workbook.`);
4551
+ const body = sheetText(parseXml(sheetBytes, `worksheet "${name}"`), shared, dateStyles, date1904);
4552
+ sections.push(body === "" ? `Sheet: ${name}` : `Sheet: ${name}\n${body}`);
4553
+ }
4554
+ return sections.join("\n\n");
4555
+ }
4556
+ /**
3395
4557
  * 文档投放面的 CSP 单一来源(0.11.0 分册 18 FR-18.3 / AC-G21)。
3396
4558
  *
3397
4559
  * ⚠️ 策略**只能**由 viewer 路由的响应头下发。
@@ -3635,4 +4797,4 @@ function originOf(blockedURI) {
3635
4797
  }
3636
4798
 
3637
4799
  //#endregion
3638
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
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 };