Back to skills
SkillHub ClubShip Full StackFull Stack

docx

全面的文档创建、编辑和分析功能,支持修订追踪、批注、格式保留和文本提取。当 Claude 需要处理专业文档(.docx 文件)时使用:(1) 创建新文档,(2) 修改或编辑内容,(3) 处理修订追踪,(4) 添加批注,或其他任何文档任务

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
271
Hot score
98
Updated
March 20, 2026
Overall rating
C4.2
Composite score
4.2
Best-practice grade
F37.6

Install command

npx @skill-hub/cli install leastbit-claude-skills-zh-cn-docx

Repository

LeastBit/Claude_skills_zh-CN

Skill path: skills/docx

全面的文档创建、编辑和分析功能,支持修订追踪、批注、格式保留和文本提取。当 Claude 需要处理专业文档(.docx 文件)时使用:(1) 创建新文档,(2) 修改或编辑内容,(3) 处理修订追踪,(4) 添加批注,或其他任何文档任务

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Proprietary. LICENSE.txt has complete terms.

Original source

Catalog source: SkillHub Club.

Repository owner: LeastBit.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install docx into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/LeastBit/Claude_skills_zh-CN before adding docx to shared team environments
  • Use docx for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: docx
description: "全面的文档创建、编辑和分析功能,支持修订追踪、批注、格式保留和文本提取。当 Claude 需要处理专业文档(.docx 文件)时使用:(1) 创建新文档,(2) 修改或编辑内容,(3) 处理修订追踪,(4) 添加批注,或其他任何文档任务"
license: Proprietary. LICENSE.txt has complete terms
---

# DOCX 创建、编辑和分析

## 概述

用户可能会要求您创建、编辑或分析 .docx 文件的内容。.docx 文件本质上是一个包含 XML 文件和其他资源的 ZIP 压缩包,您可以读取或编辑这些内容。针对不同任务,您有不同的工具和工作流程可用。

## 工作流程决策树

### 读取/分析内容
使用下方的"文本提取"或"原始 XML 访问"章节

### 创建新文档
使用"创建新 Word 文档"工作流程

### 编辑现有文档
- **您自己的文档 + 简单修改**
  使用"基础 OOXML 编辑"工作流程

- **他人的文档**
  使用 **"红线批注工作流程"**(推荐默认)

- **法律、学术、商业或政府文档**
  使用 **"红线批注工作流程"**(必须)

## 读取和分析内容

### 文本提取
如果您只需要读取文档的文本内容,应使用 pandoc 将文档转换为 markdown。Pandoc 能够很好地保留文档结构,并能显示修订追踪:

```bash
# 将文档转换为 markdown 并保留修订追踪
pandoc --track-changes=all path-to-file.docx -o output.md
# 选项:--track-changes=accept/reject/all
```

### 原始 XML 访问
以下功能需要原始 XML 访问:批注、复杂格式、文档结构、嵌入式媒体和元数据。对于这些功能,您需要解包文档并读取其原始 XML 内容。

#### 解包文件
`python ooxml/scripts/unpack.py <office_file> <output_directory>`

#### 关键文件结构
* `word/document.xml` - 主文档内容
* `word/comments.xml` - document.xml 中引用的批注
* `word/media/` - 嵌入的图片和媒体文件
* 修订追踪使用 `<w:ins>`(插入)和 `<w:del>`(删除)标签

## 创建新 Word 文档

从头创建新 Word 文档时,使用 **docx-js**,它允许您使用 JavaScript/TypeScript 创建 Word 文档。

### 工作流程
1. **必须 - 完整阅读文件**:从头到尾完整阅读 [`docx-js.md`](docx-js.md)(约 500 行)。**读取此文件时切勿设置任何范围限制。** 在开始创建文档之前,完整阅读文件内容以了解详细语法、关键格式规则和最佳实践。
2. 使用 Document、Paragraph、TextRun 组件创建 JavaScript/TypeScript 文件(您可以假设所有依赖项已安装,如果没有,请参阅下方的依赖项章节)
3. 使用 Packer.toBuffer() 导出为 .docx

## 编辑现有 Word 文档

编辑现有 Word 文档时,使用 **Document 库**(用于 OOXML 操作的 Python 库)。该库自动处理基础设施设置,并提供文档操作方法。对于复杂场景,您可以通过该库直接访问底层 DOM。

### 工作流程
1. **必须 - 完整阅读文件**:从头到尾完整阅读 [`ooxml.md`](ooxml.md)(约 600 行)。**读取此文件时切勿设置任何范围限制。** 完整阅读文件内容以了解 Document 库 API 和直接编辑文档文件的 XML 模式。
2. 解包文档:`python ooxml/scripts/unpack.py <office_file> <output_directory>`
3. 使用 Document 库创建并运行 Python 脚本(参见 ooxml.md 中的"Document 库"章节)
4. 打包最终文档:`python ooxml/scripts/pack.py <input_directory> <office_file>`

Document 库提供用于常见操作的高级方法和用于复杂场景的直接 DOM 访问。

## 文档审阅的红线批注工作流程

此工作流程允许您在使用 markdown 规划全面的修订追踪后,在 OOXML 中实现这些修改。**关键**:要实现完整的修订追踪,您必须系统地实现所有修改。

**批处理策略**:将相关修改分组为 3-10 个修改一批。这使调试更易管理,同时保持效率。在进入下一批之前测试每一批。

**原则:最小化、精确的编辑**
实现修订追踪时,只标记实际更改的文本。重复未更改的文本会使编辑更难审阅,看起来也不专业。将替换分解为:[未更改文本] + [删除] + [插入] + [未更改文本]。通过提取原始的 `<w:r>` 元素并重用它,保留未更改文本的原始 RSID。

示例 - 将句子中的"30 days"改为"60 days":
```python
# 错误 - 替换整个句子
'<w:del><w:r><w:delText>The term is 30 days.</w:delText></w:r></w:del><w:ins><w:r><w:t>The term is 60 days.</w:t></w:r></w:ins>'

# 正确 - 只标记更改的部分,保留原始 <w:r> 的未更改文本
'<w:r w:rsidR="00AB12CD"><w:t>The term is </w:t></w:r><w:del><w:r><w:delText>30</w:delText></w:r></w:del><w:ins><w:r><w:t>60</w:t></w:r></w:ins><w:r w:rsidR="00AB12CD"><w:t> days.</w:t></w:r>'
```

### 修订追踪工作流程

1. **获取 markdown 表示**:将文档转换为保留修订追踪的 markdown:
   ```bash
   pandoc --track-changes=all path-to-file.docx -o current.md
   ```

2. **识别和分组修改**:审阅文档并识别所有需要的修改,将它们组织成逻辑批次:

   **定位方法**(用于在 XML 中查找修改):
   - 章节/标题编号(例如"Section 3.2"、"Article IV")
   - 段落标识符(如果有编号)
   - 使用唯一周围文本的 Grep 模式
   - 文档结构(例如"第一段"、"签名区块")
   - **不要使用 markdown 行号** - 它们不对应 XML 结构

   **批次组织**(每批分组 3-10 个相关修改):
   - 按章节:"批次 1:第 2 节修订"、"批次 2:第 5 节更新"
   - 按类型:"批次 1:日期更正"、"批次 2:当事人名称更改"
   - 按复杂度:从简单的文本替换开始,然后处理复杂的结构性更改
   - 按顺序:"批次 1:第 1-3 页"、"批次 2:第 4-6 页"

3. **阅读文档并解包**:
   - **必须 - 完整阅读文件**:从头到尾完整阅读 [`ooxml.md`](ooxml.md)(约 600 行)。**读取此文件时切勿设置任何范围限制。** 特别注意"Document 库"和"修订追踪模式"章节。
   - **解包文档**:`python ooxml/scripts/unpack.py <file.docx> <dir>`
   - **注意建议的 RSID**:解包脚本会建议一个用于修订追踪的 RSID。复制此 RSID 用于步骤 4b。

4. **批量实现修改**:按逻辑分组修改(按章节、按类型或按相近位置),并在单个脚本中一起实现它们。这种方法:
   - 使调试更容易(较小的批次 = 更容易隔离错误)
   - 允许渐进式进展
   - 保持效率(3-10 个修改的批次大小效果良好)

   **建议的批次分组:**
   - 按文档章节(例如"第 3 节修改"、"定义"、"终止条款")
   - 按修改类型(例如"日期修改"、"当事人名称更新"、"法律术语替换")
   - 按相近位置(例如"第 1-3 页的修改"、"文档前半部分的修改")

   对于每批相关修改:

   **a. 将文本映射到 XML**:在 `word/document.xml` 中使用 Grep 验证文本如何跨 `<w:r>` 元素分割。

   **b. 创建并运行脚本**:使用 `get_node` 查找节点,实现修改,然后 `doc.save()`。参见 ooxml.md 中的 **"Document 库"** 章节的模式。

   **注意**:在编写脚本之前,始终立即 grep `word/document.xml` 以获取当前行号并验证文本内容。每次脚本运行后行号都会改变。

5. **打包文档**:所有批次完成后,将解包的目录转换回 .docx:
   ```bash
   python ooxml/scripts/pack.py unpacked reviewed-document.docx
   ```

6. **最终验证**:对完整文档进行全面检查:
   - 将最终文档转换为 markdown:
     ```bash
     pandoc --track-changes=all reviewed-document.docx -o verification.md
     ```
   - 验证所有修改已正确应用:
     ```bash
     grep "original phrase" verification.md  # 应该找不到
     grep "replacement phrase" verification.md  # 应该找到
     ```
   - 检查是否引入了意外的修改


## 将文档转换为图片

要可视化分析 Word 文档,使用两步过程将其转换为图片:

1. **将 DOCX 转换为 PDF**:
   ```bash
   soffice --headless --convert-to pdf document.docx
   ```

2. **将 PDF 页面转换为 JPEG 图片**:
   ```bash
   pdftoppm -jpeg -r 150 document.pdf page
   ```
   这将创建 `page-1.jpg`、`page-2.jpg` 等文件。

选项:
- `-r 150`:设置分辨率为 150 DPI(根据质量/大小平衡调整)
- `-jpeg`:输出 JPEG 格式(如果首选 PNG 则使用 `-png`)
- `-f N`:要转换的第一页(例如 `-f 2` 从第 2 页开始)
- `-l N`:要转换的最后一页(例如 `-l 5` 在第 5 页停止)
- `page`:输出文件的前缀

指定范围的示例:
```bash
pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page  # 只转换第 2-5 页
```

## 代码风格指南
**重要**:生成 DOCX 操作代码时:
- 编写简洁的代码
- 避免冗长的变量名和冗余操作
- 避免不必要的 print 语句

## 依赖项

必需的依赖项(如果不可用则安装):

- **pandoc**:`sudo apt-get install pandoc`(用于文本提取)
- **docx**:`npm install -g docx`(用于创建新文档)
- **LibreOffice**:`sudo apt-get install libreoffice`(用于 PDF 转换)
- **Poppler**:`sudo apt-get install poppler-utils`(用于 pdftoppm 将 PDF 转换为图片)
- **defusedxml**:`pip install defusedxml`(用于安全的 XML 解析)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### docx-js.md

```markdown
# DOCX 库教程

使用 JavaScript/TypeScript 生成 .docx 文件。

**重要:开始之前请阅读完整文档。** 关键的格式规则和常见陷阱贯穿全文 - 跳过章节可能导致文件损坏或渲染问题。

## 设置
假设 docx 已全局安装
如果未安装:`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');

// 创建和保存
const doc = new Document({ sections: [{ children: [/* 内容 */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js
Packer.toBlob(doc).then(blob => { /* 下载逻辑 */ }); // 浏览器
```

## 文本和格式
```javascript
// 重要:永远不要使用 \n 换行 - 始终使用单独的 Paragraph 元素
// ❌ 错误:new TextRun("第一行\n第二行")
// ✅ 正确:new Paragraph({ children: [new TextRun("第一行")] }), new Paragraph({ children: [new TextRun("第二行")] })

// 包含所有格式选项的基本文本
new Paragraph({
  alignment: AlignmentType.CENTER,
  spacing: { before: 200, after: 200 },
  indent: { left: 720, right: 720 },
  children: [
    new TextRun({ text: "粗体", bold: true }),
    new TextRun({ text: "斜体", italics: true }),
    new TextRun({ text: "下划线", underline: { type: UnderlineType.DOUBLE, color: "FF0000" } }),
    new TextRun({ text: "彩色", color: "FF0000", size: 28, font: "Arial" }), // Arial 默认
    new TextRun({ text: "高亮", highlight: "yellow" }),
    new TextRun({ text: "删除线", strike: true }),
    new TextRun({ text: "x2", superScript: true }),
    new TextRun({ text: "H2O", subScript: true }),
    new TextRun({ text: "小型大写", smallCaps: true }),
    new SymbolRun({ char: "2022", font: "Symbol" }), // 项目符号 •
    new SymbolRun({ char: "00A9", font: "Arial" })   // 版权符号 © - 使用 Arial 字体
  ]
})
```

## 样式和专业格式

```javascript
const doc = new Document({
  styles: {
    default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt 默认
    paragraphStyles: [
      // 文档标题样式 - 覆盖内置 Title 样式
      { id: "Title", name: "Title", basedOn: "Normal",
        run: { size: 56, bold: true, color: "000000", font: "Arial" },
        paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } },
      // 重要:通过使用精确的 ID 覆盖内置标题样式
      { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 32, bold: true, color: "000000", font: "Arial" }, // 16pt
        paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // TOC 必需
      { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
        run: { size: 28, bold: true, color: "000000", font: "Arial" }, // 14pt
        paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
      // 自定义样式使用你自己的 ID
      { id: "myStyle", name: "My Style", basedOn: "Normal",
        run: { size: 28, bold: true, color: "000000" },
        paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER } }
    ],
    characterStyles: [{ id: "myCharStyle", name: "My Char Style",
      run: { color: "FF0000", bold: true, underline: { type: UnderlineType.SINGLE } } }]
  },
  sections: [{
    properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } },
    children: [
      new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("文档标题")] }), // 使用覆盖的 Title 样式
      new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("标题 1")] }), // 使用覆盖的 Heading1 样式
      new Paragraph({ style: "myStyle", children: [new TextRun("自定义段落样式")] }),
      new Paragraph({ children: [
        new TextRun("普通文本带有"),
        new TextRun({ text: "自定义字符样式", style: "myCharStyle" })
      ]})
    ]
  }]
});
```

**专业字体组合:**
- **Arial(标题)+ Arial(正文)** - 最广泛支持,简洁专业
- **Times New Roman(标题)+ Arial(正文)** - 经典衬线标题配现代无衬线正文
- **Georgia(标题)+ Verdana(正文)** - 屏幕阅读优化,优雅对比

**关键样式原则:**
- **覆盖内置样式**:使用精确的 ID 如 "Heading1"、"Heading2"、"Heading3" 来覆盖 Word 的内置标题样式
- **HeadingLevel 常量**:`HeadingLevel.HEADING_1` 使用 "Heading1" 样式,`HeadingLevel.HEADING_2` 使用 "Heading2" 样式,等等
- **包含 outlineLevel**:H1 设置 `outlineLevel: 0`,H2 设置 `outlineLevel: 1`,等等,以确保目录正常工作
- **使用自定义样式**而非内联格式以保持一致性
- **设置默认字体**使用 `styles.default.document.run.font` - Arial 是通用支持的
- **建立视觉层次**使用不同字号(标题 > 子标题 > 正文)
- **添加适当间距**使用 `before` 和 `after` 段落间距
- **谨慎使用颜色**:标题默认使用黑色(000000)和灰色阴影(标题1、标题2等)
- **设置一致的边距**(1440 = 1 英寸是标准)


## 列表(始终使用正确的列表 - 永远不要使用 Unicode 项目符号)
```javascript
// 项目符号 - 始终使用 numbering 配置,而不是 unicode 符号
// 关键:使用 LevelFormat.BULLET 常量,而不是字符串 "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", // 不同的 reference = 从 1 重新开始
        levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
          style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }
    ]
  },
  sections: [{
    children: [
      // 项目符号列表项
      new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
        children: [new TextRun("第一个项目符号")] }),
      new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
        children: [new TextRun("第二个项目符号")] }),
      // 编号列表项
      new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
        children: [new TextRun("第一个编号项")] }),
      new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
        children: [new TextRun("第二个编号项")] }),
      // ⚠️ 关键:不同的 reference = 独立列表,从 1 重新开始
      // 相同的 reference = 继续之前的编号
      new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 },
        children: [new TextRun("从 1 重新开始(因为使用了不同的 reference)")] })
    ]
  }]
});

// ⚠️ 关键编号规则:每个 reference 创建一个独立的编号列表
// - 相同 reference = 继续编号(1, 2, 3... 然后 4, 5, 6...)
// - 不同 reference = 从 1 重新开始(1, 2, 3... 然后 1, 2, 3...)
// 为每个单独的编号部分使用唯一的 reference 名称!

// ⚠️ 关键:永远不要使用 unicode 项目符号 - 它们创建的假列表无法正常工作
// new TextRun("• 项目")           // 错误
// new SymbolRun({ char: "2022" }) // 错误
// ✅ 始终使用带有 LevelFormat.BULLET 的 numbering 配置来创建真正的 Word 列表
```

## 表格
```javascript
// 包含边距、边框、表头和项目符号的完整表格
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder };

new Table({
  columnWidths: [4680, 4680], // ⚠️ 关键:在表格级别设置列宽 - 值使用 DXA(点的二十分之一)
  margins: { top: 100, bottom: 100, left: 180, right: 180 }, // 为所有单元格统一设置
  rows: [
    new TableRow({
      tableHeader: true,
      children: [
        new TableCell({
          borders: cellBorders,
          width: { size: 4680, type: WidthType.DXA }, // 也在每个单元格上设置宽度
          // ⚠️ 关键:始终使用 ShadingType.CLEAR 以防止 Word 中出现黑色背景
          shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
          verticalAlign: VerticalAlign.CENTER,
          children: [new Paragraph({
            alignment: AlignmentType.CENTER,
            children: [new TextRun({ text: "表头", bold: true, size: 22 })]
          })]
        }),
        new TableCell({
          borders: cellBorders,
          width: { size: 4680, type: WidthType.DXA }, // 也在每个单元格上设置宽度
          shading: { fill: "D5E8F0", type: ShadingType.CLEAR },
          children: [new Paragraph({
            alignment: AlignmentType.CENTER,
            children: [new TextRun({ text: "项目符号", bold: true, size: 22 })]
          })]
        })
      ]
    }),
    new TableRow({
      children: [
        new TableCell({
          borders: cellBorders,
          width: { size: 4680, type: WidthType.DXA }, // 也在每个单元格上设置宽度
          children: [new Paragraph({ children: [new TextRun("常规数据")] })]
        }),
        new TableCell({
          borders: cellBorders,
          width: { size: 4680, type: WidthType.DXA }, // 也在每个单元格上设置宽度
          children: [
            new Paragraph({
              numbering: { reference: "bullet-list", level: 0 },
              children: [new TextRun("第一个项目符号")]
            }),
            new Paragraph({
              numbering: { reference: "bullet-list", level: 0 },
              children: [new TextRun("第二个项目符号")]
            })
          ]
        })
      ]
    })
  ]
})
```

**重要:表格宽度和边框**
- 同时使用 `columnWidths: [width1, width2, ...]` 数组和每个单元格上的 `width: { size: X, type: WidthType.DXA }`
- 值使用 DXA(点的二十分之一):1440 = 1 英寸,Letter 纸张可用宽度 = 9360 DXA(1 英寸边距)
- 将边框应用于单独的 `TableCell` 元素,而不是 `Table` 本身

**预计算的列宽(Letter 尺寸,1 英寸边距 = 总共 9360 DXA):**
- **2 列:** `columnWidths: [4680, 4680]`(等宽)
- **3 列:** `columnWidths: [3120, 3120, 3120]`(等宽)

## 链接和导航
```javascript
// 目录(需要标题)- 关键:只使用 HeadingLevel,不要使用自定义样式
// ❌ 错误:new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("标题")] })
// ✅ 正确:new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("标题")] })
new TableOfContents("目录", { hyperlink: true, headingStyleRange: "1-3" }),

// 外部链接
new Paragraph({
  children: [new ExternalHyperlink({
    children: [new TextRun({ text: "Google", style: "Hyperlink" })],
    link: "https://www.google.com"
  })]
}),

// 内部链接和书签
new Paragraph({
  children: [new InternalHyperlink({
    children: [new TextRun({ text: "跳转到章节", style: "Hyperlink" })],
    anchor: "section1"
  })]
}),
new Paragraph({
  children: [new TextRun("章节内容")],
  bookmark: { id: "section1", name: "section1" }
}),
```

## 图片和媒体
```javascript
// 带有尺寸和定位的基本图片
// 关键:始终指定 'type' 参数 - ImageRun 必需
new Paragraph({
  alignment: AlignmentType.CENTER,
  children: [new ImageRun({
    type: "png", // 新要求:必须指定图片类型(png, jpg, jpeg, gif, bmp, svg)
    data: fs.readFileSync("image.png"),
    transformation: { width: 200, height: 150, rotation: 0 }, // rotation 以度为单位
    altText: { title: "Logo", description: "公司标志", name: "Name" } // 重要:三个字段都是必需的
  })]
})
```

## 分页符
```javascript
// 手动分页符
new Paragraph({ children: [new PageBreak()] }),

// 段落前分页
new Paragraph({
  pageBreakBefore: true,
  children: [new TextRun("这将在新页面开始")]
})

// ⚠️ 关键:永远不要单独使用 PageBreak - 它会创建无效的 XML,Word 无法打开
// ❌ 错误:new PageBreak()
// ✅ 正确:new Paragraph({ children: [new PageBreak()] })
```

## 页眉/页脚和页面设置
```javascript
const doc = new Document({
  sections: [{
    properties: {
      page: {
        margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 英寸
        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("页眉文本")]
      })] })
    },
    footers: {
      default: new Footer({ children: [new Paragraph({
        alignment: AlignmentType.CENTER,
        children: [new TextRun("第 "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" 页,共 "), new TextRun({ children: [PageNumber.TOTAL_PAGES] }), new TextRun(" 页")]
      })] })
    },
    children: [/* 内容 */]
  }]
});
```

## 制表符
```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("左对齐\t居中\t右对齐")]
})
```

## 常量和快速参考
- **下划线:** `SINGLE`、`DOUBLE`、`WAVY`、`DASH`
- **边框:** `SINGLE`、`DOUBLE`、`DASHED`、`DOTTED`
- **编号:** `DECIMAL`(1,2,3)、`UPPER_ROMAN`(I,II,III)、`LOWER_LETTER`(a,b,c)
- **制表符:** `LEFT`、`CENTER`、`RIGHT`、`DECIMAL`
- **符号:** `"2022"`(•)、`"00A9"`(©)、`"00AE"`(®)、`"2122"`(™)、`"00B0"`(°)、`"F070"`(✓)、`"F0FC"`(✗)

## 关键问题和常见错误
- **关键:PageBreak 必须始终在 Paragraph 内部** - 单独的 PageBreak 会创建无效的 XML,Word 无法打开
- **始终对表格单元格着色使用 ShadingType.CLEAR** - 永远不要使用 ShadingType.SOLID(会导致黑色背景)
- 度量单位使用 DXA(1440 = 1 英寸)| 每个表格单元格需要 ≥1 个 Paragraph | 目录只需要 HeadingLevel 样式
- **始终使用自定义样式**配合 Arial 字体以获得专业外观和正确的视觉层次
- **始终设置默认字体**使用 `styles.default.document.run.font` - 推荐 Arial
- **始终对表格使用 columnWidths 数组** + 单独的单元格宽度以确保兼容性
- **永远不要使用 unicode 符号作为项目符号** - 始终使用带有 `LevelFormat.BULLET` 常量的正确 numbering 配置(不是字符串 "bullet")
- **永远不要在任何地方使用 \n 换行** - 始终使用单独的 Paragraph 元素作为每一行
- **始终在 Paragraph 的 children 中使用 TextRun 对象** - 永远不要直接在 Paragraph 上使用 text 属性
- **图片关键:** ImageRun 必须有 `type` 参数 - 始终指定 "png"、"jpg"、"jpeg"、"gif"、"bmp" 或 "svg"
- **项目符号关键:** 必须使用 `LevelFormat.BULLET` 常量,而不是字符串 "bullet",并包含 `text: "•"` 作为项目符号字符
- **编号关键:** 每个 numbering reference 创建一个独立的列表。相同 reference = 继续编号(1,2,3 然后 4,5,6)。不同 reference = 从 1 重新开始(1,2,3 然后 1,2,3)。为每个单独的编号部分使用唯一的 reference 名称!
- **目录关键:** 使用 TableOfContents 时,标题必须只使用 HeadingLevel - 不要向标题段落添加自定义样式,否则目录将无法正常工作
- **表格:** 设置 `columnWidths` 数组 + 单独的单元格宽度,将边框应用于单元格而不是表格
- **在表格级别设置表格边距**以获得一致的单元格内边距(避免每个单元格重复设置)

```

### ooxml.md

```markdown
# Office Open XML 技术参考

**重要:开始之前请阅读完整文档。** 本文档涵盖:
- [技术指南](#技术指南) - 架构合规规则和验证要求
- [文档内容模式](#文档内容模式) - 标题、列表、表格、格式等的 XML 模式
- [Document 库 (Python)](#document-库-python) - 推荐的 OOXML 操作方法,带自动基础设施设置
- [修订追踪(红线批注)](#修订追踪红线批注) - 实现修订追踪的 XML 模式

## 技术指南

### 架构合规
- **`<w:pPr>` 中的元素顺序**:`<w:pStyle>`、`<w:numPr>`、`<w:spacing>`、`<w:ind>`、`<w:jc>`
- **空白字符**:为包含前导/尾随空格的 `<w:t>` 元素添加 `xml:space='preserve'`
- **Unicode**:在 ASCII 内容中转义字符:`"` 变为 `&#8220;`
  - **字符编码参考**:弯引号 `""` 变为 `&#8220;&#8221;`,撇号 `'` 变为 `&#8217;`,破折号 `—` 变为 `&#8212;`
- **修订追踪**:在 `<w:r>` 元素外部使用带有 `w:author="Claude"` 的 `<w:del>` 和 `<w:ins>` 标签
  - **关键**:`<w:ins>` 以 `</w:ins>` 结束,`<w:del>` 以 `</w:del>` 结束 - 永远不要混淆
  - **RSID 必须是 8 位十六进制**:使用如 `00AB1234` 的值(仅限 0-9、A-F 字符)
  - **trackRevisions 位置**:在 settings.xml 中 `<w:proofState>` 之后添加 `<w:trackRevisions/>`
- **图片**:添加到 `word/media/`,在 `document.xml` 中引用,设置尺寸以防止溢出

## 文档内容模式

### 基本结构
```xml
<w:p>
  <w:r><w:t>文本内容</w:t></w:r>
</w:p>
```

### 标题和样式
```xml
<w:p>
  <w:pPr>
    <w:pStyle w:val="Title"/>
    <w:jc w:val="center"/>
  </w:pPr>
  <w:r><w:t>文档标题</w:t></w:r>
</w:p>

<w:p>
  <w:pPr><w:pStyle w:val="Heading2"/></w:pPr>
  <w:r><w:t>章节标题</w:t></w:r>
</w:p>
```

### 文本格式
```xml
<!-- 粗体 -->
<w:r><w:rPr><w:b/><w:bCs/></w:rPr><w:t>粗体</w:t></w:r>
<!-- 斜体 -->
<w:r><w:rPr><w:i/><w:iCs/></w:rPr><w:t>斜体</w:t></w:r>
<!-- 下划线 -->
<w:r><w:rPr><w:u w:val="single"/></w:rPr><w:t>下划线</w:t></w:r>
<!-- 高亮 -->
<w:r><w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>高亮</w:t></w:r>
```

### 列表
```xml
<!-- 编号列表 -->
<w:p>
  <w:pPr>
    <w:pStyle w:val="ListParagraph"/>
    <w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr>
    <w:spacing w:before="240"/>
  </w:pPr>
  <w:r><w:t>第一项</w:t></w:r>
</w:p>

<!-- 从 1 重新开始编号列表 - 使用不同的 numId -->
<w:p>
  <w:pPr>
    <w:pStyle w:val="ListParagraph"/>
    <w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr>
    <w:spacing w:before="240"/>
  </w:pPr>
  <w:r><w:t>新列表第 1 项</w:t></w:r>
</w:p>

<!-- 项目符号列表(级别 2) -->
<w:p>
  <w:pPr>
    <w:pStyle w:val="ListParagraph"/>
    <w:numPr><w:ilvl w:val="1"/><w:numId w:val="1"/></w:numPr>
    <w:spacing w:before="240"/>
    <w:ind w:left="900"/>
  </w:pPr>
  <w:r><w:t>项目符号项</w:t></w:r>
</w:p>
```

### 表格
```xml
<w:tbl>
  <w:tblPr>
    <w:tblStyle w:val="TableGrid"/>
    <w:tblW w:w="0" w:type="auto"/>
  </w:tblPr>
  <w:tblGrid>
    <w:gridCol w:w="4675"/><w:gridCol w:w="4675"/>
  </w:tblGrid>
  <w:tr>
    <w:tc>
      <w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
      <w:p><w:r><w:t>单元格 1</w:t></w:r></w:p>
    </w:tc>
    <w:tc>
      <w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
      <w:p><w:r><w:t>单元格 2</w:t></w:r></w:p>
    </w:tc>
  </w:tr>
</w:tbl>
```

### 布局
```xml
<!-- 新章节前分页(常见模式) -->
<w:p>
  <w:r>
    <w:br w:type="page"/>
  </w:r>
</w:p>
<w:p>
  <w:pPr>
    <w:pStyle w:val="Heading1"/>
  </w:pPr>
  <w:r>
    <w:t>新章节标题</w:t>
  </w:r>
</w:p>

<!-- 居中段落 -->
<w:p>
  <w:pPr>
    <w:spacing w:before="240" w:after="0"/>
    <w:jc w:val="center"/>
  </w:pPr>
  <w:r><w:t>居中文本</w:t></w:r>
</w:p>

<!-- 字体更改 - 段落级别(应用于所有 run) -->
<w:p>
  <w:pPr>
    <w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
  </w:pPr>
  <w:r><w:t>等宽文本</w:t></w:r>
</w:p>

<!-- 字体更改 - run 级别(仅适用于此文本) -->
<w:p>
  <w:r>
    <w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
    <w:t>这段文本是 Courier New</w:t>
  </w:r>
  <w:r><w:t> 而这段文本使用默认字体</w:t></w:r>
</w:p>
```

## 文件更新

添加内容时,更新以下文件:

**`word/_rels/document.xml.rels`:**
```xml
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
```

**`[Content_Types].xml`:**
```xml
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
```

### 图片
**关键**:计算尺寸以防止页面溢出并保持纵横比。

```xml
<!-- 最小必需结构 -->
<w:p>
  <w:r>
    <w:drawing>
      <wp:inline>
        <wp:extent cx="2743200" cy="1828800"/>
        <wp:docPr id="1" name="Picture 1"/>
        <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
          <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
              <pic:nvPicPr>
                <pic:cNvPr id="0" name="image1.png"/>
                <pic:cNvPicPr/>
              </pic:nvPicPr>
              <pic:blipFill>
                <a:blip r:embed="rId5"/>
                <!-- 添加以保持纵横比的拉伸填充 -->
                <a:stretch>
                  <a:fillRect/>
                </a:stretch>
              </pic:blipFill>
              <pic:spPr>
                <a:xfrm>
                  <a:ext cx="2743200" cy="1828800"/>
                </a:xfrm>
                <a:prstGeom prst="rect"/>
              </pic:spPr>
            </pic:pic>
          </a:graphicData>
        </a:graphic>
      </wp:inline>
    </w:drawing>
  </w:r>
</w:p>
```

### 链接(超链接)

**重要**:所有超链接(内部和外部)都需要在 styles.xml 中定义 Hyperlink 样式。没有此样式,链接将看起来像普通文本而不是蓝色下划线可点击链接。

**外部链接:**
```xml
<!-- 在 document.xml 中 -->
<w:hyperlink r:id="rId5">
  <w:r>
    <w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
    <w:t>链接文本</w:t>
  </w:r>
</w:hyperlink>

<!-- 在 word/_rels/document.xml.rels 中 -->
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
              Target="https://www.example.com/" TargetMode="External"/>
```

**内部链接:**

```xml
<!-- 链接到书签 -->
<w:hyperlink w:anchor="myBookmark">
  <w:r>
    <w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
    <w:t>链接文本</w:t>
  </w:r>
</w:hyperlink>

<!-- 书签目标 -->
<w:bookmarkStart w:id="0" w:name="myBookmark"/>
<w:r><w:t>目标内容</w:t></w:r>
<w:bookmarkEnd w:id="0"/>
```

**Hyperlink 样式(styles.xml 中必需):**
```xml
<w:style w:type="character" w:styleId="Hyperlink">
  <w:name w:val="Hyperlink"/>
  <w:basedOn w:val="DefaultParagraphFont"/>
  <w:uiPriority w:val="99"/>
  <w:unhideWhenUsed/>
  <w:rPr>
    <w:color w:val="467886" w:themeColor="hyperlink"/>
    <w:u w:val="single"/>
  </w:rPr>
</w:style>
```

## Document 库 (Python)

使用 `scripts/document.py` 中的 Document 类处理所有修订追踪和批注。它自动处理基础设施设置(people.xml、RSID、settings.xml、批注文件、关系、内容类型)。仅对库不支持的复杂场景使用直接 XML 操作。

**处理 Unicode 和实体:**
- **搜索**:实体表示法和 Unicode 字符都可用 - `contains="&#8220;Company"` 和 `contains="\u201cCompany"` 查找相同的文本
- **替换**:使用实体(`&#8220;`)或 Unicode(`\u201c`)都可以 - 两者都会根据文件编码(ascii → 实体,utf-8 → Unicode)适当转换

### 初始化

**找到 docx 技能根目录**(包含 `scripts/` 和 `ooxml/` 的目录):
```bash
# 搜索 document.py 以定位技能根目录
# 注意:/mnt/skills 仅作示例;请检查您的上下文以获取实际位置
find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1
# 示例输出:/mnt/skills/docx/scripts/document.py
# 技能根目录是:/mnt/skills/docx
```

**运行脚本时设置 PYTHONPATH** 为 docx 技能根目录:
```bash
PYTHONPATH=/mnt/skills/docx python your_script.py
```

**在脚本中**,从技能根目录导入:
```python
from scripts.document import Document, DocxXMLEditor

# 基本初始化(自动创建临时副本并设置基础设施)
doc = Document('unpacked')

# 自定义作者和首字母缩写
doc = Document('unpacked', author="John Doe", initials="JD")

# 启用修订追踪模式
doc = Document('unpacked', track_revisions=True)

# 指定自定义 RSID(如果未提供则自动生成)
doc = Document('unpacked', rsid="07DC5ECB")
```

### 创建修订追踪

**关键**:只标记实际更改的文本。将所有未更改的文本保留在 `<w:del>`/`<w:ins>` 标签外部。标记未更改的文本会使编辑不专业且更难审阅。

**属性处理**:Document 类会自动向新元素注入属性(w:id、w:date、w:rsidR、w:rsidDel、w16du:dateUtc、xml:space)。保留原始文档中未更改的文本时,复制带有现有属性的原始 `<w:r>` 元素以保持文档完整性。

**方法选择指南**:
- **向普通文本添加您自己的更改**:使用带有 `<w:del>`/`<w:ins>` 标签的 `replace_node()`,或使用 `suggest_deletion()` 删除整个 `<w:r>` 或 `<w:p>` 元素
- **部分修改其他作者的修订追踪**:使用 `replace_node()` 将您的更改嵌套在他们的 `<w:ins>`/`<w:del>` 中
- **完全拒绝其他作者的插入**:对 `<w:ins>` 元素使用 `revert_insertion()`(而不是 `suggest_deletion()`)
- **完全拒绝其他作者的删除**:对 `<w:del>` 元素使用 `revert_deletion()` 以使用修订追踪恢复删除的内容

```python
# 最小编辑 - 更改一个词:"The report is monthly" → "The report is quarterly"
# 原始:<w:r w:rsidR="00AB12CD"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>The report is monthly</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00AB12CD">{rpr}<w:t>The report is </w:t></w:r><w:del><w:r>{rpr}<w:delText>monthly</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>quarterly</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)

# 最小编辑 - 更改数字:"within 30 days" → "within 45 days"
# 原始:<w:r w:rsidR="00XYZ789"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>within 30 days</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00XYZ789">{rpr}<w:t>within </w:t></w:r><w:del><w:r>{rpr}<w:delText>30</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>45</w:t></w:r></w:ins><w:r w:rsidR="00XYZ789">{rpr}<w:t> days</w:t></w:r>'
doc["word/document.xml"].replace_node(node, replacement)

# 完全替换 - 即使替换所有文本也要保留格式
node = doc["word/document.xml"].get_node(tag="w:r", contains="apple")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:del><w:r>{rpr}<w:delText>apple</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>banana orange</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)

# 插入新内容(无需属性 - 自动注入)
node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text")
doc["word/document.xml"].insert_after(node, '<w:ins><w:r><w:t>new text</w:t></w:r></w:ins>')

# 部分删除其他作者的插入
# 原始:<w:ins w:author="Jane Smith" w:date="..."><w:r><w:t>quarterly financial report</w:t></w:r></w:ins>
# 目标:只删除"financial"使其变为"quarterly report"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
# 重要:保留外层 <w:ins> 上的 w:author="Jane Smith" 以维护作者身份
replacement = '''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
  <w:r><w:t>quarterly </w:t></w:r>
  <w:del><w:r><w:delText>financial </w:delText></w:r></w:del>
  <w:r><w:t>report</w:t></w:r>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)

# 更改其他作者插入的一部分
# 原始:<w:ins w:author="Jane Smith"><w:r><w:t>in silence, safe and sound</w:t></w:r></w:ins>
# 目标:将"safe and sound"改为"soft and unbound"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"})
replacement = f'''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
  <w:r><w:t>in silence, </w:t></w:r>
</w:ins>
<w:ins>
  <w:r><w:t>soft and unbound</w:t></w:r>
</w:ins>
<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
  <w:del><w:r><w:delText>safe and sound</w:delText></w:r></w:del>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)

# 删除整个 run(仅在删除所有内容时使用;部分删除请使用 replace_node)
node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete")
doc["word/document.xml"].suggest_deletion(node)

# 删除整个段落(就地处理,同时处理普通段落和编号列表段落)
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete")
doc["word/document.xml"].suggest_deletion(para)

# 添加新编号列表项
target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item")
pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else ""
new_item = f'<w:p>{pPr}<w:r><w:t>New item</w:t></w:r></w:p>'
tracked_para = DocxXMLEditor.suggest_paragraph(new_item)
doc["word/document.xml"].insert_after(target_para, tracked_para)
# 可选:在内容前添加间距段落以获得更好的视觉分隔
# spacing = DocxXMLEditor.suggest_paragraph('<w:p><w:pPr><w:pStyle w:val="ListParagraph"/></w:pPr></w:p>')
# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para)
```

### 添加批注

```python
# 添加跨越两个现有修订追踪的批注
# 注意:w:id 是自动生成的。仅当您从 XML 检查中知道 w:id 时才按 w:id 搜索
start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"})
doc.add_comment(start=start_node, end=end_node, text="此更改的解释")

# 在段落上添加批注
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
doc.add_comment(start=para, end=para, text="关于此段落的批注")

# 在新创建的修订追踪上添加批注
# 首先创建修订追踪
node = doc["word/document.xml"].get_node(tag="w:r", contains="old")
new_nodes = doc["word/document.xml"].replace_node(
    node,
    '<w:del><w:r><w:delText>old</w:delText></w:r></w:del><w:ins><w:r><w:t>new</w:t></w:r></w:ins>'
)
# 然后在新创建的元素上添加批注
# new_nodes[0] 是 <w:del>,new_nodes[1] 是 <w:ins>
doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="根据要求将 old 改为 new")

# 回复现有批注
doc.reply_to_comment(parent_comment_id=0, text="我同意这个更改")
```

### 拒绝修订追踪

**重要**:使用 `revert_insertion()` 拒绝插入,使用 `revert_deletion()` 使用修订追踪恢复删除。仅对普通未标记内容使用 `suggest_deletion()`。

```python
# 拒绝插入(将其包装在删除中)
# 当其他作者插入了您想删除的文本时使用此方法
ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
nodes = doc["word/document.xml"].revert_insertion(ins)  # 返回 [ins]

# 拒绝删除(创建插入以恢复删除的内容)
# 当其他作者删除了您想恢复的文本时使用此方法
del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"})
nodes = doc["word/document.xml"].revert_deletion(del_elem)  # 返回 [del_elem, new_ins]

# 拒绝段落中的所有插入
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_insertion(para)  # 返回 [para]

# 拒绝段落中的所有删除
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_deletion(para)  # 返回 [para]
```

### 插入图片

**关键**:Document 类使用位于 `doc.unpacked_path` 的临时副本。始终将图片复制到此临时目录,而不是原始解包文件夹。

```python
from PIL import Image
import shutil, os

# 首先初始化文档
doc = Document('unpacked')

# 复制图片并计算保持纵横比的全宽尺寸
media_dir = os.path.join(doc.unpacked_path, 'word/media')
os.makedirs(media_dir, exist_ok=True)
shutil.copy('image.png', os.path.join(media_dir, 'image1.png'))
img = Image.open(os.path.join(media_dir, 'image1.png'))
width_emus = int(6.5 * 914400)  # 6.5 英寸可用宽度,914400 EMU/英寸
height_emus = int(width_emus * img.size[1] / img.size[0])

# 添加关系和内容类型
rels_editor = doc['word/_rels/document.xml.rels']
next_rid = rels_editor.get_next_rid()
rels_editor.append_to(rels_editor.dom.documentElement,
    f'<Relationship Id="{next_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>')
doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement,
    '<Default Extension="png" ContentType="image/png"/>')

# 插入图片
node = doc["word/document.xml"].get_node(tag="w:p", line_number=100)
doc["word/document.xml"].insert_after(node, f'''<w:p>
  <w:r>
    <w:drawing>
      <wp:inline distT="0" distB="0" distL="0" distR="0">
        <wp:extent cx="{width_emus}" cy="{height_emus}"/>
        <wp:docPr id="1" name="Picture 1"/>
        <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
          <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
              <pic:nvPicPr><pic:cNvPr id="1" name="image1.png"/><pic:cNvPicPr/></pic:nvPicPr>
              <pic:blipFill><a:blip r:embed="{next_rid}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
              <pic:spPr><a:xfrm><a:ext cx="{width_emus}" cy="{height_emus}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
            </pic:pic>
          </a:graphicData>
        </a:graphic>
      </wp:inline>
    </w:drawing>
  </w:r>
</w:p>''')
```

### 获取节点

```python
# 按文本内容
node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text")

# 按行号范围
para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150))

# 按属性
node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})

# 按精确行号(必须是标签打开的行号)
para = doc["word/document.xml"].get_node(tag="w:p", line_number=42)

# 组合过滤器
node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text")

# 当文本多次出现时消歧 - 添加 line_number 范围
node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500))
```

### 保存

```python
# 自动验证保存(复制回原始目录)
doc.save()  # 默认验证,验证失败时抛出错误

# 保存到不同位置
doc.save('modified-unpacked')

# 跳过验证(仅用于调试 - 生产中需要这个表明存在 XML 问题)
doc.save(validate=False)
```

### 直接 DOM 操作

对于库未涵盖的复杂场景:

```python
# 访问任何 XML 文件
editor = doc["word/document.xml"]
editor = doc["word/comments.xml"]

# 直接 DOM 访问(defusedxml.minidom.Document)
node = doc["word/document.xml"].get_node(tag="w:p", line_number=5)
parent = node.parentNode
parent.removeChild(node)
parent.appendChild(node)  # 移动到末尾

# 通用文档操作(不带修订追踪)
old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text")
doc["word/document.xml"].replace_node(old_node, "<w:p><w:r><w:t>replacement text</w:t></w:r></w:p>")

# 多次插入 - 使用返回值保持顺序
node = doc["word/document.xml"].get_node(tag="w:r", line_number=100)
nodes = doc["word/document.xml"].insert_after(node, "<w:r><w:t>A</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>B</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>C</w:t></w:r>")
# 结果:original_node, A, B, C
```

## 修订追踪(红线批注)

**对所有修订追踪使用上面的 Document 类。** 以下模式仅供构建替换 XML 字符串时参考。

### 验证规则
验证器检查撤销 Claude 更改后文档文本是否与原始文档匹配。这意味着:
- **永远不要修改其他作者 `<w:ins>` 或 `<w:del>` 标签内的文本**
- **始终使用嵌套删除**来删除其他作者的插入
- **每个编辑都必须正确追踪**使用 `<w:ins>` 或 `<w:del>` 标签

### 修订追踪模式

**关键规则**:
1. 永远不要修改其他作者修订追踪内的内容。始终使用嵌套删除。
2. **XML 结构**:始终将 `<w:del>` 和 `<w:ins>` 放在包含完整 `<w:r>` 元素的段落级别。永远不要嵌套在 `<w:r>` 元素内部 - 这会创建破坏文档处理的无效 XML。

**文本插入:**
```xml
<w:ins w:id="1" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
  <w:r w:rsidR="00792858">
    <w:t>插入的文本</w:t>
  </w:r>
</w:ins>
```

**文本删除:**
```xml
<w:del w:id="2" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
  <w:r w:rsidDel="00792858">
    <w:delText>删除的文本</w:delText>
  </w:r>
</w:del>
```

**删除其他作者的插入(必须使用嵌套结构):**
```xml
<!-- 将删除嵌套在原始插入中 -->
<w:ins w:author="Jane Smith" w:id="16">
  <w:del w:author="Claude" w:id="40">
    <w:r><w:delText>monthly</w:delText></w:r>
  </w:del>
</w:ins>
<w:ins w:author="Claude" w:id="41">
  <w:r><w:t>weekly</w:t></w:r>
</w:ins>
```

**恢复其他作者的删除:**
```xml
<!-- 保留他们的删除不变,在其后添加新插入 -->
<w:del w:author="Jane Smith" w:id="50">
  <w:r><w:delText>within 30 days</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="51">
  <w:r><w:t>within 30 days</w:t></w:r>
</w:ins>
```

```

### ooxml/scripts/pack.py

```python
#!/usr/bin/env python3
"""
将目录打包为 .docx、.pptx 或 .xlsx 文件的工具,同时移除 XML 格式化。

使用示例:
    python pack.py <input_directory> <office_file> [--force]
"""

import argparse
import shutil
import subprocess
import sys
import tempfile
import defusedxml.minidom
import zipfile
from pathlib import Path


def main():
    parser = argparse.ArgumentParser(description="将目录打包为 Office 文件")
    parser.add_argument("input_directory", help="解压后的 Office 文档目录")
    parser.add_argument("output_file", help="输出的 Office 文件(.docx/.pptx/.xlsx)")
    parser.add_argument("--force", action="store_true", help="跳过验证")
    args = parser.parse_args()

    try:
        success = pack_document(
            args.input_directory, args.output_file, validate=not args.force
        )

        # 如果跳过验证则显示警告
        if args.force:
            print("警告:已跳过验证,文件可能损坏", file=sys.stderr)
        # 如果验证失败则以错误退出
        elif not success:
            print("内容将产生损坏的文件。", file=sys.stderr)
            print("请在重新打包前验证 XML。", file=sys.stderr)
            print("使用 --force 跳过验证并强制打包。", file=sys.stderr)
            sys.exit(1)

    except ValueError as e:
        sys.exit(f"错误:{e}")


def pack_document(input_dir, output_file, validate=False):
    """将目录打包为 Office 文件(.docx/.pptx/.xlsx)。

    参数:
        input_dir: 解压后的 Office 文档目录路径
        output_file: 输出的 Office 文件路径
        validate: 如果为 True,使用 soffice 进行验证(默认:False)

    返回:
        bool: 成功返回 True,验证失败返回 False
    """
    input_dir = Path(input_dir)
    output_file = Path(output_file)

    if not input_dir.is_dir():
        raise ValueError(f"{input_dir} 不是目录")
    if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
        raise ValueError(f"{output_file} 必须是 .docx、.pptx 或 .xlsx 文件")

    # 在临时目录中工作,避免修改原始文件
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_content_dir = Path(temp_dir) / "content"
        shutil.copytree(input_dir, temp_content_dir)

        # 处理 XML 文件以移除格式化空白
        for pattern in ["*.xml", "*.rels"]:
            for xml_file in temp_content_dir.rglob(pattern):
                condense_xml(xml_file)

        # 创建最终的 Office 文件作为 zip 压缩包
        output_file.parent.mkdir(parents=True, exist_ok=True)
        with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
            for f in temp_content_dir.rglob("*"):
                if f.is_file():
                    zf.write(f, f.relative_to(temp_content_dir))

        # 如果请求则进行验证
        if validate:
            if not validate_document(output_file):
                output_file.unlink()  # 删除损坏的文件
                return False

    return True


def validate_document(doc_path):
    """使用 soffice 转换为 HTML 来验证文档。"""
    # 根据文件扩展名确定正确的过滤器
    match doc_path.suffix.lower():
        case ".docx":
            filter_name = "html:HTML"
        case ".pptx":
            filter_name = "html:impress_html_Export"
        case ".xlsx":
            filter_name = "html:HTML (StarCalc)"

    with tempfile.TemporaryDirectory() as temp_dir:
        try:
            result = subprocess.run(
                [
                    "soffice",
                    "--headless",
                    "--convert-to",
                    filter_name,
                    "--outdir",
                    temp_dir,
                    str(doc_path),
                ],
                capture_output=True,
                timeout=10,
                text=True,
            )
            if not (Path(temp_dir) / f"{doc_path.stem}.html").exists():
                error_msg = result.stderr.strip() or "文档验证失败"
                print(f"验证错误:{error_msg}", file=sys.stderr)
                return False
            return True
        except FileNotFoundError:
            print("警告:未找到 soffice。跳过验证。", file=sys.stderr)
            return True
        except subprocess.TimeoutExpired:
            print("验证错误:转换超时", file=sys.stderr)
            return False
        except Exception as e:
            print(f"验证错误:{e}", file=sys.stderr)
            return False


def condense_xml(xml_file):
    """移除不必要的空白和注释。"""
    with open(xml_file, "r", encoding="utf-8") as f:
        dom = defusedxml.minidom.parse(f)

    # 处理每个元素以移除空白和注释
    for element in dom.getElementsByTagName("*"):
        # 跳过 w:t 元素及其处理
        if element.tagName.endswith(":t"):
            continue

        # 移除仅包含空白的文本节点和注释节点
        for child in list(element.childNodes):
            if (
                child.nodeType == child.TEXT_NODE
                and child.nodeValue
                and child.nodeValue.strip() == ""
            ) or child.nodeType == child.COMMENT_NODE:
                element.removeChild(child)

    # 写回压缩后的 XML
    with open(xml_file, "wb") as f:
        f.write(dom.toxml(encoding="UTF-8"))


if __name__ == "__main__":
    main()

```

### ooxml/scripts/unpack.py

```python
#!/usr/bin/env python3
"""解压并格式化 Office 文件(.docx、.pptx、.xlsx)的 XML 内容"""

import random
import sys
import defusedxml.minidom
import zipfile
from pathlib import Path

# 获取命令行参数
assert len(sys.argv) == 3, "用法:python unpack.py <office_file> <output_dir>"
input_file, output_dir = sys.argv[1], sys.argv[2]

# 解压并格式化
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
zipfile.ZipFile(input_file).extractall(output_path)

# 格式化打印所有 XML 文件
xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels"))
for xml_file in xml_files:
    content = xml_file.read_text(encoding="utf-8")
    dom = defusedxml.minidom.parseString(content)
    xml_file.write_bytes(dom.toprettyxml(indent="  ", encoding="ascii"))

# 对于 .docx 文件,建议一个用于修订追踪的 RSID
if input_file.endswith(".docx"):
    suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8))
    print(f"建议用于编辑会话的 RSID:{suggested_rsid}")

```