严格化国标版头与全条款核验
This commit is contained in:
@@ -135,7 +135,8 @@ Windows:
|
|||||||
- [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。
|
- [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。
|
||||||
- [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。
|
- [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。
|
||||||
- [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。
|
- [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。
|
||||||
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 12 份 DOCX、渲染 23 张 PNG,并逐条记录截图证据与人工边界。
|
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 13 份 DOCX、渲染 24 张 PNG,并逐条记录截图证据与人工边界。
|
||||||
|
- [版头坐标量测](tests/measure-header-coordinates.mjs):用 PDF 字形坐标独立核对份号首行、机关标志 35 mm 定位及可选字段不移位。
|
||||||
- [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。
|
- [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有
|
|||||||
|
|
||||||
本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。
|
本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。
|
||||||
|
|
||||||
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志使用首面固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。
|
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。
|
||||||
|
|
||||||
`references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。
|
`references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有
|
|||||||
node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format ordinary --title "文档标题"
|
node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format ordinary --title "文档标题"
|
||||||
```
|
```
|
||||||
|
|
||||||
普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头要求目标环境存在可核对的小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。
|
普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。完整电子红头的机关标志按标准推荐使用小标宋体,标题按标准使用小标宋体;生成器会在缺失时明确警告并记录实际替代字体,需要硬性阻止替代输出时加 `--require-standard-fonts`。红线下标题按28磅版心网格空二行。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm
|
# 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm
|
||||||
@@ -35,9 +35,9 @@ 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 "文档标题"
|
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` 和相应字段。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。
|
`letter`、`command`、`minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center` 与 `none` 供普通材料选择。
|
||||||
|
|
||||||
附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明仍通过 `--attachment-note` 提供,工具会统一为“附件:”并去除末尾标点。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。
|
附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明通过 `--attachment-note` 提供,工具会统一为“附件:”并去除末尾标点,同时拒绝顺序或标题与附件文件不一致的输入。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。
|
||||||
|
|
||||||
正式版式的 `--doc-no` 采用“单位发〔2026〕1号”这类标准写法;`--date`、`--print-date` 采用“2026年9月9日”,月份和日期不补零。明显不符合这两类规则的输入会被拒绝,避免把错误字段排成看似规范的文件。
|
正式版式的 `--doc-no` 采用“单位发〔2026〕1号”这类标准写法;`--date`、`--print-date` 采用“2026年9月9日”,月份和日期不补零。明显不符合这两类规则的输入会被拒绝,避免把错误字段排成看似规范的文件。
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 702 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 421 KiB |
@@ -0,0 +1 @@
|
|||||||
|
Header coordinate measurement passed: copy=39.88mm, agency=71.41mm, no-fields-agency=71.41mm, delta=0.00mm
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 263 KiB |
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
本表按 GB/T 9704-2012 编制,用于机关权威模板完成后的人机协同核验。每项记录“适用/不适用、证据位置、核验人、日期”;不适用应说明原因。它不替代发文机关公文处理制度。
|
本表按 GB/T 9704-2012 编制,用于机关权威模板完成后的人机协同核验。每项记录“适用/不适用、证据位置、核验人、日期”;不适用应说明原因。它不替代发文机关公文处理制度。
|
||||||
|
|
||||||
|
| 条款记录 | 适用/不适用及原因 | 证据位置 | 核验人 | 日期 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1—3 范围、引用文件、术语 | | | | |
|
||||||
|
| 4—6 纸张、版面、印制装订 | | | | |
|
||||||
|
| 7 版头、主体、版记、页码 | | | | |
|
||||||
|
| 8—9 横排表格、单位标点数字 | | | | |
|
||||||
|
| 10—11 特定格式、式样 | | | | |
|
||||||
|
|
||||||
## 1. 前提与制作
|
## 1. 前提与制作
|
||||||
|
|
||||||
- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command` 或 `minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
|
- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command` 或 `minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
|
||||||
@@ -11,13 +19,13 @@
|
|||||||
- [ ] 无特殊说明时采用三号仿宋;一般每面22行、每行28字并撑满版心。不得把固定28pt当作国标唯一强制值。
|
- [ ] 无特殊说明时采用三号仿宋;一般每面22行、每行28字并撑满版心。不得把固定28pt当作国标唯一强制值。
|
||||||
- [ ] 文本颜色为黑色;需红色的发文机关标志、分隔线、印章或签名章已按规则处理。
|
- [ ] 文本颜色为黑色;需红色的发文机关标志、分隔线、印章或签名章已按规则处理。
|
||||||
- [ ] 已检查目标电脑字体:发文机关标志和标题应使用可核对的小标宋体;正文应使用可用的仿宋体。生成器报告缺失时不得把替代字体当作国标字体,必要时使用 `--require-standard-fonts` 阻止生成。
|
- [ ] 已检查目标电脑字体:发文机关标志和标题应使用可核对的小标宋体;正文应使用可用的仿宋体。生成器报告缺失时不得把替代字体当作国标字体,必要时使用 `--require-standard-fonts` 阻止生成。
|
||||||
- [ ] 制作环节已确认双面印刷、左侧装订、页码套正、钉位及成品质量要求;这些项目不能从 DOCX 自动推定。
|
- [ ] 制作环节已确认双面印刷、左侧装订、页码套正、钉位及成品质量要求:版心误差≤1mm;双面套正误差≤2mm;黑墨BL100%、红墨Y80%/M80%;两页页码误差≤4mm;成品尺寸±2mm;两钉外订眼距上下边缘70mm±4mm;平订钉锯距书脊3—5mm。这些项目不能从 DOCX 自动推定。
|
||||||
|
|
||||||
## 2. 版头
|
## 2. 版头
|
||||||
|
|
||||||
- [ ] 份号适用时为6位三号阿拉伯数字,左上角第一行顶格;与密级、紧急程度一样固定在首面版头区域,不因流式段落把机关标志或预印留白向下推移。
|
- [ ] 份号适用时为6位三号阿拉伯数字,左上角第一行顶格;与密级、紧急程度一样固定在首面版头区域,不因流式段落把机关标志或预印留白向下推移。
|
||||||
- [ ] 密级和保密期限、紧急程度适用时均为三号黑体,按份号、密级和期限、紧急程度顺序自上而下排列。
|
- [ ] 密级和保密期限、紧急程度适用时均为三号黑体,按份号、密级和期限、紧急程度顺序自上而下排列。
|
||||||
- [ ] 发文机关标志使用全称或规范简称,可加“文件”;居中、红色、推荐小标宋,上边缘距版心上边缘35mm。
|
- [ ] 发文机关标志使用全称或规范简称,可加“文件”;居中、红色,推荐使用小标宋,上边缘距版心上边缘35mm;缺少字体时已记录并按严格模式处理。
|
||||||
- [ ] 发文字号位于机关标志下空二行;年份为全称阿拉伯数字并使用六角括号〔〕,序号不加“第”、不补零、后加“号”。
|
- [ ] 发文字号位于机关标志下空二行;年份为全称阿拉伯数字并使用六角括号〔〕,序号不加“第”、不补零、后加“号”。
|
||||||
- [ ] 上行文文号左空一字;签发人与文号在同一行同一区域,右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。
|
- [ ] 上行文文号左空一字;签发人与文号在同一行同一区域,右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。
|
||||||
- [ ] 红色分隔线在文号下4mm处,与版心等宽,线高按模板核定(常用约0.35—0.5mm)。
|
- [ ] 红色分隔线在文号下4mm处,与版心等宽,线高按模板核定(常用约0.35—0.5mm)。
|
||||||
@@ -29,12 +37,12 @@
|
|||||||
- [ ] 标题为二号小标宋,红线下空二行居中;多行标题词意完整、排列对称,呈梯形或菱形。
|
- [ ] 标题为二号小标宋,红线下空二行居中;多行标题词意完整、排列对称,呈梯形或菱形。
|
||||||
- [ ] 主送机关在标题下空一行顶格,回行顶格,最后名称后用全角冒号;过多而影响首页正文时移入版记。
|
- [ ] 主送机关在标题下空一行顶格,回行顶格,最后名称后用全角冒号;过多而影响首页正文时移入版记。
|
||||||
- [ ] 首页显示正文;正文一般三号仿宋,每个自然段左空二字、回行顶格。
|
- [ ] 首页显示正文;正文一般三号仿宋,每个自然段左空二字、回行顶格。
|
||||||
- [ ] 层次序数按“一、”“(一)”“1.”“(1)”使用;第一层黑体、第二层楷体、第三和第四层仿宋,均为三号。
|
- [ ] 如采用层次序数,按“一、”“(一)”“1.”“(1)”依次编排;第一层黑体、第二层楷体、第三和第四层仿宋,均为三号。
|
||||||
- [ ] 四级标题分别应用 Word/WPS 的“标题1—标题4”样式和相应大纲级别;文档总标题不进入目录。需要目录时,已在目标软件中插入并更新自动目录。
|
- [ ] 四级标题分别应用 Word/WPS 的“标题1—标题4”样式和相应大纲级别;文档总标题不进入目录。需要目录时,已在目标软件中插入并更新自动目录。
|
||||||
- [ ] 附件说明位于正文下空一行、左空二字;“附件:”后名称无标点;多附件编号正确,长名称回行与首字对齐。
|
- [ ] 附件说明位于正文下空一行、左空二字;“附件:”后名称无标点;多附件编号正确,长名称回行与首字对齐。
|
||||||
- [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。
|
- [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理:签发人签名章模式正文下空二行、职务左空二字,签名章下空一行、日期右空四字;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。空白不足时记录行距、字距调整。
|
||||||
- [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。
|
- [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。
|
||||||
- [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;与正文一起装订的附件页码连续。
|
- [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;附件序号和标题与正文附件说明逐项一致;与正文一起装订的附件页码连续。不能一起装订时,左上补排发文字号及“附件”序号。
|
||||||
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。
|
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。
|
||||||
|
|
||||||
## 4. 版记、页码与特殊格式
|
## 4. 版记、页码与特殊格式
|
||||||
@@ -44,7 +52,7 @@
|
|||||||
- [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。
|
- [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。
|
||||||
- [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。
|
- [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。
|
||||||
- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。
|
- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。
|
||||||
- [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;首页无页码且版记不画分隔线。
|
- [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;如有份号、密级、紧急程度,均置于第一条双线下并距线3号字高7/8;文号顶格居右;首页无页码且版记不画分隔线。当前生成器对这些可选信函版头字段要求使用专用模板。
|
||||||
- [ ] 命令(令)格式已核对发文机关标志上边缘距版心20mm、令号下空二行、正文下空二行。
|
- [ ] 命令(令)格式已核对发文机关全称加“命令”或“令”、标志上边缘距版心20mm、令号下空二行、正文下空二行;签发章和日期按7.3.5.3核对。
|
||||||
- [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。
|
- [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。
|
||||||
- [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。
|
- [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。
|
||||||
|
|||||||
@@ -36,12 +36,18 @@
|
|||||||
| 订口 | 28mm±1mm | 左边距1588 DXA |
|
| 订口 | 28mm±1mm | 左边距1588 DXA |
|
||||||
| 版心 | 156mm×225mm | 由页边距共同形成 |
|
| 版心 | 156mm×225mm | 由页边距共同形成 |
|
||||||
| 推导白边 | 右约26mm、下约35mm | 右1474、下1984 DXA |
|
| 推导白边 | 右约26mm、下约35mm | 右1474、下1984 DXA |
|
||||||
| 字体 | 无特殊说明一般3号仿宋 | 16pt,`w:sz=32` |
|
| 字体 | 无特殊说明一般3号仿宋 | 16pt,`w:sz=32`;目标环境缺字体时必须告警,严格模式拒绝替代输出 |
|
||||||
| 行数和字数 | 一般每面22行、每行28字,特定情况可调整 | 标准要求是版面结果;28pt固定行距是常用实现值,不是国标唯一强制参数 |
|
| 行数和字数 | 一般每面22行、每行28字,特定情况可调整 | 标准要求是版面结果;28pt固定行距是常用实现值,不是国标唯一强制参数 |
|
||||||
| 文字颜色 | 无特殊说明为黑色 | `000000` |
|
| 文字颜色 | 无特殊说明为黑色 | `000000` |
|
||||||
| 印刷 | 正式公文双面印刷,页码套正 | 属打印环节,DOCX无法自动保证 |
|
| 印刷 | 正式公文双面印刷,页码套正 | 属打印环节,DOCX无法自动保证 |
|
||||||
| 装订 | 左侧装订,正式公文执行钉位等要求 | 属制作环节,需人工检查 |
|
| 装订 | 左侧装订,正式公文执行钉位等要求 | 属制作环节,需人工检查 |
|
||||||
|
|
||||||
|
### 印制装订验收值
|
||||||
|
|
||||||
|
- 制版:版心误差不超过1mm,版面干净、字迹清楚无断划。
|
||||||
|
- 印刷:双面印刷,页码套正误差不超过2mm;黑色油墨达到BL100%,红色油墨达到Y80%、M80%。
|
||||||
|
- 装订:两页页码误差不超过4mm,成品尺寸允许误差±2mm;两钉外订眼距版面上下边缘各70mm,允许误差±4mm;平订钉锯与书脊距离3—5mm,并检查不掉页、无坏钉漏钉重钉、钉脚平伏牢固。
|
||||||
|
|
||||||
## 四、正文主体格式
|
## 四、正文主体格式
|
||||||
|
|
||||||
### 1. 标题
|
### 1. 标题
|
||||||
@@ -60,7 +66,7 @@
|
|||||||
|
|
||||||
- 一般用3号仿宋。
|
- 一般用3号仿宋。
|
||||||
- 每个自然段左空二字,回行顶格。
|
- 每个自然段左空二字,回行顶格。
|
||||||
- 层次序数依次使用“一、”“(一)”“1.”“(1)”。
|
- 如采用层次序数,可依次使用“一、”“(一)”“1.”“(1)”。
|
||||||
|
|
||||||
| 层级 | 序数示例 | 字体 | 字号 | 段落 |
|
| 层级 | 序数示例 | 字体 | 字号 | 段落 |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
@@ -79,9 +85,11 @@
|
|||||||
|
|
||||||
### 5. 发文机关署名、日期和印章
|
### 5. 发文机关署名、日期和印章
|
||||||
|
|
||||||
- 加盖印章:成文日期一般右空四字;印章应为红色并下压机关署名和日期。
|
- 加盖印章:成文日期一般右空四字;印章应为红色并下压机关署名和日期;单一机关、联合机关的印章位置和排列按7.3.5.1逐项核对。
|
||||||
- 不加印章:正文或附件说明下空一行,机关署名右空二字,下一行编排日期。
|
- 不加印章:正文或附件说明下空一行,机关署名右空二字,下一行编排日期。
|
||||||
|
- 签发人签名章:正文或附件说明下空二行,职务左空二字;签名章下空一行,日期右空四字;签名章一般用红色。生成器只排文字占位,不伪造签名章图像。
|
||||||
- 成文日期使用阿拉伯数字完整标注年月日,月、日不补零。
|
- 成文日期使用阿拉伯数字完整标注年月日,月、日不补零。
|
||||||
|
- 空白不足时,可在目标 Word/WPS 中按7.3.5.5调整行距、字距。
|
||||||
- 印章和签名章是独立图片或实体印章对象;本生成器不内置其图像,可在生成后的 Word/WPS 文件中按需插入。
|
- 印章和签名章是独立图片或实体印章对象;本生成器不内置其图像,可在生成后的 Word/WPS 文件中按需插入。
|
||||||
|
|
||||||
### 6. 附注与附件正文
|
### 6. 附注与附件正文
|
||||||
@@ -99,7 +107,7 @@
|
|||||||
| 份号 | 6位3号阿拉伯数字,版心左上角第一行顶格 |
|
| 份号 | 6位3号阿拉伯数字,版心左上角第一行顶格 |
|
||||||
| 密级和期限 | 3号黑体,按规则置于左上区域 |
|
| 密级和期限 | 3号黑体,按规则置于左上区域 |
|
||||||
| 紧急程度 | 3号黑体,与份号、密级按顺序排列 |
|
| 紧急程度 | 3号黑体,与份号、密级按顺序排列 |
|
||||||
| 发文机关标志 | 机关全称或规范简称,可加“文件”;红色小标宋,居中 |
|
| 发文机关标志 | 机关全称或规范简称,可加“文件”;居中、红色,推荐使用小标宋体 |
|
||||||
| 发文字号 | 年份全称、六角括号、序号不加“第”且不补零 |
|
| 发文字号 | 年份全称、六角括号、序号不加“第”且不补零 |
|
||||||
| 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;必须与文号同一行同一区域编排 |
|
| 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;必须与文号同一行同一区域编排 |
|
||||||
| 分隔线 | 文号下4mm,红色,与版心等宽;常用线高约0.35—0.5mm |
|
| 分隔线 | 文号下4mm,红色,与版心等宽;常用线高约0.35—0.5mm |
|
||||||
@@ -135,7 +143,7 @@
|
|||||||
- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。
|
- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。
|
||||||
- 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。
|
- 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。
|
||||||
- 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。
|
- 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。
|
||||||
- 命令(令)发文机关标志上边缘距版心20mm,标志下空二行编排令号,令号下空二行编排正文。
|
- 命令(令)发文机关标志由机关全称加“命令”或“令”组成,上边缘距版心20mm;标志下空二行编排令号,令号下空二行编排正文;签发人职务、签名章和日期按7.3.5.3处理。
|
||||||
- 纪要标志上边缘距版心35mm;“出席”“请假”“列席”标签用黑体,人员名单用仿宋。
|
- 纪要标志上边缘距版心35mm;“出席”“请假”“列席”标签用黑体,人员名单用仿宋。
|
||||||
- 特定格式必须使用对应模板或专用生成逻辑,不得以普通报告模板代替。
|
- 特定格式必须使用对应模板或专用生成逻辑,不得以普通报告模板代替。
|
||||||
|
|
||||||
@@ -159,6 +167,7 @@
|
|||||||
## 十、建议检查
|
## 十、建议检查
|
||||||
|
|
||||||
1. 核对文种、行文关系、主送、文号、签发人、印章和版记等需要使用的字段。
|
1. 核对文种、行文关系、主送、文号、签发人、印章和版记等需要使用的字段。
|
||||||
|
2. 记录适用性、证据位置、核验人和日期;不适用项目说明原因。
|
||||||
3. 检查A4、版心、正文字体字号、22行28字的版面结果及四级标题映射;固定28pt仅作实现参数复核。
|
3. 检查A4、版心、正文字体字号、22行28字的版面结果及四级标题映射;固定28pt仅作实现参数复核。
|
||||||
4. 检查每个自然段及各级标题左空二字、回行顶格。
|
4. 检查每个自然段及各级标题左空二字、回行顶格。
|
||||||
5. 检查页码模式:打印稿居中;正式公文单页右、双页左。
|
5. 检查页码模式:打印稿居中;正式公文单页右、双页左。
|
||||||
|
|||||||
@@ -6,39 +6,39 @@
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | 适用范围 | 说明文档明确 `ordinary`、`formal` 和特定格式的使用场景,不因机构名称自动画红头 | `SKILL.md`、`README.md`;仍由使用者确认文种和机关模板 |
|
| 1 | 适用范围 | 说明文档明确 `ordinary`、`formal` 和特定格式的使用场景,不因机构名称自动画红头 | `SKILL.md`、`README.md`;仍由使用者确认文种和机关模板 |
|
||||||
| 2 | 规范性引用文件 | 在摘要和清单中列出 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835 | `references/gbt9704-2012-summary.md`;计量单位、标点和数字的语义仍需人工复核 |
|
| 2 | 规范性引用文件 | 在摘要和清单中列出 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835 | `references/gbt9704-2012-summary.md`;计量单位、标点和数字的语义仍需人工复核 |
|
||||||
| 3 | 字、行等术语 | 以 3 号字和 28 磅版心网格实现常用“行” | `w:docGrid`、正文段落;特殊机关模板可调整 |
|
| 3 | 字、行等术语 | 记录“一字”为一个汉字宽度、“一行”为一个汉字高度加3号字高度7/8;生成器采用28磅网格作为实现参数 | `w:docGrid`、摘要和清单;术语定义不能由截图单独证明 |
|
||||||
| 4 | 用纸技术指标 | 不在 DOCX 中伪造纸张克重、白度、耐折度、不透明度、pH | `formal-checklist.md`;采购纸张和成品检测人工完成 |
|
| 4 | 用纸技术指标 | 记录定量60—80 g/m²、白度80%—90%、横向耐折度≥15次、不透明度≥85%、pH 7.5—9.5;不在 DOCX 中伪造检测结果 | `formal-checklist.md`;采购纸张和成品检测人工完成 |
|
||||||
| 5.1 | A4 成品幅面 | 固定 210mm×297mm | `verify_gongwen_docx.mjs` 的 A4 检查;仍需检查实际裁切误差 |
|
| 5.1 | A4 成品幅面 | 固定 210mm×297mm | `verify_gongwen_docx.mjs` 的 A4 检查;仍需检查实际裁切误差 |
|
||||||
| 5.2.1 | 天头、订口、版心 | 37mm、28mm、156mm×225mm写入 DOCX | 版心和页边检查;打印缩放必须关闭并现场复核 |
|
| 5.2.1 | 天头、订口、版心 | 37mm、28mm、156mm×225mm写入 DOCX | 版心和页边检查;打印缩放必须关闭并现场复核 |
|
||||||
| 5.2.2 | 默认 3 号仿宋 | 运行环境检测仿宋;缺失时明确警告并写入替代字体,严格模式直接失败 | `--require-standard-fonts`;当前机器若缺小标宋/仿宋不能把替代字体当标准字体 |
|
| 5.2.2 | 默认 3 号仿宋 | 运行环境检测仿宋;缺失时明确警告并写入替代字体,严格模式直接失败 | `--require-standard-fonts`;当前机器若缺小标宋/仿宋不能把替代字体当标准字体 |
|
||||||
| 5.2.3 | 一般 22 行、28 字并撑满版心 | 3 号、28 磅固定网格和版心参数已写入 | `w:docGrid` 与 PDF 实测;长标题、表格和特殊内容仍需看最终分页 |
|
| 5.2.3 | 一般 22 行、28 字并撑满版心 | 3 号、28 磅固定网格和版心参数已写入 | `w:docGrid` 与 PDF 实测;长标题、表格和特殊内容仍需看最终分页 |
|
||||||
| 5.2.4 | 默认黑色 | 正文、标题层级、字段默认为黑色,红色只用于规定的版头/特定格式/版记线 | 生成器和 PDF 视觉检查 |
|
| 5.2.4 | 默认黑色 | 正文、标题层级、字段默认为黑色,红色只用于规定的版头/特定格式/版记线 | 生成器和 PDF 视觉检查 |
|
||||||
| 6.1 | 制版清洁、字迹、尺寸、版心误差 | 不在 XML 中虚报制版质量 | PDF/打印成品人工检查 |
|
| 6.1 | 制版清洁、字迹、尺寸、版心误差 | 记录版心误差≤1mm及版面干净、字迹清楚无断划要求;不在 XML 中虚报制版质量 | PDF/打印成品人工检查 |
|
||||||
| 6.2 | 双面印刷、套正、油墨和印品质量 | 正式模式生成单页右、双页左页码;不控制打印机和油墨 | `--page-number standard`;双面打印和套正人工检查 |
|
| 6.2 | 双面印刷、套正、油墨和印品质量 | 记录双面套正误差≤2mm、黑墨BL100%、红墨Y80%/M80%;正式模式生成单页右、双页左页码但不控制打印机和油墨 | `--page-number standard`;双面打印和套正人工检查 |
|
||||||
| 6.3 | 左侧装订、钉位、裁切和成品 | 不生成虚假的装订结果 | `formal-checklist.md`;成品人工检查 |
|
| 6.3 | 左侧装订、钉位、裁切和成品 | 记录两页页码误差≤4mm、成品尺寸±2mm、两钉外订眼距上下边缘70mm±4mm、平订钉锯距书脊3—5mm;不生成虚假的装订结果 | `formal-checklist.md`;成品人工检查 |
|
||||||
| 7.1 | 版头、主体、版记三区和页码在版心外 | 版式分支、页脚和版记结构分别生成 | DOCX/PDF 逐页检查 |
|
| 7.1 | 版头、主体、版记三区和页码在版心外 | 版式分支、页脚和版记结构分别生成 | DOCX/PDF 逐页检查 |
|
||||||
| 7.2.1 | 份号六位、3 号、左上第一行 | `--copy-no` 只接受 1—6 位阿拉伯数字并补足六位 | 结构检查;实际是否需要份号由发文机关确认 |
|
| 7.2.1 | 份号六位、3 号、左上第一行 | `--copy-no` 只接受 1—6 位阿拉伯数字并补足六位 | 结构检查;实际是否需要份号由发文机关确认 |
|
||||||
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | `--secret` 按黑体 3 号写入 | 文字内容和期限写法人工复核 |
|
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | `--secret` 按黑体3号写入;没有份号时自动保留第一行空位,使密级仍在第二行 | 结构检查和视觉审计;文字内容和期限写法人工复核 |
|
||||||
| 7.2.3 | 紧急程度及排列顺序 | `--urgent` 按黑体 3 号写入,并按份号、密级、紧急程度顺序下移 | 适用性和紧急级别人工确认 |
|
| 7.2.3 | 紧急程度及排列顺序 | `--urgent` 按黑体 3 号写入,并按份号、密级、紧急程度顺序下移 | 适用性和紧急级别人工确认 |
|
||||||
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段用固定定位,不推动机关标志或预留区;联合行文仍需模板 | `verify_gongwen_docx.mjs`、`references/gbt9704-visual-audit.md` 的 `formal-digital-all-1.png`/`formal-preprinted-1.png`;目标电脑字体和联合行文人工核对 |
|
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文仍需模板 | `verify_gongwen_docx.mjs`、`tests/measure-header-coordinates.mjs` 和视觉核验记录;当前机缺少推荐字体时严格模式失败 |
|
||||||
| 7.2.5 | 文号下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter` 对 `--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 |
|
| 7.2.5 | 文号下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter` 对 `--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 |
|
||||||
| 7.2.6 | 上行文签发人同文号一行;标签仿宋、姓名楷体 | `--upward true --signer` 同行生成单一签发人;多签发人、联合行文转人工模板 | 结构校验和 PDF;多个签发人需按机关顺序人工排布 |
|
| 7.2.6 | 上行文文号左空一字、签发人右空一字并同一行;标签仿宋、姓名楷体 | `--upward true --signer` 同行生成单一或两个签发人,左右各空一字;多签发人、联合行文转人工模板 | 结构校验和 PDF;复杂联合行文需按机关顺序人工排布 |
|
||||||
| 7.2.7 | 文号下 4mm、版心等宽红线 | `redRule()` 生成 156mm 红线,文号段后 4mm;预印模式不重绘 | PDF 量测;红色油墨和实际纸样人工核对 |
|
| 7.2.7 | 文号下 4mm、版心等宽红线 | `redRule()` 生成 156mm 红线,文号段后 4mm;预印模式不重绘 | PDF 量测;红色油墨和实际纸样人工核对 |
|
||||||
| 7.3.1 | 标题 2 号小标宋,红线下空二行、居中 | 标题使用 `GongwenTitle`,红线/预印线后插入两个显式 28 磅空段落 | XML + PDF 视觉门;小标宋缺失时严格模式失败 |
|
| 7.3.1 | 标题 2 号小标宋,红线下空二行、居中 | 标题使用 `GongwenTitle`,红线/预印线后插入两个显式 28 磅空段落 | XML + PDF 视觉门;小标宋缺失时严格模式失败 |
|
||||||
| 7.3.2 | 主送机关标题下空一行、顶格、全角冒号 | `--to` 或首段识别后写入一行并规范化末尾全角冒号 | 过多主送机关移版记仍需人工决定 |
|
| 7.3.2 | 主送机关标题下空一行、顶格、全角冒号 | `--to` 或首段识别后写入一行并规范化末尾全角冒号 | 过多主送机关移版记仍需人工决定 |
|
||||||
| 7.3.3 | 首页有正文;正文 3 号仿宋、自然段左空二字;四级序数和字体 | Markdown 序数映射 `Heading1`—`Heading4`、大纲级别和字体已写入 | Word/WPS 目录可引用;特殊层次和分页人工复核 |
|
| 7.3.3 | 首页有正文;正文 3 号仿宋、自然段左空二字;四级序数和字体 | Markdown 序数映射 `Heading1`—`Heading4`、大纲级别和字体已写入 | Word/WPS 目录可引用;特殊层次和分页人工复核 |
|
||||||
| 7.3.4 | 附件说明正文下空一行、左空二字、名称末无标点 | `--attachment-note` 统一为“附件:”并去除末尾标点,表格后不重复空行 | 长名称回行和多附件编号仍需 PDF/模板复核 |
|
| 7.3.4 | 附件说明正文下空一行、左空二字、名称末无标点 | `--attachment-note` 统一为“附件:”并去除末尾标点,表格后不重复空行 | 长名称回行和多附件编号仍需 PDF/模板复核 |
|
||||||
| 7.3.5 | 署名、成文日期、印章/签名章 | 不加章署名日期按左右空字规则生成;日期校验为全年月日且月日不补零;不伪造印章 | `--seal-mode` 仍需把实体章或已取得的图片放入 Word/WPS;联合行文人工完成 |
|
| 7.3.5 | 署名、成文日期、印章/签名章 | 分别记录加盖印章、不加盖印章、签发人签名章、日期数字和特殊空白处理;生成器排文字占位并补足签名章模式的两行/一行间距,不伪造印章 | `--seal-mode` 仍需把实体章或已取得的图片放入 Word/WPS;单/联合机关印章、红色签名章和空白不足时调整人工完成 |
|
||||||
| 7.3.6 | 附注在日期下一行左空二字、圆括号 | `--note` 规范化为全角圆括号 | 日期与附注的最终分页人工核对 |
|
| 7.3.6 | 附注在日期下一行左空二字、圆括号 | `--note` 规范化为全角圆括号 | 日期与附注的最终分页人工核对 |
|
||||||
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续 | `--attachment-file` 用分页符、显式空行和标题样式生成,附件共用连续页脚 | 不能一起装订时的文号+附件标识、复杂附件版式人工处理 |
|
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | 不能一起装订时的文号+附件标识、复杂附件版式人工处理 |
|
||||||
| 7.4.1 | 版记首末粗线、中间细线,末线贴最后一页版心下边缘 | 版记改为浮动表,锚定最后一页版心底部;粗线 0.35mm、细线 0.25mm | 已用长文 PDF 截图实测;Word/WPS 和极端分页仍需抽检 |
|
| 7.4.1 | 版记首末粗线、中间细线,末线贴最后一页版心下边缘 | 版记改为浮动表,锚定最后一页版心底部;粗线 0.35mm、细线 0.25mm | 已用长文 PDF 截图实测;Word/WPS 和极端分页仍需抽检 |
|
||||||
| 7.4.2 | 抄送 4 号仿宋、左右一字、回行对齐、末尾句号 | `--cc` 生成 4 号、左右一字、全角冒号和句号 | 多行抄送回行和主送移版记人工复核 |
|
| 7.4.2 | 抄送 4 号仿宋、左右一字、回行对齐、末尾句号 | `--cc` 生成 4 号、左右一字、全角冒号和句号 | 多行抄送回行和主送移版记人工复核 |
|
||||||
| 7.4.3 | 印发机关/日期 4 号仿宋,左右一字,日期后“印发”,细线隔开 | `--print-org`、`--print-date` 生成右制表位、日期校验和末条粗线 | 实际机关名称长度和目标软件分页人工复核 |
|
| 7.4.3 | 印发机关/日期 4 号仿宋,左右一字,日期后“印发”,细线隔开 | `--print-org`、`--print-date` 生成右制表位、日期校验和末条粗线 | 实际机关名称长度和目标软件分页人工复核 |
|
||||||
| 7.5 | 4 号半角宋体页码、两侧一字线、距版心下边缘 7mm、单双页位置、附件连续 | 正式页脚用半角宋体、单双页字段,footer 距离调整为 7mm 位置;信函关闭页码 | XML + PDF bbox;空白页/特定版记页的例外仍需人工复核 |
|
| 7.5 | 4 号半角宋体页码、两侧一字线、距版心下边缘 7mm、单双页位置、附件连续 | 正式页脚用半角宋体、单双页字段,footer 距离调整为 7mm 位置;信函关闭页码 | XML + PDF bbox;空白页/特定版记页的例外仍需人工复核 |
|
||||||
| 8 | 横排表格页码与表头方向 | 普通 Markdown 表格按纵向版心生成;视觉审计保留 `table-1.png` 作为未自动化证据,不把纵向表格冒充横排 | 横排表格须在 Word/WPS 采用专用横向模板并人工检查 |
|
| 8 | 横排表格页码与表头方向 | 普通 Markdown 表格按纵向版心生成;视觉审计保留 `table-1.png` 作为未自动化证据,不把纵向表格冒充横排 | 横排表格须在 Word/WPS 采用专用横向模板并人工检查 |
|
||||||
| 9 | 计量单位、标点、数字按引用标准 | 结构字段(文号、日期、附件冒号、句号)做边界校验 | 正文语义、单位、标点和数字用法需人工或专门文本审校 |
|
| 9 | 计量单位、标点、数字按引用标准 | 结构字段(文号、日期、附件冒号、句号)做边界校验 | 正文语义、单位、标点和数字用法需人工或专门文本审校 |
|
||||||
| 10.1 | 信函上下双红线、30mm/20mm、170mm、标题二行、无首页页码、无版记线 | `--format letter` 生成双线、专用边距、文号距离、标题空行、底部双线页脚和无页码 | PDF 实测;联合行文及模板字段人工复核 |
|
| 10.1 | 信函上下双红线、30mm/20mm、170mm、可选版头字段、标题二行、无首页页码、无版记线 | `--format letter` 生成双线、专用边距、文号距离、标题空行、底部双线页脚和无页码,并支持份号、密级、紧急程度在第一条双线下按顺序编排 | PDF 实测;联合行文及模板字段人工复核 |
|
||||||
| 10.2 | 命令(令)标志上边缘 20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org`、`--doc-no`,两个间隔均为显式 28 磅空行 | 令号文字和签名章/日期按专用模板人工复核 |
|
| 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.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,并按两字起排、悬挂缩进处理回行 | `references/gbt9704-visual-audit.md` 的 `minutes-1.png`/`minutes-2.png`;纪要具体版式可按机关制度调整 |
|
||||||
| 11 | 标准式样图 | 各分支按条款参数实现,不把截图当作唯一依据 | 最终仍需与机关现行式样、Word/WPS 和打印样张逐页比对 |
|
| 11 | 标准式样图 | 各分支按条款参数实现,不把截图当作唯一依据 | 最终仍需与机关现行式样、Word/WPS 和打印样张逐页比对 |
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本实际生成 12 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 23 张 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。当前批次的文件清单保存在运行目录的 `manifest.tsv`,每份文件的校验日志保存在 `logs/`。
|
脚本实际生成 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/`。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
|||||||
| 附件 | `formal-attachments-1.png`、`formal-attachments-3.png`、`formal-attachments-4.png` | 附件另页,第一行是“附件1/附件2”,第三行是标题,附件页码连续。 |
|
| 附件 | `formal-attachments-1.png`、`formal-attachments-3.png`、`formal-attachments-4.png` | 附件另页,第一行是“附件1/附件2”,第三行是标题,附件页码连续。 |
|
||||||
| 长文版记 | `formal-long-1.png`、`formal-long-2.png`、`formal-long-3.png` | 单页右、双页左页码连续;末页版记贴近版心底部,粗细线顺序可见。 |
|
| 长文版记 | `formal-long-1.png`、`formal-long-2.png`、`formal-long-3.png` | 单页右、双页左页码连续;末页版记贴近版心底部,粗细线顺序可见。 |
|
||||||
| 盖章/签名章位置 | `formal-seal-2.png`、`formal-signed-2.png` | 文字落款和日期位置已生成;实体印章或签名章图片没有被伪造,需在 Word/WPS 或打印环节完成。 |
|
| 盖章/签名章位置 | `formal-seal-2.png`、`formal-signed-2.png` | 文字落款和日期位置已生成;实体印章或签名章图片没有被伪造,需在 Word/WPS 或打印环节完成。 |
|
||||||
| 信函 | `letter-1.png` | 上粗下细双红线、文号、标题和页末上细下粗双红线可见;不显示页码。 |
|
| 信函 | `letter-1.png`、`letter-fields-1.png` | 上粗下细双红线、文号、标题和页末上细下粗双红线可见;带份号、密级、紧急程度的回归页也按第一条双线下顺序出现;不显示页码。 |
|
||||||
| 命令(令) | `command-1.png` | 机关标志、令号下空二行、正文前再空二行可见。 |
|
| 命令(令) | `command-1.png` | 机关标志、令号下空二行、正文前再空二行可见。 |
|
||||||
| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐。 |
|
| 纪要 | `minutes-1.png`、`minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐。 |
|
||||||
| 横排表格 | `table-1.png` | 当前示例仍是纵向表格。这一张图是故意保留的缺口证据,不能把普通纵向表格宣称为标准横排表格。 |
|
| 横排表格 | `table-1.png` | 当前示例仍是纵向表格。这一张图是故意保留的缺口证据,不能把普通纵向表格宣称为标准横排表格。 |
|
||||||
@@ -46,39 +46,39 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | 适用范围 | 结构通过,仍需人工 | `ordinary`、`formal`、`letter`、`command`、`minutes` 分开;文种和机关现行模板仍由使用者确认。 |
|
| 1 | 适用范围 | 结构通过,仍需人工 | `ordinary`、`formal`、`letter`、`command`、`minutes` 分开;文种和机关现行模板仍由使用者确认。 |
|
||||||
| 2 | 规范性引用文件 | 结构通过,仍需人工 | 摘要和清单列出 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835;正文单位、标点、数字仍需文本审校。 |
|
| 2 | 规范性引用文件 | 结构通过,仍需人工 | 摘要和清单列出 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835;正文单位、标点、数字仍需文本审校。 |
|
||||||
| 3 | 字、行术语 | 截图通过 | 3 号字、28 磅实现网格可在 `formal-digital-all-1.png`、`ordinary-1.png` 观察;特殊模板可以调整。 |
|
| 3 | 字、行术语 | 结构通过,仍需人工 | “一字”为一个汉字宽度,“一行”为一个汉字高度加3号字高度的7/8;生成器采用28磅网格,不能用截图替代术语定义核对。 |
|
||||||
| 4 | 纸张技术指标 | 当前未自动化 | 克重 60—80 g/m²、白度、耐折度、不透明度、pH 属纸张采购和成品检测,DOCX/PDF 无法证明。 |
|
| 4 | 纸张技术指标 | 当前未自动化 | 纸张定量60—80 g/m²、白度80%—90%、横向耐折度≥15次、不透明度≥85%、pH 7.5—9.5;这些必须以纸张和成品检测证明。 |
|
||||||
| 5.1 | A4 210 mm×297 mm | 截图通过 | 所有场景由 PDF 页面尺寸核对为 A4;仍需现场检查裁切误差。 |
|
| 5.1 | A4 210 mm×297 mm | 截图通过 | 所有场景由 PDF 页面尺寸核对为 A4;仍需现场检查裁切误差。 |
|
||||||
| 5.2.1 | 天头 37 mm、订口 28 mm、版心 156 mm×225 mm | 结构通过,仍需人工 | 生成器写入固定页边距;打印缩放必须关闭并实测。 |
|
| 5.2.1 | 天头 37 mm、订口 28 mm、版心 156 mm×225 mm | 结构通过,仍需人工 | 生成器写入固定页边距;打印缩放必须关闭并实测。 |
|
||||||
| 5.2.2 | 默认 3 号仿宋 | 结构通过,仍需人工 | 本机缺少标准仿宋,生成器已明确警告并使用 STFangsong 替代;`--require-standard-fonts` 会阻止输出。 |
|
| 5.2.2 | 默认 3 号仿宋 | 结构通过,仍需人工 | 本机缺少标准仿宋,生成器已明确警告并使用 STFangsong 替代;`--require-standard-fonts` 会阻止输出。 |
|
||||||
| 5.2.3 | 一般 22 行、28 字 | 结构通过,仍需人工 | 28 磅网格和版心已写入;长标题、表格、字体替代造成的换行必须看最终 PDF/WPS。固定 28 磅不是把所有文稿都强行塞成 22 行的声明。 |
|
| 5.2.3 | 一般 22 行、28 字 | 结构通过,仍需人工 | 28 磅网格和版心已写入;长标题、表格、字体替代造成的换行必须看最终 PDF/WPS。固定 28 磅不是把所有文稿都强行塞成 22 行的声明。 |
|
||||||
| 5.2.4 | 默认黑色 | 截图通过 | `ordinary-1.png` 和正文区域为黑色;红色仅出现在电子红头、信函和版记线。 |
|
| 5.2.4 | 默认黑色 | 截图通过 | `ordinary-1.png` 和正文区域为黑色;红色仅出现在电子红头、信函和版记线。 |
|
||||||
| 6.1 | 制版清洁、字迹、尺寸和版心误差 | 当前未自动化 | 需检查实际印品,不从 PDF 颜色或 XML 推断。 |
|
| 6.1 | 制版清洁、字迹、尺寸和版心误差 | 当前未自动化 | 版心误差≤1mm,且版面干净、字迹清楚无断划;需检查实际印品。 |
|
||||||
| 6.2 | 双面印刷、套正、油墨和印品质量 | 当前未自动化 | `formal-long-2.png` 只证明双面页码方向的 PDF 模拟效果,不能替代双面打印和套正检查。 |
|
| 6.2 | 双面印刷、套正、油墨和印品质量 | 当前未自动化 | 双面套正误差≤2mm;黑墨BL100%、红墨Y80%/M80%;`formal-long-2.png`不能替代现场检查。 |
|
||||||
| 6.3 | 左侧装订、钉位、裁切、成品 | 当前未自动化 | 按 `references/formal-checklist.md` 在成品上检查。 |
|
| 6.3 | 左侧装订、钉位、裁切、成品 | 当前未自动化 | 两页页码误差≤4mm、成品尺寸±2mm、两钉外订眼距上下边缘70mm±4mm、平订钉锯距书脊3—5mm;按清单在成品上检查。 |
|
||||||
| 7.1 | 版头、主体、版记三区和页码 | 截图通过 | `formal-digital-all-1.png`、`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`;首面字段采用固定定位。 |
|
| 7.2.1 | 份号六位、左上第一行 | 截图通过 | `formal-digital-all-1.png` 和 `formal-preprinted-1.png` 显示 `000007`;首面字段采用固定定位。 |
|
||||||
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | 截图通过 | `formal-digital-all-1.png` 显示左上第二行;保密期限具体写法仍需机关确认。 |
|
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | 截图通过 | `formal-digital-all-1.png` 显示左上第二行;保密期限具体写法仍需机关确认。 |
|
||||||
| 7.2.3 | 紧急程度及顺序 | 截图通过 | `formal-digital-all-1.png` 显示份号、密级、紧急程度顺序;是否适用由发文机关决定。 |
|
| 7.2.3 | 紧急程度及顺序 | 截图通过 | `formal-digital-all-1.png` 显示份号、密级、紧急程度顺序;是否适用由发文机关决定。 |
|
||||||
| 7.2.4 | 机关标志居中、红色、距版心上边缘 35 mm | 截图通过 | `formal-digital-all-1.png` 以渲染坐标检查;左上字段不再推动机关标志。完整电子红头缺少小标宋时严格模式失败。联合行文标志仍需专用模板。 |
|
| 7.2.4 | 机关标志居中、红色、距版心上边缘 35 mm | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png` 加 `references/assets/visual-audit/header-coordinates.log` 的 PDF 字形量测;左上字段不再推动机关标志。机关标志字体按标准为推荐小标宋,本机缺少该字体,严格模式失败。联合行文标志仍需专用模板。 |
|
||||||
| 7.2.5 | 文号下空二行、年份、六角括号、序号规则 | 截图通过 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;输入验证拒绝“第”和前导零。 |
|
| 7.2.5 | 文号下空二行、年份、六角括号、序号规则 | 截图通过 | `formal-digital-all-1.png`、`formal-digital-upward-1.png`;输入验证拒绝“第”和前导零。 |
|
||||||
| 7.2.6 | 上行文签发人及多签发人 | 结构通过,仍需人工 | `formal-digital-upward-1.png` 已见同一行和两个签发人;超过两个签发人或联合行文转机关模板。 |
|
| 7.2.6 | 上行文签发人及多签发人 | 结构通过,仍需人工 | `formal-digital-upward-1.png` 已见同一行和两个签发人;超过两个签发人或联合行文转机关模板。 |
|
||||||
| 7.2.7 | 文号下 4 mm、版心等宽红线 | 截图通过 | `formal-digital-all-1.png` 红线与版心同宽;预印模式不重绘,改由纸样提供。 |
|
| 7.2.7 | 文号下 4 mm、版心等宽红线 | 截图通过 | `formal-digital-all-1.png` 红线与版心同宽;预印模式不重绘,改由纸样提供。 |
|
||||||
| 7.3.1 | 标题二号小标宋、红线下空二行、居中 | 截图通过 | `formal-digital-all-1.png`、`formal-preprinted-1.png`;本机缺小标宋时使用替代并报警,不能把替代字体称为标准字体。 |
|
| 7.3.1 | 标题二号小标宋、红线下空二行、居中 | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png`、`formal-preprinted-1.png`;本机缺小标宋时使用替代并报警,不能把替代字体称为标准字体。 |
|
||||||
| 7.3.2 | 主送机关标题下空一行、顶格、全角冒号 | 截图通过 | `formal-digital-all-1.png` 只出现一次“各主送机关:”,重复传入带冒号或不带冒号均归一化。 |
|
| 7.3.2 | 主送机关标题下空一行、顶格、全角冒号 | 截图通过 | `formal-digital-all-1.png` 只出现一次“各主送机关:”,重复传入带冒号或不带冒号均归一化。 |
|
||||||
| 7.3.3 | 首页正文、三号仿宋、两字缩进、层次字体 | 截图通过 | `formal-digital-all-1.png` 和 `ordinary-1.png`;四级标题均有 Heading1—Heading4 和大纲级别。 |
|
| 7.3.3 | 首页正文、三号仿宋、两字缩进、层次字体 | 版式结构通过,标准字体需复核 | `formal-digital-all-1.png` 和 `ordinary-1.png`;四级标题均有 Heading1—Heading4 和大纲级别;本机缺标准仿宋时严格模式失败。 |
|
||||||
| 7.3.4 | 附件说明 | 结构通过,仍需人工 | `--attachment-note` 统一冒号和末标点;长名称回行、复杂多附件需目标软件核对。 |
|
| 7.3.4 | 附件说明 | 结构通过,仍需人工 | `--attachment-note` 统一冒号和末标点;长名称回行、复杂多附件需目标软件核对。 |
|
||||||
| 7.3.5 | 署名、日期、印章/签名章 | 结构通过,仍需人工 | `formal-seal-2.png`、`formal-signed-2.png`;日期校验已覆盖不补零,印章和签名章必须使用真实图片或实体章。联合行文人工处理。 |
|
| 7.3.5 | 署名、日期、印章/签名章 | 结构通过,仍需人工 | 覆盖加盖印章、不加盖印章和签发人签名章的文字位置;单/联合机关印章、红色签名章图片、空白不足时的行距字距调整仍须按7.3.5.1—7.3.5.5在目标软件或印制环节完成。 |
|
||||||
| 7.3.6 | 附注 | 结构通过,仍需人工 | `--note` 生成全角圆括号并接在日期后;最终分页仍需抽检。 |
|
| 7.3.6 | 附注 | 结构通过,仍需人工 | `--note` 生成全角圆括号并接在日期后;最终分页仍需抽检。 |
|
||||||
| 7.3.7 | 附件另面、标签、标题、版记前、页码连续 | 截图通过 | `formal-attachments-3.png`、`formal-attachments-4.png`;不能一起装订的附件需要补排发文字号和附件标识。 |
|
| 7.3.7 | 附件另面、标签、标题、版记前、页码连续 | 结构通过,仍需人工 | `formal-attachments-3.png`、`formal-attachments-4.png`;生成器会校验附件说明与附件标题、顺序一致;不能一起装订的附件需要补排发文字号和附件标识。 |
|
||||||
| 7.4.1 | 版记粗细线和末线位置 | 截图通过 | `formal-long-3.png` 显示粗—细—粗结构和末页底部锚定;极端分页仍需抽检。 |
|
| 7.4.1 | 版记粗细线和末线位置 | 截图通过 | `formal-long-3.png` 显示粗—细—粗结构和末页底部锚定;极端分页仍需抽检。 |
|
||||||
| 7.4.2 | 抄送机关字号、缩进、句号 | 截图通过 | `formal-long-3.png`;多行和主送移版记仍需结合机关模板。 |
|
| 7.4.2 | 抄送机关字号、缩进、句号 | 截图通过 | `formal-long-3.png`;多行和主送移版记仍需结合机关模板。 |
|
||||||
| 7.4.3 | 印发机关、日期、印发和细线 | 截图通过 | `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`;空白页及特殊版记例外需人工复核。 |
|
| 7.5 | 页码字号、单双页位置、7 mm、附件连续 | 截图通过 | `formal-long-2.png`、`formal-long-3.png`、`formal-attachments-4.png`;空白页及特殊版记例外需人工复核。 |
|
||||||
| 8 | 横排表格 | 当前未自动化 | `table-1.png` 明确显示当前生成器输出纵向表格;横排 A4 表格、表头方向和页码方向必须使用 Word/WPS 专用横向模板。 |
|
| 8 | 横排表格 | 当前未自动化 | `table-1.png` 明确显示当前生成器输出纵向表格;横排 A4 表格、表头方向和页码方向必须使用 Word/WPS 专用横向模板。 |
|
||||||
| 9 | 计量单位、标点、数字 | 结构通过,仍需人工 | 字段边界已校验;正文语义必须按引用标准审校。 |
|
| 9 | 计量单位、标点、数字 | 结构通过,仍需人工 | 字段边界已校验;正文语义必须按引用标准审校。 |
|
||||||
| 10.1 | 信函格式 | 截图通过 | `letter-1.png` 显示 170 mm 上粗下细、下粗上细双线、文号、标题、无首页页码和无版记分隔线。 |
|
| 10.1 | 信函格式 | 结构通过,仍需人工 | `letter-1.png` 显示 170 mm 上粗下细、下粗上细双线、文号、标题、无首页页码和无版记分隔线;可选份号、密级、紧急程度已在回归场景中按第一条双线下的顺序生成,联合行文仍需专用模板。 |
|
||||||
| 10.2 | 命令(令)格式 | 截图通过 | `command-1.png` 显示机关标志、令号下空二行和正文下空二行。签名章仍需人工。 |
|
| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 |
|
||||||
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志 35 mm、人员标签字体和两字起排已观察。 |
|
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png`、`minutes-2.png`;机关标志 35 mm、人员标签字体和两字起排已观察。 |
|
||||||
| 11 | 标准式样图 | 结构通过,仍需人工 | 本记录使用标准条文和实际截图;最终发文仍须对照本单位现行式样图或授权模板。 |
|
| 11 | 标准式样图 | 结构通过,仍需人工 | 本记录使用标准条文和实际截图;最终发文仍须对照本单位现行式样图或授权模板。 |
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ const PAGE_H = 16838;
|
|||||||
const MARGIN = { top: 2098, bottom: 1984, left: 1588, right: 1474 };
|
const MARGIN = { top: 2098, bottom: 1984, left: 1588, right: 1474 };
|
||||||
const CONTENT_W = PAGE_W - MARGIN.left - MARGIN.right;
|
const CONTENT_W = PAGE_W - MARGIN.left - MARGIN.right;
|
||||||
const FIRST_LINE_INDENT = 640;
|
const FIRST_LINE_INDENT = 640;
|
||||||
// Word frame coordinates are relative to the type-area margin in the Office
|
const ONE_CHAR_INDENT = FIRST_LINE_INDENT / 2;
|
||||||
// and LibreOffice renderers used by this skill. These values keep first-page
|
// First-page red-head frames use the type-area top as their vertical anchor.
|
||||||
// header fields and the centered agency mark independent of one another.
|
// GB/T 9704-2012 7.2.1-7.2.4 then become direct coordinates: the first
|
||||||
const HEADER_FIELD_BEFORE = 1760;
|
// header line is y=0 and the agency mark begins 35 mm below it. Keeping the
|
||||||
const AGENCY_FRAME_BEFORE = 2015;
|
// paragraph spacing at zero prevents optional left-corner fields from moving
|
||||||
|
// the centred mark or changing the preprinted-paper reserve.
|
||||||
|
const MM_TO_DXA = 56.692913;
|
||||||
|
const HEADER_FIELD_BEFORE = 0;
|
||||||
|
const AGENCY_FRAME_BEFORE = 0;
|
||||||
const AGENCY_FLOW_BEFORE = 1880;
|
const AGENCY_FLOW_BEFORE = 1880;
|
||||||
const HEADER_FIELD_Y = 275;
|
const HEADER_FIELD_Y = 0;
|
||||||
const AGENCY_FRAME_Y = 2126;
|
const AGENCY_FRAME_Y = Math.round(35 * MM_TO_DXA);
|
||||||
|
|
||||||
function installedFontFamilies() {
|
function installedFontFamilies() {
|
||||||
try {
|
try {
|
||||||
@@ -215,7 +219,7 @@ function floatingHeaderFieldParagraph(text, fontPreset, index) {
|
|||||||
fontPreset,
|
fontPreset,
|
||||||
size: "32",
|
size: "32",
|
||||||
before: HEADER_FIELD_BEFORE,
|
before: HEADER_FIELD_BEFORE,
|
||||||
frame: { hAnchor: "margin", vAnchor: "page", xAlign: "left", y: HEADER_FIELD_Y + index * 560, w: CONTENT_W, h: 560 },
|
frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "left", y: HEADER_FIELD_Y + index * 560, w: CONTENT_W, h: 560 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +231,7 @@ function floatingAgencyMarkParagraph(text) {
|
|||||||
size: "56",
|
size: "56",
|
||||||
color: "FF0000",
|
color: "FF0000",
|
||||||
before: AGENCY_FRAME_BEFORE,
|
before: AGENCY_FRAME_BEFORE,
|
||||||
frame: { hAnchor: "margin", vAnchor: "page", 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 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +274,11 @@ function documentNumberParagraph(text, upward = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function upwardHeaderParagraph(docNo, signer) {
|
function upwardHeaderParagraph(docNo, signer) {
|
||||||
const tabPosition = CONTENT_W - FIRST_LINE_INDENT;
|
const tabPosition = CONTENT_W - ONE_CHAR_INDENT;
|
||||||
const signers = String(signer).split(/[、,,;;\s]+/).map((name) => name.trim()).filter(Boolean);
|
const signers = String(signer).split(/[、,,;;\s]+/).map((name) => name.trim()).filter(Boolean);
|
||||||
if (signers.length > 2) throw new Error("upward --signer supports up to two names; use the authority's joint-signature template for more signers");
|
if (signers.length > 2) throw new Error("upward --signer supports up to two names; use the authority's joint-signature template for more signers");
|
||||||
const signerRuns = signers.map((name, index) => `${index ? run(" ", { fontPreset: "h2", size: "32" }) : ""}${run(name, { fontPreset: "h2", size: "32" })}`).join("");
|
const signerRuns = signers.map((name, index) => `${index ? run(" ", { fontPreset: "h2", size: "32" }) : ""}${run(name, { fontPreset: "h2", size: "32" })}`).join("");
|
||||||
return `<w:p><w:pPr><w:jc w:val="both"/><w:ind w:left="${FIRST_LINE_INDENT}" w:right="${FIRST_LINE_INDENT}"/><w:tabs><w:tab w:val="right" w:pos="${tabPosition}"/></w:tabs><w:spacing w:before="0" w:after="227" w:line="560" w:lineRule="exact"/><w:adjustRightInd w:val="true"/><w:snapToGrid w:val="true"/><w:kinsoku w:val="true"/></w:pPr>${run(docNo, { fontPreset: "body", size: "32" })}<w:r><w:tab/></w:r>${run("签发人:", { fontPreset: "body", size: "32" })}${signerRuns}</w:p>`;
|
return `<w:p><w:pPr><w:jc w:val="both"/><w:ind w:left="${ONE_CHAR_INDENT}" w:right="${ONE_CHAR_INDENT}"/><w:tabs><w:tab w:val="right" w:pos="${tabPosition}"/></w:tabs><w:spacing w:before="0" w:after="227" w:line="560" w:lineRule="exact"/><w:adjustRightInd w:val="true"/><w:snapToGrid w:val="true"/><w:kinsoku w:val="true"/></w:pPr>${run(docNo, { fontPreset: "body", size: "32" })}<w:r><w:tab/></w:r>${run("签发人:", { fontPreset: "body", size: "32" })}${signerRuns}</w:p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function subtitleParagraph(text) {
|
function subtitleParagraph(text) {
|
||||||
@@ -333,6 +337,40 @@ function normalizeAttachmentNote(text) {
|
|||||||
return `附件:${names}`;
|
return `附件:${names}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizedAttachmentName(text) {
|
||||||
|
return String(text ?? "")
|
||||||
|
.trim()
|
||||||
|
.replace(/^附件[::]\s*/, "")
|
||||||
|
.replace(/^附件\s*[一二三四五六七八九十百\d]+\s*[::]\s*/, "")
|
||||||
|
.replace(/^\d+\s*[..、]\s*/, "")
|
||||||
|
.replace(/[。;;]+$/, "")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentNamesFromNote(text) {
|
||||||
|
return normalizeAttachmentNote(text)
|
||||||
|
.replace(/^附件:/, "")
|
||||||
|
.split(/[;;]/)
|
||||||
|
.map(normalizedAttachmentName)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentTitleFromFile(filePath) {
|
||||||
|
const blocks = parseMarkdown(fs.readFileSync(filePath, "utf8"));
|
||||||
|
const heading = blocks.find((block) => block.type === "heading");
|
||||||
|
return normalizedAttachmentName(heading?.text ?? path.basename(filePath, path.extname(filePath)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAttachmentConsistency(note, files) {
|
||||||
|
if (!files.length) return;
|
||||||
|
if (!note) throw new Error("--attachment-file requires --attachment-note so attachment order and titles can be checked");
|
||||||
|
const noted = attachmentNamesFromNote(note);
|
||||||
|
const actual = files.map(attachmentTitleFromFile);
|
||||||
|
if (noted.length !== actual.length || noted.some((name, index) => name !== actual[index])) {
|
||||||
|
throw new Error(`--attachment-note must match attachment titles and order: note=[${noted.join("、")}] files=[${actual.join("、")}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function h1(text) {
|
function h1(text) {
|
||||||
return paragraph(text, {
|
return paragraph(text, {
|
||||||
align: "left",
|
align: "left",
|
||||||
@@ -419,6 +457,11 @@ function headerFieldParagraph(text, fontPreset = "h1", opts = {}) {
|
|||||||
return paragraph(text, { align: opts.align ?? "left", indent: false, fontPreset, size: "32", before: opts.before ?? "0", after: opts.after ?? "0", left: opts.left, right: opts.right });
|
return paragraph(text, { align: opts.align ?? "left", indent: false, fontPreset, size: "32", before: opts.before ?? "0", after: opts.after ?? "0", left: opts.left, right: opts.right });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function letterHeaderRow(leftText, docNo) {
|
||||||
|
const cell = (text, align, fontPreset) => `<w:tc><w:tcPr><w:tcW w:w="4422" w:type="dxa"/><w:tcMar><w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/></w:tcMar></w:tcPr>${paragraph(text, { align, indent: false, fontPreset, size: "32", before: "280", after: "0" })}</w:tc>`;
|
||||||
|
return `<w:tbl><w:tblPr><w:tblW w:w="${CONTENT_W}" w:type="dxa"/><w:tblBorders><w:top w:val="nil"/><w:left w:val="nil"/><w:bottom w:val="nil"/><w:right w:val="nil"/><w:insideH w:val="nil"/><w:insideV w:val="nil"/></w:tblBorders><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="0" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="0" w:type="dxa"/></w:tblCellMar></w:tblPr><w:tblGrid><w:gridCol w:w="4422"/><w:gridCol w:w="4422"/></w:tblGrid><w:tr>${cell(leftText, "left", "body")}${cell(docNo, "right", "body")}</w:tr></w:tbl>`;
|
||||||
|
}
|
||||||
|
|
||||||
function attachmentNote(text) {
|
function attachmentNote(text) {
|
||||||
return paragraph(normalizeAttachmentNote(text), { align: "left", fontPreset: "body", size: "32", before: "560", firstLine: "-960", left: "1600" });
|
return paragraph(normalizeAttachmentNote(text), { align: "left", fontPreset: "body", size: "32", before: "560", firstLine: "-960", left: "1600" });
|
||||||
}
|
}
|
||||||
@@ -431,7 +474,7 @@ function signatureParagraphs(args) {
|
|||||||
if (args["seal-mode"] === "signed") {
|
if (args["seal-mode"] === "signed") {
|
||||||
if (!args.signer || !args["signer-title"]) throw new Error("signed seal mode requires --signer and --signer-title");
|
if (!args.signer || !args["signer-title"]) throw new Error("signed seal mode requires --signer and --signer-title");
|
||||||
const lines = [rightWithIndent(`${args["signer-title"]} ${args.signer}`, FIRST_LINE_INDENT * 2)];
|
const lines = [rightWithIndent(`${args["signer-title"]} ${args.signer}`, FIRST_LINE_INDENT * 2)];
|
||||||
if (args.date) lines.push(rightWithIndent(args.date, FIRST_LINE_INDENT * 2));
|
if (args.date) lines.push(...blankGridLines(1), rightWithIndent(args.date, FIRST_LINE_INDENT * 2));
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
if (args["seal-mode"] === "seal") {
|
if (args["seal-mode"] === "seal") {
|
||||||
@@ -729,6 +772,7 @@ function buildDocument(blocks, args) {
|
|||||||
if (args["copy-no"] && !/^\d{1,6}$/.test(String(args["copy-no"]))) throw new Error("--copy-no must contain one to six Arabic digits");
|
if (args["copy-no"] && !/^\d{1,6}$/.test(String(args["copy-no"]))) throw new Error("--copy-no must contain one to six Arabic digits");
|
||||||
let headerFieldIndex = 0;
|
let headerFieldIndex = 0;
|
||||||
if (args["copy-no"]) body.push(floatingHeaderFieldParagraph(String(args["copy-no"]).padStart(6, "0"), "body", headerFieldIndex++));
|
if (args["copy-no"]) body.push(floatingHeaderFieldParagraph(String(args["copy-no"]).padStart(6, "0"), "body", headerFieldIndex++));
|
||||||
|
else if (args.secret) body.push(floatingHeaderFieldParagraph("", "body", headerFieldIndex++));
|
||||||
if (args.secret) body.push(floatingHeaderFieldParagraph(args.secret, "h1", headerFieldIndex++));
|
if (args.secret) body.push(floatingHeaderFieldParagraph(args.secret, "h1", headerFieldIndex++));
|
||||||
if (args.urgent) body.push(floatingHeaderFieldParagraph(args.urgent, "h1", headerFieldIndex++));
|
if (args.urgent) body.push(floatingHeaderFieldParagraph(args.urgent, "h1", headerFieldIndex++));
|
||||||
if (letterhead === "digital" && args.org) {
|
if (letterhead === "digital" && args.org) {
|
||||||
@@ -754,15 +798,26 @@ function buildDocument(blocks, args) {
|
|||||||
titleGapLines = 2;
|
titleGapLines = 2;
|
||||||
} else if (format === "letter") {
|
} else if (format === "letter") {
|
||||||
if (!args.org) throw new Error("letter format requires --org");
|
if (!args.org) throw new Error("letter format requires --org");
|
||||||
|
if (args["copy-no"] && !/^\d{1,6}$/.test(String(args["copy-no"]))) throw new Error("--copy-no must contain one to six Arabic digits");
|
||||||
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "227" }));
|
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "227" }));
|
||||||
body.push(redDoubleRule({ upperMm: "0.35", lowerMm: "0.25" }));
|
body.push(redDoubleRule({ upperMm: "0.35", lowerMm: "0.25" }));
|
||||||
if (args["doc-no"]) {
|
if (args["doc-no"]) args["doc-no"] = normalizeDocumentNumber(args["doc-no"]);
|
||||||
args["doc-no"] = normalizeDocumentNumber(args["doc-no"]);
|
const letterFields = [];
|
||||||
body.push(headerFieldParagraph(args["doc-no"], "body", { align: "right", before: "280" }));
|
if (args["copy-no"]) letterFields.push({ text: String(args["copy-no"]).padStart(6, "0"), fontPreset: "body" });
|
||||||
|
else if (args.secret) letterFields.push({ text: "", fontPreset: "body" });
|
||||||
|
if (args.secret) letterFields.push({ text: args.secret, fontPreset: "h1" });
|
||||||
|
if (args.urgent) letterFields.push({ text: args.urgent, fontPreset: "h1" });
|
||||||
|
if (letterFields.length || args["doc-no"]) {
|
||||||
|
const first = letterFields[0] ?? { text: "", fontPreset: "body" };
|
||||||
|
body.push(letterHeaderRow(first.text, args["doc-no"] ?? ""));
|
||||||
|
for (const field of letterFields.slice(1)) {
|
||||||
|
body.push(headerFieldParagraph(field.text, field.fontPreset));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
titleGapLines = 2;
|
titleGapLines = 2;
|
||||||
} else if (format === "command") {
|
} else if (format === "command") {
|
||||||
if (!args.org) throw new Error("command format requires --org");
|
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 令号");
|
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" }));
|
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "0" }));
|
||||||
if (args["doc-no"]) {
|
if (args["doc-no"]) {
|
||||||
@@ -801,14 +856,18 @@ function buildDocument(blocks, args) {
|
|||||||
body.push(attachmentNote(args["attachment-note"]));
|
body.push(attachmentNote(args["attachment-note"]));
|
||||||
}
|
}
|
||||||
if (args.sender || args.date) {
|
if (args.sender || args.date) {
|
||||||
body.push(emptyLine());
|
body.push(...(args["seal-mode"] === "signed" ? blankGridLines(2) : [emptyLine()]));
|
||||||
body.push(...signatureParagraphs(args));
|
body.push(...signatureParagraphs(args));
|
||||||
}
|
}
|
||||||
if (args.note) body.push(paragraph(`(${args.note.replace(/^(|)$/g, "")})`, { align: "left", fontPreset: "body", size: "32" }));
|
if (args.note) body.push(paragraph(`(${args.note.replace(/^(|)$/g, "")})`, { align: "left", fontPreset: "body", size: "32" }));
|
||||||
const attachmentFiles = Array.isArray(args["attachment-file"]) ? args["attachment-file"] : args["attachment-file"] ? [args["attachment-file"]] : [];
|
const attachmentFiles = Array.isArray(args["attachment-file"]) ? args["attachment-file"] : args["attachment-file"] ? [args["attachment-file"]] : [];
|
||||||
attachmentFiles.forEach((filePath, index) => {
|
const absoluteAttachmentFiles = attachmentFiles.map((filePath) => {
|
||||||
const absolutePath = path.resolve(filePath);
|
const absolutePath = path.resolve(filePath);
|
||||||
if (!fs.existsSync(absolutePath)) throw new Error(`attachment file not found: ${filePath}`);
|
if (!fs.existsSync(absolutePath)) throw new Error(`attachment file not found: ${filePath}`);
|
||||||
|
return absolutePath;
|
||||||
|
});
|
||||||
|
validateAttachmentConsistency(args["attachment-note"], absoluteAttachmentFiles);
|
||||||
|
absoluteAttachmentFiles.forEach((absolutePath, index) => {
|
||||||
const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8"));
|
const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8"));
|
||||||
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath));
|
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath));
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ package_traework() {
|
|||||||
stage=$(mktemp -d "${TMPDIR:-/tmp}/gongwen-traework.XXXXXX")
|
stage=$(mktemp -d "${TMPDIR:-/tmp}/gongwen-traework.XXXXXX")
|
||||||
output="$skill_dir/dist/traework-gongwen-skill.zip"
|
output="$skill_dir/dist/traework-gongwen-skill.zip"
|
||||||
mkdir -p "$skill_dir/dist" "$stage/gongwen"
|
mkdir -p "$skill_dir/dist" "$stage/gongwen"
|
||||||
cp -R "$skill_dir/SKILL.md" "$skill_dir/scripts" "$skill_dir/references" "$stage/gongwen/"
|
cp -R "$skill_dir/SKILL.md" "$skill_dir/scripts" "$skill_dir/references" "$skill_dir/tests" "$stage/gongwen/"
|
||||||
rm -f "$output"
|
rm -f "$output"
|
||||||
(cd "$stage" && zip -qr "$output" gongwen)
|
(cd "$stage" && zip -qr "$output" gongwen)
|
||||||
unzip -t "$output" >/dev/null
|
unzip -t "$output" >/dev/null
|
||||||
|
|||||||
@@ -129,7 +129,15 @@ if (profile === "formal") {
|
|||||||
check("Red rule width", /style="width:156mm;height:0\.5mm"/.test(doc), "header separator spans the 156 mm type area");
|
check("Red rule width", /style="width:156mm;height:0\.5mm"/.test(doc), "header separator spans the 156 mm type area");
|
||||||
const docParagraphs = paragraphs(doc);
|
const docParagraphs = paragraphs(doc);
|
||||||
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p));
|
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p));
|
||||||
check("Digital red-head agency position", agencyIndex >= 0 && /w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="page"[^>]*w:xAlign="center"[^>]*w:y="2126"/.test(docParagraphs[agencyIndex]) && /w:before="2015"/.test(docParagraphs[agencyIndex]), "agency mark is independently fixed at the 35 mm type-area position");
|
// 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);
|
||||||
|
check("Digital red-head agency position", agencyIndex >= 0 && new RegExp(`w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="margin"[^>]*w:xAlign="center"[^>]*w:y="${agencyOffsetFromTypeAreaTop}"`).test(docParagraphs[agencyIndex]) && /w:before="0"/.test(docParagraphs[agencyIndex]), "agency mark is independently fixed 35 mm below the type-area top");
|
||||||
|
const fixedHeaderFrames = docParagraphs
|
||||||
|
.filter((p) => /w:framePr[^>]*w:hAnchor="margin"[^>]*w:vAnchor="margin"/.test(p))
|
||||||
|
.map((p) => Number((p.match(/w:y="(\d+)"/) ?? [])[1]))
|
||||||
|
.filter((value) => Number.isFinite(value));
|
||||||
|
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 docNoIndex = docParagraphs.findIndex((p) => /〔\d{4}〕[1-9]\d*号/.test(textOf(p)));
|
||||||
const agencyToDocNo = agencyIndex >= 0 && docNoIndex > agencyIndex ? docParagraphs.slice(agencyIndex + 1, docNoIndex) : [];
|
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="1880"/.test(p));
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
const [withFieldsPdf, withoutFieldsPdf, agencyName = "示例单位文件", copyNo = "000007"] = process.argv.slice(2);
|
||||||
|
if (!withFieldsPdf || !withoutFieldsPdf || !fs.existsSync(withFieldsPdf) || !fs.existsSync(withoutFieldsPdf)) {
|
||||||
|
console.error("Usage: measure-header-coordinates.mjs WITH_FIELDS.pdf WITHOUT_FIELDS.pdf [agency-name] [copy-number]");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHtml(value) {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
||||||
|
.replace(/&#(\d+);/g, (_, decimal) => String.fromCodePoint(Number(decimal)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function words(pdf) {
|
||||||
|
const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" });
|
||||||
|
return [...html.matchAll(/<word\s+[^>]*yMin="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
|
||||||
|
yMinPt: Number(match[1]),
|
||||||
|
text: decodeHtml(match[2]),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findWord(pdfWords, expected) {
|
||||||
|
const word = pdfWords.find(({ text }) => text.includes(expected));
|
||||||
|
if (!word) throw new Error(`${expected} was not found on the first page`);
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mm(word) {
|
||||||
|
return word.yMinPt * 25.4 / 72;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withFields = words(withFieldsPdf);
|
||||||
|
const withoutFields = words(withoutFieldsPdf);
|
||||||
|
const copy = findWord(withFields, copyNo);
|
||||||
|
const agencyWithFields = findWord(withFields, agencyName);
|
||||||
|
const agencyWithoutFields = findWord(withoutFields, agencyName);
|
||||||
|
const copyMm = mm(copy);
|
||||||
|
const agencyWithFieldsMm = mm(agencyWithFields);
|
||||||
|
const agencyWithoutFieldsMm = mm(agencyWithoutFields);
|
||||||
|
const agencyDeltaMm = Math.abs(agencyWithFieldsMm - agencyWithoutFieldsMm);
|
||||||
|
|
||||||
|
// PDF glyph tops sit slightly below or above the frame edge depending on the
|
||||||
|
// installed font. These ranges still independently verify the paper positions:
|
||||||
|
// first type-area line at 37 mm, agency mark 35 mm below it (72 mm page top).
|
||||||
|
if (copyMm < 37 || copyMm > 45) throw new Error(`copy number glyph is at ${copyMm.toFixed(2)} mm; expected first type-area line near 37 mm`);
|
||||||
|
if (agencyWithFieldsMm < 68 || agencyWithFieldsMm > 77) throw new Error(`agency mark glyph is at ${agencyWithFieldsMm.toFixed(2)} mm; expected 35 mm below the 37 mm type-area top`);
|
||||||
|
if (agencyDeltaMm > 0.8) throw new Error(`optional left-corner fields moved the agency mark by ${agencyDeltaMm.toFixed(2)} mm`);
|
||||||
|
|
||||||
|
console.log(`Header coordinate measurement passed: copy=${copyMm.toFixed(2)}mm, agency=${agencyWithFieldsMm.toFixed(2)}mm, no-fields-agency=${agencyWithoutFieldsMm.toFixed(2)}mm, delta=${agencyDeltaMm.toFixed(2)}mm`);
|
||||||
+25
-4
@@ -31,14 +31,18 @@ unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q 'w:snapToGrid w:va
|
|||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q '示例单位文件'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q '示例单位文件'
|
||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'FF0000'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'FF0000'
|
||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'height:0.5mm'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'height:0.5mm'
|
||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:framePr.*w:xAlign="center".*w:y="2126"'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="center".*w:y="1984"'
|
||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="2015"'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:before="0"'
|
||||||
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:snapToGrid w:val="false"'
|
unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'w:snapToGrid w:val="false"'
|
||||||
|
|
||||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital-fields.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔2026〕3号" --title "版头字段定位测试"
|
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital-fields.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔2026〕3号" --title "版头字段定位测试"
|
||||||
"$NODE" "$VERIFY" --input "$TMP_DIR/digital-fields.docx" --profile formal --letterhead digital
|
"$NODE" "$VERIFY" --input "$TMP_DIR/digital-fields.docx" --profile formal --letterhead digital
|
||||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:xAlign="left".*w:y="275"'
|
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="left".*w:y="0"'
|
||||||
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:before="2015"'
|
unzip -p "$TMP_DIR/digital-fields.docx" word/document.xml | grep -q 'w:framePr.*w:hAnchor="margin".*w:vAnchor="margin".*w:xAlign="left".*w:y="560"'
|
||||||
|
|
||||||
|
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/secret-only.docx" --format formal --letterhead digital --secret "机密★3年" --org "示例单位文件" --doc-no "示例发〔2026〕5号" --title "密级首行留空测试"
|
||||||
|
"$NODE" "$VERIFY" --input "$TMP_DIR/secret-only.docx" --profile formal --letterhead digital
|
||||||
|
unzip -p "$TMP_DIR/secret-only.docx" word/document.xml | grep -q 'w:framePr.*w:vAnchor="margin".*w:y="560"'
|
||||||
|
|
||||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/preprinted-fields.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔2026〕4号" --title "预印字段定位测试"
|
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/preprinted-fields.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔2026〕4号" --title "预印字段定位测试"
|
||||||
"$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-fields.docx" --profile formal --letterhead preprinted
|
"$NODE" "$VERIFY" --input "$TMP_DIR/preprinted-fields.docx" --profile formal --letterhead preprinted
|
||||||
@@ -70,9 +74,21 @@ 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 '附件:1. 测试附件'
|
||||||
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore'
|
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore'
|
||||||
|
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-attachment.out"
|
||||||
|
|
||||||
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令正文"
|
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令正文"
|
||||||
"$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command
|
"$NODE" "$VERIFY" --input "$TMP_DIR/command.docx" --profile command
|
||||||
|
|
||||||
|
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-command.docx" --format command --org "示例机关" --doc-no "第1号" --title "命令正文" >"$TMP_DIR/bad-command.out" 2>&1; then
|
||||||
|
echo "command without 命令/令 unexpectedly succeeded" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
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" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要正文" --attendees "张三、李四" --absent "王五"
|
||||||
"$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes
|
"$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes
|
||||||
|
|
||||||
@@ -91,6 +107,11 @@ if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-date.docx" --format
|
|||||||
fi
|
fi
|
||||||
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
|
grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
|
||||||
|
|
||||||
|
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔2026〕2号" --title "信函字段边界"
|
||||||
|
"$NODE" "$VERIFY" --input "$TMP_DIR/letter-fields.docx" --profile letter
|
||||||
|
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年'
|
||||||
|
|
||||||
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 "$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
|
if ! fc-list -f '%{family}\n' | grep -Eq '方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong'; then
|
||||||
echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2
|
echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ for path in \
|
|||||||
done
|
done
|
||||||
|
|
||||||
unzip -t "$skill_dir/dist/traework-gongwen-skill.zip" >/dev/null
|
unzip -t "$skill_dir/dist/traework-gongwen-skill.zip" >/dev/null
|
||||||
unzip -l "$skill_dir/dist/traework-gongwen-skill.zip" | grep -q 'gongwen/SKILL.md'
|
unzip -l "$skill_dir/dist/traework-gongwen-skill.zip" >"$test_home/traework-listing.txt"
|
||||||
|
grep -q 'gongwen/SKILL.md' "$test_home/traework-listing.txt"
|
||||||
|
grep -q 'gongwen/tests/visual-audit.sh' "$test_home/traework-listing.txt"
|
||||||
printf '%s\n' 'cross-platform installation tests passed'
|
printf '%s\n' 'cross-platform installation tests passed'
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format
|
|||||||
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.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 "$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 "$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/letter.docx" --format letter --org "示例机关" --doc-no "示例〔2026〕12号" --title "信函格式实测" --page-number none
|
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/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 "$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/table.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔2026〕13号" --title "表格版式实测" --page-number standard
|
||||||
@@ -33,7 +34,7 @@ for file in "$out_dir/docx"/*.docx; do
|
|||||||
base=$(basename "$file" .docx)
|
base=$(basename "$file" .docx)
|
||||||
case "$base" in
|
case "$base" in
|
||||||
ordinary) profile=ordinary ;;
|
ordinary) profile=ordinary ;;
|
||||||
letter) profile=letter ;;
|
letter|letter-fields) profile=letter ;;
|
||||||
command) profile=command ;;
|
command) profile=command ;;
|
||||||
minutes) profile=minutes ;;
|
minutes) profile=minutes ;;
|
||||||
*) profile=formal ;;
|
*) profile=formal ;;
|
||||||
@@ -54,6 +55,10 @@ for file in "$out_dir/pdf"/*.pdf; do
|
|||||||
base=$(basename "$file" .pdf)
|
base=$(basename "$file" .pdf)
|
||||||
pdftoppm -png -r 150 "$file" "$out_dir/png/$base" >"$out_dir/logs/$base.pdftoppm.log" 2>&1
|
pdftoppm -png -r 150 "$file" "$out_dir/png/$base" >"$out_dir/logs/$base.pdftoppm.log" 2>&1
|
||||||
done
|
done
|
||||||
|
node "$skill_dir/tests/measure-header-coordinates.mjs" \
|
||||||
|
"$out_dir/pdf/formal-digital-all.pdf" \
|
||||||
|
"$out_dir/pdf/formal-digital-upward.pdf" \
|
||||||
|
"示例单位文件" "000007" | tee "$out_dir/logs/header-coordinates.log"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo -e "profile\tdocx\tpdf\tscreenshot_prefix"
|
echo -e "profile\tdocx\tpdf\tscreenshot_prefix"
|
||||||
|
|||||||
Reference in New Issue
Block a user