Word 格式提取与保真套用
四个脚本
| 脚本 | 作用 |
|---|---|
scripts/auto_plan.py |
主力:读 Word/Markdown/txt → 自动推断角色 → 生成 plan.json |
scripts/apply_content.py |
plan.json + 模板 → 标准格式 Word(格式零改动) |
scripts/extract_format.py |
反向:任意 docx → 格式规格(JSON + MD 说明书) |
scripts/dump_flow.py |
验收:逐段打印每个格式槽的颜色/粗细 + 表格位置 |
scripts/check_cache.py |
查面板同步:比对技能面板缓存与磁盘上的 description / version |
版本与面板同步
当前版本写在 SKILL.md frontmatter 的 version: 字段 —— 技能面板读的就是它。
脚本改完落盘即生效,不需要重装或重启 WorkBuddy。
python selfcheck.py # 环境 + 5 个脚本版本 + 面板版本声明
python scripts/check_cache.py # 面板缓存是否已同步(比对 description + version)
两处都通过(自检里出现「面板版本声明 …… OK」、check_cache 输出「★面板已同步到 vX.Y.Z」)
才说明面板里看到的就是最新版。若显示未同步,等几秒让客户端重新扫描
~/.workbuddy/.skill-list-cache.json,再跑一次即可。
⭐ 主力用法:给内容文件,自动出标准格式
这是最常用的入口。张博士给 Word / Markdown / txt,不需要给 JSON。
# 三步:内容 → 计划 → 标准格式 Word
python scripts/auto_plan.py "内容.md" --dump # ① 先看推断结果(可选)
python scripts/auto_plan.py "内容.docx" -o plan.json # ② 生成内容计划
python scripts/apply_content.py assets/TEMPLATE-课程方案-0.5天.docx plan.json -o "输出.docx"
支持的输入:.md / .docx / .txt(Markdown 表格、带粗体标记的列表都能处理)。
角色怎么推断出来的(双通道,优先级从高到低)
| 通道 | 依据 | 适用 |
|---|---|---|
| ① Word 颜色 | run 的 w:color:蓝→h1,橘→h2 |
最可靠(源文档已带格式) |
| ② 显式标题 | Word outlineLvl / Markdown #~### |
Markdown 主线结构 |
| ③ 居中大标题 | Word jc=center + outlineLvl |
文档主副标题 |
| ④ 形态启发式 | 见下表 | 无格式的纯文本 |
形态启发式规则表(按优先级,命中即停):
| 特征 | role | 例 |
|---|---|---|
● 开头 |
dot |
● 让日常重复工作自动化 |
1. / 1) 开头 |
item |
1. 每周写销售周报 |
| 短标签+全角冒号结尾(无正文) | h1 |
课程背景: |
| 独立讲次名(2~8字,无标点,非编号) | h1 |
课程大纲 |
| 第N讲 / 导入篇 / 结语 | h1 |
第一讲:快速上手 |
一、 / 【…】 |
h2 |
一、日常耗时任务应用场景 |
2.1 多级编号 |
h2 |
2.1 课程背景 |
| 短标签+冒号+正文 | field(2 槽) |
痛点1:数据分散,格式多样 |
| 冒号前 ≥4 字 + 有正文 | label |
课程结束时,学员能够: |
| 长度 ≥30 字 | body |
长段落 |
| 其余 | item |
短句 |
常用参数
--titles 2 # 文档最前面 2 个 H1 当主/副标题(默认 2,设 0 关闭)
--dump # 只打印推断结果不写文件 —— 推断不对时先看这个
自动表格处理
内容源里的 Markdown/Word 表格行列数可以任意:
脚本克隆模板的课程工具表(3行×3列),按需克隆行/列补齐,边框与列宽自动沿用。
实测把 8行×2列 / 4行×3列 / 10行×2列 / 11行×2列 四种不同规格的表都正确套上了。
推断不准时怎么办
- 先跑
--dump看错在哪 - 常见修正:把内容源里的标题层级调一下(
#/##用对),比改正则更快 - 仍有偏差 → 直接改
plan.json里那一两条role,再跑apply_content.py(推荐,比调规则快) - 改规则要动
scripts/auto_plan.py的ROLE_RULES(按优先级从上到下,命中即停)
核心认知(先读,决定后面所有动作)
WorkBuddy 没有任何内置工具能"一键提取 Word 格式"。
tencent-docx / 本地编辑器 SDK(doc_*)都是内容层接口:能读文字、能改段落,但拿不到
字体、字号、颜色这些格式层设置。唯一可靠路径是直接解析 docx 的 OOXML。
"格式永远一致"是可以做到的,原理不是"重新描述格式",而是:
> 完全不碰格式节点,只往 <w:t> 里写文字。
> 段落骨架(pPr / rPr / numPr)、styles.xml、numbering.xml、theme1.xml
> 原封不动搬运 —— 格式自然分毫不差。
标准工作流
场景 A:已有模板,只要换内容(最常用)
-
(可选但强烈建议)先跑一次提取,拿到段号与语义角色对照表:
bash python scripts/extract_format.py "参考.docx" --out-dir ./spec
产出:
-*.spec.md—— 人读的格式说明书(页面设置、样式表、角色映射、逐段对照表、表格规格)
-*.spec.json—— 机器读的全量结构 -
写内容计划 JSON(详见下一节「两种灌内容模式」)。最简:
json { "mode": "role", "blocks": [ {"role":"h1","text":"课程大纲"} ] } -
生成:
bash python scripts/apply_content.py "模板.docx" plan.json -o "输出.docx" -
交付两个路径:① 线上/预览打开链接 ② 桌面离线文件路径。
场景 B:从零建一份符合该格式的文档
先 create_doc 或拿一份该格式的空白 docx 做模板,再走场景 A 的 role 模式。
不要用 create_doc 后手工设格式 —— 那样永远对不齐原文档。
场景 C:给别的 docx 提取格式(换一套模板)
直接 extract_format.py 指向新文件即可,脚本与具体文档无关。
两种灌内容模式(选哪个)
| 模式 | 何时用 | 说明 |
|---|---|---|
role ⭐ 推荐 |
常规套格式 | 写 {"role":"h1","text":"第一讲:…"},脚本自动找对应样板段。不会串层级 |
index / build |
特殊需求 | 手写段号。build 可克隆任意段做变体,index 适合原位替换 |
role 模式的 12 个角色(华师大纲层级体系)
| role | 层级 | 格式 | 槽位 | 用于 |
|---|---|---|---|---|
title |
主标题 | 蓝 #1F4E79 粗体 居中 16pt |
1 | 课程名 + 副标题 |
h1 |
讲次层 | 蓝 #1F4E79 粗体,outlineLvl=0 |
1 | 课程背景/收益/工具/大纲、导入篇、第N讲、结语 |
field |
蓝色行内字段 | 蓝粗前缀 + 无色常规正文 | 2 | 「课程时间:0.5天,3小时/天」 |
h2 |
小节层 | 橘 #C45911 粗体,outlineLvl=1 |
1 | 一、二、三、【…】 |
label |
要点标签 | 黑色粗体,单格式 | 1 | 「操作流程:」「——方法论总结:四步法」 |
label_mix |
黑色行内混排 | 粗前缀 + 常规正文 | 2 | 对比:/痛点导入:/场景任务:/底层原理:/实操演示: |
dot |
要点条目 | #121212,● 开头 |
1 | 课程收益 4 条 |
num_item |
自动编号条目 | 自动 1)2)3) |
1 | 场景任务下的能力清单 |
num_h2 |
自动编号小节 | 橘 + 自动编号(一、二、三) |
1 | Word/PPT 自动化 |
item |
普通条目 | 黑色常规 | 1 | 1. 1) 编号段 |
body |
正文 | 首行缩进 2 字符 | 1 | 课程背景的正文段 |
blank |
空行 | — | 1 | 分隔 |
先探测本模板的角色样板段与槽位数:
{ "mode": "role", "print_roles": true, "blocks": [{"role":"h1","text":"探针"}] }
field vs label_mix:长得一样,靠区段区分(v1.2.0 起自动判定)
两者都是「粗前缀 + 常规正文」两槽,文本形态完全相同(『授课时间:半天』和『认知破冰:理清AI…』),
唯一区别是槽1 的颜色:field 蓝 #1F4E79,label_mix 黑 #000000。
auto_plan.py 按下面两条判定(命中任一即 field):
- 标签名命中属性白名单 —— 授课讲师/讲师/授课时间/课程时间/授课时长/课程时长/
授课对象/课程对象/适用对象/授课方式/课程方式/授课工具/课程工具/课程编号/授课地点/课程地点 - 当前 h1 区段名含 授课信息 / 课程信息 / 基本信息 / 授课安排 / 课程安排
其余一律 label_mix。
依据:模板 spec 里授课信息区(P016–P018)是蓝标签字段;而 P033「对比:」、P039「底层原理:」、
P060「痛点导入:」是粗黑行内混排。要改判定,动 auto_plan.py 的
PROFILE_FIELD_SECTIONS / PROFILE_FIELD_LABELS 两个常量即可。
🔴 行内混排必须用 slots
核心概念:格式槽(slot)。段内连续的同格式 run 归为一个槽,槽才是"一段文字"的单位。
P016「课程时间:0.5天,3小时/天」
槽1(蓝 #1F4E79 粗体) = r1'课程时间' + r2':' + r3'0.5' ← 3 个 run
槽2(无色 常规) = r4'天,' + r5'3' + r6'小时/天' ← 3 个 run
⚠️ run 数量(6) ≠ 逻辑片段数(2)。按 run 位置填 2 段文字 → 第 2 段落进 r2(仍是蓝色粗体)→ 整行被染成蓝色。
{"role": "field", "slots": ["课程时间:", "0.5天,3小时/天"]}
{"role": "label_mix", "slots": ["底层原理:", "Agent=大模型理解+工具调用+自主规划"]}
{"role": "label_mix", "slots": ["数据揭示(建立认知):", ""]} ← 槽2 留空
槽数不足会直接报错,不静默丢格式。
表格必须显式声明位置
{"table": 1} 把模板第 1 张表格克隆到你指定的位置。
不写它的话,表格会被甩到文档末尾(旧版 bug,已修)。
{"role": "h1", "text": "课程工具:"},
{ "table": 1, "cells": { "1,1": "序号", "2,2": "新工具名" }},
{"role": "blank", "text": ""}
cells 的键是 "行,列"(从 1 起),不填 cells 则整表原样克隆。
完整示例
{
"mode": "role",
"blocks": [
{"role": "title", "text": "AI智能体办公实战工作坊"},
{"role": "title", "text": "——用Workbuddy打造你的数字员工"},
{"role": "h1", "text": "课程背景:"},
{"role": "body", "text": "正文段,首行缩进两字。"},
{"role": "field", "slots": ["课程时间:", "0.5天,3小时/天"]},
{"role": "h1", "text": "课程工具:"},
{ "table": 1 },
{"role": "h1", "text": "课程大纲"},
{"role": "h1", "text": "导入篇:为什么是AI Agent"},
{"role": "h2", "text": "一、日常耗时任务应用场景"},
{"role": "label_mix", "slots": ["对比:", "相比对话AI工具,它是会干活的AI"]},
{"role": "h1", "text": "第一讲:快速上手"},
{"role": "h2", "text": "一、三种模式的场景匹配指南"},
{"role": "label_mix", "slots": ["1. Ask(顾问):", "只问不做,纯聊天"]},
{"role": "h1", "text": "结语:学习总结与落地行动"}
]
}
层级铁律:h1(蓝)= 讲次;h2(橘)= 讲次下面的小节。导入篇/第N讲是 h1,不是 h2。
关键规则(都是踩过坑的)
-
颜色/字号必须读 run 的
rPr,不能只读pPr/rPr
pPr/rPr里的w:color常是残留旧值。实测 P110「场景任务:」的pPr/rPr有C45911,
但 run 上无颜色 → 读段落属性会把它误判成橘色小节。
正确做法:取第一个有文字的 run 的rPr作为该段落的生效格式。 -
🔴 段内填字必须按「格式槽」分组,不能按 run 位置
run 数量 ≠ 逻辑片段数(P016 = 6 run / 2 槽)。按位置填必然把第 2 段落进加粗 run → 整行被染色。
用set_para_slots():按 rPr 签名(color+b+sz+font)连续分组,每组一个槽。 -
比较颜色值要补
#
w:color/@w:val本身是1F4E79不带#。写'#1F4E79' != '1F4E79'恒成立,
会导致所有蓝色/橘色标题都被降级成普通段落 —— 这个 bug 曾导致「导入篇」被错染成橘色。 -
非段落块(表格)必须显式声明位置
早期build模式用keep = [ch for ch in body if ch.tag != WQ+'p']保留表格,
结果表格被 append 到文档末尾。现在改为在 plan 里写{"table": N}显式插入。 -
必须同时读
styles.xml+document.xml
只读styles.xml会拿到错的颜色 —— 样式级 heading1 是2E74B5(蓝),
但正文实际用的是直接格式1F4E79/C45911。 -
字号单位是半磅:
w:sz val=32→ 16pt → 中文「三号」。脚本已自动换算。 -
🔴 严禁用正则改
word/document.xml
正则<w:pPr>.*?</w:pPr>会在已含 pPr 的段落上嵌套出<w:pPr><w:pPr>并吞掉闭合标签,
XML 直接不合法,Word 打不开,用户文件报废。本工具只用ElementTree解析 + 重新序列化。 -
ET 序列化必须注册全部 15 个命名空间前缀
否则w14:paraId/w:rsid等前缀丢失,Word 报「文件已损坏」。apply_content.py顶部NS字典勿删。 -
zip 重排顺序:
[Content_Types].xml必须第一个。脚本已处理,改脚本时勿动。 -
改完必须四重校验(第 4 条最关键,别省)
bash python -c "import zipfile;assert zipfile.ZipFile('out.docx').testzip() is None" python -c "import xml.etree.ElementTree as ET,zipfile;ET.fromstring(zipfile.ZipFile('out.docx').read('word/document.xml'))" # 逐段回读实际格式(核验层级与混排就靠它): python scripts/dump_flow.py out.docx # ★ 真渲染:docx → PDF → 逐页看图,唯一能查出「分页」问题的手段
scripts/dump_flow.py把每段的每个格式槽连同颜色/粗细一起打出,并标出表格所在位置。
🔴 最后一步必须用 WPS COM 真渲染(本机没有 Microsoft Word;Word.Application
这个 ProgID 已被 WPS 接管,两者在本机是同一件事):
powershell
$app = [Activator]::CreateInstance([Type]::GetTypeFromProgID('KWPS.Application'))
$app.Visible=$false; $app.DisplayAlerts=0
$doc = $app.Documents.Open($docx, $false, $true) # ConfirmConversions, ReadOnly
$doc.ExportAsFixedFormat($pdf, 17) # 17 = wdExportFormatPDF
$doc.Close(0); $app.Quit()
再把 PDF 逐页转 PNG 目检(pypdfium2,装在隔离 venv
C:\Users\ZLS\.workbuddy\binaries\python\envs\default)。
- 为什么不能省:文件级校验(前三步)看不见分页。实测一次真渲染就抓出
「末尾多一张空白页」,而前三步全绿。
- New-Object -ComObject 被安全策略拦截,必须用上面的 [Activator]::CreateInstance 写法。
- 会启动本机 Office 进程(用完自动退出、零残留),使用前告知张博士。
⛔ 不要用 edsdk.py open_file 做验收!
open_file 的结果经 SSE /stream 推送,裸调用不消费该流 → 整个本地文档服务挂起
(面板里所有 docx 都打不开,只能重启 WorkBuddy)。详见 .learnings/ERRORS.md E-002。
os.replace覆盖 docx 可能报PermissionError(被预览面板锁定)→ 换新文件名输出。
本 skill 已附带的现成资产
assets/ 下是 张闻川《AI智能体办公实战工作坊——对外-0.5天》 的完整格式规格:
-
TEMPLATE-课程方案-0.5天.docx—— 模板原件,直接拿它当apply_content.py的第一个参数即可。
改内容绝不要改这个文件(os.replace也可能因预览面板锁定而失败)。 -
TEMPLATE-课程方案-0.5天.spec.md—— 格式说明书(由上面的提取脚本生成,已修正层级判定)。
该文档的层级体系(华师课程大纲标准):
| role | 层级 | 格式 | 该文档中的实例 |
|---|---|---|---|
title |
主标题 | 居中 16pt 粗 #1F4E79 |
AI智能体办公实战工作坊 + 副标题 |
h1 |
讲次层 | 粗 #1F4E79,outlineLvl=0 |
课程背景/收益/工具/大纲、导入篇、第一讲、第二讲、结语 |
field |
蓝色行内字段 | 蓝粗前缀 + 无色正文 | 课程时间/对象/方式 |
h2 |
小节层 | 粗 #C45911,outlineLvl=1 |
一、二、三、【…】、Excel/Word/PPT自动化 |
label |
要点标签 | 粗黑 #000000 单格式 |
数据揭示(建立认知): |
label_mix |
黑色行内混排 | 粗前缀 + 常规正文 | 痛点导入:/场景任务:/底层原理: |
dot |
要点条目 | #121212,● 开头 |
课程收益 4 条 |
num_item |
自动编号条目 | numId=1,1) |
场景任务下的能力清单 |
num_h2 |
自动编号小节 | 橘 + numId=2,一、二、三 |
Word/PPT 自动化 |
item |
普通条目 | 常规 #000000 |
1. 1) 编号段 |
body |
正文 | 首行缩进 2 字符 | 课程背景正文 |
blank |
空行 | — | 分隔 |
其他规格:表格 = 表样式 26(Grid Table Light),边框 #BEBEBE 0.5 磅,表宽 4994 pct,列宽 458/2219/2322 pct;
页面 = A4,四边页边距 56.7 磅(2cm),行高 311 twips 起;全文行距 24 磅固定值。
- 通用颜色标识:
| 颜色 | 十六进制 | 用途 |
|---|---|---|
| 深蓝 |#1F4E79| 主标题、讲次层(导入篇/第N讲/结语)、蓝色行内字段 |
| 橘红 |#C45911| 小节层(一、二、三) |
| 正文黑 |#000000| 正文、要点标签 |
| 灰黑 |#121212| ● 要点 |
| 边框灰 |#BEBEBE| 表格线 |
换新课程时:把参考 docx 换掉,重跑 extract_format.py 覆盖 assets 即可,脚本无需改动。
若新模板的层级体系不同(比如讲次用别的颜色),先跑 print_roles 看自动识别的角色表,
不符时调 build_role_index() 里的判据(颜色常量在 BLUE / ORANGE)。
禁止的做法
- ❌ 用
create_doc从空白建然后手工设格式(永远对不齐) - ❌ 用
tencent-docx的美化流程去"套格式"(那是让 AI 重新设计,不是复刻) - ❌ regex 改
document.xml - ❌ 改
styles.xml里的颜色(会污染模板,且大概率不是用户实际用的颜色) - ❌ 只交付截图不给文件路径