package flow import ( "ai-agent/workflow/consts/flow" "ai-agent/workflow/consts/node" flowDao "ai-agent/workflow/dao/flow" "ai-agent/workflow/model/dto" flowDto "ai-agent/workflow/model/dto/flow" "ai-agent/workflow/model/entity" "context" "fmt" "regexp" "strconv" "strings" "time" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, node *entity.FlowNode) (map[string]any, map[string]any, map[string]any) { input := make(map[string]any) output := make(map[string]any) model := make(map[string]any) // 1. 有引用 → 取引用节点的字段值 if len(node.InputSource) > 0 { for _, source := range node.InputSource { refNodeID := source.NodeId isQuoteOutput := source.QuoteOutput fields := source.Field refNode, ok := execInput.ConfigMap[refNodeID] if !ok { continue } inputMap := buildInputMap(refNode) outputMap := mergeOutput(refNode.OutputResult) modelMap := mergeModel(refNode.ModelConfig) if isQuoteOutput { for k, v := range outputMap { output[k] = v } } if len(fields) > 0 { // 取指定字段 for _, f := range fields { if v, ok := inputMap[f]; ok { input[f] = v } if v, ok := modelMap[f]; ok { model[f] = v } } } else { // 取全部 for k, v := range inputMap { input[k] = v } for k, v := range modelMap { model[k] = v } } } } return input, output, model } // buildInputMap 从 FormConfig 构造输入map func buildInputMap(node *entity.FlowNode) map[string]any { m := make(map[string]any) for _, item := range node.FormConfig { m[item.Label] = item } return m } // mergeOutput 合并节点输出 []map → 单map func mergeOutput(output []node.NodeFormField) map[string]any { m := make(map[string]any) for _, item := range output { m[item.Label] = item } return m } // mergeOutput 合并节点输出 []map → 单map // 合并成你需要的 { key: { value: xxx } } 结构 func mergeModel(output node.ModelItem) map[string]any { m := make(map[string]any) // 遍历 output.ModelForm 里的每一个 key 和原始值 for key, rawValue := range output.ModelForm { // 包装成 { "value": 原始值 } m[key] = map[string]any{ "value": rawValue, } } return m } func StartLambda(ctx context.Context, input any) (any, error) { return input, nil } func FormLambda(ctx context.Context, input any) (any, error) { return input, nil } func IntentLambda(ctx context.Context, input any) (any, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { return nil, fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) } // 1. 直接用你原来的方法(返回两个 map) inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) var outputResult []node.NodeFormField for _, valueAny := range inputMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } for _, valueAny := range outputMap { if field, ok := valueAny.(node.NodeFormField); ok { if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { outputResult = append(outputResult, field) } } } for _, valueAny := range modelMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } nodeInput.Config.OutputResult = outputResult return nodeInput, nil } // JudgeLambda 分支判断核心:读取IntentLambda的输出 → 返回目标节点ID做路由 func JudgeLambda(ctx context.Context, input any) (string, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { return "", fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) } out := new([]node.NodeFormField) err := gconv.Structs(nodeInput.Config.OutputResult, out) if err != nil { return "", err } contextParts := "" for _, v := range nodeInput.Config.FormConfig { contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) } for _, v := range *out { contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) } configMap := gconv.Map(nodeInput.Config.Config) ids := gconv.Strings(configMap["branch_ids"]) branchIdNameMap := gconv.Map(configMap["branch_id_name_map"]) // 【重构】构建提示词:展示ID和对应的名称 var branchIdNameLines []string for _, id := range ids { name := gconv.String(branchIdNameMap[id]) branchIdNameLines = append(branchIdNameLines, fmt.Sprintf("%s: %s", id, name)) } prompt := fmt.Sprintf(` 你是流程路由助手,你的任务是根据上下文,选择一个正确的节点ID返回。 规则: 1. 只允许从下面的可选节点ID列表中选择一个返回 2. 不要返回任何多余文字、标点、解释、标题 3. 只返回纯节点ID 可选节点ID(ID: 节点描述): %s 上下文内容: %s `, strings.Join(branchIdNameLines, "\n"), contextParts) getIsChatModel, err := GetIsChatModel(ctx) if err != nil { return "", err } req := flowDto.ComposeMessagesReq{ ModelName: getIsChatModel.ModelName, SkillName: "", IsBuild: true, Cause: "判断节点", Form: map[string]any{}, UserForm: map[string]any{"prompt": prompt}, UserFiles: nodeInput.Global.FileUrl, SessionId: nodeInput.Global.SessionId, } msg, err := ComposeMessages(ctx, &req) if err != nil { return "", err } taskResult, err := GatewayTask(ctx, msg.EpicycleId, getIsChatModel.ModelName, msg.Messages) if err != nil { return "", err } result, err := GetTaskResult(ctx, taskResult) if err != nil { return "", err } mapTaskResult := gconv.Map(result.Text) content := "" for key, _ := range getIsChatModel.ResponseBody { content = gconv.String(mapTaskResult[key]) } fmt.Printf("JudgeLambda路由:目标节点ID=%s\n", gconv.String(content)) return content, nil } // TextModelLambda 构建文案 func TextModelLambda(ctx context.Context, input any) (any, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { return nil, fmt.Errorf("入参类型错误") } // 1. 直接用你原来的方法(返回两个 map) inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) var outputResult []node.NodeFormField for _, valueAny := range inputMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } for _, valueAny := range outputMap { if field, ok := valueAny.(node.NodeFormField); ok { if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { outputResult = append(outputResult, field) } } } for _, valueAny := range modelMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } resultUserFrom := make(map[string]any) for _, item := range outputResult { resultUserFrom[item.Label] = item } for _, item := range nodeInput.Config.FormConfig { resultUserFrom[item.Label] = item } if !g.IsEmpty(nodeInput.Global.Desc) { resultUserFrom["desc"] = node.NodeFormField{ Value: nodeInput.Global.Desc, Field: "desc", Label: "描述", Type: "text", } } resultFrom := make(map[string]any) for key, item := range nodeInput.Config.ModelConfig.ModelForm { resultFrom[key] = map[string]any{ "value": item, } } var skillName = nodeInput.Config.SkillName if g.IsEmpty(nodeInput.Config.SkillName) { skillName = nodeInput.Global.SkillName } contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用
包裹(序号从1开始)\n10. 每条文案内部必须在最上方添加一行固定格式:

需要配图:N 张

N 是这条文案需要的图片数量,只能是数字,不能是其他文字\n11. 只输出 HTML 结构,不输出任何额外文字" resultUserFrom["prompt"] = contentStr req := flowDto.ComposeMessagesReq{ ModelName: nodeInput.Config.ModelConfig.ModelName, SkillName: skillName, IsBuild: true, Cause: "文案节点", Form: resultFrom, UserForm: resultUserFrom, UserFiles: nodeInput.Global.FileUrl, SessionId: nodeInput.Global.SessionId, } //contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

  • ...
\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 只输出 HTML 结构,不输出任何额外文字" //contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

  • ...
\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用
包裹(序号从1开始)\n10. 只输出 HTML 结构,不输出任何额外文字" msg, err := ComposeMessages(ctx, &req) if err != nil { return nil, err } taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) if err != nil { return "", err } result, err := GetTaskResult(ctx, taskResult) if err != nil { return "", err } mapTaskResult := gconv.Map(result.Text) resultContent := "" for key, _ := range nodeInput.Config.ModelConfig.ModelResponse { resultContent = gconv.String(mapTaskResult[key]) } // 拆分多条文案 contentList := SplitMultiContents(resultContent) outputRes := make([]node.NodeFormField, 0) for i, content := range contentList { // 文案内容:content_0, content_1, content_2... outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("text_content_%d", i), Value: content, Label: fmt.Sprintf("文案内容_%d", i), Type: "string", Expand: extractImageCount(content), }) // 1. 去掉 HTML 标签,生成纯文本 plainText := stripHtmlTags(content) // 2. 上传纯文本到 OSS textFileName := fmt.Sprintf("ai_text_%d_%d.txt", time.Now().UnixMilli(), i) textUrl, err := Upload(ctx, &dto.UploadFileBytesReq{ FileBytes: []byte(plainText), FileName: textFileName, }) if err != nil { return nil, err } // 3. 把纯文本地址存入输出 outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("%v:text_url:%d", nodeInput.Config.Id, i), Value: textUrl.FileURL, Label: fmt.Sprintf("文案纯文本_%d", i), Type: "string", Expand: extractImageCount(content), }) } nodeInput.Config.OutputResult = outputRes return nodeInput, nil } // 从 HTML 内容里提取图片数量(例如从

需要配图:3 张

拿到 3) func extractImageCount(content string) int { re := regexp.MustCompile(`

需要配图:(\d+) 张

`) match := re.FindStringSubmatch(content) if len(match) >= 2 { num, _ := strconv.Atoi(match[1]) return num } return 0 // 没找到默认 0 } // stripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 func stripHtmlTags(html string) string { // 1. 替换块级标签为换行,保证排版 blockTags := regexp.MustCompile(`]*>`) text := blockTags.ReplaceAllString(html, "\n") // 2. 去掉所有剩余的 HTML 标签 allTags := regexp.MustCompile(`<[^>]+>`) text = allTags.ReplaceAllString(text, "") // 3. 🔥 新增:删除 "需要配图:X 张" 这一行(含前后可能的空格/换行) imageCountLine := regexp.MustCompile(`(?m)^\s*需要配图:\d+\s*张\s*$`) text = imageCountLine.ReplaceAllString(text, "") // 4. 清理多余空行(多个换行只保留一个,更干净) text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n") // 5. 只去掉首尾空白,中间换行保留 text = strings.TrimSpace(text) return text } // SplitMultiContents 拆分模型返回的多条文案(基于HTML标签分隔) func SplitMultiContents(htmlContent string) []string { var contents []string // 正则匹配
包裹的内容 re := regexp.MustCompile(`
([\s\S]*?)
`) matches := re.FindAllStringSubmatch(htmlContent, -1) for _, match := range matches { if len(match) > 1 { // 清理空内容 trimmed := strings.TrimSpace(match[1]) if trimmed != "" { contents = append(contents, trimmed) } } } // 兜底:如果没有匹配到结构化内容,按换行/分隔符拆分 if len(contents) == 0 { contents = strings.Split(htmlContent, "===分隔符===") // 提示词中可新增此兜底规则 } return contents } // ImageModelLambda 构建图片 func ImageModelLambda(ctx context.Context, input any) (any, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { return nil, fmt.Errorf("入参类型错误") } // 1. 直接用你原来的方法(返回两个 map) inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) var outputResult []node.NodeFormField for _, valueAny := range inputMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } for _, valueAny := range outputMap { if field, ok := valueAny.(node.NodeFormField); ok { if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { outputResult = append(outputResult, field) } } } for _, valueAny := range modelMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } resultUserFrom := make(map[string]any) for _, item := range outputResult { resultUserFrom[item.Label] = item } for _, item := range nodeInput.Config.FormConfig { resultUserFrom[item.Label] = item } if !g.IsEmpty(nodeInput.Global.Desc) { resultUserFrom["desc"] = node.NodeFormField{ Value: nodeInput.Global.Desc, Field: "desc", Label: "描述", Type: "text", } } resultFrom := make(map[string]any) for key, item := range nodeInput.Config.ModelConfig.ModelForm { resultFrom[key] = map[string]any{ "value": item, } } var skillName = nodeInput.Config.SkillName if g.IsEmpty(nodeInput.Config.SkillName) { skillName = nodeInput.Global.SkillName } req := flowDto.ComposeMessagesReq{ ModelName: nodeInput.Config.ModelConfig.ModelName, SkillName: skillName, IsBuild: true, Cause: "图片节点", Form: resultFrom, UserForm: resultUserFrom, UserFiles: nodeInput.Global.FileUrl, SessionId: nodeInput.Global.SessionId, } msg, err := ComposeMessages(ctx, &req) if err != nil { return nil, err } taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) if err != nil { return "", err } result, err := GetTaskResult(ctx, taskResult) if err != nil { return "", err } //result := new(flowDto.TaskCallback) //result.Text = "{\n \"content\": [\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/8d/20260512/76483b06/306aac7b-915e-479d-94d4-adc3cf1d6f22.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=3a3KDmPNeO%2BVjHJbAV8t0R7UF6Q%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/c9/20260512/76483b06/f8f3e9be-2920-48b8-93f5-acbf26e52b0c.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=li%2FpcoX5i7FJrk3PCpw5jrbWy2k%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/89/20260512/76483b06/38d55abe-8230-4837-85d3-426265139be0.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=uNRV9RQY2O60frAtIg6JvCcVhDw%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/82/20260512/76483b06/e100070d-2a79-4ec8-be72-105226854bab.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=7UCh7FmYt0%2FYxyItNoLELp7zPF0%3D\"\n }\n ]\n}" mapTaskResult := gconv.Map(result.Text) imgs := []string{} for key, _ := range nodeInput.Config.ModelConfig.ModelResponse { imgs = gconv.Strings(mapTaskResult[key]) } var images []string for _, item := range imgs { mapItem := gconv.Map(item) for _, value := range mapItem { values := "" values, ok = value.(string) if !ok { return nil, fmt.Errorf("图片地址类型错误") } // 下载官方临时图片 imgBytes, _, err := GetImageBytesFromURL(values) if err != nil { return nil, fmt.Errorf("下载图片失败: %w", err) } // 构造文件名 fileName := fmt.Sprintf("ai_image_%d.png", time.Now().UnixMilli()) // 上传到你的OSS(你项目已有的Upload方法) upResp, err := Upload(ctx, &dto.UploadFileBytesReq{ FileName: fileName, FileBytes: imgBytes, }) if err != nil { return nil, fmt.Errorf("上传OSS失败: %w", err) } images = append(images, upResp.FileURL) } } url, err := utils.GetFileAddressPrefix(ctx) if err != nil { return nil, err } outputRes := make([]node.NodeFormField, 0) for i, item := range images { // 图片:image_0, image_1, image_2... outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("image_%d", i), Value: fmt.Sprintf("%s%s", url, item), Label: fmt.Sprintf("图片_%d", i), Type: "string", }) // 额外存储关联关系 outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("%v:img_url:%d", nodeInput.Config.Id, i), Value: fmt.Sprintf("%s%s", url, item), Label: fmt.Sprintf("图片_%d关联文案ID", i), Type: "string", }) } nodeInput.Config.OutputResult = outputRes return input, nil } func MergeLambda(ctx context.Context, input any) (any, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { return nil, fmt.Errorf("汇总节点入参类型错误") } // 1. 把所有节点输出拍平成 字段名->内容 的map dataMap := make(map[string]node.NodeFormField) _, outputMap, _ := GetNodeContextContent(nodeInput.Global, nodeInput.Config) for _, valueAny := range outputMap { if field, ok := valueAny.(node.NodeFormField); ok { dataMap[field.Field] = field } } // 2. 提取所有文案:text_content_0,1,2... var contents []node.NodeFormField for i := 0; ; i++ { key := fmt.Sprintf("text_content_%d", i) val, has := dataMap[key] if !has || val.Value == "" { break } contents = append(contents, val) } // 3. 提取所有图片:image_0,1,2... var images []string for i := 0; ; i++ { key := fmt.Sprintf("image_%d", i) val, has := dataMap[key] if !has || val.Value == "" { break } images = append(images, val.Value) } // 4. 🔥 核心算法:图片按顺序连续归属给每条文案 textImgMap := make(map[int][]string) // key:文案下标,value:图片列表 if len(contents) > 0 && len(images) > 0 { imgIndex := 0 // 当前用到第几张图片 totalImg := len(images) for i, item := range contents { // 图片已分配完,直接退出 if imgIndex >= totalImg { break } // 当前文案需要挂载的图片数量 needCount := gconv.Int(item.Expand) if needCount <= 0 { continue } var imgList []string for imgc := 0; imgc < needCount; imgc++ { // 关键:必须判断是否越界 if imgIndex >= totalImg { break } imgList = append(imgList, images[imgIndex]) imgIndex++ } // 有图片才存入 map if len(imgList) > 0 { textImgMap[i] = imgList } } } type Item struct { Content string // 文案(可为空) Images []string // 图片(可空、可多张) } // 🔥 把现有数据转换成通用 Item 列表(支持:纯文案、纯图片、图文任意组合) var allItems []Item // 情况1:有文案 → 按文案条目生成 Item(每条文案+对应图片) if len(contents) > 0 { for i, val := range contents { item := Item{ Content: val.Value, // 文案 Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片) } allItems = append(allItems, item) } } else { // 情况2:没有文案,只有图片 → 每张/每组图片生成独立 Item(纯图片条目) if len(images) > 0 { for _, img := range images { allItems = append(allItems, Item{ Content: "", Images: []string{img}, }) } } } // 5. 生成多条独立HTML记录(通用方案:任意图文组合,每条独立生成+独立上传) var outputRecords []node.NodeFormField // 遍历所有【独立图文条目】 → 每条生成独立HTML、独立上传OSS、独立输出记录 for idx, item := range allItems { // item 结构包含:Content(string) + Images([]string) // 支持任意来源:文生图、图生文、单独文、单独图、文图合并 // 生成单条HTML var htmlBuilder strings.Builder htmlBuilder.WriteString(`
`) // 写入图片(支持0张、1张、多张) if len(item.Images) > 0 { htmlBuilder.WriteString(`
`) for _, imgUrl := range item.Images { htmlBuilder.WriteString(fmt.Sprintf(`图片`, imgUrl)) } htmlBuilder.WriteString(`
`) } // 🔥 写入文案前:删除

需要配图:X 张

if item.Content != "" { // 正则删除整行 re := regexp.MustCompile(`

需要配图:\d+ 张

`) cleanContent := re.ReplaceAllString(item.Content, "") // 写入清理后的文案 htmlBuilder.WriteString(fmt.Sprintf(`
%s
`, cleanContent)) } htmlBuilder.WriteString(`
`) htmlContent := htmlBuilder.String() // 上传OSS(每条独立上传) fileName := fmt.Sprintf("item_%d_%d.html", idx, time.Now().UnixMilli()) ossResult, err := Upload(ctx, &dto.UploadFileBytesReq{ FileBytes: []byte(htmlContent), FileName: fileName, }) if err != nil { return nil, err } // 拼接成一条输出记录 // 每条记录包含:HTML内容 + 访问URL + 文案 + 图片列表 outputRecords = append(outputRecords, node.NodeFormField{ Field: fmt.Sprintf("item_html_%d", idx), Value: htmlContent, Label: fmt.Sprintf("条目%d HTML", idx+1), Type: "textarea", }, node.NodeFormField{ Field: fmt.Sprintf("item_html_url_%d", idx), Value: ossResult.FileURL, Label: fmt.Sprintf("条目%d 地址", idx+1), Type: "text", }, node.NodeFormField{ Field: fmt.Sprintf("item_text_%d", idx), Value: item.Content, Label: fmt.Sprintf("条目%d 文案", idx+1), Type: "text", }, node.NodeFormField{ Field: fmt.Sprintf("item_images_%d", idx), Value: strings.Join(item.Images, ","), Label: fmt.Sprintf("条目%d 图片", idx+1), Type: "text", }, ) } // 最终输出多条记录 nodeInput.Config.OutputResult = outputRecords 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) } // 聚合所有已执行节点的输出结果 var summaryResult []map[string]interface{} for _, nodeID := range execInput.Global.ExecutedNodes { nodeConfig := execInput.Global.ConfigMap[nodeID] if nodeConfig != nil && len(nodeConfig.OutputResult) > 0 { for _, field := range nodeConfig.OutputResult { if strings.Contains(field.Field, "item_html_url") || strings.Contains(field.Field, "img_url") || strings.Contains(field.Field, "text_url") { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = field.Value summaryResult = append(summaryResult, item) } } } } // 把汇总结果存入当前节点的输出 g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult)) executionReq := flowDto.UpdateFlowExecutionReq{ Id: execInput.Global.ExecutionId, OutputParams: summaryResult, } executionReq.Status = flow.FlowExecutionStatusSuccess.Code() _, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq) return execInput, err } //func SummaryLambda(ctx context.Context, input any) (any, error) { // execInput, ok := input.(*flowDto.NodeExecutionInput) // if !ok { // return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input) // } // // // 1. 定义临时映射:按条目序号(如item_0)聚合html/img/text // // key: 条目序号(如0/1/2), value: {html:"", img:"", text:""} // itemMap := make(map[int]map[string]string) // // 存储每个条目对应的时间戳(一个条目一个唯一时间戳) // itemTimeMap := make(map[int]int64) // // // 2. 遍历已执行节点,解析输出字段并分组 // for _, nodeID := range execInput.Global.ExecutedNodes { // nodeConfig := execInput.Global.ConfigMap[nodeID] // if nodeConfig == nil || len(nodeConfig.OutputResult) == 0 { // continue // } // // // 遍历节点的输出字段 // for _, field := range nodeConfig.OutputResult { // var itemIndex int // var fieldType string // var fieldValue string // // // 匹配「条目HTML地址」字段(如item_html_url_0) // if match := regexp.MustCompile(`item_html_url_(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { // itemIndex, _ = strconv.Atoi(match[1]) // fieldType = "html" // fieldValue = gconv.String(field.Value) // } else if match := regexp.MustCompile(`img_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { // itemIndex, _ = strconv.Atoi(match[1]) // fieldType = "img" // fieldValue = gconv.String(field.Value) // } else if match := regexp.MustCompile(`text_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { // itemIndex, _ = strconv.Atoi(match[1]) // fieldType = "text" // fieldValue = gconv.String(field.Value) // } else { // // 非目标字段,跳过 // continue // } // // // 初始化条目映射(首次遇到该条目时) // if _, exists := itemMap[itemIndex]; !exists { // itemMap[itemIndex] = map[string]string{ // "html": "", // "img": "", // "text": "", // } // // 为该条目生成唯一时间戳(毫秒级) // itemTimeMap[itemIndex] = time.Now().UnixMilli() // } // // // 填充该条目对应的字段值 // itemMap[itemIndex][fieldType] = fieldValue // } // } // // // 3. 组装最终的汇总结构:[{内容N:{html:"",img:"",text:""},时间戳:xxx}, ...] // var summaryResult []map[string]interface{} // // 按条目序号排序(保证顺序一致) // itemIndexes := make([]int, 0, len(itemMap)) // for idx := range itemMap { // itemIndexes = append(itemIndexes, idx) // } // sort.Ints(itemIndexes) // // // 遍历排序后的条目,组装结构 // for _, idx := range itemIndexes { // itemData := itemMap[idx] // timeStamp := itemTimeMap[idx] // // // 单条目结构:{"内容X": {html:"",img:"",text:""}, "时间戳": xxx} // itemResult := make(map[string]interface{}) // itemResult[fmt.Sprintf("内容%d", idx+1)] = map[string]string{ // "html": itemData["html"], // "img": itemData["img"], // "text": itemData["text"], // } // itemResult["时间戳"] = timeStamp // // summaryResult = append(summaryResult, itemResult) // } // // // 4. 打印调试&更新数据库 // g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult)) // executionReq := flowDto.UpdateFlowExecutionReq{ // Id: execInput.Global.ExecutionId, // OutputParams: summaryResult, // Status: flow.FlowExecutionStatusSuccess.Code(), // } // _, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq) // // return execInput, err //} // VideoModelLambda 构建视频 func VideoModelLambda(ctx context.Context, input any) (any, error) { fmt.Println("VideoModelLambda:", input) return input, nil } // AudioModelLambda 构建音频 func AudioModelLambda(ctx context.Context, input any) (any, error) { fmt.Println("AudioModelLambda:", input) return input, nil } // CustomLambda 构建自定义 func CustomLambda(ctx context.Context, input any) (any, error) { fmt.Println("CustomLambda:", input) return input, nil }