Files
ai-agent/workflow/service/flow/lambda_summary.go
T
19904408334 8c6305f267 fix(workflow): 修复取消/中断漏扣费与终态提前落库问题
- launchExecution 失败时返回真实 execId,避免 wrapper 兜底失效
- summary 节点移除执行终态与计费写库,统一由 recordWorkflow 结算
- 更新数据库连接凭据
2026-09-04 13:29:47 +08:00

101 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}