Files
KDL_WORK/kdl-wasm/web/scripts/generate-grl-user-manual.mjs
2026-06-28 20:58:18 +08:00

599 lines
33 KiB
JavaScript
Raw 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,
readFileSync,
rmSync,
writeFileSync
} from "node:fs";
import { 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 docRoot = join(root, "work", "doc");
const stagingRoot = join(webRoot, "test-results", "grl-manual-docx-build");
const outputPath = join(docRoot, "GRL功能语法逻辑与程序创建使用手册.docx");
const flowDir = join(docRoot, "通用机器人项目功能与数据流程图-png");
const flowImages = [
{ source: join(flowDir, "flow-01.png"), title: "系统功能总览图", cx: 5943600, cy: 3343275 },
{ source: join(flowDir, "flow-02.png"), title: "GRL 到 IR 编译流程图", cx: 5943600, cy: 3343275 },
{ source: join(flowDir, "flow-03.png"), title: "运动规划与 KDL 数据流图", cx: 5943600, cy: 3343275 },
{ source: join(flowDir, "flow-04.png"), title: "虚拟控制器执行流程图", cx: 5943600, cy: 3343275 },
{ source: join(flowDir, "flow-05.png"), title: "后处理与导入回读流程图", cx: 5943600, cy: 3343275 }
];
const uiImages = [
{
source: join(webRoot, "test-results", "virtual-controller", "virtual-controller-desktop.png"),
title: "虚拟控制器桌面端界面",
cx: 5943600,
cy: 3962400
},
{
source: join(webRoot, "test-results", "virtual-controller", "virtual-controller-mobile.png"),
title: "虚拟控制器移动端界面",
cx: 2743200,
cy: 5943600
}
];
const smokeSourcePath = join(webRoot, "tests", "fixtures", "abb120", "programs", "A120_00_Smoke.grl");
const blendSourcePath = join(webRoot, "tests", "fixtures", "abb120", "programs", "A120_20_CartesianBlend.grl");
const ioSourcePath = join(webRoot, "tests", "fixtures", "abb120", "spec-programs", "runtime", "W2_60_IOWaitPulse.grl");
const fullExamplePath = join(webRoot, "tests", "fixtures", "abb120", "spec-programs", "runtime", "W2_99_FullSpecExample.grl");
const generatedAt = formatShanghaiTime(new Date());
buildDocx();
console.log(JSON.stringify({ outputPath }, 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([...flowImages, ...uiImages]);
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 documentXml(images) {
const body = [];
body.push(heading("GRL 功能、语法逻辑与程序创建使用手册", 1));
body.push(p(`生成时间:${generatedAt}`));
body.push(p("适用对象:通用机器人离线编程系统用户、工艺工程师、自动编程开发人员、虚拟调试人员。"));
body.push(p("手册目标:用中文说明 GRL 的功能边界、语法逻辑、程序对象关系、创建程序步骤、仿真检查和后处理输出方式,并通过图示和源码示例帮助用户快速编写可运行程序。"));
body.push(heading("一、GRL 是什么", 1));
body.push(p("GRL全称 Generic Robot Language是本离线编程系统中的通用机器人程序语言。它不是 ABB、FANUC 或 KUKA 任一品牌语言的拷贝而是把多品牌机器人共同具备的目标点、运动、路径、工艺、IO、流程控制、异常和后处理语义抽象为统一文本。"));
body.push(table([
["目标", "说明"],
["统一描述", "用同一套 GRL 描述机器人程序、路径、工艺动作和 IO 逻辑。"],
["可编译", "GRL 经 lexer/parser/semantic analyzer 编译为统一 Executable IR。"],
["可仿真", "IR 可进入虚拟控制器运行,产生 trace、motion queue、IO image 和诊断。"],
["可后处理", "同一 IR 可输出 ABB RAPID、FANUC LS/TP 风格文本、KUKA KRL 原型程序。"],
["可维护", "文本稳定、可读、可 diff适合版本管理和自动生成。"]
]));
body.push(heading("二、系统功能总览", 1));
body.push(p("离线编程系统从项目数据、机器人模型和 GRL 程序出发经过语义编译、KDL 运动规划、虚拟控制器执行、诊断报告和多品牌后处理,形成完整离线开发闭环。"));
body.push(imageWithCaption(images[0], 1, "图 1 系统功能总览图"));
body.push(table([
["模块", "主要职责"],
["GRL 源码", "声明工具、坐标系、目标点、速度、zone、path、operation 和 proc。"],
["编译器", "把源码解析为 AST做符号解析、单位归一化、语义检查生成 IR 和 KDL request。"],
["KDL 运动规划", "处理 FK、IK、MoveJ、MoveL、MoveC、Path planning、可达性和轨迹采样。"],
["虚拟控制器", "执行 IR维护程序计数器、调用栈、报警队列、IO 镜像、运动队列和 trace。"],
["后处理器", "按 ABB/FANUC/KUKA 规则输出品牌程序,并记录无法保真映射的转换报告。"]
]));
body.push(heading("三、GRL 程序基本结构", 1));
body.push(p("一个 GRL 文件通常由 language 声明、module 声明、数据声明、目标点、路径、工艺操作和 main 子程序组成。最小程序只需要定义模块入口和一条运动。"));
body.push(codeBlock(readFileSync(smokeSourcePath, "utf8")));
body.push(table([
["结构", "作用"],
["language grl 0.1", "声明语言版本,便于后续兼容升级。"],
["module", "程序模块,类似 ABB MODULE、KUKA DEF 文件或 FANUC 程序容器。"],
["const/persistent/var", "声明常量、持久数据或局部变量。"],
["target", "声明 joint_target 或 pose_target。"],
["proc main()", "默认入口程序。虚拟控制器从 main 开始执行。"],
["movej/movel/movec", "基础运动指令,最终进入 KDL 规划或品牌后处理。"]
]));
body.push(heading("四、对象模型和语法逻辑", 1));
body.push(p("GRL 的语法逻辑建议按“数据先声明、路径再组织、工艺再封装、入口只调用”的方式理解。手写小程序可以直接在 main 中写运动,自动编程和复杂工艺应优先生成 path 和 operation。"));
body.push(imageWithCaption(images[1], 2, "图 2 GRL 到 IR 编译流程图"));
body.push(table([
["对象", "语法", "使用建议"],
["工具", "persistent tool gripper = tool { ... }", "描述 TCP、质量、重心。运动时通过 tool 参数或 path defaults 引用。"],
["坐标系", "persistent frame fixture = frame { ... }", "描述工件坐标或基坐标。目标点和路径应显式绑定 frame。"],
["速度", "const speed v = linear(300 mm/s) / joint(50 %)", "按运动类型选择 joint、linear、angular。"],
["过渡", "const zone z10 = z(10 mm) / fine / cnt(30)", "fine 精确到点z/cnt/continuous 用于连续过渡。"],
["目标点", "target pick = pose_target { pose: pose(...) }", "关节目标用 joint_target笛卡尔点用 pose_target。"],
["路径", "path pick_path { defaults ... point ... event ... }", "自动编程优先生成 path点名保持稳定。"],
["工艺", "operation pick_op { kind: handling path: pick_path ... }", "封装路径、工艺参数、start_action/end_action。"],
["入口程序", "proc main() run_operation pick_op end", "入口程序保持短,便于调试和后处理。"]
]));
body.push(heading("五、表达式、单位和常量求值", 1));
body.push(p("GRL 支持带单位表达式。编译器会在需要常量的位置做单位归一化,例如 mm 转 m、deg 转 rad、ms 转 s、% 转比例。这样同一程序在仿真和后处理中使用统一数值。"));
body.push(table([
["类别", "示例", "说明"],
["长度", "100 mm、0.25 m", "内部统一为 meter。"],
["角度", "90 deg、1.57 rad", "内部统一为 radian。"],
["时间", "200 ms、2 s", "内部统一为 second。"],
["速度", "linear(300 mm/s)、joint(50 %)", "分别表示 TCP 线速度和关节速度比例。"],
["数学函数", "atan2(0, 1)、max(50 mm/s, 150 mm/s)", "支持常用三角函数和数值函数。"],
["单位检查", "10 mm + 2 s", "不同维度相加会产生诊断。"]
]));
body.push(heading("六、运动语法", 1));
body.push(p("运动指令分为 MoveJ、MoveL、MoveC 三类。MoveJ 走关节空间MoveL 走 TCP 直线MoveC 走 TCP 圆弧。每条运动最终都需要确定 target、speed、zone、tool 和 frame。"));
body.push(imageWithCaption(images[2], 3, "图 3 运动规划与 KDL 数据流图"));
body.push(table([
["指令", "语法", "语义", "品牌映射"],
["movej", "movej home speed v_joint zone fine", "关节空间运动,适合回零和快速接近。", "ABB MoveJ / FANUC J / KUKA PTP"],
["movel", "movel pick speed v_linear zone z10", "TCP 直线运动,适合接近、加工、搬运直线段。", "ABB MoveL / FANUC L / KUKA LIN"],
["movec", "movec via mid target arc_end speed v zone z10", "TCP 圆弧运动,通过 via 点到终点。", "ABB MoveC / FANUC C / KUKA CIRC"],
["run_path", "run_path cart_path", "执行已声明路径。", "可展开为品牌运动语句"],
["run_operation", "run_operation pick_op", "执行工艺操作,包含 action 和 path。", "按品牌能力展开或报告"]
]));
body.push(p("下面是当前工作区中的笛卡尔混合路径示例,包含 tool/frame、表达式速度、zone clamp、MoveL、MoveC 和 run_path。"));
body.push(codeBlock(readFileSync(blendSourcePath, "utf8")));
body.push(heading("七、Path 和 Operation", 1));
body.push(p("Path 是离线编程的核心对象,适合承载 CAD 曲线、示教点、工艺采样点和路径事件。Operation 是对 Path 的工艺封装,适合表达搬运、焊接、喷涂、打磨、上下料等任务。"));
body.push(table([
["写法", "说明"],
["defaults", "为路径点设置默认 speed、zone、tool、frame减少重复。"],
["source", "记录 CAD 曲线、采样间距、规划来源等元数据。"],
["point", "定义路径点和该点运动方式。"],
["event before/after/at", "定义路径事件,例如到点后开夹爪、距离某点前开焊接。"],
["operation process", "记录工艺参数,例如 gripper、weld_mode、paint_recipe。"],
["start_action/end_action", "操作开始或结束时执行 IO、wait、pulse 等动作。"]
]));
body.push(heading("八、IO、wait 和 pulse", 1));
body.push(p("GRL 中的 IO 使用统一引用格式io.di、io.do、io.ai、io.ao、io.gi、io.go、io.ri、io.ro 和 io.alias。离线系统运行时维护虚拟 IO 镜像,程序、用户和脚本都可以写入这份镜像。"));
body.push(table([
["语法", "含义"],
["io.do[1] = true", "写数字输出。"],
["io.ao[1] = 0.75", "写模拟输出。"],
["wait io.di[1] == true timeout 2 s", "等待输入满足条件,超时产生诊断或报警。"],
["wait all(...)", "所有条件满足才继续。"],
["wait any(...)", "任一条件满足即继续。"],
["rising/falling/changed", "边沿或变化检测。"],
["pulse io.do[2] duration 100 ms", "输出置 true持续指定时间后自动复位。"]
]));
body.push(codeBlock(readFileSync(ioSourcePath, "utf8")));
body.push(heading("九、流程控制、子程序和异常", 1));
body.push(p("GRL 支持结构化控制和子程序。简单运行逻辑写在 proc 中,纯计算逻辑写在 func 中。func 默认不应产生运动、IO、wait、pulse 等副作用。"));
body.push(table([
["能力", "语法", "说明"],
["条件", "if / elseif / else / end", "按布尔条件选择执行分支。"],
["循环", "while、for to step", "执行重复逻辑break/continue 必须处于合法块内。"],
["多分支", "switch / case / default", "case 必须是常量表达式,不能重复。"],
["子程序", "proc name(in int slot) ... end", "可定义 in/out/inout 参数。"],
["函数", "func bool ok() return ... end", "有返回值,建议无副作用。"],
["报警", "alarm ID \"message\" severity warning", "进入虚拟控制器报警队列。"],
["异常", "raise、try/catch/finally", "P0 支持基础语义,复杂品牌恢复分阶段扩展。"]
]));
body.push(heading("十、从零创建一个 GRL 程序", 1));
body.push(p("建议按以下顺序创建程序。先保证数据和坐标清楚,再组织路径和工艺,最后在 main 中调用。"));
body.push(table([
["步骤", "操作", "检查点"],
["1", "创建 .grl 文件,写 language 和 module。", "模块名使用 ASCII 标识符,文件 UTF-8。"],
["2", "声明 tool 和 frame。", "TCP、质量、工件坐标单位明确。"],
["3", "声明 speed 和 zone。", "速度和过渡统一命名,例如 v_fast、v_pick、z10。"],
["4", "声明 target。", "home 用 joint_target工艺点用 pose_target。"],
["5", "创建 path。", "设置 defaults点名按 p000/p001 或语义名称稳定输出。"],
["6", "添加 path event 或 operation action。", "夹具、等待、脉冲、工艺开关不要散落在 main 中。"],
["7", "创建 operation。", "把 path 和工艺参数封装成可复用操作。"],
["8", "编写 proc main。", "只做 set_tool/set_frame、run_operation、回 home 等主流程。"],
["9", "运行编译和测试。", "执行 npm test 或 suite检查 diagnostics、trace、motion queue。"],
["10", "后处理输出。", "检查 ABB/FANUC/KUKA 转换报告,不支持项不能忽略。"]
]));
body.push(heading("十一、完整程序模板", 1));
body.push(p("下面给出一个可直接改写的模板,适合搬运、上下料、夹具动作和基础路径。"));
body.push(codeBlock(`language grl 0.1
module DemoPickPlace
persistent tool gripper = tool {
tcp: pose(0 mm, 0 mm, 120 mm, 0 deg, 0 deg, 0 deg),
mass: 1.5 kg
}
persistent frame fixture = frame {
origin: pose(800 mm, 0 mm, 0 mm, 0 deg, 0 deg, 0 deg)
}
const speed v_joint = joint(50 %)
const speed v_pick = linear(250 mm/s)
const zone z10 = z(10 mm)
const zone zf = fine
target home = joint_target { joints: [0 deg, 0 deg, 0 deg, 0 deg, 0 deg, 0 deg] }
target pick = pose_target { pose: pose(500 mm, 100 mm, 200 mm, 180 deg, 0 deg, 90 deg), tool: gripper, frame: fixture }
target place = pose_target { pose: pose(650 mm, -100 mm, 200 mm, 180 deg, 0 deg, 90 deg), tool: gripper, frame: fixture }
path pick_path {
defaults { speed: v_pick, zone: z10, tool: gripper, frame: fixture }
point approach movel pick offset z 100 mm
point at_pick movel pick zone zf
event after at_pick io.do[1] = true
event after at_pick wait io.di[1] == true timeout 2 s on_timeout alarm "Clamp close timeout"
point leave movel pick offset z 100 mm
}
operation pick_op {
kind: handling
path: pick_path
process { gripper: "main_clamp" }
}
proc main()
set_tool gripper
set_frame fixture
movej home speed v_joint zone zf
run_operation pick_op
movej home speed v_joint zone zf
end
end`));
body.push(heading("十二、编译、仿真和截图验证", 1));
body.push(imageWithCaption(images[3], 4, "图 4 虚拟控制器执行流程图"));
body.push(p("编译后,虚拟控制器会加载 IR从 main 开始执行,采集运行 trace、motion queue、IO image、报警队列和诊断。桌面端和移动端界面可用于检查程序结构、状态、编辑器、面板和运行结果。"));
body.push(imageWithCaption(images[5], 6, "图 5 虚拟控制器桌面端界面"));
body.push(imageWithCaption(images[6], 7, "图 6 虚拟控制器移动端界面"));
body.push(table([
["命令", "用途"],
["npm run typecheck", "检查 TypeScript 类型。"],
["npm test", "运行 KDL、GRL、runtime、post/import、integration 全量测试。"],
["npm run suite:abb120-spec", "运行 19 个 ABB120 GRL 规范程序,生成 job/report/program 证据。"],
["npm run verify:virtual-controller", "打开虚拟控制器 HTML执行交互并生成桌面/移动截图。"]
]));
body.push(heading("十三、后处理和跨品牌输出", 1));
body.push(imageWithCaption(images[4], 5, "图 7 后处理与导入回读流程图"));
body.push(table([
["GRL", "ABB RAPID", "FANUC LS/TP", "KUKA KRL"],
["module", "MODULE", "/PROG 或模块", "DEF/.src"],
["proc", "PROC", "CALL 子程序", "DEF 子程序"],
["movej", "MoveJ", "J P[...] ", "PTP"],
["movel", "MoveL", "L P[...] ", "LIN"],
["movec", "MoveC", "C P[...] ", "CIRC"],
["io.do[1] = true", "SetDO do1, 1", "DO[1]=ON", "$OUT[1]=TRUE"],
["wait io.di[1] == true", "WaitDI di1, 1", "WAIT DI[1]=ON", "WAIT FOR $IN[1]==TRUE"]
]));
body.push(p("后处理器遇到品牌不支持或只能近似表达的语义时,应写入转换报告。用户必须检查报告,再到真实控制器中做品牌校验和现场低速试运行。"));
body.push(heading("十四、常见诊断和处理建议", 1));
body.push(table([
["诊断", "常见原因", "处理方式"],
["GRL_EXPR_UNKNOWN_SYMBOL", "表达式引用未声明常量或变量。", "检查拼写和声明顺序。"],
["GRL_EXPR_UNIT_MISMATCH", "不同物理单位混合计算。", "统一长度、时间、角度或速度维度。"],
["GRL_TARGET_NOT_FOUND", "运动或路径点引用不存在目标点。", "补充 target 或修正名称。"],
["KDL_TARGET_UNREACHABLE", "笛卡尔点超出机器人工作空间。", "调整目标点、工具、坐标系或机器人配置。"],
["KDL_JOINT_LIMIT", "目标关节超过限位。", "修改 joint_target 或使用 IK 可达姿态。"],
["GRL_PATH_EMPTY", "path 没有 point。", "至少添加一个 point。"],
["VC_WAIT_TIMEOUT", "wait 条件在 timeout 内未满足。", "检查 IO 输入、脚本、超时时间和 on_timeout 动作。"],
["GRL_POST_UNSUPPORTED", "目标品牌后处理暂不支持该语义。", "查看转换报告,必要时改写为品牌可支持结构。"]
]));
body.push(heading("十五、编写规范建议", 1));
body.push(table([
["建议", "说明"],
["名称稳定", "target、path point、operation 命名稳定,便于 diff 和回读。"],
["显式单位", "自动生成程序尽量输出 mm、deg、mm/s、ms 等单位。"],
["入口简短", "main 中只保留主流程调用,复杂路径放入 path/operation。"],
["默认集中", "path defaults 中集中设置 speed、zone、tool、frame。"],
["IO 事件贴近路径", "与路径点相关的夹具和工艺开关写成 event。"],
["诊断必看", "error 必须修复warning 必须人工确认info 可作为转换记录。"],
["先仿真后后处理", "先通过 GRL 编译和虚拟控制器,再输出品牌程序。"],
["真实运行前校验", "后处理程序必须经过品牌控制器验证和现场低速试运行。"]
]));
body.push(heading("十六、参考完整示例", 1));
body.push(p("以下是规范套件中的完整示例程序,覆盖 tool/frame、path、operation、IO event、run_operation 和回 home可作为复杂程序的参考。"));
body.push(codeBlock(readFileSync(fullExamplePath, "utf8")));
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:o="urn:schemas-microsoft-com:office:office"
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: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 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 copyImages(imageInputs) {
return imageInputs.map((image, index) => {
const fileName = `image-${String(index + 1).padStart(2, "0")}.png`;
copyFileSync(image.source, join(stagingRoot, "word", "media", fileName));
return {
...image,
id: `rIdImage${index + 1}`,
fileName
};
});
}
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 table(rows) {
const width = Math.floor(9200 / 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 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 imageWithCaption(image, docPrId, caption) {
return `${imageParagraph(image, docPrId)}${paragraph(caption, "Caption")}`;
}
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:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dc:title>GRL 功能语法逻辑与程序创建使用手册</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="Caption">
<w:name w:val="Caption"/>
<w:basedOn w:val="Normal"/>
<w:pPr><w:jc w:val="center"/><w:spacing w:after="160"/></w:pPr>
<w:rPr><w:i/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="18"/><w:color w:val="475569"/></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 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`;
}