refactor: 重构工作流执行逻辑并提取单模型调用

This commit is contained in:
2026-05-18 18:58:04 +08:00
parent d5206df131
commit 1fbed2febd
4 changed files with 273 additions and 1228 deletions

View File

@@ -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
}