Files
ai-agent/workflow/service/flow/lambda_node_imp.go
qhd 03c95c3601 feat: 新增主动拉取与多类型回调功能
- 新增 ActivePull 实体、DAO、DTO 及 Service,支持主动拉取任务管理
- 新增 ComposeCallback、VideoCallback、HttpNodeCallback 多类型回调接口
- FlowExecution 增加 NodeGroupId 和 TotalTokens 字段,支持节点组追踪与 Token 统计
- ExecutedNodes 结构由字符串列表改为包含执行状态的节点对象列表
- 重构回调通知机制,统一 Notify 函数调用
- 优化输出项类型判断逻辑,新增文件类型标识
2026-06-10 14:23:55 +08:00

1081 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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、整体用 <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
}