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 }