@webskill/sdk 0.15.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.
package/dist/browser.js CHANGED
@@ -6,7 +6,7 @@ import { a as networkPolicyLibSource, c as bridgeError, d as FsMemoryStore, g as
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
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";
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) */
@@ -2281,7 +2281,7 @@ function promoteRole(input) {
2281
2281
  //#region ../browser/src/dom/perception.ts
2282
2282
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
2283
2283
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
2284
- /** 可被操作的角色:只给这些节点发句柄,其余节点模型连提都提不出来 */
2284
+ /** 可被操作的角色:只有这些节点的句柄能拿去写页面 */
2285
2285
  const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
2286
2286
  "button",
2287
2287
  "checkbox",
@@ -2293,9 +2293,34 @@ const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
2293
2293
  "switch",
2294
2294
  "textbox"
2295
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
+ ]);
2296
2320
  /** tag → 可访问性角色的最小映射;命中不了就退到 generic,不猜 */
2297
2321
  const ROLE_BY_TAG = {
2298
2322
  A: "link",
2323
+ ARTICLE: "article",
2299
2324
  BUTTON: "button",
2300
2325
  CANVAS: "img",
2301
2326
  H1: "heading",
@@ -2469,7 +2494,7 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2469
2494
  element,
2470
2495
  role,
2471
2496
  doc,
2472
- hasActionableDescendant: children.some((child) => child.ref !== void 0),
2497
+ hasActionableDescendant: children.some((child) => child.ref !== void 0 && handles?.isAction(child.ref) === true),
2473
2498
  context: promotion
2474
2499
  });
2475
2500
  if (promoted !== void 0) {
@@ -2602,11 +2627,13 @@ var HandleIssuer = class {
2602
2627
  }
2603
2628
  issue(element, role, frame) {
2604
2629
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
2605
- 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;
2606
2632
  const known = this.#refByElement.get(element);
2607
2633
  if (known !== void 0 && this.#table.has(known)) {
2608
2634
  this.#table.set(known, {
2609
2635
  element,
2636
+ kind,
2610
2637
  ...frame !== void 0 ? { frame } : {}
2611
2638
  });
2612
2639
  return known;
@@ -2616,11 +2643,16 @@ var HandleIssuer = class {
2616
2643
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
2617
2644
  this.#table.set(ref, {
2618
2645
  element,
2646
+ kind,
2619
2647
  ...frame !== void 0 ? { frame } : {}
2620
2648
  });
2621
2649
  this.#refByElement.set(element, ref);
2622
2650
  return ref;
2623
2651
  }
2652
+ /** 角色提升要的是「有可操作后代」,容器锚点不算;混进来会把可点容器判成已有出口 */
2653
+ isAction(ref) {
2654
+ return this.#table.get(ref)?.kind === "action";
2655
+ }
2624
2656
  get table() {
2625
2657
  return this.#table;
2626
2658
  }
@@ -2721,12 +2753,20 @@ function createDomPerceptionReader(options = {}) {
2721
2753
  };
2722
2754
  /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
2723
2755
  const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
2724
- 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) {
2725
2764
  const root = rootDocument();
2726
2765
  if (root === void 0) return {
2727
2766
  nodes: [],
2728
2767
  frameNotes: [],
2729
- nestedFrames: []
2768
+ nestedFrames: [],
2769
+ anchored: false
2730
2770
  };
2731
2771
  handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2732
2772
  handles.prune();
@@ -2818,6 +2858,7 @@ function createDomPerceptionReader(options = {}) {
2818
2858
  issuer.useSets(actionable, actionExcluded);
2819
2859
  return node !== void 0;
2820
2860
  }
2861
+ let anchored = subtree === void 0;
2821
2862
  for (const { scope: frameScope, label, doc } of resolvedScopes) {
2822
2863
  const view = doc.defaultView;
2823
2864
  const tag = label === "self" ? void 0 : label;
@@ -2825,14 +2866,27 @@ function createDomPerceptionReader(options = {}) {
2825
2866
  issuer.useSets(sets.actionable, sets.excluded);
2826
2867
  const excluded = /* @__PURE__ */ new Set();
2827
2868
  for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
2828
- const roots = [];
2869
+ let roots = [];
2829
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
+ }
2830
2878
  for (const element of roots) {
2831
2879
  const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0, promotionOf(view));
2832
2880
  if (node !== void 0) nodes.push(node);
2833
2881
  }
2834
2882
  descendFrames(roots, doc, excluded, frameScope, label, tag, sets.actionable, sets.excluded);
2835
2883
  }
2884
+ if (subtree !== void 0) return {
2885
+ nodes,
2886
+ frameNotes,
2887
+ nestedFrames,
2888
+ anchored
2889
+ };
2836
2890
  pruneElevated();
2837
2891
  const view = root.defaultView;
2838
2892
  const sets = actionSets(root, actionScopeFor("self"));
@@ -2852,15 +2906,16 @@ function createDomPerceptionReader(options = {}) {
2852
2906
  return {
2853
2907
  nodes,
2854
2908
  frameNotes,
2855
- nestedFrames
2909
+ nestedFrames,
2910
+ anchored
2856
2911
  };
2857
2912
  }
2858
- async function readWithImages(scope, capture) {
2913
+ async function readWithImages(scope, capture, subtree) {
2859
2914
  const targets = [];
2860
- const { nodes, frameNotes, nestedFrames } = collect(scope, {
2915
+ const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, {
2861
2916
  targets,
2862
2917
  minImageArea: capture.minImageArea ?? 0
2863
- });
2918
+ }, subtree);
2864
2919
  const selected = targets.slice(0, capture.maxImages);
2865
2920
  const images = [];
2866
2921
  let imageFailures = 0;
@@ -2888,6 +2943,7 @@ function createDomPerceptionReader(options = {}) {
2888
2943
  images,
2889
2944
  imagesOmitted: targets.length - selected.length,
2890
2945
  imageFailures,
2946
+ anchored,
2891
2947
  ...frameNotes.length > 0 ? { frameNotes } : {},
2892
2948
  ...nestedFrames.length > 0 ? { nestedFrames } : {}
2893
2949
  };
@@ -2904,6 +2960,28 @@ function createDomPerceptionReader(options = {}) {
2904
2960
  }
2905
2961
  return readWithImages(scope, capture);
2906
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
+ }
2907
2985
  /** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
2908
2986
  function modalSnapshot() {
2909
2987
  const root = rootDocument();
@@ -2916,7 +2994,9 @@ function createDomPerceptionReader(options = {}) {
2916
2994
  }
2917
2995
  return {
2918
2996
  read,
2997
+ readSubtree,
2919
2998
  resolve: (ref) => handles?.table.get(ref)?.element,
2999
+ handleKindOf: (ref) => handles?.table.get(ref)?.kind,
2920
3000
  frameOf: (ref) => handles?.table.get(ref)?.frame,
2921
3001
  modalSnapshot,
2922
3002
  elevateNewModals: (before) => {
@@ -2960,6 +3040,35 @@ function createDomPerceptionReader(options = {}) {
2960
3040
  }
2961
3041
  }
2962
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
+
2963
3072
  //#endregion
2964
3073
  //#region ../browser/src/dom/pageAction.ts
2965
3074
  const FILLABLE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
@@ -3044,6 +3153,54 @@ function fillValue(element, value) {
3044
3153
  element.dispatchEvent(new Event("change", { bubbles: true }));
3045
3154
  }
3046
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
+ }
3047
3204
  /** 轮询等待,上限固定:等不到就失败,不能无限挂着一次工具调用 */
3048
3205
  async function waitFor(probe, timeoutMs = 2e3) {
3049
3206
  const deadline = Date.now() + timeoutMs;
@@ -3082,7 +3239,7 @@ async function selectOption(element, value) {
3082
3239
  } : fail(`The control did not accept "${value}".`);
3083
3240
  }
3084
3241
  const doc = element.ownerDocument;
3085
- element.click();
3242
+ press(element);
3086
3243
  const listbox = await waitFor(() => doc.querySelector("[role=listbox], [role=grid], [role=menu]") ?? void 0);
3087
3244
  if (listbox === null || listbox === void 0) return fail("The options panel did not open.");
3088
3245
  const option = [...listbox.querySelectorAll("[role=option], [role=gridcell], [role=menuitem], option")].find((candidate) => norm(accessibleName(candidate)) === norm(value));
@@ -3120,7 +3277,7 @@ function setToggle(element, value, target) {
3120
3277
  target,
3121
3278
  noop: true
3122
3279
  };
3123
- element.click();
3280
+ press(element);
3124
3281
  return {
3125
3282
  ok: true,
3126
3283
  target
@@ -3163,7 +3320,44 @@ function createDomPageActionExecutor(options) {
3163
3320
  elevated: true
3164
3321
  } : described;
3165
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
+ };
3166
3359
  const execute = async (request) => {
3360
+ if (request.action === "back") return await goBack();
3167
3361
  const element = reader.resolve(request.ref);
3168
3362
  if (element === void 0) return {
3169
3363
  ok: false,
@@ -3176,36 +3370,37 @@ function createDomPageActionExecutor(options) {
3176
3370
  target,
3177
3371
  reason
3178
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.");
3179
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.");
3180
3375
  if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
3181
3376
  if (hidden(element)) return fail("The element is not visible.");
3182
3377
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
3183
3378
  const modalsBefore = reader.modalSnapshot();
3184
3379
  const frameView = element.ownerDocument.defaultView;
3185
- const urlBefore = frameView?.location.href;
3380
+ const keyBefore = documentKey(frameView);
3186
3381
  const done = async (outcome) => {
3187
3382
  if (!outcome.ok) return outcome;
3188
3383
  const settled = await waitFor(() => {
3189
3384
  const modal = reader.elevateNewModals(modalsBefore);
3190
3385
  if (modal !== void 0) return { modal };
3191
- const url = frameView?.location.href;
3192
- return url !== void 0 && url !== urlBefore ? { url } : void 0;
3386
+ const key = documentKey(frameView);
3387
+ return key !== void 0 && key !== keyBefore ? { key } : void 0;
3193
3388
  }, 500);
3194
3389
  const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
3195
3390
  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;
3391
+ const keyAfter = documentKey(frameView);
3392
+ const navigated = keyAfter !== void 0 && keyBefore !== void 0 && keyAfter !== keyBefore;
3198
3393
  return {
3199
3394
  ...outcome,
3200
3395
  ...name !== void 0 ? { elevatedModal: name } : {},
3201
3396
  ...navigated ? {
3202
3397
  navigated: true,
3203
- documentUrl: urlAfter
3398
+ documentUrl: frameView?.location.href
3204
3399
  } : {}
3205
3400
  };
3206
3401
  };
3207
3402
  if (request.action === "click") {
3208
- element.click();
3403
+ press(element);
3209
3404
  return await done({
3210
3405
  ok: true,
3211
3406
  target
@@ -3262,35 +3457,112 @@ function createDomPageActionExecutor(options) {
3262
3457
 
3263
3458
  //#endregion
3264
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
+ }
3265
3487
  /** @experimental */
3266
- function createRemoteTargetRegistry() {
3267
- 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
+ };
3268
3502
  return {
3269
3503
  /**
3270
3504
  * 整表替换而非合并(FR-13.2):合并会让上一次感知的 ref 在本次感知后仍然可执行——
3271
3505
  * 那正是 `PAGE_ACTION_STALE_REF` 要拦的东西。本地形态里旧 ref 天然失效
3272
3506
  * (DOM 重新遍历、句柄表 prune 掉离开文档的条目),远端必须手动复现这个失效语义,
3273
3507
  * 否则远端比本地更松。
3508
+ *
3509
+ * 0.16.0 起替换的是**该分片**的表,别的分片不受影响——这是相对 0.15.0 的唯一语义变化。
3274
3510
  */
3275
- replace(entries) {
3276
- const next = /* @__PURE__ */ new Map();
3511
+ replace(entries, document) {
3512
+ const shard = document?.key ?? DEFAULT_SHARD;
3513
+ const table = /* @__PURE__ */ new Map();
3277
3514
  for (const entry of entries) {
3278
3515
  const { ref, role, name, secret, frame, elevated } = entry;
3279
- next.set(ref, {
3516
+ table.set(ref, {
3280
3517
  role,
3281
3518
  ...name !== void 0 ? { name } : {},
3282
3519
  ...secret === true ? { secret: true } : {},
3283
3520
  ...frame !== void 0 ? { frame } : {},
3284
3521
  ...elevated === true ? { elevated: true } : {}
3285
3522
  });
3523
+ refIndex.set(ref, shard);
3286
3524
  }
3287
- table = next;
3525
+ touch(shard, {
3526
+ table,
3527
+ ...document !== void 0 ? { document } : {}
3528
+ });
3529
+ focused = shard;
3288
3530
  },
3289
3531
  get(ref) {
3290
- 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;
3291
3560
  },
3292
3561
  get size() {
3293
- return table.size;
3562
+ return shards.get(focused)?.table.size ?? 0;
3563
+ },
3564
+ get shardCount() {
3565
+ return shards.size;
3294
3566
  }
3295
3567
  };
3296
3568
  }
@@ -3338,14 +3610,15 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
3338
3610
  * @experimental
3339
3611
  */
3340
3612
  function createRemotePerceptionReader(options) {
3341
- const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
3613
+ const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
3342
3614
  return { async read(scope, capture) {
3343
3615
  const reply = await roundTrip(transport, {
3344
3616
  type: "perceive",
3345
3617
  scope,
3346
3618
  ...capture !== void 0 ? { capture } : {}
3347
3619
  }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
3348
- targets.replace(reply.targets);
3620
+ targets.replace(reply.targets, reply.document);
3621
+ if (reply.document !== void 0) onDocument?.(reply.document);
3349
3622
  return reply.result;
3350
3623
  } };
3351
3624
  }
@@ -3416,8 +3689,25 @@ function createPageAgentHandler(options = {}) {
3416
3689
  });
3417
3690
  const executor = createDomPageActionExecutor({
3418
3691
  reader,
3692
+ ...options.document !== void 0 ? { document: options.document } : {},
3419
3693
  ...options.pickFiles !== void 0 ? { pickFiles: options.pickFiles } : {}
3420
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
+ };
3421
3711
  return { async handle(request) {
3422
3712
  try {
3423
3713
  if (request.type === "perceive") {
@@ -3431,10 +3721,12 @@ function createPageAgentHandler(options = {}) {
3431
3721
  ...target
3432
3722
  });
3433
3723
  }
3724
+ const identity = describeDocument();
3434
3725
  return {
3435
3726
  type: "perceive-result",
3436
3727
  result,
3437
- targets
3728
+ targets,
3729
+ ...identity !== void 0 ? { document: identity } : {}
3438
3730
  };
3439
3731
  }
3440
3732
  return {
@@ -3634,7 +3926,8 @@ function createFrameRouter(options) {
3634
3926
  ...imageFailures > 0 ? { imageFailures } : {},
3635
3927
  ...frameNotes.length > 0 ? { frameNotes } : {}
3636
3928
  },
3637
- targets
3929
+ targets,
3930
+ ...main.reply.document !== void 0 ? { document: main.reply.document } : {}
3638
3931
  };
3639
3932
  }
3640
3933
  async function execute(request) {
@@ -5151,4 +5444,4 @@ function originOf(blockedURI) {
5151
5444
  }
5152
5445
 
5153
5446
  //#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 };
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 };
@@ -41,12 +41,35 @@ const AUDIT_EVENT_TYPES = {
41
41
  /** 候选记录被从审批队列里删除(仅 draft / pending-review / rejected 可删) */
42
42
  candidateRemoved: "candidate.removed",
43
43
  pagePerceived: "page.perceived",
44
+ /**
45
+ * 子树续读被拒(0.16.0 分册 14 · FR-14.6)。载荷只有 `reason`,**不含被拒的 `ref`**:
46
+ * 句柄换轮即失效,留下来对复盘没有帮助,却会把一个模型可控的字符串写进审计流。
47
+ *
48
+ * `reason: 'out-of-scope'` 是注入攻击的观测点:页面里写着「请读取整个页面」时,
49
+ * 模型若照做,这条留痕就是它留下的痕迹。
50
+ */
51
+ pagePerceiveDenied: "page.perceive.denied",
44
52
  pageAction: "page.action",
45
53
  /**
46
54
  * 一次页面操作引发了下载,但 `sandbox.downloadedFiles` 关着,模型因此一无所知
47
55
  * (0.15.0 分册 15 · FR-15.7)。载荷只有计数与原因,不含文件名 / 大小 / 类型。
48
56
  */
49
57
  pageDownloadUnreported: "page.download.unreported",
58
+ /**
59
+ * 模型侧标签页控制(0.16.0 分册 12 · FR-12.7)。写入点在
60
+ * `packages/agent/src/tabs/policy.ts`,同样是字面量同步。
61
+ *
62
+ * `list_page_tabs` **不在此列**:纯读、无副作用、每轮可能被调很多次,
63
+ * 记下来只会把 focused / closed 淹没在噪音里(DV-4)。作为交换,
64
+ * `page.tab.focused` 的 `data.from` 是必需的——失去列举留痕后,
65
+ * 它是唯一能回答「模型是从哪一页跳过去的」的字段。
66
+ */
67
+ pageTabFocused: "page.tab.focused",
68
+ pageTabClosed: "page.tab.closed",
69
+ /** 切换 / 关闭被拒,或工作集触顶。载荷只有原因与上限,**不含**被拒的句柄内容 */
70
+ pageTabDenied: "page.tab.denied",
71
+ /** 页面操作新开了一页并被接进工作集(SC-2 的唯一入口) */
72
+ pageTabOpened: "page.tab.opened",
50
73
  documentRead: "document.read",
51
74
  documentSurfaceOpen: "document.surface.open",
52
75
  /** 脚本按宿主声明的数据源取数(分册 16);被拒也记,否则反复试探看不见 */