rpa_core 运行时链路图

全文用一个厨房类比贯穿:workflow 是菜谱,orchestrator 是厨师长,executor 是各工种厨师。 每个环节列出「做了什么」的具体动作,末尾一行保留设计原因。

⓪ 名词速查(先看这页再往下读)

workflow(工作流)model/workflow.py

一份 JSON 写的自动化剧本:先做什么、遇到条件怎么办、循环几次。≈ 菜谱。包含唯一 id、默认输入 inputs 和一棵节点树 root。

AST(workflow AST)model/workflow.py

Abstract Syntax Tree,抽象语法树:workflow JSON 加载进内存后的树状结构——「sequence 依次做」是父节点,if / forEach 是分支 / 循环节点,action(调一条命令)是叶子。运行时不逐字读 JSON,而是递归遍历这棵树来执行。≈ 把菜谱拆成有嵌套关系的步骤树。

manifest(命令清单)commands/*.json

每条命令的「操作规范卡」:叫什么(id 如 browser.click)、哪个执行器接活、输入输出长什么样(JSON Schema)、对外部世界有什么影响(effect)、能不能重试。≈ 每道工序的工艺卡。

catalog(命令目录)catalog/loader.py

启动时把 commands/ 下全部 manifest 加载成的只读总表:命令 id → 规范。附带 sha256 digest(指纹)——内容变一个字节指纹就变,用于检测「恢复时契约是否还是当初那份」。≈ 整本装订好的规范册 + 封条编号。

compiler(编译器)compiler/compiler.py

开跑前的静态检查员:遍历 workflow 树,查引用是否存在、命令是否在 catalog 里、能力是否被授权、危险命令有没有配重试。检查通过才发「执行单」。≈ 开灶前对照规范册逐项核对食材和工具。

ExecutionPlan(执行计划)compiler/plan.py

检查通过后冻结的执行依据:workflow 树 + catalog 指纹 + 所需能力。之后 orchestrator 只认这份单据,不再回头翻 JSON。≈ 盖章封存的单子,中途不许改。

orchestrator(编排器)runtime/orchestrator.py

整个运行的总指挥:拿着执行单从树根开始走,把每个 action 派给对应执行器;管超时、重试、取消、进度检查点和终态判定。它自己不直接操作浏览器/窗口/文件。≈ 厨师长:只派活、看表、记账,不掌勺。

executor(执行器)executors/

真正动手干活的组件:browser.playwright 开浏览器点页面、desktop.uia / desktop.win32 操作 Windows 窗口控件、python.worker 在子进程里跑用户 Python。输入一条 CommandInvocation(这次调用谁、参数是什么),回一张 CommandResult(结果回执),仅此而已。≈ 各工种厨师。

ExecutorRegistry(执行器注册表)executors/registry.py

一张排班表:执行器名字 → 执行器实例(如 "browser.playwright" → PlaywrightExecutor)。orchestrator 从 manifest 里读到「这活归 browser.playwright」,就按名字来这里要人。CLI 启动时组装一次。

CommandResult / CommandInvocationmodel/command.py

厨师长派活的工单(Invocation:命令、参数、第几次尝试)和厨师交回的回执(Result:成功与否、产出值、副作用证据 effects、诊断信息)。规则:厨师只填回执,绝不直接改厨师长的账本。

scopes(变量作用域)/ resolver(解析器)runtime/resolver.py

运行中的备菜台:inputs 放用户原料、steps 放每步产出、loop 放当前循环轮次。workflow 里写 ${steps.step1.outputs.title},resolver 负责整牌替换——按这个取料牌去备菜台取值,找不到立刻报错。

effect / replay / idempotency(副作用契约)model/command.py

每条命令声明三件事:effect.kind 对外界的影响类型(只读 / 幂等写 / 危险写 / 会话);replay 重做一次是否安全;idempotency 幂等键从哪来。失败回执里若带 unknown 副作用证据 = 「做没做成不知道」,运行时绝不自动重试。≈ 工艺卡上的风险等级。

events.jsonl / result.json(运行证据)runtime/events.py

流水线日志(每动一下追加一行)和最终质检报告(run 的终态、产出、错误)。两个都落盘 run 才算成功。≈ 操作日志 + 质检单。

checkpoint(检查点)runtime/checkpoint.py

每个 action 干完后拍的进度快照:哪些步骤确认完成、备菜台上是什么值。进程崩了或想接着跑,就从最近一份快照恢复,已完成的活不再重做。≈ 阶段完工拍照存档。

cancellation(取消)/ deadline(截止时间)

叫停铃和总时限。取消用 asyncio Event 传播:先通知执行器自行收尾清理,等它退出再终判。deadline 到点整棵执行树判超时。每次重试前都会查「还来不来得及」。

RunHandle / RunResultmodel/runtime.py

RunHandle 是启动后拿到的遥控器(cancel / wait);RunResult 是终态成绩单(状态、起止时间、outputs、返回值、错误、digest)。

① 名词关系图(谁持有谁、谁调用谁)

编译期(run 之前) 运行期(run 之中) workflow.json 菜谱(落盘文件) 加载 Workflow · AST 树 内存中的步骤树 sequence / if / forEach / action commands/*.json 23 张工艺卡文件 CommandManifest 规范卡(校验后驻内存) CommandCatalog 只读总表 + sha256 digest compiler 静态检查(编译器) ExecutionPlan 执行单 冻结:AST 树 + digest + 能力 digest 写入 orchestrator 编排器(厨师长) 走 AST · 派单 · 收回执 超时 / 重试 / 取消 / 检查点 终态判定 + 落证据 start / resume ExecutorRegistry 排班表 执行器名 → 执行器实例 按名字要人 executor × 4(真正掌勺) browser.playwright desktop.uia / desktop.win32 python.worker 入参工单 → 只回 CommandResult 派单 → ← 回执 scopes 备菜台 inputs 原料 / steps 各步产出 loop 当前轮次 resolver 按 ${...} 取值 读写 RunHandle 遥控器 cancel/wait RunResult 成绩单 终态 + outputs + error events.jsonl(流水线日志) result.json(终态质检单) checkpoint.json(进度快照) 追加 / 落盘 resume:崩溃 / 失败后过 5 道门,从快照继续 图例:蓝箭头 = 调用 / 数据流 · 绿框 = 落盘文件 · 黄虚线 = 恢复路径 · 橙框 = 编译期检查员

② 一次 run 从启动到结束,每个环节做了什么

1CLI 入口validate / run / resumesrc/rpa_core/cli.py
输入:workflow 路径、--artifacts 工件根目录 输出:控制台 JSON + 退出码
为什么:先做垂直切片,不引入服务化复杂度。规则 10
2加载命令目录load_catalogsrc/rpa_core/catalog/loader.py
输入:commands 目录 输出:CommandCatalog(含 digest)
为什么:一次运行一份契约快照;digest 之后用于恢复门槛。规则 7 / 不变量 10
3编译工作流(≈ 开灶前核对)WorkflowCompiler.compilesrc/rpa_core/compiler/compiler.py
输入:Workflow + catalog + 授权能力 输出:ExecutionPlan(workflow + digest + capabilities,不可变)
为什么:所有能提前发现的错误都在 run 之前拒绝;plan 冻结后恢复时才能与 checkpoint 对账。不变量 10
4启动运行(厨师长接班)Orchestrator.start / resumesrc/rpa_core/runtime/orchestrator.py
输入:ExecutionPlan(+ 恢复时 checkpoint) 输出:RunHandle,最终 await 到 RunResult
为什么:取消用 Event 传播而不是杀进程——执行器要先自行清理资源。规则 11
5遍历执行 AST(按步骤树走单)_execute_nodeorchestrator.py
输入:当前节点 + 路径 + scopes 输出:无(状态都写进 scopes)
为什么:路径键带 #i 迭代段,forEach 才能按轮恢复,不会误跳过未跑的轮次。ADR 0004
6执行单条命令(派单 → 收回执 → 记账)_execute_actionorchestrator.py
输入:ActionNode + scopes + 执行器 输出:scopes 更新 + checkpoint + 事件
为什么:执行器只回 CommandResult、状态单向流经 scopes(可测试、可审计);checkpoint 先于事件落盘,崩溃窗口最多只丢一条事件、绝不重放副作用。规则 3 / ADR 0004
7四个执行器(真正掌勺的人)executors/src/rpa_core/executors/
browser.playwright
  • launch 起浏览器并生成 UUID sessionId
  • 其余操作必须带 sessionId,在对应页面上导航/点击/输入/等待/读文本/查询元素
desktop.uia
  • attachWindow 按标题/类名/进程绑窗口 → sessionId
  • UIA 语义定位控件,执行点击/输入/读文本
desktop.win32
  • 同上,但走 Win32 消息/句柄路线
  • 额外支持 hotkey、menuSelect(菜单/对话框)
python.worker
  • 起子进程跑 workers/python_worker.py,stdin/stdout 传 JSON
  • 限定 workspace 目录内读写
共同契约:收到取消尽快退出并清理资源;返回 CommandResult(回执);绝不触碰编排器内部状态
为什么:用户 Python 只进子进程(崩溃不伤主进程);浏览器/桌面操作必须显式 session 防串扰。规则 4/5/6
8落证据(日志 + 质检单)EventWriter + result.jsonsrc/rpa_core/runtime/events.py
输出:run_artifacts/<run_id>/ 下的 events.jsonl + result.json + checkpoint.json
为什么:runFinished + result.json 都落盘 run 才算成功(规则 12);事件流让崩溃后能回答"崩在哪一步"。
9终态收口(交班结算)_run_to_terminalorchestrator.py
输出:RunResult(正常 run 和恢复 run 走同一条收口路径,证据格式完全一致)
为什么:每个 run 必达终态(规则 8);收口统一防止两条路径证据不一致。

③ checkpoint 里有什么,恢复时每道门检查什么

checkpoint.json(version=1)src/rpa_core/runtime/checkpoint.py
写入时机:每个 action 成功后、stepCompleted 事件之前,原子写(临时文件 + replace)
resume 的 5 道门(每道做什么)
1. 读文件 + version/结构校验文件缺失、JSON 损坏、版本不是 1、字段缺失/类型错 → 抛 CheckpointError
2. workflowId 与 plan 一致防止把 A 工作流的进度套到 B 工作流上
3. catalogDigest 与重新编译的 plan 一致命令契约改过 → 旧 checkpoint 作废,必须重跑
4. result.json 状态门上次终态是 indeterminate 且未传 allow_indeterminate=True → 拒绝,要求人工确认
5. 恢复执行scopes/completedSteps 从 checkpoint 取;deadline 重新按 timeout_seconds 计算(monotonic 时间跨进程无意义);重走 AST,已完成路径键直接跳过,其余照常执行
为什么逐门拒绝:checkpoint 损坏时"猜状态续跑"等于在未知进度上执行副作用,明确失败让人介入更安全。ADR 0004

④ 各状态什么时候出现,出现后发生什么

succeeded何时:所有节点执行完或 return 触发。之后:runFinished + result.json 落盘,退出码 0
failed何时:命令失败/超时/意外异常且结果确定。之后:可 resume,未完成的步骤会重新执行
cancelled何时:调用了 cancel()。之后:执行器清理完资源再收口,已完成步骤保留可续跑
indeterminate何时:命令失败且带 unknown effect(外部可能已生效)。之后:绝不自动重试,resume 必须人工 ack
recovery_required何时:action 边界的 checkpoint 写入失败。之后:run 证据完整,但快照可能落后,恢复需人工确认副作用安全
paused何时:RunHandle.pause() 在 action 边界生效。之后:证据落盘、进程结束;非终态驻留,必须被 resume 或显式放弃(ADR 0005)
abandoned何时:显式放弃(预留终态)
为什么 failed 不够用:"失败"和"外部结果未知"风险不同——后者自动重跑可能重复提交表单/扣款;折叠进 failed 调度层就会当普通失败重跑。ADR 0002 / 0004
暂停优先级:取消 > 暂停 > 超时——暂停只在 action 入口与每次新 attempt 前生效,不打断进行中的命令;workflow deadline 在 attempt 进行中触发仍判 failed(TIMEOUT)。ADR 0005