--- name: docx-cn description: "基于 docx-js (Node.js) 创建和编辑符合 GB/T 9704-2012 中国公文格式标准的 Word 文档。当用户需要生成公文、研究报告、内部文件等中文 Word 文档时使用。触发词:公文、Word、docx、报告排版、格式规范、GB/T 9704。" --- # 中文公文 Word 文档生成(docx-js) ## 概述 基于官方 docx skill 微调,专为中文公文 / 金融研究报告场景优化。 使用 **docx-js**(Node.js)生成文档,配合官方 skill 的 unpack/pack/validate 工具链编辑现有文档。 ## 核心工具链 | 任务 | 方法 | |------|------| | **新建文档** | `docx-js` — 本文件中的模板 | | **编辑现有文档** | unpack XML → 编辑 → repack(见「编辑现有文档」节) | | **验证** | `python .claude/skills/docx/scripts/office/validate.py output.docx` | | **读取内容** | `pandoc document.docx -o output.md` | --- ## GB/T 9704-2012 公文格式常量 ### 单位换算速查 ``` 1 inch = 1440 DXA = 72 pt = 25.4 mm 1 mm = 56.693 DXA 1 pt = 20 DXA(spacing/行距用) font size 单位 = half-point(半磅),如 14pt = 28 ``` ### 页面设置(A4) ```javascript const PAGE = { WIDTH: 11906, // 210mm HEIGHT: 16838, // 297mm MARGIN_TOP: 2098, // 37mm MARGIN_BOTTOM: 1985, // 35mm MARGIN_LEFT: 1588, // 28mm MARGIN_RIGHT: 1474, // 26mm CONTENT_WIDTH: 8844, // 210mm - 28mm - 26mm }; ``` ### 字号对照表 | 中文字号 | pt | half-point (docx-js size) | 用途 | |----------|-----|---------------------------|------| | 二号 | 22 | 44 | 大标题 | | 三号 | 16 | 32 | 一级标题 | | 四号 | 14 | 28 | 正文 / 二级标题 | | 小四 | 12 | 24 | 表格内容 | | 五号 | 10.5 | 21 | 页脚、注释 | ### 字体规范 ```javascript // 中文字体必须同时设置 eastAsia,否则 Word 渲染时可能回退到宋体 const FONT = { HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" }, // 标题 FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" }, // 正文 KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" }, // 二级标题 SONGTI: { ascii: "SimSun", eastAsia: "宋体", hAnsi: "SimSun" }, // 备用 }; ``` ### 行距与段落 ```javascript const SPACING = { // 行距(twips = pt × 20),lineRule: "exact" → 固定值 BODY: { line: 560, lineRule: "exact" }, // 28pt 固定行距 HEADING1: { line: 640, lineRule: "exact" }, // 32pt HEADING2: { line: 560, lineRule: "exact" }, // 28pt TABLE: { line: 440, lineRule: "exact" }, // 22pt // 段前段后(twips) H1_BEFORE: 240, H1_AFTER: 120, // 12pt / 6pt H2_BEFORE: 120, H2_AFTER: 60, // 6pt / 3pt }; const INDENT = { FIRST_LINE: 560, // 首行缩进 2 字符 ≈ 28pt = 560 DXA }; ``` --- ## 创建新文档 ### 安装 ```bash npm install docx # 项目内安装即可,无需全局 ``` ### 完整模板:公文报告(无封面) ```javascript const fs = require("fs"); const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, Header, Footer, AlignmentType, HeadingLevel, BorderStyle, WidthType, ShadingType, PageNumber, PageBreak, LevelFormat, TabStopType, TabStopPosition, } = require("docx"); // ─── GB/T 9704-2012 格式常量 ───────────────────────────── const PAGE = { WIDTH: 11906, HEIGHT: 16838, MARGIN_TOP: 2098, MARGIN_BOTTOM: 1985, MARGIN_LEFT: 1588, MARGIN_RIGHT: 1474, CONTENT_WIDTH: 8844, }; const FONT = { HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" }, FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" }, KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" }, }; // ─── 样式定义 ───────────────────────────────────────────── const styles = { default: { document: { run: { font: FONT.FANGSONG, size: 28 }, // 默认:仿宋四号 paragraph: { spacing: { line: 560, lineRule: "exact" }, }, }, }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { font: FONT.HEITI, size: 32, bold: true }, // 黑体三号 paragraph: { spacing: { line: 640, lineRule: "exact", before: 240, after: 120 }, outlineLevel: 0, }, }, { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { font: FONT.KAITI, size: 28, bold: true }, // 楷体四号加粗 paragraph: { spacing: { line: 560, lineRule: "exact", before: 120, after: 60 }, outlineLevel: 1, }, }, ], }; // ─── 列表定义 ───────────────────────────────────────────── const numbering = { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT, style: { run: { font: FONT.FANGSONG, size: 28 }, paragraph: { indent: { left: 1120, hanging: 560 } }, // 缩进 2 字符 + 悬挂 }, }], }, ], }; // ─── 辅助函数 ───────────────────────────────────────────── /** 创建正文段落(仿宋四号,首行缩进 2 字符) */ function bodyParagraph(text, options = {}) { const { bold = false, alignment = AlignmentType.JUSTIFIED } = options; return new Paragraph({ alignment, indent: { firstLine: 560 }, spacing: { line: 560, lineRule: "exact" }, children: [new TextRun({ text, font: FONT.FANGSONG, size: 28, bold })], }); } /** 创建一级标题(黑体三号) */ function heading1(text) { return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, font: FONT.HEITI, size: 32, bold: true })], }); } /** 创建二级标题(楷体四号加粗) */ function heading2(text) { return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, font: FONT.KAITI, size: 28, bold: true })], }); } /** 创建三线表 */ function threeLineTable(headers, rows) { const noBorder = { style: BorderStyle.NONE, size: 0 }; const thickBorder = { style: BorderStyle.SINGLE, size: 12, color: "000000" }; // 1.5pt const thinBorder = { style: BorderStyle.SINGLE, size: 6, color: "000000" }; // 0.75pt // 表头行边框:顶粗 + 底细 const headerBorders = { top: thickBorder, bottom: thinBorder, left: noBorder, right: noBorder, }; // 数据行边框:无顶底左右(靠 insideH 提供行间线) const dataBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder, }; // 最后一行底部:粗线 const lastRowBorders = { top: noBorder, bottom: thickBorder, left: noBorder, right: noBorder, }; const colWidths = headers.map(() => Math.floor(PAGE.CONTENT_WIDTH / headers.length)); function makeCell(text, isHeader, borders) { return new TableCell({ borders, width: { size: colWidths[0], type: WidthType.DXA }, shading: isHeader ? { fill: "F2F2F2", type: ShadingType.CLEAR } : undefined, margins: { top: 40, bottom: 40, left: 80, right: 80 }, children: [new Paragraph({ alignment: AlignmentType.CENTER, spacing: { line: 440, lineRule: "exact" }, children: [new TextRun({ text, font: isHeader ? FONT.HEITI : FONT.FANGSONG, size: 24, // 小四 12pt bold: isHeader, })], })], }); } const tableRows = [ // 表头 new TableRow({ children: headers.map(h => makeCell(h, true, headerBorders)), }), // 数据行 ...rows.map((row, rowIdx) => new TableRow({ children: row.map(cell => makeCell(cell, false, rowIdx === rows.length - 1 ? lastRowBorders : dataBorders) ), }) ), ]; return new Table({ width: { size: PAGE.CONTENT_WIDTH, type: WidthType.DXA }, columnWidths: colWidths, rows: tableRows, }); } /** 报告标题区(居中,无封面) */ function titleBlock(title, subtitle, date, institution) { return [ new Paragraph({ alignment: AlignmentType.CENTER, spacing: { line: 640, lineRule: "exact", after: 0 }, children: [new TextRun({ text: title, font: FONT.HEITI, size: 32, bold: true })], }), subtitle ? new Paragraph({ alignment: AlignmentType.CENTER, spacing: { line: 640, lineRule: "exact", after: 120 }, children: [new TextRun({ text: subtitle, font: FONT.HEITI, size: 32 })], }) : null, new Paragraph({ alignment: AlignmentType.CENTER, spacing: { line: 560, lineRule: "exact", after: 0 }, children: [new TextRun({ text: date, font: FONT.FANGSONG, size: 28 })], }), institution ? new Paragraph({ alignment: AlignmentType.CENTER, spacing: { line: 560, lineRule: "exact", after: 240 }, children: [new TextRun({ text: institution, font: FONT.FANGSONG, size: 28 })], }) : null, ].filter(Boolean); } // ─── 使用示例:构建文档 ────────────────────────────────── const doc = new Document({ styles, numbering, sections: [{ properties: { page: { size: { width: PAGE.WIDTH, height: PAGE.HEIGHT }, margin: { top: PAGE.MARGIN_TOP, bottom: PAGE.MARGIN_BOTTOM, left: PAGE.MARGIN_LEFT, right: PAGE.MARGIN_RIGHT, }, }, }, headers: { default: new Header({ children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: "内部资料 注意保密", font: FONT.FANGSONG, size: 21, color: "808080", })], })], }), }, footers: { default: new Footer({ children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [ new TextRun({ text: "— ", font: FONT.FANGSONG, size: 21 }), new TextRun({ children: [PageNumber.CURRENT], font: FONT.FANGSONG, size: 21 }), new TextRun({ text: " —", font: FONT.FANGSONG, size: 21 }), ], })], }), }, children: [ // 标题区 ...titleBlock( "zn04(鲁肆号)年金组合", "权益投资分析与策略展望报告", "估值日:2026年3月10日", "工银瑞信基金管理有限公司" ), // 正文 heading1("一、投资策略与研判框架"), bodyParagraph("本组合采用"多维度全局配置"策略,横跨A股和港股市场..."), heading2("1.1 八大核心研判及验证"), threeLineTable( ["#", "核心判断", "验证状态", "关键证据"], [ ["1", "恒生科技有超额收益", "1月验证后逆转", "1月+4.2%→3月-12.45%"], ["2", "AI国内参与性价比不高", "已验证", "A股AI概念高位回落"], ] ), heading1("二、组合现状概览"), bodyParagraph("截至估值日,组合单位净值1.4430,权益仓位合计占比约23%。"), ], }], }); // 导出 Packer.toBuffer(doc).then(buffer => { fs.writeFileSync("output.docx", buffer); console.log("✅ 已生成: output.docx"); }); ``` ### 运行 ```bash node generate_report.js # 验证(可选,使用官方 docx skill 的验证工具) python .claude/skills/docx/scripts/office/validate.py output.docx ``` --- ## 辅助函数速查 | 函数 | 用途 | 格式 | |------|------|------| | `titleBlock(title, subtitle, date, institution)` | 报告标题区(无封面) | 黑体三号居中 | | `heading1(text)` | 一级标题 | 黑体三号 16pt | | `heading2(text)` | 二级标题 | 楷体四号加粗 | | `bodyParagraph(text, {bold, alignment})` | 正文段落 | 仿宋四号,首行缩进,28pt行距 | | `threeLineTable(headers, rows)` | 三线表 | 顶底1.5pt,内部0.75pt,无竖线 | --- ## 编辑现有文档 当需要修改已有 .docx 文件时,使用官方 docx skill 的 unpack/pack 工作流: ```bash # 1. 解包 python .claude/skills/docx/scripts/office/unpack.py document.docx unpacked/ # 2. 编辑 unpacked/word/document.xml(直接用 Edit 工具修改 XML) # 3. 重新打包 python .claude/skills/docx/scripts/office/pack.py unpacked/ output.docx --original document.docx ``` ### 公文格式关键 XML 片段 #### 页面设置(插入 `` 中) ```xml ``` #### 正文段落格式 ```xml ``` #### 一级标题 ```xml ``` #### 三线表边框(`` 内) ```xml ``` --- ## 从现有文档提取内容并重建 当需要对已有文档「重排版」时,推荐提取内容后用 docx-js 重建: ```bash # 提取为 Markdown pandoc original.docx -o content.md # 然后读取 content.md,用上面的模板函数构建新文档 ``` 这比原地修改格式更可靠,避免了样式冲突和继承问题。 --- ## 格式检查清单 - [ ] 纸张:A4(11906 × 16838 DXA) - [ ] 页边距:上37 / 下35 / 左28 / 右26 mm - [ ] 一级标题:黑体三号(16pt),固定行距 32pt - [ ] 二级标题:楷体四号加粗(14pt),固定行距 28pt - [ ] 正文:仿宋四号(14pt),固定行距 28pt,首行缩进 2 字符 - [ ] 表格:三线表,表头黑体小四 + 浅灰底,内容仿宋小四 - [ ] 页脚:居中 "— 页码 —",仿宋五号 - [ ] 字体必须设置 `eastAsia` 属性 ## 依赖 - **Node.js**: v18+ - **docx**: `npm install docx`(docx-js 库) - **pandoc**: 文本提取(可选) - **官方 docx skill**: 提供 validate.py / unpack.py / pack.py