From 2dd6729f0779b47c3d9c61ff24024ce8e865dc93 Mon Sep 17 00:00:00 2001 From: mizzlelover Date: Wed, 9 Sep 2026 20:19:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=20GB/T=209704-2012=20?= =?UTF-8?q?=E5=85=AC=E6=96=87=E6=8E=92=E7=89=88=E6=8A=80=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 ++ README.md | 68 ++++ SKILL.md | 45 +++ dist/traework-gongwen-skill.zip | Bin 0 -> 20023 bytes docs/platform-support.md | 47 +++ references/formal-checklist.md | 40 +++ references/gbt9704-2012-summary.md | 160 +++++++++ scripts/generate_gongwen_docx.mjs | 534 +++++++++++++++++++++++++++++ scripts/install.ps1 | 41 +++ scripts/install.sh | 91 +++++ scripts/verify_gongwen_docx.mjs | 91 +++++ tests/fixture.md | 17 + tests/run_tests.sh | 31 ++ tests/test-install.sh | 26 ++ 14 files changed, 1212 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SKILL.md create mode 100644 dist/traework-gongwen-skill.zip create mode 100644 docs/platform-support.md create mode 100644 references/formal-checklist.md create mode 100644 references/gbt9704-2012-summary.md create mode 100755 scripts/generate_gongwen_docx.mjs create mode 100644 scripts/install.ps1 create mode 100755 scripts/install.sh create mode 100755 scripts/verify_gongwen_docx.mjs create mode 100644 tests/fixture.md create mode 100755 tests/run_tests.sh create mode 100755 tests/test-install.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..591d414 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mizzlelover + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3ccf87 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Gongwen GB/T 9704-2012 Skill + +一个面向中文正式材料的 Skill:生成参照 GB/T 9704-2012 的公文版式 DOCX,并提供可选的格式核验清单。 + +## 为什么需要它 + +许多 AI 只会把标题设为黑体、正文设为仿宋,便把输出称为“公文”。本项目把 A4、版心、正文、层次、首页和页码等版式参数做成可生成、可检查的 DOCX 输出,并接受用户提供的机构名称、文号、署名、日期和页码模式。 + +## 能力与边界 + +| 场景 | 本项目的处理方式 | +| --- | --- | +| 文档主体 | A4、版心、标题、正文、层次、署名和日期排版 | +| 机构名称与文号 | 使用 `--org`、`--doc-no` 按输入排版 | +| 页码 | 居中、单双页或不显示 | +| 印章、签名章 | 在生成的 DOCX 中按实际需要插入图片或完成实体盖章 | +| 特定格式 | 可在此版式基础上继续编辑,或使用对应模板 | + +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 +``` + +然后用 Word 或 WPS 打开,并转换 PDF 检查标题、正文、表格、分页和页码。生成器支持 `center`、`standard` 和 `none` 页码模式,机构名称和文号按输入写入文档。 + +## 跨平台安装 + +本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、WorkBuddy 和 ZCode 使用。 + +macOS / Linux: + +```bash +scripts/install.sh --all +``` + +Windows: + +```powershell +.\scripts\install.ps1 -All +``` + +安装器将同一份源目录链接到各平台的用户级技能目录;Windows 默认复制以避免符号链接权限问题。TraeWork 使用可导入包 [`dist/traework-gongwen-skill.zip`](dist/traework-gongwen-skill.zip)。完整目录、验证方法和官方依据见[跨平台安装说明](docs/platform-support.md)。 + +## 项目内容 + +- [SKILL.md](SKILL.md):使用指引与版式参数。 +- [标准执行摘要](references/gbt9704-2012-summary.md):国家标准的可执行要点。 +- [正式公文核验表](references/formal-checklist.md):模板完成后的全要素检查。 +- [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。 +- [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。 +- [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。 +- [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。 + +## 验证 + +```bash +tests/run_tests.sh +``` + +这项测试证明生成器、校验器和可打印文件链路可运行。最终文件可按实际用途在目标 Word/WPS 与打印条件中检查。 + +## 许可 + +MIT。标准文本及机关模板应按各自权利和管理规则取得与使用。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..1d41784 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,45 @@ +--- +name: gongwen +description: 将中文正式材料生成参照 GB/T 9704-2012 的公文版式 DOCX,支持机构名称、文号、正文、署名、日期和居中或单双页页码,并提供可选格式检查。 +--- + +# 公文版式工具 + +## 标准依据与边界 + +GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有效。其适用于党政机关制发公文;其他机关、单位可以参照执行。 + +先读 `references/gbt9704-2012-summary.md`;需要逐项复核时再读 `references/formal-checklist.md`。标准规定纸张、版面、要素编排和印制装订要求。 + +## 工具定位 + +本工具只处理排版。它不判断用户材料的用途、单位身份、授权状态或后续盖章方式,也不推断用户未提供的要素。机构名称、文号、主送机关、署名、日期和页码模式均按用户输入排入 DOCX。 + +生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。 + +`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 +``` + +支持 `center`、`standard` 与 `none` 三种页码模式,默认 `center`。`standard` 使用单页右、双页左的页码位置。`--org` 以红色居中机构名称排版,`--doc-no` 在其下方排版。 + +Markdown 标题优先按序数识别:`一、`、`(一)`、`1.`、`(1)`;序数缺失时才按 Markdown 层级映射。正文、四级标题均左空二字,回行顶格。标题字体在运行机器缺失时,会保留请求的字体名;打开文件时仍须确认 Office 的实际替代字体,不把普通宋体视作小标宋的等价替代。 + +## 验证 + +```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 +``` + +校验器核验生成器可检查的版式要素。`layout` 用于居中页码,`standard-pages` 用于单双页页码。输出后可在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码和装订。 + +## 输出说明 + +- 版式检查通过后,可写“已按 GB/T 9704-2012 的相关版式参数生成”。 +- 使用者可按实际用途决定是否采用 `formal-checklist.md` 逐项复核。 diff --git a/dist/traework-gongwen-skill.zip b/dist/traework-gongwen-skill.zip new file mode 100644 index 0000000000000000000000000000000000000000..3d48a1da9f14bfec887089fa893148b93f198f74 GIT binary patch literal 20023 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 literal 0 HcmV?d00001 diff --git a/docs/platform-support.md b/docs/platform-support.md new file mode 100644 index 0000000..bf888d8 --- /dev/null +++ b/docs/platform-support.md @@ -0,0 +1,47 @@ +# 平台安装与验证 + +本项目采用 Agent Skills 通用目录形态:一个以 `SKILL.md` 为入口的目录,附带 `scripts/` 和 `references/`。所有目标平台安装的是同一份源目录,不维护改写版提示词。 + +| 平台 | 用户级位置或导入方式 | 验证方式 | +| --- | --- | --- | +| Codex | `~/.codex/skills/gongwen` | 新会话中确认 `gongwen` 出现在技能列表 | +| Claude Code | `~/.claude/skills/gongwen` | 输入 `/gongwen` 或在任务中显式提及技能 | +| OpenCode | `~/.config/opencode/skills/gongwen` | 在支持技能的 Agent 中确认 `gongwen` 可加载 | +| Trae Code | `~/.trae-cn/skills/gongwen` | 设置中的“技能与命令”面板刷新后确认 | +| Trae CLI | `~/.traecli/skills/gongwen` | 重启后执行 `/skills` | +| Kimi Code CLI | `~/.kimi/skills/gongwen` | 新会话中执行 `/skill:gongwen` | +| Kimi Code | `~/.kimi-code/skills/gongwen` | 新会话中执行 `/skill:gongwen` | +| WorkBuddy | `~/.workbuddy/skills/gongwen` | 在技能管理界面刷新并启用 | +| ZCode | `~/.zcode/skills/gongwen` | 设置 → Skills 刷新并确认启用 | +| TraeWork | 导入 `dist/traework-gongwen-skill.zip` | 导入后在企业技能列表确认启用 | + +## macOS 与 Linux + +```bash +git clone https://github.com/mizzlelover/gongwen-gbt9704-skill.git +cd gongwen-gbt9704-skill +scripts/install.sh --all +``` + +安装器默认创建符号链接,因此 GitHub 拉取更新后,所有已安装平台都会使用同一份最新技能。若目标已有非链接的 `gongwen`,脚本会拒绝覆盖;核对后才可传入 `--force`。 + +## Windows + +```powershell +git clone https://github.com/mizzlelover/gongwen-gbt9704-skill.git +cd gongwen-gbt9704-skill +.\scripts\install.ps1 -All +``` + +PowerShell 默认复制目录,避免依赖管理员权限或开发者模式。需要实时同步时,使用 `-Link`;已有同名目录需明确传入 `-Force`。 + +## 官方目录依据 + +- [Claude Code Skills](https://docs.anthropic.com/en/docs/claude-code/skills) +- [OpenCode Agent Skills](https://opencode.ai/docs/skills) +- [Trae Code 技能](https://docs.trae.cn/ide_skills) 与 [Trae CLI 技能](https://docs.trae.cn/cli_skills) +- [Kimi Code CLI Agent Skills](https://moonshotai.github.io/kimi-cli/en/customization/skills) +- [ZCode Skills](https://zcode.z.ai/en/docs/skill) +- [TraeWork 企业技能](https://docs.trae.cn/enterprise_skills) + +WorkBuddy 公开文档将 `skills//SKILL.md` 作为技能目录结构;本项目同时保留用户级目录安装,便于本地技能管理器发现。 diff --git a/references/formal-checklist.md b/references/formal-checklist.md new file mode 100644 index 0000000..84ae5fe --- /dev/null +++ b/references/formal-checklist.md @@ -0,0 +1,40 @@ +# 正式党政机关公文逐项核验表 + +本表按 GB/T 9704-2012 编制,用于机关权威模板完成后的人机协同核验。每项记录“适用/不适用、证据位置、核验人、日期”;不适用应说明原因。它不替代发文机关公文处理制度。 + +## 1. 前提与制作 + +- [ ] 已确认发文机关、文种、行文关系、主送范围、发文字号、签发权限及机关现行模板。 +- [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。 +- [ ] 无特殊说明时采用三号仿宋;一般每面22行、每行28字并撑满版心。不得把固定28pt当作国标唯一强制值。 +- [ ] 文本颜色为黑色;需红色的发文机关标志、分隔线、印章或签名章已按规则处理。 +- [ ] 制作环节已确认双面印刷、左侧装订、页码套正、钉位及成品质量要求;这些项目不能从 DOCX 自动推定。 + +## 2. 版头 + +- [ ] 份号适用时为6位三号阿拉伯数字,左上角第一行顶格。 +- [ ] 密级和保密期限、紧急程度适用时均为三号黑体,按份号、密级和期限、紧急程度顺序自上而下排列。 +- [ ] 发文机关标志使用全称或规范简称,可加“文件”;居中、红色、推荐小标宋,上边缘距版心上边缘35mm。 +- [ ] 发文字号位于机关标志下空二行;年份为全称阿拉伯数字并使用六角括号〔〕,序号不加“第”、不补零、后加“号”。 +- [ ] 上行文文号左空一字;签发人同区右空一字。“签发人:”为仿宋三号,姓名为楷体三号;多签发人按机关顺序编排。 +- [ ] 红色分隔线在文号下4mm处,与版心等宽。 + +## 3. 主体 + +- [ ] 标题为二号小标宋,红线下空二行居中;多行标题词意完整、排列对称,呈梯形或菱形。 +- [ ] 主送机关在标题下空一行顶格,回行顶格,最后名称后用全角冒号;过多而影响首页正文时移入版记。 +- [ ] 首页显示正文;正文一般三号仿宋,每个自然段左空二字、回行顶格。 +- [ ] 层次序数按“一、”“(一)”“1.”“(1)”使用;第一层黑体、第二层楷体、第三和第四层仿宋,均为三号。 +- [ ] 附件说明位于正文下空一行、左空二字;“附件:”后名称无标点;多附件编号正确,长名称回行与首字对齐。 +- [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。 +- [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。 +- [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;与正文一起装订的附件页码连续。 + +## 4. 版记、页码与特殊格式 + +- [ ] 版记首末分隔线为粗线、中间线为细线;末条线与最后一面版心下边缘重合。 +- [ ] 抄送机关适用时为四号仿宋、左右各空一字、回行与冒号后首字对齐、末尾句号;主送移入版记时按“主送”规则处理。 +- [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。 +- [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。 +- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代。 +- [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。 diff --git a/references/gbt9704-2012-summary.md b/references/gbt9704-2012-summary.md new file mode 100644 index 0000000..37782a5 --- /dev/null +++ b/references/gbt9704-2012-summary.md @@ -0,0 +1,160 @@ +# GB/T 9704-2012《党政机关公文格式》执行摘要 + +## 一、依据与状态 + +- 标准号:GB/T 9704-2012。 +- 标准名称:党政机关公文格式。 +- 发布日期:2012年6月29日。 +- 实施日期:2012年7月1日。 +- 当前状态:现行;全国标准信息公共服务平台显示2025年5月30日复审结论为继续有效。 +- 官方信息页:https://openstd.samr.gov.cn/bzgk/std/newGbInfo?hcno=F3CC9BEF482524C895FDA7A08BB4A70E +- 标准状态页: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纸、标准版心、字号、行距、标题层级和页码,可按输入加入机构名称、文号、主送机关、署名和日期。 + +### 2. 正式党政机关公文 + +除页面和正文格式外,还可按实际需要处理版头、主体、版记、页码、印章或签名章。生成器排版用户提供的文字字段;印章和签名章可在生成后的 Word/WPS 文件中按需插入。 + +## 三、纸张、版面和印制 + +| 项目 | 标准口径 | 生成参数 | +|---|---|---| +| 纸张 | A4,210mm×297mm | 11906×16838 DXA | +| 天头 | 37mm±1mm | 上边距2098 DXA | +| 订口 | 28mm±1mm | 左边距1588 DXA | +| 版心 | 156mm×225mm | 由页边距共同形成 | +| 推导白边 | 右约26mm、下约35mm | 右1474、下1984 DXA | +| 字体 | 无特殊说明一般3号仿宋 | 16pt,`w:sz=32` | +| 行数和字数 | 一般每面22行、每行28字,特定情况可调整 | 标准要求是版面结果;28pt固定行距是常用实现值,不是国标唯一强制参数 | +| 文字颜色 | 无特殊说明为黑色 | `000000` | +| 印刷 | 正式公文双面印刷,页码套正 | 属打印环节,DOCX无法自动保证 | +| 装订 | 左侧装订,正式公文执行钉位等要求 | 属制作环节,需人工检查 | + +## 四、正文主体格式 + +### 1. 标题 + +- 一般用2号小标宋体,位于红色分隔线下空二行,居中。 +- 多行标题应词意完整、排列对称,宜呈梯形或菱形。 +- 公文版式打印稿无红色分隔线时,标题仍用小标宋二号居中,但应根据篇幅控制标题后间距。 + +### 2. 主送机关 + +- 位于标题下空一行,左顶格;回行仍顶格;末尾用全角冒号。 +- 主送机关过多导致首页不能显示正文时,移至版记。 +- 公文首页必须显示正文。 + +### 3. 正文 + +- 一般用3号仿宋。 +- 每个自然段左空二字,回行顶格。 +- 层次序数依次使用“一、”“(一)”“1.”“(1)”。 + +| 层级 | 序数示例 | 字体 | 字号 | 段落 | +|---|---|---|---|---| +| 第一层 | 一、 | 黑体 | 三号 | 左空二字,回行顶格 | +| 第二层 | (一) | 楷体 | 三号 | 左空二字,回行顶格 | +| 第三层 | 1. | 仿宋 | 三号 | 左空二字,回行顶格 | +| 第四层 | (1) | 仿宋 | 三号 | 左空二字,回行顶格 | + +字体名称已经体现字重,不额外对小标宋、黑体、楷体设置Word“加粗”。 + +### 4. 附件说明 + +- 正文下空一行,左空二字标注“附件:”。 +- 多个附件用阿拉伯数字排序。 +- 附件名称后不加标点;长名称回行与名称首字对齐。 + +### 5. 发文机关署名、日期和印章 + +- 加盖印章:成文日期一般右空四字;印章应为红色并下压机关署名和日期。 +- 不加印章:正文或附件说明下空一行,机关署名右空二字,下一行编排日期。 +- 成文日期使用阿拉伯数字完整标注年月日,月、日不补零。 +- 印章和签名章是独立图片或实体印章对象;本生成器不内置其图像,可在生成后的 Word/WPS 文件中按需插入。 + +### 6. 附注与附件正文 + +- 附注位于成文日期下一行,左空二字,加圆括号。 +- 附件另面编排、置于版记之前。“附件”及顺序号用3号黑体顶格置于版心左上角第一行,附件标题在版心第三行居中。 +- 附件与正文一起装订时页码连续。 + +## 五、版头要素 + +正式红头公文按需包含:份号、密级和保密期限、紧急程度、发文机关标志、发文字号、签发人和红色分隔线。 + +| 要素 | 核心要求 | +|---|---| +| 份号 | 6位3号阿拉伯数字,版心左上角第一行顶格 | +| 密级和期限 | 3号黑体,按规则置于左上区域 | +| 紧急程度 | 3号黑体,与份号、密级按顺序排列 | +| 发文机关标志 | 机关全称或规范简称,可加“文件”;红色小标宋,居中 | +| 发文字号 | 年份全称、六角括号、序号不加“第”且不补零 | +| 上行文签发人 | “签发人:”仿宋三号,姓名楷体三号;与文号同区编排 | +| 分隔线 | 文号下4mm,红色,与版心等宽 | + +版头位置和红色要素可按用户提供的字段和模板进一步调整。 + +## 六、版记 + +- 版记位于公文末页,包括分隔线、抄送机关、印发机关和印发日期等。 +- 抄送机关一般用4号仿宋,左右各空一字;回行与冒号后首字对齐,末尾用句号。 +- 印发机关左空一字,印发日期右空一字,日期不补零,后加“印发”。 +- 版记分隔线有粗细区分,并与版心等宽。 +- 版记字段可按实际需要在生成后的文件中补充或调整。 + +## 七、页码 + +### 1. 正式公文标准页码 + +- 4号半角宋体阿拉伯数字。 +- 数字左右各放一条一字线。 +- 一字线上距版心下边缘7mm。 +- 单页码居右空一字,双页码居左空一字。 +- 空白页和特定版记页按标准处理;正文与附件一起装订时连续编排。 + +单双页页码与居中页码分别对应不同版式需求,生成时可显式选择。 + +### 2. 公文版式打印稿页码 + +方案、报告等材料常采用页脚居中“— 1 —”。使用 `standard` 模式时,单页右、双页左。 + +## 八、横排表格和特定格式 + +- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。 +- 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。 +- 信函首页不显示页码,版记规则也不同。 +- 特定格式必须使用对应模板或专用生成逻辑,不得以普通报告模板代替。 + +## 九、技能能力边界 + +| 能力 | 自动生成 | 必须人工或模板处理 | +|---|---|---| +| A4、版心、字体、行距、正文缩进 | 是 | 打印设备误差复核 | +| 四级正文标题 | 是 | 特殊机关模板调整 | +| 报告居中页码 | 是 | 无 | +| 正式单双页页码 | 是 | `--page-number standard` | +| 普通表格 | 是 | 横排表格方向和复杂跨页表格复核 | +| 红色机构名称、文号 | 是 | `--org`、`--doc-no` | +| 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 | +| 版记 | 可编辑 | 按实际字段补充 | +| 信函、命令、纪要 | 可编辑 | 按实际版式继续调整 | + +## 十、建议检查 + +1. 核对文种、行文关系、主送、文号、签发人、印章和版记等需要使用的字段。 +3. 检查A4、版心、正文字体字号、22行28字的版面结果及四级标题映射;固定28pt仅作实现参数复核。 +4. 检查每个自然段及各级标题左空二字、回行顶格。 +5. 检查页码模式:打印稿居中;正式公文单页右、双页左。 +6. 检查首页是否显示正文,标题和主送机关是否留距正确。 +7. 检查附件说明、附件另页、附件编号和连续页码。 +8. 检查表格是否超版心,跨页是否断裂,横排方向是否正确。 +9. 执行DOCX结构验证、PDF转换和逐页可视检查。 +10. 需要逐项复核时,使用 `formal-checklist.md` 记录结果。 diff --git a/scripts/generate_gongwen_docx.mjs b/scripts/generate_gongwen_docx.mjs new file mode 100755 index 0000000..ec6ea83 --- /dev/null +++ b/scripts/generate_gongwen_docx.mjs @@ -0,0 +1,534 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { execFileSync } from "node:child_process"; + +const PAGE_W = 11906; +const PAGE_H = 16838; +const MARGIN = { top: 2098, bottom: 1984, left: 1588, right: 1474 }; +const CONTENT_W = PAGE_W - MARGIN.left - MARGIN.right; +const FIRST_LINE_INDENT = 640; + +function installedFontFamilies() { + try { + return new Set( + execFileSync("fc-list", ["-f", "%{family}\n"], { encoding: "utf8" }) + .split(/\r?\n/) + .flatMap((line) => line.split(",")) + .map((name) => name.trim()) + .filter(Boolean), + ); + } catch { + return new Set(); + } +} + +const INSTALLED_FONTS = installedFontFamilies(); +function preferredFont(candidates) { + return candidates.find((font) => INSTALLED_FONTS.has(font)) ?? candidates[0]; +} + +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" }, + 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: "宋体" }, +}; + +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] + +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. + - --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.`); +} + +function parseArgs(argv) { + const args = {}; + const flags = new Set(["help", "no-page-number", "no-page-numbers"]); + for (let i = 2; i < argv.length; i += 1) { + const key = argv[i]; + if (!key.startsWith("--")) continue; + const name = key.slice(2); + if (flags.has(name)) { + args[name] = true; + continue; + } + args[name] = argv[i + 1]; + i += 1; + } + return args; +} + +function esc(value) { + return String(value ?? "") + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/`([^`]+)`/g, "$1") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function attr(value) { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +function fontAttrs(opts = {}) { + const preset = opts.fontPreset ? FONT[opts.fontPreset] : null; + const eastAsia = opts.eastAsia ?? opts.font ?? preset?.eastAsia ?? FONT.body.eastAsia; + const ascii = opts.ascii ?? preset?.ascii ?? FONT.body.ascii; + const hAnsi = opts.hAnsi ?? ascii; + const cs = opts.cs ?? preset?.cs ?? ascii; + return `w:ascii="${attr(ascii)}" w:hAnsi="${attr(hAnsi)}" w:eastAsia="${attr(eastAsia)}" w:cs="${attr(cs)}" w:hint="eastAsia"`; +} + +function run(text, opts = {}) { + const size = opts.size ?? "32"; + const bold = opts.bold ? "" : ""; + const color = opts.color ?? "000000"; + return `${bold}${esc(text)}`; +} + +function paragraph(text, opts = {}) { + const alignValue = opts.align === undefined ? "both" : opts.align; + const align = alignValue ? `` : ""; + const indAttrs = []; + if (opts.indent !== false) indAttrs.push(`w:firstLine="${opts.firstLine ?? FIRST_LINE_INDENT}"`); + if (opts.left) indAttrs.push(`w:left="${opts.left}"`); + if (opts.right) indAttrs.push(`w:right="${opts.right}"`); + const indent = indAttrs.length ? `` : ""; + const keepNext = opts.keepNext ? "" : ""; + const before = opts.before ?? "0"; + const after = opts.after ?? "0"; + const pPr = `${keepNext}${align}${indent}`; + return `${pPr}${run(text, opts)}`; +} + +function titleParagraph(text) { + return paragraph(text, { + align: "center", + indent: false, + fontPreset: "title", + size: "44", + before: "0", + after: "560", + }); +} + +function agencyMarkParagraph(text) { + return paragraph(text, { + align: "center", + indent: false, + fontPreset: "title", + size: "56", + color: "FF0000", + after: "280", + }); +} + +function documentNumberParagraph(text) { + return paragraph(text, { + align: "left", + indent: false, + fontPreset: "body", + size: "32", + after: "160", + }); +} + +function subtitleParagraph(text) { + return paragraph(text, { + align: "center", + indent: false, + fontPreset: "subtitle", + size: "32", + after: "280", + }); +} + +function mainSendParagraph(text) { + return paragraph(text, { + align: "left", + indent: false, + fontPreset: "body", + size: "32", + }); +} + +function h1(text) { + return paragraph(text, { + align: "left", + fontPreset: "h1", + size: "32", + keepNext: true, + before: "160", + after: "0", + }); +} + +function h2(text) { + return paragraph(text, { + align: "left", + fontPreset: "h2", + size: "32", + keepNext: true, + before: "80", + after: "0", + }); +} + +function h3(text) { + return paragraph(text, { + align: "left", + fontPreset: "body", + size: "32", + keepNext: true, + before: "40", + after: "0", + }); +} + +function h4(text) { + return paragraph(text, { + align: "left", + fontPreset: "body", + size: "32", + keepNext: true, + before: "0", + after: "0", + }); +} + +function right(text) { + return paragraph(text, { align: "right", indent: false, right: FIRST_LINE_INDENT }); +} + +function emptyLine() { + return ``; +} + +function tableXml(headers, rows) { + const colCount = headers.length; + const colWidth = Math.floor(CONTENT_W / colCount); + const grid = Array.from({ length: colCount }, () => ``).join(""); + const tr = (cells, header = false) => { + const trPr = header ? "" : ""; + return `${trPr}${cells.map((cell) => tableCell(cell, colWidth, header)).join("")}`; + }; + return `${grid}${tr(headers, true)}${rows.map((row) => tr(row)).join("")}`; +} + +function tableCell(text, width, header) { + const shade = header ? '' : ""; + const props = `${shade}`; + const pPr = ''; + const r = run( + text, + header + ? { fontPreset: "h1", size: "32" } + : { size: "32" }, + ); + return `${props}${pPr}${r}`; +} + +function stripFrontmatter(md) { + return md.replace(/^---\n[\s\S]*?\n---\n?/, ""); +} + +function parseMarkdown(md) { + const lines = stripFrontmatter(md).split(/\r?\n/); + const blocks = []; + let para = []; + let i = 0; + const flush = () => { + const text = para.join("").trim(); + if (text) blocks.push({ type: "paragraph", text }); + para = []; + }; + + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + if (!trimmed) { + flush(); + i += 1; + continue; + } + if (/^\|.+\|$/.test(trimmed)) { + flush(); + const tableLines = []; + while (i < lines.length && /^\|.+\|$/.test(lines[i].trim())) { + tableLines.push(lines[i].trim()); + i += 1; + } + const rows = tableLines + .filter((l) => !/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(l)) + .map((l) => l.replace(/^\||\|$/g, "").split("|").map((c) => c.trim())); + if (rows.length > 0) blocks.push({ type: "table", headers: rows[0], rows: rows.slice(1) }); + continue; + } + const heading = trimmed.match(/^(#{1,6})\s+(.+)$/); + if (heading) { + flush(); + blocks.push({ type: "heading", level: heading[1].length, text: heading[2].trim() }); + i += 1; + continue; + } + const bullet = trimmed.match(/^[-*]\s+(.+)$/); + if (bullet) { + flush(); + blocks.push({ type: "paragraph", text: bullet[1].trim() }); + i += 1; + continue; + } + const ordered = trimmed.match(/^(\d+[..、])\s*(.+)$/); + if (ordered) { + flush(); + blocks.push({ type: "paragraph", text: `${ordered[1]} ${ordered[2].trim()}` }); + i += 1; + continue; + } + para.push(trimmed); + i += 1; + } + flush(); + return blocks; +} + +function normalizeSignatureText(text) { + return String(text ?? "").replace(/\s+/g, ""); +} + +function isMatchingParagraph(block, text) { + return ( + block?.type === "paragraph" && + normalizeSignatureText(block.text) === normalizeSignatureText(text) + ); +} + +function isChineseDateParagraph(block) { + return ( + block?.type === "paragraph" && + /^\d{4}年\d{1,2}月\d{1,2}日$/.test(normalizeSignatureText(block.text)) + ); +} + +function stripTrailingMatchingSignature(blocks, args) { + if (args.date && (isMatchingParagraph(blocks.at(-1), args.date) || isChineseDateParagraph(blocks.at(-1)))) { + blocks.pop(); + } + if (args.sender && isMatchingParagraph(blocks.at(-1), args.sender)) { + blocks.pop(); + } +} + +function isFirstLayerHeadingText(text) { + return /^[一二三四五六七八九十百]+、/.test(String(text ?? "").trim()); +} + +function isSecondLayerHeadingText(text) { + return /^([一二三四五六七八九十百]+)/.test(String(text ?? "").trim()); +} + +function isThirdLayerHeadingText(text) { + return /^\d+[..]/.test(String(text ?? "").trim()); +} + +function isFourthLayerHeadingText(text) { + return /^(\d+)/.test(String(text ?? "").trim()); +} + +function looksLikeMainSend(text) { + const compact = String(text ?? "").replace(/\s+/g, ""); + if (!/[::]$/.test(compact) || compact.length > 60) return false; + if (/[。!?;;]/.test(compact)) return false; + return /(各|贵|省|市|县|区|镇|乡|机关|单位|公司|部门|委员会|办公室|厅|局|处|中心|领导|集团|党委|党总支|党支部)/.test(compact.slice(0, -1)); +} + +function renderHeading(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); + if (block.level <= 2) return h1(block.text); + if (block.level === 3) return h2(block.text); + if (block.level === 4) return h3(block.text); + return h4(block.text); +} + +function renderParagraph(block) { + if (isFirstLayerHeadingText(block.text)) return h1(block.text); + if (isSecondLayerHeadingText(block.text)) return h2(block.text); + return paragraph(block.text); +} + +function buildDocument(blocks, args) { + let title = args.title; + const pageNumberMode = args["no-page-number"] || args["no-page-numbers"] + ? "none" + : ["center", "standard", "none"].includes(args["page-number"]) + ? args["page-number"] + : "center"; + const body = []; + const sourceBlocks = [...blocks]; + if (!title && sourceBlocks[0]?.type === "heading") { + title = sourceBlocks.shift().text; + } + if (title && sourceBlocks[0]?.type === "heading" && sourceBlocks[0].text.trim() === title.trim()) { + sourceBlocks.shift(); + } + stripTrailingMatchingSignature(sourceBlocks, args); + let mainSend = args.to; + if (mainSend && isMatchingParagraph(sourceBlocks[0], mainSend)) sourceBlocks.shift(); + 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"])); + if (title) body.push(titleParagraph(title)); + if (args.subtitle) body.push(subtitleParagraph(args.subtitle)); + if (mainSend) body.push(mainSendParagraph(mainSend)); + + for (const block of sourceBlocks) { + if (block.type === "heading") { + body.push(renderHeading(block)); + continue; + } + if (block.type === "table") { + body.push(tableXml(block.headers, block.rows)); + body.push(emptyLine()); + continue; + } + body.push(renderParagraph(block)); + } + if (args.sender || args.date) { + body.push(emptyLine()); + if (args.sender) body.push(right(args.sender)); + if (args.date) body.push(right(args.date)); + } + const footerRefs = pageNumberMode === "standard" + ? '' + : pageNumberMode === "center" + ? '' + : ""; + body.push(`${footerRefs}`); + return `${body.join("")}`; +} + +function stylesXml() { + return ``; +} + +function fontDef(name, altName, family = "roman", charset = "86") { + const alt = altName ? `` : ""; + return `${alt}`; +} + +function fontTableXml() { + const fonts = [ + fontDef("方正小标宋简体", "Songti SC"), + fontDef("方正小标宋_GBK", "Songti SC"), + fontDef("FZXiaoBiaoSong-B05S", "方正小标宋简体"), + fontDef("FZXiaoBiaoSong-B13S", "方正小标宋_GBK"), + fontDef("华文中宋", "STZhongsong"), + fontDef("STZhongsong", "华文中宋"), + fontDef("仿宋", "STFangsong"), + fontDef("仿宋_GB2312", "STFangsong"), + fontDef("FangSong", "仿宋"), + fontDef("FangSong_GB2312", "仿宋_GB2312"), + fontDef("STFangsong", "仿宋"), + fontDef("黑体", "Heiti SC", "swiss"), + fontDef("SimHei", "黑体", "swiss"), + fontDef("Heiti SC", "黑体", "swiss"), + fontDef("楷体", "Kaiti SC"), + fontDef("楷体_GB2312", "Kaiti SC"), + fontDef("KaiTi", "楷体"), + fontDef("KaiTi_GB2312", "楷体_GB2312"), + fontDef("Kaiti SC", "楷体"), + fontDef("宋体", "Songti SC"), + fontDef("SimSun", "宋体"), + fontDef("Songti SC", "宋体"), + fontDef("Times New Roman", "", "roman", "00"), + ].join(""); + return `${fonts}`; +} + +function footerXml(align = "center") { + const pageRunProps = ``; + const sideIndent = align === "right" + ? '' + : align === "left" + ? '' + : ""; + return `${sideIndent}${pageRunProps}— ${pageRunProps}1${pageRunProps} —`; +} + +function settingsXml(pageNumberMode) { + const evenOdd = pageNumberMode === "standard" ? "" : ""; + return `${evenOdd}`; +} + +function documentRelsXml(pageNumberMode) { + const footerRels = pageNumberMode === "standard" + ? '' + : pageNumberMode === "center" + ? '' + : ""; + return `${footerRels}`; +} + +function writeDocx(output, documentXml, pageNumberMode) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gongwen-docx-")); + fs.mkdirSync(path.join(dir, "_rels"), { recursive: true }); + fs.mkdirSync(path.join(dir, "word"), { recursive: true }); + fs.mkdirSync(path.join(dir, "word/_rels"), { recursive: true }); + const footerTypes = pageNumberMode === "standard" + ? '' + : pageNumberMode === "center" + ? '' + : ""; + fs.writeFileSync(path.join(dir, "[Content_Types].xml"), `${footerTypes}`); + fs.writeFileSync(path.join(dir, "_rels/.rels"), ``); + fs.writeFileSync(path.join(dir, "word/_rels/document.xml.rels"), documentRelsXml(pageNumberMode)); + fs.writeFileSync(path.join(dir, "word/document.xml"), documentXml); + fs.writeFileSync(path.join(dir, "word/styles.xml"), stylesXml()); + fs.writeFileSync(path.join(dir, "word/fontTable.xml"), fontTableXml()); + fs.writeFileSync(path.join(dir, "word/settings.xml"), settingsXml(pageNumberMode)); + if (pageNumberMode === "standard") { + fs.writeFileSync(path.join(dir, "word/footerOdd.xml"), footerXml("right")); + fs.writeFileSync(path.join(dir, "word/footerEven.xml"), footerXml("left")); + } else if (pageNumberMode === "center") { + fs.writeFileSync(path.join(dir, "word/footerCenter.xml"), footerXml("center")); + } + fs.rmSync(output, { force: true }); + execFileSync("zip", ["-qr", output, "."], { cwd: dir }); + fs.rmSync(dir, { recursive: true, force: true }); +} + +const args = parseArgs(process.argv); +if (args.help || !args.input || !args.output) { + usage(); + process.exit(args.help ? 0 : 1); +} +const md = fs.readFileSync(args.input, "utf8"); +const blocks = parseMarkdown(md); +const documentXml = buildDocument(blocks, args); +const pageNumberMode = args["no-page-number"] || args["no-page-numbers"] + ? "none" + : ["center", "standard", "none"].includes(args["page-number"]) + ? args["page-number"] + : "center"; +writeDocx(path.resolve(args.output), documentXml, pageNumberMode); +console.log(`Generated: ${path.resolve(args.output)}`); diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..6ec338d --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,41 @@ +[CmdletBinding()] +param( + [ValidateSet('codex', 'claude', 'opencode', 'trae-code', 'trae-cli', 'kimi-cli', 'kimi-code', 'workbuddy', 'zcode')] + [string[]]$Target, + [switch]$All, + [switch]$Link, + [switch]$Force +) + +$SkillDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if (-not $Target -and -not $All) { throw 'Pass -All or at least one -Target.' } +if ($All) { $Target = @('codex', 'claude', 'opencode', 'trae-code', 'trae-cli', 'kimi-cli', 'kimi-code', 'workbuddy', 'zcode') } + +$Roots = @{ + 'codex' = "$HOME\.codex\skills" + 'claude' = "$HOME\.claude\skills" + 'opencode' = "$HOME\.config\opencode\skills" + 'trae-code' = "$HOME\.trae-cn\skills" + 'trae-cli' = "$HOME\.traecli\skills" + 'kimi-cli' = "$HOME\.kimi\skills" + 'kimi-code' = "$HOME\.kimi-code\skills" + 'workbuddy' = "$HOME\.workbuddy\skills" + 'zcode' = "$HOME\.zcode\skills" +} + +foreach ($Name in $Target) { + $Root = $Roots[$Name] + $Destination = Join-Path $Root 'gongwen' + New-Item -ItemType Directory -Force -Path $Root | Out-Null + if (Test-Path $Destination) { + if (-not $Force) { throw "Refusing to replace $Destination. Review it, then rerun with -Force." } + Remove-Item -Recurse -Force $Destination + } + if ($Link) { + New-Item -ItemType SymbolicLink -Path $Destination -Target $SkillDir | Out-Null + } else { + Copy-Item -Recurse -Force -Exclude '.git', 'dist' -Path $SkillDir -Destination $Destination + } + if (-not (Test-Path (Join-Path $Destination 'SKILL.md'))) { throw "Installation failed for $Name." } + Write-Output "Installed: $Name -> $Destination" +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..0c6b7c5 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +skill_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +force=false +targets=() + +usage() { + cat <<'EOF' +Usage: scripts/install.sh [--all | --target NAME ...] [--force] + +Targets: codex, claude, opencode, trae-code, trae-cli, kimi-cli, kimi-code, + workbuddy, zcode, traework + +Installs the canonical skill as a symbolic link. Existing non-linked gongwen +directories are preserved unless --force is supplied. The traework target +creates a ZIP for import in TraeWork instead of guessing an unsupported path. +EOF +} + +while (($#)); do + case "$1" in + --all) targets=(codex claude opencode trae-code trae-cli kimi-cli kimi-code workbuddy zcode traework) ;; + --target) shift; targets+=("${1:?missing target name}") ;; + --force) force=true ;; + -h|--help) usage; exit 0 ;; + *) printf 'Unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if ((${#targets[@]} == 0)); then + usage >&2 + exit 2 +fi + +target_root() { + case "$1" in + codex) printf '%s/.codex/skills' "$HOME" ;; + claude) printf '%s/.claude/skills' "$HOME" ;; + opencode) printf '%s/.config/opencode/skills' "$HOME" ;; + trae-code) printf '%s/.trae-cn/skills' "$HOME" ;; + trae-cli) printf '%s/.traecli/skills' "$HOME" ;; + kimi-cli) printf '%s/.kimi/skills' "$HOME" ;; + kimi-code) printf '%s/.kimi-code/skills' "$HOME" ;; + workbuddy) printf '%s/.workbuddy/skills' "$HOME" ;; + zcode) printf '%s/.zcode/skills' "$HOME" ;; + *) return 1 ;; + esac +} + +install_link() { + local name=$1 root destination + root=$(target_root "$name") || { printf 'Unsupported target: %s\n' "$name" >&2; exit 2; } + destination="$root/gongwen" + mkdir -p "$root" + if [[ -e "$destination" || -L "$destination" ]]; then + if [[ -L "$destination" && $(readlink "$destination") == "$skill_dir" ]]; then + printf 'Already installed: %s\n' "$name" + return + fi + if [[ "$force" != true ]]; then + printf 'Refusing to replace existing %s; rerun with --force after review.\n' "$destination" >&2 + exit 3 + fi + rm -rf "$destination" + fi + ln -s "$skill_dir" "$destination" + test -f "$destination/SKILL.md" + printf 'Installed: %s -> %s\n' "$name" "$destination" +} + +package_traework() { + local stage output + stage=$(mktemp -d "${TMPDIR:-/tmp}/gongwen-traework.XXXXXX") + 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/" + (cd "$stage" && zip -qr "$output" gongwen) + unzip -t "$output" >/dev/null + rm -rf "$stage" + printf 'Packaged TraeWork import ZIP: %s\n' "$output" +} + +for target in "${targets[@]}"; do + if [[ "$target" == traework ]]; then + package_traework + else + install_link "$target" + fi +done diff --git a/scripts/verify_gongwen_docx.mjs b/scripts/verify_gongwen_docx.mjs new file mode 100755 index 0000000..2680a5b --- /dev/null +++ b/scripts/verify_gongwen_docx.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +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]"); +} + +function parseArgs(argv) { + const args = {}; + for (let i = 2; i < argv.length; i += 1) { + if (!argv[i].startsWith("--")) continue; + args[argv[i].slice(2)] = argv[i + 1]; + i += 1; + } + return args; +} + +function readPart(file, part, optional = false) { + 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, ""))); +} + +const args = parseArgs(process.argv); +if (!args.input || !fs.existsSync(args.input)) { + usage(); + 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 checks = []; + +function check(name, ok, detail) { + checks.push({ name, ok, detail }); +} + +check("A4 page size", /]*w:w="11906"[^>]*w:h="16838"/.test(doc), "210 mm x 297 mm"); +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"); +} 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}`); +} + +const failed = checks.filter((item) => !item.ok); +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/fixture.md b/tests/fixture.md new file mode 100644 index 0000000..01b5570 --- /dev/null +++ b/tests/fixture.md @@ -0,0 +1,17 @@ +# 公文格式回归测试 + +## 一、一级标题 + +这是正文自然段,用于验证仿宋三号、固定行距和首行缩进。 + +### (一)二级标题 + +这是二级标题后的正文。 + +#### 1. 三级标题 + +这是三级标题后的正文。 + +##### (1)四级标题 + +这是四级标题后的正文。 diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..e5b3657 --- /dev/null +++ b/tests/run_tests.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +SKILL_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +NODE=${NODE:-node} +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gongwen-test.XXXXXX") +trap 'rm -rf "$TMP_DIR"' EXIT + +GEN="$SKILL_DIR/scripts/generate_gongwen_docx.mjs" +VERIFY="$SKILL_DIR/scripts/verify_gongwen_docx.mjs" +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 +unzip -t "$TMP_DIR/layout.docx" >/dev/null + +"$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 '示例单位文件' +unzip -p "$TMP_DIR/standard.docx" word/document.xml | grep -q '示例发〔2026〕1号' + +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 + test -s "$TMP_DIR/pdf/layout.pdf" +fi + +printf '%s\n' "gongwen regression tests passed" diff --git a/tests/test-install.sh b/tests/test-install.sh new file mode 100755 index 0000000..444cc4d --- /dev/null +++ b/tests/test-install.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +skill_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +test_home=$(mktemp -d "${TMPDIR:-/tmp}/gongwen-install-test.XXXXXX") +trap 'rm -rf "$test_home"' EXIT + +HOME="$test_home" "$skill_dir/scripts/install.sh" --all + +for path in \ + .codex/skills/gongwen \ + .claude/skills/gongwen \ + .config/opencode/skills/gongwen \ + .trae-cn/skills/gongwen \ + .traecli/skills/gongwen \ + .kimi/skills/gongwen \ + .kimi-code/skills/gongwen \ + .workbuddy/skills/gongwen \ + .zcode/skills/gongwen; do + test -L "$test_home/$path" + test -f "$test_home/$path/SKILL.md" +done + +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' +printf '%s\n' 'cross-platform installation tests passed'