From 9ff514268252466d4d2bb3f88e52d8c1dd2741c4 Mon Sep 17 00:00:00 2001 From: mizzlelover Date: Thu, 10 Sep 2026 16:47:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=85=AC=E6=96=87=E7=89=88?= =?UTF-8?q?=E5=BC=8F=E4=B8=8E=E7=BA=A2=E5=A4=B4=E5=A5=97=E6=89=93=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +++-- SKILL.md | 22 ++++-- dist/traework-gongwen-skill.zip | Bin 20023 -> 22095 bytes references/formal-checklist.md | 3 + references/gbt9704-2012-summary.md | 18 +++-- scripts/generate_gongwen_docx.mjs | 121 +++++++++++++++++++++++++---- scripts/install.sh | 1 + scripts/verify_gongwen_docx.mjs | 100 +++++++++++------------- tests/run_tests.sh | 18 +++-- 9 files changed, 211 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 417f42d..1fde7dc 100644 --- a/README.md +++ b/README.md @@ -75,21 +75,32 @@ https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p | 场景 | 本项目的处理方式 | | --- | --- | | 文档主体 | A4、版心、标题、正文、层次、署名和日期排版 | -| 机构名称与文号 | 使用 `--org`、`--doc-no` 按输入排版 | +| 普通报告、方案、汇报 | 默认 `ordinary`,不因机构名称自动使用红头 | +| 正式发文与红头纸 | 显式 `formal`;默认预印红头纸套打,首面留白、不重绘红色机关标志和红线 | +| 完整电子红头 | 显式 `formal --letterhead digital`,才在 DOCX 中绘制红头要素 | +| 机构名称与文号 | 使用 `--org`、`--doc-no` 按输入排版;文号仅在相应版式中定位 | +| 标题与目录 | 四级标题写入 Word/WPS 标题样式及大纲级别,可插入自动目录 | | 页码 | 居中、单双页或不显示 | | 印章、签名章 | 在生成的 DOCX 中按实际需要插入图片或完成实体盖章 | -| 特定格式 | 可在此版式基础上继续编辑,或使用对应模板 | +| 特定格式 | `letter`、`command`、`minutes` 使用对应生成分支 | GB/T 9704-2012 当前为现行标准,2025-05-30 复审继续有效。标准适用于党政机关制发公文,其他机关和单位可以参照执行。[全国标准信息公共服务平台](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p) ## 快速开始 ```bash -node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/gongwen.docx --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试" --page-number standard -node scripts/verify_gongwen_docx.mjs --input /tmp/gongwen.docx --profile standard-pages +node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/gongwen.docx --format ordinary --title "公文格式回归测试" +node scripts/verify_gongwen_docx.mjs --input /tmp/gongwen.docx --profile ordinary ``` -然后用 Word 或 WPS 打开,并转换 PDF 检查标题、正文、表格、分页和页码。生成器支持 `center`、`standard` 和 `none` 页码模式,机构名称和文号按输入写入文档。 +需要正式发文时,必须明确选择版式。下面的命令生成预印红头纸套打稿;首页只留出纸上已有红头和红线区域,DOCX 不会重绘它们。`72` 是默认预留高度,应按本单位实际红头纸改为 `37—130` 毫米。 + +```bash +node scripts/generate_gongwen_docx.mjs --input tests/fixture.md --output /tmp/formal.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试" +node scripts/verify_gongwen_docx.mjs --input /tmp/formal.docx --profile formal --letterhead preprinted +``` + +只有需要完整电子红头文件时,才加 `--letterhead digital`。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。 ## 跨平台安装 diff --git a/SKILL.md b/SKILL.md index 1d41784..dec4b6f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -13,19 +13,29 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有 ## 工具定位 -本工具只处理排版。它不判断用户材料的用途、单位身份、授权状态或后续盖章方式,也不推断用户未提供的要素。机构名称、文号、主送机关、署名、日期和页码模式均按用户输入排入 DOCX。 +本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。 -生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。 +生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。 `references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。 ## 生成打印稿 ```bash -node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题" --sender "落款单位" --date "2026年9月9日" --page-number standard +node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format ordinary --title "文档标题" ``` -支持 `center`、`standard` 与 `none` 三种页码模式,默认 `center`。`standard` 使用单页右、双页左的页码位置。`--org` 以红色居中机构名称排版,`--doc-no` 在其下方排版。 +普通材料使用 `--format ordinary`,默认居中页码,不绘制红头。正式发文须显式使用 `--format formal`;其默认 `--letterhead preprinted`,即首面预留红头纸区域、不在 DOCX 中重绘纸上已有的红色机关标志和红线。`--doc-no` 等本次需打印的黑色变量仍可保留。只有明确要求完整电子版时使用 `--letterhead digital`。 + +```bash +# 预印红头纸套打;72mm 是默认机关标志区域高度,可按实际纸样调为 37—130mm +node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "发文机关名称" --doc-no "单位发〔2026〕1号" --title "文档标题" --sender "落款单位" --date "2026年9月9日" + +# 完整电子红头版,仅在明确要求时使用 +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` 供普通材料选择。 Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)`;序数缺失时才按 Markdown 层级映射。正文、四级标题均左空二字,回行顶格。标题字体在运行机器缺失时,会保留请求的字体名;打开文件时仍须确认 Office 的实际替代字体,不把普通宋体视作小标宋的等价替代。 @@ -34,10 +44,10 @@ Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)` ```bash unzip -t output.docx soffice --headless --convert-to pdf --outdir /tmp output.docx -node scripts/verify_gongwen_docx.mjs --input output.docx --profile layout +node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --letterhead preprinted ``` -校验器核验生成器可检查的版式要素。`layout` 用于居中页码,`standard-pages` 用于单双页页码。输出后可在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码和装订。 +校验器核验生成器可检查的版式要素,并按 `ordinary`、`formal`、`letter`、`command`、`minutes` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。 ## 输出说明 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip index 3d48a1da9f14bfec887089fa893148b93f198f74..b5f0ab648685687e6f09af351152ae80d70d7bfb 100644 GIT binary patch delta 19602 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 delta 17416 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 $32&z$6(_4EJxDQEs;6vXf!&MbpT=99tw-&z1d zHKbi}B(ZuQX}gXdt)h=p{H9e>Ri`Rts&cXinUW$$3viV1PT8bPk~i$6i8!EJ0___o zJb)S_M1LaLclceG3@2XYNn;K=RLoPeZ+>204dTh9IY##r2@}Zco1XME>=-sr)plqB z{_UFHLRPFLo;Gy&7OkXz6`M?=N}GL81H@I>I>&<1ABNjm`x8D5=XX<2EKg6M=j|8v zu(9r}z~Clc5&J)%y9iBr;W1zvMAEMv7u0ezvov*p92U)RFEammm$@O%-h-Z|99!2mbuIEZGmW{#WD3Ezp|r@SGFu0c zWy*2FB#+IHuuE4#Ppfo=o78=}^ zyu%%Y>vSiz#RzS_5OA*(LYW>wzD1-aSA!So1xs9z#5Yph#(P#OQ;VJ(p|6lO2QF<+ zEJu>#**%0}=m0^EN&m&A52XU>IGuiQU8B^WknU}WFSgcPXU~`$-CjIo=DS(67Or2EtpF=SXC^zLg6qB zFv=Ds^7x)A+ml5_K~y>F$ep5VPqxgmbZoWMFY@>>=2ag!T+M&$kXt500Zu~89fc9O zN(qP;BurRK&qwf3qx2X%W5X9nRV`cjT)v9-QS0QWg}wvi+KK9ba6^ASl+kg9`p0fX$N!jRYl)j^;K$7T%+;si*fqqnJLX{$ zKLcd=w4CWClbS97-XIzR{i4p?YM5zoU(INl8@Wvf>FNdE1OBdq2&hs>-3RJsdJ^8` z0+IGuxtm+87vrd7w#j2_dc@pJMbffUbXJQm)HBLnK|RJoR+jECF+P+%f2_p*#S(D~ zixk2lt@eO}^G$jGUs9$9PvaXoHD%(R-POgmuB{lxw_R~7*)-GOz z&=*&MN>;-IPy=P$*dpoQw(j-q1{Xeib;bqzbab3~OsyU2{o`yFP9wbNB{hzX6UN}= z)NXUI;Z8{&Y*U6l4<6DoO4w_y*tLZn#%0NLG?5^^ixjP%%D+J;*(nN|Jw+cRKd-!U zqMsHZOI)u0VyO0?yd|ft6CWf_*Cl85pP%}_ru#a23roH5_eb6OlRud;hljjhBptEk zO$8&n@|EeQCYXE?P2m!otfaF~=2(zf9sip{_x#-WceI}W=zc=tyV%v5$Iy?$A7c+D+la3no2!6~>O`W&w;6?QUSEmocVrgZ}>mDk{tcwcZ>Ztf2 z)hYfXEl6qC#UBU0B?y_#c5&%dejasBqD|45MNLKgRNWOWg}oi0&+mU%v5z^)Y|`&1 zQbmWe9=Wg0s4bo78R`BiGPJXNcH6zfpZm|dy}H8Fo;dl&|Iev2|AaWM{}1f8F*3<9 z!;gNLToa@NI*(!UgJ2nEV4rRBsUUMOzaUr!%K;CD!76Hm4tQV%AMh|4Xn+F7)^7&j z>NX$%o-V<#q;WsXWQSl~m~CLihz(~93|c6PdwGD0MJHe5lw^Zi0CVyGU}>2gX$FS$ z{Jiw?)I9y-iwsD*EWXDh`uHp=2J&GXLBQ42e&_vPG ztH3bXOK`7#IYQ6fbE^b7q+A=q0QJ@h|2P9;`sOFfecfp)+u`<2_Fh4*;6+xP|}# diff --git a/references/formal-checklist.md b/references/formal-checklist.md index 84ae5fe..ab0a867 100644 --- a/references/formal-checklist.md +++ b/references/formal-checklist.md @@ -4,6 +4,8 @@ ## 1. 前提与制作 +- [ ] 已明确选择 `ordinary`、`formal`、`letter`、`command` 或 `minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。 +- [ ] 正式发文已确认采用预印红头纸套打或完整电子红头。套打稿以实际纸样核定首页预留高度,不重复打印纸上既有的红色机关标志、红线。 - [ ] 已确认发文机关、文种、行文关系、主送范围、发文字号、签发权限及机关现行模板。 - [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。 - [ ] 无特殊说明时采用三号仿宋;一般每面22行、每行28字并撑满版心。不得把固定28pt当作国标唯一强制值。 @@ -25,6 +27,7 @@ - [ ] 主送机关在标题下空一行顶格,回行顶格,最后名称后用全角冒号;过多而影响首页正文时移入版记。 - [ ] 首页显示正文;正文一般三号仿宋,每个自然段左空二字、回行顶格。 - [ ] 层次序数按“一、”“(一)”“1.”“(1)”使用;第一层黑体、第二层楷体、第三和第四层仿宋,均为三号。 +- [ ] 四级标题分别应用 Word/WPS 的“标题1—标题4”样式和相应大纲级别;文档总标题不进入目录。需要目录时,已在目标软件中插入并更新自动目录。 - [ ] 附件说明位于正文下空一行、左空二字;“附件:”后名称无标点;多附件编号正确,长名称回行与首字对齐。 - [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。 - [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。 diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md index 37782a5..a1c4e8b 100644 --- a/references/gbt9704-2012-summary.md +++ b/references/gbt9704-2012-summary.md @@ -11,17 +11,21 @@ - 标准状态页:https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p - 公文处理依据:https://www.gov.cn/zwgk/2013-02/22/content_2337704.htm -本摘要用于生成和校验,不代替标准原文。工具按用户提供的字段排版,不判断材料用途或替用户补充未提供的信息。 +本摘要用于生成和校验,不代替标准原文。工具按用户提供的字段排版,不判断单位身份、授权状态或替用户补充未提供的信息;版式模式必须由用户明确选择。 ## 二、常见使用场景 ### 1. 公文版式打印稿 -适用于方案、报告、说明、备忘材料和内部汇报。采用A4纸、标准版心、字号、行距、标题层级和页码,可按输入加入机构名称、文号、主送机关、署名和日期。 +适用于方案、报告、说明、备忘材料和内部汇报。采用A4纸、标准版心、字号、行距、标题层级和页码。出现机构名称不等于采用红头;普通材料不使用版头专属规则。 ### 2. 正式党政机关公文 -除页面和正文格式外,还可按实际需要处理版头、主体、版记、页码、印章或签名章。生成器排版用户提供的文字字段;印章和签名章可在生成后的 Word/WPS 文件中按需插入。 +在用户明确要求正式发文版式、并提供相应要素时使用。除页面和正文格式外,处理版头、主体、版记、页码、印章或签名章。生成器排版用户提供的文字字段;印章和签名章可在生成后的 Word/WPS 文件中按需插入。 + +### 3. 预印红头纸套打与完整电子版 + +预印红头纸套打是常见制作流程:DOCX 首页预留纸上已印制的机关标志、红线等区域,打印时放入对应红头纸;不在文档中重复绘制这些红色元素。标准规定最终版式,不规定只能使用预印纸。完整电子红头仅在用户明确要求时生成。套打区域高度以实际纸样为准,不能将“35mm”误作全部预留高度。 ## 三、纸张、版面和印制 @@ -142,10 +146,12 @@ | 报告居中页码 | 是 | 无 | | 正式单双页页码 | 是 | `--page-number standard` | | 普通表格 | 是 | 横排表格方向和复杂跨页表格复核 | -| 红色机构名称、文号 | 是 | `--org`、`--doc-no` | +| 普通材料 | 是 | `--format ordinary`,不绘制红头 | +| 预印红头纸套打 | 是 | `--format formal --letterhead preprinted`,按纸样设置预留高度 | +| 完整电子红头 | 是 | `--format formal --letterhead digital` | | 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 | -| 版记 | 可编辑 | 按实际字段补充 | -| 信函、命令、纪要 | 可编辑 | 按实际版式继续调整 | +| 版记 | 是 | `--cc`、`--print-org`、`--print-date` 适用时生成 | +| 信函、命令、纪要 | 是 | `--format letter|command|minutes`,按实际字段生成 | ## 十、建议检查 diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs index ec6ea83..a7973f8 100755 --- a/scripts/generate_gongwen_docx.mjs +++ b/scripts/generate_gongwen_docx.mjs @@ -32,7 +32,7 @@ function preferredFont(candidates) { const FONT = { title: { eastAsia: preferredFont(["方正小标宋简体", "方正小标宋_GBK", "FZXiaoBiaoSong-B05S"]), ascii: "Times New Roman", cs: "Times New Roman" }, subtitle: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "STFangsong"]), ascii: "Times New Roman", cs: "Times New Roman" }, - body: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "STFangsong"]), ascii: "Times New Roman", cs: "Times New Roman" }, + body: { eastAsia: preferredFont(["仿宋", "仿宋_GB2312", "FangSong", "FangSong_GB2312", "STFangsong"]), ascii: "Times New Roman", cs: "Times New Roman" }, h1: { eastAsia: preferredFont(["黑体", "SimHei", "Heiti SC", "STHeiti"]), ascii: "Times New Roman", cs: "Times New Roman" }, h2: { eastAsia: preferredFont(["楷体", "楷体_GB2312", "KaiTi", "Kaiti SC", "STKaiti"]), ascii: "Times New Roman", cs: "Times New Roman" }, song: { eastAsia: preferredFont(["宋体", "SimSun"]), ascii: "宋体", cs: "宋体" }, @@ -40,11 +40,13 @@ const FONT = { function usage() { console.log(`Usage: - node generate_gongwen_docx.mjs --input source.md --output out.docx [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--page-number center|standard|none] + 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 日期] [--page-number center|standard|none] Notes: - Converts Markdown to a GB/T 9704-2012 page-layout DOCX. - - Generates a DOCX from the supplied content and formatting fields. Its default page number is centered; use standard for odd/even page-number placement or none to suppress it. + - ordinary is the default for reports, plans and other editable materials. It never creates a red header from --org alone. + - formal is used only when a formal issuing-document layout is explicitly requested. It defaults to preprinted letterhead: red elements are reserved, not redrawn. Use --letterhead digital only for a complete electronic red-head layout. + - letter, command and minutes use their dedicated national-standard layout branches. Their required fields must be supplied explicitly. - --org and --doc-no are printed exactly as supplied. The tool formats document content; it does not infer missing information or decide the document's use. - Supports headings, paragraphs, ordered/unordered lines, and pipe tables. - No npm dependencies; requires zip in PATH.`); @@ -112,7 +114,10 @@ function paragraph(text, opts = {}) { const keepNext = opts.keepNext ? "" : ""; const before = opts.before ?? "0"; const after = opts.after ?? "0"; - const pPr = `${keepNext}${align}${indent}`; + const style = opts.style ? `` : ""; + const outline = opts.outlineLevel === undefined ? "" : ``; + const pageBreak = opts.pageBreakBefore ? "" : ""; + const pPr = `${style}${keepNext}${pageBreak}${align}${indent}${outline}`; return `${pPr}${run(text, opts)}`; } @@ -124,6 +129,7 @@ function titleParagraph(text) { size: "44", before: "0", after: "560", + style: "GongwenTitle", }); } @@ -134,17 +140,19 @@ function agencyMarkParagraph(text) { fontPreset: "title", size: "56", color: "FF0000", - after: "280", + after: "560", + before: "5953", }); } -function documentNumberParagraph(text) { +function documentNumberParagraph(text, upward = false) { return paragraph(text, { - align: "left", + align: upward ? "left" : "center", indent: false, fontPreset: "body", size: "32", - after: "160", + after: "227", + left: upward ? FIRST_LINE_INDENT : undefined, }); } @@ -175,6 +183,8 @@ function h1(text) { keepNext: true, before: "160", after: "0", + style: "Heading1", + outlineLevel: 0, }); } @@ -186,6 +196,8 @@ function h2(text) { keepNext: true, before: "80", after: "0", + style: "Heading2", + outlineLevel: 1, }); } @@ -197,6 +209,8 @@ function h3(text) { keepNext: true, before: "40", after: "0", + style: "Heading3", + outlineLevel: 2, }); } @@ -208,6 +222,8 @@ function h4(text) { keepNext: true, before: "0", after: "0", + style: "Heading4", + outlineLevel: 3, }); } @@ -215,6 +231,32 @@ function right(text) { return paragraph(text, { align: "right", indent: false, right: FIRST_LINE_INDENT }); } +function redRule(thickness = "8") { + return ``; +} + +function headerFieldParagraph(text, fontPreset = "h1", opts = {}) { + return paragraph(text, { align: opts.align ?? "left", indent: false, fontPreset, size: "32", after: opts.after ?? "0", left: opts.left, right: opts.right }); +} + +function attachmentNote(text) { + return paragraph(`附件:${text}`, { align: "left", fontPreset: "body", size: "32", before: "560", firstLine: FIRST_LINE_INDENT, left: "0" }); +} + +function colophon(args) { + if (!args["cc"] && !args["print-org"] && !args["print-date"]) return []; + const body = [redRule("4")]; + if (args.cc) body.push(paragraph(`抄送:${args.cc.replace(/[。.]?$/, "")}。`, { align: "left", indent: false, left: "320", right: "320", fontPreset: "body", size: "28" })); + if (args.cc) body.push(redRule("2")); + if (args["print-org"] || args["print-date"]) { + const left = args["print-org"] ?? ""; + const rightValue = args["print-date"] ? `${args["print-date"].replace(/印发$/, "")}印发` : ""; + body.push(`${run(left, { fontPreset: "body", size: "28" })}${run(rightValue, { fontPreset: "body", size: "28" })}`); + } + body.push(redRule("4")); + return body; +} + function emptyLine() { return ``; } @@ -371,16 +413,20 @@ function renderHeading(block) { function renderParagraph(block) { if (isFirstLayerHeadingText(block.text)) return h1(block.text); if (isSecondLayerHeadingText(block.text)) return h2(block.text); + if (isThirdLayerHeadingText(block.text)) return h3(block.text); + if (isFourthLayerHeadingText(block.text)) return h4(block.text); return paragraph(block.text); } function buildDocument(blocks, args) { let title = args.title; + const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary"; + const letterhead = args.letterhead === "digital" ? "digital" : "preprinted"; const pageNumberMode = args["no-page-number"] || args["no-page-numbers"] ? "none" : ["center", "standard", "none"].includes(args["page-number"]) ? args["page-number"] - : "center"; + : format === "formal" || format === "letter" || format === "command" || format === "minutes" ? "standard" : "center"; const body = []; const sourceBlocks = [...blocks]; if (!title && sourceBlocks[0]?.type === "heading") { @@ -395,8 +441,36 @@ function buildDocument(blocks, args) { if (!mainSend && sourceBlocks[0]?.type === "paragraph" && looksLikeMainSend(sourceBlocks[0].text)) { mainSend = sourceBlocks.shift().text; } - if (args.org) body.push(agencyMarkParagraph(args.org)); - if (args["doc-no"]) body.push(documentNumberParagraph(args["doc-no"])); + const upward = args["upward"] === "true" || args["upward"] === true; + const formal = format === "formal"; + if (formal) { + if (args["copy-no"]) body.push(headerFieldParagraph(String(args["copy-no"]).padStart(6, "0"), "body")); + if (args.secret) body.push(headerFieldParagraph(args.secret)); + if (args.urgent) body.push(headerFieldParagraph(args.urgent)); + if (letterhead === "digital" && args.org) body.push(agencyMarkParagraph(args.org)); + if (letterhead === "preprinted") { + const reserve = Number(args["letterhead-reserve-mm"] ?? 72); + if (!Number.isFinite(reserve) || reserve < 37 || reserve > 130) throw new Error("--letterhead-reserve-mm must be between 37 and 130"); + body.push(paragraph("", { indent: false, before: String(Math.round((reserve - 37) * 56.7)), after: "0", fontPreset: "body" })); + } + if (args["doc-no"]) body.push(documentNumberParagraph(args["doc-no"], upward)); + if (upward && args.signer) { + body.push(headerFieldParagraph(`签发人:${args.signer}`, "h2", { align: "right", right: FIRST_LINE_INDENT })); + } + if (letterhead === "digital" || args["preprinted-rule"] === "true") body.push(redRule()); + } else if (format === "letter") { + if (!args.org) throw new Error("letter format requires --org"); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "4252", after: "227" })); + body.push(redRule()); + if (args["doc-no"]) body.push(headerFieldParagraph(args["doc-no"], "body", { align: "right" })); + } else if (format === "command") { + if (!args.org) throw new Error("command format requires --org"); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "3402", after: "560" })); + if (args["doc-no"]) body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "560" })); + } else if (format === "minutes") { + if (!args.org) throw new Error("minutes format requires --org (for XXXXX纪要)"); + body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "5953", after: "560" })); + } if (title) body.push(titleParagraph(title)); if (args.subtitle) body.push(subtitleParagraph(args.subtitle)); if (mainSend) body.push(mainSendParagraph(mainSend)); @@ -413,22 +487,39 @@ function buildDocument(blocks, args) { } body.push(renderParagraph(block)); } + if (args["attachment-note"]) body.push(attachmentNote(args["attachment-note"])); if (args.sender || args.date) { body.push(emptyLine()); - if (args.sender) body.push(right(args.sender)); - if (args.date) body.push(right(args.date)); + if (args["seal-mode"] === "signed") { + if (!args.signer || !args["signer-title"]) throw new Error("signed seal mode requires --signer and --signer-title"); + body.push(right(`${args["signer-title"]} ${args.signer}`)); + if (args.date) body.push(right(args.date)); + } else if (args["seal-mode"] === "seal") { + if (args.sender) body.push(right(args.sender)); + if (args.date) body.push(right(args.date)); + } else { + if (args.sender) body.push(right(args.sender)); + if (args.date) body.push(right(args.date)); + } } + if (args.note) body.push(paragraph(`(${args.note.replace(/^(|)$/g, "")})`, { align: "left", fontPreset: "body", size: "32" })); + if (format === "minutes" && args.attendees) body.push(paragraph(`出席:${args.attendees}`, { align: "left", fontPreset: "body", size: "32", before: "560" })); + if (format === "minutes" && args.absent) body.push(paragraph(`请假:${args.absent}`, { align: "left", fontPreset: "body", size: "32" })); + if (format === "minutes" && args.observers) body.push(paragraph(`列席:${args.observers}`, { align: "left", fontPreset: "body", size: "32" })); + if (format !== "letter") body.push(...colophon(args)); const footerRefs = pageNumberMode === "standard" ? '' : pageNumberMode === "center" ? '' : ""; body.push(`${footerRefs}`); - return `${body.join("")}`; + return `${body.join("")}`; } function stylesXml() { - return ``; + const style = (id, name, preset, size, outline, before = "0") => ``; + const title = ``; + return `${title}${style("Heading1", "标题 1", "h1", "32", 0, "160")}${style("Heading2", "标题 2", "h2", "32", 1, "80")}${style("Heading3", "标题 3", "body", "32", 2, "40")}${style("Heading4", "标题 4", "body", "32", 3)}`; } function fontDef(name, altName, family = "roman", charset = "86") { diff --git a/scripts/install.sh b/scripts/install.sh index 0c6b7c5..eea20ea 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -76,6 +76,7 @@ package_traework() { output="$skill_dir/dist/traework-gongwen-skill.zip" mkdir -p "$skill_dir/dist" "$stage/gongwen" cp -R "$skill_dir/SKILL.md" "$skill_dir/scripts" "$skill_dir/references" "$stage/gongwen/" + rm -f "$output" (cd "$stage" && zip -qr "$output" gongwen) unzip -t "$output" >/dev/null rm -rf "$stage" diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs index 2680a5b..23a5b01 100755 --- a/scripts/verify_gongwen_docx.mjs +++ b/scripts/verify_gongwen_docx.mjs @@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs"; function usage() { - console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile layout|standard-pages]"); + console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital]"); } function parseArgs(argv) { @@ -18,74 +18,62 @@ function parseArgs(argv) { } function readPart(file, part, optional = false) { - try { - return execFileSync("unzip", ["-p", file, part], { encoding: "utf8" }); - } catch (error) { - if (optional) return ""; - throw error; - } + try { return execFileSync("unzip", ["-p", file, part], { encoding: "utf8" }); } + catch (error) { if (optional) return ""; throw error; } } -function paragraphsForText(xml, pattern) { - const paragraphs = xml.match(/]*)?>[\s\S]*?<\/w:p>/g) ?? []; - return paragraphs.filter((paragraph) => pattern.test(paragraph.replace(/<[^>]+>/g, ""))); -} +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); -} +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); } -const profile = args.profile ?? "layout"; -if (!["layout", "standard-pages"].includes(profile)) { - console.error("Unsupported profile. Use layout or standard-pages."); - process.exit(2); -} const doc = readPart(args.input, "word/document.xml"); const styles = readPart(args.input, "word/styles.xml"); -const h1 = paragraphsForText(doc, /^[一二三四五六七八九十]+、/); -const h2 = paragraphsForText(doc, /^([一二三四五六七八九十]+)/); -const h3 = paragraphsForText(doc, /(?:^|\s)\d+[..]/); -const h4 = paragraphsForText(doc, /(\d+)/); +const footerOdd = readPart(args.input, "word/footerOdd.xml", true); +const footerEven = readPart(args.input, "word/footerEven.xml", true); +const footerCenter = readPart(args.input, "word/footerCenter.xml", true); const checks = []; - -function check(name, ok, detail) { - checks.push({ name, ok, detail }); -} +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"); check("Page margins", /]*w:top="2098"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"/.test(doc), "top 37, bottom 35, left 28, right 26 mm"); -check("Document grid", /]*w:linePitch="560"/.test(doc), "28 pt grid"); -const fangSong = /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)"/; -const xiaoBiaoSong = /w:eastAsia="(?:方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong-B05S)"/; -const heiTi = /w:eastAsia="(?:黑体|SimHei|Heiti SC|STHeiti)"/; -const kaiTi = /w:eastAsia="(?:楷体|楷体_GB2312|KaiTi|KaiTi_GB2312|Kaiti SC|STKaiti)"/; -check("Default body font", fangSong.test(styles) && / paragraphs.length > 0 && paragraphs.every(matcher); -check("Title font", xiaoBiaoSong.test(doc) && / /w:firstLine="640"/.test(paragraph) && heiTi.test(paragraph) && /w:sz w:val="32"/.test(paragraph)), "every 一、 heading is Heiti-compatible 16 pt with two-character indent"); -check("Level-2 headings", allMatch(h2, (paragraph) => /w:firstLine="640"/.test(paragraph) && kaiTi.test(paragraph) && /w:sz w:val="32"/.test(paragraph)), "every (一) heading is Kaiti-compatible 16 pt with two-character indent"); -check("Level-3 headings", allMatch(h3, (paragraph) => /w:firstLine="640"/.test(paragraph) && fangSong.test(paragraph) && /w:sz w:val="32"/.test(paragraph)), "every 1. heading is FangSong-compatible 16 pt with two-character indent"); -check("Level-4 headings", allMatch(h4, (paragraph) => /w:firstLine="640"/.test(paragraph) && fangSong.test(paragraph) && /w:sz w:val="32"/.test(paragraph)), "every (1) heading is FangSong-compatible 16 pt with two-character indent"); -if (profile === "standard-pages") { - const oddFooter = readPart(args.input, "word/footerOdd.xml", true); - const evenFooter = readPart(args.input, "word/footerEven.xml", true); - check("Odd-page footer", /w:jc w:val="right"/.test(oddFooter) && /w:instr="PAGE"/.test(oddFooter) && /w:ascii="(?:宋体|SimSun)"/.test(oddFooter), "right-aligned page number in Songti-compatible font"); - check("Even-page footer", /w:jc w:val="left"/.test(evenFooter) && /w:instr="PAGE"/.test(evenFooter) && /w:ascii="(?:宋体|SimSun)"/.test(evenFooter), "left-aligned page number in Songti-compatible font"); +check("Document grid", /]*w:linePitch="560"/.test(doc), "28 pt implementation grid"); +check("Default body font", /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)"/.test(styles) && / new RegExp(`w:styleId="Heading${level}"`).test(styles)), "Heading 1 through Heading 4 are defined"); + +const textOf = (xml) => xml.replace(/<[^>]+>/g, ""); +const headingRules = [ + [/^[一二三四五六七八九十百]+、/, "Heading1", /(?:黑体|SimHei|Heiti SC|STHeiti)/], + [/^([一二三四五六七八九十百]+)/, "Heading2", /(?:楷体|楷体_GB2312|KaiTi|KaiTi_GB2312|Kaiti SC|STKaiti)/], + [/^\d+[..]/, "Heading3", /(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)/], + [/^(\d+)/, "Heading4", /(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312|STFangsong)/], +]; +for (const [pattern, style, font] of headingRules) { + const matches = paragraphs(doc).filter((p) => pattern.test(textOf(p).trim())); + check(`${style} applied when present`, matches.every((p) => new RegExp(`w:pStyle w:val="${style}"`).test(p) && /w:firstLine="640"/.test(p) && font.test(p)), matches.length ? `${matches.length} matching paragraph(s)` : "not present in this document"); +} + +if (profile === "ordinary") { + check("No formal red-head drawing", !/w:color w:val="FF0000"/.test(doc), "ordinary output has no red head or red rule"); + check("Centered layout footer", /w:jc w:val="center"/.test(footerCenter) && /w:instr="PAGE"/.test(footerCenter), "ordinary output uses centered page number"); +} else if (profile === "letter") { + check("Letter first-page page number", !footerCenter && !footerOdd && !footerEven, "letter test output suppresses first-page page number"); } else { - const footer = readPart(args.input, "word/footerCenter.xml", true); - check("Layout page footer", /w:jc w:val="center"/.test(footer) && /w:instr="PAGE"/.test(footer) && /w:ascii="(?:宋体|SimSun)"/.test(footer), "centered page number in Songti-compatible font"); -} - -for (const item of checks) { - console.log(`${item.ok ? "PASS" : "FAIL"} ${item.name}: ${item.detail}`); + check("Odd/even page fields", /w:instr="PAGE"/.test(footerOdd) && /w:instr="PAGE"/.test(footerEven), "odd and even page footers exist"); + check("Odd/even page positions", /w:jc w:val="right"/.test(footerOdd) && /w:jc w:val="left"/.test(footerEven), "odd right and even left"); +} +if (profile === "formal") { + if (args.letterhead === "digital") { + check("Digital red head", /w:color w:val="FF0000"/.test(doc), "digital formal output contains requested red elements"); + } else { + check("Preprinted letterhead reserve", /w:before="\d{4,}"/.test(doc), "first-page top reserve is present"); + check("No red drawing for preprinted letterhead", !/w:color w:val="FF0000"/.test(doc), "preprinted mode does not redraw red letterhead or rule"); + } } +for (const item of checks) console.log(`${item.ok ? "PASS" : "FAIL"} ${item.name}: ${item.detail}`); const failed = checks.filter((item) => !item.ok); -if (failed.length) { - console.error(`Verification failed: ${failed.length} item(s).`); - process.exit(1); -} - +if (failed.length) { console.error(`Verification failed: ${failed.length} item(s).`); process.exit(1); } console.log(`Verification passed (${profile} profile): ${checks.length} item(s).`); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index e5b3657..8d71c9e 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -13,15 +13,23 @@ FIXTURE="$SKILL_DIR/tests/fixture.md" "$NODE" --check "$GEN" "$NODE" --check "$VERIFY" -"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/layout.docx" --title "公文格式回归测试" --page-number center -"$NODE" "$VERIFY" --input "$TMP_DIR/layout.docx" --profile layout +"$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 unzip -t "$TMP_DIR/layout.docx" >/dev/null +! unzip -p "$TMP_DIR/layout.docx" word/document.xml | grep -q 'FF0000' +unzip -p "$TMP_DIR/layout.docx" word/styles.xml | grep -q 'styleId="Heading4"' +unzip -p "$TMP_DIR/layout.docx" word/document.xml | grep -q 'w:pStyle w:val="Heading4"' -"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/standard.docx" --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试" --page-number standard -"$NODE" "$VERIFY" --input "$TMP_DIR/standard.docx" --profile standard-pages -unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例单位文件' +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/standard.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "公文格式回归测试" --page-number standard +"$NODE" "$VERIFY" --input "$TMP_DIR/standard.docx" --profile formal --letterhead preprinted +! unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例单位文件' unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例发〔2026〕1号' +"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/digital.docx" --format formal --letterhead digital --org "示例单位文件" --doc-no "示例发〔2026〕1号" --title "电子红头测试" --page-number standard +"$NODE" "$VERIFY" --input "$TMP_DIR/digital.docx" --profile formal --letterhead digital +unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q '示例单位文件' +unzip -p "$TMP_DIR/digital.docx" word/document.xml | grep -q 'FF0000' + if command -v soffice >/dev/null 2>&1; then mkdir -p "$TMP_DIR/pdf" soffice --headless --convert-to pdf --outdir "$TMP_DIR/pdf" "$TMP_DIR/layout.docx" >/dev/null 2>&1