@webskill/sdk 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { D as exportSkills, F as parseSkillPackManifest, G as unzipWithLimits, I as readResponseWithLimit, J as verifySkillSignature, K as validateSkills, L as readSkillSignature, M as messageOf, O as isAtomicTempPath, P as parseSkillMarkdown, T as detectSkillArchiveShapeFromFs, V as resolveInsideRoot, b as buildManifest, c as SKILL_MANIFEST_FILE, d as SKILL_PACK_FILE, g as assertRemoteUrlAllowed, h as WebSkillError, i as MANIFEST_EXCLUDED_FILES, k as isValidSkillName, p as SkillDiscovery, q as verifyManifest, s as SKILLS_LOCKFILE, v as atomicWriteText, y as buildCatalog } from "./dist-Bev6i6Ip.js";
2
- import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-sdKFgERo.js";
3
- import { T as toFrameScopes, w as toActionFrameScopes } from "./dist-DU9KDAuR.js";
2
+ import { $ as bridgeError, A as ProgressiveRouter, Ct as normalizeToolError, E as GoogleGenAiClient, St as normalizeToolContent, _t as mergeCatalogEntries, b as FsMemoryStore, c as AnthropicClient, k as OpenAiCompatibleClient, nt as createWebSkillApi, s as AgentLoop, u as CapabilityApproval, wt as parseBridgeRequest, x as FsRunSnapshotStore, y as FsArtifactStore, yt as networkPolicyLibSource } from "./dist-ExSQky4C.js";
3
+ import { D as toFrameScopes, E as toActionFrameScopes, T as frameSteps, w as frameLabel } from "./dist-GK6dtjRv.js";
4
4
  import { n as MockLlmClient } from "./testing-WPTyXQYt.js";
5
5
 
6
6
  //#region ../browser/dist/index.js
@@ -1650,12 +1650,12 @@ async function captureImg(element, id, maxBytes) {
1650
1650
  const inline = splitDataUrl(src);
1651
1651
  if (inline) {
1652
1652
  const bytes = Uint8Array.from(atob(inline.data), (char) => char.charCodeAt(0));
1653
- return toBudget(id, "src", new Blob([bytes], { type: inline.mimeType }), maxBytes);
1653
+ return toBudget(id, "src", await toDeliverableBlob(new Blob([bytes], { type: inline.mimeType })), maxBytes);
1654
1654
  }
1655
1655
  const response = await fetch(src, { mode: "cors" });
1656
1656
  if (response.type === "opaque") throw new Error("the response is opaque, so its bytes cannot be read");
1657
1657
  if (!response.ok) throw new Error(`fetching the source returned HTTP ${response.status}`);
1658
- return toBudget(id, "src", await response.blob(), maxBytes);
1658
+ return toBudget(id, "src", await toDeliverableBlob(await response.blob()), maxBytes);
1659
1659
  }
1660
1660
  /** L2:`<canvas>` 读回。画布被跨域内容污染时 `toDataURL` 抛 SecurityError,不再兜底(裁决 D-3) */
1661
1661
  async function captureCanvas(element, id, maxBytes) {
@@ -1664,29 +1664,204 @@ async function captureCanvas(element, id, maxBytes) {
1664
1664
  const bytes = Uint8Array.from(atob(parsed.data), (char) => char.charCodeAt(0));
1665
1665
  return toBudget(id, "canvas", new Blob([bytes], { type: parsed.mimeType }), maxBytes);
1666
1666
  }
1667
- /** L2:内联 `<svg>` 序列化。SVG 是文本,不进压缩管线——重编码成位图反而丢信息 */
1668
- function captureSvg(element, id, maxBytes) {
1669
- const markup = new XMLSerializer().serializeToString(element);
1670
- const data = base64Of(new TextEncoder().encode(markup));
1671
- const bytes = data.length;
1672
- if (bytes > maxBytes) throw new Error(`the serialized SVG is ${bytes} bytes, which exceeds the budget`);
1667
+ const SVG_NS = "http://www.w3.org/2000/svg";
1668
+ /** 无固有尺寸的 SVG 在浏览器里的默认渲染尺寸,照抄规范而不是自创 */
1669
+ const SVG_FALLBACK_SIZE = {
1670
+ width: 300,
1671
+ height: 150
1672
+ };
1673
+ /** 栅格画布的长边上限;再大也会被 `compressImageToBudget` 缩回去,只是白占内存 */
1674
+ const MAX_RASTER_EDGE = 2048;
1675
+ function numberAttr(element, name) {
1676
+ const raw = element.getAttribute?.(name);
1677
+ if (raw === null || raw === void 0) return void 0;
1678
+ const value = Number.parseFloat(raw);
1679
+ return Number.isFinite(value) && value > 0 ? value : void 0;
1680
+ }
1681
+ /**
1682
+ * 元素的显示尺寸,取不到返回 `undefined`(0.13.0 FR-20.3)。
1683
+ *
1684
+ * 逐级回退而不是只看 `getBoundingClientRect()`:没有布局的环境里它恒为 0,
1685
+ * 只认它会把所有图都判成 0 面积。**取不到就是取不到**,由调用方决定不过滤,
1686
+ * 不在这里编一个尺寸出来。
1687
+ */
1688
+ function measuredSizeOf(element) {
1689
+ const rect = element.getBoundingClientRect?.();
1690
+ if (rect !== void 0 && rect.width > 0 && rect.height > 0) return {
1691
+ width: rect.width,
1692
+ height: rect.height
1693
+ };
1694
+ const { naturalWidth, naturalHeight } = element;
1695
+ if (typeof naturalWidth === "number" && typeof naturalHeight === "number" && naturalWidth > 0 && naturalHeight > 0) return {
1696
+ width: naturalWidth,
1697
+ height: naturalHeight
1698
+ };
1699
+ const { width: propWidth, height: propHeight } = element;
1700
+ if (typeof propWidth === "number" && typeof propHeight === "number" && propWidth > 0 && propHeight > 0) return {
1701
+ width: propWidth,
1702
+ height: propHeight
1703
+ };
1704
+ const attrWidth = numberAttr(element, "width");
1705
+ const attrHeight = numberAttr(element, "height");
1706
+ if (attrWidth !== void 0 && attrHeight !== void 0) return {
1707
+ width: attrWidth,
1708
+ height: attrHeight
1709
+ };
1710
+ const box = element.getAttribute?.("viewBox")?.trim().split(/[\s,]+/);
1711
+ if (box?.length === 4) {
1712
+ const width = Number.parseFloat(box[2] ?? "");
1713
+ const height = Number.parseFloat(box[3] ?? "");
1714
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) return {
1715
+ width,
1716
+ height
1717
+ };
1718
+ }
1719
+ }
1720
+ function rasterSizeOf(element) {
1721
+ const measured = measuredSizeOf(element) ?? SVG_FALLBACK_SIZE;
1722
+ const scale = Math.min(1, MAX_RASTER_EDGE / Math.max(measured.width, measured.height));
1673
1723
  return {
1674
- id,
1675
- mimeType: "image/svg+xml",
1676
- data,
1677
- level: "canvas",
1678
- originalBytes: bytes,
1679
- bytes
1724
+ width: Math.max(1, Math.round(measured.width * scale)),
1725
+ height: Math.max(1, Math.round(measured.height * scale))
1680
1726
  };
1681
1727
  }
1682
1728
  /**
1729
+ * 解码超时。真实浏览器里 data URL 的 SVG 解码是瞬时的(不加载任何外部资源),
1730
+ * 但环境不支持图像管线时 load / error 可能一个都不来——没有这道闸门整次感知会永远挂着。
1731
+ */
1732
+ const DECODE_TIMEOUT_MS = 2e3;
1733
+ async function loadImage(url) {
1734
+ const image = new Image();
1735
+ image.src = url;
1736
+ const decoded = typeof image.decode === "function" ? image.decode() : new Promise((resolve, reject) => {
1737
+ image.onload = () => resolve();
1738
+ image.onerror = () => reject(/* @__PURE__ */ new Error("the browser could not decode the serialized SVG"));
1739
+ });
1740
+ let timer;
1741
+ try {
1742
+ await Promise.race([decoded, new Promise((_, reject) => {
1743
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("decoding the serialized SVG timed out")), DECODE_TIMEOUT_MS);
1744
+ })]);
1745
+ } finally {
1746
+ if (timer !== void 0) clearTimeout(timer);
1747
+ }
1748
+ return image;
1749
+ }
1750
+ /** 逐像素查 alpha,命中一个不透明点立刻返回;只有真正的空图才会扫完全图 */
1751
+ function isFullyTransparent(ctx, width, height) {
1752
+ const { data } = ctx.getImageData(0, 0, width, height);
1753
+ for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return false;
1754
+ return true;
1755
+ }
1756
+ async function toPngBlob(canvas) {
1757
+ return await new Promise((resolve, reject) => {
1758
+ canvas.toBlob((blob) => {
1759
+ if (blob === null) reject(/* @__PURE__ */ new Error("the canvas could not be encoded as PNG"));
1760
+ else resolve(blob);
1761
+ }, "image/png");
1762
+ });
1763
+ }
1764
+ /**
1765
+ * L2:内联 `<svg>` 栅格化成 PNG(0.13.0 FR-20.1)。
1766
+ *
1767
+ * 三家提供方的图片通道都不收 `image/svg+xml`(分册 20 §0.1),原样序列化下发等于取不到图。
1768
+ * 经 `<img>` 加载的 SVG 被浏览器强制资源隔离:不执行脚本,不加载外部图片 / 样式 / 字体。
1769
+ * 因此这里不做污染处理**不是漏了**——画布根本不会被跨源内容污染,也不会因为取像发起外部请求。
1770
+ * 代价是页面 CSS 类与外部字体不生效,由下面的空图检测兜底(FR-20.2)。
1771
+ */
1772
+ async function captureSvg(element, id, maxBytes) {
1773
+ if (typeof Image !== "function" || typeof document === "undefined") throw new Error("this environment has no DOM image pipeline, so SVG cannot be rasterized");
1774
+ const { width, height } = rasterSizeOf(element);
1775
+ const clone = element.cloneNode(true);
1776
+ clone.setAttribute("xmlns", SVG_NS);
1777
+ clone.setAttribute("width", String(width));
1778
+ clone.setAttribute("height", String(height));
1779
+ const markup = new XMLSerializer().serializeToString(clone);
1780
+ const image = await loadImage(`data:image/svg+xml;base64,${base64Of(new TextEncoder().encode(markup))}`);
1781
+ const canvas = document.createElement("canvas");
1782
+ canvas.width = width;
1783
+ canvas.height = height;
1784
+ const ctx = canvas.getContext("2d");
1785
+ if (ctx === null) throw new Error("a 2D canvas context is unavailable");
1786
+ ctx.drawImage(image, 0, 0, width, height);
1787
+ if (isFullyTransparent(ctx, width, height)) throw new Error("the SVG rendered blank once detached from the page, so its appearance most likely comes from page CSS or external assets that do not apply inside an <img>");
1788
+ return toBudget(id, "canvas", await toPngBlob(canvas), maxBytes);
1789
+ }
1790
+ /** 模型图片通道只收这几种位图(与 runtime 的 `MODEL_IMAGE_MIME_TYPES` 同一组) */
1791
+ const DELIVERABLE_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
1792
+ "image/png",
1793
+ "image/jpeg",
1794
+ "image/gif",
1795
+ "image/webp"
1796
+ ]);
1797
+ /** 用于认出 SVG 正文的前缀嗅探长度:`<?xml`、注释、DOCTYPE 都可能排在 `<svg` 之前 */
1798
+ const SVG_SNIFF_BYTES = 1024;
1799
+ /**
1800
+ * 按魔数认图片类型(服务端声明只作兜底)。
1801
+ *
1802
+ * 图片接口常年是「无扩展名 + `application/octet-stream`」这种组合(查询串带 file id 的尤其多),
1803
+ * 而模型侧只按 MIME 判能不能投递——照抄服务端的声明,整张图会在最后一步被丢掉,
1804
+ * 模型只收到一句「这个格式发不了」。
1805
+ */
1806
+ function sniffImageMime(bytes) {
1807
+ const at = (offset, ...signature) => signature.every((byte, index) => bytes[offset + index] === byte);
1808
+ if (at(0, 137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
1809
+ if (at(0, 255, 216, 255)) return "image/jpeg";
1810
+ if (at(0, 71, 73, 70, 56)) return "image/gif";
1811
+ if (at(0, 82, 73, 70, 70) && at(8, 87, 69, 66, 80)) return "image/webp";
1812
+ if (at(0, 66, 77)) return "image/bmp";
1813
+ const head = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, SVG_SNIFF_BYTES));
1814
+ if (/<svg[\s>]/i.test(head)) return "image/svg+xml";
1815
+ }
1816
+ /** 把任意 blob 重新编码成 PNG:先让浏览器解码,再画进画布 */
1817
+ async function rasterizeToPng(blob) {
1818
+ if (typeof document === "undefined" || typeof Image !== "function" || typeof URL?.createObjectURL !== "function") throw new Error("this environment has no DOM image pipeline, so the image cannot be re-encoded");
1819
+ const url = URL.createObjectURL(blob);
1820
+ try {
1821
+ const image = await loadImage(url);
1822
+ const natural = {
1823
+ width: image.naturalWidth,
1824
+ height: image.naturalHeight
1825
+ };
1826
+ const measured = natural.width > 0 && natural.height > 0 ? natural : SVG_FALLBACK_SIZE;
1827
+ const scale = Math.min(1, MAX_RASTER_EDGE / Math.max(measured.width, measured.height));
1828
+ const width = Math.max(1, Math.round(measured.width * scale));
1829
+ const height = Math.max(1, Math.round(measured.height * scale));
1830
+ const canvas = document.createElement("canvas");
1831
+ canvas.width = width;
1832
+ canvas.height = height;
1833
+ const ctx = canvas.getContext("2d");
1834
+ if (ctx === null) throw new Error("a 2D canvas context is unavailable");
1835
+ ctx.drawImage(image, 0, 0, width, height);
1836
+ return await toPngBlob(canvas);
1837
+ } finally {
1838
+ URL.revokeObjectURL(url);
1839
+ }
1840
+ }
1841
+ /**
1842
+ * 把取到的字节归一成模型收得下的位图(0.13.0)。
1843
+ *
1844
+ * 两类真实站点上很常见的图会在最后一步被模型侧丢掉:服务端把图报成 `application/octet-stream`,
1845
+ * 以及矢量图(三家提供方的图片通道都不收 `image/svg+xml`,分册 20 §0.1)。
1846
+ * 前者只是声明错了,按魔数改回来即可;后者必须就地栅格化,否则等于没取到图。
1847
+ */
1848
+ async function toDeliverableBlob(blob) {
1849
+ const declared = blob.type.split(";")[0]?.trim().toLowerCase() ?? "";
1850
+ if (DELIVERABLE_IMAGE_MIME_TYPES.has(declared)) return blob;
1851
+ const bytes = new Uint8Array(await blob.arrayBuffer());
1852
+ const sniffed = sniffImageMime(bytes);
1853
+ if (sniffed !== void 0 && DELIVERABLE_IMAGE_MIME_TYPES.has(sniffed)) return new Blob([bytes], { type: sniffed });
1854
+ const mimeType = sniffed ?? (declared.startsWith("image/") ? declared : void 0);
1855
+ 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}")`);
1856
+ return await rasterizeToPng(new Blob([bytes], { type: mimeType }));
1857
+ }
1858
+ /**
1683
1859
  * 取一个元素的图像(0.6.0 FR-12.1)。
1684
1860
  *
1685
1861
  * 按元素类型分派,不做无意义的尝试:`<img>` 只走 L1,`<canvas>` / `<svg>` 只走 L2,
1686
1862
  * 两者互不兜底。失败返回 `CaptureFailure` 而不是抛错——一张图抓不到不该让整次感知失败。
1687
1863
  * @experimental
1688
- */
1689
- async function captureElementImage(element, options) {
1864
+ */ async function captureElementImage(element, options) {
1690
1865
  const { id, maxBytes } = options;
1691
1866
  const tag = element.tagName.toUpperCase();
1692
1867
  if (tag === "IMG") try {
@@ -1708,11 +1883,11 @@ async function captureElementImage(element, options) {
1708
1883
  };
1709
1884
  }
1710
1885
  if (tag === "SVG") try {
1711
- return captureSvg(element, id, maxBytes);
1886
+ return await captureSvg(element, id, maxBytes);
1712
1887
  } catch (e) {
1713
1888
  return {
1714
1889
  id,
1715
- reason: `L2 could not serialize the SVG: ${describeError(e)}`,
1890
+ reason: `L2 could not rasterize the SVG: ${describeError(e)}`,
1716
1891
  triedLevels: ["canvas"]
1717
1892
  };
1718
1893
  }
@@ -1859,13 +2034,25 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
1859
2034
  ]);
1860
2035
  /** 活动模态的三种形态;Radix 之流用 aria-modal,原生用 <dialog open> */
1861
2036
  const MODAL_SELECTOR = "[role=dialog][open], dialog[open], [aria-modal=true]";
1862
- function describe(element, excluded, doc, view, targets, handles, frame, provenance) {
2037
+ /** 按尺寸落选取像的说明(FR-20.3);与「超名额」「取像失败」必须可区分,三者排查方向不同 */
2038
+ const ICON_SKIPPED_NOTE = "Not captured: this image is smaller than the icon threshold";
2039
+ /**
2040
+ * 元素是否小到该当图标看待(FR-20.3)。
2041
+ * 测不到尺寸就**不过滤**:没有布局的环境里宁可多取一张,也不能把图全吞了。
2042
+ */
2043
+ function isBelowMinArea(element, minImageArea) {
2044
+ if (minImageArea <= 0) return false;
2045
+ const size = measuredSizeOf(element);
2046
+ if (size === void 0) return false;
2047
+ return size.width * size.height < minImageArea;
2048
+ }
2049
+ function describe(element, excluded, doc, view, capture, handles, frame, provenance) {
1863
2050
  if (excluded.has(element)) return void 0;
1864
2051
  if (SKIPPED_TAGS.has(element.tagName)) return void 0;
1865
2052
  if (hidden$1(element, view)) return void 0;
1866
2053
  const children = [];
1867
2054
  for (const child of element.children) {
1868
- const node = describe(child, excluded, doc, view, targets, handles, frame, provenance);
2055
+ const node = describe(child, excluded, doc, view, capture, handles, frame, provenance);
1869
2056
  if (node !== void 0) children.push(node);
1870
2057
  }
1871
2058
  const name = nameOf(element, doc);
@@ -1884,18 +2071,54 @@ function describe(element, excluded, doc, view, targets, handles, frame, provena
1884
2071
  };
1885
2072
  const ref = handles?.issue(element, role, frame);
1886
2073
  if (ref !== void 0) node.ref = ref;
1887
- if (targets !== void 0 && isCapturableElement(element)) targets.push({
2074
+ if (capture !== void 0 && isCapturableElement(element)) if (isBelowMinArea(element, capture.minImageArea)) node.imageNote = ICON_SKIPPED_NOTE;
2075
+ else capture.targets.push({
1888
2076
  element,
1889
2077
  node
1890
2078
  });
1891
2079
  return node;
1892
2080
  }
1893
2081
  /**
1894
- * 句柄发放与解析。表在 reader 实例内、每次 `read()` 整体替换:
1895
- * 全局表是第二套状态且会跨会话泄漏;不替换则页面变了还能用旧句柄点到别的东西。
2082
+ * 子树里的 `<iframe>`(含根自身),跳过 exclude 命中的整棵子树。
2083
+ * 供 FR-18.4 判断「范围里有帧但没被授权」;本函数**不读**这些帧的内容。
2084
+ */
2085
+ function nestedFramesIn(root, excluded) {
2086
+ const inExcluded = (element) => {
2087
+ for (let node = element; node !== null; node = node.parentElement) if (excluded.has(node)) return true;
2088
+ return false;
2089
+ };
2090
+ const found = [];
2091
+ if (root.tagName === "IFRAME" && !inExcluded(root)) found.push(root);
2092
+ for (const iframe of root.querySelectorAll("iframe")) if (!inExcluded(iframe)) found.push(iframe);
2093
+ return found;
2094
+ }
2095
+ /**
2096
+ * 未授权嵌套帧的指称。`<iframe>` 的可访问名来自 `title`,而 `nameOf` 走的是
2097
+ * aria/文本那一套(帧没有自身文本),所以这里单独取——**只用于说明文本**,
2098
+ * 不进任何可操作路径。
2099
+ */
2100
+ function frameHint(iframe) {
2101
+ for (const attribute of [
2102
+ "aria-label",
2103
+ "title",
2104
+ "name",
2105
+ "id"
2106
+ ]) {
2107
+ const value = iframe.getAttribute(attribute);
2108
+ if (value !== null && value.trim() !== "") return clip(value);
2109
+ }
2110
+ return "unnamed";
2111
+ }
2112
+ /**
2113
+ * 句柄发放与解析。表在 reader 实例内、**跨感知保留**(分册 18 FR-18.5):
2114
+ * 每次 read() 作废全部旧句柄会让多步任务陷入「读—点—失效—再读」的空转,
2115
+ * 而安全性本就不由句柄寿命承担——执行前的 `modalStateOf` / `inActionScope` /
2116
+ * 可见性 / 可用性四道校验才是闸门,它们按**执行那一刻**的 DOM 实时判定。
1896
2117
  */
1897
2118
  var HandleIssuer = class {
1898
2119
  #table = /* @__PURE__ */ new Map();
2120
+ /** 同一元素复用同一个 ref:模型重读页面后手里的句柄仍然指向同一个东西 */
2121
+ #refByElement = /* @__PURE__ */ new WeakMap();
1899
2122
  #actionable;
1900
2123
  #excluded;
1901
2124
  constructor(actionable, excluded) {
@@ -1907,9 +2130,24 @@ var HandleIssuer = class {
1907
2130
  this.#actionable = actionable;
1908
2131
  this.#excluded = excluded;
1909
2132
  }
2133
+ /**
2134
+ * 丢弃已离开文档的条目。**只防表随长会话无限膨胀,不是安全判据**——
2135
+ * 「元素还在但已挪出授权范围」这一类由执行前的第二道闸门拦(FR-18.5)。
2136
+ */
2137
+ prune() {
2138
+ for (const [ref, entry] of this.#table) if (!entry.element.isConnected) this.#table.delete(ref);
2139
+ }
1910
2140
  issue(element, role, frame) {
1911
2141
  if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
1912
2142
  if (!ACTIONABLE_ROLES.has(role)) return void 0;
2143
+ const known = this.#refByElement.get(element);
2144
+ if (known !== void 0 && this.#table.has(known)) {
2145
+ this.#table.set(known, {
2146
+ element,
2147
+ ...frame !== void 0 ? { frame } : {}
2148
+ });
2149
+ return known;
2150
+ }
1913
2151
  const bytes = /* @__PURE__ */ new Uint8Array(8);
1914
2152
  crypto.getRandomValues(bytes);
1915
2153
  const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -1917,6 +2155,7 @@ var HandleIssuer = class {
1917
2155
  element,
1918
2156
  ...frame !== void 0 ? { frame } : {}
1919
2157
  });
2158
+ this.#refByElement.set(element, ref);
1920
2159
  return ref;
1921
2160
  }
1922
2161
  get table() {
@@ -1942,40 +2181,53 @@ function createDomPerceptionReader(options = {}) {
1942
2181
  const elevatedModals = /* @__PURE__ */ new Set();
1943
2182
  const rootDocument = () => options.document ?? globalThis.document;
1944
2183
  /**
1945
- * 把一条帧授权解析成 Document
2184
+ * 把一条帧授权解析成 Document,逐层下钻(分册 18 FR-18.3)。
1946
2185
  *
1947
2186
  * 跨源时 `contentDocument` 在多数浏览器返回 `null` 而**不抛异常**,
1948
2187
  * 只有部分属性访问才抛 —— 所以两种形态都要处理,只写 try/catch 会漏。
1949
2188
  */
1950
2189
  function resolveFrame(frame, root) {
1951
- if (frame === "self") return { doc: root };
1952
- const element = root.querySelector(frame);
1953
- if (element === null || element.tagName !== "IFRAME") return { note: {
1954
- frame,
1955
- reason: "not-found",
1956
- message: `Frame "${frame}" was not found in the page.`
1957
- } };
1958
- let doc;
1959
- let origin;
1960
- try {
1961
- doc = element.contentDocument;
1962
- origin = element.contentWindow?.location.origin;
1963
- } catch {
1964
- doc = null;
2190
+ const steps = frameSteps(frame);
2191
+ const label = frameLabel(frame);
2192
+ let doc = root;
2193
+ const frames = [];
2194
+ for (const [index, selector] of steps.entries()) {
2195
+ const hop = `step ${index + 1} ("${selector}") of frame path "${label}"`;
2196
+ const element = doc.querySelector(selector);
2197
+ if (element === null || element.tagName !== "IFRAME") return { note: {
2198
+ frame: label,
2199
+ reason: "not-found",
2200
+ message: `Frame ${hop} was not found in the page.`
2201
+ } };
2202
+ const iframe = element;
2203
+ let next;
2204
+ let origin;
2205
+ try {
2206
+ next = iframe.contentDocument;
2207
+ origin = iframe.contentWindow?.location.origin;
2208
+ } catch {
2209
+ next = null;
2210
+ }
2211
+ if (next === null || origin === void 0) return { note: {
2212
+ frame: label,
2213
+ reason: "cross-origin",
2214
+ message: `Frame ${hop} is cross-origin and was not read.`
2215
+ } };
2216
+ const key = frameLabel(steps.slice(0, index + 1));
2217
+ const known = frameOrigins.get(key);
2218
+ if (known === void 0) frameOrigins.set(key, origin);
2219
+ else if (known !== origin) return { note: {
2220
+ frame: label,
2221
+ reason: "origin-changed",
2222
+ message: `Frame ${hop} now points at ${origin} instead of the authorized ${known}; the grant was revoked.`
2223
+ } };
2224
+ doc = next;
2225
+ frames.push(iframe);
1965
2226
  }
1966
- if (doc === null || origin === void 0) return { note: {
1967
- frame,
1968
- reason: "cross-origin",
1969
- message: `Frame "${frame}" is cross-origin and was not read.`
1970
- } };
1971
- const known = frameOrigins.get(frame);
1972
- if (known === void 0) frameOrigins.set(frame, origin);
1973
- else if (known !== origin) return { note: {
1974
- frame,
1975
- reason: "origin-changed",
1976
- message: `Frame "${frame}" now points at ${origin} instead of the authorized ${known}; the grant was revoked.`
1977
- } };
1978
- return { doc };
2227
+ return {
2228
+ doc,
2229
+ frames
2230
+ };
1979
2231
  }
1980
2232
  /** 按当前 DOM 重算一次可操作集合;发句柄与执行前各算一次 */
1981
2233
  const actionSets = (doc, scope) => {
@@ -1998,49 +2250,74 @@ function createDomPerceptionReader(options = {}) {
1998
2250
  excluded
1999
2251
  };
2000
2252
  };
2001
- /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」 */
2002
- const actionScopeFor = (frame) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => entry.frame === frame);
2003
- function collect(scope, targets) {
2253
+ /** 该帧的操作范围;没有对应条目就是「这个帧只可读不可操作」。按展示串匹配(FR-18.2) */
2254
+ const actionScopeFor = (label) => options.actionScope === void 0 ? void 0 : toActionFrameScopes(options.actionScope).find((entry) => frameLabel(entry.frame) === label);
2255
+ function collect(scope, capture) {
2004
2256
  const root = rootDocument();
2005
2257
  if (root === void 0) return {
2006
2258
  nodes: [],
2007
2259
  frameNotes: []
2008
2260
  };
2009
- handles = new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2261
+ handles ??= new HandleIssuer(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set());
2262
+ handles.prune();
2263
+ const issuer = handles;
2010
2264
  const nodes = [];
2011
2265
  const frameNotes = [];
2266
+ /** 同一个未授权 iframe 至多报一次(它可能同时落在多个 include 根的子树里) */
2267
+ const reportedUnauthorized = /* @__PURE__ */ new Set();
2268
+ const resolvedScopes = [];
2269
+ const authorizedFrames = /* @__PURE__ */ new Set();
2012
2270
  for (const frameScope of toFrameScopes(scope)) {
2013
2271
  if (frameScope.include.length === 0) continue;
2272
+ const label = frameLabel(frameScope.frame);
2014
2273
  const resolved = resolveFrame(frameScope.frame, root);
2015
2274
  if ("note" in resolved) {
2016
2275
  frameNotes.push(resolved.note);
2017
2276
  nodes.push({
2018
2277
  role: "note",
2019
2278
  name: resolved.note.message,
2020
- frame: frameScope.frame
2279
+ frame: label
2021
2280
  });
2022
2281
  continue;
2023
2282
  }
2024
- const doc = resolved.doc;
2283
+ for (const iframe of resolved.frames) authorizedFrames.add(iframe);
2284
+ resolvedScopes.push({
2285
+ scope: frameScope,
2286
+ label,
2287
+ doc: resolved.doc
2288
+ });
2289
+ }
2290
+ for (const { scope: frameScope, label, doc } of resolvedScopes) {
2025
2291
  const view = doc.defaultView;
2026
- const tag = frameScope.frame === "self" ? void 0 : frameScope.frame;
2027
- const sets = actionSets(doc, actionScopeFor(frameScope.frame));
2028
- handles.useSets(sets.actionable, sets.excluded);
2292
+ const tag = label === "self" ? void 0 : label;
2293
+ const sets = actionSets(doc, actionScopeFor(label));
2294
+ issuer.useSets(sets.actionable, sets.excluded);
2029
2295
  const excluded = /* @__PURE__ */ new Set();
2030
2296
  for (const selector of frameScope.exclude ?? []) for (const element of doc.querySelectorAll(selector)) excluded.add(element);
2031
2297
  const roots = [];
2032
2298
  for (const selector of frameScope.include) for (const element of doc.querySelectorAll(selector)) if (!roots.includes(element)) roots.push(element);
2033
2299
  for (const element of roots) {
2034
- const node = describe(element, excluded, doc, view, targets, handles, tag, void 0);
2300
+ const node = describe(element, excluded, doc, view, capture, issuer, tag, void 0);
2035
2301
  if (node !== void 0) nodes.push(node);
2036
2302
  }
2303
+ for (const element of roots) for (const iframe of nestedFramesIn(element, excluded)) {
2304
+ if (authorizedFrames.has(iframe)) continue;
2305
+ if (reportedUnauthorized.has(iframe)) continue;
2306
+ reportedUnauthorized.add(iframe);
2307
+ const hint = frameHint(iframe);
2308
+ nodes.push({
2309
+ role: "note",
2310
+ name: `A nested frame "${hint}" inside "${label}" is not part of the authorized scope and was not read. Ask the user to authorize it if its content is needed.`,
2311
+ ...tag !== void 0 ? { frame: tag } : {}
2312
+ });
2313
+ }
2037
2314
  }
2038
2315
  pruneElevated();
2039
2316
  const view = root.defaultView;
2040
2317
  const sets = actionSets(root, actionScopeFor("self"));
2041
2318
  for (const modal of elevatedModals) {
2042
- handles.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2043
- const node = describe(modal, /* @__PURE__ */ new Set(), root, view, targets, handles, void 0, "modal-elevated");
2319
+ issuer.useSets(/* @__PURE__ */ new Set([modal, ...modal.querySelectorAll("*")]), sets.excluded);
2320
+ const node = describe(modal, /* @__PURE__ */ new Set(), root, view, capture, issuer, void 0, "modal-elevated");
2044
2321
  if (node !== void 0) nodes.push(node);
2045
2322
  }
2046
2323
  for (const modal of root.querySelectorAll(MODAL_SELECTOR)) {
@@ -2058,7 +2335,10 @@ function createDomPerceptionReader(options = {}) {
2058
2335
  }
2059
2336
  async function readWithImages(scope, capture) {
2060
2337
  const targets = [];
2061
- const { nodes, frameNotes } = collect(scope, targets);
2338
+ const { nodes, frameNotes } = collect(scope, {
2339
+ targets,
2340
+ minImageArea: capture.minImageArea ?? 0
2341
+ });
2062
2342
  const selected = targets.slice(0, capture.maxImages);
2063
2343
  const images = [];
2064
2344
  let imageFailures = 0;
@@ -2137,14 +2417,21 @@ function createDomPerceptionReader(options = {}) {
2137
2417
  return sets.actionable.has(element) && !sets.excluded.has(element);
2138
2418
  }
2139
2419
  };
2140
- /** 反查一个 Document 属于哪条帧授权;不在授权列表里的帧一律判不可操作 */
2420
+ /**
2421
+ * 反查一个 Document 属于哪条帧授权,返回它的展示串;不在授权列表里的帧一律判不可操作。
2422
+ * 没有反向解析展示串(那是不成立的,D-18-1),而是把每条授权路径重新走一遍后比对
2423
+ * 文档对象:路径中任一层被换掉 / origin 变了,这里就对不上,旧句柄因此失效。
2424
+ */
2141
2425
  function frameOfDocument(doc) {
2142
2426
  const root = rootDocument();
2143
2427
  if (root === void 0) return void 0;
2144
2428
  if (doc === root) return "self";
2145
- for (const frame of frameOrigins.keys()) {
2146
- const element = root.querySelector(frame);
2147
- if (element?.tagName === "IFRAME" && element.contentDocument === doc) return frame;
2429
+ if (options.actionScope === void 0) return void 0;
2430
+ for (const entry of toActionFrameScopes(options.actionScope)) {
2431
+ if (frameSteps(entry.frame).length === 0) continue;
2432
+ const resolved = resolveFrame(entry.frame, root);
2433
+ if ("note" in resolved) continue;
2434
+ if (resolved.doc === doc) return frameLabel(entry.frame);
2148
2435
  }
2149
2436
  }
2150
2437
  }
@@ -2367,13 +2654,27 @@ function createDomPageActionExecutor(options) {
2367
2654
  if (hidden(element)) return fail("The element is not visible.");
2368
2655
  if (element.hasAttribute("disabled")) return fail("The element is disabled.");
2369
2656
  const modalsBefore = reader.modalSnapshot();
2657
+ const frameView = element.ownerDocument.defaultView;
2658
+ const urlBefore = frameView?.location.href;
2370
2659
  const done = async (outcome) => {
2371
2660
  if (!outcome.ok) return outcome;
2372
- const elevated = await waitFor(() => reader.elevateNewModals(modalsBefore), 500) ?? reader.elevateNewModals(modalsBefore);
2661
+ const settled = await waitFor(() => {
2662
+ const modal = reader.elevateNewModals(modalsBefore);
2663
+ if (modal !== void 0) return { modal };
2664
+ const url = frameView?.location.href;
2665
+ return url !== void 0 && url !== urlBefore ? { url } : void 0;
2666
+ }, 500);
2667
+ const elevated = settled !== void 0 && "modal" in settled ? settled.modal : reader.elevateNewModals(modalsBefore);
2373
2668
  const name = elevated === void 0 ? void 0 : accessibleName(elevated) ?? "dialog";
2374
- return name === void 0 ? outcome : {
2669
+ const urlAfter = frameView?.location.href;
2670
+ const navigated = urlAfter !== void 0 && urlBefore !== void 0 && urlAfter !== urlBefore;
2671
+ return {
2375
2672
  ...outcome,
2376
- elevatedModal: name
2673
+ ...name !== void 0 ? { elevatedModal: name } : {},
2674
+ ...navigated ? {
2675
+ navigated: true,
2676
+ documentUrl: urlAfter
2677
+ } : {}
2377
2678
  };
2378
2679
  };
2379
2680
  if (request.action === "click") {
@@ -3392,6 +3693,255 @@ async function extractDocxText(bytes) {
3392
3693
  return blocks.join("\n").replace(/\n{3,}/g, "\n\n").trim();
3393
3694
  }
3394
3695
  /**
3696
+ * xlsx 文本抽取(0.13.0 分册 12)。
3697
+ *
3698
+ * **零新增依赖**:xlsx 和 docx 是同一种容器(OPC = zip + XML),
3699
+ * 所以沿用 docx 那条路 —— zip 走仓内既有的 `unzipWithLimits`(fflate,
3700
+ * 自带解压体积上限,顺带挡住 zip 炸弹),XML 走浏览器原生 `DOMParser`。
3701
+ *
3702
+ * ⚠️ `DOMParser` 是浏览器 API,Node 没有 —— 所以这个模块只在 `@webskill/browser`。
3703
+ * Node 宿主没有 xlsx 能力,与 docx 同一条**显式声明**的限制,不是静默降级(备案 D55)。
3704
+ */
3705
+ /**
3706
+ * **不抽取**的内容,逐条写明而不是笼统说「尽力而为」(FR-12.4)。
3707
+ *
3708
+ * 公式只取 Excel 自己写进文件的缓存值:零依赖求值等于自写一个表达式引擎。
3709
+ */
3710
+ const XLSX_UNEXTRACTED = [
3711
+ "styles",
3712
+ "conditional formatting",
3713
+ "merged cell reconstruction",
3714
+ "charts",
3715
+ "images",
3716
+ "comments",
3717
+ "pivot tables",
3718
+ "macros",
3719
+ "hidden row and column marking",
3720
+ "formula expressions (only cached values are read)"
3721
+ ];
3722
+ const WORKBOOK = "xl/workbook.xml";
3723
+ const WORKBOOK_RELS = "xl/_rels/workbook.xml.rels";
3724
+ const SHARED_STRINGS = "xl/sharedStrings.xml";
3725
+ const STYLES = "xl/styles.xml";
3726
+ /** OLE2 复合文档签名:旧版 .xls 的开头,它不是 zip,走不到解压那一步 */
3727
+ const OLE2_SIGNATURE = [
3728
+ 208,
3729
+ 207,
3730
+ 17,
3731
+ 224,
3732
+ 161,
3733
+ 177,
3734
+ 26,
3735
+ 225
3736
+ ];
3737
+ /** 内建的日期/时间数字格式 id(ECMA-376 §18.8.30) */
3738
+ const BUILTIN_DATE_FORMATS = /* @__PURE__ */ new Set([
3739
+ 14,
3740
+ 15,
3741
+ 16,
3742
+ 17,
3743
+ 18,
3744
+ 19,
3745
+ 20,
3746
+ 21,
3747
+ 22,
3748
+ 45,
3749
+ 46,
3750
+ 47
3751
+ ]);
3752
+ function fail(message) {
3753
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", message);
3754
+ }
3755
+ /** 按 localName 取子孙元素,忽略命名空间前缀:不同生成器的前缀写法并不统一 */
3756
+ const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
3757
+ function parseXml(bytes, what) {
3758
+ const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
3759
+ if (doc.getElementsByTagName("parsererror").length > 0) fail(`The workbook ${what} could not be parsed as XML.`);
3760
+ return doc;
3761
+ }
3762
+ /** `B` → 1、`AA` → 26;拿不到列号时返回 undefined,由调用方顺位补上 */
3763
+ function columnIndex(ref) {
3764
+ if (ref === null) return void 0;
3765
+ let index = 0;
3766
+ let seen = false;
3767
+ for (const ch of ref) {
3768
+ const code = ch.charCodeAt(0);
3769
+ if (code < 65 || code > 90) break;
3770
+ index = index * 26 + (code - 64);
3771
+ seen = true;
3772
+ }
3773
+ return seen ? index - 1 : void 0;
3774
+ }
3775
+ /**
3776
+ * 一个 `<si>` 的文本。富文本会被切成多个 `<r>`,须拼接;
3777
+ * `<rPh>` 是日文注音,拼进去会在正文里插入读音噪声,跳过。
3778
+ */
3779
+ function sharedStringText(si) {
3780
+ let out = "";
3781
+ for (const child of si.children) {
3782
+ const tag = child.localName;
3783
+ if (tag === "t") out += child.textContent ?? "";
3784
+ else if (tag === "r") {
3785
+ for (const run of child.children) if (run.localName === "t") out += run.textContent ?? "";
3786
+ }
3787
+ }
3788
+ return out;
3789
+ }
3790
+ /**
3791
+ * 数字格式是否表示日期/时间。
3792
+ *
3793
+ * `[h]:mm:ss` 这类方括号包住时分秒的是**经过时长**(可以是 25:30),不是时刻,
3794
+ * 按日期换算会得出荒谬结果,先排掉。
3795
+ */
3796
+ function isDateFormatCode(code) {
3797
+ if (/\[[hms]+\]/i.test(code)) return false;
3798
+ const stripped = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "").replace(/\\./g, "");
3799
+ return /[ymdhs]/i.test(stripped);
3800
+ }
3801
+ /** 单元格样式索引 → 是否日期格式 */
3802
+ function readDateStyles(bytes) {
3803
+ if (bytes === void 0) return [];
3804
+ const doc = parseXml(bytes, "styles");
3805
+ const custom = /* @__PURE__ */ new Map();
3806
+ for (const fmt of byTag(doc, "numFmt")) {
3807
+ const id = Number(fmt.getAttribute("numFmtId"));
3808
+ const code = fmt.getAttribute("formatCode");
3809
+ if (Number.isFinite(id) && code !== null) custom.set(id, code);
3810
+ }
3811
+ const cellXfs = byTag(doc, "cellXfs")[0];
3812
+ if (cellXfs === void 0) return [];
3813
+ return [...cellXfs.children].filter((xf) => xf.localName === "xf").map((xf) => {
3814
+ const id = Number(xf.getAttribute("numFmtId") ?? "0");
3815
+ if (!Number.isFinite(id)) return false;
3816
+ const code = custom.get(id);
3817
+ return code !== void 0 ? isDateFormatCode(code) : BUILTIN_DATE_FORMATS.has(id);
3818
+ });
3819
+ }
3820
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
3821
+ /**
3822
+ * Excel 日期序列号 → ISO 8601。
3823
+ *
3824
+ * 1900 历制里序列号 60 是**不存在的 1900-02-29**:Excel 为兼容 Lotus 1-2-3
3825
+ * 保留了这个错误。不特判它,1900-03-01 之后的全部日期都会差一天。
3826
+ */
3827
+ function serialToIso(serial, date1904) {
3828
+ const whole = Math.floor(serial);
3829
+ const fraction = serial - whole;
3830
+ let seconds = Math.round(fraction * 86400);
3831
+ let days = whole;
3832
+ if (seconds >= 86400) {
3833
+ seconds -= 86400;
3834
+ days += 1;
3835
+ }
3836
+ let iso;
3837
+ if (!date1904 && days === 60) iso = "1900-02-29";
3838
+ else {
3839
+ const at = new Date((date1904 ? Date.UTC(1904, 0, 1) : days < 60 ? Date.UTC(1899, 11, 31) : Date.UTC(1899, 11, 30)) + days * 864e5);
3840
+ iso = `${pad(at.getUTCFullYear(), 4)}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())}`;
3841
+ }
3842
+ if (seconds === 0) return iso;
3843
+ const h = Math.floor(seconds / 3600);
3844
+ const m = Math.floor(seconds % 3600 / 60);
3845
+ const s = seconds % 60;
3846
+ return `${iso}T${pad(h)}:${pad(m)}:${pad(s)}`;
3847
+ }
3848
+ /** 一个 `<c>` 的呈现文本(FR-12.3) */
3849
+ function cellText(cell, shared, dateStyles, date1904) {
3850
+ const type = cell.getAttribute("t");
3851
+ if (type === "inlineStr") {
3852
+ const is = [...cell.children].find((child) => child.localName === "is");
3853
+ return is !== void 0 ? sharedStringText(is) : "";
3854
+ }
3855
+ const raw = [...cell.children].find((child) => child.localName === "v")?.textContent ?? "";
3856
+ if (raw === "") return "";
3857
+ if (type === "s") {
3858
+ const index = Number(raw);
3859
+ return Number.isInteger(index) ? shared[index] ?? "" : "";
3860
+ }
3861
+ if (type === "str" || type === "e") return raw;
3862
+ if (type === "b") return raw === "1" ? "TRUE" : "FALSE";
3863
+ const numeric = Number(raw);
3864
+ if (!Number.isFinite(numeric)) return raw;
3865
+ const styleIndex = Number(cell.getAttribute("s") ?? "0");
3866
+ if (Number.isInteger(styleIndex) && dateStyles[styleIndex] === true) return serialToIso(numeric, date1904);
3867
+ return raw;
3868
+ }
3869
+ /**
3870
+ * 一张工作表的文本:一行一行,单元格用制表符分隔(与 docx 的表格同口径)。
3871
+ * 空行与空列**照原样留着** —— 位置本身是语义,压掉之后「第 3 列」就对不上了。
3872
+ */
3873
+ function sheetText(doc, shared, dateStyles, date1904) {
3874
+ const lines = [];
3875
+ let previousRow = 0;
3876
+ for (const row of byTag(doc, "row")) {
3877
+ const declared = Number(row.getAttribute("r"));
3878
+ const rowNumber = Number.isInteger(declared) && declared > 0 ? declared : previousRow + 1;
3879
+ for (let gap = previousRow + 1; gap < rowNumber; gap++) lines.push("");
3880
+ previousRow = rowNumber;
3881
+ const cells = [];
3882
+ let nextColumn = 0;
3883
+ for (const cell of row.children) {
3884
+ if (cell.localName !== "c") continue;
3885
+ const column = columnIndex(cell.getAttribute("r")) ?? nextColumn;
3886
+ for (let gap = cells.length; gap < column; gap++) cells.push("");
3887
+ const text = cellText(cell, shared, dateStyles, date1904);
3888
+ if (cells.length > column) cells[column] = text;
3889
+ else cells.push(text);
3890
+ nextColumn = column + 1;
3891
+ }
3892
+ lines.push(cells.join(" ").replace(/\t+$/, ""));
3893
+ }
3894
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
3895
+ return lines.join("\n");
3896
+ }
3897
+ const looksLikeOle2 = (bytes) => bytes.length >= OLE2_SIGNATURE.length && OLE2_SIGNATURE.every((byte, i) => bytes[i] === byte);
3898
+ /** rels 里的 Target 可能是相对 `xl/` 的,也可能是包根绝对路径 */
3899
+ function resolveSheetPath(target) {
3900
+ const clean = target.replace(/^\.\//, "");
3901
+ return clean.startsWith("/") ? clean.slice(1) : `xl/${clean}`;
3902
+ }
3903
+ /**
3904
+ * 从 xlsx 字节里抽出文本,每个工作表一段、段首是表名。
3905
+ *
3906
+ * @throws WebSkillError 归档损坏、旧版 `.xls`、缺 `xl/workbook.xml`、工作表关系解不开、XML 解析失败
3907
+ */
3908
+ async function extractXlsxText(bytes) {
3909
+ if (looksLikeOle2(bytes)) fail("This is a legacy .xls workbook, which is not supported. Save it as .xlsx and try again.");
3910
+ let entries;
3911
+ try {
3912
+ entries = await unzipWithLimits(bytes);
3913
+ } catch {
3914
+ fail("This file could not be read as an Excel workbook: the archive is damaged or not a .xlsx file.");
3915
+ }
3916
+ const parts = new Map(entries);
3917
+ const workbookBytes = parts.get(WORKBOOK);
3918
+ if (workbookBytes === void 0) fail(`This file is not an Excel workbook: it has no ${WORKBOOK} entry.`);
3919
+ const workbook = parseXml(workbookBytes, "index");
3920
+ const workbookPr = byTag(workbook, "workbookPr")[0];
3921
+ const date1904 = workbookPr?.getAttribute("date1904") === "1" || workbookPr?.getAttribute("date1904") === "true";
3922
+ const relsBytes = parts.get(WORKBOOK_RELS);
3923
+ const targets = /* @__PURE__ */ new Map();
3924
+ if (relsBytes !== void 0) for (const rel of byTag(parseXml(relsBytes, "relationships"), "Relationship")) {
3925
+ const id = rel.getAttribute("Id");
3926
+ const target = rel.getAttribute("Target");
3927
+ if (id !== null && target !== null) targets.set(id, resolveSheetPath(target));
3928
+ }
3929
+ const sharedBytes = parts.get(SHARED_STRINGS);
3930
+ const shared = sharedBytes === void 0 ? [] : byTag(parseXml(sharedBytes, "shared strings"), "si").map(sharedStringText);
3931
+ const dateStyles = readDateStyles(parts.get(STYLES));
3932
+ const sections = [];
3933
+ for (const [ordinal, sheet] of byTag(workbook, "sheet").entries()) {
3934
+ const name = sheet.getAttribute("name") ?? `Sheet${ordinal + 1}`;
3935
+ const relId = sheet.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? sheet.getAttribute("r:id");
3936
+ const path = relId !== null ? targets.get(relId) : void 0;
3937
+ const sheetBytes = path !== void 0 ? parts.get(path) : void 0;
3938
+ if (sheetBytes === void 0) fail(`The worksheet "${name}" could not be located inside the workbook.`);
3939
+ const body = sheetText(parseXml(sheetBytes, `worksheet "${name}"`), shared, dateStyles, date1904);
3940
+ sections.push(body === "" ? `Sheet: ${name}` : `Sheet: ${name}\n${body}`);
3941
+ }
3942
+ return sections.join("\n\n");
3943
+ }
3944
+ /**
3395
3945
  * 文档投放面的 CSP 单一来源(0.11.0 分册 18 FR-18.3 / AC-G21)。
3396
3946
  *
3397
3947
  * ⚠️ 策略**只能**由 viewer 路由的响应头下发。
@@ -3635,4 +4185,4 @@ function originOf(blockedURI) {
3635
4185
  }
3636
4186
 
3637
4187
  //#endregion
3638
- export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };
4188
+ export { BrowserSkillManager, BrowserWorkerScriptExecutor, ChromeBuiltinLlmClient, DEFAULT_MAX_VIEWER_PAYLOAD_BYTES, DOCUMENT_SURFACE_AUDIT_EVENT, DOCX_UNEXTRACTED, HOST_PORT_MATRIX, IframeWorkerLike, OpfsProvider, TsTranspiler, VIEWER_SANDBOX_TOKENS, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, XLSX_UNEXTRACTED, blockedMessage, bridgeError, captureElementImage, checkDictationAvailability, compressImageToBudget, createBrowserChatbotHost, createDomPageActionExecutor, createDomPerceptionReader, createEncryptedMemoryStore, createFetchLinkedDocumentReader, createIframeWorker, createLlmClient, deleteMemoryEncryptionKey, extractDocxText, extractXlsxText, extractZipWeb, generateMemoryEncryptionKey, inspectHostWiring, installWebSkillNavigator, isCapturableElement, isCaptureFailure, isEncryptedMemoryValue, isOpfsAvailable, missingPorts, openDocumentSurface, openMemoryEncryptionKey, parseBridgeRequest, parseMainMessage, parseWorkerEvent, probeChromeBuiltinAvailability, sha256HexWeb, startDictation, startViewerShell, startWorkerRuntimeHost, viewerCspHeader, watchBlockedResources };