Install
openclaw skills install @tomuiv/docx-writer标准学术文档生成 — 通过 bayoo-docx(原生脚注支持)和模板 template.docx 生成符合学术排印规范的 Word 文档,含脚注、三线表、图片、LaTeX 公式(可输出 Word 原生可编辑公式 OMML 或 XeLaTeX 渲染图)等。当用户要生成/排版 Word(.docx)文档、学位论文、实验报告、新闻稿、推送稿、讲稿,或需要在 docx 里插入数学公式/LaTeX 公式/原生可编辑公式时使用
openclaw skills install @tomuiv/docx-writer通过 bayoo-docx(原生支持脚注的 python-docx 分支)+ lxml 生成符合学术规范的 Word 文档。脚注由库引擎原生创建,避免手工构造 OOXML 导致的格式问题。
pip install bayoo-docx lxml pyyaml pymupdf pillow
omml 模式需 pandoc;image 模式需 xelatex(TeX 发行版,如 MiKTeX/TeX Live)——见「LaTeX 公式」节。soffice)。docx-writer/
├── template.docx # 默认基底(承载原生脚注样式),被 academic 模板引用
├── templates/ # ★ 模板 = 一份 YAML 配置(页面 + 样式表 + 脚注 + 后处理)
│ ├── academic.yaml # 标准学术/论文体例(含脚注)
│ ├── push.yaml # 微信公众号推送稿体例
│ ├── news.yaml # 清华新闻网简讯体例
│ └── script.yaml # 演讲稿/答辩稿体例(大字号)
└── scripts/
├── docx_engine.py # ★ 引擎:Template(读配置) + Builder(渲染),不含任何内容
├── latex_formula.py # ★ LaTeX → 紧裁 PNG(XeLaTeX+PyMuPDF;引擎与 CLI 共用)
├── build_from_content.py # ★ 声明式渲染器:content.yaml/json → docx(CLI)
└── build_docx.py # 内容脚本示例(Python API 写法)
三条解耦边界:
Template.load('<name>'),引擎代码不动。Builder),也可声明式内容文件(build_from_content.py)。| 段 | 字段 | 说明 |
|---|---|---|
name | 模板名 | |
base_docx | 基底 docx 路径(承载脚注样式);null = 空白文档 | |
page | size | A4 / LETTER / A3 / A5 |
margin | {top,bottom,left,right},支持 cm/mm/pt/in | |
background | {color: "F7F7F7"}(Word 页面底色) | |
orientation | portrait / landscape | |
styles | 任意样式名 | 样式表,见下 |
footnote | enabled/cn/en/size/numbering/num_fmt | 脚注字体、字号、编号(per_page 每页重编 / continuous 连续) |
features | fix_quotes | 中文引号拆 run 校正字体(默认 true) |
superscript_footnotes | 正文脚注编号上标(默认 true) |
样式字段(styles.<名字>):cn(中文字体) en(西文字体) size bold italic color(十六进制) align(left/center/right/justify) indent(首行缩进) line_spacing(数字=倍数 / 18pt=固定) space_before space_after。
image 样式额外支持 width;table 样式支持 mode(three_line/grid) word_style header cell。
加一套新模板:复制任一 yaml → 改名 → 调字段即可,引擎与内容脚本无需改动。
from docx_engine import Template, Builder
b = Builder(Template.load('academic')) # 或 yaml 绝对路径
b.title('标题') # = para(style='title')
b.heading('一、引言', level=1) # level→h1/h2/h3
b.para('正文{F1}', style='body', fn_map={'F1': '注释文本'}) # {Fn} 占位转脚注
b.image('fig.jpg', caption='图1 示意', width='14cm')
b.formula(r'\alpha_0=\arctan(p_x/p_y)', caption='式(1)') # 行间公式(默认 auto)
b.formula(r'E=mc^2', mode='omml') # 强制 Word 原生可编辑公式
b.para('由 {X1} 可得', lx_map={'X1': r'P_e=P_1+P_2'}) # {Xn} 占位 → 行内公式
b.table(['列1','列2'], [['a','b']], caption='表1 示例')
b.para('文案丨xx', style='signature')
b.save('out.docx')
python scripts/build_from_content.py --content content.yaml --out out.docx
python scripts/build_from_content.py --content content.json --template push --out out.docx
content.yaml 的 blocks: 支持:title / heading{level} / para{style,fn,lx} / formula{latex,caption} / caption / note / image{path,caption,width} / table{headers,rows} / signature{lines} / blank / pagebreak。输出 stdout 为 JSON。
公式有两种呈现,由模板 formula 样式的 mode(或调用级 mode=)决定:
| mode | 产物 | Word 里可编辑 | 依赖 |
|---|---|---|---|
omml | Word 原生公式(OMML) | ✅ | pandoc |
image | XeLaTeX 紧裁 PNG | ❌(图片) | xelatex + PyMuPDF |
auto(默认) | 有 pandoc 用 omml,否则 image | 视环境 | 视环境 |
omml(推荐,需编辑时用):LaTeX →(pandoc -f markdown -t docx)→ 抽取 m:oMath/m:oMathPara → 写入段落,Word 中可点选编辑。image:遵循 AGENTS.md「数学公式渲染(转图片)」——XeLaTeX preview+tightpage 紧裁 → PyMuPDF 转 PNG → 作为图片插入(任何环境可渲染,但不可编辑)。b.formula(latex, caption=None, mode=None)。{X1} 占位 + lx_map={'X1': r'...'}(b.para(..., lx_map=...),或 build_docx.py 的 Lx(text, lx_map))。图片模式下可用模板 formula 样式的 inline_scale(默认 1.0)与 inline_border(默认 1pt)微调行距。omml 需 pandoc(定位顺序 --pandoc > OPENCODE_PANDOC > PATH);image 需 xelatex(--xelatex > OPENCODE_XELATEX > PATH)+ PyMuPDF。缺失时给出明确报错。mode: image(公式为图片,不计入字数),或精简公式、把变量与单位集中说明。核对一律以 Word 状态栏为准。# 图片模式(也可被引擎 import:from latex_formula import render_formulas)
python scripts/latex_formula.py --latex "E=mc^2" "\int_0^1 x^2\,dx=\frac{1}{3}" --outdir .\formula_png
python scripts/latex_formula.py --input formulas.txt --outdir .\formula_png --dpi 300
# Word 原生公式(OMML)
python scripts/latex_formula.py --latex "E=mc^2" --omml
图片缓存在
--outdir(引擎默认<cwd>/_docx_formula_cache),按「公式+样式+dpi+border」哈希命名,重复渲染自动复用。
| 类型 | 路径 | 生命周期 |
|---|---|---|
| SKILL 本体 | skills/docx-writer/SKILL.md | 长期 |
| 标准基底 | skills/docx-writer/template.docx | 不修改 |
| 模板配置 | skills/docx-writer/templates/*.yaml | 长期(可新增) |
| 持久脚本 | skills/docx-writer/scripts/ | 长期 |
| 临时脚本 | 项目文件夹(历史遗留 skills/docx-writer/tmp/ 保留不删) | 记录到 log.md |
| 输出 | 当前工作目录或 --out 指定处(历史遗留 skills/docx-writer/output/ 保留待迁移) | 不写 skill 目录 |
| 需求 | 推荐 | 理由 |
|---|---|---|
| 创建含脚注的 docx | bayoo-docx | paragraph.add_footnote() 原生创建,编号/间距正确 |
| 格式化(字体/字号/缩进) | bayoo-docx + lxml | 字体用 run.font API,边框/样式用 lxml 微调 |
| 加载模板样式 | bayoo-docx | Document(模板路径) 继承模板的样式和页设置 |
| 脚注字体/字号设定 | 创建后遍历 fn.findall('.//w:r') | 显式设置 w:sz=18 和 w:rFonts |
不要用的:
add_footnote)footnotes.xml(编号/分隔线不可控)SKILL 自带模板 skills/docx-writer/template.docx(已验证的初稿文件,含 Word 原生脚注)。该模板提供正确的 footnote text(样式ID=12)和 footnote reference(样式ID=20)样式。
脚本中引用方式:TEMPLATE = os.path.join(SKILL_DIR, 'template.docx')
使用 B('文本{F1}更多文本') 语法,其中 {F1} 表示脚注。
脚注编号规则:
句子内容。{F1} ✅句子内容{F1}。 ❌FN_TEXT_MAP = {
'F1': '作者. 标题. 期刊, 年份, 卷(期): 页码.',
}
| 函数 | 用途 | 字体/字号 |
|---|---|---|
P(text, ...) | 通用段落 | 宋体/Times New Roman 10.5pt |
B(text) | 正文 | 宋体/Times New Roman 10.5pt |
Bs(text) | 小字正文(摘要) | 宋体/Times New Roman 9pt |
H1(text) | 一级标题 | 黑体/Arial 12pt |
H2(text) | 二级标题 | 黑体/Arial 12pt |
H3(text) | 三级标题 | 黑体/Arial 10.5pt |
Tc(text) | 表题/图题 | 宋体/Times New Roman 9pt |
add_img(path, cap) | 插入图片+图题 | 居中 |
Fx(latex, cap=None) | 插入行间公式(XeLaTeX 渲染) | 居中,按公式宽度等比 |
Lx(text, lx_map) | 含行内公式占位 {X1} 的正文段落 | 同正文 |
add_tbl(h, rows) | 三线表格 | 小五号 |
doc.save('输出路径.docx')
不要自己写引擎。 排版逻辑固定在 scripts/docx_engine.py,体例固定在 templates/*.yaml,你只写内容。
| 文件 | 说明 | 路径 |
|---|---|---|
docx_engine.py | 引擎(Template + Builder),不修改、无 CLI | skills/docx-writer/scripts/docx_engine.py |
build_from_content.py | 声明式渲染器(content.yaml/json → docx,CLI) | skills/docx-writer/scripts/build_from_content.py |
build_docx.py | 内容脚本模板(Python API 逐句写,CLI 可覆盖) | skills/docx-writer/scripts/build_docx.py |
latex_formula.py | LaTeX → Word 原生公式(OMML,可编辑)或紧裁 PNG(pandoc / XeLaTeX+PyMuPDF,CLI + 可 import) | skills/docx-writer/scripts/latex_formula.py |
templates/*.yaml | 模板配置(academic / push / news / script) | skills/docx-writer/templates/ |
用途:LaTeX 公式 → Word 原生公式(OMML,可编辑) 或 紧裁 PNG(XeLaTeX+PyMuPDF)。输出 stdout JSON。
| 参数 | 说明 | 必填 |
|---|---|---|
--latex TEX [TEX ...] | 输入:一个或多个 LaTeX 公式字符串 | 二选一 |
--input FILE | 输入:公式文件(.txt 每行一条 / .json 字符串数组) | 二选一 |
--outdir DIR | 输出 PNG 目录(默认 ./formula_png,自动创建) | 否(仅图片模式) |
--dpi N | 渲染 DPI(默认 300) | 否 |
--inline | 行内公式(textstyle);默认行间(displaystyle) | 否 |
--omml | 改为输出 OMML XML(Word 原生可编辑公式,需 pandoc) | 否 |
--xelatex PATH | xelatex 路径(默认 OPENCODE_XELATEX 或 PATH) | 否 |
--pandoc PATH | pandoc 路径(--omml 时用,默认 OPENCODE_PANDOC 或 PATH) | 否 |
--timeout N | 单次编译超时秒(默认 180) | 否 |
示例:python scripts/latex_formula.py --latex "E=mc^2" --outdir ./png
输出:{"status":"ok","count":N,"outdir":...,"items":[{"latex","png","width_px","height_px","width_pt","height_pt"}...]}
用途:声明式内容文件(YAML/JSON)→ docx。输出 stdout JSON。
| 参数 | 说明 | 必填 |
|---|---|---|
--content FILE | 输入内容文件(YAML/JSON),内含 template: + blocks: | ✅ |
--out FILE | 输出 docx 路径(默认:当前工作目录/<content 名>.docx) | 否 |
--template NAME | 覆盖内容文件里的 template(academic/push/news 或 yaml 路径) | 否 |
--image-base DIR | 图片相对路径基准目录(覆盖内容里的 image_base) | 否 |
示例:
python scripts/build_from_content.py --content content.yaml --out out.docx
python scripts/build_from_content.py --content content.json --template push --out out.docx
python scripts/build_from_content.py --content content.yaml # 输出到 当前目录/content.docx
输出:{"status":"ok","out":abs_path,"template":"...","blocks":N}(失败 {"status":"error",...} + exit 1)。
用途:内容脚本模板——复制到工作目录后改内容,用 B/H1/H2/H3/Tc/add_img/add_tbl 逐句写。所有参数可选,不带参数时行为与原来完全一致(输出 输出路径.docx 到当前工作目录)。
| 参数 | 说明 | 必填 |
|---|---|---|
--template NAME | 模板名或 yaml/json 路径(默认 academic) | 否 |
--out FILE | 输出 docx(默认 输出路径.docx,写当前工作目录) | 否 |
--fig-dir DIR | 图片基准目录(默认 图片目录) | 否 |
--fn-map REF | 脚注映射:JSON 文件路径或内联 JSON(默认文件内 FN_TEXT_MAP) | 否 |
示例:
python scripts/build_docx.py # 原样行为
python scripts/build_docx.py --template push --out 推送稿.docx
python scripts/build_docx.py --fig-dir .\imgs --fn-map fn.json
输出:进度 Saved: <path> 写 stderr;文档写 --out。
用途:引擎库(Template / Builder),供 Python API 调用与上面两个脚本 import,无 CLI、不修改。用法见「引擎 API」节。
写法 A(声明式,推荐):写一个 content.yaml(template: 指定体例 + blocks: 列表),跑 build_from_content.py。
写法 B(API,复杂逻辑时):复制 build_docx.py → 改 TEMPLATE_NAME/OUTPUT/FIG_DIR/FN_TEXT_MAP(或走 CLI 参数)→ 在 build_content() 里用 B/H1/H2/H3/Tc/add_img/add_tbl 写 → 运行。
不要修改 scripts/docx_engine.py 与 templates/*.yaml 的既有字段语义;要换体例,新增/复制模板 yaml 即可。输出/临时文件写当前工作目录或 --out 指定处,不写进 skill 目录。
| 元素 | 中文 | 英文/数字 | 字号 | 对齐 | 缩进 | 行距 |
|---|---|---|---|---|---|---|
| 文章标题 | 黑体 | Arial | 四号(14pt) | 居中 | 无 | 1.5倍 |
| 作者行 | 楷体 | Times New Roman | 五号(10.5pt) | 居中 | 无 | 1.5倍 |
| 摘要/关键词 | 宋体 | Times New Roman | 小五(9pt) | 左对齐 | 首行缩进2字符 | 1.5倍 |
| 一级标题 | 黑体 | Arial | 小四(12pt) | 居中 | 无 | 1.5倍 |
| 二级标题 | 黑体 | Arial | 小四(12pt) | 左对齐 | 首行缩进2字符 | 1.5倍 |
| 三级标题 | 黑体 | Arial | 五号(10.5pt) | 左对齐 | 首行缩进2字符 | 1.5倍 |
| 正文 | 宋体 | Times New Roman | 五号(10.5pt) | 左对齐 | 首行缩进2字符 | 1.5倍 |
| 表题/图题 | 宋体 | Times New Roman | 小五(9pt) | 居中 | 无 | 1.5倍 |
| 表内文字 | 宋体 | Times New Roman | 小五(9pt) | 居中 | — | 1.5倍 |
| 表注 | 宋体 | Times New Roman | 小五(9pt) | 左对齐 | 首行缩进2字符 | 1.5倍 |
| 脚注正文 | 宋体 | Times New Roman | 小五(9pt) | 左对齐 | 无缩进 | 单倍行距 |
| 脚注编号(正文中) | — | — | — | — | — | 上标 |
句子内容。{F1} — 句号在脚注标记之前,符点在前,注释号在后add_picture() 直接生成的 <wp:inline> 缺少 distT/distB/distL/distR 与 wp:effectExtent 属性,Word 会对这类不完整的 inline 图渲染出黑色方框/边框(LibreOffice 转 PDF 不显示,仅 Word 可见,极易漏检)。规避方法:
p.addnext() 插入),保留 Word 原生生成的 drawing XML(含 distT/distB/distL/distR 与 wp:effectExtent),不做 add_picture 重建<wp:inline> 上补 distT="0" distB="0" distL="0" distR="0",并在 <wp:extent> 后补 <wp:effectExtent l="0" t="0" r="0" b="0"/>add_picture 无法解析相机照片的 EXIF(抛 UnexpectedEndOfFileError/UnrecognizedImageError,剥 EXIF 后常因缺 JFIF 仍失败)。做法:docx.Document(用户原文件) 提取其 w:drawing 元素 deepcopy(保留 Word 原生结构,天然含 distT/effectExtent),用 Part(PackURI('/word/media/xxx.jpeg'), 'image/jpeg', 原始字节, package) + doc.part.relate_to(part, RT.IMAGE) 得到 rId,改 a:blip 的 r:embed 后插入目标 run;图片显示尺寸沿用原 drawing 的 wp:extent。deepcopy 提取时须用 python-docx 解析过的元素(自带命名空间),不可直接 etree.fromstring 抽取片段(会 unbound prefix)keep_with_next:p.paragraph_format.keep_with_next = True 虽能防止"图片在页尾、图题流到下一页"的孤行图题,但 Word 在"显示格式标记"时会在该段左侧渲染一个小黑方块(keep-with-next 的格式标记),用户会误以为是黑框/黑块。若不能接受,宁可不设 keep_with_next(代价是图片与图题可能分页)p.alignment = WD_ALIGN_PARAGRAPH.RIGHT(枚举值 2),不用首行缩进""(U+201C/U+201D);引号内再套引号时,内层用中文单引号 ''(U+2018/U+2019)""(U+0022),字体随上下文(英文用 Times New Roman,中文用宋体)eastAsia/ascii/hAnsi 三个字体字段必须设为所在段的中文字体(正文宋体、标题黑体、副题楷体等),禁止让中文引号使用 Times New Roman/Arial 等西文字体——否则 Word 会把引号渲染成西文直引号样式(字形立、无中文引号弧度),属于排版不合格fix_quotes(parent),同时处理双引号与嵌套单引号):
"(U+0022);嵌套引号用 ASCII 单引号 '(U+0027)fix_quotes(parent) 遍历段落/表格,对每组引号内容做纯 ASCII 检测eastAsia/ascii/hAnsi 均为所在段的中文字体(默认宋体)。原 run 的字体不受影响,英文保持 Times New Romanascii/hAnsi 为宋体,否则会覆盖 run 中英文单词的字体。必须拆分 run,只修改引号字符所在 run 的字体。引擎 docx_engine.py 已内置 fix_quotes()(双引号+嵌套单引号、字体跟随所在段中文字体)为标准后处理replace/正则/拼接)只解决了"字符对不对",没有解决"字体"——中文引号仍会以 Times New Roman 渲染。字体问题必须在 OOXML run 层处理(拆分 run + 设 rFonts)。验证方法:生成后用脚本遍历 w:r,检查含引号字符的 run 其 w:rFonts 的 ascii/hAnsi 不得为 Times New Romandef B(t): return P(t) # 正文
def Bs(t): return P(t, sz=Pt(9)) # 小字
def H1(t): return P(t, cn='黑体', en='Arial', # 一级标题
sz=Pt(12), align='center', fi=None)
def H2(t): return P(t, cn='黑体', en='Arial', # 二级标题
sz=Pt(12))
def H3(t): return P(t, cn='黑体', en='Arial', # 三级标题
sz=Pt(10.5))
def Tc(t): return P(t, sz=Pt(9), align='center', # 表题/图题
fi=None)
B('文本{F1}') — 正文带脚注,句号在脚注前H1('一、标题') — 一级标题add_tbl(['列名'], [['数据']]) — 三线表add_img('路径.png', '图1 标题') — 图片+图题\u3000)。正文缩进通过 fi=Cm(0.74) 控制,表格/标题用 align 参数控制。全角空格在不同字体和排版环境下表现不一致,会导致错位。| 问题 | 原因 | 解决 |
|---|---|---|
| 脚注显示等线字体 | bayoo-docx 不自动设置中文字体 | 创建后遍历 fn.findall('.//w:r') 显式设 rFonts:eastAsia=宋体, ascii=Times New Roman, hAnsi=Times New Roman |
| 脚注五号字(应为小五) | 脚本文本 run 未设 w:sz | 显式设 sz=18 和 szCs=18(18 half-points = 9pt = 小五号) |
| 正文引用不上标 | bayoo-docx 的 rStyle 匹配不到模板的样式 ID | 后处理加 vertAlign=superscript |
| 句号位置 | 规范要求 。{F1} 而非 {F1}。 | 写作时注意句号在脚注标记前 |
| 表格全框线 | Table Grid 样式默认全框 | 用 lxml 移除所有边框后加三线:header top、header bottom、last row bottom |
| 封面垂直居中 | sectPr.vAlign=center 在 python-docx 中无效 | 用单格表格 + 行高固定(trHeight hRule=exact,值≈14000 twips) + 单元格 vAlign=center,表格宽度设100%,边框全nil |
分节后 add_table 报错 | 手工创建的 sectPr 缺少 page_width 属性 | 用 copy.deepcopy(原sectPr) 克隆而非 etree.Element() 新建 |
footerReference.r:id 报错 | 属性命名空间用错 | 用 {http://schemas.openxmlformats.org/officeDocument/2006/relationships}id,不是 w:id |
B() 不支持 sz 参数 | 函数定义未暴露字号参数 | 小字直接用 P(text, sz=Pt(9)),不用 B() |
全角括号 ((U+FF08) 导致 SyntaxError | Python 将全角括号识别为函数调用 | 必须用半角 (,字符串内用全角没问题 |
| 中文引号被渲染成西文直引号样式 | 引号字符所在 run 的 ascii/hAnsi 为 Times New Roman | 中文引号必须拆为独立 run,eastAsia/ascii/hAnsi 均设为所在段中文字体(fix_quotes() 已内置,含嵌套单引号) |
| 图片在 Word 中显示黑色方框(LibreOffice 正常) | python-docx add_picture() 生成的 <wp:inline> 缺 distT/distB/distL/distR 与 wp:effectExtent | 在用户原 Word 文件上原位重构保留 Word 原生 drawing XML;或手动补 distT="0" distB="0" distL="0" distR="0" 与 <wp:effectExtent l="0" t="0" r="0" b="0"/> |
图片段 keep_with_next 在 Word 中出现小黑方块 | 段落的分页控制属性(keep_with_next / keep_together),"显示格式标记"开启时显示 | 不可打印、不影响输出;交付给用户的稿子若在意观感,就不要给段落设这两个属性(代价:图片与图题可能分页) |
python-docx add_picture 解析含 EXIF 的相机照片失败 | 抛 UnexpectedEndOfFileError 或 UnrecognizedImageError(EXIF/TIFF 解析缺陷;剥 EXIF 后缺 JFIF 仍可能失败) | 提取用户原文件 w:drawing deepcopy + 手动建 media part(原始字节)插入,保留 Word 原生结构且零压缩 |
| 图片在页尾、图题流到下一页(孤行图题) | 图片段与图题未保持同页 | 图片段设 keep_with_next = True(注意:会带来 Word 小黑方块格式标记,见上条) |
| Word「字数」远大于正文估算(如正文 340 字、Word 却显示 543) | Word 把公式(OMML)内容也计入「字数」,行间大公式(矩阵/分式)尤甚 | 有字数上限时:① 首选 formula 用 mode: image(公式转为图片,不计入字数);② 或删行间公式、把变量与单位集中说明;③ 少用重复的行内公式。核对口径以 Word 状态栏为准 |
OMML 公式内空格丢失(显示 1.2m 而非 1.2 m) | pandoc 输出的 <m:t> 未带 xml:space="preserve",Word/LO 会吞掉首尾空格 | latex_to_omml() 已自动为含空格的 m:t 补 xml:space="preserve" |
sectPr.vAlignhRule=exact,值约14000 twips(A4可用高度≈24cm)tblW type=pct w=5000)vAlign=center,边框全 val=nilsectPr + type=nextPage,必须 copy.deepcopy(原sectPr) 后修改sectPr 中添加 footerReference,指向包含 PAGE 域的 footer partfooterReference 即可隐藏页码tab_stops.add_tab_stop(Cm(15.5), WD_TAB_ALIGNMENT.RIGHT, WD_TAB_LEADER.DOTS) 实现label + '\t' + str(pnum) 拼接page_break() + H1()(居中,14pt黑体)H3()(10.5pt黑体),问题用 B() 编号run.add_picture(FLAG, width=Cm(10))soffice.exe --headless --convert-to pdfpdf2image.convert_from_path(pdf, dpi=150) 转图片