@webskill/sdk 0.13.0 → 0.15.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 (60) hide show
  1. package/dist/agent.d.ts +3 -3
  2. package/dist/agent.js +2442 -3
  3. package/dist/{dist-ExSQky4C.js → approval-BnLLCOG0.js} +277 -2934
  4. package/dist/browser.d.ts +327 -8
  5. package/dist/browser.js +1017 -51
  6. package/dist/{openUiLibrary-BKXW7Iwx-CWWBVOkE.js → dist-CTsxblSD.js} +496 -28
  7. package/dist/{echarts-De78wXqV.js → echarts-BE7oV_Dl.js} +1 -1
  8. package/dist/{env-8cY40DXB-CGnEVZby.js → env-Bj1MI2Ww.js} +1 -1
  9. package/dist/errors-BDZNpC13.js +22 -0
  10. package/dist/{eventTypes-FllCrX-Z-DNDeHWoG.js → eventTypes-Y07N8IAC.js} +14 -2
  11. package/dist/external-_ZRQe-V9.js +53 -0
  12. package/dist/governance.d.ts +15 -3
  13. package/dist/governance.js +86 -4
  14. package/dist/{index-iBm9tJL_.d.ts → index-CQYo4tcT.d.ts} +469 -10
  15. package/dist/{index-C3XdItd_.d.ts → index-CU2md8R1.d.ts} +26 -18
  16. package/dist/{index-DkvBhJQy.d.ts → index-Oc3H89TJ.d.ts} +204 -4
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +1001 -5
  19. package/dist/kind-DxgS8LM-.js +77 -0
  20. package/dist/linkedDocument-xNXF-z8n.js +94 -0
  21. package/dist/llm-B7lLH0ZI.js +1767 -0
  22. package/dist/mcp.d.ts +56 -3
  23. package/dist/mcp.js +75 -5
  24. package/dist/memoryArtifactStore-D6dLeY55.js +48 -0
  25. package/dist/miniChart-owdCdaw-.js +533 -0
  26. package/dist/node.d.ts +3 -3
  27. package/dist/node.js +110 -13
  28. package/dist/openUiLibrary-CY_ADoe0.js +30 -0
  29. package/dist/openUiSpecLang-DuOCdw9G.js +1849 -0
  30. package/dist/pathSecurity-B1owvJAF.js +65 -0
  31. package/dist/processSandboxEntry.js +5 -1
  32. package/dist/renderResult-D9Q-Vu2x.js +143 -0
  33. package/dist/{rolldown-runtime-BOF7iYI8.js → rolldown-runtime-BHkdefai.js} +1 -2
  34. package/dist/sandboxWorkerEntry.js +5 -1
  35. package/dist/{dist-Bev6i6Ip.js → skill-CAJMsLod.js} +44 -314
  36. package/dist/{skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts → skillVersionStore-B24Jjjry-C4zomhMZ.d.ts} +14 -2
  37. package/dist/surface-DVGiCmwq.js +145 -0
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/testing.js +64 -3
  40. package/dist/{types-DLctJep_-B5G4uk2u.d.ts → types-C3V6lAeO-BIf7UB2e.d.ts} +79 -4
  41. package/dist/types-TUydnxaj.js +78 -0
  42. package/dist/ui-react.d.ts +12 -5
  43. package/dist/ui-react.js +128548 -4729
  44. package/dist/ui-vue.d.ts +1 -1
  45. package/dist/ui-vue.js +11 -2
  46. package/dist/ui.d.ts +3 -3
  47. package/dist/ui.js +1254 -3
  48. package/dist/urlSafety-CiSuCJvX.js +65 -0
  49. package/dist/webSkillApi-DOOyz2G5.js +106 -0
  50. package/dist/{webskillLitCatalog-D_zCqeQF-C9lrvvMr.js → webskillLitCatalog-DSqVFQjK.js} +11 -3
  51. package/package.json +1 -1
  52. package/dist/catalogComponents-BgAJN0p8-CYEXSk45.js +0 -124384
  53. package/dist/client-BCM6Z3yq-qUQNkKrK.js +0 -7787
  54. package/dist/dist-DqcL6jKO.js +0 -3509
  55. package/dist/dist-DxyxmXPU-DGC-dpXf.js +0 -5621
  56. package/dist/dist-GK6dtjRv.js +0 -1493
  57. package/dist/memoryArtifactStore-52Zn9npI-LbCQaqyx.js +0 -86
  58. package/dist/stdio-CFMoANJJ-BxrTeXh7.js +0 -31
  59. package/dist/testing-WPTyXQYt.js +0 -142
  60. package/dist/types-WovEf4ED-CZSDiiBU.js +0 -6215
package/dist/browser.js CHANGED
@@ -1,13 +1,21 @@
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 { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
2
+ import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest, O as MANIFEST_EXCLUDED_FILES, P as verifyManifest, b as parseSkillMarkdown, c as unzipWithLimits, f as SKILL_PACK_FILE, i as buildCatalog, k as SKILLS_LOCKFILE, m as parseSkillPackManifest, n as SkillDiscovery, o as readResponseWithLimit, p as exportSkills, t as validateSkills, u as detectSkillArchiveShapeFromFs, w as readSkillSignature, y as isValidSkillName } from "./skill-CAJMsLod.js";
3
+ import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot } from "./pathSecurity-B1owvJAF.js";
4
+ import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
5
+ import { a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, g as FsRunSnapshotStore, l as parseBridgeRequest, nt as ProgressiveRouter, p as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-BnLLCOG0.js";
6
+ import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-B7lLH0ZI.js";
7
+ import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
8
+ import { t as createWebSkillApi } from "./webSkillApi-DOOyz2G5.js";
9
+ import { a as frameSteps, i as frameLabel, n as toActionFrameScopes, r as toFrameScopes } from "./types-TUydnxaj.js";
5
10
 
6
- //#region ../browser/dist/index.js
11
+ //#region ../browser/src/fs/featureDetection.ts
7
12
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
8
13
  function isOpfsAvailable(globalScope) {
9
14
  return typeof (globalScope ?? globalThis).navigator?.storage?.getDirectory === "function";
10
15
  }
16
+
17
+ //#endregion
18
+ //#region ../browser/src/fs/opfsProvider.ts
11
19
  const isDomException = (e, name) => e instanceof DOMException && e.name === name;
12
20
  const mapError = (e, path) => {
13
21
  if (isDomException(e, "NotFoundError")) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${path}`);
@@ -174,6 +182,9 @@ var OpfsProvider = class {
174
182
  await this.remove(from);
175
183
  }
176
184
  };
185
+
186
+ //#endregion
187
+ //#region ../browser/src/executor/workerBootstrap.ts
177
188
  /**
178
189
  * Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
179
190
  * 注意:内容为纯 JS,不能含反引号与 ${ 占位(外层用模板字符串内嵌);
@@ -183,7 +194,7 @@ var OpfsProvider = class {
183
194
  *
184
195
  * 协议(宿主 ↔ Worker):
185
196
  * - 宿主 → Worker:{type:'load', id, source, networkPolicy?} 请求加载脚本定义
186
- * - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?} 请求执行
197
+ * - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?, documentSurface?} 请求执行
187
198
  * - Worker → 宿主:{type:'load-result', id, ok, definition?, error?}
188
199
  * - Worker → 宿主:{type:'execute-result', id, ok, value?, stdout, stderr, error?}
189
200
  * - Worker → 宿主:{type:'bridge', request} 能力桥 RPC(readReference/writeArtifact/confirm)
@@ -299,7 +310,7 @@ function callCapability(kind, payload) {
299
310
  }
300
311
 
301
312
  function makeContext(msg) {
302
- return {
313
+ var ctx = {
303
314
  skillName: msg.skillName,
304
315
  runId: msg.runId,
305
316
  readReference: function (path) {
@@ -329,6 +340,21 @@ function makeContext(msg) {
329
340
  return callCapability('confirm', { message: message });
330
341
  },
331
342
  };
343
+ // 能力位而非能力函数:其余能力无条件挂上去(拿不到时桥对面报错),
344
+ // documentSurface 是个供探测的布尔值,无条件挂上去就变成恒真谎言。
345
+ if (msg.documentSurface) ctx.documentSurface = true;
346
+ // 上传文件同理:宿主没接就不能给出「存在但必然失败」的假出口(分册 17 FR-17.1)
347
+ if (msg.uploadFiles) {
348
+ ctx.listUploadFiles = function () {
349
+ return callCapability('listUploadFiles', {});
350
+ };
351
+ ctx.readUploadFile = function (fileId) {
352
+ return callCapability('readUploadFile', { fileId: fileId }).then(function (bytes) {
353
+ return new Uint8Array(bytes);
354
+ });
355
+ };
356
+ }
357
+ return ctx;
332
358
  }
333
359
 
334
360
  self.onmessage = async function (event) {
@@ -404,6 +430,9 @@ self.onmessage = async function (event) {
404
430
  }
405
431
  };
406
432
  `;
433
+
434
+ //#endregion
435
+ //#region ../browser/src/executor/tsTranspiler.ts
407
436
  /** 按资源地址缓存,而不是全局单例:不同地址必须各自加载,否则先到的实例会顶替后来的配置 */
408
437
  const esbuildCache = /* @__PURE__ */ new Map();
409
438
  async function loadEsbuild(options) {
@@ -466,12 +495,18 @@ var TsTranspiler = class {
466
495
  return code;
467
496
  }
468
497
  };
498
+
499
+ //#endregion
500
+ //#region ../browser/src/skillManagement/checksumWeb.ts
469
501
  /** WebCrypto sha256(浏览器/Worker 通用;Node 20+ 全局 crypto 同样可用) */
470
502
  async function sha256HexWeb(data) {
471
503
  const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
472
504
  const digest = await crypto.subtle.digest("SHA-256", bytes);
473
505
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
474
506
  }
507
+
508
+ //#endregion
509
+ //#region ../browser/src/skillManagement/fsUtils.ts
475
510
  /** 递归拷贝目录(含子目录与二进制文件;原子写中断残留的 `.tmp-*` 不拷贝) */
476
511
  async function copyDir(fs, from, to) {
477
512
  await fs.mkdir(to);
@@ -498,6 +533,9 @@ async function removeDirQuiet(fs, dir) {
498
533
  if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
499
534
  } catch {}
500
535
  }
536
+
537
+ //#endregion
538
+ //#region ../browser/src/skillManagement/zipExtract.ts
501
539
  /**
502
540
  * zip 解包(fflate 流式,单条目/总解压体积上限,浏览器无依赖)。
503
541
  * 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
@@ -509,6 +547,9 @@ async function extractZipWeb(fs, data, destRoot, limits) {
509
547
  else await fs.writeBinary(target, content);
510
548
  }
511
549
  }
550
+
551
+ //#endregion
552
+ //#region ../browser/src/skillManagement/browserSkillManager.ts
512
553
  /**
513
554
  * 验签失败不能被折叠成 INSTALL_FAILED:四种失败的宿主处置完全不同
514
555
  * (引导选策略 / 提示重下 / 引导添加密钥 / 安全事件),与 Node 侧同口径。
@@ -885,6 +926,9 @@ var BrowserSkillManager = class {
885
926
  await atomicWriteText(this.#fs, lockfilePath(this.#managedRoot), JSON.stringify(lockfile, null, 2));
886
927
  }
887
928
  };
929
+
930
+ //#endregion
931
+ //#region ../browser/src/executor/iframeSandbox.ts
888
932
  const ENVELOPE = "__webskill_sandbox__";
889
933
  let channelSeq = 0;
890
934
  /** 每实例 channelId(防同页伪造消息注入;crypto.randomUUID 优先,无则随机回退) */
@@ -905,12 +949,16 @@ var IframeWorkerLike = class {
905
949
  #channelId;
906
950
  #hostWindow;
907
951
  #hostListener;
908
- constructor(iframe, hostWindow, channelId) {
952
+ #readyTimer;
953
+ constructor(iframe, hostWindow, channelId, readyTimeout) {
909
954
  this.#iframe = iframe;
910
955
  this.#hostWindow = hostWindow;
911
956
  this.#channelId = channelId;
912
957
  this.#hostListener = (event) => this.#onMessage(event);
913
958
  this.#hostWindow.addEventListener("message", this.#hostListener);
959
+ if (readyTimeout) this.#readyTimer = setTimeout(() => {
960
+ 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.`);
961
+ }, readyTimeout.ms);
914
962
  }
915
963
  get ready() {
916
964
  return this.#ready;
@@ -919,10 +967,38 @@ var IframeWorkerLike = class {
919
967
  return this.#channelId;
920
968
  }
921
969
  markReady() {
970
+ this.#clearReadyTimer();
922
971
  this.#ready = true;
923
972
  for (const payload of this.#queue) this.#post(payload);
924
973
  this.#queue = [];
925
974
  }
975
+ #clearReadyTimer() {
976
+ if (this.#readyTimer !== void 0) {
977
+ clearTimeout(this.#readyTimer);
978
+ this.#readyTimer = void 0;
979
+ }
980
+ }
981
+ /**
982
+ * 就绪超时(FR-12.4):`createIframeWorker` 同步返回,超时无处可抛,只能走回包通道。
983
+ * 而 `#request` 按 `data.id` 匹配回包,id 对不上的消息被静默丢弃——
984
+ * 所以必须**逐条**回复队列里每一条消息的 id,否则调用方一直挂着。
985
+ */
986
+ #failQueue(message) {
987
+ const queued = this.#queue;
988
+ this.#queue = [];
989
+ for (const payload of queued) {
990
+ const id = payload.id;
991
+ if (typeof id === "string") this.emit({
992
+ id,
993
+ ok: false,
994
+ error: {
995
+ code: "TOOL_EXECUTION_FAILED",
996
+ message
997
+ }
998
+ });
999
+ }
1000
+ this.terminate();
1001
+ }
926
1002
  #post(payload) {
927
1003
  this.#iframe.contentWindow?.postMessage({
928
1004
  [ENVELOPE]: true,
@@ -953,11 +1029,18 @@ var IframeWorkerLike = class {
953
1029
  for (const listener of this.#listeners) listener({ data });
954
1030
  }
955
1031
  terminate() {
1032
+ this.#clearReadyTimer();
956
1033
  this.#hostWindow.removeEventListener("message", this.#hostListener);
957
1034
  this.#iframe.remove();
958
1035
  }
959
1036
  };
960
- /** srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断) */
1037
+ /**
1038
+ * srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断)。
1039
+ *
1040
+ * **这段文本被 FR-12.5 / AC-12.1 冻结在 0.13.0 基线上**:既有网页宿主不给 `documentUrl`,
1041
+ * 必须逐字节拿到与升级前相同的沙箱文档。因此它不与 `SANDBOX_PAGE_SCRIPT_SOURCE` 共用模板——
1042
+ * 抽模板必然改动生成文本。两份中继的一致性改由行为对拍守卫(见 `sandboxRelayParity` 用例)。
1043
+ */
961
1044
  function sandboxDocument(bootstrapSource, channelId) {
962
1045
  const bootstrapJson = JSON.stringify(bootstrapSource).replace(/<\//g, "<\\/");
963
1046
  return `<!doctype html>
@@ -984,19 +1067,103 @@ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: { type: '
984
1067
  <\/script></body></html>`;
985
1068
  }
986
1069
  /**
987
- * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 classic Worker。
1070
+ * 静态沙箱页的中继脚本源码(分册 12 / FR-12.3)。
1071
+ *
1072
+ * 宿主把它落成一个独立文件并用 `<script src>` 引入——**不得内联**:
1073
+ * sandbox 页虽允许 `unsafe-inline`,但外链能让「沙箱页脚本 = SDK 原样产物」在审计时可核对。
1074
+ *
1075
+ * 与 srcdoc 形态的两点区别,都源于「静态页在加载完时还不知道要跑什么」:
1076
+ * - **引导源码**随宿主的 `sandbox-init` 消息到达,而不是内嵌在文档里;
1077
+ * - **channelId** 由第一条来自 `parent` 的消息 latch。SDK 导出的是一份与宿主无关的源码,
1078
+ * 不可能预知宿主这次生成的 id。latch 前先认 `event.source === parent`:少了这句,
1079
+ * 任何把本页嵌进去的第三方 frame 都能抢先 latch 一个自己的 id,
1080
+ * 让真正的宿主消息全部被丢弃——一个纯粹的拒绝服务面。
1081
+ *
1082
+ * 本源码不含任何扩展 API:sandbox 页拿不到它们(实证)。
1083
+ * `ENVELOPE` 与宿主侧同源(同一个模块常量),两边各写一份字面量会在改名时静默失配。
1084
+ * @experimental
1085
+ */
1086
+ const SANDBOX_PAGE_SCRIPT_SOURCE = `const ENVELOPE = ${JSON.stringify(ENVELOPE)};
1087
+ let CHANNEL_ID = null;
1088
+ let worker = null;
1089
+ function post(payload) {
1090
+ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload }, '*');
1091
+ }
1092
+ function startWorker(bootstrapSource) {
1093
+ if (worker || typeof bootstrapSource !== 'string') return;
1094
+ const blob = new Blob([bootstrapSource], { type: 'text/javascript' });
1095
+ const url = URL.createObjectURL(blob);
1096
+ // 注意:opaque origin 下 module Worker 无法加载(实证),classic Worker + data: URL 动态导入可用
1097
+ worker = new Worker(url);
1098
+ URL.revokeObjectURL(url);
1099
+ worker.addEventListener('message', (event) => post(event.data));
1100
+ worker.addEventListener('error', (event) => post({ type: 'sandbox-worker-error', message: event.message }));
1101
+ post({ type: 'sandbox-ready' });
1102
+ }
1103
+ window.addEventListener('message', (event) => {
1104
+ const data = event.data;
1105
+ if (!data || data[ENVELOPE] !== true) return;
1106
+ if (event.source !== parent) return;
1107
+ if (CHANNEL_ID === null) {
1108
+ if (typeof data.channelId !== 'string') return;
1109
+ CHANNEL_ID = data.channelId;
1110
+ } else if (data.channelId !== CHANNEL_ID) return;
1111
+ const payload = data.payload;
1112
+ if (payload && payload.type === 'sandbox-init') {
1113
+ startWorker(payload.bootstrapSource);
1114
+ return;
1115
+ }
1116
+ if (worker) worker.postMessage(payload);
1117
+ });
1118
+ `;
1119
+ /**
1120
+ * `Document` 判别式。不用 `'documentUrl' in v`:`Document` 上没有该属性,
1121
+ * 但一个只给了 `doc` 的选项对象上也没有,两者会被判到同一侧。
1122
+ */
1123
+ function isDocument(value) {
1124
+ return value.nodeType === 9;
1125
+ }
1126
+ /**
1127
+ * 创建 opaque iframe 沙箱 Worker:srcdoc(默认)或宿主指定的静态沙箱页承载 classic Worker。
988
1128
  * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
989
1129
  */
990
- function createIframeWorker(bootstrapSource, doc = document) {
1130
+ function createIframeWorker(bootstrapSource, options = {}) {
1131
+ const resolved = isDocument(options) ? { doc: options } : options;
1132
+ const doc = resolved.doc ?? document;
991
1133
  const channelId = nextChannelId();
992
1134
  const iframe = doc.createElement("iframe");
993
- iframe.setAttribute("sandbox", "allow-scripts");
994
1135
  iframe.style.display = "none";
995
- iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
996
- const worker = new IframeWorkerLike(iframe, doc.defaultView ?? window, channelId);
1136
+ const hostWindow = doc.defaultView ?? window;
1137
+ const { documentUrl } = resolved;
1138
+ if (documentUrl === void 0) {
1139
+ iframe.setAttribute("sandbox", "allow-scripts");
1140
+ iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
1141
+ const worker = new IframeWorkerLike(iframe, hostWindow, channelId);
1142
+ doc.body.appendChild(iframe);
1143
+ return worker;
1144
+ }
1145
+ const readyTimeoutMs = resolved.readyTimeoutMs ?? 1e4;
1146
+ const worker = new IframeWorkerLike(iframe, hostWindow, channelId, {
1147
+ ms: readyTimeoutMs,
1148
+ documentUrl
1149
+ });
1150
+ iframe.addEventListener("load", () => {
1151
+ iframe.contentWindow?.postMessage({
1152
+ [ENVELOPE]: true,
1153
+ channelId,
1154
+ payload: {
1155
+ type: "sandbox-init",
1156
+ bootstrapSource
1157
+ }
1158
+ }, "*");
1159
+ });
1160
+ iframe.src = documentUrl;
997
1161
  doc.body.appendChild(iframe);
998
1162
  return worker;
999
1163
  }
1164
+
1165
+ //#endregion
1166
+ //#region ../browser/src/executor/workerScriptExecutor.ts
1000
1167
  const blobWorkerFactory = () => {
1001
1168
  const blob = new Blob([WORKER_BOOTSTRAP_SOURCE], { type: "text/javascript" });
1002
1169
  const url = URL.createObjectURL(blob);
@@ -1109,7 +1276,9 @@ var BrowserWorkerScriptExecutor = class {
1109
1276
  runId: context.runId,
1110
1277
  source: script.source,
1111
1278
  args,
1112
- networkPolicy: this.#networkPolicy
1279
+ networkPolicy: this.#networkPolicy,
1280
+ ...context.documentSurface ? { documentSurface: true } : {},
1281
+ ...context.readUploadFile ? { uploadFiles: true } : {}
1113
1282
  }, timeoutMs, (bridgeRequest) => this.#handleBridge(bridgeRequest, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
1114
1283
  if (!response.ok) {
1115
1284
  const stderrSummary = response.stderr?.length ? ` | stderr: ${response.stderr.join(" | ").slice(0, 500)}` : "";
@@ -1195,7 +1364,7 @@ var BrowserWorkerScriptExecutor = class {
1195
1364
  ...details !== void 0 ? { details } : {}
1196
1365
  });
1197
1366
  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`);
1367
+ 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
1368
  };
1200
1369
  try {
1201
1370
  switch (request.kind) {
@@ -1222,7 +1391,7 @@ var BrowserWorkerScriptExecutor = class {
1222
1391
  };
1223
1392
  }
1224
1393
  case "fetchData": {
1225
- if (!context.fetchData) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"fetchData\" is disabled");
1394
+ 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
1395
  const denied = await gate("fetchData", `Script "${context.skillName}" wants to fetch data source "${request.sourceId}"`, { sourceId: request.sourceId });
1227
1396
  if (denied) return denied;
1228
1397
  return {
@@ -1258,12 +1427,29 @@ var BrowserWorkerScriptExecutor = class {
1258
1427
  value: await context.confirm(request.message)
1259
1428
  };
1260
1429
  }
1430
+ case "listUploadFiles":
1431
+ if (!context.listUploadFiles) return bridgeError(request.id, "TOOL_UNSUPPORTED", UPLOAD_FILES_UNAVAILABLE);
1432
+ return {
1433
+ id: request.id,
1434
+ ok: true,
1435
+ value: await context.listUploadFiles()
1436
+ };
1437
+ case "readUploadFile":
1438
+ if (!context.readUploadFile) return bridgeError(request.id, "TOOL_UNSUPPORTED", UPLOAD_FILES_UNAVAILABLE);
1439
+ return {
1440
+ id: request.id,
1441
+ ok: true,
1442
+ value: Array.from(await context.readUploadFile(request.fileId))
1443
+ };
1261
1444
  }
1262
1445
  } catch (e) {
1263
1446
  return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
1264
1447
  }
1265
1448
  }
1266
1449
  };
1450
+
1451
+ //#endregion
1452
+ //#region ../browser/src/navigator.ts
1267
1453
  /**
1268
1454
  * navigator.webskill 显式装配:OPFS 默认存储 + BrowserWorkerScriptExecutor +
1269
1455
  * BrowserSkillManager → createWebSkillApi;target 提供时挂 target.navigator
@@ -1293,6 +1479,9 @@ function installWebSkillNavigator(config) {
1293
1479
  nav["webskill"] = api;
1294
1480
  return api;
1295
1481
  }
1482
+
1483
+ //#endregion
1484
+ //#region ../browser/src/llm/chromeBuiltinClient.ts
1296
1485
  function languageModel() {
1297
1486
  const candidate = globalThis.LanguageModel;
1298
1487
  return typeof candidate?.availability === "function" && typeof candidate.create === "function" ? candidate : void 0;
@@ -1428,6 +1617,9 @@ var ChromeBuiltinLlmClient = class {
1428
1617
  return (await probeChromeBuiltinAvailability()).available;
1429
1618
  }
1430
1619
  };
1620
+
1621
+ //#endregion
1622
+ //#region ../browser/src/llm/createLlmClient.ts
1431
1623
  /**
1432
1624
  * 配置 → `LlmClient`。收在 SDK 里是为了让下游只跟 `RuntimeConfigStore` 打交道,
1433
1625
  * 不必自己维护「provider → client 类」的映射(设计 04 §5)。
@@ -1459,6 +1651,9 @@ function createLlmClient(config) {
1459
1651
  model: config.model
1460
1652
  });
1461
1653
  }
1654
+
1655
+ //#endregion
1656
+ //#region ../browser/src/media/dictation.ts
1462
1657
  function recognitionCtor() {
1463
1658
  const scope = globalThis;
1464
1659
  return scope.SpeechRecognition ?? scope.webkitSpeechRecognition;
@@ -1534,6 +1729,9 @@ function startDictation(options) {
1534
1729
  recognition.stop();
1535
1730
  } };
1536
1731
  }
1732
+
1733
+ //#endregion
1734
+ //#region ../browser/src/media/compressImage.ts
1537
1735
  /** 动图无法在不丢帧的前提下走单帧编码管线,超限直接拒绝(裁决 D-2) */
1538
1736
  const UNCOMPRESSABLE_MIME_TYPES = /* @__PURE__ */ new Set(["image/gif"]);
1539
1737
  const QUALITY_STEPS = [
@@ -1601,6 +1799,9 @@ async function encode(bitmap, width, height, quality) {
1601
1799
  function formatBytes(bytes) {
1602
1800
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1603
1801
  }
1802
+
1803
+ //#endregion
1804
+ //#region ../browser/src/dom/imageCapture.ts
1604
1805
  /** @experimental */
1605
1806
  function isCaptureFailure(result) {
1606
1807
  return "reason" in result;
@@ -1633,10 +1834,12 @@ function splitDataUrl(dataUrl) {
1633
1834
  /** 压到预算内;压不下去时把失败原因原样上抛给调用方转成 `CaptureFailure` */
1634
1835
  async function toBudget(id, level, blob, maxBytes) {
1635
1836
  const compressed = await compressImageToBudget(blob, maxBytes);
1837
+ const mimeType = compressed.blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
1838
+ 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
1839
  const buffer = await compressed.blob.arrayBuffer();
1637
1840
  return {
1638
1841
  id,
1639
- mimeType: compressed.blob.type === "" ? "application/octet-stream" : compressed.blob.type,
1842
+ mimeType,
1640
1843
  data: base64Of(new Uint8Array(buffer)),
1641
1844
  level,
1642
1845
  originalBytes: compressed.originalBytes,
@@ -1647,15 +1850,16 @@ async function toBudget(id, level, blob, maxBytes) {
1647
1850
  async function captureImg(element, id, maxBytes) {
1648
1851
  const src = element.currentSrc !== "" ? element.currentSrc : element.src;
1649
1852
  if (src === "") throw new Error("the <img> element has no resolved source");
1853
+ const oversized = Math.max(element.naturalWidth, element.naturalHeight) > MAX_RASTER_EDGE;
1650
1854
  const inline = splitDataUrl(src);
1651
1855
  if (inline) {
1652
1856
  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);
1857
+ return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType }), oversized), maxBytes);
1654
1858
  }
1655
1859
  const response = await fetch(src, { mode: "cors" });
1656
1860
  if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
1657
1861
  if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
1658
- return toBudget(id, "src", await toDeliverableBlob(await response.blob()), maxBytes);
1862
+ return toBudget(id, "src", await toDeliverableBlob(await response.blob(), oversized), maxBytes);
1659
1863
  }
1660
1864
  /** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
1661
1865
  async function captureCanvas(element, id, maxBytes) {
@@ -1797,6 +2001,18 @@ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
1797
2001
  /** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
1798
2002
  const SVG_SNIFF_BYTES = 1024;
1799
2003
  /**
2004
+ * WebP 动图:扩展头 `VP8X` 的 flags 字节第 1 位。位置固定在文件前 24 字节内,
2005
+ * 不必解码整图就能判出来。
2006
+ */
2007
+ function isAnimatedWebp(bytes) {
2008
+ return [
2009
+ 86,
2010
+ 80,
2011
+ 56,
2012
+ 88
2013
+ ].every((byte, index) => bytes[12 + index] === byte) && ((bytes[20] ?? 0) & 2) !== 0;
2014
+ }
2015
+ /**
1800
2016
  * 按魔数认图片类型(服务端声明只作兜底)。
1801
2017
  *
1802
2018
  * 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
@@ -1844,16 +2060,29 @@ async function rasterizeToPng(blob) {
1844
2060
  * 两类真实站点上很常见的图会在最后一步被模型侧丢掉:服务端把图报成 `application/octet-stream`,
1845
2061
  * 以及矢量图(三家提供方的图片通道都不收 `image/svg+xml`,分册 20 §0.1)。
1846
2062
  * 前者只是声明错了,按魔数改回来即可;后者必须就地栅格化,否则等于没取到图。
2063
+ *
2064
+ * **声明一律不采信**:大站 CDN 会按 `Accept` 头改发 AVIF 却仍旧报 `image/webp`,
2065
+ * 照抄声明会让端点解码失败并连累整次请求(一张图 400,37 张全废)。
2066
+ *
2067
+ * 魔数对得上也不等于发得出去,还有两类会被端点整次拒收,必须重编码成单帧位图:
2068
+ * - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位):图片通道普遍只收静态图;
2069
+ * - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
2070
+ * 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
2071
+ *
2072
+ * @param oversized 调用方已知原图长边超限(`<img>` 能直接读到 `naturalWidth`,不必为此解码)
1847
2073
  */
1848
- async function toDeliverableBlob(blob) {
2074
+ async function toDeliverableBlob(blob, oversized = false) {
1849
2075
  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 });
2076
+ const head = new Uint8Array(await blob.slice(0, SVG_SNIFF_BYTES).arrayBuffer());
2077
+ const sniffed = sniffImageMime(head);
2078
+ if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
2079
+ const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
2080
+ const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head);
2081
+ return oversized || animated ? await rasterizeToPng(typed) : typed;
2082
+ }
1854
2083
  const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
1855
2084
  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 }));
2085
+ return await rasterizeToPng(new Blob([blob], { type: mimeType }));
1857
2086
  }
1858
2087
  /**
1859
2088
  * 取一个元素的图像(0.6.0 FR-12.1)。
@@ -1902,6 +2131,154 @@ function describeError(e) {
1902
2131
  if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
1903
2132
  return e instanceof Error ? e.message : String(e);
1904
2133
  }
2134
+
2135
+ //#endregion
2136
+ //#region ../browser/src/dom/promotion.ts
2137
+ /**
2138
+ * 非标可交互元素的识别与角色提升(0.14.0 分册 15)。
2139
+ *
2140
+ * 现代前端用 `div` / `span` 做按钮是常态,它们在无障碍树里全落到 `generic`,
2141
+ * 于是拿不到句柄——模型读得到那几个字,却点不动。本模块补的就是这一段判定。
2142
+ *
2143
+ * 整套逻辑默认关闭,由 `createDomPerceptionReader` 的 `promoteRoles` 开启(D-15-1)。
2144
+ */
2145
+ /**
2146
+ * class / id 里的交互语义词根(FR-15.2)。
2147
+ *
2148
+ * 带词边界匹配:`table` 含 `tab`、`container` 含 `nav`,子串匹配会把它们全误伤(D-15-8)。
2149
+ * 图标类单列一支:`icon-view` / `el-icon-download` / `anticon-*` 这类命名带着具体动作语义。
2150
+ */
2151
+ 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;
2152
+ /** 从图标类名反推语义:`el-icon-view` → `view`、`icon-download` → `download`(FR-15.4 第 2 级) */
2153
+ const ICON_CLASS = /[a-z]*icons?[-_]([a-z0-9][a-z0-9-_]*)$/i;
2154
+ /** 框架路由属性:值非空即视为确凿的导航线索(FR-15.3 第 2 条) */
2155
+ const ROUTE_ATTRIBUTES = [
2156
+ "to",
2157
+ "routerlink",
2158
+ "data-href",
2159
+ "data-url"
2160
+ ];
2161
+ /** 只有这三种角色会被提升:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3) */
2162
+ const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
2163
+ "generic",
2164
+ "img",
2165
+ "cell"
2166
+ ]);
2167
+ /** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
2168
+ const SYNTHETIC_NAME_PREFIX = "unlabeled control";
2169
+ /** 往上找具名祖先的层数上限:再往上就是整页容器,拿它当限定语反而更含糊 */
2170
+ const SYNTHETIC_NAME_LOOKUP_DEPTH = 5;
2171
+ const classAndId = (element) => `${element.getAttribute("class") ?? ""} ${element.getAttribute("id") ?? ""}`;
2172
+ /** `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后 */
2173
+ function looksClickable(element, view) {
2174
+ if (view === null) return false;
2175
+ try {
2176
+ return view.getComputedStyle(element).cursor === "pointer";
2177
+ } catch {
2178
+ return false;
2179
+ }
2180
+ }
2181
+ /**
2182
+ * 五级信号,命中任一即为真(FR-15.1)。
2183
+ *
2184
+ * 求值顺序按**成本**排而不是按强度排:前面的廉价信号命中就短路,
2185
+ * 把 `getComputedStyle` 这条唯一会触发样式重算的路径留到最后。
2186
+ */
2187
+ function looksInteractive(element, context) {
2188
+ if (context.interactiveHint?.(element) === true) return true;
2189
+ const tabindex = element.getAttribute("tabindex");
2190
+ if (tabindex !== null && tabindex.trim() !== "" && tabindex.trim() !== "-1") return true;
2191
+ const title = element.getAttribute("title");
2192
+ if (title !== null && title.trim() !== "") return true;
2193
+ if (INTERACTIVE_TOKEN.test(classAndId(element))) return true;
2194
+ return looksClickable(element, context.view);
2195
+ }
2196
+ /** 图标类名反推出的语义词;没有图标类则 undefined */
2197
+ function iconHintOf(element) {
2198
+ for (const cls of (element.getAttribute("class") ?? "").split(/\s+/)) {
2199
+ const match = ICON_CLASS.exec(cls);
2200
+ if (match === null) continue;
2201
+ const semantic = match[1]?.split("__")[0];
2202
+ if (semantic !== void 0 && semantic !== "") return semantic;
2203
+ }
2204
+ }
2205
+ /** 宿主逃生舱的角色;命中即无条件提升成声明的角色(FR-15.6) */
2206
+ function hintedRoleOf(element, hints) {
2207
+ if (hints === void 0) return void 0;
2208
+ for (const hint of hints) for (const selector of hint.selectors) try {
2209
+ if (element.matches(selector)) return hint.role;
2210
+ } catch {}
2211
+ }
2212
+ /**
2213
+ * 确凿的导航线索(FR-15.3):拿得到就判 `link`,拿不到一律 `button`。
2214
+ *
2215
+ * **不看** `cursor` / class 词根 / `onclick` 这类间接迹象——猜错会让模型以为「点了会跳走」
2216
+ * 而放弃先把页内信息读完,代价比多标一个 `button` 大。
2217
+ */
2218
+ function navigationTargetOf(element, doc) {
2219
+ const own = resolvedHref(element, doc);
2220
+ if (own !== void 0) return own;
2221
+ for (const attribute of ROUTE_ATTRIBUTES) {
2222
+ const value = element.getAttribute(attribute);
2223
+ if (value !== null && value.trim() !== "") return value.trim();
2224
+ }
2225
+ const anchor = element.closest("a[href]");
2226
+ if (anchor !== null && anchor.getAttribute("role") === null) return resolvedHref(anchor, doc);
2227
+ }
2228
+ /** `href` 解析成绝对地址;不导航的形态(`#`、`javascript:`、空值)一律不算线索 */
2229
+ function resolvedHref(element, doc) {
2230
+ const raw = element.getAttribute("href") ?? element.getAttribute("xlink:href");
2231
+ if (raw === null) return void 0;
2232
+ const trimmed = raw.trim();
2233
+ if (trimmed === "" || trimmed === "#" || /^javascript:/i.test(trimmed)) return void 0;
2234
+ try {
2235
+ const url = new URL(trimmed, doc.baseURI);
2236
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
2237
+ } catch {
2238
+ return;
2239
+ }
2240
+ }
2241
+ /**
2242
+ * 算不出名字时合成一个可指称的称呼(FR-15.4 第 3 级)。
2243
+ *
2244
+ * 用「最近具名祖先 + 同级序位」而不是坐标:坐标随滚动和窗口尺寸变,
2245
+ * 用户在屏幕上对不上号,模型跨两次感知也认不出是同一个东西。
2246
+ */
2247
+ function synthesizeName(element, nameOf) {
2248
+ const siblings = element.parentElement?.children;
2249
+ let position = 0;
2250
+ if (siblings !== void 0) {
2251
+ for (const [index, sibling] of [...siblings].entries()) if (sibling === element) {
2252
+ position = index + 1;
2253
+ break;
2254
+ }
2255
+ }
2256
+ const ordinal = position > 0 ? `${SYNTHETIC_NAME_PREFIX} #${position}` : SYNTHETIC_NAME_PREFIX;
2257
+ let ancestor = element.parentElement;
2258
+ for (let depth = 0; depth < SYNTHETIC_NAME_LOOKUP_DEPTH && ancestor !== null; depth += 1) {
2259
+ const label = nameOf(ancestor);
2260
+ if (label !== void 0 && label !== "") return `${ordinal} in "${label}"`;
2261
+ ancestor = ancestor.parentElement;
2262
+ }
2263
+ return ordinal;
2264
+ }
2265
+ /** 提升后的角色与导航目标;未提升时返回 undefined */
2266
+ function promoteRole(input) {
2267
+ const { element, role, doc, hasActionableDescendant, context } = input;
2268
+ const hinted = hintedRoleOf(element, context.roleHints);
2269
+ if (hinted !== void 0) return { role: hinted };
2270
+ if (!PROMOTABLE_ROLES.has(role)) return void 0;
2271
+ if (hasActionableDescendant && role !== "img") return void 0;
2272
+ if (!looksInteractive(element, context)) return void 0;
2273
+ const href = navigationTargetOf(element, doc);
2274
+ return href !== void 0 ? {
2275
+ role: "link",
2276
+ href
2277
+ } : { role: "button" };
2278
+ }
2279
+
2280
+ //#endregion
2281
+ //#region ../browser/src/dom/perception.ts
1905
2282
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
1906
2283
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
1907
2284
  /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
@@ -1979,6 +2356,33 @@ function ownText(element) {
1979
2356
  for (const node of element.childNodes) if (node.nodeType === 3) text += node.nodeValue ?? "";
1980
2357
  return clip(text);
1981
2358
  }
2359
+ /** 名字全在元素外面的那几种控件 */
2360
+ const FORM_CONTROL_TAGS = /* @__PURE__ */ new Set([
2361
+ "INPUT",
2362
+ "TEXTAREA",
2363
+ "SELECT"
2364
+ ]);
2365
+ /**
2366
+ * 表单控件的名字来源:`<label>`(关联式与包裹式)、`placeholder`、`title`。
2367
+ *
2368
+ * 缺了这几级,一个填着「基小建」的输入框在模型眼里就是无名的 textbox——
2369
+ * 旁边的 `<label>` 文字它读得到,却接不到这个框上,于是「值明明在结果里,
2370
+ * 模型却说不出这是谁」。操作侧的 `accessibleName` 一直取这几级,两边不一致
2371
+ * 还会让「感知时叫不出名字、点下去审计里却有名字」。
2372
+ */
2373
+ function externalNameOf(element) {
2374
+ const labels = element.labels;
2375
+ if (labels && labels.length > 0) {
2376
+ const joined = [...labels].map((node) => node.textContent ?? "").filter((part) => part.trim() !== "").join(" ");
2377
+ if (joined.trim() !== "") return clip(joined);
2378
+ }
2379
+ const wrapping = element.closest("label")?.textContent ?? "";
2380
+ if (wrapping.trim() !== "") return clip(wrapping);
2381
+ for (const attribute of ["placeholder", "title"]) {
2382
+ const value = element.getAttribute(attribute);
2383
+ if (value !== null && value.trim() !== "") return clip(value);
2384
+ }
2385
+ }
1982
2386
  function nameOf(element, doc) {
1983
2387
  const label = element.getAttribute("aria-label");
1984
2388
  if (label !== null && label.trim() !== "") return clip(label);
@@ -1991,6 +2395,7 @@ function nameOf(element, doc) {
1991
2395
  const alt = element.getAttribute("alt");
1992
2396
  if (alt !== null && alt.trim() !== "") return clip(alt);
1993
2397
  }
2398
+ if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
1994
2399
  const own = ownText(element);
1995
2400
  return own === "" ? void 0 : own;
1996
2401
  }
@@ -2046,20 +2451,38 @@ function isBelowMinArea(element, minImageArea) {
2046
2451
  if (size === void 0) return false;
2047
2452
  return size.width * size.height < minImageArea;
2048
2453
  }
2049
- function describe(element, excluded, doc, view, capture, handles, frame, provenance) {
2454
+ function describe(element, excluded, doc, view, capture, handles, frame, provenance, promotion) {
2050
2455
  if (excluded.has(element)) return void 0;
2051
2456
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
2052
2457
  if (hidden$1(element, view)) return void 0;
2053
2458
  const children = [];
2054
2459
  for (const child of element.children) {
2055
- const node = describe(child, excluded, doc, view, capture, handles, frame, provenance);
2460
+ const node = describe(child, excluded, doc, view, capture, handles, frame, provenance, promotion);
2056
2461
  if (node !== void 0) children.push(node);
2057
2462
  }
2058
- const name = nameOf(element, doc);
2463
+ let name = nameOf(element, doc);
2059
2464
  const value = valueOf(element);
2060
- const role = roleOf$1(element);
2061
- const href = role === "link" ? hrefOf(element) : void 0;
2465
+ let role = roleOf$1(element);
2466
+ let promotedHref;
2467
+ if (promotion !== void 0) {
2468
+ const promoted = promoteRole({
2469
+ element,
2470
+ role,
2471
+ doc,
2472
+ hasActionableDescendant: children.some((child) => child.ref !== void 0),
2473
+ context: promotion
2474
+ });
2475
+ if (promoted !== void 0) {
2476
+ role = promoted.role;
2477
+ promotedHref = promoted.href;
2478
+ if (name === void 0) name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
2479
+ }
2480
+ }
2481
+ const href = promotedHref ?? (role === "link" ? hrefOf(element) : void 0);
2062
2482
  if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
2483
+ if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0) {
2484
+ if (children.length === 1) return children[0];
2485
+ }
2063
2486
  const node = {
2064
2487
  role,
2065
2488
  ...name !== void 0 ? { name } : {},
@@ -2110,6 +2533,46 @@ function frameHint(iframe) {
2110
2533
  return "unnamed";
2111
2534
  }
2112
2535
  /**
2536
+ * 可路由的绝对地址(分册 16 FR-16.1)。
2537
+ *
2538
+ * 只认 http(s):`about:blank` / `srcdoc` / `data:` 这些外壳寻不到址,
2539
+ * 交出去只会让它拿一个匹配不上任何 frameId 的字符串去猜。
2540
+ */
2541
+ function httpUrlOf(value, base) {
2542
+ try {
2543
+ const url = new URL(value, base);
2544
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
2545
+ } catch {
2546
+ return;
2547
+ }
2548
+ }
2549
+ function routableUrlOf(iframe, doc) {
2550
+ try {
2551
+ const live = iframe.contentWindow?.location?.href;
2552
+ if (live !== void 0 && live !== "about:blank") {
2553
+ const routable = httpUrlOf(live, doc.baseURI);
2554
+ if (routable !== void 0) return routable;
2555
+ }
2556
+ } catch {}
2557
+ const src = iframe.getAttribute("src");
2558
+ if (src === null || src.trim() === "") return void 0;
2559
+ return httpUrlOf(src, doc.baseURI);
2560
+ }
2561
+ /**
2562
+ * 同源嵌套帧的文档;跨源与尚未就绪时返回 `undefined`。
2563
+ *
2564
+ * 跨源时 `contentDocument` 返回 `null` 而**不抛异常**,某些引擎在别的属性上才抛——
2565
+ * 两种形态都要接住,只写 try/catch 会漏掉前一种。
2566
+ */
2567
+ function sameOriginDocumentOf(iframe) {
2568
+ try {
2569
+ const inner = iframe.contentDocument;
2570
+ return inner === null || inner.body === null ? void 0 : inner;
2571
+ } catch {
2572
+ return;
2573
+ }
2574
+ }
2575
+ /**
2113
2576
  * 句柄发放与解析。表在 reader 实例内、**跨感知保留**(分册 18 FR-18.5):
2114
2577
  * 每次 read() 作废全部旧句柄会让多步任务陷入「读—点—失效—再读」的空转,
2115
2578
  * 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
@@ -2180,6 +2643,12 @@ function createDomPerceptionReader(options = {}) {
2180
2643
  */
2181
2644
  const elevatedModals = /* @__PURE__ */ new Set();
2182
2645
  const rootDocument = () => options.document ?? globalThis.document;
2646
+ /** 提升未开启时返回 undefined,遍历侧据此整段跳过(AC-G10) */
2647
+ const promotionOf = (view) => options.promoteRoles === true ? {
2648
+ view,
2649
+ ...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
2650
+ ...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {}
2651
+ } : void 0;
2183
2652
  /**
2184
2653
  * 把一条帧授权解析成 Document,逐层下钻(分册 18 FR-18.3)。
2185
2654
  *
@@ -2256,13 +2725,15 @@ function createDomPerceptionReader(options = {}) {
2256
2725
  const root = rootDocument();
2257
2726
  if (root === void 0) return {
2258
2727
  nodes: [],
2259
- frameNotes: []
2728
+ frameNotes: [],
2729
+ nestedFrames: []
2260
2730
  };
2261
2731
  handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2262
2732
  handles.prune();
2263
2733
  const issuer = handles;
2264
2734
  const nodes = [];
2265
2735
  const frameNotes = [];
2736
+ const nestedFrames = [];
2266
2737
  /** 同一个未授权 iframe 至多报一次(它可能同时落在多个 include 根的子树里) */
2267
2738
  const reportedUnauthorized = /* @__PURE__ */ new Set();
2268
2739
  const resolvedScopes = [];
@@ -2287,6 +2758,66 @@ function createDomPerceptionReader(options = {}) {
2287
2758
  doc: resolved.doc
2288
2759
  });
2289
2760
  }
2761
+ /**
2762
+ * 范围内未授权嵌套帧的处置(分册 16 FR-16.1)。
2763
+ *
2764
+ * 递归而不是循环固定层数:0.13.0 D-18-8 用「路径是宿主写死的数组」论证深度上限,
2765
+ * 自动发现一进来这个前提就没了。兜底改由**帧数与节点总预算**承担,
2766
+ * 层数是错的度量(见分册 16 §0.2)。
2767
+ */
2768
+ function descendFrames(roots, doc, excluded, frameScope, label, tag, actionable, actionExcluded) {
2769
+ for (const element of roots) for (const iframe of nestedFramesIn(element, excluded)) {
2770
+ if (authorizedFrames.has(iframe)) continue;
2771
+ if (reportedUnauthorized.has(iframe)) continue;
2772
+ reportedUnauthorized.add(iframe);
2773
+ const hint = frameHint(iframe);
2774
+ if (options.discoverNestedFrames === true) {
2775
+ const url = routableUrlOf(iframe, doc);
2776
+ if (url !== void 0) {
2777
+ nestedFrames.push({
2778
+ url,
2779
+ hint,
2780
+ ...tag !== void 0 ? { frame: tag } : {}
2781
+ });
2782
+ continue;
2783
+ }
2784
+ const inner = sameOriginDocumentOf(iframe);
2785
+ if (inner !== void 0 && inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded)) continue;
2786
+ }
2787
+ nodes.push({
2788
+ role: "note",
2789
+ 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.`,
2790
+ ...tag !== void 0 ? { frame: tag } : {}
2791
+ });
2792
+ }
2793
+ }
2794
+ /**
2795
+ * 无地址同源帧的就地内联;成功返回 `true`。
2796
+ *
2797
+ * `srcdoc` / `about:blank` 没有可路由地址,外壳寻不到址,只能由父帧代读。
2798
+ *
2799
+ * 可操作性**继承自那个 `<iframe>` 元素本身**:它由父页面写入内容,视觉上就是父页面的一部分,
2800
+ * 父帧的 action scope 把它圈进来了就等于圈进了它的内容;没圈进来则帧内一个句柄都不发。
2801
+ * exclude 在子文档里重新求值——敏感控件在每一层都要剪掉,不因为它在帧里就放宽(§0.1 边界 3)。
2802
+ */
2803
+ function inlineFrame(inner, iframe, frameScope, label, tag, actionable, actionExcluded) {
2804
+ const innerExcluded = /* @__PURE__ */ new Set();
2805
+ for (const selector of frameScope.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerExcluded.add(target);
2806
+ const innerActionable = /* @__PURE__ */ new Set();
2807
+ const innerActionExcluded = /* @__PURE__ */ new Set();
2808
+ if (actionable.has(iframe)) {
2809
+ innerActionable.add(inner.body);
2810
+ for (const descendant of inner.body.querySelectorAll("*")) innerActionable.add(descendant);
2811
+ for (const selector of actionScopeFor(label)?.exclude ?? []) for (const target of inner.querySelectorAll(selector)) innerActionExcluded.add(target);
2812
+ }
2813
+ issuer.useSets(innerActionable, innerActionExcluded);
2814
+ const innerView = inner.defaultView;
2815
+ const node = describe(inner.body, innerExcluded, inner, innerView, capture, issuer, tag, "inline-frame", promotionOf(innerView));
2816
+ if (node !== void 0) nodes.push(node);
2817
+ descendFrames([inner.body], inner, innerExcluded, frameScope, label, tag, innerActionable, innerActionExcluded);
2818
+ issuer.useSets(actionable, actionExcluded);
2819
+ return node !== void 0;
2820
+ }
2290
2821
  for (const { scope: frameScope, label, doc } of resolvedScopes) {
2291
2822
  const view = doc.defaultView;
2292
2823
  const tag = label === "self" ? void 0 : label;
@@ -2297,27 +2828,17 @@ function createDomPerceptionReader(options = {}) {
2297
2828
  const roots = [];
2298
2829
  for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2299
2830
  for (const element of roots) {
2300
- const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0);
2831
+ const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
2301
2832
  if (node !== void 0) nodes.push(node);
2302
2833
  }
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
- }
2834
+ descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
2314
2835
  }
2315
2836
  pruneElevated();
2316
2837
  const view = root.defaultView;
2317
2838
  const sets = actionSets(root, actionScopeFor("self"));
2318
2839
  for (const modal of elevatedModals) {
2319
2840
  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");
2841
+ const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated", promotionOf(view));
2321
2842
  if (node !== void 0) nodes.push(node);
2322
2843
  }
2323
2844
  for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
@@ -2330,12 +2851,13 @@ function createDomPerceptionReader(options = {}) {
2330
2851
  }
2331
2852
  return {
2332
2853
  nodes,
2333
- frameNotes
2854
+ frameNotes,
2855
+ nestedFrames
2334
2856
  };
2335
2857
  }
2336
2858
  async function readWithImages(scope, capture) {
2337
2859
  const targets = [];
2338
- const { nodes, frameNotes } = collect(scope, {
2860
+ const { nodes, frameNotes, nestedFrames } = collect(scope, {
2339
2861
  targets,
2340
2862
  minImageArea: capture.minImageArea ?? 0
2341
2863
  });
@@ -2366,16 +2888,18 @@ function createDomPerceptionReader(options = {}) {
2366
2888
  images,
2367
2889
  imagesOmitted: targets.length - selected.length,
2368
2890
  imageFailures,
2369
- ...frameNotes.length > 0 ? { frameNotes } : {}
2891
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2892
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2370
2893
  };
2371
2894
  }
2372
2895
  function read(scope, capture) {
2373
2896
  if (capture === void 0) return collect(scope, void 0).nodes;
2374
2897
  if (!capture.images || capture.maxImages <= 0) {
2375
- const { nodes, frameNotes } = collect(scope, void 0);
2898
+ const { nodes, frameNotes, nestedFrames } = collect(scope, void 0);
2376
2899
  return Promise.resolve({
2377
2900
  nodes,
2378
- ...frameNotes.length > 0 ? { frameNotes } : {}
2901
+ ...frameNotes.length > 0 ? { frameNotes } : {},
2902
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
2379
2903
  });
2380
2904
  }
2381
2905
  return readWithImages(scope, capture);
@@ -2435,6 +2959,9 @@ function createDomPerceptionReader(options = {}) {
2435
2959
  }
2436
2960
  }
2437
2961
  }
2962
+
2963
+ //#endregion
2964
+ //#region ../browser/src/dom/pageAction.ts
2438
2965
  const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
2439
2966
  /** 只看显式声明,不看尺寸:jsdom 有 getComputedStyle 但没有布局 */
2440
2967
  function hidden(element) {
@@ -2732,6 +3259,396 @@ function createDomPageActionExecutor(options) {
2732
3259
  describe
2733
3260
  };
2734
3261
  }
3262
+
3263
+ //#endregion
3264
+ //#region ../browser/src/remote/registry.ts
3265
+ /** @experimental */
3266
+ function createRemoteTargetRegistry() {
3267
+ let table = /* @__PURE__ */ new Map();
3268
+ return {
3269
+ /**
3270
+ * 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
3271
+ * 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
3272
+ * (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
3273
+ * 否则远端比本地更松。
3274
+ */
3275
+ replace(entries) {
3276
+ const next = /* @__PURE__ */ new Map();
3277
+ for (const entry of entries) {
3278
+ const { ref, role, name, secret, frame, elevated } = entry;
3279
+ next.set(ref, {
3280
+ role,
3281
+ ...name !== void 0 ? { name } : {},
3282
+ ...secret === true ? { secret: true } : {},
3283
+ ...frame !== void 0 ? { frame } : {},
3284
+ ...elevated === true ? { elevated: true } : {}
3285
+ });
3286
+ }
3287
+ table = next;
3288
+ },
3289
+ get(ref) {
3290
+ return table.get(ref);
3291
+ },
3292
+ get size() {
3293
+ return table.size;
3294
+ }
3295
+ };
3296
+ }
3297
+
3298
+ //#endregion
3299
+ //#region ../browser/src/remote/transport.ts
3300
+ /** 缺省单次往返上限(FR-13.2 / FR-13.3) */
3301
+ const DEFAULT_PAGE_AGENT_TIMEOUT_MS = 1e4;
3302
+ /**
3303
+ * 一次带超时的往返,并把三类失败统一成 `WebSkillError`(FR-13.6)。
3304
+ *
3305
+ * 三类都必须显式失败,**绝不降级成空结果**:
3306
+ * 空节点数组的含义是「页面上什么都没有」,模型收到它会据此作答,
3307
+ * 比报错危险得多——报错至少不会产出一个自信的错误回答。
3308
+ *
3309
+ * @param expect 期望的应答类型;对不上通常意味着装错了 agent 版本,
3310
+ * 静默容忍会把问题推到很远的地方才暴露。
3311
+ */
3312
+ async function roundTrip(transport, request, expect, code, timeoutMs) {
3313
+ let timer;
3314
+ let reply;
3315
+ try {
3316
+ reply = await Promise.race([transport.send(request), new Promise((_resolve, reject) => {
3317
+ timer = setTimeout(() => reject(new WebSkillError(code, `Page agent request "${request.type}" timed out after ${timeoutMs}ms with no reply from the page.`)), timeoutMs);
3318
+ })]);
3319
+ } catch (e) {
3320
+ if (e instanceof WebSkillError) throw e;
3321
+ const message = e instanceof Error ? e.message : String(e);
3322
+ throw new WebSkillError(code, `Page agent request "${request.type}" failed: ${message}`);
3323
+ } finally {
3324
+ if (timer !== void 0) clearTimeout(timer);
3325
+ }
3326
+ if (reply.type === "error") throw new WebSkillError(reply.code, reply.message);
3327
+ if (reply.type !== expect) throw new WebSkillError(code, `Page agent replied with "${reply.type}" to a "${request.type}" request; expected "${expect}".`);
3328
+ return reply;
3329
+ }
3330
+
3331
+ //#endregion
3332
+ //#region ../browser/src/remote/reader.ts
3333
+ /**
3334
+ * 跨上下文的页面感知(分册 13 / FR-13.2)。
3335
+ *
3336
+ * 把句柄描述表写进 registry,**只把 `result` 返回给调用方**——
3337
+ * 返回值就是标准 `PerceptionResult`,不夹带任何新字段(AC-13.2)。
3338
+ * @experimental
3339
+ */
3340
+ function createRemotePerceptionReader(options) {
3341
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3342
+ return { async read(scope, capture) {
3343
+ const reply = await roundTrip(transport, {
3344
+ type: "perceive",
3345
+ scope,
3346
+ ...capture !== void 0 ? { capture } : {}
3347
+ }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
3348
+ targets.replace(reply.targets);
3349
+ return reply.result;
3350
+ } };
3351
+ }
3352
+
3353
+ //#endregion
3354
+ //#region ../browser/src/remote/executor.ts
3355
+ /**
3356
+ * 跨上下文的页面操作执行器(分册 13 / FR-13.3)。
3357
+ * @experimental
3358
+ */
3359
+ function createRemotePageActionExecutor(options) {
3360
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3361
+ return {
3362
+ /**
3363
+ * **同步**查表。`PageActionPolicy.act()` 在弹确认卡之前调它,
3364
+ * 拿到的 target 决定卡上写什么、是否 `secret`、是否 `elevated`——
3365
+ * 改成异步就是改 `@webskill/agent` 的公开契约。
3366
+ *
3367
+ * 表里没有即 `undefined`,于是 `act()` 抛 `PAGE_ACTION_STALE_REF`:
3368
+ * 没感知过就执行 = 拒绝,宁可让模型多感知一次(fail-closed)。
3369
+ */
3370
+ describe(ref) {
3371
+ return targets.get(ref);
3372
+ },
3373
+ /**
3374
+ * 原样返回远端**执行那一刻**回填的 outcome(FR-13.5):
3375
+ * 不用 registry 里的快照拼装,也不补默认值——
3376
+ * 补出来的 `false` 与「远端没报」在审计上不是一回事。
3377
+ */
3378
+ async execute(request) {
3379
+ return (await roundTrip(transport, {
3380
+ type: "execute",
3381
+ request
3382
+ }, "execute-result", "TOOL_EXECUTION_FAILED", timeoutMs)).outcome;
3383
+ }
3384
+ };
3385
+ }
3386
+
3387
+ //#endregion
3388
+ //#region ../browser/src/remote/handler.ts
3389
+ /** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
3390
+ function collectRefs(nodes, out = []) {
3391
+ for (const node of nodes) {
3392
+ if (node.ref !== void 0) out.push(node.ref);
3393
+ if (node.children !== void 0) collectRefs(node.children, out);
3394
+ }
3395
+ return out;
3396
+ }
3397
+ /**
3398
+ * 页面侧处理器(分册 13 / FR-13.7)。
3399
+ *
3400
+ * 必须是工厂而不是裸函数:`DomPerceptionReader` **是有状态的**——
3401
+ * `resolve(ref)` 依赖上一次感知发出的句柄表,模态提升状态也跨调用累积。
3402
+ * 每次请求新建 reader,`execute` 就永远 resolve 不到 `perceive` 发出的 ref。
3403
+ *
3404
+ * 它**不自己收发消息**:只做「请求进、应答出」。监听消息通道、判断消息是不是给自己的、
3405
+ * 回不回 `true`——那些全是宿主的事,本包不得出现任何扩展 API。
3406
+ * @experimental
3407
+ */
3408
+ function createPageAgentHandler(options = {}) {
3409
+ const reader = createDomPerceptionReader({
3410
+ ...options.document !== void 0 ? { document: options.document } : {},
3411
+ ...options.actionScope !== void 0 ? { actionScope: options.actionScope } : {},
3412
+ ...options.promoteRoles !== void 0 ? { promoteRoles: options.promoteRoles } : {},
3413
+ ...options.interactiveHint !== void 0 ? { interactiveHint: options.interactiveHint } : {},
3414
+ ...options.roleHints !== void 0 ? { roleHints: options.roleHints } : {},
3415
+ ...options.discoverNestedFrames !== void 0 ? { discoverNestedFrames: options.discoverNestedFrames } : {}
3416
+ });
3417
+ const executor = createDomPageActionExecutor({
3418
+ reader,
3419
+ ...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
3420
+ });
3421
+ return { async handle(request) {
3422
+ try {
3423
+ if (request.type === "perceive") {
3424
+ const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
3425
+ const result = Array.isArray(raw) ? { nodes: raw } : raw;
3426
+ const targets = [];
3427
+ for (const ref of collectRefs(result.nodes)) {
3428
+ const target = executor.describe(ref);
3429
+ if (target !== void 0) targets.push({
3430
+ ref,
3431
+ ...target
3432
+ });
3433
+ }
3434
+ return {
3435
+ type: "perceive-result",
3436
+ result,
3437
+ targets
3438
+ };
3439
+ }
3440
+ return {
3441
+ type: "execute-result",
3442
+ outcome: await executor.execute(request.request)
3443
+ };
3444
+ } catch (e) {
3445
+ return {
3446
+ type: "error",
3447
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
3448
+ message: e instanceof Error ? e.message : String(e)
3449
+ };
3450
+ }
3451
+ } };
3452
+ }
3453
+
3454
+ //#endregion
3455
+ //#region ../browser/src/remote/frameRouter.ts
3456
+ /** @experimental */
3457
+ const DEFAULT_FRAME_BUDGET = {
3458
+ maxFrames: 12,
3459
+ maxNodes: 1500
3460
+ };
3461
+ function countNodes(nodes) {
3462
+ let total = 0;
3463
+ for (const node of nodes) total += 1 + countNodes(node.children ?? []);
3464
+ return total;
3465
+ }
3466
+ function note(name) {
3467
+ return {
3468
+ role: "note",
3469
+ name
3470
+ };
3471
+ }
3472
+ /**
3473
+ * 地址归一化:只比较 origin + path。
3474
+ *
3475
+ * 父页面写的 `src` 常带 `?t=123` 之类的缓存参数,甚至被框架改写过 hash。
3476
+ * 带上 query/hash 比对会把明明是同一帧的判成换了页,
3477
+ * 于是本该读到的内容变成一句「导航走了」——宁可粗一点也不能漏。
3478
+ */
3479
+ function frameKey(url) {
3480
+ try {
3481
+ const parsed = new URL(url);
3482
+ return `${parsed.origin}${parsed.pathname}`;
3483
+ } catch {
3484
+ return;
3485
+ }
3486
+ }
3487
+ /**
3488
+ * 确认卡与模型上下文里指称一帧的说法(分册 16 FR-16.6)。
3489
+ *
3490
+ * 带上 host 而不只用父页面写的 `hint`:用户要能一眼分清「页面主体里的那个按钮」
3491
+ * 和「某个第三方 iframe 里的按钮」,而 `hint` 来自被嵌一方决定不了、
3492
+ * 嵌入方随便写的属性——`title="账户设置"` 的 iframe 完全可以指向别人的域。
3493
+ */
3494
+ function frameLabelOf(nested) {
3495
+ try {
3496
+ return `${nested.hint} (${new URL(nested.url).host})`;
3497
+ } catch {
3498
+ return nested.hint;
3499
+ }
3500
+ }
3501
+ /** 与 SDK 单帧取像超限时的文案同源;两边改要一起改 */
3502
+ const IMAGE_LIMIT_NOTE = "Not captured: the per-message image limit was reached";
3503
+ /**
3504
+ * 深度重写节点:打帧标签,并把 `imageId` 换成汇总后的全局 id。
3505
+ *
3506
+ * 每帧的取像 id 都是自己从 `img-1` 数起的,直接合并会让不同帧的图撞成同一个 id。
3507
+ * 落选(超整页预算)的图必须**同时**抹掉节点上的 `imageId`,否则节点指向一张
3508
+ * 根本没下发的图,模型会以为自己看过了。
3509
+ */
3510
+ function relabel(nodes, frame, imageIds) {
3511
+ return nodes.map((node) => {
3512
+ const next = { ...node };
3513
+ if (frame !== void 0) next.frame = node.frame ?? frame;
3514
+ if (node.imageId !== void 0) {
3515
+ const mapped = imageIds.get(node.imageId);
3516
+ if (mapped === void 0) {
3517
+ delete next.imageId;
3518
+ next.imageNote = node.imageNote ?? IMAGE_LIMIT_NOTE;
3519
+ } else next.imageId = mapped;
3520
+ }
3521
+ if (node.children !== void 0) next.children = relabel(node.children, frame, imageIds);
3522
+ return next;
3523
+ });
3524
+ }
3525
+ /**
3526
+ * 逐帧下钻并汇总(分册 16 FR-16.2 / FR-16.3 / FR-16.4,分册 11 FR-11.2)。
3527
+ *
3528
+ * 编排本身与浏览器 API 无关:它只认 `PageAgentRequest` / `PageAgentReply`,
3529
+ * 寻址全压在宿主实现的 `FrameDispatcher` 里。因此这个模块可以在 Service Worker
3530
+ * 里实例化,也可以服务不按帧号寻址的宿主。
3531
+ * @experimental
3532
+ */
3533
+ function createFrameRouter(options) {
3534
+ const budget = {
3535
+ ...DEFAULT_FRAME_BUDGET,
3536
+ ...options.budget
3537
+ };
3538
+ /** ref → 发放它的那一帧。`execute` 必须投回原帧,投给别的帧要么查无此 ref 要么误点同名控件 */
3539
+ const refFrames = /* @__PURE__ */ new Map();
3540
+ async function perceive(request) {
3541
+ const mainAddress = await options.dispatcher.mainFrame();
3542
+ const nodes = [];
3543
+ const targets = [];
3544
+ const frameNotes = [];
3545
+ const main = await options.dispatcher.send(mainAddress, request);
3546
+ if (main.reply.type !== "perceive-result") return main.reply;
3547
+ const images = [];
3548
+ let imagesOmitted = 0;
3549
+ let imageFailures = 0;
3550
+ const imageBudget = request.capture?.images === true ? request.capture.maxImages : 0;
3551
+ const queue = [];
3552
+ const visited = [mainAddress];
3553
+ let nodeCount = 0;
3554
+ let framesRead = 0;
3555
+ let descended = 0;
3556
+ const skipped = [];
3557
+ const absorb = (reply, address, frame, imagePrefix) => {
3558
+ framesRead += 1;
3559
+ nodeCount += countNodes(reply.result.nodes);
3560
+ const imageIds = /* @__PURE__ */ new Map();
3561
+ for (const image of reply.result.images ?? []) {
3562
+ if (images.length >= imageBudget) {
3563
+ imagesOmitted += 1;
3564
+ continue;
3565
+ }
3566
+ const id = `${imagePrefix}${image.id}`;
3567
+ imageIds.set(image.id, id);
3568
+ images.push({
3569
+ ...image,
3570
+ id
3571
+ });
3572
+ }
3573
+ imagesOmitted += reply.result.imagesOmitted ?? 0;
3574
+ imageFailures += reply.result.imageFailures ?? 0;
3575
+ nodes.push(...relabel(reply.result.nodes, frame, imageIds));
3576
+ for (const target of reply.targets) {
3577
+ targets.push(frame === void 0 ? target : {
3578
+ ...target,
3579
+ frame: target.frame ?? frame
3580
+ });
3581
+ refFrames.set(target.ref, address);
3582
+ }
3583
+ if (reply.result.frameNotes !== void 0) frameNotes.push(...reply.result.frameNotes);
3584
+ for (const nested of reply.result.nestedFrames ?? []) queue.push({
3585
+ ...nested,
3586
+ parent: address
3587
+ });
3588
+ };
3589
+ absorb(main.reply, mainAddress, options.mainFrameLabel, "");
3590
+ while (queue.length > 0) {
3591
+ const next = queue.shift();
3592
+ if (framesRead >= budget.maxFrames || nodeCount >= budget.maxNodes) {
3593
+ skipped.push(next.hint);
3594
+ continue;
3595
+ }
3596
+ const address = await options.dispatcher.resolveNestedFrame(next, {
3597
+ parent: next.parent,
3598
+ visited
3599
+ });
3600
+ if (address === void 0) {
3601
+ nodes.push(note(`A nested frame "${next.hint}" (${next.url}) could not be addressed and was not read. It may have been removed, replaced, or it is out of reach for this host.`));
3602
+ continue;
3603
+ }
3604
+ visited.push(address);
3605
+ const answer = await options.dispatcher.send(address, request).catch((e) => {
3606
+ return {
3607
+ reply: {
3608
+ type: "error",
3609
+ code: "TOOL_EXECUTION_FAILED",
3610
+ message: e instanceof Error ? e.message : String(e)
3611
+ },
3612
+ documentUrl: ""
3613
+ };
3614
+ });
3615
+ if (answer.reply.type !== "perceive-result") {
3616
+ const reason = answer.reply.type === "error" ? answer.reply.message : "it returned an unexpected reply";
3617
+ nodes.push(note(`A nested frame "${next.hint}" (${next.url}) was not read: ${reason}`));
3618
+ continue;
3619
+ }
3620
+ if (frameKey(answer.documentUrl) !== frameKey(next.url)) {
3621
+ nodes.push(note(`A nested frame "${next.hint}" (${next.url}) navigated away while it was being read (it answered from ${answer.documentUrl}); its content was discarded.`));
3622
+ continue;
3623
+ }
3624
+ descended += 1;
3625
+ absorb(answer.reply, address, frameLabelOf(next), `f${descended}-`);
3626
+ }
3627
+ if (skipped.length > 0) nodes.push(note(`${skipped.length} nested frame(s) were not read because this page exceeds the per-perception budget (${budget.maxFrames} frames / ${budget.maxNodes} nodes): ${skipped.join(", ")}. The content above is complete for the frames that were read.`));
3628
+ return {
3629
+ type: "perceive-result",
3630
+ result: {
3631
+ nodes,
3632
+ ...images.length > 0 ? { images } : {},
3633
+ ...imagesOmitted > 0 ? { imagesOmitted } : {},
3634
+ ...imageFailures > 0 ? { imageFailures } : {},
3635
+ ...frameNotes.length > 0 ? { frameNotes } : {}
3636
+ },
3637
+ targets
3638
+ };
3639
+ }
3640
+ async function execute(request) {
3641
+ const address = refFrames.get(request.request.ref) ?? await options.dispatcher.mainFrame();
3642
+ return (await options.dispatcher.send(address, request)).reply;
3643
+ }
3644
+ return {
3645
+ perceive,
3646
+ execute
3647
+ };
3648
+ }
3649
+
3650
+ //#endregion
3651
+ //#region ../browser/src/host/protocol.ts
2735
3652
  const isRecord = (v) => typeof v === "object" && v !== null;
2736
3653
  const isNonEmptyString = (v) => typeof v === "string" && v !== "";
2737
3654
  /** Worker 侧入站校验:非法消息返回 undefined(忽略) */
@@ -2819,6 +3736,9 @@ function parseWorkerEvent(data) {
2819
3736
  default: return;
2820
3737
  }
2821
3738
  }
3739
+
3740
+ //#endregion
3741
+ //#region ../browser/src/ui/workerUiBridge.ts
2822
3742
  /**
2823
3743
  * Worker 内 UiBridge:request → 'interaction' 消息到主线程,
2824
3744
  * 挂起等待 'interaction-response' → resolve(行内 await 跨线程成立)。
@@ -2873,6 +3793,9 @@ var WorkerUiBridge = class {
2873
3793
  });
2874
3794
  }
2875
3795
  };
3796
+
3797
+ //#endregion
3798
+ //#region ../browser/src/host/workerRuntimeHost.ts
2876
3799
  const serializeError = (e) => ({
2877
3800
  code: e instanceof WebSkillError ? e.code : "RUN_FAILED",
2878
3801
  message: e instanceof Error ? e.message : String(e)
@@ -3100,6 +4023,9 @@ function startWorkerRuntimeHost(scope, overrides = {}) {
3100
4023
  }
3101
4024
  });
3102
4025
  }
4026
+
4027
+ //#endregion
4028
+ //#region ../browser/src/host/workerClient.ts
3103
4029
  /** 主线程客户端:init/run/cancel/status/dispose 的 Promise API + 交互转发 */
3104
4030
  var WorkerRuntimeClient = class {
3105
4031
  #worker;
@@ -3245,6 +4171,9 @@ var WorkerRuntimeClient = class {
3245
4171
  }
3246
4172
  }
3247
4173
  };
4174
+
4175
+ //#endregion
4176
+ //#region ../browser/src/memory/encryptedMemoryStore.ts
3248
4177
  /**
3249
4178
  * 静态加密的 MemoryStore 包装(FR-19.5)。落盘形态:
3250
4179
  * `{ v: 1, iv: base64, ct: base64 }`;每次写入生成新的 12 字节随机 IV
@@ -3412,6 +4341,9 @@ async function deleteMemoryEncryptionKey(name = "user-profile") {
3412
4341
  db.close();
3413
4342
  }
3414
4343
  }
4344
+
4345
+ //#endregion
4346
+ //#region ../browser/src/host/portMatrix.ts
3415
4347
  /**
3416
4348
  * 端口 × 宿主类型的**唯一定义**(AC-17.4)。
3417
4349
  * About 页与装配工厂都读这一份;护栏 `port-matrix-single-source` 盯着不许再抄。
@@ -3561,6 +4493,9 @@ function inspectHostWiring(input) {
3561
4493
  function missingPorts(reports) {
3562
4494
  return reports.filter((report) => report.status === "missing");
3563
4495
  }
4496
+
4497
+ //#endregion
4498
+ //#region ../browser/src/host/createBrowserChatbotHost.ts
3564
4499
  function createBrowserChatbotHost(options) {
3565
4500
  const { fs, managedRoot, skillRoots, navigation, overrides } = options;
3566
4501
  const chatbotAdapter = {
@@ -3585,6 +4520,9 @@ function createBrowserChatbotHost(options) {
3585
4520
  connectFacade: overrides?.connectFacade ?? connectFacade
3586
4521
  };
3587
4522
  }
4523
+
4524
+ //#endregion
4525
+ //#region ../browser/src/net/fetchLinkedDocument.ts
3588
4526
  const DEFAULT_TIMEOUT_MS = 3e4;
3589
4527
  /**
3590
4528
  * 默认的链接文档读取实现(分册 22 §2.2)。
@@ -3615,6 +4553,9 @@ function createFetchLinkedDocumentReader(options = {}) {
3615
4553
  }
3616
4554
  };
3617
4555
  }
4556
+
4557
+ //#endregion
4558
+ //#region ../browser/src/document/docx.ts
3618
4559
  /**
3619
4560
  * docx 正文抽取(分册 22 §2.4 / FR-23.7)。
3620
4561
  *
@@ -3692,6 +4633,9 @@ async function extractDocxText(bytes) {
3692
4633
  }
3693
4634
  return blocks.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3694
4635
  }
4636
+
4637
+ //#endregion
4638
+ //#region ../browser/src/document/xlsx.ts
3695
4639
  /**
3696
4640
  * xlsx 文本抽取(0.13.0 分册 12)。
3697
4641
  *
@@ -3941,6 +4885,9 @@ async function extractXlsxText(bytes) {
3941
4885
  }
3942
4886
  return sections.join("\n\n");
3943
4887
  }
4888
+
4889
+ //#endregion
4890
+ //#region ../browser/src/viewer/viewerCsp.ts
3944
4891
  /**
3945
4892
  * 文档投放面的 CSP 单一来源(0.11.0 分册 18 FR-18.3 / AC-G21)。
3946
4893
  *
@@ -3991,6 +4938,9 @@ function viewerCspHeader(options) {
3991
4938
  }
3992
4939
  /** 本版固定的 sandbox token(供判据与装配文档引用,避免第二份清单) */
3993
4940
  const VIEWER_SANDBOX_TOKENS = SANDBOX_TOKENS;
4941
+
4942
+ //#endregion
4943
+ //#region ../browser/src/viewer/documentSurface.ts
3994
4944
  const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 2e6;
3995
4945
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
3996
4946
  /** hello 的重发间隔:viewer 脚本加载完之前投出去的消息全部丢失 */
@@ -4062,6 +5012,16 @@ async function openDocumentSurface(input, options) {
4062
5012
  function defaultOpen(url, target) {
4063
5013
  return globalThis.open?.(url, target) ?? null;
4064
5014
  }
5015
+ /**
5016
+ * 把 `openDocumentSurface` 柯里化成 `DocumentSurfaceHost`(0.15.0 分册 13 FR-13.3)。
5017
+ *
5018
+ * **只绑定选项,不新增任何逻辑**:体积上限、握手超时、`DOCUMENT_SURFACE_DECLINED`
5019
+ * 与审计事件全部沿用上面那一份实现(FR-13.5「逐条不变」的实现基础)。
5020
+ * @experimental
5021
+ */
5022
+ function createDocumentSurfaceHost(options) {
5023
+ return { open: async (input) => void await openDocumentSurface(input, options) };
5024
+ }
4065
5025
  /** viewer 是 opaque origin,`postMessage` 的 targetOrigin 只能是 `*` —— 用 channelId 做配对校验 */
4066
5026
  function viewerOrigin(_options) {
4067
5027
  return "*";
@@ -4120,6 +5080,9 @@ async function audit(options, input, data) {
4120
5080
  }
4121
5081
  }).catch(() => void 0);
4122
5082
  }
5083
+
5084
+ //#endregion
5085
+ //#region ../browser/src/viewer/shell.ts
4123
5086
  /**
4124
5087
  * 启动外壳:等主应用的 hello → 回报 ready → 收文档 → 写进 DOM。
4125
5088
  *
@@ -4152,6 +5115,9 @@ function startViewerShell(host, mount, onData) {
4152
5115
  host.removeEventListener("message", onMessage);
4153
5116
  } };
4154
5117
  }
5118
+
5119
+ //#endregion
5120
+ //#region ../browser/src/viewer/blockedResources.ts
4155
5121
  /**
4156
5122
  * 监听 CSP 违规并把被拦资源汇总成一句话。
4157
5123
  *
@@ -4185,4 +5151,4 @@ function originOf(blockedURI) {
4185
5151
  }
4186
5152
 
4187
5153
  //#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 };
5154
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_FRAME_BUDGET, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, SANDBOX_PAGE_SCRIPT_SOURCE, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDocumentSurfaceHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };