--- name: docx description: "当用户想要创建、读取、编辑或操作Word文档(.docx文件)时使用此技能。触发条件包括:任何提及\"Word文档\"、\"word document\"、\".docx\",或请求生成具有格式化(如目录、标题、页码或信头)的专业文档。也用于从.docx文件中提取或重新组织内容,在文档中插入或替换图像,在Word文件中执行查找和替换,处理修订或批注,或将内容转换为精美的Word文档。如果用户要求\"报告\"、\"备忘录\"、\"信函\"、\"模板\"或类似交付物作为Word或.docx文件,请使用此技能。不要用于PDF、电子表格、Google文档或与文档生成无关的一般编码任务。" license: 完整条款见LICENSE.txt --- # DOCX 创建、编辑和分析 ## 概述 .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: [/* 内容 */] }] }); 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: [/* 内容 */] }] ``` **常见页面大小(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, // 将短边作为宽度传递 height: 15840, // 将长边作为高度传递 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("标题")] }), ] }] }); ``` ### 列表(绝不使用unicode项目符号) ```javascript // ❌ 错误 - 永远不要手动插入项目符号字符 new Paragraph({ children: [new TextRun("• 项目")] }) // 坏 new Paragraph({ children: [new TextRun("\u2022 项目")] }) // 坏 // ✅ 正确 - 使用带有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("项目符号项")] }), new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun("编号项")] }), ] }] }); // ⚠️ 每个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("单元格")] })] }) ] }) ] }) ``` **表格宽度计算:** 始终使用`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: "标题", description: "描述", name: "名称" } // 所有三个都是必需的 })] }) ``` ### 分页符 ```javascript // 关键:PageBreak必须在Paragraph内部 new Paragraph({ children: [new PageBreak()] }) // 或使用pageBreakBefore new Paragraph({ pageBreakBefore: true, children: [new TextRun("新页面")] }) ``` ### 目录 ```javascript // 关键:标题必须仅使用HeadingLevel - 没有自定义样式 new TableOfContents("目录", { 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("页眉")] })] }) }, footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("页码 "), new TextRun({ children: [PageNumber.CURRENT] })] })] }) }, children: [/* 内容 */] }] ``` ### docx-js的关键规则 - **显式设置页面大小** - docx-js默认为A4;对于US文档使用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用于表格着色 - **TOC仅需要HeadingLevel** - 标题段落上没有自定义样式 - **覆盖内置样式** - 使用精确ID:"Heading1"、"Heading2"等 - **包含`outlineLevel`** - 目录所需(H1为0,H2为1等) --- ## 编辑现有文档 **按顺序遵循所有3个步骤。** ### 步骤1:解包 ```bash python scripts/office/unpack.py document.docx unpacked/ ``` 提取XML,美化打印,合并相邻的运行,并将智能引号转换为XML实体(`“`等),以便它们在编辑中幸存。使用`--merge-runs false`跳过运行合并。 ### 步骤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 "带有&和’的批注文本" python scripts/comment.py unpacked/ 1 "回复文本" --parent 0 # 回复批注0 python scripts/comment.py unpacked/ 0 "文本" --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、无效的元素嵌套、缺少的关系、模式违规 ### 常见陷阱 - **替换整个``元素**:添加修订时,用`......`作为兄弟替换整个`...`块。不要在运行中注入修订标记。 - **保留``格式化**:将原始运行的``块复制到修订运行中以保持粗体、字体大小等。 --- ## XML参考 ### 模式合规性 - **``中的元素顺序**:``、``、``、``、``、``最后 - **空白**:将`xml:space="preserve"`添加到带有前导/尾随空白的`` - **RSIDs**:必须是8位十六进制(例如:`00AB1234`) ### 修订 **插入:** ```xml 插入的文本 ``` **删除:** ```xml 删除的文本 ``` **在``内部**:使用``而不是``,并使用``而不是``。 **最小编辑** - 仅标记更改的内容: ```xml 期限是 30 60 天。 ``` **删除整个段落/列表项** - 从段落中删除所有内容时,也将段落标记标记为已删除,以便它与下一段落合并。在``内添加``: ```xml ... 正在删除的整个段落内容... ``` 如果没有``中的``,接受更改会留下一个空段落/列表项。 **拒绝另一个作者的插入** - 在他们的插入内嵌套删除: ```xml 他们插入的文本 ``` **恢复另一个作者的删除** - 在删除后添加插入(不要修改他们的删除): ```xml 删除的文本 删除的文本 ``` ### 批注 运行`comment.py`后(见步骤2),将标记添加到document.xml。对于回复,使用`--parent`标志并将标记嵌套在父标记内。 **关键:``和``是``的兄弟,永远不在``内部。** ```xml 已删除 更多文本 文本 ``` ### 图像 1. 将图像文件添加到`word/media/` 2. 将关系添加到`word/_rels/document.xml.rels`: ```xml ``` 3. 将内容类型添加到`[Content_Types].xml`: ```xml ``` 4. 在document.xml中引用: ```xml ``` --- ## 依赖项 - **pandoc**:文本提取 - **docx**:`npm install -g docx`(新文档) - **LibreOffice**:PDF转换(通过`scripts/office/soffice.py`为沙盒环境自动配置) - **Poppler**:`pdftoppm`用于图像