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(`
,6、列表使用
需要配图: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、整体用,6、列表使用
需要配图: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 {需要配图:X 张
if text != "" { - // 正则删除整行 - imageTagRegex := regexp.MustCompile(`[\s\S]*?
`) - //re := regexp.MustCompile(`需要配图:\d+ 张
`) - cleanContent := imageTagRegex.ReplaceAllString(text, "") - // 写入清理后的文案 - htmlBuilder.WriteString(fmt.Sprintf(`需要配图: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(`?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`) 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(`