@webskill/sdk 0.26.0 → 0.27.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
@@ -3,12 +3,12 @@ import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest
3
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-7CuZS3Z4.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-BSwffz8P.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-CZZOl9cO.js";
8
+ import { c as toBase64$1, f as SPREADSHEET_MIME_TYPE } from "./linkedDocument-0iiVPd5t.js";
9
9
  import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.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, c as isRangeTopLeft, d as rangeContains, 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";
10
+ import { t as createWebSkillApi } from "./webSkillApi-C_7CTwtB.js";
11
+ import { A as frameSteps, D as SCROLL_DIRECTIONS, E as toActionFrameScopes, O as toFrameScopes, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, c as isRangeTopLeft, d as rangeContains, 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-BXMPNuM6.js";
12
12
  import { zipSync } from "fflate";
13
13
 
14
14
  //#region ../browser/src/fs/featureDetection.ts
@@ -20,6 +20,8 @@ function isOpfsAvailable(globalScope) {
20
20
  //#endregion
21
21
  //#region ../browser/src/fs/opfsProvider.ts
22
22
  const isDomException = (e, name) => e instanceof DOMException && e.name === name;
23
+ /** move 的两种原生签名形状不匹配时,浏览器抛的是这两类;权限/找不到等真实失败不重试。 */
24
+ const isWrongMoveSignature = (e) => e instanceof TypeError || typeof DOMException !== "undefined" && e instanceof DOMException && e.name === "NotSupportedError";
23
25
  const mapError = (e, path) => {
24
26
  if (isDomException(e, "NotFoundError")) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${path}`);
25
27
  if (isDomException(e, "NotAllowedError")) return new WebSkillError("FS_PERMISSION_DENIED", `Permission denied: ${path}`, e);
@@ -224,11 +226,21 @@ var OpfsProvider = class {
224
226
  const toSegments = this.#segments(to);
225
227
  const toName = toSegments.pop();
226
228
  if (!toName) throw new WebSkillError("FS_NOT_FOUND", `Invalid path: ${to}`);
227
- const file = await (await this.#walkDir(`/${fromSegments.join("/")}`, false)).getFileHandle(fromName);
229
+ const fromDir = await this.#walkDir(`/${fromSegments.join("/")}`, false);
230
+ const file = await fromDir.getFileHandle(fromName);
228
231
  const mover = file.move;
229
232
  if (`/${fromSegments.join("/")}` === `/${toSegments.join("/")}` && typeof mover === "function") {
230
233
  await this.#wrap(from, async () => {
231
- await mover.call(file, toName);
234
+ const attempts = mover.length >= 2 ? [[fromDir, toName], [toName]] : [[toName], [fromDir, toName]];
235
+ let last;
236
+ for (const args of attempts) try {
237
+ await mover.call(file, ...args);
238
+ return;
239
+ } catch (e) {
240
+ if (!isWrongMoveSignature(e)) throw e;
241
+ last = e;
242
+ }
243
+ throw new WebSkillError("FS_PERMISSION_DENIED", `OPFS atomic rename is not supported by this browser's FileSystemFileHandle.move()`, last);
232
244
  });
233
245
  return;
234
246
  }
@@ -2792,6 +2804,14 @@ function promoteRole(input) {
2792
2804
  //#region ../browser/src/dom/perception.ts
2793
2805
  /** 值可能是凭据的输入类型:即便宿主忘了写进 exclude 也不读值。操作侧复用同一份,不另写 */
2794
2806
  const SECRET_INPUT_TYPES = /* @__PURE__ */ new Set(["password", "hidden"]);
2807
+ /**
2808
+ * 当前页地址的长度上限(0.27.0 分册 22 FR-22.4)。
2809
+ *
2810
+ * 地址是给人看的一串字,不是可解析的契约:超长时截断并标记,比让它把整次感知
2811
+ * 的预算吃掉要好。`…[truncated]` 与其它截断标记同源写法。
2812
+ */
2813
+ const PAGE_URL_LIMIT = 2048;
2814
+ const PAGE_URL_TRUNCATED = "…[truncated]";
2795
2815
  /** 可被操作的角色:只有这些节点的句柄能拿去写页面 */
2796
2816
  const ACTIONABLE_ROLES = /* @__PURE__ */ new Set([
2797
2817
  "button",
@@ -2937,6 +2957,34 @@ function nameOf(element, doc) {
2937
2957
  const title = element.getAttribute("title");
2938
2958
  return title !== null && title.trim() !== "" ? clip(title) : void 0;
2939
2959
  }
2960
+ /**
2961
+ * 稳定化签名(0.27.0 分册 17)。它是**恢复用的保守指纹**,不是选择器:
2962
+ * 角色/名称/链接/表单名/文档内路径/最近行上下文任一不同都不匹配。
2963
+ * 列表重排后同路径可能指向另一行,因此上下文缺失时调用方必须拒绝恢复。
2964
+ */
2965
+ function handleSignatureOf(element, role) {
2966
+ const doc = element.ownerDocument;
2967
+ const segments = [];
2968
+ let node = element;
2969
+ while (node !== null) {
2970
+ const parent = node.parentElement;
2971
+ const index = parent === null ? 0 : [...parent.children].indexOf(node);
2972
+ segments.unshift(`${node.tagName.toLowerCase()}[${index}]`);
2973
+ node = parent;
2974
+ }
2975
+ const ancestor = element.parentElement?.closest("tr, [role=\"row\"], li, [role=\"listitem\"], article, [role=\"article\"], [role=\"feed\"]");
2976
+ const context = ancestor === null || ancestor === void 0 ? void 0 : nameOf(ancestor, doc) ?? hrefOf(ancestor);
2977
+ const form = element.closest("form");
2978
+ const formName = element.getAttribute("name") ?? form?.getAttribute("name") ?? form?.getAttribute("aria-label") ?? void 0;
2979
+ return JSON.stringify({
2980
+ role,
2981
+ ...nameOf(element, doc) !== void 0 ? { name: nameOf(element, doc) } : {},
2982
+ ...hrefOf(element) !== void 0 ? { href: hrefOf(element) } : {},
2983
+ ...formName !== void 0 ? { formName } : {},
2984
+ path: segments.join("/"),
2985
+ ...context !== void 0 ? { context } : {}
2986
+ });
2987
+ }
2940
2988
  function valueOf(element) {
2941
2989
  if (element.tagName === "INPUT") {
2942
2990
  const type = (element.getAttribute("type") ?? "text").toLowerCase();
@@ -3208,10 +3256,16 @@ function sameOriginDocumentOf(iframe) {
3208
3256
  * 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
3209
3257
  * 可见性 / 可用性四道校验才是闸门,它们按**执行那一刻**的 DOM 实时判定。
3210
3258
  */
3211
- var HandleIssuer = class {
3259
+ var HandleIssuer = class HandleIssuer {
3212
3260
  #table = /* @__PURE__ */ new Map();
3213
3261
  /** 同一元素复用同一个 ref:模型重读页面后手里的句柄仍然指向同一个东西 */
3214
3262
  #refByElement = /* @__PURE__ */ new WeakMap();
3263
+ /**
3264
+ * 失效 ref 的签名(0.27.0 分册 17)。不随 `#table` 删除:重渲染替换实例后,
3265
+ * 旧 ref 仍要能按签名找回来;上限 4096,文档 WeakRef 销毁即视为失效。
3266
+ */
3267
+ #signatures = /* @__PURE__ */ new Map();
3268
+ static #MAX_SIGNATURES = 4096;
3215
3269
  #actionable;
3216
3270
  #excluded;
3217
3271
  constructor(actionable, excluded) {
@@ -3229,22 +3283,59 @@ var HandleIssuer = class {
3229
3283
  */
3230
3284
  prune() {
3231
3285
  for (const [ref, entry] of this.#table) if (!entry.element.isConnected) this.#table.delete(ref);
3286
+ for (const [ref, record] of this.#signatures) if (record.document.deref() === void 0) this.#signatures.delete(ref);
3287
+ this.#trimSignatures();
3288
+ }
3289
+ #trimSignatures() {
3290
+ while (this.#signatures.size > HandleIssuer.#MAX_SIGNATURES) {
3291
+ const oldest = this.#signatures.keys().next().value;
3292
+ if (oldest === void 0) return;
3293
+ this.#signatures.delete(oldest);
3294
+ }
3295
+ }
3296
+ #remember(ref, element, signature) {
3297
+ this.#signatures.delete(ref);
3298
+ this.#signatures.set(ref, {
3299
+ signature,
3300
+ document: new WeakRef(element.ownerDocument)
3301
+ });
3302
+ this.#trimSignatures();
3303
+ }
3304
+ signatureOf(ref) {
3305
+ return this.#signatures.get(ref)?.signature ?? this.#table.get(ref)?.signature;
3306
+ }
3307
+ documentOf(ref) {
3308
+ return this.#signatures.get(ref)?.document.deref();
3309
+ }
3310
+ /** 动作恢复:把旧 ref 重新指向实际元素;四道闸仍由执行器在 execute 时判。 */
3311
+ restore(ref, element, kind, frame, signature) {
3312
+ this.#table.set(ref, {
3313
+ element,
3314
+ kind,
3315
+ ...frame !== void 0 ? { frame } : {},
3316
+ signature
3317
+ });
3318
+ this.#refByElement.set(element, ref);
3319
+ this.#remember(ref, element, signature);
3232
3320
  }
3233
3321
  issue(element, role, frame) {
3234
3322
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
3235
3323
  const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
3236
3324
  if (kind === void 0) return void 0;
3237
3325
  if (kind === "action" && occupiesNoSpace(element)) return void 0;
3326
+ const signature = handleSignatureOf(element, role);
3238
3327
  const known = this.#refByElement.get(element);
3239
3328
  if (known !== void 0 && this.#table.has(known)) {
3240
3329
  this.#table.set(known, {
3241
3330
  element,
3242
3331
  kind,
3243
- ...frame !== void 0 ? { frame } : {}
3332
+ ...frame !== void 0 ? { frame } : {},
3333
+ signature
3244
3334
  });
3335
+ this.#remember(known, element, signature);
3245
3336
  return known;
3246
3337
  }
3247
- return this.#mint(element, kind, frame);
3338
+ return this.#mint(element, kind, frame, signature);
3248
3339
  }
3249
3340
  /**
3250
3341
  * 取像句柄(0.22.0 FR-12.1)。**不过操作范围那道闸**:取像是读,而该元素已经在
@@ -3256,7 +3347,7 @@ var HandleIssuer = class {
3256
3347
  if (this.#excluded.has(element)) return void 0;
3257
3348
  const known = this.#refByElement.get(element);
3258
3349
  if (known !== void 0 && this.#table.has(known)) return known;
3259
- return this.#mint(element, "image", frame);
3350
+ return this.#mint(element, "image", frame, handleSignatureOf(element, roleOf$1(element)));
3260
3351
  }
3261
3352
  /**
3262
3353
  * 可滚容器的句柄(0.23.0 分册 22)。角色白名单之外——表格容器往往是个 `generic` div,
@@ -3269,18 +3360,20 @@ var HandleIssuer = class {
3269
3360
  if (occupiesNoSpace(element)) return void 0;
3270
3361
  const known = this.#refByElement.get(element);
3271
3362
  if (known !== void 0 && this.#table.has(known)) return known;
3272
- return this.#mint(element, "action", frame);
3363
+ return this.#mint(element, "action", frame, handleSignatureOf(element, roleOf$1(element)));
3273
3364
  }
3274
- #mint(element, kind, frame) {
3365
+ #mint(element, kind, frame, signature) {
3275
3366
  const bytes = /* @__PURE__ */ new Uint8Array(8);
3276
3367
  crypto.getRandomValues(bytes);
3277
3368
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
3278
3369
  this.#table.set(ref, {
3279
3370
  element,
3280
3371
  kind,
3281
- ...frame !== void 0 ? { frame } : {}
3372
+ ...frame !== void 0 ? { frame } : {},
3373
+ signature
3282
3374
  });
3283
3375
  this.#refByElement.set(element, ref);
3376
+ this.#remember(ref, element, signature);
3284
3377
  return ref;
3285
3378
  }
3286
3379
  /** 角色提升要的是「有可操作后代」,容器锚点不算;混进来会把可点容器判成已有出口 */
@@ -3301,6 +3394,8 @@ var HandleIssuer = class {
3301
3394
  */
3302
3395
  function createDomPerceptionReader(options = {}) {
3303
3396
  let handles;
3397
+ /** 最近一次 read/readSubtree 的范围;只给内部 actionChildRefs 用,不进公开接口 */
3398
+ let lastScope;
3304
3399
  /** 授权生效时记下各帧的 origin,之后每次感知比对(FR-24.3) */
3305
3400
  const frameOrigins = /* @__PURE__ */ new Map();
3306
3401
  /**
@@ -3309,6 +3404,22 @@ function createDomPerceptionReader(options = {}) {
3309
3404
  */
3310
3405
  const elevatedModals = /* @__PURE__ */ new Set();
3311
3406
  const rootDocument = () => options.document ?? globalThis.document;
3407
+ /**
3408
+ * 当前文档的地址与标题(0.27.0 分册 22 FR-22.1)。
3409
+ *
3410
+ * 只回答「你在哪一页」,不是导航入口:没有任何入参能选目的地。
3411
+ * 拿不到(无文档、`about:blank`、`srcdoc`)时返回 `undefined`——结果里就不带这个键,
3412
+ * 而不是塞一个空串让模型以为拿到了地址(FR-22.8)。
3413
+ */
3414
+ const pageOf = (doc) => {
3415
+ const href = (doc?.defaultView)?.location?.href;
3416
+ if (typeof href !== "string" || href === "" || href === "about:blank") return void 0;
3417
+ const title = typeof doc?.title === "string" ? doc.title.trim() : "";
3418
+ return {
3419
+ url: href.length > PAGE_URL_LIMIT ? `${href.slice(0, PAGE_URL_LIMIT)}${PAGE_URL_TRUNCATED}` : href,
3420
+ ...title === "" ? {} : { title }
3421
+ };
3422
+ };
3312
3423
  /** 提升未开启时返回 undefined,遍历侧据此整段跳过(AC-G10) */
3313
3424
  const promotionOf = (view) => options.promoteRoles === true ? {
3314
3425
  view,
@@ -3402,6 +3513,7 @@ function createDomPerceptionReader(options = {}) {
3402
3513
  nestedFrames: [],
3403
3514
  anchored: false
3404
3515
  };
3516
+ const page = pageOf(root);
3405
3517
  handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
3406
3518
  handles.prune();
3407
3519
  const issuer = handles;
@@ -3519,7 +3631,8 @@ function createDomPerceptionReader(options = {}) {
3519
3631
  nodes,
3520
3632
  frameNotes,
3521
3633
  nestedFrames,
3522
- anchored
3634
+ anchored,
3635
+ ...page === void 0 ? {} : { page }
3523
3636
  };
3524
3637
  pruneElevated();
3525
3638
  const view = root.defaultView;
@@ -3541,12 +3654,13 @@ function createDomPerceptionReader(options = {}) {
3541
3654
  nodes,
3542
3655
  frameNotes,
3543
3656
  nestedFrames,
3544
- anchored
3657
+ anchored,
3658
+ ...page === void 0 ? {} : { page }
3545
3659
  };
3546
3660
  }
3547
3661
  async function readWithImages(scope, capture, subtree) {
3548
3662
  const targets = [];
3549
- const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, {
3663
+ const { nodes, frameNotes, nestedFrames, anchored, page } = collect(scope, {
3550
3664
  targets,
3551
3665
  minImageArea: capture.minImageArea ?? 0
3552
3666
  }, subtree);
@@ -3578,16 +3692,19 @@ function createDomPerceptionReader(options = {}) {
3578
3692
  imagesOmitted: targets.length - selected.length,
3579
3693
  imageFailures,
3580
3694
  anchored,
3695
+ ...page === void 0 ? {} : { page },
3581
3696
  ...frameNotes.length > 0 ? { frameNotes } : {},
3582
3697
  ...nestedFrames.length > 0 ? { nestedFrames } : {}
3583
3698
  };
3584
3699
  }
3585
3700
  function read(scope, capture) {
3701
+ lastScope = scope;
3586
3702
  if (capture === void 0) return collect(scope, void 0).nodes;
3587
3703
  if (!capture.images || capture.maxImages <= 0) {
3588
- const { nodes, frameNotes, nestedFrames } = collect(scope, void 0);
3704
+ const { nodes, frameNotes, nestedFrames, page } = collect(scope, void 0);
3589
3705
  return Promise.resolve({
3590
3706
  nodes,
3707
+ ...page === void 0 ? {} : { page },
3591
3708
  ...frameNotes.length > 0 ? { frameNotes } : {},
3592
3709
  ...nestedFrames.length > 0 ? { nestedFrames } : {}
3593
3710
  });
@@ -3602,20 +3719,113 @@ function createDomPerceptionReader(options = {}) {
3602
3719
  * 拒绝走返回值而不是抛错:跨上下文的 reject 到本地就是一个没有错误码的 `Error`。
3603
3720
  */
3604
3721
  async function readSubtree(scope, ref, capture) {
3605
- const element = handles?.table.get(ref)?.element;
3606
- if (element === void 0 || !element.isConnected) return { rejected: "unknown-ref" };
3722
+ lastScope = scope;
3723
+ let element = handles?.table.get(ref)?.element;
3724
+ if (element === void 0 || !element.isConnected) element = recoverElement(ref, "perceive");
3725
+ if (element === void 0) return { rejected: "unknown-ref" };
3607
3726
  if (capture?.images === true && capture.maxImages > 0) {
3608
3727
  const { anchored, ...result } = await readWithImages(scope, capture, element);
3609
3728
  return anchored ? result : { rejected: "out-of-scope" };
3610
3729
  }
3611
- const { nodes, frameNotes, nestedFrames, anchored } = collect(scope, void 0, element);
3730
+ const { nodes, frameNotes, nestedFrames, anchored, page } = collect(scope, void 0, element);
3612
3731
  if (!anchored) return { rejected: "out-of-scope" };
3613
3732
  return {
3614
3733
  nodes,
3734
+ ...page === void 0 ? {} : { page },
3615
3735
  ...frameNotes.length > 0 ? { frameNotes } : {},
3616
3736
  ...nestedFrames.length > 0 ? { nestedFrames } : {}
3617
3737
  };
3618
3738
  }
3739
+ /**
3740
+ * 内部能力(分册 16):容器 ref 被拒时,给出当前已持有/读一次子树后的可操作子 ref。
3741
+ * 不导出到 `DomPerceptionReader`:公开接口加方法会改 d.ts 与 API 快照。
3742
+ * 返回 `known:false` 表示无法判断,调用方必须退回旧文案,不能声称“没有子节点”。
3743
+ */
3744
+ async function actionChildRefs(ref, limit) {
3745
+ const entry = handles?.table.get(ref);
3746
+ if (entry === void 0 || entry.kind !== "anchor" || !entry.element.isConnected) return void 0;
3747
+ const container = entry.element;
3748
+ const doc = container.ownerDocument;
3749
+ const collectActionRefs = () => {
3750
+ const out = [];
3751
+ for (const [childRef, child] of handles?.table ?? []) {
3752
+ if (child.kind !== "action" || !child.element.isConnected) continue;
3753
+ if (child.element === container || !container.contains(child.element)) continue;
3754
+ if (child.element.ownerDocument !== doc) continue;
3755
+ out.push({
3756
+ ref: childRef,
3757
+ element: child.element
3758
+ });
3759
+ }
3760
+ out.sort((a, b) => {
3761
+ const position = a.element.compareDocumentPosition(b.element);
3762
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
3763
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
3764
+ return 0;
3765
+ });
3766
+ return [...new Set(out.map((item) => item.ref))];
3767
+ };
3768
+ let refs = collectActionRefs();
3769
+ if (refs.length === 0) {
3770
+ if (lastScope === void 0) return {
3771
+ refs: [],
3772
+ known: false,
3773
+ more: 0
3774
+ };
3775
+ if ("rejected" in await readSubtree(lastScope, ref)) return {
3776
+ refs: [],
3777
+ known: false,
3778
+ more: 0
3779
+ };
3780
+ refs = collectActionRefs();
3781
+ }
3782
+ const unique = [...new Set(refs)];
3783
+ return {
3784
+ refs: unique.slice(0, limit),
3785
+ known: true,
3786
+ more: Math.max(0, unique.length - limit)
3787
+ };
3788
+ }
3789
+ /**
3790
+ * 内部恢复(0.27.0 分册 17):旧 ref 的元素断开后,按签名在**当前主文档**内找唯一元素。
3791
+ * `action` 模式会恢复句柄表,`perceive` 只返回元素供本次子树读,不把旧 ref 写回。
3792
+ * 跨帧不恢复:这里只处理主文档,iframe 旧 ref 仍走既有失效路径。
3793
+ */
3794
+ function recoverElement(ref, mode) {
3795
+ const doc = rootDocument();
3796
+ const signature = handles?.signatureOf(ref);
3797
+ const oldDoc = handles?.documentOf(ref);
3798
+ if (doc === void 0 || signature === void 0 || oldDoc !== doc) return void 0;
3799
+ const scope = lastScope;
3800
+ if (scope === void 0) return void 0;
3801
+ const view = doc.defaultView;
3802
+ const candidates = [...doc.querySelectorAll("*")].filter((element) => {
3803
+ if (!element.isConnected) return false;
3804
+ if (handleSignatureOf(element, roleOf$1(element)) !== signature) return false;
3805
+ if (mode === "action") {
3806
+ const sets = actionSets(doc, actionScopeFor("self"));
3807
+ if (!sets.actionable.has(element) || sets.excluded.has(element)) return false;
3808
+ const modal = element.closest(MODAL_SELECTOR);
3809
+ if (modal !== null && !elevatedModals.has(modal)) return false;
3810
+ if (hidden$1(element, view)) return false;
3811
+ if (occupiesNoSpace(element)) return false;
3812
+ if (element.hasAttribute("disabled")) return false;
3813
+ return true;
3814
+ }
3815
+ const frameScope = "frames" in scope ? scope.frames.find((entry) => frameSteps(entry.frame).length === 0) : scope;
3816
+ if (frameScope === void 0) return false;
3817
+ const sets = actionSets(doc, frameScope);
3818
+ return sets.actionable.has(element) && !sets.excluded.has(element);
3819
+ });
3820
+ if (candidates.length !== 1) return void 0;
3821
+ const [element] = candidates;
3822
+ if (element === void 0) return void 0;
3823
+ if (mode === "action") {
3824
+ const frame = handles?.table.get(ref)?.frame;
3825
+ handles?.restore(ref, element, "action", frame, signature);
3826
+ }
3827
+ return element;
3828
+ }
3619
3829
  /** 当前活动模态;跨帧不适用(模态提升只在主文档语义下成立) */
3620
3830
  function modalSnapshot() {
3621
3831
  const root = rootDocument();
@@ -3629,6 +3839,8 @@ function createDomPerceptionReader(options = {}) {
3629
3839
  return {
3630
3840
  read,
3631
3841
  readSubtree,
3842
+ actionChildRefs,
3843
+ recoverElement,
3632
3844
  resolve: (ref) => handles?.table.get(ref)?.element,
3633
3845
  handleKindOf: (ref) => handles?.table.get(ref)?.kind,
3634
3846
  frameOf: (ref) => handles?.table.get(ref)?.frame,
@@ -3824,6 +4036,26 @@ function roleOf(element) {
3824
4036
  if (element.tagName === "FORM") return "form";
3825
4037
  return "generic";
3826
4038
  }
4039
+ const containerActionText = (reader, inside) => {
4040
+ const base = "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.";
4041
+ if (inside === void 0 || !inside.known) return base;
4042
+ const usable = [];
4043
+ for (const ref of inside.refs) {
4044
+ const element = reader.resolve(ref);
4045
+ if (element === void 0) continue;
4046
+ if (reader.modalStateOf(element) === "unelevated") continue;
4047
+ if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) continue;
4048
+ if (hidden(element)) continue;
4049
+ if (occupiesNoSpace(element)) continue;
4050
+ if (element.hasAttribute("disabled")) continue;
4051
+ const target = describeElement(element);
4052
+ usable.push(`[${ref}] ${target.role}${target.name !== void 0 ? ` "${target.name}"` : ""}`);
4053
+ }
4054
+ if (usable.length === 0) return `${base} This container has no actionable elements; retrying here will not help. Perceive the container again after a row or panel has changed.`;
4055
+ const shown = usable.slice(0, 5);
4056
+ const more = Math.max(inside.more, usable.length - shown.length);
4057
+ return `${base} Actionable references inside: ${shown.join("; ")}${more > 0 ? `; and ${more} more` : ""}.`;
4058
+ };
3827
4059
  const describeElement = (element) => {
3828
4060
  const name = accessibleName(element);
3829
4061
  return {
@@ -3971,24 +4203,28 @@ function hoverOver(element) {
3971
4203
  }));
3972
4204
  element.dispatchEvent(new MouseEvent("mousemove", base));
3973
4205
  }
3974
- /**
3975
- * 悬停之后页面有没有动(FR-17.3)。
3976
- *
3977
- * 用 `MutationObserver` 而不是比对快照:浮层可能挂到 `document.body` 末尾、
3978
- * 也可能只是把某个节点的 `hidden` 去掉,逐一枚举会漏,而漏判的代价是向模型报假成功。
3979
- * 观察器在派发**之前**挂上——晚一步就会错过同步执行的那批监听器。
3980
- */
3981
4206
  function watchMutations(element) {
3982
4207
  const doc = element.ownerDocument;
3983
4208
  const Observer = doc.defaultView?.MutationObserver;
3984
4209
  if (Observer === void 0) return {
3985
- changed: () => true,
4210
+ take: () => ({
4211
+ added: 0,
4212
+ removed: 0,
4213
+ changed: true
4214
+ }),
3986
4215
  stop: () => void 0
3987
4216
  };
4217
+ let added = 0;
4218
+ let removed = 0;
3988
4219
  let seen = false;
3989
- const observer = new Observer(() => {
3990
- seen = true;
3991
- });
4220
+ const absorb = (records) => {
4221
+ for (const record of records) {
4222
+ seen = true;
4223
+ added += record.addedNodes.length;
4224
+ removed += record.removedNodes.length;
4225
+ }
4226
+ };
4227
+ const observer = new Observer(absorb);
3992
4228
  observer.observe(doc, {
3993
4229
  subtree: true,
3994
4230
  childList: true,
@@ -3996,13 +4232,50 @@ function watchMutations(element) {
3996
4232
  characterData: true
3997
4233
  });
3998
4234
  return {
3999
- changed: () => {
4000
- if (observer.takeRecords().length > 0) seen = true;
4001
- return seen;
4235
+ take: () => {
4236
+ absorb(observer.takeRecords());
4237
+ return {
4238
+ added,
4239
+ removed,
4240
+ changed: seen
4241
+ };
4002
4242
  },
4003
4243
  stop: () => observer.disconnect()
4004
4244
  };
4005
4245
  }
4246
+ const STATUS_SELECTORS = [
4247
+ "[role=\"status\"]",
4248
+ "[role=\"alert\"]",
4249
+ ".messages .message",
4250
+ ".messages li"
4251
+ ];
4252
+ const compactText = (element) => {
4253
+ const text = element.textContent?.replace(/\s+/g, " ").trim();
4254
+ return text === void 0 || text === "" ? void 0 : text;
4255
+ };
4256
+ /** 状态文本只在固定状态区取;不读整页、不读 `.messages` 容器,避免把多条消息/正文带进模型上下文。 */
4257
+ function statusTextOf(element, request, target) {
4258
+ if (target.secret === true) return void 0;
4259
+ let text = [...element.ownerDocument.querySelectorAll(STATUS_SELECTORS.join(","))].filter((node) => !hidden(node)).map((node) => compactText(node)).filter((text) => text !== void 0).at(-1);
4260
+ if (text === void 0) return void 0;
4261
+ const value = request.value;
4262
+ if (value !== void 0 && value.length > 0) text = text.split(value).join("[redacted]");
4263
+ return text.length > 200 ? `${text.slice(0, 197)}...` : text;
4264
+ }
4265
+ function withChanges(outcome, mutations, element, request) {
4266
+ if (!outcome.ok || outcome.noop === true || outcome.navigated === true) return outcome;
4267
+ const snapshot = mutations.take();
4268
+ const status = statusTextOf(element, request, outcome.target);
4269
+ if (snapshot.added === 0 && snapshot.removed === 0 && status === void 0) return outcome;
4270
+ return {
4271
+ ...outcome,
4272
+ changes: {
4273
+ added: snapshot.added,
4274
+ removed: snapshot.removed,
4275
+ ...status !== void 0 ? { status } : {}
4276
+ }
4277
+ };
4278
+ }
4006
4279
  /**
4007
4280
  * 一次拖拽(FR-17.5)。真实页面有两套互不相通的机制,按起点自动选:
4008
4281
  *
@@ -4279,9 +4552,23 @@ function createDomPageActionExecutor(options) {
4279
4552
  ...frame !== void 0 ? { frame } : {}
4280
4553
  };
4281
4554
  };
4555
+ const recoveredRefs = /* @__PURE__ */ new Map();
4556
+ const rememberRecovered = (ref, element) => {
4557
+ recoveredRefs.delete(ref);
4558
+ recoveredRefs.set(ref, element);
4559
+ while (recoveredRefs.size > 32) {
4560
+ const oldest = recoveredRefs.keys().next().value;
4561
+ if (oldest === void 0) break;
4562
+ recoveredRefs.delete(oldest);
4563
+ }
4564
+ };
4565
+ const recoverAction = (ref) => reader.recoverElement?.(ref, "action");
4282
4566
  const describe = (ref) => {
4283
- const element = reader.resolve(ref);
4567
+ const resolved = reader.resolve(ref);
4568
+ const live = resolved?.isConnected === true ? resolved : void 0;
4569
+ const element = live ?? recoverAction(ref);
4284
4570
  if (element === void 0) return void 0;
4571
+ if (live === void 0) rememberRecovered(ref, element);
4285
4572
  const described = withFrame(ref, describeElement(element));
4286
4573
  return reader.modalStateOf(element) === "elevated" ? {
4287
4574
  ...described,
@@ -4366,7 +4653,15 @@ function createDomPageActionExecutor(options) {
4366
4653
  };
4367
4654
  const execute = async (request) => {
4368
4655
  if (request.action === "back") return await goBack();
4369
- const element = reader.resolve(request.ref);
4656
+ const cached = recoveredRefs.get(request.ref);
4657
+ const liveResolved = reader.resolve(request.ref);
4658
+ let element = cached ?? (liveResolved?.isConnected === true ? liveResolved : void 0);
4659
+ let recovered = cached !== void 0;
4660
+ if (element === void 0) {
4661
+ element = recoverAction(request.ref);
4662
+ recovered = element !== void 0;
4663
+ if (element !== void 0) rememberRecovered(request.ref, element);
4664
+ }
4370
4665
  if (element === void 0) return {
4371
4666
  ok: false,
4372
4667
  target: { role: "generic" },
@@ -4378,7 +4673,15 @@ function createDomPageActionExecutor(options) {
4378
4673
  target,
4379
4674
  reason
4380
4675
  });
4381
- 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.");
4676
+ if (reader.handleKindOf(request.ref) === "anchor") {
4677
+ let inside;
4678
+ try {
4679
+ inside = await reader.actionChildRefs?.(request.ref, 5);
4680
+ } catch {
4681
+ inside = void 0;
4682
+ }
4683
+ return fail(containerActionText(reader, inside));
4684
+ }
4382
4685
  if (reader.handleKindOf(request.ref) === "image") return fail("That reference points at an image, which cannot be acted on. Use it with capture_page_image to read the picture itself.");
4383
4686
  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.");
4384
4687
  if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
@@ -4402,6 +4705,7 @@ function createDomPageActionExecutor(options) {
4402
4705
  const navigated = keyAfter !== void 0 && keyBefore !== void 0 && keyAfter !== keyBefore;
4403
4706
  return {
4404
4707
  ...outcome,
4708
+ ...recovered ? { recovered: true } : {},
4405
4709
  ...name !== void 0 ? { elevatedModal: name } : {},
4406
4710
  ...navigated ? {
4407
4711
  navigated: true,
@@ -4409,93 +4713,96 @@ function createDomPageActionExecutor(options) {
4409
4713
  } : {}
4410
4714
  };
4411
4715
  };
4412
- if (request.action === "click") {
4413
- const watch = watchMutations(element);
4414
- press(element);
4415
- const outcome = await done({
4416
- ok: true,
4417
- target
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
- };
4425
- }
4426
- if (request.action === "fill") {
4427
- if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
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.");
4429
- fillValue(element, request.value ?? "");
4430
- return await done({
4431
- ok: true,
4432
- target
4433
- });
4434
- }
4435
- if (request.action === "select") {
4436
- const outcome = await selectOption(element, request.value ?? "");
4437
- return outcome.ok ? await done({
4438
- ...outcome,
4439
- target
4440
- }) : {
4441
- ...outcome,
4442
- target
4443
- };
4444
- }
4445
- if (request.action === "set") return await done(setToggle(element, request.value ?? "", target));
4446
- if (request.action === "scroll") {
4447
- if (!SCROLL_DIRECTIONS.includes(request.value)) return fail(`Use ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
4448
- return await scrollRegion(element, target, request.value);
4449
- }
4450
- if (request.action === "hover") {
4451
- const watch = watchMutations(element);
4452
- hoverOver(element);
4453
- await waitFor(() => watch.changed() ? true : void 0, 500);
4454
- const changed = watch.changed();
4455
- watch.stop();
4456
- return await done({
4457
- ok: true,
4458
- target,
4459
- ...changed || popoverOpenBeside(element) ? {} : { noop: true }
4460
- });
4461
- }
4462
- if (request.action === "drag") {
4463
- const destination = request.value ?? "";
4464
- if (destination === "") return fail("The drag action needs a value holding the destination ref.");
4465
- const dropTarget = reader.resolve(destination);
4466
- if (dropTarget === void 0) return fail("The destination reference is unknown or expired.");
4467
- if (reader.handleKindOf(destination) === "image") return fail("The destination reference points at an image, which cannot receive a drop.");
4468
- if (reader.modalStateOf(dropTarget) !== "elevated" && !reader.inActionScope(dropTarget)) return fail("The destination is no longer inside the actionable scope.");
4469
- if (hidden(dropTarget)) return fail("The destination is not visible.");
4470
- const refused = dragBetween(element, dropTarget);
4471
- if (refused !== void 0) return fail(refused);
4472
- return await done({
4473
- ok: true,
4474
- target
4475
- });
4476
- }
4477
- if (request.action === "attach") {
4478
- const files = await options.pickFiles?.();
4479
- if (files === void 0 || files.length === 0) return {
4480
- ok: false,
4481
- target,
4482
- reason: "The user cancelled the file selection."
4483
- };
4484
- if (element.tagName !== "INPUT" || element.type !== "file") return fail("The element is not a file input.");
4485
- const failure = attachFiles(element, files);
4486
- if (failure !== void 0) return fail(failure);
4487
- return await done({
4716
+ const mutations = watchMutations(element);
4717
+ try {
4718
+ if (request.action === "click") {
4719
+ press(element);
4720
+ const outcome = await done({
4721
+ ok: true,
4722
+ target
4723
+ });
4724
+ return mutations.take().changed || outcome.navigated === true || outcome.elevatedModal !== void 0 || popoverOpenBeside(element) ? withChanges(outcome, mutations, element, request) : {
4725
+ ...outcome,
4726
+ noop: true
4727
+ };
4728
+ }
4729
+ if (request.action === "fill") {
4730
+ if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
4731
+ 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.");
4732
+ fillValue(element, request.value ?? "");
4733
+ return withChanges(await done({
4734
+ ok: true,
4735
+ target
4736
+ }), mutations, element, request);
4737
+ }
4738
+ if (request.action === "select") {
4739
+ const outcome = await selectOption(element, request.value ?? "");
4740
+ return outcome.ok ? withChanges(await done({
4741
+ ...outcome,
4742
+ target
4743
+ }), mutations, element, request) : {
4744
+ ...outcome,
4745
+ target
4746
+ };
4747
+ }
4748
+ if (request.action === "set") return withChanges(await done(setToggle(element, request.value ?? "", target)), mutations, element, request);
4749
+ if (request.action === "scroll") {
4750
+ if (!SCROLL_DIRECTIONS.includes(request.value)) return fail(`Use ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
4751
+ const scrolled = withChanges(await scrollRegion(element, target, request.value), mutations, element, request);
4752
+ return recovered && scrolled.ok ? {
4753
+ ...scrolled,
4754
+ recovered: true
4755
+ } : scrolled;
4756
+ }
4757
+ if (request.action === "hover") {
4758
+ hoverOver(element);
4759
+ await waitFor(() => mutations.take().changed ? true : void 0, 500);
4760
+ return withChanges(await done({
4761
+ ok: true,
4762
+ target,
4763
+ ...mutations.take().changed || popoverOpenBeside(element) ? {} : { noop: true }
4764
+ }), mutations, element, request);
4765
+ }
4766
+ if (request.action === "drag") {
4767
+ const destination = request.value ?? "";
4768
+ if (destination === "") return fail("The drag action needs a value holding the destination ref.");
4769
+ const dropTarget = reader.resolve(destination);
4770
+ if (dropTarget === void 0) return fail("The destination reference is unknown or expired.");
4771
+ if (reader.handleKindOf(destination) === "image") return fail("The destination reference points at an image, which cannot receive a drop.");
4772
+ if (reader.modalStateOf(dropTarget) !== "elevated" && !reader.inActionScope(dropTarget)) return fail("The destination is no longer inside the actionable scope.");
4773
+ if (hidden(dropTarget)) return fail("The destination is not visible.");
4774
+ const refused = dragBetween(element, dropTarget);
4775
+ if (refused !== void 0) return fail(refused);
4776
+ return withChanges(await done({
4777
+ ok: true,
4778
+ target
4779
+ }), mutations, element, request);
4780
+ }
4781
+ if (request.action === "attach") {
4782
+ const files = await options.pickFiles?.();
4783
+ if (files === void 0 || files.length === 0) return {
4784
+ ok: false,
4785
+ target,
4786
+ reason: "The user cancelled the file selection."
4787
+ };
4788
+ if (element.tagName !== "INPUT" || element.type !== "file") return fail("The element is not a file input.");
4789
+ const failure = attachFiles(element, files);
4790
+ if (failure !== void 0) return fail(failure);
4791
+ return withChanges(await done({
4792
+ ok: true,
4793
+ target
4794
+ }), mutations, element, request);
4795
+ }
4796
+ const form = element.tagName === "FORM" ? element : element.form;
4797
+ if (!form) return fail("The element does not belong to a form.");
4798
+ form.requestSubmit();
4799
+ return withChanges(await done({
4488
4800
  ok: true,
4489
4801
  target
4490
- });
4802
+ }), mutations, element, request);
4803
+ } finally {
4804
+ mutations.stop();
4491
4805
  }
4492
- const form = element.tagName === "FORM" ? element : element.form;
4493
- if (!form) return fail("The element does not belong to a form.");
4494
- form.requestSubmit();
4495
- return await done({
4496
- ok: true,
4497
- target
4498
- });
4499
4806
  };
4500
4807
  return {
4501
4808
  execute,
@@ -6534,10 +6841,17 @@ async function readDocxBlocks(bytes) {
6534
6841
  * 图不是浮在表外的附件,它压着某几行几列——按锚点插回行流里,
6535
6842
  * 模型才知道「这张图说的是上面那几行」。
6536
6843
  */
6537
- /** 值得单独烧一次多模态的图:至少跨 2 列、3 行(FR-22.11) @experimental */
6844
+ /** @experimental @deprecated 0.27.0 起不再按跨度过滤图片;保留导出仅为兼容既有调用。 */
6538
6845
  const XLSX_IMAGE_MIN_COLUMNS = 2;
6539
- /** @experimental */
6846
+ /** @experimental @deprecated 0.27.0 起不再按跨度过滤图片;保留导出仅为兼容既有调用。 */
6540
6847
  const XLSX_IMAGE_MIN_ROWS = 3;
6848
+ const ANCHOR_TAGS = /* @__PURE__ */ new Set([
6849
+ "twoCellAnchor",
6850
+ "oneCellAnchor",
6851
+ "absoluteAnchor"
6852
+ ]);
6853
+ /** 绝对锚点没有行号;放在表尾,保证图片不丢。 */
6854
+ const AFTER_LAST_ROW = Number.MAX_SAFE_INTEGER;
6541
6855
  const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
6542
6856
  const byTag$2 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
6543
6857
  function parseXml$1(bytes) {
@@ -6575,7 +6889,13 @@ const anchorNumber = (anchor, name) => {
6575
6889
  const value = Number(node?.textContent ?? NaN);
6576
6890
  return Number.isInteger(value) && value >= 0 ? value : 0;
6577
6891
  };
6578
- /** 一张表上的图:行号(0 基)→ 该行下面要插的图块 */
6892
+ /**
6893
+ * 一张表上的图:行号(0 基)→ 该行下面要插的图块。
6894
+ *
6895
+ * 0.27.0 起读所有 DrawingML 锚点、所有尺寸:`twoCellAnchor`、`oneCellAnchor` 都按
6896
+ * `from` 行插回;`absoluteAnchor` 没有单元格坐标,补在表尾。旧版按跨列/跨行阈值过滤,
6897
+ * 会把小图标、单元格小图、以及本仓 `fill_template` 自己写的 `oneCellAnchor` 全部丢掉。
6898
+ */
6579
6899
  function imagesOf(parts, sheetPath) {
6580
6900
  const placed = /* @__PURE__ */ new Map();
6581
6901
  const sheetRels = readRels(parts, sheetPath);
@@ -6585,21 +6905,18 @@ function imagesOf(parts, sheetPath) {
6585
6905
  const drawing = drawingBytes === void 0 ? void 0 : parseXml$1(drawingBytes);
6586
6906
  if (drawing === void 0) continue;
6587
6907
  const drawingRels = readRels(parts, target);
6588
- for (const anchor of byTag$2(drawing, "twoCellAnchor")) {
6908
+ const anchors = [...drawing.documentElement?.children ?? []].filter((element) => ANCHOR_TAGS.has(element.localName));
6909
+ for (const anchor of anchors) {
6589
6910
  const from = byTag$2(anchor, "from")[0];
6590
- const to = byTag$2(anchor, "to")[0];
6591
- const fromRow = anchorNumber(from, "row");
6592
- const columns = anchorNumber(to, "col") - anchorNumber(from, "col");
6593
- const rows = anchorNumber(to, "row") - fromRow;
6594
- if (columns < 2 || rows < 3) continue;
6911
+ const row = anchor.localName === "absoluteAnchor" || from === void 0 ? AFTER_LAST_ROW : anchorNumber(from, "row");
6595
6912
  const blip = byTag$2(anchor, "blip")[0];
6596
6913
  const relId = blip?.getAttributeNS(RELATIONSHIPS_NS, "embed") ?? blip?.getAttribute("r:embed");
6597
6914
  const path = relId === null || relId === void 0 ? void 0 : drawingRels.get(relId);
6598
6915
  const data = path === void 0 ? void 0 : parts.get(path);
6599
6916
  if (path === void 0 || data === void 0) continue;
6600
- const list = placed.get(fromRow) ?? [];
6917
+ const list = placed.get(row) ?? [];
6601
6918
  list.push(imageBlockOf(path, () => toBase64$1(data)));
6602
- placed.set(fromRow, list);
6919
+ placed.set(row, list);
6603
6920
  }
6604
6921
  }
6605
6922
  return placed;
@@ -6897,6 +7214,14 @@ const extensionOf = (path) => {
6897
7214
  const dot = path.lastIndexOf(".");
6898
7215
  return dot < 0 ? "" : path.slice(dot + 1).toLowerCase();
6899
7216
  };
7217
+ /** 用 DOM 读登记项,不能按属性顺序写正则:有的写库(write-excel-file)把 ContentType 排在 Extension 前面 */
7218
+ function declaredTypes(text) {
7219
+ const doc = new DOMParser().parseFromString(splitXmlDeclaration(text).body, "application/xml");
7220
+ return {
7221
+ defaults: new Set(Array.from(doc.getElementsByTagName("Default"), (el) => (el.getAttribute("Extension") ?? "").toLowerCase())),
7222
+ overrides: new Set(Array.from(doc.getElementsByTagName("Override"), (el) => el.getAttribute("PartName") ?? ""))
7223
+ };
7224
+ }
6900
7225
  /**
6901
7226
  * 结构自检(AC-42.27)。
6902
7227
  *
@@ -6910,9 +7235,7 @@ function auditOoxmlPackage(pkg) {
6910
7235
  const problems = [];
6911
7236
  const contentTypes = pkg.get(CONTENT_TYPES);
6912
7237
  if (contentTypes === void 0) return [`missing ${CONTENT_TYPES}`];
6913
- const types = new TextDecoder().decode(contentTypes);
6914
- const defaults = new Set([...types.matchAll(/<Default\s+Extension="([^"]+)"/g)].map((match) => (match[1] ?? "").toLowerCase()));
6915
- const overrides = new Set([...types.matchAll(/<Override\s+PartName="([^"]+)"/g)].map((match) => match[1]));
7238
+ const { defaults, overrides } = declaredTypes(new TextDecoder().decode(contentTypes));
6916
7239
  for (const path of pkg.keys()) {
6917
7240
  if (path === CONTENT_TYPES) continue;
6918
7241
  if (!defaults.has(extensionOf(path)) && !overrides.has(`/${path}`)) problems.push(`part is not covered by [Content_Types].xml: ${path}`);
@@ -6950,7 +7273,7 @@ function ensureDefaultExtension(pkg, extension, contentType) {
6950
7273
  const bytes = pkg.get(CONTENT_TYPES);
6951
7274
  if (bytes === void 0) return;
6952
7275
  const text = new TextDecoder().decode(bytes);
6953
- if (new RegExp(`<Default\\s+Extension="${extension}"`, "i").test(text)) return;
7276
+ if (declaredTypes(text).defaults.has(extension.toLowerCase())) return;
6954
7277
  const { declaration, body } = splitXmlDeclaration(text);
6955
7278
  const inserted = body.replace("<Default", `<Default Extension="${extension}" ContentType="${contentType}"/><Default`);
6956
7279
  pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + inserted));
@@ -7326,6 +7649,25 @@ function rowsOf(doc) {
7326
7649
  const sheetData = byTag$1(doc, "sheetData")[0];
7327
7650
  return sheetData === void 0 ? [] : [...sheetData.children].filter((node) => node.localName === "row");
7328
7651
  }
7652
+ const rowIndexes = /* @__PURE__ */ new WeakMap();
7653
+ /** 逐格写入时每次线性扫全表是 O(格数×行数):2 万行的表加一列实测 160 s。只在插行之后建 */
7654
+ function rowIndexOf(doc) {
7655
+ let index = rowIndexes.get(doc);
7656
+ if (index === void 0) {
7657
+ index = {
7658
+ rows: /* @__PURE__ */ new Map(),
7659
+ last: 0
7660
+ };
7661
+ for (const row of rowsOf(doc)) {
7662
+ const number = rowNumberOf(row);
7663
+ if (Number.isNaN(number)) continue;
7664
+ if (!index.rows.has(number)) index.rows.set(number, row);
7665
+ index.last = Math.max(index.last, number);
7666
+ }
7667
+ rowIndexes.set(doc, index);
7668
+ }
7669
+ return index;
7670
+ }
7329
7671
  /** 把一个元素上的行号整体平移:`<row r>` 与其中每个 `<c r>` */
7330
7672
  function shiftRow(row, delta) {
7331
7673
  const next = rowNumberOf(row) + delta;
@@ -7498,7 +7840,7 @@ function fillCell(doc, address, value, guards) {
7498
7840
  code: "TEMPLATE_CELL_IS_COVERED",
7499
7841
  reason: `Cell ${normalized} is covered by the merged region ${rangeLabel(covering.start, covering.end)}, so anything written there is never displayed. Write ${formatCellAddress(covering.start)} instead.`
7500
7842
  };
7501
- const row = rowsOf(doc).find((candidate) => rowNumberOf(candidate) === parsed.row + 1) ?? createRow(doc, parsed.row + 1);
7843
+ const row = rowIndexOf(doc).rows.get(parsed.row + 1) ?? createRow(doc, parsed.row + 1);
7502
7844
  let cell = byTag$1(row, "c").find((candidate) => candidate.getAttribute("r") === normalized);
7503
7845
  if (cell === void 0) cell = createCell(doc, row, parsed.column, normalized);
7504
7846
  const formula = [...cell.children].find((child) => child.localName === "f");
@@ -7558,8 +7900,11 @@ function createRow(doc, rowNumber) {
7558
7900
  if (sheetData === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", "The worksheet part has no sheetData element.");
7559
7901
  const row = doc.createElementNS(MAIN_NS, "row");
7560
7902
  row.setAttribute("r", String(rowNumber));
7561
- const anchor = rowsOf(doc).find((candidate) => rowNumberOf(candidate) > rowNumber) ?? null;
7903
+ const index = rowIndexOf(doc);
7904
+ const anchor = rowNumber > index.last ? null : rowsOf(doc).find((candidate) => rowNumberOf(candidate) > rowNumber) ?? null;
7562
7905
  sheetData.insertBefore(row, anchor);
7906
+ index.rows.set(rowNumber, row);
7907
+ index.last = Math.max(index.last, rowNumber);
7563
7908
  return row;
7564
7909
  }
7565
7910
  function createCell(doc, row, column, address) {