--- name: docx description: "当用户需要创建、读取、编辑或处理 Word 文档(.docx)时,使用此技能。触发场景包括提到“Word 文档”、“.docx”,或要求生成带目录、标题、页码、信头等格式的专业文档;也包括提取或重组 .docx 内容、插入或替换图片、在 Word 文件中查找替换、处理修订或批注,以及将内容整理为正式 Word 文档。如果用户要求生成“报告”“备忘录”“信函”“模板”等 Word / .docx 交付物,也应使用此技能。不要用于 PDF、电子表格、Google Docs,或与文档生成无关的一般编程任务。" license: Proprietary. LICENSE.txt has complete terms metadata: builtin_skill_version: "1.1" --- > **重要:** 所有 `scripts/` 路径均相对于本技能目录。 > 运行方式: `cd {this_skill_dir} && python scripts/...` > 或使用 `execute_shell_command` 的 `cwd` 参数。 # DOCX 创建、编辑与分析 ## 前置依赖 - **docx** (`npm install -g docx`): 新文档创建 - **LibreOffice** (`soffice`): `.doc` -> `.docx` 转换、修订接受和 PDF 导出 - **pandoc**: 文本提取 - **pdftoppm** (poppler-utils): 文档转图片工作流 - 如果 `pdftoppm` 不可用,Python 备用路径可能使用 `pdf2image`。 - 在 Windows 上,依赖项必须已安装并在 `PATH` 中可用;如缺失,请报告依赖问题并停止(不要反复重试)。 ## 概述 .docx 文件是包含 XML 文件的 ZIP 压缩包。 ## 快速参考 | 任务 | 方法 | |------|------| | 读取/分析内容 | `pandoc` 或解压获取原始 XML | | 创建新文档 | 使用 `docx-js` - 参见下方"创建新文档" | | 编辑现有文档 | 解压 → 编辑 XML → 重新打包 - 参见下方"编辑现有文档" | ### 将 .doc 转换为 .docx 旧版 `.doc` 文件必须先转换才能编辑: ```bash python scripts/office/soffice.py --headless --convert-to docx document.doc ``` ### 读取内容 ```bash # 提取包含修订的文本 pandoc --track-changes=all document.docx -o output.md # 访问原始 XML python scripts/office/unpack.py document.docx unpacked/ ``` ### 转换为图片 ```bash python scripts/office/soffice.py --headless --convert-to pdf document.docx pdftoppm -jpeg -r 150 document.pdf page ``` ### 接受修订 生成接受所有修订后的干净文档(需要 LibreOffice): ```bash python scripts/accept_changes.py input.docx output.docx ``` --- ## 创建新文档 使用 JavaScript 生成 .docx 文件,然后进行验证。安装: `npm install -g docx` ### 初始设置 ```javascript const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType, VerticalAlign, PageNumber, PageBreak } = require('docx'); const doc = new Document({ sections: [{ children: [/* content */] }] }); Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); ``` ### 验证 创建文件后进行验证。如果验证失败,解压、修复 XML 并重新打包。 ```bash python scripts/office/validate.py doc.docx ``` ### 页面尺寸 ```javascript // 关键: docx-js 默认为 A4,而非 US Letter // 始终显式设置页面尺寸以获得一致的结果 sections: [{ properties: { page: { size: { width: 12240, // 8.5 英寸(DXA 单位) height: 15840 // 11 英寸(DXA 单位) }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 英寸边距 } }, children: [/* content */] }] ``` **常见页面尺寸(DXA 单位,1440 DXA = 1 英寸):** | 纸张 | 宽度 | 高度 | 内容宽度(1 英寸边距) | |------|------|------|----------------------| | US Letter | 12,240 | 15,840 | 9,360 | | A4(默认) | 11,906 | 16,838 | 9,026 | **横向方向:** docx-js 在内部会交换宽度/高度,因此传入纵向尺寸并让其处理交换: ```javascript size: { width: 12240, // 将短边作为 width 传入 height: 15840, // 将长边作为 height 传入 orientation: PageOrientation.LANDSCAPE // docx-js 会在 XML 中交换它们 }, // 内容宽度 = 15840 - 左边距 - 右边距(使用长边) ``` ### 样式(覆盖内置标题) 使用 Arial 作为默认字体(通用支持)。标题保持黑色以确保可读性。 ```javascript const doc = new Document({ styles: { default: { document: { run: { font: "Arial", size: 24 } } }, // 默认 12pt paragraphStyles: [ // 重要: 使用精确的 ID 来覆盖内置样式 { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 32, bold: true, font: "Arial" }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel 是目录所必需的 { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 28, bold: true, font: "Arial" }, paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, ] }, sections: [{ children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }), ] }] }); ``` ### 列表(绝对不要使用 Unicode 符号) ```javascript // ❌ 错误 - 绝不手动插入项目符号字符 new Paragraph({ children: [new TextRun("• Item")] }) // 错误 new Paragraph({ children: [new TextRun("\u2022 Item")] }) // 错误 // ✅ 正确 - 使用 LevelFormat.BULLET 的编号配置 const doc = new Document({ numbering: { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, { reference: "numbers", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, ] }, sections: [{ children: [ new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [new TextRun("Bullet item")] }), new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun("Numbered item")] }), ] }] }); // ⚠️ 每个 reference 创建独立的编号序列 // 相同 reference = 继续编号(1,2,3 然后 4,5,6) // 不同 reference = 重新开始(1,2,3 然后 1,2,3) ``` ### 表格 **关键: 表格需要双重宽度设置** - 必须同时在表格上设置 `columnWidths` 和在每个单元格上设置 `width`。缺少任一设置,表格在某些平台上会渲染不正确。 ```javascript // 关键: 始终设置表格宽度以确保一致的渲染效果 // 关键: 使用 ShadingType.CLEAR(而非 SOLID)以防止黑色背景 const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; const borders = { top: border, bottom: border, left: border, right: border }; new Table({ width: { size: 9360, type: WidthType.DXA }, // 始终使用 DXA(百分比在 Google Docs 中会出问题) columnWidths: [4680, 4680], // 必须加起来等于表格宽度(DXA: 1440 = 1 英寸) rows: [ new TableRow({ children: [ new TableCell({ borders, width: { size: 4680, type: WidthType.DXA }, // 同样需要在每个单元格上设置 shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // 用 CLEAR 而非 SOLID margins: { top: 80, bottom: 80, left: 120, right: 120 }, // 单元格内边距(内部的,不会增加宽度) children: [new Paragraph({ children: [new TextRun("Cell")] })] }) ] }) ] }) ``` **表格宽度计算:** 始终使用 `WidthType.DXA` -- `WidthType.PERCENTAGE` 在 Google Docs 中会出问题。 ```javascript // 表格宽度 = columnWidths 之和 = 内容宽度 // US Letter 配合 1 英寸边距: 12240 - 2880 = 9360 DXA width: { size: 9360, type: WidthType.DXA }, columnWidths: [7000, 2360] // 必须加起来等于表格宽度 ``` **宽度规则:** - **始终使用 `WidthType.DXA`** -- 绝不使用 `WidthType.PERCENTAGE`(与 Google Docs 不兼容) - 表格宽度必须等于 `columnWidths` 之和 - 单元格 `width` 必须与对应的 `columnWidth` 匹配 - 单元格 `margins` 是内部边距 - 它们减少内容区域,而不是增加单元格宽度 - 全宽表格: 使用内容宽度(页面宽度减去左右边距) ### 图片 ```javascript // 关键: type 参数是必需的 new Paragraph({ children: [new ImageRun({ type: "png", // 必需: png, jpg, jpeg, gif, bmp, svg data: fs.readFileSync("image.png"), transformation: { width: 200, height: 150 }, altText: { title: "Title", description: "Desc", name: "Name" } // 三个都是必需的 })] }) ``` ### 分页符 ```javascript // 关键: PageBreak 必须在 Paragraph 内部 new Paragraph({ children: [new PageBreak()] }) // 或者使用 pageBreakBefore new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] }) ``` ### 目录 ```javascript // 关键: 标题必须仅使用 HeadingLevel - 不使用自定义样式 new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }) ``` ### 页眉/页脚 ```javascript sections: [{ properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 英寸 }, headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] }) }, footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })] })] }) }, children: [/* content */] }] ``` ### docx-js 关键规则 - **显式设置页面尺寸** - docx-js 默认为 A4;美国文档使用 US Letter(12240 x 15840 DXA) - **横向: 传入纵向尺寸** - docx-js 在内部交换宽度/高度;将短边作为 `width`,长边作为 `height`,并设置 `orientation: PageOrientation.LANDSCAPE` - **绝不使用 `\n`** - 使用独立的 Paragraph 元素 - **绝不使用 Unicode 符号** - 使用 `LevelFormat.BULLET` 配合编号配置 - **PageBreak 必须在 Paragraph 内** - 独立使用会创建无效的 XML - **ImageRun 需要 `type`** - 始终指定 png/jpg 等 - **始终使用 DXA 设置表格 `width`** - 绝不使用 `WidthType.PERCENTAGE`(在 Google Docs 中会出问题) - **表格需要双重宽度** - `columnWidths` 数组和单元格 `width`,两者必须匹配 - **表格宽度 = columnWidths 之和** - 对于 DXA,确保它们精确相加 - **始终添加单元格边距** - 使用 `margins: { top: 80, bottom: 80, left: 120, right: 120 }` 获得可读的内边距 - **使用 `ShadingType.CLEAR`** - 表格底纹绝不使用 SOLID - **目录需要仅使用 HeadingLevel** - 标题段落不使用自定义样式 - **覆盖内置样式** - 使用精确的 ID: "Heading1"、"Heading2" 等 - **包含 `outlineLevel`** - 目录所必需(H1 为 0,H2 为 1,依此类推) --- ## 编辑现有文档 **按顺序执行以下 3 个步骤。** ### 第 1 步: 解压 ```bash python scripts/office/unpack.py document.docx unpacked/ ``` 提取 XML,进行格式化打印,合并相邻的 run,并将智能引号转换为 XML 实体(`“` 等)以便在编辑过程中保留。使用 `--merge-runs false` 跳过 run 合并。 ### 第 2 步: 编辑 XML 编辑 `unpacked/word/` 中的文件。参见下方的 XML 参考获取相关模式。 **使用 "Claude" 作为作者**来添加修订和批注,除非用户明确要求使用其他名称。 **直接使用 Edit 工具进行字符串替换。不要编写 Python 脚本。**脚本会引入不必要的复杂性。Edit 工具能准确显示被替换的内容。 **关键: 新内容使用智能引号。**添加包含撇号或引号的文本时,使用 XML 实体生成智能引号: ```xml Here’s a quote: “Hello” ``` | 实体 | 字符 | |------|------| | `‘` | ' (左单引号) | | `’` | ' (右单引号 / 撇号) | | `“` | " (左双引号) | | `”` | " (右双引号) | **添加批注:** 使用 `comment.py` 处理跨多个 XML 文件的样板代码(文本必须是预转义的 XML): ```bash python scripts/comment.py unpacked/ 0 "Comment text with & and ’" python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # 回复批注 0 python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # 自定义作者名 ``` 然后在 document.xml 中添加标记(参见 XML 参考中的批注部分)。 ### 第 3 步: 打包 ```bash python scripts/office/pack.py unpacked/ output.docx --original document.docx ``` 进行验证并自动修复,压缩 XML,然后创建 DOCX。使用 `--validate false` 跳过验证。 **自动修复可以修复的问题:** - `durableId` >= 0x7FFFFFFF(重新生成有效 ID) - `` 上缺少的 `xml:space="preserve"`(当有空白字符时) **自动修复无法修复的问题:** - 格式错误的 XML、无效的元素嵌套、缺失的关系、架构违规 ### 常见陷阱 - **替换整个 `` 元素**: 添加修订时,将整个 `...` 块替换为作为兄弟元素的 `......`。不要在 run 内部注入修订标签。 - **保留 `` 格式**: 将原始 run 的 `` 块复制到修订 run 中,以保持粗体、字号等格式。 --- ## XML 参考 ### 架构合规性 - **`` 中的元素顺序**: ``、``、``、``、``、`` 在最后 - **空白字符**: 在含有前导/尾随空格的 `` 上添加 `xml:space="preserve"` - **RSIDs**: 必须是 8 位十六进制数(例如 `00AB1234`) ### 修订 **插入:** ```xml inserted text ``` **删除:** ```xml deleted text ``` **在 `` 内部**: 使用 `` 替代 ``,使用 `` 替代 ``。 **最小化编辑** - 只标记变更的部分: ```xml The term is 30 60 days. ``` **删除整个段落/列表项** - 当删除段落中的所有内容时,还需将段落标记标记为已删除,使其与下一段落合并。在 `` 中添加 ``: ```xml ... Entire paragraph content being deleted... ``` 如果 `` 中没有 ``,接受修订后会留下空段落/列表项。 **拒绝其他作者的插入** - 将删除嵌套在其插入内部: ```xml their inserted text ``` **恢复其他作者的删除** - 在之后添加插入(不修改其删除标记): ```xml deleted text deleted text ``` ### 批注 运行 `comment.py`(参见第 2 步)后,在 document.xml 中添加标记。对于回复,使用 `--parent` 标志并将标记嵌套在父批注内部。 **关键: `` 和 `` 是 `` 的兄弟元素,绝不在 `` 内部。** ```xml deleted more text text ``` ### 图片 1. 将图片文件添加到 `word/media/` 2. 在 `word/_rels/document.xml.rels` 中添加关系: ```xml ``` 3. 在 `[Content_Types].xml` 中添加内容类型: ```xml ``` 4. 在 document.xml 中引用: ```xml ```