执行模型
流程在运行时做的一切都来自一个小循环:取一个节点,运行它,看它返回什么,沿一条连线去下一个节点。一旦你能预测这个循环,你就能预测你的流程。
本页按运行时的真实实现来讲解这个循环。若流程的行为让你意外,答案几乎总在本页。
目的
理解执行模型让你能够:
- 不运行流程就预测下一个执行的节点。
- 区分"我的流程失败了"与"我的流程走了另一条分支"——日志里长得像,含义却相反。
- 知道一次运行为何停止,以及是三道护栏中的哪一道停了它。
- 有意识地连接错误路径,而不是靠试错。
何时需要本页
在构建任何带分支、循环或错误处理的东西之前读它。以下情形你需要它:
- 某节点"成功"了,流程却去了意外的地方。
- 运行立刻结束,没有明显错误。
- 你在犹豫用重试、用失败连线,还是用 If 来处理一个问题。
搭一条纯 Click 加 Wait 的直线不需要本页。
一次一个节点
一个运行中的流程是一个会话(session)。每个会话同一时刻只执行一个节点,持有一个"当前节点"指针。
这比听上去重要:
- 没有后台节点。某节点运行时,流程里没有任何别的东西在跑。
- 慢节点阻塞整个会话。一个 30 秒超时的
Find Image会把该会话卡住最多 30 秒。 - 并行来自更多会话(更多实例),不是把一个流程劈开。
想同时做两件事,就在两个实例上跑流程,或用 Flow Queue 在实例池上运行多个流程。单个会话内,执行严格串行。
循环
这是运行时的主循环,按真实执行顺序:
两个值得背下来的推论:
fatal与cancelled永不沿连线走。 接上Fatal端口不会在正常运行中创造救援路径——运行时一见该状态即终止。想恢复的一切都用failure。- 没有连线的 route 会结束运行。 节点返回
failure而 Failure 端口空着,运行就以failure、TRANSITION_NOT_FOUND终止。它不会悄悄继续。
Status 与 route——最重要的区别
每个节 点返回两个独立的东西:
| 它描述什么 | 谁读它 | |
|---|---|---|
status | 执行健康度——节点本身跑成了吗? | 运行时,用于护栏与重试 |
route | 流程从哪个端口离开 | 连线解析,用于选下一个节点 |
它们不是同一个值,且检测节点上二者刻意不同。
五种 status
| Status | 含义 | 可重试 | 沿连线走 |
|---|---|---|---|
success | 节点正常完成 | — | 是 |
failure | 可恢复的失败 | 是 | 是 |
timeout | 期望条件未按时达成 | 是 | 是 |
cancelled | 运行在节点中途被停止 | 否 | 否——终止 |
fatal | 不可恢复的系统或配置错误 | 否 | 否——终止 |
为什么"未找到"不是失败
这是最常见的困惑来源,值得直说。
Find Image 搜模板、超时未命中时:
- status 是
success——检测正常运行了,只是没找到图; - route 是
failure——于是流程从 Failure 端口离开。
实际效果:一次"未找到"永不计入连续失败护栏,也永不触发节点重试。 轮询稀有事件一千次的流程不会因此被杀。这是刻意的——"按钮还没出来"是正常结果,不是错误。
同样的模式适用于 Check Region(passed / 未通过)和 Read Text。
对检测节点,把端口读成一个问题的答案——"它在吗?"——而不是"这节点跑成了吗?"。两种情况下节点都跑成了。
连线解析
节点返回 route R 时,运行时查 (该节点, R) 出发的连线:
- 候选在编译期按
(fromNodeId, fromPort)建好索引。 - 按 priority 升序、再按连线 id 升序排序。
- 第一个无条 件的候选者胜出。
- 带条件的候选者求值;
true选中,false跳过。 - 条件报错或返回非布尔值,该候选被跳过、继续下一个。
- 无人选中 → "没有连线"——运行以
failure终止。
连线解析是纯函数:它从不抓帧、发输入、改变量或写文件。它只做选择。
节点执行管线
"运行节点"内部,阶段永远按此顺序:
容易踩坑的点:
- 参数里的
{{变量}}解析不了时,节点在任何重试之前就返回fatal、PARAM_EXPRESSION_INVALID。坏表达式是配置缺陷,重试只会一模一样地失败。 - 输出映射恰好运行一次,作用于最终尝试。重试三次的节点不会写三次变量。
护栏
三个上限保护你免于永不结束的流程。三者都是流程设置,由运行时亲自检查。
| 护栏 | 检查时机 | 超限效果 |
|---|---|---|
maxNodeExecutions | 每个节点前 | 终止 fatal,MAX_NODE_EXECUTIONS_EXCEEDED |
maxRuntimeDurationMs | 每个节点前 | 终止 fatal,MAX_RUNTIME_DURATION_EXCEEDED |
maxConsecutiveFailures | 每个节点后 | 终止 fatal,MAX_CONSECUTIVE_FAILURES_EXCEEDED |
连续失败计数的行为:
failure与timeout递增它。success清零。fatal与cancelled在计数被使用前就终止运行。
由于检测的"未找到"带 success 状态,它清零计数而不是递增。
maxNodeExecutions 数每个节点,包括 Start 和 Stop。一个 200 轮、12 节点的循环要执行约 2400 个节点。本文档的农场式流程把 maxNodeExecutions 设为 5000 正是此因。长流程死于 MAX_NODE_EXECUTIONS_EXCEEDED 时,去提高这个值,而不是砍短循环。
重试的 backoff 时间不计入单次尝试的超时,但计入 maxRuntimeDurationMs。
一次运行如何结束
运行恰以下列方式之一终止:
| 原因 | 最终状态 |
|---|---|
到达 Stop 节点 | 按 Stop 的声明(success / failure / cancelled) |
到达 Return 节点 | 按 Return 的声明 |
某节点返回 fatal | fatal |
某节点返回 cancelled,或用户停止运行 | cancelled |
| route 无匹配连线 | failure(TRANSITION_NOT_FOUND) |
| 越过护栏 | fatal |
"跑着跑着就没了"的流程不存在——每条路径要么抵达终止节点,要么以 TRANSITION_NOT_FOUND 终止。
逐步示例
看一个小的登录步骤:
按钮晚出现时的轨迹:
| 步 | 节点 | status | route | 下一个 |
|---|---|---|---|---|
| 1 | Start | success | success | Find Image |
| 2 | Find Image | success | failure | Wait 2s |
| 3 | Wait 2s | success | success | Find Image(第 2 个) |
| 4 | Find Image(第 2 个) | success | success | Click |
| 5 | Click | success | success | Stop |
| 6 | Stop | — | — | 终止 success |
注意第 2 步虽然路由到 Failure,status 仍是 success。连续失败计数纹丝未动,没有任何重试参与。流程只是走了它的另一条分支。
最佳实践
- 连上每个检测节点的 Failure 端口。 空着的 Failure 端口把"未找到"变成被终止的运行。
- 用
failure做恢复,别用fatal。fatal立刻终止且接不住。 - 对"它出来了吗?"优先分支而非重试。 重试属于真正失败的操作;分支属于处在不同状态的屏幕。
- 给长流程慷慨的
maxNodeExecutions。 它是安全网,不是预算。 - 让
success清零你的失败计数。 在危险步骤之间穿插成功步骤,自然使maxConsecutiveFailures不被引爆。
性能笔记
- 会话一次跑一个节点,所以流程总时长是节点时长之和。给流程提速最快的办法,是缩短那些通常瞬间成功的节点的超时,而不是重排图。
- 轮询节点的
timeoutMs覆盖整个轮询循环——抓帧、裁剪、匹配、发事件。2 秒超时里放 1 秒轮询间隔,得到的约是两次尝试,不是两秒匹配。 - 连线解析是纯函数且已建索引,分支本身近乎零成本。别为速度压平一张可读的图。
内存笔记
- 每个会话持有自己的执行上下文:变量、
nodeResults、lastResult。同一流程跑在 10 个实例上就是 10 份独立上下文。 nodeResults随每个执行过的节点增长——每个节点的结果都被记录以供后续引用(If能查上游节点结果靠的正是它)。maxNodeExecutions极高、迭代数千次的流程会在运行全程积累结果数据。- 超长时间的自动化,优先拆成被反复调用的 Sub Flow,或拆成多次计划运行,而不是一个执行数小时的会话。
常见错误
| 错误 | 后果 | 修复 |
|---|---|---|
| Failure 端口空着 | 运行以 TRANSITION_NOT_FOUND 结束 | 连上,哪怕只连到一个 Stop |
指望 Fatal 当兜底 | 永不被走;运行终止 | 可恢复的问题放在 failure 上处理 |
给 Find Image 加重试想"找得更狠" | 重试不触发——未找到带 success 状态 | 提高 timeoutMs,或在 Failure 上分支 |
| 读日志时把"未找到"当错误 | 把健康流程误读成坏的 | 看 route,别只看 status |
超长流程用默认 maxNodeExecutions | 中途死于 MAX_NODE_EXECUTIONS_EXCEEDED | 在流程设置里调高 |
故障排查
运行立刻结束,日志写着 TRANSITION_NOT_FOUND。
某节点返回了没有连线的 route。找日志里最后的节点,看它走了哪个端口。
运行以 MAX_CONSECUTIVE_FAILURES_EXCEEDED 结束,节点却个个看着正常。
有东西在反复返回 failure 或 timeout,中间没有任何成功。找持续失败的操作节点(不是检测节点)。
带重试策略的节点从不重试。
要么它返回了 fatal(永不重试),要么返回了 success 状态加 failure 路由(检测未找到)——那不是可重试的结果。
流程走了预期之外的分支。 查节点的 route,不是 status。检测节点上两者设计上就不同。
FAQ
fatal 会回滚什么吗?
不会。fatal 终止运行。已发往设备的输入已经发生。
一个流程里两个节点能同时跑吗? 不能。一个会话一次执行一个节点。要并发就用多个实例。
节点重试 3 次,输出映射跑 3 次吗? 不——恰好一次,应用于最终尝试的结果。
cancelled 连线有被走过的时候吗?
没有。画布上可以存在 cancelled 端口以备未来兼容,但运行时在 cancelled 上直接终止,不走连线。