开源项目 OfficeCLI 的 Mermaid 图形编译链路拆解
本文基于 OfficeCLI 仓库 commit 459b1a4(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/iOfficeAI/OfficeCLI 最新代码与文档为准。
**这条链路值得读,不是因为它把图画出来了,而是因为它把「图」拆成了两层中间表示(IR):一层只有拓扑没有坐标,一层只有坐标没有语义。**前一层决定了以后能不能接别的图形语言,后一层决定了同一份布局能不能同时落到 Word 和 PPT。绝大多数「文本转图」的实现是一步到位的,读完这份代码你会明白一步到位省下的那点工作量,后面要用什么来还。
先消歧:OfficeCLI 是 iOfficeAI 在 GitHub 上放出的一个开源项目(Apache-2.0,NOTICE 里写明 Copyright 2026 OfficeCLI,由 goworm 创建维护),不是「用命令行操作 Office」这个泛指说法,也和微软没有任何从属或授权关系——正文里出现的 Word、Excel、PowerPoint 指的是文件格式和应用本身。
一、这条链路要解决的问题
Agent 写文档时,画图是最尴尬的一环。让模型输出一张图片,文档里就多了一块死掉的位图:改一个字要重新生成整张图,甲方在 PowerPoint 里想挪一下框都挪不动。让模型直接吐 OOXML(Office 的文件格式,本质是一个 zip 包,里面装着一堆描述文档结构的 XML),命中率又低得离谱——形状的几何、连接线的翻转、文本框的内边距,任何一处写错,文件就打不开。
OfficeCLI 在 add --type diagram 上给的答案是:输入端收 Mermaid 文本(模型最熟的图形 DSL),输出端生成一组原生形状和连接线,落进 .docx 或 .pptx 里就是普通图形,人可以双击改文字、拖动位置。中间那段「Mermaid 有拓扑没坐标,Office 要坐标不认拓扑」的落差,由一个自带的布局引擎补上。
它同时留了第二条路。render 这个属性有三个取值方向:native / shapes 走内置合成器;image / svg / browser 调真正的 mermaid.js(找得到 mermaid-cli 的 mmdc,或者 Chrome 系无头浏览器)渲染成 PNG 再嵌进去;不写就是 auto——有浏览器走图片,没有回落到 native。两条路的分工很清楚:native 换来可编辑性,代价是只支持 flowchart 和 sequenceDiagram 两类;image 换来全类型全保真,代价是要有外部依赖,而且产出是一张栅格图。
顺带把本站几篇相近的文章分工说清:这篇只讲 OfficeCLI 这条 Mermaid 到 Office 原生图形的编译链路,模型结构化输出本身为什么不稳、怎么兜底,看结构化输出不稳的处理;另一个开源项目怎么把三维场景导成二维平面图,是完全不同的一套几何问题,看Pascal Editor 的平面图导出;「一段文本进、一份可交付产物出」这类工具怎么做选型判断,可以对照翻译工具的选型口径。
二、四步链路各归各位
src/officecli/Core/Diagram/ 这个目录下一共 7 个 .cs 文件(ls 一数就知道),职责切得很干净。
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
DiagramCompiler | 嗅探首行 header,分派到对应布局引擎 | src/officecli/Core/Diagram/DiagramCompiler.cs | 报「diagram type … is not supported yet」时 |
MermaidParser | Mermaid 文本 → 语义 IR(节点、边、方向) | src/officecli/Core/Diagram/MermaidParser.cs | 节点形状不对、某条边莫名其妙没了 |
FlowchartLayout | 分层布局 + 正交走线,产出带坐标的几何 IR | src/officecli/Core/Diagram/FlowchartLayout.cs | 连线打架、节点挤在一起、整张图被缩小 |
SequenceLayout | 时序图的解析和生命线布局,产出同一份几何 IR | src/officecli/Core/Diagram/SequenceLayout.cs | 时序图里 alt/loop/note 没画出来 |
DiagramModel | 两层 IR 的类型定义(DiagramGraph / LaidOutGraph) | src/officecli/Core/Diagram/DiagramModel.cs | 想接新的图形前端或新的输出格式 |
DiagramStyles | 形状枚举 → OOXML 预设几何名 + 填充/描边色 | src/officecli/Core/Diagram/DiagramStyles.cs | 想换配色、想知道菱形到底映射成什么 |
MermaidImageRenderer | 另一条路:调外部 mermaid.js 渲染成图片 | src/officecli/Core/Diagram/MermaidImageRenderer.cs | 走 image 模式、找不到渲染后端、语法报错带行号 |
| pptx / docx 两个发射器 | 把几何 IR 翻译成各自格式的 DrawingML(OOXML 里描述图形几何、填充、线条的那套 XML 词汇) | src/officecli/Handlers/Pptx/PowerPointHandler.Add.Diagram.cs、src/officecli/Handlers/Word/WordHandler.Add.Diagram.cs | 位置、缩放、Word 里中文显示不出来 |
两个发射器文件名里的 .Add.Diagram 是 C# 分部类(partial class)的惯用切法——同一个类的代码拆到多个文件里,编译时再拼回一个类,所以 PowerPointHandler 这种上千行的处理器可以按功能分文件放。
入口是 DiagramCompiler.Compile。它取第一行非空、非 %% 注释的内容当 header:匹配 flowchart 或 graph 就走 FlowchartLayout.Layout(MermaidParser.Parse(...)),匹配 sequenceDiagram 就走 SequenceLayout;header 为空或首字符不是字母,按 Mermaid 自己的宽松默认当 flowchart 处理;剩下的情况直接抛异常,消息里带上识别出的类型名和「currently: flowchart, sequenceDiagram」。这个设计的注释写得很直白:不支持的类型宁可报错,也不要画出垃圾——diagram 这个统称才站得住。
三、前端极度宽容,后端极度讲究
MermaidParser 的取向是「能认多少认多少,认不出来就丢,永远不抛异常」。它认 11 条节点形状正则,按最具体优先排:([stadium])、[[subroutine]]、[(database)]、((circle))、{{hexagon}}、{diamond}、[/parallelogram/]、[\trapezoid\](和上一条同归平行四边形)、>flag]、(round)、[rect]。链式边、A -->|text| B 的管道标签、A -- text --> B 的中缀标签(用一条正则折叠成管道形式)、A & B --> C & D 的分组展开都在。subgraph、end、style、classDef、class、linkStyle、click 这些指令整行跳过,免得变成一堆垃圾节点;行尾的 :::className 和行首的边 id(形如 e1@)也被剥掉。语句分隔既认换行也认分号,所以单行写法能过。
有两个细节值得单独说。一是节点 id 的字符集用的是 [\p{L}\p{N}_]+,也就是任意 Unicode 字母和数字,注释里说明了原因:不这么写,一张全中文的流程图会解析出零个节点,然后报「diagram has no nodes」。二是 A & B 的拆分是括号深度感知的,标签文本里出现 & 不会被误切。这两处都是被真实输入教出来的。
后端就是另一副面孔了。FlowchartLayout 的流水线是教科书式的 Sugiyama 分层布局:DFS 找回边打破环 → 最长路径定层 → 跨多层的边上插虚拟节点(id 以 __d 开头,尺寸固定 0.5×0.5)→ 重心法做 6 轮上下交叉扫描 → Brandes–Köpf 算横轴坐标 → 整体缩放 → 自己算正交走线 → 处理自环和平行边标签。
Brandes–Köpf 那一段是四遍对齐(上下 × 左右四个方向各跑一遍垂直对齐加压缩),最后把四组坐标排序取中间两个的平均。这是 dagre 里的经典做法,好处是对称的图能得到对称的结果。
尺寸估算这块很务实:TextExtent 按字符宽度累加,码位大于 0x2E80 的算 0.58,其余算 0.30,单行上限 5.0,超了就折行。菱形(Decision)宽度按 tw * 2.2 + 1.0 算并且高度至少 2.2,因为菱形四角是空的,文字要塞进内接矩形。圆形取长宽较大值再乘 1.35。所有节点最小 2.4×1.1。这些都是厘米——整条布局链路以厘米为单位,到发射器才乘 360000 换成 EMU(OOXML 的内部长度单位,1 厘米 = 360000 EMU)。
走线部分有两个我觉得挺聪明的处理。一是端口分配:源端和目标端的挂点是放在一起按「节点 + 所在的那一面」分组的。注释里点名了老版本的 bug——分开分组时,一个节点同一条边上既有进边又有出边,两条都拿到「独占该边、居中」的挂点,于是重叠成一条线;合并分组后各自错开。二是拐点让道:所有拐角按坐标除以 0.8 分桶,桶内区间重叠的拐点排进不同轨道,再以 0.6 为间距向两侧摊开,避免多条线压在同一条水平/垂直线上。
SequenceLayout 是独立的一条线:参与者盒高 1.1、水平间距 1.4、消息行距 1.15,生命线是一条虚线(Dashed = true、不带箭头),返回消息按操作符是否以 -- 开头判虚实,自消息画成一个小回环。它认 participant / actor 声明和 as 别名,也容忍 A->>+B 这种激活控制符——但只是不让消息被丢掉,激活条并不画。参与者 id 同样放开到 Unicode 字母。
四、一份几何 IR,两个发射器
DiagramModel.cs 里把两层边界写得很明确。语义 IR(DiagramGraph)是前端边界:只有节点、边、方向,没有任何坐标,注释里说 Mermaid、draw.io、graphviz-dot 都可以映射到这一层。几何 IR(LaidOutGraph)是后端边界:PlacedNode 带厘米坐标和尺寸,RoutedEdge 是一条正交折线,EdgeLabel 带一个 Opaque 开关,还有画布宽高和一个 FontScale。注释里那句解释了两层拆分的全部理由:draw.io 的输入本身自带坐标,它应该从几何 IR 这一层进来,跳过布局引擎。
DiagramStyles 是共享的一小张表,把 10 种形状映射到 OOXML 的预设几何名(rect、diamond、roundRect、ellipse、hexagon、parallelogram、can)加一组填充色和描边色,边线统一 4D4D4D。放在共享层的目的写在注释里:pptx 和 docx 两个发射器不能各画各的,那样迟早跑偏。
两个发射器的差异,恰好就是两种文档格式的差异。
pptx 这边把整张图包进一个组合形状:组合的 chOff/chExt 和 off/ext 设成相同值,也就是子元素坐标系和幻灯片坐标系一比一,加进去时每个子形状保持自己算好的绝对位置;等有人后来 set 组合的宽高,才按这个基线整体缩放,子形状的字号也跟着重算。这样人拖一个对象就能挪整张图,Agent 也只需要记一个稳定路径 /slide[N]/group[K]。docx 那边的 schema 说明里补了一句反面情形:在 Word 里用鼠标拖拽缩放组合,几何会跟着变,字号不会重算,想让文字也等比就得走命令行的 set。连接线用的是 StraightConnector1,代码里按四个象限分别设水平/垂直翻转,注释解释了为什么必须这么做:不翻转的话,向右上方走的那一段会画成错误的对角线,箭头还会长在反的一端。字号是 18 * FontScale * 缩放系数 取整、下限 1 磅——注释特意说明为什么不设一个更高的下限:布局阶段每个盒子就是按基准字号量好的,缩放时字号跟着缩才不会溢出,硬保一个较大的下限反而会让字比盒子还大。
docx 这边没有幻灯片可以改尺寸,所以整张图缩放到节的正文区宽度,而且默认只缩不放。它是直接拼 XML 字符串生成 wpg 组合(Word 的组合图形),边不用连接线而是用 custGeom 自定义几何画折线,并且给轴对齐的那种「零宽或零高」的段留了 1 磅(12700 EMU)的最小包围盒,否则退化成一个面积为零的框。还有一处很有意思的补丁:文本运行里显式写了 <w:rFonts w:eastAsia="SimSun" w:hint="eastAsia"/>,注释说明原因是 PowerPoint 会自动套用主题的中日韩字体,Word 不会——不写这一句,文本框里的中文可能直接渲染成空白。
五、边界与代价
这套设计明确放弃了一些东西,值得逐条摆出来。
它不是一个持久化元素。 代码注释和 schema 都写了:diagram 是「只增(ADD-ONLY)合成器」,和 equation 同类,文档里根本不存在一个叫 diagram 的节点,schema 的 operations 里 set/get/query/remove 四项全是 false。图一旦落进文档,它就是一堆普通形状了,你没法回头说「把这张图的第三个节点改成菱形」。你能操作的只是 Add 返回的那个组合路径——对着它可以读回位置尺寸、可以整体缩放、可以整个删掉,但改不了图的语义。改内容的办法只有一个:删掉重新生成。
native 模式只认两类图。 flowchart/graph 和 sequenceDiagram,其它 Mermaid 类型走到 DiagramCompiler 就被明确拒绝。时序图里的激活条、alt/opt/loop 分支块、note,源码注释里标为 deferred,不画。想要全类型只能走 image 模式,那就换回了一张不可编辑的位图。
它自带的排版不是 mermaid.js 的排版。 同一份源码,native 和 image 两条路出来的图长得不一样——形状配色是这个项目自己那套,字体度量是按码位粗估的,中日韩字符一律按 0.58 算宽度。混排的标签、比例特殊的字体,估宽会有偏差,兜底靠形状上的自动缩排。
超长图有硬上限。 布局阶段先把整图按 55 的画布上限等比缩小(只缩不放);pptx 的 poster 模式把幻灯片撑到图的自然尺寸,但每条边被夹到 142.24 厘米(56 英寸),超过这个值 PowerPoint 会拒绝打开文件;Word 的页面边长上限更低,55.88 厘米(22 英寸)。真正超长的流程图,正确答案是拆图,不是把它塞进一页。
image 模式有外部依赖,而且会联网。 它先找 mermaid-cli 的 mmdc(OFFICECLI_MMDC 环境变量可以指定路径),找不到再找 Chrome 系浏览器;走浏览器时页面要加载 mermaid 的运行时,代码里固定了主版本 11,取用顺序是本地缓存、项目自己的镜像地址 d.officecli.ai、公共 CDN cdn.jsdelivr.net。也就是说这条路在离线或受限网络里会退化甚至失败,暴露面是一次对外部主机的脚本下载。介意的话就显式写 render=native。
六、上手与避坑清单
一、别默认 render 是 native。 不写 render 就是 auto,而 auto 在有浏览器的机器上会走 image。踩坑场景很典型:本地 CI 容器里没浏览器,出来是可编辑形状;开发机上有 Chrome,同样的脚本出来是张图片,两边评审结果对不上。要哪种就写死哪种。
二、纯中文流程图先确认节点解析出来了。 解析器放开了 Unicode id,但如果你的图只有 style、subgraph 这类被整行跳过的语句,或者写法落在解析器认不出的分支里,最终会撞上「diagram has no nodes」这个守卫。它是布局阶段主动抛的——因为没有这层守卫,后面求包围盒的 Min/Max 会抛一句「Sequence contains no elements」,那种报错完全没法排查。看到它就回头看源文本,而不是怀疑环境。
三、poster=true 改的是整个演示文稿。 pptx 只有一个全局幻灯片尺寸,所以 poster 会把每一张幻灯片都撑成这张图的自然大小。往一份既有的多页 deck 里加图时用了它,等于顺手把别人的版式毁了。默认路径是不动幻灯片尺寸、把图缩进去并居中——这个默认值就是为这件事设的。
四、只给 width 不给 height,行为是不一样的。 pptx 这边,两个都不给是「缩进内容区,且只缩不放」;给了任意一个就变成「填满这个盒子」,小图会被放大。docx 那边默认按节的正文区宽度缩放且不放大,给了 width/height 才允许放大。想要确定性就两个都给,等比是始终保证的。
五、Word 里的图挪不动,别去找 x/y。 docx 的 diagram 在 Add 时不接受 x/y,图是以边距为参照的浮动锚定。要挪位置,用 Add 返回的组合路径去 set。这一条在项目自带的技能说明里写得很清楚,但按 pptx 的经验直觉去试一定会撞墙。
六、语法错误在 image 模式下不会回落。 代码里对 mermaid 语法异常做了单独处理:直接往上抛,带着 mermaid 自己的行号信息,绝不回落到 native。理由是同一段坏文本,内置合成器要么同样拒绝、要么画出垃圾。所以看到带行号的报错,是让你去改源文本,不是让你换渲染模式。
七、想清楚落盘时机。 这个工具改的是磁盘上的真实文件,不是副本。单条 add 是安全的:编译和布局都在往文档树追加之前完成,任何异常都不会留下半张图。但一批命令里前面已经成功的那些改动是真的写进去了。常驻进程模式下还多一层:文档树在内存里,什么时候刷盘由刷新策略决定——each 是每条改动命令返回前刷,auto(默认)是空闲防抖、间隔按实测保存耗时自适应,也可以给固定秒数,off 则只在显式保存/关闭/停止时刷。用第三方库或 Office 直接打开同一个文件去看结果之前,先确认它已经落盘。重要文件先备份,这条不用解释。
收尾
真要评估这条链路能不能用在你的产线上,我建议按这个顺序打开文件:先看 DiagramCompiler.cs(40 多行,看清分派边界),再看 DiagramModel.cs(两层 IR 的契约,决定了扩展性的上限在哪),然后按你的输出格式挑一个发射器读——它们才是「同一份布局怎么变成两种格式」这个问题的答案。FlowchartLayout.cs 放最后读,那是一份可以独立拿走的 Sugiyama 实现,和 Office 没有关系。
自检三问:你要的图在 flowchart 和 sequenceDiagram 这两类里吗?产出必须可编辑,还是一张图片也能交差?运行环境有没有浏览器、允不允许联网?三个答案连起来,就能定下 render 该写什么。至于图本身该怎么画得让人看懂,那是另一个题目,可以顺着让 AI 画架构图那一篇往下想。
本篇属于一个把开源Office 文档读写套件 OfficeCLI逐层拆开讲的系列,整体地图见 OfficeCLI 是什么:给 AI Agent 用的开源文档读写套件;沿着这条线往下,还可以看 开源项目 OfficeCLI 怎么读写 Word 的表单域、修订与批注 和 拆开源项目 OfficeCLI 的插件协议:第三方格式处理器如何以独立进程接进主程序。