915 lines
27 KiB
Go
915 lines
27 KiB
Go
package flow
|
||
|
||
import (
|
||
"ai-agent/workflow/consts/flow"
|
||
"ai-agent/workflow/consts/node"
|
||
"ai-agent/workflow/consts/public"
|
||
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"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gitea.com/red-future/common/db/gfdb"
|
||
"gitea.com/red-future/common/utils"
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"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
|
||
}
|
||
|
||
func FormLambda(ctx context.Context, input any) (any, error) {
|
||
return input, nil
|
||
}
|
||
|
||
func IntentLambda(ctx context.Context, input any) (any, error) {
|
||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||
if !ok {
|
||
return nil, fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input)
|
||
}
|
||
// 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)
|
||
}
|
||
}
|
||
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)
|
||
}
|
||
}
|
||
}
|
||
for _, valueAny := range modelMap {
|
||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||
outputResult = append(outputResult, field)
|
||
}
|
||
}
|
||
|
||
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 {
|
||
contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value)
|
||
}
|
||
}
|
||
|
||
if !g.IsEmpty(nodeInput.Global.Desc) {
|
||
contextParts = fmt.Sprintf("%s,%s:%s", contextParts, "描述", nodeInput.Global.Desc)
|
||
}
|
||
configMap := gconv.Map(nodeInput.Config.Config)
|
||
ids := gconv.Strings(configMap["branch_ids"])
|
||
branchIdNameMap := gconv.Map(configMap["branch_id_name_map"])
|
||
|
||
// 【重构】构建提示词:展示ID和对应的名称
|
||
var branchIdNameLines []string
|
||
for _, id := range ids {
|
||
name := gconv.String(branchIdNameMap[id])
|
||
branchIdNameLines = append(branchIdNameLines, fmt.Sprintf("%s: %s", id, name))
|
||
}
|
||
|
||
getIsChatModel, err := GetIsChatModel(ctx)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
req := flowDto.ComposeMessagesReq{
|
||
BuildType: 2,
|
||
ModelName: getIsChatModel.ModelName,
|
||
SkillName: "",
|
||
Cause: "判断节点",
|
||
Form: map[string]any{"prompt": strings.Join(branchIdNameLines, "\n")},
|
||
UserForm: map[string]any{"prompt": contextParts},
|
||
UserFiles: nodeInput.Global.FileUrl,
|
||
SessionId: nodeInput.Global.SessionId,
|
||
}
|
||
msg, err := ComposeMessages(ctx, &req)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if g.IsEmpty(msg.Messages) {
|
||
return "", fmt.Errorf("msg is empty")
|
||
}
|
||
|
||
content := ""
|
||
for key, _ := range getIsChatModel.ResponseBody {
|
||
content = gconv.String(msg.Messages[key])
|
||
}
|
||
|
||
fmt.Printf("JudgeLambda路由:目标节点ID=%s\n", gconv.String(content))
|
||
|
||
return content, nil
|
||
}
|
||
|
||
// TextModelLambda 构建文案
|
||
func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||
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)
|
||
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
|
||
}
|
||
|
||
// ImageModelLambda 构建图片
|
||
func ImageModelLambda(ctx context.Context, input any) (any, error) {
|
||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||
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)
|
||
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
|
||
}
|
||
|
||
func MergeLambda(ctx context.Context, input any) (any, error) {
|
||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||
if !ok {
|
||
return nil, fmt.Errorf("汇总节点入参类型错误")
|
||
}
|
||
|
||
// 1. 把所有节点输出拍平成 字段名->内容 的map
|
||
dataMap := make(map[string]node.NodeFormField)
|
||
_, outputMap, _ := GetNodeContextContent(nodeInput.Global, nodeInput.Config)
|
||
for _, valueAny := range outputMap {
|
||
if field, ok := valueAny.(node.NodeFormField); ok {
|
||
dataMap[field.Field] = field
|
||
}
|
||
}
|
||
|
||
// 2. 提取所有文案:text_content_0,1,2...
|
||
var contents []node.NodeFormField
|
||
for i := 0; ; i++ {
|
||
key := fmt.Sprintf("text_url_%d", i)
|
||
val, has := dataMap[key]
|
||
if !has || val.Value == "" {
|
||
break
|
||
}
|
||
contents = append(contents, val)
|
||
}
|
||
|
||
// 3. 提取所有图片:image_0,1,2...
|
||
var images []string
|
||
for i := 0; ; i++ {
|
||
key := fmt.Sprintf("image_%d", i)
|
||
val, has := dataMap[key]
|
||
if !has || val.Value == "" {
|
||
break
|
||
}
|
||
images = append(images, val.Value)
|
||
}
|
||
|
||
// 4. 🔥 核心算法:图片按顺序连续归属给每条文案
|
||
textImgMap := make(map[int][]string) // key:文案下标,value:图片列表
|
||
if len(contents) > 0 && len(images) > 0 {
|
||
imgIndex := 0 // 当前用到第几张图片
|
||
totalImg := len(images)
|
||
|
||
for i, item := range contents {
|
||
// 图片已分配完,直接退出
|
||
if imgIndex >= totalImg {
|
||
break
|
||
}
|
||
|
||
// 当前文案需要挂载的图片数量
|
||
needCount := gconv.Int(item.Expand)
|
||
if needCount <= 0 {
|
||
continue
|
||
}
|
||
|
||
var imgList []string
|
||
for imgc := 0; imgc < needCount; imgc++ {
|
||
// 关键:必须判断是否越界
|
||
if imgIndex >= totalImg {
|
||
break
|
||
}
|
||
imgList = append(imgList, images[imgIndex])
|
||
imgIndex++
|
||
}
|
||
|
||
// 有图片才存入 map
|
||
if len(imgList) > 0 {
|
||
textImgMap[i] = imgList
|
||
}
|
||
}
|
||
}
|
||
type Item struct {
|
||
Content string // 文案(可为空)
|
||
Images []string // 图片(可空、可多张)
|
||
}
|
||
|
||
// 🔥 把现有数据转换成通用 Item 列表(支持:纯文案、纯图片、图文任意组合)
|
||
var allItems []Item
|
||
|
||
// 情况1:有文案 → 按文案条目生成 Item(每条文案+对应图片)
|
||
if len(contents) > 0 {
|
||
for i, val := range contents {
|
||
item := Item{
|
||
Content: val.Value, // 文案
|
||
Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片)
|
||
}
|
||
allItems = append(allItems, item)
|
||
}
|
||
} else {
|
||
// 情况2:没有文案,只有图片 → 每张/每组图片生成独立 Item(纯图片条目)
|
||
if len(images) > 0 {
|
||
for _, img := range images {
|
||
allItems = append(allItems, Item{
|
||
Content: "",
|
||
Images: []string{img},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. 生成多条独立HTML记录(通用方案:任意图文组合,每条独立生成+独立上传)
|
||
var outputRecords []node.NodeFormField
|
||
|
||
// 遍历所有【独立图文条目】 → 每条生成独立HTML、独立上传OSS、独立输出记录
|
||
for idx, item := range allItems {
|
||
// item 结构包含:Content(string) + Images([]string)
|
||
// 支持任意来源:文生图、图生文、单独文、单独图、文图合并
|
||
|
||
// 生成单条HTML
|
||
htmlContent := BuildHtml(item.Content, item.Images)
|
||
|
||
// 上传OSS(每条独立上传)
|
||
fileName := fmt.Sprintf("item_%d_%d.html", idx, time.Now().UnixMilli())
|
||
ossResult, err := Upload(ctx, &dto.UploadFileBytesReq{
|
||
FileBytes: []byte(htmlContent),
|
||
FileName: fileName,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 拼接成一条输出记录
|
||
// 每条记录包含:HTML内容 + 访问URL + 文案 + 图片列表
|
||
outputRecords = append(outputRecords,
|
||
node.NodeFormField{
|
||
Field: fmt.Sprintf("item_html_%d", idx),
|
||
Value: htmlContent,
|
||
Label: fmt.Sprintf("条目%d HTML", idx+1),
|
||
Type: "textarea",
|
||
},
|
||
node.NodeFormField{
|
||
Field: fmt.Sprintf("item_html_url_%d", idx),
|
||
Value: ossResult.FileURL,
|
||
Label: fmt.Sprintf("条目%d 地址", idx+1),
|
||
Type: "text",
|
||
},
|
||
node.NodeFormField{
|
||
Field: fmt.Sprintf("item_text_%d", idx),
|
||
Value: item.Content,
|
||
Label: fmt.Sprintf("条目%d 文案", idx+1),
|
||
Type: "text",
|
||
},
|
||
node.NodeFormField{
|
||
Field: fmt.Sprintf("item_images_%d", idx),
|
||
Value: strings.Join(item.Images, ","),
|
||
Label: fmt.Sprintf("条目%d 图片", idx+1),
|
||
Type: "text",
|
||
},
|
||
)
|
||
}
|
||
|
||
// 最终输出多条记录
|
||
nodeInput.Config.OutputResult = outputRecords
|
||
return nodeInput, nil
|
||
}
|
||
|
||
func SummaryLambda(ctx context.Context, input any) (any, error) {
|
||
execInput, ok := input.(*flowDto.NodeExecutionInput)
|
||
if !ok {
|
||
return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input)
|
||
}
|
||
|
||
// 聚合所有已执行节点的输出结果
|
||
var summaryResult []map[string]interface{}
|
||
for _, nodeID := range execInput.Global.ExecutedNodes {
|
||
nodeConfig := execInput.Global.ConfigMap[nodeID]
|
||
if nodeConfig != nil && len(nodeConfig.OutputResult) > 0 {
|
||
for _, field := range nodeConfig.OutputResult {
|
||
if strings.Contains(field.Field, "item_html_url") || strings.Contains(field.Field, "img_url") || strings.Contains(field.Field, "text_url") {
|
||
// 生成 毫秒时间戳 作为 KEY
|
||
timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||
item := make(map[string]interface{})
|
||
item[timeKey] = field.Value
|
||
summaryResult = append(summaryResult, item)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 把汇总结果存入当前节点的输出
|
||
g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult))
|
||
|
||
err := gfdb.DB(ctx, public.DbNameBlackDeacon).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{
|
||
SessionId: execInput.Global.SessionId,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||
Id: execInput.Global.ExecutionId,
|
||
Status: flow.FlowExecutionStatusSuccess.Code(),
|
||
OutputParams: summaryResult,
|
||
}
|
||
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||
|
||
if flowInfo != nil {
|
||
var url string
|
||
url, err = utils.GetFileAddressPrefix(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
createFileTempReq := make([]*fileDto.CreateFileTempReq, 0, len(flowInfo.OutputParams))
|
||
for _, fileUrl := range flowInfo.OutputParams {
|
||
m := gconv.Map(fileUrl)
|
||
for _, v := range m {
|
||
var createReq = new(fileDto.CreateFileTempReq)
|
||
createReq.BusinessId = flowInfo.SessionId
|
||
createReq.FileUrl = url + gconv.String(v)
|
||
createFileTempReq = append(createFileTempReq, createReq)
|
||
}
|
||
}
|
||
if len(createFileTempReq) > 0 {
|
||
_, err = fileDao.FileTempDao.BatchInsert(ctx, createFileTempReq)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
|
||
return execInput, err
|
||
}
|
||
|
||
// VideoModelLambda 构建视频
|
||
func VideoModelLambda(ctx context.Context, input any) (any, error) {
|
||
fmt.Println("VideoModelLambda:", input)
|
||
return input, nil
|
||
}
|
||
|
||
// AudioModelLambda 构建音频
|
||
func AudioModelLambda(ctx context.Context, input any) (any, error) {
|
||
fmt.Println("AudioModelLambda:", input)
|
||
return input, nil
|
||
}
|
||
|
||
// CustomLambda 构建自定义
|
||
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
|
||
}
|