尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MiroFish:将Miro白板流程图编译为可执行命令序列

MiroFish:将Miro白板流程图编译为可执行命令序列 流程图画了三遍落地的时候还是有人在群里问这步到底先跑哪个。——这类场面我见过太多次了。白板上画得漂漂亮亮的一条链路箭头、颜色、便签、分组框一应俱全评审会上一片清晰结果真到执行的时候还是得有个人对着屏幕一步一步手工敲敲漏一步就得从头来。我在过去两年里反复琢磨过这件事白板是给人看的命令行是给机器跑的这两者中间隔着一个纯手工的翻译层而这个翻译层恰恰是最容易出错、最没人愿意维护的地方。MiroFish 就是我为填这个断口写的一个小工具名字取的是白板Miro 命令行Fish shell的拼接核心思路一句话可以说清楚把 Miro 白板上那张带箭头的流程图直接解析成一张有向无环图再按拓扑序生成或调度可执行的命令序列。画完白板流程本身就成了可运行的东西不用再手工翻译一遍。它适合那些流程经常变、步骤又多、还散落在不同人手里的团队——运维发布、数据处理、内容生产、活动运营只要你的流程能用框箭头表达它就能派上用场。下面我把这个项目从动机到实现、从数据模型到踩过的坑完整拆一遍。会有不少 Miro API 的细节、图算法的取舍还有几个我调试了大半天才定位到的问题希望能帮你少走一点弯路。1. 我为什么要做 MiroFish白板与命令行之间的那道断口1.1 一次发布流程演练暴露的问题起因是一次季度发布演练。我们的发布流程有十七个步骤涉及预检、备份、灰度切流、校验、回滚判断等等全画在 Miro 上画得相当规范每个步骤一个圆角矩形判断节点用菱形串行关系用实线箭头并行分支用分组框圈起来。评审的时候大家都说好然后负责执行的同学把这张图截图存进文档打开终端开始照着图敲。结果第一次演练就出事了——两个本该并行的分支被串行执行了整体耗时翻倍更麻烦的是一个判断节点的两个出口执行的同学看错了分支方向走进了本该跳过的回滚路径。事后复盘问题不在人在于那张图只有人类可读这一种形态。箭头方向对人是语义对机器什么都不是分组框对人是视觉组织对机器也什么都不是。每次流程变更改动落在白板上命令行那边完全感知不到两套东西靠人的记忆去同步迟早会错位。我当时的第一反应是那就别用白板了直接写脚本。但试了一周就放弃了。流程的真正决策者是业务和运维的同学他们不写脚本让他们去改 YAML 或 shell沟通成本比画图高得多。白板之所以被选中恰恰是因为它是最低门槛的表达方式。所以我改变了方向不是把人从白板赶到命令行而是让白板本身成为唯一事实来源让工具去读懂它。1.2 现成方案为什么都不太合适在动手之前我把能想到的替代方案都过了一遍。自动化编排平台n8n、Make 这类功能很强但它们的定位是在平台上定义流程白板只是一个可选的辅助说明流程定义依然要落到平台自己的可视化编辑器里等于还是两套东西。如果是纯技术团队用代码定义 DAG比如 Airflow、Dagster当然更靠谱但那又回到了业务同学不写代码的老问题。至于从白板直接触发动作Miro 本身有自动化能力但它的能力边界主要是卡片移动触发某件事这种事件级的联动表达不了从 A 到 G 一共八步其中 C 和 D 并行E 是判断分支这种带拓扑结构的全流程语义。我需要的是把整张图当成一个程序来解读而不是把图上的单个事件当成触发器。所以 MiroFish 的定位就很清楚了它是一个图编译器。输入是白板上的图元集合输出是可执行的执行计划。它不接管流程的定义方式只负责把已有的定义翻译成机器能跑的顺序。这个边界划清楚之后后面所有的技术选型都变得简单了——我不用做华丽的 UI不用做权限体系只要把读图和排程这两件事做扎实就行。1.3 三个必须守住的设计底线动工之前我给自己定了三条底线后面证明这三条决定了整个项目的形态。第一白板必须保持人类可读。如果为了机器解析方便要求大家用特定的图元类型、特定的颜色、特定的标签格式那这个工具的迁移成本会劝退所有人。我的做法是只依赖两个最基础的结构图元之间的连接关系箭头和图元自身的文本内容。颜色、样式、字号一律不参与解析最多作为可选的辅助提示。这样即使有人画得花花绿绿工具照样能读懂。第二解析失败必须说人话。图这种东西人看着合理机器看着可能就是一堆歧义。这时候报错信息不能是解析异常了事而要精确到某某便签有两条出边但没有标记条件无法判断分支。用图的人不是工程师报错必须直接告诉他在白板上改哪里。第三执行必须可中断、可续跑。流程里总会有耗时很长的步骤如果跑到第七步断了重跑要回到第一步这个工具就没法用在真实环境。所以执行状态必须落盘每一步的完成情况都要能恢复。这条底线直接决定了后面幂等和状态存储的设计。2. 拆解 Miro 白板的数据模型把图看懂比画出来难2.1 一块白板上到底有哪些对象要让程序读图第一步得先知道图里有哪些东西。Miro 的 REST API 把白板上的元素统称为 items通过GET /v2/boards/{board_id}/items可以一次性拉取也可以用type参数过滤特定类型。常见类型包括卡片card、便签sticky_note、形状shape、文本text、连接线connector、框架/分组框frame、图片、嵌入内容等等。这里有个新手很容易忽略的点不是所有 items 都是流程节点。有人在白板上留了一句吐槽便签放了个标题文本贴了一张参考截图这些都会出现在接口返回里。如果不加过滤就当成节点处理执行计划里会冒出一堆莫名其妙的步骤。我的做法是分两层筛选先按图元类型过滤掉明显不属于流程的元素图片、嵌入内容再按是否参与连接做二次筛选——一个便签如果既没有入边也没有出边那它就是游离的注释不进图。还有一种中间情况笔记性质的孤立便签但作者希望它被执行。这种我会提供一个显式约定比如便签文本以某个特定前缀开头我用的是run:解析时把它提升为节点。约定要尽量少最好只有一个因为每多一个约定白板作者就多一份记忆负担。2.2 连接线的方向性箭头画反了会怎样连接线是整个项目里最关键、也最容易出问题的一类对象。Miro 的 connector 数据里带有起点和终点的引用信息大致是startItem和endItem两个对象各自包含id字段指向被连接的那个图元。理论上只要顺着这个方向读就能得到一张有向图。但现实中白板是多人协作的方向经常画错。我遇到过最典型的一种作者想表达 A 到 B但画的时候是从 B 拖到 A然后把箭头样式改成双向或者干脆没箭头视觉上看着像 A 到 B数据里却是 B 到 A。这种错误在视觉审查时几乎不可能发现因为看起来完全一样。我的处理方式是在工具里加了一个方向自检环节解析完边之后把每条边的方向对应的执行顺序打印出来给用户确认比如备份 - 校验和校验 - 备份这两条完全不同让人一眼看出有没有反。听上去有点笨但这是成本最低的纠错手段——与其在真实执行时才发现顺序错了不如在生成计划时就让人确认一遍。另外对于视觉上必须表达反向的场景我会建议在白板约定里统一用从上游拖到下游的绘制习惯把这条写进团队规范里比在工具里做花哨的自动纠错有效得多。2.3 从图元到 DAG顶点识别与边的归一化把 items 和 connectors 拉下来之后要做一次归一化才能得到干净的有向无环图。顶点侧的处理包括三件事。一是稳定 ID 映射Miro 的 item id 是稳定的字符串直接用作图节点的键不要用文本内容做 key——因为白板上出现两个叫校验的便签是很常见的。二是文本清洗便签内容可能带换行、Markdown 记号、行内引用需要统一成单行再解析动作语义。三是类型标注根据图元形状矩形还是菱形或者文本前缀标注这个节点是普通动作还是判断分支。边侧的处理核心是去重和自环消除。同一对节点之间画了两条连接线在视觉上可能只是作者手抖但在图里就是两条边如果不去重拓扑排序时这个节点的入度会算成 2永远排不出来。自环A 连到 A几乎一定是画错了我选择直接报错而不是静默忽略因为自环在流程语义里没有合理解释。处理完之后得到的就是一个标准的邻接表结构后面所有算法都基于它跑。3. 拓扑排序与环检测让流程真的能跑起来3.1 用 Kahn 算法排出执行顺序有了邻接表排执行顺序就是个经典问题了。我用的是 Kahn 算法思路很直白先算出每个节点的入度把所有入度为 0 的节点放进一个待执行队列每次从队列里取出一个节点执行同时把它所有后继节点的入度减 1减到 0 的就进队列。队列空了如果已执行的节点数等于总节点数说明排序成功如果少于总数说明剩下的节点里存在环。选 Kahn 而不是基于 DFS 的拓扑排序原因有两个。一是它天然给出了分层的能力——同一批入度为 0 的节点是可以并行执行的这一点对流程执行很有价值。二是不需要递归栈深度不会成为问题白板上画出一百多个节点的流程也照样跑。代码大致是这样from collections import deque def topo_layers(graph): indeg {n: 0 for n in graph} for u in graph: for v in graph[u]: indeg[v] 1 layers [] current deque([n for n in graph if indeg[n] 0]) while current: layer list(current) layers.append(layer) nxt deque() for u in layer: for v in graph[u]: indeg[v] - 1 if indeg[v] 0: nxt.append(v) current nxt if sum(len(l) for l in layers) ! len(graph): raise ValueError(图中存在环无法确定执行顺序) return layers返回的layers是一个二维列表外层是执行阶段内层是同一阶段内可并行的节点。这个结构对后面做并行调度非常关键。3.2 环、孤岛节点、重复边的处理策略环检测在理论上是可选的在实践中是必须的。流程图里出现环通常意味着两种情况一种是真的想表达循环比如校验失败则重试另一种是画错了。两者必须区分对待因为静默当成错误会让合理的重试需求无处表达静默当成循环又会让执行引擎陷入死循环。我的处理是把循环从图中拆出去变成一个节点的内部属性。具体做法是判断节点上允许标注重试语义比如文本里出现重试或失败回退解析时把这个判断节点和它的回退目标合并成一个带重试次数的复合节点图本身依然保持无环。这样拓扑排序不受影响重试语义由执行器在单个节点内部处理。孤岛节点的处理我在第 2 章提过一半这里补全真正游离的孤岛无入边无出边默认忽略但如果用户显式要求包含所有节点就把它当成起点的独立任务执行。这个开关很有用因为有些流程确实包含无论如何都要跑一次的步骤比如发通知。重复边则一律去重并且在日志里提示出来让作者知道他的白板上有多余的线。3.3 分组框当成并行块的可行性Miro 的分组框frame在视觉上是把几个图元圈在一起语义上通常表示这一组是一起的。我一开始想得很简单同一分组框里的节点全部并行执行。实际做下来发现这个假设太乐观了。问题在于分组框里的节点自己也可能有先后关系。比如一个框里放了备份和校验但框内又有一根箭头从备份指向校验。这时候如果无视框内箭头全并行就会把顺序搞乱。所以正确的做法是分组框只在没有内部边的节点之间生效。具体做法是先把所有边都建立好然后遍历分组框对于框内两两之间都不存在可达路径的节点才把它们标记为可并行。判断是否存在可达路径不能只看直接边要看传递闭包——A 和 C 中间隔着 BA 依然不能和 C 并行因为它必须等 B。我在实现时用了一次 Floyd-Warshall 式的传递闭包计算节点规模通常在一百以内完全够用把结果缓存下来后续并行判断直接查表。这个细节看着不起眼但它直接决定了并行是加速还是制造竞态。4. 从 DAG 到可执行动作脚本生成还是直接调度4.1 两条技术路线的取舍拿到分层执行计划之后面临一个选择是把它编译成一段可执行的脚本文件交给别人跑还是由工具自己当执行器按序调度命令这两条路我都实现过各有长短。生成脚本的优点是简单、透明、可审计。产物是一份纯文本人能读能放进代码仓库做版本对比能交给运维在发布窗口人工执行。缺点是失去了动态控制能力——并行、重试、条件分支都要在脚本层面用 shell 语法写出来而 shell 表达这些本来就别扭容易出现难以维护的嵌套结构。直接调度则相反控制逻辑全在 Python 里并行用线程池重试用装饰器条件分支用函数调用灵活很多。但代价是引入了执行器本身的复杂度状态怎么存、日志怎么归集、失败怎么中断全都要自己实现。而且用户看不见要跑什么只能看日志信任成本更高。我最后的方案是两个都要但生成脚本作为默认输出。理由很实际这个工具的多数使用场景是把白板上的流程变成一份可以被审查的执行清单而不是无人值守地自动跑完全程。尤其在发布、运维这类场景里人工确认这一环不能被绕过。所以 MiroFish 的主产物是一份带注释的脚本调度模式作为可选能力存在供那些已经跑顺了、想再省一步的场景使用。默认值和高级能力分开是我在多个项目里反复验证过的稳妥做法。4.2 动作节点的语义约定节点文本要变成可执行命令中间需要一层约定。我采用的规则是节点文本的第一行作为动作标识后续行作为参数。动作标识支持三种形式。第一种是直接的命令行比如pg_dump --hostdb-primary --dbnameapp工具原样输出到脚本。第二种是动作别名比如#backup工具去一张配置表里查出实际命令模板再把节点里写的参数填进去。别名的好处是命令集中管理改一次全局生效也不用把所有细节暴露在白板上有些命令带路径和凭据引用画在白板上不合适。第三种是注释节点以//开头只出现在生成的脚本注释里单纯用来给流程分段说明。参数解析上我吃过一次亏。最初我用空格切分参数结果有个节点的参数里包含带空格的路径直接被切成了两段命令跑起来报文件不存在。后来改成支持引号包裹的参数并且对带特殊字符的值强制要求加引号同时在解析时给出提示。这个改动很小但它把白板上随手写的参数和真实 shell 语义之间的鸿沟填上了一些。4.3 为什么执行层选了 Fish shell很多人问过为什么不生成 Bash。答案有点私人团队里负责执行的同学平时就用 Fish而且 Fish 有几个特性在生成脚本这个场景下确实省事。第一个是变量处理更安全。Fish 里变量默认不会因为未定义就静默为空test -z $VAR的写法在检查上更直接而 Bash 里忘了加引号导致的空变量展开是经典事故源。生成的脚本里如果有一个路径变量没设上Fish 更可能在检查处就停下来而不是带着空路径一路往下跑。第二个是命令替换和条件语法更接近自然语言。Fish 用(...)做命令替换用and/or做逻辑连接对于生成代码来说可读性更好人为审查时不容易看漏。语法大致长这样# 由 MiroFish 生成发布流程 v3 set -l release_tag 2024.11.03-1 if test -z $release_tag echo release_tag 未设置中止 2 exit 1 end for step in precheck backup cutover verify echo 执行 $step and myctl run $step --tag $release_tag or begin echo $step 失败流程中止 2 exit 1 end end需要说明的是这只是默认选择。如果团队确定用 Bash配置里改一个模板把语法换掉就行图解析那一层完全不受影响。把解析和输出格式解耦是让这个工具能适应不同团队的关键设计。5. 踩过的坑从鉴权到重复投递5.1 鉴权与令牌过期接入 Miro API 用的是 OAuth 流程拿到访问令牌之后调接口。我一开始把令牌存进本地配置文件想着一次配好长期使用结果第二天所有请求都开始返回 401。原因是访问令牌有有效期过期之后必须用刷新令牌换新的。这个坑本身不难解决真正麻烦的是错误处理的位置。最初我把令牌刷新逻辑写在启动时也就是程序一运行就检查并刷新。这在单次运行的场景下没问题但 MiroFish 有一部分模式是长驻监听白板变更后面会讲跑上几个小时后令牌过期后续请求全部失败日志里满屏 401看起来像是权限问题实际只是时间到了。后来改成两层保险调用接口时如果收到 401先自动尝试刷新令牌并重试一次重试再失败才向上抛出错误。同时在本地缓存里记录令牌的签发时间提前一段时间主动刷新避免正好卡在过期边界上。这个改动之后长驻模式下再也没有因为令牌问题中断过。5.2 白板变更通知的重复投递监听模式依赖 Miro 的事件推送机制白板内容发生变化时收到通知然后触发重新解析。这带来一个必须处理的问题事件通知是至少一次投递不是恰好一次。同一张白板的同一处改动你可能收到两次甚至更多次通知。如果不做幂等后果是执行计划被重复生成、重复触发下游动作。我在测试环境里就见过一次因为重复投递某个清理动作被执行了两遍第二次因为目标已经不存在而报错但报错本身又触发了告警最后演变成一场虚惊。解决办法是给每次解析结果算一个内容指纹——把参与流程的节点和边的规范化表示序列化成字符串取哈希值。收到通知后重新拉取白板内容计算指纹如果和上一次相同直接跳过。这样即使通知来十次实际处理只有一次。指纹的粒度也要注意我一开始对整个白板的所有元素取指纹结果有人在白板上挪了个不相干的便签指纹就变了触发了无意义的重新生成。后来改成只对参与流程的节点和边取指纹噪声立刻降下来了。5.3 坐标漂移与看起来连着其实没连这个坑最隐蔽。白板上两根线相交视觉上像是一个连接但数据里它们只是交叉的两条独立连接线之间没有任何引用关系。反过来有人把两个框挪得很近看起来像是有关系实际上没有连线。更麻烦的是移动图元导致连接关系变化。Miro 的 connector 是绑定到图元上的正常移动图元连接会跟着走。但如果有人用复制粘贴的方式把一组图元复制到白板另一处连接关系可能保留也可能丢失取决于操作方式。我曾经遇到过一个流程作者复制了一份旧流程做修改结果新副本里所有的连接线都没跟过来全部变成孤岛节点——解析出来一看十四个节点零条边工具直接报错说没有任何连接关系请检查白板反倒救了一命。我的应对策略是完全不依赖坐标。节点间的远近、对齐、间距在解析里一律不参与判断。唯一的依据是显式的连接线。同时加一个连通性检查如果一张有多于一个节点的白板解析出来的图里存在超过一个连通分量就给出警告提示作者可能有节点漏连了。这个警告救回来过好几次。6. 幂等、重试与失败续跑让流程敢在真实环境跑6.1 幂等键怎么设计续跑的前提是幂等——同一个步骤重跑一次不能产生副作用。但这件事工具本身做不到它取决于步骤实际执行的是什么。工具能做的是提供一个幂等键的传递机制让被调用的命令自己判断。我的设计是每个节点在生成执行指令时都会带上一组环境变量包括流程实例 ID、节点 ID、以及一个由流程 ID 节点 ID 输入指纹组成的幂等键。被执行的脚本可以拿这个键去查自己的记录表如果已经执行过就跳过。工具不强制要求但会把变量准备好用不用是各个步骤自己的事。这套机制看起来是把责任推给了使用方但我觉得这是对的分工。工具不可能知道部署这个动作是否幂等只有写这个动作的人知道。工具能做的是让幂等实现变得容易而不是假装自己能兜底。6.2 重试与熔断的边界重试策略上我用的是指数退避加最大次数。第一次失败等一小段时间第二次等更久最多重试固定次数超过就标记该节点失败。对于并行分支一个分支失败不会立刻终止其他分支——让正在跑的跑完再统一汇总这样反而能收集到更完整的失败信息。熔断是后面加的。有些失败是环境级的比如依赖的服务整体不可用这时候每个节点都重试三次只是把失败时间拉长。所以我加了一个判断如果同一个流程实例里连续多个节点以同类错误失败就提前终止整个流程不再做无谓的等待。这里的同类错误用的是退出码加错误输出的前缀匹配粗糙但够用。6.3 执行状态存在哪里状态存储我选了最简单的方案一个本地 JSON 文件按流程实例分行记录。每次节点开始和结束都追加一行写的时候用写临时文件再原子重命名的方式避免中途崩溃写出半个文件。选文件而不是数据库是因为这个工具的部署环境太杂——有的团队跑在开发机上有的跑在跳板机里有的在容器里临时起一个。引入数据库意味着额外的部署步骤很多人就卡在这一步不想用了。JSON 文件虽然并发能力弱但 MiroFish 的典型场景是单实例串行执行流程瓶颈根本不在存储。续跑的逻辑也很直白启动时读状态文件找出所有状态为成功的节点在图上把它们标记为已完成然后重新跑拓扑排序跳过已完成的从第一个未完成节点继续。配合前面说的幂等键即使某个节点的成功状态没来得及落盘重跑也不会造成重复副作用。7. 把它交到团队手里约定比代码更重要7.1 命名规范和模板板工具写得再聪明也救不了一白板的随意涂鸦。真正让 MiroFish 在团队里跑起来的是一份写在文档里的白板规范内容不多大概这几条流程方向统一从左到右或从上到下一个节点只做一件事动作文本第一行写命令或别名判断节点用菱形两个出口的线上必须写条件标签分组框只用来表达并行框内如果有顺序关系必须画线。比规范更有用的是模板板。我们做了一块空白白板里面预置了几种常见结构串行链路、并行分支、条件判断、带重试的校验环。大家新画流程的时候直接从模板复制结构和连线都是现成的不用从零开始。这个做法的效果远超我的预期——规范是要求别人做对的事模板是让别人自然而然就做对。7.2 改白板等于改生产最后是流程变更的治理。因为白板成了事实上的流程定义文件改白板就等于改生产流程这件事必须让所有人意识到。我们的做法是把白板的链接和生成的执行脚本一起纳入版本管理每次生成脚本把白板 ID、生成时间、内容指纹写进脚本头部注释脚本进代码仓库走正常的评审流程。这样从白板上的一次拖动到生产上的一次执行中间有完整的追溯链条。这个机制还带来一个额外好处当有人问上周那次发布到底跑的是哪版流程直接查脚本仓库就能回答。而在以前这个问题只能靠翻聊天记录和截图。我在实际推行这套东西的过程中最大的体会是工具解决的是技术可行性规范解决的是协作可靠性两者缺一不可。我见过太多类似的内部工具技术上做得挺漂亮最后死在没人愿意按你的方式画图上。所以如果你的团队也在做类似的事我的建议是先花时间把模板板和约定做扎实代码反而可以晚一点、糙一点。另外一个小技巧是把解析失败时的报错信息当成产品来打磨——它是用户唯一会认真读的文档写清楚去哪里、改成什么样比任何使用手册都有用。
返回列表