@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/agent.d.ts +3 -3
- package/dist/agent.js +3 -3
- package/dist/browser.d.ts +28 -5
- package/dist/browser.js +624 -74
- package/dist/{catalogComponents-BgAJN0p8-C3K8klJd.js → catalogComponents-BgAJN0p8-CYEXSk45.js} +346 -12
- package/dist/{dist-sdKFgERo.js → dist-ExSQky4C.js} +324 -30
- package/dist/{dist-DU9KDAuR.js → dist-GK6dtjRv.js} +29 -4
- package/dist/governance.d.ts +1 -1
- package/dist/{index-3fCHc1mQ.d.ts → index-C3XdItd_.d.ts} +1 -1
- package/dist/{index-C9pzXLKy.d.ts → index-DkvBhJQy.d.ts} +40 -3
- package/dist/{index-DFhU1uks.d.ts → index-iBm9tJL_.d.ts} +60 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +23 -2
- package/dist/mcp.js +43 -2
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/{openUiLibrary-BKXW7Iwx-DaymVubt.js → openUiLibrary-BKXW7Iwx-CWWBVOkE.js} +1 -1
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +11 -9
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +1 -1
- package/package.json +2 -1
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-
|
|
3
|
-
import {
|
|
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
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
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 = (
|
|
2003
|
-
function collect(scope,
|
|
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
|
|
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:
|
|
2279
|
+
frame: label
|
|
2021
2280
|
});
|
|
2022
2281
|
continue;
|
|
2023
2282
|
}
|
|
2024
|
-
const
|
|
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 =
|
|
2027
|
-
const sets = actionSets(doc, actionScopeFor(
|
|
2028
|
-
|
|
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,
|
|
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
|
-
|
|
2043
|
-
const node = describe(modal, /* @__PURE__ */ new Set(), root, view,
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
2146
|
-
|
|
2147
|
-
if (
|
|
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
|
|
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
|
-
|
|
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 };
|