znlgis 博客

GIS开发与技术分享 — GDAL · GeoServer · PostGIS · QGIS · OpenLayers · Cesium · FreeCAD · NPOI

第16章 - 架构设计与源码剖析

前面的章节我们从「使用者」视角学习了 robotgo-flow。本章切换到「开发者」视角,系统剖析框架的整体架构、模块划分、核心接口与数据流。理解架构,既能帮你排查疑难问题,也是下一章二次开发的基础。

16.1 分层架构

robotgo-flow 的 Go 端采用清晰的分层设计,自底向上大致是:

              ┌─────────────────────────┐
   入口层     │  cmd/robotgo-flow/main.go │  CLI 分发
              └─────────────────────────┘
                         │
   ┌──────────┬──────────┼──────────┬──────────┐
   │          │          │          │          │
配置层      执行层      动作层      工具层     服务层
config     executor    action     capture     serve
                                   recorder
                         │
              ┌─────────────────────────┐
   引擎层     │        engine            │  封装 robotgo
              │  engine / human / matcher │
              └─────────────────────────┘
                         │
              ┌─────────────────────────┐
   底层       │        robotgo           │  鼠标/键盘/屏幕/位图
              └─────────────────────────┘

每一层职责单一、依赖向下,这让代码易读、易测、易扩展。

16.2 目录与模块职责

Go 源码位于 src/go/,各模块职责如下:

职责
cmd/robotgo-flow CLI 入口,解析子命令与参数
internal/config YAML 数据结构定义、加载、验证、默认值
internal/engine 核心引擎:封装 robotgo(鼠标、键盘、滚动、截图、浏览器、模板缓存)、人类行为、图像匹配
internal/action 动作定义:Runner/Engine 接口、工厂、19 种动作实现
internal/executor 工作流执行器:遍历步骤、错误处理、进度回调、变量解析、context 取消
internal/serve JSON-Line 协议服务,供 GUI 调用
internal/notify 输入框、确认框、通知、错误日志等标准输入输出交互
internal/recorder 交互式录制器
internal/capture 截图工具
internal/geom 屏幕几何类型(Point
internal/encoding GBK ↔ UTF-8 编码转换
internal/ffi 导出为 C 的函数(DLL 模式,供 WPF P/Invoke)

16.3 核心数据流

一次 run 的完整数据流:

main.go (cmdRun)
  │
  ├─ config.Load(path)           读 YAML → encoding.ToUTF8 → Unmarshal
  │                              → applyDefaults → Validate → *Workflow
  │
  ├─ engine.NewEngine(...)       创建引擎(模板缓存、截图目录)
  │     └─ EnableHuman(...)      若启用则装配人类行为
  │
  ├─ executor.New(eng, cfg, ...) 绑定引擎与工作流
  │     ├─ 收集 inputs 变量 → ResolveInputs 替换 $input.<name>
  │     ├─ WithContext(ctx)      注入取消支持
  │     └─ RunFromStep(from)
  │           └─ runSteps()
  │                 for step in steps:
  │                   for action in step.actions:
  │                     act := action.FromConfig(actionCfg, timeout)
  │                     act.Execute(eng)   ← 按错误策略 abort/skip/retry
  │                       └─ eng.Click / TypeText / FindElement ...
  │                             └─ robotgo.Click / Type / FindBitmap ...
  │
  └─ 完成 / 失败(退出码)

可以看到:配置被解析为内存对象 → 变量被替换 → 执行器逐步逐动作驱动 → 动作调用引擎 → 引擎调用 robotgo。这条链路上每一环都对应一个包。

16.4 关键接口:Runner 与 Engine

internal/action/action.go 定义了两个核心接口,它们是「执行器 ↔ 引擎」之间的契约,实现了关注点分离:

16.4.1 Runner 接口

type Runner interface {
    Execute(eng Engine) error
}

每一种动作都实现 Runner——只有一个 Execute 方法,接受一个 Engine 并返回错误。执行器不关心动作内部细节,只调用 Execute

16.4.2 Engine 接口

Engine 接口声明了动作可以使用的全部 RPA 原语(约 22 个方法),分组包括:

  • 鼠标ClickDoubleClickRightClickDrag
  • 键盘TypeTextPressKeyPressCombo
  • 元素FindElementWaitForElementWaitForElementGone
  • 滚动ScrollDownScrollUp
  • 浏览器RefreshPageBackForwardSwitchTabOpenURLFindBrowserWindow
  • 工具CaptureScreenWait

16.4.3 接口分离的价值

动作只依赖 Engine 接口,而非具体的 *engine.Engine 实现。这带来两大好处:

  1. 可测试:测试动作时可以用 engine_mock.go 提供的 Mock 引擎,不必真的操作鼠标键盘;
  2. 可替换:只要实现 Engine 接口,就能替换底层引擎(例如换成跨平台实现)。

这是「面向接口编程」的经典范例。

16.5 工厂模式:FromConfig

internal/action/factory.goFromConfig(cfg config.Action, defaultTimeout int) (Runner, error) 是连接「配置」与「动作」的桥梁——它把一个 config.Action(从 YAML 解析来的数据)转换成对应的 Runner 实现。

它内部根据 cfg 中哪个字段被设置,创建对应的动作对象,并处理灵活的参数解析:

  • parseTargetedAction[T](泛型):解析 click/double_click/right_click,兼容「字符串(模板)」或「{x, y} 映射」;
  • parseWait:解析 wait,兼容「字符串」或「{template, timeout} 映射」;
  • parseCoord / toInt:从映射提取坐标,toInt 兼容 int/int64/float64(因为 YAML 解析后数字类型可能不确定)。

16.6 引擎的封装智慧

engine.go 是全框架最核心的文件,它把 robotgo 的原子能力封装成更高层、更智能的方法。几个值得学习的设计:

  • 模板缓存:用带 sync.RWMutex 的 map 缓存位图,避免重复解码,关闭时统一释放;
  • 人类模式的透明切换:每个方法内部判断 human 是否为 nil,决定走「快速」还是「拟人」分支,对上层完全透明;
  • 浏览器状态管理:把浏览器 PID 与窗口坐标作为引擎状态维护,支撑「窗口内优先搜索」;
  • 编码桥接:所有传给 CGo 层的文件路径都经 encoding.ToGBK 转换,屏蔽中文路径问题。

16.7 执行器的健壮性设计

executor.go 体现了对「真实世界」的周全考虑:

  • 三种错误策略统一在执行循环中处理;
  • 每个动作边界检查 ctx.Done(),实现协作式安全取消;
  • 进度回调对称:即便跳过(skip)动作,也调用 OnActionDone,保证 GUI 状态一致;
  • ResolveInputs 作为独立导出函数,可脱离执行器单独使用(serve 模式就直接调用它);
  • 变量替换按名称长度排序,避免前缀冲突。

16.8 编码转换:encoding 包

encoding.go 虽小,却解决了 Windows 中文环境的一大痛点。它提供:

  • ToGBK(s):UTF-8 → GBK,用于给 CGo 层传中文文件路径;
  • ToUTF8(data):GBK → UTF-8。先判断是否已是合法 UTF-8,若是则原样返回;转换失败或结果含替换字符(U+FFFD)也返回原值——这种「保守转换」避免了误伤;
  • ToUTF8String(s):字符串版本。

底层用 golang.org/x/text 的简体中文编码与 transform 实现。正是这个包让用户「无需手动处理编码」。

16.9 如何阅读源码

如果你想深入源码,推荐的阅读顺序:

  1. config/workflow.go:先搞清数据结构(Workflow/Step/Action……);
  2. cmd/robotgo-flow/main.go:看命令如何分发;
  3. executor/executor.go:看执行主循环;
  4. action/factory.go + 各 action/*.go:看动作如何创建与执行;
  5. engine/engine.go:看引擎如何封装 robotgo;
  6. engine/matcher.go + engine/human.go:看两大核心机制;
  7. serve/serve.go:看服务化协议。

按「数据结构 → 入口 → 执行 → 动作 → 引擎 → 机制 → 服务」的顺序,就能顺藤摸瓜理清全貌。

16.10 小结

本章我们从开发者视角剖析了 robotgo-flow 的架构:清晰的分层(入口/配置/执行/动作/工具/服务/引擎/底层)、各包的职责、一次 run 的完整数据流;重点解读了 Runner/Engine 两大接口如何实现关注点分离与可测试性、FromConfig 工厂如何灵活解析配置、引擎的封装智慧(缓存/人类模式透明切换/浏览器状态/编码桥接)、执行器的健壮性设计与 encoding 包的保守转换策略,并给出了源码阅读顺序。下一章,我们动手做二次开发——扩展一个新的动作类型。