feat: 新增主动拉取与多类型回调功能

- 新增 ActivePull 实体、DAO、DTO 及 Service,支持主动拉取任务管理
- 新增 ComposeCallback、VideoCallback、HttpNodeCallback 多类型回调接口
- FlowExecution 增加 NodeGroupId 和 TotalTokens 字段,支持节点组追踪与 Token 统计
- ExecutedNodes 结构由字符串列表改为包含执行状态的节点对象列表
- 重构回调通知机制,统一 Notify 函数调用
- 优化输出项类型判断逻辑,新增文件类型标识
This commit is contained in:
2026-06-10 14:23:55 +08:00
parent ab3a2d967e
commit 03c95c3601
33 changed files with 3207 additions and 615 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"gitea.com/red-future/common/beans"
commonHttp "gitea.com/red-future/common/http"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
@@ -15,35 +16,122 @@ var NodeLibraryService = &nodeLibraryService{}
type nodeLibraryService struct{}
func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.WorkflowNodeTreeReq) (*nodeDto.WorkflowNodeTreeRes, error) {
func GetModelType(ctx context.Context) (mainTypeMap map[int]string, err error) {
headers := make(map[string]string)
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
res := new(nodeDto.ModelTypeResponse)
err = commonHttp.Get(ctx, "model-gateway/model/listType", headers, res, nil)
// 通用过滤:只保留 能被 100 整除的主类型100/200/300...
mainTypeMap = make(map[int]string)
for typ, name := range res.Type {
if typ%100 == 0 {
mainTypeMap[typ] = name
}
}
return
}
func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.WorkflowNodeTreeReq) (*nodeDto.WorkflowNodeTreeRes, error) {
WorkflowNodeGroups := []node.NodeGroupItem{
{
Group: node.NodeGroupComponent,
Label: node.NodeGroupNameComponent,
Items: []node.NodeItem{
{
NodeCode: node.NodeTypeTextModel,
NodeName: node.NodeNameTextModel,
ModelType: node.ModelTypeText,
SkillOption: true,
FormConfig: []node.NodeFormField{}, // 技能下拉
ModelConfig: []node.ModelItem{},
NodeCode: node.NodeTypeTextModel,
NodeName: node.NodeNameTextModel,
ModelType: node.ModelTypeText,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeImageModel,
NodeName: node.NodeNameImageModel,
ModelType: node.ModelTypeImage,
SkillOption: true,
FormConfig: []node.NodeFormField{}, // 技能下拉
ModelConfig: []node.ModelItem{},
NodeCode: node.NodeTypeImageModel,
NodeName: node.NodeNameImageModel,
ModelType: node.ModelTypeImage,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeVideoModel,
NodeName: node.NodeNameVideoModel,
ModelType: node.ModelTypeVideo,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeAudioModel,
NodeName: node.NodeNameAudioModel,
ModelType: node.ModelTypeAudio,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeBatchModel,
NodeName: node.NodeNameBatchModel,
ModelType: node.ModelTypeText,
SkillOption: false,
PromptOption: true,
IsSaveFile: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
//{
// NodeCode: node.NodeTypeSenseOptimizeModel,
// NodeName: node.NodeNameSenseOptimizeModel,
// ModelType: node.ModelTypeText,
// SkillOption: false,
// FormConfig: []node.NodeFormField{},
// ModelConfig: []node.ModelItem{},
//},
//{
// NodeCode: node.NodeTypeStoryOptimizeModel,
// NodeName: node.NodeNameStoryOptimizeModel,
// ModelType: node.ModelTypeText,
// SkillOption: false,
// FormConfig: []node.NodeFormField{},
// ModelConfig: []node.ModelItem{},
//},
//{
// NodeCode: node.NodeTypeScriptOptimizeModel,
// NodeName: node.NodeNameScriptOptimizeModel,
// ModelType: node.ModelTypeText,
// SkillOption: false,
// FormConfig: []node.NodeFormField{},
// ModelConfig: []node.ModelItem{},
//},
},
},
{
Group: node.NodeGroupBase,
Label: node.NodeGroupNameBase,
Items: []node.NodeItem{
{
NodeCode: node.NodeTypeDataConversionModel,
NodeName: node.NodeNameDataConversionModel,
ModelType: node.ModelTypeText,
SkillOption: false,
PromptOption: true,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeMerge,
NodeName: node.NodeNameMerge,
@@ -51,6 +139,13 @@ func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.Wo
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeDataMerge,
NodeName: node.NodeNameDataMerge,
SkillOption: false,
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeJudge,
NodeName: node.NodeNameJudge,
@@ -67,6 +162,161 @@ func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.Wo
FormConfig: []node.NodeFormField{},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeHttp,
NodeName: node.NodeNameHttp,
SkillOption: false,
IsSaveFile: true,
FormConfig: []node.NodeFormField{
{
Field: "method",
Label: "请求方式",
Type: "select",
Required: true,
Options: []node.SelectOption{
{Label: "GET", Value: "GET"},
{Label: "POST", Value: "POST"},
{Label: "PUT", Value: "PUT"},
{Label: "DELETE", Value: "DELETE"},
},
},
{
Field: "url",
Label: "请求地址",
Type: "input",
Required: true,
},
{
Field: "headers",
Label: "请求头支持Authorization鉴权",
Type: "keyValue",
Required: false,
},
{
Field: "bodyType",
Label: "请求体类型",
Type: "select",
Required: true,
Options: []node.SelectOption{
{Label: "无", Value: "None"},
{Label: "JSON", Value: "JSON"},
//{Label: "表单", Value: "FormUrlEncoded"},
//{Label: "文件上传", Value: "FormData"},
//{Label: "原生文本", Value: "Raw"},
},
},
{
Field: "body",
Label: "请求体内容",
Type: "keyValue",
Required: false,
},
{
Field: "response",
Label: "结果返回结构",
Type: "keyValue",
Required: false,
},
{
Field: "responseType",
Label: "结果返回方式",
Type: "select",
Required: true,
Options: []node.SelectOption{
{Label: "同步返回", Value: "sync"},
{Label: "等候回调", Value: "callback"},
{Label: "主动拉取", Value: "pull"},
},
Expand: []node.NodeFormField{
{
Field: "method",
Label: "请求方式",
Type: "select",
Required: true,
Options: []node.SelectOption{
{Label: "GET", Value: "GET"},
{Label: "POST", Value: "POST"},
{Label: "PUT", Value: "PUT"},
{Label: "DELETE", Value: "DELETE"},
},
},
{
Field: "url",
Label: "请求地址",
Type: "input",
Required: true,
},
{
Field: "headers",
Label: "请求头支持Authorization鉴权",
Type: "keyValue",
Required: false,
},
{
Field: "bodyType",
Label: "请求体类型",
Type: "select",
Required: true,
Options: []node.SelectOption{
{Label: "无", Value: "None"},
{Label: "JSON", Value: "JSON"},
//{Label: "表单", Value: "FormUrlEncoded"},
//{Label: "文件上传", Value: "FormData"},
//{Label: "原生文本", Value: "Raw"},
},
},
{
Field: "body",
Label: "请求体内容",
Type: "keyValue",
Required: false,
},
{
Field: "response",
Label: "结果返回结构",
Type: "keyValue",
Required: false,
},
{
Field: "timeout",
Label: "超时时间(秒)",
Type: "inputNumber",
Required: false,
Default: 30,
},
{
Field: "insecureSkipVerify",
Label: "跳过HTTPS证书校验",
Type: "switch",
Required: false,
Default: false,
},
},
},
{
Field: "callbackUrl",
Label: "回调地址(只需要填写字段名称)",
Type: "input",
Required: false,
Default: "",
},
{
Field: "timeout",
Label: "超时时间(秒)",
Type: "inputNumber",
Required: false,
Default: 30,
},
{
Field: "insecureSkipVerify",
Label: "跳过HTTPS证书校验",
Type: "switch",
Required: false,
Default: false,
},
},
ModelConfig: []node.ModelItem{},
},
//{
// NodeCode: node.NodeTypeModel,
// NodeName: node.NodeNameModel,
@@ -76,22 +326,22 @@ func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.Wo
//},
},
},
{
Group: node.NodeGroupCustom,
Label: node.NodeGroupNameCustom,
Items: []node.NodeItem{
{
NodeCode: node.NodeTypeCustomNode,
NodeName: node.NodeNameCustomNode,
SkillOption: true,
FormConfig: []node.NodeFormField{
{Field: "nodeName", Label: node.FormLabelApiKey, Type: "input", Required: true},
{Field: "nodeType", Label: node.FormLabelModel, Type: "input", Required: true},
},
ModelConfig: []node.ModelItem{},
},
},
},
//{
// Group: node.NodeGroupCustom,
// Label: node.NodeGroupNameCustom,
// Items: []node.NodeItem{
// {
// NodeCode: node.NodeTypeCustomNode,
// NodeName: node.NodeNameCustomNode,
// SkillOption: true,
// FormConfig: []node.NodeFormField{
// {Field: "nodeName", Label: node.FormLabelApiKey, Type: "input", Required: true},
// {Field: "nodeType", Label: node.FormLabelModel, Type: "input", Required: true},
// },
// ModelConfig: []node.ModelItem{},
// },
// },
//},
}
tree := &nodeDto.WorkflowNodeTreeRes{
Groups: WorkflowNodeGroups,
@@ -104,17 +354,19 @@ func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.Wo
// 遍历分组下的每个节点
for itemIdx := range group.Items {
item := &group.Items[itemIdx]
if item.NodeCode == node.NodeTypeTextModel {
if item.NodeCode == node.NodeTypeTextModel ||
item.NodeCode == node.NodeTypeImageModel ||
item.NodeCode == node.NodeTypeVideoModel ||
item.NodeCode == node.NodeTypeAudioModel ||
item.NodeCode == node.NodeTypeBatchModel ||
item.NodeCode == node.NodeTypeDataConversionModel ||
item.NodeCode == node.NodeTypeSenseOptimizeModel ||
item.NodeCode == node.NodeTypeStoryOptimizeModel ||
item.NodeCode == node.NodeTypeScriptOptimizeModel {
item.ModelConfig = append(item.ModelConfig, node.ModelItem{
ModelName: "自定义",
})
}
if item.NodeCode == node.NodeTypeImageModel {
item.ModelConfig = append(item.ModelConfig, node.ModelItem{
ModelName: "自定义",
})
}
}
}

View File

@@ -0,0 +1,123 @@
package node
import (
"ai-agent/workflow/consts/node"
nodeDao "ai-agent/workflow/dao/node"
nodeDto "ai-agent/workflow/model/dto/node"
"ai-agent/workflow/service"
"context"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/frame/g"
)
var NodePromptService = &nodePromptService{}
type nodePromptService struct{}
// Create 创建节点提示词
func (s *nodePromptService) Create(ctx context.Context, req *nodeDto.CreateNodePromptReq) (res *nodeDto.CreateNodePromptRes, err error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
get, err := nodeDao.NodePromptDao.Get(ctx, &nodeDto.GetNodePromptReq{Prompt: req.Prompt, Creator: user.UserName})
if err != nil {
return
}
if g.IsEmpty(get) {
var isAdmin bool
isAdmin, err = service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
if isAdmin {
req.SourceType = node.SourceTypeSystem.Code()
} else {
req.SourceType = node.SourceTypeUser.Code()
}
var id int64
id, err = nodeDao.NodePromptDao.Insert(ctx, req)
if err != nil {
return
}
return &nodeDto.CreateNodePromptRes{Id: id}, err
}
return
}
// Update 更新节点提示词
func (s *nodePromptService) Update(ctx context.Context, req *nodeDto.UpdateNodePromptReq) (err error) {
get, err := nodeDao.NodePromptDao.Get(ctx, &nodeDto.GetNodePromptReq{Id: req.Id})
if err != nil {
return
}
isAdmin, err := service.UtilService.IsAdmin(ctx)
if err != nil {
return
}
if !isAdmin && get.SourceType == node.SourceTypeSystem.Code() {
_, err = s.Create(ctx, &nodeDto.CreateNodePromptReq{
NodeType: req.NodeType,
Prompt: req.Prompt,
})
} else {
_, err = nodeDao.NodePromptDao.Update(ctx, req)
}
return
}
// Delete 删除节点提示词
func (s *nodePromptService) Delete(ctx context.Context, req *nodeDto.DeleteNodePromptReq) (err error) {
_, err = nodeDao.NodePromptDao.Delete(ctx, req)
return
}
// GetById 根据ID查询节点提示词
func (s *nodePromptService) GetById(ctx context.Context, req *nodeDto.GetNodePromptReq) (res *nodeDto.NodePromptResp, err error) {
r, err := nodeDao.NodePromptDao.Get(ctx, req)
if err != nil {
return nil, err
}
res = &nodeDto.NodePromptResp{
NodePrompt: r,
}
return
}
// ListMy 查询当前创建人自己创建的提示词列表
func (s *nodePromptService) ListMy(ctx context.Context, req *nodeDto.ListMyNodePromptReq) (*nodeDto.ListNodePromptResp, error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
req.Creator = user.UserName
list, total, err := nodeDao.NodePromptDao.ListByOnlyCreator(ctx, req)
if err != nil {
return nil, err
}
return &nodeDto.ListNodePromptResp{
List: list,
Total: total,
}, nil
}
// ListWithSystem 查询当前创建人+系统的提示词列表
func (s *nodePromptService) ListWithSystem(ctx context.Context, req *nodeDto.ListNodePromptReq) (*nodeDto.ListNodePromptResp, error) {
user, err := utils.GetUserInfo(ctx)
if err != nil {
return nil, err
}
// 如果请求中没有传creator使用当前用户
if g.IsEmpty(req.Creator) {
req.Creator = user.UserName
}
list, total, err := nodeDao.NodePromptDao.ListByCreator(ctx, req)
if err != nil {
return nil, err
}
return &nodeDto.ListNodePromptResp{
List: list,
Total: total,
}, nil
}