1001 lines
32 KiB
Go
1001 lines
32 KiB
Go
|
|
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. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用 <div class=\"content-item\" id=\"content-{序号}\"> 包裹(序号从1开始)\n10. 每条文案内部必须在最上方添加一行固定格式:<p class=\"image-count\">需要配图:N 张</p> 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. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 只输出 HTML 结构,不输出任何额外文字"
|
|||
|
|
|
|||
|
|
//contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用 <div class=\"content-item\" id=\"content-{序号}\"> 包裹(序号从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 内容里提取图片数量(例如从 <p class="image-count">需要配图:3 张</p> 拿到 3)
|
|||
|
|
func extractImageCount(content string) int {
|
|||
|
|
re := regexp.MustCompile(`<p class="image-count">需要配图:(\d+) 张</p>`)
|
|||
|
|
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(`</?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`)
|
|||
|
|
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
|
|||
|
|
// 正则匹配<div class="content-item" id="content-{序号}">包裹的内容
|
|||
|
|
re := regexp.MustCompile(`<div class="content-item" id="content-\d+">([\s\S]*?)</div>`)
|
|||
|
|
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(`
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<style>
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
color: #333;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
.container {
|
|||
|
|
max-width: 900px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.item {
|
|||
|
|
padding: 30px;
|
|||
|
|
}
|
|||
|
|
.image-group {
|
|||
|
|
margin-bottom: 25px;
|
|||
|
|
}
|
|||
|
|
.image-group img {
|
|||
|
|
width: 100%;
|
|||
|
|
height: auto;
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
.image-group img:last-child {
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
.text {
|
|||
|
|
padding: 0;
|
|||
|
|
font-size: 15px;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
color: #555;
|
|||
|
|
}
|
|||
|
|
.text h2 {
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #1a1a1a;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
.text h3 {
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #2c3e50;
|
|||
|
|
margin: 20px 0 12px;
|
|||
|
|
padding-left: 12px;
|
|||
|
|
border-left: 4px solid #409eff;
|
|||
|
|
}
|
|||
|
|
.text p {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
text-align: justify;
|
|||
|
|
}
|
|||
|
|
.text strong {
|
|||
|
|
color: #e74c3c;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.text ul {
|
|||
|
|
list-style: none;
|
|||
|
|
padding: 0;
|
|||
|
|
margin: 15px 0;
|
|||
|
|
}
|
|||
|
|
.text ul li {
|
|||
|
|
padding: 10px 0 10px 30px;
|
|||
|
|
position: relative;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
.text ul li:before {
|
|||
|
|
content: "●";
|
|||
|
|
color: #409eff;
|
|||
|
|
font-size: 12px;
|
|||
|
|
position: absolute;
|
|||
|
|
left: 12px;
|
|||
|
|
top: 12px;
|
|||
|
|
}
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
body {
|
|||
|
|
padding: 10px;
|
|||
|
|
}
|
|||
|
|
.item {
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
.text h2 {
|
|||
|
|
font-size: 24px;
|
|||
|
|
}
|
|||
|
|
.text h3 {
|
|||
|
|
font-size: 18px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="container">
|
|||
|
|
<div class="item">
|
|||
|
|
`)
|
|||
|
|
|
|||
|
|
// 写入图片(支持0张、1张、多张)
|
|||
|
|
if len(item.Images) > 0 {
|
|||
|
|
htmlBuilder.WriteString(`<div class="image-group">`)
|
|||
|
|
for _, imgUrl := range item.Images {
|
|||
|
|
htmlBuilder.WriteString(fmt.Sprintf(`<img src="%s" alt="图片"/>`, imgUrl))
|
|||
|
|
}
|
|||
|
|
htmlBuilder.WriteString(`</div>`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 🔥 写入文案前:删除 <p class="image-count">需要配图:X 张</p>
|
|||
|
|
if item.Content != "" {
|
|||
|
|
// 正则删除整行
|
|||
|
|
re := regexp.MustCompile(`<p class="image-count">需要配图:\d+ 张</p>`)
|
|||
|
|
cleanContent := re.ReplaceAllString(item.Content, "")
|
|||
|
|
|
|||
|
|
// 写入清理后的文案
|
|||
|
|
htmlBuilder.WriteString(fmt.Sprintf(`<div class="text">%s</div>`, cleanContent))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
htmlBuilder.WriteString(`</div>
|
|||
|
|
</div>
|
|||
|
|
</body>
|
|||
|
|
</html>`)
|
|||
|
|
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
|
|||
|
|
}
|