rpa_core 后端架构 · ELI5(程序员版)

给懂代码的人讲的架构:只讲职责边界、数据流和为什么这样切。

① 一句话总览

一份 JSON 菜谱(workflow)经过静态检查变成不可变的执行单(plan),厨师长(orchestrator)按单依次派活给各工种执行器,每一步的产出写进备菜台(scopes)、拍一张进度快照(checkpoint)、记一行日志(events.jsonl),最后交一份质检单(result.json)。

② 分层依赖(单向,禁止反向)

model (纯协议:pydantic 类型,谁都不 import) ▲ ├─ catalog (命令目录加载 + 不可变快照 + sha256 digest) ├─ compiler (AST 静态校验 → 冻结的 ExecutionPlan) ├─ executors (浏览器 / 桌面 / 子进程干活,只回 CommandResult) ├─ runtime (编排器:状态机、取消、超时、重试、checkpoint、证据) ├─ devserver (设计期工具:只读复用 catalog/compiler,不承载 run) └─ cli (入口)

为什么单向:依赖箭头永远指向更底层——model 是纯类型,谁都能依赖它;runtime 不知道有 devserver 存在。这条规则由 check_architecture.py 机械检查(import 方向分析),改不动。

③ 各层职责(配厨房类比)

catalog catalog/loader.py

启动时读 commands/*.json 成 manifest,算 sha256 digest 封存。运行期间内容不可变。

类比:装订成册的工艺规范 + 封条编号。恢复时拿 digest 对账,目录变一个字节就拒绝续跑。

compiler compiler/compiler.py

开跑前静态检查:节点 id 查重、命令存在性、${...} 引用是否可解析、能力授权、unsafe 命令禁重试。通过才产出冻结的 ExecutionPlan。

类比:开灶前核对食材和授权,通过盖章,单子封存不许改。

orchestrator runtime/orchestrator.py

递归走 AST、派单给执行器、收 CommandResult 回执。管取消(Event 传播)、attempt 级超时竞争、退避重试、路径键完成集合、checkpoint 原子写入、终态收口。

类比:厨师长——只派活、看表、记账,不掌勺。

executors executors/

四类:Playwright 浏览器、UIA 桌面、Win32 桌面、Python 子进程。输入 CommandInvocation,返回 CommandResult(value/outputs/effects/diagnostics/error),绝不触碰编排器状态。

类比:各工种厨师——接单干活交回执,不改菜单。

④ 关键设计决策(工程权衡)

决策原因代价
无 FastAPI / 无框架(stdlib http.server) 设计期工具无需 Web 框架;ADR 0006 明排,依赖面为零 没有现成的路由/校验/并发基础设施,全部手写(已覆盖 405/413/403 等边界)
catalog 不可变 + digest 一次运行一份契约快照;digest 是 resume 的合法性门槛 运行中改 manifest 必须重跑(恢复时直接拒绝)
handler 只回 CommandResult 执行器不改编排器状态 → 状态单向流动、可测试、可换实现 多一次 JSON 往返的开销
用户 Python 走子进程 崩溃/恶意代码不伤编排器进程 进程启动开销 + stdin/stdout 序列化
checkpoint 写在 stepCompleted 事件之前 崩溃窗口语义:进度权威优先于日志——宁可丢一行日志,不可重放副作用 日志可能缺行,审计时需注意
unknown effect → indeterminate 终态 失败和"外部结果未知"是两种风险等级,绝不自动重试 调用方必须处理这个状态,不能折叠进 failed
devserver 不承载 run 设计期工具与运行时隔离(ADR 0007),UI 崩了不影响已提交的 run 运行态和控制态分离,跨进程看结果要读 artifacts 文件
桌面捕获走独立 agent 子进程 UIA/COM 状态不污染 devserver;崩溃隔离 每次 pick 一个进程生命周期

⑤ 一次 run 的数据流

workflow.json → Workflow → compile → ExecutionPlan(冻结) ↓ Orchestrator.run() → _execute_node → _execute_action ↓ 每次 attempt Executor.execute(CommandInvocation) → CommandResult ↓ 成功且校验通过 scopes[steps][id] 记账 → checkpoint.json 原子写 → stepCompleted 事件 ↓ 所有节点走完或异常 终态收口 → runFinished 事件 + result.json → 最终 checkpoint

⑥ So what(对你意味着什么)

改代码时记三条线:状态只经 scopes 单向流动(任何 executor 直接改 orchestrator 状态 = 违规);崩溃恢复只在节点边界(路径键完成集合,forEach 按迭代跳过);证据比执行优先(写日志失败立刻中止 run,绝不假成功)。违反这三条的 PR 会被合同测试拦住。