Files
ai-agent/workflow/service/flow/flow_user_service.go
19904408334 b3f4b94b21 feat: 支持模板子流程递归拷贝与段级续跑优化
* 模板拷贝用户流程时递归复制子流程,并重写 sub_flow 节点引用
* 段级续跑改用列表位置作为段身份,移除对 segment_index 的依赖
* 段结果保存移到每段生成完成时立即落库,降低崩溃丢失风险
* 移除视频分段续跑设计与对应测试
2026-08-25 13:56:55 +08:00

277 lines
7.9 KiB
Go

package flow
import (
"ai-agent/workflow/consts/flow"
"ai-agent/workflow/consts/public"
flowDao "ai-agent/workflow/dao/flow"
flowDto "ai-agent/workflow/model/dto/flow"
"ai-agent/workflow/model/entity"
"ai-agent/workflow/service"
"context"
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"gitea.redpowerfuture.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"
)
var FlowUserService = &flowUserService{}
type flowUserService struct{}
func (s *flowUserService) Create(ctx context.Context, req *flowDto.CreateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) {
admin, err := service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent)
var id int64
if admin {
id, err = flowDao.FlowTemplateDao.Insert(ctx, &flowDto.CreateFlowTemplateReq{
FlowTemplateName: req.FlowName,
Description: req.Description,
FlowContent: req.FlowContent,
NodeInputParams: req.NodeInputParams,
Status: flow.FlowTemplateStatusEnable.Code(),
})
} else {
id, err = flowDao.FlowUserDao.Insert(ctx, req)
}
return &flowDto.CreateFlowUserRes{Id: id}, err
}
func (s *flowUserService) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) {
id := req.Id
admin, err := service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent)
if admin {
_, err = flowDao.FlowTemplateDao.Update(ctx, &flowDto.UpdateFlowTemplateReq{
Id: req.Id,
FlowTemplateName: req.FlowName,
Description: req.Description,
FlowContent: req.FlowContent,
NodeInputParams: req.NodeInputParams,
Status: flow.FlowTemplateStatusEnable.Code(),
})
} else {
var get *entity.FlowTemplate
get, err = flowDao.FlowTemplateDao.Get(ctx, &flowDto.GetFlowTemplateReq{
Id: req.Id,
})
if err != nil {
return nil, err
}
if !g.IsEmpty(get) {
// 模版 → 用户流程拷贝(含递归子流程拷贝)整体放一个事务:任一拷贝失败则整体回滚,
// 避免主流程已落库而子流程缺失/引用错乱的脏数据。
// Transaction 会把 tx 注入回调 ctx,回调内用该 ctx 调 DAO 即自动进入事务。
txErr := gfdb.DB(ctx, public.DbNameBlackDeacon).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 子流程引用重写:模版里的 sub_flow 节点指向子模版/子流程 id,
// 拷贝成用户自己的流程时要把子工作流也复制一份(子流程可能再嵌套子流程,递归),
// 并把主流程 SubConfig.WorkflowId 改写为新拷贝的 id
subIdMap, copyErr := copySubFlows(ctx, req.SubFlows)
if copyErr != nil {
return copyErr
}
rewriteSubFlowWorkflowIds(req.FlowContent, subIdMap)
// SubConfig 改写后重新提取节点参数,保证落库的 NodeInputParams 与 FlowContent 一致
req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent)
newId, insertErr := flowDao.FlowUserDao.Insert(ctx, &flowDto.CreateFlowUserReq{
FlowName: req.FlowName,
Description: req.Description,
FlowContent: req.FlowContent,
NodeInputParams: req.NodeInputParams,
SourceFlowTemplateId: get.Id,
})
if insertErr != nil {
return insertErr
}
id = newId
return nil
})
if txErr != nil {
return nil, txErr
}
} else {
_, err = flowDao.FlowUserDao.Update(ctx, req)
}
}
return &flowDto.CreateFlowUserRes{Id: id}, err
}
func (s *flowUserService) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (err error) {
admin, err := service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
if admin {
_, err = flowDao.FlowTemplateDao.Delete(ctx, &flowDto.DeleteFlowTemplateReq{
Id: req.Id,
})
} else {
_, err = flowDao.FlowUserDao.Delete(ctx, req)
}
return
}
func (s *flowUserService) Get(ctx context.Context, req *flowDto.GetFlowUserReq) (res *flowDto.FlowUserVO, err error) {
var flowInfo *entity.FlowTemplate
flowInfo, err = flowDao.FlowTemplateDao.Get(ctx, &flowDto.GetFlowTemplateReq{
Id: req.Id,
})
if err != nil {
return
}
if flowInfo != nil {
res = new(flowDto.FlowUserVO)
res.FlowName = flowInfo.FlowTemplateName
err = gconv.Struct(flowInfo, res)
return
}
var flowUserInfo *entity.FlowUser
flowUserInfo, err = flowDao.FlowUserDao.Get(ctx, req)
if err != nil {
return
}
res = new(flowDto.FlowUserVO)
err = gconv.Struct(flowUserInfo, res)
return
}
func (s *flowUserService) List(ctx context.Context, req *flowDto.ListFlowUserReq) (res *flowDto.ListFlowRes, err error) {
admin, err := service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
if admin {
var t int
var l []*entity.FlowTemplate
l, t, err = flowDao.FlowTemplateDao.List(ctx, &flowDto.ListFlowTemplateReq{
Keyword: req.Keyword,
Page: req.Page,
})
if err != nil {
return
}
r := &flowDto.ListFlowTemplateRes{
Total: t,
}
err = gconv.Struct(l, &r.List)
if err != nil {
return
}
res = &flowDto.ListFlowRes{
ListFlowUserRes: nil,
ListFlowTemplateRes: r,
IsAdmin: admin,
}
return
}
res = &flowDto.ListFlowRes{
IsAdmin: admin,
}
if !req.IsOwn {
var t int
var l []*entity.FlowTemplate
l, t, err = flowDao.FlowTemplateDao.List(ctx, &flowDto.ListFlowTemplateReq{
Keyword: req.Keyword,
Page: req.Page,
})
if err != nil {
return
}
r := &flowDto.ListFlowTemplateRes{
Total: t,
}
err = gconv.Struct(l, &r.List)
if err != nil {
return
}
res.ListFlowTemplateRes = r
}
var user *beans.User
user, err = utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
req.Creator = user.UserName
list, total, err := flowDao.FlowUserDao.List(ctx, req)
if err != nil {
return
}
re := &flowDto.ListFlowUserRes{
Total: total,
}
err = gconv.Struct(list, &re.List)
if err != nil {
return
}
res.ListFlowUserRes = re
return
}
// copySubFlows 递归拷贝子工作流(子流程可能再包含子流程),返回 旧id→新id 映射。
// 映射键是子工作流在来源(模版)中的 id,即父流程 sub_flow 节点 SubConfig.WorkflowId 指向的值;
// 拷贝时把子工作流落为当前用户自己的流程(SourceFlowTemplateId 记录来源)。
// 同一子流程被多个节点引用时只拷贝一份,避免产生孤儿副本。
func copySubFlows(ctx context.Context, subs []flowDto.UpdateFlowUserReq) (map[int64]int64, error) {
idMap := make(map[int64]int64)
for i := range subs {
sub := subs[i]
if _, done := idMap[sub.Id]; done {
continue
}
nestedMap, err := copySubFlows(ctx, sub.SubFlows)
if err != nil {
return nil, err
}
for k, v := range nestedMap {
idMap[k] = v
}
// 改写本子流程对更深层子流程的引用后,再落库为新的用户流程
rewriteSubFlowWorkflowIds(sub.FlowContent, idMap)
var nodeInputParams []*entity.FlowNode
if sub.FlowContent != nil {
nodeInputParams = ExtractFlowNodeFrom(sub.FlowContent)
}
newId, err := flowDao.FlowUserDao.Insert(ctx, &flowDto.CreateFlowUserReq{
FlowName: sub.FlowName,
Description: sub.Description,
FlowContent: sub.FlowContent,
NodeInputParams: nodeInputParams,
SourceFlowTemplateId: sub.Id,
})
if err != nil {
return nil, err
}
idMap[sub.Id] = newId
}
return idMap, nil
}
// rewriteSubFlowWorkflowIds 把 flowContent 中 sub_flow 节点的 WorkflowId 按 idMap 改写为新拷贝的流程 id
func rewriteSubFlowWorkflowIds(flowContent *entity.FlowInfo, idMap map[int64]int64) {
if flowContent == nil {
return
}
for i := range flowContent.Nodes {
n := &flowContent.Nodes[i]
if n.SubConfig == nil || n.SubConfig.WorkflowId == 0 {
continue
}
if newId, ok := idMap[n.SubConfig.WorkflowId]; ok {
n.SubConfig.WorkflowId = newId
}
}
}