@webskill/sdk 0.21.0 → 0.23.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 +1174 -169
- package/dist/{approval-DwN2o2QG.js → approval-7CuZS3Z4.js} +104 -18
- 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 +455 -49
- package/dist/browser.js +2134 -138
- 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-CRhuySKV.js} +169 -12
- package/dist/gitGraph-4MIJSDKK-ChdVlrk7.js +4 -0
- package/dist/gitGraphDiagram-X574FWY7-DKuxJymB.js +1153 -0
- package/dist/governance.d.ts +22 -4
- package/dist/governance.js +100 -10
- package/dist/graphlib-tk9gQYuN.js +4194 -0
- package/dist/{index-DHopiH_m.d.ts → index-7No55dRb.d.ts} +161 -11
- package/dist/{index-B8Tgmpwh.d.ts → index-ShvCB5I9.d.ts} +2 -2
- package/dist/{index-CYLt5u4L.d.ts → index-V76_OFj2.d.ts} +789 -21
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -9
- 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-CZZOl9cO.js +456 -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 +12 -5
- 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/{pathSecurity-B1owvJAF.js → pathSecurity-CirkQwWP.js} +12 -1
- 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/{skill-CAJMsLod.js → skill-WSUDp6A1.js} +1 -1
- package/dist/{skillVersionStore-B24Jjjry-BkOk9wI_.d.ts → skillVersionStore-S4_HJ_Fl-Cvy9LqVu.d.ts} +35 -4
- 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-BiZBXGm4.d.ts} +66 -8
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-react.js +3746 -96
- 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-D5VMUnrv.js} +2 -2
- 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";
|
|
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-
|
|
3
|
-
import {
|
|
1
|
+
import { n as messageOf$1, t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
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-WSUDp6A1.js";
|
|
3
|
+
import { o as atomicWriteText, r as resolveInsideRoot, s as isAtomicTempPath } from "./pathSecurity-CirkQwWP.js";
|
|
4
4
|
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
5
|
-
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-7CuZS3Z4.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-CZZOl9cO.js";
|
|
9
9
|
import { n as normalizeToolContent, t as mergeCatalogEntries } from "./external-_ZRQe-V9.js";
|
|
10
|
-
import { t as createWebSkillApi } from "./webSkillApi-
|
|
11
|
-
import {
|
|
10
|
+
import { t as createWebSkillApi } from "./webSkillApi-D5VMUnrv.js";
|
|
11
|
+
import { A as frameSteps, D as SCROLL_DIRECTIONS, E as toActionFrameScopes, O as toFrameScopes, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, f as readImageSize, k as frameLabel, l as parseCellAddress, n as EMU_PER_POINT, o as fitWithin, p as PDF_LARGE_IMAGE_RATIO, r as EMU_PER_TWIP$1, s as formatCellAddress, t as EMU_PER_PIXEL, u as parseCellRange, v as takesImage } from "./geometry-CRhuySKV.js";
|
|
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
|
/**
|
|
@@ -2424,24 +2634,56 @@ const ROUTE_ATTRIBUTES = [
|
|
|
2424
2634
|
* 选项往往就是不带 role 的 `<li>`(0.17.0 分册 11)。它与另外三个一样受
|
|
2425
2635
|
* 「有可操作后代就不提升」约束,所以真正的语义列表不会被误伤。
|
|
2426
2636
|
*
|
|
2427
|
-
*
|
|
2637
|
+
* 判据是「条目 / 单元」对「容器」(0.23.0 FR-11.1):一格、一项、一栏能承载一次点击,
|
|
2638
|
+
* `grid` / `menu` / `tablist` 这类容器不能,所以它们不在这里。
|
|
2639
|
+
*
|
|
2640
|
+
* 0.23.0 补齐了显式 ARIA 写法:过去 `<td>`(隐式 `cell`)能被提升,而
|
|
2641
|
+
* `<td role="gridcell">` 不能——把无障碍角色写对反而被降级。自绘日历、菜单、
|
|
2642
|
+
* 下拉普遍写显式角色,于是整片点不到(0.23.0 分册 11)。
|
|
2643
|
+
*
|
|
2644
|
+
* 这张白名单**只约束启发式信号**,放宽的是「够不够格被考察」,不是「够不够格被提升」:
|
|
2645
|
+
* 后面三道闸门(自身可操作 / 有可操作后代 / 看起来可交互)一个都没松。
|
|
2646
|
+
* 宿主探针点名的元素走另一条路(分册 12)。
|
|
2428
2647
|
*/
|
|
2429
2648
|
const PROMOTABLE_ROLES = /* @__PURE__ */ new Set([
|
|
2430
2649
|
"generic",
|
|
2431
2650
|
"img",
|
|
2432
2651
|
"cell",
|
|
2433
|
-
"listitem"
|
|
2652
|
+
"listitem",
|
|
2653
|
+
"columnheader",
|
|
2654
|
+
"gridcell",
|
|
2655
|
+
"rowheader",
|
|
2656
|
+
"option",
|
|
2657
|
+
"treeitem",
|
|
2658
|
+
"tab",
|
|
2659
|
+
"menuitem",
|
|
2660
|
+
"menuitemcheckbox",
|
|
2661
|
+
"menuitemradio"
|
|
2434
2662
|
]);
|
|
2435
2663
|
/** 合成名的前缀,必须让模型看得出「这不是页面上的字,别拿它去搜」(FR-15.4 第 3 级) */
|
|
2436
2664
|
const SYNTHETIC_NAME_PREFIX = "unlabeled control";
|
|
2437
2665
|
/** 往上找具名祖先的层数上限:再往上就是整页容器,拿它当限定语反而更含糊 */
|
|
2438
2666
|
const SYNTHETIC_NAME_LOOKUP_DEPTH = 5;
|
|
2439
2667
|
const classAndId = (element) => `${element.getAttribute("class") ?? ""} ${element.getAttribute("id") ?? ""}`;
|
|
2440
|
-
/**
|
|
2668
|
+
/**
|
|
2669
|
+
* `cursor: pointer` —— 页面自己声明了可点。`getComputedStyle` 最贵,殿后。
|
|
2670
|
+
*
|
|
2671
|
+
* 只认元素**自身引入**的那一次声明:`cursor` 是 CSS 继承属性,一句
|
|
2672
|
+
* `.card { cursor: pointer }` 会让整张卡片里的每段文字计算值都是 `pointer`。
|
|
2673
|
+
* 实测某差旅站点首页 1537 个可见元素里 **1050 个**仅凭继承来的 `cursor`
|
|
2674
|
+
* 就被判成可交互,感知里因此冒出 826 个「按钮」(0.23.0 FR-16.1)。
|
|
2675
|
+
*
|
|
2676
|
+
* 判据是与父元素的计算值**有没有差异**:没有差异就当作继承来的。
|
|
2677
|
+
* 父子都显式声明 `pointer` 的情形分辨不了(没有任何 DOM API 能做到),
|
|
2678
|
+
* 那类元素由其余四级信号兜底,见 0.23.0 设计 17 §3。
|
|
2679
|
+
*/
|
|
2441
2680
|
function looksClickable(element, view) {
|
|
2442
2681
|
if (view === null) return false;
|
|
2443
2682
|
try {
|
|
2444
|
-
|
|
2683
|
+
if (view.getComputedStyle(element).cursor !== "pointer") return false;
|
|
2684
|
+
const parent = element.parentElement;
|
|
2685
|
+
if (parent === null) return true;
|
|
2686
|
+
return view.getComputedStyle(parent).cursor !== "pointer";
|
|
2445
2687
|
} catch {
|
|
2446
2688
|
return false;
|
|
2447
2689
|
}
|
|
@@ -2691,7 +2933,9 @@ function nameOf(element, doc) {
|
|
|
2691
2933
|
}
|
|
2692
2934
|
if (FORM_CONTROL_TAGS.has(element.tagName)) return externalNameOf(element);
|
|
2693
2935
|
const own = ownText(element);
|
|
2694
|
-
|
|
2936
|
+
if (own !== "") return own;
|
|
2937
|
+
const title = element.getAttribute("title");
|
|
2938
|
+
return title !== null && title.trim() !== "" ? clip(title) : void 0;
|
|
2695
2939
|
}
|
|
2696
2940
|
function valueOf(element) {
|
|
2697
2941
|
if (element.tagName === "INPUT") {
|
|
@@ -2724,6 +2968,35 @@ function hidden$1(element, view) {
|
|
|
2724
2968
|
}
|
|
2725
2969
|
return false;
|
|
2726
2970
|
}
|
|
2971
|
+
/**
|
|
2972
|
+
* 页面是否有真实布局。jsdom 有 `getComputedStyle` 却没有布局引擎,那里所有元素的
|
|
2973
|
+
* rect 都是 0×0,把零尺寸判定套上去会让**全部**控件失去句柄(0.23.0 FR-10.4)。
|
|
2974
|
+
*
|
|
2975
|
+
* 判据与被判据用**同一种测量手段**:拿根元素的 rect 为下面的 rect 背书。
|
|
2976
|
+
* 真实浏览器里根元素必有尺寸,jsdom 里恒为 0×0。
|
|
2977
|
+
*
|
|
2978
|
+
* 不用 `clientHeight`,更不用 `view.innerHeight`:
|
|
2979
|
+
* 前者与 rect 是两套独立机制,测试桩(以及 `overflow` 的各种写法)可以只动其中一套,
|
|
2980
|
+
* 用 A 去为 B 背书会得出「有布局」却量不到尺寸的矛盾结论;
|
|
2981
|
+
* 后者在 jsdom 里被编了 1024×768,两边根本看不出区别。
|
|
2982
|
+
*/
|
|
2983
|
+
function hasLayout(doc) {
|
|
2984
|
+
const rect = doc.documentElement.getBoundingClientRect();
|
|
2985
|
+
return rect.width > 0 || rect.height > 0;
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* 元素在屏幕上不占任何像素(0.23.0 FR-10.1)。人的鼠标点不到它,
|
|
2989
|
+
* 用户也无从在逐次确认卡上核对它——`act_on_page` 的安全模型建立在「用户看得懂自己批准了什么」上。
|
|
2990
|
+
*
|
|
2991
|
+
* 不复用 `measuredSizeOf()`:那个函数在 rect 为 0 时会回退去读 naturalWidth /
|
|
2992
|
+
* canvas 属性 / viewBox,为的是「测不到尺寸就别过滤图片」。这里要的恰恰相反:
|
|
2993
|
+
* 必须分得清「确实量到了 0」和「没有布局」,一档都不能回退。
|
|
2994
|
+
*/
|
|
2995
|
+
function occupiesNoSpace(element) {
|
|
2996
|
+
if (!hasLayout(element.ownerDocument)) return false;
|
|
2997
|
+
const rect = element.getBoundingClientRect();
|
|
2998
|
+
return rect.width === 0 || rect.height === 0;
|
|
2999
|
+
}
|
|
2727
3000
|
/** script / style 的正文是代码,不是页面内容 */
|
|
2728
3001
|
const SKIPPED_TAGS = /* @__PURE__ */ new Set([
|
|
2729
3002
|
"SCRIPT",
|
|
@@ -2733,6 +3006,33 @@ const SKIPPED_TAGS = /* @__PURE__ */ new Set([
|
|
|
2733
3006
|
]);
|
|
2734
3007
|
/** 活动模态的三种形态;Radix 之流用 aria-modal,原生用 <dialog open> */
|
|
2735
3008
|
const MODAL_SELECTOR = "[role=dialog][open], dialog[open], [aria-modal=true]";
|
|
3009
|
+
const scrollsOverflow = (overflow) => overflow === "auto" || overflow === "scroll" || overflow === "overlay";
|
|
3010
|
+
/**
|
|
3011
|
+
* 该元素**自身**还能往哪些方向滚(0.23.0 分册 22 FR-22.1)。
|
|
3012
|
+
*
|
|
3013
|
+
* 先读布局属性再查计算样式,顺序不能倒:实测某 ERP 页 2488 个元素里只有 18 个
|
|
3014
|
+
* 在第一道过筛(1.4ms),对这 18 个查 overflow 只要 0.1ms;对全部 2488 个查要 5.8ms。
|
|
3015
|
+
* 这与 `looksClickable` 把 `getComputedStyle` 殿后是同一条原则。
|
|
3016
|
+
*
|
|
3017
|
+
* 只判元素自身、不上溯,标示因此只落在容器节点上(FR-22.3)。
|
|
3018
|
+
* jsdom 没有布局,这些尺寸恒为 0,筛子自然不命中。
|
|
3019
|
+
*/
|
|
3020
|
+
function scrollableDirectionsOf(element, view) {
|
|
3021
|
+
const overflowsX = element.scrollWidth > element.clientWidth;
|
|
3022
|
+
const overflowsY = element.scrollHeight > element.clientHeight;
|
|
3023
|
+
if (!overflowsX && !overflowsY) return [];
|
|
3024
|
+
const style = view.getComputedStyle(element);
|
|
3025
|
+
const out = [];
|
|
3026
|
+
if (overflowsX && scrollsOverflow(style.overflowX)) {
|
|
3027
|
+
if (element.scrollLeft > 0) out.push("left");
|
|
3028
|
+
if (element.scrollLeft < element.scrollWidth - element.clientWidth - 1) out.push("right");
|
|
3029
|
+
}
|
|
3030
|
+
if (overflowsY && scrollsOverflow(style.overflowY)) {
|
|
3031
|
+
if (element.scrollTop > 0) out.push("up");
|
|
3032
|
+
if (element.scrollTop < element.scrollHeight - element.clientHeight - 1) out.push("down");
|
|
3033
|
+
}
|
|
3034
|
+
return out;
|
|
3035
|
+
}
|
|
2736
3036
|
/** 按尺寸落选取像的说明(FR-20.3);与「超名额」「取像失败」必须可区分,三者排查方向不同 */
|
|
2737
3037
|
const ICON_SKIPPED_NOTE = "Not captured: this image is smaller than the icon threshold";
|
|
2738
3038
|
/**
|
|
@@ -2758,6 +3058,8 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
|
|
|
2758
3058
|
const value = valueOf(element);
|
|
2759
3059
|
let role = roleOf$1(element);
|
|
2760
3060
|
let promotedHref;
|
|
3061
|
+
let roleBeforePromotion;
|
|
3062
|
+
let nameWasSynthesized = false;
|
|
2761
3063
|
if (promotion !== void 0) {
|
|
2762
3064
|
const promoted = promoteRole({
|
|
2763
3065
|
element,
|
|
@@ -2768,14 +3070,37 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
|
|
|
2768
3070
|
context: promotion
|
|
2769
3071
|
});
|
|
2770
3072
|
if (promoted !== void 0) {
|
|
3073
|
+
roleBeforePromotion = role;
|
|
2771
3074
|
role = promoted.role;
|
|
2772
3075
|
promotedHref = promoted.href;
|
|
2773
|
-
if (name === void 0)
|
|
3076
|
+
if (name === void 0) {
|
|
3077
|
+
name = clip(iconHintOf(element) ?? synthesizeName(element, (a) => nameOf(a, doc)));
|
|
3078
|
+
nameWasSynthesized = true;
|
|
3079
|
+
}
|
|
2774
3080
|
}
|
|
2775
3081
|
}
|
|
3082
|
+
let ref = handles?.issue(element, role, frame);
|
|
3083
|
+
if (roleBeforePromotion !== void 0 && ref === void 0) {
|
|
3084
|
+
role = roleBeforePromotion;
|
|
3085
|
+
promotedHref = void 0;
|
|
3086
|
+
if (nameWasSynthesized) name = void 0;
|
|
3087
|
+
ref = handles?.issue(element, role, frame);
|
|
3088
|
+
}
|
|
2776
3089
|
const href = promotedHref ?? (role === "link" ? hrefOf(element) : void 0);
|
|
2777
|
-
|
|
2778
|
-
|
|
3090
|
+
let scrollable;
|
|
3091
|
+
let scrollRef;
|
|
3092
|
+
if (view !== null) {
|
|
3093
|
+
const directions = scrollableDirectionsOf(element, view);
|
|
3094
|
+
if (directions.length > 0) {
|
|
3095
|
+
const issued = ref ?? handles?.issueScrollable(element, frame);
|
|
3096
|
+
if (issued !== void 0) {
|
|
3097
|
+
scrollable = directions;
|
|
3098
|
+
scrollRef = issued;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if (role === "generic" && name === void 0 && value === void 0 && children.length === 0 && scrollable === void 0) return;
|
|
3103
|
+
if (promotion !== void 0 && role === "generic" && name === void 0 && value === void 0 && scrollable === void 0) {
|
|
2779
3104
|
if (children.length === 1) return children[0];
|
|
2780
3105
|
}
|
|
2781
3106
|
const node = {
|
|
@@ -2787,13 +3112,23 @@ function describe(element, excluded, doc, view, capture, handles, frame, provena
|
|
|
2787
3112
|
...provenance !== void 0 ? { provenance } : {},
|
|
2788
3113
|
...children.length > 0 ? { children } : {}
|
|
2789
3114
|
};
|
|
2790
|
-
const ref = handles?.issue(element, role, frame);
|
|
2791
3115
|
if (ref !== void 0) node.ref = ref;
|
|
2792
|
-
if (
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
3116
|
+
if (scrollable !== void 0) {
|
|
3117
|
+
node.ref = scrollRef;
|
|
3118
|
+
node.scrollable = scrollable;
|
|
3119
|
+
}
|
|
3120
|
+
if (isCapturableElement(element)) {
|
|
3121
|
+
const captureRef = node.ref ?? handles?.issueCapture(element, frame);
|
|
3122
|
+
if (captureRef !== void 0) {
|
|
3123
|
+
node.ref = captureRef;
|
|
3124
|
+
node.capturable = true;
|
|
3125
|
+
}
|
|
3126
|
+
if (capture !== void 0) if (isBelowMinArea(element, capture.minImageArea)) node.imageNote = ICON_SKIPPED_NOTE;
|
|
3127
|
+
else capture.targets.push({
|
|
3128
|
+
element,
|
|
3129
|
+
node
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
2797
3132
|
return node;
|
|
2798
3133
|
}
|
|
2799
3134
|
/**
|
|
@@ -2899,6 +3234,7 @@ var HandleIssuer = class {
|
|
|
2899
3234
|
if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
|
|
2900
3235
|
const kind = ACTIONABLE_ROLES.has(role) ? "action" : ANCHOR_ROLES.has(role) ? "anchor" : void 0;
|
|
2901
3236
|
if (kind === void 0) return void 0;
|
|
3237
|
+
if (kind === "action" && occupiesNoSpace(element)) return void 0;
|
|
2902
3238
|
const known = this.#refByElement.get(element);
|
|
2903
3239
|
if (known !== void 0 && this.#table.has(known)) {
|
|
2904
3240
|
this.#table.set(known, {
|
|
@@ -2908,6 +3244,34 @@ var HandleIssuer = class {
|
|
|
2908
3244
|
});
|
|
2909
3245
|
return known;
|
|
2910
3246
|
}
|
|
3247
|
+
return this.#mint(element, kind, frame);
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* 取像句柄(0.22.0 FR-12.1)。**不过操作范围那道闸**:取像是读,而该元素已经在
|
|
3251
|
+
* 感知输出里,读它的授权就是读页面的授权;写的授权另有 `capture_page_image` 的卡。
|
|
3252
|
+
*
|
|
3253
|
+
* 已有句柄就原样复用且**不改 kind**:一个被提升成按钮的 `<img>` 仍然点得动。
|
|
3254
|
+
*/
|
|
3255
|
+
issueCapture(element, frame) {
|
|
3256
|
+
if (this.#excluded.has(element)) return void 0;
|
|
3257
|
+
const known = this.#refByElement.get(element);
|
|
3258
|
+
if (known !== void 0 && this.#table.has(known)) return known;
|
|
3259
|
+
return this.#mint(element, "image", frame);
|
|
3260
|
+
}
|
|
3261
|
+
/**
|
|
3262
|
+
* 可滚容器的句柄(0.23.0 分册 22)。角色白名单之外——表格容器往往是个 `generic` div,
|
|
3263
|
+
* 但 `scroll` 确实作用在它身上。不发就成了「标了能滚、模型指认不了」,正是 AC-G8 禁的那种幽灵能力。
|
|
3264
|
+
*
|
|
3265
|
+
* 仍过操作范围与零尺寸两道闸(FR-22.12);已有句柄就原样复用且**不改 kind**。
|
|
3266
|
+
*/
|
|
3267
|
+
issueScrollable(element, frame) {
|
|
3268
|
+
if (!this.#actionable.has(element) || this.#excluded.has(element)) return void 0;
|
|
3269
|
+
if (occupiesNoSpace(element)) return void 0;
|
|
3270
|
+
const known = this.#refByElement.get(element);
|
|
3271
|
+
if (known !== void 0 && this.#table.has(known)) return known;
|
|
3272
|
+
return this.#mint(element, "action", frame);
|
|
3273
|
+
}
|
|
3274
|
+
#mint(element, kind, frame) {
|
|
2911
3275
|
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
2912
3276
|
crypto.getRandomValues(bytes);
|
|
2913
3277
|
const ref = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
@@ -3380,9 +3744,42 @@ function hidden(element) {
|
|
|
3380
3744
|
}
|
|
3381
3745
|
return false;
|
|
3382
3746
|
}
|
|
3747
|
+
/**
|
|
3748
|
+
* 「开着的浮层」里必须有可操作的东西。空的定位容器(装饰角标、测量层、定位锚点)
|
|
3749
|
+
* 满页都是,只看定位会把它们全当成浮层——实测该页面误伤 337/400(0.23.0 设计 16 §3.1)。
|
|
3750
|
+
*/
|
|
3751
|
+
const POPOVER_CONTENT_SELECTOR = "a[href],button,input,select,textarea,[role=\"button\"],[role=\"link\"],[role=\"option\"],[role=\"menuitem\"],[role=\"gridcell\"],[role=\"tab\"],[role=\"checkbox\"],[role=\"radio\"]";
|
|
3752
|
+
/**
|
|
3753
|
+
* 这个元素旁边是不是已经有一个开着的浮层(0.23.0 FR-15.1)。
|
|
3754
|
+
*
|
|
3755
|
+
* 零 DOM 变更有两种成因:元素真的不响应,和**页面已经处于这个动作要达成的状态**。
|
|
3756
|
+
* 后者按分册 12 的原判据会被报成 `noop`,模型于是放弃一条已经走通的路。
|
|
3757
|
+
*
|
|
3758
|
+
* 只看**父元素这一层**:自绘下拉的写法是触发器与浮层同属一个包装容器,
|
|
3759
|
+
* 往上多走一层就开始把无关区块算进来(误伤 3/400 → 31/400)。
|
|
3760
|
+
*
|
|
3761
|
+
* 用 `hidden()` 而不是尺寸:jsdom 没有布局,`getBoundingClientRect()` 恒为 0,
|
|
3762
|
+
* 依赖尺寸这条判据就没法被测试守住(设计 16 §3.2)。
|
|
3763
|
+
*/
|
|
3764
|
+
function popoverOpenBeside(element) {
|
|
3765
|
+
if (element.getAttribute("aria-expanded") === "true") return true;
|
|
3766
|
+
const parent = element.parentElement;
|
|
3767
|
+
if (parent === null) return false;
|
|
3768
|
+
const view = element.ownerDocument.defaultView;
|
|
3769
|
+
if (view === null) return false;
|
|
3770
|
+
for (const candidate of parent.querySelectorAll("*")) {
|
|
3771
|
+
if (candidate === element || element.contains(candidate) || candidate.contains(element)) continue;
|
|
3772
|
+
const position = view.getComputedStyle(candidate).position;
|
|
3773
|
+
if (position !== "absolute" && position !== "fixed") continue;
|
|
3774
|
+
if (hidden(candidate)) continue;
|
|
3775
|
+
if (candidate.querySelector(POPOVER_CONTENT_SELECTOR) === null) continue;
|
|
3776
|
+
return true;
|
|
3777
|
+
}
|
|
3778
|
+
return false;
|
|
3779
|
+
}
|
|
3383
3780
|
const inputType = (element) => (element.getAttribute("type") ?? "text").toLowerCase();
|
|
3384
3781
|
const isSecret = (element) => element.tagName === "INPUT" && SECRET_INPUT_TYPES.has(inputType(element));
|
|
3385
|
-
const textOf = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
3782
|
+
const textOf$2 = (element) => element?.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
3386
3783
|
/**
|
|
3387
3784
|
* 可访问名称,按 accname 的优先级取。
|
|
3388
3785
|
*
|
|
@@ -3394,19 +3791,19 @@ function accessibleName(element) {
|
|
|
3394
3791
|
const labelledBy = element.getAttribute("aria-labelledby");
|
|
3395
3792
|
if (labelledBy !== null && labelledBy.trim() !== "") {
|
|
3396
3793
|
const doc = element.ownerDocument;
|
|
3397
|
-
const joined = labelledBy.split(/\s+/).map((id) => textOf(doc.getElementById(id))).filter((part) => part !== "").join(" ");
|
|
3794
|
+
const joined = labelledBy.split(/\s+/).map((id) => textOf$2(doc.getElementById(id))).filter((part) => part !== "").join(" ");
|
|
3398
3795
|
if (joined !== "") return joined;
|
|
3399
3796
|
}
|
|
3400
3797
|
const label = element.getAttribute("aria-label");
|
|
3401
3798
|
if (label !== null && label.trim() !== "") return label.trim();
|
|
3402
3799
|
const labels = element.labels;
|
|
3403
3800
|
if (labels && labels.length > 0) {
|
|
3404
|
-
const joined = [...labels].map((node) => textOf(node)).filter((part) => part !== "").join(" ");
|
|
3801
|
+
const joined = [...labels].map((node) => textOf$2(node)).filter((part) => part !== "").join(" ");
|
|
3405
3802
|
if (joined !== "") return joined;
|
|
3406
3803
|
}
|
|
3407
|
-
const own = textOf(element);
|
|
3804
|
+
const own = textOf$2(element);
|
|
3408
3805
|
if (own !== "") return own;
|
|
3409
|
-
const wrapping = textOf(element.closest("label"));
|
|
3806
|
+
const wrapping = textOf$2(element.closest("label"));
|
|
3410
3807
|
if (wrapping !== "") return wrapping;
|
|
3411
3808
|
const placeholder = element.getAttribute("placeholder");
|
|
3412
3809
|
if (placeholder !== null && placeholder.trim() !== "") return placeholder.trim();
|
|
@@ -3519,6 +3916,187 @@ async function waitFor(probe, timeoutMs = 2e3) {
|
|
|
3519
3916
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
3520
3917
|
}
|
|
3521
3918
|
}
|
|
3919
|
+
/** 元素中心点的视口坐标,指针类事件都按它构造 */
|
|
3920
|
+
function centerOf(element) {
|
|
3921
|
+
const rect = element.getBoundingClientRect();
|
|
3922
|
+
return {
|
|
3923
|
+
clientX: rect.left + rect.width / 2,
|
|
3924
|
+
clientY: rect.top + rect.height / 2
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
/** 指针事件构造器;跨 realm 时要拿目标自己那个 window 上的,不能用全局的 */
|
|
3928
|
+
function pointerCtorOf(element) {
|
|
3929
|
+
return element.ownerDocument.defaultView?.PointerEvent;
|
|
3930
|
+
}
|
|
3931
|
+
/**
|
|
3932
|
+
* 一次「像人一样」的悬停(FR-17.2)。
|
|
3933
|
+
*
|
|
3934
|
+
* `enter` 那两个**不冒泡**,必须单独派发:只发 `over` 的话 React 的 `onMouseEnter`
|
|
3935
|
+
* 一个都收不到,而它正是组件库开浮层最常用的那个钩子。
|
|
3936
|
+
* 末尾补 `move` 是因为不少实现按「指针在里面动过」才认,静止的 over 被当成误触。
|
|
3937
|
+
*
|
|
3938
|
+
* ⚠️ 这套合成事件改变不了 CSS `:hover` 伪类——它由浏览器按**真实**指针位置判定。
|
|
3939
|
+
* 纯 CSS 开的浮层因此打不开,调用方要靠 `noop` 把这件事如实说给模型(FR-17.3)。
|
|
3940
|
+
*/
|
|
3941
|
+
function hoverOver(element) {
|
|
3942
|
+
const base = {
|
|
3943
|
+
bubbles: true,
|
|
3944
|
+
cancelable: true,
|
|
3945
|
+
composed: true,
|
|
3946
|
+
...centerOf(element)
|
|
3947
|
+
};
|
|
3948
|
+
const noBubble = {
|
|
3949
|
+
...base,
|
|
3950
|
+
bubbles: false
|
|
3951
|
+
};
|
|
3952
|
+
const pointer = {
|
|
3953
|
+
pointerId: 1,
|
|
3954
|
+
pointerType: "mouse",
|
|
3955
|
+
isPrimary: true
|
|
3956
|
+
};
|
|
3957
|
+
const Pointer = pointerCtorOf(element);
|
|
3958
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerover", {
|
|
3959
|
+
...base,
|
|
3960
|
+
...pointer
|
|
3961
|
+
}));
|
|
3962
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointerenter", {
|
|
3963
|
+
...noBubble,
|
|
3964
|
+
...pointer
|
|
3965
|
+
}));
|
|
3966
|
+
element.dispatchEvent(new MouseEvent("mouseover", base));
|
|
3967
|
+
element.dispatchEvent(new MouseEvent("mouseenter", noBubble));
|
|
3968
|
+
if (Pointer !== void 0) element.dispatchEvent(new Pointer("pointermove", {
|
|
3969
|
+
...base,
|
|
3970
|
+
...pointer
|
|
3971
|
+
}));
|
|
3972
|
+
element.dispatchEvent(new MouseEvent("mousemove", base));
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* 悬停之后页面有没有动(FR-17.3)。
|
|
3976
|
+
*
|
|
3977
|
+
* 用 `MutationObserver` 而不是比对快照:浮层可能挂到 `document.body` 末尾、
|
|
3978
|
+
* 也可能只是把某个节点的 `hidden` 去掉,逐一枚举会漏,而漏判的代价是向模型报假成功。
|
|
3979
|
+
* 观察器在派发**之前**挂上——晚一步就会错过同步执行的那批监听器。
|
|
3980
|
+
*/
|
|
3981
|
+
function watchMutations(element) {
|
|
3982
|
+
const doc = element.ownerDocument;
|
|
3983
|
+
const Observer = doc.defaultView?.MutationObserver;
|
|
3984
|
+
if (Observer === void 0) return {
|
|
3985
|
+
changed: () => true,
|
|
3986
|
+
stop: () => void 0
|
|
3987
|
+
};
|
|
3988
|
+
let seen = false;
|
|
3989
|
+
const observer = new Observer(() => {
|
|
3990
|
+
seen = true;
|
|
3991
|
+
});
|
|
3992
|
+
observer.observe(doc, {
|
|
3993
|
+
subtree: true,
|
|
3994
|
+
childList: true,
|
|
3995
|
+
attributes: true,
|
|
3996
|
+
characterData: true
|
|
3997
|
+
});
|
|
3998
|
+
return {
|
|
3999
|
+
changed: () => {
|
|
4000
|
+
if (observer.takeRecords().length > 0) seen = true;
|
|
4001
|
+
return seen;
|
|
4002
|
+
},
|
|
4003
|
+
stop: () => observer.disconnect()
|
|
4004
|
+
};
|
|
4005
|
+
}
|
|
4006
|
+
/**
|
|
4007
|
+
* 一次拖拽(FR-17.5)。真实页面有两套互不相通的机制,按起点自动选:
|
|
4008
|
+
*
|
|
4009
|
+
* - 起点带 `draggable` 且环境给得出 `DragEvent`/`DataTransfer` → HTML5 拖放;
|
|
4010
|
+
* - 否则 → 指针拖拽,`pointerdown` 起点、`pointermove` / `pointerup` 落终点坐标。
|
|
4011
|
+
*
|
|
4012
|
+
* 指针那一套的 `move` / `up` 必须冒泡:这类实现为了不被换位打断,
|
|
4013
|
+
* 监听是挂在 `document` 上的(把手一旦在 DOM 里搬走,指针捕获就断了),
|
|
4014
|
+
* 不冒泡的话一路只动得了第一格,甚至一格都不动。
|
|
4015
|
+
*
|
|
4016
|
+
* 构造器缺失时**降级**而不是抛:执行器的契约是一律给出结构化结果,
|
|
4017
|
+
* 抛出去会连带跳过策略层的审计,那比操作失败本身更糟(同 `attachFiles` 的理由)。
|
|
4018
|
+
*
|
|
4019
|
+
* @returns 失败原因;成功返回 `undefined`
|
|
4020
|
+
*/
|
|
4021
|
+
function dragBetween(from, to) {
|
|
4022
|
+
const view = from.ownerDocument.defaultView;
|
|
4023
|
+
const start = centerOf(from);
|
|
4024
|
+
const end = centerOf(to);
|
|
4025
|
+
const Drag = view?.DragEvent;
|
|
4026
|
+
const Transfer = view?.DataTransfer;
|
|
4027
|
+
if (from.closest("[draggable=\"true\"]") !== null && Drag !== void 0 && Transfer !== void 0) {
|
|
4028
|
+
const init = {
|
|
4029
|
+
bubbles: true,
|
|
4030
|
+
cancelable: true,
|
|
4031
|
+
composed: true,
|
|
4032
|
+
dataTransfer: new Transfer()
|
|
4033
|
+
};
|
|
4034
|
+
from.dispatchEvent(new Drag("dragstart", {
|
|
4035
|
+
...init,
|
|
4036
|
+
...start
|
|
4037
|
+
}));
|
|
4038
|
+
to.dispatchEvent(new Drag("dragenter", {
|
|
4039
|
+
...init,
|
|
4040
|
+
...end
|
|
4041
|
+
}));
|
|
4042
|
+
const accepted = !to.dispatchEvent(new Drag("dragover", {
|
|
4043
|
+
...init,
|
|
4044
|
+
...end
|
|
4045
|
+
}));
|
|
4046
|
+
if (accepted) to.dispatchEvent(new Drag("drop", {
|
|
4047
|
+
...init,
|
|
4048
|
+
...end
|
|
4049
|
+
}));
|
|
4050
|
+
from.dispatchEvent(new Drag("dragend", {
|
|
4051
|
+
...init,
|
|
4052
|
+
...end
|
|
4053
|
+
}));
|
|
4054
|
+
return accepted ? void 0 : "The destination does not accept dropped items.";
|
|
4055
|
+
}
|
|
4056
|
+
const Pointer = view?.PointerEvent;
|
|
4057
|
+
const mouse = {
|
|
4058
|
+
bubbles: true,
|
|
4059
|
+
cancelable: true,
|
|
4060
|
+
composed: true,
|
|
4061
|
+
button: 0
|
|
4062
|
+
};
|
|
4063
|
+
const pointer = {
|
|
4064
|
+
...mouse,
|
|
4065
|
+
pointerId: 1,
|
|
4066
|
+
pointerType: "mouse",
|
|
4067
|
+
isPrimary: true
|
|
4068
|
+
};
|
|
4069
|
+
if (Pointer !== void 0) from.dispatchEvent(new Pointer("pointerdown", {
|
|
4070
|
+
...pointer,
|
|
4071
|
+
...start,
|
|
4072
|
+
buttons: 1
|
|
4073
|
+
}));
|
|
4074
|
+
from.dispatchEvent(new MouseEvent("mousedown", {
|
|
4075
|
+
...mouse,
|
|
4076
|
+
...start,
|
|
4077
|
+
buttons: 1
|
|
4078
|
+
}));
|
|
4079
|
+
if (Pointer !== void 0) to.dispatchEvent(new Pointer("pointermove", {
|
|
4080
|
+
...pointer,
|
|
4081
|
+
...end,
|
|
4082
|
+
buttons: 1
|
|
4083
|
+
}));
|
|
4084
|
+
to.dispatchEvent(new MouseEvent("mousemove", {
|
|
4085
|
+
...mouse,
|
|
4086
|
+
...end,
|
|
4087
|
+
buttons: 1
|
|
4088
|
+
}));
|
|
4089
|
+
if (Pointer !== void 0) to.dispatchEvent(new Pointer("pointerup", {
|
|
4090
|
+
...pointer,
|
|
4091
|
+
...end,
|
|
4092
|
+
buttons: 0
|
|
4093
|
+
}));
|
|
4094
|
+
to.dispatchEvent(new MouseEvent("mouseup", {
|
|
4095
|
+
...mouse,
|
|
4096
|
+
...end,
|
|
4097
|
+
buttons: 0
|
|
4098
|
+
}));
|
|
4099
|
+
}
|
|
3522
4100
|
/**
|
|
3523
4101
|
* 一个动作覆盖三种选择控件(FR-25.2):原生 select 直接设值,
|
|
3524
4102
|
* ARIA 组合与日期格点走「点开 → 等浮层 → 按可访问名匹配 → 点中」。
|
|
@@ -3615,6 +4193,12 @@ function attachFiles(input, files) {
|
|
|
3615
4193
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3616
4194
|
}
|
|
3617
4195
|
const scrolls = (overflow) => overflow === "auto" || overflow === "scroll" || overflow === "overlay";
|
|
4196
|
+
const axisOf = (direction) => direction === "left" || direction === "right" ? "x" : "y";
|
|
4197
|
+
/** 该元素自身在这条轴上能不能滚。两个条件的合取,见 `scrollTargetOf` 的注释 */
|
|
4198
|
+
function scrollsOn(node, axis, view) {
|
|
4199
|
+
const style = view.getComputedStyle(node);
|
|
4200
|
+
return axis === "y" ? scrolls(style.overflowY) && node.scrollHeight > node.clientHeight : scrolls(style.overflowX) && node.scrollWidth > node.clientWidth;
|
|
4201
|
+
}
|
|
3618
4202
|
/**
|
|
3619
4203
|
* 滚动落点(FR-10.3):从被指认元素**自身**起沿祖先链上溯,取第一个真正能滚的容器。
|
|
3620
4204
|
*
|
|
@@ -3628,20 +4212,21 @@ const scrolls = (overflow) => overflow === "auto" || overflow === "scroll" || ov
|
|
|
3628
4212
|
* 根滚动容器单独兜底而不是靠链上命中:`documentElement` 的 `overflow-y` 计算值
|
|
3629
4213
|
* 通常是 `visible`(滚动被传播到视口),不会被上面的判据命中,但文档确实可滚。
|
|
3630
4214
|
* 顺序必须是先链后兜底——嵌套场景下先命中的必须是内层列表。
|
|
4215
|
+
*
|
|
4216
|
+
* **按轴判(0.23.0 分册 22 FR-22.7)**:同一个容器常常只在一条轴上溢出,
|
|
4217
|
+
* 拿纵向判据去找横向落点会整体判成不可滚,那正是横向滚动此前失败的原因。
|
|
3631
4218
|
*/
|
|
3632
|
-
function scrollTargetOf(element) {
|
|
4219
|
+
function scrollTargetOf(element, axis) {
|
|
3633
4220
|
const doc = element.ownerDocument;
|
|
3634
4221
|
const view = doc.defaultView;
|
|
3635
4222
|
if (view === null) return void 0;
|
|
3636
|
-
for (let node = element; node !== null; node = node.parentElement) {
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
isRoot: false
|
|
3641
|
-
};
|
|
3642
|
-
}
|
|
4223
|
+
for (let node = element; node !== null; node = node.parentElement) if (scrollsOn(node, axis, view)) return {
|
|
4224
|
+
container: node,
|
|
4225
|
+
isRoot: false
|
|
4226
|
+
};
|
|
3643
4227
|
const root = doc.scrollingElement ?? doc.documentElement;
|
|
3644
|
-
|
|
4228
|
+
const rootOverflows = root !== null && (axis === "y" ? root.scrollHeight > root.clientHeight : root.scrollWidth > root.clientWidth);
|
|
4229
|
+
if (root !== null && rootOverflows) return {
|
|
3645
4230
|
container: root,
|
|
3646
4231
|
isRoot: true
|
|
3647
4232
|
};
|
|
@@ -3746,11 +4331,12 @@ function createDomPageActionExecutor(options) {
|
|
|
3746
4331
|
* 稳定化是滚动自己的收尾,见 `watchQuiescence`。
|
|
3747
4332
|
*/
|
|
3748
4333
|
const scrollRegion = async (element, target, direction) => {
|
|
3749
|
-
const
|
|
4334
|
+
const axis = axisOf(direction);
|
|
4335
|
+
const found = scrollTargetOf(element, axis);
|
|
3750
4336
|
if (found === void 0) return {
|
|
3751
4337
|
ok: false,
|
|
3752
4338
|
target,
|
|
3753
|
-
reason:
|
|
4339
|
+
reason: `This area cannot be scrolled ${direction}: nothing around this element scrolls ${axis === "y" ? "vertically" : "horizontally"}.`
|
|
3754
4340
|
};
|
|
3755
4341
|
const { container, isRoot } = found;
|
|
3756
4342
|
if (!isRoot && !reader.inActionScope(container)) return {
|
|
@@ -3758,17 +4344,22 @@ function createDomPageActionExecutor(options) {
|
|
|
3758
4344
|
target,
|
|
3759
4345
|
reason: "The scrollable region around this element is outside the actionable scope."
|
|
3760
4346
|
};
|
|
3761
|
-
const
|
|
3762
|
-
const
|
|
4347
|
+
const extent = axis === "y" ? container.clientHeight : container.clientWidth;
|
|
4348
|
+
const step = Math.max(extent - SCROLL_OVERLAP_PX, Math.round(extent / 2), 1);
|
|
4349
|
+
const forward = direction === "down" || direction === "right";
|
|
4350
|
+
const before = axis === "y" ? container.scrollTop : container.scrollLeft;
|
|
3763
4351
|
const watch = watchQuiescence(container);
|
|
3764
|
-
|
|
4352
|
+
const next = forward ? before + step : before - step;
|
|
4353
|
+
if (axis === "y") container.scrollTop = next;
|
|
4354
|
+
else container.scrollLeft = next;
|
|
3765
4355
|
await watch.settle(clampScrollSettleMs(options.scrollSettleMs?.()));
|
|
3766
|
-
const after = container.scrollTop;
|
|
4356
|
+
const after = axis === "y" ? container.scrollTop : container.scrollLeft;
|
|
4357
|
+
const limit = axis === "y" ? container.scrollHeight - container.clientHeight - 1 : container.scrollWidth - container.clientWidth - 1;
|
|
3767
4358
|
return {
|
|
3768
4359
|
ok: true,
|
|
3769
4360
|
target,
|
|
3770
4361
|
scrolled: {
|
|
3771
|
-
atEnd:
|
|
4362
|
+
atEnd: forward ? after >= limit : after <= 0,
|
|
3772
4363
|
movedBy: Math.abs(after - before)
|
|
3773
4364
|
}
|
|
3774
4365
|
};
|
|
@@ -3788,9 +4379,11 @@ function createDomPageActionExecutor(options) {
|
|
|
3788
4379
|
reason
|
|
3789
4380
|
});
|
|
3790
4381
|
if (reader.handleKindOf(request.ref) === "anchor") return fail("That reference points at a container, which cannot be acted on. Use it with perceive_page to read inside it, then act on an element that carries its own reference. If nothing inside it has one, this part of the page offers no action and retrying here will not help.");
|
|
4382
|
+
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
4383
|
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
4384
|
if (reader.modalStateOf(element) !== "elevated" && !reader.inActionScope(element)) return fail("The element is no longer inside the actionable scope.");
|
|
3793
4385
|
if (hidden(element)) return fail("The element is not visible.");
|
|
4386
|
+
if (occupiesNoSpace(element)) return fail("The element takes up no space on screen, so it cannot be clicked or pointed at.");
|
|
3794
4387
|
if (element.hasAttribute("disabled")) return fail("The element is disabled.");
|
|
3795
4388
|
const modalsBefore = reader.modalSnapshot();
|
|
3796
4389
|
const frameView = element.ownerDocument.defaultView;
|
|
@@ -3817,15 +4410,22 @@ function createDomPageActionExecutor(options) {
|
|
|
3817
4410
|
};
|
|
3818
4411
|
};
|
|
3819
4412
|
if (request.action === "click") {
|
|
4413
|
+
const watch = watchMutations(element);
|
|
3820
4414
|
press(element);
|
|
3821
|
-
|
|
4415
|
+
const outcome = await done({
|
|
3822
4416
|
ok: true,
|
|
3823
4417
|
target
|
|
3824
4418
|
});
|
|
4419
|
+
const changed = watch.changed();
|
|
4420
|
+
watch.stop();
|
|
4421
|
+
return changed || outcome.navigated === true || outcome.elevatedModal !== void 0 || popoverOpenBeside(element) ? outcome : {
|
|
4422
|
+
...outcome,
|
|
4423
|
+
noop: true
|
|
4424
|
+
};
|
|
3825
4425
|
}
|
|
3826
4426
|
if (request.action === "fill") {
|
|
3827
4427
|
if (!FILLABLE_TAGS.has(element.tagName)) return fail("The element is not a text control.");
|
|
3828
|
-
if (element.hasAttribute("readonly")) return fail("The element is read-only.");
|
|
4428
|
+
if (element.hasAttribute("readonly")) return fail("The element is read-only. Read-only text fields often open a picker when clicked, so try the click action on this same element.");
|
|
3829
4429
|
fillValue(element, request.value ?? "");
|
|
3830
4430
|
return await done({
|
|
3831
4431
|
ok: true,
|
|
@@ -3844,9 +4444,36 @@ function createDomPageActionExecutor(options) {
|
|
|
3844
4444
|
}
|
|
3845
4445
|
if (request.action === "set") return await done(setToggle(element, request.value ?? "", target));
|
|
3846
4446
|
if (request.action === "scroll") {
|
|
3847
|
-
if (
|
|
4447
|
+
if (!SCROLL_DIRECTIONS.includes(request.value)) return fail(`Use ${SCROLL_DIRECTIONS.map((d) => `"${d}"`).join(" / ")}.`);
|
|
3848
4448
|
return await scrollRegion(element, target, request.value);
|
|
3849
4449
|
}
|
|
4450
|
+
if (request.action === "hover") {
|
|
4451
|
+
const watch = watchMutations(element);
|
|
4452
|
+
hoverOver(element);
|
|
4453
|
+
await waitFor(() => watch.changed() ? true : void 0, 500);
|
|
4454
|
+
const changed = watch.changed();
|
|
4455
|
+
watch.stop();
|
|
4456
|
+
return await done({
|
|
4457
|
+
ok: true,
|
|
4458
|
+
target,
|
|
4459
|
+
...changed || popoverOpenBeside(element) ? {} : { noop: true }
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
if (request.action === "drag") {
|
|
4463
|
+
const destination = request.value ?? "";
|
|
4464
|
+
if (destination === "") return fail("The drag action needs a value holding the destination ref.");
|
|
4465
|
+
const dropTarget = reader.resolve(destination);
|
|
4466
|
+
if (dropTarget === void 0) return fail("The destination reference is unknown or expired.");
|
|
4467
|
+
if (reader.handleKindOf(destination) === "image") return fail("The destination reference points at an image, which cannot receive a drop.");
|
|
4468
|
+
if (reader.modalStateOf(dropTarget) !== "elevated" && !reader.inActionScope(dropTarget)) return fail("The destination is no longer inside the actionable scope.");
|
|
4469
|
+
if (hidden(dropTarget)) return fail("The destination is not visible.");
|
|
4470
|
+
const refused = dragBetween(element, dropTarget);
|
|
4471
|
+
if (refused !== void 0) return fail(refused);
|
|
4472
|
+
return await done({
|
|
4473
|
+
ok: true,
|
|
4474
|
+
target
|
|
4475
|
+
});
|
|
4476
|
+
}
|
|
3850
4477
|
if (request.action === "attach") {
|
|
3851
4478
|
const files = await options.pickFiles?.();
|
|
3852
4479
|
if (files === void 0 || files.length === 0) return {
|
|
@@ -4034,21 +4661,76 @@ async function roundTrip(transport, request, expect, code, timeoutMs) {
|
|
|
4034
4661
|
*/
|
|
4035
4662
|
function createRemotePerceptionReader(options) {
|
|
4036
4663
|
const { transport, targets, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS, onDocument } = options;
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4664
|
+
/**
|
|
4665
|
+
* 上一批写进分片的条目(0.23.0 分册 13 / FR-13.4)。
|
|
4666
|
+
*
|
|
4667
|
+
* `registry.replace` 是**整体替换分片**,而子树读取只回传子树内的条目——
|
|
4668
|
+
* 照搬就等于「读完日期面板,页面别处全部失去句柄」。本地形态没有这个问题:
|
|
4669
|
+
* `HandleIssuer` 的表跨感知保留,子树读取只是少发几个 ref,既有的一个都不丢。
|
|
4670
|
+
* 差值由这里补出来,而不是转嫁给模型。
|
|
4671
|
+
*
|
|
4672
|
+
* 状态留在发起方而不是给 `RemoteTargetRegistry` 加 `merge()`:
|
|
4673
|
+
* 后者对所有实现方都是破坏性变更,需求 §2.2 把它列为禁止项。
|
|
4674
|
+
*/
|
|
4675
|
+
let lastShard;
|
|
4676
|
+
const commit = (entries, document, merge) => {
|
|
4677
|
+
const sameDocument = lastShard !== void 0 && lastShard.key === document?.key;
|
|
4678
|
+
let next = entries;
|
|
4679
|
+
if (merge && sameDocument && lastShard !== void 0) {
|
|
4680
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
4681
|
+
for (const entry of lastShard.entries) byRef.set(entry.ref, entry);
|
|
4682
|
+
for (const entry of entries) byRef.set(entry.ref, entry);
|
|
4683
|
+
next = [...byRef.values()];
|
|
4684
|
+
}
|
|
4685
|
+
targets.replace(next, document);
|
|
4686
|
+
lastShard = {
|
|
4687
|
+
key: document?.key,
|
|
4688
|
+
entries: next
|
|
4689
|
+
};
|
|
4690
|
+
if (document !== void 0) onDocument?.(document);
|
|
4691
|
+
};
|
|
4692
|
+
return {
|
|
4693
|
+
async read(scope, capture) {
|
|
4694
|
+
const reply = await roundTrip(transport, {
|
|
4695
|
+
type: "perceive",
|
|
4696
|
+
scope,
|
|
4697
|
+
...capture !== void 0 ? { capture } : {}
|
|
4698
|
+
}, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
|
|
4699
|
+
commit(reply.targets, reply.document, false);
|
|
4700
|
+
return reply.result;
|
|
4701
|
+
},
|
|
4702
|
+
/**
|
|
4703
|
+
* 子树续读(0.23.0 分册 13 / FR-13.1)。
|
|
4704
|
+
*
|
|
4705
|
+
* 本地形态早就有这个方法,缺的只是这一段跨上下文接线;不实现它,
|
|
4706
|
+
* policy 会直接抛 `PERCEPTION_SUBTREE_UNSUPPORTED`,模型在浏览器扩展这类
|
|
4707
|
+
* 宿主里就永远没有「第一段没找到 → 展开某个子树再看」这条自救路径。
|
|
4708
|
+
*/
|
|
4709
|
+
async readSubtree(scope, ref, capture) {
|
|
4710
|
+
let reply;
|
|
4711
|
+
try {
|
|
4712
|
+
reply = await roundTrip(transport, {
|
|
4713
|
+
type: "perceive",
|
|
4714
|
+
scope,
|
|
4715
|
+
ref,
|
|
4716
|
+
...capture !== void 0 ? { capture } : {}
|
|
4717
|
+
}, "perceive-result", "PERCEPTION_FAILED", timeoutMs);
|
|
4718
|
+
} catch (e) {
|
|
4719
|
+
if (e instanceof WebSkillError) {
|
|
4720
|
+
if (e.code === "PERCEPTION_SUBTREE_OUT_OF_SCOPE") return { rejected: "out-of-scope" };
|
|
4721
|
+
if (e.code === "PERCEPTION_SUBTREE_UNKNOWN_REF") return { rejected: "unknown-ref" };
|
|
4722
|
+
}
|
|
4723
|
+
throw e;
|
|
4724
|
+
}
|
|
4725
|
+
commit(reply.targets, reply.document, true);
|
|
4726
|
+
return reply.result;
|
|
4727
|
+
}
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4731
|
+
//#endregion
|
|
4732
|
+
//#region ../browser/src/remote/executor.ts
|
|
4733
|
+
/**
|
|
4052
4734
|
* 跨上下文的页面操作执行器(分册 13 / FR-13.3)。
|
|
4053
4735
|
* @experimental
|
|
4054
4736
|
*/
|
|
@@ -4080,6 +4762,27 @@ function createRemotePageActionExecutor(options) {
|
|
|
4080
4762
|
};
|
|
4081
4763
|
}
|
|
4082
4764
|
|
|
4765
|
+
//#endregion
|
|
4766
|
+
//#region ../browser/src/remote/pageImage.ts
|
|
4767
|
+
/**
|
|
4768
|
+
* 跨上下文的取像端口(分册 12 §2)。
|
|
4769
|
+
*
|
|
4770
|
+
* 与 `createRemotePageActionExecutor` 同一条装配规矩:句柄表在页面那一侧,
|
|
4771
|
+
* 这边只负责把一次请求送过去。**不与感知期的取像共用任何状态**——
|
|
4772
|
+
* 那条流是为了让模型看懂,这条流是为了落产物,两者不得互相触发。
|
|
4773
|
+
* @experimental
|
|
4774
|
+
*/
|
|
4775
|
+
function createRemotePageImageHost(options) {
|
|
4776
|
+
const { transport, timeoutMs = DEFAULT_PAGE_AGENT_TIMEOUT_MS } = options;
|
|
4777
|
+
return { async capture(ref, maxBytes) {
|
|
4778
|
+
return (await roundTrip(transport, {
|
|
4779
|
+
type: "capture-image",
|
|
4780
|
+
ref,
|
|
4781
|
+
maxBytes
|
|
4782
|
+
}, "capture-image-result", "TOOL_EXECUTION_FAILED", timeoutMs)).result;
|
|
4783
|
+
} };
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4083
4786
|
//#endregion
|
|
4084
4787
|
//#region ../browser/src/remote/handler.ts
|
|
4085
4788
|
/** 深度收集句柄;顺序即遍历序,便于与本地形态逐条比对 */
|
|
@@ -4091,6 +4794,24 @@ function collectRefs(nodes, out = []) {
|
|
|
4091
4794
|
return out;
|
|
4092
4795
|
}
|
|
4093
4796
|
/**
|
|
4797
|
+
* 子树被拒 → 既有的 `error` 应答变体(0.23.0 分册 13 / FR-13.5)。
|
|
4798
|
+
* 不新增应答形态,是把本册的契约变更压到「一个可选请求字段」的关键。
|
|
4799
|
+
*
|
|
4800
|
+
* 这两个错误码本来就在 `WebSkillErrorCode` 里,远程 reader 靠它们还原出
|
|
4801
|
+
* `{ rejected }`,policy 那边的 `#recordDenied` 才能照常留痕。
|
|
4802
|
+
*/
|
|
4803
|
+
function rejectionReply(rejected) {
|
|
4804
|
+
return rejected === "out-of-scope" ? {
|
|
4805
|
+
type: "error",
|
|
4806
|
+
code: "PERCEPTION_SUBTREE_OUT_OF_SCOPE",
|
|
4807
|
+
message: "that reference is outside the perception scope, so its subtree cannot be read"
|
|
4808
|
+
} : {
|
|
4809
|
+
type: "error",
|
|
4810
|
+
code: "PERCEPTION_SUBTREE_UNKNOWN_REF",
|
|
4811
|
+
message: "that reference is not on this page any more; perceive the page again to get a fresh reference"
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4094
4815
|
* 页面侧处理器(分册 13 / FR-13.7)。
|
|
4095
4816
|
*
|
|
4096
4817
|
* 必须是工厂而不是裸函数:`DomPerceptionReader` **是有状态的**——
|
|
@@ -4134,8 +4855,15 @@ function createPageAgentHandler(options = {}) {
|
|
|
4134
4855
|
return { async handle(request) {
|
|
4135
4856
|
try {
|
|
4136
4857
|
if (request.type === "perceive") {
|
|
4137
|
-
|
|
4138
|
-
|
|
4858
|
+
let result;
|
|
4859
|
+
if (request.ref !== void 0) {
|
|
4860
|
+
const out = request.capture === void 0 ? await reader.readSubtree(request.scope, request.ref) : await reader.readSubtree(request.scope, request.ref, request.capture);
|
|
4861
|
+
if ("rejected" in out) return rejectionReply(out.rejected);
|
|
4862
|
+
result = out;
|
|
4863
|
+
} else {
|
|
4864
|
+
const raw = request.capture === void 0 ? reader.read(request.scope) : await reader.read(request.scope, request.capture);
|
|
4865
|
+
result = Array.isArray(raw) ? { nodes: raw } : raw;
|
|
4866
|
+
}
|
|
4139
4867
|
const targets = [];
|
|
4140
4868
|
for (const ref of collectRefs(result.nodes)) {
|
|
4141
4869
|
const target = executor.describe(ref);
|
|
@@ -4152,6 +4880,24 @@ function createPageAgentHandler(options = {}) {
|
|
|
4152
4880
|
...identity !== void 0 ? { document: identity } : {}
|
|
4153
4881
|
};
|
|
4154
4882
|
}
|
|
4883
|
+
if (request.type === "capture-image") {
|
|
4884
|
+
const element = reader.resolve(request.ref);
|
|
4885
|
+
if (element === void 0) return {
|
|
4886
|
+
type: "capture-image-result",
|
|
4887
|
+
result: {
|
|
4888
|
+
id: request.ref,
|
|
4889
|
+
reason: "that reference is not on this page any more; perceive the page again to get a fresh reference",
|
|
4890
|
+
triedLevels: []
|
|
4891
|
+
}
|
|
4892
|
+
};
|
|
4893
|
+
return {
|
|
4894
|
+
type: "capture-image-result",
|
|
4895
|
+
result: await captureDocumentImage(element, {
|
|
4896
|
+
id: request.ref,
|
|
4897
|
+
maxBytes: request.maxBytes
|
|
4898
|
+
})
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4155
4901
|
return {
|
|
4156
4902
|
type: "execute-result",
|
|
4157
4903
|
outcome: await executor.execute(request.request)
|
|
@@ -4357,9 +5103,14 @@ function createFrameRouter(options) {
|
|
|
4357
5103
|
const address = refFrames.get(request.request.ref) ?? await options.dispatcher.mainFrame();
|
|
4358
5104
|
return (await options.dispatcher.send(address, request)).reply;
|
|
4359
5105
|
}
|
|
5106
|
+
async function captureImage(request) {
|
|
5107
|
+
const address = refFrames.get(request.ref) ?? await options.dispatcher.mainFrame();
|
|
5108
|
+
return (await options.dispatcher.send(address, request)).reply;
|
|
5109
|
+
}
|
|
4360
5110
|
return {
|
|
4361
5111
|
perceive,
|
|
4362
|
-
execute
|
|
5112
|
+
execute,
|
|
5113
|
+
captureImage
|
|
4363
5114
|
};
|
|
4364
5115
|
}
|
|
4365
5116
|
|
|
@@ -5413,8 +6164,8 @@ function fail(message) {
|
|
|
5413
6164
|
throw new WebSkillError("TOOL_EXECUTION_FAILED", message);
|
|
5414
6165
|
}
|
|
5415
6166
|
/** 按 localName 取子孙元素,忽略命名空间前缀:不同生成器的前缀写法并不统一 */
|
|
5416
|
-
const byTag$
|
|
5417
|
-
function parseXml$
|
|
6167
|
+
const byTag$3 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
|
|
6168
|
+
function parseXml$3(bytes, what) {
|
|
5418
6169
|
const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
|
|
5419
6170
|
if (doc.getElementsByTagName("parsererror").length > 0) fail(`The workbook ${what} could not be parsed as XML.`);
|
|
5420
6171
|
return doc;
|
|
@@ -5461,14 +6212,14 @@ function isDateFormatCode(code) {
|
|
|
5461
6212
|
/** 单元格样式索引 → 是否日期格式 */
|
|
5462
6213
|
function readDateStyles(bytes) {
|
|
5463
6214
|
if (bytes === void 0) return [];
|
|
5464
|
-
const doc = parseXml$
|
|
6215
|
+
const doc = parseXml$3(bytes, "styles");
|
|
5465
6216
|
const custom = /* @__PURE__ */ new Map();
|
|
5466
|
-
for (const fmt of byTag$
|
|
6217
|
+
for (const fmt of byTag$3(doc, "numFmt")) {
|
|
5467
6218
|
const id = Number(fmt.getAttribute("numFmtId"));
|
|
5468
6219
|
const code = fmt.getAttribute("formatCode");
|
|
5469
6220
|
if (Number.isFinite(id) && code !== null) custom.set(id, code);
|
|
5470
6221
|
}
|
|
5471
|
-
const cellXfs = byTag$
|
|
6222
|
+
const cellXfs = byTag$3(doc, "cellXfs")[0];
|
|
5472
6223
|
if (cellXfs === void 0) return [];
|
|
5473
6224
|
return [...cellXfs.children].filter((xf) => xf.localName === "xf").map((xf) => {
|
|
5474
6225
|
const id = Number(xf.getAttribute("numFmtId") ?? "0");
|
|
@@ -5506,7 +6257,7 @@ function serialToIso(serial, date1904) {
|
|
|
5506
6257
|
return `${iso}T${pad(h)}:${pad(m)}:${pad(s)}`;
|
|
5507
6258
|
}
|
|
5508
6259
|
/** 一个 `<c>` 的呈现文本(FR-12.3) */
|
|
5509
|
-
function cellText(cell, shared, dateStyles, date1904) {
|
|
6260
|
+
function cellText$1(cell, shared, dateStyles, date1904) {
|
|
5510
6261
|
const type = cell.getAttribute("t");
|
|
5511
6262
|
if (type === "inlineStr") {
|
|
5512
6263
|
const is = [...cell.children].find((child) => child.localName === "is");
|
|
@@ -5533,7 +6284,7 @@ function cellText(cell, shared, dateStyles, date1904) {
|
|
|
5533
6284
|
function sheetLines(doc, shared, dateStyles, date1904) {
|
|
5534
6285
|
const lines = [];
|
|
5535
6286
|
let previousRow = 0;
|
|
5536
|
-
for (const row of byTag$
|
|
6287
|
+
for (const row of byTag$3(doc, "row")) {
|
|
5537
6288
|
const declared = Number(row.getAttribute("r"));
|
|
5538
6289
|
const rowNumber = Number.isInteger(declared) && declared > 0 ? declared : previousRow + 1;
|
|
5539
6290
|
for (let gap = previousRow + 1; gap < rowNumber; gap++) lines.push("");
|
|
@@ -5544,7 +6295,7 @@ function sheetLines(doc, shared, dateStyles, date1904) {
|
|
|
5544
6295
|
if (cell.localName !== "c") continue;
|
|
5545
6296
|
const column = columnIndex(cell.getAttribute("r")) ?? nextColumn;
|
|
5546
6297
|
for (let gap = cells.length; gap < column; gap++) cells.push("");
|
|
5547
|
-
const text = cellText(cell, shared, dateStyles, date1904);
|
|
6298
|
+
const text = cellText$1(cell, shared, dateStyles, date1904);
|
|
5548
6299
|
if (cells.length > column) cells[column] = text;
|
|
5549
6300
|
else cells.push(text);
|
|
5550
6301
|
nextColumn = column + 1;
|
|
@@ -5576,21 +6327,21 @@ async function readXlsxWorkbook(bytes) {
|
|
|
5576
6327
|
const parts = new Map(entries);
|
|
5577
6328
|
const workbookBytes = parts.get(WORKBOOK);
|
|
5578
6329
|
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$
|
|
6330
|
+
const workbook = parseXml$3(workbookBytes, "index");
|
|
6331
|
+
const workbookPr = byTag$3(workbook, "workbookPr")[0];
|
|
5581
6332
|
const date1904 = workbookPr?.getAttribute("date1904") === "1" || workbookPr?.getAttribute("date1904") === "true";
|
|
5582
6333
|
const relsBytes = parts.get(WORKBOOK_RELS);
|
|
5583
6334
|
const targets = /* @__PURE__ */ new Map();
|
|
5584
|
-
if (relsBytes !== void 0) for (const rel of byTag$
|
|
6335
|
+
if (relsBytes !== void 0) for (const rel of byTag$3(parseXml$3(relsBytes, "relationships"), "Relationship")) {
|
|
5585
6336
|
const id = rel.getAttribute("Id");
|
|
5586
6337
|
const target = rel.getAttribute("Target");
|
|
5587
6338
|
if (id !== null && target !== null) targets.set(id, resolveSheetPath(target));
|
|
5588
6339
|
}
|
|
5589
6340
|
const sharedBytes = parts.get(SHARED_STRINGS);
|
|
5590
|
-
const shared = sharedBytes === void 0 ? [] : byTag$
|
|
6341
|
+
const shared = sharedBytes === void 0 ? [] : byTag$3(parseXml$3(sharedBytes, "shared strings"), "si").map(sharedStringText);
|
|
5591
6342
|
const dateStyles = readDateStyles(parts.get(STYLES));
|
|
5592
6343
|
const sheets = [];
|
|
5593
|
-
for (const [ordinal, sheet] of byTag$
|
|
6344
|
+
for (const [ordinal, sheet] of byTag$3(workbook, "sheet").entries()) {
|
|
5594
6345
|
const name = sheet.getAttribute("name") ?? `Sheet${ordinal + 1}`;
|
|
5595
6346
|
const relId = sheet.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? sheet.getAttribute("r:id");
|
|
5596
6347
|
const path = relId !== null ? targets.get(relId) : void 0;
|
|
@@ -5599,7 +6350,7 @@ async function readXlsxWorkbook(bytes) {
|
|
|
5599
6350
|
sheets.push({
|
|
5600
6351
|
name,
|
|
5601
6352
|
path,
|
|
5602
|
-
lines: sheetLines(parseXml$
|
|
6353
|
+
lines: sheetLines(parseXml$3(sheetBytes, `worksheet "${name}"`), shared, dateStyles, date1904)
|
|
5603
6354
|
});
|
|
5604
6355
|
}
|
|
5605
6356
|
return {
|
|
@@ -5660,7 +6411,7 @@ function imageBlockOf(path, data) {
|
|
|
5660
6411
|
*
|
|
5661
6412
|
* 零新增依赖,沿用同一条路:zip 走 `unzipWithLimits`,XML 走 `DOMParser`。
|
|
5662
6413
|
*/
|
|
5663
|
-
const DOCUMENT = "word/document.xml";
|
|
6414
|
+
const DOCUMENT$1 = "word/document.xml";
|
|
5664
6415
|
const RELS = "word/_rels/document.xml.rels";
|
|
5665
6416
|
/** 图宽占正文宽度的门槛:低于它的是签名章、页眉 logo、装饰线(FR-22.9) @experimental */
|
|
5666
6417
|
const DOCX_IMAGE_WIDTH_RATIO = .3;
|
|
@@ -5671,7 +6422,7 @@ const DEFAULT_BODY_TWIPS = 9360;
|
|
|
5671
6422
|
function attr(node, name) {
|
|
5672
6423
|
for (const item of node.attributes) if (item.name === name || item.localName === name) return item.value;
|
|
5673
6424
|
}
|
|
5674
|
-
function parseXml$
|
|
6425
|
+
function parseXml$2(bytes, what) {
|
|
5675
6426
|
const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
|
|
5676
6427
|
if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The Word document's ${what} could not be parsed as XML.`);
|
|
5677
6428
|
return doc;
|
|
@@ -5732,12 +6483,12 @@ function createDocxBlockReader() {
|
|
|
5732
6483
|
async function readDocxBlocks(bytes) {
|
|
5733
6484
|
const entries = await unzipWithLimits(bytes);
|
|
5734
6485
|
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$
|
|
6486
|
+
const main = files.get(DOCUMENT$1);
|
|
6487
|
+
if (main === void 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a Word document: it has no ${DOCUMENT$1} entry.`);
|
|
6488
|
+
const doc = parseXml$2(main, "body");
|
|
5738
6489
|
const targets = /* @__PURE__ */ new Map();
|
|
5739
6490
|
const rels = files.get(RELS);
|
|
5740
|
-
if (rels !== void 0) for (const rel of parseXml$
|
|
6491
|
+
if (rels !== void 0) for (const rel of parseXml$2(rels, "relationships").getElementsByTagName("Relationship")) {
|
|
5741
6492
|
const id = attr(rel, "Id");
|
|
5742
6493
|
const target = attr(rel, "Target");
|
|
5743
6494
|
if (id !== void 0 && target !== void 0) targets.set(id, `word/${target.replace(/^\.?\//, "")}`);
|
|
@@ -5779,13 +6530,13 @@ const XLSX_IMAGE_MIN_COLUMNS = 2;
|
|
|
5779
6530
|
/** @experimental */
|
|
5780
6531
|
const XLSX_IMAGE_MIN_ROWS = 3;
|
|
5781
6532
|
const RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
5782
|
-
const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
|
|
5783
|
-
function parseXml(bytes) {
|
|
6533
|
+
const byTag$2 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
|
|
6534
|
+
function parseXml$1(bytes) {
|
|
5784
6535
|
const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
|
|
5785
6536
|
return doc.getElementsByTagName("parsererror").length > 0 ? void 0 : doc;
|
|
5786
6537
|
}
|
|
5787
6538
|
/** rels 的 Target 可能是 `../media/x.png` 这种相对路径,得按所在目录归一化 */
|
|
5788
|
-
function resolveRelative(baseDir, target) {
|
|
6539
|
+
function resolveRelative$1(baseDir, target) {
|
|
5789
6540
|
if (target.startsWith("/")) return target.slice(1);
|
|
5790
6541
|
const segments = [...baseDir.split("/").filter((part) => part !== ""), ...target.split("/")];
|
|
5791
6542
|
const out = [];
|
|
@@ -5801,17 +6552,17 @@ const relsOf = (path) => `${dirOf(path)}_rels/${path.slice(path.lastIndexOf("/")
|
|
|
5801
6552
|
function readRels(parts, path) {
|
|
5802
6553
|
const out = /* @__PURE__ */ new Map();
|
|
5803
6554
|
const bytes = parts.get(relsOf(path));
|
|
5804
|
-
const doc = bytes === void 0 ? void 0 : parseXml(bytes);
|
|
6555
|
+
const doc = bytes === void 0 ? void 0 : parseXml$1(bytes);
|
|
5805
6556
|
if (doc === void 0) return out;
|
|
5806
|
-
for (const rel of byTag(doc, "Relationship")) {
|
|
6557
|
+
for (const rel of byTag$2(doc, "Relationship")) {
|
|
5807
6558
|
const id = rel.getAttribute("Id");
|
|
5808
6559
|
const target = rel.getAttribute("Target");
|
|
5809
|
-
if (id !== null && target !== null) out.set(id, resolveRelative(dirOf(path), target));
|
|
6560
|
+
if (id !== null && target !== null) out.set(id, resolveRelative$1(dirOf(path), target));
|
|
5810
6561
|
}
|
|
5811
6562
|
return out;
|
|
5812
6563
|
}
|
|
5813
6564
|
const anchorNumber = (anchor, name) => {
|
|
5814
|
-
const node = anchor === void 0 ? void 0 : byTag(anchor, name)[0];
|
|
6565
|
+
const node = anchor === void 0 ? void 0 : byTag$2(anchor, name)[0];
|
|
5815
6566
|
const value = Number(node?.textContent ?? NaN);
|
|
5816
6567
|
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
5817
6568
|
};
|
|
@@ -5822,17 +6573,17 @@ function imagesOf(parts, sheetPath) {
|
|
|
5822
6573
|
for (const [, target] of sheetRels) {
|
|
5823
6574
|
if (!target.includes("/drawings/")) continue;
|
|
5824
6575
|
const drawingBytes = parts.get(target);
|
|
5825
|
-
const drawing = drawingBytes === void 0 ? void 0 : parseXml(drawingBytes);
|
|
6576
|
+
const drawing = drawingBytes === void 0 ? void 0 : parseXml$1(drawingBytes);
|
|
5826
6577
|
if (drawing === void 0) continue;
|
|
5827
6578
|
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];
|
|
6579
|
+
for (const anchor of byTag$2(drawing, "twoCellAnchor")) {
|
|
6580
|
+
const from = byTag$2(anchor, "from")[0];
|
|
6581
|
+
const to = byTag$2(anchor, "to")[0];
|
|
5831
6582
|
const fromRow = anchorNumber(from, "row");
|
|
5832
6583
|
const columns = anchorNumber(to, "col") - anchorNumber(from, "col");
|
|
5833
6584
|
const rows = anchorNumber(to, "row") - fromRow;
|
|
5834
6585
|
if (columns < 2 || rows < 3) continue;
|
|
5835
|
-
const blip = byTag(anchor, "blip")[0];
|
|
6586
|
+
const blip = byTag$2(anchor, "blip")[0];
|
|
5836
6587
|
const relId = blip?.getAttributeNS(RELATIONSHIPS_NS, "embed") ?? blip?.getAttribute("r:embed");
|
|
5837
6588
|
const path = relId === null || relId === void 0 ? void 0 : drawingRels.get(relId);
|
|
5838
6589
|
const data = path === void 0 ? void 0 : parts.get(path);
|
|
@@ -5874,6 +6625,1122 @@ async function readXlsxBlocks(bytes) {
|
|
|
5874
6625
|
return { sheets: out };
|
|
5875
6626
|
}
|
|
5876
6627
|
|
|
6628
|
+
//#endregion
|
|
6629
|
+
//#region ../browser/src/document/pptxBlocks.ts
|
|
6630
|
+
/**
|
|
6631
|
+
* pptx → 逐页的块序列(0.22.0 分册 18)。
|
|
6632
|
+
*
|
|
6633
|
+
* 这不是渲染器。OOXML 包里**没有**渲染好的页面图,浏览器里也没有 pptx 渲染器,
|
|
6634
|
+
* 所以这条路给得出「这一页写了什么、插了哪几张图」,给不出「这一页长什么样」。
|
|
6635
|
+
* 要版式只能退到截屏。
|
|
6636
|
+
*
|
|
6637
|
+
* 与 docx/xlsx 同一条路:zip 走 `unzipWithLimits`,XML 走 `DOMParser`,
|
|
6638
|
+
* 解不了的媒体走 `imageBlockOf` 的 `skipped`——不假装读完是硬要求(FR-22.14)。
|
|
6639
|
+
*/
|
|
6640
|
+
const PRESENTATION = "ppt/presentation.xml";
|
|
6641
|
+
const MAIN_NS$1 = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
6642
|
+
/** `p:sldId` / `p:spTree` 在 presentationml 里,不在 drawingml 里 */
|
|
6643
|
+
const PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main";
|
|
6644
|
+
const REL_NS$2 = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
6645
|
+
const DOC_REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
6646
|
+
function parseXml(files, path, what) {
|
|
6647
|
+
const bytes = files.get(path);
|
|
6648
|
+
if (bytes === void 0) return void 0;
|
|
6649
|
+
const doc = new DOMParser().parseFromString(new TextDecoder().decode(bytes), "application/xml");
|
|
6650
|
+
if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The presentation's ${what} could not be parsed as XML.`);
|
|
6651
|
+
return doc;
|
|
6652
|
+
}
|
|
6653
|
+
/**
|
|
6654
|
+
* 某个部件的关系表,键是 `rId`,值是**已经解算成包内绝对路径**的 target。
|
|
6655
|
+
* 外部链接(`TargetMode="External"`)不收:那是别处的 URL,不是包里的东西。
|
|
6656
|
+
*/
|
|
6657
|
+
function relationshipsOf(files, part) {
|
|
6658
|
+
const slash = part.lastIndexOf("/");
|
|
6659
|
+
const dir = part.slice(0, slash);
|
|
6660
|
+
const doc = parseXml(files, `${dir}/_rels/${part.slice(slash + 1)}.rels`, "relationships");
|
|
6661
|
+
const map = /* @__PURE__ */ new Map();
|
|
6662
|
+
if (doc === void 0) return map;
|
|
6663
|
+
for (const node of Array.from(doc.getElementsByTagNameNS(REL_NS$2, "Relationship"))) {
|
|
6664
|
+
const id = node.getAttribute("Id");
|
|
6665
|
+
const target = node.getAttribute("Target");
|
|
6666
|
+
if (id === null || target === null || node.getAttribute("TargetMode") === "External") continue;
|
|
6667
|
+
map.set(id, resolveTarget(dir, target));
|
|
6668
|
+
}
|
|
6669
|
+
return map;
|
|
6670
|
+
}
|
|
6671
|
+
/** `../media/image1.png` 相对于部件所在目录解算,得到包内绝对路径 */
|
|
6672
|
+
function resolveTarget(dir, target) {
|
|
6673
|
+
if (target.startsWith("/")) return target.slice(1);
|
|
6674
|
+
const segments = dir.split("/");
|
|
6675
|
+
for (const part of target.split("/")) {
|
|
6676
|
+
if (part === "." || part === "") continue;
|
|
6677
|
+
if (part === "..") segments.pop();
|
|
6678
|
+
else segments.push(part);
|
|
6679
|
+
}
|
|
6680
|
+
return segments.join("/");
|
|
6681
|
+
}
|
|
6682
|
+
/**
|
|
6683
|
+
* 放映顺序取自 `p:sldIdLst`,**不是**文件名里的数字。
|
|
6684
|
+
* `slide7.xml` 完全可以排在第二张——删过页的文稿里这是常态。
|
|
6685
|
+
*/
|
|
6686
|
+
function slideOrder(files) {
|
|
6687
|
+
const presentation = parseXml(files, PRESENTATION, "presentation part");
|
|
6688
|
+
if (presentation === void 0) return [];
|
|
6689
|
+
const rels = relationshipsOf(files, PRESENTATION);
|
|
6690
|
+
const ordered = [];
|
|
6691
|
+
for (const node of Array.from(presentation.getElementsByTagNameNS(PRESENTATION_NS, "sldId"))) {
|
|
6692
|
+
const id = node.getAttributeNS(DOC_REL_NS, "id");
|
|
6693
|
+
const target = id === null ? void 0 : rels.get(id);
|
|
6694
|
+
if (target !== void 0 && files.has(target)) ordered.push(target);
|
|
6695
|
+
}
|
|
6696
|
+
if (ordered.length > 0) return ordered;
|
|
6697
|
+
return [...files.keys()].filter((name) => /^ppt\/slides\/slide\d+\.xml$/.test(name)).sort((a, b) => slideNumber(a) - slideNumber(b));
|
|
6698
|
+
}
|
|
6699
|
+
function slideNumber(path) {
|
|
6700
|
+
return Number(/(\d+)\.xml$/.exec(path)?.[1] ?? 0);
|
|
6701
|
+
}
|
|
6702
|
+
/** 段落之间换行,段落内的多个 run 直接相接——run 的切分是格式造成的,不是语义 */
|
|
6703
|
+
function textOf$1(node) {
|
|
6704
|
+
const lines = [];
|
|
6705
|
+
for (const paragraph of Array.from(node.getElementsByTagNameNS(MAIN_NS$1, "p"))) {
|
|
6706
|
+
const line = Array.from(paragraph.getElementsByTagNameNS(MAIN_NS$1, "t")).map((run) => run.textContent ?? "").join("").trim();
|
|
6707
|
+
if (line !== "") lines.push(line);
|
|
6708
|
+
}
|
|
6709
|
+
return lines.join("\n");
|
|
6710
|
+
}
|
|
6711
|
+
/**
|
|
6712
|
+
* 备注页的文本。多一道过滤:备注页上除备注框外还挂着页码之类的字段占位符,
|
|
6713
|
+
* 取值是一个孤零零的符号。**不含任何字母或数字的条目不是备注**。
|
|
6714
|
+
*/
|
|
6715
|
+
function notesOf(files, rels) {
|
|
6716
|
+
const target = [...rels.values()].find((value) => value.includes("/notesSlides/"));
|
|
6717
|
+
const doc = target === void 0 ? void 0 : parseXml(files, target, "speaker notes");
|
|
6718
|
+
if (doc === void 0) return "";
|
|
6719
|
+
const lines = [];
|
|
6720
|
+
for (const tree of Array.from(doc.getElementsByTagNameNS(PRESENTATION_NS, "spTree"))) for (const shape of Array.from(tree.children)) {
|
|
6721
|
+
const text = textOf$1(shape);
|
|
6722
|
+
if (text !== "" && /[\p{L}\p{N}]/u.test(text)) lines.push(text);
|
|
6723
|
+
}
|
|
6724
|
+
return lines.join("\n");
|
|
6725
|
+
}
|
|
6726
|
+
function readSlide(files, path) {
|
|
6727
|
+
const doc = parseXml(files, path, "slide");
|
|
6728
|
+
if (doc === void 0) return { blocks: [] };
|
|
6729
|
+
const rels = relationshipsOf(files, path);
|
|
6730
|
+
const blocks = [];
|
|
6731
|
+
for (const tree of Array.from(doc.getElementsByTagNameNS(PRESENTATION_NS, "spTree"))) for (const shape of Array.from(tree.children)) {
|
|
6732
|
+
const text = textOf$1(shape);
|
|
6733
|
+
if (text !== "") blocks.push({
|
|
6734
|
+
kind: "text",
|
|
6735
|
+
text
|
|
6736
|
+
});
|
|
6737
|
+
}
|
|
6738
|
+
for (const target of rels.values()) {
|
|
6739
|
+
if (!target.startsWith("ppt/media/")) continue;
|
|
6740
|
+
const bytes = files.get(target);
|
|
6741
|
+
if (bytes === void 0) continue;
|
|
6742
|
+
blocks.push(imageBlockOf(target, () => toBase64$1(bytes)));
|
|
6743
|
+
}
|
|
6744
|
+
const notes = notesOf(files, rels);
|
|
6745
|
+
if (notes !== "") blocks.push({
|
|
6746
|
+
kind: "text",
|
|
6747
|
+
text: `Speaker notes: ${notes}`
|
|
6748
|
+
});
|
|
6749
|
+
return { blocks };
|
|
6750
|
+
}
|
|
6751
|
+
/** @experimental */
|
|
6752
|
+
function createPptxBlockReader() {
|
|
6753
|
+
return { read: readPptxBlocks };
|
|
6754
|
+
}
|
|
6755
|
+
/**
|
|
6756
|
+
* @throws WebSkillError 归档损坏、缺 `ppt/presentation.xml`、XML 解析失败
|
|
6757
|
+
* @experimental
|
|
6758
|
+
*/
|
|
6759
|
+
async function readPptxBlocks(bytes) {
|
|
6760
|
+
const files = new Map(await unzipWithLimits(bytes));
|
|
6761
|
+
if (!files.has(PRESENTATION)) throw new WebSkillError("TOOL_EXECUTION_FAILED", `This file is not a PowerPoint presentation: it has no ${PRESENTATION} entry.`);
|
|
6762
|
+
return { slides: slideOrder(files).map((path) => readSlide(files, path)) };
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
//#endregion
|
|
6766
|
+
//#region ../browser/src/document/pptx.ts
|
|
6767
|
+
/**
|
|
6768
|
+
* pptx 正文抽取(0.22.0 分册 18)。
|
|
6769
|
+
*
|
|
6770
|
+
* 与 `readPptxBlocks` 同源:文本形态只是把逐页的文本块按放映顺序拼起来,
|
|
6771
|
+
* 两条路给出的文字因此**永远一致**——同一份文稿不会因为走哪条路而多出或少掉一段。
|
|
6772
|
+
*
|
|
6773
|
+
* ⚠️ 与 docx/xlsx 一样依赖 `DOMParser`,所以只在 `@webskill/browser`。
|
|
6774
|
+
*/
|
|
6775
|
+
/**
|
|
6776
|
+
* **不抽取**的内容,逐条写明而不是笼统说「尽力而为」:
|
|
6777
|
+
* 版式、动画、切换效果、母版与版式上的模板文字、图表的底层数据。
|
|
6778
|
+
*
|
|
6779
|
+
* 插图不在此列但也不在文本里:图要按页取,走 `read_document`。
|
|
6780
|
+
*/
|
|
6781
|
+
const PPTX_UNEXTRACTED = [
|
|
6782
|
+
"layout and positioning",
|
|
6783
|
+
"animations and transitions",
|
|
6784
|
+
"master and layout placeholders",
|
|
6785
|
+
"chart source data",
|
|
6786
|
+
"pictures"
|
|
6787
|
+
];
|
|
6788
|
+
/** 抽不出任何文字时的占位,与 docx/xlsx 同款口径 @experimental */
|
|
6789
|
+
const PPTX_UNEXTRACTED_NOTICE = "This presentation has no extractable text: its slides are most likely pictures. Use read_document to look at them.";
|
|
6790
|
+
/**
|
|
6791
|
+
* @throws WebSkillError 归档损坏、缺 `ppt/presentation.xml`、XML 解析失败
|
|
6792
|
+
* @experimental
|
|
6793
|
+
*/
|
|
6794
|
+
async function extractPptxText(bytes) {
|
|
6795
|
+
const { slides } = await readPptxBlocks(bytes);
|
|
6796
|
+
const pages = [];
|
|
6797
|
+
slides.forEach((slide, index) => {
|
|
6798
|
+
const text = slide.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
|
|
6799
|
+
if (text.trim() !== "") pages.push(`--- Slide ${index + 1} ---\n${text}`);
|
|
6800
|
+
});
|
|
6801
|
+
return pages.length === 0 ? PPTX_UNEXTRACTED_NOTICE : pages.join("\n\n");
|
|
6802
|
+
}
|
|
6803
|
+
|
|
6804
|
+
//#endregion
|
|
6805
|
+
//#region ../browser/src/document/ooxmlPart.ts
|
|
6806
|
+
/**
|
|
6807
|
+
* OOXML 包的拆装地基(0.22.0 分册 42 · 设计 43 §1.1、§1.7)。
|
|
6808
|
+
*
|
|
6809
|
+
* 这一册的整条路子是「解开模板 zip,只改承载数据的那几个部件,其余字节原样写回」。
|
|
6810
|
+
* 地基只做三件事,xlsx 与 docx 两侧都走它,谁都不许自己碰 XML 声明。
|
|
6811
|
+
*/
|
|
6812
|
+
const CONTENT_TYPES = "[Content_Types].xml";
|
|
6813
|
+
const XML_DECLARATION = /^<\?xml[^?]*\?>\r?\n?/;
|
|
6814
|
+
/**
|
|
6815
|
+
* 切下 XML 声明。
|
|
6816
|
+
*
|
|
6817
|
+
* 设计 43 §1.1 实测:jsdom 的 `XMLSerializer` 丢掉声明,Chromium 保留它却吃掉结尾的 `\r\n`。
|
|
6818
|
+
* 两个引擎两种错法,而且只有在真实浏览器里才会碰到第二种——所以声明必须在解析前切下来留着,
|
|
6819
|
+
* 序列化之后把**原始那几个字节**原样拼回去,任何路径都不读序列化器吐出来的前言。
|
|
6820
|
+
*/
|
|
6821
|
+
function splitXmlDeclaration(text) {
|
|
6822
|
+
const declaration = XML_DECLARATION.exec(text)?.[0] ?? "";
|
|
6823
|
+
return {
|
|
6824
|
+
declaration,
|
|
6825
|
+
body: text.slice(declaration.length)
|
|
6826
|
+
};
|
|
6827
|
+
}
|
|
6828
|
+
/** @throws WebSkillError 该部件不存在或不是合法 XML */
|
|
6829
|
+
function parseXmlPart(pkg, path) {
|
|
6830
|
+
const bytes = pkg.get(path);
|
|
6831
|
+
if (bytes === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The template package has no ${path} part.`);
|
|
6832
|
+
const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(bytes));
|
|
6833
|
+
const doc = new DOMParser().parseFromString(body, "application/xml");
|
|
6834
|
+
if (doc.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The ${path} part of the template is not well-formed XML.`);
|
|
6835
|
+
return {
|
|
6836
|
+
declaration,
|
|
6837
|
+
doc
|
|
6838
|
+
};
|
|
6839
|
+
}
|
|
6840
|
+
/** 序列化并把原始声明原样拼回(见 {@link splitXmlDeclaration})。 */
|
|
6841
|
+
function serializeXmlPart(declaration, doc) {
|
|
6842
|
+
const { body } = splitXmlDeclaration(new XMLSerializer().serializeToString(doc));
|
|
6843
|
+
return new TextEncoder().encode(declaration + body);
|
|
6844
|
+
}
|
|
6845
|
+
/** @throws WebSkillError 归档损坏,或不是 OOXML(缺 `[Content_Types].xml`) */
|
|
6846
|
+
async function readOoxmlPackage(bytes) {
|
|
6847
|
+
const entries = await unzipWithLimits(bytes);
|
|
6848
|
+
const pkg = new Map(entries.filter(([path]) => !path.endsWith("/")));
|
|
6849
|
+
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.`);
|
|
6850
|
+
return pkg;
|
|
6851
|
+
}
|
|
6852
|
+
function writeOoxmlPackage(pkg) {
|
|
6853
|
+
return zipSync(Object.fromEntries(pkg), { level: 6 });
|
|
6854
|
+
}
|
|
6855
|
+
/** `../media/x.png` 这种相对 Target 按所在目录归一化 */
|
|
6856
|
+
function resolveRelative(baseDir, target) {
|
|
6857
|
+
if (target.startsWith("/")) return target.slice(1);
|
|
6858
|
+
const out = [];
|
|
6859
|
+
for (const segment of [...baseDir.split("/"), ...target.split("/")]) {
|
|
6860
|
+
if (segment === "" || segment === ".") continue;
|
|
6861
|
+
if (segment === "..") out.pop();
|
|
6862
|
+
else out.push(segment);
|
|
6863
|
+
}
|
|
6864
|
+
return out.join("/");
|
|
6865
|
+
}
|
|
6866
|
+
const extensionOf = (path) => {
|
|
6867
|
+
const dot = path.lastIndexOf(".");
|
|
6868
|
+
return dot < 0 ? "" : path.slice(dot + 1).toLowerCase();
|
|
6869
|
+
};
|
|
6870
|
+
/**
|
|
6871
|
+
* 结构自检(AC-42.27)。
|
|
6872
|
+
*
|
|
6873
|
+
* 「打开不提示修复」没法进 CI(本仓没有可脚本化的 Office 实现,见设计 10 §6.2),
|
|
6874
|
+
* 但迄今实测出的坏文件成因都是结构性的:悬空关系、没登记的扩展名、解析不了的部件。
|
|
6875
|
+
* 这三条能当场查出来,所以每次重封之前都查一遍。
|
|
6876
|
+
*
|
|
6877
|
+
* @returns 英文问题描述,空数组表示干净
|
|
6878
|
+
*/
|
|
6879
|
+
function auditOoxmlPackage(pkg) {
|
|
6880
|
+
const problems = [];
|
|
6881
|
+
const contentTypes = pkg.get(CONTENT_TYPES);
|
|
6882
|
+
if (contentTypes === void 0) return [`missing ${CONTENT_TYPES}`];
|
|
6883
|
+
const types = new TextDecoder().decode(contentTypes);
|
|
6884
|
+
const defaults = new Set([...types.matchAll(/<Default\s+Extension="([^"]+)"/g)].map((match) => (match[1] ?? "").toLowerCase()));
|
|
6885
|
+
const overrides = new Set([...types.matchAll(/<Override\s+PartName="([^"]+)"/g)].map((match) => match[1]));
|
|
6886
|
+
for (const path of pkg.keys()) {
|
|
6887
|
+
if (path === CONTENT_TYPES) continue;
|
|
6888
|
+
if (!defaults.has(extensionOf(path)) && !overrides.has(`/${path}`)) problems.push(`part is not covered by [Content_Types].xml: ${path}`);
|
|
6889
|
+
}
|
|
6890
|
+
for (const [path, bytes] of pkg) {
|
|
6891
|
+
if (!path.endsWith(".rels")) continue;
|
|
6892
|
+
const relsDir = path.slice(0, path.lastIndexOf("_rels/"));
|
|
6893
|
+
for (const match of new TextDecoder().decode(bytes).matchAll(/<Relationship\s[^>]*>/g)) {
|
|
6894
|
+
const tag = match[0];
|
|
6895
|
+
if (/TargetMode="External"/.test(tag)) continue;
|
|
6896
|
+
const target = /Target="([^"]+)"/.exec(tag)?.[1];
|
|
6897
|
+
if (target === void 0 || /^[a-z]+:/i.test(target)) continue;
|
|
6898
|
+
const resolved = resolveRelative(relsDir, decodeURIComponent(target));
|
|
6899
|
+
if (!pkg.has(resolved)) problems.push(`dangling relationship in ${path}: ${target}`);
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
for (const [path, bytes] of pkg) {
|
|
6903
|
+
if (extensionOf(path) !== "xml" && !path.endsWith(".rels")) continue;
|
|
6904
|
+
const { body } = splitXmlDeclaration(new TextDecoder().decode(bytes));
|
|
6905
|
+
if (new DOMParser().parseFromString(body, "application/xml").getElementsByTagName("parsererror").length > 0) problems.push(`part is not well-formed XML: ${path}`);
|
|
6906
|
+
}
|
|
6907
|
+
return problems;
|
|
6908
|
+
}
|
|
6909
|
+
/** @throws WebSkillError 自检不干净——宁可当场失败,也不要交一个会弹修复的文件 */
|
|
6910
|
+
function assertOoxmlPackage(pkg) {
|
|
6911
|
+
const problems = auditOoxmlPackage(pkg);
|
|
6912
|
+
if (problems.length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", `The generated Office file would be structurally invalid: ${problems.join("; ")}.`);
|
|
6913
|
+
}
|
|
6914
|
+
/**
|
|
6915
|
+
* 往 `[Content_Types].xml` 补一条 `<Default Extension>`,已有则原样返回。
|
|
6916
|
+
*
|
|
6917
|
+
* 插图时必须补(AC-42.18 只允许这一处变化);不插图时一个字节都不能动(FR-42.20)。
|
|
6918
|
+
*/
|
|
6919
|
+
function ensureDefaultExtension(pkg, extension, contentType) {
|
|
6920
|
+
const bytes = pkg.get(CONTENT_TYPES);
|
|
6921
|
+
if (bytes === void 0) return;
|
|
6922
|
+
const text = new TextDecoder().decode(bytes);
|
|
6923
|
+
if (new RegExp(`<Default\\s+Extension="${extension}"`, "i").test(text)) return;
|
|
6924
|
+
const { declaration, body } = splitXmlDeclaration(text);
|
|
6925
|
+
const inserted = body.replace("<Default", `<Default Extension="${extension}" ContentType="${contentType}"/><Default`);
|
|
6926
|
+
pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + inserted));
|
|
6927
|
+
}
|
|
6928
|
+
/** 往 `[Content_Types].xml` 补一条 `<Override PartName>`,已有则原样返回。 */
|
|
6929
|
+
function ensureOverride(pkg, partName, contentType) {
|
|
6930
|
+
const bytes = pkg.get(CONTENT_TYPES);
|
|
6931
|
+
if (bytes === void 0) return;
|
|
6932
|
+
const text = new TextDecoder().decode(bytes);
|
|
6933
|
+
if (text.includes(`PartName="${partName}"`)) return;
|
|
6934
|
+
const { declaration, body } = splitXmlDeclaration(text);
|
|
6935
|
+
const inserted = body.replace("</Types>", `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`);
|
|
6936
|
+
pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + inserted));
|
|
6937
|
+
}
|
|
6938
|
+
/** 摘掉一个部件连同它在 `[Content_Types].xml` 与指定 rels 里的登记(FR-42.4 修订项)。 */
|
|
6939
|
+
function dropPart(pkg, partName, relsPath) {
|
|
6940
|
+
if (!pkg.delete(partName)) return;
|
|
6941
|
+
const fileName = partName.slice(partName.lastIndexOf("/") + 1);
|
|
6942
|
+
const types = pkg.get(CONTENT_TYPES);
|
|
6943
|
+
if (types !== void 0) {
|
|
6944
|
+
const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(types));
|
|
6945
|
+
const stripped = body.replace(new RegExp(`<Override\\s+PartName="/${partName}"[^>]*/>`), "");
|
|
6946
|
+
pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + stripped));
|
|
6947
|
+
}
|
|
6948
|
+
const rels = pkg.get(relsPath);
|
|
6949
|
+
if (rels !== void 0) {
|
|
6950
|
+
const { declaration, body } = splitXmlDeclaration(new TextDecoder().decode(rels));
|
|
6951
|
+
const stripped = body.replace(new RegExp(`<Relationship\\s[^>]*Target="[^"]*${fileName}"[^>]*/>`), "");
|
|
6952
|
+
pkg.set(relsPath, new TextEncoder().encode(declaration + stripped));
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
/** VBA 工程部件的 content type 前缀。宏的判据看这个,不看文件路径。 */
|
|
6956
|
+
const VBA_CONTENT_TYPE = "application/vnd.ms-office.vbaProject";
|
|
6957
|
+
/** macroEnabled 的主部件 content type → 对应的非宏值(0.23.0 分册 25 设计 26 §3.1)。 */
|
|
6958
|
+
const NON_MACRO_MAIN_TYPE = /* @__PURE__ */ new Map([
|
|
6959
|
+
["application/vnd.ms-excel.sheet.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"],
|
|
6960
|
+
["application/vnd.ms-excel.template.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"],
|
|
6961
|
+
["application/vnd.ms-word.document.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"],
|
|
6962
|
+
["application/vnd.ms-word.template.macroEnabled.main+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"]
|
|
6963
|
+
]);
|
|
6964
|
+
const attributeOf = (tag, name) => new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1] ?? "";
|
|
6965
|
+
/**
|
|
6966
|
+
* 把宏痕迹从包里摘干净(0.23.0 分册 25 · FR-25.4)。
|
|
6967
|
+
*
|
|
6968
|
+
* 我们填完模板交出去的文件叫 `.xlsx` / `.docx`,声明的也是非宏 MIME。如果里面还留着
|
|
6969
|
+
* macroEnabled 的 content type 或 `vbaProject.bin`,文件就自相矛盾,Excel 会拒绝打开。
|
|
6970
|
+
* 宿主报来的那类文件更极端:`vbaProject.bin` 本身已经损坏或丢失,只剩标记——所以判据是
|
|
6971
|
+
* **`[Content_Types].xml` 里的登记**,不是「包里有没有那个部件」。
|
|
6972
|
+
*
|
|
6973
|
+
* 不是宏包时**一个字节都不改**(FR-25.5):无条件重写 `[Content_Types].xml` 会动到属性顺序
|
|
6974
|
+
* 和空白,非宏模板的产物字节就跟着变了。所以第一件事是判断,不中就原样返回。
|
|
6975
|
+
*/
|
|
6976
|
+
function stripMacros(pkg) {
|
|
6977
|
+
const typesBytes = pkg.get(CONTENT_TYPES);
|
|
6978
|
+
if (typesBytes === void 0) return;
|
|
6979
|
+
const text = new TextDecoder().decode(typesBytes);
|
|
6980
|
+
if (!text.includes(VBA_CONTENT_TYPE) && !text.includes("macroEnabled.main+xml")) return;
|
|
6981
|
+
const { declaration, body } = splitXmlDeclaration(text);
|
|
6982
|
+
let rewritten = body;
|
|
6983
|
+
for (const match of body.matchAll(/<Override\s[^>]*\/>/g)) {
|
|
6984
|
+
const tag = match[0];
|
|
6985
|
+
const contentType = attributeOf(tag, "ContentType");
|
|
6986
|
+
if (contentType.startsWith(VBA_CONTENT_TYPE)) {
|
|
6987
|
+
pkg.delete(attributeOf(tag, "PartName").replace(/^\//, ""));
|
|
6988
|
+
rewritten = rewritten.replace(tag, "");
|
|
6989
|
+
continue;
|
|
6990
|
+
}
|
|
6991
|
+
const replacement = NON_MACRO_MAIN_TYPE.get(contentType);
|
|
6992
|
+
if (replacement !== void 0) rewritten = rewritten.replace(tag, tag.replace(contentType, replacement));
|
|
6993
|
+
}
|
|
6994
|
+
pkg.set(CONTENT_TYPES, new TextEncoder().encode(declaration + rewritten));
|
|
6995
|
+
for (const [path, bytes] of pkg) {
|
|
6996
|
+
if (!path.endsWith(".rels")) continue;
|
|
6997
|
+
const split = splitXmlDeclaration(new TextDecoder().decode(bytes));
|
|
6998
|
+
let strippedRels = split.body;
|
|
6999
|
+
for (const match of split.body.matchAll(/<Relationship\s[^>]*\/>/g)) if (attributeOf(match[0], "Type").includes("/vbaProject")) strippedRels = strippedRels.replace(match[0], "");
|
|
7000
|
+
if (strippedRels !== split.body) pkg.set(path, new TextEncoder().encode(split.declaration + strippedRels));
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
//#endregion
|
|
7005
|
+
//#region ../browser/src/document/xlsxTemplate.ts
|
|
7006
|
+
/**
|
|
7007
|
+
* 照着 xlsx 模板出新文件(0.22.0 分册 42 · 设计 43 §4)。
|
|
7008
|
+
*
|
|
7009
|
+
* 不从零生成,而是解开模板 zip 只改 `sheet*.xml`,其余字节原样写回。
|
|
7010
|
+
* 模板里的空单元格本来就是带样式索引的空壳 `<c r="B3" s="6"/>`,
|
|
7011
|
+
* 所以「填值保格式」不是要实现的特性,是不要破坏的既有事实(设计 43 §1.2)。
|
|
7012
|
+
*/
|
|
7013
|
+
const MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
7014
|
+
const REL_NS$1 = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
7015
|
+
const PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
7016
|
+
const DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
|
|
7017
|
+
const DRAWING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.drawing+xml";
|
|
7018
|
+
const DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
|
|
7019
|
+
const byTag$1 = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
|
|
7020
|
+
/** 工作表名 → 部件路径。名字来自 workbook.xml,路径要经 rels 绕一道。 */
|
|
7021
|
+
function listSheets(pkg) {
|
|
7022
|
+
const { doc } = parseXmlPart(pkg, "xl/workbook.xml");
|
|
7023
|
+
const { doc: relsDoc } = parseXmlPart(pkg, "xl/_rels/workbook.xml.rels");
|
|
7024
|
+
const targets = /* @__PURE__ */ new Map();
|
|
7025
|
+
for (const rel of byTag$1(relsDoc, "Relationship")) {
|
|
7026
|
+
const id = rel.getAttribute("Id");
|
|
7027
|
+
const target = rel.getAttribute("Target");
|
|
7028
|
+
if (id !== null && target !== null) targets.set(id, target.startsWith("/") ? target.slice(1) : `xl/${target}`);
|
|
7029
|
+
}
|
|
7030
|
+
const sheets = [];
|
|
7031
|
+
for (const sheet of byTag$1(doc, "sheet")) {
|
|
7032
|
+
const name = sheet.getAttribute("name");
|
|
7033
|
+
const id = sheet.getAttributeNS(REL_NS$1, "id") ?? sheet.getAttribute("r:id");
|
|
7034
|
+
const path = id === null ? void 0 : targets.get(id);
|
|
7035
|
+
if (name !== null && path !== void 0) sheets.push({
|
|
7036
|
+
name,
|
|
7037
|
+
path
|
|
7038
|
+
});
|
|
7039
|
+
}
|
|
7040
|
+
return sheets;
|
|
7041
|
+
}
|
|
7042
|
+
/** @throws WebSkillError 模板里没有这个工作表 */
|
|
7043
|
+
function resolveSheet(sheets, name) {
|
|
7044
|
+
if (name === void 0) {
|
|
7045
|
+
const first = sheets[0];
|
|
7046
|
+
if (first === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", "The template has no worksheet.");
|
|
7047
|
+
return first;
|
|
7048
|
+
}
|
|
7049
|
+
const found = sheets.find((sheet) => sheet.name === name);
|
|
7050
|
+
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(", ")}.`);
|
|
7051
|
+
return found;
|
|
7052
|
+
}
|
|
7053
|
+
function sharedStringsOf(pkg) {
|
|
7054
|
+
if (!pkg.has("xl/sharedStrings.xml")) return [];
|
|
7055
|
+
const { doc } = parseXmlPart(pkg, "xl/sharedStrings.xml");
|
|
7056
|
+
return byTag$1(doc, "si").map((si) => byTag$1(si, "t").map((node) => node.textContent ?? "").join(""));
|
|
7057
|
+
}
|
|
7058
|
+
function cellText(cell, shared) {
|
|
7059
|
+
const type = cell.getAttribute("t");
|
|
7060
|
+
if (type === "inlineStr") return byTag$1(cell, "t").map((node) => node.textContent ?? "").join("");
|
|
7061
|
+
const value = byTag$1(cell, "v")[0]?.textContent ?? "";
|
|
7062
|
+
if (type === "s") {
|
|
7063
|
+
const index = Number(value);
|
|
7064
|
+
return Number.isInteger(index) ? shared[index] ?? "" : "";
|
|
7065
|
+
}
|
|
7066
|
+
return value;
|
|
7067
|
+
}
|
|
7068
|
+
/** 共享公式的宿主:`si` → 公式体与宿主所在行 */
|
|
7069
|
+
function sharedFormulaHosts(doc) {
|
|
7070
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
7071
|
+
for (const formula of byTag$1(doc, "f")) {
|
|
7072
|
+
const si = formula.getAttribute("si");
|
|
7073
|
+
const body = formula.textContent ?? "";
|
|
7074
|
+
if (si === null || body === "") continue;
|
|
7075
|
+
const address = formula.parentElement?.getAttribute("r");
|
|
7076
|
+
if (address === null || address === void 0) continue;
|
|
7077
|
+
hosts.set(si, {
|
|
7078
|
+
body,
|
|
7079
|
+
row: parseCellAddress(address).row
|
|
7080
|
+
});
|
|
7081
|
+
}
|
|
7082
|
+
return hosts;
|
|
7083
|
+
}
|
|
7084
|
+
/** 共享公式的引用格本身不带公式体,按行差从宿主展开,模型才知道它读的是哪几格 */
|
|
7085
|
+
function formulaTextOf(cell, hosts) {
|
|
7086
|
+
const formula = [...cell.children].find((child) => child.localName === "f");
|
|
7087
|
+
if (formula === void 0) return void 0;
|
|
7088
|
+
const body = formula.textContent ?? "";
|
|
7089
|
+
if (body !== "") return body;
|
|
7090
|
+
const host = hosts.get(formula.getAttribute("si") ?? "");
|
|
7091
|
+
const address = cell.getAttribute("r");
|
|
7092
|
+
if (host === void 0 || address === null) return "";
|
|
7093
|
+
return offsetFormulaRows(host.body, parseCellAddress(address).row - host.row);
|
|
7094
|
+
}
|
|
7095
|
+
/** 读带坐标的骨架(FR-42.1)。模型据此决定往哪儿写。 */
|
|
7096
|
+
async function readXlsxTemplate(bytes) {
|
|
7097
|
+
const pkg = await readOoxmlPackage(bytes);
|
|
7098
|
+
const shared = sharedStringsOf(pkg);
|
|
7099
|
+
const sheets = [];
|
|
7100
|
+
for (const entry of listSheets(pkg)) {
|
|
7101
|
+
const { doc } = parseXmlPart(pkg, entry.path);
|
|
7102
|
+
const hosts = sharedFormulaHosts(doc);
|
|
7103
|
+
const cells = [];
|
|
7104
|
+
for (const cell of byTag$1(doc, "c")) {
|
|
7105
|
+
const address = cell.getAttribute("r");
|
|
7106
|
+
if (address === null) continue;
|
|
7107
|
+
const text = cellText(cell, shared);
|
|
7108
|
+
const formula = formulaTextOf(cell, hosts);
|
|
7109
|
+
if (formula !== void 0) cells.push({
|
|
7110
|
+
address,
|
|
7111
|
+
text,
|
|
7112
|
+
formula
|
|
7113
|
+
});
|
|
7114
|
+
else if (text !== "") cells.push({
|
|
7115
|
+
address,
|
|
7116
|
+
text
|
|
7117
|
+
});
|
|
7118
|
+
}
|
|
7119
|
+
const columnWidths = [];
|
|
7120
|
+
for (const col of byTag$1(doc, "col")) {
|
|
7121
|
+
const min = Number(col.getAttribute("min"));
|
|
7122
|
+
const max = Number(col.getAttribute("max"));
|
|
7123
|
+
const width = Number(col.getAttribute("width"));
|
|
7124
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || !Number.isFinite(width)) continue;
|
|
7125
|
+
for (let index = min; index <= max; index += 1) columnWidths[index - 1] = width;
|
|
7126
|
+
}
|
|
7127
|
+
sheets.push({
|
|
7128
|
+
name: entry.name,
|
|
7129
|
+
range: byTag$1(doc, "dimension")[0]?.getAttribute("ref") ?? "",
|
|
7130
|
+
cells,
|
|
7131
|
+
merges: byTag$1(doc, "mergeCell").map((merge) => merge.getAttribute("ref")).filter((ref) => ref !== null),
|
|
7132
|
+
columnWidths
|
|
7133
|
+
});
|
|
7134
|
+
}
|
|
7135
|
+
return { sheets };
|
|
7136
|
+
}
|
|
7137
|
+
const rowNumberOf = (element) => Number(element.getAttribute("r") ?? NaN);
|
|
7138
|
+
function rowsOf(doc) {
|
|
7139
|
+
const sheetData = byTag$1(doc, "sheetData")[0];
|
|
7140
|
+
return sheetData === void 0 ? [] : [...sheetData.children].filter((node) => node.localName === "row");
|
|
7141
|
+
}
|
|
7142
|
+
/** 把一个元素上的行号整体平移:`<row r>` 与其中每个 `<c r>` */
|
|
7143
|
+
function shiftRow(row, delta) {
|
|
7144
|
+
const next = rowNumberOf(row) + delta;
|
|
7145
|
+
row.setAttribute("r", String(next));
|
|
7146
|
+
for (const cell of byTag$1(row, "c")) {
|
|
7147
|
+
const address = cell.getAttribute("r");
|
|
7148
|
+
if (address === null) continue;
|
|
7149
|
+
const parsed = parseCellAddress(address);
|
|
7150
|
+
cell.setAttribute("r", formatCellAddress({
|
|
7151
|
+
column: parsed.column,
|
|
7152
|
+
row: next - 1
|
|
7153
|
+
}));
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
const shiftAddress = (address, at, count) => {
|
|
7157
|
+
const parsed = parseCellAddress(address);
|
|
7158
|
+
return parsed.row + 1 >= at ? formatCellAddress({
|
|
7159
|
+
...parsed,
|
|
7160
|
+
row: parsed.row + count
|
|
7161
|
+
}) : address;
|
|
7162
|
+
};
|
|
7163
|
+
/**
|
|
7164
|
+
* 插行(FR-42.7)。设计 43 §1.5 实测:必须同时改六处,少动一处就是一个会弹修复的文件。
|
|
7165
|
+
*
|
|
7166
|
+
* @throws WebSkillError 插入位置越界
|
|
7167
|
+
*/
|
|
7168
|
+
function insertRows(pkg, doc, insert) {
|
|
7169
|
+
const { at, count } = insert;
|
|
7170
|
+
const rows = rowsOf(doc);
|
|
7171
|
+
const lastRow = rows[rows.length - 1];
|
|
7172
|
+
const last = lastRow === void 0 ? 0 : rowNumberOf(lastRow);
|
|
7173
|
+
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}.`);
|
|
7174
|
+
const styleRowNumber = insert.copyStyleFrom ?? at - 1;
|
|
7175
|
+
const template = rows.find((row) => rowNumberOf(row) === styleRowNumber);
|
|
7176
|
+
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.`);
|
|
7177
|
+
const formulas = formulasOf(template);
|
|
7178
|
+
for (const row of [...rows].reverse()) if (rowNumberOf(row) >= at) shiftRow(row, count);
|
|
7179
|
+
const sheetData = byTag$1(doc, "sheetData")[0];
|
|
7180
|
+
const anchor = rowsOf(doc).find((row) => rowNumberOf(row) >= at + count) ?? null;
|
|
7181
|
+
const made = [];
|
|
7182
|
+
for (let index = 0; index < count; index += 1) {
|
|
7183
|
+
const row = template.cloneNode(true);
|
|
7184
|
+
row.setAttribute("r", String(at + index));
|
|
7185
|
+
for (const cell of byTag$1(row, "c")) {
|
|
7186
|
+
const address = cell.getAttribute("r");
|
|
7187
|
+
if (address !== null) {
|
|
7188
|
+
const parsed = parseCellAddress(address);
|
|
7189
|
+
cell.setAttribute("r", formatCellAddress({
|
|
7190
|
+
column: parsed.column,
|
|
7191
|
+
row: at + index - 1
|
|
7192
|
+
}));
|
|
7193
|
+
}
|
|
7194
|
+
cell.removeAttribute("t");
|
|
7195
|
+
while (cell.firstChild !== null) cell.removeChild(cell.firstChild);
|
|
7196
|
+
}
|
|
7197
|
+
sheetData?.insertBefore(row, anchor);
|
|
7198
|
+
made.push(row);
|
|
7199
|
+
}
|
|
7200
|
+
for (const formula of byTag$1(doc, "f")) {
|
|
7201
|
+
const ref = formula.getAttribute("ref");
|
|
7202
|
+
if (ref !== null) formula.setAttribute("ref", growRange(ref, at, count));
|
|
7203
|
+
const body = formula.textContent;
|
|
7204
|
+
if (body !== null && body !== "") formula.textContent = growFormulaRanges(body, at, count);
|
|
7205
|
+
}
|
|
7206
|
+
for (const merge of byTag$1(doc, "mergeCell")) {
|
|
7207
|
+
const ref = merge.getAttribute("ref");
|
|
7208
|
+
if (ref === null) continue;
|
|
7209
|
+
const [from = "", to] = ref.split(":");
|
|
7210
|
+
merge.setAttribute("ref", to === void 0 ? shiftAddress(from, at, count) : `${shiftAddress(from, at, count)}:${shiftAddress(to, at, count)}`);
|
|
7211
|
+
}
|
|
7212
|
+
const dimension = byTag$1(doc, "dimension")[0];
|
|
7213
|
+
const dimensionRef = dimension?.getAttribute("ref");
|
|
7214
|
+
if (dimension !== void 0 && dimensionRef != null) dimension.setAttribute("ref", growRange(dimensionRef, at, count));
|
|
7215
|
+
dropPart(pkg, "xl/calcChain.xml", "xl/_rels/workbook.xml.rels");
|
|
7216
|
+
for (const [index, row] of made.entries()) restoreFormulas(row, formulas, at + index - styleRowNumber);
|
|
7217
|
+
}
|
|
7218
|
+
/**
|
|
7219
|
+
* 记下一行里每个格的 `<f>`。
|
|
7220
|
+
*
|
|
7221
|
+
* 共享公式宿主(带 `ref`)不能被复制成第二个宿主——同一个 `si` 只许一个宿主,
|
|
7222
|
+
* 多一个 Excel 就报损坏。复制出来的一律降级成引用:`<f t="shared" si="0"/>`。
|
|
7223
|
+
*/
|
|
7224
|
+
function formulasOf(row) {
|
|
7225
|
+
const copied = [];
|
|
7226
|
+
for (const cell of byTag$1(row, "c")) {
|
|
7227
|
+
const address = cell.getAttribute("r");
|
|
7228
|
+
const formula = [...cell.children].find((child) => child.localName === "f");
|
|
7229
|
+
if (address === null || formula === void 0) continue;
|
|
7230
|
+
const host = formula.getAttribute("ref") !== null;
|
|
7231
|
+
const value = [...cell.children].find((child) => child.localName === "v");
|
|
7232
|
+
copied.push({
|
|
7233
|
+
column: parseCellAddress(address).column,
|
|
7234
|
+
attributes: [...formula.attributes].filter((attribute) => attribute.name !== "ref").map((attribute) => [attribute.name, attribute.value]),
|
|
7235
|
+
body: host || formula.getAttribute("t") === "shared" ? "" : formula.textContent ?? "",
|
|
7236
|
+
cached: value?.textContent ?? void 0,
|
|
7237
|
+
type: cell.getAttribute("t") ?? void 0
|
|
7238
|
+
});
|
|
7239
|
+
}
|
|
7240
|
+
return copied;
|
|
7241
|
+
}
|
|
7242
|
+
function restoreFormulas(row, formulas, delta) {
|
|
7243
|
+
if (formulas.length === 0) return;
|
|
7244
|
+
const byColumn = new Map(byTag$1(row, "c").map((cell) => [parseCellAddress(cell.getAttribute("r") ?? "A1").column, cell]));
|
|
7245
|
+
for (const formula of formulas) {
|
|
7246
|
+
const cell = byColumn.get(formula.column);
|
|
7247
|
+
if (cell === void 0) continue;
|
|
7248
|
+
const doc = row.ownerDocument;
|
|
7249
|
+
const made = doc.createElementNS(MAIN_NS, "f");
|
|
7250
|
+
for (const [name, value] of formula.attributes) made.setAttribute(name, value);
|
|
7251
|
+
if (formula.body !== "") made.textContent = offsetFormulaRows(formula.body, delta);
|
|
7252
|
+
cell.appendChild(made);
|
|
7253
|
+
if (formula.cached === void 0) continue;
|
|
7254
|
+
if (formula.type !== void 0) cell.setAttribute("t", formula.type);
|
|
7255
|
+
const value = doc.createElementNS(MAIN_NS, "v");
|
|
7256
|
+
value.textContent = formula.cached;
|
|
7257
|
+
cell.appendChild(value);
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
/** 公式里的相对行引用按行差重写,`$12` 这种绝对行原样不动——就是 Excel 向下填充的语义 */
|
|
7261
|
+
const RELATIVE_ROW = /(\$?[A-Z]{1,3})(\$?)([1-9][0-9]*)/g;
|
|
7262
|
+
function offsetFormulaRows(body, delta) {
|
|
7263
|
+
return body.replace(RELATIVE_ROW, (match, column, absolute, row) => absolute === "$" ? match : `${column}${Math.max(1, Number(row) + delta)}`);
|
|
7264
|
+
}
|
|
7265
|
+
/** 区间的下界在插入点之后才平移,上界只要不在插入点之前就跟着长 */
|
|
7266
|
+
function growRange(ref, at, count) {
|
|
7267
|
+
const [from = "", to] = ref.split(":");
|
|
7268
|
+
if (to === void 0) return shiftAddress(from, at, count);
|
|
7269
|
+
const start = parseCellAddress(from);
|
|
7270
|
+
const end = parseCellAddress(to);
|
|
7271
|
+
const nextStart = start.row + 1 >= at ? {
|
|
7272
|
+
...start,
|
|
7273
|
+
row: start.row + count
|
|
7274
|
+
} : start;
|
|
7275
|
+
const nextEnd = end.row + 1 >= at - 1 ? {
|
|
7276
|
+
...end,
|
|
7277
|
+
row: end.row + count
|
|
7278
|
+
} : end;
|
|
7279
|
+
return `${formatCellAddress(nextStart)}:${formatCellAddress(nextEnd)}`;
|
|
7280
|
+
}
|
|
7281
|
+
const RANGE_IN_FORMULA = /\$?([A-Z]{1,3})\$?([1-9][0-9]*):\$?([A-Z]{1,3})\$?([1-9][0-9]*)/g;
|
|
7282
|
+
/** `SUM(G6:G12)` → `SUM(G6:G25)`:公式里的区间同样要跟着长 */
|
|
7283
|
+
function growFormulaRanges(formula, at, count) {
|
|
7284
|
+
return formula.replace(RANGE_IN_FORMULA, (whole, startCol, startRow, endCol, endRow) => {
|
|
7285
|
+
const start = Number(startRow);
|
|
7286
|
+
const end = Number(endRow);
|
|
7287
|
+
const nextStart = start >= at ? start + count : start;
|
|
7288
|
+
const nextEnd = end >= at - 1 ? end + count : end;
|
|
7289
|
+
return nextStart === start && nextEnd === end ? whole : `${startCol}${nextStart}:${endCol}${nextEnd}`;
|
|
7290
|
+
});
|
|
7291
|
+
}
|
|
7292
|
+
/**
|
|
7293
|
+
* 写值(FR-42.5)。数字写成数字,其余写成文本,不做类型推断(FR-42.26)。
|
|
7294
|
+
*
|
|
7295
|
+
* 用 `inlineStr` 而不是往共享串表追加:改动的数据部件从两个降到一个(设计 43 §1.4)。
|
|
7296
|
+
* 单元格有三种形态(空壳 / 带值 / 不存在),三支都显式处理——静默跳过正是 FR-42.6 要禁的。
|
|
7297
|
+
*
|
|
7298
|
+
* @throws WebSkillError 目标格是公式格
|
|
7299
|
+
*/
|
|
7300
|
+
function fillCell(doc, address, value) {
|
|
7301
|
+
const parsed = parseCellAddress(address);
|
|
7302
|
+
const normalized = formatCellAddress(parsed);
|
|
7303
|
+
const row = rowsOf(doc).find((candidate) => rowNumberOf(candidate) === parsed.row + 1) ?? createRow(doc, parsed.row + 1);
|
|
7304
|
+
let cell = byTag$1(row, "c").find((candidate) => candidate.getAttribute("r") === normalized);
|
|
7305
|
+
if (cell === void 0) cell = createCell(doc, row, parsed.column, normalized);
|
|
7306
|
+
const formula = [...cell.children].find((child) => child.localName === "f");
|
|
7307
|
+
if (formula !== void 0) {
|
|
7308
|
+
const body = formula.textContent ?? "";
|
|
7309
|
+
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.`);
|
|
7310
|
+
}
|
|
7311
|
+
while (cell.firstChild !== null) cell.removeChild(cell.firstChild);
|
|
7312
|
+
if (value === "") {
|
|
7313
|
+
cell.removeAttribute("t");
|
|
7314
|
+
return;
|
|
7315
|
+
}
|
|
7316
|
+
if (looksNumeric(value)) {
|
|
7317
|
+
cell.removeAttribute("t");
|
|
7318
|
+
const v = doc.createElementNS(MAIN_NS, "v");
|
|
7319
|
+
v.textContent = value;
|
|
7320
|
+
cell.appendChild(v);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
cell.setAttribute("t", "inlineStr");
|
|
7324
|
+
const is = doc.createElementNS(MAIN_NS, "is");
|
|
7325
|
+
const t = doc.createElementNS(MAIN_NS, "t");
|
|
7326
|
+
if (value !== value.trim()) t.setAttribute("xml:space", "preserve");
|
|
7327
|
+
t.textContent = value;
|
|
7328
|
+
is.appendChild(t);
|
|
7329
|
+
cell.appendChild(is);
|
|
7330
|
+
}
|
|
7331
|
+
/**
|
|
7332
|
+
* 只有**原样往返**的才算数字:`Number(x)` 转回字符串必须与原文逐字符相同。
|
|
7333
|
+
*
|
|
7334
|
+
* 这一条同时挡住了 `"0123"`(编号、身份证、电话)与 `" 12 "`(带空白的文本),
|
|
7335
|
+
* 它们转回来分别是 `"123"` 和 `"12"`,与原文不同,所以仍然写成文本(AC-42.9)。
|
|
7336
|
+
*/
|
|
7337
|
+
function looksNumeric(value) {
|
|
7338
|
+
const parsed = Number(value);
|
|
7339
|
+
return Number.isFinite(parsed) && String(parsed) === value;
|
|
7340
|
+
}
|
|
7341
|
+
/** 新建节点必须用 createElementNS:在有默认命名空间的文档里 createElement 会带出 `xmlns=""`,Excel 拒收 */
|
|
7342
|
+
function createRow(doc, rowNumber) {
|
|
7343
|
+
const sheetData = byTag$1(doc, "sheetData")[0];
|
|
7344
|
+
if (sheetData === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", "The worksheet part has no sheetData element.");
|
|
7345
|
+
const row = doc.createElementNS(MAIN_NS, "row");
|
|
7346
|
+
row.setAttribute("r", String(rowNumber));
|
|
7347
|
+
const anchor = rowsOf(doc).find((candidate) => rowNumberOf(candidate) > rowNumber) ?? null;
|
|
7348
|
+
sheetData.insertBefore(row, anchor);
|
|
7349
|
+
return row;
|
|
7350
|
+
}
|
|
7351
|
+
function createCell(doc, row, column, address) {
|
|
7352
|
+
const cell = doc.createElementNS(MAIN_NS, "c");
|
|
7353
|
+
cell.setAttribute("r", address);
|
|
7354
|
+
const anchor = byTag$1(row, "c").find((candidate) => {
|
|
7355
|
+
const other = candidate.getAttribute("r");
|
|
7356
|
+
return other !== null && parseCellAddress(other).column > column;
|
|
7357
|
+
}) ?? null;
|
|
7358
|
+
row.insertBefore(cell, anchor);
|
|
7359
|
+
return cell;
|
|
7360
|
+
}
|
|
7361
|
+
function geometryOf(doc) {
|
|
7362
|
+
const columnWidths = [];
|
|
7363
|
+
for (const col of byTag$1(doc, "col")) {
|
|
7364
|
+
const min = Number(col.getAttribute("min"));
|
|
7365
|
+
const max = Number(col.getAttribute("max"));
|
|
7366
|
+
const width = Number(col.getAttribute("width"));
|
|
7367
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || !Number.isFinite(width)) continue;
|
|
7368
|
+
for (let index = min; index <= max; index += 1) columnWidths[index - 1] = width;
|
|
7369
|
+
}
|
|
7370
|
+
const rowHeights = /* @__PURE__ */ new Map();
|
|
7371
|
+
for (const row of rowsOf(doc)) {
|
|
7372
|
+
const height = Number(row.getAttribute("ht"));
|
|
7373
|
+
if (Number.isFinite(height)) rowHeights.set(rowNumberOf(row), height);
|
|
7374
|
+
}
|
|
7375
|
+
return {
|
|
7376
|
+
columnWidths,
|
|
7377
|
+
rowHeights,
|
|
7378
|
+
defaultRowHeight: Number(byTag$1(doc, "sheetFormatPr")[0]?.getAttribute("defaultRowHeight") ?? 15),
|
|
7379
|
+
merges: byTag$1(doc, "mergeCell").map((merge) => merge.getAttribute("ref")).filter((ref) => ref !== null)
|
|
7380
|
+
};
|
|
7381
|
+
}
|
|
7382
|
+
const DEFAULT_COLUMN_WIDTH = 8.43;
|
|
7383
|
+
/** 锚格所在的框。落在合并区左上角时按整个合并区算(FR-42.11)。 */
|
|
7384
|
+
function anchorBox(geometry, address) {
|
|
7385
|
+
const cell = parseCellAddress(address);
|
|
7386
|
+
let lastColumn = cell.column;
|
|
7387
|
+
let lastRow = cell.row;
|
|
7388
|
+
for (const ref of geometry.merges) {
|
|
7389
|
+
const range = parseCellRange(ref);
|
|
7390
|
+
if (range.start.column === cell.column && range.start.row === cell.row) {
|
|
7391
|
+
lastColumn = range.end.column;
|
|
7392
|
+
lastRow = range.end.row;
|
|
7393
|
+
break;
|
|
7394
|
+
}
|
|
7395
|
+
}
|
|
7396
|
+
let width = 0;
|
|
7397
|
+
for (let column = cell.column; column <= lastColumn; column += 1) width += columnWidthToPixels(geometry.columnWidths[column] ?? DEFAULT_COLUMN_WIDTH) * EMU_PER_PIXEL;
|
|
7398
|
+
let height = 0;
|
|
7399
|
+
for (let row = cell.row; row <= lastRow; row += 1) height += (geometry.rowHeights.get(row + 1) ?? geometry.defaultRowHeight) * EMU_PER_POINT;
|
|
7400
|
+
return {
|
|
7401
|
+
width,
|
|
7402
|
+
height,
|
|
7403
|
+
span: lastColumn - cell.column + 1
|
|
7404
|
+
};
|
|
7405
|
+
}
|
|
7406
|
+
function xmlPart(text) {
|
|
7407
|
+
return new TextEncoder().encode(DECLARATION + text);
|
|
7408
|
+
}
|
|
7409
|
+
/** 给工作表装上 drawing 全家桶:drawing 部件、两条 rels、media、Content_Types 登记 */
|
|
7410
|
+
function attachImages(pkg, doc, sheetPath, images, geometry) {
|
|
7411
|
+
if (images.length === 0) return;
|
|
7412
|
+
const sheetFile = sheetPath.slice(sheetPath.lastIndexOf("/") + 1);
|
|
7413
|
+
const sheetIndex = /(\d+)\.xml$/.exec(sheetFile)?.[1] ?? "1";
|
|
7414
|
+
const drawingPath = `xl/drawings/drawing${sheetIndex}.xml`;
|
|
7415
|
+
const anchors = [];
|
|
7416
|
+
const drawingRels = [];
|
|
7417
|
+
images.forEach((image, index) => {
|
|
7418
|
+
const size = readImageSize(image.bytes);
|
|
7419
|
+
if (size === void 0) throw new WebSkillError("TEMPLATE_IMAGE_UNRESOLVED", `The image for cell ${image.address} is not a PNG or JPEG file.`);
|
|
7420
|
+
const mediaName = `image${index + 1}.${image.extension}`;
|
|
7421
|
+
pkg.set(`xl/media/${mediaName}`, image.bytes);
|
|
7422
|
+
ensureDefaultExtension(pkg, image.extension, image.contentType);
|
|
7423
|
+
drawingRels.push(`<Relationship Id="rIdImg${index + 1}" Type="${REL_NS$1}/image" Target="../media/${mediaName}"/>`);
|
|
7424
|
+
const box = anchorBox(geometry, image.address);
|
|
7425
|
+
const fitted = fitWithin({
|
|
7426
|
+
width: size.width * EMU_PER_PIXEL,
|
|
7427
|
+
height: size.height * EMU_PER_PIXEL
|
|
7428
|
+
}, box);
|
|
7429
|
+
const cell = parseCellAddress(image.address);
|
|
7430
|
+
const id = index + 1;
|
|
7431
|
+
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>`);
|
|
7432
|
+
});
|
|
7433
|
+
pkg.set(drawingPath, xmlPart(`<xdr:wsDr xmlns:xdr="${DRAWING_NS}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">${anchors.join("")}</xdr:wsDr>`));
|
|
7434
|
+
pkg.set(`xl/drawings/_rels/drawing${sheetIndex}.xml.rels`, xmlPart(`<Relationships xmlns="${PACKAGE_REL_NS}">${drawingRels.join("")}</Relationships>`));
|
|
7435
|
+
ensureOverride(pkg, `/${drawingPath}`, DRAWING_CONTENT_TYPE);
|
|
7436
|
+
const relId = addSheetRelationship(pkg, `xl/worksheets/_rels/${sheetFile}.rels`, `${REL_NS$1}/drawing`, `../drawings/drawing${sheetIndex}.xml`);
|
|
7437
|
+
const existing = byTag$1(doc, "drawing")[0];
|
|
7438
|
+
if (existing !== void 0) existing.setAttributeNS(REL_NS$1, "r:id", relId);
|
|
7439
|
+
else {
|
|
7440
|
+
const drawing = doc.createElementNS(MAIN_NS, "drawing");
|
|
7441
|
+
drawing.setAttributeNS(REL_NS$1, "r:id", relId);
|
|
7442
|
+
doc.documentElement.appendChild(drawing);
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
/** 工作表的 rels 可能整个不存在(工作面移交表就没有),缺了就建 */
|
|
7446
|
+
function addSheetRelationship(pkg, relsPath, type, target) {
|
|
7447
|
+
const existing = pkg.get(relsPath);
|
|
7448
|
+
if (existing === void 0) {
|
|
7449
|
+
pkg.set(relsPath, xmlPart(`<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rIdDrawing" Type="${type}" Target="${target}"/></Relationships>`));
|
|
7450
|
+
return "rIdDrawing";
|
|
7451
|
+
}
|
|
7452
|
+
const text = new TextDecoder().decode(existing);
|
|
7453
|
+
const already = new RegExp(`<Relationship[^>]*Target="${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*/>`).exec(text);
|
|
7454
|
+
if (already !== null) return /Id="([^"]+)"/.exec(already[0])?.[1] ?? "rIdDrawing";
|
|
7455
|
+
const updated = text.replace("</Relationships>", `<Relationship Id="rIdDrawing" Type="${type}" Target="${target}"/></Relationships>`);
|
|
7456
|
+
pkg.set(relsPath, new TextEncoder().encode(updated));
|
|
7457
|
+
return "rIdDrawing";
|
|
7458
|
+
}
|
|
7459
|
+
/**
|
|
7460
|
+
* 照模板出新文件。
|
|
7461
|
+
*
|
|
7462
|
+
* 顺序是**先插行、后填值**:填值用的地址是插行之后的坐标,反过来会逼模型心算偏移。
|
|
7463
|
+
*
|
|
7464
|
+
* @throws WebSkillError 模板不是 xlsx、地址不合法、目标不存在、插行越界
|
|
7465
|
+
*/
|
|
7466
|
+
async function fillXlsxTemplate(bytes, spec) {
|
|
7467
|
+
const pkg = await readOoxmlPackage(bytes);
|
|
7468
|
+
const sheets = listSheets(pkg);
|
|
7469
|
+
const touched = /* @__PURE__ */ new Map();
|
|
7470
|
+
const partOf = (name) => {
|
|
7471
|
+
const entry = resolveSheet(sheets, name);
|
|
7472
|
+
const cached = touched.get(entry.path) ?? parseXmlPart(pkg, entry.path);
|
|
7473
|
+
touched.set(entry.path, cached);
|
|
7474
|
+
return {
|
|
7475
|
+
entry,
|
|
7476
|
+
...cached
|
|
7477
|
+
};
|
|
7478
|
+
};
|
|
7479
|
+
for (const insert of spec.rows ?? []) {
|
|
7480
|
+
const { doc } = partOf(insert.sheet);
|
|
7481
|
+
insertRows(pkg, doc, insert);
|
|
7482
|
+
}
|
|
7483
|
+
for (const fill of spec.cells ?? []) {
|
|
7484
|
+
const { doc } = partOf(fill.sheet);
|
|
7485
|
+
fillCell(doc, fill.address, fill.value);
|
|
7486
|
+
}
|
|
7487
|
+
const bySheet = /* @__PURE__ */ new Map();
|
|
7488
|
+
for (const image of spec.images ?? []) {
|
|
7489
|
+
const { entry } = partOf(image.sheet);
|
|
7490
|
+
bySheet.set(entry.path, [...bySheet.get(entry.path) ?? [], image]);
|
|
7491
|
+
}
|
|
7492
|
+
for (const [path, images] of bySheet) {
|
|
7493
|
+
const cached = touched.get(path);
|
|
7494
|
+
if (cached === void 0) continue;
|
|
7495
|
+
attachImages(pkg, cached.doc, path, images, geometryOf(cached.doc));
|
|
7496
|
+
}
|
|
7497
|
+
if ([...touched.values()].some(({ doc }) => byTag$1(doc, "f").length > 0)) requestFullCalcOnLoad(pkg);
|
|
7498
|
+
for (const [path, { declaration, doc }] of touched) pkg.set(path, serializeXmlPart(declaration, doc));
|
|
7499
|
+
stripMacros(pkg);
|
|
7500
|
+
assertOoxmlPackage(pkg);
|
|
7501
|
+
return writeOoxmlPackage(pkg);
|
|
7502
|
+
}
|
|
7503
|
+
/** `<calcPr>` 在 CT_Workbook 的序列里排在这几个之后;追加到末尾会落到 `extLst` 后面,那是不合法的 */
|
|
7504
|
+
const BEFORE_CALC_PR = [
|
|
7505
|
+
"sheets",
|
|
7506
|
+
"functionGroups",
|
|
7507
|
+
"externalReferences",
|
|
7508
|
+
"definedNames"
|
|
7509
|
+
];
|
|
7510
|
+
/**
|
|
7511
|
+
* 让 Excel 打开时重算一遍(FR-42.27)。
|
|
7512
|
+
*
|
|
7513
|
+
* 公式格里存着上一次的计算结果。改了它依赖的那些格之后,这个结果就是错的,
|
|
7514
|
+
* 而 Excel 默认信任缓存——用户看到的是一张「明细都填好了、合计还是 0」的表,
|
|
7515
|
+
* 直到他随手双击一个格才恢复正常。只在动过的表里确实有公式时才写这一笔。
|
|
7516
|
+
*/
|
|
7517
|
+
function requestFullCalcOnLoad(pkg) {
|
|
7518
|
+
const { declaration, doc } = parseXmlPart(pkg, "xl/workbook.xml");
|
|
7519
|
+
const existing = byTag$1(doc, "calcPr")[0];
|
|
7520
|
+
const calcPr = existing ?? doc.createElementNS(MAIN_NS, "calcPr");
|
|
7521
|
+
calcPr.setAttribute("fullCalcOnLoad", "1");
|
|
7522
|
+
if (existing === void 0) {
|
|
7523
|
+
const workbook = doc.documentElement;
|
|
7524
|
+
const after = [...workbook.children].filter((node) => BEFORE_CALC_PR.includes(node.localName)).pop();
|
|
7525
|
+
workbook.insertBefore(calcPr, after?.nextSibling ?? null);
|
|
7526
|
+
}
|
|
7527
|
+
pkg.set("xl/workbook.xml", serializeXmlPart(declaration, doc));
|
|
7528
|
+
}
|
|
7529
|
+
|
|
7530
|
+
//#endregion
|
|
7531
|
+
//#region ../browser/src/document/docxTemplate.ts
|
|
7532
|
+
/**
|
|
7533
|
+
* 照着 docx 模板出新文件(0.22.0 分册 42 · 设计 43 §5)。
|
|
7534
|
+
*
|
|
7535
|
+
* 与 xlsx 同一条路,结构还更简单:只有 `word/document.xml` 一个数据部件。
|
|
7536
|
+
* 保格式的办法也同构——改已有 run 的文字而不是重建 run,`<w:rPr>` 就自己活下来了。
|
|
7537
|
+
*/
|
|
7538
|
+
const WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
7539
|
+
const REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
7540
|
+
const DOCUMENT = "word/document.xml";
|
|
7541
|
+
const DOCUMENT_RELS = "word/_rels/document.xml.rels";
|
|
7542
|
+
const byTag = (scope, name) => [...scope.getElementsByTagNameNS("*", name)];
|
|
7543
|
+
const childrenNamed = (scope, name) => [...scope.children].filter((node) => node.localName === name);
|
|
7544
|
+
/** 顶层表格(不含嵌套表),顺序即模型看到的表序 */
|
|
7545
|
+
function tablesOf(doc) {
|
|
7546
|
+
const body = byTag(doc, "body")[0];
|
|
7547
|
+
return body === void 0 ? [] : childrenNamed(body, "tbl");
|
|
7548
|
+
}
|
|
7549
|
+
/** 正文段落:只算 body 的直接子节点,表格内的段落不计入序号 */
|
|
7550
|
+
function paragraphsOf(doc) {
|
|
7551
|
+
const body = byTag(doc, "body")[0];
|
|
7552
|
+
return body === void 0 ? [] : childrenNamed(body, "p");
|
|
7553
|
+
}
|
|
7554
|
+
const textOf = (scope) => byTag(scope, "t").map((node) => node.textContent ?? "").join("");
|
|
7555
|
+
const intAttr = (element, name) => {
|
|
7556
|
+
const raw = element?.getAttributeNS(WORD_NS, name) ?? element?.getAttribute(`w:${name}`) ?? element?.getAttribute(name);
|
|
7557
|
+
const value = Number(raw);
|
|
7558
|
+
return Number.isFinite(value) ? value : void 0;
|
|
7559
|
+
};
|
|
7560
|
+
/** 读带坐标的骨架(FR-42.1) */
|
|
7561
|
+
async function readDocxTemplate(bytes) {
|
|
7562
|
+
const { doc } = parseXmlPart(await readOoxmlPackage(bytes), DOCUMENT);
|
|
7563
|
+
const paragraphs = paragraphsOf(doc).map((paragraph, index) => ({
|
|
7564
|
+
index,
|
|
7565
|
+
text: textOf(paragraph)
|
|
7566
|
+
}));
|
|
7567
|
+
const cells = [];
|
|
7568
|
+
tablesOf(doc).forEach((table, tableIndex) => {
|
|
7569
|
+
childrenNamed(table, "tr").forEach((row, rowIndex) => {
|
|
7570
|
+
childrenNamed(row, "tc").forEach((cell, columnIndex) => {
|
|
7571
|
+
const properties = childrenNamed(cell, "tcPr")[0];
|
|
7572
|
+
const gridSpan = properties === void 0 ? void 0 : intAttr(childrenNamed(properties, "gridSpan")[0], "val");
|
|
7573
|
+
const merge = properties === void 0 ? void 0 : childrenNamed(properties, "vMerge")[0];
|
|
7574
|
+
cells.push({
|
|
7575
|
+
table: tableIndex,
|
|
7576
|
+
row: rowIndex,
|
|
7577
|
+
column: columnIndex,
|
|
7578
|
+
text: textOf(cell),
|
|
7579
|
+
...gridSpan === void 0 ? {} : { gridSpan },
|
|
7580
|
+
...merge === void 0 ? {} : { vMerge: merge.getAttributeNS(WORD_NS, "val") ?? merge.getAttribute("w:val") ?? "continue" }
|
|
7581
|
+
});
|
|
7582
|
+
});
|
|
7583
|
+
});
|
|
7584
|
+
});
|
|
7585
|
+
return {
|
|
7586
|
+
paragraphs,
|
|
7587
|
+
cells
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
7590
|
+
/** @throws WebSkillError 坐标指到不存在的表 / 行 / 列 / 段落 */
|
|
7591
|
+
function locate(doc, target) {
|
|
7592
|
+
if (target.table !== void 0) {
|
|
7593
|
+
const tables = tablesOf(doc);
|
|
7594
|
+
const table = tables[target.table];
|
|
7595
|
+
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).`);
|
|
7596
|
+
const rows = childrenNamed(table, "tr");
|
|
7597
|
+
const row = rows[target.row ?? -1];
|
|
7598
|
+
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).`);
|
|
7599
|
+
const cells = childrenNamed(row, "tc");
|
|
7600
|
+
const cell = cells[target.column ?? -1];
|
|
7601
|
+
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).`);
|
|
7602
|
+
return cell;
|
|
7603
|
+
}
|
|
7604
|
+
const paragraphs = paragraphsOf(doc);
|
|
7605
|
+
const paragraph = paragraphs[target.paragraph ?? -1];
|
|
7606
|
+
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).`);
|
|
7607
|
+
return paragraph;
|
|
7608
|
+
}
|
|
7609
|
+
/**
|
|
7610
|
+
* 写文字:改已有 run 的 `<w:t>`,多余的 run 删掉。
|
|
7611
|
+
*
|
|
7612
|
+
* 不重建 run,是因为 `<w:rPr>`(字体、字号、加粗)就挂在 run 上——
|
|
7613
|
+
* 与 xlsx 那边靠 `<c>` 的 `s` 属性保格式是同一个道理(设计 43 §1.2)。
|
|
7614
|
+
*/
|
|
7615
|
+
function fillText(doc, scope, value) {
|
|
7616
|
+
const paragraphs = scope.localName === "p" ? [scope] : childrenNamed(scope, "p");
|
|
7617
|
+
const first = paragraphs[0];
|
|
7618
|
+
if (first === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", "The target has no paragraph to write text into.");
|
|
7619
|
+
paragraphs.slice(1).forEach((paragraph) => {
|
|
7620
|
+
for (const run of childrenNamed(paragraph, "r")) paragraph.removeChild(run);
|
|
7621
|
+
});
|
|
7622
|
+
const runs = childrenNamed(first, "r").filter((run) => byTag(run, "drawing").length === 0);
|
|
7623
|
+
const keep = runs[0];
|
|
7624
|
+
runs.slice(1).forEach((run) => first.removeChild(run));
|
|
7625
|
+
if (keep === void 0) {
|
|
7626
|
+
if (value === "") return;
|
|
7627
|
+
const run = doc.createElementNS(WORD_NS, "w:r");
|
|
7628
|
+
run.appendChild(textNode(doc, value));
|
|
7629
|
+
first.appendChild(run);
|
|
7630
|
+
return;
|
|
7631
|
+
}
|
|
7632
|
+
if (value === "") {
|
|
7633
|
+
first.removeChild(keep);
|
|
7634
|
+
return;
|
|
7635
|
+
}
|
|
7636
|
+
for (const node of childrenNamed(keep, "t")) keep.removeChild(node);
|
|
7637
|
+
keep.appendChild(textNode(doc, value));
|
|
7638
|
+
}
|
|
7639
|
+
/** 新建节点用 createElementNS 带前缀:document.xml 的默认命名空间不是 w,裸 createElement 会带出 xmlns="" */
|
|
7640
|
+
function textNode(doc, value) {
|
|
7641
|
+
const node = doc.createElementNS(WORD_NS, "w:t");
|
|
7642
|
+
if (value !== value.trim()) node.setAttribute("xml:space", "preserve");
|
|
7643
|
+
node.textContent = value;
|
|
7644
|
+
return node;
|
|
7645
|
+
}
|
|
7646
|
+
/** 版心宽度:`pgSz` 减两边 `pgMar`(twip) */
|
|
7647
|
+
function bodyWidthTwips(doc) {
|
|
7648
|
+
const size = byTag(doc, "pgSz")[0];
|
|
7649
|
+
const margin = byTag(doc, "pgMar")[0];
|
|
7650
|
+
const width = intAttr(size, "w") ?? 11906;
|
|
7651
|
+
const left = intAttr(margin, "left") ?? 1440;
|
|
7652
|
+
const right = intAttr(margin, "right") ?? 1440;
|
|
7653
|
+
return Math.max(width - left - right, 0);
|
|
7654
|
+
}
|
|
7655
|
+
/**
|
|
7656
|
+
* 落点的可用框(twip)。
|
|
7657
|
+
*
|
|
7658
|
+
* 宽度优先取单元格自己的 `tcW`——`工作面移交表.docx` 的表格是故意比版心宽的,
|
|
7659
|
+
* 退回版心宽会把图算小(设计 43 §1.6)。`tcW` 缺失时按 `gridSpan` 累加 `tblGrid`。
|
|
7660
|
+
* 行声明了 `trHeight` 才有高度约束,没有就只受宽度约束。
|
|
7661
|
+
*/
|
|
7662
|
+
function boxTwipsOf(doc, scope) {
|
|
7663
|
+
if (scope.localName !== "tc") return {
|
|
7664
|
+
width: bodyWidthTwips(doc),
|
|
7665
|
+
height: 0
|
|
7666
|
+
};
|
|
7667
|
+
const properties = childrenNamed(scope, "tcPr")[0];
|
|
7668
|
+
const width = intAttr(properties === void 0 ? void 0 : childrenNamed(properties, "tcW")[0], "w");
|
|
7669
|
+
const row = scope.parentElement;
|
|
7670
|
+
const table = row?.parentElement;
|
|
7671
|
+
const span = properties === void 0 ? 1 : intAttr(childrenNamed(properties, "gridSpan")[0], "val") ?? 1;
|
|
7672
|
+
let resolved = width !== void 0 && width > 0 ? width : 0;
|
|
7673
|
+
if (resolved === 0 && table !== null && table !== void 0) {
|
|
7674
|
+
const grid = byTag(table, "gridCol").map((column) => intAttr(column, "w") ?? 0);
|
|
7675
|
+
const start = row === null ? 0 : childrenNamed(row, "tc").indexOf(scope);
|
|
7676
|
+
resolved = grid.slice(start, start + span).reduce((sum, value) => sum + value, 0);
|
|
7677
|
+
}
|
|
7678
|
+
if (resolved === 0) resolved = bodyWidthTwips(doc);
|
|
7679
|
+
const rowProperties = row === null ? void 0 : childrenNamed(row, "trPr")[0];
|
|
7680
|
+
const height = rowProperties === void 0 ? 0 : intAttr(childrenNamed(rowProperties, "trHeight")[0], "val") ?? 0;
|
|
7681
|
+
return {
|
|
7682
|
+
width: resolved,
|
|
7683
|
+
height
|
|
7684
|
+
};
|
|
7685
|
+
}
|
|
7686
|
+
function nextRelationshipId(pkg) {
|
|
7687
|
+
const text = new TextDecoder().decode(pkg.get(DOCUMENT_RELS) ?? /* @__PURE__ */ new Uint8Array());
|
|
7688
|
+
let max = 0;
|
|
7689
|
+
for (const match of text.matchAll(/Id="rId(\d+)"/g)) max = Math.max(max, Number(match[1]));
|
|
7690
|
+
return `rId${max + 1}`;
|
|
7691
|
+
}
|
|
7692
|
+
function addImageRelationship(pkg, id, target) {
|
|
7693
|
+
const bytes = pkg.get(DOCUMENT_RELS);
|
|
7694
|
+
if (bytes === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `The template has no ${DOCUMENT_RELS} part.`);
|
|
7695
|
+
const updated = new TextDecoder().decode(bytes).replace("</Relationships>", `<Relationship Id="${id}" Type="${REL_NS}/image" Target="${target}"/></Relationships>`);
|
|
7696
|
+
pkg.set(DOCUMENT_RELS, new TextEncoder().encode(updated));
|
|
7697
|
+
}
|
|
7698
|
+
/**
|
|
7699
|
+
* 插图(FR-42.18)。用 `<wp:inline>` 而不是 `<wp:anchor>`:
|
|
7700
|
+
* inline 参与文档流,跟着单元格走;anchor 是浮动的,分页一变就跑位。
|
|
7701
|
+
*/
|
|
7702
|
+
function attachImage(pkg, doc, scope, image, index) {
|
|
7703
|
+
const size = readImageSize(image.bytes);
|
|
7704
|
+
if (size === void 0) throw new WebSkillError("TEMPLATE_IMAGE_UNRESOLVED", "The image to insert is not a PNG or JPEG file.");
|
|
7705
|
+
const mediaName = `webskillImage${index + 1}.${image.extension}`;
|
|
7706
|
+
pkg.set(`word/media/${mediaName}`, image.bytes);
|
|
7707
|
+
ensureDefaultExtension(pkg, image.extension, image.contentType);
|
|
7708
|
+
const relId = nextRelationshipId(pkg);
|
|
7709
|
+
addImageRelationship(pkg, relId, `media/${mediaName}`);
|
|
7710
|
+
const box = boxTwipsOf(doc, scope);
|
|
7711
|
+
const fitted = fitWithin({
|
|
7712
|
+
width: size.width * EMU_PER_PIXEL,
|
|
7713
|
+
height: size.height * EMU_PER_PIXEL
|
|
7714
|
+
}, {
|
|
7715
|
+
width: box.width * 635,
|
|
7716
|
+
height: box.height * 635
|
|
7717
|
+
});
|
|
7718
|
+
const id = index + 1;
|
|
7719
|
+
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>`;
|
|
7720
|
+
const parsed = new DOMParser().parseFromString(drawing, "application/xml");
|
|
7721
|
+
if (parsed.getElementsByTagName("parsererror").length > 0) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Failed to build the DrawingML fragment for the image.");
|
|
7722
|
+
const run = doc.importNode(parsed.documentElement, true);
|
|
7723
|
+
const paragraphs = scope.localName === "p" ? [scope] : childrenNamed(scope, "p");
|
|
7724
|
+
const host = paragraphs[paragraphs.length - 1];
|
|
7725
|
+
if (host === void 0) throw new WebSkillError("TEMPLATE_TARGET_MISSING", "The target has no paragraph to place the image in.");
|
|
7726
|
+
host.appendChild(run);
|
|
7727
|
+
}
|
|
7728
|
+
/**
|
|
7729
|
+
* 照模板出新文件。
|
|
7730
|
+
*
|
|
7731
|
+
* @throws WebSkillError 模板不是 docx、坐标指到不存在的表 / 行 / 列 / 段落、图片认不出来
|
|
7732
|
+
*/
|
|
7733
|
+
async function fillDocxTemplate(bytes, spec) {
|
|
7734
|
+
const pkg = await readOoxmlPackage(bytes);
|
|
7735
|
+
const { declaration, doc } = parseXmlPart(pkg, DOCUMENT);
|
|
7736
|
+
for (const text of spec.texts ?? []) fillText(doc, locate(doc, text), text.value);
|
|
7737
|
+
(spec.images ?? []).forEach((image, index) => attachImage(pkg, doc, locate(doc, image), image, index));
|
|
7738
|
+
pkg.set(DOCUMENT, serializeXmlPart(declaration, doc));
|
|
7739
|
+
stripMacros(pkg);
|
|
7740
|
+
assertOoxmlPackage(pkg);
|
|
7741
|
+
return writeOoxmlPackage(pkg);
|
|
7742
|
+
}
|
|
7743
|
+
|
|
5877
7744
|
//#endregion
|
|
5878
7745
|
//#region ../browser/src/viewer/viewerCsp.ts
|
|
5879
7746
|
/**
|
|
@@ -5908,6 +7775,24 @@ const SANDBOX_TOKENS = [
|
|
|
5908
7775
|
"allow-downloads"
|
|
5909
7776
|
];
|
|
5910
7777
|
/**
|
|
7778
|
+
* 投放面图片来源的单一事实源(0.22.0 分册 13 FR-13.9)。
|
|
7779
|
+
*
|
|
7780
|
+
* **不含宿主自身**:两套宿主写法不同——站点写自己的 origin(opaque origin 下 `'self'` 匹配不上),
|
|
7781
|
+
* 扩展的 sandbox 页写 `'self'`。能收敛的是**除宿主之外**放行了哪些来源,
|
|
7782
|
+
* 而那恰恰是会漂走的部分(扩展的 sandbox CSP 此前整条 `img-src` 都不存在,等于全放)。
|
|
7783
|
+
* @experimental
|
|
7784
|
+
*/
|
|
7785
|
+
const VIEWER_IMG_SRC_BASE = ["data:"];
|
|
7786
|
+
/**
|
|
7787
|
+
* 降级形态(外链图,FR-13.6)额外需要的来源。
|
|
7788
|
+
*
|
|
7789
|
+
* ⚠️ 加它进来的代价见 `ViewerCspOptions.imgSrc`:投放面因此多了一条外发信道。
|
|
7790
|
+
* `http:` 也在其中,因为需求裁决了「`http` 尽力而为」(分册 13 §2.2)——
|
|
7791
|
+
* 抓得到就嵌字节,抓不到才落到这条外链上,此时拦掉它只会让图变成一个破图标。
|
|
7792
|
+
* @experimental
|
|
7793
|
+
*/
|
|
7794
|
+
const VIEWER_IMG_SRC_REMOTE = ["https:", "http:"];
|
|
7795
|
+
/**
|
|
5911
7796
|
* 宿主配置值不得夹带 CSP 分隔符 —— 否则一个来源字符串就能追加任意指令,
|
|
5912
7797
|
* 白名单形同虚设。这是配置注入,不是理论风险。
|
|
5913
7798
|
*/
|
|
@@ -5933,7 +7818,7 @@ function viewerCspHeader(options) {
|
|
|
5933
7818
|
"default-src 'none'",
|
|
5934
7819
|
`script-src ${host}`,
|
|
5935
7820
|
`style-src ${host} 'unsafe-inline'`,
|
|
5936
|
-
directive("img-src", [host,
|
|
7821
|
+
directive("img-src", [host, ...VIEWER_IMG_SRC_BASE], options.imgSrc),
|
|
5937
7822
|
directive("font-src", [host], options.fontSrc),
|
|
5938
7823
|
directive("connect-src", [], options.connectSrc)
|
|
5939
7824
|
].join("; ");
|
|
@@ -5957,13 +7842,101 @@ function diffSandboxTokens(sdk, host) {
|
|
|
5957
7842
|
|
|
5958
7843
|
//#endregion
|
|
5959
7844
|
//#region ../browser/src/viewer/documentSurface.ts
|
|
5960
|
-
|
|
7845
|
+
/**
|
|
7846
|
+
* 载荷硬兜底(0.22.0 FR-10.1)。
|
|
7847
|
+
*
|
|
7848
|
+
* 从 0.11.0 的 2 MB 提到这里,是因为那个值从未被论证过,而实测表明它离任何真实边界都很远:
|
|
7849
|
+
* 128 MB 的结构化克隆只要 23 ms,V8 的字符串上限在 512 MB 左右才会抛 `RangeError`。
|
|
7850
|
+
* 留 4 倍余量是给同一个标签页里的 React、图表库与对话历史用的。
|
|
7851
|
+
*/
|
|
7852
|
+
const DEFAULT_MAX_VIEWER_PAYLOAD_BYTES = 128e6;
|
|
7853
|
+
/** 载荷软阈值:到这里不是「慢」,而是「这份文档异常大」,值得先告诉用户一声 */
|
|
7854
|
+
const DEFAULT_SOFT_VIEWER_PAYLOAD_BYTES = 32e6;
|
|
7855
|
+
/** 像素硬兜底,约合 1 GB 位图;实测 60 张 4000×3000 达 2.7 GB,那是崩标签页的量级 */
|
|
7856
|
+
const DEFAULT_MAX_VIEWER_PAYLOAD_PIXELS = 256e6;
|
|
7857
|
+
/** 像素软阈值,约合 256 MB 位图;实测 60 M 像素仍然流畅 */
|
|
7858
|
+
const DEFAULT_SOFT_VIEWER_PAYLOAD_PIXELS = 64e6;
|
|
5961
7859
|
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
5962
7860
|
/** hello 的重发间隔:viewer 脚本加载完之前投出去的消息全部丢失 */
|
|
5963
7861
|
const HELLO_RETRY_MS = 100;
|
|
7862
|
+
/** 拒绝消息里列举的「最大的几张图」条数:两三张巨图撑起整份文档是常态,列太多反而没重点 */
|
|
7863
|
+
const LARGEST_IMAGES_IN_MESSAGE = 3;
|
|
5964
7864
|
/** 审计事件类型(与既有事件同一条链) */
|
|
5965
7865
|
const DOCUMENT_SURFACE_AUDIT_EVENT = "document.surface.open";
|
|
5966
|
-
|
|
7866
|
+
/**
|
|
7867
|
+
* 数载荷有多大,**不物化任何中间副本**(FR-10.3)。
|
|
7868
|
+
*
|
|
7869
|
+
* 被换掉的写法是 `new TextEncoder().encode(JSON.stringify(value)).length`:
|
|
7870
|
+
* 它为了读一个 `.length`,先物化一份与文档等长的字符串、再物化一份等长的字节数组,
|
|
7871
|
+
* 峰值内存是文档的三倍。实测 341 MB 的文档上它要 712 ms,
|
|
7872
|
+
* 而它保护的那次 `postMessage` 只要 95 ms——度量比传输还慢七倍。
|
|
7873
|
+
*
|
|
7874
|
+
* 用显式栈而不是递归:投放的是技能生成的数据,嵌套深度不由我们决定。
|
|
7875
|
+
*/
|
|
7876
|
+
function payloadUnits(value) {
|
|
7877
|
+
let units = 0;
|
|
7878
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
7879
|
+
const stack = [value];
|
|
7880
|
+
while (stack.length > 0) {
|
|
7881
|
+
const current = stack.pop();
|
|
7882
|
+
if (typeof current === "string") {
|
|
7883
|
+
units += current.length;
|
|
7884
|
+
continue;
|
|
7885
|
+
}
|
|
7886
|
+
if (typeof current !== "object" || current === null) continue;
|
|
7887
|
+
if (seen.has(current)) continue;
|
|
7888
|
+
seen.add(current);
|
|
7889
|
+
if (Array.isArray(current)) {
|
|
7890
|
+
for (const item of current) stack.push(item);
|
|
7891
|
+
continue;
|
|
7892
|
+
}
|
|
7893
|
+
if (ArrayBuffer.isView(current)) {
|
|
7894
|
+
units += current.byteLength;
|
|
7895
|
+
continue;
|
|
7896
|
+
}
|
|
7897
|
+
for (const [key, item] of Object.entries(current)) {
|
|
7898
|
+
units += key.length;
|
|
7899
|
+
stack.push(item);
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
return units;
|
|
7903
|
+
}
|
|
7904
|
+
function payloadCost(input) {
|
|
7905
|
+
let pixels = 0;
|
|
7906
|
+
for (const image of input.images ?? []) pixels += image.width * image.height;
|
|
7907
|
+
return {
|
|
7908
|
+
units: payloadUnits(input.document),
|
|
7909
|
+
pixels
|
|
7910
|
+
};
|
|
7911
|
+
}
|
|
7912
|
+
function limitsOf(options) {
|
|
7913
|
+
return {
|
|
7914
|
+
softUnits: options.softPayloadBytes ?? 32e6,
|
|
7915
|
+
hardUnits: options.maxPayloadBytes ?? 128e6,
|
|
7916
|
+
softPixels: options.softPayloadPixels ?? 64e6,
|
|
7917
|
+
hardPixels: options.maxPayloadPixels ?? 256e6
|
|
7918
|
+
};
|
|
7919
|
+
}
|
|
7920
|
+
/** 两维取并集:任一维过线即触发,不得加权合并(FR-10.7) */
|
|
7921
|
+
function triggeredBy(overUnits, overPixels) {
|
|
7922
|
+
if (overUnits && overPixels) return "both";
|
|
7923
|
+
if (overUnits) return "bytes";
|
|
7924
|
+
if (overPixels) return "pixels";
|
|
7925
|
+
}
|
|
7926
|
+
/**
|
|
7927
|
+
* 拒绝消息必须可执行(FR-10.2):说清是哪一维超的、超了多少、以及该动哪张图。
|
|
7928
|
+
*
|
|
7929
|
+
* 「最大的几张图」不是锦上添花——一份超限的文档通常就是两三张巨图撑起来的,
|
|
7930
|
+
* 不报出来用户只知道「太大了」,不知道该换掉哪一张。
|
|
7931
|
+
*/
|
|
7932
|
+
function rejectionMessage(cost, limits, trigger, images) {
|
|
7933
|
+
const parts = [];
|
|
7934
|
+
if (trigger !== "pixels") parts.push(`payload is ${cost.units} units, over the ${limits.hardUnits} unit budget`);
|
|
7935
|
+
if (trigger !== "bytes") parts.push(`images total ${cost.pixels} pixels, over the ${limits.hardPixels} pixel budget`);
|
|
7936
|
+
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)`);
|
|
7937
|
+
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";
|
|
7938
|
+
return `Document ${parts.join(" and ")} (dimension: ${trigger}); ${advice}`;
|
|
7939
|
+
}
|
|
5967
7940
|
/**
|
|
5968
7941
|
* 打开一个文档投放面。
|
|
5969
7942
|
*
|
|
@@ -5973,23 +7946,46 @@ const payloadBytes = (value) => new TextEncoder().encode(JSON.stringify(value) ?
|
|
|
5973
7946
|
* 无 `allow-top-navigation` 改不了它的地址。
|
|
5974
7947
|
*/
|
|
5975
7948
|
async function openDocumentSurface(input, options) {
|
|
5976
|
-
const
|
|
5977
|
-
const
|
|
5978
|
-
|
|
7949
|
+
const images = input.images ?? [];
|
|
7950
|
+
const omissions = input.omissions ?? [];
|
|
7951
|
+
const limits = limitsOf(options);
|
|
7952
|
+
const cost = payloadCost(input);
|
|
7953
|
+
const facts = {
|
|
7954
|
+
units: cost.units,
|
|
7955
|
+
pixels: cost.pixels,
|
|
7956
|
+
imageCount: images.length,
|
|
7957
|
+
...omissions.length > 0 ? { omissions: [...omissions] } : {}
|
|
7958
|
+
};
|
|
7959
|
+
const rejected = triggeredBy(cost.units > limits.hardUnits, cost.pixels > limits.hardPixels);
|
|
7960
|
+
if (rejected !== void 0) {
|
|
5979
7961
|
await audit(options, input, {
|
|
5980
7962
|
outcome: "rejected",
|
|
5981
7963
|
reason: "payload-too-large",
|
|
5982
|
-
|
|
7964
|
+
...facts,
|
|
7965
|
+
warned: false,
|
|
7966
|
+
trigger: rejected
|
|
5983
7967
|
});
|
|
5984
|
-
throw new WebSkillError("VALIDATION_FAILED",
|
|
7968
|
+
throw new WebSkillError("VALIDATION_FAILED", rejectionMessage(cost, limits, rejected, images));
|
|
5985
7969
|
}
|
|
7970
|
+
const warned = triggeredBy(cost.units > limits.softUnits, cost.pixels > limits.softPixels);
|
|
7971
|
+
const sizeNotice = warned === void 0 ? void 0 : {
|
|
7972
|
+
units: cost.units,
|
|
7973
|
+
pixels: cost.pixels,
|
|
7974
|
+
trigger: warned
|
|
7975
|
+
};
|
|
7976
|
+
const audited = {
|
|
7977
|
+
...facts,
|
|
7978
|
+
warned: warned !== void 0,
|
|
7979
|
+
...warned === void 0 ? {} : { trigger: warned }
|
|
7980
|
+
};
|
|
5986
7981
|
if (!await options.confirm({
|
|
5987
7982
|
skillName: input.skillName,
|
|
5988
|
-
dataSource: input.dataSource
|
|
7983
|
+
dataSource: input.dataSource,
|
|
7984
|
+
...sizeNotice === void 0 ? {} : { sizeNotice }
|
|
5989
7985
|
})) {
|
|
5990
7986
|
await audit(options, input, {
|
|
5991
7987
|
outcome: "cancelled",
|
|
5992
|
-
|
|
7988
|
+
...audited
|
|
5993
7989
|
});
|
|
5994
7990
|
throw new WebSkillError("DOCUMENT_SURFACE_DECLINED", "The user declined to open the document surface");
|
|
5995
7991
|
}
|
|
@@ -5998,7 +7994,7 @@ async function openDocumentSurface(input, options) {
|
|
|
5998
7994
|
await audit(options, input, {
|
|
5999
7995
|
outcome: "blocked",
|
|
6000
7996
|
reason: "popup-blocked",
|
|
6001
|
-
|
|
7997
|
+
...audited
|
|
6002
7998
|
});
|
|
6003
7999
|
throw new WebSkillError("DOCUMENT_SURFACE_UNAVAILABLE", "The browser blocked the viewer window; open it from a direct user gesture");
|
|
6004
8000
|
}
|
|
@@ -6009,8 +8005,8 @@ async function openDocumentSurface(input, options) {
|
|
|
6009
8005
|
target.close();
|
|
6010
8006
|
await audit(options, input, {
|
|
6011
8007
|
outcome: "failed",
|
|
6012
|
-
reason: messageOf(e),
|
|
6013
|
-
|
|
8008
|
+
reason: messageOf$1(e),
|
|
8009
|
+
...audited
|
|
6014
8010
|
});
|
|
6015
8011
|
throw e;
|
|
6016
8012
|
}
|
|
@@ -6021,7 +8017,7 @@ async function openDocumentSurface(input, options) {
|
|
|
6021
8017
|
}, viewerOrigin(options));
|
|
6022
8018
|
await audit(options, input, {
|
|
6023
8019
|
outcome: "opened",
|
|
6024
|
-
|
|
8020
|
+
...audited
|
|
6025
8021
|
});
|
|
6026
8022
|
return target;
|
|
6027
8023
|
}
|
|
@@ -7279,7 +9275,7 @@ function toolError(e) {
|
|
|
7279
9275
|
content: [],
|
|
7280
9276
|
error: {
|
|
7281
9277
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
7282
|
-
message: messageOf(e)
|
|
9278
|
+
message: messageOf$1(e)
|
|
7283
9279
|
}
|
|
7284
9280
|
};
|
|
7285
9281
|
}
|
|
@@ -7393,4 +9389,4 @@ function readContent(result, images) {
|
|
|
7393
9389
|
}
|
|
7394
9390
|
|
|
7395
9391
|
//#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 };
|
|
9392
|
+
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 };
|