feat(workflow): 增加工作流计费与执行生命周期管理

- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费
- 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库
- 新增异步任务等待/通知机制(Wait/Notify)
- 重构执行记录落库与进度上报,统一失败分类与重试语义
- 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go
- 更新 .gitignore 与数据库密码配置
This commit is contained in:
2026-09-03 13:22:22 +08:00
parent 67d049e586
commit d699f7ce14
46 changed files with 2690 additions and 2450 deletions
+122
View File
@@ -0,0 +1,122 @@
package flow
import (
"ai-agent/workflow/consts/flow"
"ai-agent/workflow/consts/public"
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"
sessionDto "ai-agent/workflow/model/dto/session"
"ai-agent/workflow/model/entity"
"context"
"fmt"
"strings"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
func DataMergeLambda(ctx context.Context, input any) (res any, err error) {
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
if !ok {
return nil, fmt.Errorf("参数合并入参类型错误")
}
return nodeInput, nil
}
func SummaryLambda(ctx context.Context, input any) (any, error) {
execInput, ok := input.(*flowDto.NodeExecutionInput)
if !ok {
return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input)
}
// 聚合所有已执行节点中需入库的文件结果(两层规则)
summaryResult := collectSaveFileResults(ctx, execInput.Global)
// 把汇总结果存入当前节点的输出
g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult))
err := gfdb.DB(ctx, public.DbNameBlackDeacon).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
res, _, err := nodeDao.NodeExecutionDao.ListByFlowExecutionId(ctx, &nodeDto.ListNodeExecutionByFlowReq{
NodeGroupId: execInput.Global.NodeGroupId,
}, entity.NodeExecutionCol.TokenInfo)
if err != nil {
return err
}
var totalTokens int
var totalFee float64
for _, item := range res {
for _, itemToken := range item.TokenInfo {
m := gconv.Map(itemToken)
totalTokens += gconv.Int(m["total_tokens"])
totalFee += gconv.Float64(m["total_fee"])
}
}
_, err = sessionDao.ExecWorkflowDao.Update(ctx, &sessionDto.UpdateWorkflowReq{
Id: execInput.Global.ExecutionId,
Status: flow.FlowExecutionStatusSuccess.Code(),
TotalTokens: totalTokens,
TotalFee: totalFee,
})
if err != nil {
return err
}
if len(summaryResult) > 0 {
_, err = sessionDao.ExecWorkflowResultDao.BatchInsert(ctx, summaryResult)
if err != nil {
return err
}
}
return nil
})
return execInput, err
}
// collectSaveFileResults 按两层规则收集需入库的文件结果:
// 第一层:节点须开启"保存文件"IsSaveFile);
// 第二层:key 取自节点 OutputResult 的各字段,命中 ModelResponseBodyMapping 才入库;
// 原始响应体 key(respBody)恒入库(不要求映射声明);HTTP 节点产出以 http_file_url:{key}
// 标记的字段(IsSaveFile 时由 HttpCallResultLambda 生成)恒入库(无模型响应映射可查)。
// 结果值为 http(s) URL 或 MinIO 对象裸路径直接使用;非路径值(base64 图片/文本)先上传 OSS 换取 URL,
// 文本内容以 .inc 扩展名存储。
func collectSaveFileResults(ctx context.Context, execInput *flowDto.FlowExecutionInput) []*sessionDto.CreateWorkflowResultReq {
if execInput == nil {
return nil
}
var summaryResult []*sessionDto.CreateWorkflowResultReq
for _, executedNode := range execInput.ExecutedNodes {
nodeConfig := execInput.ConfigMap[executedNode.NodeId]
if nodeConfig == nil || len(nodeConfig.OutputResult) == 0 || !nodeConfig.IsSaveFile {
continue
}
// 第二层:key 取自节点 OutputResult 的各字段,
// 命中 ModelResponseBodyMapping 才入库;respBody 与 HTTP 节点 http_file_url:{key} 标记恒入库
saveKeys := nodeConfig.ModelConfig.ModelResponseBodyMapping
for _, respBody := range nodeConfig.OutputResult {
for key, val := range gconv.Map(respBody) {
isHTTPFile := strings.HasPrefix(key, "http_file_url:")
if !isHTTPFile {
if _, ok := saveKeys[key]; !ok && key != "respBody" {
continue
}
}
fileUrl, err := resolveSaveFileResult(ctx, val)
if err != nil {
g.Log().Warningf(ctx, "collectSaveFileResults 上传结果文件失败 key=%s err=%v", key, err)
continue
}
summaryResult = append(summaryResult, &sessionDto.CreateWorkflowResultReq{
SessionId: execInput.SessionId,
FlowId: execInput.FlowId,
ExecId: execInput.ExecutionId,
ResultFileUrl: fileUrl,
})
}
}
}
return summaryResult
}