@webskill/sdk 0.14.0 → 0.16.0

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