diff --git a/README.md b/README.md index ab5c005..02f0d80 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p | 标题与目录 | 四级标题写入 Word/WPS 标题样式及大纲级别,可插入自动目录 | | 页码 | 居中、单双页或不显示 | | 印章、签名章 | 在生成的 DOCX 中按实际需要插入图片或完成实体盖章 | -| 特定格式 | `letter`、`command`、`minutes` 使用对应生成分支 | +| 特定格式 | `letter`、`command`、`minutes`、`horizontal-table` 使用对应生成分支;联合行文用 `--joint-org`,分离装订附件用 `--attachment-detached` | GB/T 9704-2012 当前为现行标准,2025-05-30 复审继续有效。标准适用于党政机关制发公文,其他机关和单位可以参照执行。[全国标准信息公共服务平台](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p) @@ -106,11 +106,11 @@ node scripts/verify_gongwen_docx.mjs --input /tmp/formal.docx --profile formal - 只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会写入两个显式的28磅版心空行,不会依赖渲染器自行解释段前距;发文机关标志到文号也按相同方式留出两行。上行文的文号和签发人会在同一行编排。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 -信函、命令(令)和纪要走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志下空二行、令号下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。 +信函、命令(令)、纪要和横排表格走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志距版心上边缘20mm、令号下空二行、正文下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋,并放在正文或附件说明下一行;横排表格生成横向A4和225mm表格,单双页表头方向仍需目标Word/WPS模板核对。联合行文可重复传入 `--joint-org`,不能与正文一起装订的附件加 `--attachment-detached` 会在首行补排文号和附件序号。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。 ## 跨平台安装 -本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、WorkBuddy 和 ZCode 使用。 +本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、TraeWork、WorkBuddy 和 ZCode 使用。 macOS / Linux: @@ -135,7 +135,8 @@ Windows: - [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。 - [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。 - [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。 -- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 13 份 DOCX、渲染 24 张 PNG,并逐条记录截图证据与人工边界。 +- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 18 份 DOCX、37 个 PDF 页面和 PNG,并逐条记录截图证据与人工边界。 +- [全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md):把 GB/T 9704-2012 每个条款绑定到实际截图,明确哪些项目截图不能证明。 - [版头坐标量测](tests/measure-header-coordinates.mjs):用 PDF 字形坐标独立核对份号首行、机关标志 35 mm 定位及可选字段不移位。 - [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。 diff --git a/SKILL.md b/SKILL.md index 66bb8f2..8787470 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,7 +15,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有 本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。 -生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。 +生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要和横排表格使用各自专用版式;电子联合行文可重复传入 `--joint-org`,分离装订附件可加 `--attachment-detached`;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。 `references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。 @@ -35,7 +35,7 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx -- node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead digital --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题" ``` -`letter`、`command`、`minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。 +`letter`、`command`、`minutes`、`horizontal-table` 分别对应信函、命令(令)、纪要和横排表格格式;需要时显式传入 `--format` 和相应字段。横排表格会生成横向A4页面和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板核对。联合行文使用 `--joint-org`;附件不能与正文一起装订时使用 `--attachment-detached`,附件首行会补排文号和附件序号。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。 附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明通过 `--attachment-note` 提供,工具会统一为“附件:”并去除末尾标点,同时拒绝顺序或标题与附件文件不一致的输入。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。 @@ -55,9 +55,9 @@ node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --lett bash tests/visual-audit.sh /tmp/gongwen-visual-audit ``` -全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)。横排表格、联合行文、实体印章、纸张和装订不能只由 DOCX 生成器证明。 +全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)和[全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md)。横排表格的单双页表头方向、实体印章、纸张和装订不能只由 DOCX 生成器证明。 -校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。 +校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes`、`horizontal-table` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。 ## 输出说明 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index 31bc310..1f842d8 100644 Binary files a/dist/traework-gongwen-skill.zip and b/dist/traework-gongwen-skill.zip differ diff --git a/references/assets/visual-audit/all-rules/command-1.png b/references/assets/visual-audit/all-rules/command-1.png new file mode 100644 index 0000000..b624f4d Binary files /dev/null and b/references/assets/visual-audit/all-rules/command-1.png differ diff --git a/references/assets/visual-audit/all-rules/command-2.png b/references/assets/visual-audit/all-rules/command-2.png new file mode 100644 index 0000000..c0657ca Binary files /dev/null and b/references/assets/visual-audit/all-rules/command-2.png differ diff --git a/references/assets/visual-audit/all-rules/contact-sheet.png b/references/assets/visual-audit/all-rules/contact-sheet.png new file mode 100644 index 0000000..30a629a Binary files /dev/null and b/references/assets/visual-audit/all-rules/contact-sheet.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-3.png b/references/assets/visual-audit/all-rules/formal-attachments-3.png new file mode 100644 index 0000000..7ce6dd0 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-attachments-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-4.png b/references/assets/visual-audit/all-rules/formal-attachments-4.png new file mode 100644 index 0000000..e4470fb Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-attachments-4.png differ diff --git a/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png b/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png new file mode 100644 index 0000000..d3b9174 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-attachments-detached-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-all-1.png b/references/assets/visual-audit/all-rules/formal-digital-all-1.png new file mode 100644 index 0000000..ed99d47 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-digital-all-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-all-2.png b/references/assets/visual-audit/all-rules/formal-digital-all-2.png new file mode 100644 index 0000000..4686e22 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-digital-all-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-upward-1.png b/references/assets/visual-audit/all-rules/formal-digital-upward-1.png new file mode 100644 index 0000000..bc05cb1 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-digital-upward-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-digital-upward-2.png b/references/assets/visual-audit/all-rules/formal-digital-upward-2.png new file mode 100644 index 0000000..f2859b6 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-digital-upward-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-joint-1.png b/references/assets/visual-audit/all-rules/formal-joint-1.png new file mode 100644 index 0000000..2bbb726 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-joint-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-joint-2.png b/references/assets/visual-audit/all-rules/formal-joint-2.png new file mode 100644 index 0000000..f2859b6 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-joint-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-long-2.png b/references/assets/visual-audit/all-rules/formal-long-2.png new file mode 100644 index 0000000..99daf94 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-long-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-long-3.png b/references/assets/visual-audit/all-rules/formal-long-3.png new file mode 100644 index 0000000..d18f095 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-long-3.png differ diff --git a/references/assets/visual-audit/all-rules/formal-preprinted-1.png b/references/assets/visual-audit/all-rules/formal-preprinted-1.png new file mode 100644 index 0000000..565b026 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-preprinted-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-preprinted-2.png b/references/assets/visual-audit/all-rules/formal-preprinted-2.png new file mode 100644 index 0000000..c0657ca Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-preprinted-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-seal-2.png b/references/assets/visual-audit/all-rules/formal-seal-2.png new file mode 100644 index 0000000..078d99e Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-seal-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-signed-2.png b/references/assets/visual-audit/all-rules/formal-signed-2.png new file mode 100644 index 0000000..26bd70e Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-signed-2.png differ diff --git a/references/assets/visual-audit/all-rules/formal-unsigned-1.png b/references/assets/visual-audit/all-rules/formal-unsigned-1.png new file mode 100644 index 0000000..471a2d5 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-unsigned-1.png differ diff --git a/references/assets/visual-audit/all-rules/formal-unsigned-2.png b/references/assets/visual-audit/all-rules/formal-unsigned-2.png new file mode 100644 index 0000000..fbdf9c0 Binary files /dev/null and b/references/assets/visual-audit/all-rules/formal-unsigned-2.png differ diff --git a/references/assets/visual-audit/all-rules/header-coordinates.log b/references/assets/visual-audit/all-rules/header-coordinates.log new file mode 100644 index 0000000..faf894e --- /dev/null +++ b/references/assets/visual-audit/all-rules/header-coordinates.log @@ -0,0 +1 @@ +Header coordinate measurement passed: copy=39.88mm, agency=71.41mm, no-fields-agency=71.41mm, delta=0.00mm diff --git a/references/assets/visual-audit/all-rules/horizontal-table-1.png b/references/assets/visual-audit/all-rules/horizontal-table-1.png new file mode 100644 index 0000000..9e10cb2 Binary files /dev/null and b/references/assets/visual-audit/all-rules/horizontal-table-1.png differ diff --git a/references/assets/visual-audit/all-rules/letter-1.png b/references/assets/visual-audit/all-rules/letter-1.png new file mode 100644 index 0000000..e714fcd Binary files /dev/null and b/references/assets/visual-audit/all-rules/letter-1.png differ diff --git a/references/assets/visual-audit/all-rules/letter-fields-1.png b/references/assets/visual-audit/all-rules/letter-fields-1.png new file mode 100644 index 0000000..86c4019 Binary files /dev/null and b/references/assets/visual-audit/all-rules/letter-fields-1.png differ diff --git a/references/assets/visual-audit/all-rules/manifest.tsv b/references/assets/visual-audit/all-rules/manifest.tsv new file mode 100644 index 0000000..ed0a231 --- /dev/null +++ b/references/assets/visual-audit/all-rules/manifest.tsv @@ -0,0 +1,19 @@ +scenario profile pdf_pages committed_screenshot_examples +command command 2 references/assets/visual-audit/all-rules/command-1.png, references/assets/visual-audit/all-rules/command-2.png +formal-attachments-detached digital 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png +formal-attachments digital 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.png +formal-digital-all digital 2 references/assets/visual-audit/all-rules/formal-digital-all-1.png, references/assets/visual-audit/all-rules/formal-digital-all-2.png +formal-digital-upward digital 2 references/assets/visual-audit/all-rules/formal-digital-upward-1.png, references/assets/visual-audit/all-rules/formal-digital-upward-2.png +formal-joint digital 2 references/assets/visual-audit/all-rules/formal-joint-1.png, references/assets/visual-audit/all-rules/formal-joint-2.png +formal-long digital 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png +formal-preprinted digital 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png +formal-seal digital 2 references/assets/visual-audit/all-rules/formal-seal-2.png +formal-signed digital 2 references/assets/visual-audit/all-rules/formal-signed-2.png +formal-unsigned digital 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-2.png +horizontal-table horizontal-table 1 references/assets/visual-audit/all-rules/horizontal-table-1.png +letter-fields letter 2 references/assets/visual-audit/all-rules/letter-fields-1.png +letter letter 1 references/assets/visual-audit/all-rules/letter-1.png +minutes digital 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.png +ordinary-calibration ordinary 2 references/assets/visual-audit/all-rules/ordinary-calibration-1.png +ordinary ordinary 1 references/assets/visual-audit/all-rules/ordinary-1.png +table ordinary 1 references/assets/visual-audit/all-rules/table-1.png diff --git a/references/assets/visual-audit/all-rules/minutes-1.png b/references/assets/visual-audit/all-rules/minutes-1.png new file mode 100644 index 0000000..26fb44b Binary files /dev/null and b/references/assets/visual-audit/all-rules/minutes-1.png differ diff --git a/references/assets/visual-audit/all-rules/minutes-2.png b/references/assets/visual-audit/all-rules/minutes-2.png new file mode 100644 index 0000000..e9c3a35 Binary files /dev/null and b/references/assets/visual-audit/all-rules/minutes-2.png differ diff --git a/references/assets/visual-audit/all-rules/ordinary-1.png b/references/assets/visual-audit/all-rules/ordinary-1.png new file mode 100644 index 0000000..89c5a4a Binary files /dev/null and b/references/assets/visual-audit/all-rules/ordinary-1.png differ diff --git a/references/assets/visual-audit/all-rules/ordinary-calibration-1.png b/references/assets/visual-audit/all-rules/ordinary-calibration-1.png new file mode 100644 index 0000000..3e0cd76 Binary files /dev/null and b/references/assets/visual-audit/all-rules/ordinary-calibration-1.png differ diff --git a/references/assets/visual-audit/all-rules/table-1.png b/references/assets/visual-audit/all-rules/table-1.png new file mode 100644 index 0000000..5443313 Binary files /dev/null and b/references/assets/visual-audit/all-rules/table-1.png differ diff --git a/references/formal-checklist.md b/references/formal-checklist.md index d12b4c3..50f32ef 100644 --- a/references/formal-checklist.md +++ b/references/formal-checklist.md @@ -12,7 +12,7 @@ ## 1. 前提与制作 -- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command` 或 `minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。 +- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command`、`minutes` 或 `horizontal-table`;报告、方案、汇报材料不因出现机构名称而自动采用红头。 - [ ] 正式发文已确认采用预印红头纸套打或完整电子红头。套打稿以实际纸样核定首页预留高度,不重复打印纸上既有的红色机关标志、红线。 - [ ] 已确认发文机关、文种、行文关系、主送范围、发文字号、签发权限及机关现行模板。 - [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。 @@ -43,7 +43,7 @@ - [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理:签发人签名章模式正文下空二行、职务左空二字,签名章下空一行、日期右空四字;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。空白不足时记录行距、字距调整。 - [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。 - [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;附件序号和标题与正文附件说明逐项一致;与正文一起装订的附件页码连续。不能一起装订时,左上补排发文字号及“附件”序号。 -- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。 +- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;使用 `--attachment-detached` 时已确认附件首行补排发文字号和“附件”标识;复杂附件仍需人工核对。 ## 4. 版记、页码与特殊格式 @@ -51,8 +51,11 @@ - [ ] 抄送机关适用时为四号仿宋、左右各空一字、回行与冒号后首字对齐、末尾句号;主送移入版记时按“主送”规则处理。 - [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。 - [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。 -- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。 +- [ ] 横排表格使用 `--format horizontal-table` 或目标 Word/WPS 横向模板;本项目已实际渲染横向页面和225mm表格,但单双页表头在订口/切口的方向仍需目标软件核对。 +- [ ] 信函、命令(令)、纪要、联合行文已使用相应专用版式;`formal --joint-org` 的截图已确认主办机关在前,复杂机关标志仍按现行模板核对;不能用普通打印稿替代。 - [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;如有份号、密级、紧急程度,均置于第一条双线下并距线3号字高7/8;文号顶格居右;首页无页码且版记不画分隔线。当前生成器对这些可选信函版头字段要求使用专用模板。 - [ ] 命令(令)格式已核对发文机关全称加“命令”或“令”、标志上边缘距版心20mm、令号下空二行、正文下空二行;签发章和日期按7.3.5.3核对。 - [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。 - [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。 + +- [ ] 已保存本次实际渲染的[全条款截图证据矩阵](gbt9704-screenshot-evidence.md),并逐项记录截图通过或截图不能证明的边界。 diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md index 0c99075..d951737 100644 --- a/references/gbt9704-2012-summary.md +++ b/references/gbt9704-2012-summary.md @@ -140,7 +140,7 @@ ## 八、横排表格和特定格式 -- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。 +- A4横排表格的页码位置与其他页一致;生成器的 `--format horizontal-table` 会生成横向A4和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板处理。 - 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。 - 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。 - 命令(令)发文机关标志由机关全称加“命令”或“令”组成,上边缘距版心20mm;标志下空二行编排令号,令号下空二行编排正文;签发人职务、签名章和日期按7.3.5.3处理。 @@ -155,14 +155,15 @@ | 四级正文标题 | 是 | 特殊机关模板调整 | | 报告居中页码 | 是 | 无 | | 正式单双页页码 | 是 | `--page-number standard`;页脚按版心下边缘下7mm的位置生成 | -| 普通表格 | 是 | 横排表格方向和复杂跨页表格复核 | +| 横排表格 | 部分通过 | `--format horizontal-table` 生成横向页面和225mm表格;单双页表头方向、复杂跨页表格复核 | | 普通材料 | 是 | `--format ordinary`,不绘制红头 | | 预印红头纸套打 | 是 | `--format formal --letterhead preprinted`,按纸样设置预留高度 | | 完整电子红头 | 是 | `--format formal --letterhead digital` | | 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 | -| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文 | 无法一起装订时的发文字号标注、复杂附件版式仍需人工复核 | +| 联合行文 | 通过 `--joint-org` 同行生成,主办机关在前 | 复杂机关专用标志和联署模板复核 | +| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文;`--attachment-detached` 补排文号和附件序号 | 复杂附件版式仍需人工复核 | | 版记 | 是 | `--cc`、`--print-org`、`--print-date` 适用时生成 | -| 信函、命令、纪要 | 是 | `--format letter|command|minutes`,按实际字段生成 | +| 信函、命令、纪要、横排表格 | 是/部分通过 | `--format letter|command|minutes|horizontal-table`,按实际字段生成;横排表头方向需模板复核 | ## 十、建议检查 diff --git a/references/gbt9704-audit-matrix.md b/references/gbt9704-audit-matrix.md index e50c2e0..45eb9ec 100644 --- a/references/gbt9704-audit-matrix.md +++ b/references/gbt9704-audit-matrix.md @@ -20,7 +20,7 @@ | 7.2.1 | 份号六位、3 号、左上第一行 | `--copy-no` 只接受 1—6 位阿拉伯数字并补足六位 | 结构检查;实际是否需要份号由发文机关确认 | | 7.2.2 | 密级和保密期限第二行、3 号黑体 | `--secret` 按黑体3号写入;没有份号时自动保留第一行空位,使密级仍在第二行 | 结构检查和视觉审计;文字内容和期限写法人工复核 | | 7.2.3 | 紧急程度及排列顺序 | `--urgent` 按黑体 3 号写入,并按份号、密级、紧急程度顺序下移 | 适用性和紧急级别人工确认 | -| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文仍需模板 | `verify_gongwen_docx.mjs`、`tests/measure-header-coordinates.mjs` 和视觉核验记录;当前机缺少推荐字体时严格模式失败 | +| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文通过 `--joint-org` 同行生成,主办机关在前,名称过长时自适应标志字号 | `verify_gongwen_docx.mjs`、`tests/measure-header-coordinates.mjs` 和[`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md);当前机缺少推荐字体时严格模式失败,复杂机关专用标志仍需模板核对 | | 7.2.5 | 文号下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter` 对 `--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 | | 7.2.6 | 上行文文号左空一字、签发人右空一字并同一行;标签仿宋、姓名楷体 | `--upward true --signer` 同行生成单一或两个签发人,左右各空一字;多签发人、联合行文转人工模板 | 结构校验和 PDF;复杂联合行文需按机关顺序人工排布 | | 7.2.7 | 文号下 4mm、版心等宽红线 | `redRule()` 生成 156mm 红线,文号段后 4mm;预印模式不重绘 | PDF 量测;红色油墨和实际纸样人工核对 | @@ -30,16 +30,16 @@ | 7.3.4 | 附件说明正文下空一行、左空二字、名称末无标点 | `--attachment-note` 统一为“附件:”并去除末尾标点,表格后不重复空行 | 长名称回行和多附件编号仍需 PDF/模板复核 | | 7.3.5 | 署名、成文日期、印章/签名章 | 分别记录加盖印章、不加盖印章、签发人签名章、日期数字和特殊空白处理;生成器排文字占位并补足签名章模式的两行/一行间距,不伪造印章 | `--seal-mode` 仍需把实体章或已取得的图片放入 Word/WPS;单/联合机关印章、红色签名章和空白不足时调整人工完成 | | 7.3.6 | 附注在日期下一行左空二字、圆括号 | `--note` 规范化为全角圆括号 | 日期与附注的最终分页人工核对 | -| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | 不能一起装订时的文号+附件标识、复杂附件版式人工处理 | +| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | `--attachment-detached` 会在附件首行补排文号和“附件”序号;复杂附件版式仍需人工处理 | | 7.4.1 | 版记首末粗线、中间细线,末线贴最后一页版心下边缘 | 版记改为浮动表,锚定最后一页版心底部;粗线 0.35mm、细线 0.25mm | 已用长文 PDF 截图实测;Word/WPS 和极端分页仍需抽检 | | 7.4.2 | 抄送 4 号仿宋、左右一字、回行对齐、末尾句号 | `--cc` 生成 4 号、左右一字、全角冒号和句号 | 多行抄送回行和主送移版记人工复核 | | 7.4.3 | 印发机关/日期 4 号仿宋,左右一字,日期后“印发”,细线隔开 | `--print-org`、`--print-date` 生成右制表位、日期校验和末条粗线 | 实际机关名称长度和目标软件分页人工复核 | | 7.5 | 4 号半角宋体页码、两侧一字线、距版心下边缘 7mm、单双页位置、附件连续 | 正式页脚用半角宋体、单双页字段,footer 距离调整为 7mm 位置;信函关闭页码 | XML + PDF bbox;空白页/特定版记页的例外仍需人工复核 | -| 8 | 横排表格页码与表头方向 | 普通 Markdown 表格按纵向版心生成;视觉审计保留 `table-1.png` 作为未自动化证据,不把纵向表格冒充横排 | 横排表格须在 Word/WPS 采用专用横向模板并人工检查 | +| 8 | 横排表格页码与表头方向 | 新增 `--format horizontal-table`,实际生成横向 A4 和225mm表格,页码保持标准单双页位置;单双页表头在订口/切口方向尚未自动化 | [`horizontal-table-1.png`](gbt9704-screenshot-evidence.md);表头方向须在 Word/WPS 专用横向模板中人工检查 | | 9 | 计量单位、标点、数字按引用标准 | 结构字段(文号、日期、附件冒号、句号)做边界校验 | 正文语义、单位、标点和数字用法需人工或专门文本审校 | | 10.1 | 信函上下双红线、30mm/20mm、170mm、可选版头字段、标题二行、无首页页码、无版记线 | `--format letter` 生成双线、专用边距、文号距离、标题空行、底部双线页脚和无页码,并支持份号、密级、紧急程度在第一条双线下按顺序编排 | PDF 实测;联合行文及模板字段人工复核 | -| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`,两个间隔均为显式28磅空行 | 令号文字和签名章/日期按7.3.5.3人工复核 | -| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,并按两字起排、悬挂缩进处理回行 | `references/gbt9704-visual-audit.md` 的 `minutes-1.png`/`minutes-2.png`;纪要具体版式可按机关制度调整 | +| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`,机关标志从版心上边缘下20mm开始,令号和正文均为空二行 | [`command-1.png`](gbt9704-screenshot-evidence.md);签名章/日期按7.3.5.3人工复核 | +| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,紧跟正文或附件说明下一行,并按两字起排、悬挂缩进处理回行 | [`minutes-1.png`](gbt9704-screenshot-evidence.md)/[`minutes-2.png`](gbt9704-screenshot-evidence.md);纪要具体版式可按机关制度调整 | | 11 | 标准式样图 | 各分支按条款参数实现,不把截图当作唯一依据 | 最终仍需与机关现行式样、Word/WPS 和打印样张逐页比对 | ## 本轮硬性修正 @@ -50,4 +50,4 @@ - 文号、成文日期、附件说明的结构边界现在会拒绝明显不符合标准的输入;缺失小标宋或仿宋时仍会明确告警,严格模式会停止生成。 -本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)。 +本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)和[全条款截图证据矩阵](gbt9704-screenshot-evidence.md)。 diff --git a/references/gbt9704-screenshot-evidence.md b/references/gbt9704-screenshot-evidence.md new file mode 100644 index 0000000..93245f4 --- /dev/null +++ b/references/gbt9704-screenshot-evidence.md @@ -0,0 +1,111 @@ +# GB/T 9704-2012 全条款截图证据矩阵 + +本文件是本次修订的逐条复核记录。它把国标条款、生成场景、实际渲染 PNG 和可证明边界逐一对应起来。截图只证明截图中可观察到的版式结果;纸张指标、油墨、套印、装订、机关语义和目标 Word/WPS 的专用模板行为,明确标为“截图不能证明”,不以 DOCX ZIP 属性替代。 + +标准依据: + +- [全国标准信息公共服务平台现行状态页](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p):GB/T 9704-2012,2025-05-30 复审结论为继续有效。 +- [GB/T 9704-2012 条文全文转载页](https://dzb.cumtb.edu.cn/info/1040/1184.htm):本次逐条对照的公开全文。 + +## 本次实际渲染批次 + +运行命令: + +```bash +bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules +``` + +结果:18 份 DOCX、18 份 PDF、37 个 PDF 页面;每份 DOCX 先由校验器检查,再由 LibreOffice 转 PDF,最后用 `pdftoppm` 生成 PNG。`tests/measure-header-coordinates.mjs` 还用 `pdftotext -bbox-layout` 独立量测了电子红头首面:份号 39.88mm,机关标志 71.41mm,带字段和不带字段的位置差 0.00mm。 + +提交到仓库的证据目录为 [`references/assets/visual-audit/all-rules/`](assets/visual-audit/all-rules/),其中: + +- [`manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv) 记录 18 个场景、页数和已提交截图。 +- [`contact-sheet.png`](assets/visual-audit/all-rules/contact-sheet.png) 是场景总览;单页 PNG 保留原始截图尺寸。 +- [`header-coordinates.log`](assets/visual-audit/all-rules/header-coordinates.log) 是独立坐标量测结果。 + +## 场景与截图索引 + +| 场景 | 用途 | 截图 | +| --- | --- | --- | +| `ordinary` | 普通材料,不因机构名称变红头 | [`ordinary-1.png`](assets/visual-audit/all-rules/ordinary-1.png) | +| `ordinary-calibration` | 22 行、28 字网格校准页 | [`ordinary-calibration-1.png`](assets/visual-audit/all-rules/ordinary-calibration-1.png) | +| `formal-digital-all` | 电子红头全要素、附件说明、附注、版记 | [`formal-digital-all-1.png`](assets/visual-audit/all-rules/formal-digital-all-1.png)、[`formal-digital-all-2.png`](assets/visual-audit/all-rules/formal-digital-all-2.png) | +| `formal-digital-upward` | 上行文文号、签发人、单双页页码 | [`formal-digital-upward-1.png`](assets/visual-audit/all-rules/formal-digital-upward-1.png)、[`formal-digital-upward-2.png`](assets/visual-audit/all-rules/formal-digital-upward-2.png) | +| `formal-preprinted` | 预印红头纸套打留白 | [`formal-preprinted-1.png`](assets/visual-audit/all-rules/formal-preprinted-1.png)、[`formal-preprinted-2.png`](assets/visual-audit/all-rules/formal-preprinted-2.png) | +| `formal-joint` | 联合行文机关标志 | [`formal-joint-1.png`](assets/visual-audit/all-rules/formal-joint-1.png)、[`formal-joint-2.png`](assets/visual-audit/all-rules/formal-joint-2.png) | +| `formal-unsigned` | 不加盖印章文字位置 | [`formal-unsigned-1.png`](assets/visual-audit/all-rules/formal-unsigned-1.png)、[`formal-unsigned-2.png`](assets/visual-audit/all-rules/formal-unsigned-2.png) | +| `formal-seal` / `formal-signed` | 印章、签名章文字占位 | [`formal-seal-2.png`](assets/visual-audit/all-rules/formal-seal-2.png)、[`formal-signed-2.png`](assets/visual-audit/all-rules/formal-signed-2.png) | +| `formal-attachments` / `formal-attachments-detached` | 附件另面、分离装订附件 | [`formal-attachments-3.png`](assets/visual-audit/all-rules/formal-attachments-3.png)、[`formal-attachments-4.png`](assets/visual-audit/all-rules/formal-attachments-4.png)、[`formal-attachments-detached-3.png`](assets/visual-audit/all-rules/formal-attachments-detached-3.png) | +| `formal-long` | 版记底部锚定、单双页页码 | [`formal-long-2.png`](assets/visual-audit/all-rules/formal-long-2.png)、[`formal-long-3.png`](assets/visual-audit/all-rules/formal-long-3.png) | +| `letter` / `letter-fields` | 信函双线和可选字段 | [`letter-1.png`](assets/visual-audit/all-rules/letter-1.png)、[`letter-fields-1.png`](assets/visual-audit/all-rules/letter-fields-1.png) | +| `command` | 命令(令)标志、令号、正文间距 | [`command-1.png`](assets/visual-audit/all-rules/command-1.png) | +| `minutes` | 纪要标志和出席、请假、列席人员 | [`minutes-1.png`](assets/visual-audit/all-rules/minutes-1.png)、[`minutes-2.png`](assets/visual-audit/all-rules/minutes-2.png) | +| `horizontal-table` / `table` | 横排表格和纵向表格对照 | [`horizontal-table-1.png`](assets/visual-audit/all-rules/horizontal-table-1.png)、[`table-1.png`](assets/visual-audit/all-rules/table-1.png) | + +## 逐条核对 + +状态只使用以下三种含义: + +- **截图通过**:本次 PNG 中已经看到条款要求的可视结果,并有结构或坐标检查辅助。 +- **截图通过,字体/目标软件仍需核对**:位置或结构可见,但本机缺标准字体,或 Word/WPS 的专用行为还要在目标环境复核。 +- **截图不能证明**:标准要求的是纸张、印刷、装订、语义或实体印章等外部条件;工具没有伪造通过结论。 + +| 条款 | 国标要求 | 本批截图与结果 | 状态及边界 | +| --- | --- | --- | --- | +| 1 | 规定党政机关公文的纸张、版面、印制装订、格式要素和式样;其他单位可参照。 | `ordinary-1.png`、`formal-digital-all-1.png`、`letter-1.png`、`command-1.png`、`minutes-1.png` 展示了不同格式分支。 | 截图通过;具体文种、行文关系和机关模板仍由使用者确认。 | +| 2 | 引用 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835。 | 本仓库摘要、矩阵和本证据文件均列出引用文件;截图不能验证引用标准的语义执行。 | 截图不能证明正文单位、标点和数字语义。 | +| 3.1 | 一字为一个汉字宽度。 | `ordinary-calibration-1.png` 显示固定字宽网格样例。 | 截图通过,实际字体字面宽度随目标字体核对。 | +| 3.2 | 一行为一个汉字高度加三号字高度 7/8。 | `ordinary-calibration-1.png` 显示 28 磅实现网格;XML 校验确认 `w:linePitch=560`。 | 截图通过,术语本身不能只靠截图定义。 | +| 4 | 纸张 60—80g/m²、白度80%—90%、横向耐折度≥15、不透明度≥85%、pH7.5—9.5。 | 生成器没有把这些物性写成 DOCX 属性;截图只显示页面。 | 截图不能证明;须以纸张规格和成品检测为准。 | +| 5.1 | A4 成品 210mm×297mm。 | 18 个 PDF 场景均由脚本检查 A4;`horizontal-table-1.png` 为 A4 横向页面。 | 截图通过;裁切误差仍属成品核验。 | +| 5.2.1 | 天头37±1mm、订口28±1mm、版心156×225mm。 | `formal-digital-all-1.png`、`formal-preprinted-1.png`、`ordinary-calibration-1.png`;校验器检查页边,标题和正文落在版心内。 | 截图通过;打印缩放关闭后需再量纸样。 | +| 5.2.2 | 默认三号仿宋,特殊情况可调整。 | 全部截图可见三号网格;本机实际使用 `STFangsong` 替代并输出警告,严格模式会失败。 | 截图通过,字体仍需目标机安装方正小标宋/仿宋后复核。 | +| 5.2.3 | 一般每面22行、每行28字并撑满版心。 | `ordinary-calibration-1.png` 是 22 行×28 字校准页;普通长文和特定格式按内容自然分页。 | 截图通过;长标题、表格和目标字体导致的特殊调整需逐文档看图。 | +| 5.2.4 | 未特别说明时文字为黑色。 | `ordinary-1.png`、`formal-digital-all-2.png` 的正文、标题、版记文字为黑色;红色仅用于规定的红头和线。 | 截图通过。 | +| 6.1 | 版面干净无底灰、字迹清楚无断划、尺寸标准、版心不斜,误差≤1mm。 | `contact-sheet.png` 中所有 PDF 页面均无生成的底灰或倾斜;屏幕截图不能证明实体制版误差。 | 截图部分通过;实体误差和印品质量不能由本工具证明。 | +| 6.2 | 双面印刷、套正≤2mm、黑墨BL100%、红墨Y80%/M80%,着墨均匀无缺陷。 | `formal-long-2.png`、`formal-long-3.png` 展示双页页码位置;无实体双面印刷样。 | 截图不能证明油墨、套正和印品质量。 | +| 6.3 | 左侧装订、页码误差≤4mm、成品±2mm、两钉距上下边缘70±4mm、平订距书脊3—5mm等。 | `formal-long-3.png` 只显示页面顺序和版记位置。 | 截图不能证明装订、裁切、钉位和封皮。 | +| 7.1 | 版头、主体、版记三区;页码位于版心外。 | `formal-digital-all-1.png` 展示版头与主体,`formal-long-3.png` 展示末页版记和页码。 | 截图通过。 | +| 7.2.1 | 份号六位三号阿拉伯数字,版心左上第一行。 | `formal-digital-all-1.png`、`formal-preprinted-1.png` 显示 `000007`;独立坐标日志显示 39.88mm 首行位置。 | 截图通过。 | +| 7.2.2 | 密级和保密期限第二行三号黑体,数字用阿拉伯数字。 | `formal-digital-all-1.png` 显示 `机密★3年` 位于第二行。 | 截图通过;密级文字及期限是否适用需机关确认。 | +| 7.2.3 | 紧急程度三号黑体;与份号、密级同时出现时按份号、密级、紧急程度自上而下。 | `formal-digital-all-1.png` 显示三项顺序;`letter-fields-1.png` 显示信函双线下同顺序。 | 截图通过。 | +| 7.2.4 | 机关全称/规范简称可加“文件”,居中红色;上边缘距版心上边缘35mm;联合行文主办机关在前,联署名称上下居中。 | `formal-digital-all-1.png`、`formal-joint-1.png`;坐标日志显示机关标志 71.41mm 页面坐标,字段差0.00mm;联合行文以主办机关在前并自适应同一行字号。 | 截图通过,推荐小标宋字体需目标机复核;复杂机关专用标志仍需模板核对。 | +| 7.2.5 | 机关标志下空二行;年份全称、六角括号、顺序号不加“第”不补零、末尾“号”;上行文文号左空一字。 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;文号输入边界和显式空行均经校验。 | 截图通过。 | +| 7.2.6 | “签发人:”三号仿宋、姓名三号楷体,右空一字;多签发人按顺序、一般每行两名、回行对齐。 | `formal-digital-upward-1.png` 显示同一行双签发人,姓名字体和标签分 run。 | 截图通过;超过两名或联合行文复杂签发人仍须专用模板。 | +| 7.2.7 | 文号下4mm处、版心等宽红色分隔线。 | `formal-digital-all-1.png`、`formal-joint-1.png`;线宽156mm,红线下标题空两行。 | 截图通过;预印模式明确不重绘红线。 | +| 7.3.1 | 标题二号小标宋,红线下空二行,居中;多行应词意完整、对称、梯形或菱形。 | `formal-digital-all-1.png`、`formal-preprinted-1.png`;标题样式为 `GongwenTitle`,空行在 PNG 中可见。 | 截图通过;小标宋缺失时严格模式失败;长标题断行需逐稿人工调整。 | +| 7.3.2 | 标题下空一行,主送机关顶格,回行顶格,末尾全角冒号;过多可移版记。 | `formal-digital-all-1.png`、`formal-joint-1.png`。 | 截图通过;自动移版记的决定由发文机关确认。 | +| 7.3.3 | 首页有正文;正文三号仿宋、自然段左空二字回行顶格;层次序数依次“一、”“(一)”“1.”“(1)”,字体黑体/楷体/仿宋。 | `formal-digital-all-1.png`、`ordinary-1.png` 显示首页正文和四级层次;校验器确认 Heading1—Heading4、大纲级别。 | 截图通过,标准字体和复杂分页仍需目标软件复核。 | +| 7.3.4 | 正文下空一行左空二字写“附件:”;多附件编号;附件名称末不加标点,回行与首字对齐。 | `formal-digital-all-2.png` 显示附件说明;生成器校验说明与附件标题、顺序一致。 | 截图通过;极长名称需看最终换行。 | +| 7.3.5.1 | 加盖印章:日期右空四字,红色印章压署名和日期,印章顶端距正文/附件说明一行内,不得空白印章;联合章须排列整齐。 | `formal-seal-2.png` 展示署名和日期占位;生成器不写入虚假印章。 | 截图不能证明实体章和联合章几何关系;需 Word/WPS 或打印盖章。 | +| 7.3.5.2 | 不盖章:署名正文下空一行右空二字,日期下一行首字右移二字;联合署名自上而下。 | `formal-unsigned-2.png` 显示署名日期两行和右侧定位。 | 截图通过;多机关联合署名需目标模板复核。 | +| 7.3.5.3 | 签发人签名章:正文下空二行,职务左空二字,签名章右空四字;日期下空一行右空四字;联合逐行对齐。 | `formal-signed-2.png` 展示职务/签名章文字占位和日期行。 | 截图部分通过;实体签名章颜色、尺寸、联合对齐不能由本工具证明。 | +| 7.3.5.4 | 年月日使用阿拉伯数字,年份全称,月日不补零。 | `formal-digital-all-2.png`、`formal-unsigned-2.png` 显示 `2026年9月10日`;输入校验拒绝 `09月`。 | 截图通过。 | +| 7.3.5.5 | 空白不足时可调行距、字距容纳印章或签名章、日期。 | `formal-seal-2.png`、`formal-signed-2.png` 仅覆盖正常空间。 | 截图不能证明极端内容自动调节;需按稿件人工调节。 | +| 7.3.6 | 附注在日期下一行左空二字,用圆括号。 | `formal-digital-all-2.png` 显示 `(此件公开)`。 | 截图通过。 | +| 7.3.7 | 附件另面、版记前;第一行附件标签三号黑体顶格,标题第三行居中;与附件说明一致;不能一起装订时左上补发文字号和附件序号。 | `formal-attachments-3.png`、`formal-attachments-4.png`、`formal-attachments-detached-3.png`;分离装订截图显示文号+附件序号。 | 截图通过;复杂附件内容和实体装订仍需复核。 | +| 7.4.1 | 版记线与版心等宽,首末粗线约0.35mm,中间细线约0.25mm,末线与末页版心下边缘重合。 | `formal-long-3.png` 显示粗—细—粗顺序;版记表锚定末页版心底部。 | 截图通过;极端内容在 Word/WPS 需抽检。 | +| 7.4.2 | 抄送四号仿宋,印发机关日期上一行,左右各空一字,回行对齐,末尾句号;主送移版记时改“主送”。 | `formal-digital-all-2.png`、`formal-long-3.png` 显示抄送、回行和句号。 | 截图通过;主送自动移版记仍是人工决定。 | +| 7.4.3 | 印发机关日期四号仿宋,末条线之上,左一字右一字,日期后“印发”;其他要素用细线隔开。 | `formal-digital-all-2.png`、`formal-long-3.png`。 | 截图通过。 | +| 7.5 | 四号半角宋体页码,两侧一字线,线上距版心下边缘7mm;单页右、双页左;空白/版记例外;附件连续。 | `formal-long-2.png`、`formal-long-3.png`、`formal-attachments-4.png`;校验器和 PDF 坐标检查页脚。 | 截图通过;空白页例外和实体套印仍需人工。 | +| 8 | 横排 A4 表格页码位置不变,单页表头在订口边、双页表头在切口边。 | `horizontal-table-1.png` 已实际渲染 A4 横向页面、225mm 表格和标准页码;截图同时暴露当前表头仍在横向页面上边缘,尚未实现单双页订口/切口方向转换。 | 部分通过;表头朝向必须在 Word/WPS 横向模板中处理,不能宣称本生成器已完全覆盖8。 | +| 9 | 计量单位、标点、数字分别符合引用标准。 | `formal-digital-all-2.png` 有 `10 kg、5 m、3.14` 观察样例;工具只校验字段边界。 | 截图不能证明全文语义;需文本审校。 | +| 10.1 | 信函:标志距上页边30mm;上粗下细双线、下页边20mm处上细下粗,均170mm;可选字段顺序;标题空二行;首页无页码;版记无印发/分隔线。 | `letter-1.png`、`letter-fields-1.png`;校验器检查双线宽度、方向、底线页脚和无页码。 | 截图通过;联合行文信函和机关专用模板仍需复核。 | +| 10.2 | 命令(令):机关全称加“命令/令”,标志上边缘距版心上边缘20mm;令号下空二行,正文下空二行;签章日期按7.3.5.3。 | `command-1.png`;独立截图中标志约57mm页面坐标,令号和正文间隔可见。 | 截图通过;签章和日期实体环节仍需人工。 | +| 10.3 | 纪要标志上边缘距版心上边缘35mm;出席、请假、列席标签三号黑体,人员三号仿宋,正文/附件说明下空一行左空二字,回行对齐。 | `minutes-1.png`、`minutes-2.png`;出席信息已移动到正文/附件说明之后,而不是错误地追加到版记之后。 | 截图通过;纪要可按机关实际制度调整。 | +| 11 | 给出 A4 页边、普通首页、联合行文、末页、附件、信函、命令等式样。 | `contact-sheet.png` 覆盖普通、联合、末页、附件、信函、命令和纪要实际渲染场景。 | 截图通过;正式发文仍应对照本机关现行式样图。 | + +## 本次发现并修正的实现问题 + +1. 命令(令)分支曾把20mm误当成纸张上边距,导致机关标志距离纸顶约20mm;现在恢复标准37mm天头,并在版心上边缘下20mm处排标志,实际截图已复核。 +2. 联合行文曾把联署机关名称拆成多行,容易把“上下居中”误做成额外机关标志行;现在主办机关在前,联署名称同一居中行,自适应较小标志字号以保持一行。 +3. 纪要出席、请假、列席名单曾追加在附件和版记之后;现在紧跟正文或附件说明下一行生成,符合10.3的编排位置。 +4. PDF 字形量测曾按 `yMin` 粗略分组,遇到高字形会把同一视觉行拆开;现在按字形垂直重叠合并后再量测,独立坐标结果稳定。 +5. 视觉审计补入了横排表格、22×28校准页、联合行文、分离装订附件、不盖章公文和命令(令)等场景,不再只依赖电子红头样例。 + +## 不能由生成器或截图单独保证的边界 + +- 当前机器没有方正小标宋和标准仿宋字体;生成器会明确报警,严格模式会停止。要交付标准字体稿,应在目标机器安装标准字体后重新生成并截图。 +- 纸张克重、白度、耐折度、不透明度、pH、黑红油墨、双面套正、裁切、装订、钉位和实体印章属于制作环节。 +- 正文单位、标点、数字语义,以及文种、行文关系、主送范围、机关名称规范化和发文权限属于内容或机关审校。 +- 横排表格的单双页表头朝向、复杂联合机关标志、超过两名签发人、复杂联合印章和目标机关专用模板仍需在 Word/WPS 或打印样张中完成。 diff --git a/references/gbt9704-visual-audit.md b/references/gbt9704-visual-audit.md index 3878bf0..3f19def 100644 --- a/references/gbt9704-visual-audit.md +++ b/references/gbt9704-visual-audit.md @@ -4,19 +4,17 @@ ## 核验方法 -2026 年 9 月 10 日运行: +2026 年 9 月 11 日运行: ```bash -bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest +bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules ``` -脚本实际生成 13 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 24 张 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近,并把带版头字段与不带字段的机关标志位置差控制在 0.8 mm 内;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在运行目录的 `manifest.tsv`,每份文件的校验日志保存在 `logs/`。 +脚本实际生成 18 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 37 个 PDF 页面和 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近,并把带版头字段与不带字段的机关标志位置差控制在 0.8 mm 内;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在提交的 [`assets/visual-audit/all-rules/manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv),单页证据和总览图保存在同目录;每份文件的校验日志保存在运行目录的 `logs/`。完整逐条矩阵见 [`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md)。 -![各场景首页视觉核验](assets/visual-audit/first-pages.png) +![本次18个场景全览](assets/visual-audit/all-rules/contact-sheet.png) -![全部核验页缩略图](assets/visual-audit/all-pages.png) - -![单双页页码与末页版记](assets/visual-audit/long-pages-colophon.png) +本批次的原始单页截图、场景清单和坐标日志见 [`assets/visual-audit/all-rules/`](assets/visual-audit/all-rules)。 ## 场景证据 @@ -30,9 +28,9 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest | 长文版记 | `formal-long-1.png`、`formal-long-2.png`、`formal-long-3.png` | 单页右、双页左页码连续;末页版记贴近版心底部,粗细线顺序可见。 | | 盖章/签名章位置 | `formal-seal-2.png`、`formal-signed-2.png` | 文字落款和日期位置已生成;实体印章或签名章图片没有被伪造,需在 Word/WPS 或打印环节完成。 | | 信函 | `letter-1.png`、`letter-fields-1.png` | 上粗下细双红线、文号、标题和页末上细下粗双红线可见;带份号、密级、紧急程度的回归页也按第一条双线下顺序出现;不显示页码。 | -| 命令(令) | `command-1.png` | 机关标志、令号下空二行、正文前再空二行可见。 | -| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐。 | -| 横排表格 | `table-1.png` | 当前示例仍是纵向表格。这一张图是故意保留的缺口证据,不能把普通纵向表格宣称为标准横排表格。 | +| 命令(令) | `command-1.png` | 机关标志距版心上边缘20mm,令号下空二行、正文前再空二行可见。 | +| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐,且已移动到正文/附件说明下一行。 | +| 横排表格 | `horizontal-table-1.png`、`table-1.png` | `horizontal-table-1.png` 已实际渲染 A4 横向页面和225mm表格;`table-1.png` 保留纵向对照。单双页表头朝向仍需目标 Word/WPS 模板处理。 | ## 条款逐项对照 @@ -75,19 +73,22 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest | 7.4.2 | 抄送机关字号、缩进、句号 | 截图通过 | `formal-long-3.png`;多行和主送移版记仍需结合机关模板。 | | 7.4.3 | 印发机关、日期、印发和细线 | 截图通过 | `formal-long-3.png`;实际名称过长时仍需在目标软件确认。 | | 7.5 | 页码字号、单双页位置、7 mm、附件连续 | 截图通过 | `formal-long-2.png`、`formal-long-3.png`、`formal-attachments-4.png`;空白页及特殊版记例外需人工复核。 | -| 8 | 横排表格 | 当前未自动化 | `table-1.png` 明确显示当前生成器输出纵向表格;横排 A4 表格、表头方向和页码方向必须使用 Word/WPS 专用横向模板。 | +| 8 | 横排表格 | 部分通过,表头方向未自动化 | `horizontal-table-1.png` 显示横向 A4 表格和标准页码;`table-1.png` 是纵向对照。单双页表头在订口/切口的方向切换尚未自动化,必须使用 Word/WPS 专用横向模板。 | | 9 | 计量单位、标点、数字 | 结构通过,仍需人工 | 字段边界已校验;正文语义必须按引用标准审校。 | | 10.1 | 信函格式 | 结构通过,仍需人工 | `letter-1.png` 显示 170 mm 上粗下细、下粗上细双线、文号、标题、无首页页码和无版记分隔线;可选份号、密级、紧急程度已在回归场景中按第一条双线下的顺序生成,联合行文仍需专用模板。 | -| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 | -| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志 35 mm、人员标签字体和两字起排已观察。 | +| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、标志距版心上边缘20mm、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 | +| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志35mm、人员标签字体、两字起排和人员名单在正文/附件说明下一行均已观察。 | | 11 | 标准式样图 | 结构通过,仍需人工 | 本记录使用标准条文和实际截图;最终发文仍须对照本单位现行式样图或授权模板。 | ## 发现并修正的实现问题 1. 版头字段曾以普通段落排列,份号、密级、紧急程度会把电子红头机关标志向下推移,预印红头套打的留白也会被挤开。现在这些字段改为首面固定定位,版心中保留等高占位;`formal-digital-all-1.png` 和 `formal-preprinted-1.png` 已重新观察。 -2. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离。 -3. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。 -4. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐。 +2. 命令(令)分支曾把20mm误当成纸顶边距;现在恢复37mm天头,并从版心上边缘下20mm量测。 +3. 联合行文曾把联署机关拆成多行;现在主办机关在前、联署名称同一居中行,并在名称过长时自适应标志字号。 +4. 纪要人员曾追加在附件和版记之后;现在紧跟正文或附件说明下一行。 +5. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离。 +6. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。 +7. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐。 ## 仍然不能由本工具单独保证的事项 diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs index 354aed0..127e824 100755 --- a/scripts/generate_gongwen_docx.mjs +++ b/scripts/generate_gongwen_docx.mjs @@ -108,7 +108,7 @@ function reportFontStatus(args) { function usage() { console.log(`Usage: - node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--page-number center|standard|none] [--require-standard-fonts] + node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--org 发文机关] [--joint-org 联署机关 ...] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--attachment-detached] [--page-number center|standard|none] [--require-standard-fonts] Notes: - Converts Markdown to a GB/T 9704-2012 page-layout DOCX. @@ -124,8 +124,8 @@ Notes: function parseArgs(argv) { const args = {}; - const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts"]); - const repeatable = new Set(["attachment-file"]); + const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts", "attachment-detached"]); + const repeatable = new Set(["attachment-file", "joint-org"]); for (let i = 2; i < argv.length; i += 1) { const key = argv[i]; if (!key.startsWith("--")) continue; @@ -175,7 +175,8 @@ function run(text, opts = {}) { const size = opts.size ?? "32"; const bold = opts.bold ? "" : ""; const color = opts.color ?? "000000"; - return `${bold}${esc(text)}`; + const textXml = String(text ?? "").split("\n").map((line, index) => `${index ? "" : ""}${esc(line)}`).join(""); + return `${bold}${textXml}`; } function paragraph(text, opts = {}) { @@ -223,19 +224,19 @@ function floatingHeaderFieldParagraph(text, fontPreset, index) { }); } -function floatingAgencyMarkParagraph(text) { +function floatingAgencyMarkParagraph(text, lineCount = 1, size = "56") { return paragraph(text, { align: "center", indent: false, fontPreset: "title", - size: "56", + size, color: "FF0000", before: AGENCY_FRAME_BEFORE, - frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 }, + frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 * Math.max(1, lineCount) }, }); } -function agencyMarkFlowSpacer() { +function agencyMarkFlowSpacer(extraDxa = 0) { return paragraph("\u00a0", { align: "left", indent: false, @@ -243,7 +244,7 @@ function agencyMarkFlowSpacer() { size: "2", line: "560", snapToGrid: false, - before: AGENCY_FLOW_BEFORE, + before: AGENCY_FLOW_BEFORE + extraDxa, after: "0", }); } @@ -547,15 +548,15 @@ function blankGridLines(count = 2) { return Array.from({ length: count }, () => blankGridLine()); } -function tableXml(headers, rows) { +function tableXml(headers, rows, width = CONTENT_W) { const colCount = headers.length; - const colWidth = Math.floor(CONTENT_W / colCount); + const colWidth = Math.floor(width / colCount); const grid = Array.from({ length: colCount }, () => ``).join(""); const tr = (cells, header = false) => { const trPr = header ? "" : ""; return `${trPr}${cells.map((cell) => tableCell(cell, colWidth, header)).join("")}`; }; - return `${grid}${tr(headers, true)}${rows.map((row) => tr(row)).join("")}`; + return `${grid}${tr(headers, true)}${rows.map((row) => tr(row)).join("")}`; } function tableCell(text, width, header) { @@ -715,14 +716,14 @@ function renderParagraph(block) { return paragraph(block.text); } -function attachmentPage(blocks, index, filePath) { +function attachmentPage(blocks, index, filePath, detachedDocNo = "") { const attachmentBlocks = [...blocks]; let attachmentTitle = path.basename(filePath, path.extname(filePath)); if (attachmentBlocks[0]?.type === "heading") { attachmentTitle = attachmentBlocks.shift().text; } const body = [ - paragraph(`附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }), + paragraph(detachedDocNo ? `${detachedDocNo} 附件${index}` : `附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }), emptyLine(), titleParagraph(attachmentTitle), ]; @@ -738,18 +739,22 @@ function resolvePageNumberMode(args, format) { if (args["no-page-number"] || args["no-page-numbers"]) return "none"; if (["center", "standard", "none"].includes(args["page-number"])) return args["page-number"]; if (format === "letter") return "none"; - if (format === "formal" || format === "command" || format === "minutes") return "standard"; + if (format === "formal" || format === "command" || format === "minutes" || format === "horizontal-table") return "standard"; return "center"; } function buildDocument(blocks, args) { let title = args.title; - const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; + const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary"; const letterhead = args.letterhead === "digital" ? "digital" : "preprinted"; const pageNumberMode = resolvePageNumberMode(args, format); + const horizontalTable = format === "horizontal-table" || args["horizontal-table"] === true || args["horizontal-table"] === "true"; + const horizontalContentWidth = mmToDxa(225); const body = []; let titleGapLines = 0; - const pageMarginTop = format === "letter" ? 1701 : format === "command" ? 1134 : 2098; + // All formats keep the standard 37 mm top white margin. Special formats + // offset their marks inside the type area, measured from that boundary. + const pageMarginTop = format === "letter" ? 1701 : 2098; const sourceBlocks = [...blocks]; if (!title && sourceBlocks[0]?.type === "heading") { title = sourceBlocks.shift().text; @@ -779,7 +784,18 @@ function buildDocument(blocks, args) { // GB/T 9704-2012 7.2.4 fixes the agency mark at 35 mm below the // type-area top. Left-corner fields occupy their own column and must // not move the centered mark upward. - body.push(floatingAgencyMarkParagraph(args.org)); + const jointOrganizations = Array.isArray(args["joint-org"]) ? args["joint-org"].map((name) => String(name).trim()).filter(Boolean) : []; + const agencyNames = [String(args.org).trim(), ...jointOrganizations]; + // GB/T 9704-2012 7.2.4.2 keeps joint agency names on one centered + // mark row, with the host agency first. Full-width spaces preserve a + // visible separation without creating extra logo rows that would move + // the document number and title downward. + const host = String(args.org).trim(); + const jointText = jointOrganizations.length && host.endsWith("文件") + ? [...[host.slice(0, -2), ...jointOrganizations], "文件"].join(" ") + : agencyNames.join(" "); + const agencyMarkSize = agencyNames.length > 1 ? (jointText.length > 16 ? "44" : "48") : "56"; + body.push(floatingAgencyMarkParagraph(jointText, 1, agencyMarkSize)); body.push(agencyMarkFlowSpacer()); if (args["doc-no"]) body.push(...blankGridLines(2)); } @@ -819,7 +835,9 @@ function buildDocument(blocks, args) { if (!args.org) throw new Error("command format requires --org"); if (!/(?:命令|令)$/.test(String(args.org).trim())) throw new Error("command --org must end with“命令”or“令”"); if (!args["doc-no"]) throw new Error("command format requires --doc-no for the 令号"); - body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "0" })); + // GB/T 9704-2012 10.2 measures the command mark from the top edge of + // the type area, not from the paper edge: 37 mm + 20 mm from the page. + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: String(mmToDxa(20)), after: "0" })); if (args["doc-no"]) { body.push(...blankGridLines(2)); body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "0" })); @@ -845,7 +863,7 @@ function buildDocument(blocks, args) { continue; } if (block.type === "table") { - body.push(tableXml(block.headers, block.rows)); + body.push(tableXml(block.headers, block.rows, horizontalTable ? horizontalContentWidth : CONTENT_W)); body.push(emptyLine()); continue; } @@ -855,6 +873,13 @@ function buildDocument(blocks, args) { if (body.at(-1) === emptyLine()) body.pop(); body.push(attachmentNote(args["attachment-note"])); } + // GB/T 9704-2012 10.3 places attendance information one line below the + // minutes body (or its attachment note), before any trailing signature or + // note material. Keeping it here also makes the screenshot evidence match + // the standard's “正文或附件说明下空一行” rule. + if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560")); + if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent)); + if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers)); if (args.sender || args.date) { body.push(...(args["seal-mode"] === "signed" ? blankGridLines(2) : [emptyLine()])); body.push(...signatureParagraphs(args)); @@ -867,13 +892,11 @@ function buildDocument(blocks, args) { return absolutePath; }); validateAttachmentConsistency(args["attachment-note"], absoluteAttachmentFiles); + if (args["attachment-detached"] && !args["doc-no"]) throw new Error("--attachment-detached requires --doc-no so the detached attachment can carry the document number"); absoluteAttachmentFiles.forEach((absolutePath, index) => { const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8")); - body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath)); + body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath, args["attachment-detached"] ? args["doc-no"] : "")); }); - if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560")); - if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent)); - if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers)); if (format !== "letter") body.push(...colophon(args)); const letterFooter = format === "letter" ? '' : ""; const footerRefs = letterFooter || (pageNumberMode === "standard" @@ -882,7 +905,10 @@ function buildDocument(blocks, args) { ? '' : ""); const footerDistance = format === "letter" || pageNumberMode === "standard" ? "1134" : "1588"; - body.push(`${footerRefs}`); + const pageSize = horizontalTable + ? `` + : ``; + body.push(`${footerRefs}${pageSize}`); return `${body.join("")}`; } @@ -1003,7 +1029,7 @@ const md = fs.readFileSync(args.input, "utf8"); try { const blocks = parseMarkdown(md); const documentXml = buildDocument(blocks, args); - const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; + const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary"; const pageNumberMode = resolvePageNumberMode(args, format); writeDocx(path.resolve(args.output), documentXml, pageNumberMode, format); console.log(`Generated: ${path.resolve(args.output)}`); diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index 6d44775..9ca0c29 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -6,7 +6,7 @@ import os from "node:os"; import path from "node:path"; function usage() { - console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--require-standard-fonts]"); + console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--require-standard-fonts]"); } function parseArgs(argv) { @@ -35,7 +35,7 @@ function paragraphs(xml) { return xml.match(/]*)?>[\s\S]*?<\/w:p>/g) const args = parseArgs(process.argv); if (!args.input || !fs.existsSync(args.input)) { usage(); process.exit(2); } const profile = args.profile ?? "ordinary"; -if (!["ordinary", "formal", "letter", "command", "minutes"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); } +if (!["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); } const doc = readPart(args.input, "word/document.xml"); const styles = readPart(args.input, "word/styles.xml"); @@ -71,9 +71,10 @@ const installedFonts = (() => { const checks = []; function check(name, ok, detail) { checks.push({ name, ok, detail }); } -check("A4 page size", /]*w:w="11906"[^>]*w:h="16838"/.test(doc), "210 mm x 297 mm"); -const expectedTopMargin = profile === "letter" ? "1701" : profile === "command" ? "1134" : "2098"; -check("Page margins", new RegExp(`]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : expectedTopMargin === "1701" ? 30 : 20} mm, bottom 35, left 28, right 26 mm`); +const horizontal = profile === "horizontal-table"; +check("A4 page size", horizontal ? /]*w:w="16838"[^>]*w:h="11906"[^>]*w:orient="landscape"/.test(doc) : /]*w:w="11906"[^>]*w:h="16838"/.test(doc), horizontal ? "297 mm x 210 mm landscape table page" : "210 mm x 297 mm"); +const expectedTopMargin = profile === "letter" ? "1701" : "2098"; +check("Page margins", new RegExp(`]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : 30} mm, bottom 35, left 28, right 26 mm`); check("Document grid", /]*w:linePitch="560"/.test(doc), "28 pt implementation grid"); const bodyFontStandard = /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312)"/.test(styles); const bodyFontAvailable = bodyFontStandard || (!requireStandardFonts && /w:eastAsia="STFangsong"/.test(styles)); @@ -122,13 +123,16 @@ if (profile === "ordinary") { check("Odd/even page positions", /w:jc w:val="right"/.test(footerOdd) && /w:jc w:val="left"/.test(footerEven), "odd right and even left"); check("Formal page-number distance", /w:footer="1134"/.test(doc), "page-number footer is 7 mm below the 225 mm type area"); } +if (horizontal) { + check("Horizontal table width", / /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p)); + const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="(?:56|48|44)"/.test(p) && /w:color w:val="FF0000"/.test(p)); // Keep this expected value independent from the generator's calibration // constants. It is 35 mm in twentieths of a point, as required by 7.2.4. const agencyOffsetFromTypeAreaTop = Math.round(35 * 56.692913); @@ -140,7 +144,7 @@ if (profile === "formal") { check("Digital red-head field frames", fixedHeaderFrames.every((value, index) => value === index * 560 || value === agencyOffsetFromTypeAreaTop), "left-corner fields use first-line grid positions without changing the agency mark anchor"); const docNoIndex = docParagraphs.findIndex((p) => /〔\d{4}〕[1-9]\d*号/.test(textOf(p))); const agencyToDocNo = agencyIndex >= 0 && docNoIndex > agencyIndex ? docParagraphs.slice(agencyIndex + 1, docNoIndex) : []; - const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="1880"/.test(p)); + const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="[1-9]\d*"/.test(p) && /w:snapToGrid w:val="false"/.test(p)); const agencyGapLines = agencyToDocNo.slice(-2); check("Agency mark to document number two blank lines", docNoIndex < 0 || (agencyFlowSpacer.length === 1 && agencyGapLines.length === 2 && agencyGapLines.every(isExplicitBlankGridLine)), "document number follows the fixed agency mark with two exact 28-point grid lines"); check("Two blank lines below red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the red rule"); @@ -165,6 +169,7 @@ if (profile === "letter") { if (profile === "command") { const commandParagraphs = paragraphs(doc); const commandAgencyIndex = commandParagraphs.findIndex((p) => /w:color w:val="FF0000"/.test(p)); + check("Command agency position", commandAgencyIndex >= 0 && /w:before="1134"/.test(commandParagraphs[commandAgencyIndex]), "command mark starts 20 mm below the 37 mm type-area top"); const commandOrderIndex = commandParagraphs.findIndex((p, index) => index > commandAgencyIndex && /w:jc w:val="center"/.test(p) && /w:sz w:val="32"/.test(p) && !/w:color w:val="FF0000"/.test(p)); const agencyToOrder = commandAgencyIndex >= 0 && commandOrderIndex > commandAgencyIndex ? commandParagraphs.slice(commandAgencyIndex + 1, commandOrderIndex) : []; check("Command agency-to-order spacing", commandOrderIndex < 0 || (agencyToOrder.length === 2 && agencyToOrder.every(isExplicitBlankGridLine)), "command agency mark is followed by two exact 28-point grid lines"); diff --git a/tests/fixtures/visual-calibration.md b/tests/fixtures/visual-calibration.md new file mode 100644 index 0000000..cda5b7f --- /dev/null +++ b/tests/fixtures/visual-calibration.md @@ -0,0 +1,43 @@ +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 + +甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙 diff --git a/tests/measure-header-coordinates.mjs b/tests/measure-header-coordinates.mjs index b07b87d..83f5d60 100644 --- a/tests/measure-header-coordinates.mjs +++ b/tests/measure-header-coordinates.mjs @@ -20,11 +20,12 @@ function decodeHtml(value) { function words(pdf) { const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" }); - return [...html.matchAll(/]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({ + return [...html.matchAll(/]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*yMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({ xMinPt: Number(match[1]), yMinPt: Number(match[2]), xMaxPt: Number(match[3]), - text: decodeHtml(match[4]), + yMaxPt: Number(match[4]), + text: decodeHtml(match[5]), })); } @@ -34,9 +35,12 @@ function findText(pdfWords, expected) { // measurement is independent of that tokenization choice. const rows = []; for (const word of [...pdfWords].sort((left, right) => left.yMinPt - right.yMinPt || left.xMinPt - right.xMinPt)) { - const row = rows.find(({ yMinPt }) => Math.abs(yMinPt - word.yMinPt) <= 1.5); - if (row) row.words.push(word); - else rows.push({ yMinPt: word.yMinPt, words: [word] }); + const row = rows.find(({ yMinPt, yMaxPt }) => word.yMinPt <= yMaxPt + 1.5 && word.yMaxPt >= yMinPt - 1.5); + if (row) { + row.words.push(word); + row.yMinPt = Math.min(row.yMinPt, word.yMinPt); + row.yMaxPt = Math.max(row.yMaxPt, word.yMaxPt); + } else rows.push({ yMinPt: word.yMinPt, yMaxPt: word.yMaxPt, words: [word] }); } for (const row of rows) { const ordered = row.words.sort((left, right) => left.xMinPt - right.xMinPt); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index bad6c15..fc23e49 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -12,6 +12,7 @@ FIXTURE="$SKILL_DIR/tests/fixture.md" "$NODE" --check "$GEN" "$NODE" --check "$VERIFY" +"$NODE" --check "$SKILL_DIR/tests/measure-header-coordinates.mjs" "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/layout.docx" --format ordinary --org "不应变红头的单位" --title "公文格式回归测试" --page-number center "$NODE" "$VERIFY" --input "$TMP_DIR/layout.docx" --profile ordinary @@ -74,6 +75,9 @@ unzip -p "$TMP_DIR/letter.docx" word/footerLetter.xml | grep -q 'w:fill="FF0000" unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q '附件:1. 测试附件' unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore' +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/detached-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕1号" --title "分离附件测试" --attachment-note "1. 测试附件" --attachment-file "$TMP_DIR/attachment.md" --attachment-detached +unzip -p "$TMP_DIR/detached-attachment.docx" word/document.xml | grep -q '示例发〔2026〕1号 附件1' + if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕6号" --title "附件一致性测试" --attachment-note "1. 另一份附件" --attachment-file "$TMP_DIR/attachment.md" >"$TMP_DIR/bad-attachment.out" 2>&1; then echo "mismatched attachment note unexpectedly succeeded" >&2 exit 1 @@ -91,6 +95,10 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-command.out" "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要正文" --attendees "张三、李四" --absent "王五" "$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes +minutes_xml=$(unzip -p "$TMP_DIR/minutes.docx" word/document.xml) +test "${minutes_xml%%出席:*}" != "$minutes_xml" +test "${minutes_xml%%w:sectPr*}" != "$minutes_xml" +test "${minutes_xml%%出席:*}" != "${minutes_xml%%w:sectPr*}" "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/seal-layout.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签章位置测试" --sender "示例机关" --date "2026年9月10日" --seal-mode seal unzip -p "$TMP_DIR/seal-layout.docx" word/document.xml | grep -q 'w:right="1280"' @@ -112,6 +120,18 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out" unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '000007' unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '机密★3年' +"$NODE" "$GEN" --input "$SKILL_DIR/tests/fixtures/visual-horizontal.md" --output "$TMP_DIR/horizontal-table.docx" --format horizontal-table --title "横排表格测试" --page-number standard +"$NODE" "$VERIFY" --input "$TMP_DIR/horizontal-table.docx" --profile horizontal-table +unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:orient="landscape"' +unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:tblW w:w="12756"' + +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关" --doc-no "示例发〔2026〕3号" --title "联合行文测试" +"$NODE" "$VERIFY" --input "$TMP_DIR/joint.docx" --profile formal --letterhead digital +unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '主办机关' +unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '协办机关' +unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '文件' +unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q 'w:sz w:val="48"' + if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/strict.docx" --format formal --letterhead digital --org "示例单位文件" --title "严格字体测试" --require-standard-fonts >"$TMP_DIR/strict.out" 2>&1; then if ! fc-list -f '%{family}\n' | grep -Eq '方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong'; then echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2 diff --git a/tests/visual-audit.sh b/tests/visual-audit.sh index 6f90f1a..cf14281 100755 --- a/tests/visual-audit.sh +++ b/tests/visual-audit.sh @@ -15,6 +15,7 @@ long="$skill_dir/tests/fixtures/visual-long.md" attach_a="$skill_dir/tests/fixtures/visual-attachment-a.md" attach_b="$skill_dir/tests/fixtures/visual-attachment-b.md" horizontal="$skill_dir/tests/fixtures/visual-horizontal.md" +calibration="$skill_dir/tests/fixtures/visual-calibration.md" node "$gen" --input "$all" --output "$out_dir/docx/ordinary.docx" --format ordinary --title "普通材料全要素实测" --page-number center node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-all.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔2026〕7号" --title "正式公文全要素实测" --to "各主送机关" --sender "示例单位" --date "2026年9月10日" --note "此件公开" --attachment-note "1. 数据清单;2. 实施计划。" --cc "办公室、财务部" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard @@ -22,25 +23,31 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-upward.docx" - node "$gen" --input "$all" --output "$out_dir/docx/formal-preprinted.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔2026〕9号" --title "预印红头套打实测" --page-number standard node "$gen" --input "$all" --output "$out_dir/docx/formal-seal.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "盖章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode seal --page-number standard node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签名章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode signed --signer-title "主要负责人" --signer "张三" --page-number standard +node "$gen" --input "$all" --output "$out_dir/docx/formal-unsigned.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "不盖章公文实测" --sender "示例单位" --date "2026年9月10日" --page-number standard node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕10号" --title "多附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --page-number standard +node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments-detached.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕15号" --title "分离装订附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --attachment-detached --page-number standard node "$gen" --input "$long" --output "$out_dir/docx/formal-long.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕11号" --title "长文版记与单双页实测" --cc "办公室、财务部、审计部、采购部、法务部、信息中心" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard +node "$gen" --input "$all" --output "$out_dir/docx/formal-joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关一" --joint-org "协办机关二" --doc-no "示例发〔2026〕16号" --title "联合行文实测" --page-number standard node "$gen" --input "$all" --output "$out_dir/docx/letter.docx" --format letter --org "示例机关" --doc-no "示例〔2026〕12号" --title "信函格式实测" --page-number none node "$gen" --input "$all" --output "$out_dir/docx/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔2026〕14号" --title "信函版头字段实测" --page-number none node "$gen" --input "$all" --output "$out_dir/docx/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令格式实测" --page-number standard node "$gen" --input "$all" --output "$out_dir/docx/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要格式实测" --attendees "张三(办公室)、李四(财务部)" --absent "王五(审计部)" --observers "赵六(采购部)" --page-number standard node "$gen" --input "$horizontal" --output "$out_dir/docx/table.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕13号" --title "表格版式实测" --page-number standard +node "$gen" --input "$horizontal" --output "$out_dir/docx/horizontal-table.docx" --format horizontal-table --title "横排表格实测" --page-number standard +node "$gen" --input "$calibration" --output "$out_dir/docx/ordinary-calibration.docx" --format ordinary --page-number center for file in "$out_dir/docx"/*.docx; do base=$(basename "$file" .docx) case "$base" in - ordinary) profile=ordinary ;; + ordinary|ordinary-calibration) profile=ordinary ;; letter|letter-fields) profile=letter ;; + horizontal-table) profile=horizontal-table ;; command) profile=command ;; minutes) profile=minutes ;; *) profile=formal ;; esac case "$base" in - formal-digital-*) letterhead=digital ;; + formal-digital-*|formal-joint) letterhead=digital ;; *) letterhead=preprinted ;; esac node "$verify" --input "$file" --profile "$profile" --letterhead "$letterhead" >"$out_dir/logs/$base.verify.log"