skills/docx/docx-js.md
wangyitong a65adcc2e5 Initial commit: merged, deduplicated, and vetted skill collection
Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:47:12 +08:00

499 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DOCX Library Tutorial
Generate .docx files with JavaScript/TypeScript.
**Important: Read this entire document before starting.** Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues.
## Setup
Assumes docx is already installed globally
If not installed: `npm install -g docx`
```javascript
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Media,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
InternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, TabStopType,
TabStopPosition, UnderlineType, ShadingType, VerticalAlign, SymbolRun, PageNumber,
FootnoteReferenceRun, Footnote, PageBreak } = require('docx');
// Create & Save
const doc = new Document({ sections: [{ children: [/* content */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js
Packer.toBlob(doc).then(blob => { /* download logic */ }); // Browser
```
## Text & Formatting
```javascript
// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements
// ❌ WRONG: new TextRun("Line 1\nLine 2")
// ✅ CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] })
// Basic text with all formatting options (公文配置:仿宋 14pt 默认)
new Paragraph({
alignment: AlignmentType.JUSTIFIED, // 公文两端对齐
spacing: { before: 200, after: 200 },
indent: { firstLine: 560 }, // 公文首行缩进 2em
children: [
new TextRun({ text: "加粗", bold: true }),
new TextRun({ text: "斜体", italics: true }),
new TextRun({ text: "下划线", underline: { type: UnderlineType.SINGLE, color: "000000" } }), // 公文用黑色
new TextRun({ text: "指定字号", size: 28, font: "STFangsong" }), // 仿宋 14pt
new TextRun({ text: "高亮", highlight: "yellow" }),
new TextRun({ text: "删除线", strike: true }),
new TextRun({ text: "x2", superScript: true }),
new TextRun({ text: "H2O", subScript: true }),
new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet •
new SymbolRun({ char: "00A9", font: "STFangsong" }) // Copyright ©
]
})
```
## Styles — AbleMind 公文 UI 设计系统
### 字体体系
| 变量 | 字体栈 | 用途 |
|------|--------|------|
| `--gov-font-body` | STFangsong → FangSong → Fangsong SC → Noto Serif SC → serif | 正文(仿宋体) |
| `--gov-font-heading` | Heiti SC → PingFang SC → SimHei → Noto Sans SC → sans-serif | 标题(黑体) |
| `--gov-font-mono` | IBM Plex Mono → JetBrains Mono → monospace | UI 等宽 |
| `--gov-font-code` | Courier New → monospace | 代码块 |
在 docx-js 中使用时font 值按优先级取第一个系统可用字体即可macOS 优先 STFangsong / Heiti SC
### 公文排版规范
| 元素 | 字体 | 字号 | 其他 |
|------|------|------|------|
| 正文 | 仿宋 (STFangsong) | 14pt (size: 28) | 行距 1.5,首行缩进 2em两端对齐 |
| h1 | 黑体 (Heiti SC) | 16pt (size: 32) | 居中,加粗 |
| h2 | 黑体 (Heiti SC) | 15pt (size: 30) | 左对齐,加粗 |
| h3h6 | 黑体 (Heiti SC) | 14pt (size: 28) | 左对齐,加粗 |
| 表格 | 仿宋 (STFangsong) | 小四 12pt (size: 24) | 全线框,表头灰底 |
| 代码 | Courier New | 12pt (size: 24) | 灰底框线 |
| 链接 | 同正文 | 同正文 | 黑色下划线(公文不用彩色链接) |
### 标准公文样式模板
```javascript
// AbleMind 公文配置 — 默认样式
const GOV_FONT_BODY = "STFangsong"; // 仿宋体(正文)
const GOV_FONT_HEADING = "Heiti SC"; // 黑体(标题)
const GOV_FONT_CODE = "Courier New"; // 代码块
const doc = new Document({
styles: {
default: {
document: {
run: { font: GOV_FONT_BODY, size: 28 }, // 仿宋 14pt
paragraph: {
spacing: { line: 360 }, // 行距 1.5 (240 * 1.5)
alignment: AlignmentType.JUSTIFIED // 两端对齐
}
}
},
paragraphStyles: [
// 公文标题 — 黑体 16pt 居中
{ id: "Title", name: "Title", basedOn: "Normal",
run: { size: 32, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } },
// h1 — 黑体 16pt 居中
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 240, after: 240, line: 360 }, alignment: AlignmentType.CENTER, outlineLevel: 0 } },
// h2 — 黑体 15pt 左对齐
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 30, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 180, after: 180, line: 360 }, outlineLevel: 1 } },
// h3h6 — 黑体 14pt 左对齐
{ id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 120, after: 120, line: 360 }, outlineLevel: 2 } },
{ id: "Heading4", name: "Heading 4", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 120, after: 120, line: 360 }, outlineLevel: 3 } },
// 自定义样式仍可添加
{ id: "govNote", name: "Gov Note", basedOn: "Normal",
run: { size: 24, color: "333333", font: GOV_FONT_BODY },
paragraph: { spacing: { after: 60 } } }
],
characterStyles: [
// 公文链接:黑色下划线,不用彩色
{ id: "Hyperlink", name: "Hyperlink",
run: { color: "000000", underline: { type: UnderlineType.SINGLE, color: "000000" } } },
{ id: "govEmphasis", name: "Gov Emphasis",
run: { bold: true, font: GOV_FONT_HEADING } }
]
},
sections: [{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // A4 标准页边距 1 英寸
size: { width: 11906, height: 16838 } // A4 尺寸 (210mm × 297mm in DXA)
}
},
children: [
new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("公文标题")] }),
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("一级标题")] }),
new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun("二级标题")] }),
// 正文段落 — 首行缩进 2em仿宋14pt ≈ 560 DXA
new Paragraph({
indent: { firstLine: 560 },
children: [new TextRun("正文内容仿宋14pt行距1.5首行缩进2em两端对齐。")]
})
]
}]
});
```
### 公文首行缩进说明
- 首行缩进 2em = 2 × 字号对应的 DXA 值
- 仿宋 14pt → `firstLine: 560` (14pt × 20 DXA/pt × 2)
- 小四 12pt → `firstLine: 480` (12pt × 20 DXA/pt × 2)
- 通过 `indent: { firstLine: 560 }` 设置在每个正文 Paragraph 上
### 跨平台字体回退
- **macOS**: STFangsong / Heiti SC系统自带
- **Windows**: FangSong / SimHei系统自带
- **Linux/CI**: Noto Serif SC / Noto Sans SC需安装 Google Noto CJK
- docx-js 的 `font` 属性只写一个字体名Word 打开时自动使用系统可用字体
**Key Styling Principles:**
- **Override built-in styles**: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles
- **HeadingLevel constants**: `HeadingLevel.HEADING_1` uses "Heading1" style, `HeadingLevel.HEADING_2` uses "Heading2" style, etc.
- **Include outlineLevel**: Set `outlineLevel: 0` for H1, `outlineLevel: 1` for H2, etc. to ensure TOC works correctly
- **公文字体一致性**: 正文统一仿宋,标题统一黑体,不混用其他字体
- **公文不用彩色**: 链接、标题全部黑色,不使用蓝色超链接或灰色标题
- **A4 纸张**: 使用 `size: { width: 11906, height: 16838 }` 设置 A4 尺寸
- **行距 1.5**: 在 default paragraph spacing 中设置 `line: 360`
- **首行缩进**: 正文段落添加 `indent: { firstLine: 560 }`
## Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS)
```javascript
// Bullets - ALWAYS use the numbering config, NOT unicode symbols
// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet"
const doc = new Document({
numbering: {
config: [
{ reference: "bullet-list",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "first-numbered-list",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "second-numbered-list", // Different reference = restarts at 1
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }
]
},
sections: [{
children: [
// Bullet list items
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("First bullet point")] }),
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("Second bullet point")] }),
// Numbered list items
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("First numbered item")] }),
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("Second numbered item")] }),
// ⚠️ CRITICAL: Different reference = INDEPENDENT list that restarts at 1
// Same reference = CONTINUES previous numbering
new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 },
children: [new TextRun("Starts at 1 again (because different reference)")] })
]
}]
});
// ⚠️ CRITICAL NUMBERING RULE: Each reference creates an INDEPENDENT numbered list
// - Same reference = continues numbering (1, 2, 3... then 4, 5, 6...)
// - Different reference = restarts at 1 (1, 2, 3... then 1, 2, 3...)
// Use unique reference names for each separate numbered section!
// ⚠️ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly
// new TextRun("• Item") // WRONG
// new SymbolRun({ char: "2022" }) // WRONG
// ✅ ALWAYS use numbering config with LevelFormat.BULLET for real Word lists
```
## Tables — 公文表格规范
```javascript
// 公文表格:小四 12pt 仿宋,全线框,表头灰底居中加粗
const GOV_FONT_BODY = "STFangsong";
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "000000" }; // 公文用黑色全线框
const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder };
new Table({
columnWidths: [4680, 4680], // ⚠️ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point)
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Set once for all cells
rows: [
// 表头行:灰底居中加粗
new TableRow({
tableHeader: true,
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
// ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word.
shading: { fill: "D9D9D9", type: ShadingType.CLEAR }, // 浅灰底
verticalAlign: VerticalAlign.CENTER,
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "表头", bold: true, size: 24, font: GOV_FONT_BODY })] // 小四 12pt
})]
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
shading: { fill: "D9D9D9", type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER,
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "列标题", bold: true, size: 24, font: GOV_FONT_BODY })]
})]
})
]
}),
// 数据行:小四仿宋,左对齐
new TableRow({
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
children: [new Paragraph({ children: [new TextRun({ text: "数据内容", size: 24, font: GOV_FONT_BODY })] })]
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
children: [
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun({ text: "列表项一", size: 24, font: GOV_FONT_BODY })]
}),
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun({ text: "列表项二", size: 24, font: GOV_FONT_BODY })]
})
]
})
]
})
]
})
```
**IMPORTANT: Table Width & Borders**
- Use BOTH `columnWidths: [width1, width2, ...]` array AND `width: { size: X, type: WidthType.DXA }` on each cell
- Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins)
- Apply borders to individual `TableCell` elements, NOT the `Table` itself
**Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):**
- **2 columns:** `columnWidths: [4680, 4680]` (equal width)
- **3 columns:** `columnWidths: [3120, 3120, 3120]` (equal width)
## Links & Navigation
```javascript
// TOC (requires headings) - CRITICAL: Use HeadingLevel only, NOT custom styles
// ❌ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] })
// ✅ CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] })
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }),
// External link
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Google", style: "Hyperlink" })],
link: "https://www.google.com"
})]
}),
// Internal link & bookmark
new Paragraph({
children: [new InternalHyperlink({
children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })],
anchor: "section1"
})]
}),
new Paragraph({
children: [new TextRun("Section Content")],
bookmark: { id: "section1", name: "section1" }
}),
```
## Images & Media
```javascript
// Basic image with sizing & positioning
// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun
new Paragraph({
alignment: AlignmentType.CENTER,
children: [new ImageRun({
type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg)
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees
altText: { title: "Logo", description: "Company logo", name: "Name" } // IMPORTANT: All three fields are required
})]
})
```
## Page Breaks
```javascript
// Manual page break
new Paragraph({ children: [new PageBreak()] }),
// Page break before paragraph
new Paragraph({
pageBreakBefore: true,
children: [new TextRun("This starts on a new page")]
})
// ⚠️ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open
// ❌ WRONG: new PageBreak()
// ✅ CORRECT: new Paragraph({ children: [new PageBreak()] })
```
## Headers/Footers & Page Setup
```javascript
const doc = new Document({
sections: [{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch
size: { orientation: PageOrientation.LANDSCAPE },
pageNumbers: { start: 1, formatType: "decimal" } // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter"
}
},
headers: {
default: new Header({ children: [new Paragraph({
alignment: AlignmentType.RIGHT,
children: [new TextRun("Header Text")]
})] })
},
footers: {
default: new Footer({ children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" of "), new TextRun({ children: [PageNumber.TOTAL_PAGES] })]
})] })
},
children: [/* content */]
}]
});
```
## Tabs
```javascript
new Paragraph({
tabStops: [
{ type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 },
{ type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 },
{ type: TabStopType.RIGHT, position: TabStopPosition.MAX * 3 / 4 }
],
children: [new TextRun("Left\tCenter\tRight")]
})
```
## Constants & Quick Reference
- **Underlines:** `SINGLE`, `DOUBLE`, `WAVY`, `DASH`
- **Borders:** `SINGLE`, `DOUBLE`, `DASHED`, `DOTTED`
- **Numbering:** `DECIMAL` (1,2,3), `UPPER_ROMAN` (I,II,III), `LOWER_LETTER` (a,b,c)
- **Tabs:** `LEFT`, `CENTER`, `RIGHT`, `DECIMAL`
- **Symbols:** `"2022"` (•), `"00A9"` (©), `"00AE"` (®), `"2122"` (™), `"00B0"` (°), `"F070"` (✓), `"F0FC"` (✗)
## Cross-Platform 路径处理Windows / macOS / Linux
**根本原因**: Windows 用 `\` 作路径分隔符macOS/Linux 用 `/`。在 JS 字符串中 `\` 是转义符,直接写 `"C:\Users\file"` 会被解析为 `"C:Usersile"`
### 必须遵守的规则
```javascript
const path = require('path');
const fs = require('fs');
// ❌ 硬编码斜杠 — Windows 上可能失败
const img = fs.readFileSync("images/logo.png");
const out = "output/report.docx";
// ✅ 始终用 path.join() 拼接路径
const img = fs.readFileSync(path.join("images", "logo.png"));
const out = path.join("output", "report.docx");
// ❌ 模板字符串拼路径
const file = `${dir}/report.docx`;
// ✅ path.join 拼接
const file = path.join(dir, "report.docx");
// ❌ __dirname + 硬编码斜杠
const tpl = __dirname + "/templates/header.xml";
// ✅ path.join(__dirname, ...)
const tpl = path.join(__dirname, "templates", "header.xml");
```
### 输出文件名注意事项
```javascript
// ✅ 写文件前确保目录存在
const outDir = path.join("output");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
Packer.toBuffer(doc).then(buf => fs.writeFileSync(path.join(outDir, "report.docx"), buf));
```
### Python 脚本同样适用
```python
import os
# ❌ 硬编码斜杠
doc_path = "word/document.xml"
# ✅ os.path.join
doc_path = os.path.join("word", "document.xml")
# ✅ pathlib (Python 3.4+) 更优雅
from pathlib import Path
doc_path = Path("word") / "document.xml"
```
### Shell 命令中的路径
```bash
# ✅ 正斜杠在所有平台的 shell 中都能工作(包括 Windows PowerShell/cmd
python ooxml/scripts/unpack.py input.docx output_dir
# ⚠️ 但如果路径来自变量且含空格,务必加引号
python "ooxml/scripts/unpack.py" "$INPUT_FILE" "$OUTPUT_DIR"
```
### 快速检查清单
| 检查项 | 说明 |
|--------|------|
| 不出现 `"/"` 拼路径 | 用 `path.join()` / `os.path.join()` |
| 不出现 `"\\"` 拼路径 | 同上 |
| 不出现 `` `${x}/y` `` 拼路径 | 用 `path.join(x, "y")` |
| `fs.mkdirSync``recursive` | 确保输出目录存在 |
| 文件名不含 `: * ? " < > \|` | Windows 保留字符,会导致写入失败 |
| 路径含空格时加引号 | shell 命令中 `"$PATH"` |
## Critical Issues & Common Mistakes
- **CRITICAL: PageBreak must ALWAYS be inside a Paragraph** - standalone PageBreak creates invalid XML that Word cannot open
- **ALWAYS use ShadingType.CLEAR for table cell shading** - Never use ShadingType.SOLID (causes black background).
- Measurements in DXA (1440 = 1 inch) | Each table cell needs ≥1 Paragraph | TOC requires HeadingLevel styles only
- **公文字体**: 正文用仿宋 (STFangsong),标题用黑体 (Heiti SC),表格用小四仿宋,代码用 Courier New
- **公文默认字号**: 正文 14pt (size: 28)h1 16pt (size: 32)h2 15pt (size: 30)h3+ 14pt (size: 28),表格 12pt (size: 24)
- **公文行距**: 在 default paragraph 中设置 `spacing: { line: 360 }` (1.5 倍行距)
- **公文首行缩进**: 正文段落添加 `indent: { firstLine: 560 }`14pt × 20 × 2
- **公文纸张**: A4 尺寸 `size: { width: 11906, height: 16838 }`
- **公文链接**: 黑色下划线,覆盖 Hyperlink 字符样式为 `color: "000000"`
- **公文表格**: 黑色全线框 `color: "000000"`,表头灰底 `fill: "D9D9D9"`
- **ALWAYS use columnWidths array for tables** + individual cell widths for compatibility
- **NEVER use unicode symbols for bullets** - always use proper numbering configuration with `LevelFormat.BULLET` constant (NOT the string "bullet")
- **NEVER use \n for line breaks anywhere** - always use separate Paragraph elements for each line
- **ALWAYS use TextRun objects within Paragraph children** - never use text property directly on Paragraph
- **CRITICAL for images**: ImageRun REQUIRES `type` parameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg"
- **CRITICAL for bullets**: Must use `LevelFormat.BULLET` constant, not string "bullet", and include `text: "•"` for the bullet character
- **CRITICAL for numbering**: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section!
- **CRITICAL for TOC**: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break
- **Tables**: Set `columnWidths` array + individual cell widths, apply borders to cells not table
- **Set table margins at TABLE level** for consistent cell padding (avoids repetition per cell)
- **跨平台路径**: 始终用 `path.join()` 拼接路径,不硬编码 `/``\\`。写文件前用 `fs.mkdirSync(dir, { recursive: true })` 确保目录存在