@webskill/sdk 0.15.0 → 0.17.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 (32) hide show
  1. package/dist/agent.d.ts +3 -3
  2. package/dist/agent.js +466 -66
  3. package/dist/{approval-BnLLCOG0.js → approval-CBPnGPIm.js} +57 -4
  4. package/dist/browser.d.ts +214 -7
  5. package/dist/browser.js +487 -47
  6. package/dist/{eventTypes-Y07N8IAC.js → eventTypes-CwciaqCU.js} +23 -0
  7. package/dist/governance.d.ts +26 -3
  8. package/dist/governance.js +1 -1
  9. package/dist/{index-CQYo4tcT.d.ts → index-1sQZ4VBN.d.ts} +324 -16
  10. package/dist/{index-Oc3H89TJ.d.ts → index-BDyXe-a5.d.ts} +1 -1
  11. package/dist/{index-CU2md8R1.d.ts → index-CaLsVI-m.d.ts} +7 -2
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +3 -3
  14. package/dist/mcp.d.ts +2 -2
  15. package/dist/{miniChart-owdCdaw-.js → miniChart-lauNtZu8.js} +2 -7
  16. package/dist/node.d.ts +3 -3
  17. package/dist/node.js +14 -8
  18. package/dist/{openUiLibrary-CY_ADoe0.js → openUiLibrary-BJtCG5B1.js} +1 -1
  19. package/dist/{openUiSpecLang-DuOCdw9G.js → openUiSpecLang-B4QQ9Tfo.js} +4 -3
  20. package/dist/{skillVersionStore-B24Jjjry-C4zomhMZ.d.ts → skillVersionStore-B24Jjjry-v2CCU0Xo.d.ts} +1 -1
  21. package/dist/testing.d.ts +1 -1
  22. package/dist/{types-C3V6lAeO-BIf7UB2e.d.ts → types-Btpdd1y--BcxQ10Fa.d.ts} +20 -3
  23. package/dist/{types-TUydnxaj.js → types-BuacBov_.js} +14 -3
  24. package/dist/ui-react.d.ts +9 -3
  25. package/dist/ui-react.js +15 -4
  26. package/dist/ui-vue.d.ts +1 -1
  27. package/dist/ui-vue.js +2 -2
  28. package/dist/ui.d.ts +3 -3
  29. package/dist/ui.js +4 -4
  30. package/dist/{webSkillApi-DOOyz2G5.js → webSkillApi-Ce2OO5S5.js} +1 -1
  31. package/dist/{webskillLitCatalog-DSqVFQjK.js → webskillLitCatalog-BBkQjKuN.js} +2 -2
  32. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -2,11 +2,11 @@ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
2
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
3
  import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot } from "./pathSecurity-B1owvJAF.js";
4
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";
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-CBPnGPIm.js";
6
6
  import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-B7lLH0ZI.js";
7
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";
8
+ import { t as createWebSkillApi } from "./webSkillApi-Ce2OO5S5.js";
9
+ import { a as frameLabel, i as toFrameScopes, o as frameSteps, r as toActionFrameScopes } from "./types-BuacBov_.js";
10
10
 
11
11
  //#region ../browser/src/fs/featureDetection.ts
12
12
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -1355,13 +1355,16 @@ var BrowserWorkerScriptExecutor = class {
1355
1355
  }
1356
1356
  /** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
1357
1357
  async #handleBridge(request, context) {
1358
- const gate = async (capability, message, details) => {
1358
+ const gate = async (capability, message, details = {}) => {
1359
1359
  const decision = await this.#approval.authorize({
1360
1360
  runId: context.runId,
1361
1361
  capability,
1362
1362
  mode: this.#capabilities[capability],
1363
1363
  message,
1364
- ...details !== void 0 ? { details } : {}
1364
+ details: {
1365
+ skill: context.skillName,
1366
+ ...details
1367
+ }
1365
1368
  });
1366
1369
  if (decision === "allowed") return void 0;
1367
1370
  return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled by the sandbox capability settings` : `Capability "${capability}" was denied by the user`);
@@ -1419,7 +1422,7 @@ var BrowserWorkerScriptExecutor = class {
1419
1422
  }
1420
1423
  case "confirm": {
1421
1424
  if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
1422
- const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
1425
+ const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`, { question: request.message });
1423
1426
  if (denied) return denied;
1424
1427
  return {
1425
1428
  id: request.id,
@@ -1677,7 +1680,7 @@ function checkDictationAvailability() {
1677
1680
  return { available: true };
1678
1681
  }
1679
1682
  /** 浏览器的 error 码 → 稳定错误码;权限被拒要单独区分,UI 的降级提示不一样(FR-7.6) */
1680
- function toError(code, message) {
1683
+ function toError$1(code, message) {
1681
1684
  if (code === "not-allowed" || code === "service-not-allowed") return new WebSkillError("DICTATION_PERMISSION_DENIED", "Microphone access was denied. Voice input is unavailable until you allow it in the browser.", { reason: code });
1682
1685
  return new WebSkillError("DICTATION_FAILED", message ?? `Speech recognition failed: ${code}`, { reason: code });
1683
1686
  }
@@ -1713,7 +1716,7 @@ function startDictation(options) {
1713
1716
  recognition.onerror = (event) => {
1714
1717
  if (stopped) return;
1715
1718
  if (event.error === "no-speech" || event.error === "aborted") return;
1716
- options.onError(toError(event.error, event.message));
1719
+ options.onError(toError$1(event.error, event.message));
1717
1720
  };
1718
1721
  try {
1719
1722
  recognition.start();
@@ -1730,6 +1733,107 @@ function startDictation(options) {
1730
1733
  } };
1731
1734
  }
1732
1735
 
1736
+ //#endregion
1737
+ //#region ../browser/src/media/camera.ts
1738
+ /**
1739
+ * 默认最长边:宿主要「最大分辨率」,给一个远超当前消费级摄像头的 ideal 让浏览器自己贴顶
1740
+ * @experimental
1741
+ */
1742
+ const DEFAULT_CAMERA_MAX_DIMENSION = 5e3;
1743
+ function mediaDevices() {
1744
+ return globalThis.navigator?.mediaDevices;
1745
+ }
1746
+ /**
1747
+ * 渲染期调用(FR-14.2):不可用时按钮置灰并说明原因。
1748
+ *
1749
+ * 顺序与语音一致——`getUserMedia` 压根不存在时谈安全上下文没有意义;
1750
+ * API 在但站点是 http,取流一定会被浏览器拒绝,这条原因用户能自己解决。
1751
+ * @experimental
1752
+ */
1753
+ function checkCameraAvailability() {
1754
+ if (typeof mediaDevices()?.getUserMedia !== "function") return {
1755
+ available: false,
1756
+ reason: "api-missing"
1757
+ };
1758
+ if (globalThis.isSecureContext === false) return {
1759
+ available: false,
1760
+ reason: "insecure-context"
1761
+ };
1762
+ return { available: true };
1763
+ }
1764
+ /** DOMException 的 name → 稳定错误码;权限被拒要单独区分,UI 的降级提示不一样(FR-14.6) */
1765
+ function toError(e) {
1766
+ const raw = e?.name;
1767
+ const name = typeof raw === "string" ? raw : "";
1768
+ if (name === "NotAllowedError" || name === "SecurityError") return new WebSkillError("CAMERA_PERMISSION_DENIED", "Camera access was denied. Photo capture is unavailable until you allow it in the browser.", { reason: name });
1769
+ const message = e?.message;
1770
+ return new WebSkillError("CAMERA_UNAVAILABLE", `The camera could not be opened: ${typeof message === "string" && message !== "" ? message : String(e)}`, { ...name !== "" ? { reason: name } : {} });
1771
+ }
1772
+ /**
1773
+ * 打开摄像头取景(FR-14.1)。
1774
+ *
1775
+ * **只能由用户手势调用**(FR-14.4)。设计上的保证是它不进工具列表:
1776
+ * agent 拿不到「打开摄像头」这个动作,因此不存在自主开摄像头的路径。
1777
+ * @experimental
1778
+ */
1779
+ async function openCamera(options = {}) {
1780
+ const devices = mediaDevices();
1781
+ if (typeof devices?.getUserMedia !== "function") throw new WebSkillError("CAMERA_UNAVAILABLE", "Camera capture is not available in this browser.");
1782
+ const max = options.maxDimension ?? 5e3;
1783
+ let stream;
1784
+ try {
1785
+ stream = await devices.getUserMedia({
1786
+ video: {
1787
+ width: { ideal: max },
1788
+ height: { ideal: max },
1789
+ ...options.facingMode !== void 0 ? { facingMode: options.facingMode } : {}
1790
+ },
1791
+ audio: false
1792
+ });
1793
+ } catch (e) {
1794
+ throw toError(e);
1795
+ }
1796
+ return {
1797
+ stream,
1798
+ resolution() {
1799
+ const settings = stream.getVideoTracks()[0]?.getSettings();
1800
+ return {
1801
+ width: settings?.width ?? 0,
1802
+ height: settings?.height ?? 0
1803
+ };
1804
+ },
1805
+ close() {
1806
+ for (const track of stream.getTracks()) track.stop();
1807
+ }
1808
+ };
1809
+ }
1810
+ /**
1811
+ * 把取景元素的**当前一帧**编码成文件(FR-14.5)。
1812
+ *
1813
+ * 尺寸取 `videoWidth`/`videoHeight`(轨道原生分辨率),不取 CSS 尺寸——
1814
+ * 预览区被布局缩到 320px 宽,照着画出来的就是 320px 的照片。
1815
+ * 编码交给平台的 canvas,不手写编码器(AGENTS.md §6)。
1816
+ * @experimental
1817
+ */
1818
+ async function capturePhoto(video, options = {}) {
1819
+ const width = video.videoWidth;
1820
+ const height = video.videoHeight;
1821
+ if (width === 0 || height === 0) throw new WebSkillError("CAMERA_CAPTURE_FAILED", "The camera has not produced a frame yet.");
1822
+ const type = options.type ?? "image/jpeg";
1823
+ const canvas = document.createElement("canvas");
1824
+ canvas.width = width;
1825
+ canvas.height = height;
1826
+ const context = canvas.getContext("2d");
1827
+ if (context === null) throw new WebSkillError("CAMERA_CAPTURE_FAILED", "This browser did not provide a 2D canvas context.");
1828
+ context.drawImage(video, 0, 0, width, height);
1829
+ const blob = await new Promise((resolve) => {
1830
+ canvas.toBlob(resolve, type, options.quality ?? .92);
1831
+ });
1832
+ if (blob === null) throw new WebSkillError("CAMERA_CAPTURE_FAILED", `The captured frame could not be encoded as ${type}.`);
1833
+ const name = options.fileName ?? `photo-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jpg`;
1834
+ return new File([blob], name, { type: blob.type === "" ? type : blob.type });
1835
+ }
1836
+
1733
1837
  //#endregion
1734
1838
  //#region ../browser/src/media/compressImage.ts
1735
1839
  /** 动图无法在不丢帧的前提下走单帧编码管线,超限直接拒绝(裁决 D-2) */
@@ -2013,6 +2117,17 @@ function isAnimatedWebp(bytes) {
2013
2117
  ].every((byte, index) => bytes[12 + index] === byte) && ((bytes[20] ?? 0) & 2) !== 0;
2014
2118
  }
2015
2119
  /**
2120
+ * APNG:`acTL` 块。规范要求它排在第一个 `IDAT` 之前,所以嗅探头部就够。
2121
+ * 魔数与静态 PNG 完全相同,只看魔数会让加载动画这类素材当作静态图原样发出去。
2122
+ */
2123
+ function isAnimatedPng(bytes) {
2124
+ for (let i = 8; i + 4 <= bytes.length; i += 1) {
2125
+ if (bytes[i] === 73 && bytes[i + 1] === 68 && bytes[i + 2] === 65 && bytes[i + 3] === 84) return false;
2126
+ if (bytes[i] === 97 && bytes[i + 1] === 99 && bytes[i + 2] === 84 && bytes[i + 3] === 76) return true;
2127
+ }
2128
+ return false;
2129
+ }
2130
+ /**
2016
2131
  * 按魔数认图片类型(服务端声明只作兜底)。
2017
2132
  *
2018
2133
  * 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
@@ -2065,7 +2180,7 @@ async function rasterizeToPng(blob) {
2065
2180
  * 照抄声明会让端点解码失败并连累整次请求(一张图 400,37 张全废)。
2066
2181
  *
2067
2182
  * 魔数对得上也不等于发得出去,还有两类会被端点整次拒收,必须重编码成单帧位图:
2068
- * - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位):图片通道普遍只收静态图;
2183
+ * - **动图**(GIF 天然多帧,WebP 看 `VP8X` 的动画位,PNG 看 `acTL` 块):图片通道普遍只收静态图;
2069
2184
  * - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
2070
2185
  * 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
2071
2186
  *
@@ -2077,7 +2192,7 @@ async function toDeliverableBlob(blob, oversized = false) {
2077
2192
  const sniffed = sniffImageMime(head);
2078
2193
  if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
2079
2194
  const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
2080
- const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head);
2195
+ const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head) || sniffed === "image/png" && isAnimatedPng(head);
2081
2196
  return oversized || animated ? await rasterizeToPng(typed) : typed;
2082
2197
  }
2083
2198
  const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
@@ -2158,11 +2273,20 @@ const ROUTE_ATTRIBUTES = [
2158
2273
  "data-href",
2159
2274
  "data-url"
2160
2275
  ];
2161
- /** 只有这三种角色会被提升:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3) */
2276
+ /**
2277
+ * 可被启发式信号提升的角色:其余角色要么本就能发句柄,要么带着不该被覆盖的语义(D-15-3)。
2278
+ *
2279
+ * `listitem` 在列表里本该只是结构,但 `<ul><li>` 是自绘下拉 / 菜单的主流写法,
2280
+ * 选项往往就是不带 role 的 `<li>`(0.17.0 分册 11)。它与另外三个一样受
2281
+ * 「有可操作后代就不提升」约束,所以真正的语义列表不会被误伤。
2282
+ *
2283
+ * 这张白名单**只约束启发式信号**。宿主探针点名的元素走另一条路(分册 12)。
2284
+ */
2162
2285
  const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
2163
2286
  "generic",
2164
2287
  "img",
2165
- "cell"
2288
+ "cell",
2289
+ "listitem"
2166
2290
  ]);
2167
2291
  /** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
2168
2292
  const SYNTHETIC_NAME_PREFIX = "unlabeled control";
@@ -2264,10 +2388,11 @@ function synthesizeName(element, nameOf) {
2264
2388
  }
2265
2389
  /** 提升后的角色与导航目标;未提升时返回 undefined */
2266
2390
  function promoteRole(input) {
2267
- const { element, role, doc, hasActionableDescendant, context } = input;
2391
+ const { element, role, doc, hasActionableDescendant, selfActionable, context } = input;
2268
2392
  const hinted = hintedRoleOf(element, context.roleHints);
2269
2393
  if (hinted !== void 0) return { role: hinted };
2270
- if (!PROMOTABLE_ROLES.has(role)) return void 0;
2394
+ if (selfActionable) return void 0;
2395
+ if (!(context.interactiveHint?.(element) === true) && !PROMOTABLE_ROLES.has(role)) return void 0;
2271
2396
  if (hasActionableDescendant && role !== "img") return void 0;
2272
2397
  if (!looksInteractive(element, context)) return void 0;
2273
2398
  const href = navigationTargetOf(element, doc);
@@ -2281,7 +2406,7 @@ function promoteRole(input) {
2281
2406
  //#region ../browser/src/dom/perception.ts
2282
2407
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
2283
2408
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
2284
- /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
2409
+ /** 可被操作的角色:只有这些节点的句柄能拿去写页面 */
2285
2410
  const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
2286
2411
  "button",
2287
2412
  "checkbox",
@@ -2293,9 +2418,34 @@ const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
2293
2418
  "switch",
2294
2419
  "textbox"
2295
2420
  ]);
2421
+ /**
2422
+ * 只能做子树锚点的容器角色(分册 14 §6.1)。
2423
+ *
2424
+ * 这些节点拿到的是“读”句柄:`perceive_page(ref)` 能以它为根续读,
2425
+ * `act_on_page` 拿到它则一律被拒。不取 generic:那等于给每个 div 发句柄。
2426
+ */
2427
+ const ANCHOR_ROLES = /* @__PURE__ */ new Set([
2428
+ "article",
2429
+ "dialog",
2430
+ "feed",
2431
+ "grid",
2432
+ "group",
2433
+ "list",
2434
+ "listitem",
2435
+ "main",
2436
+ "navigation",
2437
+ "region",
2438
+ "row",
2439
+ "rowgroup",
2440
+ "table",
2441
+ "tabpanel",
2442
+ "tree",
2443
+ "treeitem"
2444
+ ]);
2296
2445
  /** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
2297
2446
  const ROLE_BY_TAG = {
2298
2447
  A: "link",
2448
+ ARTICLE: "article",
2299
2449
  BUTTON: "button",
2300
2450
  CANVAS: "img",
2301
2451
  H1: "heading",
@@ -2469,7 +2619,8 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2469
2619
  element,
2470
2620
  role,
2471
2621
  doc,
2472
- hasActionableDescendant: children.some((child) => child.ref !== void 0),
2622
+ hasActionableDescendant: children.some((child) => child.ref !== void 0 && handles?.isAction(child.ref) === true),
2623
+ selfActionable: ACTIONABLE_ROLES.has(role),
2473
2624
  context: promotion
2474
2625
  });
2475
2626
  if (promoted !== void 0) {
@@ -2602,11 +2753,13 @@ var HandleIssuer = class {
2602
2753
  }
2603
2754
  issue(element, role, frame) {
2604
2755
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
2605
- if (!ACTIONABLE_ROLES.has(role)) return void 0;
2756
+ const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
2757
+ if (kind === void 0) return void 0;
2606
2758
  const known = this.#refByElement.get(element);
2607
2759
  if (known !== void 0 && this.#table.has(known)) {
2608
2760
  this.#table.set(known, {
2609
2761
  element,
2762
+ kind,
2610
2763
  ...frame !== void 0 ? { frame } : {}
2611
2764
  });
2612
2765
  return known;
@@ -2616,11 +2769,16 @@ var HandleIssuer = class {
2616
2769
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
2617
2770
  this.#table.set(ref, {
2618
2771
  element,
2772
+ kind,
2619
2773
  ...frame !== void 0 ? { frame } : {}
2620
2774
  });
2621
2775
  this.#refByElement.set(element, ref);
2622
2776
  return ref;
2623
2777
  }
2778
+ /** 角色提升要的是「有可操作后代」,容器锚点不算;混进来会把可点容器判成已有出口 */
2779
+ isAction(ref) {
2780
+ return this.#table.get(ref)?.kind === "action";
2781
+ }
2624
2782
  get table() {
2625
2783
  return this.#table;
2626
2784
  }
@@ -2721,12 +2879,20 @@ function createDomPerceptionReader(options = {}) {
2721
2879
  };
2722
2880
  /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
2723
2881
  const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
2724
- function collect(scope, capture) {
2882
+ /**
2883
+ * @param subtree 不为 `undefined` 时只读这一棵子树(分册 14)。
2884
+ * 它**不是一个新的范围来源**:子树必须先落在某条帧授权的 include 根下、
2885
+ * 且未被 exclude 命中,否则遍历**根本不开始**(`anchored: false`)。
2886
+ * 命中时也只是把那一帧的 `roots` 换成这一个元素,遍历器与 exclude 判定一字未改——
2887
+ * 「带 `ref` 的结果是不带它时的子集」因此是个结构事实(FR-14.2)。
2888
+ */
2889
+ function collect(scope, capture, subtree) {
2725
2890
  const root = rootDocument();
2726
2891
  if (root === void 0) return {
2727
2892
  nodes: [],
2728
2893
  frameNotes: [],
2729
- nestedFrames: []
2894
+ nestedFrames: [],
2895
+ anchored: false
2730
2896
  };
2731
2897
  handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2732
2898
  handles.prune();
@@ -2818,6 +2984,7 @@ function createDomPerceptionReader(options = {}) {
2818
2984
  issuer.useSets(actionable, actionExcluded);
2819
2985
  return node !== void 0;
2820
2986
  }
2987
+ let anchored = subtree === void 0;
2821
2988
  for (const { scope: frameScope, label, doc } of resolvedScopes) {
2822
2989
  const view = doc.defaultView;
2823
2990
  const tag = label === "self" ? void 0 : label;
@@ -2825,14 +2992,27 @@ function createDomPerceptionReader(options = {}) {
2825
2992
  issuer.useSets(sets.actionable, sets.excluded);
2826
2993
  const excluded = /* @__PURE__ */ new Set();
2827
2994
  for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
2828
- const roots = [];
2995
+ let roots = [];
2829
2996
  for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2997
+ if (subtree !== void 0) {
2998
+ const inScope = roots.some((element) => element === subtree || element.contains(subtree));
2999
+ const cut = [...excluded].some((element) => element === subtree || element.contains(subtree));
3000
+ if (!inScope || cut) continue;
3001
+ roots = [subtree];
3002
+ anchored = true;
3003
+ }
2830
3004
  for (const element of roots) {
2831
3005
  const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
2832
3006
  if (node !== void 0) nodes.push(node);
2833
3007
  }
2834
3008
  descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
2835
3009
  }
3010
+ if (subtree !== void 0) return {
3011
+ nodes,
3012
+ frameNotes,
3013
+ nestedFrames,
3014
+ anchored
3015
+ };
2836
3016
  pruneElevated();
2837
3017
  const view = root.defaultView;
2838
3018
  const sets = actionSets(root, actionScopeFor("self"));
@@ -2852,15 +3032,16 @@ function createDomPerceptionReader(options = {}) {
2852
3032
  return {
2853
3033
  nodes,
2854
3034
  frameNotes,
2855
- nestedFrames
3035
+ nestedFrames,
3036
+ anchored
2856
3037
  };
2857
3038
  }
2858
- async function readWithImages(scope, capture) {
3039
+ async function readWithImages(scope, capture, subtree) {
2859
3040
  const targets = [];
2860
- const { nodes, frameNotes, nestedFrames } = collect(scope, {
3041
+ const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, {
2861
3042
  targets,
2862
3043
  minImageArea: capture.minImageArea ?? 0
2863
- });
3044
+ }, subtree);
2864
3045
  const selected = targets.slice(0, capture.maxImages);
2865
3046
  const images = [];
2866
3047
  let imageFailures = 0;
@@ -2888,6 +3069,7 @@ function createDomPerceptionReader(options = {}) {
2888
3069
  images,
2889
3070
  imagesOmitted: targets.length - selected.length,
2890
3071
  imageFailures,
3072
+ anchored,
2891
3073
  ...frameNotes.length > 0 ? { frameNotes } : {},
2892
3074
  ...nestedFrames.length > 0 ? { nestedFrames } : {}
2893
3075
  };
@@ -2904,6 +3086,28 @@ function createDomPerceptionReader(options = {}) {
2904
3086
  }
2905
3087
  return readWithImages(scope, capture);
2906
3088
  }
3089
+ /**
3090
+ * 子树续读(分册 14 FR-14.1)。三条判定全在遍历开始**之前**:
3091
+ * 句柄不认识 → `unknown-ref`;解析得到的元素已离开文档、
3092
+ * 或不在任何一条帧授权的 include 根下、或被 exclude 连根剪掉 → `out-of-scope`。
3093
+ *
3094
+ * 拒绝走返回值而不是抛错:跨上下文的 reject 到本地就是一个没有错误码的 `Error`。
3095
+ */
3096
+ async function readSubtree(scope, ref, capture) {
3097
+ const element = handles?.table.get(ref)?.element;
3098
+ if (element === void 0 || !element.isConnected) return { rejected: "unknown-ref" };
3099
+ if (capture?.images === true && capture.maxImages > 0) {
3100
+ const { anchored, ...result } = await readWithImages(scope, capture, element);
3101
+ return anchored ? result : { rejected: "out-of-scope" };
3102
+ }
3103
+ const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, void 0, element);
3104
+ if (!anchored) return { rejected: "out-of-scope" };
3105
+ return {
3106
+ nodes,
3107
+ ...frameNotes.length > 0 ? { frameNotes } : {},
3108
+ ...nestedFrames.length > 0 ? { nestedFrames } : {}
3109
+ };
3110
+ }
2907
3111
  /** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
2908
3112
  function modalSnapshot() {
2909
3113
  const root = rootDocument();
@@ -2916,7 +3120,9 @@ function createDomPerceptionReader(options = {}) {
2916
3120
  }
2917
3121
  return {
2918
3122
  read,
3123
+ readSubtree,
2919
3124
  resolve: (ref) => handles?.table.get(ref)?.element,
3125
+ handleKindOf: (ref) => handles?.table.get(ref)?.kind,
2920
3126
  frameOf: (ref) => handles?.table.get(ref)?.frame,
2921
3127
  modalSnapshot,
2922
3128
  elevateNewModals: (before) => {
@@ -2960,6 +3166,35 @@ function createDomPerceptionReader(options = {}) {
2960
3166
  }
2961
3167
  }
2962
3168
 
3169
+ //#endregion
3170
+ //#region ../browser/src/remote/documentIdentity.ts
3171
+ /**
3172
+ * 文档标识(0.16.0 分册 11 · FR-11.1)。
3173
+ *
3174
+ * 它有且只有两个消费者,且**必须是同一个函数**:
3175
+ * ① 感知回复里的 `document.key`,决定这批句柄进哪个分片;
3176
+ * ② `PageActionOutcome.navigated`,执行前后各取一次。
3177
+ * 两处各写一套判定,就会出现「操作说导航了、分片说没换」的不一致(AC-11.4 / AC-13.4 验的就是这个)。
3178
+ */
3179
+ /** 每个文档实例一次。页面重载即换,因此真导航天然换 key */
3180
+ const instance = (() => {
3181
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
3182
+ crypto.getRandomValues(bytes);
3183
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
3184
+ })();
3185
+ /**
3186
+ * `instance` + 当前地址。真导航换 `instance`,帧内路由(SPA)换 `href`,
3187
+ * 两类都被这一个串覆盖——靠 origin 比对识别不出后者。
3188
+ *
3189
+ * `href` 含 hash 是有意的:纯 hash 跳转在很多站点上会换掉整块内容,
3190
+ * 判成换文档是安全侧的选择(代价是多感知一次;漏判没有便宜的补救)。
3191
+ * @experimental
3192
+ */
3193
+ function documentKey(view) {
3194
+ const href = view?.location.href;
3195
+ return href === void 0 ? void 0 : `${instance}\u0000${href}`;
3196
+ }
3197
+
2963
3198
  //#endregion
2964
3199
  //#region ../browser/src/dom/pageAction.ts
2965
3200
  const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
@@ -3044,6 +3279,67 @@ function fillValue(element, value) {
3044
3279
  element.dispatchEvent(new Event("change", { bubbles: true }));
3045
3280
  }
3046
3281
  const norm = (text) => (text ?? "").replace(/\s+/g, " ").trim().toLowerCase();
3282
+ /**
3283
+ * 激活一个元素。
3284
+ *
3285
+ * `click()` 只定义在 `HTMLElement` 上:内联 `<svg>`(以及其它外来命名空间的元素)
3286
+ * 拿到的是 `undefined`,直接调会抛 `TypeError`,整次操作倒在最后一步。
3287
+ * 图标把监听器挂在 `<svg>` 本体上是常见写法,所以这条路径必须通。
3288
+ * 退路是派发一个真正的 `click` 事件:它照样冒泡,祖先 `<a>` 的激活行为也照样跑。
3289
+ */
3290
+ function activate(element, init) {
3291
+ const native = element.click;
3292
+ if (typeof native === "function") native.call(element);
3293
+ else element.dispatchEvent(new MouseEvent("click", init));
3294
+ }
3295
+ /**
3296
+ * 一次「像人一样」的点击。
3297
+ *
3298
+ * `HTMLElement.click()` **只**派发 `click`,不派发指针/鼠标序列,也不移动焦点。
3299
+ * 大量自研下拉、弹层、菜单是在 `pointerdown` / `mousedown` 上开的(这样拖拽和
3300
+ * 「点外面关闭」才好写),于是模型「点了但什么都没发生」,人手点却正常——
3301
+ * 差别不在权限,在事件序列。真实用户点击本来就会先后产生 down 与 click 两拨事件,
3302
+ * 因此补齐序列不会造成「点两下」。
3303
+ *
3304
+ * 激活行为仍交给 `.click()`:链接跳转、复选框翻转这些默认动作由它保证。
3305
+ */
3306
+ function press(element) {
3307
+ const view = element.ownerDocument.defaultView;
3308
+ const rect = element.getBoundingClientRect();
3309
+ const base = {
3310
+ bubbles: true,
3311
+ cancelable: true,
3312
+ composed: true,
3313
+ detail: 1,
3314
+ button: 0,
3315
+ clientX: rect.left + rect.width / 2,
3316
+ clientY: rect.top + rect.height / 2
3317
+ };
3318
+ const pointer = {
3319
+ ...base,
3320
+ pointerId: 1,
3321
+ pointerType: "mouse",
3322
+ isPrimary: true
3323
+ };
3324
+ const Pointer = view?.PointerEvent;
3325
+ if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerdown", {
3326
+ ...pointer,
3327
+ buttons: 1
3328
+ }));
3329
+ if (element.dispatchEvent(new MouseEvent("mousedown", {
3330
+ ...base,
3331
+ buttons: 1
3332
+ }))) element.focus?.();
3333
+ if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerup", {
3334
+ ...pointer,
3335
+ buttons: 0
3336
+ }));
3337
+ element.dispatchEvent(new MouseEvent("mouseup", {
3338
+ ...base,
3339
+ buttons: 0
3340
+ }));
3341
+ activate(element, base);
3342
+ }
3047
3343
  /** 轮询等待,上限固定:等不到就失败,不能无限挂着一次工具调用 */
3048
3344
  async function waitFor(probe, timeoutMs = 2e3) {
3049
3345
  const deadline = Date.now() + timeoutMs;
@@ -3082,12 +3378,18 @@ async function selectOption(element, value) {
3082
3378
  } : fail(`The control did not accept "${value}".`);
3083
3379
  }
3084
3380
  const doc = element.ownerDocument;
3085
- element.click();
3381
+ press(element);
3086
3382
  const listbox = await waitFor(() => doc.querySelector("[role=listbox], [role=grid], [role=menu]") ?? void 0);
3087
3383
  if (listbox === null || listbox === void 0) return fail("The options panel did not open.");
3088
3384
  const option = [...listbox.querySelectorAll("[role=option], [role=gridcell], [role=menuitem], option")].find((candidate) => norm(accessibleName(candidate)) === norm(value));
3089
3385
  if (option === void 0) return fail(`No option named "${value}" is available.`);
3090
- option.click();
3386
+ activate(option, {
3387
+ bubbles: true,
3388
+ cancelable: true,
3389
+ composed: true,
3390
+ detail: 1,
3391
+ button: 0
3392
+ });
3091
3393
  return await waitFor(() => {
3092
3394
  const shown = `${accessibleName(element) ?? ""} ${element.value ?? ""}`;
3093
3395
  return norm(shown).includes(norm(value)) ? true : void 0;
@@ -3120,7 +3422,7 @@ function setToggle(element, value, target) {
3120
3422
  target,
3121
3423
  noop: true
3122
3424
  };
3123
- element.click();
3425
+ press(element);
3124
3426
  return {
3125
3427
  ok: true,
3126
3428
  target
@@ -3163,7 +3465,44 @@ function createDomPageActionExecutor(options) {
3163
3465
  elevated: true
3164
3466
  } : described;
3165
3467
  };
3468
+ /**
3469
+ * 后退(0.16.0 分册 13 / FR-13.6)。
3470
+ *
3471
+ * 两种「没退成」都返回 `ok: true, noop: true` 而不是失败:
3472
+ * 返回失败模型会重试,而重试同样退不动——它需要的是「这条路走不通」的事实,
3473
+ * 不是一个可以再试一次的错误(与 `set` 的 noop 同惯例)。
3474
+ */
3475
+ const goBack = async () => {
3476
+ const view = (options.document ?? globalThis.document)?.defaultView;
3477
+ const target = { role: "document" };
3478
+ if (view === void 0 || view === null) return {
3479
+ ok: false,
3480
+ target,
3481
+ reason: "No browsing context is available."
3482
+ };
3483
+ if (view.history.length <= 1) return {
3484
+ ok: true,
3485
+ target,
3486
+ noop: true
3487
+ };
3488
+ const keyBefore = documentKey(view);
3489
+ view.history.back();
3490
+ return await waitFor(() => {
3491
+ const key = documentKey(view);
3492
+ return key !== void 0 && key !== keyBefore ? key : void 0;
3493
+ }, 500) === void 0 ? {
3494
+ ok: true,
3495
+ target,
3496
+ noop: true
3497
+ } : {
3498
+ ok: true,
3499
+ target,
3500
+ navigated: true,
3501
+ documentUrl: view.location.href
3502
+ };
3503
+ };
3166
3504
  const execute = async (request) => {
3505
+ if (request.action === "back") return await goBack();
3167
3506
  const element = reader.resolve(request.ref);
3168
3507
  if (element === void 0) return {
3169
3508
  ok: false,
@@ -3176,36 +3515,37 @@ function createDomPageActionExecutor(options) {
3176
3515
  target,
3177
3516
  reason
3178
3517
  });
3518
+ 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 that carries its own reference. If nothing inside it has one, this part of the page offers no action and retrying here will not help.");
3179
3519
  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.");
3180
3520
  if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
3181
3521
  if (hidden(element)) return fail("The element is not visible.");
3182
3522
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
3183
3523
  const modalsBefore = reader.modalSnapshot();
3184
3524
  const frameView = element.ownerDocument.defaultView;
3185
- const urlBefore = frameView?.location.href;
3525
+ const keyBefore = documentKey(frameView);
3186
3526
  const done = async (outcome) => {
3187
3527
  if (!outcome.ok) return outcome;
3188
3528
  const settled = await waitFor(() => {
3189
3529
  const modal = reader.elevateNewModals(modalsBefore);
3190
3530
  if (modal !== void 0) return { modal };
3191
- const url = frameView?.location.href;
3192
- return url !== void 0 && url !== urlBefore ? { url } : void 0;
3531
+ const key = documentKey(frameView);
3532
+ return key !== void 0 && key !== keyBefore ? { key } : void 0;
3193
3533
  }, 500);
3194
3534
  const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
3195
3535
  const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
3196
- const urlAfter = frameView?.location.href;
3197
- const navigated = urlAfter !== void 0 && urlBefore !== void 0 && urlAfter !== urlBefore;
3536
+ const keyAfter = documentKey(frameView);
3537
+ const navigated = keyAfter !== void 0 && keyBefore !== void 0 && keyAfter !== keyBefore;
3198
3538
  return {
3199
3539
  ...outcome,
3200
3540
  ...name !== void 0 ? { elevatedModal: name } : {},
3201
3541
  ...navigated ? {
3202
3542
  navigated: true,
3203
- documentUrl: urlAfter
3543
+ documentUrl: frameView?.location.href
3204
3544
  } : {}
3205
3545
  };
3206
3546
  };
3207
3547
  if (request.action === "click") {
3208
- element.click();
3548
+ press(element);
3209
3549
  return await done({
3210
3550
  ok: true,
3211
3551
  target
@@ -3262,35 +3602,114 @@ function createDomPageActionExecutor(options) {
3262
3602
 
3263
3603
  //#endregion
3264
3604
  //#region ../browser/src/remote/registry.ts
3605
+ /**
3606
+ * 分片数的防泄漏软上限(分册 11 / DV-2)。
3607
+ *
3608
+ * 它**不是** FR-11.5 那个容量闸门:那一条管的是「同时有几个**标签页**可操作」,
3609
+ * 触顶硬拒绝,落在 `TabWorksetPolicy`。这里管的是同一个标签页里连续跳页攒下的历史分片,
3610
+ * 那些本来就该失效,按最久未使用逐出即可,逐出不构成对模型的拒绝。
3611
+ * 软上限必须宽于 `TAB_WORKSET_MAX_MAX`(分册 18),否则工作集里**还活着**的页
3612
+ * 会因为别的页新发分片而被逐出;64 = 天花板 32 × 每页两代历史。
3613
+ */
3614
+ const SHARD_SOFT_LIMIT = 64;
3615
+ /** 不给 `shard` 时用的分片键;单分片宿主全程待在这一个分片里 */
3616
+ const DEFAULT_SHARD = "";
3617
+ /**
3618
+ * 把解析结果翻译成给模型看的补充说明(分册 11 / FR-11.6);
3619
+ * 接到 `PageActionPolicyOptions.diagnose` 上即可:
3620
+ *
3621
+ * ```ts
3622
+ * diagnose: (ref) => explainResolution(targets.resolve(ref))
3623
+ * ```
3624
+ *
3625
+ * `unknown` 返回 `undefined` 而不是自己拼一句:让 policy 落回它的缺省文案,
3626
+ * 于是 AC-11.3 要求的「与 0.15.0 逐字相同」是**构造性**成立的,
3627
+ * 而不是靠两个文件里的字符串抄得一样。
3628
+ * @experimental
3629
+ */
3630
+ function explainResolution(resolution) {
3631
+ if (resolution.status === "unfocused") return "This reference belongs to a different page. Switch to that page first, then retry.";
3632
+ if (resolution.status === "expired") return "The page this reference belongs to has changed or was closed. Perceive the page again and retry.";
3633
+ }
3265
3634
  /** @experimental */
3266
- function createRemoteTargetRegistry() {
3267
- let table = /* @__PURE__ */ new Map();
3635
+ function createRemoteTargetRegistry(options = {}) {
3636
+ const maxShards = Math.max(1, Math.trunc(options.maxShards ?? 64));
3637
+ let shards = /* @__PURE__ */ new Map();
3638
+ let refIndex = /* @__PURE__ */ new Map();
3639
+ let focused = DEFAULT_SHARD;
3640
+ const touch = (key, shard) => {
3641
+ shards.delete(key);
3642
+ shards.set(key, shard);
3643
+ while (shards.size > maxShards) {
3644
+ const oldest = shards.keys().next();
3645
+ if (oldest.done === true) break;
3646
+ shards.delete(oldest.value);
3647
+ }
3648
+ };
3268
3649
  return {
3269
3650
  /**
3270
3651
  * 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
3271
3652
  * 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
3272
3653
  * (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
3273
3654
  * 否则远端比本地更松。
3655
+ *
3656
+ * 0.16.0 起替换的是**该分片**的表,别的分片不受影响——这是相对 0.15.0 的唯一语义变化。
3274
3657
  */
3275
- replace(entries) {
3276
- const next = /* @__PURE__ */ new Map();
3658
+ replace(entries, document) {
3659
+ const shard = document?.key ?? DEFAULT_SHARD;
3660
+ const table = /* @__PURE__ */ new Map();
3277
3661
  for (const entry of entries) {
3278
3662
  const { ref, role, name, secret, frame, elevated } = entry;
3279
- next.set(ref, {
3663
+ table.set(ref, {
3280
3664
  role,
3281
3665
  ...name !== void 0 ? { name } : {},
3282
3666
  ...secret === true ? { secret: true } : {},
3283
3667
  ...frame !== void 0 ? { frame } : {},
3284
3668
  ...elevated === true ? { elevated: true } : {}
3285
3669
  });
3670
+ refIndex.set(ref, shard);
3286
3671
  }
3287
- table = next;
3672
+ touch(shard, {
3673
+ table,
3674
+ ...document !== void 0 ? { document } : {}
3675
+ });
3676
+ focused = shard;
3288
3677
  },
3289
3678
  get(ref) {
3290
- return table.get(ref);
3679
+ return shards.get(focused)?.table.get(ref);
3680
+ },
3681
+ resolve(ref) {
3682
+ const target = shards.get(focused)?.table.get(ref);
3683
+ if (target !== void 0) return {
3684
+ status: "ok",
3685
+ target
3686
+ };
3687
+ const owner = refIndex.get(ref);
3688
+ if (owner === void 0) return { status: "unknown" };
3689
+ return shards.get(owner)?.table.has(ref) === true ? { status: "unfocused" } : { status: "expired" };
3690
+ },
3691
+ focus(shard) {
3692
+ focused = shard;
3693
+ },
3694
+ get focused() {
3695
+ return focused === DEFAULT_SHARD ? void 0 : focused;
3696
+ },
3697
+ describePage() {
3698
+ return shards.get(focused)?.document;
3699
+ },
3700
+ drop(shard) {
3701
+ shards.delete(shard);
3702
+ },
3703
+ clear() {
3704
+ shards = /* @__PURE__ */ new Map();
3705
+ refIndex = /* @__PURE__ */ new Map();
3706
+ focused = DEFAULT_SHARD;
3291
3707
  },
3292
3708
  get size() {
3293
- return table.size;
3709
+ return shards.get(focused)?.table.size ?? 0;
3710
+ },
3711
+ get shardCount() {
3712
+ return shards.size;
3294
3713
  }
3295
3714
  };
3296
3715
  }
@@ -3338,14 +3757,15 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
3338
3757
  * @experimental
3339
3758
  */
3340
3759
  function createRemotePerceptionReader(options) {
3341
- const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3760
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
3342
3761
  return { async read(scope, capture) {
3343
3762
  const reply = await roundTrip(transport, {
3344
3763
  type: "perceive",
3345
3764
  scope,
3346
3765
  ...capture !== void 0 ? { capture } : {}
3347
3766
  }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
3348
- targets.replace(reply.targets);
3767
+ targets.replace(reply.targets, reply.document);
3768
+ if (reply.document !== void 0) onDocument?.(reply.document);
3349
3769
  return reply.result;
3350
3770
  } };
3351
3771
  }
@@ -3416,8 +3836,25 @@ function createPageAgentHandler(options = {}) {
3416
3836
  });
3417
3837
  const executor = createDomPageActionExecutor({
3418
3838
  reader,
3839
+ ...options.document !== void 0 ? { document: options.document } : {},
3419
3840
  ...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
3420
3841
  });
3842
+ /**
3843
+ * 这批句柄属于哪个文档(FR-11.1)。`key` 与 `PageActionOutcome.navigated`
3844
+ * 出自同一个 `documentKey()` —— 这是「操作说导航了、分片说没换」不可能发生的构造性保证。
3845
+ */
3846
+ const describeDocument = () => {
3847
+ const doc = options.document ?? globalThis.document;
3848
+ const key = documentKey(doc?.defaultView);
3849
+ if (key === void 0) return void 0;
3850
+ const url = doc.defaultView?.location.href;
3851
+ const title = doc.title;
3852
+ return {
3853
+ key,
3854
+ ...url !== void 0 ? { url } : {},
3855
+ ...title !== void 0 && title !== "" ? { title } : {}
3856
+ };
3857
+ };
3421
3858
  return { async handle(request) {
3422
3859
  try {
3423
3860
  if (request.type === "perceive") {
@@ -3431,10 +3868,12 @@ function createPageAgentHandler(options = {}) {
3431
3868
  ...target
3432
3869
  });
3433
3870
  }
3871
+ const identity = describeDocument();
3434
3872
  return {
3435
3873
  type: "perceive-result",
3436
3874
  result,
3437
- targets
3875
+ targets,
3876
+ ...identity !== void 0 ? { document: identity } : {}
3438
3877
  };
3439
3878
  }
3440
3879
  return {
@@ -3634,7 +4073,8 @@ function createFrameRouter(options) {
3634
4073
  ...imageFailures > 0 ? { imageFailures } : {},
3635
4074
  ...frameNotes.length > 0 ? { frameNotes } : {}
3636
4075
  },
3637
- targets
4076
+ targets,
4077
+ ...main.reply.document !== void 0 ? { document: main.reply.document } : {}
3638
4078
  };
3639
4079
  }
3640
4080
  async function execute(request) {
@@ -5151,4 +5591,4 @@ function originOf(blockedURI) {
5151
5591
  }
5152
5592
 
5153
5593
  //#endregion
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 };
5594
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_CAMERA_MAX_DIMENSION, 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, capturePhoto, checkCameraAvailability, 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, openCamera, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };