- launchExecution 失败时返回真实 execId,避免 wrapper 兜底失效 - summary 节点移除执行终态与计费写库,统一由 recordWorkflow 结算 - 更新数据库连接凭据
101 lines
4.1 KiB
Go
101 lines
4.1 KiB
Go
package flow
|
||
|
||
import (
|
||
"ai-agent/workflow/consts/public"
|
||
sessionDao "ai-agent/workflow/dao/session"
|
||
flowDto "ai-agent/workflow/model/dto/flow"
|
||
sessionDto "ai-agent/workflow/model/dto/session"
|
||
"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)
|
||
}
|
||
|
||
// 汇总节点只做本职:聚合本次执行已产出、需入库的文件结果(两层规则)并落 exec_workflow_result。
|
||
// 终态(exec_workflow.status / total_tokens / total_fee)与计费**禁止在图内写**——
|
||
// summary 会随子流程/多末端/断点续跑在整体还没跑完时提前执行,此处若把 exec 置成 status=2
|
||
// "成功",会骗过 wrapper 的失败兜底(recordExecutionFailure 只标记 Running 记录)→ 取消/中断后
|
||
// recordWorkflow/settleBilling 永不触发,计费单遗留 CREATED、已消耗 token 漏扣
|
||
//(线上 per_token/per_second 取消不扣费根因,2026-09-03)。终态只允许 BuildExecution 返回后的
|
||
// recordWorkflow 单点落库并结算(含 total_tokens/total_fee/actual_amount 回填),图内不再越权写 exec 行。
|
||
summaryResult := collectSaveFileResults(ctx, execInput.Global)
|
||
|
||
// 把汇总结果存入当前节点的输出
|
||
g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult))
|
||
|
||
if len(summaryResult) > 0 {
|
||
err := gfdb.DB(ctx, public.DbNameBlackDeacon).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
_, err := sessionDao.ExecWorkflowResultDao.BatchInsert(ctx, summaryResult)
|
||
return err
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return execInput, nil
|
||
}
|
||
|
||
// 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
|
||
}
|