feat: 添加对话式工作流节点执行与结果合并逻辑
This commit is contained in:
@@ -12,7 +12,6 @@ import (
|
||||
"ai-agent/workflow/model/entity"
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -240,7 +239,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
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)
|
||||
field.Value = StripHtmlTags(field.Value, false)
|
||||
}
|
||||
resultUserFrom[field.Label] = field
|
||||
}
|
||||
@@ -277,7 +276,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
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 结构,不输出任何额外文字"
|
||||
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{
|
||||
@@ -325,13 +324,14 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
Value: content,
|
||||
Label: fmt.Sprintf("文案内容_%d", i),
|
||||
Type: "string",
|
||||
Expand: extractImageCount(content),
|
||||
Expand: ExtractImageCount(content),
|
||||
})
|
||||
|
||||
// 1. 去掉 HTML 标签,生成纯文本
|
||||
plainText := stripHtmlTags(content, true)
|
||||
//plainText := StripHtmlTags(content, true)
|
||||
plainText := BuildText(content)
|
||||
// 2. 上传纯文本到 OSS
|
||||
textFileName := fmt.Sprintf("ai_text_%d_%d.txt", time.Now().UnixMilli(), i)
|
||||
textFileName := fmt.Sprintf("ai_text_%d_%d.inc", time.Now().UnixMilli(), i)
|
||||
textUrl, err := Upload(ctx, &dto.UploadFileBytesReq{
|
||||
FileBytes: []byte(plainText),
|
||||
FileName: textFileName,
|
||||
@@ -341,11 +341,11 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
}
|
||||
// 3. 把纯文本地址存入输出
|
||||
outputRes = append(outputRes, node.NodeFormField{
|
||||
Field: fmt.Sprintf("%v:text_url:%d", nodeInput.Config.Id, i),
|
||||
Field: fmt.Sprintf("text_url_%d", i),
|
||||
Value: textUrl.FileURL,
|
||||
Label: fmt.Sprintf("文案纯文本_txt_%d", i),
|
||||
Type: "string",
|
||||
Expand: extractImageCount(content),
|
||||
Expand: ExtractImageCount(content),
|
||||
})
|
||||
}
|
||||
nodeInput.Config.OutputResult = outputRes
|
||||
@@ -353,64 +353,6 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
|
||||
return nodeInput, nil
|
||||
}
|
||||
|
||||
// 从 HTML 内容里提取图片数量(例如从 <p class="image-count">需要配图:3 张</p> 拿到 3)
|
||||
func extractImageCount(content string) int {
|
||||
re := regexp.MustCompile(`<p class="image-count">[^\d]*(\d+)[^\d]*</p>`)
|
||||
match := re.FindStringSubmatch(content)
|
||||
if len(match) >= 2 {
|
||||
num, _ := strconv.Atoi(match[1])
|
||||
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, "")
|
||||
}
|
||||
|
||||
// 1. 替换块级标签为换行,保证排版
|
||||
blockTags := regexp.MustCompile(`</?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`)
|
||||
text := blockTags.ReplaceAllString(html, "\n")
|
||||
|
||||
// 2. 去掉所有剩余的 HTML 标签
|
||||
allTags := regexp.MustCompile(`<[^>]+>`)
|
||||
text = allTags.ReplaceAllString(text, "")
|
||||
|
||||
// 4. 清理多余空行(多个换行只保留一个)
|
||||
text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n")
|
||||
|
||||
// 5. 只去掉首尾空白,中间换行保留
|
||||
text = strings.TrimSpace(text)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// SplitMultiContents 拆分模型返回的多条文案(基于HTML标签分隔)
|
||||
func SplitMultiContents(htmlContent string) []string {
|
||||
var contents []string
|
||||
// 正则匹配<div class="content-item" id="content-{序号}">包裹的内容
|
||||
re := regexp.MustCompile(`<div class="content-item" id="content-\d+">([\s\S]*?)</div>`)
|
||||
matches := re.FindAllStringSubmatch(htmlContent, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
// 清理空内容
|
||||
trimmed := strings.TrimSpace(match[1])
|
||||
if trimmed != "" {
|
||||
contents = append(contents, trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 兜底:如果没有匹配到结构化内容,按换行/分隔符拆分
|
||||
if len(contents) == 0 {
|
||||
contents = strings.Split(htmlContent, "===分隔符===") // 提示词中可新增此兜底规则
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
// ImageModelLambda 构建图片
|
||||
func ImageModelLambda(ctx context.Context, input any) (any, error) {
|
||||
nodeInput, ok := input.(*flowDto.NodeExecutionInput)
|
||||
@@ -432,7 +374,7 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
|
||||
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)
|
||||
field.Value = StripHtmlTags(field.Value, false)
|
||||
}
|
||||
resultUserFrom[field.Label] = field
|
||||
}
|
||||
@@ -509,9 +451,8 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
|
||||
for _, item := range imgs {
|
||||
mapItem := gconv.Map(item)
|
||||
for _, value := range mapItem {
|
||||
values := ""
|
||||
values, ok = value.(string)
|
||||
if !ok {
|
||||
values, imgOk := value.(string)
|
||||
if !imgOk {
|
||||
return nil, fmt.Errorf("图片地址类型错误")
|
||||
}
|
||||
// 下载官方临时图片
|
||||
@@ -577,7 +518,7 @@ func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
// 2. 提取所有文案:text_content_0,1,2...
|
||||
var contents []node.NodeFormField
|
||||
for i := 0; ; i++ {
|
||||
key := fmt.Sprintf("text_content_%d", i)
|
||||
key := fmt.Sprintf("text_url_%d", i)
|
||||
val, has := dataMap[key]
|
||||
if !has || val.Value == "" {
|
||||
break
|
||||
@@ -668,143 +609,7 @@ func MergeLambda(ctx context.Context, input any) (any, error) {
|
||||
// 支持任意来源:文生图、图生文、单独文、单独图、文图合并
|
||||
|
||||
// 生成单条HTML
|
||||
var htmlBuilder strings.Builder
|
||||
htmlBuilder.WriteString(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
.item {
|
||||
padding: 30px;
|
||||
}
|
||||
.image-group {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.image-group img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.image-group img:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.text {
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
color: #555;
|
||||
}
|
||||
.text h2 {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.text h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin: 20px 0 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
.text p {
|
||||
margin-bottom: 15px;
|
||||
text-align: justify;
|
||||
}
|
||||
.text strong {
|
||||
color: #e74c3c;
|
||||
font-weight: 600;
|
||||
}
|
||||
.text ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.text ul li {
|
||||
padding: 10px 0 10px 30px;
|
||||
position: relative;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.text ul li:before {
|
||||
content: "●";
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 12px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
.item {
|
||||
padding: 20px;
|
||||
}
|
||||
.text h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
.text h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="item">
|
||||
`)
|
||||
|
||||
// 写入图片(支持0张、1张、多张)
|
||||
if len(item.Images) > 0 {
|
||||
htmlBuilder.WriteString(`<div class="image-group">`)
|
||||
for _, imgUrl := range item.Images {
|
||||
htmlBuilder.WriteString(fmt.Sprintf(`<img src="%s" alt="图片"/>`, imgUrl))
|
||||
}
|
||||
htmlBuilder.WriteString(`</div>`)
|
||||
}
|
||||
|
||||
// 🔥 写入文案前:删除 <p class="image-count">需要配图:X 张</p>
|
||||
if item.Content != "" {
|
||||
// 正则删除整行
|
||||
re := regexp.MustCompile(`<p class="image-count">需要配图:\d+ 张</p>`)
|
||||
cleanContent := re.ReplaceAllString(item.Content, "")
|
||||
|
||||
// 写入清理后的文案
|
||||
htmlBuilder.WriteString(fmt.Sprintf(`<div class="text">%s</div>`, cleanContent))
|
||||
}
|
||||
|
||||
htmlBuilder.WriteString(`</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`)
|
||||
htmlContent := htmlBuilder.String()
|
||||
htmlContent := BuildHtml(item.Content, item.Images)
|
||||
|
||||
// 上传OSS(每条独立上传)
|
||||
fileName := fmt.Sprintf("item_%d_%d.html", idx, time.Now().UnixMilli())
|
||||
@@ -887,9 +692,9 @@ func SummaryLambda(ctx context.Context, input any) (any, error) {
|
||||
|
||||
executionReq := flowDto.UpdateFlowExecutionReq{
|
||||
Id: execInput.Global.ExecutionId,
|
||||
Status: flow.FlowExecutionStatusSuccess.Code(),
|
||||
OutputParams: summaryResult,
|
||||
}
|
||||
executionReq.Status = flow.FlowExecutionStatusSuccess.Code()
|
||||
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
|
||||
|
||||
if flowInfo != nil {
|
||||
@@ -939,3 +744,171 @@ 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user