SKILL 14 / DOCS · DEEP DIVE

文档类 Skill 凭什么「不坏文件」

解包-改-校验-重打包 · 把 XSD 当 oracle · 确定性操作为什么必须写成脚本

2026-09-15 · BigCat

上一期给了办公四件套的清单。这一期把 docx / pptx / xlsx 三个 skill 的 scripts/ 目录拆开看:它们真正值钱的不是 SKILL.md 里那几百字提示词,是那十来个你可以单独拿走、脱离 skill 也能跑的 Python 脚本。

前置 → Skill 13 办公产出类 Skills 榜单(四件套的两个版本)

// WHY THIS MATTERS

一个 .docx 打不开,用户不会说「模型幻觉了」,只会觉得你这套东西不能用。办公产出类之所以是 skill 生态里最容易见效的一类,就在于它有客观 oracle——文件要么符合 schema,要么不符合,不需要任何人的主观判断。本期拆的就是这个 oracle 怎么被接回循环里:unzip → merge_runs → 改 XML → zip -Xr → validate.py。每一步都点名到具体脚本路径,你可以一个一个拿走用。

// 01

它替你干掉什么:干掉「改完不知道有没有坏」

论断:它干掉的不是「写 XML」。手改 OOXML 从来不算难,难的是改完没有任何东西告诉你哪一处不合规——你只能在 Word 弹出「内容有问题」时才知道。

怎么装

# Claude Code:四件套在 document-skills 这个插件里
/plugin marketplace add anthropics/skills
/plugin install document-skills@anthropic-agent-skills

# 只想要脚本、不想装 skill:直接 clone,scripts/ 目录可独立使用
git clone https://github.com/anthropics/skills

# 系统依赖(docx/SKILL.md 自己列的四项)
# docx(npm,环境预装)· pandoc · LibreOffice(soffice)· pdftoppm(Poppler)

第一条立刻能跑的用法(照抄 docx/SKILL.md 的编辑流程)

unzip -q doc.docx -d unpacked/
find unpacked -type l -delete              # 剥掉符号链接
python scripts/merge_runs.py unpacked/     # 先把碎掉的 run 合并
# 手工编辑 unpacked/word/document.xml —— 不要 reformat
(cd unpacked && rm -f ../out.docx && zip -Xr ../out.docx .)
python scripts/office/validate.py out.docx --original doc.docx

这六行里没有一行是装饰:find -type l -delete 防的是符号链接被打进包;merge_runs.py 决定你能不能在 XML 里搜到一句完整的话;zip -Xr 且必须 cd 进目录里打包,否则压出来多一层目录、Word 直接不认;--original 让校验器拿原件做基线。

最常见的三个翻车点:在 unpacked 外面执行 zip -r unpacked/(多一层目录)· 用编辑器「格式化 XML」后保存(空白进了 w:t,文本就变了)· 跳过 merge_runs.py 直接搜字符串(搜不到,然后以为文档里没有这句话)。
一手源 · anthropics/skills · skills/docx/SKILL.md(上面这段命令与依赖清单的出处)
// 02

机制:有效的不是提示词,是那一层 XSD

论断:这三个 skill 没打算把模型「教会」OOXML。它们做的是把「合不合规」压成一个退出码,让模型自己撞上去、自己改回来。

校验器长在哪

skills/docx/scripts/ ├── merge_runs.py 合并碎 run(编辑前必跑) ├── accept_changes.py 接受全部修订 ├── comment.py 插批注 / 回复批注 └── office/ ← docx · pptx · xlsx 三个 skill 共用这一个包 ├── validate.py 统一入口,退出码即结论 ├── soffice.py LibreOffice 包装(转 PDF / 转格式) ├── validators/ base.py · docx.py · pptx.py · redlining.py └── schemas/ ISO-IEC29500-4_2016 · ecma 第四版 · mce · microsoft

validate.py 接受一个解包目录或一个打包文件,另有四个开关:--original 拿原件做基线、--auto-repair 自动修、--author NAME 开修订校验、-v 详细输出。退出码:0 全过、1 校验失败、2 参数或文件错——这就是可以直接写进循环的那个信号。

它到底修什么、查什么

为什么 merge_runs 要排在编辑之前

Word 会把一句话切成十几个 <w:r>:拼写检查、语言标记、rsid 都会在中间插刀。于是 XML 里根本不存在「the quick brown fox」这个连续字符串,模型按字面查找必然扑空——然后它会开始瞎猜位置,那才是真正坏文件的起点。先合并再编辑,是把一类幻觉从源头掐掉。

xlsx 不在这条线上:validate.py 对 xlsx 明确跳过、不做 schema 校验。你以为「跑过校验了」,其实什么都没校——xlsx 的 oracle 是下一节的 recalc.py。
// 03

真实用法与边界:三条产线,三种完全不同的 oracle

论断:docx / pptx / xlsx 三个 skill 长得像,验收方式却毫不相干。搞错了这件事,你会在一个根本没有校验的格式上反复「校验通过」。

docx 线 · oracle = XSD + 修订比对

python scripts/accept_changes.py in.docx out.docx         # 接受全部修订
python scripts/comment.py unpacked/ "这里口径不一致"        # 目录模式(编辑 XML 时用这个)
python scripts/comment.py unpacked/ "同意" --parent 0       # 回复第 0 条
python scripts/comment.py contract.docx "问一句" -o annotated.docx
python scripts/office/validate.py out.docx --original doc.docx --author "BigCat"

目录模式下 comment.py 会把 <w:commentRangeStart> / <w:commentRangeEnd> / <w:commentReference> 三个片段打印出来给你放进 word/document.xml——批注的挂载点由人决定,编号与关系文件的记账由脚本做。这个分工是整套设计的缩影。

pptx 线 · oracle = XSD + 关系与内容类型

python scripts/thumbnail.py template.pptx template-thumbs   # 先看版式,第二个参数务必给
python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall('unpacked')" deck.pptx
python scripts/add_slide.py unpacked/ slide2.xml --after slide2.xml   # 复制页,别手抄
# 调序 / 删页 = 改 ppt/presentation.xml 里的 <p:sldIdLst>
python scripts/clean.py unpacked/                            # 删完清孤儿 slide/media/rels
(cd unpacked && rm -f ../out.pptx && zip -Xr ../out.pptx .)
python scripts/office/validate.py out.pptx --original deck.pptx

两条硬规矩:add_slide.py 直接作用在 .pptx 上时必须带 -o,否则原地改写输入;模板派生的 deck 必须带 --original,否则模板自带的报错会把你自己的错淹掉。

xlsx 线 · oracle = 重算,不是 schema

python scripts/recalc.py output.xlsx [timeout_seconds]   # 默认 30 秒
# LibreOffice 算完每一个公式并原地重写文件,返回 JSON:
# status(success|errors_found) · total_formulas · total_errors · error_summary
这里有个反直觉的坑:errors_found 的退出码是 0;只有 JSON 里出现 error 键(= 压根没重算成)才非零退出。所以干净的退出码不等于干净的工作簿,必须去读 JSON 正文。另外 recalc 全绿只证明公式能算,不证明它算得对——所以先写两三条公式核一下口径,再动手铺整张表。

收尾统一走三层 QA

# ① 文件 QA
python scripts/office/validate.py out.pptx --original src.pptx
# ② 内容 QA:读回来,抓占位符
markitdown out.pptx | grep -iE "\bx{3,}\b|lorem|ipsum|\bTODO|\[insert"
# ③ 视觉 QA:转 PDF 再出图,自己看一眼
python scripts/office/soffice.py --headless --convert-to pdf out.pptx
pdftoppm -jpeg -r 150 out.pdf slide && ls -1 slide-*.jpg

三层各挡各的:schema 挡打不开,grep 挡「xxx / TODO / 请在此处填写」漏进终稿,出图挡文字溢出框和白底白字。少一层就漏一类,而这三类没有任何交集。

一手源 · skills/pptx/SKILL.md · skills/xlsx/SKILL.md · microsoft/markitdown(pip install 'markitdown[all]';据 GitHub 仓库页 2026-09 约 18.4 万 star、MIT)
// 04

代价与替代品:这套流水线不便宜

论断:它的代价是一整套系统依赖。没有 LibreOffice,xlsx 的 oracle 和全部视觉 QA 会一起消失,而剩下那半套会让你误以为还在验收。

四条诚实的代价

四种「不走这条路」的替代品

一句话代价:这套东西真正卖给你的是「确定性操作交给脚本,判断交给模型」这条分界线。分界线画错了——让模型现编 zip 参数、现编关系文件编号、现数 slide 序号——它照样会坏文件,而且坏得更隐蔽。

// 一页选购卡

先拿这三个脚本(哪怕你完全不用这套 skill):scripts/office/validate.py(退出码即结论,docx/pptx 通用)· scripts/recalc.py(xlsx 唯一的 oracle)· scripts/merge_runs.py(编辑 docx 之前必跑)。

按格式走:docx = unzip → merge_runs.py → 改 word/document.xml → zip -Xr → validate.py --original;pptx = thumbnail.py 看版式 → add_slide.py 复制页 → clean.py 清孤儿 → validate.py --original;xlsx = openpyxl 写 → recalc.py 重算 → 读 JSON 正文。

收尾三层:validate.py(文件)· markitdown | grep(内容)· soffice.py + pdftoppm(视觉)。三层都要,顺序别反。

明确别做:手抄 slide XML 去复制页 · 在 unpacked 外面打 zip · 把 recalc 的 0 退出码当成没错 · 对模板派生文件不加 --original · 用 data_only=True 读完顺手 save · 以为 validate.py 也校了 xlsx。

一句话判据:你这一步有没有一个能退出非零的东西在看着?没有,就别让模型做这一步——把它写成脚本,或者把它从流程里删掉。

// 术语表

OOXML(Office Open XML)
docx / pptx / xlsx 的真身:一个 zip 包,里面是一堆 XML part 加媒体文件。「解包-改-重打包」之所以成立,全靠这一点。
part 与 [Content_Types].xml
包里每个 XML 文件叫一个 part,包根的 [Content_Types].xml 声明每类 part 是什么。它和 _rels/ 里的关系文件一起构成账本——手工增删文件而不记账,就是坏文件最常见的方式。
run(<w:r>)
Word 里格式一致的一小段文本。一句话常被切成十几个 run,merge_runs.py 要合并的就是它。
rsid
Word 给每次编辑会话打的标记。它是 run 被切碎的主要元凶之一,对内容毫无意义。
XSD(XML Schema Definition)
描述「什么样的 XML 算合法」的机器可读规范。schemas/ 里放的是 ISO-IEC29500-4:2016 与 ECMA 第四版两套,外加 mce、microsoft 两组扩展。
MCE(Markup Compatibility)
OOXML 里让新版特性在旧版里优雅降级的机制(mc:AlternateContent)。它也是为什么校验不能只认一套 schema。
redlining(修订留痕)
用 <w:ins> / <w:del> 把改动本身写进文档。validate.py --author 校的就是「所有文本差异是否都留了痕」。
有损往返(lossy round-trip)
读进来再存回去,某些内容悄悄消失。openpyxl 对图表与图片就是如此——它不报错,所以只能靠知道来防。

// 延伸阅读

// 想一想

为什么「文档类」是 skill 最容易做好的一类,而「写作类」最难?
因为前者有一个不需要商量的 oracle:schema 过不过、文件开不开得了。后者的验收标准每个人都不一样,于是只能靠样例去逼近。判断一类任务适不适合做成 skill,先问「有没有一个能退出非零的东西」。
把 zip -Xr 这种命令写进 SKILL.md 当提示词,和把它包成一个脚本,差别在哪?
差别在失败模式。写成提示词,模型某次心情不同就换了参数,而且错法千奇百怪;写成脚本,它要么跑通要么报错。确定性操作交给代码,是这三个 skill 里贯彻得最彻底的一条。
recalc.py 返回 errors_found 却退出码 0,这是 bug 吗?
不是。「文件里有 #REF!」和「重算这件事失败了」是两回事,前者需要人看、后者需要流程停。把两者压进同一个退出码才是真 bug。代价是你必须读 JSON 正文——设计者用一句文档换掉了一个假阴性。