| 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 文件必须先转换才能编辑:
python scripts/office/soffice.py --headless --convert-to docx document.doc
读取内容
pandoc --track-changes=all document.docx -o output.md
python scripts/office/unpack.py document.docx unpacked/
转换为图片
python scripts/office/soffice.py --headless --convert-to pdf document.docx
pdftoppm -jpeg -r 150 document.pdf page
接受修订
生成接受所有修订后的干净文档(需要 LibreOffice):
python scripts/accept_changes.py input.docx output.docx
创建新文档
使用 JavaScript 生成 .docx 文件,然后进行验证。安装: npm install -g docx
初始设置
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 并重新打包。
python scripts/office/validate.py doc.docx
页面尺寸
sections: [{
properties: {
page: {
size: {
width: 12240,
height: 15840
},
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
}
},
children: []
}]
常见页面尺寸(DXA 单位,1440 DXA = 1 英寸):
| 纸张 | 宽度 | 高度 | 内容宽度(1 英寸边距) |
|---|
| US Letter | 12,240 | 15,840 | 9,360 |
| A4(默认) | 11,906 | 16,838 | 9,026 |
横向方向: docx-js 在内部会交换宽度/高度,因此传入纵向尺寸并让其处理交换:
size: {
width: 12240,
height: 15840,
orientation: PageOrientation.LANDSCAPE
},
样式(覆盖内置标题)
使用 Arial 作为默认字体(通用支持)。标题保持黑色以确保可读性。
const doc = new Document({
styles: {
default: { document: { run: { font: "Arial", size: 24 } } },
paragraphStyles: [
{ 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 } },
{ 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 符号)
new Paragraph({ children: [new TextRun("• Item")] })
new Paragraph({ children: [new TextRun("\u2022 Item")] })
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")] }),
]
}]
});
表格
关键: 表格需要双重宽度设置 - 必须同时在表格上设置 columnWidths 和在每个单元格上设置 width。缺少任一设置,表格在某些平台上会渲染不正确。
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 },
columnWidths: [4680, 4680],
rows: [
new TableRow({
children: [
new TableCell({
borders,
width: { size: 4680, type: WidthType.DXA },
shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
margins: { top: 80, bottom: 80, left: 120, right: 120 },
children: [new Paragraph({ children: [new TextRun("Cell")] })]
})
]
})
]
})
表格宽度计算:
始终使用 WidthType.DXA -- WidthType.PERCENTAGE 在 Google Docs 中会出问题。
width: { size: 9360, type: WidthType.DXA },
columnWidths: [7000, 2360]
宽度规则:
- 始终使用
WidthType.DXA -- 绝不使用 WidthType.PERCENTAGE(与 Google Docs 不兼容)
- 表格宽度必须等于
columnWidths 之和
- 单元格
width 必须与对应的 columnWidth 匹配
- 单元格
margins 是内部边距 - 它们减少内容区域,而不是增加单元格宽度
- 全宽表格: 使用内容宽度(页面宽度减去左右边距)
图片
new Paragraph({
children: [new ImageRun({
type: "png",
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150 },
altText: { title: "Title", description: "Desc", name: "Name" }
})]
})
分页符
new Paragraph({ children: [new PageBreak()] })
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
目录
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
页眉/页脚
sections: [{
properties: {
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } }
},
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: []
}]
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 步: 解压
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 实体生成智能引号:
<w:t>Here’s a quote: “Hello”</w:t>
| 实体 | 字符 |
|---|
‘ | ' (左单引号) |
’ | ' (右单引号 / 撇号) |
“ | " (左双引号) |
” | " (右双引号) |
添加批注: 使用 comment.py 处理跨多个 XML 文件的样板代码(文本必须是预转义的 XML):
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author"
然后在 document.xml 中添加标记(参见 XML 参考中的批注部分)。
第 3 步: 打包
python scripts/office/pack.py unpacked/ output.docx --original document.docx
进行验证并自动修复,压缩 XML,然后创建 DOCX。使用 --validate false 跳过验证。
自动修复可以修复的问题:
durableId >= 0x7FFFFFFF(重新生成有效 ID)
<w:t> 上缺少的 xml:space="preserve"(当有空白字符时)
自动修复无法修复的问题:
- 格式错误的 XML、无效的元素嵌套、缺失的关系、架构违规
常见陷阱
- 替换整个
<w:r> 元素: 添加修订时,将整个 <w:r>...</w:r> 块替换为作为兄弟元素的 <w:del>...<w:ins>...。不要在 run 内部注入修订标签。
- 保留
<w:rPr> 格式: 将原始 run 的 <w:rPr> 块复制到修订 run 中,以保持粗体、字号等格式。
XML 参考
架构合规性
<w:pPr> 中的元素顺序: <w:pStyle>、<w:numPr>、<w:spacing>、<w:ind>、<w:jc>、<w:rPr> 在最后
- 空白字符: 在含有前导/尾随空格的
<w:t> 上添加 xml:space="preserve"
- RSIDs: 必须是 8 位十六进制数(例如
00AB1234)
修订
插入:
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>
删除:
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
在 <w:del> 内部: 使用 <w:delText> 替代 <w:t>,使用 <w:delInstrText> 替代 <w:instrText>。
最小化编辑 - 只标记变更的部分:
<w:r><w:t>The term is </w:t></w:r>
<w:del w:id="1" w:author="Claude" w:date="...">
<w:r><w:delText>30</w:delText></w:r>
</w:del>
<w:ins w:id="2" w:author="Claude" w:date="...">
<w:r><w:t>60</w:t></w:r>
</w:ins>
<w:r><w:t> days.</w:t></w:r>
删除整个段落/列表项 - 当删除段落中的所有内容时,还需将段落标记标记为已删除,使其与下一段落合并。在 <w:pPr><w:rPr> 中添加 <w:del/>:
<w:p>
<w:pPr>
<w:numPr>...</w:numPr>
<w:rPr>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z"/>
</w:rPr>
</w:pPr>
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>Entire paragraph content being deleted...</w:delText></w:r>
</w:del>
</w:p>
如果 <w:pPr><w:rPr> 中没有 <w:del/>,接受修订后会留下空段落/列表项。
拒绝其他作者的插入 - 将删除嵌套在其插入内部:
<w:ins w:author="Jane" w:id="5">
<w:del w:author="Claude" w:id="10">
<w:r><w:delText>their inserted text</w:delText></w:r>
</w:del>
</w:ins>
恢复其他作者的删除 - 在之后添加插入(不修改其删除标记):
<w:del w:author="Jane" w:id="5">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="10">
<w:r><w:t>deleted text</w:t></w:r>
</w:ins>
批注
运行 comment.py(参见第 2 步)后,在 document.xml 中添加标记。对于回复,使用 --parent 标志并将标记嵌套在父批注内部。
关键: <w:commentRangeStart> 和 <w:commentRangeEnd> 是 <w:r> 的兄弟元素,绝不在 <w:r> 内部。
<w:commentRangeStart w:id="0"/>
<w:del w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
<w:r><w:delText>deleted</w:delText></w:r>
</w:del>
<w:r><w:t> more text</w:t></w:r>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:commentRangeStart w:id="0"/>
<w:commentRangeStart w:id="1"/>
<w:r><w:t>text</w:t></w:r>
<w:commentRangeEnd w:id="1"/>
<w:commentRangeEnd w:id="0"/>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="0"/></w:r>
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="1"/></w:r>
图片
- 将图片文件添加到
word/media/
- 在
word/_rels/document.xml.rels 中添加关系:
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
- 在
[Content_Types].xml 中添加内容类型:
<Default Extension="png" ContentType="image/png"/>
- 在 document.xml 中引用:
<w:drawing>
<wp:inline>
<wp:extent cx="914400" cy="914400"/>
<a:graphic>
<a:graphicData uri=".../picture">
<pic:pic>
<pic:blipFill><a:blip r:embed="rId5"/></pic:blipFill>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>