From 1fbed2febd38e5ca1f3bbc9de9ae5108cd6af376 Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Mon, 18 May 2026 18:58:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=E6=89=A7=E8=A1=8C=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=8D=95=E6=A8=A1=E5=9E=8B=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- workflow/consts/node/node_template.go | 123 +--- .../service/flow/flow_execution_service.go | 623 ++++-------------- workflow/service/flow/lambda_node.go | 563 +--------------- workflow/service/flow/lambda_node_util.go | 192 +++--- 4 files changed, 273 insertions(+), 1228 deletions(-) diff --git a/workflow/consts/node/node_template.go b/workflow/consts/node/node_template.go index b1f9907..600cae4 100644 --- a/workflow/consts/node/node_template.go +++ b/workflow/consts/node/node_template.go @@ -10,15 +10,19 @@ const ( // 节点名称 const ( - NodeNameTextModel = "生成文案" - NodeNameImageModel = "生成图片" - NodeNameVideoModel = "视频" - NodeNameAudioModel = "音频" - NodeNameModel = "模型" - NodeNameMerge = "结果合并" - NodeNameJudge = "条件判断" - NodeNameForm = "表单" - NodeNameCustomNode = "自定义节点" + NodeNameTextModel = "生成文案" + NodeNameImageModel = "生成图片" + NodeNameVideoModel = "生成视频" + NodeNameSenseOptimize = "语义优化" + NodeNameStoryOptimize = "分镜优化" + NodeNameScriptOptimize = "剧本优化" + NodeNameAudioModel = "音频" + NodeNameModel = "模型" + NodeNameMerge = "结果合并" + NodeNameJudge = "条件判断" + NodeNameForm = "表单" + NodeNameHttp = "HTTP(S)接口" + NodeNameCustomNode = "自定义节点" ) // 表单字段 Label @@ -42,10 +46,13 @@ type NodeType string const ( // 组件 - NodeTypeTextModel NodeType = "text_model" - NodeTypeImageModel NodeType = "image_model" - NodeTypeVideoModel NodeType = "video_model" - NodeTypeAudioModel NodeType = "audio_model" + NodeTypeTextModel NodeType = "text_model" + NodeTypeImageModel NodeType = "image_model" + NodeTypeVideoModel NodeType = "video_model" + NodeTypeSenseOptimize NodeType = "sense_optimize" + NodeTypeStoryOptimize NodeType = "story_optimize" + NodeTypeScriptOptimize NodeType = "script_optimize" + NodeTypeAudioModel NodeType = "audio_model" // 基础 NodeTypeModel NodeType = "model" @@ -53,7 +60,7 @@ const ( NodeTypeJudge NodeType = "judge" NodeTypeForm NodeType = "form" NodeTypeIntent NodeType = "intent" - + NodeTypeHttp NodeType = "http" // 自定义 NodeTypeCustomNode NodeType = "custom_node" ) @@ -102,91 +109,3 @@ type NodeGroupItem struct { Label string `json:"label"` // 从常量来 Items []NodeItem `json:"items"` } - -// -//// 文案模型节点定义 -//func NewTextModelNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeTextModel, -// NodeName: NodeNameTextModel, -// FormConfig: []ModelItem{}, -// } -//} -// -//// 图片模型节点 -//func NewImageModelNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeImageModel, -// NodeName: NodeNameImageModel, -// FormConfig: []ModelItem{}, -// } -//} -// -//// 音频模型节点 -//func NewAudioModelNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeAudioModel, -// NodeName: NodeNameAudioModel, -// FormConfig: []ModelItem{}, -// } -//} -// -//// 视频模型节点 -//func NewVideoModelNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeVideoModel, -// NodeName: NodeNameVideoModel, -// FormConfig: []ModelItem{}, -// } -//} -// -//// 基础模型节点 -//func NewModelNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeModel, -// NodeName: NodeNameModel, -// FormConfig: []ModelItem{ -// { -// ModelName: "模型名称", -// ModelForm: []NodeFormField{ -// {Field: "apiKey", Label: FormLabelApiKey, Type: "input", Required: true}, -// {Field: "model", Label: FormLabelModel, Type: "input", Required: true}, -// }, -// }, -// }, -// } -//} -// -//// 判断节点 -//func NewJudgeNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeJudge, -// NodeName: NodeNameJudge, -// FormConfig: []ModelItem{ -// { -// ModelName: "判断条件", -// ModelForm: []NodeFormField{ -// {Field: "condition", Label: FormLabelCondition, Type: "input", Required: true}, -// }, -// }, -// }, -// } -//} -// -//// 表单参数节点 -//func NewFormNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeForm, -// NodeName: NodeNameForm, -// FormConfig: []ModelItem{}, -// } -//} -// -//// 自定义节点 -//func NewCustomNode() NodeItem { -// return NodeItem{ -// NodeCode: NodeTypeCustomNode, -// NodeName: NodeNameCustomNode, -// FormConfig: []ModelItem{}, -// } -//} diff --git a/workflow/service/flow/flow_execution_service.go b/workflow/service/flow/flow_execution_service.go index 602fb04..63bb0df 100644 --- a/workflow/service/flow/flow_execution_service.go +++ b/workflow/service/flow/flow_execution_service.go @@ -5,20 +5,16 @@ import ( "ai-agent/workflow/consts/node" fileDao "ai-agent/workflow/dao/file" flowDao "ai-agent/workflow/dao/flow" - "ai-agent/workflow/model/dto" fileDto "ai-agent/workflow/model/dto/file" flowDto "ai-agent/workflow/model/dto/flow" "ai-agent/workflow/model/entity" "context" "errors" "fmt" - "os" - "regexp" "sort" "strconv" "strings" "sync" - "time" "gitea.com/red-future/common/utils" "github.com/cloudwego/eino/compose" @@ -131,7 +127,7 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx suffix = "图片" case strings.Contains(val, "html") || strings.Contains(val, "HTML"): suffix = "HTML" - case strings.Contains(val, "txt") || len(val) > 50: + case strings.Contains(val, "inc") || len(val) > 50: suffix = "文案" } @@ -355,516 +351,127 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute } } - if isDialogue && !g.IsEmpty(flowInfo) { - // 查询节点中是否包含结果合并节点 - var htmlUrl []string - var textNodeId string - var textModelName string - var textModelResponse map[string]any - textResultFrom := make(map[string]any) - var imgNodeId string - var imgModelName string - var imgModelResponse map[string]any - imgResultFrom := make(map[string]any) - for _, item := range flowInfo.NodeInputParams { - if item.NodeCode == node.NodeTypeMerge { - for _, outputParamsItem := range flowInfo.OutputParams { - outputParamsMap := gconv.Map(outputParamsItem) - for _, mapItem := range outputParamsMap { - if strings.HasSuffix(gconv.String(mapItem), ".html") { - htmlUrl = append(htmlUrl, gconv.String(mapItem)) - } - } - } - } - if item.NodeCode == node.NodeTypeTextModel { - textNodeId = item.Id - textModelName = item.ModelConfig.ModelName - textModelResponse = item.ModelConfig.ModelResponse - for key, modelFormItem := range item.ModelConfig.ModelForm { - textResultFrom[key] = map[string]any{ - "value": modelFormItem, - } - } - } - if item.NodeCode == node.NodeTypeImageModel { - imgNodeId = item.Id - imgModelName = item.ModelConfig.ModelName - imgModelResponse = item.ModelConfig.ModelResponse - for key, modelFormItem := range item.ModelConfig.ModelForm { - imgResultFrom[key] = map[string]any{ - "value": modelFormItem, - } - } - } + if isDialogue && !g.IsEmpty(flowInfo) && !g.IsEmpty(req.ResultUrl) { + if strings.HasSuffix(gconv.String(req.ResultUrl), ".inc") { + err = TextModelSingleLambda(ctx, req, flowInfo) + return + } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".png") { + err = ImgModelSingleLambda(ctx, req, flowInfo) + return + } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".html") { + err = TextImgModelSingleLambda(ctx, req, flowInfo) + return } - var url string - url, err = utils.GetFileAddressPrefix(ctx) - if err != nil { - return nil, err - } - if strings.HasSuffix(gconv.String(req.ResultUrl), ".md") { - resultUserFrom := make(map[string]any) - resultUserFrom["desc"] = req.Desc + return nil, errors.New("文件格式不支持") + } - var textNode []node.NodeFormField - textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, resultUserFrom, textModelResponse, req.FileUrl) - if err != nil { - return nil, err + // ========================================================================= + // ✅【第1步】给所有判断节点自动生成意图识别节点 + // ========================================================================= + judge2IntentNodeMap := make(map[string]string) + finalNodes := make([]entity.FlowNode, 0, len(req.FlowContent.Nodes)*2) + for _, item := range req.FlowContent.Nodes { + finalNodes = append(finalNodes, item) + // 判断节点自动加 intent 节点 + if item.NodeCode == node.NodeTypeJudge { + intentNodeID := fmt.Sprintf("intent_%s", item.Id) + intentNode := entity.FlowNode{ + Id: intentNodeID, + NodeCode: node.NodeTypeIntent, + Name: fmt.Sprintf("意图识别-%s", item.Name), + InputSource: item.InputSource, // ✅ 正确赋值 + FormConfig: item.FormConfig, // ✅ 用户配置 + ModelConfig: item.ModelConfig, // ✅ 系统配置 } - var textContent []string - var textUrl []string - for _, item := range textNode { - if strings.Contains(item.Field, "text_content") { - textContent = append(textContent, item.Value) - } - if strings.Contains(item.Field, "text_url") { - textUrl = append(textUrl, item.Value) - } - } - - } - content := "" - // 第二步 执行目标节点 - if content == "text" { - resultUserFrom := make(map[string]any) - resultUserFrom["desc"] = req.Desc - - var textNode []node.NodeFormField - textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, resultUserFrom, textModelResponse, req.FileUrl) - if err != nil { - return nil, err - } - var htmlTags []string - var textUrl []string - for _, item := range textNode { - if strings.Contains(item.Field, "text_content") { - htmlTags = append(htmlTags, item.Value) - } - if strings.Contains(item.Field, "text_url") { - textUrl = append(textUrl, item.Value) - } - } - - var htmlContentUrl []string - if !g.IsEmpty(htmlUrl) { - for i, item := range htmlUrl { - // 获取当前要替换的文本内容 - textContent := htmlTags[i] - // 1. 读取 HTML 文件内容 - var htmlBytes []byte - htmlBytes, err = os.ReadFile(url + item) - if err != nil { - fmt.Printf("读取文件失败 %s: %v", url+item, err) - continue - } - htmlContent := string(htmlBytes) - // 2. 构建要替换成的新 div 标签 - newTextTag := fmt.Sprintf(`
%s
`, textContent) - re := regexp.MustCompile(`.*?`) - result := re.ReplaceAllString(htmlContent, newTextTag) - - fmt.Printf("成功处理文件:%s", result) - - // 上传OSS(每条独立上传) - fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli()) - var ossResult *dto.UploadFileBytesRes - ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ - FileBytes: []byte(result), - FileName: fileName, - }) - if err != nil { - return nil, err - } - fmt.Printf("上传OSS成功:%s", ossResult.FileURL) - htmlContentUrl = append(htmlContentUrl, ossResult.FileURL) - } - } - var summaryResult []map[string]interface{} - if !g.IsEmpty(textUrl) { - for _, outputParamsItem := range flowInfo.OutputParams { - if !g.IsEmpty(htmlContentUrl) { - if strings.HasSuffix(gconv.String(outputParamsItem), ".html") { - continue - } - } - if strings.HasSuffix(gconv.String(outputParamsItem), ".txt") { - continue - } - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = outputParamsItem - summaryResult = append(summaryResult, item) - } - for _, textItem := range textUrl { - // 生成 毫秒时间戳 作为 KEY - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = textItem - summaryResult = append(summaryResult, item) - } - } - if !g.IsEmpty(htmlContentUrl) { - for _, outputParamsItem := range flowInfo.OutputParams { - if !g.IsEmpty(textUrl) { - if strings.HasSuffix(gconv.String(outputParamsItem), ".txt") { - continue - } - } - if strings.HasSuffix(gconv.String(outputParamsItem), ".html") { - continue - } - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = outputParamsItem - summaryResult = append(summaryResult, item) - } - for _, textItem := range htmlContentUrl { - // 生成 毫秒时间戳 作为 KEY - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = textItem - summaryResult = append(summaryResult, item) - } - - } - if !g.IsEmpty(summaryResult) { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: flowInfo.Id, - Status: flow.FlowExecutionStatusSuccess.Code(), - OutputParams: summaryResult, - } - _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - } - } else if content == "img" { - resultUserFrom := make(map[string]any) - resultUserFrom["desc"] = req.Desc - - var imgNode []node.NodeFormField - imgNode, err = ImgNode(ctx, imgNodeId, req.SessionId, imgModelName, req.SkillName, imgResultFrom, resultUserFrom, imgModelResponse, req.FileUrl) - if err != nil { - return nil, err - } - - var imgCount int - var imgUrl []string - var htmlBuilder strings.Builder - htmlBuilder.WriteString(`
`) - for _, item := range imgNode { - if strings.Contains(item.Field, "img_url") { - imgCount = imgCount + 1 - htmlBuilder.WriteString(fmt.Sprintf(`图片`, item.Value)) - imgUrl = append(imgUrl, item.Value) - } - } - htmlBuilder.WriteString(`
`) - var htmlContentUrl []string - if !g.IsEmpty(htmlUrl) && imgCount > 0 { - for i, item := range htmlUrl { - // 1. 读取 HTML 文件内容 - var htmlBytes []byte - htmlBytes, err = os.ReadFile(url + item) - if err != nil { - fmt.Printf("读取文件失败 %s: %v", url+item, err) - continue - } - htmlContent := string(htmlBytes) - - re := regexp.MustCompile(`
[\s\S]*?
`) - result := re.ReplaceAllString(htmlContent, htmlBuilder.String()) - - fmt.Printf("成功处理文件:%s", result) - - // 上传OSS(每条独立上传) - fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli()) - var ossResult *dto.UploadFileBytesRes - ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ - FileBytes: []byte(result), - FileName: fileName, - }) - if err != nil { - return nil, err - } - fmt.Printf("上传OSS成功:%s", ossResult.FileURL) - htmlContentUrl = append(htmlContentUrl, ossResult.FileURL) - } - } - var summaryResult []map[string]interface{} - if !g.IsEmpty(imgCount) { - for _, outputParamsItem := range flowInfo.OutputParams { - if !g.IsEmpty(htmlContentUrl) { - if strings.HasSuffix(gconv.String(outputParamsItem), ".html") { - continue - } - } - if strings.HasSuffix(gconv.String(outputParamsItem), ".png") { - continue - } - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = outputParamsItem - summaryResult = append(summaryResult, item) - } - for _, textItem := range imgUrl { - // 生成 毫秒时间戳 作为 KEY - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = textItem - summaryResult = append(summaryResult, item) - } - } - if !g.IsEmpty(htmlContentUrl) { - for _, outputParamsItem := range flowInfo.OutputParams { - if !g.IsEmpty(imgUrl) { - if strings.HasSuffix(gconv.String(outputParamsItem), ".png") { - continue - } - } - if strings.HasSuffix(gconv.String(outputParamsItem), ".html") { - continue - } - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = outputParamsItem - summaryResult = append(summaryResult, item) - } - for _, textItem := range htmlContentUrl { - // 生成 毫秒时间戳 作为 KEY - timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - item := make(map[string]interface{}) - item[timeKey] = textItem - summaryResult = append(summaryResult, item) - } - } - if !g.IsEmpty(summaryResult) { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: flowInfo.Id, - Status: flow.FlowExecutionStatusSuccess.Code(), - OutputParams: summaryResult, - } - _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - } - } else if content == "text_img" { - //userFrom := make(map[string]any) - //userFrom["desc"] = req.Desc - // - //var textNode []node.NodeFormField - //textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, userFrom, textModelResponse, req.FileUrl) - //if err != nil { - // return nil, err - //} - // - //var htmlTags []string - //var textUrl []string - //for _, item := range textNode { - // if strings.Contains(item.Field, "text_content") { - // htmlTags = append(htmlTags, StripHtmlTags(item.Value, false)) - // } - // if strings.Contains(item.Field, "text_url") { - // textUrl = append(textUrl, item.Value) - // } - //} - // - //userFrom["prompt"] = htmlTags - //var imgNode []node.NodeFormField - //imgNode, err = ImgNode(ctx, imgNodeId, req.SessionId, imgModelName, req.SkillName, imgResultFrom, userFrom, imgModelResponse, req.FileUrl) - //if err != nil { - // return nil, err - //} - //var imgCount int - //var imgUrl []string - //var htmlBuilder strings.Builder - //htmlBuilder.WriteString(`
`) - //for _, item := range imgNode { - // if strings.Contains(item.Field, "img_url") { - // imgCount = imgCount + 1 - // htmlBuilder.WriteString(fmt.Sprintf(`图片`, item.Value)) - // imgUrl = append(imgUrl, item.Value) - // } - //} - //htmlBuilder.WriteString(`
`) - // - //var htmlContentUrl []string - //if !g.IsEmpty(htmlUrl) && imgCount > 0 { - // for i, item := range htmlUrl { - // // 获取当前要替换的文本内容 - // textContent := htmlTags[i] - // // 1. 读取 HTML 文件内容 - // var htmlBytes []byte - // htmlBytes, err = os.ReadFile(url + item) - // if err != nil { - // fmt.Printf("读取文件失败 %s: %v", url+item, err) - // continue - // } - // htmlContent := string(htmlBytes) - // - // re := regexp.MustCompile(`
[\s\S]*?
`) - // result := re.ReplaceAllString(htmlContent, htmlBuilder.String()) - // // 2. 构建要替换成的新 div 标签 - // newTextTag := fmt.Sprintf(`
%s
`, textContent) - // ret := regexp.MustCompile(`.*?`) - // result = ret.ReplaceAllString(htmlContent, newTextTag) - // fmt.Printf("成功处理文件:%s", result) - // - // // 上传OSS(每条独立上传) - // fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli()) - // var ossResult *dto.UploadFileBytesRes - // ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ - // FileBytes: []byte(result), - // FileName: fileName, - // }) - // if err != nil { - // return nil, err - // } - // fmt.Printf("上传OSS成功:%s", ossResult.FileURL) - // htmlContentUrl = append(htmlContentUrl, ossResult.FileURL) - // } - //} - //if !g.IsEmpty(htmlContentUrl) { - // for _, outputParamsItem := range flowInfo.OutputParams { - // if !g.IsEmpty(imgUrl) { - // if strings.HasSuffix(gconv.String(outputParamsItem), ".png") { - // continue - // } - // } - // if strings.HasSuffix(gconv.String(outputParamsItem), ".html") { - // continue - // } - // timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - // item := make(map[string]interface{}) - // item[timeKey] = outputParamsItem - // summaryResult = append(summaryResult, item) - // } - // for _, textItem := range htmlContentUrl { - // // 生成 毫秒时间戳 作为 KEY - // timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) - // item := make(map[string]interface{}) - // item[timeKey] = textItem - // summaryResult = append(summaryResult, item) - // } - //} - //if !g.IsEmpty(summaryResult) { - // executionReq := flowDto.UpdateFlowExecutionReq{ - // Id: flowInfo.Id, - // Status: flow.FlowExecutionStatusSuccess.Code(), - // OutputParams: summaryResult, - // } - // _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - //} - } else { - return nil, fmt.Errorf("意图识别失败") - } - } else { - // ========================================================================= - // ✅【第1步】给所有判断节点自动生成意图识别节点 - // ========================================================================= - judge2IntentNodeMap := make(map[string]string) - finalNodes := make([]entity.FlowNode, 0, len(req.FlowContent.Nodes)*2) - for _, item := range req.FlowContent.Nodes { - finalNodes = append(finalNodes, item) - // 判断节点自动加 intent 节点 - if item.NodeCode == node.NodeTypeJudge { - intentNodeID := fmt.Sprintf("intent_%s", item.Id) - intentNode := entity.FlowNode{ - Id: intentNodeID, - NodeCode: node.NodeTypeIntent, - Name: fmt.Sprintf("意图识别-%s", item.Name), - InputSource: item.InputSource, // ✅ 正确赋值 - FormConfig: item.FormConfig, // ✅ 用户配置 - ModelConfig: item.ModelConfig, // ✅ 系统配置 - } - finalNodes = append(finalNodes, intentNode) - judge2IntentNodeMap[item.Id] = intentNodeID - } - } - - summaryNodeID := "summary_node" - summaryNode := entity.FlowNode{ - Id: summaryNodeID, - NodeCode: node.NodeTypeCustomNode, // 复用自定义节点类型,也可新增专属类型 - Name: "结果汇总节点", - InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出 - FormConfig: nil, - ModelConfig: node.ModelItem{}, - } - finalNodes = append(finalNodes, summaryNode) - - // 替换节点列表 - req.FlowContent.Nodes = finalNodes - - // ========================================================================= - // ✅【第2步】构建执行图 - // ========================================================================= - var runGraph compose.Runnable[any, any] - runGraph, err = BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID) - if err != nil { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusFailed.Code(), - ErrorMessage: err.Error(), - } - _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) - if err1 != nil { - return - } - return nil, fmt.Errorf("执行工作流失败: %v", err) - } - - // ========================================================================= - // ✅【第3步】构建 ConfigMap - // ========================================================================= - configMap := make(map[string]*entity.FlowNode) - for _, cfg := range req.NodeInputParams { - configMap[cfg.Id] = cfg - } - // 自动给意图节点复制配置 - for judgeID, intentID := range judge2IntentNodeMap { - if cfg, ok := configMap[judgeID]; ok { - configMap[intentID] = cfg - } - } - // 初始化汇总节点配置 - configMap[summaryNodeID] = &summaryNode - - // ========================================================================= - // ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!) - // ========================================================================= - execInput := &flowDto.FlowExecutionInput{ - IsDialogue: isDialogue, - ExecutionId: executionId, - ConfigMap: configMap, - SessionId: req.SessionId, - Desc: req.Desc, - SkillName: req.SkillName, - FileUrl: req.FileUrl, - } - // 执行工作流 - _, err = runGraph.Invoke(execCtx, execInput) - if err != nil { - // 检测是否是取消导致的错误 - if errors.Is(execCtx.Err(), context.Canceled) { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusCancel.Code(), - } - _, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - return nil, fmt.Errorf("工作流已被取消: %v", err) - } - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusFailed.Code(), - ErrorMessage: err.Error(), - } - _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) - if err1 != nil { - return - } - return nil, fmt.Errorf("执行工作流失败: %v", err) + finalNodes = append(finalNodes, intentNode) + judge2IntentNodeMap[item.Id] = intentNodeID } } - return + summaryNodeID := "summary_node" + summaryNode := entity.FlowNode{ + Id: summaryNodeID, + NodeCode: node.NodeTypeCustomNode, // 复用自定义节点类型,也可新增专属类型 + Name: "结果汇总节点", + InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出 + FormConfig: nil, + ModelConfig: node.ModelItem{}, + } + finalNodes = append(finalNodes, summaryNode) + // 替换节点列表 + req.FlowContent.Nodes = finalNodes + + // ========================================================================= + // ✅【第2步】构建执行图 + // ========================================================================= + var runGraph compose.Runnable[any, any] + runGraph, err = BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID) + if err != nil { + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusFailed.Code(), + ErrorMessage: err.Error(), + } + _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) + if err1 != nil { + return + } + return nil, fmt.Errorf("执行工作流失败: %v", err) + } + + // ========================================================================= + // ✅【第3步】构建 ConfigMap + // ========================================================================= + configMap := make(map[string]*entity.FlowNode) + for _, cfg := range req.NodeInputParams { + configMap[cfg.Id] = cfg + } + // 自动给意图节点复制配置 + for judgeID, intentID := range judge2IntentNodeMap { + if cfg, ok := configMap[judgeID]; ok { + configMap[intentID] = cfg + } + } + // 初始化汇总节点配置 + configMap[summaryNodeID] = &summaryNode + + // ========================================================================= + // ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!) + // ========================================================================= + execInput := &flowDto.FlowExecutionInput{ + IsDialogue: isDialogue, + ExecutionId: executionId, + ConfigMap: configMap, + SessionId: req.SessionId, + Desc: req.Desc, + SkillName: req.SkillName, + FileUrl: req.FileUrl, + } + // 执行工作流 + _, err = runGraph.Invoke(execCtx, execInput) + if err != nil { + // 检测是否是取消导致的错误 + if errors.Is(execCtx.Err(), context.Canceled) { + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusCancel.Code(), + } + _, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq) + return nil, fmt.Errorf("工作流已被取消: %v", err) + } + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + Status: flow.FlowExecutionStatusFailed.Code(), + ErrorMessage: err.Error(), + } + _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) + if err1 != nil { + return + } + return nil, fmt.Errorf("执行工作流失败: %v", err) + } + return } // BuildGraphFromFlowContent 根据前端保存的工作流JSON,自动构建执行图 diff --git a/workflow/service/flow/lambda_node.go b/workflow/service/flow/lambda_node.go index 71e2d6d..8d6a15c 100644 --- a/workflow/service/flow/lambda_node.go +++ b/workflow/service/flow/lambda_node.go @@ -9,7 +9,6 @@ import ( "ai-agent/workflow/model/dto" fileDto "ai-agent/workflow/model/dto/file" flowDto "ai-agent/workflow/model/dto/flow" - "ai-agent/workflow/model/entity" "context" "fmt" "strconv" @@ -23,91 +22,6 @@ import ( "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 } @@ -117,9 +31,14 @@ func FormLambda(ctx context.Context, input any) (any, error) { } func IntentLambda(ctx context.Context, input any) (any, error) { + return input, nil +} + +// JudgeLambda 分支判断核心:读取IntentLambda的输出 → 返回目标节点ID做路由 +func JudgeLambda(ctx context.Context, input any) (string, error) { nodeInput, ok := input.(*flowDto.NodeExecutionInput) if !ok { - return nil, fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) + return "", fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) } // 1. 直接用你原来的方法(返回两个 map) inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) @@ -131,9 +50,7 @@ func IntentLambda(ctx context.Context, input any) (any, error) { } 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) - } + outputResult = append(outputResult, field) } } for _, valueAny := range modelMap { @@ -142,30 +59,12 @@ func IntentLambda(ctx context.Context, input any) (any, error) { } } - 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) } if !nodeInput.Global.IsDialogue { - for _, v := range *out { + for _, v := range outputResult { contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) } } @@ -188,7 +87,6 @@ func JudgeLambda(ctx context.Context, input any) (string, error) { if err != nil { return "", err } - req := flowDto.ComposeMessagesReq{ BuildType: 2, ModelName: getIsChatModel.ModelName, @@ -223,133 +121,12 @@ func TextModelLambda(ctx context.Context, input any) (any, error) { 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) - } - } - - resultUserFrom := make(map[string]any) - - for _, valueAny := range outputMap { - if field, ok := valueAny.(node.NodeFormField); ok { - if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") { - if strings.Contains(field.Field, "text_content") { - field.Value = StripHtmlTags(field.Value, false) - } - resultUserFrom[field.Label] = field - } - } - } - for _, valueAny := range modelMap { - if field, ok := valueAny.(node.NodeFormField); ok { - outputResult = append(outputResult, field) - } - } - if !nodeInput.Global.IsDialogue { - 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 := "你是专业内容生成助手,请严格按以下规则输出内容:1、输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释,2、整体用
包裹,3、主标题使用

,4、章节标题使用

,5、正文段落使用

,6、列表使用

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

需要配图:N 张

N 是这条文案需要的图片数量,只能是数字,不能是其他文字,11、只输出 HTML 结构,不输出任何额外文字" - resultUserFrom["prompt"] = contentStr - - req := flowDto.ComposeMessagesReq{ - BuildType: 1, - ModelName: nodeInput.Config.ModelConfig.ModelName, - SkillName: skillName, - Cause: "文案节点", - Form: resultFrom, - UserForm: resultUserFrom, - UserFiles: nodeInput.Global.FileUrl, - SessionId: nodeInput.Global.SessionId, - } - - msg, err := ComposeMessages(ctx, &req) + skillName, from, userFrom := BuildParam(nodeInput) + outputRes, err := TextNode(ctx, nodeInput.Global.SessionId, nodeInput.Config.ModelConfig.ModelName, skillName, from, userFrom, nodeInput.Config.ModelConfig.ModelResponse, nodeInput.Global.FileUrl) if err != nil { return nil, err } - if g.IsEmpty(msg.Messages) { - return nil, fmt.Errorf("msg is empty") - } - taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) - if err != nil { - return nil, 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, true) - plainText := BuildText(content) - // 2. 上传纯文本到 OSS - textFileName := fmt.Sprintf("ai_text_%d_%d.inc", 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("text_url_%d", i), - Value: textUrl.FileURL, - Label: fmt.Sprintf("文案纯文本_txt_%d", i), - Type: "string", - Expand: ExtractImageCount(content), - }) - } nodeInput.Config.OutputResult = outputRes - return nodeInput, nil } @@ -359,145 +136,13 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) { 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) - } - } - - resultUserFrom := make(map[string]any) - - for _, valueAny := range outputMap { - if field, ok := valueAny.(node.NodeFormField); ok { - if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") { - if strings.Contains(field.Field, "text_content") { - field.Value = StripHtmlTags(field.Value, false) - } - resultUserFrom[field.Label] = field - } - } - } - for _, valueAny := range modelMap { - if field, ok := valueAny.(node.NodeFormField); ok { - outputResult = append(outputResult, field) - } - } - - if !nodeInput.Global.IsDialogue { - 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{ - BuildType: 1, - ModelName: nodeInput.Config.ModelConfig.ModelName, - SkillName: skillName, - Cause: "图片节点", - Form: resultFrom, - UserForm: resultUserFrom, - UserFiles: nodeInput.Global.FileUrl, - SessionId: nodeInput.Global.SessionId, - } - msg, err := ComposeMessages(ctx, &req) + skillName, from, userFrom := BuildParam(nodeInput) + outputRes, err := ImgNode(ctx, nodeInput.Global.SessionId, nodeInput.Config.ModelConfig.ModelName, skillName, from, userFrom, nodeInput.Config.ModelConfig.ModelResponse, nodeInput.Global.FileUrl) if err != nil { return nil, err } - if g.IsEmpty(msg.Messages) { - return nil, fmt.Errorf("msg is empty") - } - 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) - - 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, imgOk := value.(string) - if !imgOk { - 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("图片_img_%d关联文案ID", i), - Type: "string", - }) - } nodeInput.Config.OutputResult = outputRes - return input, nil + return nodeInput, nil } func MergeLambda(ctx context.Context, input any) (any, error) { @@ -529,7 +174,7 @@ func MergeLambda(ctx context.Context, input any) (any, error) { // 3. 提取所有图片:image_0,1,2... var images []string for i := 0; ; i++ { - key := fmt.Sprintf("image_%d", i) + key := fmt.Sprintf("img_url%d", i) val, has := dataMap[key] if !has || val.Value == "" { break @@ -579,12 +224,16 @@ func MergeLambda(ctx context.Context, input any) (any, error) { // 🔥 把现有数据转换成通用 Item 列表(支持:纯文案、纯图片、图文任意组合) var allItems []Item + url, err := utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } // 情况1:有文案 → 按文案条目生成 Item(每条文案+对应图片) if len(contents) > 0 { for i, val := range contents { item := Item{ - Content: val.Value, // 文案 - Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片) + Content: url + val.Value, // 文案 + Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片) } allItems = append(allItems, item) } @@ -637,13 +286,13 @@ func MergeLambda(ctx context.Context, input any) (any, error) { Type: "text", }, node.NodeFormField{ - Field: fmt.Sprintf("item_text_%d", idx), + Field: fmt.Sprintf("item_txt_url_%d", idx), Value: item.Content, Label: fmt.Sprintf("条目%d 文案", idx+1), Type: "text", }, node.NodeFormField{ - Field: fmt.Sprintf("item_images_%d", idx), + Field: fmt.Sprintf("item_image_url_%d", idx), Value: strings.Join(item.Images, ","), Label: fmt.Sprintf("条目%d 图片", idx+1), Type: "text", @@ -744,171 +393,3 @@ func CustomLambda(ctx context.Context, input any) (any, error) { fmt.Println("CustomLambda:", input) return input, nil } - -func TextNode(ctx context.Context, nodeId, sessionId, modelName, skillName string, from, userFrom, modelResponse map[string]any, fileUrl []string) ([]node.NodeFormField, error) { - contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:1、输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释,2、整体用
包裹,3、主标题使用

,4、章节标题使用

,5、正文段落使用

,6、列表使用

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

需要配图:N 张

N 是这条文案需要的图片数量,只能是数字,不能是其他文字,11、只输出 HTML 结构,不输出任何额外文字" - userFrom["prompt"] = contentStr - - textMsgReq := flowDto.ComposeMessagesReq{ - BuildType: 1, - ModelName: modelName, - SkillName: skillName, - Cause: "文案节点", - Form: from, - UserForm: userFrom, - UserFiles: fileUrl, - SessionId: sessionId, - } - msg, err := ComposeMessages(ctx, &textMsgReq) - if err != nil { - return nil, err - } - if g.IsEmpty(msg.Messages) { - return nil, fmt.Errorf("msg is empty") - } - - var taskResult any - taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages) - if err != nil { - return nil, err - } - var getTaskResult *flowDto.TaskCallback - getTaskResult, err = GetTaskResult(ctx, taskResult) - if err != nil { - return nil, err - } - mapTaskResult := gconv.Map(getTaskResult.Text) - - resultContent := "" - for key, _ := range modelResponse { - resultContent = gconv.String(mapTaskResult[key]) - } - - // 拆分多条文案 - contentList := SplitMultiContents(resultContent) - - outputRes := make([]node.NodeFormField, 0) - for i, contentItem := range contentList { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("text_content_%d", i), - Value: contentItem, - Label: fmt.Sprintf("文案内容_%d", i), - Type: "string", - Expand: ExtractImageCount(contentItem), - }) - - // 1. 去掉 HTML 标签,生成纯文本 - //plainText := StripHtmlTags(contentItem, true) - plainText := BuildHtml(contentItem, nil) - // 2. 上传纯文本到 OSS - textFileName := fmt.Sprintf("ai_text_%d_%d.md", time.Now().UnixMilli(), i) - var textUrl *dto.UploadFileBytesRes - 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", nodeId, i), - Value: textUrl.FileURL, - Label: fmt.Sprintf("文案纯文本_txt_%d", i), - Type: "string", - Expand: ExtractImageCount(contentItem), - }) - } - return outputRes, nil -} - -func ImgNode(ctx context.Context, nodeId, sessionId, modelName, skillName string, form, userForm, modelResponse map[string]any, fileUrl []string) ([]node.NodeFormField, error) { - imgMsgReq := flowDto.ComposeMessagesReq{ - BuildType: 1, - ModelName: modelName, - SkillName: skillName, - Cause: "图片节点", - Form: form, - UserForm: userForm, - UserFiles: fileUrl, - SessionId: sessionId, - } - msg, err := ComposeMessages(ctx, &imgMsgReq) - if err != nil { - return nil, err - } - if g.IsEmpty(msg.Messages) { - return nil, fmt.Errorf("msg is empty") - } - var taskResult any - taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages) - if err != nil { - return nil, err - } - var getTaskResult *flowDto.TaskCallback - getTaskResult, err = GetTaskResult(ctx, taskResult) - if err != nil { - return nil, err - } - mapTaskResult := gconv.Map(getTaskResult.Text) - - var resultContent []string - for key, _ := range modelResponse { - resultContent = gconv.Strings(mapTaskResult[key]) - } - - var images []string - for _, item := range resultContent { - mapItem := gconv.Map(item) - for _, value := range mapItem { - values, ok := value.(string) - if !ok { - return nil, fmt.Errorf("图片地址类型错误") - } - // 下载官方临时图片 - var imgBytes []byte - 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方法) - var upResp *dto.UploadFileBytesRes - 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) - } - } - - var url string - 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", nodeId, i), - Value: fmt.Sprintf("%s%s", url, item), - Label: fmt.Sprintf("图片_img_%d关联文案ID", i), - Type: "string", - }) - } - - return outputRes, nil -} diff --git a/workflow/service/flow/lambda_node_util.go b/workflow/service/flow/lambda_node_util.go index 6cae42a..419615e 100644 --- a/workflow/service/flow/lambda_node_util.go +++ b/workflow/service/flow/lambda_node_util.go @@ -33,6 +33,66 @@ func GetIsChatModel(ctx context.Context) (res *flowDto.GetIsChatModelRes, err er return } +func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (res *flowDto.ComposeMessagesRes, err error) { + headers := make(map[string]string) + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + } + res = new(flowDto.ComposeMessagesRes) + err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req) + return +} + +func GetModelResult(ctx context.Context, modelName, skillName string, form, userFrom map[string]any, fileUrl []string, sessionId string, cause string) (mapTaskResult map[string]any, err error) { + msgReq := flowDto.ComposeMessagesReq{ + BuildType: 1, + ModelName: modelName, + SkillName: skillName, + Cause: cause, + Form: form, + UserForm: userFrom, + UserFiles: fileUrl, + SessionId: sessionId, + } + msg, err := ComposeMessages(ctx, &msgReq) + if err != nil { + return + } + if g.IsEmpty(msg.Messages) { + return nil, fmt.Errorf("msg is empty") + } + var taskResult any + taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages) + if err != nil { + return + } + var getTaskResult *flowDto.TaskCallback + getTaskResult, err = GetTaskResult(ctx, taskResult) + if err != nil { + return + } + mapTaskResult = gconv.Map(getTaskResult.Text) + return mapTaskResult, nil +} + +func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) { + modelTaskId, err := CreateGatewayTask(ctx, &flowDto.CreateTaskReq{ + ModelName: model, + BizName: g.Cfg().MustGet(ctx, "server.name").String(), + CallbackUrl: "/flow/execution/modelCallback", + RequestPayload: content, + EpicycleId: epicycleId, + }) + if err != nil { + return nil, err + } + return Wait(ctx, modelTaskId) +} + func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, error) { headers := make(map[string]string) if r := g.RequestFromCtx(ctx); r != nil { @@ -51,34 +111,6 @@ func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, return res.TaskId, nil } -func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (res *flowDto.ComposeMessagesRes, err error) { - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - } - res = new(flowDto.ComposeMessagesRes) - err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req) - return -} - -func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) { - modelTaskId, err := CreateGatewayTask(ctx, &flowDto.CreateTaskReq{ - ModelName: model, - BizName: g.Cfg().MustGet(ctx, "server.name").String(), - CallbackUrl: "/flow/execution/modelCallback", - RequestPayload: content, - EpicycleId: epicycleId, - }) - if err != nil { - return nil, err - } - return Wait(ctx, modelTaskId) -} - func GetTaskResult(ctx context.Context, result any) (*flowDto.TaskCallback, error) { task := new(flowDto.TaskCallback) if err := gconv.Struct(result, task); err != nil { @@ -116,17 +148,22 @@ func FetchRemoteJsonFile(ctx context.Context, fileUrl string) ([]byte, error) { return io.ReadAll(resp.Body) } -func GetImageBytesFromURL(url string) (all []byte, contentType string, err error) { +func GetFileBytesFromURL(url string) (all []byte, err error) { resp, err := http.Get(url) if err != nil { + fmt.Printf("请求失败 %s: %v", url, err) return } defer resp.Body.Close() - all, err = io.ReadAll(resp.Body) - if err != nil { + if resp.StatusCode != http.StatusOK { + fmt.Printf("请求失败,状态码: %d\n", resp.StatusCode) + return + } + all, err = io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取内容失败 %s: %v", url, err) return } - contentType = resp.Header.Get("Content-Type") return } @@ -197,24 +234,24 @@ func BuildText(text string) string { .item { padding: 30px; } - .image-group { - margin-bottom: 25px; - } .image-group img { width: 100%; height: auto; display: block; - margin-bottom: 15px; + margin-bottom: 6px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .image-group img:last-child { margin-bottom: 0; } + .image-group { + margin-bottom: 25px; + } .text { padding: 0; font-size: 15px; - line-height: 1.8; + line-height: 1.4; color: #555; } .text h2 { @@ -222,7 +259,7 @@ func BuildText(text string) string { font-weight: bold; color: #1a1a1a; margin-bottom: 15px; - line-height: 1.4; + line-height: 1.2; } .text h3 { font-size: 20px; @@ -233,7 +270,7 @@ func BuildText(text string) string { border-left: 4px solid #409eff; } .text p { - margin-bottom: 15px; + margin-bottom: 12px; text-align: justify; } .text strong { @@ -243,12 +280,12 @@ func BuildText(text string) string { .text ul { list-style: none; padding: 0; - margin: 15px 0; + margin: 8px 0; } .text ul li { padding: 10px 0 10px 30px; position: relative; - line-height: 1.6; + line-height: 1.2; } .text ul li:before { content: "●"; @@ -262,9 +299,6 @@ func BuildText(text string) string { body { padding: 10px; } - .item { - padding: 20px; - } .text h2 { font-size: 24px; } @@ -278,27 +312,11 @@ func BuildText(text string) string {
`) - - //// 写入图片(支持0张、1张、多张) - //if len(images) > 0 { - // htmlBuilder.WriteString(`
`) - // for _, imgUrl := range images { - // htmlBuilder.WriteString(fmt.Sprintf(`图片`, imgUrl)) - // } - // htmlBuilder.WriteString(`
`) - //} - // 🔥 写入文案前:删除

需要配图:X 张

if text != "" { - // 正则删除整行 - imageTagRegex := regexp.MustCompile(`

[\s\S]*?

`) - //re := regexp.MustCompile(`

需要配图:\d+ 张

`) - cleanContent := imageTagRegex.ReplaceAllString(text, "") - // 写入清理后的文案 - htmlBuilder.WriteString(fmt.Sprintf(`
%s
`, cleanContent)) + htmlBuilder.WriteString(fmt.Sprintf(`
%s
`, ImageTagRegex(text))) } - htmlBuilder.WriteString(`
@@ -336,9 +354,7 @@ func BuildHtml(text string, images []string) string { border-radius: 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); } - #content { - white-space: pre-line; - } + @@ -352,7 +368,6 @@ func BuildHtml(text string, images []string) string { } htmlBuilder.WriteString(`
`) } - htmlBuilder.WriteString(`
加载中...

@@ -365,7 +380,7 @@ func BuildHtml(text string, images []string) string { return res.text(); }) .then(text => { - document.getElementById("content").textContent = text; + document.getElementById("content").innerHTML = text; }) .catch(err => { document.getElementById("content").innerHTML = "加载失败:" + err.message; @@ -377,25 +392,28 @@ func BuildHtml(text string, images []string) string { return htmlBuilder.String() } -// ExtractImageCount 从 HTML 内容里提取图片数量(例如从

需要配图:3 张

拿到 3) +// ExtractImageCount 修复:支持单引号/双引号 + 换行 + 空格 func ExtractImageCount(content string) int { - re := regexp.MustCompile(`

[^\d]*(\d+)[^\d]*

`) + // 🔥 关键:支持 class='image-count' (单引号) + re := regexp.MustCompile(`

]*>.*?(\d+).*?

`) match := re.FindStringSubmatch(content) if len(match) >= 2 { - num, _ := strconv.Atoi(match[1]) - return num + num, err := strconv.Atoi(match[1]) + if err == nil { + return num + } } return 0 } -// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 -func StripHtmlTags(html string, delImageCount bool) string { - if delImageCount { - // 🔥 第一步:直接删除整个

...

标签(包含内容) - imageTagRegex := regexp.MustCompile(`

[\s\S]*?

`) - html = imageTagRegex.ReplaceAllString(html, "") - } +func ImageTagRegex(html string) string { + // 🔥 修复:支持单引号、双引号、空格、换行,100% 删除

+ imageTagRegex := regexp.MustCompile(`

]*>[\s\S]*?

`) + return imageTagRegex.ReplaceAllString(html, "") +} +// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 +func StripHtmlTags(html string) string { // 1. 替换块级标签为换行,保证排版 blockTags := regexp.MustCompile(`]*>`) text := blockTags.ReplaceAllString(html, "\n") @@ -434,3 +452,23 @@ func SplitMultiContents(htmlContent string) []string { } return contents } + +// GetAllImgSrcFromHtml 先把提取img src的工具方法放在外面 +func GetAllImgSrcFromHtml(html string) []string { + var imgSrcList []string + re := regexp.MustCompile(`]*src\s*=\s*["']([^"']+)["']`) + matchs := re.FindAllStringSubmatch(html, -1) + for _, match := range matchs { + if len(match) >= 2 { + imgSrcList = append(imgSrcList, match[1]) + } + } + return imgSrcList +} + +// ReplaceImgSrc 替换img src的方法 +func ReplaceImgSrc(html string, oldSrc string, newSrc string) string { + // 精准替换:找到 并替换 + re := regexp.MustCompile(`(]*src\s*=\s*["'])` + regexp.QuoteMeta(oldSrc) + `(["'])`) + return re.ReplaceAllString(html, `${1}`+newSrc+`${2}`) +}