- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费 - 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库 - 新增异步任务等待/通知机制(Wait/Notify) - 重构执行记录落库与进度上报,统一失败分类与重试语义 - 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go - 更新 .gitignore 与数据库密码配置
264 lines
11 KiB
Go
264 lines
11 KiB
Go
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
|
||
}
|