- 新增 ActivePull 实体、DAO、DTO 及 Service,支持主动拉取任务管理 - 新增 ComposeCallback、VideoCallback、HttpNodeCallback 多类型回调接口 - FlowExecution 增加 NodeGroupId 和 TotalTokens 字段,支持节点组追踪与 Token 统计 - ExecutedNodes 结构由字符串列表改为包含执行状态的节点对象列表 - 重构回调通知机制,统一 Notify 函数调用 - 优化输出项类型判断逻辑,新增文件类型标识
1081 lines
34 KiB
Go
1081 lines
34 KiB
Go
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
|
||
}
|