Files
KDL_WORK/kdl-wasm/web/scripts/generate-abb120-test-document.mjs
2026-06-28 20:58:18 +08:00

611 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { execFileSync } from "node:child_process";
import {
copyFileSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { dirname, join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const root = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const webRoot = join(root, "kdl-wasm", "web");
const resultRoot = join(webRoot, "test-results", "abb120-spec");
const specRoot = join(webRoot, "tests", "fixtures", "abb120", "spec-programs");
const virtualControllerEvidencePath = join(webRoot, "test-results", "virtual-controller", "evidence.json");
const outputPath = join(root, "work", "doc", "通用机器人离线编程系统测试文档.docx");
const stagingRoot = join(webRoot, "test-results", "docx-build");
const jobId = process.argv[2] ?? latestJobId();
const jobDir = join(resultRoot, jobId);
const job = readJson(join(jobDir, "job.json"));
const report = readJson(join(jobDir, "report.json"));
const manifest = readJson(join(jobDir, "manifest.json"));
const uiEvidence = readJson(virtualControllerEvidencePath);
const generatedAt = formatShanghaiTime(new Date());
const descriptionByProgram = {
W2_00_MinimalModule: "最小模块、language/module/proc main、常量 speed/zone、joint_target、MoveJ 基础执行。",
W2_10_DataTargetToolFrame: "数据类型、tool、frame、pose、poseq、joint_target、pose_target、工具和工件坐标切换。",
W2_20_ExpressionMath: "算术、比较、逻辑、括号、单位后缀、pi/e、三角函数和编译期求值。",
W2_30_MotionAllTypes: "MoveJ、MoveL、MoveC、set_tool、set_frame、set_speed、set_zone 和 inline 参数优先级。",
W2_40_PathEvents: "path defaults/source、point、event before/after/at distance、run_path。",
W2_50_OperationProcess: "operation kind/process/start_action/end_action、run_operation。",
W2_60_IOWaitPulse: "io.di/do/ai/ao/gi/go/ri/ro、wait、timeout、on_timeout、pulse 和边沿条件。",
W2_70_ControlFlow: "if/elseif/else、while、for、switch、break、continue 流程控制。",
W2_80_ProcFuncCall: "proc、in/out/inout、call、return、func 返回值和作用域。",
W2_90_ExceptionAlarm: "alarm、raise、try/catch/finally、trap/task P1 边界诊断。",
W2_99_FullSpecExample: "完整示例,覆盖 tool/frame、path、operation、IO event、run_operation 和回 home。",
W2_E10_ExpressionDiagnostics: "表达式负向测试:未知符号、未知函数、参数数量、数学域、除零、单位不匹配。",
W2_E20_SemanticDiagnostics: "语义负向测试重复符号、out/inout 非左值、缺失调用目标、函数副作用、缺失目标。",
W2_E30_MotionDiagnostics: "运动负向测试:关节限位、不可达 pose、空 path 等 KDL/Path 诊断。",
W2_E40_ControlFlowDiagnostics: "流程控制负向测试:非法 break/continue、jump、重复 case、非常量 case、缺失 label。",
W2_E50_RuntimeTimeout: "运行时超时负向测试wait timeout、on_timeout、报警 source map。",
W2_P10_BrandHints: "品牌提示后处理测试post_hint、@brand.* 消费或报告。",
W2_P20_CrossBrandMotion: "跨品牌运动后处理ABB/FANUC/KUKA motion、speed、zone、IO、wait 映射。",
W2_P30_Roundtrip: "后处理和导入回读测试post 后 ABB import roundtrip记录差异和警告。"
};
const commandResults = [
{
command: "npm run typecheck",
scope: "TypeScript 静态类型检查",
process: "执行 tsc -p kdl-wasm/web/tsconfig.json --noEmit检查 Web、GRL、runtime、post 和 suite 代码类型。",
result: "通过,未输出 TypeScript 错误。"
},
{
command: "npm test",
scope: "全量 Vitest 自动化测试",
process: "执行 vitest run --config kdl-wasm/web/vitest.config.ts覆盖 KDL、GRL、runtime、controller、post/import、workspace、report、docs 和 ABB120 integration。",
result: "通过41 个 test files、155 个 tests 全部 passed。"
},
{
command: "npm run suite:abb120-spec",
scope: "ABB120 GRL 规范程序套件",
process: "读取 manifest.json 和 19 个 GRL 程序,逐项执行 parse、semantic compile、runtime/static/post 检查,并输出 job/report/program artifacts。",
result: `通过,生成 ${job.job_id}programs=${report.summary.programs}pass=${report.summary.pass}expected diagnostic=${report.summary.diagnostic}fail=${report.summary.fail}missingSections=${report.coverage.missingSections.length}`
},
{
command: "npm run verify:virtual-controller",
scope: "虚拟控制器 UI 和截图验证",
process: "使用 Chrome DevTools Protocol 打开 virtual-controller.html分别验证 desktop/mobile 视口、点击 load/run/DI1、检查主要面板、状态、wait 和横向溢出,并截图。",
result: `通过desktop=${uiEvidence.desktop.screenshotBytes} bytesmobile=${uiEvidence.mobile.screenshotBytes} bytesstate=${uiEvidence.desktop.state}/${uiEvidence.mobile.state}wait=${uiEvidence.desktop.wait}/${uiEvidence.mobile.wait}`
}
];
const programs = job.programs.map((program) => {
const sourcePath = join(specRoot, program.file);
return {
...program,
manifestEntry: manifest.programs.find((entry) => entry.program_id === program.program_id),
sourcePath,
source: readFileSync(sourcePath, "utf8")
};
});
buildDocx();
console.log(JSON.stringify({ outputPath, jobId, programs: programs.length }, null, 2));
function buildDocx() {
assertSafeStaging(stagingRoot);
rmSync(stagingRoot, { recursive: true, force: true });
mkdirSync(join(stagingRoot, "_rels"), { recursive: true });
mkdirSync(join(stagingRoot, "docProps"), { recursive: true });
mkdirSync(join(stagingRoot, "word", "_rels"), { recursive: true });
mkdirSync(join(stagingRoot, "word", "media"), { recursive: true });
const images = copyImages();
writeFileSync(join(stagingRoot, "[Content_Types].xml"), contentTypesXml(), "utf8");
writeFileSync(join(stagingRoot, "_rels", ".rels"), packageRelsXml(), "utf8");
writeFileSync(join(stagingRoot, "docProps", "core.xml"), corePropsXml(), "utf8");
writeFileSync(join(stagingRoot, "docProps", "app.xml"), appPropsXml(), "utf8");
writeFileSync(join(stagingRoot, "word", "styles.xml"), stylesXml(), "utf8");
writeFileSync(join(stagingRoot, "word", "_rels", "document.xml.rels"), documentRelsXml(images), "utf8");
writeFileSync(join(stagingRoot, "word", "document.xml"), documentXml(images), "utf8");
if (existsSync(outputPath)) {
rmSync(outputPath, { force: true });
}
zipDirectory(stagingRoot, outputPath);
}
function copyImages() {
const images = [
{
id: "rIdImage1",
fileName: "virtual-controller-desktop.png",
source: uiEvidence.desktop.screenshot,
title: "虚拟控制器桌面端截图",
cx: 5943600,
cy: 3962400
},
{
id: "rIdImage2",
fileName: "virtual-controller-mobile.png",
source: uiEvidence.mobile.screenshot,
title: "虚拟控制器移动端截图",
cx: 2743200,
cy: 5943600
}
];
for (const image of images) {
copyFileSync(image.source, join(stagingRoot, "word", "media", image.fileName));
}
return images;
}
function documentXml(images) {
const body = [];
body.push(heading("通用机器人离线编程系统测试文档", 1));
body.push(p(`生成时间:${generatedAt}`));
body.push(p(`测试对象:通用机器人离线编程系统 / ABB IRB120 虚拟控制器 / GRL 规范程序套件`));
body.push(p(`工作区:${root}`));
body.push(p(`最新测试 job${job.job_id}`));
body.push(heading("一、测试结论", 1));
body.push(table([
["项目", "结果"],
["测试结论", "通过。本轮未发现非预期失败5 个 diagnostic 程序为预期负向测试。"],
["规范覆盖", `覆盖章节 ${report.coverage.coveredSections.join("、")};缺失章节:${report.coverage.missingSections.length === 0 ? "无" : report.coverage.missingSections.join("、")}`],
["程序统计", `${report.summary.programs} 个 GRL 规范程序pass=${report.summary.pass}expected diagnostic=${report.summary.diagnostic}fail=${report.summary.fail}`],
["测试层级", `Runtime=${report.coverage.runtimePrograms}Static=${report.coverage.staticPrograms}Post=${report.coverage.postPrograms}`],
["主要证据", `${relative(jobDir)}${relative(dirname(uiEvidence.desktop.screenshot))}`]
]));
body.push(heading("二、测试范围", 1));
body.push(p("本次测试覆盖离线编程系统的语法解析、语义编译、KDL 运动规划接口、虚拟控制器状态机、IO/wait/pulse、路径和工艺操作、异常报警、三品牌后处理、导入回读、工作台 UI 响应和截图证据。"));
body.push(table([
["范围", "覆盖内容"],
["GRL 语言", "词法、顶层结构、类型系统、表达式、目标点、速度、zone、motion、path、operation、IO、流程控制、proc/func、异常和 P1 边界。"],
["运动与 KDL", "ABB IRB120 fixture、MoveJ/MoveL/MoveC、path planning、joint limit、unreachable target、trajectory summary。"],
["虚拟控制器", "loadProgram、start、stepInto、hold、resume、stepMotion、stop、resetFault、runtime trace、motion queue、IO image。"],
["后处理与导入", "ABB/FANUC/KUKA 输出文件、post report、ABB import roundtrip。"],
["用户界面", "virtual-controller.html 桌面端和移动端渲染、交互、主要面板可见性和截图。"]
]));
body.push(heading("三、测试命令与执行过程", 1));
body.push(table([
["命令", "测试内容", "执行过程", "执行结果"],
...commandResults.map((item) => [item.command, item.scope, item.process, item.result])
]));
body.push(heading("四、截图证据", 1));
body.push(p(`截图验证应用:${uiEvidence.app}`));
body.push(p(`Chrome${uiEvidence.chrome}`));
body.push(heading("4.1 桌面端截图", 2));
body.push(p(`视口:${uiEvidence.desktop.viewport.width}x${uiEvidence.desktop.viewport.height}state=${uiEvidence.desktop.state}wait=${uiEvidence.desktop.wait}trace=${uiEvidence.desktop.trace};截图:${relative(uiEvidence.desktop.screenshot)}`));
body.push(imageParagraph(images[0], 1));
body.push(heading("4.2 移动端截图", 2));
body.push(p(`视口:${uiEvidence.mobile.viewport.width}x${uiEvidence.mobile.viewport.height}deviceScaleFactor=${uiEvidence.mobile.viewport.deviceScaleFactor}state=${uiEvidence.mobile.state}wait=${uiEvidence.mobile.wait}trace=${uiEvidence.mobile.trace};截图:${relative(uiEvidence.mobile.screenshot)}`));
body.push(imageParagraph(images[1], 2));
body.push(heading("五、规范覆盖矩阵", 1));
body.push(table([
["规范章节", "覆盖层级", "程序"],
...manifest.coverage_matrix.map((item) => [
String(item.spec_section),
item.coverage_level,
item.program_ids.join("、")
])
]));
body.push(heading("六、全部测试程序明细与源码", 1));
for (const [index, program] of programs.entries()) {
appendProgramSection(body, program, index + 1);
}
body.push(heading("七、主要产物索引", 1));
body.push(table([
["产物", "路径"],
["job.json", relative(join(jobDir, "job.json"))],
["report.json", relative(join(jobDir, "report.json"))],
["report.html", relative(join(jobDir, "report.html"))],
["manifest.json", relative(join(jobDir, "manifest.json"))],
["桌面端截图", relative(uiEvidence.desktop.screenshot)],
["移动端截图", relative(uiEvidence.mobile.screenshot)],
["UI evidence.json", relative(virtualControllerEvidencePath)]
]));
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16du wp14">
<w:body>
${body.join("\n")}
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="850" w:header="708" w:footer="708" w:gutter="0"/>
<w:cols w:space="708"/>
<w:docGrid w:linePitch="312"/>
</w:sectPr>
</w:body>
</w:document>`;
}
function appendProgramSection(body, program, index) {
const expectedDiagnostics = program.manifestEntry?.expected_diagnostics ?? [];
const actualCodes = [...new Set(program.diagnostics.map((diagnostic) => diagnostic.code))];
body.push(heading(`6.${index} ${program.program_id}`, 2));
body.push(table([
["字段", "内容"],
["测试内容", descriptionByProgram[program.program_id] ?? "按 manifest 执行规范测试。"],
["源码文件", relative(program.sourcePath)],
["覆盖层级", program.coverage_level],
["覆盖章节", program.spec_sections.join("、")],
["预期状态", program.expected_status],
["实际状态", program.status],
["预期诊断", expectedDiagnostics.length === 0 ? "无" : expectedDiagnostics.join("、")],
["实际诊断代码", actualCodes.length === 0 ? "无" : actualCodes.join("、")],
["输入哈希", program.input_hash],
["源码行数", String(program.source.split(/\r?\n/).length)]
]));
body.push(heading("测试过程", 3));
for (const step of programSteps(program)) {
body.push(p(step));
}
body.push(heading("执行结果摘要", 3));
body.push(table(programResultRows(program)));
if (program.controller?.commands?.length) {
body.push(heading("虚拟控制器命令轨迹", 3));
body.push(table([
["命令", "前状态", "后状态", "结果", "诊断"],
...program.controller.commands.map((command) => [
command.command,
command.before ?? "",
command.after ?? "",
command.result ?? "",
command.diagnostics.length === 0 ? "无" : command.diagnostics.map((diagnostic) => diagnostic.code).join("、")
])
]));
}
body.push(heading("证据文件", 3));
body.push(table([
["类型", "路径"],
...Object.entries(program.artifacts).map(([key, value]) => [key, relative(join(resultRoot, value))])
]));
body.push(heading("程序源码", 3));
body.push(codeBlock(program.source));
}
function programSteps(program) {
const steps = [
`1. 读取源码 ${relative(program.sourcePath)},加载 program_id、spec_sections、coverage_level、expected_status 和 expected_diagnostics。`,
"2. 执行 parseGrl完成词法、语法、模块结构和 source map 解析。",
"3. 执行 compileSemanticProgram生成语义 IR、符号表、procedure/path/operation、KDL motion/path request 和诊断列表。"
];
if (program.coverage_level === "Runtime") {
steps.push(`4. 加载 IR 到 VirtualController${manifest.runtime_commands.join(" -> ")} 执行控制器命令,采集 controller snapshot、motion queue、runtime trace、IO image 和 trajectory summary。`);
steps.push("5. 对比实际状态与 manifest 预期状态;对比实际诊断代码与 expected_diagnostics写入 compile/controller/motion-queue/trace/io/trajectory/post/roundtrip 证据。");
} else if (program.coverage_level === "Static") {
steps.push("4. 执行 static preflight diagnostics覆盖表达式、语义、运动、path、流程控制等负向检查不要求进入 happy-path 运行。");
steps.push("5. 对比实际诊断代码与 expected_diagnostics确认 diagnostic 状态为预期负向结果,并写入 compile 及各类空/摘要证据文件。");
} else {
steps.push("4. 执行 postProcessAllBrands生成 ABB/FANUC/KUKA 输出文件名和 post report。");
steps.push("5. 执行 ABB import roundtrip 摘要检查,记录 pass/warn/fail 与导入诊断,并写入 post-report/roundtrip 证据。");
}
return steps;
}
function programResultRows(program) {
const rows = [
["项目", "结果"],
["解析", program.compile.parsed ? "parsed=true" : "parsed=false"],
["模块名", program.compile.moduleName ?? ""],
["sourceMapEntries", String(program.compile.sourceMapEntries)],
["symbol/procedure/path/operation", `${program.compile.symbolCount}/${program.compile.procedureCount}/${program.compile.pathCount}/${program.compile.operationCount}`],
["KDL motion/path requests", `${program.compile.kdlMotionRequests}/${program.compile.kdlPathRequests}`],
["状态", program.status]
];
if (program.controller) {
rows.push(["controller final state", program.controller.snapshot.state.state]);
rows.push(["runtime trace events", String(program.trace?.length ?? 0)]);
rows.push(["motion queue items", String(program.motionQueue?.items?.length ?? 0)]);
rows.push(["IO image count", String(Object.keys(program.io?.image ?? {}).length)]);
}
if (program.trajectory) {
rows.push(["trajectory sample/duration", `${program.trajectory.sampleCount}/${program.trajectory.duration}`]);
}
if (program.post) {
rows.push(["post filenames", program.post.filenames.join("、")]);
rows.push(["post report codes", program.post.report.length === 0 ? "无" : [...new Set(program.post.report.map((issue) => issue.code))].join("、")]);
}
if (program.roundtrip) {
rows.push(["roundtrip", `${program.roundtrip.status}; ${program.roundtrip.diagnostics.map((diagnostic) => diagnostic.code).join("、") || "无诊断"}`]);
}
rows.push(["诊断数量", String(program.diagnostics.length)]);
return rows;
}
function heading(text, level) {
return paragraph(text, `Heading${Math.min(level, 3)}`);
}
function p(text) {
return paragraph(text, "BodyText");
}
function paragraph(text, style) {
return `<w:p><w:pPr><w:pStyle w:val="${style}"/></w:pPr>${run(text)}</w:p>`;
}
function run(text) {
return `<w:r><w:t xml:space="preserve">${escapeXml(String(text))}</w:t></w:r>`;
}
function codeBlock(text) {
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
return lines.map((line) =>
`<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr><w:r><w:t xml:space="preserve">${escapeXml(line === "" ? " " : line)}</w:t></w:r></w:p>`
).join("\n");
}
function table(rows) {
const width = Math.floor(9000 / Math.max(1, rows[0]?.length ?? 1));
return `<w:tbl>
<w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0"/></w:tblPr>
${rows.map((row, rowIndex) => `<w:tr>${row.map((cell) => tableCell(cell, width, rowIndex === 0)).join("")}</w:tr>`).join("\n")}
</w:tbl>`;
}
function tableCell(text, width, header) {
return `<w:tc>
<w:tcPr><w:tcW w:w="${width}" w:type="dxa"/></w:tcPr>
<w:p><w:pPr>${header ? "<w:pStyle w:val=\"TableHeader\"/>" : ""}</w:pPr>${run(text)}</w:p>
</w:tc>`;
}
function imageParagraph(image, docPrId) {
return `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="${image.cx}" cy="${image.cy}"/>
<wp:effectExtent l="0" t="0" r="0" b="0"/>
<wp:docPr id="${docPrId}" name="${escapeXml(image.title)}"/>
<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="${docPrId}" name="${escapeXml(image.fileName)}"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="${image.id}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${image.cx}" cy="${image.cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing></w:r></w:p>`;
}
function contentTypesXml() {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>`;
}
function packageRelsXml() {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>`;
}
function documentRelsXml(images) {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
${images.map((image) => `<Relationship Id="${image.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${image.fileName}"/>`).join("\n")}
</Relationships>`;
}
function corePropsXml() {
const iso = new Date().toISOString();
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dc:title>通用机器人离线编程系统测试文档</dc:title>
<dc:creator>Codex</dc:creator>
<cp:lastModifiedBy>Codex</cp:lastModifiedBy>
<dcterms:created xsi:type="dcterms:W3CDTF">${iso}</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">${iso}</dcterms:modified>
</cp:coreProperties>`;
}
function appPropsXml() {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<Application>Codex</Application>
<DocSecurity>0</DocSecurity>
<ScaleCrop>false</ScaleCrop>
<Company>KDL Work</Company>
<LinksUpToDate>false</LinksUpToDate>
<SharedDoc>false</SharedDoc>
<HyperlinksChanged>false</HyperlinksChanged>
<AppVersion>16.0000</AppVersion>
</Properties>`;
}
function stylesXml() {
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
<w:name w:val="Normal"/>
<w:qFormat/>
<w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="BodyText">
<w:name w:val="Body Text"/>
<w:basedOn w:val="Normal"/>
<w:pPr><w:spacing w:after="120" w:line="276" w:lineRule="auto"/></w:pPr>
<w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading1">
<w:name w:val="heading 1"/>
<w:basedOn w:val="Normal"/>
<w:next w:val="BodyText"/>
<w:qFormat/>
<w:pPr><w:keepNext/><w:spacing w:before="360" w:after="180"/></w:pPr>
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="32"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="heading 2"/>
<w:basedOn w:val="Normal"/>
<w:next w:val="BodyText"/>
<w:qFormat/>
<w:pPr><w:keepNext/><w:spacing w:before="280" w:after="140"/></w:pPr>
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="27"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading3">
<w:name w:val="heading 3"/>
<w:basedOn w:val="Normal"/>
<w:next w:val="BodyText"/>
<w:qFormat/>
<w:pPr><w:keepNext/><w:spacing w:before="180" w:after="100"/></w:pPr>
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="23"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Code">
<w:name w:val="Code"/>
<w:basedOn w:val="Normal"/>
<w:pPr><w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>
<w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:eastAsia="Consolas"/><w:sz w:val="16"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="TableHeader">
<w:name w:val="Table Header"/>
<w:basedOn w:val="Normal"/>
<w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/></w:rPr>
</w:style>
<w:style w:type="table" w:default="1" w:styleId="TableNormal">
<w:name w:val="Normal Table"/>
<w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr>
</w:style>
<w:style w:type="table" w:styleId="TableGrid">
<w:name w:val="Table Grid"/>
<w:basedOn w:val="TableNormal"/>
<w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:left w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:right w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/></w:tblBorders></w:tblPr>
</w:style>
</w:styles>`;
}
function latestJobId() {
const jobs = readdirSync(resultRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && existsSync(join(resultRoot, entry.name, "job.json")))
.map((entry) => ({
name: entry.name,
mtimeMs: statSync(join(resultRoot, entry.name, "job.json")).mtimeMs
}))
.sort((a, b) => b.mtimeMs - a.mtimeMs);
if (jobs.length === 0) {
throw new Error(`No abb120 spec job found in ${resultRoot}`);
}
return jobs[0].name;
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function relative(path) {
return path.replace(`${root}${sep}`, "").replaceAll("\\", "/");
}
function escapeXml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&apos;");
}
function psSingleQuoted(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
function zipDirectory(sourceDir, destinationFile) {
const script = [
"Add-Type -AssemblyName System.IO.Compression.FileSystem",
`$src = ${psSingleQuoted(sourceDir)}`,
`$dst = ${psSingleQuoted(destinationFile)}`,
"if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
"[System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dst)"
].join("\n");
execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
stdio: "inherit"
});
}
function assertSafeStaging(path) {
const allowedRoot = join(webRoot, "test-results") + sep;
const resolved = resolve(path);
if (!resolved.startsWith(allowedRoot)) {
throw new Error(`Refusing to clear staging path outside test-results: ${resolved}`);
}
}
function formatShanghaiTime(date) {
const parts = new Intl.DateTimeFormat("zh-CN", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}).formatToParts(date);
const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}:${value.second} Asia/Shanghai`;
}