refactor: 重构工作流执行逻辑并提取单模型调用
This commit is contained in:
@@ -10,15 +10,19 @@ const (
|
||||
|
||||
// 节点名称
|
||||
const (
|
||||
NodeNameTextModel = "生成文案"
|
||||
NodeNameImageModel = "生成图片"
|
||||
NodeNameVideoModel = "视频"
|
||||
NodeNameAudioModel = "音频"
|
||||
NodeNameModel = "模型"
|
||||
NodeNameMerge = "结果合并"
|
||||
NodeNameJudge = "条件判断"
|
||||
NodeNameForm = "表单"
|
||||
NodeNameCustomNode = "自定义节点"
|
||||
NodeNameTextModel = "生成文案"
|
||||
NodeNameImageModel = "生成图片"
|
||||
NodeNameVideoModel = "生成视频"
|
||||
NodeNameSenseOptimize = "语义优化"
|
||||
NodeNameStoryOptimize = "分镜优化"
|
||||
NodeNameScriptOptimize = "剧本优化"
|
||||
NodeNameAudioModel = "音频"
|
||||
NodeNameModel = "模型"
|
||||
NodeNameMerge = "结果合并"
|
||||
NodeNameJudge = "条件判断"
|
||||
NodeNameForm = "表单"
|
||||
NodeNameHttp = "HTTP(S)接口"
|
||||
NodeNameCustomNode = "自定义节点"
|
||||
)
|
||||
|
||||
// 表单字段 Label
|
||||
@@ -42,10 +46,13 @@ type NodeType string
|
||||
|
||||
const (
|
||||
// 组件
|
||||
NodeTypeTextModel NodeType = "text_model"
|
||||
NodeTypeImageModel NodeType = "image_model"
|
||||
NodeTypeVideoModel NodeType = "video_model"
|
||||
NodeTypeAudioModel NodeType = "audio_model"
|
||||
NodeTypeTextModel NodeType = "text_model"
|
||||
NodeTypeImageModel NodeType = "image_model"
|
||||
NodeTypeVideoModel NodeType = "video_model"
|
||||
NodeTypeSenseOptimize NodeType = "sense_optimize"
|
||||
NodeTypeStoryOptimize NodeType = "story_optimize"
|
||||
NodeTypeScriptOptimize NodeType = "script_optimize"
|
||||
NodeTypeAudioModel NodeType = "audio_model"
|
||||
|
||||
// 基础
|
||||
NodeTypeModel NodeType = "model"
|
||||
@@ -53,7 +60,7 @@ const (
|
||||
NodeTypeJudge NodeType = "judge"
|
||||
NodeTypeForm NodeType = "form"
|
||||
NodeTypeIntent NodeType = "intent"
|
||||
|
||||
NodeTypeHttp NodeType = "http"
|
||||
// 自定义
|
||||
NodeTypeCustomNode NodeType = "custom_node"
|
||||
)
|
||||
@@ -102,91 +109,3 @@ type NodeGroupItem struct {
|
||||
Label string `json:"label"` // 从常量来
|
||||
Items []NodeItem `json:"items"`
|
||||
}
|
||||
|
||||
//
|
||||
//// 文案模型节点定义
|
||||
//func NewTextModelNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeTextModel,
|
||||
// NodeName: NodeNameTextModel,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 图片模型节点
|
||||
//func NewImageModelNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeImageModel,
|
||||
// NodeName: NodeNameImageModel,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 音频模型节点
|
||||
//func NewAudioModelNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeAudioModel,
|
||||
// NodeName: NodeNameAudioModel,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 视频模型节点
|
||||
//func NewVideoModelNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeVideoModel,
|
||||
// NodeName: NodeNameVideoModel,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 基础模型节点
|
||||
//func NewModelNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeModel,
|
||||
// NodeName: NodeNameModel,
|
||||
// FormConfig: []ModelItem{
|
||||
// {
|
||||
// ModelName: "模型名称",
|
||||
// ModelForm: []NodeFormField{
|
||||
// {Field: "apiKey", Label: FormLabelApiKey, Type: "input", Required: true},
|
||||
// {Field: "model", Label: FormLabelModel, Type: "input", Required: true},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 判断节点
|
||||
//func NewJudgeNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeJudge,
|
||||
// NodeName: NodeNameJudge,
|
||||
// FormConfig: []ModelItem{
|
||||
// {
|
||||
// ModelName: "判断条件",
|
||||
// ModelForm: []NodeFormField{
|
||||
// {Field: "condition", Label: FormLabelCondition, Type: "input", Required: true},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 表单参数节点
|
||||
//func NewFormNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeForm,
|
||||
// NodeName: NodeNameForm,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// 自定义节点
|
||||
//func NewCustomNode() NodeItem {
|
||||
// return NodeItem{
|
||||
// NodeCode: NodeTypeCustomNode,
|
||||
// NodeName: NodeNameCustomNode,
|
||||
// FormConfig: []ModelItem{},
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -5,20 +5,16 @@ import (
|
||||
"ai-agent/workflow/consts/node"
|
||||
fileDao "ai-agent/workflow/dao/file"
|
||||
flowDao "ai-agent/workflow/dao/flow"
|
||||
"ai-agent/workflow/model/dto"
|
||||
fileDto "ai-agent/workflow/model/dto/file"
|
||||
flowDto "ai-agent/workflow/model/dto/flow"
|
||||
"ai-agent/workflow/model/entity"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.com/red-future/common/utils"
|
||||
"github.com/cloudwego/eino/compose"
|
||||
@@ -131,7 +127,7 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx
|
||||
suffix = "图片"
|
||||
case strings.Contains(val, "html") || strings.Contains(val, "HTML"):
|
||||
suffix = "HTML"
|
||||
case strings.Contains(val, "txt") || len(val) > 50:
|
||||
case strings.Contains(val, "inc") || len(val) > 50:
|
||||
suffix = "文案"
|
||||
}
|
||||
|
||||
@@ -355,516 +351,127 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
|
||||
}
|
||||
}
|
||||
|
||||
if isDialogue && !g.IsEmpty(flowInfo) {
|
||||
// 查询节点中是否包含结果合并节点
|
||||
var htmlUrl []string
|
||||
var textNodeId string
|
||||
var textModelName string
|
||||
var textModelResponse map[string]any
|
||||
textResultFrom := make(map[string]any)
|
||||
var imgNodeId string
|
||||
var imgModelName string
|
||||
var imgModelResponse map[string]any
|
||||
imgResultFrom := make(map[string]any)
|
||||
for _, item := range flowInfo.NodeInputParams {
|
||||
if item.NodeCode == node.NodeTypeMerge {
|
||||
for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
outputParamsMap := gconv.Map(outputParamsItem)
|
||||
for _, mapItem := range outputParamsMap {
|
||||
if strings.HasSuffix(gconv.String(mapItem), ".html") {
|
||||
htmlUrl = append(htmlUrl, gconv.String(mapItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if item.NodeCode == node.NodeTypeTextModel {
|
||||
textNodeId = item.Id
|
||||
textModelName = item.ModelConfig.ModelName
|
||||
textModelResponse = item.ModelConfig.ModelResponse
|
||||
for key, modelFormItem := range item.ModelConfig.ModelForm {
|
||||
textResultFrom[key] = map[string]any{
|
||||
"value": modelFormItem,
|
||||
}
|
||||
}
|
||||
}
|
||||
if item.NodeCode == node.NodeTypeImageModel {
|
||||
imgNodeId = item.Id
|
||||
imgModelName = item.ModelConfig.ModelName
|
||||
imgModelResponse = item.ModelConfig.ModelResponse
|
||||
for key, modelFormItem := range item.ModelConfig.ModelForm {
|
||||
imgResultFrom[key] = map[string]any{
|
||||
"value": modelFormItem,
|
||||
}
|
||||
}
|
||||
}
|
||||
if isDialogue && !g.IsEmpty(flowInfo) && !g.IsEmpty(req.ResultUrl) {
|
||||
if strings.HasSuffix(gconv.String(req.ResultUrl), ".inc") {
|
||||
err = TextModelSingleLambda(ctx, req, flowInfo)
|
||||
return
|
||||
} else if strings.HasSuffix(gconv.String(req.ResultUrl), ".png") {
|
||||
err = ImgModelSingleLambda(ctx, req, flowInfo)
|
||||
return
|
||||
} else if strings.HasSuffix(gconv.String(req.ResultUrl), ".html") {
|
||||
err = TextImgModelSingleLambda(ctx, req, flowInfo)
|
||||
return
|
||||
}
|
||||
var url string
|
||||
url, err = utils.GetFileAddressPrefix(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.HasSuffix(gconv.String(req.ResultUrl), ".md") {
|
||||
resultUserFrom := make(map[string]any)
|
||||
resultUserFrom["desc"] = req.Desc
|
||||
return nil, errors.New("文件格式不支持")
|
||||
}
|
||||
|
||||
var textNode []node.NodeFormField
|
||||
textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, resultUserFrom, textModelResponse, req.FileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// =========================================================================
|
||||
// ✅【第1步】给所有判断节点自动生成意图识别节点
|
||||
// =========================================================================
|
||||
judge2IntentNodeMap := make(map[string]string)
|
||||
finalNodes := make([]entity.FlowNode, 0, len(req.FlowContent.Nodes)*2)
|
||||
for _, item := range req.FlowContent.Nodes {
|
||||
finalNodes = append(finalNodes, item)
|
||||
// 判断节点自动加 intent 节点
|
||||
if item.NodeCode == node.NodeTypeJudge {
|
||||
intentNodeID := fmt.Sprintf("intent_%s", item.Id)
|
||||
intentNode := entity.FlowNode{
|
||||
Id: intentNodeID,
|
||||
NodeCode: node.NodeTypeIntent,
|
||||
Name: fmt.Sprintf("意图识别-%s", item.Name),
|
||||
InputSource: item.InputSource, // ✅ 正确赋值
|
||||
FormConfig: item.FormConfig, // ✅ 用户配置
|
||||
ModelConfig: item.ModelConfig, // ✅ 系统配置
|
||||
}
|
||||
var textContent []string
|
||||
var textUrl []string
|
||||
for _, item := range textNode {
|
||||
if strings.Contains(item.Field, "text_content") {
|
||||
textContent = append(textContent, item.Value)
|
||||
}
|
||||
if strings.Contains(item.Field, "text_url") {
|
||||
textUrl = append(textUrl, item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
content := ""
|
||||
// 第二步 执行目标节点
|
||||
if content == "text" {
|
||||
resultUserFrom := make(map[string]any)
|
||||
resultUserFrom["desc"] = req.Desc
|
||||
|
||||
var textNode []node.NodeFormField
|
||||
textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, resultUserFrom, textModelResponse, req.FileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var htmlTags []string
|
||||
var textUrl []string
|
||||
for _, item := range textNode {
|
||||
if strings.Contains(item.Field, "text_content") {
|
||||
htmlTags = append(htmlTags, item.Value)
|
||||
}
|
||||
if strings.Contains(item.Field, "text_url") {
|
||||
textUrl = append(textUrl, item.Value)
|
||||
}
|
||||
}
|
||||
|
||||
var htmlContentUrl []string
|
||||
if !g.IsEmpty(htmlUrl) {
|
||||
for i, item := range htmlUrl {
|
||||
// 获取当前要替换的文本内容
|
||||
textContent := htmlTags[i]
|
||||
// 1. 读取 HTML 文件内容
|
||||
var htmlBytes []byte
|
||||
htmlBytes, err = os.ReadFile(url + item)
|
||||
if err != nil {
|
||||
fmt.Printf("读取文件失败 %s: %v", url+item, err)
|
||||
continue
|
||||
}
|
||||
htmlContent := string(htmlBytes)
|
||||
// 2. 构建要替换成的新 div 标签
|
||||
newTextTag := fmt.Sprintf(`<div class="text">%s</div>`, textContent)
|
||||
re := regexp.MustCompile(`<text>.*?</text>`)
|
||||
result := re.ReplaceAllString(htmlContent, newTextTag)
|
||||
|
||||
fmt.Printf("成功处理文件:%s", result)
|
||||
|
||||
// 上传OSS(每条独立上传)
|
||||
fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli())
|
||||
var ossResult *dto.UploadFileBytesRes
|
||||
ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileBytes: []byte(result),
|
||||
FileName: fileName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("上传OSS成功:%s", ossResult.FileURL)
|
||||
htmlContentUrl = append(htmlContentUrl, ossResult.FileURL)
|
||||
}
|
||||
}
|
||||
var summaryResult []map[string]interface{}
|
||||
if !g.IsEmpty(textUrl) {
|
||||
for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
if !g.IsEmpty(htmlContentUrl) {
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".html") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".txt") {
|
||||
continue
|
||||
}
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = outputParamsItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
for _, textItem := range textUrl {
|
||||
// 生成 毫秒时间戳 作为 KEY
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = textItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(htmlContentUrl) {
|
||||
for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
if !g.IsEmpty(textUrl) {
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".txt") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".html") {
|
||||
continue
|
||||
}
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = outputParamsItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
for _, textItem := range htmlContentUrl {
|
||||
// 生成 毫秒时间戳 作为 KEY
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = textItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
|
||||
}
|
||||
if !g.IsEmpty(summaryResult) {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: flowInfo.Id,
|
||||
Status: flow.FlowExecutionStatusSuccess.Code(),
|
||||
OutputParams: summaryResult,
|
||||
}
|
||||
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
}
|
||||
} else if content == "img" {
|
||||
resultUserFrom := make(map[string]any)
|
||||
resultUserFrom["desc"] = req.Desc
|
||||
|
||||
var imgNode []node.NodeFormField
|
||||
imgNode, err = ImgNode(ctx, imgNodeId, req.SessionId, imgModelName, req.SkillName, imgResultFrom, resultUserFrom, imgModelResponse, req.FileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var imgCount int
|
||||
var imgUrl []string
|
||||
var htmlBuilder strings.Builder
|
||||
htmlBuilder.WriteString(`<div class="image-group">`)
|
||||
for _, item := range imgNode {
|
||||
if strings.Contains(item.Field, "img_url") {
|
||||
imgCount = imgCount + 1
|
||||
htmlBuilder.WriteString(fmt.Sprintf(`<img src="%s" alt="图片"/>`, item.Value))
|
||||
imgUrl = append(imgUrl, item.Value)
|
||||
}
|
||||
}
|
||||
htmlBuilder.WriteString(`</div>`)
|
||||
var htmlContentUrl []string
|
||||
if !g.IsEmpty(htmlUrl) && imgCount > 0 {
|
||||
for i, item := range htmlUrl {
|
||||
// 1. 读取 HTML 文件内容
|
||||
var htmlBytes []byte
|
||||
htmlBytes, err = os.ReadFile(url + item)
|
||||
if err != nil {
|
||||
fmt.Printf("读取文件失败 %s: %v", url+item, err)
|
||||
continue
|
||||
}
|
||||
htmlContent := string(htmlBytes)
|
||||
|
||||
re := regexp.MustCompile(`<div class="image-group">[\s\S]*?</div>`)
|
||||
result := re.ReplaceAllString(htmlContent, htmlBuilder.String())
|
||||
|
||||
fmt.Printf("成功处理文件:%s", result)
|
||||
|
||||
// 上传OSS(每条独立上传)
|
||||
fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli())
|
||||
var ossResult *dto.UploadFileBytesRes
|
||||
ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileBytes: []byte(result),
|
||||
FileName: fileName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("上传OSS成功:%s", ossResult.FileURL)
|
||||
htmlContentUrl = append(htmlContentUrl, ossResult.FileURL)
|
||||
}
|
||||
}
|
||||
var summaryResult []map[string]interface{}
|
||||
if !g.IsEmpty(imgCount) {
|
||||
for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
if !g.IsEmpty(htmlContentUrl) {
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".html") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".png") {
|
||||
continue
|
||||
}
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = outputParamsItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
for _, textItem := range imgUrl {
|
||||
// 生成 毫秒时间戳 作为 KEY
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = textItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(htmlContentUrl) {
|
||||
for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
if !g.IsEmpty(imgUrl) {
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".png") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(gconv.String(outputParamsItem), ".html") {
|
||||
continue
|
||||
}
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = outputParamsItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
for _, textItem := range htmlContentUrl {
|
||||
// 生成 毫秒时间戳 作为 KEY
|
||||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
item := make(map[string]interface{})
|
||||
item[timeKey] = textItem
|
||||
summaryResult = append(summaryResult, item)
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(summaryResult) {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: flowInfo.Id,
|
||||
Status: flow.FlowExecutionStatusSuccess.Code(),
|
||||
OutputParams: summaryResult,
|
||||
}
|
||||
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
}
|
||||
} else if content == "text_img" {
|
||||
//userFrom := make(map[string]any)
|
||||
//userFrom["desc"] = req.Desc
|
||||
//
|
||||
//var textNode []node.NodeFormField
|
||||
//textNode, err = TextNode(ctx, textNodeId, req.SessionId, textModelName, req.SkillName, textResultFrom, userFrom, textModelResponse, req.FileUrl)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//
|
||||
//var htmlTags []string
|
||||
//var textUrl []string
|
||||
//for _, item := range textNode {
|
||||
// if strings.Contains(item.Field, "text_content") {
|
||||
// htmlTags = append(htmlTags, StripHtmlTags(item.Value, false))
|
||||
// }
|
||||
// if strings.Contains(item.Field, "text_url") {
|
||||
// textUrl = append(textUrl, item.Value)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//userFrom["prompt"] = htmlTags
|
||||
//var imgNode []node.NodeFormField
|
||||
//imgNode, err = ImgNode(ctx, imgNodeId, req.SessionId, imgModelName, req.SkillName, imgResultFrom, userFrom, imgModelResponse, req.FileUrl)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//var imgCount int
|
||||
//var imgUrl []string
|
||||
//var htmlBuilder strings.Builder
|
||||
//htmlBuilder.WriteString(`<div class="image-group">`)
|
||||
//for _, item := range imgNode {
|
||||
// if strings.Contains(item.Field, "img_url") {
|
||||
// imgCount = imgCount + 1
|
||||
// htmlBuilder.WriteString(fmt.Sprintf(`<img src="%s" alt="图片"/>`, item.Value))
|
||||
// imgUrl = append(imgUrl, item.Value)
|
||||
// }
|
||||
//}
|
||||
//htmlBuilder.WriteString(`</div>`)
|
||||
//
|
||||
//var htmlContentUrl []string
|
||||
//if !g.IsEmpty(htmlUrl) && imgCount > 0 {
|
||||
// for i, item := range htmlUrl {
|
||||
// // 获取当前要替换的文本内容
|
||||
// textContent := htmlTags[i]
|
||||
// // 1. 读取 HTML 文件内容
|
||||
// var htmlBytes []byte
|
||||
// htmlBytes, err = os.ReadFile(url + item)
|
||||
// if err != nil {
|
||||
// fmt.Printf("读取文件失败 %s: %v", url+item, err)
|
||||
// continue
|
||||
// }
|
||||
// htmlContent := string(htmlBytes)
|
||||
//
|
||||
// re := regexp.MustCompile(`<div class="image-group">[\s\S]*?</div>`)
|
||||
// result := re.ReplaceAllString(htmlContent, htmlBuilder.String())
|
||||
// // 2. 构建要替换成的新 div 标签
|
||||
// newTextTag := fmt.Sprintf(`<div class="text">%s</div>`, textContent)
|
||||
// ret := regexp.MustCompile(`<text>.*?</text>`)
|
||||
// result = ret.ReplaceAllString(htmlContent, newTextTag)
|
||||
// fmt.Printf("成功处理文件:%s", result)
|
||||
//
|
||||
// // 上传OSS(每条独立上传)
|
||||
// fileName := fmt.Sprintf("item_%d_%d.html", i, time.Now().UnixMilli())
|
||||
// var ossResult *dto.UploadFileBytesRes
|
||||
// ossResult, err = Upload(ctx, &dto.UploadFileBytesReq{
|
||||
// FileBytes: []byte(result),
|
||||
// FileName: fileName,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// fmt.Printf("上传OSS成功:%s", ossResult.FileURL)
|
||||
// htmlContentUrl = append(htmlContentUrl, ossResult.FileURL)
|
||||
// }
|
||||
//}
|
||||
//if !g.IsEmpty(htmlContentUrl) {
|
||||
// for _, outputParamsItem := range flowInfo.OutputParams {
|
||||
// if !g.IsEmpty(imgUrl) {
|
||||
// if strings.HasSuffix(gconv.String(outputParamsItem), ".png") {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// if strings.HasSuffix(gconv.String(outputParamsItem), ".html") {
|
||||
// continue
|
||||
// }
|
||||
// timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
// item := make(map[string]interface{})
|
||||
// item[timeKey] = outputParamsItem
|
||||
// summaryResult = append(summaryResult, item)
|
||||
// }
|
||||
// for _, textItem := range htmlContentUrl {
|
||||
// // 生成 毫秒时间戳 作为 KEY
|
||||
// timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
// item := make(map[string]interface{})
|
||||
// item[timeKey] = textItem
|
||||
// summaryResult = append(summaryResult, item)
|
||||
// }
|
||||
//}
|
||||
//if !g.IsEmpty(summaryResult) {
|
||||
// executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
// Id: flowInfo.Id,
|
||||
// Status: flow.FlowExecutionStatusSuccess.Code(),
|
||||
// OutputParams: summaryResult,
|
||||
// }
|
||||
// _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
//}
|
||||
} else {
|
||||
return nil, fmt.Errorf("意图识别失败")
|
||||
}
|
||||
} else {
|
||||
// =========================================================================
|
||||
// ✅【第1步】给所有判断节点自动生成意图识别节点
|
||||
// =========================================================================
|
||||
judge2IntentNodeMap := make(map[string]string)
|
||||
finalNodes := make([]entity.FlowNode, 0, len(req.FlowContent.Nodes)*2)
|
||||
for _, item := range req.FlowContent.Nodes {
|
||||
finalNodes = append(finalNodes, item)
|
||||
// 判断节点自动加 intent 节点
|
||||
if item.NodeCode == node.NodeTypeJudge {
|
||||
intentNodeID := fmt.Sprintf("intent_%s", item.Id)
|
||||
intentNode := entity.FlowNode{
|
||||
Id: intentNodeID,
|
||||
NodeCode: node.NodeTypeIntent,
|
||||
Name: fmt.Sprintf("意图识别-%s", item.Name),
|
||||
InputSource: item.InputSource, // ✅ 正确赋值
|
||||
FormConfig: item.FormConfig, // ✅ 用户配置
|
||||
ModelConfig: item.ModelConfig, // ✅ 系统配置
|
||||
}
|
||||
finalNodes = append(finalNodes, intentNode)
|
||||
judge2IntentNodeMap[item.Id] = intentNodeID
|
||||
}
|
||||
}
|
||||
|
||||
summaryNodeID := "summary_node"
|
||||
summaryNode := entity.FlowNode{
|
||||
Id: summaryNodeID,
|
||||
NodeCode: node.NodeTypeCustomNode, // 复用自定义节点类型,也可新增专属类型
|
||||
Name: "结果汇总节点",
|
||||
InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出
|
||||
FormConfig: nil,
|
||||
ModelConfig: node.ModelItem{},
|
||||
}
|
||||
finalNodes = append(finalNodes, summaryNode)
|
||||
|
||||
// 替换节点列表
|
||||
req.FlowContent.Nodes = finalNodes
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第2步】构建执行图
|
||||
// =========================================================================
|
||||
var runGraph compose.Runnable[any, any]
|
||||
runGraph, err = BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID)
|
||||
if err != nil {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusFailed.Code(),
|
||||
ErrorMessage: err.Error(),
|
||||
}
|
||||
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
return nil, fmt.Errorf("执行工作流失败: %v", err)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第3步】构建 ConfigMap
|
||||
// =========================================================================
|
||||
configMap := make(map[string]*entity.FlowNode)
|
||||
for _, cfg := range req.NodeInputParams {
|
||||
configMap[cfg.Id] = cfg
|
||||
}
|
||||
// 自动给意图节点复制配置
|
||||
for judgeID, intentID := range judge2IntentNodeMap {
|
||||
if cfg, ok := configMap[judgeID]; ok {
|
||||
configMap[intentID] = cfg
|
||||
}
|
||||
}
|
||||
// 初始化汇总节点配置
|
||||
configMap[summaryNodeID] = &summaryNode
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!)
|
||||
// =========================================================================
|
||||
execInput := &flowDto.FlowExecutionInput{
|
||||
IsDialogue: isDialogue,
|
||||
ExecutionId: executionId,
|
||||
ConfigMap: configMap,
|
||||
SessionId: req.SessionId,
|
||||
Desc: req.Desc,
|
||||
SkillName: req.SkillName,
|
||||
FileUrl: req.FileUrl,
|
||||
}
|
||||
// 执行工作流
|
||||
_, err = runGraph.Invoke(execCtx, execInput)
|
||||
if err != nil {
|
||||
// 检测是否是取消导致的错误
|
||||
if errors.Is(execCtx.Err(), context.Canceled) {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusCancel.Code(),
|
||||
}
|
||||
_, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
return nil, fmt.Errorf("工作流已被取消: %v", err)
|
||||
}
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusFailed.Code(),
|
||||
ErrorMessage: err.Error(),
|
||||
}
|
||||
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
return nil, fmt.Errorf("执行工作流失败: %v", err)
|
||||
finalNodes = append(finalNodes, intentNode)
|
||||
judge2IntentNodeMap[item.Id] = intentNodeID
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
summaryNodeID := "summary_node"
|
||||
summaryNode := entity.FlowNode{
|
||||
Id: summaryNodeID,
|
||||
NodeCode: node.NodeTypeCustomNode, // 复用自定义节点类型,也可新增专属类型
|
||||
Name: "结果汇总节点",
|
||||
InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出
|
||||
FormConfig: nil,
|
||||
ModelConfig: node.ModelItem{},
|
||||
}
|
||||
finalNodes = append(finalNodes, summaryNode)
|
||||
|
||||
// 替换节点列表
|
||||
req.FlowContent.Nodes = finalNodes
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第2步】构建执行图
|
||||
// =========================================================================
|
||||
var runGraph compose.Runnable[any, any]
|
||||
runGraph, err = BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID)
|
||||
if err != nil {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusFailed.Code(),
|
||||
ErrorMessage: err.Error(),
|
||||
}
|
||||
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
return nil, fmt.Errorf("执行工作流失败: %v", err)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第3步】构建 ConfigMap
|
||||
// =========================================================================
|
||||
configMap := make(map[string]*entity.FlowNode)
|
||||
for _, cfg := range req.NodeInputParams {
|
||||
configMap[cfg.Id] = cfg
|
||||
}
|
||||
// 自动给意图节点复制配置
|
||||
for judgeID, intentID := range judge2IntentNodeMap {
|
||||
if cfg, ok := configMap[judgeID]; ok {
|
||||
configMap[intentID] = cfg
|
||||
}
|
||||
}
|
||||
// 初始化汇总节点配置
|
||||
configMap[summaryNodeID] = &summaryNode
|
||||
|
||||
// =========================================================================
|
||||
// ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!)
|
||||
// =========================================================================
|
||||
execInput := &flowDto.FlowExecutionInput{
|
||||
IsDialogue: isDialogue,
|
||||
ExecutionId: executionId,
|
||||
ConfigMap: configMap,
|
||||
SessionId: req.SessionId,
|
||||
Desc: req.Desc,
|
||||
SkillName: req.SkillName,
|
||||
FileUrl: req.FileUrl,
|
||||
}
|
||||
// 执行工作流
|
||||
_, err = runGraph.Invoke(execCtx, execInput)
|
||||
if err != nil {
|
||||
// 检测是否是取消导致的错误
|
||||
if errors.Is(execCtx.Err(), context.Canceled) {
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusCancel.Code(),
|
||||
}
|
||||
_, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
return nil, fmt.Errorf("工作流已被取消: %v", err)
|
||||
}
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: executionId,
|
||||
Status: flow.FlowExecutionStatusFailed.Code(),
|
||||
ErrorMessage: err.Error(),
|
||||
}
|
||||
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
if err1 != nil {
|
||||
return
|
||||
}
|
||||
return nil, fmt.Errorf("执行工作流失败: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BuildGraphFromFlowContent 根据前端保存的工作流JSON,自动构建执行图
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"ai-agent/workflow/model/dto"
|
||||
fileDto "ai-agent/workflow/model/dto/file"
|
||||
flowDto "ai-agent/workflow/model/dto/flow"
|
||||
"ai-agent/workflow/model/entity"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
@@ -23,91 +22,6 @@ import (
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, node *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(node.InputSource) > 0 {
|
||||
for _, source := range node.InputSource {
|
||||
refNodeID := source.NodeId
|
||||
isQuoteOutput := source.QuoteOutput
|
||||
fields := source.Field
|
||||
|
||||
refNode, ok := execInput.ConfigMap[refNodeID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
inputMap := buildInputMap(refNode)
|
||||
outputMap := mergeOutput(refNode.OutputResult)
|
||||
modelMap := mergeModel(refNode.ModelConfig)
|
||||
if isQuoteOutput {
|
||||
for k, v := range outputMap {
|
||||
output[k] = v
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 取全部
|
||||
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
|
||||
// 合并成你需要的 { key: { value: xxx } } 结构
|
||||
func mergeModel(output node.ModelItem) map[string]any {
|
||||
m := make(map[string]any)
|
||||
|
||||
// 遍历 output.ModelForm 里的每一个 key 和原始值
|
||||
for key, rawValue := range output.ModelForm {
|
||||
// 包装成 { "value": 原始值 }
|
||||
m[key] = map[string]any{
|
||||
"value": rawValue,
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func StartLambda(ctx context.Context, input any) (any, error) {
|
||||
return input, nil
|
||||
}
|
||||
@@ -117,9 +31,14 @@ func FormLambda(ctx context.Context, input any) (any, error) {
|
||||
}
|
||||
|
||||
func IntentLambda(ctx context.Context, input any) (any, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// JudgeLambda 分支判断核心:读取IntentLambda的输出 → 返回目标节点ID做路由
|
||||
func JudgeLambda(ctx context.Context, input any) (string, error) {
|
||||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input)
|
||||
return "", fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input)
|
||||
}
|
||||
// 1. 直接用你原来的方法(返回两个 map)
|
||||
inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
|
||||
@@ -131,9 +50,7 @@ func IntentLambda(ctx context.Context, input any) (any, error) {
|
||||
}
|
||||
for _, valueAny := range outputMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") {
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
}
|
||||
for _, valueAny := range modelMap {
|
||||
@@ -142,30 +59,12 @@ func IntentLambda(ctx context.Context, input any) (any, error) {
|
||||
}
|
||||
}
|
||||
|
||||
nodeInput.Config.OutputResult = outputResult
|
||||
|
||||
return nodeInput, nil
|
||||
}
|
||||
|
||||
// JudgeLambda 分支判断核心:读取IntentLambda的输出 → 返回目标节点ID做路由
|
||||
func JudgeLambda(ctx context.Context, input any) (string, error) {
|
||||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input)
|
||||
}
|
||||
|
||||
out := new([]node.NodeFormField)
|
||||
err := gconv.Structs(nodeInput.Config.OutputResult, out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contextParts := ""
|
||||
for _, v := range nodeInput.Config.FormConfig {
|
||||
contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value)
|
||||
}
|
||||
if !nodeInput.Global.IsDialogue {
|
||||
for _, v := range *out {
|
||||
for _, v := range outputResult {
|
||||
contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value)
|
||||
}
|
||||
}
|
||||
@@ -188,7 +87,6 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req := flowDto.ComposeMessagesReq{
|
||||
BuildType: 2,
|
||||
ModelName: getIsChatModel.ModelName,
|
||||
@@ -223,133 +121,12 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("入参类型错误")
|
||||
}
|
||||
|
||||
// 1. 直接用你原来的方法(返回两个 map)
|
||||
inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
|
||||
var outputResult []node.NodeFormField
|
||||
for _, valueAny := range inputMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
}
|
||||
|
||||
resultUserFrom := make(map[string]any)
|
||||
|
||||
for _, valueAny := range outputMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") {
|
||||
if strings.Contains(field.Field, "text_content") {
|
||||
field.Value = StripHtmlTags(field.Value, false)
|
||||
}
|
||||
resultUserFrom[field.Label] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, valueAny := range modelMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
}
|
||||
if !nodeInput.Global.IsDialogue {
|
||||
for _, item := range outputResult {
|
||||
resultUserFrom[item.Label] = item
|
||||
}
|
||||
for _, item := range nodeInput.Config.FormConfig {
|
||||
resultUserFrom[item.Label] = item
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(nodeInput.Global.Desc) {
|
||||
resultUserFrom["desc"] = node.NodeFormField{
|
||||
Value: nodeInput.Global.Desc,
|
||||
Field: "desc",
|
||||
Label: "描述",
|
||||
Type: "text",
|
||||
}
|
||||
}
|
||||
resultFrom := make(map[string]any)
|
||||
for key, item := range nodeInput.Config.ModelConfig.ModelForm {
|
||||
resultFrom[key] = map[string]any{
|
||||
"value": item,
|
||||
}
|
||||
}
|
||||
var skillName = nodeInput.Config.SkillName
|
||||
if g.IsEmpty(nodeInput.Config.SkillName) {
|
||||
skillName = nodeInput.Global.SkillName
|
||||
}
|
||||
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 结构,不输出任何额外文字"
|
||||
resultUserFrom["prompt"] = contentStr
|
||||
|
||||
req := flowDto.ComposeMessagesReq{
|
||||
BuildType: 1,
|
||||
ModelName: nodeInput.Config.ModelConfig.ModelName,
|
||||
SkillName: skillName,
|
||||
Cause: "文案节点",
|
||||
Form: resultFrom,
|
||||
UserForm: resultUserFrom,
|
||||
UserFiles: nodeInput.Global.FileUrl,
|
||||
SessionId: nodeInput.Global.SessionId,
|
||||
}
|
||||
|
||||
msg, err := ComposeMessages(ctx, &req)
|
||||
skillName, from, userFrom := BuildParam(nodeInput)
|
||||
outputRes, err := TextNode(ctx, nodeInput.Global.SessionId, nodeInput.Config.ModelConfig.ModelName, skillName, from, userFrom, nodeInput.Config.ModelConfig.ModelResponse, nodeInput.Global.FileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.IsEmpty(msg.Messages) {
|
||||
return nil, fmt.Errorf("msg is empty")
|
||||
}
|
||||
taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err := GetTaskResult(ctx, taskResult)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mapTaskResult := gconv.Map(result.Text)
|
||||
|
||||
resultContent := ""
|
||||
for key, _ := range nodeInput.Config.ModelConfig.ModelResponse {
|
||||
resultContent = gconv.String(mapTaskResult[key])
|
||||
}
|
||||
|
||||
// 拆分多条文案
|
||||
contentList := SplitMultiContents(resultContent)
|
||||
|
||||
outputRes := make([]node.NodeFormField, 0)
|
||||
for i, content := range contentList {
|
||||
// 文案内容:content_0, content_1, content_2...
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("text_content_%d", i),
|
||||
Value: content,
|
||||
Label: fmt.Sprintf("文案内容_%d", i),
|
||||
Type: "string",
|
||||
Expand: ExtractImageCount(content),
|
||||
})
|
||||
|
||||
// 1. 去掉 HTML 标签,生成纯文本
|
||||
//plainText := StripHtmlTags(content, true)
|
||||
plainText := BuildText(content)
|
||||
// 2. 上传纯文本到 OSS
|
||||
textFileName := fmt.Sprintf("ai_text_%d_%d.inc", time.Now().UnixMilli(), i)
|
||||
textUrl, err := Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileBytes: []byte(plainText),
|
||||
FileName: textFileName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 3. 把纯文本地址存入输出
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("text_url_%d", i),
|
||||
Value: textUrl.FileURL,
|
||||
Label: fmt.Sprintf("文案纯文本_txt_%d", i),
|
||||
Type: "string",
|
||||
Expand: ExtractImageCount(content),
|
||||
})
|
||||
}
|
||||
nodeInput.Config.OutputResult = outputRes
|
||||
|
||||
return nodeInput, nil
|
||||
}
|
||||
|
||||
@@ -359,145 +136,13 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("入参类型错误")
|
||||
}
|
||||
// 1. 直接用你原来的方法(返回两个 map)
|
||||
inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
|
||||
var outputResult []node.NodeFormField
|
||||
for _, valueAny := range inputMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
}
|
||||
|
||||
resultUserFrom := make(map[string]any)
|
||||
|
||||
for _, valueAny := range outputMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") {
|
||||
if strings.Contains(field.Field, "text_content") {
|
||||
field.Value = StripHtmlTags(field.Value, false)
|
||||
}
|
||||
resultUserFrom[field.Label] = field
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, valueAny := range modelMap {
|
||||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||||
outputResult = append(outputResult, field)
|
||||
}
|
||||
}
|
||||
|
||||
if !nodeInput.Global.IsDialogue {
|
||||
for _, item := range outputResult {
|
||||
resultUserFrom[item.Label] = item
|
||||
}
|
||||
for _, item := range nodeInput.Config.FormConfig {
|
||||
resultUserFrom[item.Label] = item
|
||||
}
|
||||
}
|
||||
if !g.IsEmpty(nodeInput.Global.Desc) {
|
||||
resultUserFrom["desc"] = node.NodeFormField{
|
||||
Value: nodeInput.Global.Desc,
|
||||
Field: "desc",
|
||||
Label: "描述",
|
||||
Type: "text",
|
||||
}
|
||||
}
|
||||
resultFrom := make(map[string]any)
|
||||
for key, item := range nodeInput.Config.ModelConfig.ModelForm {
|
||||
resultFrom[key] = map[string]any{
|
||||
"value": item,
|
||||
}
|
||||
}
|
||||
var skillName = nodeInput.Config.SkillName
|
||||
if g.IsEmpty(nodeInput.Config.SkillName) {
|
||||
skillName = nodeInput.Global.SkillName
|
||||
}
|
||||
|
||||
req := flowDto.ComposeMessagesReq{
|
||||
BuildType: 1,
|
||||
ModelName: nodeInput.Config.ModelConfig.ModelName,
|
||||
SkillName: skillName,
|
||||
Cause: "图片节点",
|
||||
Form: resultFrom,
|
||||
UserForm: resultUserFrom,
|
||||
UserFiles: nodeInput.Global.FileUrl,
|
||||
SessionId: nodeInput.Global.SessionId,
|
||||
}
|
||||
msg, err := ComposeMessages(ctx, &req)
|
||||
skillName, from, userFrom := BuildParam(nodeInput)
|
||||
outputRes, err := ImgNode(ctx, nodeInput.Global.SessionId, nodeInput.Config.ModelConfig.ModelName, skillName, from, userFrom, nodeInput.Config.ModelConfig.ModelResponse, nodeInput.Global.FileUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.IsEmpty(msg.Messages) {
|
||||
return nil, fmt.Errorf("msg is empty")
|
||||
}
|
||||
taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, err := GetTaskResult(ctx, taskResult)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mapTaskResult := gconv.Map(result.Text)
|
||||
|
||||
imgs := []string{}
|
||||
for key, _ := range nodeInput.Config.ModelConfig.ModelResponse {
|
||||
imgs = gconv.Strings(mapTaskResult[key])
|
||||
}
|
||||
|
||||
var images []string
|
||||
for _, item := range imgs {
|
||||
mapItem := gconv.Map(item)
|
||||
for _, value := range mapItem {
|
||||
values, imgOk := value.(string)
|
||||
if !imgOk {
|
||||
return nil, fmt.Errorf("图片地址类型错误")
|
||||
}
|
||||
// 下载官方临时图片
|
||||
imgBytes, _, err := GetImageBytesFromURL(values)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载图片失败: %w", err)
|
||||
}
|
||||
// 构造文件名
|
||||
fileName := fmt.Sprintf("ai_image_%d.png", time.Now().UnixMilli())
|
||||
// 上传到你的OSS(你项目已有的Upload方法)
|
||||
upResp, err := Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileName: fileName,
|
||||
FileBytes: imgBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("上传OSS失败: %w", err)
|
||||
}
|
||||
images = append(images, upResp.FileURL)
|
||||
}
|
||||
}
|
||||
|
||||
url, err := utils.GetFileAddressPrefix(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputRes := make([]node.NodeFormField, 0)
|
||||
|
||||
for i, item := range images {
|
||||
// 图片:image_0, image_1, image_2...
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("image_%d", i),
|
||||
Value: fmt.Sprintf("%s%s", url, item),
|
||||
Label: fmt.Sprintf("图片_%d", i),
|
||||
Type: "string",
|
||||
})
|
||||
// 额外存储关联关系
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("%v:img_url:%d", nodeInput.Config.Id, i),
|
||||
Value: fmt.Sprintf("%s%s", url, item),
|
||||
Label: fmt.Sprintf("图片_img_%d关联文案ID", i),
|
||||
Type: "string",
|
||||
})
|
||||
}
|
||||
nodeInput.Config.OutputResult = outputRes
|
||||
return input, nil
|
||||
return nodeInput, nil
|
||||
}
|
||||
|
||||
func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
@@ -529,7 +174,7 @@ func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
// 3. 提取所有图片:image_0,1,2...
|
||||
var images []string
|
||||
for i := 0; ; i++ {
|
||||
key := fmt.Sprintf("image_%d", i)
|
||||
key := fmt.Sprintf("img_url%d", i)
|
||||
val, has := dataMap[key]
|
||||
if !has || val.Value == "" {
|
||||
break
|
||||
@@ -579,12 +224,16 @@ func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
// 🔥 把现有数据转换成通用 Item 列表(支持:纯文案、纯图片、图文任意组合)
|
||||
var allItems []Item
|
||||
|
||||
url, err := utils.GetFileAddressPrefix(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 情况1:有文案 → 按文案条目生成 Item(每条文案+对应图片)
|
||||
if len(contents) > 0 {
|
||||
for i, val := range contents {
|
||||
item := Item{
|
||||
Content: val.Value, // 文案
|
||||
Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片)
|
||||
Content: url + val.Value, // 文案
|
||||
Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片)
|
||||
}
|
||||
allItems = append(allItems, item)
|
||||
}
|
||||
@@ -637,13 +286,13 @@ func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
Type: "text",
|
||||
},
|
||||
node.NodeFormField{
|
||||
Field: fmt.Sprintf("item_text_%d", idx),
|
||||
Field: fmt.Sprintf("item_txt_url_%d", idx),
|
||||
Value: item.Content,
|
||||
Label: fmt.Sprintf("条目%d 文案", idx+1),
|
||||
Type: "text",
|
||||
},
|
||||
node.NodeFormField{
|
||||
Field: fmt.Sprintf("item_images_%d", idx),
|
||||
Field: fmt.Sprintf("item_image_url_%d", idx),
|
||||
Value: strings.Join(item.Images, ","),
|
||||
Label: fmt.Sprintf("条目%d 图片", idx+1),
|
||||
Type: "text",
|
||||
@@ -744,171 +393,3 @@ func CustomLambda(ctx context.Context, input any) (any, error) {
|
||||
fmt.Println("CustomLambda:", input)
|
||||
return input, nil
|
||||
}
|
||||
|
||||
func TextNode(ctx context.Context, nodeId, sessionId, modelName, skillName string, from, userFrom, modelResponse map[string]any, fileUrl []string) ([]node.NodeFormField, error) {
|
||||
contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:1、输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释,2、整体用 <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 结构,不输出任何额外文字"
|
||||
userFrom["prompt"] = contentStr
|
||||
|
||||
textMsgReq := flowDto.ComposeMessagesReq{
|
||||
BuildType: 1,
|
||||
ModelName: modelName,
|
||||
SkillName: skillName,
|
||||
Cause: "文案节点",
|
||||
Form: from,
|
||||
UserForm: userFrom,
|
||||
UserFiles: fileUrl,
|
||||
SessionId: sessionId,
|
||||
}
|
||||
msg, err := ComposeMessages(ctx, &textMsgReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.IsEmpty(msg.Messages) {
|
||||
return nil, fmt.Errorf("msg is empty")
|
||||
}
|
||||
|
||||
var taskResult any
|
||||
taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var getTaskResult *flowDto.TaskCallback
|
||||
getTaskResult, err = GetTaskResult(ctx, taskResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapTaskResult := gconv.Map(getTaskResult.Text)
|
||||
|
||||
resultContent := ""
|
||||
for key, _ := range modelResponse {
|
||||
resultContent = gconv.String(mapTaskResult[key])
|
||||
}
|
||||
|
||||
// 拆分多条文案
|
||||
contentList := SplitMultiContents(resultContent)
|
||||
|
||||
outputRes := make([]node.NodeFormField, 0)
|
||||
for i, contentItem := range contentList {
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("text_content_%d", i),
|
||||
Value: contentItem,
|
||||
Label: fmt.Sprintf("文案内容_%d", i),
|
||||
Type: "string",
|
||||
Expand: ExtractImageCount(contentItem),
|
||||
})
|
||||
|
||||
// 1. 去掉 HTML 标签,生成纯文本
|
||||
//plainText := StripHtmlTags(contentItem, true)
|
||||
plainText := BuildHtml(contentItem, nil)
|
||||
// 2. 上传纯文本到 OSS
|
||||
textFileName := fmt.Sprintf("ai_text_%d_%d.md", time.Now().UnixMilli(), i)
|
||||
var textUrl *dto.UploadFileBytesRes
|
||||
textUrl, err = Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileBytes: []byte(plainText),
|
||||
FileName: textFileName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 3. 把纯文本地址存入输出
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("%v:text_url:%d", nodeId, i),
|
||||
Value: textUrl.FileURL,
|
||||
Label: fmt.Sprintf("文案纯文本_txt_%d", i),
|
||||
Type: "string",
|
||||
Expand: ExtractImageCount(contentItem),
|
||||
})
|
||||
}
|
||||
return outputRes, nil
|
||||
}
|
||||
|
||||
func ImgNode(ctx context.Context, nodeId, sessionId, modelName, skillName string, form, userForm, modelResponse map[string]any, fileUrl []string) ([]node.NodeFormField, error) {
|
||||
imgMsgReq := flowDto.ComposeMessagesReq{
|
||||
BuildType: 1,
|
||||
ModelName: modelName,
|
||||
SkillName: skillName,
|
||||
Cause: "图片节点",
|
||||
Form: form,
|
||||
UserForm: userForm,
|
||||
UserFiles: fileUrl,
|
||||
SessionId: sessionId,
|
||||
}
|
||||
msg, err := ComposeMessages(ctx, &imgMsgReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.IsEmpty(msg.Messages) {
|
||||
return nil, fmt.Errorf("msg is empty")
|
||||
}
|
||||
var taskResult any
|
||||
taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var getTaskResult *flowDto.TaskCallback
|
||||
getTaskResult, err = GetTaskResult(ctx, taskResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapTaskResult := gconv.Map(getTaskResult.Text)
|
||||
|
||||
var resultContent []string
|
||||
for key, _ := range modelResponse {
|
||||
resultContent = gconv.Strings(mapTaskResult[key])
|
||||
}
|
||||
|
||||
var images []string
|
||||
for _, item := range resultContent {
|
||||
mapItem := gconv.Map(item)
|
||||
for _, value := range mapItem {
|
||||
values, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("图片地址类型错误")
|
||||
}
|
||||
// 下载官方临时图片
|
||||
var imgBytes []byte
|
||||
imgBytes, _, err = GetImageBytesFromURL(values)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载图片失败: %w", err)
|
||||
}
|
||||
// 构造文件名
|
||||
fileName := fmt.Sprintf("ai_image_%d.png", time.Now().UnixMilli())
|
||||
// 上传到你的OSS(你项目已有的Upload方法)
|
||||
var upResp *dto.UploadFileBytesRes
|
||||
upResp, err = Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileName: fileName,
|
||||
FileBytes: imgBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("上传OSS失败: %w", err)
|
||||
}
|
||||
images = append(images, upResp.FileURL)
|
||||
}
|
||||
}
|
||||
|
||||
var url string
|
||||
url, err = utils.GetFileAddressPrefix(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputRes := make([]node.NodeFormField, 0)
|
||||
|
||||
for i, item := range images {
|
||||
// 图片:image_0, image_1, image_2...
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("image_%d", i),
|
||||
Value: fmt.Sprintf("%s%s", url, item),
|
||||
Label: fmt.Sprintf("图片_%d", i),
|
||||
Type: "string",
|
||||
})
|
||||
// 额外存储关联关系
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("%v:img_url:%d", nodeId, i),
|
||||
Value: fmt.Sprintf("%s%s", url, item),
|
||||
Label: fmt.Sprintf("图片_img_%d关联文案ID", i),
|
||||
Type: "string",
|
||||
})
|
||||
}
|
||||
|
||||
return outputRes, nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,66 @@ func GetIsChatModel(ctx context.Context) (res *flowDto.GetIsChatModelRes, err er
|
||||
return
|
||||
}
|
||||
|
||||
func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (res *flowDto.ComposeMessagesRes, err error) {
|
||||
headers := make(map[string]string)
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Request.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
res = new(flowDto.ComposeMessagesRes)
|
||||
err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req)
|
||||
return
|
||||
}
|
||||
|
||||
func GetModelResult(ctx context.Context, modelName, skillName string, form, userFrom map[string]any, fileUrl []string, sessionId string, cause string) (mapTaskResult map[string]any, err error) {
|
||||
msgReq := flowDto.ComposeMessagesReq{
|
||||
BuildType: 1,
|
||||
ModelName: modelName,
|
||||
SkillName: skillName,
|
||||
Cause: cause,
|
||||
Form: form,
|
||||
UserForm: userFrom,
|
||||
UserFiles: fileUrl,
|
||||
SessionId: sessionId,
|
||||
}
|
||||
msg, err := ComposeMessages(ctx, &msgReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if g.IsEmpty(msg.Messages) {
|
||||
return nil, fmt.Errorf("msg is empty")
|
||||
}
|
||||
var taskResult any
|
||||
taskResult, err = GatewayTask(ctx, msg.EpicycleId, modelName, msg.Messages)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var getTaskResult *flowDto.TaskCallback
|
||||
getTaskResult, err = GetTaskResult(ctx, taskResult)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mapTaskResult = gconv.Map(getTaskResult.Text)
|
||||
return mapTaskResult, nil
|
||||
}
|
||||
|
||||
func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) {
|
||||
modelTaskId, err := CreateGatewayTask(ctx, &flowDto.CreateTaskReq{
|
||||
ModelName: model,
|
||||
BizName: g.Cfg().MustGet(ctx, "server.name").String(),
|
||||
CallbackUrl: "/flow/execution/modelCallback",
|
||||
RequestPayload: content,
|
||||
EpicycleId: epicycleId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Wait(ctx, modelTaskId)
|
||||
}
|
||||
|
||||
func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, error) {
|
||||
headers := make(map[string]string)
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
@@ -51,34 +111,6 @@ func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string,
|
||||
return res.TaskId, nil
|
||||
}
|
||||
|
||||
func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (res *flowDto.ComposeMessagesRes, err error) {
|
||||
headers := make(map[string]string)
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Request.Header {
|
||||
if len(v) > 0 {
|
||||
headers[k] = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
res = new(flowDto.ComposeMessagesRes)
|
||||
err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req)
|
||||
return
|
||||
}
|
||||
|
||||
func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) {
|
||||
modelTaskId, err := CreateGatewayTask(ctx, &flowDto.CreateTaskReq{
|
||||
ModelName: model,
|
||||
BizName: g.Cfg().MustGet(ctx, "server.name").String(),
|
||||
CallbackUrl: "/flow/execution/modelCallback",
|
||||
RequestPayload: content,
|
||||
EpicycleId: epicycleId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Wait(ctx, modelTaskId)
|
||||
}
|
||||
|
||||
func GetTaskResult(ctx context.Context, result any) (*flowDto.TaskCallback, error) {
|
||||
task := new(flowDto.TaskCallback)
|
||||
if err := gconv.Struct(result, task); err != nil {
|
||||
@@ -116,17 +148,22 @@ func FetchRemoteJsonFile(ctx context.Context, fileUrl string) ([]byte, error) {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func GetImageBytesFromURL(url string) (all []byte, contentType string, err error) {
|
||||
func GetFileBytesFromURL(url string) (all []byte, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败 %s: %v", url, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
all, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("请求失败,状态码: %d\n", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
all, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("读取内容失败 %s: %v", url, err)
|
||||
return
|
||||
}
|
||||
contentType = resp.Header.Get("Content-Type")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,24 +234,24 @@ func BuildText(text string) string {
|
||||
.item {
|
||||
padding: 30px;
|
||||
}
|
||||
.image-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.image-group img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.image-group img:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.image-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.text {
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
line-height: 1.4;
|
||||
color: #555;
|
||||
}
|
||||
.text h2 {
|
||||
@@ -222,7 +259,7 @@ func BuildText(text string) string {
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.text h3 {
|
||||
font-size: 20px;
|
||||
@@ -233,7 +270,7 @@ func BuildText(text string) string {
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
.text p {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 12px;
|
||||
text-align: justify;
|
||||
}
|
||||
.text strong {
|
||||
@@ -243,12 +280,12 @@ func BuildText(text string) string {
|
||||
.text ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 15px 0;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.text ul li {
|
||||
padding: 10px 0 10px 30px;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.text ul li:before {
|
||||
content: "●";
|
||||
@@ -262,9 +299,6 @@ func BuildText(text string) string {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
.item {
|
||||
padding: 20px;
|
||||
}
|
||||
.text h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -278,27 +312,11 @@ func BuildText(text string) string {
|
||||
<div class="container">
|
||||
<div class="item">
|
||||
`)
|
||||
|
||||
//// 写入图片(支持0张、1张、多张)
|
||||
//if len(images) > 0 {
|
||||
// htmlBuilder.WriteString(`<div class="image-group">`)
|
||||
// for _, imgUrl := range images {
|
||||
// htmlBuilder.WriteString(fmt.Sprintf(`<img src="%s" alt="图片"/>`, imgUrl))
|
||||
// }
|
||||
// htmlBuilder.WriteString(`</div>`)
|
||||
//}
|
||||
|
||||
// 🔥 写入文案前:删除 <p class="image-count">需要配图:X 张</p>
|
||||
if text != "" {
|
||||
// 正则删除整行
|
||||
imageTagRegex := regexp.MustCompile(`<p class="image-count">[\s\S]*?</p>`)
|
||||
//re := regexp.MustCompile(`<p class="image-count">需要配图:\d+ 张</p>`)
|
||||
cleanContent := imageTagRegex.ReplaceAllString(text, "")
|
||||
|
||||
// 写入清理后的文案
|
||||
htmlBuilder.WriteString(fmt.Sprintf(`<div class="text">%s</div>`, cleanContent))
|
||||
htmlBuilder.WriteString(fmt.Sprintf(`<div class="text">%s</div>`, ImageTagRegex(text)))
|
||||
}
|
||||
|
||||
htmlBuilder.WriteString(`</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -336,9 +354,7 @@ func BuildHtml(text string, images []string) string {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
#content {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -352,7 +368,6 @@ func BuildHtml(text string, images []string) string {
|
||||
}
|
||||
htmlBuilder.WriteString(`</div>`)
|
||||
}
|
||||
|
||||
htmlBuilder.WriteString(`
|
||||
<div id="content">加载中...</div>
|
||||
</div>
|
||||
@@ -365,7 +380,7 @@ func BuildHtml(text string, images []string) string {
|
||||
return res.text();
|
||||
})
|
||||
.then(text => {
|
||||
document.getElementById("content").textContent = text;
|
||||
document.getElementById("content").innerHTML = text;
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById("content").innerHTML = "加载失败:" + err.message;
|
||||
@@ -377,25 +392,28 @@ func BuildHtml(text string, images []string) string {
|
||||
return htmlBuilder.String()
|
||||
}
|
||||
|
||||
// ExtractImageCount 从 HTML 内容里提取图片数量(例如从 <p class="image-count">需要配图:3 张</p> 拿到 3)
|
||||
// ExtractImageCount 修复:支持单引号/双引号 + 换行 + 空格
|
||||
func ExtractImageCount(content string) int {
|
||||
re := regexp.MustCompile(`<p class="image-count">[^\d]*(\d+)[^\d]*</p>`)
|
||||
// 🔥 关键:支持 class='image-count' (单引号)
|
||||
re := regexp.MustCompile(`<p class=['"]image-count['"][^>]*>.*?(\d+).*?</p>`)
|
||||
match := re.FindStringSubmatch(content)
|
||||
if len(match) >= 2 {
|
||||
num, _ := strconv.Atoi(match[1])
|
||||
return num
|
||||
num, err := strconv.Atoi(match[1])
|
||||
if err == nil {
|
||||
return num
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行
|
||||
func StripHtmlTags(html string, delImageCount bool) string {
|
||||
if delImageCount {
|
||||
// 🔥 第一步:直接删除整个 <p class="image-count">...</p> 标签(包含内容)
|
||||
imageTagRegex := regexp.MustCompile(`<p class="image-count">[\s\S]*?</p>`)
|
||||
html = imageTagRegex.ReplaceAllString(html, "")
|
||||
}
|
||||
func ImageTagRegex(html string) string {
|
||||
// 🔥 修复:支持单引号、双引号、空格、换行,100% 删除 <p class='image-count'>
|
||||
imageTagRegex := regexp.MustCompile(`<p class=['"]image-count['"][^>]*>[\s\S]*?</p>`)
|
||||
return imageTagRegex.ReplaceAllString(html, "")
|
||||
}
|
||||
|
||||
// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行
|
||||
func StripHtmlTags(html string) string {
|
||||
// 1. 替换块级标签为换行,保证排版
|
||||
blockTags := regexp.MustCompile(`</?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`)
|
||||
text := blockTags.ReplaceAllString(html, "\n")
|
||||
@@ -434,3 +452,23 @@ func SplitMultiContents(htmlContent string) []string {
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
// GetAllImgSrcFromHtml 先把提取img src的工具方法放在外面
|
||||
func GetAllImgSrcFromHtml(html string) []string {
|
||||
var imgSrcList []string
|
||||
re := regexp.MustCompile(`<img[^>]*src\s*=\s*["']([^"']+)["']`)
|
||||
matchs := re.FindAllStringSubmatch(html, -1)
|
||||
for _, match := range matchs {
|
||||
if len(match) >= 2 {
|
||||
imgSrcList = append(imgSrcList, match[1])
|
||||
}
|
||||
}
|
||||
return imgSrcList
|
||||
}
|
||||
|
||||
// ReplaceImgSrc 替换img src的方法
|
||||
func ReplaceImgSrc(html string, oldSrc string, newSrc string) string {
|
||||
// 精准替换:找到 <img xxx src="oldSrc" xxx> 并替换
|
||||
re := regexp.MustCompile(`(<img[^>]*src\s*=\s*["'])` + regexp.QuoteMeta(oldSrc) + `(["'])`)
|
||||
return re.ReplaceAllString(html, `${1}`+newSrc+`${2}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user