---
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 片段

#### 页面设置（插入 `<w:sectPr>` 中）

```xml
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="2098" w:right="1474" w:bottom="1985" w:left="1588"
         w:header="851" w:footer="851" w:gutter="0"/>
```

#### 正文段落格式

```xml
<w:pPr>
  <w:spacing w:line="560" w:lineRule="exact"/>
  <w:ind w:firstLine="560"/>
  <w:jc w:val="both"/>
</w:pPr>
<w:rPr>
  <w:rFonts w:ascii="FangSong" w:eastAsia="仿宋" w:hAnsi="FangSong"/>
  <w:sz w:val="28"/>
  <w:szCs w:val="28"/>
</w:rPr>
```

#### 一级标题

```xml
<w:pPr>
  <w:pStyle w:val="Heading1"/>
  <w:spacing w:line="640" w:lineRule="exact" w:before="240" w:after="120"/>
</w:pPr>
<w:rPr>
  <w:rFonts w:ascii="SimHei" w:eastAsia="黑体" w:hAnsi="SimHei"/>
  <w:b/>
  <w:sz w:val="32"/>
  <w:szCs w:val="32"/>
</w:rPr>
```

#### 三线表边框（`<w:tblBorders>` 内）

```xml
<w:top w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="6" w:space="0" w:color="000000"/>
<w:left w:val="none" w:sz="0" w:space="0" w:color="auto"/>
<w:right w:val="none" w:sz="0" w:space="0" w:color="auto"/>
<w:insideV w:val="none" w:sz="0" w:space="0" w:color="auto"/>
```

---

## 从现有文档提取内容并重建

当需要对已有文档「重排版」时，推荐提取内容后用 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
