- AI 应用
- 人工智能
- AI Agent
- 本地部署
- 前端
- 后端
- 工作流自动化
【免费下载链接】ekko-studio
Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.
本篇技术指南聚焦 Ekko Studio 仓库中packages/ekko-agent/skills/docx这一文档处理 Skill 的核心难点——Word 修订追踪(w:ins/w:del)与批注(Comments)的底层 WordprocessingML 处理。文章以 revisions-and-comments.md 为骨架,结合 docx_revisions.py 与 docx_comments.py 的源码实现,帮助读者理解修订接受/拒绝的决议语义、批注的"三件套"XML 结构,以及如何在日常自动化流程中安全地使用这些命令。读完本文,你将能读懂任意 .docx 中的修订与批注 XML,并能用命令行完成列出、接受、拒绝、增删批注等全部操作。
一、docx Skill 中修订与批注的定位
Ekko Studio 的 docx Skill 是一套围绕 python-docx 与 lxml 构建的 Word 文档处理工具集,其入口与总览见 SKILL.md。其中与本文主题直接相关的两个脚本是:
- docx_revisions.py:检查并决议修订追踪(
w:ins/w:del); - docx_comments.py:列出、添加、删除批注。
这两个脚本被定位为"深层参考"(deep reference),日常使用只需按 SKILL.md 的操作流程走;只有需要推理原始 WordprocessingML、扩展脚本或调试异常文档时,才需要进入 revisions-and-comments.md 这一层。SKILL.md 还给出了明确的安全约定:除非用户明确要求,绝不丢弃批注或修订;accept-all/reject-all与批量删除批注均属于破坏性变换,操作前必须确认范围。
脚本运行环境仅需两个 Python 依赖(见 SKILL.md 的 Core dependency 段):
python3 -m pip install python-docx lxml所有脚本均以python3 <脚本路径> ...方式调用,例如:
python3 packages/ekko-agent/skills/docx/scripts/docx_read.py input.docx --json python3 packages/ekko-agent/skills/docx/scripts/docx_validate.py output.docx二、修订追踪的 XML 结构:w:ins/w:del
Word 将"运行级"(run-level)修订记录为段落w:p内部的包装元素(wrapper element),命名空间为:
http://schemas.openxmlformats.org/wordprocessingml/2006/mainrevisions-and-comments.md 给出的典型结构如下:
<w:p> <w:r><w:t>Base </w:t></w:r> <w:ins w:id="1" w:author="Editor" w:date="2026-01-02T03:04:05Z"> <w:r><w:t>inserted text</w:t></w:r> </w:ins> <w:del w:id="2" w:author="Editor" w:date="2026-01-02T03:04:05Z"> <w:r><w:delText>deleted text</w:delText></w:r> </w:del> </w:p>两个关键事实决定了脚本的全部行为:
- 被删除的文本存放在
w:delText而非w:t中。正因为如此,普通的纯文本提取(如 python-docx 的paragraph.text)天然呈现"接受修订"后的视图——插入可见、删除隐藏。这可以从 docx_read.py 的 docstring 中得到印证:"Body text is the accepted/as-is text (python-docx ignores deleted-in-revision text and shows inserted text)"。 - 决议(resolve)语义是确定性的,见下表:
| 修订类型 | 动作 | 处理方式 |
|---|---|---|
w:ins | 接受(accept) | 解包(unwrap):把子 runs 上移到父级,移除包装元素 |
w:ins | 拒绝(reject) | 移除包装元素及其全部内容 |
w:del | 接受(accept) | 移除包装元素及其全部内容 |
w:del | 拒绝(reject) | 把每个w:delText重命名为w:t,然后解包 |
上述"解包"逻辑在 docx_revisions.py 中有直接实现:_unwrap找到父元素中当前元素的位置,把其子节点逐个插入到原位置并移除包装器;_apply则按上表分支执行——拒绝删除时对delText改名再解包,从而实现"恢复被删文本"。
三、docx_revisions.py:五个子命令与调用链
docx_revisions.py 提供五个子命令(对应argparsesubparsers):
| 子命令 | 说明 | 额外参数 |
|---|---|---|
list | 以 JSON 列出全部修订:id、author、date、type、text | 无 |
accept-all | 接受全部插入与删除 | -o/--output |
reject-all | 拒绝全部插入与删除 | -o/--output |
accept | 按w:id接受单个修订 | --id(必填) |
reject | 按w:id拒绝单个修订 | --id(必填) |
所有命令的通用参数是输入path;-o/--output省略时原地覆盖输入文件(源码中out = args.output or args.path),因此生产环境建议总是显式传-o。
典型用法(摘自脚本 docstring):
python3 docx_revisions.py list report.docx python3 docx_revisions.py accept-all report.docx -o accepted.docx python3 docx_revisions.py reject report.docx --id 3 -o out.docx输出为结构化 JSON,例如list返回{"ok": true, "revisions": [...]};accept/reject返回{"ok": true, "output": ..., "resolved": n, "action": "accept"|"reject"}。当按--id决议但找不到该 id 时,返回{"ok": false, "error": "no revision with id ..."}并以退出码 1 结束(见 docx_revisions.py)。
覆盖范围:正文、表格、页眉页脚
脚本遍历的是"body 根 + 每个页眉/页脚部件根",核心是root.iter(W+"ins", W+"del"),它按文档顺序递归查找任意深度的元素。提供这套遍历的是公共模块 docx_common.py 中的iter_part_roots:它依次产出 body 根,以及每个 section 的 header / footer / first_page_header / first_page_footer / even_page_header / even_page_footer 的 XML 根(用id(part._element)去重,避免同源部件重复处理)。因此:
- 正文段落、表格单元格(含嵌套表格)、页眉、页脚、文本框中的修订都能被发现和决议;
- 修订可以出现在任何允许块级内容(block content)的位置。
关于w:id的注意事项
w:id的值在每个修订元素上是唯一的,但一次逻辑上的编辑会话可能产生多个元素。因此accept/reject --id精确作用于携带该 id 的一个或多个元素——源码resolve()中rev_id is None or el.get(q("id")) == rev_id正是这种"按 id 精确匹配"的语义。
四、脚本不处理的修订类型与检测手段
revisions-and-comments.md 明确列出了脚本不做决议、仅检测的修订类型:
- 段落标记修订(paragraph-mark revisions,即
w:pPr上的w:rPr/w:ins); - 表格行插入/删除(
w:trPr/w:ins); - 格式变更记录(
w:rPrChange、w:pPrChange); - 移动修订(
w:moveFrom/w:moveTo)。
其中移动修订在常见编辑器中较为罕见,文档建议:如果文档中存在移动修订,直接用 Word 本身处理,不要猜测。
这些类型的"检测"由 docx_read.py 的--revisions参数完成,其detect_revisions()实现方式非常轻量:直接以 zipfile 打开 .docx,扫描word/下所有 XML 部件的原始字节,匹配<w:ins、<w:del、<w:rPrChange以及word/comments部件是否存在(见 docx_read.py),返回has_tracked_changes、comments等布尔标记。建议任何编辑操作前先运行它做"是否有修订/批注"的摸底。
五、批注的"三件套"XML 结构
批注由三个相互协作的部分组成(见 revisions-and-comments.md 的 Comments 一节):
word/comments.xml部件——每个批注一个w:comment元素,携带w:id、w:author、w:initials、w:date及正文段落。它通过关系类型.../comments与 document.xml 关联,内容类型为application/vnd...wordprocessingml.comments+xml,同时需要在[Content_Types].xml中登记 override——python-docx 的 part 机制在部件注册时会自动补上。- 故事(story)中的范围标记——锚定文本之前放置
w:commentRangeStart w:id="N",之后放置w:commentRangeEnd w:id="N"。 - 引用 run——一个包含
w:commentReference w:id="N"的w:r,紧跟范围结束标记之后,它把批注气泡与位置绑定。
三者的位置关系可示意为:
<w:p> <w:r><w:t>Q3 </w:t></w:r> <w:commentRangeStart w:id="0"/> <w:r><w:t>revenue</w:t></w:r> <w:commentRangeEnd w:id="0"/> <w:r><w:commentReference w:id="0"/></w:r> </w:p>六、docx_comments.py:list / add / delete 的实现细节
docx_comments.py 提供三个子命令:
| 子命令 | 说明 | 关键参数 |
|---|---|---|
list | 按批注输出 JSON:id、author、initials、date、text、anchored_text | path |
add | 在--target文本首次出现处锚定新批注 | --target、--text必填;--author(默认Hermes)、--initials、--xml |
delete | 按--id删除批注及其全部范围标记 | --id必填 |
list:XML 层的锚定文本重建
list/delete始终工作在 XML 层,因此能处理任何生产者生成的文档。anchored_text的重建算法(见 docx_comments.py)是:遍历每个部件根(同样复用iter_part_roots,保证按文档顺序),维护一个"活跃 id 集合"——遇到commentRangeStart加入 id,遇到commentRangeEnd移除 id;期间遇到的所有w:t文本都追加到该 id 的文本缓冲中。这保证跨 run、甚至跨段落锚定的文本都能被正确拼接。
add:先切分 run 再锚定
add的第一步是把目标文本隔离成完整的 run。find_anchor_runs在全文(正文+表格+页眉页脚,经 docx_common.py 的iter_all_paragraphs)中查找--target的首次出现;若匹配起点或终点落在某个 run 中间,_split_run会在边界处把 run 一分为二——切分时会深拷贝w:rPr(right = deepcopy(run_el)),因此格式(粗体、斜体、颜色等)得以保留,并且新w:t会设置xml:space="preserve"防止前后空格丢失。
随后按环境二选一:
- python-docx >= 1.2:使用原生
document.add_comment(runs, ...)API,由 python-docx 自己创建 comments 部件、范围标记和引用 run(见add_comment_native); - 旧版本或显式
--xml:脚本自行构建word/comments.xml——通过 OPC 层创建Part(pack URI 为/word/comments.xml,内容类型为 comments+xml),用part.relate_to(part, RT.COMMENTS)注册关系,再手工插入范围标记与引用 run(见add_comment_xml)。为让编辑结果能写回保存,代码还给 part 动态换上了自定义 blob 属性,每次保存时重新序列化 live 的 XML 树。
新批注的 id 由_next_id计算:取 comments 部件中现存全部数字 id 的max + 1,避免冲突。
delete:同时清理四类痕迹
删除批注会移除w:comment元素以及该 id 的全部三种标记(commentRangeStart、commentRangeEnd、commentReference),其中引用 run 标记还会连带删除其外层w:r(见 docx_comments.py)。被锚定的文档正文文本不受影响——这一点在测试中也被明确断言:删除后docx_read.py --text仍能读到完整句子。
commentsExtended.xml 的边界
现代 Word 还会写出commentsExtended.xml,用于记录回复(threading)与"已解决"(resolved)状态。脚本既不读取也不产出该部件:回复和 resolved 标记在此不可见,由本 Skill 添加的批注都是顶层(top-level)批注。这是使用前必须知晓的能力边界。
七、实战组合:完整操作流程
结合 SKILL.md 的工作流,一个典型的"修订+批注"自动化场景如下:
# 1. 摸底:是否有修订/批注 python3 docx_read.py report.docx --revisions # 2. 查看全部修订 python3 docx_revisions.py list report.docx # 3. 拒绝某条错误的插入(按 id) python3 docx_revisions.py reject report.docx --id 3 -o step1.docx # 4. 接受其余全部修订 python3 docx_revisions.py accept-all step1.docx -o step2.docx # 5. 在关键段落添加批注 python3 docx_comments.py add step2.docx --target "Q3 revenue" \ --text "Needs a source" --author "Reviewer" --initials "R" -o step3.docx # 6. 查看批注(含锚定文本) python3 docx_comments.py list step3.docx # 7. 结构校验后交付 python3 docx_validate.py step3.docx其中第 7 步 docx_validate.py 做的是"健康检查"而非完整 XSD 校验:验证 zip 可读、必需部件存在、所有关系可解析(悬空引用报错)、r:id/r:embed引用有效、嵌入图片非空且魔数正确、文档引用的样式 id 在 styles.xml 中存在,并尝试用 python-docx 打开。任何 error 级问题都会使退出码为 1。
八、测试套件如何背书这些语义
这些行为并非仅靠文档描述,端到端测试 test_docx_skill.py 直接以子进程方式运行脚本并断言结果:
- TestRevisions:构造同时含正文与表格单元格修订的文档(
_add_ins/_add_del直接向w:p注入w:ins/w:del),验证list输出 4 条记录、accept-all后正文变为Base ADDED且表格变为Cell CELLADD、reject-all后为Base REMOVED/Cell CELLGONE、按--id单条决议只影响目标 id、未知 id 返回退出码 1。 - TestComments:验证
add→list→delete全链路——anchored_text精确等于目标文本、文档正文不受影响、--xml强制走回退路径后文件仍可被 python-docx 正常打开、目标文本不存在时报错退出。 - 测试还固定了
LC_ALL=C与PYTHONIOENCODING=utf-8,证明脚本在无本地化环境下的非 ASCII 文本处理是稳定的。
这些测试文件位于 packages/ekko-agent/skills/docx/tests,是阅读本文后继续深挖底层行为的最佳入口。
九、总结与安全边界
- 修订决议是纯 XML 层的确定性操作:接受插入=解包,拒绝插入=移除;接受删除=移除,拒绝删除=
delText改名w:t后解包。 - 批注由 comments 部件、范围标记、引用 run 三件套构成;
list/delete通用兼容任何生产者,add会先切分 run 保留格式,再选择原生 API 或 XML 回退路径。 - 边界:段落标记修订、表格行修订、格式变更、移动修订只检测不决议;
commentsExtended.xml的回复与 resolved 状态不可见。 - 安全:删除批注或批量决议修订属于破坏性操作,应遵循 SKILL.md 的约定——先
docx_read.py --revisions摸底、保留可恢复的原始副本、默认输出到新文件、操作后运行docx_validate.py校验,并在布局敏感的文档上用 LibreOffice 渲染核对。
对于需要对接 Word 协作工作流的 Agent 与自动化管线,理解本文的 WordprocessingML 细节,是避免"修订丢失""批注错位""文件损坏"等问题的前提。
- AI 应用
- 人工智能
- AI Agent
- 本地部署
- 前端
- 后端
- 工作流自动化
【免费下载链接】ekko-studio
Ekko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.
相关推荐
pandoc 批注处理实战:深入解析 Word 修订与评论的 `--track-changes` 机制
pandoc 批注处理实战:深入解析 Word 修订与评论的 track changes 机制 导读 本文以 pandoc 仓库中的命令测试用例 test/co
文档开发工具CLIpandoc 转换带 Word 修订标记的 docx 时如何设置 --track-changes?
pandoc 转换带 Word 修订标记的 docx 时如何设置 track changes? 如果你用 pandoc 转换由 Word 生成的 .docx 文
文档开发工具CLIdocx 修订追踪(Track Changes)完整指南:用 InsertedTextRun、DeletedTextRun 与 revision 属性生成带修订标记的 Word 文档
docx 修订追踪(Track Changes)完整指南:用 InsertedTextRun、DeletedTextRun 与 revision 属性生成带修订
文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考