第17章 - 二次开发:扩展新动作类型

robotgo-flow 内置的 19 种动作已能覆盖大多数场景,但总有特殊需求需要自定义。得益于清晰的分层与接口设计,为框架添加一种新动作是一件有章可循的事。本章以「添加一个新动作类型」为主线,完整走一遍二次开发流程。

本章面向有 Go 基础、希望修改/扩展框架源码的读者。若你只是使用者,可略读了解扩展点即可。

17.1 扩展新动作的五个步骤

官方文档明确给出了添加新动作类型的标准流程,共五步,分布在不同的包中:

  1. 在 internal/config/workflow.go 的 Action 结构体中添加新字段,并更新 ActionType() 与 Label() 方法;
  2. 在 internal/action/ 下新建文件,实现 Runner 接口(Execute 方法);
  3. 在 internal/action/factory.go 的 FromConfig() 中添加对应分支;
  4. 在 internal/config/loader.go 的 validateSingleAction()(计数字段)与 templatePaths()(模板存在性校验)中处理新类型(isEmpty() 已自动派生自 ActionType(),无需改动);
  5. 在 internal/recorder/recorder.go 中添加录制支持。

下面我们以一个假想的新动作 hover(鼠标悬停) 为例,逐步实现。

17.2 第一步:定义配置字段

打开 internal/config/workflow.go,在 Action 结构体中新增字段:

type Action struct {
    // ... 已有字段 ...
    Hover any `yaml:"hover,omitempty"`   // string(模板) 或 {x, y}
}

hover 与 click 类似,支持模板或坐标,故用 any 类型。

接着更新 ActionType() 方法,让它能识别新动作(注意方法定义在指针接收者上,未匹配时返回空字符串 ""):

func (a *Action) ActionType() string {
    switch {
    // ... 已有分支 ...
    case a.Hover != nil:
        return "hover"
    }
    return ""
}

再更新 Label(),为进度/日志显示提供可读标签:

func (a *Action) Label() string {
    switch {
    // ... 已有分支 ...
    case a.Hover != nil:
        return "悬停"
    }
    return "未知动作"
}

17.3 第二步:实现 Runner

在 internal/action/ 下新建 hover.go,实现动作逻辑。参考 click.go 的 targetedAction 复用「模板或坐标」的定位逻辑:

package action

type HoverAction struct {
    targetedAction   // 复用模板/坐标解析
}

func (a *HoverAction) Execute(eng Engine) error {
    x, y, err := a.resolveTarget(eng)
    if err != nil {
        return err
    }
    return eng.Hover(x, y)   // 调用引擎的悬停能力
}

这里假设我们在 Engine 接口与引擎实现中新增了 Hover(x, y int) error 方法(内部可用 robotgo.Move 或 robotgo.MoveSmooth 把鼠标移到目标而不点击)。因此扩展新动作有时也要扩展 Engine 接口:

  1. 在 action/action.go 的 Engine 接口中加 Hover(x, y int) error;
  2. 在 engine/engine.go 中实现 Hover;
  3. 在 action/engine_mock.go 的 Mock 中也补上 Hover,以便测试。

17.4 第三步:注册到工厂

打开 internal/action/factory.go,在 FromConfig 中添加对新字段的分支。parseTargetedAction 是泛型解析器,需要传入「模板构造闭包」「坐标构造闭包」与动作名(这也是 click 家族支持 {template: 路径} 与 {x, y} 两种映射写法的由来):

func FromConfig(cfg config.Action, defaultTimeout int) (Runner, error) {
    switch {
    // ... 已有分支 ...
    case cfg.Hover != nil:
        return parseTargetedAction(cfg.Hover,
            func(tpl string) *HoverAction { return &HoverAction{targetedAction: targetedAction{template: tpl}} },
            func(pt geom.Point) *HoverAction { return &HoverAction{targetedAction: targetedAction{coord: &pt}} },
            "hover",
        )
    }
    return nil, fmt.Errorf("未知的动作类型")
}

这样,当 YAML 中出现 hover 字段时,工厂就能创建出 HoverAction。

17.5 第四步:加入验证

打开 internal/config/loader.go,让验证逻辑认识新动作:

  • isEmpty() 现在直接由 ActionType() 派生(return a.ActionType() == ""),只要第一步给 ActionType() 加了 Hover 分支,无需再改 isEmpty();
  • 在 validateSingleAction() 中,把 Hover 计入「已设置字段计数」,否则 click 与 hover 同时出现时只计到 1 个,「一个动作项只能有一个动作字段」的约束对新动作失效;
  • 若 hover 用模板,还应确保 templatePaths() 能收集到它的模板路径(字符串或 {template} 写法经 appendTargetPath 提取),从而参与「模板文件存在性」校验。
// 现状:isEmpty 无需改动
func (a *Action) isEmpty() bool { return a.ActionType() == "" }

17.6 第五步:支持录制

最后,在 internal/recorder/recorder.go 的动作菜单中加入新动作,让用户能在 record 交互录制时选择它:

  • 在 askAction() 的菜单里增加一个选项(如「16. Hover」);
  • 对该选项调用 askTemplateOrCoord(复用模板/坐标询问逻辑),把结果写入 Action.Hover。

至此,hover 动作就从「配置定义 → 执行实现 → 工厂注册 → 验证 → 录制」全链路打通,可以在 YAML 中这样使用:

- hover: "templates/menu_trigger.png"
- hover: {x: 400, y: 200}

17.7 测试你的新动作

框架自带完善的测试。二次开发后,应补充并运行测试:

cd src\go

# 运行所有测试
go test ./...

# 运行相关包测试
go test ./internal/config/ -v
go test ./internal/action/ -v
go test ./internal/executor/ -v

# 带覆盖率
go test ./... -cover

参考已有的测试文件:

  • config/loader_test.go、config/loader_ext_test.go:配置加载与验证测试;
  • config/validation_test.go:加载期校验规则测试(on_error 枚举、max_retries、inputs 唯一性等);
  • cmd/robotgo-flow/main_test.go:CLI 参数重排(reorderFlags)等入口测试;
  • action/factory_test.go:工厂创建测试;
  • action/execute_test.go:动作执行集成测试(用 engine_mock.go 的 Mock 引擎);
  • action/interact_test.go:交互动作测试。

用 Mock 引擎测试动作是关键技巧:由于动作只依赖 Engine 接口,你可以在测试中传入 engine_mock.go 的 Mock,断言动作调用了哪些引擎方法、传了什么参数,而无需真的操作鼠标。为 HoverAction 写一个测试,验证它正确调用了 eng.Hover(x, y) 即可。

17.8 扩展其它维度

除了新增动作,你还可以在其它维度扩展框架:

  • 新增引擎能力:在 Engine 接口与实现中增加方法(如剪贴板高级操作、特定应用适配),供动作调用;
  • 新增交互方式:扩展 notify 包,接入更丰富的对话框或通知渠道;
  • 增强图像匹配:在 matcher.go 中引入容差匹配、多尺度匹配(应对 DPI 差异)等;
  • 新增前端:只要遵循 JSON-Line 协议,就能开发 Web、Electron 等其它前端来驱动 serve。

17.9 二次开发注意事项

  • 保持接口契约:改动 Engine 接口时,务必同步更新真实实现与 Mock,否则编译不过;
  • 五步要齐全:漏了第一步(ActionType() 没有新分支)会导致新动作被当成空动作;漏掉验证(第四步)会破坏「单动作字段」约束;漏掉工厂(第三步)会导致 YAML 里写了却不执行;
  • 复用已有解析器:parseTargetedAction、parseWait 等能省很多事,优先复用;
  • 补测试:新动作至少补一个基于 Mock 的执行测试;
  • 注意编码:涉及文件路径时记得走 encoding 转换。

17.10 小结

本章我们完整走了一遍二次开发流程——以新增 hover 动作为例,演示了官方规定的五个步骤:在 config 定义字段并更新 ActionType/Label、在 action 实现 Runner(必要时扩展 Engine 接口与 Mock)、在 factory 注册分支、在 loader 加入验证、在 recorder 支持录制。我们还讲了如何用 Mock 引擎测试动作、其它扩展维度以及注意事项。清晰的分层与接口设计,让扩展 robotgo-flow 变得有章可循。下一章,我们通过一个综合实战项目把所学融会贯通。