Add ABB120 demo suite and docs

This commit is contained in:
wangdequan
2026-06-28 20:58:18 +08:00
parent 84a5b54195
commit 65187499d6
820 changed files with 215410 additions and 211 deletions

View File

@@ -0,0 +1,610 @@
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`;
}