feat: 新增执行记录实体与文件上传能力

This commit is contained in:
2026-08-18 09:47:51 +08:00
parent 155a5cad0c
commit a939c45508
67 changed files with 7341 additions and 4001 deletions
+50 -365
View File
@@ -1,14 +1,14 @@
package node
import (
"context"
"strconv"
"ai-agent/gateway"
"ai-agent/workflow/consts/model"
"ai-agent/workflow/consts/node"
nodeDto "ai-agent/workflow/model/dto/node"
"context"
"fmt"
"gitea.redpowerfuture.com/red-future/common/beans"
commonHttp "gitea.redpowerfuture.com/red-future/common/http"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
)
@@ -16,370 +16,55 @@ var NodeLibraryService = &nodeLibraryService{}
type nodeLibraryService struct{}
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: false,
PromptOption: true,
IsSaveFile: 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{},
},
},
},
{
Group: node.NodeGroupBase,
Label: node.NodeGroupNameBase,
Items: []node.NodeItem{
{
NodeCode: node.NodeTypeSubFlow,
NodeName: node.NodeSubFlow,
SkillOption: false,
PromptOption: false,
IsSaveFile: false,
FormConfig: []node.NodeFormField{
{Field: "maxConcurrency", Label: "最大并发数", Type: "input", Required: true},
},
ModelConfig: []node.ModelItem{},
},
{
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,
SkillOption: false,
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,
SkillOption: false,
FormConfig: []node.NodeFormField{
{Field: "condition", Label: node.FormLabelCondition, Type: "input", Required: true},
},
ModelConfig: []node.ModelItem{},
},
{
NodeCode: node.NodeTypeForm,
NodeName: node.NodeNameForm,
SkillOption: false,
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,
// SkillOption: true,
// FormConfig: []node.NodeFormField{},
// 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 := node.GetFilterNodeTree([]node.NodeGroup{node.NodeGroupBase})
if opts, err := videoModelOptions(ctx); err != nil {
g.Log().Warningf(ctx, "加载视频模型选项失败,节点库降级返回: %v", err)
} else {
applyVideoModelOptions(tree, opts)
}
tree := &nodeDto.WorkflowNodeTreeRes{
Groups: WorkflowNodeGroups,
}
// 3. 遍历分组,根据 typeId=1 给【文本模型节点】追加固定表单
for gIdx := range tree.Groups {
group := &tree.Groups[gIdx]
// 遍历分组下的每个节点
for itemIdx := range group.Items {
item := &group.Items[itemIdx]
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.ModelConfig = append(item.ModelConfig, node.ModelItem{
ModelName: "自定义",
})
}
}
}
return tree, nil
return &nodeDto.WorkflowNodeTreeRes{
Groups: tree,
}, nil
}
// SetUserInfo 设置用户信息
func (s *nodeLibraryService) SetUserInfo(ctx context.Context, creator string, tenantId uint64) (headers map[string]string, err error) {
// 创建完整的用户信息
userInfo := &beans.User{
UserName: creator,
TenantId: tenantId,
}
ctx = context.WithValue(ctx, "user", *userInfo)
// 提取并保存请求头(在连接升级前)
headers = make(map[string]string)
// 提取其他headers
if r := g.RequestFromCtx(ctx); r != nil {
for k, v := range r.Request.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
}
// 将完整用户信息序列化为JSON,放到X-User-Info请求头
userInfoJson, err := gjson.Encode(userInfo)
// videoModelOptions 按视频模型类型(600)查模型网关,转成 modelId 下拉选项:key=模型IDvalue=模型名称。
func videoModelOptions(ctx context.Context) ([]node.SelectOption, error) {
res, err := gateway.ListModelManage(ctx, &gateway.ListModelManageReq{ModelType: model.TypeVideo})
if err != nil {
return nil, fmt.Errorf("用户信息序列化失败: %w", err)
return nil, err
}
opts := make([]node.SelectOption, 0, len(res.List))
for _, item := range res.List {
if item == nil || item.Id <= 0 || item.ModelName == "" {
continue
}
opts = append(opts, node.SelectOption{
Key: strconv.FormatInt(item.Id, 10),
Value: item.ModelName,
})
}
return opts, nil
}
// applyVideoModelOptions 把 script_transcribe 节点的 modelId 预设下拉替换为视频模型选项。
// 深拷贝 PresetOption 后再改,避免改写全局 NodeTypeMetaList。
func applyVideoModelOptions(tree []node.NodeGroupTree, opts []node.SelectOption) {
for gi := range tree {
for ni := range tree[gi].Nodes {
n := &tree[gi].Nodes[ni]
if n.Key != node.NodeTypeScriptTranscribe {
continue
}
presets := append([]node.NodePresetField(nil), n.PresetOption...)
for pi := range presets {
if presets[pi].Field == "modelId" {
presets[pi].Options = opts
break
}
}
n.PresetOption = presets
return
}
}
headers["X-User-Info"] = string(userInfoJson)
return
}