Files
ai-agent/workflow/service/flow/lambda_node_imp.go

1081 lines
34 KiB
Go
Raw Normal View History

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
2026-06-08 13:39:20 +08:00
textModelName = ""
textResultFrom = []map[string]any{}
imgPromptContent = ""
imgIsSaveFile = false
2026-06-08 13:39:20 +08:00
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、整体用 <div class='report-container'> 包裹3、主标题使用 <h2 class='title'>4、章节标题使用 <h3 class='section-title'>5、正文段落使用 <p class='paragraph'>6、列表使用 <ul class='list'><li>...</li></ul>7、重点内容使用 <strong> 加粗8、段落之间清晰分隔结构规整9、如果生成多条文案每条文案独立用 <div class='content-item' id='content-{序号}'> 包裹序号从1开始10、每条文案内部必须在最上方添加一行固定格式<p class='image-count'>需要配图N 张</p> 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
}