@webskill/sdk 0.22.0 → 0.23.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
@@ -1,14 +1,14 @@
1
1
  import { n as messageOf$1, 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";
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-WSUDp6A1.js";
3
+ import { o as atomicWriteText, r as resolveInsideRoot, s as isAtomicTempPath } from "./pathSecurity-CirkQwWP.js";
4
4
  import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
5
5
  import { t as ATTACHMENT_TEXT_LIMIT } from "./kind-Dc8x0HWz.js";
6
- import { _ as FsRunSnapshotStore, a as networkPolicyLibSource, c as bridgeError, ct as ProgressiveRouter, d as FsMemoryStore, l as parseBridgeRequest, m as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-DWQlDPbY.js";
6
+ import { _ as FsRunSnapshotStore, a as networkPolicyLibSource, c as bridgeError, ct as ProgressiveRouter, d as FsMemoryStore, l as parseBridgeRequest, m as AgentLoop, r as normalizeToolError, s as UPLOAD_FILES_UNAVAILABLE, t as CapabilityApproval, u as FsArtifactStore } from "./approval-7CuZS3Z4.js";
7
7
  import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-eIQNO9tr.js";
8
- import { c as toBase64$1, f as SPREADSHEET_MIME_TYPE } from "./linkedDocument-CIuh_Yp0.js";
8
+ import { c as toBase64$1, f as SPREADSHEET_MIME_TYPE } from "./linkedDocument-CZZOl9cO.js";
9
9
  import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
10
- import { t as createWebSkillApi } from "./webSkillApi-Dy-Zjv0y.js";
11
- import { D as toFrameScopes, E as toActionFrameScopes, O as frameLabel, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, f as readImageSize, k as frameSteps, l as parseCellAddress, n as EMU_PER_POINT, o as fitWithin, p as PDF_LARGE_IMAGE_RATIO, r as EMU_PER_TWIP$1, s as formatCellAddress, t as EMU_PER_PIXEL, u as parseCellRange, v as takesImage } from "./geometry-wx5ZTYzi.js";
10
+ import { t as createWebSkillApi } from "./webSkillApi-D5VMUnrv.js";
11
+ import { A as frameSteps, D as SCROLL_DIRECTIONS, E as toActionFrameScopes, O as toFrameScopes, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, f as readImageSize, k as frameLabel, l as parseCellAddress, n as EMU_PER_POINT, o as fitWithin, p as PDF_LARGE_IMAGE_RATIO, r as EMU_PER_TWIP$1, s as formatCellAddress, t as EMU_PER_PIXEL, u as parseCellRange, v as takesImage } from "./geometry-CRhuySKV.js";
12
12
  import { zipSync } from "fflate";
13
13
 
14
14
  //#region ../browser/src/fs/featureDetection.ts
@@ -2634,24 +2634,56 @@ const ROUTE_ATTRIBUTES = [
2634
2634
  * 选项往往就是不带 role 的 `<li>`(0.17.0 分册 11)。它与另外三个一样受
2635
2635
  * 「有可操作后代就不提升」约束,所以真正的语义列表不会被误伤。
2636
2636
  *
2637
- * 这张白名单**只约束启发式信号**。宿主探针点名的元素走另一条路(分册 12)。
2637
+ * 判据是「条目 / 单元」对「容器」(0.23.0 FR-11.1):一格、一项、一栏能承载一次点击,
2638
+ * `grid` / `menu` / `tablist` 这类容器不能,所以它们不在这里。
2639
+ *
2640
+ * 0.23.0 补齐了显式 ARIA 写法:过去 `<td>`(隐式 `cell`)能被提升,而
2641
+ * `<td role="gridcell">` 不能——把无障碍角色写对反而被降级。自绘日历、菜单、
2642
+ * 下拉普遍写显式角色,于是整片点不到(0.23.0 分册 11)。
2643
+ *
2644
+ * 这张白名单**只约束启发式信号**,放宽的是「够不够格被考察」,不是「够不够格被提升」:
2645
+ * 后面三道闸门(自身可操作 / 有可操作后代 / 看起来可交互)一个都没松。
2646
+ * 宿主探针点名的元素走另一条路(分册 12)。
2638
2647
  */
2639
2648
  const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
2640
2649
  "generic",
2641
2650
  "img",
2642
2651
  "cell",
2643
- "listitem"
2652
+ "listitem",
2653
+ "columnheader",
2654
+ "gridcell",
2655
+ "rowheader",
2656
+ "option",
2657
+ "treeitem",
2658
+ "tab",
2659
+ "menuitem",
2660
+ "menuitemcheckbox",
2661
+ "menuitemradio"
2644
2662
  ]);
2645
2663
  /** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
2646
2664
  const SYNTHETIC_NAME_PREFIX = "unlabeled control";
2647
2665
  /** 往上找具名祖先的层数上限:再往上就是整页容器,拿它当限定语反而更含糊 */
2648
2666
  const SYNTHETIC_NAME_LOOKUP_DEPTH = 5;
2649
2667
  const classAndId = (element) => `${element.getAttribute("class") ?? ""} ${element.getAttribute("id") ?? ""}`;
2650
- /** `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后 */
2668
+ /**
2669
+ * `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后。
2670
+ *
2671
+ * 只认元素**自身引入**的那一次声明:`cursor` 是 CSS 继承属性,一句
2672
+ * `.card { cursor: pointer }` 会让整张卡片里的每段文字计算值都是 `pointer`。
2673
+ * 实测某差旅站点首页 1537 个可见元素里 **1050 个**仅凭继承来的 `cursor`
2674
+ * 就被判成可交互,感知里因此冒出 826 个「按钮」(0.23.0 FR-16.1)。
2675
+ *
2676
+ * 判据是与父元素的计算值**有没有差异**:没有差异就当作继承来的。
2677
+ * 父子都显式声明 `pointer` 的情形分辨不了(没有任何 DOM API 能做到),
2678
+ * 那类元素由其余四级信号兜底,见 0.23.0 设计 17 §3。
2679
+ */
2651
2680
  function looksClickable(element, view) {
2652
2681
  if (view === null) return false;
2653
2682
  try {
2654
- return view.getComputedStyle(element).cursor === "pointer";
2683
+ if (view.getComputedStyle(element).cursor !== "pointer") return false;
2684
+ const parent = element.parentElement;
2685
+ if (parent === null) return true;
2686
+ return view.getComputedStyle(parent).cursor !== "pointer";
2655
2687
  } catch {
2656
2688
  return false;
2657
2689
  }
@@ -2901,7 +2933,9 @@ function nameOf(element, doc) {
2901
2933
  }
2902
2934
  if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
2903
2935
  const own = ownText(element);
2904
- return own === "" ? void 0 : own;
2936
+ if (own !== "") return own;
2937
+ const title = element.getAttribute("title");
2938
+ return title !== null && title.trim() !== "" ? clip(title) : void 0;
2905
2939
  }
2906
2940
  function valueOf(element) {
2907
2941
  if (element.tagName === "INPUT") {
@@ -2934,6 +2968,35 @@ function hidden$1(element, view) {
2934
2968
  }
2935
2969
  return false;
2936
2970
  }
2971
+ /**
2972
+ * 页面是否有真实布局。jsdom 有 `getComputedStyle` 却没有布局引擎,那里所有元素的
2973
+ * rect 都是 0×0,把零尺寸判定套上去会让**全部**控件失去句柄(0.23.0 FR-10.4)。
2974
+ *
2975
+ * 判据与被判据用**同一种测量手段**:拿根元素的 rect 为下面的 rect 背书。
2976
+ * 真实浏览器里根元素必有尺寸,jsdom 里恒为 0×0。
2977
+ *
2978
+ * 不用 `clientHeight`,更不用 `view.innerHeight`:
2979
+ * 前者与 rect 是两套独立机制,测试桩(以及 `overflow` 的各种写法)可以只动其中一套,
2980
+ * 用 A 去为 B 背书会得出「有布局」却量不到尺寸的矛盾结论;
2981
+ * 后者在 jsdom 里被编了 1024×768,两边根本看不出区别。
2982
+ */
2983
+ function hasLayout(doc) {
2984
+ const rect = doc.documentElement.getBoundingClientRect();
2985
+ return rect.width > 0 || rect.height > 0;
2986
+ }
2987
+ /**
2988
+ * 元素在屏幕上不占任何像素(0.23.0 FR-10.1)。人的鼠标点不到它,
2989
+ * 用户也无从在逐次确认卡上核对它——`act_on_page` 的安全模型建立在「用户看得懂自己批准了什么」上。
2990
+ *
2991
+ * 不复用 `measuredSizeOf()`:那个函数在 rect 为 0 时会回退去读 naturalWidth /
2992
+ * canvas 属性 / viewBox,为的是「测不到尺寸就别过滤图片」。这里要的恰恰相反:
2993
+ * 必须分得清「确实量到了 0」和「没有布局」,一档都不能回退。
2994
+ */
2995
+ function occupiesNoSpace(element) {
2996
+ if (!hasLayout(element.ownerDocument)) return false;
2997
+ const rect = element.getBoundingClientRect();
2998
+ return rect.width === 0 || rect.height === 0;
2999
+ }
2937
3000
  /** script / style 的正文是代码,不是页面内容 */
2938
3001
  const SKIPPED_TAGS = /* @__PURE__ */ new Set([
2939
3002
  "SCRIPT",
@@ -2943,6 +3006,33 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
2943
3006
  ]);
2944
3007
  /** 活动模态的三种形态;Radix 之流用 aria-modal,原生用 <dialog open> */
2945
3008
  const MODAL_SELECTOR = "[role=dialog][open], dialog[open], [aria-modal=true]";
3009
+ const scrollsOverflow = (overflow) => overflow === "auto" || overflow === "scroll" || overflow === "overlay";
3010
+ /**
3011
+ * 该元素**自身**还能往哪些方向滚(0.23.0 分册 22 FR-22.1)。
3012
+ *
3013
+ * 先读布局属性再查计算样式,顺序不能倒:实测某 ERP 页 2488 个元素里只有 18 个
3014
+ * 在第一道过筛(1.4ms),对这 18 个查 overflow 只要 0.1ms;对全部 2488 个查要 5.8ms。
3015
+ * 这与 `looksClickable` 把 `getComputedStyle` 殿后是同一条原则。
3016
+ *
3017
+ * 只判元素自身、不上溯,标示因此只落在容器节点上(FR-22.3)。
3018
+ * jsdom 没有布局,这些尺寸恒为 0,筛子自然不命中。
3019
+ */
3020
+ function scrollableDirectionsOf(element, view) {
3021
+ const overflowsX = element.scrollWidth > element.clientWidth;
3022
+ const overflowsY = element.scrollHeight > element.clientHeight;
3023
+ if (!overflowsX && !overflowsY) return [];
3024
+ const style = view.getComputedStyle(element);
3025
+ const out = [];
3026
+ if (overflowsX && scrollsOverflow(style.overflowX)) {
3027
+ if (element.scrollLeft > 0) out.push("left");
3028
+ if (element.scrollLeft < element.scrollWidth - element.clientWidth - 1) out.push("right");
3029
+ }
3030
+ if (overflowsY && scrollsOverflow(style.overflowY)) {
3031
+ if (element.scrollTop > 0) out.push("up");
3032
+ if (element.scrollTop < element.scrollHeight - element.clientHeight - 1) out.push("down");
3033
+ }
3034
+ return out;
3035
+ }
2946
3036
  /** 按尺寸落选取像的说明(FR-20.3);与「超名额」「取像失败」必须可区分,三者排查方向不同 */
2947
3037
  const ICON_SKIPPED_NOTE = "Not captured: this image is smaller than the icon threshold";
2948
3038
  /**
@@ -2968,6 +3058,8 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2968
3058
  const value = valueOf(element);
2969
3059
  let role = roleOf$1(element);
2970
3060
  let promotedHref;
3061
+ let roleBeforePromotion;
3062
+ let nameWasSynthesized = false;
2971
3063
  if (promotion !== void 0) {
2972
3064
  const promoted = promoteRole({
2973
3065
  element,
@@ -2978,14 +3070,37 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2978
3070
  context: promotion
2979
3071
  });
2980
3072
  if (promoted !== void 0) {
3073
+ roleBeforePromotion = role;
2981
3074
  role = promoted.role;
2982
3075
  promotedHref = promoted.href;
2983
- if (name === void 0) name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
3076
+ if (name === void 0) {
3077
+ name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
3078
+ nameWasSynthesized = true;
3079
+ }
2984
3080
  }
2985
3081
  }
3082
+ let ref = handles?.issue(element, role, frame);
3083
+ if (roleBeforePromotion !== void 0 && ref === void 0) {
3084
+ role = roleBeforePromotion;
3085
+ promotedHref = void 0;
3086
+ if (nameWasSynthesized) name = void 0;
3087
+ ref = handles?.issue(element, role, frame);
3088
+ }
2986
3089
  const href = promotedHref ?? (role === "link" ? hrefOf(element) : void 0);
2987
- if (role === "generic" && name === void 0 && value === void 0 && children.length === 0) return void 0;
2988
- if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0) {
3090
+ let scrollable;
3091
+ let scrollRef;
3092
+ if (view !== null) {
3093
+ const directions = scrollableDirectionsOf(element, view);
3094
+ if (directions.length > 0) {
3095
+ const issued = ref ?? handles?.issueScrollable(element, frame);
3096
+ if (issued !== void 0) {
3097
+ scrollable = directions;
3098
+ scrollRef = issued;
3099
+ }
3100
+ }
3101
+ }
3102
+ if (role === "generic" && name === void 0 && value === void 0 && children.length === 0 && scrollable === void 0) return;
3103
+ if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0 && scrollable === void 0) {
2989
3104
  if (children.length === 1) return children[0];
2990
3105
  }
2991
3106
  const node = {
@@ -2997,8 +3112,11 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2997
3112
  ...provenance !== void 0 ? { provenance } : {},
2998
3113
  ...children.length > 0 ? { children } : {}
2999
3114
  };
3000
- const ref = handles?.issue(element, role, frame);
3001
3115
  if (ref !== void 0) node.ref = ref;
3116
+ if (scrollable !== void 0) {
3117
+ node.ref = scrollRef;
3118
+ node.scrollable = scrollable;
3119
+ }
3002
3120
  if (isCapturableElement(element)) {
3003
3121
  const captureRef = node.ref ?? handles?.issueCapture(element, frame);
3004
3122
  if (captureRef !== void 0) {
@@ -3116,6 +3234,7 @@ var HandleIssuer = class {
3116
3234
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
3117
3235
  const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
3118
3236
  if (kind === void 0) return void 0;
3237
+ if (kind === "action" && occupiesNoSpace(element)) return void 0;
3119
3238
  const known = this.#refByElement.get(element);
3120
3239
  if (known !== void 0 && this.#table.has(known)) {
3121
3240
  this.#table.set(known, {
@@ -3139,6 +3258,19 @@ var HandleIssuer = class {
3139
3258
  if (known !== void 0 && this.#table.has(known)) return known;
3140
3259
  return this.#mint(element, "image", frame);
3141
3260
  }
3261
+ /**
3262
+ * 可滚容器的句柄(0.23.0 分册 22)。角色白名单之外——表格容器往往是个 `generic` div,
3263
+ * 但 `scroll` 确实作用在它身上。不发就成了「标了能滚、模型指认不了」,正是 AC-G8 禁的那种幽灵能力。
3264
+ *
3265
+ * 仍过操作范围与零尺寸两道闸(FR-22.12);已有句柄就原样复用且**不改 kind**。
3266
+ */
3267
+ issueScrollable(element, frame) {
3268
+ if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
3269
+ if (occupiesNoSpace(element)) return void 0;
3270
+ const known = this.#refByElement.get(element);
3271
+ if (known !== void 0 && this.#table.has(known)) return known;
3272
+ return this.#mint(element, "action", frame);
3273
+ }
3142
3274
  #mint(element, kind, frame) {
3143
3275
  const bytes = /* @__PURE__ */ new Uint8Array(8);
3144
3276
  crypto.getRandomValues(bytes);
@@ -3612,6 +3744,39 @@ function hidden(element) {
3612
3744
  }
3613
3745
  return false;
3614
3746
  }
3747
+ /**
3748
+ * 「开着的浮层」里必须有可操作的东西。空的定位容器(装饰角标、测量层、定位锚点)
3749
+ * 满页都是,只看定位会把它们全当成浮层——实测该页面误伤 337/400(0.23.0 设计 16 §3.1)。
3750
+ */
3751
+ const POPOVER_CONTENT_SELECTOR = "a[href],button,input,select,textarea,[role=\"button\"],[role=\"link\"],[role=\"option\"],[role=\"menuitem\"],[role=\"gridcell\"],[role=\"tab\"],[role=\"checkbox\"],[role=\"radio\"]";
3752
+ /**
3753
+ * 这个元素旁边是不是已经有一个开着的浮层(0.23.0 FR-15.1)。
3754
+ *
3755
+ * 零 DOM 变更有两种成因:元素真的不响应,和**页面已经处于这个动作要达成的状态**。
3756
+ * 后者按分册 12 的原判据会被报成 `noop`,模型于是放弃一条已经走通的路。
3757
+ *
3758
+ * 只看**父元素这一层**:自绘下拉的写法是触发器与浮层同属一个包装容器,
3759
+ * 往上多走一层就开始把无关区块算进来(误伤 3/400 → 31/400)。
3760
+ *
3761
+ * 用 `hidden()` 而不是尺寸:jsdom 没有布局,`getBoundingClientRect()` 恒为 0,
3762
+ * 依赖尺寸这条判据就没法被测试守住(设计 16 §3.2)。
3763
+ */
3764
+ function popoverOpenBeside(element) {
3765
+ if (element.getAttribute("aria-expanded") === "true") return true;
3766
+ const parent = element.parentElement;
3767
+ if (parent === null) return false;
3768
+ const view = element.ownerDocument.defaultView;
3769
+ if (view === null) return false;
3770
+ for (const candidate of parent.querySelectorAll("*")) {
3771
+ if (candidate === element || element.contains(candidate) || candidate.contains(element)) continue;
3772
+ const position = view.getComputedStyle(candidate).position;
3773
+ if (position !== "absolute" && position !== "fixed") continue;
3774
+ if (hidden(candidate)) continue;
3775
+ if (candidate.querySelector(POPOVER_CONTENT_SELECTOR) === null) continue;
3776
+ return true;
3777
+ }
3778
+ return false;
3779
+ }
3615
3780
  const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
3616
3781
  const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
3617
3782
  const textOf$2 = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
@@ -4028,6 +4193,12 @@ function attachFiles(input, files) {
4028
4193
  input.dispatchEvent(new Event("change", { bubbles: true }));
4029
4194
  }
4030
4195
  const scrolls = (overflow) => overflow === "auto" || overflow === "scroll" || overflow === "overlay";
4196
+ const axisOf = (direction) => direction === "left" || direction === "right" ? "x" : "y";
4197
+ /** 该元素自身在这条轴上能不能滚。两个条件的合取,见 `scrollTargetOf` 的注释 */
4198
+ function scrollsOn(node, axis, view) {
4199
+ const style = view.getComputedStyle(node);
4200
+ return axis === "y" ? scrolls(style.overflowY) && node.scrollHeight > node.clientHeight : scrolls(style.overflowX) && node.scrollWidth > node.clientWidth;
4201
+ }
4031
4202
  /**
4032
4203
  * 滚动落点(FR-10.3):从被指认元素**自身**起沿祖先链上溯,取第一个真正能滚的容器。
4033
4204
  *
@@ -4041,20 +4212,21 @@ const scrolls = (overflow) => overflow === "auto" || overflow === "scroll" || ov
4041
4212
  * 根滚动容器单独兜底而不是靠链上命中:`documentElement` 的 `overflow-y` 计算值
4042
4213
  * 通常是 `visible`(滚动被传播到视口),不会被上面的判据命中,但文档确实可滚。
4043
4214
  * 顺序必须是先链后兜底——嵌套场景下先命中的必须是内层列表。
4215
+ *
4216
+ * **按轴判(0.23.0 分册 22 FR-22.7)**:同一个容器常常只在一条轴上溢出,
4217
+ * 拿纵向判据去找横向落点会整体判成不可滚,那正是横向滚动此前失败的原因。
4044
4218
  */
4045
- function scrollTargetOf(element) {
4219
+ function scrollTargetOf(element, axis) {
4046
4220
  const doc = element.ownerDocument;
4047
4221
  const view = doc.defaultView;
4048
4222
  if (view === null) return void 0;
4049
- for (let node = element; node !== null; node = node.parentElement) {
4050
- const style = view.getComputedStyle(node);
4051
- if (scrolls(style.overflowY) && node.scrollHeight > node.clientHeight) return {
4052
- container: node,
4053
- isRoot: false
4054
- };
4055
- }
4223
+ for (let node = element; node !== null; node = node.parentElement) if (scrollsOn(node, axis, view)) return {
4224
+ container: node,
4225
+ isRoot: false
4226
+ };
4056
4227
  const root = doc.scrollingElement ?? doc.documentElement;
4057
- if (root !== null && root.scrollHeight > root.clientHeight) return {
4228
+ const rootOverflows = root !== null && (axis === "y" ? root.scrollHeight > root.clientHeight : root.scrollWidth > root.clientWidth);
4229
+ if (root !== null && rootOverflows) return {
4058
4230
  container: root,
4059
4231
  isRoot: true
4060
4232
  };
@@ -4159,11 +4331,12 @@ function createDomPageActionExecutor(options) {
4159
4331
  * 稳定化是滚动自己的收尾,见 `watchQuiescence`。
4160
4332
  */
4161
4333
  const scrollRegion = async (element, target, direction) => {
4162
- const found = scrollTargetOf(element);
4334
+ const axis = axisOf(direction);
4335
+ const found = scrollTargetOf(element, axis);
4163
4336
  if (found === void 0) return {
4164
4337
  ok: false,
4165
4338
  target,
4166
- reason: "This area cannot be scrolled: nothing around this element has a scrollable region."
4339
+ reason: `This area cannot be scrolled ${direction}: nothing around this element scrolls ${axis === "y" ? "vertically" : "horizontally"}.`
4167
4340
  };
4168
4341
  const { container, isRoot } = found;
4169
4342
  if (!isRoot && !reader.inActionScope(container)) return {
@@ -4171,17 +4344,22 @@ function createDomPageActionExecutor(options) {
4171
4344
  target,
4172
4345
  reason: "The scrollable region around this element is outside the actionable scope."
4173
4346
  };
4174
- const step = Math.max(container.clientHeight - SCROLL_OVERLAP_PX, Math.round(container.clientHeight / 2), 1);
4175
- const before = container.scrollTop;
4347
+ const extent = axis === "y" ? container.clientHeight : container.clientWidth;
4348
+ const step = Math.max(extent - SCROLL_OVERLAP_PX, Math.round(extent / 2), 1);
4349
+ const forward = direction === "down" || direction === "right";
4350
+ const before = axis === "y" ? container.scrollTop : container.scrollLeft;
4176
4351
  const watch = watchQuiescence(container);
4177
- container.scrollTop = direction === "down" ? before + step : before - step;
4352
+ const next = forward ? before + step : before - step;
4353
+ if (axis === "y") container.scrollTop = next;
4354
+ else container.scrollLeft = next;
4178
4355
  await watch.settle(clampScrollSettleMs(options.scrollSettleMs?.()));
4179
- const after = container.scrollTop;
4356
+ const after = axis === "y" ? container.scrollTop : container.scrollLeft;
4357
+ const limit = axis === "y" ? container.scrollHeight - container.clientHeight - 1 : container.scrollWidth - container.clientWidth - 1;
4180
4358
  return {
4181
4359
  ok: true,
4182
4360
  target,
4183
4361
  scrolled: {
4184
- atEnd: direction === "down" ? after >= container.scrollHeight - container.clientHeight - 1 : after <= 0,
4362
+ atEnd: forward ? after >= limit : after <= 0,
4185
4363
  movedBy: Math.abs(after - before)
4186
4364
  }
4187
4365
  };
@@ -4205,6 +4383,7 @@ function createDomPageActionExecutor(options) {
4205
4383
  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.");
4206
4384
  if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
4207
4385
  if (hidden(element)) return fail("The element is not visible.");
4386
+ if (occupiesNoSpace(element)) return fail("The element takes up no space on screen, so it cannot be clicked or pointed at.");
4208
4387
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
4209
4388
  const modalsBefore = reader.modalSnapshot();
4210
4389
  const frameView = element.ownerDocument.defaultView;
@@ -4231,15 +4410,22 @@ function createDomPageActionExecutor(options) {
4231
4410
  };
4232
4411
  };
4233
4412
  if (request.action === "click") {
4413
+ const watch = watchMutations(element);
4234
4414
  press(element);
4235
- return await done({
4415
+ const outcome = await done({
4236
4416
  ok: true,
4237
4417
  target
4238
4418
  });
4419
+ const changed = watch.changed();
4420
+ watch.stop();
4421
+ return changed || outcome.navigated === true || outcome.elevatedModal !== void 0 || popoverOpenBeside(element) ? outcome : {
4422
+ ...outcome,
4423
+ noop: true
4424
+ };
4239
4425
  }
4240
4426
  if (request.action === "fill") {
4241
4427
  if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
4242
- if (element.hasAttribute("readonly")) return fail("The element is read-only.");
4428
+ if (element.hasAttribute("readonly")) return fail("The element is read-only. Read-only text fields often open a picker when clicked, so try the click action on this same element.");
4243
4429
  fillValue(element, request.value ?? "");
4244
4430
  return await done({
4245
4431
  ok: true,
@@ -4258,7 +4444,7 @@ function createDomPageActionExecutor(options) {
4258
4444
  }
4259
4445
  if (request.action === "set") return await done(setToggle(element, request.value ?? "", target));
4260
4446
  if (request.action === "scroll") {
4261
- if (request.value !== "down" && request.value !== "up") return fail("Use \"down\" or \"up\".");
4447
+ if (!SCROLL_DIRECTIONS.includes(request.value)) return fail(`Use ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
4262
4448
  return await scrollRegion(element, target, request.value);
4263
4449
  }
4264
4450
  if (request.action === "hover") {
@@ -4270,7 +4456,7 @@ function createDomPageActionExecutor(options) {
4270
4456
  return await done({
4271
4457
  ok: true,
4272
4458
  target,
4273
- ...changed ? {} : { noop: true }
4459
+ ...changed || popoverOpenBeside(element) ? {} : { noop: true }
4274
4460
  });
4275
4461
  }
4276
4462
  if (request.action === "drag") {
@@ -4475,16 +4661,71 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
4475
4661
  */
4476
4662
  function createRemotePerceptionReader(options) {
4477
4663
  const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
4478
- return { async read(scope, capture) {
4479
- const reply = await roundTrip(transport, {
4480
- type: "perceive",
4481
- scope,
4482
- ...capture !== void 0 ? { capture } : {}
4483
- }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
4484
- targets.replace(reply.targets, reply.document);
4485
- if (reply.document !== void 0) onDocument?.(reply.document);
4486
- return reply.result;
4487
- } };
4664
+ /**
4665
+ * 上一批写进分片的条目(0.23.0 分册 13 / FR-13.4)。
4666
+ *
4667
+ * `registry.replace` 是**整体替换分片**,而子树读取只回传子树内的条目——
4668
+ * 照搬就等于「读完日期面板,页面别处全部失去句柄」。本地形态没有这个问题:
4669
+ * `HandleIssuer` 的表跨感知保留,子树读取只是少发几个 ref,既有的一个都不丢。
4670
+ * 差值由这里补出来,而不是转嫁给模型。
4671
+ *
4672
+ * 状态留在发起方而不是给 `RemoteTargetRegistry` 加 `merge()`:
4673
+ * 后者对所有实现方都是破坏性变更,需求 §2.2 把它列为禁止项。
4674
+ */
4675
+ let lastShard;
4676
+ const commit = (entries, document, merge) => {
4677
+ const sameDocument = lastShard !== void 0 && lastShard.key === document?.key;
4678
+ let next = entries;
4679
+ if (merge && sameDocument && lastShard !== void 0) {
4680
+ const byRef = /* @__PURE__ */ new Map();
4681
+ for (const entry of lastShard.entries) byRef.set(entry.ref, entry);
4682
+ for (const entry of entries) byRef.set(entry.ref, entry);
4683
+ next = [...byRef.values()];
4684
+ }
4685
+ targets.replace(next, document);
4686
+ lastShard = {
4687
+ key: document?.key,
4688
+ entries: next
4689
+ };
4690
+ if (document !== void 0) onDocument?.(document);
4691
+ };
4692
+ return {
4693
+ async read(scope, capture) {
4694
+ const reply = await roundTrip(transport, {
4695
+ type: "perceive",
4696
+ scope,
4697
+ ...capture !== void 0 ? { capture } : {}
4698
+ }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
4699
+ commit(reply.targets, reply.document, false);
4700
+ return reply.result;
4701
+ },
4702
+ /**
4703
+ * 子树续读(0.23.0 分册 13 / FR-13.1)。
4704
+ *
4705
+ * 本地形态早就有这个方法,缺的只是这一段跨上下文接线;不实现它,
4706
+ * policy 会直接抛 `PERCEPTION_SUBTREE_UNSUPPORTED`,模型在浏览器扩展这类
4707
+ * 宿主里就永远没有「第一段没找到 → 展开某个子树再看」这条自救路径。
4708
+ */
4709
+ async readSubtree(scope, ref, capture) {
4710
+ let reply;
4711
+ try {
4712
+ reply = await roundTrip(transport, {
4713
+ type: "perceive",
4714
+ scope,
4715
+ ref,
4716
+ ...capture !== void 0 ? { capture } : {}
4717
+ }, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
4718
+ } catch (e) {
4719
+ if (e instanceof WebSkillError) {
4720
+ if (e.code === "PERCEPTION_SUBTREE_OUT_OF_SCOPE") return { rejected: "out-of-scope" };
4721
+ if (e.code === "PERCEPTION_SUBTREE_UNKNOWN_REF") return { rejected: "unknown-ref" };
4722
+ }
4723
+ throw e;
4724
+ }
4725
+ commit(reply.targets, reply.document, true);
4726
+ return reply.result;
4727
+ }
4728
+ };
4488
4729
  }
4489
4730
 
4490
4731
  //#endregion
@@ -4553,6 +4794,24 @@ function collectRefs(nodes, out = []) {
4553
4794
  return out;
4554
4795
  }
4555
4796
  /**
4797
+ * 子树被拒 → 既有的 `error` 应答变体(0.23.0 分册 13 / FR-13.5)。
4798
+ * 不新增应答形态,是把本册的契约变更压到「一个可选请求字段」的关键。
4799
+ *
4800
+ * 这两个错误码本来就在 `WebSkillErrorCode` 里,远程 reader 靠它们还原出
4801
+ * `{ rejected }`,policy 那边的 `#recordDenied` 才能照常留痕。
4802
+ */
4803
+ function rejectionReply(rejected) {
4804
+ return rejected === "out-of-scope" ? {
4805
+ type: "error",
4806
+ code: "PERCEPTION_SUBTREE_OUT_OF_SCOPE",
4807
+ message: "that reference is outside the perception scope, so its subtree cannot be read"
4808
+ } : {
4809
+ type: "error",
4810
+ code: "PERCEPTION_SUBTREE_UNKNOWN_REF",
4811
+ message: "that reference is not on this page any more; perceive the page again to get a fresh reference"
4812
+ };
4813
+ }
4814
+ /**
4556
4815
  * 页面侧处理器(分册 13 / FR-13.7)。
4557
4816
  *
4558
4817
  * 必须是工厂而不是裸函数:`DomPerceptionReader` **是有状态的**——
@@ -4596,8 +4855,15 @@ function createPageAgentHandler(options = {}) {
4596
4855
  return { async handle(request) {
4597
4856
  try {
4598
4857
  if (request.type === "perceive") {
4599
- const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
4600
- const result = Array.isArray(raw) ? { nodes: raw } : raw;
4858
+ let result;
4859
+ if (request.ref !== void 0) {
4860
+ const out = request.capture === void 0 ? await reader.readSubtree(request.scope, request.ref) : await reader.readSubtree(request.scope, request.ref, request.capture);
4861
+ if ("rejected" in out) return rejectionReply(out.rejected);
4862
+ result = out;
4863
+ } else {
4864
+ const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
4865
+ result = Array.isArray(raw) ? { nodes: raw } : raw;
4866
+ }
4601
4867
  const targets = [];
4602
4868
  for (const ref of collectRefs(result.nodes)) {
4603
4869
  const target = executor.describe(ref);
@@ -6579,7 +6845,7 @@ function serializeXmlPart(declaration, doc) {
6579
6845
  /** @throws WebSkillError 归档损坏,或不是 OOXML(缺 `[Content_Types].xml`) */
6580
6846
  async function readOoxmlPackage(bytes) {
6581
6847
  const entries = await unzipWithLimits(bytes);
6582
- const pkg = new Map(entries);
6848
+ const pkg = new Map(entries.filter(([path]) => !path.endsWith("/")));
6583
6849
  if (!pkg.has(CONTENT_TYPES)) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `This file is not an Office Open XML document: it has no ${CONTENT_TYPES} entry.`);
6584
6850
  return pkg;
6585
6851
  }
@@ -6686,6 +6952,54 @@ function dropPart(pkg, partName, relsPath) {
6686
6952
  pkg.set(relsPath, new TextEncoder().encode(declaration + stripped));
6687
6953
  }
6688
6954
  }
6955
+ /** VBA 工程部件的 content type 前缀。宏的判据看这个,不看文件路径。 */
6956
+ const VBA_CONTENT_TYPE = "application/vnd.ms-office.vbaProject";
6957
+ /** macroEnabled 的主部件 content type → 对应的非宏值(0.23.0 分册 25 设计 26 §3.1)。 */
6958
+ const NON_MACRO_MAIN_TYPE = /* @__PURE__ */ new Map([
6959
+ ["application/vnd.ms-excel.sheet.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"],
6960
+ ["application/vnd.ms-excel.template.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"],
6961
+ ["application/vnd.ms-word.document.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"],
6962
+ ["application/vnd.ms-word.template.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"]
6963
+ ]);
6964
+ const attributeOf = (tag, name) => new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1] ?? "";
6965
+ /**
6966
+ * 把宏痕迹从包里摘干净(0.23.0 分册 25 · FR-25.4)。
6967
+ *
6968
+ * 我们填完模板交出去的文件叫 `.xlsx` / `.docx`,声明的也是非宏 MIME。如果里面还留着
6969
+ * macroEnabled 的 content type 或 `vbaProject.bin`,文件就自相矛盾,Excel 会拒绝打开。
6970
+ * 宿主报来的那类文件更极端:`vbaProject.bin` 本身已经损坏或丢失,只剩标记——所以判据是
6971
+ * **`[Content_Types].xml` 里的登记**,不是「包里有没有那个部件」。
6972
+ *
6973
+ * 不是宏包时**一个字节都不改**(FR-25.5):无条件重写 `[Content_Types].xml` 会动到属性顺序
6974
+ * 和空白,非宏模板的产物字节就跟着变了。所以第一件事是判断,不中就原样返回。
6975
+ */
6976
+ function stripMacros(pkg) {
6977
+ const typesBytes = pkg.get(CONTENT_TYPES);
6978
+ if (typesBytes === void 0) return;
6979
+ const text = new TextDecoder().decode(typesBytes);
6980
+ if (!text.includes(VBA_CONTENT_TYPE) && !text.includes("macroEnabled.main+xml")) return;
6981
+ const { declaration, body } = splitXmlDeclaration(text);
6982
+ let rewritten = body;
6983
+ for (const match of body.matchAll(/<Override\s[^>]*\/>/g)) {
6984
+ const tag = match[0];
6985
+ const contentType = attributeOf(tag, "ContentType");
6986
+ if (contentType.startsWith(VBA_CONTENT_TYPE)) {
6987
+ pkg.delete(attributeOf(tag, "PartName").replace(/^\//, ""));
6988
+ rewritten = rewritten.replace(tag, "");
6989
+ continue;
6990
+ }
6991
+ const replacement = NON_MACRO_MAIN_TYPE.get(contentType);
6992
+ if (replacement !== void 0) rewritten = rewritten.replace(tag, tag.replace(contentType, replacement));
6993
+ }
6994
+ pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + rewritten));
6995
+ for (const [path, bytes] of pkg) {
6996
+ if (!path.endsWith(".rels")) continue;
6997
+ const split = splitXmlDeclaration(new TextDecoder().decode(bytes));
6998
+ let strippedRels = split.body;
6999
+ for (const match of split.body.matchAll(/<Relationship\s[^>]*\/>/g)) if (attributeOf(match[0], "Type").includes("/vbaProject")) strippedRels = strippedRels.replace(match[0], "");
7000
+ if (strippedRels !== split.body) pkg.set(path, new TextEncoder().encode(split.declaration + strippedRels));
7001
+ }
7002
+ }
6689
7003
 
6690
7004
  //#endregion
6691
7005
  //#region ../browser/src/document/xlsxTemplate.ts
@@ -7182,6 +7496,7 @@ async function fillXlsxTemplate(bytes, spec) {
7182
7496
  }
7183
7497
  if ([...touched.values()].some(({ doc }) => byTag$1(doc, "f").length > 0)) requestFullCalcOnLoad(pkg);
7184
7498
  for (const [path, { declaration, doc }] of touched) pkg.set(path, serializeXmlPart(declaration, doc));
7499
+ stripMacros(pkg);
7185
7500
  assertOoxmlPackage(pkg);
7186
7501
  return writeOoxmlPackage(pkg);
7187
7502
  }
@@ -7421,6 +7736,7 @@ async function fillDocxTemplate(bytes, spec) {
7421
7736
  for (const text of spec.texts ?? []) fillText(doc, locate(doc, text), text.value);
7422
7737
  (spec.images ?? []).forEach((image, index) => attachImage(pkg, doc, locate(doc, image), image, index));
7423
7738
  pkg.set(DOCUMENT, serializeXmlPart(declaration, doc));
7739
+ stripMacros(pkg);
7424
7740
  assertOoxmlPackage(pkg);
7425
7741
  return writeOoxmlPackage(pkg);
7426
7742
  }
@@ -43,6 +43,13 @@ function toFrameScopes(scope) {
43
43
  ...scope.exclude ? { exclude: scope.exclude } : {}
44
44
  }];
45
45
  }
46
+ /** 四个方向的规范清单;工具层校验与文案生成都从这里取,不各写一份 */
47
+ const SCROLL_DIRECTIONS = [
48
+ "up",
49
+ "down",
50
+ "left",
51
+ "right"
52
+ ];
46
53
 
47
54
  //#endregion
48
55
  //#region ../agent/src/pageAction/types.ts
@@ -712,4 +719,4 @@ function readJpegSize(view, length) {
712
719
  }
713
720
 
714
721
  //#endregion
715
- export { DOCUMENT_READ_BUDGET as C, toFrameScopes as D, toActionFrameScopes as E, frameLabel as O, sniffDocumentFormat as S, PAGE_DOCUMENT_REF as T, defersImage as _, columnWidthToPixels as a, isPdfTextTrustworthy as b, isRangeTopLeft as c, rangeContains as d, readImageSize as f, asReadMode as g, createDocumentToolSource as h, MAX_DIGIT_WIDTH as i, frameSteps as k, parseCellAddress as l, READ_DOCUMENT_TOOL as m, EMU_PER_POINT as n, fitWithin as o, PDF_LARGE_IMAGE_RATIO as p, EMU_PER_TWIP as r, formatCellAddress as s, EMU_PER_PIXEL as t, parseCellRange as u, takesImage as v, PAGE_ACTION_KINDS as w, measurePdfTextQuality as x, PDF_GARBLED_RATIO as y };
722
+ export { frameSteps as A, DOCUMENT_READ_BUDGET as C, SCROLL_DIRECTIONS as D, toActionFrameScopes as E, toFrameScopes as O, sniffDocumentFormat as S, PAGE_DOCUMENT_REF as T, defersImage as _, columnWidthToPixels as a, isPdfTextTrustworthy as b, isRangeTopLeft as c, rangeContains as d, readImageSize as f, asReadMode as g, createDocumentToolSource as h, MAX_DIGIT_WIDTH as i, frameLabel as k, parseCellAddress as l, READ_DOCUMENT_TOOL as m, EMU_PER_POINT as n, fitWithin as o, PDF_LARGE_IMAGE_RATIO as p, EMU_PER_TWIP as r, formatCellAddress as s, EMU_PER_PIXEL as t, parseCellRange as u, takesImage as v, PAGE_ACTION_KINDS as w, measurePdfTextQuality as x, PDF_GARBLED_RATIO as y };