@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.
- package/dist/abnfDiagram-O67JEVCF-DtYFjfqO.js +109 -0
- package/dist/agent.d.ts +3 -3
- package/dist/agent.js +1012 -130
- package/dist/{approval-DwN2o2QG.js → approval-DWQlDPbY.js} +102 -16
- package/dist/arc-8U_6NQWm.js +130 -0
- package/dist/architecture-7GRP2DOG-RTw1wpAq.js +4 -0
- package/dist/architectureDiagram-NJMV4G6O-XmmXrJAK.js +7473 -0
- package/dist/array-DQfpQNY8.js +8 -0
- package/dist/blockDiagram-BEXU5L5S-D4yjnCD1.js +2582 -0
- package/dist/browser.d.ts +444 -48
- package/dist/browser.js +1771 -91
- package/dist/c4Diagram-YGBWAQC7-BLmA_JSd.js +3668 -0
- package/dist/channel-CZxxLrG7.js +9 -0
- package/dist/chunk-2Q5K7J3B-Ck03gkFR.js +21 -0
- package/dist/chunk-3FUC2YCW-tr6jmbQf.js +530 -0
- package/dist/chunk-5DYCD2WN-Bt97YGTd.js +58 -0
- package/dist/chunk-5VM5RSS4-BBxa48HL.js +21 -0
- package/dist/chunk-6AEJRKK7-JXK89I-O.js +14 -0
- package/dist/chunk-742MDFTN-CPmgCGwC.js +466 -0
- package/dist/chunk-7INBJB4K-C9jq9o66.js +6552 -0
- package/dist/chunk-7M6MHVWA-BrKCWcR1.js +4894 -0
- package/dist/chunk-7PRAP22T-BHED6GVs.js +95 -0
- package/dist/chunk-DUW6YSOI-DoZj78PO.js +19 -0
- package/dist/chunk-FOHPRMQF-Dkbp819z.js +27189 -0
- package/dist/chunk-GNY47TPC-Bpvq1T49.js +87 -0
- package/dist/chunk-GTNCS2PH-CLmh4I2c.js +686 -0
- package/dist/chunk-GWA4HPMP-B6vN3fkL.js +95 -0
- package/dist/chunk-J5ZVWO5B-Bi_Pcobw.js +26 -0
- package/dist/chunk-JWPE2WC7-D1rC79GS.js +12 -0
- package/dist/chunk-LNGE3PJU-D5OIoe3Z.js +2582 -0
- package/dist/chunk-MBY4JIJT-C34L7a4f.js +2276 -0
- package/dist/chunk-NETBCI7D-Cg84k8N8.js +321 -0
- package/dist/chunk-O7XYJQB3-B6K0KOSL.js +8846 -0
- package/dist/chunk-UA2S7LBM-DxHM29_K.js +612 -0
- package/dist/chunk-WEXAMYUT-Ba6jFWhB.js +33 -0
- package/dist/chunk-XXDRQBXY-DPbSy4kg.js +12 -0
- package/dist/chunk-Y2CYZVJY-U3QOMqrW.js +15 -0
- package/dist/chunk-Z7XXMR3K-oLg1h8F3.js +1038 -0
- package/dist/chunk-ZIGJFQKS-DqEFBuHR.js +2215 -0
- package/dist/classDiagram-v2-NBCMYWYE-BqnOWIUt.js +3845 -0
- package/dist/cose-bilkent-JH36ORCC-C0dTAHVK.js +4270 -0
- package/dist/cynefin-OW5HDTMX-B2QvgEQj.js +4 -0
- package/dist/cynefinDiagram-VND7K2PF-yAWc6Z3W.js +479 -0
- package/dist/cytoscape.esm-C64mRh9B.js +28325 -0
- package/dist/dagre-6A5THRUB-D0w2W27v.js +4816 -0
- package/dist/defaultLocale-B_EoOkSF.js +249 -0
- package/dist/diagram-22UHCM2B-DvgzhUzP.js +6115 -0
- package/dist/diagram-3UASUU5V-DWgUB-ef.js +795 -0
- package/dist/diagram-ATOU4E4O-P1olqwIs.js +553 -0
- package/dist/diagram-CDSNMT55-BUlZuLUe.js +402 -0
- package/dist/diagram-MLGK6HIB-6w5qmkGt.js +186 -0
- package/dist/diagram-MPIPVDR6-DEFiSo8o.js +289 -0
- package/dist/dist-UajCDt3n.js +92 -0
- package/dist/ebnfDiagram-ZINNZB2B-CI8liCfR.js +123 -0
- package/dist/elk-276RUBZZ-3SvW6OAK.js +90034 -0
- package/dist/erDiagram-OPXOYQCR-CPTbKhwP.js +2301 -0
- package/dist/eventmodeling-NTZA5JFV-BlyuqGp0.js +4 -0
- package/dist/flowDiagram-KWPJA3E3-CcUJ6HbV.js +18 -0
- package/dist/ganttDiagram-FUAMR5RP-31UjY0KK.js +3939 -0
- package/dist/{toolSource-C9oNuKmr.js → geometry-wx5ZTYzi.js} +162 -12
- package/dist/gitGraph-4MIJSDKK-ChdVlrk7.js +4 -0
- package/dist/gitGraphDiagram-X574FWY7-DKuxJymB.js +1153 -0
- package/dist/governance.d.ts +3 -3
- package/dist/graphlib-tk9gQYuN.js +4194 -0
- package/dist/{index-B8Tgmpwh.d.ts → index-BiO9_XRk.d.ts} +2 -2
- package/dist/{index-CYLt5u4L.d.ts → index-CjwdZQOS.d.ts} +700 -12
- package/dist/{index-DHopiH_m.d.ts → index-YPd8ZSEa.d.ts} +156 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/info-A6RAGUB7-CxRAt9S9.js +4 -0
- package/dist/infoDiagram-VRGFBTTK-pRspGfjZ.js +25 -0
- package/dist/init-CMOIWdg-.js +16 -0
- package/dist/ishikawaDiagram-OU5B5YK6-DKerAPAg.js +975 -0
- package/dist/journeyDiagram-ZHPQQLJL-C4MK-YO5.js +1227 -0
- package/dist/kanban-definition-PNTS6WVX-BqsZpMXx.js +1244 -0
- package/dist/katex-BcRRvyh_.js +26465 -0
- package/dist/{kind-DaKqLX2F.js → kind-Dc8x0HWz.js} +54 -7
- package/dist/line-LgV8iVMV.js +48 -0
- package/dist/linear-BWKR0Tqd.js +393 -0
- package/dist/linkedDocument-CIuh_Yp0.js +416 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/{memoryArtifactStore-D6dLeY55.js → memoryArtifactStore-Bs7glPqm.js} +14 -0
- package/dist/mermaid-parser.core-pYR5siSx.js +754 -0
- package/dist/mermaid.core-DMtwGCHy.js +1756 -0
- package/dist/mindmap-definition-NLK3R4M7-CXbD-myG.js +1312 -0
- package/dist/node.d.ts +3 -3
- package/dist/node.js +2 -2
- package/dist/{openUiLibrary-b0i7F_oZ.js → openUiLibrary-B7j3rLrm.js} +1 -1
- package/dist/{openUiSpecLang-CLXJEeRU.js → openUiSpecLang-BSYYnjay.js} +1 -1
- package/dist/ordinal-CxA6GHQS.js +84 -0
- package/dist/packet-AYTQ26CC-BOm08cxl.js +4 -0
- package/dist/path-C27Z2Bjv.js +106 -0
- package/dist/pegDiagram-GJSIUBJH-BLGJOewQ.js +115 -0
- package/dist/pie-WAS4IAKB-Dvg7glpk.js +4 -0
- package/dist/pieDiagram-5QR66LMP-laoaV_PB.js +298 -0
- package/dist/quadrantDiagram-O4NWA36T-Db_wFBkT.js +2236 -0
- package/dist/radar-RG4KPBEZ-BZDaTu06.js +4 -0
- package/dist/railroad-74A4TZTK-C-UhjkOy.js +4 -0
- package/dist/railroad-abnf-HS5TGJTU-dUKVA4OS.js +4 -0
- package/dist/railroad-ebnf-LZEXJU2U-DzQumk18.js +4 -0
- package/dist/railroad-peg-WCYAUIDC-B3vvTWed.js +4 -0
- package/dist/railroadDiagram-XR7U4H2S-BY0sTM4X.js +89 -0
- package/dist/requirementDiagram-PLB6GJNP-3rkQfEhs.js +2474 -0
- package/dist/rough.cjs-AmDd_NQk.js +1395 -0
- package/dist/sankeyDiagram-IPEJSGJF-CGAJhyG2.js +3578 -0
- package/dist/sequenceDiagram-PO4LG4MO-9FJjFX06.js +5522 -0
- package/dist/sizeCapture-INFHLROL-D8PWxdOn.js +56 -0
- package/dist/{skillVersionStore-B24Jjjry-BkOk9wI_.d.ts → skillVersionStore-B24Jjjry-BY1H7Krp.d.ts} +1 -1
- package/dist/src-NkhI3UMl.js +2653 -0
- package/dist/stateDiagram-v2-GCMORJYK-B0O3wc1-.js +2715 -0
- package/dist/swimlanes-2SLR337P-D4EOnrpu.js +6637 -0
- package/dist/swimlanesDiagram-TC7HE7FX-BUx29EJ9.js +30 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +1 -1
- package/dist/timeline-definition-EJHVYXUP-CrgiiB4C.js +1513 -0
- package/dist/treeView-Q6P3EWNA-C1by-Gzc.js +4 -0
- package/dist/treemap-WGGIJYW6-mC2NZ6ad.js +4 -0
- package/dist/{types-BvTV_05--BW8zDuEk.d.ts → types-CpDRZ0rA-BM5FI9oN.d.ts} +59 -8
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-react.js +3718 -77
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +2 -2
- package/dist/usecaseDiagram-POWQR4AR-BUt_TQOY.js +14397 -0
- package/dist/vennDiagram-UO4OBE2U-CwXr0wjA.js +2808 -0
- package/dist/wardley-WFR3VGLG-CVSYjq_S.js +4 -0
- package/dist/wardleyDiagram-VNRHLVJA-Cpfd47dQ.js +874 -0
- package/dist/{webSkillApi-Cib6G-94.js → webSkillApi-Dy-Zjv0y.js} +1 -1
- package/dist/{webskillLitCatalog-W06npwej.js → webskillLitCatalog-D5BCiMCU.js} +1 -1
- package/dist/xychartDiagram-PMCCYNJV-e1TeVoc6.js +2704 -0
- package/package.json +1 -1
- 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-
|
|
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-
|
|
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 {
|
|
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-
|
|
11
|
-
import { _ as
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
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,
|
|
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 &&
|
|
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 (
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
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$
|
|
5417
|
-
function parseXml$
|
|
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$
|
|
5949
|
+
const doc = parseXml$3(bytes, "styles");
|
|
5465
5950
|
const custom = /* @__PURE__ */ new Map();
|
|
5466
|
-
for (const fmt of byTag$
|
|
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$
|
|
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$
|
|
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$
|
|
5580
|
-
const workbookPr = byTag$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5977
|
-
const
|
|
5978
|
-
|
|
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
|
-
|
|
7648
|
+
...facts,
|
|
7649
|
+
warned: false,
|
|
7650
|
+
trigger: rejected
|
|
5983
7651
|
});
|
|
5984
|
-
throw new WebSkillError("VALIDATION_FAILED",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|