feat: 添加工作流取消与临时文件管理功能

- 新增临时文件(FileTemp)的实体、DAO和DTO,支持文件临时存储与批量操作
- 实现工作流执行取消功能,使用sync.Map管理context.CancelFunc,支持按会话取消运行中的流程
- 将流程执行状态"暂停"变更为"取消",并处理取消导致的错误
- 引入IsDialogue标识区分对话模式,调整判断/文案/图片节点的表单数据组装逻辑
- 重构ComposeMessagesReq,使用BuildType替代IsBuild和ModelTypeId
- 优化HTML内容提取逻辑,修复文案纯文本与图片URL的标签过滤及标签命名
- 在结果汇总节点中使用事务更新执行状态并批量保存输出文件记录
This commit is contained in:
2026-05-15 09:37:23 +08:00
parent 2c3cbab11d
commit b1ee117f6c
18 changed files with 730 additions and 336 deletions

View File

@@ -3,10 +3,13 @@ package flow
import (
"ai-agent/workflow/consts/flow"
"ai-agent/workflow/consts/node"
fileDao "ai-agent/workflow/dao/file"
flowDao "ai-agent/workflow/dao/flow"
fileDto "ai-agent/workflow/model/dto/file"
flowDto "ai-agent/workflow/model/dto/flow"
"ai-agent/workflow/model/entity"
"context"
"errors"
"fmt"
"sort"
"strconv"
@@ -31,6 +34,10 @@ func (s *flowExecutionService) Get(ctx context.Context, req *flowDto.GetFlowExec
return nil, err
}
res = new(flowDto.VOFlowExecution)
res.ImgAddressPrefix, err = utils.GetFileAddressPrefix(ctx)
if err != nil {
return nil, err
}
err = gconv.Struct(r, &res)
return res, err
}
@@ -227,15 +234,61 @@ func Notify(taskId string, result any) {
delete(asyncTasks, taskId)
}
//func (s *flowExecutionService) Cancel(ctx context.Context, req *flowDto.CancelReq) (err error) {
// res, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{
// Id: req.FlowId,
// })
// res.TraceId
// return
//}
// ===================== 核心改造:替换为 sync.Map 存储取消上下文 =====================
var (
// cancelMap: traceID -> context.CancelFunc
cancelMap sync.Map
)
func (s *flowExecutionService) Cancel(ctx context.Context, req *flowDto.CancelReq) (err error) {
getRes, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{
SessionId: req.SessionId,
})
if err != nil {
return err
}
if g.IsEmpty(getRes) {
return fmt.Errorf("会话[%s] 不存在", req.SessionId)
}
// 从 sync.Map 获取取消函数
cancelVal, exist := cancelMap.Load(getRes.TraceId)
if !exist {
return fmt.Errorf("traceID[%s] 不存在或已执行完成", getRes.TraceId)
}
// 执行取消
cancel, ok := cancelVal.(context.CancelFunc)
if !ok {
return fmt.Errorf("traceID[%s] 对应的取消函数类型错误", getRes.TraceId)
}
cancel()
// 取消后清理(可选:也可以在流程结束时统一清理)
cancelMap.Delete(getRes.TraceId)
// 同步更新流程执行状态为已取消
_, err = flowDao.FlowExecutionDao.Update(ctx, &flowDto.UpdateFlowExecutionReq{
Id: getRes.Id,
Status: flow.FlowExecutionStatusCancel.Code(),
})
if err != nil {
return fmt.Errorf("更新取消状态失败: %v", err)
}
return nil
}
func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) {
// ===================== 核心改造1创建可取消的上下文 =====================
execCtx, cancel := context.WithCancel(ctx)
traceId := ""
defer func() {
// 流程结束(成功/失败)时清理 cancelMap
if traceId != "" {
cancelMap.Delete(traceId)
}
cancel()
}()
flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{
SessionId: req.SessionId,
@@ -244,7 +297,9 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
return
}
var executionId int64
var isDialogue bool
if flowInfo == nil {
isDialogue = false
var r = new(flowDto.CreateFlowExecutionReq)
r.FlowUserId = req.FlowId
r.FlowName = req.FlowName
@@ -256,24 +311,55 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
span := trace.SpanFromContext(ctx)
if span != nil && span.SpanContext().HasTraceID() {
r.TraceId = span.SpanContext().TraceID().String()
traceId = r.TraceId
cancelMap.Store(traceId, cancel)
}
executionId, err = flowDao.FlowExecutionDao.Insert(ctx, r)
if err != nil {
return
}
} else {
isDialogue = true
executionId = flowInfo.Id
span := trace.SpanFromContext(ctx)
if span != nil && span.SpanContext().HasTraceID() {
traceId = span.SpanContext().TraceID().String()
cancelMap.Store(traceId, cancel)
}
executionReq := flowDto.UpdateFlowExecutionReq{
Id: executionId,
Status: flow.FlowExecutionStatusRunning.Code(),
TraceId: traceId,
}
_, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq)
if err != nil {
return
}
}
_, err = flowDao.FlowUserDao.Update(ctx, &flowDto.UpdateFlowUserReq{
Id: req.FlowId,
FlowContent: req.FlowContent,
NodeInputParams: req.NodeInputParams,
})
if err != nil {
return nil, err
if !g.IsEmpty(req.FileUrl) {
createFileTempReq := make([]*fileDto.CreateFileTempReq, 0, len(req.FileUrl))
for _, fileUrl := range req.FileUrl {
var createReq = new(fileDto.CreateFileTempReq)
createReq.BusinessId = req.SessionId
createReq.FileUrl = fileUrl
createFileTempReq = append(createFileTempReq, createReq)
}
_, err = fileDao.FileTempDao.BatchInsert(ctx, createFileTempReq)
if err != nil {
return nil, err
}
}
//_, err = flowDao.FlowUserDao.Update(ctx, &flowDto.UpdateFlowUserReq{
// Id: req.FlowId,
// FlowContent: req.FlowContent,
// NodeInputParams: req.NodeInputParams,
//})
//if err != nil {
// return nil, err
//}
//nodeInsert := make([]*nodeDto.CreateNodeExecutionReq, 0, len(flowInfo.NodeInputParams))
//for _, flowNode := range flowInfo.NodeInputParams {
// nodeInsert = append(nodeInsert, &nodeDto.CreateNodeExecutionReq{
@@ -329,18 +415,18 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
// =========================================================================
// ✅【第2步】构建执行图
// =========================================================================
runGraph, err := BuildGraphFromFlowContent(ctx, req.FlowContent, judge2IntentNodeMap, summaryNodeID)
runGraph, err := BuildGraphFromFlowContent(execCtx, req.FlowContent, judge2IntentNodeMap, summaryNodeID)
if err != nil {
executionReq := flowDto.UpdateFlowExecutionReq{
Id: executionId,
Id: executionId,
Status: flow.FlowExecutionStatusFailed.Code(),
ErrorMessage: err.Error(),
}
executionReq.Status = flow.FlowExecutionStatusFailed.Code()
executionReq.ErrorMessage = err.Error()
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
if err1 != nil {
return
}
return nil, err
return nil, fmt.Errorf("执行工作流失败: %v", err)
}
// =========================================================================
@@ -363,6 +449,7 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
// ✅【第4步】构建全局执行入参现在 schemaMap 是有值的!)
// =========================================================================
execInput := &flowDto.FlowExecutionInput{
IsDialogue: isDialogue,
ExecutionId: executionId,
ConfigMap: configMap,
SessionId: req.SessionId,
@@ -371,18 +458,27 @@ func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.Execute
FileUrl: req.FileUrl,
}
// 执行工作流
_, err = runGraph.Invoke(ctx, execInput)
_, err = runGraph.Invoke(execCtx, execInput)
if err != nil {
executionReq := flowDto.UpdateFlowExecutionReq{
Id: executionId,
// 检测是否是取消导致的错误
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(),
}
executionReq.Status = flow.FlowExecutionStatusFailed.Code()
executionReq.ErrorMessage = err.Error()
_, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
if err1 != nil {
return
}
return nil, err
return nil, fmt.Errorf("执行工作流失败: %v", err)
}
return

View File

@@ -3,8 +3,11 @@ 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"
@@ -14,7 +17,9 @@ import (
"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"
)
@@ -160,10 +165,15 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
for _, v := range nodeInput.Config.FormConfig {
contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value)
}
for _, v := range *out {
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"])
@@ -175,33 +185,18 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
branchIdNameLines = append(branchIdNameLines, fmt.Sprintf("%s: %s", id, name))
}
prompt := fmt.Sprintf(`
你是流程路由助手你的任务是根据上下文选择一个正确的节点ID返回。
规则:
1. 只允许从下面的可选节点ID列表中选择一个返回
2. 不要返回任何多余文字、标点、解释、标题
3. 只返回纯节点ID
可选节点IDID: 节点描述):
%s
上下文内容:
%s
`, strings.Join(branchIdNameLines, "\n"), contextParts)
getIsChatModel, err := GetIsChatModel(ctx)
if err != nil {
return "", err
}
req := flowDto.ComposeMessagesReq{
BuildType: 2,
ModelName: getIsChatModel.ModelName,
SkillName: "",
IsBuild: true,
Cause: "判断节点",
Form: map[string]any{},
UserForm: map[string]any{"prompt": prompt},
Form: map[string]any{"prompt": strings.Join(branchIdNameLines, "\n")},
UserForm: map[string]any{"prompt": contextParts},
UserFiles: nodeInput.Global.FileUrl,
SessionId: nodeInput.Global.SessionId,
}
@@ -209,19 +204,13 @@ func JudgeLambda(ctx context.Context, input any) (string, error) {
if err != nil {
return "", err
}
taskResult, err := GatewayTask(ctx, msg.EpicycleId, getIsChatModel.ModelName, msg.Messages)
if err != nil {
return "", err
if g.IsEmpty(msg.Messages) {
return "", fmt.Errorf("msg is empty")
}
result, err := GetTaskResult(ctx, taskResult)
if err != nil {
return "", err
}
mapTaskResult := gconv.Map(result.Text)
content := ""
for key, _ := range getIsChatModel.ResponseBody {
content = gconv.String(mapTaskResult[key])
content = gconv.String(msg.Messages[key])
}
fmt.Printf("JudgeLambda路由目标节点ID=%s\n", gconv.String(content))
@@ -244,10 +233,16 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
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, "html") && !strings.Contains(field.Field, "img") {
outputResult = append(outputResult, field)
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
}
}
}
@@ -256,13 +251,13 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
outputResult = append(outputResult, field)
}
}
resultUserFrom := make(map[string]any)
for _, item := range outputResult {
resultUserFrom[item.Label] = item
}
for _, item := range nodeInput.Config.FormConfig {
resultUserFrom[item.Label] = item
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{
@@ -282,31 +277,30 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
if g.IsEmpty(nodeInput.Config.SkillName) {
skillName = nodeInput.Global.SkillName
}
contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown不要 ``` 符号,不要多余解释\n2. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用 <div class=\"content-item\" id=\"content-{序号}\"> 包裹序号从1开始\n10. 每条文案内部必须在最上方添加一行固定格式:<p class=\"image-count\">需要配图N 张</p> N 是这条文案需要的图片数量,只能是数字,不能是其他文字\n11. 只输出 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{
BuildType: 1,
ModelName: nodeInput.Config.ModelConfig.ModelName,
SkillName: skillName,
IsBuild: true,
Cause: "文案节点",
Form: resultFrom,
UserForm: resultUserFrom,
UserFiles: nodeInput.Global.FileUrl,
SessionId: nodeInput.Global.SessionId,
}
//contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown不要 ``` 符号,不要多余解释\n2. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 只输出 HTML 结构,不输出任何额外文字"
//contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown不要 ``` 符号,不要多余解释\n2. 整体用 <div class=\"report-container\"> 包裹\n3. 主标题使用 <h2 class=\"title\">\n4. 章节标题使用 <h3 class=\"section-title\">\n5. 正文段落使用 <p class=\"paragraph\">\n6. 列表使用 <ul class=\"list\"><li>...</li></ul>\n7. 重点内容使用 <strong> 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用 <div class=\"content-item\" id=\"content-{序号}\"> 包裹序号从1开始\n10. 只输出 HTML 结构,不输出任何额外文字"
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
return nil, err
}
result, err := GetTaskResult(ctx, taskResult)
@@ -335,7 +329,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
})
// 1. 去掉 HTML 标签,生成纯文本
plainText := stripHtmlTags(content)
plainText := stripHtmlTags(content, true)
// 2. 上传纯文本到 OSS
textFileName := fmt.Sprintf("ai_text_%d_%d.txt", time.Now().UnixMilli(), i)
textUrl, err := Upload(ctx, &dto.UploadFileBytesReq{
@@ -349,7 +343,7 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
outputRes = append(outputRes, node.NodeFormField{
Field: fmt.Sprintf("%v:text_url:%d", nodeInput.Config.Id, i),
Value: textUrl.FileURL,
Label: fmt.Sprintf("文案纯文本_%d", i),
Label: fmt.Sprintf("文案纯文本_txt_%d", i),
Type: "string",
Expand: extractImageCount(content),
})
@@ -361,17 +355,23 @@ func TextModelLambda(ctx context.Context, input any) (any, error) {
// 从 HTML 内容里提取图片数量(例如从 <p class="image-count">需要配图3 张</p> 拿到 3
func extractImageCount(content string) int {
re := regexp.MustCompile(`<p class="image-count">需要配图:(\d+) 张</p>`)
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 // 没找到默认 0
return 0
}
// stripHtmlTags 去掉所有HTML标签保留换行和文本结构并删除配图标记行
func stripHtmlTags(html string) string {
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")
@@ -380,11 +380,7 @@ func stripHtmlTags(html string) string {
allTags := regexp.MustCompile(`<[^>]+>`)
text = allTags.ReplaceAllString(text, "")
// 3. 🔥 新增:删除 "需要配图X 张" 这一行(含前后可能的空格/换行
imageCountLine := regexp.MustCompile(`(?m)^\s*需要配图:\d+\s*张\s*$`)
text = imageCountLine.ReplaceAllString(text, "")
// 4. 清理多余空行(多个换行只保留一个,更干净)
// 4. 清理多余空行(多个换行只保留一个
text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n")
// 5. 只去掉首尾空白,中间换行保留
@@ -429,10 +425,16 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
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, "html") && !strings.Contains(field.Field, "img") {
outputResult = append(outputResult, field)
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
}
}
}
@@ -442,12 +444,13 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
}
}
resultUserFrom := make(map[string]any)
for _, item := range outputResult {
resultUserFrom[item.Label] = item
}
for _, item := range nodeInput.Config.FormConfig {
resultUserFrom[item.Label] = item
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{
@@ -469,9 +472,9 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
}
req := flowDto.ComposeMessagesReq{
BuildType: 1,
ModelName: nodeInput.Config.ModelConfig.ModelName,
SkillName: skillName,
IsBuild: true,
Cause: "图片节点",
Form: resultFrom,
UserForm: resultUserFrom,
@@ -482,7 +485,9 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
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
@@ -493,8 +498,6 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
return "", err
}
//result := new(flowDto.TaskCallback)
//result.Text = "{\n \"content\": [\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/8d/20260512/76483b06/306aac7b-915e-479d-94d4-adc3cf1d6f22.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=3a3KDmPNeO%2BVjHJbAV8t0R7UF6Q%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/c9/20260512/76483b06/f8f3e9be-2920-48b8-93f5-acbf26e52b0c.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=li%2FpcoX5i7FJrk3PCpw5jrbWy2k%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/89/20260512/76483b06/38d55abe-8230-4837-85d3-426265139be0.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=uNRV9RQY2O60frAtIg6JvCcVhDw%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/82/20260512/76483b06/e100070d-2a79-4ec8-be72-105226854bab.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=7UCh7FmYt0%2FYxyItNoLELp7zPF0%3D\"\n }\n ]\n}"
mapTaskResult := gconv.Map(result.Text)
imgs := []string{}
@@ -548,7 +551,7 @@ func ImageModelLambda(ctx context.Context, input any) (any, error) {
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("图片_%d关联文案ID", i),
Label: fmt.Sprintf("图片_img_%d关联文案ID", i),
Type: "string",
})
}
@@ -874,113 +877,51 @@ func SummaryLambda(ctx context.Context, input any) (any, error) {
// 把汇总结果存入当前节点的输出
g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult))
executionReq := flowDto.UpdateFlowExecutionReq{
Id: execInput.Global.ExecutionId,
OutputParams: summaryResult,
}
executionReq.Status = flow.FlowExecutionStatusSuccess.Code()
_, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
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,
OutputParams: summaryResult,
}
executionReq.Status = flow.FlowExecutionStatusSuccess.Code()
_, 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
}
//func SummaryLambda(ctx context.Context, input any) (any, error) {
// execInput, ok := input.(*flowDto.NodeExecutionInput)
// if !ok {
// return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input)
// }
//
// // 1. 定义临时映射按条目序号如item_0聚合html/img/text
// // key: 条目序号如0/1/2, value: {html:"", img:"", text:""}
// itemMap := make(map[int]map[string]string)
// // 存储每个条目对应的时间戳(一个条目一个唯一时间戳)
// itemTimeMap := make(map[int]int64)
//
// // 2. 遍历已执行节点,解析输出字段并分组
// for _, nodeID := range execInput.Global.ExecutedNodes {
// nodeConfig := execInput.Global.ConfigMap[nodeID]
// if nodeConfig == nil || len(nodeConfig.OutputResult) == 0 {
// continue
// }
//
// // 遍历节点的输出字段
// for _, field := range nodeConfig.OutputResult {
// var itemIndex int
// var fieldType string
// var fieldValue string
//
// // 匹配「条目HTML地址」字段如item_html_url_0
// if match := regexp.MustCompile(`item_html_url_(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 {
// itemIndex, _ = strconv.Atoi(match[1])
// fieldType = "html"
// fieldValue = gconv.String(field.Value)
// } else if match := regexp.MustCompile(`img_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 {
// itemIndex, _ = strconv.Atoi(match[1])
// fieldType = "img"
// fieldValue = gconv.String(field.Value)
// } else if match := regexp.MustCompile(`text_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 {
// itemIndex, _ = strconv.Atoi(match[1])
// fieldType = "text"
// fieldValue = gconv.String(field.Value)
// } else {
// // 非目标字段,跳过
// continue
// }
//
// // 初始化条目映射(首次遇到该条目时)
// if _, exists := itemMap[itemIndex]; !exists {
// itemMap[itemIndex] = map[string]string{
// "html": "",
// "img": "",
// "text": "",
// }
// // 为该条目生成唯一时间戳(毫秒级)
// itemTimeMap[itemIndex] = time.Now().UnixMilli()
// }
//
// // 填充该条目对应的字段值
// itemMap[itemIndex][fieldType] = fieldValue
// }
// }
//
// // 3. 组装最终的汇总结构:[{内容N:{html:"",img:"",text:""},时间戳:xxx}, ...]
// var summaryResult []map[string]interface{}
// // 按条目序号排序(保证顺序一致)
// itemIndexes := make([]int, 0, len(itemMap))
// for idx := range itemMap {
// itemIndexes = append(itemIndexes, idx)
// }
// sort.Ints(itemIndexes)
//
// // 遍历排序后的条目,组装结构
// for _, idx := range itemIndexes {
// itemData := itemMap[idx]
// timeStamp := itemTimeMap[idx]
//
// // 单条目结构:{"内容X": {html:"",img:"",text:""}, "时间戳": xxx}
// itemResult := make(map[string]interface{})
// itemResult[fmt.Sprintf("内容%d", idx+1)] = map[string]string{
// "html": itemData["html"],
// "img": itemData["img"],
// "text": itemData["text"],
// }
// itemResult["时间戳"] = timeStamp
//
// summaryResult = append(summaryResult, itemResult)
// }
//
// // 4. 打印调试&更新数据库
// g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult))
// executionReq := flowDto.UpdateFlowExecutionReq{
// Id: execInput.Global.ExecutionId,
// OutputParams: summaryResult,
// Status: flow.FlowExecutionStatusSuccess.Code(),
// }
// _, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq)
//
// return execInput, err
//}
// VideoModelLambda 构建视频
func VideoModelLambda(ctx context.Context, input any) (any, error) {
fmt.Println("VideoModelLambda:", input)

View File

@@ -9,7 +9,6 @@ import (
"io"
"mime/multipart"
"net/http"
"strings"
commonHttp "gitea.com/red-future/common/http"
"gitea.com/red-future/common/utils"
@@ -17,7 +16,7 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
func GetIsChatModel(ctx context.Context) (*flowDto.GetIsChatModelRes, error) {
func GetIsChatModel(ctx context.Context) (res *flowDto.GetIsChatModelRes, err error) {
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
@@ -26,13 +25,9 @@ func GetIsChatModel(ctx context.Context) (*flowDto.GetIsChatModelRes, error) {
}
}
}
res := new(flowDto.GetIsChatModelRes)
err := commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil)
if err != nil {
return nil, err
}
return res, nil
res = new(flowDto.GetIsChatModelRes)
err = commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil)
return
}
func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, error) {
@@ -53,7 +48,7 @@ func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string,
return res.TaskId, nil
}
func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (*flowDto.ComposeMessagesRes, error) {
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 {
@@ -62,13 +57,9 @@ func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (*flo
}
}
}
res := new(flowDto.ComposeMessagesRes)
err := commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req)
if err != nil {
return nil, err
}
return res, nil
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) {
@@ -169,77 +160,3 @@ func Upload(ctx context.Context, req *dto.UploadFileBytesReq) (*dto.UploadFileBy
g.Log().Infof(ctx, "[Upload] success url=%s size=%d", res.FileURL, res.FileSize)
return res, nil
}
func buildMergeHtml(texts []string, images []string) string {
html := strings.Builder{}
html.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", Arial, sans-serif;
background: #fff;
color: #333;
}
.container {
max-width: 960px;
margin: 0 auto;
}
/* 图片:完全贴边,无额外间距 */
.image-block {
width: 100%;
margin: 0;
padding: 0;
}
.image-block img {
width: 100%;
height: auto;
display: block;
border-radius: 0;
}
/* 文案:极致紧凑 */
.text-block {
margin: 0;
padding: 16px; /* 仅保留内边距,不设外边距 */
line-height: 1.6;
font-size: 14px;
color: #444;
white-space: pre-wrap;
}
/* 分割线:完全去掉,改用内边距自然分隔 */
</style>
</head>
<body>
<div class="container">
`)
// 1. 先渲染图片(无任何上下边距,占满宽度)
if len(images) > 0 {
html.WriteString(`<div class="image-block">`)
for _, img := range images {
html.WriteString(fmt.Sprintf(`<img src="%s" alt="" />`, img))
}
html.WriteString(`</div>`)
}
// 2. 渲染文案(紧贴图片下方,仅用内边距留白)
if len(texts) > 0 {
html.WriteString(`<div class="text-block">`)
// 段落之间用 <br> 而不是 <br><br>,减少空行
html.WriteString(strings.Join(texts, "<br>"))
html.WriteString(`</div>`)
}
html.WriteString(`
</div>
</body>
</html>
`)
return html.String()
}