package flow import ( "context" "errors" "fmt" "time" "gitea.redpowerfuture.com/red-future/common/oss" "github.com/cloudwego/eino/compose" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "ai-agent/gateway" "ai-agent/workflow/consts/flow" "ai-agent/workflow/consts/node" nodeDao "ai-agent/workflow/dao/node" sessionDao "ai-agent/workflow/dao/session" flowDto "ai-agent/workflow/model/dto/flow" nodeDto "ai-agent/workflow/model/dto/node" "ai-agent/workflow/model/entity" ) // ====================== 执行记录落库 ====================== // recordExecutionFailure 记录一次失败状态。有 execId 直接更新该记录;拿不到 execId // (executeOrResume 在创建记录后、返回前 panic,或查询/创建执行记录失败)时, // 兜底按会话+工作流查最近一条仍处于"运行中"的记录标记为失败,避免前端已报错但记录卡在 Running。 // 最近记录已是成功/失败状态则不处理(可能是上一次执行的结果,不应误改)。 func recordExecutionFailure(ctx context.Context, sessionId string, flowId int64, execId int64, runErr error) { // 兜底失败路径同样携带 retryable 分类(与 handleExecute 一致): // 用户取消=0;其余(程序关停中断/程序报错)可重试=1,保证任意 status=3 写库都带分类 var retryable, retryCnt *int if runErr != nil { if errors.Is(runErr, context.Canceled) && !IsShuttingDown() { retryable, retryCnt = intptr(0), intptr(0) } else { retryable, retryCnt = intptr(1), intptr(0) } } if !g.IsEmpty(execId) { recordWorkflow(ctx, execId, 0, runErr, retryable, retryCnt) return } lastExec, err := sessionDao.ExecWorkflowDao.GetLatestBySessionAndFlow(ctx, sessionId, flowId) if err != nil || lastExec == nil { glog.Errorf(ctx, "兜底标记失败状态失败: sessionId=%s flowId=%d err=%v", sessionId, flowId, err) return } if lastExec.Status == nil || *lastExec.Status != *flow.FlowExecutionStatusRunning.Code() { return } recordWorkflow(ctx, lastExec.Id, 0, runErr, retryable, retryCnt) } // intptr 取 int 值指针(recordWorkflow 的 retryable/retryCount 参数:nil=不改动) func intptr(v int) *int { return &v } // recordWorkflow 把一次工作流执行写入 exec_workflow/exec_workflow_result:运行记录 + 输出文件结果。 // retryable/retryCount 非 nil 时随终态同语句原子落库,避免"先 UpdateRetry 再 Update"两步写部分生效 // 导致 retryable 与 status/error_message 不一致(关停中断场景曾出现 retryable=0 但 message=程序关停中断) func recordWorkflow(ctx context.Context, id int64, duration time.Duration, runErr error, retryable, retryCount *int) { // exec_workflow 状态沿用 1-运行中,2-成功,3-失败;前端结果卡片也只识别 1/2/3 // (4 会误显示为"运行中"),故取消同样记为失败,错误信息写"用户已终止执行" // error_message 存友好提示,error 存原始错误明细 status := flow.FlowExecutionStatusSuccess var errorMessage, errorDetail string if runErr != nil { status = flow.FlowExecutionStatusFailed switch { case errors.Is(runErr, context.Canceled): errorMessage = errWorkflowTerminated case errors.Is(runErr, errInterruptedByShutdown): errorMessage = "程序关停中断" default: errorMessage = "工作流执行失败" errorDetail = runErr.Error() } } data := map[string]any{ entity.ExecWorkflowCol.Status: *status.Code(), } if d := int64(duration.Seconds()); d != 0 { data[entity.ExecWorkflowCol.Duration] = d } if errorMessage != "" { data[entity.ExecWorkflowCol.ErrorMessage] = errorMessage } if errorDetail != "" { data[entity.ExecWorkflowCol.Error] = errorDetail } if retryable != nil { data[entity.ExecWorkflowCol.Retryable] = *retryable data[entity.ExecWorkflowCol.RetryCount] = *retryCount } if err := sessionDao.ExecWorkflowDao.UpdateMap(ctx, id, data); err != nil { glog.Errorf(ctx, "exec_workflow 终态落库失败 execId=%d: %v", id, err) return } // 执行成功:重新执行复用了同一条记录,需显式清空,避免上一次失败的报错残留 if runErr == nil { if _, err := sessionDao.ExecWorkflowDao.ClearError(ctx, id); err != nil { glog.Errorf(ctx, "exec_workflow 报错信息清空失败: %v", err) } } // 工作流计费:终态结算(成功→Settle/用户取消→Cancel/永久失败→Fail/可恢复→跳过)。 // recordWorkflow 汇聚全部路径(WS/恢复/panic),此处一处接线全覆盖;计费错误仅记日志不拖垮落库 settleBilling(ctx, id, runErr, retryable, retryCount) } // workflowResultFileUrls 查询指定工作流执行保存的结果文件路径(带文件前缀,与 session/get 返回一致) func workflowResultFileUrls(ctx context.Context, execId int64) []string { results, err := sessionDao.ExecWorkflowResultDao.ListByExecId(ctx, execId) if err != nil { glog.Errorf(ctx, "查询工作流结果路径失败: %v", err) return nil } prefix, _ := oss.GetFileAddressPrefix(ctx) urls := make([]string, 0, len(results)) for _, r := range results { if r.ResultFileUrl != "" { urls = append(urls, prefix+r.ResultFileUrl) } } return urls } // ====================== 节点执行记录 ====================== // BuildNodeExecutionInput 构建节点执行入参,包含中断恢复逻辑 func BuildNodeExecutionInput(ctx context.Context, input any, flowNode entity.FlowNode) (*flowDto.FlowExecutionInput, *flowDto.NodeExecutionInput, error) { execInput := new(flowDto.FlowExecutionInput) wasInterrupted, _, _ := compose.GetInterruptState[any](ctx) if wasInterrupted { if err := compose.ProcessState(ctx, func(_ context.Context, s *flowDto.NodeExecutionState) error { execInput = s.SavedFlowInput return nil }); err != nil { return nil, nil, fmt.Errorf("节点:%v 进程状态读取失败: %v", flowNode.Name, err) } // 兼容旧 checkpoint(无 SavedFlowInput 时,降级使用 input 参数) if execInput == nil { var ok bool execInput, ok = input.(*flowDto.FlowExecutionInput) if !ok { return nil, nil, fmt.Errorf("节点:%v 进程状态为空,节点入参类型不匹配", flowNode.Name) } if g.IsEmpty(execInput) { return nil, nil, fmt.Errorf("节点:%v 进程状态为空,节点入参参数为空", flowNode.Name) } } // 续跑必定非全新执行:checkpoint 恢复的 SavedFlowInput 里 ForceNewRun 是上次(fresh)执行留下的 true, // 不清则 ModelLambda 误走"清段重生成"而非复用已成功段 execInput.ForceNewRun = false } else { var ok bool execInput, ok = input.(*flowDto.FlowExecutionInput) if !ok { return nil, nil, fmt.Errorf("节点:%v 入参类型不匹配", flowNode.Name) } if g.IsEmpty(execInput) { return nil, nil, fmt.Errorf("节点:%v 入参参数为空", flowNode.Name) } } configMap := execInput.ConfigMap currentConfig := configMap[flowNode.Id] if currentConfig == nil { return nil, nil, fmt.Errorf("节点:%v 节点信息为空", flowNode.Name) } // 构建节点执行入参 realInput := &flowDto.NodeExecutionInput{ Config: currentConfig, Global: execInput, } return execInput, realInput, nil } // HandleSuccessfulNodeExecution 处理节点执行成功的后续操作 func HandleSuccessfulNodeExecution(ctx context.Context, execInput *flowDto.FlowExecutionInput, realInput *flowDto.NodeExecutionInput, nodeExecutionId int64, flowNode entity.FlowNode, durationMs int64) error { // 上传输出到OSS ossResult, err := gateway.Upload(ctx, fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()), gconv.Bytes(gconv.String(realInput))) if err != nil { return fmt.Errorf("节点:%v 上传OSS失败: %v", realInput.Config.Name, err) } // 更新执行记录为成功 if err := UpdateNodeExecutionRecord(ctx, nodeExecutionId, durationMs, node.NodeExecutionStatusSuccess.Code(), ossResult, ""); err != nil { return fmt.Errorf("节点:%v 更新成功状态错误: %v", flowNode.Name, err) } // 记录成功到已执行列表 RecordExecutionResult(execInput, flowNode.Id, node.NodeExecutionStatusSuccess.Code()) return nil } // HandleFailedNodeExecution 处理节点执行失败的后续操作 func HandleFailedNodeExecution(ctx context.Context, execInput *flowDto.FlowExecutionInput, nodeExecutionId int64, flowNode entity.FlowNode, err error, durationMs int64) error { // 保存状态用于续跑 if stateErr := compose.ProcessState(ctx, func(_ context.Context, s *flowDto.NodeExecutionState) error { s.CompletedNodes = append(s.CompletedNodes, flowNode.Name) s.SavedFlowInput = execInput s.ExecutionCount++ return nil }); stateErr != nil { fmt.Printf("节点:%v 进程状态保存失败: %v", flowNode.Name, stateErr) } if !g.IsEmpty(nodeExecutionId) { // 更新执行记录为失败 if updateErr := UpdateNodeExecutionRecord(ctx, nodeExecutionId, durationMs, node.NodeExecutionStatusFailed.Code(), "", err.Error()); updateErr != nil { fmt.Printf("节点:%v 更新失败状态错误: %v", flowNode.Name, updateErr) } } // 触发中断 return compose.Interrupt(ctx, map[string]string{ "node": flowNode.Name, "error": err.Error(), }) } // RecordExecutionResult 将节点执行结果写入 Global.ExecutedNodes func RecordExecutionResult(execInput *flowDto.FlowExecutionInput, nodeId string, status node.NodeExecutionStatus) { execInput.ExecutedNodes = append(execInput.ExecutedNodes, flowDto.ExecutedNode{ NodeId: nodeId, Status: status, }) } // UpdateNodeExecutionRecord 更新节点执行记录 func UpdateNodeExecutionRecord(ctx context.Context, nodeExecutionId int64, durationMs int64, status node.NodeExecutionStatus, outputParamsPath string, errMsg string) error { if _, err := nodeDao.NodeExecutionDao.Update(ctx, &nodeDto.UpdateNodeExecutionReq{ Id: nodeExecutionId, DurationMs: durationMs, Status: status, OutputParamsPath: outputParamsPath, ErrorMessage: errMsg, }); err != nil { return err } return nil } // CreateNodeExecutionRecord 创建节点执行记录,返回记录ID func CreateNodeExecutionRecord(ctx context.Context, execInput *flowDto.FlowExecutionInput, flowNode entity.FlowNode, inputOssUrl string) (int64, error) { id, err := nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ FlowExecutionId: execInput.ExecutionId, NodeId: flowNode.Id, NodeName: flowNode.Name, NodeGroupId: execInput.NodeGroupId, InputParamsPath: inputOssUrl, Status: node.NodeExecutionStatusRunning.Code(), }) if err != nil { return 0, fmt.Errorf("节点:%v 创建节点执行记录失败: %v", flowNode.Name, err) } return id, nil }