package flow import ( "ai-agent/workflow/consts/flow" "ai-agent/workflow/consts/node" flowDao "ai-agent/workflow/dao/flow" nodeDao "ai-agent/workflow/dao/node" "ai-agent/workflow/model/dto" flowDto "ai-agent/workflow/model/dto/flow" nodeDto "ai-agent/workflow/model/dto/node" "ai-agent/workflow/model/entity" "context" "fmt" "regexp" "strconv" "strings" "time" commonHttp "gitea.com/red-future/common/http" "gitea.com/red-future/common/utils" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/util/gconv" "github.com/google/uuid" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) func getNodeInfo(flowInfo *entity.FlowExecution) (htmlUrl []string, textIsSaveFile bool, textPromptContent, textModelName string, textResultFrom []map[string]any, imgIsSaveFile bool, imgPromptContent, imgModelName string, imgResultFrom []map[string]any) { textPromptContent = "" textIsSaveFile = false textModelName = "" textResultFrom = []map[string]any{} imgPromptContent = "" imgIsSaveFile = false imgModelName = "" imgResultFrom = []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 { textPromptContent = item.PromptContent textIsSaveFile = item.IsSaveFile textModelName = item.ModelConfig.ModelName for key, modelFormItem := range item.ModelConfig.ModelForm { textResultFrom[key] = map[string]any{ "value": modelFormItem, } } } if item.NodeCode == node.NodeTypeImageModel { imgPromptContent = item.PromptContent imgIsSaveFile = item.IsSaveFile imgModelName = item.ModelConfig.ModelName for key, modelFormItem := range item.ModelConfig.ModelForm { imgResultFrom[key] = map[string]any{ "value": modelFormItem, } } } } return htmlUrl, textIsSaveFile, textPromptContent, textModelName, textResultFrom, imgIsSaveFile, imgPromptContent, imgModelName, imgResultFrom } func TextImgModelSingleLambda(ctx context.Context, req *flowDto.ExecuteReq, flowInfo *entity.FlowExecution) (err error) { textStartTime := time.Now() _, textIsSaveFile, textPromptContent, textModelName, textResultFrom, imgIsSaveFile, imgPromptContent, imgModelName, imgResultFrom := getNodeInfo(flowInfo) resultUserFrom := []map[string]any{ { "desc": req.Desc, }, } var textNode []node.NodeFormField textNodeInput := new(flowDto.NodeExecutionInput) textNodeInput.Global.SessionId = req.SessionId textNodeInput.Global.NodeGroupId = req.NodeGroupId textNodeInput.Global.Desc = req.Desc textNodeInput.Global.FileUrl = req.FileUrl textNodeInput.Config.IsSaveFile = textIsSaveFile textNodeInput.Config.PromptContent = textPromptContent textNodeInput.Config.ModelConfig.ModelName = textModelName var textNodeExecutionId int64 textNodeExecutionId, err = nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ FlowExecutionId: textNodeInput.Global.ExecutionId, NodeId: textNodeInput.Config.Id, NodeName: textNodeInput.Config.Name, NodeGroupId: textNodeInput.Global.NodeGroupId, InputParams: textNodeInput, Status: node.NodeExecutionStatusRunning.Code(), }) if err != nil { return } textNode, err = TextNode(ctx, textNodeInput, req.SkillName, textResultFrom, resultUserFrom) textUpdateReq := &nodeDto.UpdateNodeExecutionReq{ Id: textNodeExecutionId, InputParams: textNodeInput, } if err != nil { textUpdateReq.Status = node.NodeExecutionStatusFailed.Code() textUpdateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, textUpdateReq) return } textUpdateReq.DurationMs = time.Since(textStartTime).Milliseconds() textUpdateReq.Status = node.NodeExecutionStatusSuccess.Code() _, err = nodeDao.NodeExecutionDao.Update(ctx, textUpdateReq) var textContent string var textUrl string for _, item := range textNode { if strings.Contains(item.Field, "text_url") { textUrl = gconv.String(item.Value) } } imgStartTime := time.Now() resultUserFrom = append(resultUserFrom, map[string]any{ "text_content": textContent, }) var imgNode []node.NodeFormField imgNodeInput := new(flowDto.NodeExecutionInput) imgNodeInput.Global.SessionId = req.SessionId imgNodeInput.Global.NodeGroupId = req.NodeGroupId imgNodeInput.Global.Desc = req.Desc imgNodeInput.Global.FileUrl = req.FileUrl imgNodeInput.Config.IsSaveFile = imgIsSaveFile imgNodeInput.Config.PromptContent = imgPromptContent imgNodeInput.Config.ModelConfig.ModelName = imgModelName var imgNodeExecutionId int64 imgNodeExecutionId, err = nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ FlowExecutionId: imgNodeInput.Global.ExecutionId, NodeId: imgNodeInput.Config.Id, NodeName: imgNodeInput.Config.Name, NodeGroupId: imgNodeInput.Global.NodeGroupId, InputParams: imgNodeInput, Status: node.NodeExecutionStatusRunning.Code(), }) if err != nil { return } imgNode, err = ImgNode(ctx, imgNodeInput, req.SkillName, imgResultFrom, resultUserFrom) imgUpdateReq := &nodeDto.UpdateNodeExecutionReq{ Id: imgNodeExecutionId, InputParams: imgNodeInput, } if err != nil { imgUpdateReq.Status = node.NodeExecutionStatusFailed.Code() imgUpdateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, imgUpdateReq) return } var imgUrl []string for _, item := range imgNode { if strings.Contains(item.Field, "img_url") { imgUrl = append(imgUrl, gconv.String(item.Value)) } } // 生成单条HTML htmlContent := BuildHtml(textUrl, imgUrl) // 上传OSS(每条独立上传) fileName := fmt.Sprintf("item_%d_%d.html", 0, time.Now().UnixMilli()) var ossResult *dto.UploadFileBytesRes ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ FileBytes: []byte(htmlContent), FileName: fileName, }) if err != nil { imgUpdateReq.Status = node.NodeExecutionStatusFailed.Code() imgUpdateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, imgUpdateReq) return } fmt.Printf("上传OSS成功:%s", ossResult.FileURL) var summaryResult []map[string]interface{} for _, outputParamsItem := range flowInfo.OutputParams { mapItem := gconv.Map(outputParamsItem) for _, mapValue := range mapItem { if strings.Contains(req.ResultUrl, gconv.String(mapValue)) { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = ossResult.FileURL summaryResult = append(summaryResult, item) continue } summaryResult = append(summaryResult, outputParamsItem) } } if !g.IsEmpty(summaryResult) { executionReq := flowDto.UpdateFlowExecutionReq{ Id: flowInfo.Id, Status: flow.FlowExecutionStatusSuccess.Code(), OutputParams: summaryResult, } _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) imgUpdateReq.DurationMs = time.Since(imgStartTime).Milliseconds() imgUpdateReq.Status = node.NodeExecutionStatusSuccess.Code() _, err = nodeDao.NodeExecutionDao.Update(ctx, imgUpdateReq) } return } func ImgModelSingleLambda(ctx context.Context, req *flowDto.ExecuteReq, flowInfo *entity.FlowExecution) (err error) { startTime := time.Now() var url string url, err = utils.GetFileAddressPrefix(ctx) if err != nil { return } htmlUrl, _, _, _, _, imgIsSaveFile, imgPromptContent, imgModelName, imgResultFrom := getNodeInfo(flowInfo) resultUserFrom := []map[string]any{ { "desc": req.Desc, }, } var imgNode []node.NodeFormField imgNodeInput := new(flowDto.NodeExecutionInput) imgNodeInput.Global.SessionId = req.SessionId imgNodeInput.Global.NodeGroupId = req.NodeGroupId imgNodeInput.Global.Desc = req.Desc imgNodeInput.Global.FileUrl = req.FileUrl imgNodeInput.Config.IsSaveFile = imgIsSaveFile imgNodeInput.Config.PromptContent = imgPromptContent imgNodeInput.Config.ModelConfig.ModelName = imgModelName var nodeExecutionId int64 nodeExecutionId, err = nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ FlowExecutionId: imgNodeInput.Global.ExecutionId, NodeId: imgNodeInput.Config.Id, NodeName: imgNodeInput.Config.Name, NodeGroupId: imgNodeInput.Global.NodeGroupId, InputParams: imgNodeInput, Status: node.NodeExecutionStatusRunning.Code(), }) if err != nil { return } imgNode, err = ImgNode(ctx, imgNodeInput, req.SkillName, imgResultFrom, resultUserFrom) updateReq := &nodeDto.UpdateNodeExecutionReq{ Id: nodeExecutionId, InputParams: imgNodeInput, } if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } var imgUrl string for _, item := range imgNode { if strings.Contains(item.Field, "img_url") { imgUrl = gconv.String(item.Value) } } var htmlContentUrl string var oldHtmlUrl string if !g.IsEmpty(htmlUrl) { for i, item := range htmlUrl { var htmlBytes []byte htmlBytes, err = GetFileBytesFromURL(ctx, url+item) if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } htmlContent := string(htmlBytes) imgSrcFromHtml := GetAllImgSrcFromHtml(htmlContent) // 3. 标记是否需要替换 needReplace := false for _, imgSrc := range imgSrcFromHtml { if imgSrc == req.ResultUrl { needReplace = true break // 找到一个就可以替换 } } // 4. 如果匹配到,执行替换(把旧的 req.ResultUrl 替换成 新链接) if needReplace { oldHtmlUrl = url + item htmlContent = ReplaceImgSrc(htmlContent, req.ResultUrl, imgUrl) // 上传OSS(每条独立上传) fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli()) var ossResult *dto.UploadFileBytesRes ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ FileBytes: []byte(htmlContent), FileName: fileName, }) if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } fmt.Printf("上传OSS成功:%s", ossResult.FileURL) htmlContentUrl = ossResult.FileURL } } } var summaryResult []map[string]interface{} if !g.IsEmpty(imgUrl) { for _, outputParamsItem := range flowInfo.OutputParams { mapItem := gconv.Map(outputParamsItem) for _, mapValue := range mapItem { if strings.Contains(oldHtmlUrl, gconv.String(mapValue)) || strings.Contains(req.ResultUrl, gconv.String(mapValue)) { if strings.Contains(oldHtmlUrl, gconv.String(mapValue)) { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = htmlContentUrl summaryResult = append(summaryResult, item) } if strings.Contains(req.ResultUrl, gconv.String(mapValue)) { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = imgUrl summaryResult = append(summaryResult, item) } continue } summaryResult = append(summaryResult, outputParamsItem) } } } if !g.IsEmpty(summaryResult) { executionReq := flowDto.UpdateFlowExecutionReq{ Id: flowInfo.Id, Status: flow.FlowExecutionStatusSuccess.Code(), OutputParams: summaryResult, } _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) updateReq.DurationMs = time.Since(startTime).Milliseconds() updateReq.Status = node.NodeExecutionStatusSuccess.Code() _, err = nodeDao.NodeExecutionDao.Update(ctx, updateReq) } return } func TextModelSingleLambda(ctx context.Context, req *flowDto.ExecuteReq, flowInfo *entity.FlowExecution) (err error) { startTime := time.Now() var url string url, err = utils.GetFileAddressPrefix(ctx) if err != nil { return } htmlUrl, textIsSaveFile, textPromptContent, textModelName, textResultFrom, _, _, _, _ := getNodeInfo(flowInfo) resultUserFrom := []map[string]any{ { "desc": req.Desc, }, } var textNode []node.NodeFormField nodeInput := new(flowDto.NodeExecutionInput) nodeInput.Global.SessionId = req.SessionId nodeInput.Global.NodeGroupId = req.NodeGroupId nodeInput.Global.Desc = req.Desc nodeInput.Global.FileUrl = req.FileUrl nodeInput.Config.IsSaveFile = textIsSaveFile nodeInput.Config.PromptContent = textPromptContent nodeInput.Config.ModelConfig.ModelName = textModelName var nodeExecutionId int64 nodeExecutionId, err = nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ FlowExecutionId: nodeInput.Global.ExecutionId, NodeId: nodeInput.Config.Id, NodeName: nodeInput.Config.Name, NodeGroupId: nodeInput.Global.NodeGroupId, InputParams: nodeInput, Status: node.NodeExecutionStatusRunning.Code(), }) if err != nil { return } textNode, err = TextNode(ctx, nodeInput, req.SkillName, textResultFrom, resultUserFrom) updateReq := &nodeDto.UpdateNodeExecutionReq{ Id: nodeExecutionId, InputParams: nodeInput, } if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } var textUrl string for _, item := range textNode { if strings.Contains(item.Field, "text_url") { textUrl = gconv.String(item.Value) } } var htmlContentUrl string var oldHtmlUrl string if !g.IsEmpty(htmlUrl) { for i, item := range htmlUrl { var htmlBytes []byte htmlBytes, err = GetFileBytesFromURL(ctx, url+item) if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } htmlContent := string(htmlBytes) // 1) 匹配出 incUrl 的值 incRegex := regexp.MustCompile(`incUrl\s*=\s*"([^"]+)"`) match := incRegex.FindStringSubmatch(htmlContent) // 2) 获取模板里原来的 incUrl oldIncUrl := "" if len(match) >= 2 { oldIncUrl = match[1] // 这是模板里的旧链接 } // 3) 对比:不一样才替换 if oldIncUrl == req.ResultUrl { oldHtmlUrl = url + item // 替换成新的链接 htmlContent = incRegex.ReplaceAllString(htmlContent, fmt.Sprintf(`incUrl = "%s"`, url+textUrl)) // 上传OSS(每条独立上传) fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli()) var ossResult *dto.UploadFileBytesRes ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{ FileBytes: []byte(htmlContent), FileName: fileName, }) if err != nil { updateReq.Status = node.NodeExecutionStatusFailed.Code() updateReq.ErrorMessage = err.Error() _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) return } fmt.Printf("上传OSS成功:%s", ossResult.FileURL) htmlContentUrl = ossResult.FileURL } } } var summaryResult []map[string]interface{} if !g.IsEmpty(textUrl) { for _, outputParamsItem := range flowInfo.OutputParams { mapItem := gconv.Map(outputParamsItem) for _, mapValue := range mapItem { if strings.Contains(oldHtmlUrl, gconv.String(mapValue)) || strings.Contains(req.ResultUrl, gconv.String(mapValue)) { if strings.Contains(oldHtmlUrl, gconv.String(mapValue)) { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = htmlContentUrl summaryResult = append(summaryResult, item) } if strings.Contains(req.ResultUrl, gconv.String(mapValue)) { // 生成 毫秒时间戳 作为 KEY timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) item := make(map[string]interface{}) item[timeKey] = textUrl summaryResult = append(summaryResult, item) } continue } summaryResult = append(summaryResult, outputParamsItem) } } } if !g.IsEmpty(summaryResult) { executionReq := flowDto.UpdateFlowExecutionReq{ Id: flowInfo.Id, Status: flow.FlowExecutionStatusSuccess.Code(), OutputParams: summaryResult, } _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) updateReq.DurationMs = time.Since(startTime).Milliseconds() updateReq.Status = node.NodeExecutionStatusSuccess.Code() _, err = nodeDao.NodeExecutionDao.Update(ctx, updateReq) } return } func TextNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { //contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:1、输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释,2、整体用
,6、列表使用
需要配图:N 张
N 是这条文案需要的图片数量,只能是数字,不能是其他文字,11、只输出 HTML 结构,不输出任何额外文字" mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) if err != nil { return nil, err } if g.IsEmpty(mapTaskResult) { return nil, fmt.Errorf("生成内容为空") } outputRes := make([]node.NodeFormField, 0) for _, item := range mapTaskResult { for k, v := range item { // 拆分多条文案 contentList := SplitMultiContents(gconv.String(v)) for i, contentItem := range contentList { if nodeInput.Config.IsSaveFile { // 1. 构建html文本 plainText := BuildText(contentItem) // 2. 上传纯文本到 OSS textFileName := fmt.Sprintf("ai_text_%d_%d.inc", 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("text_url:%v:%d", k, i), Value: textUrl.FileURL, Label: fmt.Sprintf("text_url:%v:%d", k, i), Type: "string", Expand: ExtractImageCount(contentItem), }) } outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("text_content:%v:%d", k, i), Value: contentItem, Label: fmt.Sprintf("文案内容%v:%d", k, i), Type: "string", Expand: ExtractImageCount(gconv.String(v)), }) } } } return outputRes, nil } func ImgNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) if err != nil { return nil, err } if g.IsEmpty(mapTaskResult) { return nil, fmt.Errorf("生成内容为空") } outputRes := make([]node.NodeFormField, 0) for i, item := range mapTaskResult { for k, v := range item { if nodeInput.Config.IsSaveFile { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("img_oss_url:%v:%d", k, i), Value: v, Label: fmt.Sprintf("img_oss_url%v:%d", k, i), Type: "string", }) } outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("img_url:%v:%d", k, i), Value: v, Label: fmt.Sprintf("img_url%v:%d", k, i), Type: "string", }) } } //var resultContent []string //for _, item := range mapTaskResult { // for _, i := range gconv.Strings(item[modelInfo.Model.ResponseBody]) { // resultContent = append(resultContent, i) // } //} //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 = GetFileBytesFromURL(ctx, 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 { // // 额外存储关联关系 // outputRes = append(outputRes, node.NodeFormField{ // Field: fmt.Sprintf("img_url:%d", i), // Value: fmt.Sprintf("%s%s", url, item), // Label: fmt.Sprintf("图片路径:%d", i), // Type: "string", // }) //} return outputRes, nil } func AudioOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { mapTaskResult, err := GetModelResult(ctx, "", nodeInput, skillName, form, userForm) if err != nil { return nil, err } if g.IsEmpty(mapTaskResult) { return nil, fmt.Errorf("生成内容为空") } outputRes := make([]node.NodeFormField, 0) for i, item := range mapTaskResult { for k, v := range item { if nodeInput.Config.IsSaveFile { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("audio_oss_url:%v:%d", k, i), Value: v, Label: fmt.Sprintf("audio_oss_url:%v:%d", k, i), Type: "string", }) } if k == "sentences" { a := new([]flowDto.Sentence) err = gconv.Structs(v, a) v, err = BuildSubtitles(a) if err != nil { return nil, err } } outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("audio_url:%v:%d", k, i), Value: v, Label: fmt.Sprintf("audio_url:%v:%d", k, i), Type: "string", }) } } return outputRes, nil } func splitTextByPunct(raw string) []string { // 按标点切分+拼接标点 slice := regexp.MustCompile(`([,。;!?])`).Split(raw, -1) var res []string var builder strings.Builder for idx, s := range slice { if s == "" { continue } builder.WriteString(s) // 偶数位是分隔标点(split后规律:文本、标点、文本、标点...) if idx%2 == 1 { res = append(res, builder.String()) builder.Reset() } } if builder.Len() > 0 { res = append(res, builder.String()) } return res } // BuildSubtitles 核心工具:单个sentence生成多条subtitle func BuildSubtitles(sents *[]flowDto.Sentence) ([]flowDto.Subtitle, error) { var subtitles []flowDto.Subtitle for _, sent := range *sents { segList := splitTextByPunct(sent.Text) if len(segList) == 0 { return nil, nil } var subs []flowDto.Subtitle wordIdx := 0 allWords := sent.Words for _, seg := range segList { var collectWords []flowDto.Word currentText := "" // 循环取 word,直到拼接内容 包含/匹配 seg for { if wordIdx >= len(allWords) { break } word := allWords[wordIdx] currentText += word.Word collectWords = append(collectWords, word) wordIdx++ // 只要包含分段文本,就认为匹配(无视末尾标点差异) if strings.Contains(currentText, seg) { break } } if len(collectWords) == 0 { continue } // 生成字幕 sub := flowDto.Subtitle{ Start: collectWords[0].StartTime, End: collectWords[len(collectWords)-1].EndTime, Text: seg, } subs = append(subs, sub) } subtitles = append(subtitles, subs...) } return subtitles, nil } func VideoOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) if err != nil { return nil, err } if g.IsEmpty(mapTaskResult) { return nil, fmt.Errorf("生成内容为空") } outputRes := make([]node.NodeFormField, 0) for i, item := range mapTaskResult { for k, v := range item { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("video_url:%v:%d", k, i), Value: v, Label: fmt.Sprintf("video_url:%v:%d", k, i), Type: "string", }) } } return outputRes, nil } func DataConversionNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { jsonStr := `` jsonVal := "输出字段规范:" for _, field := range nodeInput.Config.OutputConfig { jsonStr, _ = sjson.Set(jsonStr, field.Field, "") jsonVal += fmt.Sprintf("%s:%s;", field.Field, field.Value) } jsonVal += fmt.Sprintf("输出模板结构,仅修改每个字段对应数值:%s", jsonStr) nodeInput.Config.PromptContent = fmt.Sprintf("%s;%s", nodeInput.Config.PromptContent, jsonVal) mapTaskResult, err := GetModelResult(ctx, "", nodeInput, skillName, form, userForm) if err != nil { return nil, err } if g.IsEmpty(mapTaskResult) { return nil, fmt.Errorf("生成内容为空") } outputRes := make([]node.NodeFormField, 0) for i, item := range mapTaskResult { for k, v := range item { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("data_conversion:%v:%d", k, i), Value: v, Label: fmt.Sprintf("data_conversion:%v:%d", k, i), Type: "string", }) } } return outputRes, nil } func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]node.NodeFormField, error) { var method, url, responseType, callbackUrl string var headers map[string]string var body map[string]any var responseMapping map[string]any for _, item := range nodeInput.Config.FormConfig { switch item.Field { case "method": method = gconv.String(item.Value) case "url": url = gconv.String(item.Value) case "headers": headers = gconv.MapStrStr(item.Value) case "body": body = gconv.Map(item.Value) case "response": responseMapping = gconv.Map(item.Value) case "responseType": responseType = gconv.String(item.Value) case "callbackUrl": callbackUrl = gconv.String(item.Value) } } if method == "" { return nil, fmt.Errorf("method为空") } if url == "" { return nil, fmt.Errorf("url为空") } if headers == nil { 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] } } } } // 构建请求参数 newBody := BuildNestedJson(body, nodeInput.Global.ConfigMap) // 1. 自己生成唯一 taskId(不用前端给) taskId := "my_task_" + uuid.New().String() // 自己生成唯一ID if responseType == "callback" { newBody[callbackUrl] = utils.GetCallbackURL(ctx, "/httpNodeCallback?task_id="+taskId) } // ====================== 核心改动 ====================== // 1. 定义一个空map接收原始HTTP返回结果 var rawHttpResult map[string]any // 2. 发送请求(不变) var err error if method == "GET" { err = commonHttp.Get(ctx, url, headers, &rawHttpResult, newBody) } else if method == "POST" { err = commonHttp.Post(ctx, url, headers, &rawHttpResult, newBody) } else if method == "PUT" { err = commonHttp.Put(ctx, url, headers, &rawHttpResult, newBody) } else if method == "DELETE" { err = commonHttp.Delete(ctx, url, headers, &rawHttpResult, newBody) } else { return nil, fmt.Errorf("method 不支持") } if err != nil { return nil, err } finalResult := make(map[string]any) if responseType == "sync" { httpResultJson := gconv.String(rawHttpResult) for key, jsonPath := range responseMapping { path := gconv.String(jsonPath) if !g.IsEmpty(gjson.Get(httpResultJson, path).Value()) { finalResult[key] = gjson.Get(httpResultJson, path).Value() } } } if responseType == "callback" { var waitResult any waitResult, err = Wait(ctx, taskId) if err != nil { return nil, err } request, ok := waitResult.(*ghttp.Request) if !ok { return nil, fmt.Errorf("入参类型错误") } bodyStr := request.GetBodyString() for key, jsonPath := range responseMapping { path := gconv.String(jsonPath) val := gjson.Get(bodyStr, path) // 如果是数组,直接返回整个数组 if val.IsArray() { finalResult[key] = val.Value() } else { // 普通值,非空才赋值 if !g.IsEmpty(val.Value()) { finalResult[key] = val.Value() } } } } if responseType == "pull" { } outputRes := make([]node.NodeFormField, 0) for i, item := range finalResult { if nodeInput.Config.IsSaveFile { outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("http_file_url:%v", i), Value: item, Label: fmt.Sprintf("http_file_url:%v", i), Type: "string", }) } outputRes = append(outputRes, node.NodeFormField{ Field: fmt.Sprintf("%v", i), Value: item, Label: fmt.Sprintf("%v", i), Type: "string", }) } return outputRes, nil } func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, resultFrom []map[string]any, resultUserFrom []map[string]any) { 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 = []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(gconv.String(field.Value)) } resultUserFrom = append(resultUserFrom, map[string]any{ field.Label: field.Value, }) } } } for _, valueAny := range modelMap { if field, ok := valueAny.(node.NodeFormField); ok { outputResult = append(outputResult, field) } } //if !nodeInput.Global.IsDialogue { for _, item := range outputResult { resultUserFrom = append(resultUserFrom, map[string]any{ item.Label: item.Value, }) } for _, item := range nodeInput.Config.FormConfig { resultUserFrom = append(resultUserFrom, map[string]any{ item.Label: item.Value, }) } //} if !g.IsEmpty(nodeInput.Global.Desc) { resultUserFrom = append(resultUserFrom, map[string]any{ "desc": nodeInput.Global.Desc, }) } resultFrom = []map[string]any{} for _, item := range nodeInput.Config.ModelConfig.ModelForm { if g.IsEmpty(item.Value) { continue } resultFrom = append(resultFrom, map[string]any{ item.Label: item.Value, }) } skillName = nodeInput.Config.SkillName if g.IsEmpty(nodeInput.Config.SkillName) { skillName = nodeInput.Global.SkillName } return skillName, resultFrom, resultUserFrom } func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *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(nodeEntity.InputSource) > 0 { for _, source := range nodeEntity.InputSource { refNodeID := source.NodeId fields := source.Field refNode, ok := execInput.ConfigMap[refNodeID] if !ok { continue } inputMap := buildInputMap(refNode) outputMap := mergeOutput(refNode.OutputResult) modelMap := mergeModel(refNode.ModelConfig) 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 } for k, v := range outputMap { if strings.Contains(k, f) { model[k] = v } } } } else { // 取全部 if refNode.NodeCode != node.NodeTypeHttp { 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 func mergeModel(output node.ModelItem) map[string]any { m := make(map[string]any) // 遍历 output.ModelForm 里的每一个 key 和原始值 for _, rawValue := range output.ModelForm { if g.IsEmpty(rawValue.Value) { continue } // 包装成 { "value": 原始值 } m[rawValue.Label] = rawValue.Value } return m }