使用状态机模式,借助飞书机器人实现RPA流程的远程调度

288 阅读5分钟

使用状态机模式,借助飞书机器人实现RPA流程的远程调度

功能介绍

飞书机器人可以设置事件回调,并支持长链接模式,开发者通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道,当有事件回调发生时,开放平台会通过该通道向开发者发送消息。 与传统的 Webhook 模式相比,长连接模式大大降低了接入成本,将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下:

  1. 测试阶段无需使用内网穿透工具,通过长连接模式在本地开发环境中即可接收事件回调;
  2. 只在建连时进行鉴权,后续事件推送均为明文数据,无需开发者再处理解密和验签逻辑;
  3. 只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名;
  4. 无需部署防火墙和配置白名单。

接入限制:

  1. 目前仅支持企业自建应用
  2. 每个应用最多建立 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, "已重新初始化,请点击下方的操作按钮,并根据提示进行输入")  
    }  
}

效果如下

d9febd134ea318dcb3ebbd6d784d26c1.jpg

c7ca5e66fd8d4be465be169300b6944f.jpg

483889f55f2fccfae4f1f692c5b87b16.jpg

96af2ef6a84e73d4e233c0b55777893d.jpg