@webskill/sdk 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/abnfDiagram-O67JEVCF-DtYFjfqO.js +109 -0
  2. package/dist/agent.d.ts +3 -3
  3. package/dist/agent.js +1012 -130
  4. package/dist/{approval-DwN2o2QG.js → approval-DWQlDPbY.js} +102 -16
  5. package/dist/arc-8U_6NQWm.js +130 -0
  6. package/dist/architecture-7GRP2DOG-RTw1wpAq.js +4 -0
  7. package/dist/architectureDiagram-NJMV4G6O-XmmXrJAK.js +7473 -0
  8. package/dist/array-DQfpQNY8.js +8 -0
  9. package/dist/blockDiagram-BEXU5L5S-D4yjnCD1.js +2582 -0
  10. package/dist/browser.d.ts +444 -48
  11. package/dist/browser.js +1771 -91
  12. package/dist/c4Diagram-YGBWAQC7-BLmA_JSd.js +3668 -0
  13. package/dist/channel-CZxxLrG7.js +9 -0
  14. package/dist/chunk-2Q5K7J3B-Ck03gkFR.js +21 -0
  15. package/dist/chunk-3FUC2YCW-tr6jmbQf.js +530 -0
  16. package/dist/chunk-5DYCD2WN-Bt97YGTd.js +58 -0
  17. package/dist/chunk-5VM5RSS4-BBxa48HL.js +21 -0
  18. package/dist/chunk-6AEJRKK7-JXK89I-O.js +14 -0
  19. package/dist/chunk-742MDFTN-CPmgCGwC.js +466 -0
  20. package/dist/chunk-7INBJB4K-C9jq9o66.js +6552 -0
  21. package/dist/chunk-7M6MHVWA-BrKCWcR1.js +4894 -0
  22. package/dist/chunk-7PRAP22T-BHED6GVs.js +95 -0
  23. package/dist/chunk-DUW6YSOI-DoZj78PO.js +19 -0
  24. package/dist/chunk-FOHPRMQF-Dkbp819z.js +27189 -0
  25. package/dist/chunk-GNY47TPC-Bpvq1T49.js +87 -0
  26. package/dist/chunk-GTNCS2PH-CLmh4I2c.js +686 -0
  27. package/dist/chunk-GWA4HPMP-B6vN3fkL.js +95 -0
  28. package/dist/chunk-J5ZVWO5B-Bi_Pcobw.js +26 -0
  29. package/dist/chunk-JWPE2WC7-D1rC79GS.js +12 -0
  30. package/dist/chunk-LNGE3PJU-D5OIoe3Z.js +2582 -0
  31. package/dist/chunk-MBY4JIJT-C34L7a4f.js +2276 -0
  32. package/dist/chunk-NETBCI7D-Cg84k8N8.js +321 -0
  33. package/dist/chunk-O7XYJQB3-B6K0KOSL.js +8846 -0
  34. package/dist/chunk-UA2S7LBM-DxHM29_K.js +612 -0
  35. package/dist/chunk-WEXAMYUT-Ba6jFWhB.js +33 -0
  36. package/dist/chunk-XXDRQBXY-DPbSy4kg.js +12 -0
  37. package/dist/chunk-Y2CYZVJY-U3QOMqrW.js +15 -0
  38. package/dist/chunk-Z7XXMR3K-oLg1h8F3.js +1038 -0
  39. package/dist/chunk-ZIGJFQKS-DqEFBuHR.js +2215 -0
  40. package/dist/classDiagram-v2-NBCMYWYE-BqnOWIUt.js +3845 -0
  41. package/dist/cose-bilkent-JH36ORCC-C0dTAHVK.js +4270 -0
  42. package/dist/cynefin-OW5HDTMX-B2QvgEQj.js +4 -0
  43. package/dist/cynefinDiagram-VND7K2PF-yAWc6Z3W.js +479 -0
  44. package/dist/cytoscape.esm-C64mRh9B.js +28325 -0
  45. package/dist/dagre-6A5THRUB-D0w2W27v.js +4816 -0
  46. package/dist/defaultLocale-B_EoOkSF.js +249 -0
  47. package/dist/diagram-22UHCM2B-DvgzhUzP.js +6115 -0
  48. package/dist/diagram-3UASUU5V-DWgUB-ef.js +795 -0
  49. package/dist/diagram-ATOU4E4O-P1olqwIs.js +553 -0
  50. package/dist/diagram-CDSNMT55-BUlZuLUe.js +402 -0
  51. package/dist/diagram-MLGK6HIB-6w5qmkGt.js +186 -0
  52. package/dist/diagram-MPIPVDR6-DEFiSo8o.js +289 -0
  53. package/dist/dist-UajCDt3n.js +92 -0
  54. package/dist/ebnfDiagram-ZINNZB2B-CI8liCfR.js +123 -0
  55. package/dist/elk-276RUBZZ-3SvW6OAK.js +90034 -0
  56. package/dist/erDiagram-OPXOYQCR-CPTbKhwP.js +2301 -0
  57. package/dist/eventmodeling-NTZA5JFV-BlyuqGp0.js +4 -0
  58. package/dist/flowDiagram-KWPJA3E3-CcUJ6HbV.js +18 -0
  59. package/dist/ganttDiagram-FUAMR5RP-31UjY0KK.js +3939 -0
  60. package/dist/{toolSource-C9oNuKmr.js → geometry-wx5ZTYzi.js} +162 -12
  61. package/dist/gitGraph-4MIJSDKK-ChdVlrk7.js +4 -0
  62. package/dist/gitGraphDiagram-X574FWY7-DKuxJymB.js +1153 -0
  63. package/dist/governance.d.ts +3 -3
  64. package/dist/graphlib-tk9gQYuN.js +4194 -0
  65. package/dist/{index-B8Tgmpwh.d.ts → index-BiO9_XRk.d.ts} +2 -2
  66. package/dist/{index-CYLt5u4L.d.ts → index-CjwdZQOS.d.ts} +700 -12
  67. package/dist/{index-DHopiH_m.d.ts → index-YPd8ZSEa.d.ts} +156 -9
  68. package/dist/index.d.ts +4 -4
  69. package/dist/index.js +6 -6
  70. package/dist/info-A6RAGUB7-CxRAt9S9.js +4 -0
  71. package/dist/infoDiagram-VRGFBTTK-pRspGfjZ.js +25 -0
  72. package/dist/init-CMOIWdg-.js +16 -0
  73. package/dist/ishikawaDiagram-OU5B5YK6-DKerAPAg.js +975 -0
  74. package/dist/journeyDiagram-ZHPQQLJL-C4MK-YO5.js +1227 -0
  75. package/dist/kanban-definition-PNTS6WVX-BqsZpMXx.js +1244 -0
  76. package/dist/katex-BcRRvyh_.js +26465 -0
  77. package/dist/{kind-DaKqLX2F.js → kind-Dc8x0HWz.js} +54 -7
  78. package/dist/line-LgV8iVMV.js +48 -0
  79. package/dist/linear-BWKR0Tqd.js +393 -0
  80. package/dist/linkedDocument-CIuh_Yp0.js +416 -0
  81. package/dist/mcp.d.ts +2 -2
  82. package/dist/{memoryArtifactStore-D6dLeY55.js → memoryArtifactStore-Bs7glPqm.js} +14 -0
  83. package/dist/mermaid-parser.core-pYR5siSx.js +754 -0
  84. package/dist/mermaid.core-DMtwGCHy.js +1756 -0
  85. package/dist/mindmap-definition-NLK3R4M7-CXbD-myG.js +1312 -0
  86. package/dist/node.d.ts +3 -3
  87. package/dist/node.js +2 -2
  88. package/dist/{openUiLibrary-b0i7F_oZ.js → openUiLibrary-B7j3rLrm.js} +1 -1
  89. package/dist/{openUiSpecLang-CLXJEeRU.js → openUiSpecLang-BSYYnjay.js} +1 -1
  90. package/dist/ordinal-CxA6GHQS.js +84 -0
  91. package/dist/packet-AYTQ26CC-BOm08cxl.js +4 -0
  92. package/dist/path-C27Z2Bjv.js +106 -0
  93. package/dist/pegDiagram-GJSIUBJH-BLGJOewQ.js +115 -0
  94. package/dist/pie-WAS4IAKB-Dvg7glpk.js +4 -0
  95. package/dist/pieDiagram-5QR66LMP-laoaV_PB.js +298 -0
  96. package/dist/quadrantDiagram-O4NWA36T-Db_wFBkT.js +2236 -0
  97. package/dist/radar-RG4KPBEZ-BZDaTu06.js +4 -0
  98. package/dist/railroad-74A4TZTK-C-UhjkOy.js +4 -0
  99. package/dist/railroad-abnf-HS5TGJTU-dUKVA4OS.js +4 -0
  100. package/dist/railroad-ebnf-LZEXJU2U-DzQumk18.js +4 -0
  101. package/dist/railroad-peg-WCYAUIDC-B3vvTWed.js +4 -0
  102. package/dist/railroadDiagram-XR7U4H2S-BY0sTM4X.js +89 -0
  103. package/dist/requirementDiagram-PLB6GJNP-3rkQfEhs.js +2474 -0
  104. package/dist/rough.cjs-AmDd_NQk.js +1395 -0
  105. package/dist/sankeyDiagram-IPEJSGJF-CGAJhyG2.js +3578 -0
  106. package/dist/sequenceDiagram-PO4LG4MO-9FJjFX06.js +5522 -0
  107. package/dist/sizeCapture-INFHLROL-D8PWxdOn.js +56 -0
  108. package/dist/{skillVersionStore-B24Jjjry-BkOk9wI_.d.ts → skillVersionStore-B24Jjjry-BY1H7Krp.d.ts} +1 -1
  109. package/dist/src-NkhI3UMl.js +2653 -0
  110. package/dist/stateDiagram-v2-GCMORJYK-B0O3wc1-.js +2715 -0
  111. package/dist/swimlanes-2SLR337P-D4EOnrpu.js +6637 -0
  112. package/dist/swimlanesDiagram-TC7HE7FX-BUx29EJ9.js +30 -0
  113. package/dist/testing.d.ts +2 -1
  114. package/dist/testing.js +1 -1
  115. package/dist/timeline-definition-EJHVYXUP-CrgiiB4C.js +1513 -0
  116. package/dist/treeView-Q6P3EWNA-C1by-Gzc.js +4 -0
  117. package/dist/treemap-WGGIJYW6-mC2NZ6ad.js +4 -0
  118. package/dist/{types-BvTV_05--BW8zDuEk.d.ts → types-CpDRZ0rA-BM5FI9oN.d.ts} +59 -8
  119. package/dist/ui-react.d.ts +3 -3
  120. package/dist/ui-react.js +3718 -77
  121. package/dist/ui-vue.d.ts +1 -1
  122. package/dist/ui.d.ts +3 -3
  123. package/dist/ui.js +2 -2
  124. package/dist/usecaseDiagram-POWQR4AR-BUt_TQOY.js +14397 -0
  125. package/dist/vennDiagram-UO4OBE2U-CwXr0wjA.js +2808 -0
  126. package/dist/wardley-WFR3VGLG-CVSYjq_S.js +4 -0
  127. package/dist/wardleyDiagram-VNRHLVJA-Cpfd47dQ.js +874 -0
  128. package/dist/{webSkillApi-Cib6G-94.js → webSkillApi-Dy-Zjv0y.js} +1 -1
  129. package/dist/{webskillLitCatalog-W06npwej.js → webskillLitCatalog-D5BCiMCU.js} +1 -1
  130. package/dist/xychartDiagram-PMCCYNJV-e1TeVoc6.js +2704 -0
  131. package/package.json +1 -1
  132. package/dist/linkedDocument-C0gj1sMq.js +0 -245
package/dist/browser.js CHANGED
@@ -1,14 +1,15 @@
1
- import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
1
+ import { n as messageOf$1, t as WebSkillError } from "./errors-BDZNpC13.js";
2
2
  import { A as SKILL_MANIFEST_FILE, D as verifySkillSignature, M as buildManifest, O as MANIFEST_EXCLUDED_FILES, P as verifyManifest, b as parseSkillMarkdown, c as unzipWithLimits, f as SKILL_PACK_FILE, i as buildCatalog, k as SKILLS_LOCKFILE, m as parseSkillPackManifest, n as SkillDiscovery, o as readResponseWithLimit, p as exportSkills, t as validateSkills, u as detectSkillArchiveShapeFromFs, w as readSkillSignature, y as isValidSkillName } from "./skill-CAJMsLod.js";
3
3
  import { a as atomicWriteText, o as isAtomicTempPath, r as resolveInsideRoot } from "./pathSecurity-B1owvJAF.js";
4
4
  import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
5
- import { t as ATTACHMENT_TEXT_LIMIT } from "./kind-DaKqLX2F.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-DwN2o2QG.js";
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";
7
7
  import { a as GoogleGenAiClient, o as AnthropicClient, s as OpenAiCompatibleClient, t as MockLlmClient } from "./llm-eIQNO9tr.js";
8
- import { o as toBase64$1, u as SPREADSHEET_MIME_TYPE } from "./linkedDocument-C0gj1sMq.js";
8
+ import { c as toBase64$1, f as SPREADSHEET_MIME_TYPE } from "./linkedDocument-CIuh_Yp0.js";
9
9
  import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
10
- import { t as createWebSkillApi } from "./webSkillApi-Cib6G-94.js";
11
- import { _ as frameSteps, a as defersImage, c as isPdfTextTrustworthy, g as frameLabel, h as toFrameScopes, m as toActionFrameScopes, o as takesImage, t as PDF_LARGE_IMAGE_RATIO } from "./toolSource-C9oNuKmr.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";
12
+ import { zipSync } from "fflate";
12
13
 
13
14
  //#region ../browser/src/fs/featureDetection.ts
14
15
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -542,7 +543,7 @@ var TsTranspiler = class {
542
543
  try {
543
544
  esbuild = await loadEsbuild(this.#options);
544
545
  } catch (e) {
545
- throw new WebSkillError("TS_TRANSPILER_UNAVAILABLE", `Failed to load the TypeScript transpiler from "${this.#options.esbuildUrl}": ${messageOf(e)}`, e);
546
+ throw new WebSkillError("TS_TRANSPILER_UNAVAILABLE", `Failed to load the TypeScript transpiler from "${this.#options.esbuildUrl}": ${messageOf$1(e)}`, e);
546
547
  }
547
548
  let code;
548
549
  try {
@@ -552,7 +553,7 @@ var TsTranspiler = class {
552
553
  format: "esm"
553
554
  })).code;
554
555
  } catch (e) {
555
- throw new WebSkillError("TS_TRANSPILE_FAILED", `Failed to transpile ${scriptPath}: ${messageOf(e)}`, e);
556
+ throw new WebSkillError("TS_TRANSPILE_FAILED", `Failed to transpile ${scriptPath}: ${messageOf$1(e)}`, e);
556
557
  }
557
558
  await this.#fs.writeText(cacheKey, code);
558
559
  return code;
@@ -625,7 +626,7 @@ const PRESERVED_INSTALL_CODES = /* @__PURE__ */ new Set([
625
626
  "SIGNATURE_UNSUPPORTED",
626
627
  "SKILL_DUPLICATE_NAME"
627
628
  ]);
628
- const asInstallFailed = (e) => e instanceof WebSkillError && (e.code === "INSTALL_FAILED" || e.code === "TOOL_UNSUPPORTED" || PRESERVED_INSTALL_CODES.has(e.code)) ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
629
+ const asInstallFailed = (e) => e instanceof WebSkillError && (e.code === "INSTALL_FAILED" || e.code === "TOOL_UNSUPPORTED" || PRESERVED_INSTALL_CODES.has(e.code)) ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf$1(e)}`, e);
629
630
  const lockfilePath = (root) => `${root}/${SKILLS_LOCKFILE}`;
630
631
  /** 未注入信任库时的空实现:策略仍然生效,只是没有任何受信公钥 */
631
632
  const EMPTY_TRUSTED_KEYS = {
@@ -696,7 +697,7 @@ var BrowserSkillManager = class {
696
697
  res = await (this.#fetchImpl ?? fetch)(url.href);
697
698
  } catch (e) {
698
699
  if (e instanceof WebSkillError) throw e;
699
- throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf(e)}`, e);
700
+ throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
700
701
  }
701
702
  if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
702
703
  data = await readResponseWithLimit(res, this.#archiveLimits);
@@ -725,7 +726,7 @@ var BrowserSkillManager = class {
725
726
  name = metadata.name;
726
727
  version = typeof metadata["version"] === "string" ? metadata["version"] : void 0;
727
728
  } catch (e) {
728
- throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md: ${messageOf(e)}`, e);
729
+ throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md: ${messageOf$1(e)}`, e);
729
730
  }
730
731
  if (!isValidSkillName(name)) throw new WebSkillError("INSTALL_FAILED", `Invalid skill name in SKILL.md (path traversal rejected): ${JSON.stringify(name)}`);
731
732
  if (options?.replace === "reject" && await fs.exists(`${this.#managedRoot}/${name}`)) throw new WebSkillError("SKILL_DUPLICATE_NAME", `A skill named "${name}" is already installed. Installing it again replaces the existing files; use the packages flow if that is intended.`);
@@ -797,7 +798,7 @@ var BrowserSkillManager = class {
797
798
  name = metadata.name;
798
799
  versions.set(name, typeof metadata["version"] === "string" ? metadata["version"] : void 0);
799
800
  } catch (e) {
800
- throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md of pack entry "${entry.name}": ${messageOf(e)}`, e);
801
+ throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md of pack entry "${entry.name}": ${messageOf$1(e)}`, e);
801
802
  }
802
803
  if (name !== entry.name) throw new WebSkillError("INSTALL_FAILED", `Skill pack entry "${entry.name}" does not match the SKILL.md name "${name}"`);
803
804
  await copyDir(fs, skillDir, `${finalRoot}/${name}`);
@@ -899,7 +900,7 @@ var BrowserSkillManager = class {
899
900
  await this.#removeLockEntry(name);
900
901
  this.#onChanged?.();
901
902
  } catch (e) {
902
- throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
903
+ throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf$1(e)}`, e);
903
904
  }
904
905
  }
905
906
  async verifyIntegrity(name) {
@@ -919,7 +920,7 @@ var BrowserSkillManager = class {
919
920
  try {
920
921
  return JSON.parse(await this.#fs.readText(path));
921
922
  } catch (e) {
922
- throw new WebSkillError("INTEGRITY_FAILED", `Skills lockfile at ${path} is corrupted: ${messageOf(e)}`, e);
923
+ throw new WebSkillError("INTEGRITY_FAILED", `Skills lockfile at ${path} is corrupted: ${messageOf$1(e)}`, e);
923
924
  }
924
925
  }
925
926
  /**
@@ -975,7 +976,7 @@ var BrowserSkillManager = class {
975
976
  try {
976
977
  return JSON.parse(await this.#fs.readText(path));
977
978
  } catch (e) {
978
- throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${messageOf(e)}`, e);
979
+ throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${messageOf$1(e)}`, e);
979
980
  }
980
981
  }
981
982
  async #upsertLockEntry(name, entry) {
@@ -1008,7 +1009,7 @@ const DEFAULT_BINARY_EXTENSIONS = [
1008
1009
  ];
1009
1010
  const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
1010
1011
  const joinUrl = (baseUrl, path) => `${baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`}${path.replace(/^\/+/, "")}`;
1011
- const extensionOf = (file) => {
1012
+ const extensionOf$1 = (file) => {
1012
1013
  const dot = file.lastIndexOf(".");
1013
1014
  return dot === -1 ? "" : file.slice(dot + 1).toLowerCase();
1014
1015
  };
@@ -1043,7 +1044,7 @@ async function seedSkillsFromHttp(fs, options) {
1043
1044
  const url = joinUrl(baseUrl, `${sourceRoot}/${skill}/${file}`);
1044
1045
  const response = await fetchImpl(url);
1045
1046
  if (!response.ok) throw new Error(`Builtin skill asset missing: ${url} (HTTP ${response.status})`);
1046
- if (binary.has(extensionOf(file))) {
1047
+ if (binary.has(extensionOf$1(file))) {
1047
1048
  await fs.writeBinary(`${skillRoot}/${file}`, new Uint8Array(await response.arrayBuffer()));
1048
1049
  continue;
1049
1050
  }
@@ -1392,7 +1393,7 @@ var BrowserWorkerScriptExecutor = class {
1392
1393
  content: [],
1393
1394
  error: {
1394
1395
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
1395
- message: messageOf(e)
1396
+ message: messageOf$1(e)
1396
1397
  }
1397
1398
  };
1398
1399
  }
@@ -1437,7 +1438,7 @@ var BrowserWorkerScriptExecutor = class {
1437
1438
  content: [],
1438
1439
  error: {
1439
1440
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
1440
- message: messageOf(e)
1441
+ message: messageOf$1(e)
1441
1442
  }
1442
1443
  };
1443
1444
  } finally {
@@ -1472,7 +1473,7 @@ var BrowserWorkerScriptExecutor = class {
1472
1473
  respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
1473
1474
  return;
1474
1475
  }
1475
- onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
1476
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$1(e))));
1476
1477
  return;
1477
1478
  }
1478
1479
  if (data.id !== id || settled) return;
@@ -1590,7 +1591,7 @@ var BrowserWorkerScriptExecutor = class {
1590
1591
  };
1591
1592
  }
1592
1593
  } catch (e) {
1593
- return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
1594
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$1(e));
1594
1595
  }
1595
1596
  }
1596
1597
  };
@@ -1660,10 +1661,10 @@ async function probeChromeBuiltinAvailability() {
1660
1661
  };
1661
1662
  }
1662
1663
  }
1663
- const textOf$1 = (message) => message.content.map((part) => part.type === "text" ? part.text : `[${part.type}]`).join("").trim();
1664
+ const textOf$3 = (message) => message.content.map((part) => part.type === "text" ? part.text : `[${part.type}]`).join("").trim();
1664
1665
  /** tool 角色 Prompt API 不认识,降级成 user 文本——内置模型本来就不做工具调用 */
1665
1666
  function toInitialPrompt(message) {
1666
- const text = textOf$1(message);
1667
+ const text = textOf$3(message);
1667
1668
  if (message.role === "assistant") return {
1668
1669
  role: "assistant",
1669
1670
  content: text
@@ -1674,7 +1675,7 @@ function toInitialPrompt(message) {
1674
1675
  };
1675
1676
  }
1676
1677
  function splitInput(input) {
1677
- const system = input.messages.filter((m) => m.role === "system").map(textOf$1).filter((text) => text !== "");
1678
+ const system = input.messages.filter((m) => m.role === "system").map(textOf$3).filter((text) => text !== "");
1678
1679
  const rest = input.messages.filter((m) => m.role !== "system");
1679
1680
  const last = rest.at(-1);
1680
1681
  return {
@@ -2102,12 +2103,12 @@ async function captureImg(element, id, maxBytes) {
2102
2103
  const inline = splitDataUrl(src);
2103
2104
  if (inline) {
2104
2105
  const bytes = Uint8Array.from(atob(inline.data), (char) => char.charCodeAt(0));
2105
- return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType }), oversized), maxBytes);
2106
+ return toBudget(id, "src", (await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType }), { oversized })).blob, maxBytes);
2106
2107
  }
2107
2108
  const response = await fetch(src, { mode: "cors" });
2108
2109
  if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
2109
2110
  if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
2110
- return toBudget(id, "src", await toDeliverableBlob(await response.blob(), oversized), maxBytes);
2111
+ return toBudget(id, "src", (await toDeliverableBlob(await response.blob(), { oversized })).blob, maxBytes);
2111
2112
  }
2112
2113
  /** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
2113
2114
  async function captureCanvas(element, id, maxBytes) {
@@ -2246,6 +2247,16 @@ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
2246
2247
  "image/gif",
2247
2248
  "image/webp"
2248
2249
  ]);
2250
+ /**
2251
+ * 投放与导出通道的目标集:`docx` 与 `pptxgenjs` 都不吃 WebP,
2252
+ * 而 WebP 在现代网页上遍地都是。归一化放在写进 `src` 之前,
2253
+ * 下游三处永远只见得到 PNG 与 JPEG(设计 12 §4.2)。
2254
+ *
2255
+ * 导出编码器判断「这张图嵌不嵌得进去」必须读同一份清单(0.22.0 FR-14.3):
2256
+ * 各写一遍就会出现「投放时归一成了 A,导出却认为 A 不受支持」。
2257
+ * @experimental
2258
+ */
2259
+ const DOCUMENT_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
2249
2260
  /** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
2250
2261
  const SVG_SNIFF_BYTES = 1024;
2251
2262
  /**
@@ -2308,7 +2319,13 @@ async function rasterizeToPng(blob) {
2308
2319
  const ctx = canvas.getContext("2d");
2309
2320
  if (ctx === null) throw new Error("a 2D canvas context is unavailable");
2310
2321
  ctx.drawImage(image, 0, 0, width, height);
2311
- return await toPngBlob(canvas);
2322
+ return {
2323
+ blob: await toPngBlob(canvas),
2324
+ size: {
2325
+ width,
2326
+ height
2327
+ }
2328
+ };
2312
2329
  } finally {
2313
2330
  URL.revokeObjectURL(url);
2314
2331
  }
@@ -2328,21 +2345,61 @@ async function rasterizeToPng(blob) {
2328
2345
  * - **超长边**:横幅类素材常见 7680×120 这种尺寸,字节数却很小,
2329
2346
  * 纯按字节压缩的预算根本不会触发,原样发出去就撞上端点的边长上限。
2330
2347
  *
2331
- * @param oversized 调用方已知原图长边超限(`<img>` 能直接读到 `naturalWidth`,不必为此解码)
2348
+ * @param options.oversized 调用方已知原图长边超限(`<img>` 能直接读到 `naturalWidth`,不必为此解码)
2349
+ * @param options.targets 目标类型集。模型图片通道与投放/导出通道的可接受集合不同(设计 12 §4.2)
2332
2350
  */
2333
- async function toDeliverableBlob(blob, oversized = false) {
2351
+ async function toDeliverableBlob(blob, options = {}) {
2352
+ const targets = options.targets ?? DELIVERABLE_IMAGE_MIME_TYPES;
2334
2353
  const declared = blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
2335
2354
  const head = new Uint8Array(await blob.slice(0, SVG_SNIFF_BYTES).arrayBuffer());
2336
2355
  const sniffed = sniffImageMime(head);
2337
- if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) {
2356
+ if (sniffed !== void 0 && targets.has(sniffed)) {
2338
2357
  const typed = sniffed === declared ? blob : new Blob([blob], { type: sniffed });
2339
2358
  const animated = sniffed === "image/gif" || sniffed === "image/webp" && isAnimatedWebp(head) || sniffed === "image/png" && isAnimatedPng(head);
2340
- return oversized || animated ? await rasterizeToPng(typed) : typed;
2359
+ return options.oversized === true || animated ? await rasterizeToPng(typed) : { blob: typed };
2341
2360
  }
2342
2361
  const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
2343
2362
  if (mimeType === void 0) throw new Error(`the response is not a recognizable image (the server described it as "${blob.type === "" ? "no content type" : blob.type}")`);
2344
2363
  return await rasterizeToPng(new Blob([blob], { type: mimeType }));
2345
2364
  }
2365
+ /** 直通分支只差尺寸:解一次位图读完立刻释放,任何时刻只持有一张(设计 12 §4.2) */
2366
+ async function imageSizeOf(blob) {
2367
+ if (typeof createImageBitmap !== "function") throw new Error("this environment cannot decode images, so the image dimensions cannot be measured");
2368
+ const bitmap = await createImageBitmap(blob);
2369
+ try {
2370
+ return {
2371
+ width: bitmap.width,
2372
+ height: bitmap.height
2373
+ };
2374
+ } finally {
2375
+ bitmap.close();
2376
+ }
2377
+ }
2378
+ /**
2379
+ * 把任意来源的图片字节归一成投放文档能显示、两个导出编码器都收得下的形态(0.22.0 FR-11.6)。
2380
+ *
2381
+ * 上传图、页面图、外链图在这里走的是**同一个函数**——AC-G1 要的不是三条分支行为一致,
2382
+ * 而是根本只有一条分支,所以这个签名里没有「来源」这个参数。
2383
+ *
2384
+ * 宽高一律实测:像素数是投放载荷预算的硬兜底维度(FR-10.7),采信声明等于让被限制方自报限额。
2385
+ * 失败抛错并带可执行的原因,由调用方转成丢图归因(FR-11.8)。
2386
+ * @experimental
2387
+ */
2388
+ async function toDocumentImage(bytes, options) {
2389
+ const deliverable = await toDeliverableBlob(new Blob([new Uint8Array(bytes)], { type: options.declaredMimeType ?? "" }), { targets: DOCUMENT_IMAGE_MIME_TYPES });
2390
+ const compressed = await compressImageToBudget(deliverable.blob, options.maxBytes);
2391
+ const size = compressed.scaled || deliverable.size === void 0 ? await imageSizeOf(compressed.blob) : deliverable.size;
2392
+ const mimeType = compressed.blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
2393
+ if (!DOCUMENT_IMAGE_MIME_TYPES.has(mimeType)) throw new Error(`the bytes normalized to "${mimeType === "" ? "no content type" : mimeType}", which is not one of ${[...DOCUMENT_IMAGE_MIME_TYPES].join(", ")}`);
2394
+ const buffer = await compressed.blob.arrayBuffer();
2395
+ return {
2396
+ data: base64Of(new Uint8Array(buffer)),
2397
+ mimeType,
2398
+ width: size.width,
2399
+ height: size.height,
2400
+ bytes: compressed.compressedBytes
2401
+ };
2402
+ }
2346
2403
  /**
2347
2404
  * 取一个元素的图像(0.6.0 FR-12.1)。
2348
2405
  *
@@ -2381,16 +2438,169 @@ async function toDeliverableBlob(blob, oversized = false) {
2381
2438
  }
2382
2439
  return {
2383
2440
  id,
2384
- reason: `<${element.tagName.toLowerCase()}> is not a capturable element`,
2441
+ reason: `<${element.tagName.toLowerCase()}> is not a capturable element; only ${[...CAPTURABLE_TAGS].map((t) => `<${t.toLowerCase()}>`).join(", ")} can be captured`,
2385
2442
  triedLevels: []
2386
2443
  };
2387
2444
  }
2445
+ /**
2446
+ * 取一张宿主页面上的图,并归一成投放 / 导出通道吃得下的形态(0.22.0 FR-12.1)。
2447
+ *
2448
+ * 取像本身**原样复用** `captureElementImage`:两级策略与「互不兜底」的裁决 D-3 一字未改。
2449
+ * 这里只把归一化的目标集换成 `png` / `jpeg`——产物一旦落盘就该是下游直接能用的格式,
2450
+ * 否则「投放看得见、导出才消失」会以另一种形态回来。
2451
+ * @experimental
2452
+ */
2453
+ async function captureDocumentImage(element, options) {
2454
+ const captured = await captureElementImage(element, options);
2455
+ if (isCaptureFailure(captured)) return captured;
2456
+ try {
2457
+ return await toDocumentImage(Uint8Array.from(atob(captured.data), (c) => c.charCodeAt(0)), {
2458
+ maxBytes: options.maxBytes,
2459
+ declaredMimeType: captured.mimeType
2460
+ });
2461
+ } catch (e) {
2462
+ return {
2463
+ id: options.id,
2464
+ reason: `the captured bytes could not be converted for documents: ${describeError(e)}`,
2465
+ triedLevels: [captured.level]
2466
+ };
2467
+ }
2468
+ }
2388
2469
  function describeError(e) {
2389
2470
  if (e instanceof DOMException && e.name === "SecurityError") return "the canvas is tainted by cross-origin data";
2390
2471
  if (e instanceof TypeError) return "the request was blocked, most likely by CORS";
2391
2472
  return e instanceof Error ? e.message : String(e);
2392
2473
  }
2393
2474
 
2475
+ //#endregion
2476
+ //#region ../browser/src/net/fetchImage.ts
2477
+ /** 单次抓取超时(设计 14 §5.1)。一个不响应的 URL 不得把整个 run 拖死 @experimental */
2478
+ const REMOTE_IMAGE_TIMEOUT_MS = 15e3;
2479
+ /** 单张外链图的原始字节上界(设计 14 §5.1)。超限先压,压不下去才丢 @experimental */
2480
+ const MAX_REMOTE_IMAGE_BYTES = 32 * 1024 * 1024;
2481
+ /**
2482
+ * 把一次抓取失败归档(AC-13.4)。
2483
+ *
2484
+ * 纯函数:实测表明扩展上 `http` 全通(设计 14 §1),「被浏览器阻断」在 e2e 里
2485
+ * 根本构造不出来,所以这一档的正确性只能由单测按真实错误形态喂进来验。
2486
+ * @experimental
2487
+ */
2488
+ function classifyFetchFailure(error, response) {
2489
+ if (response !== void 0 && !response.ok) return "refused";
2490
+ if (error instanceof DOMException && error.name === "AbortError") return "timeout";
2491
+ if (error instanceof Error && error.name === "TimeoutError") return "timeout";
2492
+ if (error instanceof TypeError) {
2493
+ const text = error.message.toLowerCase();
2494
+ if (text.includes("blocked") || text.includes("mixed content") || text.includes("cors")) return "blocked";
2495
+ return "unreachable";
2496
+ }
2497
+ return "unreachable";
2498
+ }
2499
+ /** 分类 → 一句能照着做的话。措辞面向模型,因此一律英文(AGENTS.md) */
2500
+ function reasonOf(kind, url, detail) {
2501
+ const host = hostOf(url);
2502
+ switch (kind) {
2503
+ case "blocked": return `the browser blocked the request to ${host}${detail}; an https:// address is usually allowed where an http:// one is not`;
2504
+ case "unreachable": return `${host} could not be reached${detail}; check the address or use a different image`;
2505
+ case "refused": return `${host} refused to serve the image${detail}`;
2506
+ case "not-an-image": return `${host} answered, but the response is not an image${detail}`;
2507
+ case "too-large": return `the image at ${host} is too large${detail}`;
2508
+ case "timeout": return `${host} did not answer${detail}`;
2509
+ }
2510
+ }
2511
+ function hostOf(url) {
2512
+ try {
2513
+ return new URL(url).host;
2514
+ } catch {
2515
+ return url;
2516
+ }
2517
+ }
2518
+ /**
2519
+ * 抓一张外链图并归一成可直接写进文档的形态(分册 13 FR-13.1 ~ FR-13.5)。
2520
+ *
2521
+ * **授权不在这里**:调用方必须在调用之前拿到用户同意(AC-13.5 要求断言网络层零请求,
2522
+ * 所以判定必须发生在 `fetch` 之前,不能靠拿到响应再丢弃)。
2523
+ *
2524
+ * SSRF 由 `assertRemoteUrlAllowed` 原样兜住,不另写一份(FR-13.5)。已知口子:
2525
+ * 它校验的是**初始 URL**,跟随重定向落到私有网段它管不着(见 docs/deferred-items.md)。
2526
+ * @experimental
2527
+ */
2528
+ async function fetchRemoteImage(url, options) {
2529
+ const maxSourceBytes = options.maxSourceBytes ?? 33554432;
2530
+ const timeoutMs = options.timeoutMs ?? 15e3;
2531
+ const doFetch = options.fetch ?? globalThis.fetch;
2532
+ try {
2533
+ assertRemoteUrlAllowed(url, { allowHttp: true });
2534
+ } catch (e) {
2535
+ if (e instanceof WebSkillError) throw e;
2536
+ throw new WebSkillError("VALIDATION_FAILED", `Not a usable image URL: ${JSON.stringify(url)}`);
2537
+ }
2538
+ const controller = new AbortController();
2539
+ const timer = setTimeout(() => {
2540
+ controller.abort();
2541
+ }, timeoutMs);
2542
+ let response;
2543
+ try {
2544
+ response = await doFetch(url, {
2545
+ signal: controller.signal,
2546
+ redirect: "follow"
2547
+ });
2548
+ if (!response.ok) return {
2549
+ kind: "refused",
2550
+ reason: reasonOf("refused", url, ` (HTTP ${String(response.status)})`)
2551
+ };
2552
+ const declared = Number(response.headers.get("content-length") ?? NaN);
2553
+ if (Number.isFinite(declared) && declared > maxSourceBytes) return {
2554
+ kind: "too-large",
2555
+ reason: reasonOf("too-large", url, ` (${describeBytes(declared)}, limit ${describeBytes(maxSourceBytes)})`)
2556
+ };
2557
+ const blob = await response.blob();
2558
+ if (blob.size > maxSourceBytes) return {
2559
+ kind: "too-large",
2560
+ reason: reasonOf("too-large", url, ` (${describeBytes(blob.size)}, limit ${describeBytes(maxSourceBytes)})`)
2561
+ };
2562
+ const bytes = new Uint8Array(await blob.arrayBuffer());
2563
+ try {
2564
+ return await toDocumentImage(bytes, {
2565
+ maxBytes: options.maxBytes,
2566
+ declaredMimeType: blob.type
2567
+ });
2568
+ } catch (e) {
2569
+ return {
2570
+ kind: "not-an-image",
2571
+ reason: reasonOf("not-an-image", url, `: ${messageOf(e)}`)
2572
+ };
2573
+ }
2574
+ } catch (e) {
2575
+ if (e instanceof WebSkillError) throw e;
2576
+ const kind = classifyFetchFailure(e, response);
2577
+ return {
2578
+ kind,
2579
+ reason: reasonOf(kind, url, kind === "timeout" ? ` within ${String(Math.round(timeoutMs / 1e3))}s` : ` (${messageOf(e)})`)
2580
+ };
2581
+ } finally {
2582
+ clearTimeout(timer);
2583
+ }
2584
+ }
2585
+ /** @experimental */
2586
+ function isRemoteImageFailure(result) {
2587
+ return "kind" in result;
2588
+ }
2589
+ /** @experimental */
2590
+ function createRemoteImageHost(options = {}) {
2591
+ return { fetch: (url, maxBytes) => fetchRemoteImage(url, {
2592
+ maxBytes,
2593
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
2594
+ }) };
2595
+ }
2596
+ function describeBytes(bytes) {
2597
+ const mb = bytes / (1024 * 1024);
2598
+ return mb >= 1 ? `${mb.toFixed(1)} MB` : `${String(Math.round(bytes / 1024))} KB`;
2599
+ }
2600
+ function messageOf(error) {
2601
+ return error instanceof Error ? error.message : String(error);
2602
+ }
2603
+
2394
2604
  //#endregion
2395
2605
  //#region ../browser/src/dom/promotion.ts
2396
2606
  /**
@@ -2789,11 +2999,18 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
2789
2999
  };
2790
3000
  const ref = handles?.issue(element, role, frame);
2791
3001
  if (ref !== void 0) node.ref = ref;
2792
- if (capture !== void 0 && isCapturableElement(element)) if (isBelowMinArea(element, capture.minImageArea)) node.imageNote = ICON_SKIPPED_NOTE;
2793
- else capture.targets.push({
2794
- element,
2795
- node
2796
- });
3002
+ if (isCapturableElement(element)) {
3003
+ const captureRef = node.ref ?? handles?.issueCapture(element, frame);
3004
+ if (captureRef !== void 0) {
3005
+ node.ref = captureRef;
3006
+ node.capturable = true;
3007
+ }
3008
+ if (capture !== void 0) if (isBelowMinArea(element, capture.minImageArea)) node.imageNote = ICON_SKIPPED_NOTE;
3009
+ else capture.targets.push({
3010
+ element,
3011
+ node
3012
+ });
3013
+ }
2797
3014
  return node;
2798
3015
  }
2799
3016
  /**
@@ -2908,6 +3125,21 @@ var HandleIssuer = class {
2908
3125
  });
2909
3126
  return known;
2910
3127
  }
3128
+ return this.#mint(element, kind, frame);
3129
+ }
3130
+ /**
3131
+ * 取像句柄(0.22.0 FR-12.1)。**不过操作范围那道闸**:取像是读,而该元素已经在
3132
+ * 感知输出里,读它的授权就是读页面的授权;写的授权另有 `capture_page_image` 的卡。
3133
+ *
3134
+ * 已有句柄就原样复用且**不改 kind**:一个被提升成按钮的 `<img>` 仍然点得动。
3135
+ */
3136
+ issueCapture(element, frame) {
3137
+ if (this.#excluded.has(element)) return void 0;
3138
+ const known = this.#refByElement.get(element);
3139
+ if (known !== void 0 && this.#table.has(known)) return known;
3140
+ return this.#mint(element, "image", frame);
3141
+ }
3142
+ #mint(element, kind, frame) {
2911
3143
  const bytes = /* @__PURE__ */ new Uint8Array(8);
2912
3144
  crypto.getRandomValues(bytes);
2913
3145
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -3382,7 +3614,7 @@ function hidden(element) {
3382
3614
  }
3383
3615
  const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
3384
3616
  const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
3385
- const textOf = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
3617
+ const textOf$2 = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
3386
3618
  /**
3387
3619
  * 可访问名称,按 accname 的优先级取。
3388
3620
  *
@@ -3394,19 +3626,19 @@ function accessibleName(element) {
3394
3626
  const labelledBy = element.getAttribute("aria-labelledby");
3395
3627
  if (labelledBy !== null && labelledBy.trim() !== "") {
3396
3628
  const doc = element.ownerDocument;
3397
- const joined = labelledBy.split(/\s+/).map((id) => textOf(doc.getElementById(id))).filter((part) => part !== "").join(" ");
3629
+ const joined = labelledBy.split(/\s+/).map((id) => textOf$2(doc.getElementById(id))).filter((part) => part !== "").join(" ");
3398
3630
  if (joined !== "") return joined;
3399
3631
  }
3400
3632
  const label = element.getAttribute("aria-label");
3401
3633
  if (label !== null && label.trim() !== "") return label.trim();
3402
3634
  const labels = element.labels;
3403
3635
  if (labels && labels.length > 0) {
3404
- const joined = [...labels].map((node) => textOf(node)).filter((part) => part !== "").join(" ");
3636
+ const joined = [...labels].map((node) => textOf$2(node)).filter((part) => part !== "").join(" ");
3405
3637
  if (joined !== "") return joined;
3406
3638
  }
3407
- const own = textOf(element);
3639
+ const own = textOf$2(element);
3408
3640
  if (own !== "") return own;
3409
- const wrapping = textOf(element.closest("label"));
3641
+ const wrapping = textOf$2(element.closest("label"));
3410
3642
  if (wrapping !== "") return wrapping;
3411
3643
  const placeholder = element.getAttribute("placeholder");
3412
3644
  if (placeholder !== null && placeholder.trim() !== "") return placeholder.trim();
@@ -3519,6 +3751,187 @@ async function waitFor(probe, timeoutMs = 2e3) {
3519
3751
  await new Promise((resolve) => setTimeout(resolve, 25));
3520
3752
  }
3521
3753
  }
3754
+ /** 元素中心点的视口坐标,指针类事件都按它构造 */
3755
+ function centerOf(element) {
3756
+ const rect = element.getBoundingClientRect();
3757
+ return {
3758
+ clientX: rect.left + rect.width / 2,
3759
+ clientY: rect.top + rect.height / 2
3760
+ };
3761
+ }
3762
+ /** 指针事件构造器;跨 realm 时要拿目标自己那个 window 上的,不能用全局的 */
3763
+ function pointerCtorOf(element) {
3764
+ return element.ownerDocument.defaultView?.PointerEvent;
3765
+ }
3766
+ /**
3767
+ * 一次「像人一样」的悬停(FR-17.2)。
3768
+ *
3769
+ * `enter` 那两个**不冒泡**,必须单独派发:只发 `over` 的话 React 的 `onMouseEnter`
3770
+ * 一个都收不到,而它正是组件库开浮层最常用的那个钩子。
3771
+ * 末尾补 `move` 是因为不少实现按「指针在里面动过」才认,静止的 over 被当成误触。
3772
+ *
3773
+ * ⚠️ 这套合成事件改变不了 CSS `:hover` 伪类——它由浏览器按**真实**指针位置判定。
3774
+ * 纯 CSS 开的浮层因此打不开,调用方要靠 `noop` 把这件事如实说给模型(FR-17.3)。
3775
+ */
3776
+ function hoverOver(element) {
3777
+ const base = {
3778
+ bubbles: true,
3779
+ cancelable: true,
3780
+ composed: true,
3781
+ ...centerOf(element)
3782
+ };
3783
+ const noBubble = {
3784
+ ...base,
3785
+ bubbles: false
3786
+ };
3787
+ const pointer = {
3788
+ pointerId: 1,
3789
+ pointerType: "mouse",
3790
+ isPrimary: true
3791
+ };
3792
+ const Pointer = pointerCtorOf(element);
3793
+ if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerover", {
3794
+ ...base,
3795
+ ...pointer
3796
+ }));
3797
+ if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerenter", {
3798
+ ...noBubble,
3799
+ ...pointer
3800
+ }));
3801
+ element.dispatchEvent(new MouseEvent("mouseover", base));
3802
+ element.dispatchEvent(new MouseEvent("mouseenter", noBubble));
3803
+ if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointermove", {
3804
+ ...base,
3805
+ ...pointer
3806
+ }));
3807
+ element.dispatchEvent(new MouseEvent("mousemove", base));
3808
+ }
3809
+ /**
3810
+ * 悬停之后页面有没有动(FR-17.3)。
3811
+ *
3812
+ * 用 `MutationObserver` 而不是比对快照:浮层可能挂到 `document.body` 末尾、
3813
+ * 也可能只是把某个节点的 `hidden` 去掉,逐一枚举会漏,而漏判的代价是向模型报假成功。
3814
+ * 观察器在派发**之前**挂上——晚一步就会错过同步执行的那批监听器。
3815
+ */
3816
+ function watchMutations(element) {
3817
+ const doc = element.ownerDocument;
3818
+ const Observer = doc.defaultView?.MutationObserver;
3819
+ if (Observer === void 0) return {
3820
+ changed: () => true,
3821
+ stop: () => void 0
3822
+ };
3823
+ let seen = false;
3824
+ const observer = new Observer(() => {
3825
+ seen = true;
3826
+ });
3827
+ observer.observe(doc, {
3828
+ subtree: true,
3829
+ childList: true,
3830
+ attributes: true,
3831
+ characterData: true
3832
+ });
3833
+ return {
3834
+ changed: () => {
3835
+ if (observer.takeRecords().length > 0) seen = true;
3836
+ return seen;
3837
+ },
3838
+ stop: () => observer.disconnect()
3839
+ };
3840
+ }
3841
+ /**
3842
+ * 一次拖拽(FR-17.5)。真实页面有两套互不相通的机制,按起点自动选:
3843
+ *
3844
+ * - 起点带 `draggable` 且环境给得出 `DragEvent`/`DataTransfer` → HTML5 拖放;
3845
+ * - 否则 → 指针拖拽,`pointerdown` 起点、`pointermove` / `pointerup` 落终点坐标。
3846
+ *
3847
+ * 指针那一套的 `move` / `up` 必须冒泡:这类实现为了不被换位打断,
3848
+ * 监听是挂在 `document` 上的(把手一旦在 DOM 里搬走,指针捕获就断了),
3849
+ * 不冒泡的话一路只动得了第一格,甚至一格都不动。
3850
+ *
3851
+ * 构造器缺失时**降级**而不是抛:执行器的契约是一律给出结构化结果,
3852
+ * 抛出去会连带跳过策略层的审计,那比操作失败本身更糟(同 `attachFiles` 的理由)。
3853
+ *
3854
+ * @returns 失败原因;成功返回 `undefined`
3855
+ */
3856
+ function dragBetween(from, to) {
3857
+ const view = from.ownerDocument.defaultView;
3858
+ const start = centerOf(from);
3859
+ const end = centerOf(to);
3860
+ const Drag = view?.DragEvent;
3861
+ const Transfer = view?.DataTransfer;
3862
+ if (from.closest("[draggable=\"true\"]") !== null && Drag !== void 0 && Transfer !== void 0) {
3863
+ const init = {
3864
+ bubbles: true,
3865
+ cancelable: true,
3866
+ composed: true,
3867
+ dataTransfer: new Transfer()
3868
+ };
3869
+ from.dispatchEvent(new Drag("dragstart", {
3870
+ ...init,
3871
+ ...start
3872
+ }));
3873
+ to.dispatchEvent(new Drag("dragenter", {
3874
+ ...init,
3875
+ ...end
3876
+ }));
3877
+ const accepted = !to.dispatchEvent(new Drag("dragover", {
3878
+ ...init,
3879
+ ...end
3880
+ }));
3881
+ if (accepted) to.dispatchEvent(new Drag("drop", {
3882
+ ...init,
3883
+ ...end
3884
+ }));
3885
+ from.dispatchEvent(new Drag("dragend", {
3886
+ ...init,
3887
+ ...end
3888
+ }));
3889
+ return accepted ? void 0 : "The destination does not accept dropped items.";
3890
+ }
3891
+ const Pointer = view?.PointerEvent;
3892
+ const mouse = {
3893
+ bubbles: true,
3894
+ cancelable: true,
3895
+ composed: true,
3896
+ button: 0
3897
+ };
3898
+ const pointer = {
3899
+ ...mouse,
3900
+ pointerId: 1,
3901
+ pointerType: "mouse",
3902
+ isPrimary: true
3903
+ };
3904
+ if (Pointer !== void 0) from.dispatchEvent(new Pointer("pointerdown", {
3905
+ ...pointer,
3906
+ ...start,
3907
+ buttons: 1
3908
+ }));
3909
+ from.dispatchEvent(new MouseEvent("mousedown", {
3910
+ ...mouse,
3911
+ ...start,
3912
+ buttons: 1
3913
+ }));
3914
+ if (Pointer !== void 0) to.dispatchEvent(new Pointer("pointermove", {
3915
+ ...pointer,
3916
+ ...end,
3917
+ buttons: 1
3918
+ }));
3919
+ to.dispatchEvent(new MouseEvent("mousemove", {
3920
+ ...mouse,
3921
+ ...end,
3922
+ buttons: 1
3923
+ }));
3924
+ if (Pointer !== void 0) to.dispatchEvent(new Pointer("pointerup", {
3925
+ ...pointer,
3926
+ ...end,
3927
+ buttons: 0
3928
+ }));
3929
+ to.dispatchEvent(new MouseEvent("mouseup", {
3930
+ ...mouse,
3931
+ ...end,
3932
+ buttons: 0
3933
+ }));
3934
+ }
3522
3935
  /**
3523
3936
  * 一个动作覆盖三种选择控件(FR-25.2):原生 select 直接设值,
3524
3937
  * ARIA 组合与日期格点走「点开 → 等浮层 → 按可访问名匹配 → 点中」。
@@ -3788,6 +4201,7 @@ function createDomPageActionExecutor(options) {
3788
4201
  reason
3789
4202
  });
3790
4203
  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.");
4204
+ 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.");
3791
4205
  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.");
3792
4206
  if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
3793
4207
  if (hidden(element)) return fail("The element is not visible.");
@@ -3847,6 +4261,33 @@ function createDomPageActionExecutor(options) {
3847
4261
  if (request.value !== "down" && request.value !== "up") return fail("Use \"down\" or \"up\".");
3848
4262
  return await scrollRegion(element, target, request.value);
3849
4263
  }
4264
+ if (request.action === "hover") {
4265
+ const watch = watchMutations(element);
4266
+ hoverOver(element);
4267
+ await waitFor(() => watch.changed() ? true : void 0, 500);
4268
+ const changed = watch.changed();
4269
+ watch.stop();
4270
+ return await done({
4271
+ ok: true,
4272
+ target,
4273
+ ...changed ? {} : { noop: true }
4274
+ });
4275
+ }
4276
+ if (request.action === "drag") {
4277
+ const destination = request.value ?? "";
4278
+ if (destination === "") return fail("The drag action needs a value holding the destination ref.");
4279
+ const dropTarget = reader.resolve(destination);
4280
+ if (dropTarget === void 0) return fail("The destination reference is unknown or expired.");
4281
+ if (reader.handleKindOf(destination) === "image") return fail("The destination reference points at an image, which cannot receive a drop.");
4282
+ if (reader.modalStateOf(dropTarget) !== "elevated" && !reader.inActionScope(dropTarget)) return fail("The destination is no longer inside the actionable scope.");
4283
+ if (hidden(dropTarget)) return fail("The destination is not visible.");
4284
+ const refused = dragBetween(element, dropTarget);
4285
+ if (refused !== void 0) return fail(refused);
4286
+ return await done({
4287
+ ok: true,
4288
+ target
4289
+ });
4290
+ }
3850
4291
  if (request.action === "attach") {
3851
4292
  const files = await options.pickFiles?.();
3852
4293
  if (files === void 0 || files.length === 0) return {
@@ -4080,6 +4521,27 @@ function createRemotePageActionExecutor(options) {
4080
4521
  };
4081
4522
  }
4082
4523
 
4524
+ //#endregion
4525
+ //#region ../browser/src/remote/pageImage.ts
4526
+ /**
4527
+ * 跨上下文的取像端口(分册 12 §2)。
4528
+ *
4529
+ * 与 `createRemotePageActionExecutor` 同一条装配规矩:句柄表在页面那一侧,
4530
+ * 这边只负责把一次请求送过去。**不与感知期的取像共用任何状态**——
4531
+ * 那条流是为了让模型看懂,这条流是为了落产物,两者不得互相触发。
4532
+ * @experimental
4533
+ */
4534
+ function createRemotePageImageHost(options) {
4535
+ const { transport, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
4536
+ return { async capture(ref, maxBytes) {
4537
+ return (await roundTrip(transport, {
4538
+ type: "capture-image",
4539
+ ref,
4540
+ maxBytes
4541
+ }, "capture-image-result", "TOOL_EXECUTION_FAILED", timeoutMs)).result;
4542
+ } };
4543
+ }
4544
+
4083
4545
  //#endregion
4084
4546
  //#region ../browser/src/remote/handler.ts
4085
4547
  /** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
@@ -4152,6 +4614,24 @@ function createPageAgentHandler(options = {}) {
4152
4614
  ...identity !== void 0 ? { document: identity } : {}
4153
4615
  };
4154
4616
  }
4617
+ if (request.type === "capture-image") {
4618
+ const element = reader.resolve(request.ref);
4619
+ if (element === void 0) return {
4620
+ type: "capture-image-result",
4621
+ result: {
4622
+ id: request.ref,
4623
+ reason: "that reference is not on this page any more; perceive the page again to get a fresh reference",
4624
+ triedLevels: []
4625
+ }
4626
+ };
4627
+ return {
4628
+ type: "capture-image-result",
4629
+ result: await captureDocumentImage(element, {
4630
+ id: request.ref,
4631
+ maxBytes: request.maxBytes
4632
+ })
4633
+ };
4634
+ }
4155
4635
  return {
4156
4636
  type: "execute-result",
4157
4637
  outcome: await executor.execute(request.request)
@@ -4357,9 +4837,14 @@ function createFrameRouter(options) {
4357
4837
  const address = refFrames.get(request.request.ref) ?? await options.dispatcher.mainFrame();
4358
4838
  return (await options.dispatcher.send(address, request)).reply;
4359
4839
  }
4840
+ async function captureImage(request) {
4841
+ const address = refFrames.get(request.ref) ?? await options.dispatcher.mainFrame();
4842
+ return (await options.dispatcher.send(address, request)).reply;
4843
+ }
4360
4844
  return {
4361
4845
  perceive,
4362
- execute
4846
+ execute,
4847
+ captureImage
4363
4848
  };
4364
4849
  }
4365
4850
 
@@ -5413,8 +5898,8 @@ function fail(message) {
5413
5898
  throw new WebSkillError("TOOL_EXECUTION_FAILED", message);
5414
5899
  }
5415
5900
  /** 按 localName 取子孙元素,忽略命名空间前缀:不同生成器的前缀写法并不统一 */
5416
- const byTag$1 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
5417
- function parseXml$2(bytes, what) {
5901
+ const byTag$3 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
5902
+ function parseXml$3(bytes, what) {
5418
5903
  const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
5419
5904
  if (doc.getElementsByTagName("parsererror").length > 0) fail(`The workbook ${what} could not be parsed as XML.`);
5420
5905
  return doc;
@@ -5461,14 +5946,14 @@ function isDateFormatCode(code) {
5461
5946
  /** 单元格样式索引 → 是否日期格式 */
5462
5947
  function readDateStyles(bytes) {
5463
5948
  if (bytes === void 0) return [];
5464
- const doc = parseXml$2(bytes, "styles");
5949
+ const doc = parseXml$3(bytes, "styles");
5465
5950
  const custom = /* @__PURE__ */ new Map();
5466
- for (const fmt of byTag$1(doc, "numFmt")) {
5951
+ for (const fmt of byTag$3(doc, "numFmt")) {
5467
5952
  const id = Number(fmt.getAttribute("numFmtId"));
5468
5953
  const code = fmt.getAttribute("formatCode");
5469
5954
  if (Number.isFinite(id) && code !== null) custom.set(id, code);
5470
5955
  }
5471
- const cellXfs = byTag$1(doc, "cellXfs")[0];
5956
+ const cellXfs = byTag$3(doc, "cellXfs")[0];
5472
5957
  if (cellXfs === void 0) return [];
5473
5958
  return [...cellXfs.children].filter((xf) => xf.localName === "xf").map((xf) => {
5474
5959
  const id = Number(xf.getAttribute("numFmtId") ?? "0");
@@ -5506,7 +5991,7 @@ function serialToIso(serial, date1904) {
5506
5991
  return `${iso}T${pad(h)}:${pad(m)}:${pad(s)}`;
5507
5992
  }
5508
5993
  /** 一个 `<c>` 的呈现文本(FR-12.3) */
5509
- function cellText(cell, shared, dateStyles, date1904) {
5994
+ function cellText$1(cell, shared, dateStyles, date1904) {
5510
5995
  const type = cell.getAttribute("t");
5511
5996
  if (type === "inlineStr") {
5512
5997
  const is = [...cell.children].find((child) => child.localName === "is");
@@ -5533,7 +6018,7 @@ function cellText(cell, shared, dateStyles, date1904) {
5533
6018
  function sheetLines(doc, shared, dateStyles, date1904) {
5534
6019
  const lines = [];
5535
6020
  let previousRow = 0;
5536
- for (const row of byTag$1(doc, "row")) {
6021
+ for (const row of byTag$3(doc, "row")) {
5537
6022
  const declared = Number(row.getAttribute("r"));
5538
6023
  const rowNumber = Number.isInteger(declared) && declared > 0 ? declared : previousRow + 1;
5539
6024
  for (let gap = previousRow + 1; gap < rowNumber; gap++) lines.push("");
@@ -5544,7 +6029,7 @@ function sheetLines(doc, shared, dateStyles, date1904) {
5544
6029
  if (cell.localName !== "c") continue;
5545
6030
  const column = columnIndex(cell.getAttribute("r")) ?? nextColumn;
5546
6031
  for (let gap = cells.length; gap < column; gap++) cells.push("");
5547
- const text = cellText(cell, shared, dateStyles, date1904);
6032
+ const text = cellText$1(cell, shared, dateStyles, date1904);
5548
6033
  if (cells.length > column) cells[column] = text;
5549
6034
  else cells.push(text);
5550
6035
  nextColumn = column + 1;
@@ -5576,21 +6061,21 @@ async function readXlsxWorkbook(bytes) {
5576
6061
  const parts = new Map(entries);
5577
6062
  const workbookBytes = parts.get(WORKBOOK);
5578
6063
  if (workbookBytes === void 0) fail(`This file is not an Excel workbook: it has no ${WORKBOOK} entry.`);
5579
- const workbook = parseXml$2(workbookBytes, "index");
5580
- const workbookPr = byTag$1(workbook, "workbookPr")[0];
6064
+ const workbook = parseXml$3(workbookBytes, "index");
6065
+ const workbookPr = byTag$3(workbook, "workbookPr")[0];
5581
6066
  const date1904 = workbookPr?.getAttribute("date1904") === "1" || workbookPr?.getAttribute("date1904") === "true";
5582
6067
  const relsBytes = parts.get(WORKBOOK_RELS);
5583
6068
  const targets = /* @__PURE__ */ new Map();
5584
- if (relsBytes !== void 0) for (const rel of byTag$1(parseXml$2(relsBytes, "relationships"), "Relationship")) {
6069
+ if (relsBytes !== void 0) for (const rel of byTag$3(parseXml$3(relsBytes, "relationships"), "Relationship")) {
5585
6070
  const id = rel.getAttribute("Id");
5586
6071
  const target = rel.getAttribute("Target");
5587
6072
  if (id !== null && target !== null) targets.set(id, resolveSheetPath(target));
5588
6073
  }
5589
6074
  const sharedBytes = parts.get(SHARED_STRINGS);
5590
- const shared = sharedBytes === void 0 ? [] : byTag$1(parseXml$2(sharedBytes, "shared strings"), "si").map(sharedStringText);
6075
+ const shared = sharedBytes === void 0 ? [] : byTag$3(parseXml$3(sharedBytes, "shared strings"), "si").map(sharedStringText);
5591
6076
  const dateStyles = readDateStyles(parts.get(STYLES));
5592
6077
  const sheets = [];
5593
- for (const [ordinal, sheet] of byTag$1(workbook, "sheet").entries()) {
6078
+ for (const [ordinal, sheet] of byTag$3(workbook, "sheet").entries()) {
5594
6079
  const name = sheet.getAttribute("name") ?? `Sheet${ordinal + 1}`;
5595
6080
  const relId = sheet.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? sheet.getAttribute("r:id");
5596
6081
  const path = relId !== null ? targets.get(relId) : void 0;
@@ -5599,7 +6084,7 @@ async function readXlsxWorkbook(bytes) {
5599
6084
  sheets.push({
5600
6085
  name,
5601
6086
  path,
5602
- lines: sheetLines(parseXml$2(sheetBytes, `worksheet "${name}"`), shared, dateStyles, date1904)
6087
+ lines: sheetLines(parseXml$3(sheetBytes, `worksheet "${name}"`), shared, dateStyles, date1904)
5603
6088
  });
5604
6089
  }
5605
6090
  return {
@@ -5660,7 +6145,7 @@ function imageBlockOf(path, data) {
5660
6145
  *
5661
6146
  * 零新增依赖,沿用同一条路:zip 走 `unzipWithLimits`,XML 走 `DOMParser`。
5662
6147
  */
5663
- const DOCUMENT = "word/document.xml";
6148
+ const DOCUMENT$1 = "word/document.xml";
5664
6149
  const RELS = "word/_rels/document.xml.rels";
5665
6150
  /** 图宽占正文宽度的门槛:低于它的是签名章、页眉 logo、装饰线(FR-22.9) @experimental */
5666
6151
  const DOCX_IMAGE_WIDTH_RATIO = .3;
@@ -5671,7 +6156,7 @@ const DEFAULT_BODY_TWIPS = 9360;
5671
6156
  function attr(node, name) {
5672
6157
  for (const item of node.attributes) if (item.name === name || item.localName === name) return item.value;
5673
6158
  }
5674
- function parseXml$1(bytes, what) {
6159
+ function parseXml$2(bytes, what) {
5675
6160
  const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
5676
6161
  if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The Word document's ${what} could not be parsed as XML.`);
5677
6162
  return doc;
@@ -5732,12 +6217,12 @@ function createDocxBlockReader() {
5732
6217
  async function readDocxBlocks(bytes) {
5733
6218
  const entries = await unzipWithLimits(bytes);
5734
6219
  const files = new Map(entries);
5735
- const main = files.get(DOCUMENT);
5736
- if (main === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a Word document: it has no ${DOCUMENT} entry.`);
5737
- const doc = parseXml$1(main, "body");
6220
+ const main = files.get(DOCUMENT$1);
6221
+ if (main === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a Word document: it has no ${DOCUMENT$1} entry.`);
6222
+ const doc = parseXml$2(main, "body");
5738
6223
  const targets = /* @__PURE__ */ new Map();
5739
6224
  const rels = files.get(RELS);
5740
- if (rels !== void 0) for (const rel of parseXml$1(rels, "relationships").getElementsByTagName("Relationship")) {
6225
+ if (rels !== void 0) for (const rel of parseXml$2(rels, "relationships").getElementsByTagName("Relationship")) {
5741
6226
  const id = attr(rel, "Id");
5742
6227
  const target = attr(rel, "Target");
5743
6228
  if (id !== void 0 && target !== void 0) targets.set(id, `word/${target.replace(/^\.?\//, "")}`);
@@ -5779,13 +6264,13 @@ const XLSX_IMAGE_MIN_COLUMNS = 2;
5779
6264
  /** @experimental */
5780
6265
  const XLSX_IMAGE_MIN_ROWS = 3;
5781
6266
  const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
5782
- const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
5783
- function parseXml(bytes) {
6267
+ const byTag$2 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
6268
+ function parseXml$1(bytes) {
5784
6269
  const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
5785
6270
  return doc.getElementsByTagName("parsererror").length > 0 ? void 0 : doc;
5786
6271
  }
5787
6272
  /** rels 的 Target 可能是 `../media/x.png` 这种相对路径,得按所在目录归一化 */
5788
- function resolveRelative(baseDir, target) {
6273
+ function resolveRelative$1(baseDir, target) {
5789
6274
  if (target.startsWith("/")) return target.slice(1);
5790
6275
  const segments = [...baseDir.split("/").filter((part) => part !== ""), ...target.split("/")];
5791
6276
  const out = [];
@@ -5801,17 +6286,17 @@ const relsOf = (path) => `${dirOf(path)}_rels/${path.slice(path.lastIndexOf("/")
5801
6286
  function readRels(parts, path) {
5802
6287
  const out = /* @__PURE__ */ new Map();
5803
6288
  const bytes = parts.get(relsOf(path));
5804
- const doc = bytes === void 0 ? void 0 : parseXml(bytes);
6289
+ const doc = bytes === void 0 ? void 0 : parseXml$1(bytes);
5805
6290
  if (doc === void 0) return out;
5806
- for (const rel of byTag(doc, "Relationship")) {
6291
+ for (const rel of byTag$2(doc, "Relationship")) {
5807
6292
  const id = rel.getAttribute("Id");
5808
6293
  const target = rel.getAttribute("Target");
5809
- if (id !== null && target !== null) out.set(id, resolveRelative(dirOf(path), target));
6294
+ if (id !== null && target !== null) out.set(id, resolveRelative$1(dirOf(path), target));
5810
6295
  }
5811
6296
  return out;
5812
6297
  }
5813
6298
  const anchorNumber = (anchor, name) => {
5814
- const node = anchor === void 0 ? void 0 : byTag(anchor, name)[0];
6299
+ const node = anchor === void 0 ? void 0 : byTag$2(anchor, name)[0];
5815
6300
  const value = Number(node?.textContent ?? NaN);
5816
6301
  return Number.isInteger(value) && value >= 0 ? value : 0;
5817
6302
  };
@@ -5822,17 +6307,17 @@ function imagesOf(parts, sheetPath) {
5822
6307
  for (const [, target] of sheetRels) {
5823
6308
  if (!target.includes("/drawings/")) continue;
5824
6309
  const drawingBytes = parts.get(target);
5825
- const drawing = drawingBytes === void 0 ? void 0 : parseXml(drawingBytes);
6310
+ const drawing = drawingBytes === void 0 ? void 0 : parseXml$1(drawingBytes);
5826
6311
  if (drawing === void 0) continue;
5827
6312
  const drawingRels = readRels(parts, target);
5828
- for (const anchor of byTag(drawing, "twoCellAnchor")) {
5829
- const from = byTag(anchor, "from")[0];
5830
- const to = byTag(anchor, "to")[0];
6313
+ for (const anchor of byTag$2(drawing, "twoCellAnchor")) {
6314
+ const from = byTag$2(anchor, "from")[0];
6315
+ const to = byTag$2(anchor, "to")[0];
5831
6316
  const fromRow = anchorNumber(from, "row");
5832
6317
  const columns = anchorNumber(to, "col") - anchorNumber(from, "col");
5833
6318
  const rows = anchorNumber(to, "row") - fromRow;
5834
6319
  if (columns < 2 || rows < 3) continue;
5835
- const blip = byTag(anchor, "blip")[0];
6320
+ const blip = byTag$2(anchor, "blip")[0];
5836
6321
  const relId = blip?.getAttributeNS(RELATIONSHIPS_NS, "embed") ?? blip?.getAttribute("r:embed");
5837
6322
  const path = relId === null || relId === void 0 ? void 0 : drawingRels.get(relId);
5838
6323
  const data = path === void 0 ? void 0 : parts.get(path);
@@ -5874,6 +6359,1072 @@ async function readXlsxBlocks(bytes) {
5874
6359
  return { sheets: out };
5875
6360
  }
5876
6361
 
6362
+ //#endregion
6363
+ //#region ../browser/src/document/pptxBlocks.ts
6364
+ /**
6365
+ * pptx → 逐页的块序列(0.22.0 分册 18)。
6366
+ *
6367
+ * 这不是渲染器。OOXML 包里**没有**渲染好的页面图,浏览器里也没有 pptx 渲染器,
6368
+ * 所以这条路给得出「这一页写了什么、插了哪几张图」,给不出「这一页长什么样」。
6369
+ * 要版式只能退到截屏。
6370
+ *
6371
+ * 与 docx/xlsx 同一条路:zip 走 `unzipWithLimits`,XML 走 `DOMParser`,
6372
+ * 解不了的媒体走 `imageBlockOf` 的 `skipped`——不假装读完是硬要求(FR-22.14)。
6373
+ */
6374
+ const PRESENTATION = "ppt/presentation.xml";
6375
+ const MAIN_NS$1 = "http://schemas.openxmlformats.org/drawingml/2006/main";
6376
+ /** `p:sldId` / `p:spTree` 在 presentationml 里,不在 drawingml 里 */
6377
+ const PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main";
6378
+ const REL_NS$2 = "http://schemas.openxmlformats.org/package/2006/relationships";
6379
+ const DOC_REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
6380
+ function parseXml(files, path, what) {
6381
+ const bytes = files.get(path);
6382
+ if (bytes === void 0) return void 0;
6383
+ const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
6384
+ if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The presentation's ${what} could not be parsed as XML.`);
6385
+ return doc;
6386
+ }
6387
+ /**
6388
+ * 某个部件的关系表,键是 `rId`,值是**已经解算成包内绝对路径**的 target。
6389
+ * 外部链接(`TargetMode="External"`)不收:那是别处的 URL,不是包里的东西。
6390
+ */
6391
+ function relationshipsOf(files, part) {
6392
+ const slash = part.lastIndexOf("/");
6393
+ const dir = part.slice(0, slash);
6394
+ const doc = parseXml(files, `${dir}/_rels/${part.slice(slash + 1)}.rels`, "relationships");
6395
+ const map = /* @__PURE__ */ new Map();
6396
+ if (doc === void 0) return map;
6397
+ for (const node of Array.from(doc.getElementsByTagNameNS(REL_NS$2, "Relationship"))) {
6398
+ const id = node.getAttribute("Id");
6399
+ const target = node.getAttribute("Target");
6400
+ if (id === null || target === null || node.getAttribute("TargetMode") === "External") continue;
6401
+ map.set(id, resolveTarget(dir, target));
6402
+ }
6403
+ return map;
6404
+ }
6405
+ /** `../media/image1.png` 相对于部件所在目录解算,得到包内绝对路径 */
6406
+ function resolveTarget(dir, target) {
6407
+ if (target.startsWith("/")) return target.slice(1);
6408
+ const segments = dir.split("/");
6409
+ for (const part of target.split("/")) {
6410
+ if (part === "." || part === "") continue;
6411
+ if (part === "..") segments.pop();
6412
+ else segments.push(part);
6413
+ }
6414
+ return segments.join("/");
6415
+ }
6416
+ /**
6417
+ * 放映顺序取自 `p:sldIdLst`,**不是**文件名里的数字。
6418
+ * `slide7.xml` 完全可以排在第二张——删过页的文稿里这是常态。
6419
+ */
6420
+ function slideOrder(files) {
6421
+ const presentation = parseXml(files, PRESENTATION, "presentation part");
6422
+ if (presentation === void 0) return [];
6423
+ const rels = relationshipsOf(files, PRESENTATION);
6424
+ const ordered = [];
6425
+ for (const node of Array.from(presentation.getElementsByTagNameNS(PRESENTATION_NS, "sldId"))) {
6426
+ const id = node.getAttributeNS(DOC_REL_NS, "id");
6427
+ const target = id === null ? void 0 : rels.get(id);
6428
+ if (target !== void 0 && files.has(target)) ordered.push(target);
6429
+ }
6430
+ if (ordered.length > 0) return ordered;
6431
+ return [...files.keys()].filter((name) => /^ppt\/slides\/slide\d+\.xml$/.test(name)).sort((a, b) => slideNumber(a) - slideNumber(b));
6432
+ }
6433
+ function slideNumber(path) {
6434
+ return Number(/(\d+)\.xml$/.exec(path)?.[1] ?? 0);
6435
+ }
6436
+ /** 段落之间换行,段落内的多个 run 直接相接——run 的切分是格式造成的,不是语义 */
6437
+ function textOf$1(node) {
6438
+ const lines = [];
6439
+ for (const paragraph of Array.from(node.getElementsByTagNameNS(MAIN_NS$1, "p"))) {
6440
+ const line = Array.from(paragraph.getElementsByTagNameNS(MAIN_NS$1, "t")).map((run) => run.textContent ?? "").join("").trim();
6441
+ if (line !== "") lines.push(line);
6442
+ }
6443
+ return lines.join("\n");
6444
+ }
6445
+ /**
6446
+ * 备注页的文本。多一道过滤:备注页上除备注框外还挂着页码之类的字段占位符,
6447
+ * 取值是一个孤零零的符号。**不含任何字母或数字的条目不是备注**。
6448
+ */
6449
+ function notesOf(files, rels) {
6450
+ const target = [...rels.values()].find((value) => value.includes("/notesSlides/"));
6451
+ const doc = target === void 0 ? void 0 : parseXml(files, target, "speaker notes");
6452
+ if (doc === void 0) return "";
6453
+ const lines = [];
6454
+ for (const tree of Array.from(doc.getElementsByTagNameNS(PRESENTATION_NS, "spTree"))) for (const shape of Array.from(tree.children)) {
6455
+ const text = textOf$1(shape);
6456
+ if (text !== "" && /[\p{L}\p{N}]/u.test(text)) lines.push(text);
6457
+ }
6458
+ return lines.join("\n");
6459
+ }
6460
+ function readSlide(files, path) {
6461
+ const doc = parseXml(files, path, "slide");
6462
+ if (doc === void 0) return { blocks: [] };
6463
+ const rels = relationshipsOf(files, path);
6464
+ const blocks = [];
6465
+ for (const tree of Array.from(doc.getElementsByTagNameNS(PRESENTATION_NS, "spTree"))) for (const shape of Array.from(tree.children)) {
6466
+ const text = textOf$1(shape);
6467
+ if (text !== "") blocks.push({
6468
+ kind: "text",
6469
+ text
6470
+ });
6471
+ }
6472
+ for (const target of rels.values()) {
6473
+ if (!target.startsWith("ppt/media/")) continue;
6474
+ const bytes = files.get(target);
6475
+ if (bytes === void 0) continue;
6476
+ blocks.push(imageBlockOf(target, () => toBase64$1(bytes)));
6477
+ }
6478
+ const notes = notesOf(files, rels);
6479
+ if (notes !== "") blocks.push({
6480
+ kind: "text",
6481
+ text: `Speaker notes: ${notes}`
6482
+ });
6483
+ return { blocks };
6484
+ }
6485
+ /** @experimental */
6486
+ function createPptxBlockReader() {
6487
+ return { read: readPptxBlocks };
6488
+ }
6489
+ /**
6490
+ * @throws WebSkillError 归档损坏、缺 `ppt/presentation.xml`、XML 解析失败
6491
+ * @experimental
6492
+ */
6493
+ async function readPptxBlocks(bytes) {
6494
+ const files = new Map(await unzipWithLimits(bytes));
6495
+ if (!files.has(PRESENTATION)) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a PowerPoint presentation: it has no ${PRESENTATION} entry.`);
6496
+ return { slides: slideOrder(files).map((path) => readSlide(files, path)) };
6497
+ }
6498
+
6499
+ //#endregion
6500
+ //#region ../browser/src/document/pptx.ts
6501
+ /**
6502
+ * pptx 正文抽取(0.22.0 分册 18)。
6503
+ *
6504
+ * 与 `readPptxBlocks` 同源:文本形态只是把逐页的文本块按放映顺序拼起来,
6505
+ * 两条路给出的文字因此**永远一致**——同一份文稿不会因为走哪条路而多出或少掉一段。
6506
+ *
6507
+ * ⚠️ 与 docx/xlsx 一样依赖 `DOMParser`,所以只在 `@webskill/browser`。
6508
+ */
6509
+ /**
6510
+ * **不抽取**的内容,逐条写明而不是笼统说「尽力而为」:
6511
+ * 版式、动画、切换效果、母版与版式上的模板文字、图表的底层数据。
6512
+ *
6513
+ * 插图不在此列但也不在文本里:图要按页取,走 `read_document`。
6514
+ */
6515
+ const PPTX_UNEXTRACTED = [
6516
+ "layout and positioning",
6517
+ "animations and transitions",
6518
+ "master and layout placeholders",
6519
+ "chart source data",
6520
+ "pictures"
6521
+ ];
6522
+ /** 抽不出任何文字时的占位,与 docx/xlsx 同款口径 @experimental */
6523
+ const PPTX_UNEXTRACTED_NOTICE = "This presentation has no extractable text: its slides are most likely pictures. Use read_document to look at them.";
6524
+ /**
6525
+ * @throws WebSkillError 归档损坏、缺 `ppt/presentation.xml`、XML 解析失败
6526
+ * @experimental
6527
+ */
6528
+ async function extractPptxText(bytes) {
6529
+ const { slides } = await readPptxBlocks(bytes);
6530
+ const pages = [];
6531
+ slides.forEach((slide, index) => {
6532
+ const text = slide.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
6533
+ if (text.trim() !== "") pages.push(`--- Slide ${index + 1} ---\n${text}`);
6534
+ });
6535
+ return pages.length === 0 ? PPTX_UNEXTRACTED_NOTICE : pages.join("\n\n");
6536
+ }
6537
+
6538
+ //#endregion
6539
+ //#region ../browser/src/document/ooxmlPart.ts
6540
+ /**
6541
+ * OOXML 包的拆装地基(0.22.0 分册 42 · 设计 43 §1.1、§1.7)。
6542
+ *
6543
+ * 这一册的整条路子是「解开模板 zip,只改承载数据的那几个部件,其余字节原样写回」。
6544
+ * 地基只做三件事,xlsx 与 docx 两侧都走它,谁都不许自己碰 XML 声明。
6545
+ */
6546
+ const CONTENT_TYPES = "[Content_Types].xml";
6547
+ const XML_DECLARATION = /^<\?xml[^?]*\?>\r?\n?/;
6548
+ /**
6549
+ * 切下 XML 声明。
6550
+ *
6551
+ * 设计 43 §1.1 实测:jsdom 的 `XMLSerializer` 丢掉声明,Chromium 保留它却吃掉结尾的 `\r\n`。
6552
+ * 两个引擎两种错法,而且只有在真实浏览器里才会碰到第二种——所以声明必须在解析前切下来留着,
6553
+ * 序列化之后把**原始那几个字节**原样拼回去,任何路径都不读序列化器吐出来的前言。
6554
+ */
6555
+ function splitXmlDeclaration(text) {
6556
+ const declaration = XML_DECLARATION.exec(text)?.[0] ?? "";
6557
+ return {
6558
+ declaration,
6559
+ body: text.slice(declaration.length)
6560
+ };
6561
+ }
6562
+ /** @throws WebSkillError 该部件不存在或不是合法 XML */
6563
+ function parseXmlPart(pkg, path) {
6564
+ const bytes = pkg.get(path);
6565
+ if (bytes === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The template package has no ${path} part.`);
6566
+ const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(bytes));
6567
+ const doc = new DOMParser().parseFromString(body, "application/xml");
6568
+ if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The ${path} part of the template is not well-formed XML.`);
6569
+ return {
6570
+ declaration,
6571
+ doc
6572
+ };
6573
+ }
6574
+ /** 序列化并把原始声明原样拼回(见 {@link splitXmlDeclaration})。 */
6575
+ function serializeXmlPart(declaration, doc) {
6576
+ const { body } = splitXmlDeclaration(new XMLSerializer().serializeToString(doc));
6577
+ return new TextEncoder().encode(declaration + body);
6578
+ }
6579
+ /** @throws WebSkillError 归档损坏,或不是 OOXML(缺 `[Content_Types].xml`) */
6580
+ async function readOoxmlPackage(bytes) {
6581
+ const entries = await unzipWithLimits(bytes);
6582
+ const pkg = new Map(entries);
6583
+ 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
+ return pkg;
6585
+ }
6586
+ function writeOoxmlPackage(pkg) {
6587
+ return zipSync(Object.fromEntries(pkg), { level: 6 });
6588
+ }
6589
+ /** `../media/x.png` 这种相对 Target 按所在目录归一化 */
6590
+ function resolveRelative(baseDir, target) {
6591
+ if (target.startsWith("/")) return target.slice(1);
6592
+ const out = [];
6593
+ for (const segment of [...baseDir.split("/"), ...target.split("/")]) {
6594
+ if (segment === "" || segment === ".") continue;
6595
+ if (segment === "..") out.pop();
6596
+ else out.push(segment);
6597
+ }
6598
+ return out.join("/");
6599
+ }
6600
+ const extensionOf = (path) => {
6601
+ const dot = path.lastIndexOf(".");
6602
+ return dot < 0 ? "" : path.slice(dot + 1).toLowerCase();
6603
+ };
6604
+ /**
6605
+ * 结构自检(AC-42.27)。
6606
+ *
6607
+ * 「打开不提示修复」没法进 CI(本仓没有可脚本化的 Office 实现,见设计 10 §6.2),
6608
+ * 但迄今实测出的坏文件成因都是结构性的:悬空关系、没登记的扩展名、解析不了的部件。
6609
+ * 这三条能当场查出来,所以每次重封之前都查一遍。
6610
+ *
6611
+ * @returns 英文问题描述,空数组表示干净
6612
+ */
6613
+ function auditOoxmlPackage(pkg) {
6614
+ const problems = [];
6615
+ const contentTypes = pkg.get(CONTENT_TYPES);
6616
+ if (contentTypes === void 0) return [`missing ${CONTENT_TYPES}`];
6617
+ const types = new TextDecoder().decode(contentTypes);
6618
+ const defaults = new Set([...types.matchAll(/<Default\s+Extension="([^"]+)"/g)].map((match) => (match[1] ?? "").toLowerCase()));
6619
+ const overrides = new Set([...types.matchAll(/<Override\s+PartName="([^"]+)"/g)].map((match) => match[1]));
6620
+ for (const path of pkg.keys()) {
6621
+ if (path === CONTENT_TYPES) continue;
6622
+ if (!defaults.has(extensionOf(path)) && !overrides.has(`/${path}`)) problems.push(`part is not covered by [Content_Types].xml: ${path}`);
6623
+ }
6624
+ for (const [path, bytes] of pkg) {
6625
+ if (!path.endsWith(".rels")) continue;
6626
+ const relsDir = path.slice(0, path.lastIndexOf("_rels/"));
6627
+ for (const match of new TextDecoder().decode(bytes).matchAll(/<Relationship\s[^>]*>/g)) {
6628
+ const tag = match[0];
6629
+ if (/TargetMode="External"/.test(tag)) continue;
6630
+ const target = /Target="([^"]+)"/.exec(tag)?.[1];
6631
+ if (target === void 0 || /^[a-z]+:/i.test(target)) continue;
6632
+ const resolved = resolveRelative(relsDir, decodeURIComponent(target));
6633
+ if (!pkg.has(resolved)) problems.push(`dangling relationship in ${path}: ${target}`);
6634
+ }
6635
+ }
6636
+ for (const [path, bytes] of pkg) {
6637
+ if (extensionOf(path) !== "xml" && !path.endsWith(".rels")) continue;
6638
+ const { body } = splitXmlDeclaration(new TextDecoder().decode(bytes));
6639
+ if (new DOMParser().parseFromString(body, "application/xml").getElementsByTagName("parsererror").length > 0) problems.push(`part is not well-formed XML: ${path}`);
6640
+ }
6641
+ return problems;
6642
+ }
6643
+ /** @throws WebSkillError 自检不干净——宁可当场失败,也不要交一个会弹修复的文件 */
6644
+ function assertOoxmlPackage(pkg) {
6645
+ const problems = auditOoxmlPackage(pkg);
6646
+ if (problems.length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The generated Office file would be structurally invalid: ${problems.join("; ")}.`);
6647
+ }
6648
+ /**
6649
+ * 往 `[Content_Types].xml` 补一条 `<Default Extension>`,已有则原样返回。
6650
+ *
6651
+ * 插图时必须补(AC-42.18 只允许这一处变化);不插图时一个字节都不能动(FR-42.20)。
6652
+ */
6653
+ function ensureDefaultExtension(pkg, extension, contentType) {
6654
+ const bytes = pkg.get(CONTENT_TYPES);
6655
+ if (bytes === void 0) return;
6656
+ const text = new TextDecoder().decode(bytes);
6657
+ if (new RegExp(`<Default\\s+Extension="${extension}"`, "i").test(text)) return;
6658
+ const { declaration, body } = splitXmlDeclaration(text);
6659
+ const inserted = body.replace("<Default", `<Default Extension="${extension}" ContentType="${contentType}"/><Default`);
6660
+ pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + inserted));
6661
+ }
6662
+ /** 往 `[Content_Types].xml` 补一条 `<Override PartName>`,已有则原样返回。 */
6663
+ function ensureOverride(pkg, partName, contentType) {
6664
+ const bytes = pkg.get(CONTENT_TYPES);
6665
+ if (bytes === void 0) return;
6666
+ const text = new TextDecoder().decode(bytes);
6667
+ if (text.includes(`PartName="${partName}"`)) return;
6668
+ const { declaration, body } = splitXmlDeclaration(text);
6669
+ const inserted = body.replace("</Types>", `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`);
6670
+ pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + inserted));
6671
+ }
6672
+ /** 摘掉一个部件连同它在 `[Content_Types].xml` 与指定 rels 里的登记(FR-42.4 修订项)。 */
6673
+ function dropPart(pkg, partName, relsPath) {
6674
+ if (!pkg.delete(partName)) return;
6675
+ const fileName = partName.slice(partName.lastIndexOf("/") + 1);
6676
+ const types = pkg.get(CONTENT_TYPES);
6677
+ if (types !== void 0) {
6678
+ const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(types));
6679
+ const stripped = body.replace(new RegExp(`<Override\\s+PartName="/${partName}"[^>]*/>`), "");
6680
+ pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + stripped));
6681
+ }
6682
+ const rels = pkg.get(relsPath);
6683
+ if (rels !== void 0) {
6684
+ const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(rels));
6685
+ const stripped = body.replace(new RegExp(`<Relationship\\s[^>]*Target="[^"]*${fileName}"[^>]*/>`), "");
6686
+ pkg.set(relsPath, new TextEncoder().encode(declaration + stripped));
6687
+ }
6688
+ }
6689
+
6690
+ //#endregion
6691
+ //#region ../browser/src/document/xlsxTemplate.ts
6692
+ /**
6693
+ * 照着 xlsx 模板出新文件(0.22.0 分册 42 · 设计 43 §4)。
6694
+ *
6695
+ * 不从零生成,而是解开模板 zip 只改 `sheet*.xml`,其余字节原样写回。
6696
+ * 模板里的空单元格本来就是带样式索引的空壳 `<c r="B3" s="6"/>`,
6697
+ * 所以「填值保格式」不是要实现的特性,是不要破坏的既有事实(设计 43 §1.2)。
6698
+ */
6699
+ const MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
6700
+ const REL_NS$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
6701
+ const PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
6702
+ const DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
6703
+ const DRAWING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.drawing+xml";
6704
+ const DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
6705
+ const byTag$1 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
6706
+ /** 工作表名 → 部件路径。名字来自 workbook.xml,路径要经 rels 绕一道。 */
6707
+ function listSheets(pkg) {
6708
+ const { doc } = parseXmlPart(pkg, "xl/workbook.xml");
6709
+ const { doc: relsDoc } = parseXmlPart(pkg, "xl/_rels/workbook.xml.rels");
6710
+ const targets = /* @__PURE__ */ new Map();
6711
+ for (const rel of byTag$1(relsDoc, "Relationship")) {
6712
+ const id = rel.getAttribute("Id");
6713
+ const target = rel.getAttribute("Target");
6714
+ if (id !== null && target !== null) targets.set(id, target.startsWith("/") ? target.slice(1) : `xl/${target}`);
6715
+ }
6716
+ const sheets = [];
6717
+ for (const sheet of byTag$1(doc, "sheet")) {
6718
+ const name = sheet.getAttribute("name");
6719
+ const id = sheet.getAttributeNS(REL_NS$1, "id") ?? sheet.getAttribute("r:id");
6720
+ const path = id === null ? void 0 : targets.get(id);
6721
+ if (name !== null && path !== void 0) sheets.push({
6722
+ name,
6723
+ path
6724
+ });
6725
+ }
6726
+ return sheets;
6727
+ }
6728
+ /** @throws WebSkillError 模板里没有这个工作表 */
6729
+ function resolveSheet(sheets, name) {
6730
+ if (name === void 0) {
6731
+ const first = sheets[0];
6732
+ if (first === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", "The template has no worksheet.");
6733
+ return first;
6734
+ }
6735
+ const found = sheets.find((sheet) => sheet.name === name);
6736
+ if (found === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", `The template has no sheet named "${name}". Available sheets: ${sheets.map((s) => s.name).join(", ")}.`);
6737
+ return found;
6738
+ }
6739
+ function sharedStringsOf(pkg) {
6740
+ if (!pkg.has("xl/sharedStrings.xml")) return [];
6741
+ const { doc } = parseXmlPart(pkg, "xl/sharedStrings.xml");
6742
+ return byTag$1(doc, "si").map((si) => byTag$1(si, "t").map((node) => node.textContent ?? "").join(""));
6743
+ }
6744
+ function cellText(cell, shared) {
6745
+ const type = cell.getAttribute("t");
6746
+ if (type === "inlineStr") return byTag$1(cell, "t").map((node) => node.textContent ?? "").join("");
6747
+ const value = byTag$1(cell, "v")[0]?.textContent ?? "";
6748
+ if (type === "s") {
6749
+ const index = Number(value);
6750
+ return Number.isInteger(index) ? shared[index] ?? "" : "";
6751
+ }
6752
+ return value;
6753
+ }
6754
+ /** 共享公式的宿主:`si` → 公式体与宿主所在行 */
6755
+ function sharedFormulaHosts(doc) {
6756
+ const hosts = /* @__PURE__ */ new Map();
6757
+ for (const formula of byTag$1(doc, "f")) {
6758
+ const si = formula.getAttribute("si");
6759
+ const body = formula.textContent ?? "";
6760
+ if (si === null || body === "") continue;
6761
+ const address = formula.parentElement?.getAttribute("r");
6762
+ if (address === null || address === void 0) continue;
6763
+ hosts.set(si, {
6764
+ body,
6765
+ row: parseCellAddress(address).row
6766
+ });
6767
+ }
6768
+ return hosts;
6769
+ }
6770
+ /** 共享公式的引用格本身不带公式体,按行差从宿主展开,模型才知道它读的是哪几格 */
6771
+ function formulaTextOf(cell, hosts) {
6772
+ const formula = [...cell.children].find((child) => child.localName === "f");
6773
+ if (formula === void 0) return void 0;
6774
+ const body = formula.textContent ?? "";
6775
+ if (body !== "") return body;
6776
+ const host = hosts.get(formula.getAttribute("si") ?? "");
6777
+ const address = cell.getAttribute("r");
6778
+ if (host === void 0 || address === null) return "";
6779
+ return offsetFormulaRows(host.body, parseCellAddress(address).row - host.row);
6780
+ }
6781
+ /** 读带坐标的骨架(FR-42.1)。模型据此决定往哪儿写。 */
6782
+ async function readXlsxTemplate(bytes) {
6783
+ const pkg = await readOoxmlPackage(bytes);
6784
+ const shared = sharedStringsOf(pkg);
6785
+ const sheets = [];
6786
+ for (const entry of listSheets(pkg)) {
6787
+ const { doc } = parseXmlPart(pkg, entry.path);
6788
+ const hosts = sharedFormulaHosts(doc);
6789
+ const cells = [];
6790
+ for (const cell of byTag$1(doc, "c")) {
6791
+ const address = cell.getAttribute("r");
6792
+ if (address === null) continue;
6793
+ const text = cellText(cell, shared);
6794
+ const formula = formulaTextOf(cell, hosts);
6795
+ if (formula !== void 0) cells.push({
6796
+ address,
6797
+ text,
6798
+ formula
6799
+ });
6800
+ else if (text !== "") cells.push({
6801
+ address,
6802
+ text
6803
+ });
6804
+ }
6805
+ const columnWidths = [];
6806
+ for (const col of byTag$1(doc, "col")) {
6807
+ const min = Number(col.getAttribute("min"));
6808
+ const max = Number(col.getAttribute("max"));
6809
+ const width = Number(col.getAttribute("width"));
6810
+ if (!Number.isInteger(min) || !Number.isInteger(max) || !Number.isFinite(width)) continue;
6811
+ for (let index = min; index <= max; index += 1) columnWidths[index - 1] = width;
6812
+ }
6813
+ sheets.push({
6814
+ name: entry.name,
6815
+ range: byTag$1(doc, "dimension")[0]?.getAttribute("ref") ?? "",
6816
+ cells,
6817
+ merges: byTag$1(doc, "mergeCell").map((merge) => merge.getAttribute("ref")).filter((ref) => ref !== null),
6818
+ columnWidths
6819
+ });
6820
+ }
6821
+ return { sheets };
6822
+ }
6823
+ const rowNumberOf = (element) => Number(element.getAttribute("r") ?? NaN);
6824
+ function rowsOf(doc) {
6825
+ const sheetData = byTag$1(doc, "sheetData")[0];
6826
+ return sheetData === void 0 ? [] : [...sheetData.children].filter((node) => node.localName === "row");
6827
+ }
6828
+ /** 把一个元素上的行号整体平移:`<row r>` 与其中每个 `<c r>` */
6829
+ function shiftRow(row, delta) {
6830
+ const next = rowNumberOf(row) + delta;
6831
+ row.setAttribute("r", String(next));
6832
+ for (const cell of byTag$1(row, "c")) {
6833
+ const address = cell.getAttribute("r");
6834
+ if (address === null) continue;
6835
+ const parsed = parseCellAddress(address);
6836
+ cell.setAttribute("r", formatCellAddress({
6837
+ column: parsed.column,
6838
+ row: next - 1
6839
+ }));
6840
+ }
6841
+ }
6842
+ const shiftAddress = (address, at, count) => {
6843
+ const parsed = parseCellAddress(address);
6844
+ return parsed.row + 1 >= at ? formatCellAddress({
6845
+ ...parsed,
6846
+ row: parsed.row + count
6847
+ }) : address;
6848
+ };
6849
+ /**
6850
+ * 插行(FR-42.7)。设计 43 §1.5 实测:必须同时改六处,少动一处就是一个会弹修复的文件。
6851
+ *
6852
+ * @throws WebSkillError 插入位置越界
6853
+ */
6854
+ function insertRows(pkg, doc, insert) {
6855
+ const { at, count } = insert;
6856
+ const rows = rowsOf(doc);
6857
+ const lastRow = rows[rows.length - 1];
6858
+ const last = lastRow === void 0 ? 0 : rowNumberOf(lastRow);
6859
+ if (!Number.isInteger(at) || at < 2 || at > last + 1 || !Number.isInteger(count) || count < 1) throw new WebSkillError("TEMPLATE_ROW_INSERT_RANGE", `Cannot insert ${count} row(s) at row ${at}: the sheet has rows 1..${last}.`);
6860
+ const styleRowNumber = insert.copyStyleFrom ?? at - 1;
6861
+ const template = rows.find((row) => rowNumberOf(row) === styleRowNumber);
6862
+ if (template === void 0) throw new WebSkillError("TEMPLATE_ROW_INSERT_RANGE", `Cannot copy style from row ${styleRowNumber}: that row does not exist in the sheet.`);
6863
+ const formulas = formulasOf(template);
6864
+ for (const row of [...rows].reverse()) if (rowNumberOf(row) >= at) shiftRow(row, count);
6865
+ const sheetData = byTag$1(doc, "sheetData")[0];
6866
+ const anchor = rowsOf(doc).find((row) => rowNumberOf(row) >= at + count) ?? null;
6867
+ const made = [];
6868
+ for (let index = 0; index < count; index += 1) {
6869
+ const row = template.cloneNode(true);
6870
+ row.setAttribute("r", String(at + index));
6871
+ for (const cell of byTag$1(row, "c")) {
6872
+ const address = cell.getAttribute("r");
6873
+ if (address !== null) {
6874
+ const parsed = parseCellAddress(address);
6875
+ cell.setAttribute("r", formatCellAddress({
6876
+ column: parsed.column,
6877
+ row: at + index - 1
6878
+ }));
6879
+ }
6880
+ cell.removeAttribute("t");
6881
+ while (cell.firstChild !== null) cell.removeChild(cell.firstChild);
6882
+ }
6883
+ sheetData?.insertBefore(row, anchor);
6884
+ made.push(row);
6885
+ }
6886
+ for (const formula of byTag$1(doc, "f")) {
6887
+ const ref = formula.getAttribute("ref");
6888
+ if (ref !== null) formula.setAttribute("ref", growRange(ref, at, count));
6889
+ const body = formula.textContent;
6890
+ if (body !== null && body !== "") formula.textContent = growFormulaRanges(body, at, count);
6891
+ }
6892
+ for (const merge of byTag$1(doc, "mergeCell")) {
6893
+ const ref = merge.getAttribute("ref");
6894
+ if (ref === null) continue;
6895
+ const [from = "", to] = ref.split(":");
6896
+ merge.setAttribute("ref", to === void 0 ? shiftAddress(from, at, count) : `${shiftAddress(from, at, count)}:${shiftAddress(to, at, count)}`);
6897
+ }
6898
+ const dimension = byTag$1(doc, "dimension")[0];
6899
+ const dimensionRef = dimension?.getAttribute("ref");
6900
+ if (dimension !== void 0 && dimensionRef != null) dimension.setAttribute("ref", growRange(dimensionRef, at, count));
6901
+ dropPart(pkg, "xl/calcChain.xml", "xl/_rels/workbook.xml.rels");
6902
+ for (const [index, row] of made.entries()) restoreFormulas(row, formulas, at + index - styleRowNumber);
6903
+ }
6904
+ /**
6905
+ * 记下一行里每个格的 `<f>`。
6906
+ *
6907
+ * 共享公式宿主(带 `ref`)不能被复制成第二个宿主——同一个 `si` 只许一个宿主,
6908
+ * 多一个 Excel 就报损坏。复制出来的一律降级成引用:`<f t="shared" si="0"/>`。
6909
+ */
6910
+ function formulasOf(row) {
6911
+ const copied = [];
6912
+ for (const cell of byTag$1(row, "c")) {
6913
+ const address = cell.getAttribute("r");
6914
+ const formula = [...cell.children].find((child) => child.localName === "f");
6915
+ if (address === null || formula === void 0) continue;
6916
+ const host = formula.getAttribute("ref") !== null;
6917
+ const value = [...cell.children].find((child) => child.localName === "v");
6918
+ copied.push({
6919
+ column: parseCellAddress(address).column,
6920
+ attributes: [...formula.attributes].filter((attribute) => attribute.name !== "ref").map((attribute) => [attribute.name, attribute.value]),
6921
+ body: host || formula.getAttribute("t") === "shared" ? "" : formula.textContent ?? "",
6922
+ cached: value?.textContent ?? void 0,
6923
+ type: cell.getAttribute("t") ?? void 0
6924
+ });
6925
+ }
6926
+ return copied;
6927
+ }
6928
+ function restoreFormulas(row, formulas, delta) {
6929
+ if (formulas.length === 0) return;
6930
+ const byColumn = new Map(byTag$1(row, "c").map((cell) => [parseCellAddress(cell.getAttribute("r") ?? "A1").column, cell]));
6931
+ for (const formula of formulas) {
6932
+ const cell = byColumn.get(formula.column);
6933
+ if (cell === void 0) continue;
6934
+ const doc = row.ownerDocument;
6935
+ const made = doc.createElementNS(MAIN_NS, "f");
6936
+ for (const [name, value] of formula.attributes) made.setAttribute(name, value);
6937
+ if (formula.body !== "") made.textContent = offsetFormulaRows(formula.body, delta);
6938
+ cell.appendChild(made);
6939
+ if (formula.cached === void 0) continue;
6940
+ if (formula.type !== void 0) cell.setAttribute("t", formula.type);
6941
+ const value = doc.createElementNS(MAIN_NS, "v");
6942
+ value.textContent = formula.cached;
6943
+ cell.appendChild(value);
6944
+ }
6945
+ }
6946
+ /** 公式里的相对行引用按行差重写,`$12` 这种绝对行原样不动——就是 Excel 向下填充的语义 */
6947
+ const RELATIVE_ROW = /(\$?[A-Z]{1,3})(\$?)([1-9][0-9]*)/g;
6948
+ function offsetFormulaRows(body, delta) {
6949
+ return body.replace(RELATIVE_ROW, (match, column, absolute, row) => absolute === "$" ? match : `${column}${Math.max(1, Number(row) + delta)}`);
6950
+ }
6951
+ /** 区间的下界在插入点之后才平移,上界只要不在插入点之前就跟着长 */
6952
+ function growRange(ref, at, count) {
6953
+ const [from = "", to] = ref.split(":");
6954
+ if (to === void 0) return shiftAddress(from, at, count);
6955
+ const start = parseCellAddress(from);
6956
+ const end = parseCellAddress(to);
6957
+ const nextStart = start.row + 1 >= at ? {
6958
+ ...start,
6959
+ row: start.row + count
6960
+ } : start;
6961
+ const nextEnd = end.row + 1 >= at - 1 ? {
6962
+ ...end,
6963
+ row: end.row + count
6964
+ } : end;
6965
+ return `${formatCellAddress(nextStart)}:${formatCellAddress(nextEnd)}`;
6966
+ }
6967
+ const RANGE_IN_FORMULA = /\$?([A-Z]{1,3})\$?([1-9][0-9]*):\$?([A-Z]{1,3})\$?([1-9][0-9]*)/g;
6968
+ /** `SUM(G6:G12)` → `SUM(G6:G25)`:公式里的区间同样要跟着长 */
6969
+ function growFormulaRanges(formula, at, count) {
6970
+ return formula.replace(RANGE_IN_FORMULA, (whole, startCol, startRow, endCol, endRow) => {
6971
+ const start = Number(startRow);
6972
+ const end = Number(endRow);
6973
+ const nextStart = start >= at ? start + count : start;
6974
+ const nextEnd = end >= at - 1 ? end + count : end;
6975
+ return nextStart === start && nextEnd === end ? whole : `${startCol}${nextStart}:${endCol}${nextEnd}`;
6976
+ });
6977
+ }
6978
+ /**
6979
+ * 写值(FR-42.5)。数字写成数字,其余写成文本,不做类型推断(FR-42.26)。
6980
+ *
6981
+ * 用 `inlineStr` 而不是往共享串表追加:改动的数据部件从两个降到一个(设计 43 §1.4)。
6982
+ * 单元格有三种形态(空壳 / 带值 / 不存在),三支都显式处理——静默跳过正是 FR-42.6 要禁的。
6983
+ *
6984
+ * @throws WebSkillError 目标格是公式格
6985
+ */
6986
+ function fillCell(doc, address, value) {
6987
+ const parsed = parseCellAddress(address);
6988
+ const normalized = formatCellAddress(parsed);
6989
+ const row = rowsOf(doc).find((candidate) => rowNumberOf(candidate) === parsed.row + 1) ?? createRow(doc, parsed.row + 1);
6990
+ let cell = byTag$1(row, "c").find((candidate) => candidate.getAttribute("r") === normalized);
6991
+ if (cell === void 0) cell = createCell(doc, row, parsed.column, normalized);
6992
+ const formula = [...cell.children].find((child) => child.localName === "f");
6993
+ if (formula !== void 0) {
6994
+ const body = formula.textContent ?? "";
6995
+ throw new WebSkillError("TEMPLATE_CELL_IS_FORMULA", `Cell ${normalized} is computed by the template${body === "" ? "" : ` (=${body})`} and must not be overwritten. Fill the cells it reads instead; the template works this one out when the file is opened.`);
6996
+ }
6997
+ while (cell.firstChild !== null) cell.removeChild(cell.firstChild);
6998
+ if (value === "") {
6999
+ cell.removeAttribute("t");
7000
+ return;
7001
+ }
7002
+ if (looksNumeric(value)) {
7003
+ cell.removeAttribute("t");
7004
+ const v = doc.createElementNS(MAIN_NS, "v");
7005
+ v.textContent = value;
7006
+ cell.appendChild(v);
7007
+ return;
7008
+ }
7009
+ cell.setAttribute("t", "inlineStr");
7010
+ const is = doc.createElementNS(MAIN_NS, "is");
7011
+ const t = doc.createElementNS(MAIN_NS, "t");
7012
+ if (value !== value.trim()) t.setAttribute("xml:space", "preserve");
7013
+ t.textContent = value;
7014
+ is.appendChild(t);
7015
+ cell.appendChild(is);
7016
+ }
7017
+ /**
7018
+ * 只有**原样往返**的才算数字:`Number(x)` 转回字符串必须与原文逐字符相同。
7019
+ *
7020
+ * 这一条同时挡住了 `"0123"`(编号、身份证、电话)与 `" 12 "`(带空白的文本),
7021
+ * 它们转回来分别是 `"123"` 和 `"12"`,与原文不同,所以仍然写成文本(AC-42.9)。
7022
+ */
7023
+ function looksNumeric(value) {
7024
+ const parsed = Number(value);
7025
+ return Number.isFinite(parsed) && String(parsed) === value;
7026
+ }
7027
+ /** 新建节点必须用 createElementNS:在有默认命名空间的文档里 createElement 会带出 `xmlns=""`,Excel 拒收 */
7028
+ function createRow(doc, rowNumber) {
7029
+ const sheetData = byTag$1(doc, "sheetData")[0];
7030
+ if (sheetData === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", "The worksheet part has no sheetData element.");
7031
+ const row = doc.createElementNS(MAIN_NS, "row");
7032
+ row.setAttribute("r", String(rowNumber));
7033
+ const anchor = rowsOf(doc).find((candidate) => rowNumberOf(candidate) > rowNumber) ?? null;
7034
+ sheetData.insertBefore(row, anchor);
7035
+ return row;
7036
+ }
7037
+ function createCell(doc, row, column, address) {
7038
+ const cell = doc.createElementNS(MAIN_NS, "c");
7039
+ cell.setAttribute("r", address);
7040
+ const anchor = byTag$1(row, "c").find((candidate) => {
7041
+ const other = candidate.getAttribute("r");
7042
+ return other !== null && parseCellAddress(other).column > column;
7043
+ }) ?? null;
7044
+ row.insertBefore(cell, anchor);
7045
+ return cell;
7046
+ }
7047
+ function geometryOf(doc) {
7048
+ const columnWidths = [];
7049
+ for (const col of byTag$1(doc, "col")) {
7050
+ const min = Number(col.getAttribute("min"));
7051
+ const max = Number(col.getAttribute("max"));
7052
+ const width = Number(col.getAttribute("width"));
7053
+ if (!Number.isInteger(min) || !Number.isInteger(max) || !Number.isFinite(width)) continue;
7054
+ for (let index = min; index <= max; index += 1) columnWidths[index - 1] = width;
7055
+ }
7056
+ const rowHeights = /* @__PURE__ */ new Map();
7057
+ for (const row of rowsOf(doc)) {
7058
+ const height = Number(row.getAttribute("ht"));
7059
+ if (Number.isFinite(height)) rowHeights.set(rowNumberOf(row), height);
7060
+ }
7061
+ return {
7062
+ columnWidths,
7063
+ rowHeights,
7064
+ defaultRowHeight: Number(byTag$1(doc, "sheetFormatPr")[0]?.getAttribute("defaultRowHeight") ?? 15),
7065
+ merges: byTag$1(doc, "mergeCell").map((merge) => merge.getAttribute("ref")).filter((ref) => ref !== null)
7066
+ };
7067
+ }
7068
+ const DEFAULT_COLUMN_WIDTH = 8.43;
7069
+ /** 锚格所在的框。落在合并区左上角时按整个合并区算(FR-42.11)。 */
7070
+ function anchorBox(geometry, address) {
7071
+ const cell = parseCellAddress(address);
7072
+ let lastColumn = cell.column;
7073
+ let lastRow = cell.row;
7074
+ for (const ref of geometry.merges) {
7075
+ const range = parseCellRange(ref);
7076
+ if (range.start.column === cell.column && range.start.row === cell.row) {
7077
+ lastColumn = range.end.column;
7078
+ lastRow = range.end.row;
7079
+ break;
7080
+ }
7081
+ }
7082
+ let width = 0;
7083
+ for (let column = cell.column; column <= lastColumn; column += 1) width += columnWidthToPixels(geometry.columnWidths[column] ?? DEFAULT_COLUMN_WIDTH) * EMU_PER_PIXEL;
7084
+ let height = 0;
7085
+ for (let row = cell.row; row <= lastRow; row += 1) height += (geometry.rowHeights.get(row + 1) ?? geometry.defaultRowHeight) * EMU_PER_POINT;
7086
+ return {
7087
+ width,
7088
+ height,
7089
+ span: lastColumn - cell.column + 1
7090
+ };
7091
+ }
7092
+ function xmlPart(text) {
7093
+ return new TextEncoder().encode(DECLARATION + text);
7094
+ }
7095
+ /** 给工作表装上 drawing 全家桶:drawing 部件、两条 rels、media、Content_Types 登记 */
7096
+ function attachImages(pkg, doc, sheetPath, images, geometry) {
7097
+ if (images.length === 0) return;
7098
+ const sheetFile = sheetPath.slice(sheetPath.lastIndexOf("/") + 1);
7099
+ const sheetIndex = /(\d+)\.xml$/.exec(sheetFile)?.[1] ?? "1";
7100
+ const drawingPath = `xl/drawings/drawing${sheetIndex}.xml`;
7101
+ const anchors = [];
7102
+ const drawingRels = [];
7103
+ images.forEach((image, index) => {
7104
+ const size = readImageSize(image.bytes);
7105
+ if (size === void 0) throw new WebSkillError("TEMPLATE_IMAGE_UNRESOLVED", `The image for cell ${image.address} is not a PNG or JPEG file.`);
7106
+ const mediaName = `image${index + 1}.${image.extension}`;
7107
+ pkg.set(`xl/media/${mediaName}`, image.bytes);
7108
+ ensureDefaultExtension(pkg, image.extension, image.contentType);
7109
+ drawingRels.push(`<Relationship Id="rIdImg${index + 1}" Type="${REL_NS$1}/image" Target="../media/${mediaName}"/>`);
7110
+ const box = anchorBox(geometry, image.address);
7111
+ const fitted = fitWithin({
7112
+ width: size.width * EMU_PER_PIXEL,
7113
+ height: size.height * EMU_PER_PIXEL
7114
+ }, box);
7115
+ const cell = parseCellAddress(image.address);
7116
+ const id = index + 1;
7117
+ anchors.push(`<xdr:oneCellAnchor><xdr:from><xdr:col>${cell.column}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>${cell.row}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from><xdr:ext cx="${fitted.width}" cy="${fitted.height}"/><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${id}" name="Picture ${id}"/><xdr:cNvPicPr><a:picLocks noChangeAspect="1"/></xdr:cNvPicPr></xdr:nvPicPr><xdr:blipFill><a:blip xmlns:r="${REL_NS$1}" r:embed="rIdImg${id}"/><a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${fitted.width}" cy="${fitted.height}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:oneCellAnchor>`);
7118
+ });
7119
+ pkg.set(drawingPath, xmlPart(`<xdr:wsDr xmlns:xdr="${DRAWING_NS}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">${anchors.join("")}</xdr:wsDr>`));
7120
+ pkg.set(`xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, xmlPart(`<Relationships xmlns="${PACKAGE_REL_NS}">${drawingRels.join("")}</Relationships>`));
7121
+ ensureOverride(pkg, `/${drawingPath}`, DRAWING_CONTENT_TYPE);
7122
+ const relId = addSheetRelationship(pkg, `xl/worksheets/_rels/${sheetFile}.rels`, `${REL_NS$1}/drawing`, `../drawings/drawing${sheetIndex}.xml`);
7123
+ const existing = byTag$1(doc, "drawing")[0];
7124
+ if (existing !== void 0) existing.setAttributeNS(REL_NS$1, "r:id", relId);
7125
+ else {
7126
+ const drawing = doc.createElementNS(MAIN_NS, "drawing");
7127
+ drawing.setAttributeNS(REL_NS$1, "r:id", relId);
7128
+ doc.documentElement.appendChild(drawing);
7129
+ }
7130
+ }
7131
+ /** 工作表的 rels 可能整个不存在(工作面移交表就没有),缺了就建 */
7132
+ function addSheetRelationship(pkg, relsPath, type, target) {
7133
+ const existing = pkg.get(relsPath);
7134
+ if (existing === void 0) {
7135
+ pkg.set(relsPath, xmlPart(`<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rIdDrawing" Type="${type}" Target="${target}"/></Relationships>`));
7136
+ return "rIdDrawing";
7137
+ }
7138
+ const text = new TextDecoder().decode(existing);
7139
+ const already = new RegExp(`<Relationship[^>]*Target="${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*/>`).exec(text);
7140
+ if (already !== null) return /Id="([^"]+)"/.exec(already[0])?.[1] ?? "rIdDrawing";
7141
+ const updated = text.replace("</Relationships>", `<Relationship Id="rIdDrawing" Type="${type}" Target="${target}"/></Relationships>`);
7142
+ pkg.set(relsPath, new TextEncoder().encode(updated));
7143
+ return "rIdDrawing";
7144
+ }
7145
+ /**
7146
+ * 照模板出新文件。
7147
+ *
7148
+ * 顺序是**先插行、后填值**:填值用的地址是插行之后的坐标,反过来会逼模型心算偏移。
7149
+ *
7150
+ * @throws WebSkillError 模板不是 xlsx、地址不合法、目标不存在、插行越界
7151
+ */
7152
+ async function fillXlsxTemplate(bytes, spec) {
7153
+ const pkg = await readOoxmlPackage(bytes);
7154
+ const sheets = listSheets(pkg);
7155
+ const touched = /* @__PURE__ */ new Map();
7156
+ const partOf = (name) => {
7157
+ const entry = resolveSheet(sheets, name);
7158
+ const cached = touched.get(entry.path) ?? parseXmlPart(pkg, entry.path);
7159
+ touched.set(entry.path, cached);
7160
+ return {
7161
+ entry,
7162
+ ...cached
7163
+ };
7164
+ };
7165
+ for (const insert of spec.rows ?? []) {
7166
+ const { doc } = partOf(insert.sheet);
7167
+ insertRows(pkg, doc, insert);
7168
+ }
7169
+ for (const fill of spec.cells ?? []) {
7170
+ const { doc } = partOf(fill.sheet);
7171
+ fillCell(doc, fill.address, fill.value);
7172
+ }
7173
+ const bySheet = /* @__PURE__ */ new Map();
7174
+ for (const image of spec.images ?? []) {
7175
+ const { entry } = partOf(image.sheet);
7176
+ bySheet.set(entry.path, [...bySheet.get(entry.path) ?? [], image]);
7177
+ }
7178
+ for (const [path, images] of bySheet) {
7179
+ const cached = touched.get(path);
7180
+ if (cached === void 0) continue;
7181
+ attachImages(pkg, cached.doc, path, images, geometryOf(cached.doc));
7182
+ }
7183
+ if ([...touched.values()].some(({ doc }) => byTag$1(doc, "f").length > 0)) requestFullCalcOnLoad(pkg);
7184
+ for (const [path, { declaration, doc }] of touched) pkg.set(path, serializeXmlPart(declaration, doc));
7185
+ assertOoxmlPackage(pkg);
7186
+ return writeOoxmlPackage(pkg);
7187
+ }
7188
+ /** `<calcPr>` 在 CT_Workbook 的序列里排在这几个之后;追加到末尾会落到 `extLst` 后面,那是不合法的 */
7189
+ const BEFORE_CALC_PR = [
7190
+ "sheets",
7191
+ "functionGroups",
7192
+ "externalReferences",
7193
+ "definedNames"
7194
+ ];
7195
+ /**
7196
+ * 让 Excel 打开时重算一遍(FR-42.27)。
7197
+ *
7198
+ * 公式格里存着上一次的计算结果。改了它依赖的那些格之后,这个结果就是错的,
7199
+ * 而 Excel 默认信任缓存——用户看到的是一张「明细都填好了、合计还是 0」的表,
7200
+ * 直到他随手双击一个格才恢复正常。只在动过的表里确实有公式时才写这一笔。
7201
+ */
7202
+ function requestFullCalcOnLoad(pkg) {
7203
+ const { declaration, doc } = parseXmlPart(pkg, "xl/workbook.xml");
7204
+ const existing = byTag$1(doc, "calcPr")[0];
7205
+ const calcPr = existing ?? doc.createElementNS(MAIN_NS, "calcPr");
7206
+ calcPr.setAttribute("fullCalcOnLoad", "1");
7207
+ if (existing === void 0) {
7208
+ const workbook = doc.documentElement;
7209
+ const after = [...workbook.children].filter((node) => BEFORE_CALC_PR.includes(node.localName)).pop();
7210
+ workbook.insertBefore(calcPr, after?.nextSibling ?? null);
7211
+ }
7212
+ pkg.set("xl/workbook.xml", serializeXmlPart(declaration, doc));
7213
+ }
7214
+
7215
+ //#endregion
7216
+ //#region ../browser/src/document/docxTemplate.ts
7217
+ /**
7218
+ * 照着 docx 模板出新文件(0.22.0 分册 42 · 设计 43 §5)。
7219
+ *
7220
+ * 与 xlsx 同一条路,结构还更简单:只有 `word/document.xml` 一个数据部件。
7221
+ * 保格式的办法也同构——改已有 run 的文字而不是重建 run,`<w:rPr>` 就自己活下来了。
7222
+ */
7223
+ const WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
7224
+ const REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
7225
+ const DOCUMENT = "word/document.xml";
7226
+ const DOCUMENT_RELS = "word/_rels/document.xml.rels";
7227
+ const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
7228
+ const childrenNamed = (scope, name) => [...scope.children].filter((node) => node.localName === name);
7229
+ /** 顶层表格(不含嵌套表),顺序即模型看到的表序 */
7230
+ function tablesOf(doc) {
7231
+ const body = byTag(doc, "body")[0];
7232
+ return body === void 0 ? [] : childrenNamed(body, "tbl");
7233
+ }
7234
+ /** 正文段落:只算 body 的直接子节点,表格内的段落不计入序号 */
7235
+ function paragraphsOf(doc) {
7236
+ const body = byTag(doc, "body")[0];
7237
+ return body === void 0 ? [] : childrenNamed(body, "p");
7238
+ }
7239
+ const textOf = (scope) => byTag(scope, "t").map((node) => node.textContent ?? "").join("");
7240
+ const intAttr = (element, name) => {
7241
+ const raw = element?.getAttributeNS(WORD_NS, name) ?? element?.getAttribute(`w:${name}`) ?? element?.getAttribute(name);
7242
+ const value = Number(raw);
7243
+ return Number.isFinite(value) ? value : void 0;
7244
+ };
7245
+ /** 读带坐标的骨架(FR-42.1) */
7246
+ async function readDocxTemplate(bytes) {
7247
+ const { doc } = parseXmlPart(await readOoxmlPackage(bytes), DOCUMENT);
7248
+ const paragraphs = paragraphsOf(doc).map((paragraph, index) => ({
7249
+ index,
7250
+ text: textOf(paragraph)
7251
+ }));
7252
+ const cells = [];
7253
+ tablesOf(doc).forEach((table, tableIndex) => {
7254
+ childrenNamed(table, "tr").forEach((row, rowIndex) => {
7255
+ childrenNamed(row, "tc").forEach((cell, columnIndex) => {
7256
+ const properties = childrenNamed(cell, "tcPr")[0];
7257
+ const gridSpan = properties === void 0 ? void 0 : intAttr(childrenNamed(properties, "gridSpan")[0], "val");
7258
+ const merge = properties === void 0 ? void 0 : childrenNamed(properties, "vMerge")[0];
7259
+ cells.push({
7260
+ table: tableIndex,
7261
+ row: rowIndex,
7262
+ column: columnIndex,
7263
+ text: textOf(cell),
7264
+ ...gridSpan === void 0 ? {} : { gridSpan },
7265
+ ...merge === void 0 ? {} : { vMerge: merge.getAttributeNS(WORD_NS, "val") ?? merge.getAttribute("w:val") ?? "continue" }
7266
+ });
7267
+ });
7268
+ });
7269
+ });
7270
+ return {
7271
+ paragraphs,
7272
+ cells
7273
+ };
7274
+ }
7275
+ /** @throws WebSkillError 坐标指到不存在的表 / 行 / 列 / 段落 */
7276
+ function locate(doc, target) {
7277
+ if (target.table !== void 0) {
7278
+ const tables = tablesOf(doc);
7279
+ const table = tables[target.table];
7280
+ if (table === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", `The document has no table at index ${target.table}; it has ${tables.length} table(s).`);
7281
+ const rows = childrenNamed(table, "tr");
7282
+ const row = rows[target.row ?? -1];
7283
+ if (row === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", `Table ${target.table} has no row at index ${target.row}; it has ${rows.length} row(s).`);
7284
+ const cells = childrenNamed(row, "tc");
7285
+ const cell = cells[target.column ?? -1];
7286
+ if (cell === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", `Row ${target.row} of table ${target.table} has no cell at index ${target.column}; it has ${cells.length} cell(s).`);
7287
+ return cell;
7288
+ }
7289
+ const paragraphs = paragraphsOf(doc);
7290
+ const paragraph = paragraphs[target.paragraph ?? -1];
7291
+ if (paragraph === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", `The document has no body paragraph at index ${target.paragraph}; it has ${paragraphs.length} paragraph(s).`);
7292
+ return paragraph;
7293
+ }
7294
+ /**
7295
+ * 写文字:改已有 run 的 `<w:t>`,多余的 run 删掉。
7296
+ *
7297
+ * 不重建 run,是因为 `<w:rPr>`(字体、字号、加粗)就挂在 run 上——
7298
+ * 与 xlsx 那边靠 `<c>` 的 `s` 属性保格式是同一个道理(设计 43 §1.2)。
7299
+ */
7300
+ function fillText(doc, scope, value) {
7301
+ const paragraphs = scope.localName === "p" ? [scope] : childrenNamed(scope, "p");
7302
+ const first = paragraphs[0];
7303
+ if (first === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", "The target has no paragraph to write text into.");
7304
+ paragraphs.slice(1).forEach((paragraph) => {
7305
+ for (const run of childrenNamed(paragraph, "r")) paragraph.removeChild(run);
7306
+ });
7307
+ const runs = childrenNamed(first, "r").filter((run) => byTag(run, "drawing").length === 0);
7308
+ const keep = runs[0];
7309
+ runs.slice(1).forEach((run) => first.removeChild(run));
7310
+ if (keep === void 0) {
7311
+ if (value === "") return;
7312
+ const run = doc.createElementNS(WORD_NS, "w:r");
7313
+ run.appendChild(textNode(doc, value));
7314
+ first.appendChild(run);
7315
+ return;
7316
+ }
7317
+ if (value === "") {
7318
+ first.removeChild(keep);
7319
+ return;
7320
+ }
7321
+ for (const node of childrenNamed(keep, "t")) keep.removeChild(node);
7322
+ keep.appendChild(textNode(doc, value));
7323
+ }
7324
+ /** 新建节点用 createElementNS 带前缀:document.xml 的默认命名空间不是 w,裸 createElement 会带出 xmlns="" */
7325
+ function textNode(doc, value) {
7326
+ const node = doc.createElementNS(WORD_NS, "w:t");
7327
+ if (value !== value.trim()) node.setAttribute("xml:space", "preserve");
7328
+ node.textContent = value;
7329
+ return node;
7330
+ }
7331
+ /** 版心宽度:`pgSz` 减两边 `pgMar`(twip) */
7332
+ function bodyWidthTwips(doc) {
7333
+ const size = byTag(doc, "pgSz")[0];
7334
+ const margin = byTag(doc, "pgMar")[0];
7335
+ const width = intAttr(size, "w") ?? 11906;
7336
+ const left = intAttr(margin, "left") ?? 1440;
7337
+ const right = intAttr(margin, "right") ?? 1440;
7338
+ return Math.max(width - left - right, 0);
7339
+ }
7340
+ /**
7341
+ * 落点的可用框(twip)。
7342
+ *
7343
+ * 宽度优先取单元格自己的 `tcW`——`工作面移交表.docx` 的表格是故意比版心宽的,
7344
+ * 退回版心宽会把图算小(设计 43 §1.6)。`tcW` 缺失时按 `gridSpan` 累加 `tblGrid`。
7345
+ * 行声明了 `trHeight` 才有高度约束,没有就只受宽度约束。
7346
+ */
7347
+ function boxTwipsOf(doc, scope) {
7348
+ if (scope.localName !== "tc") return {
7349
+ width: bodyWidthTwips(doc),
7350
+ height: 0
7351
+ };
7352
+ const properties = childrenNamed(scope, "tcPr")[0];
7353
+ const width = intAttr(properties === void 0 ? void 0 : childrenNamed(properties, "tcW")[0], "w");
7354
+ const row = scope.parentElement;
7355
+ const table = row?.parentElement;
7356
+ const span = properties === void 0 ? 1 : intAttr(childrenNamed(properties, "gridSpan")[0], "val") ?? 1;
7357
+ let resolved = width !== void 0 && width > 0 ? width : 0;
7358
+ if (resolved === 0 && table !== null && table !== void 0) {
7359
+ const grid = byTag(table, "gridCol").map((column) => intAttr(column, "w") ?? 0);
7360
+ const start = row === null ? 0 : childrenNamed(row, "tc").indexOf(scope);
7361
+ resolved = grid.slice(start, start + span).reduce((sum, value) => sum + value, 0);
7362
+ }
7363
+ if (resolved === 0) resolved = bodyWidthTwips(doc);
7364
+ const rowProperties = row === null ? void 0 : childrenNamed(row, "trPr")[0];
7365
+ const height = rowProperties === void 0 ? 0 : intAttr(childrenNamed(rowProperties, "trHeight")[0], "val") ?? 0;
7366
+ return {
7367
+ width: resolved,
7368
+ height
7369
+ };
7370
+ }
7371
+ function nextRelationshipId(pkg) {
7372
+ const text = new TextDecoder().decode(pkg.get(DOCUMENT_RELS) ?? /* @__PURE__ */ new Uint8Array());
7373
+ let max = 0;
7374
+ for (const match of text.matchAll(/Id="rId(\d+)"/g)) max = Math.max(max, Number(match[1]));
7375
+ return `rId${max + 1}`;
7376
+ }
7377
+ function addImageRelationship(pkg, id, target) {
7378
+ const bytes = pkg.get(DOCUMENT_RELS);
7379
+ if (bytes === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The template has no ${DOCUMENT_RELS} part.`);
7380
+ const updated = new TextDecoder().decode(bytes).replace("</Relationships>", `<Relationship Id="${id}" Type="${REL_NS}/image" Target="${target}"/></Relationships>`);
7381
+ pkg.set(DOCUMENT_RELS, new TextEncoder().encode(updated));
7382
+ }
7383
+ /**
7384
+ * 插图(FR-42.18)。用 `<wp:inline>` 而不是 `<wp:anchor>`:
7385
+ * inline 参与文档流,跟着单元格走;anchor 是浮动的,分页一变就跑位。
7386
+ */
7387
+ function attachImage(pkg, doc, scope, image, index) {
7388
+ const size = readImageSize(image.bytes);
7389
+ if (size === void 0) throw new WebSkillError("TEMPLATE_IMAGE_UNRESOLVED", "The image to insert is not a PNG or JPEG file.");
7390
+ const mediaName = `webskillImage${index + 1}.${image.extension}`;
7391
+ pkg.set(`word/media/${mediaName}`, image.bytes);
7392
+ ensureDefaultExtension(pkg, image.extension, image.contentType);
7393
+ const relId = nextRelationshipId(pkg);
7394
+ addImageRelationship(pkg, relId, `media/${mediaName}`);
7395
+ const box = boxTwipsOf(doc, scope);
7396
+ const fitted = fitWithin({
7397
+ width: size.width * EMU_PER_PIXEL,
7398
+ height: size.height * EMU_PER_PIXEL
7399
+ }, {
7400
+ width: box.width * 635,
7401
+ height: box.height * 635
7402
+ });
7403
+ const id = index + 1;
7404
+ const drawing = `<w:r xmlns:w="${WORD_NS}"><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"><wp:extent cx="${fitted.width}" cy="${fitted.height}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="${1e3 + id}" name="Picture ${id}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:nvPicPr><pic:cNvPr id="${1e3 + id}" name="${mediaName}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip xmlns:r="${REL_NS}" r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${fitted.width}" cy="${fitted.height}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
7405
+ const parsed = new DOMParser().parseFromString(drawing, "application/xml");
7406
+ if (parsed.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Failed to build the DrawingML fragment for the image.");
7407
+ const run = doc.importNode(parsed.documentElement, true);
7408
+ const paragraphs = scope.localName === "p" ? [scope] : childrenNamed(scope, "p");
7409
+ const host = paragraphs[paragraphs.length - 1];
7410
+ if (host === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", "The target has no paragraph to place the image in.");
7411
+ host.appendChild(run);
7412
+ }
7413
+ /**
7414
+ * 照模板出新文件。
7415
+ *
7416
+ * @throws WebSkillError 模板不是 docx、坐标指到不存在的表 / 行 / 列 / 段落、图片认不出来
7417
+ */
7418
+ async function fillDocxTemplate(bytes, spec) {
7419
+ const pkg = await readOoxmlPackage(bytes);
7420
+ const { declaration, doc } = parseXmlPart(pkg, DOCUMENT);
7421
+ for (const text of spec.texts ?? []) fillText(doc, locate(doc, text), text.value);
7422
+ (spec.images ?? []).forEach((image, index) => attachImage(pkg, doc, locate(doc, image), image, index));
7423
+ pkg.set(DOCUMENT, serializeXmlPart(declaration, doc));
7424
+ assertOoxmlPackage(pkg);
7425
+ return writeOoxmlPackage(pkg);
7426
+ }
7427
+
5877
7428
  //#endregion
5878
7429
  //#region ../browser/src/viewer/viewerCsp.ts
5879
7430
  /**
@@ -5908,6 +7459,24 @@ const SANDBOX_TOKENS = [
5908
7459
  "allow-downloads"
5909
7460
  ];
5910
7461
  /**
7462
+ * 投放面图片来源的单一事实源(0.22.0 分册 13 FR-13.9)。
7463
+ *
7464
+ * **不含宿主自身**:两套宿主写法不同——站点写自己的 origin(opaque origin 下 `'self'` 匹配不上),
7465
+ * 扩展的 sandbox 页写 `'self'`。能收敛的是**除宿主之外**放行了哪些来源,
7466
+ * 而那恰恰是会漂走的部分(扩展的 sandbox CSP 此前整条 `img-src` 都不存在,等于全放)。
7467
+ * @experimental
7468
+ */
7469
+ const VIEWER_IMG_SRC_BASE = ["data:"];
7470
+ /**
7471
+ * 降级形态(外链图,FR-13.6)额外需要的来源。
7472
+ *
7473
+ * ⚠️ 加它进来的代价见 `ViewerCspOptions.imgSrc`:投放面因此多了一条外发信道。
7474
+ * `http:` 也在其中,因为需求裁决了「`http` 尽力而为」(分册 13 §2.2)——
7475
+ * 抓得到就嵌字节,抓不到才落到这条外链上,此时拦掉它只会让图变成一个破图标。
7476
+ * @experimental
7477
+ */
7478
+ const VIEWER_IMG_SRC_REMOTE = ["https:", "http:"];
7479
+ /**
5911
7480
  * 宿主配置值不得夹带 CSP 分隔符 —— 否则一个来源字符串就能追加任意指令,
5912
7481
  * 白名单形同虚设。这是配置注入,不是理论风险。
5913
7482
  */
@@ -5933,7 +7502,7 @@ function viewerCspHeader(options) {
5933
7502
  "default-src 'none'",
5934
7503
  `script-src ${host}`,
5935
7504
  `style-src ${host} 'unsafe-inline'`,
5936
- directive("img-src", [host, "data:"], options.imgSrc),
7505
+ directive("img-src", [host, ...VIEWER_IMG_SRC_BASE], options.imgSrc),
5937
7506
  directive("font-src", [host], options.fontSrc),
5938
7507
  directive("connect-src", [], options.connectSrc)
5939
7508
  ].join("; ");
@@ -5957,13 +7526,101 @@ function diffSandboxTokens(sdk, host) {
5957
7526
 
5958
7527
  //#endregion
5959
7528
  //#region ../browser/src/viewer/documentSurface.ts
5960
- const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 2e6;
7529
+ /**
7530
+ * 载荷硬兜底(0.22.0 FR-10.1)。
7531
+ *
7532
+ * 从 0.11.0 的 2 MB 提到这里,是因为那个值从未被论证过,而实测表明它离任何真实边界都很远:
7533
+ * 128 MB 的结构化克隆只要 23 ms,V8 的字符串上限在 512 MB 左右才会抛 `RangeError`。
7534
+ * 留 4 倍余量是给同一个标签页里的 React、图表库与对话历史用的。
7535
+ */
7536
+ const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 128e6;
7537
+ /** 载荷软阈值:到这里不是「慢」,而是「这份文档异常大」,值得先告诉用户一声 */
7538
+ const DEFAULT_SOFT_VIEWER_PAYLOAD_BYTES = 32e6;
7539
+ /** 像素硬兜底,约合 1 GB 位图;实测 60 张 4000×3000 达 2.7 GB,那是崩标签页的量级 */
7540
+ const DEFAULT_MAX_VIEWER_PAYLOAD_PIXELS = 256e6;
7541
+ /** 像素软阈值,约合 256 MB 位图;实测 60 M 像素仍然流畅 */
7542
+ const DEFAULT_SOFT_VIEWER_PAYLOAD_PIXELS = 64e6;
5961
7543
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
5962
7544
  /** hello 的重发间隔:viewer 脚本加载完之前投出去的消息全部丢失 */
5963
7545
  const HELLO_RETRY_MS = 100;
7546
+ /** 拒绝消息里列举的「最大的几张图」条数:两三张巨图撑起整份文档是常态,列太多反而没重点 */
7547
+ const LARGEST_IMAGES_IN_MESSAGE = 3;
5964
7548
  /** 审计事件类型(与既有事件同一条链) */
5965
7549
  const DOCUMENT_SURFACE_AUDIT_EVENT = "document.surface.open";
5966
- const payloadBytes = (value) => new TextEncoder().encode(JSON.stringify(value) ?? "").length;
7550
+ /**
7551
+ * 数载荷有多大,**不物化任何中间副本**(FR-10.3)。
7552
+ *
7553
+ * 被换掉的写法是 `new TextEncoder().encode(JSON.stringify(value)).length`:
7554
+ * 它为了读一个 `.length`,先物化一份与文档等长的字符串、再物化一份等长的字节数组,
7555
+ * 峰值内存是文档的三倍。实测 341 MB 的文档上它要 712 ms,
7556
+ * 而它保护的那次 `postMessage` 只要 95 ms——度量比传输还慢七倍。
7557
+ *
7558
+ * 用显式栈而不是递归:投放的是技能生成的数据,嵌套深度不由我们决定。
7559
+ */
7560
+ function payloadUnits(value) {
7561
+ let units = 0;
7562
+ const seen = /* @__PURE__ */ new WeakSet();
7563
+ const stack = [value];
7564
+ while (stack.length > 0) {
7565
+ const current = stack.pop();
7566
+ if (typeof current === "string") {
7567
+ units += current.length;
7568
+ continue;
7569
+ }
7570
+ if (typeof current !== "object" || current === null) continue;
7571
+ if (seen.has(current)) continue;
7572
+ seen.add(current);
7573
+ if (Array.isArray(current)) {
7574
+ for (const item of current) stack.push(item);
7575
+ continue;
7576
+ }
7577
+ if (ArrayBuffer.isView(current)) {
7578
+ units += current.byteLength;
7579
+ continue;
7580
+ }
7581
+ for (const [key, item] of Object.entries(current)) {
7582
+ units += key.length;
7583
+ stack.push(item);
7584
+ }
7585
+ }
7586
+ return units;
7587
+ }
7588
+ function payloadCost(input) {
7589
+ let pixels = 0;
7590
+ for (const image of input.images ?? []) pixels += image.width * image.height;
7591
+ return {
7592
+ units: payloadUnits(input.document),
7593
+ pixels
7594
+ };
7595
+ }
7596
+ function limitsOf(options) {
7597
+ return {
7598
+ softUnits: options.softPayloadBytes ?? 32e6,
7599
+ hardUnits: options.maxPayloadBytes ?? 128e6,
7600
+ softPixels: options.softPayloadPixels ?? 64e6,
7601
+ hardPixels: options.maxPayloadPixels ?? 256e6
7602
+ };
7603
+ }
7604
+ /** 两维取并集:任一维过线即触发,不得加权合并(FR-10.7) */
7605
+ function triggeredBy(overUnits, overPixels) {
7606
+ if (overUnits && overPixels) return "both";
7607
+ if (overUnits) return "bytes";
7608
+ if (overPixels) return "pixels";
7609
+ }
7610
+ /**
7611
+ * 拒绝消息必须可执行(FR-10.2):说清是哪一维超的、超了多少、以及该动哪张图。
7612
+ *
7613
+ * 「最大的几张图」不是锦上添花——一份超限的文档通常就是两三张巨图撑起来的,
7614
+ * 不报出来用户只知道「太大了」,不知道该换掉哪一张。
7615
+ */
7616
+ function rejectionMessage(cost, limits, trigger, images) {
7617
+ const parts = [];
7618
+ if (trigger !== "pixels") parts.push(`payload is ${cost.units} units, over the ${limits.hardUnits} unit budget`);
7619
+ if (trigger !== "bytes") parts.push(`images total ${cost.pixels} pixels, over the ${limits.hardPixels} pixel budget`);
7620
+ const largest = [...images].sort(trigger === "bytes" ? (a, b) => b.units - a.units : (a, b) => b.width * b.height - a.width * a.height).slice(0, LARGEST_IMAGES_IN_MESSAGE).map((image) => `${image.ref} (${image.width}x${image.height}, ${image.units} units)`);
7621
+ const advice = largest.length > 0 ? `remove or downscale the largest images before opening the viewer: ${largest.join(", ")}` : "reduce the document content before opening the viewer";
7622
+ return `Document ${parts.join(" and ")} (dimension: ${trigger}); ${advice}`;
7623
+ }
5967
7624
  /**
5968
7625
  * 打开一个文档投放面。
5969
7626
  *
@@ -5973,23 +7630,46 @@ const payloadBytes = (value) => new TextEncoder().encode(JSON.stringify(value) ?
5973
7630
  * 无 `allow-top-navigation` 改不了它的地址。
5974
7631
  */
5975
7632
  async function openDocumentSurface(input, options) {
5976
- const limit = options.maxPayloadBytes ?? 2e6;
5977
- const size = payloadBytes(input.document);
5978
- if (size > limit) {
7633
+ const images = input.images ?? [];
7634
+ const omissions = input.omissions ?? [];
7635
+ const limits = limitsOf(options);
7636
+ const cost = payloadCost(input);
7637
+ const facts = {
7638
+ units: cost.units,
7639
+ pixels: cost.pixels,
7640
+ imageCount: images.length,
7641
+ ...omissions.length > 0 ? { omissions: [...omissions] } : {}
7642
+ };
7643
+ const rejected = triggeredBy(cost.units > limits.hardUnits, cost.pixels > limits.hardPixels);
7644
+ if (rejected !== void 0) {
5979
7645
  await audit(options, input, {
5980
7646
  outcome: "rejected",
5981
7647
  reason: "payload-too-large",
5982
- bytes: size
7648
+ ...facts,
7649
+ warned: false,
7650
+ trigger: rejected
5983
7651
  });
5984
- throw new WebSkillError("VALIDATION_FAILED", `Document payload is ${size} bytes, over the ${limit} byte budget; reduce the data before opening the viewer`);
7652
+ throw new WebSkillError("VALIDATION_FAILED", rejectionMessage(cost, limits, rejected, images));
5985
7653
  }
7654
+ const warned = triggeredBy(cost.units > limits.softUnits, cost.pixels > limits.softPixels);
7655
+ const sizeNotice = warned === void 0 ? void 0 : {
7656
+ units: cost.units,
7657
+ pixels: cost.pixels,
7658
+ trigger: warned
7659
+ };
7660
+ const audited = {
7661
+ ...facts,
7662
+ warned: warned !== void 0,
7663
+ ...warned === void 0 ? {} : { trigger: warned }
7664
+ };
5986
7665
  if (!await options.confirm({
5987
7666
  skillName: input.skillName,
5988
- dataSource: input.dataSource
7667
+ dataSource: input.dataSource,
7668
+ ...sizeNotice === void 0 ? {} : { sizeNotice }
5989
7669
  })) {
5990
7670
  await audit(options, input, {
5991
7671
  outcome: "cancelled",
5992
- bytes: size
7672
+ ...audited
5993
7673
  });
5994
7674
  throw new WebSkillError("DOCUMENT_SURFACE_DECLINED", "The user declined to open the document surface");
5995
7675
  }
@@ -5998,7 +7678,7 @@ async function openDocumentSurface(input, options) {
5998
7678
  await audit(options, input, {
5999
7679
  outcome: "blocked",
6000
7680
  reason: "popup-blocked",
6001
- bytes: size
7681
+ ...audited
6002
7682
  });
6003
7683
  throw new WebSkillError("DOCUMENT_SURFACE_UNAVAILABLE", "The browser blocked the viewer window; open it from a direct user gesture");
6004
7684
  }
@@ -6009,8 +7689,8 @@ async function openDocumentSurface(input, options) {
6009
7689
  target.close();
6010
7690
  await audit(options, input, {
6011
7691
  outcome: "failed",
6012
- reason: messageOf(e),
6013
- bytes: size
7692
+ reason: messageOf$1(e),
7693
+ ...audited
6014
7694
  });
6015
7695
  throw e;
6016
7696
  }
@@ -6021,7 +7701,7 @@ async function openDocumentSurface(input, options) {
6021
7701
  }, viewerOrigin(options));
6022
7702
  await audit(options, input, {
6023
7703
  outcome: "opened",
6024
- bytes: size
7704
+ ...audited
6025
7705
  });
6026
7706
  return target;
6027
7707
  }
@@ -7279,7 +8959,7 @@ function toolError(e) {
7279
8959
  content: [],
7280
8960
  error: {
7281
8961
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
7282
- message: messageOf(e)
8962
+ message: messageOf$1(e)
7283
8963
  }
7284
8964
  };
7285
8965
  }
@@ -7393,4 +9073,4 @@ function readContent(result, images) {
7393
9073
  }
7394
9074
 
7395
9075
  //#endregion
7396
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_CAMERA_MAX_DIMENSION, DEFAULT_FRAME_BUDGET, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_IMAGE_WIDTH_RATIO, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, LIST_WEBOFFICE_DOCUMENTS_TOOL, OpfsProvider, READ_WEBOFFICE_DOCUMENT_TOOL, SANDBOX_PAGE_SCRIPT_SOURCE, SHARD_SOFT_LIMIT, TsTranspiler, VIEWER_SANDBOX_TOKENS, WEBOFFICE_BUDGET, WEBOFFICE_UNEXTRACTED, WEB_OFFICE_OFFICE_TYPES, WEB_OFFICE_READ_METHODS, WEB_OFFICE_SUPPORTED_TYPES, WORKER_BOOTSTRAP_SOURCE, WebOfficeBudgetGuard, WebOfficePolicy, WorkerRuntimeClient, WorkerUiBridge, XLSX_IMAGE_MIN_COLUMNS, XLSX_IMAGE_MIN_ROWS, XLSX_UNEXTRACTED, assertWebOfficeFingerprint, blockedMessage, bridgeError, callWebOfficeMethod, captureByScrolling, captureElementImage, capturePhoto, checkCameraAvailability, checkDictationAvailability, checkWebOfficeFingerprint, compressImageToBudget, contentKindOf, createBrowserChatbotHost, createDocumentSurfaceHost, createDocxBlockReader, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, createIframeWorker, createLlmClient, createPageAgentHandler, createRemotePageActionExecutor, createRemotePerceptionReader, createRemoteTargetRegistry, createWebOfficeHandle, createWebOfficeToolSource, createXlsxBlockReader, deleteMemoryEncryptionKey, describeScreenshotOverlap, diffSandboxTokens, documentKey, explainResolution, extractDocxText, extractWebOfficeContent, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, headingLevelOf, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, isWebOfficeReadMethod, missingPorts, openCamera, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, readDocxBlocks, readXlsxBlocks, resolveWebOfficeAuthorization, seedSkillsFromHttp, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources, webOfficeFailure };
9076
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_CAMERA_MAX_DIMENSION, DEFAULT_FRAME_BUDGET, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DEFAULT_MAX_VIEWER_PAYLOAD_PIXELS, DEFAULT_SOFT_VIEWER_PAYLOAD_BYTES, DEFAULT_SOFT_VIEWER_PAYLOAD_PIXELS, DOCUMENT_IMAGE_MIME_TYPES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_IMAGE_WIDTH_RATIO, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, LIST_WEBOFFICE_DOCUMENTS_TOOL, MAX_REMOTE_IMAGE_BYTES, OpfsProvider, PPTX_UNEXTRACTED, PPTX_UNEXTRACTED_NOTICE, READ_WEBOFFICE_DOCUMENT_TOOL, REMOTE_IMAGE_TIMEOUT_MS, SANDBOX_PAGE_SCRIPT_SOURCE, SHARD_SOFT_LIMIT, TsTranspiler, VIEWER_IMG_SRC_BASE, VIEWER_IMG_SRC_REMOTE, VIEWER_SANDBOX_TOKENS, WEBOFFICE_BUDGET, WEBOFFICE_UNEXTRACTED, WEB_OFFICE_OFFICE_TYPES, WEB_OFFICE_READ_METHODS, WEB_OFFICE_SUPPORTED_TYPES, WORKER_BOOTSTRAP_SOURCE, WebOfficeBudgetGuard, WebOfficePolicy, WorkerRuntimeClient, WorkerUiBridge, XLSX_IMAGE_MIN_COLUMNS, XLSX_IMAGE_MIN_ROWS, XLSX_UNEXTRACTED, assertWebOfficeFingerprint, auditOoxmlPackage, blockedMessage, bridgeError, callWebOfficeMethod, captureByScrolling, captureDocumentImage, captureElementImage, capturePhoto, checkCameraAvailability, checkDictationAvailability, checkWebOfficeFingerprint, classifyFetchFailure, compressImageToBudget, contentKindOf, createBrowserChatbotHost, createDocumentSurfaceHost, createDocxBlockReader, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createFrameRouter, createIframeWorker, createLlmClient, createPageAgentHandler, createPptxBlockReader, createRemoteImageHost, createRemotePageActionExecutor, createRemotePageImageHost, createRemotePerceptionReader, createRemoteTargetRegistry, createWebOfficeHandle, createWebOfficeToolSource, createXlsxBlockReader, deleteMemoryEncryptionKey, describeScreenshotOverlap, diffSandboxTokens, documentKey, explainResolution, extractDocxText, extractPptxText, extractWebOfficeContent, extractXlsxText, extractZipWeb, fetchRemoteImage, fillDocxTemplate, fillXlsxTemplate, generateMemoryEncryptionKey, headingLevelOf, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, isRemoteImageFailure, isWebOfficeReadMethod, missingPorts, openCamera, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, readDocxBlocks, readDocxTemplate, readOoxmlPackage, readPptxBlocks, readXlsxBlocks, readXlsxTemplate, resolveWebOfficeAuthorization, seedSkillsFromHttp, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, toDocumentImage, viewerCspHeader, watchBlockedResources, webOfficeFailure, writeOoxmlPackage };