@webskill/sdk 0.13.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 +225 -7
  4. package/dist/browser.js +659 -47
  5. package/dist/{catalogComponents-BgAJN0p8-CYEXSk45.js → catalogComponents-BgAJN0p8-Cr55ouOg.js} +2 -2
  6. package/dist/{dist-ExSQky4C.js → dist-Bfga1TmS.js} +45 -5
  7. package/dist/{dist-GK6dtjRv.js → dist-BnqAkSS6.js} +940 -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-DkvBhJQy.d.ts → index-CWoKOFBP.d.ts} +47 -3
  13. package/dist/{index-iBm9tJL_.d.ts → index-DQwGvHAI.d.ts} +389 -9
  14. package/dist/{index-C3XdItd_.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 +56 -3
  18. package/dist/mcp.js +36 -3
  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-CWWBVOkE.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 +5 -5
  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 +1 -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-ExSQky4C.js";
3
- import { D as toFrameScopes, E as toActionFrameScopes, T as frameSteps, w as frameLabel } from "./dist-GK6dtjRv.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", await toDeliverableBlob(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 toDeliverableBlob(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) {
@@ -1797,6 +1919,18 @@ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
1797
1919
  /** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
1798
1920
  const SVG_SNIFF_BYTES = 1024;
1799
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
+ /**
1800
1934
  * 按魔数认图片类型(服务端声明只作兜底)。
1801
1935
  *
1802
1936
  * 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
@@ -1844,16 +1978,29 @@ async function rasterizeToPng(blob) {
1844
1978
  * 两类真实站点上很常见的图会在最后一步被模型侧丢掉:服务端把图报成 `application/octet-stream`,
1845
1979
  * 以及矢量图(三家提供方的图片通道都不收 `image/svg+xml`,分册 20 §0.1)。
1846
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`,不必为此解码)
1847
1991
  */
1848
- async function toDeliverableBlob(blob) {
1992
+ async function toDeliverableBlob(blob, oversized = false) {
1849
1993
  const declared = blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
1850
- if (DELIVERABLE_IMAGE_MIME_TYPES.has(declared)) return blob;
1851
- const bytes = new Uint8Array(await blob.arrayBuffer());
1852
- const sniffed = sniffImageMime(bytes);
1853
- if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) return new Blob([bytes], { type: sniffed });
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
+ }
1854
2001
  const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
1855
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}")`);
1856
- return await rasterizeToPng(new Blob([bytes], { type: mimeType }));
2003
+ return await rasterizeToPng(new Blob([blob], { type: mimeType }));
1857
2004
  }
1858
2005
  /**
1859
2006
  * 取一个元素的图像(0.6.0 FR-12.1)。
@@ -1902,6 +2049,148 @@ function describeError(e) {
1902
2049
  if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
1903
2050
  return e instanceof Error ? e.message : String(e);
1904
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
+ }
1905
2194
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
1906
2195
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1907
2196
  /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
@@ -1979,6 +2268,33 @@ function ownText(element) {
1979
2268
  for (const node of element.childNodes) if (node.nodeType === 3) text += node.nodeValue ?? "";
1980
2269
  return clip(text);
1981
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
+ }
1982
2298
  function nameOf(element, doc) {
1983
2299
  const label = element.getAttribute("aria-label");
1984
2300
  if (label !== null && label.trim() !== "") return clip(label);
@@ -1991,6 +2307,7 @@ function nameOf(element, doc) {
1991
2307
  const alt = element.getAttribute("alt");
1992
2308
  if (alt !== null && alt.trim() !== "") return clip(alt);
1993
2309
  }
2310
+ if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
1994
2311
  const own = ownText(element);
1995
2312
  return own === "" ? void 0 : own;
1996
2313
  }
@@ -2046,20 +2363,38 @@ function isBelowMinArea(element, minImageArea) {
2046
2363
  if (size === void 0) return false;
2047
2364
  return size.width * size.height < minImageArea;
2048
2365
  }
2049
- function describe(element, excluded, doc, view, capture, handles, frame, provenance) {
2366
+ function describe(element, excluded, doc, view, capture, handles, frame, provenance, promotion) {
2050
2367
  if (excluded.has(element)) return void 0;
2051
2368
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
2052
2369
  if (hidden$1(element, view)) return void 0;
2053
2370
  const children = [];
2054
2371
  for (const child of element.children) {
2055
- const node = describe(child, excluded, doc, view, capture, handles, frame, provenance);
2372
+ const node = describe(child, excluded, doc, view, capture, handles, frame, provenance, promotion);
2056
2373
  if (node !== void 0) children.push(node);
2057
2374
  }
2058
- const name = nameOf(element, doc);
2375
+ let name = nameOf(element, doc);
2059
2376
  const value = valueOf(element);
2060
- const role = roleOf$1(element);
2061
- 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);
2062
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
+ }
2063
2398
  const node = {
2064
2399
  role,
2065
2400
  ...name !== void 0 ? { name } : {},
@@ -2110,6 +2445,46 @@ function frameHint(iframe) {
2110
2445
  return "unnamed";
2111
2446
  }
2112
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
+ /**
2113
2488
  * 句柄发放与解析。表在 reader 实例内、**跨感知保留**(分册 18 FR-18.5):
2114
2489
  * 每次 read() 作废全部旧句柄会让多步任务陷入「读—点—失效—再读」的空转,
2115
2490
  * 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
@@ -2180,6 +2555,12 @@ function createDomPerceptionReader(options = {}) {
2180
2555
  */
2181
2556
  const elevatedModals = /* @__PURE__ */ new Set();
2182
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;
2183
2564
  /**
2184
2565
  * 把一条帧授权解析成 Document,逐层下钻(分册 18 FR-18.3)。
2185
2566
  *
@@ -2256,13 +2637,15 @@ function createDomPerceptionReader(options = {}) {
2256
2637
  const root = rootDocument();
2257
2638
  if (root === void 0) return {
2258
2639
  nodes: [],
2259
- frameNotes: []
2640
+ frameNotes: [],
2641
+ nestedFrames: []
2260
2642
  };
2261
2643
  handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2262
2644
  handles.prune();
2263
2645
  const issuer = handles;
2264
2646
  const nodes = [];
2265
2647
  const frameNotes = [];
2648
+ const nestedFrames = [];
2266
2649
  /** 同一个未授权 iframe 至多报一次(它可能同时落在多个 include 根的子树里) */
2267
2650
  const reportedUnauthorized = /* @__PURE__ */ new Set();
2268
2651
  const resolvedScopes = [];
@@ -2287,6 +2670,66 @@ function createDomPerceptionReader(options = {}) {
2287
2670
  doc: resolved.doc
2288
2671
  });
2289
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
+ }
2290
2733
  for (const { scope: frameScope, label, doc } of resolvedScopes) {
2291
2734
  const view = doc.defaultView;
2292
2735
  const tag = label === "self" ? void 0 : label;
@@ -2297,27 +2740,17 @@ function createDomPerceptionReader(options = {}) {
2297
2740
  const roots = [];
2298
2741
  for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2299
2742
  for (const element of roots) {
2300
- const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0);
2743
+ const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
2301
2744
  if (node !== void 0) nodes.push(node);
2302
2745
  }
2303
- for (const element of roots) for (const iframe of nestedFramesIn(element, excluded)) {
2304
- if (authorizedFrames.has(iframe)) continue;
2305
- if (reportedUnauthorized.has(iframe)) continue;
2306
- reportedUnauthorized.add(iframe);
2307
- const hint = frameHint(iframe);
2308
- nodes.push({
2309
- role: "note",
2310
- 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.`,
2311
- ...tag !== void 0 ? { frame: tag } : {}
2312
- });
2313
- }
2746
+ descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
2314
2747
  }
2315
2748
  pruneElevated();
2316
2749
  const view = root.defaultView;
2317
2750
  const sets = actionSets(root, actionScopeFor("self"));
2318
2751
  for (const modal of elevatedModals) {
2319
2752
  issuer.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2320
- const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated");
2753
+ const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated", promotionOf(view));
2321
2754
  if (node !== void 0) nodes.push(node);
2322
2755
  }
2323
2756
  for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
@@ -2330,12 +2763,13 @@ function createDomPerceptionReader(options = {}) {
2330
2763
  }
2331
2764
  return {
2332
2765
  nodes,
2333
- frameNotes
2766
+ frameNotes,
2767
+ nestedFrames
2334
2768
  };
2335
2769
  }
2336
2770
  async function readWithImages(scope, capture) {
2337
2771
  const targets = [];
2338
- const { nodes, frameNotes } = collect(scope, {
2772
+ const { nodes, frameNotes, nestedFrames } = collect(scope, {
2339
2773
  targets,
2340
2774
  minImageArea: capture.minImageArea ?? 0
2341
2775
  });
@@ -2366,16 +2800,18 @@ function createDomPerceptionReader(options = {}) {
2366
2800
  images,
2367
2801
  imagesOmitted: targets.length - selected.length,
2368
2802
  imageFailures,
2369
- ...frameNotes.length > 0 ? { frameNotes } : {}
2803
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2804
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2370
2805
  };
2371
2806
  }
2372
2807
  function read(scope, capture) {
2373
2808
  if (capture === void 0) return collect(scope, void 0).nodes;
2374
2809
  if (!capture.images || capture.maxImages <= 0) {
2375
- const { nodes, frameNotes } = collect(scope, void 0);
2810
+ const { nodes, frameNotes, nestedFrames } = collect(scope, void 0);
2376
2811
  return Promise.resolve({
2377
2812
  nodes,
2378
- ...frameNotes.length > 0 ? { frameNotes } : {}
2813
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2814
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2379
2815
  });
2380
2816
  }
2381
2817
  return readWithImages(scope, capture);
@@ -2732,6 +3168,182 @@ function createDomPageActionExecutor(options) {
2732
3168
  describe
2733
3169
  };
2734
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
+ }
2735
3347
  const isRecord = (v) => typeof v === "object" && v !== null;
2736
3348
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
2737
3349
  /** Worker 侧入站校验:非法消息返回 undefined(忽略) */
@@ -4185,4 +4797,4 @@ function originOf(blockedURI) {
4185
4797
  }
4186
4798
 
4187
4799
  //#endregion
4188
- 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, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, 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 };