# HTML To DOCX

> 将 HTML 字符串或文件高保真转换为 Microsoft Word (.docx) 文档。 支持 CSS 变量预处理、10+ 种 HTML 元素精确映射、14 种 CSS 属性映射、中文字体、4 种装饰组件、TOC 目录、页眉页脚、图片嵌入。 提供 CLI（subprocess）和 Python API 双模式调用；在需要将 HTML 文档转为可编辑 Word 文件时使用此 skill。

- Skill: `tomjiu/html-to-docx` (Agent Skill, multi-file: 72 files)
- Install (CLI): `npx skillmds@latest add tomjiu/html-to-docx`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomjiu/html-to-docx/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomjiu (https://skillmd.com/u/tomjiu)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/tomjiu/html-to-docx

---


# html-to-docx Skill

## 概述

`html-to-docx` 是一个 Python 转换引擎，支持：

- CSS 变量（`var(--x)`）预处理
- 10+ 种 HTML 元素精确映射（标题/段落/表格/列表/图片/blockquote 等）
- 14 种 CSS 属性映射（字体/颜色/缩进/行距/对齐等）
- 中文字体支持（宋体/黑体/仿宋/楷体，含 `rFonts.eastAsia` 设置）
- 4 种装饰组件（callout / divider / section-marker / data-card）+ data-card-grid
- 语义 `<section role>` 分节 + CSS `@page` 页眉页脚 + 动态域（PAGE/NUMPAGES/STYLEREF）
- TOC 目录渲染（3 级）
- 页眉/页脚渲染
- 图片嵌入（base64 / 本地路径 / 远程 URL）
- CLI 和 Python API 双模式调用

---

## 调用方式

### 前置条件：Python 环境与依赖安装

本 Skill 使用 **托管 venv**（默认 `$HOME/.venv-html-to-docx`）运行，避免污染系统 Python，也绕开 IDE safe-delete 拦截与跨机器 venv 二进制损坏。**环境准备已封装为幂等脚本**，首次 ~30s、之后 ~0.5s，agent 无需自行 `python -m venv` / `pip install`。

> ⚠️ **首次调用需要外网**（幂等脚本首跑会连以下三处公网）：
> - `https://astral.sh/uv/install.sh` — 装 uv（若本机未装）
> - `astral-sh/python-build-standalone` GitHub Releases — 拉 Python 3.12 独立发行版（uv 托管）
> - `https://pypi.org/simple` — 下载 `python-docx` / `lxml` / `Pillow` 等 wheel
>
> 私有化 / 无外网环境：请在**首次调用前**导出以下环境变量指向内网镜像（uv 与 pip 均遵循）：
> ```bash
> export UV_INDEX_URL="https://<内网 PyPI 镜像>/simple"
> export UV_PYTHON_INSTALL_MIRROR="https://<内网 python-build-standalone 镜像>"
> # uv 本身请预先由运维分发到 $HOME/.local/bin/uv，脚本探测到已装则跳过 curl 安装
> ```
> 未配置镜像且无外网时，`setup-html-to-docx.sh` 首跑必然失败——这是**已知交付前置条件**，非 skill 缺陷。**Agent 在私有化交付场景下调用本 skill 前，应先确认 IOA/运维已完成上述环境变量或离线 wheel 分发**，否则应转 Markdown 降级（见"错误处理与降级"）而非重试。

**统一入口（推荐，workbuddy / local 通道一键装齐）**：

```bash
bash <plugin_root>/scripts/wb/local/setup.sh
```

**单独准备本 Skill 的环境**：

```bash
bash <plugin_root>/scripts/wb/local/setup-html-to-docx.sh
```

> 说明：环境安装脚本位于 plugin 根的 `scripts/wb/local/setup-html-to-docx.sh`（workbuddy 本地通道专属）。

脚本输出末尾会打印 `Python runner: <venv>/bin/python`，可直接 `export HTML_TO_DOCX_PY=<那个路径>` 供后续调用。

**调用（首选：环境变量方式）**：

```bash
bash <plugin_root>/scripts/wb/local/setup-html-to-docx.sh   # 幂等，已就绪则秒退
export HTML_TO_DOCX_PY="${HTML_TO_DOCX_VENV:-$HOME/.venv-html-to-docx}/bin/python"
cd <plugin_root>/skills/html-to-docx/scripts
"$HTML_TO_DOCX_PY" -m html_to_docx convert input.html -o output.docx [OPTIONS]
```

**调用（备选：一行式，用于 subprocess）**：

```bash
bash <plugin_root>/scripts/wb/local/setup-html-to-docx.sh \
  && "${HTML_TO_DOCX_VENV:-$HOME/.venv-html-to-docx}/bin/python" \
       -m html_to_docx convert input.html -o output.docx [OPTIONS]
```

> **Agent 集成提示**：
> - `setup-html-to-docx.sh` 是幂等的，可以每次调用前无脑跑一遍，已就绪时秒退。
> - `cwd` 需要设为 `<plugin_root>/skills/html-to-docx/scripts`（`html_to_docx` 包在此），或用 `PYTHONPATH=<plugin_root>/skills/html-to-docx/scripts`。
> - 需要把 venv 落到别处时，调用前 `export HTML_TO_DOCX_VENV=/自定义/路径` 即可。
> - **禁止**在 `<plugin_root>/skills/html-to-docx/scripts/` 下自建 `.venv/`——那是过去踩过的坑（跨机器 shebang 失效、rm 被 safe-delete 拦）。
> - Plugin 部署态下，SessionStart hook 已经自动跑过一次安装脚本，agent 调用时通常无需再手动 bash。

**依赖清单**：以 `<plugin_root>/skills/html-to-docx/scripts/requirements.txt` 为准，`setup-html-to-docx.sh` 用 `--only-binary=:all:` 强制装 wheel，规避 lxml 在无 libxml2/libxslt 环境下源码构建失败。

**Python 版本要求**：Python 3.12（由 `setup-html-to-docx.sh` 通过 `uv` 自动安装并锁定）。

---

### CLI（Subprocess 模式）

**路径定位**：`html_to_docx` 包位于本 Skill 的 `scripts/` 目录下。**必须**使用 `setup-html-to-docx.sh` 输出的托管 venv 解释器（下文简写为 `$HTML_TO_DOCX_PY`），并把工作目录切换到 `scripts/` 目录，或通过 `PYTHONPATH` 指定：

```bash
# 前置：确保环境就绪（幂等，可无脑跑）
bash <plugin_root>/scripts/wb/local/setup-html-to-docx.sh
export HTML_TO_DOCX_PY="${HTML_TO_DOCX_VENV:-$HOME/.venv-html-to-docx}/bin/python"

# 方式 1：cd 到 scripts 目录执行
cd <plugin_root>/skills/html-to-docx/scripts
"$HTML_TO_DOCX_PY" -m html_to_docx convert input.html -o output.docx [OPTIONS]

# 方式 2：用 PYTHONPATH
PYTHONPATH=<plugin_root>/skills/html-to-docx/scripts "$HTML_TO_DOCX_PY" -m html_to_docx convert input.html -o output.docx [OPTIONS]
```

**路径变量说明**：
- `<plugin_root>` = plugin 根目录的绝对路径（即产物里的 `tencent-docx/` 所在位置）
- `<workspace>` = 用户工作区目录（仅用于落盘产物路径，如 `<workspace>/output/<request_id>/output.docx`）

> **Agent 集成提示**：在 subprocess 调用时，推荐使用 `cwd = path.join(pluginRoot, "skills/html-to-docx/scripts")` + `executable = $HTML_TO_DOCX_PY`，无需设置 PYTHONPATH。**不要**使用系统 `python` / `python3`——依赖装在托管 venv 里，系统解释器会 `ModuleNotFoundError`。

**参数**：

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `input_path` | （必须）| 输入 HTML 文件路径 |
| `--output / -o` | 自动生成 | 输出 .docx 文件路径 |
| `--page-size` | `A4` | `A4` / `Letter` / `A3` |
| `--orientation` | `portrait` | `portrait` / `landscape` |
| `--margin-top` | `2.54` | 上边距（cm） |
| `--margin-bottom` | `2.54` | 下边距（cm） |
| `--margin-left` | `3.17` | 左边距（cm） |
| `--margin-right` | `3.17` | 右边距（cm） |

**退出码**：
- `0` — 成功，stdout 输出 JSON `ConvertResult`
- `1` — 失败，stderr 输出 JSON 错误信息

**输出示例（成功）**：
```json
{"success": true, "docx_path": "/tmp/output.docx", "warnings": []}
```

**输出示例（失败）**：
```json
{"success": false, "error": "...", "markdown_fallback": "# Title\n...", "warnings": []}
```

### Python API 模式

```python
from html_to_docx import convert, ConvertOptions

result = convert(
    "<h1>Hello</h1><p>World</p>",
    output_path="/tmp/output.docx",          # 可选
    options=ConvertOptions(page_size="A4"),   # 可选
)

if result.success:
    print(result.docx_path)    # str: 输出文件路径
else:
    print(result.error)         # str: 错误信息
    print(result.markdown_fallback)  # str: 降级 Markdown
```

---

## 输入契约

### HTML 字符串规范

支持：
- 标准 HTML5 结构（`<body>` 内容）
- CSS 变量（`<style>:root { --color: red; }</style>`）
- 特殊区域标签：
  - `<header class="doc-header">` — 文档页眉
  - `<footer class="doc-footer">` — 文档页脚
  - `<nav class="doc-toc">` — 目录
  - `<div data-component="callout|divider|section-marker|data-card">` — 装饰组件
  - `<section role="...">` — 语义分节（自动映射 docx section，详见下）
  - `<style>@page { ... }</style>` — CSS Paged Media 页眉页脚模型（详见下）
  - `<div class="data-card-grid">` — 卡片网格容器
  - `<span data-docx-field="party_a" data-placeholder="请输入甲方名称"></span>` — 行内待填写字段，转换后写为 DOCX 书签

#### 待填写字段与 DOCX 书签

对合同、报价单等需要后续精确填写的位置，使用行内 `span`：

```html
<p>甲方：<span data-docx-field="party_a" data-placeholder="请输入甲方名称"></span></p>
```

- `data-docx-field` 必填，须在同一 HTML 中唯一，并匹配
  `^[A-Za-z][A-Za-z0-9_-]{0,63}$`。
- `data-placeholder` 可选；未提供时显示默认待填写提示。
- 仅支持同一段落或表格单元格内的行内字段；不支持跨段落、页眉页脚或重复行字段。
- 成功转换的 `ConvertResult.fields` 和 CLI 成功 JSON 会返回字段键与实际 `bookmark_name` 映射。后续编辑必须按该书签定位；字段缺失时不得回退为全文文本替换。

#### 语义 Section 与 CSS @page 页面模型

**1. 语义分节 `<section>`**

`<body>` 下的**顶层** `<section>` 按 DOM 顺序映射为独立 docx section，节边界自动插入分节符（`nextPage`）。无 `<section>` 时按单节处理（向后兼容）。

```html
<body>
  <section role="cover"> ... </section>                              <!-- 封面节 -->
  <section role="body" data-page-restart="1"> ... </section>          <!-- 正文，页码从 1 重起 -->
  <section role="financials" data-orientation="landscape"> ... </section>  <!-- 横向节 -->
</body>
```

| 属性 | 说明 | 默认值 |
|------|------|--------|
| `role` | 开放字符串；用于 `section[role=X]{page:Y}` 命名页绑定 | 无 |
| `data-orientation` | `portrait` / `landscape`（尺寸取文档默认并交换 w/h） | 文档默认 |
| `data-margin-top/bottom/left/right` | 该节页边距（cm），per-property 合并，未设项回落文档默认 | 文档默认 |
| `data-page-restart` | 该节页码起始值（写入 `pgNumType@w:start`） | 不重起 |

> 嵌套 `<section>` 仅顶层计为节，内层降级为普通块 + warning。请把封面/正文内的语义块用 `<div>` 表达。

**2. CSS `@page` 页眉页脚（CSS Paged Media 有界子集）**

在 `<style>` 中用 `@page` 声明 6 个 margin-box（`@top-left/center/right`、`@bottom-left/center/right`）的页眉页脚内容；命名页 `@page <name>` + `section[role=X]{page:Y}` 实现"封面无、正文有"。

```html
<style>
  @page { @bottom-center { content: counter(page) " / " counter(pages); }
          @top-right     { content: string(chapter); } }
  @page cover { @bottom-center { content: none; } }   /* 封面真正无家具 */
  section[role="cover"] { page: cover; }
  h1 { string-set: chapter content(text); }           /* STYLEREF 章节名 */
</style>
```

| `content` 取值 | 渲染为 | 说明 |
|------|------|------|
| `counter(page)` | Word `PAGE` 域 | 当前页码 |
| `counter(pages)` | Word `NUMPAGES` 域 | 总页数（原生物理总数，不减一） |
| `string(<name>)` | `STYLEREF <styleId>` 域 | 配合 `string-set`，引用样式 styleId（如裸 `h1`→`Heading1`，locale 无关） |
| `"文本"` / `'文本'` | 普通文本 | 字符串字面量 |
| `none` | 不产内容 | 某节相关 box 全 `none` → 真正无页眉/页脚（封面） |

> 域写入后自动置位 `<w:updateFields val="true"/>`，Word 打开/按 F9 即刷新。
> 同区域多个 box 合并进单段落，用 Tab 制表位（左/中/右）定位；连续节共用同一 `@page` 时继承同一 header/footer part（横向节也继承、不重算制表位）。
>
> **边界（Python 解析有界子集 C-CON-001）**：仅支持上述子集；不支持的语法（如 `@page :first` 伪类、`@page { size: ... }`）→ 忽略 + warning，绝不中断转换。`content: ""`（空字符串）非法 → 等同未设置 + warning（沿用上一节继承）。

### ConvertOptions 参数

```python
@dataclass
class ConvertOptions:
    page_size: str = "A4"           # "A4" | "Letter" | "A3"
    orientation: str = "portrait"   # "portrait" | "landscape"
    margin_top: float = 2.54        # cm
    margin_bottom: float = 2.54     # cm
    margin_left: float = 3.17       # cm
    margin_right: float = 3.17      # cm
    output_path: str | None = None  # None → 自动生成临时文件
```

---

## 输出契约

```python
@dataclass
class ConvertResult:
    success: bool
    docx_path: str | None = None         # 成功时的 .docx 文件路径
    error: str | None = None             # 失败时的错误信息
    markdown_fallback: str | None = None # 失败时的 Markdown 降级内容
    warnings: list[str] = field(default_factory=list)  # 非致命警告
    fields: list[FieldBinding] = field(default_factory=list)  # 已生成的待填写字段书签
```

---

## 错误处理与降级

| 场景 | 行为 |
|------|------|
| 整体转换失败 | `success=False` + `error` + `markdown_fallback`（bs4 提取文本转 Markdown） |
| 单个组件渲染失败 | 跳过该组件 + 追加 `warnings`，不中断整体转换 |
| 图片下载超时（5s） | 插入占位段落 `[图片: URL]` |
| 图片文件不存在 | 插入占位段落 `[图片无法加载]` |
| 未知 data-component | 跳过 + 追加警告 |

---

## 性能

| 文档规模 | P95 目标 | 实测参考 |
|---------|---------|---------|
| 10 页（典型） | ≤ 3000ms | ~87ms |

---

## 组件扩展

添加新组件只需：

1. 创建 `html_to_docx/components/<name>.py`
2. 使用 `@register("<component-type>")` 装饰渲染函数
3. 无需修改其他文件（自动发现机制）

```python
# html_to_docx/components/my_widget.py
from . import register

@register("my-widget")
def render_my_widget(element, document) -> None:
    text = element.get_text(strip=True)
    document.add_paragraph(f"[Widget] {text}")
```

调用方式：`<div data-component="my-widget">内容</div>`

---

## 已知限制

- 复杂嵌套 colspan/rowspan 表格可能有偏差
- CSS `background-color` 仅部分支持（需 python-docx 表格单元格）
- 远程图片依赖网络，超时（5s）时降级为占位文本
- Word 2007 以下格式不支持

