使用状态机模式,借助飞书机器人实现RPA流程的远程调度
功能介绍
飞书机器人可以设置事件回调,并支持长链接模式,开发者通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道,当有事件回调发生时,开放平台会通过该通道向开发者发送消息。 与传统的 Webhook 模式相比,长连接模式大大降低了接入成本,将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下:
- 测试阶段无需使用内网穿透工具,通过长连接模式在本地开发环境中即可接收事件回调;
- 只在建连时进行鉴权,后续事件推送均为明文数据,无需开发者再处理解密和验签逻辑;
- 只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名;
- 无需部署防火墙和配置白名单。
接入限制:
- 目前仅支持企业自建应用
- 每个应用最多建立 50 个连接(每初始化一个 client 就是一个连接)
借助飞书的这个能力,我们可以开发一个飞书机器人,实现跟我们的RPA客户端进行交互,实现远程控制的功能。这里我们借助状态机模式,实现远程控制任务的启停,代码如下:
package services
import (
"context"
"encoding/json"
"errors"
"fmt"
lark "github.com/larksuite/oapi-sdk-go/v3"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
"github.com/spf13/viper"
"gobot/backend/constants"
"gobot/backend/log"
"gobot/backend/models"
"gobot/backend/utils"
"os"
"path/filepath"
"strconv"
"strings"
)
func InitLarkListener(ctx context.Context) {
larkAppId := viper.GetString("lark.app_id")
larkAppSecret := viper.GetString("lark.app_secret")
if larkAppId != "" && larkAppSecret != "" {
client := lark.NewClient(larkAppId, larkAppSecret)
stateMachine := StateMachine{state: InitialState, ctx: ctx}
// 注册事件回调,OnP2MessageReceiveV1 为接收消息 v2.0;OnCustomizedEvent 内的 message 为接收消息 v1.0。
eventHandler := dispatcher.NewEventDispatcher("", "").
OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
fmt.Printf("[ OnP2MessageReceiveV1 access ], data: %s\n", larkcore.Prettify(event))
content := event.Event.Message.Content
contentResult := map[string]string{}
err := json.Unmarshal([]byte(*content), &contentResult)
if err != nil {
return err
}
stateMachine.handleInput(contentResult["text"], *event.Event.Message.MessageId, client)
return nil
})
// 创建Client
cli := larkws.NewClient(larkAppId, larkAppSecret,
larkws.WithEventHandler(eventHandler),
larkws.WithLogLevel(larkcore.LogLevelDebug),
)
// 启动客户端
err := cli.Start(context.Background())
if err != nil {
log.Logger.Logger.Err(err).Msg("飞书事件回调注册失败")
} else {
log.Logger.Logger.Info().Msg("飞书事件回调停止")
}
}
}
func replayTextMsg(client *lark.Client, msgId string, msg string) error {
// 构建文本消息
content := larkim.NewTextMsgBuilder().
Text(msg).
Build()
// 创建请求对象
req := larkim.NewReplyMessageReqBuilder().
MessageId(msgId).
Body(larkim.NewReplyMessageReqBodyBuilder().
Content(content).
MsgType(larkim.MsgTypeText).
Build()).
Build()
// 发起请求
resp, err := client.Im.Message.Reply(context.Background(), req)
// 处理错误
if err != nil {
return err
}
// 服务端错误处理
if !resp.Success() {
return errors.New(fmt.Sprintf("%d,%s,%s", resp.Code, resp.Msg, resp.RequestId()))
}
// 业务处理
return nil
}
// 定义状态机的状态
const (
InitialState = iota
QueryProcessListState
SetProcessParamsState StartProcessConfirmState QueryRunningTasksState StopProcessConfirmState)
type StateMachine struct {
ctx context.Context
state int
projectId string
taskId string
paramArray []string
paramIndex int
paramData map[string]interface{}
}
// 处理用户输入
func (sm *StateMachine) handleInput(input string, messageId string, client *lark.Client) {
if strings.ToLower(input) == "初始化" {
sm.handleRestoreInitialState(true, messageId, client)
return
}
switch sm.state {
case InitialState:
sm.handleInitialState(input, messageId, client)
case QueryProcessListState:
sm.handleQueryProcessListState(input, messageId, client)
case SetProcessParamsState:
sm.handleSetProcessParamsState(input, messageId, client)
case StartProcessConfirmState:
sm.handleStartProcessConfirmState(input, messageId, client)
case QueryRunningTasksState:
sm.handleQueryRunningTasksState(input, messageId, client)
case StopProcessConfirmState:
sm.handleStopProcessConfirmState(input, messageId, client)
}
}
// 初始状态处理
func (sm *StateMachine) handleInitialState(input string, messageId string, client *lark.Client) {
if strings.ToLower(input) == "运行流程" {
projects, err := QueryProjectPage()
if err != nil {
_ = replayTextMsg(client, messageId, "查询流程列表失败,请稍后重试")
return
}
message := "请选择要运行的流程序号:"
for i, project := range projects {
message += fmt.Sprintf("%d. %s ", i+1, project.Name)
}
_ = replayTextMsg(client, messageId, message)
sm.state = QueryProcessListState
} else if strings.ToLower(input) == "停止流程" {
runningFlows, err := GetRunningFlows()
if err != nil {
_ = replayTextMsg(client, messageId, "查询运行中的任务列表失败,请稍后重试")
return
}
message := "请选择要停止的任务序号:"
for i, runningFlow := range runningFlows {
message += fmt.Sprintf("%d. %s ", i+1, runningFlow.ProjectName)
}
_ = replayTextMsg(client, messageId, message)
sm.state = QueryRunningTasksState
} else {
_ = replayTextMsg(client, messageId, "无效的输入,请点击下方的操作按钮,并根据提示进行输入")
}
}
// 查询流程列表状态处理
func (sm *StateMachine) handleQueryProcessListState(input string, messageId string, client *lark.Client) {
processID, _ := strconv.Atoi(input)
projects, err := QueryProjectPage()
if err != nil {
_ = replayTextMsg(client, messageId, "查询流程列表失败,请稍后重试")
return
}
if processID >= 1 && processID <= len(projects) {
project := projects[processID-1]
sm.projectId = project.Id
subFlowPath := filepath.Join(project.Path, constants.BaseDir, constants.DevDir, "main.flow")
if !utils.PathExist(subFlowPath) {
return
}
data, err := os.ReadFile(subFlowPath)
if err != nil {
return
}
flowData := models.FlowData{}
err = json.Unmarshal(data, &flowData)
if err != nil {
return
}
sm.paramArray = make([]string, 0)
for _, param := range flowData.Params {
if param.Direction == "In" {
sm.paramArray = append(sm.paramArray, param.Name)
}
}
sm.paramIndex = 0
sm.paramData = make(map[string]interface{})
if len(sm.paramArray) == 0 {
_ = replayTextMsg(client, messageId, "该流程无需设置启动参数,是否启动流程?(是/否)")
sm.state = StartProcessConfirmState
} else {
_ = replayTextMsg(client, messageId, "请设置流程参数:【"+sm.paramArray[sm.paramIndex]+"】的值")
sm.state = SetProcessParamsState
}
} else {
_ = replayTextMsg(client, messageId, "无效的流程编号,请重新输入。")
}
}
// 设置流程参数状态处理
func (sm *StateMachine) handleSetProcessParamsState(input string, messageId string, client *lark.Client) {
sm.paramData[sm.paramArray[sm.paramIndex]] = input
sm.paramIndex++
if sm.paramIndex < len(sm.paramArray) {
_ = replayTextMsg(client, messageId, "请设置流程参数:【"+sm.paramArray[sm.paramIndex]+"】的值")
sm.state = SetProcessParamsState
return
} else {
_ = replayTextMsg(client, messageId, "参数设置完成,是否启动流程?(是/否)")
sm.state = StartProcessConfirmState
}
}
// 启动流程确认状态处理
func (sm *StateMachine) handleStartProcessConfirmState(input string, messageId string, client *lark.Client) {
if input == "是" {
project, err := QueryProjectById(sm.projectId)
if err != nil {
return
}
project.InputParam = sm.paramData
err = ModifyProject(*project)
if err != nil {
return
}
err = RunProject(sm.ctx, sm.projectId, "飞书机器人")
if err != nil {
_ = replayTextMsg(client, messageId, "流程启动失败,失败原因:"+err.Error())
return
} else {
_ = replayTextMsg(client, messageId, "流程启动成功")
}
// 这里可以添加启动流程的代码
sm.handleRestoreInitialState(false, messageId, client)
} else if input == "否" {
_ = replayTextMsg(client, messageId, "取消启动流程,如有其他需求,请点击下方的按钮发起。")
sm.handleRestoreInitialState(false, messageId, client)
} else {
_ = replayTextMsg(client, messageId, "无效的命令,请重新输入是或否。")
}
}
// 查询运行中任务状态处理
func (sm *StateMachine) handleQueryRunningTasksState(input string, messageId string, client *lark.Client) {
runningFlows, err := GetRunningFlows()
if err != nil {
_ = replayTextMsg(client, messageId, "查询运行中的任务列表失败,请稍后重试")
return
}
taskID, _ := strconv.Atoi(input)
if taskID >= 1 && taskID <= len(runningFlows) {
sm.taskId = runningFlows[taskID-1].Id
_ = replayTextMsg(client, messageId, "是否要停止对应的任务?(是/否)")
sm.state = StopProcessConfirmState
} else {
_ = replayTextMsg(client, messageId, "无效的任务编号,请重新输入。")
}
}
// 停止流程确认状态处理
func (sm *StateMachine) handleStopProcessConfirmState(input string, messageId string, client *lark.Client) {
if input == "是" {
err := TerminateFlow(sm.ctx, sm.taskId)
if err != nil {
_ = replayTextMsg(client, messageId, "任务停止失败,失败原因:"+err.Error())
return
} else {
_ = replayTextMsg(client, messageId, "任务停止成功")
}
// 这里可以添加停止任务的代码
sm.handleRestoreInitialState(false, messageId, client)
} else if input == "否" {
_ = replayTextMsg(client, messageId, "取消停止任务,如有其他需求,请点击下方的按钮发起。")
sm.handleRestoreInitialState(false, messageId, client)
} else {
_ = replayTextMsg(client, messageId, "无效的命令,请重新输入是或否。")
}
}
// 停止流程确认状态处理
func (sm *StateMachine) handleRestoreInitialState(replay bool, messageId string, client *lark.Client) {
sm.paramData = make(map[string]interface{})
sm.paramArray = make([]string, 0)
sm.paramIndex = 0
sm.projectId = ""
sm.taskId = ""
sm.state = InitialState
if replay {
_ = replayTextMsg(client, messageId, "已重新初始化,请点击下方的操作按钮,并根据提示进行输入")
}
}