Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
40a8a809
authored
Jun 09, 2026
by
Archer
Committed by
GitHub
Jun 09, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: load agent runtime tool schemas (#7077)
* fix: load agent runtime tool schemas * perf: create api
parent
f9ba442c
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
664 additions
and
105 deletions
+664
-105
packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts
+403
-102
packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts
+247
-0
pro
+1
-1
projects/app/src/pages/api/core/app/create.ts
+13
-2
No files found.
packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts
View file @
40a8a809
import
type
{
SkillToolType
}
from
'@fastgpt/global/core/ai/skill/type'
;
import
type
{
SkillToolType
}
from
'@fastgpt/global/core/ai/skill/type'
;
import
{
splitCombineToolId
}
from
'@fastgpt/global/core/app/tool/utils'
;
import
{
splitCombineToolId
}
from
'@fastgpt/global/core/app/tool/utils'
;
import
type
{
localeType
}
from
'@fastgpt/global/common/i18n/type'
;
import
type
{
localeType
}
from
'@fastgpt/global/common/i18n/type'
;
import
{
getChildAppPreviewNode
}
from
'../../../../../../app/tool/controller'
;
import
{
ReadPermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
ReadPermissionVal
}
from
'@fastgpt/global/support/permission/constant'
;
import
{
authAppByTmbId
}
from
'../../../../../../../support/permission/app/auth'
;
import
{
authAppByTmbId
}
from
'../../../../../../../support/permission/app/auth'
;
import
{
getErrText
}
from
'@fastgpt/global/common/error/utils'
;
import
{
getErrText
}
from
'@fastgpt/global/common/error/utils'
;
import
{
FlowNodeTypeEnum
}
from
'@fastgpt/global/core/workflow/node/constant'
;
import
{
FlowNodeInputTypeEnum
,
FlowNodeOutputTypeEnum
,
FlowNodeTypeEnum
}
from
'@fastgpt/global/core/workflow/node/constant'
;
import
{
getSystemToolRunTimeNodeFromSystemToolset
}
from
'../../../../../../workflow/utils'
;
import
{
getSystemToolRunTimeNodeFromSystemToolset
}
from
'../../../../../../workflow/utils'
;
import
{
MongoApp
}
from
'../../../../../../app/schema'
;
import
{
getMCPChildren
}
from
'../../../../../../app/mcp'
;
import
{
getMCPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/mcpTool/utils'
;
import
{
getMCPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/mcpTool/utils'
;
import
{
getHTTPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/httpTool/utils'
;
import
{
getHTTPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/httpTool/utils'
;
import
type
{
ChatCompletionTool
}
from
'@fastgpt/global/core/ai/llm/type'
;
import
type
{
ChatCompletionTool
}
from
'@fastgpt/global/core/ai/llm/type'
;
...
@@ -19,13 +20,44 @@ import {
...
@@ -19,13 +20,44 @@ import {
toolValueTypeList
,
toolValueTypeList
,
valueTypeJsonSchemaMap
valueTypeJsonSchemaMap
}
from
'@fastgpt/global/core/workflow/constants'
;
}
from
'@fastgpt/global/core/workflow/constants'
;
import
type
{
McpToolDataType
}
from
'@fastgpt/global/core/app/tool/mcpTool/type'
;
import
type
{
McpToolConfigType
,
McpToolDataType
}
from
'@fastgpt/global/core/app/tool/mcpTool/type'
;
import
type
{
HttpToolConfigType
}
from
'@fastgpt/global/core/app/tool/httpTool/type'
;
import
type
{
HttpToolConfigType
}
from
'@fastgpt/global/core/app/tool/httpTool/type'
;
import
type
{
SubAppInitType
}
from
'../type'
;
import
type
{
SubAppInitType
}
from
'../type'
;
import
{
getToolConfigStatus
}
from
'@fastgpt/global/core/app/formEdit/utils'
;
import
{
getToolConfigStatus
}
from
'@fastgpt/global/core/app/formEdit/utils'
;
import
{
getLogger
,
LogCategories
}
from
'../../../../../../../common/logger'
;
import
{
getLogger
,
LogCategories
}
from
'../../../../../../../common/logger'
;
import
{
AppToolSourceEnum
}
from
'@fastgpt/global/core/app/tool/constants'
;
import
{
AppToolSourceEnum
}
from
'@fastgpt/global/core/app/tool/constants'
;
import
type
{
RuntimeNodeItemType
}
from
'@fastgpt/global/core/workflow/runtime/type'
;
import
{
appData2FlowNodeIO
,
pluginData2FlowNodeIO
,
toolData2FlowNodeIO
}
from
'@fastgpt/global/core/workflow/utils'
;
import
type
{
AppSchemaType
}
from
'@fastgpt/global/core/app/type'
;
import
{
getAppVersionById
}
from
'../../../../../../app/version/controller'
;
import
{
AppFolderTypeList
}
from
'@fastgpt/global/core/app/constants'
;
import
{
PluginErrEnum
}
from
'@fastgpt/global/common/error/code/plugin'
;
import
{
parseI18nString
}
from
'@fastgpt/global/common/i18n/utils'
;
import
{
SystemToolRepo
}
from
'../../../../../../app/tool/systemTool/systemTool.repo'
;
import
{
Output_Template_Error_Message
}
from
'@fastgpt/global/core/workflow/template/output'
;
import
type
{
NodeToolConfigType
}
from
'@fastgpt/global/core/workflow/type/node'
;
type
AgentRuntimeNode
=
RuntimeNodeItemType
&
{
currentCost
?:
number
;
hasSystemSecret
?:
boolean
;
hasTokenFee
?:
boolean
;
systemKeyCost
?:
number
;
};
/**
* 将 Agent 选择的工具配置转换成 LLM function calling 与 runtime 执行共用的工具描述。
*
* 这里刻意不调用面向前端预览的 getChildAppPreviewNode:Agent runtime 只关心鉴权后的执行
* 节点、toolConfig 和 JSON Schema。App 类工具统一读取当前发布版本;MCP/HTTP 工具集只使用
* 当前版本节点里保存的 toolList,不额外兼容旧版 MCP 数据源。
*/
export
const
getAgentRuntimeTools
=
async
({
export
const
getAgentRuntimeTools
=
async
({
tools
,
tools
,
tmbId
,
tmbId
,
...
@@ -35,13 +67,312 @@ export const getAgentRuntimeTools = async ({
...
@@ -35,13 +67,312 @@ export const getAgentRuntimeTools = async ({
tmbId
:
string
;
tmbId
:
string
;
lang
?:
localeType
;
lang
?:
localeType
;
}):
Promise
<
SubAppInitType
[]
>
=>
{
}):
Promise
<
SubAppInitType
[]
>
=>
{
// Agent 工具执行需要统一的错误输出口,方便 workflow runtime 收敛失败结果。
const
appendErrorOutput
=
(
outputs
:
RuntimeNodeItemType
[
'outputs'
]
=
[])
=>
{
return
outputs
.
some
((
item
)
=>
item
.
type
===
FlowNodeOutputTypeEnum
.
error
)
?
outputs
:
[...
outputs
,
Output_Template_Error_Message
];
};
// toolsetData2FlowNodeIO 偏前端展示,会丢掉部分 schema 相关信息;runtime 直接保留节点 IO。
const
getToolSetNodeIO
=
({
nodes
}:
{
nodes
:
AppSchemaType
[
'modules'
];
}):
{
inputs
:
RuntimeNodeItemType
[
'inputs'
];
outputs
:
RuntimeNodeItemType
[
'outputs'
];
toolConfig
?:
NodeToolConfigType
;
}
=>
{
const
toolSetNode
=
nodes
.
find
((
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
toolSet
);
return
{
inputs
:
toolSetNode
?.
inputs
||
[],
outputs
:
toolSetNode
?.
outputs
||
[],
toolConfig
:
toolSetNode
?.
toolConfig
};
};
/**
* 系统工具和商业工具来自 SystemToolRepo,不需要 App 鉴权和版本查询。
* associatedPluginId 表示这个系统能力最终应按插件工作流执行。
*/
const
formatSystemToolNode
=
async
({
toolId
,
nodeId
,
source
}:
{
toolId
:
string
;
nodeId
:
string
;
source
:
AppToolSourceEnum
.
systemTool
|
AppToolSourceEnum
.
commercial
;
}):
Promise
<
AgentRuntimeNode
>
=>
{
const
systemToolRepo
=
SystemToolRepo
.
getInstance
();
const
toolDetail
=
await
systemToolRepo
.
getSystemToolDetail
({
pluginId
:
toolId
,
lang
,
source
:
source
===
AppToolSourceEnum
.
commercial
?
AppToolSourceEnum
.
commercial
:
'system'
});
const
isWorkflowTool
=
!!
toolDetail
.
associatedPluginId
;
// secrets 是运行时私密配置,只放进隐藏 input,不能暴露成模型可填写参数。
const
inputs
=
[
...(
toolDetail
.
secrets
?.
length
?
[
{
key
:
NodeInputKeyEnum
.
systemInputConfig
,
label
:
''
,
renderTypeList
:
[
FlowNodeInputTypeEnum
.
hidden
],
inputList
:
toolDetail
.
secrets
}
satisfies
FlowNodeInputItemType
]
:
[]),
...(
toolDetail
.
inputs
??
[])
];
return
{
nodeId
,
pluginId
:
toolId
,
flowNodeType
:
isWorkflowTool
?
FlowNodeTypeEnum
.
pluginModule
:
toolDetail
.
isToolSet
?
FlowNodeTypeEnum
.
toolSet
:
FlowNodeTypeEnum
.
tool
,
avatar
:
toolDetail
.
avatar
,
name
:
toolDetail
.
name
,
intro
:
toolDetail
.
intro
,
toolDescription
:
toolDetail
.
toolDescription
,
version
:
''
,
inputs
,
outputs
:
appendErrorOutput
(
toolDetail
.
outputs
??
[]),
currentCost
:
toolDetail
.
currentCost
,
hasSystemSecret
:
toolDetail
.
hasSystemSecret
,
hasTokenFee
:
toolDetail
.
hasTokenFee
,
systemKeyCost
:
toolDetail
.
systemKeyCost
,
...(
isWorkflowTool
?
{}
:
{
toolConfig
:
{
...(
toolDetail
.
isToolSet
?
{
systemToolSet
:
{
toolId
,
toolList
:
toolDetail
.
children
?.
map
((
child
)
=>
({
description
:
child
.
description
??
''
,
name
:
child
.
name
,
toolId
:
child
.
id
}))
??
[]
}
}
:
{
systemTool
:
{
toolId
}
})
}
})
};
};
// 当前 Agent 工具始终取 App 的当前发布版本;没有发布版本时由 controller 回落到 app.modules。
const
getVersionNodes
=
async
({
app
}:
{
app
:
AppSchemaType
})
=>
{
const
version
=
await
getAppVersionById
({
appId
:
String
(
app
.
_id
),
app
});
return
{
...
version
,
nodes
:
version
.
nodes
};
};
/**
* 普通 App 需要根据当前版本节点形态判断运行时类型:
* - pluginInput: 插件工作流
* - 单 toolSet: MCP/HTTP/System toolset
* - 单 tool: 普通工具节点
* - 其他: 子应用 workflow
*/
const
formatPersonalAppNode
=
async
({
app
}:
{
app
:
AppSchemaType
;
}):
Promise
<
AgentRuntimeNode
>
=>
{
if
(
AppFolderTypeList
.
includes
(
app
.
type
))
{
return
Promise
.
reject
(
PluginErrEnum
.
unExist
);
}
const
version
=
await
getVersionNodes
({
app
});
const
nodes
=
version
.
nodes
;
const
baseNode
=
{
nodeId
:
String
(
app
.
_id
),
pluginId
:
String
(
app
.
_id
),
avatar
:
app
.
avatar
,
name
:
parseI18nString
(
app
.
name
,
lang
),
intro
:
parseI18nString
(
app
.
intro
,
lang
),
version
:
''
,
showStatus
:
true
,
inputs
:
[],
outputs
:
[]
};
// pluginInput 是插件工作流的显式标识,后续执行走 dispatchPlugin。
if
(
nodes
.
find
((
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
pluginInput
))
{
const
nodeIO
=
pluginData2FlowNodeIO
({
nodes
});
return
{
...
baseNode
,
flowNodeType
:
FlowNodeTypeEnum
.
pluginModule
,
inputs
:
nodeIO
.
inputs
,
outputs
:
appendErrorOutput
(
nodeIO
.
outputs
)
};
}
const
toolSetNode
=
nodes
.
find
((
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
toolSet
);
if
(
toolSetNode
&&
nodes
.
length
===
1
)
{
// MCP/HTTP toolset 的 json schema 保存在 toolConfig.toolList 上,必须保留原始 toolConfig。
const
nodeIO
=
getToolSetNodeIO
({
nodes
});
return
{
...
baseNode
,
flowNodeType
:
FlowNodeTypeEnum
.
toolSet
,
inputs
:
nodeIO
.
inputs
,
outputs
:
appendErrorOutput
(
nodeIO
.
outputs
),
toolConfig
:
nodeIO
.
toolConfig
};
}
const
toolNode
=
nodes
.
find
((
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
tool
);
if
(
toolNode
&&
nodes
.
length
===
1
)
{
// 单工具节点可能直接带 jsonSchema,优先作为 function parameters 使用。
const
nodeIO
=
toolData2FlowNodeIO
({
nodes
});
return
{
...
baseNode
,
flowNodeType
:
FlowNodeTypeEnum
.
tool
,
inputs
:
nodeIO
.
inputs
,
outputs
:
appendErrorOutput
(
nodeIO
.
outputs
),
toolConfig
:
nodeIO
.
toolConfig
,
jsonSchema
:
(
toolNode
as
RuntimeNodeItemType
).
jsonSchema
};
}
const
nodeIO
=
appData2FlowNodeIO
({
chatConfig
:
version
.
chatConfig
});
return
{
...
baseNode
,
flowNodeType
:
FlowNodeTypeEnum
.
appModule
,
inputs
:
nodeIO
.
inputs
,
outputs
:
appendErrorOutput
(
nodeIO
.
outputs
)
};
};
/**
* 解析单个 MCP 工具 id: mcp-${appId}/${toolName}。
* 只读取当前版本 toolConfig.mcpToolSet.toolList;不再通过 getMCPChildren 补旧版数据。
*/
const
formatMcpToolNode
=
async
({
app
,
pluginId
}:
{
app
:
AppSchemaType
;
pluginId
:
string
;
}):
Promise
<
AgentRuntimeNode
>
=>
{
const
[,
...
toolNameParts
]
=
pluginId
.
split
(
'/'
);
const
toolName
=
toolNameParts
.
join
(
'/'
);
const
version
=
await
getVersionNodes
({
app
});
const
toolList
=
version
.
nodes
[
0
]?.
toolConfig
?.
mcpToolSet
?.
toolList
??
[];
const
tool
=
toolList
.
find
((
item
)
=>
item
.
name
===
toolName
);
if
(
!
tool
)
return
Promise
.
reject
(
PluginErrEnum
.
unExist
);
const
node
=
getMCPToolRuntimeNode
({
nodeId
:
pluginId
.
replace
(
/
[^
a-zA-Z0-9_-
]
/g
,
''
),
toolSetId
:
String
(
app
.
_id
),
toolsetName
:
app
.
name
,
avatar
:
app
.
avatar
,
tool
});
// 单独选择子工具时,模型侧展示子工具名即可,不需要带 toolset 前缀。
return
{
...
node
,
name
:
tool
.
name
,
intro
:
tool
.
description
};
};
/**
* 解析单个 HTTP 工具 id: http-${appId}/${toolName}。
* HTTP function parameters 使用 requestSchema,而不是展示表单用的 inputSchema。
*/
const
formatHttpToolNode
=
async
({
app
,
pluginId
}:
{
app
:
AppSchemaType
;
pluginId
:
string
;
}):
Promise
<
AgentRuntimeNode
>
=>
{
const
[,
...
toolNameParts
]
=
pluginId
.
split
(
'/'
);
const
toolName
=
toolNameParts
.
join
(
'/'
);
const
version
=
await
getVersionNodes
({
app
});
const
tool
=
version
.
nodes
[
0
]?.
toolConfig
?.
httpToolSet
?.
toolList
.
find
(
(
item
)
=>
item
.
name
===
toolName
);
if
(
!
tool
)
return
Promise
.
reject
(
PluginErrEnum
.
unExist
);
const
node
=
getHTTPToolRuntimeNode
({
nodeId
:
pluginId
.
replace
(
/
[^
a-zA-Z0-9_-
]
/g
,
''
),
toolSetId
:
String
(
app
.
_id
),
toolsetName
:
app
.
name
,
avatar
:
app
.
avatar
,
tool
});
// 单独选择子工具时,模型侧展示子工具名即可,不需要带 toolset 前缀。
return
{
...
node
,
name
:
tool
.
name
,
intro
:
tool
.
description
};
};
const
getRuntimeToolNode
=
async
({
source
,
pluginId
,
toolId
,
app
}:
{
source
:
AppToolSourceEnum
;
pluginId
:
string
;
toolId
:
string
;
app
?:
AppSchemaType
;
}):
Promise
<
AgentRuntimeNode
>
=>
{
// Agent 运行时只需要节点执行和 schema 信息,不能依赖面向前端展示的 preview controller。
if
(
source
===
AppToolSourceEnum
.
systemTool
||
source
===
AppToolSourceEnum
.
commercial
)
{
return
formatSystemToolNode
({
toolId
,
nodeId
:
pluginId
,
source
});
}
if
(
!
app
)
return
Promise
.
reject
(
PluginErrEnum
.
unExist
);
if
(
source
===
AppToolSourceEnum
.
mcp
)
{
return
formatMcpToolNode
({
app
,
pluginId
});
}
if
(
source
===
AppToolSourceEnum
.
http
)
{
return
formatHttpToolNode
({
app
,
pluginId
});
}
return
formatPersonalAppNode
({
app
});
};
/**
* 生成 OpenAI-compatible function schema。
* schema 优先级:显式 jsonSchema > toolData.inputSchema > 带 toolDescription 的普通 inputs。
*/
const
formatSchema
=
({
const
formatSchema
=
({
toolId
,
toolId
,
inputs
,
inputs
,
flowNodeType
,
flowNodeType
,
name
,
name
,
toolDescription
,
toolDescription
,
intro
intro
,
jsonSchema
}:
{
}:
{
toolId
:
string
;
toolId
:
string
;
inputs
:
FlowNodeInputItemType
[];
inputs
:
FlowNodeInputItemType
[];
...
@@ -49,17 +380,19 @@ export const getAgentRuntimeTools = async ({
...
@@ -49,17 +380,19 @@ export const getAgentRuntimeTools = async ({
name
:
string
;
name
:
string
;
toolDescription
?:
string
;
toolDescription
?:
string
;
intro
?:
string
;
intro
?:
string
;
jsonSchema
?:
JSONSchemaInputType
;
}):
ChatCompletionTool
=>
{
}):
ChatCompletionTool
=>
{
const
toolParams
:
FlowNodeInputItemType
[]
=
[];
const
toolParams
:
FlowNodeInputItemType
[]
=
[];
let
jsonSchema
:
JSONSchemaInputType
|
undefined
;
let
schema
=
jsonSchema
;
for
(
const
input
of
inputs
)
{
for
(
const
input
of
inputs
)
{
// 没有 JSON Schema 的普通工具,用 toolDescription 标识哪些 input 可以交给模型填写。
if
(
input
.
toolDescription
)
{
if
(
input
.
toolDescription
)
{
toolParams
.
push
(
input
);
toolParams
.
push
(
input
);
}
}
if
(
input
.
key
===
NodeInputKeyEnum
.
toolData
)
{
if
(
!
schema
&&
input
.
key
===
NodeInputKeyEnum
.
toolData
)
{
jsonS
chema
=
(
input
.
value
as
McpToolDataType
)?.
inputSchema
;
s
chema
=
(
input
.
value
as
McpToolDataType
)?.
inputSchema
;
}
}
}
}
...
@@ -68,15 +401,16 @@ export const getAgentRuntimeTools = async ({
...
@@ -68,15 +401,16 @@ export const getAgentRuntimeTools = async ({
name
:
name
,
name
:
name
,
intro
:
toolDescription
||
intro
intro
:
toolDescription
||
intro
});
});
const
formatToolId
=
`t
${
toolId
}
`
;
// 仅数字开头的工具名需要补前缀,避免破坏 runtime 使用原始 tool id 反查工具。
const
formatToolId
=
/^
\d
/
.
test
(
toolId
)
?
`t
${
toolId
}
`
:
toolId
;
if
(
jsonS
chema
)
{
if
(
s
chema
)
{
return
{
return
{
type
:
'function'
,
type
:
'function'
,
function
:
{
function
:
{
name
:
formatToolId
,
name
:
formatToolId
,
description
,
description
,
parameters
:
jsonS
chema
parameters
:
s
chema
}
}
};
};
}
}
...
@@ -112,31 +446,37 @@ export const getAgentRuntimeTools = async ({
...
@@ -112,31 +446,37 @@ export const getAgentRuntimeTools = async ({
tools
.
map
<
Promise
<
SubAppInitType
[]
>>
(
async
(
tool
)
=>
{
tools
.
map
<
Promise
<
SubAppInitType
[]
>>
(
async
(
tool
)
=>
{
try
{
try
{
const
{
pluginId
,
authAppId
,
source
}
=
splitCombineToolId
(
tool
.
id
);
const
{
pluginId
,
authAppId
,
source
}
=
splitCombineToolId
(
tool
.
id
);
// 工具间整体并发;单个 App 类工具必须先鉴权拿到 app,才能读取对应版本节点。
const
authAppPromise
=
authAppId
?
authAppByTmbId
({
tmbId
,
appId
:
authAppId
,
per
:
ReadPermissionVal
})
:
Promise
.
resolve
(
undefined
);
const
[
toolNode
]
=
await
Promise
.
all
([
const
[
authResult
,
toolNode
]
=
await
Promise
.
all
([
getChildAppPreviewNode
({
authAppPromise
,
appId
:
tool
.
id
,
authAppPromise
.
then
((
authResult
)
=>
versionId
:
''
,
getRuntimeToolNode
({
lang
source
,
}),
pluginId
,
...(
authAppId
app
:
authResult
?.
app
,
?
[
toolId
:
tool
.
id
authAppByTmbId
({
})
tmbId
,
)
appId
:
authAppId
,
per
:
ReadPermissionVal
})
]
:
[])
]);
]);
// 1. Add config value to toolNode.inputs
const
authApp
=
authResult
?.
app
;
// 合并用户在 Agent 工具面板里保存的配置;false/0/空字符串也是有效配置值。
toolNode
.
inputs
.
forEach
((
input
)
=>
{
toolNode
.
inputs
.
forEach
((
input
)
=>
{
const
value
=
tool
.
config
[
input
.
key
];
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
tool
.
config
,
input
.
key
))
{
if
(
value
)
{
const
value
=
tool
.
config
[
input
.
key
];
input
.
value
=
value
;
input
.
value
=
value
;
}
}
});
});
// 2. Check config status
// 缺少必填运行配置时,不把该工具注册给模型,避免模型调用后才失败。
const
configStatus
=
getToolConfigStatus
({
const
configStatus
=
getToolConfigStatus
({
tool
:
toolNode
tool
:
toolNode
});
});
...
@@ -161,12 +501,33 @@ export const getAgentRuntimeTools = async ({
...
@@ -161,12 +501,33 @@ export const getAgentRuntimeTools = async ({
return
'tool'
;
return
'tool'
;
})();
})();
// toolset 展开后的子工具统一走 tool 执行;params 仍继承父工具配置。
const
buildSubApp
=
(
child
:
RuntimeNodeItemType
,
id
=
child
.
nodeId
):
SubAppInitType
=>
({
type
:
'tool'
,
id
,
name
:
child
.
name
,
avatar
:
child
.
avatar
,
version
:
child
.
version
,
toolConfig
:
child
.
toolConfig
,
params
:
tool
.
config
,
requestSchema
:
formatSchema
({
toolId
:
id
,
inputs
:
child
.
inputs
,
flowNodeType
:
child
.
flowNodeType
,
name
:
child
.
name
,
toolDescription
:
child
.
toolDescription
,
intro
:
child
.
intro
,
jsonSchema
:
child
.
jsonSchema
})
});
if
(
toolNode
.
flowNodeType
===
FlowNodeTypeEnum
.
toolSet
)
{
if
(
toolNode
.
flowNodeType
===
FlowNodeTypeEnum
.
toolSet
)
{
const
systemToolId
=
toolNode
.
toolConfig
?.
systemToolSet
?.
toolId
;
const
systemToolId
=
toolNode
.
toolConfig
?.
systemToolSet
?.
toolId
;
const
mcpToolsetVal
=
toolNode
.
toolConfig
?.
mcpToolSet
??
toolNode
.
inputs
[
0
]?.
value
;
const
mcpToolsetVal
=
toolNode
.
toolConfig
?.
mcpToolSet
??
toolNode
.
inputs
[
0
]?.
value
;
const
httpToolsetVal
=
toolNode
.
toolConfig
?.
httpToolSet
;
const
httpToolsetVal
=
toolNode
.
toolConfig
?.
httpToolSet
;
if
(
systemToolId
)
{
if
(
systemToolId
)
{
// System toolset 的子工具由系统工具仓库展开,可能包含内置运行配置。
const
children
=
await
getSystemToolRunTimeNodeFromSystemToolset
({
const
children
=
await
getSystemToolRunTimeNodeFromSystemToolset
({
toolSetNode
:
{
toolSetNode
:
{
toolConfig
:
toolNode
.
toolConfig
,
toolConfig
:
toolNode
.
toolConfig
,
...
@@ -176,29 +537,12 @@ export const getAgentRuntimeTools = async ({
...
@@ -176,29 +537,12 @@ export const getAgentRuntimeTools = async ({
lang
lang
});
});
return
children
.
map
((
child
)
=>
({
return
children
.
map
((
child
)
=>
buildSubApp
(
child
));
type
:
'tool'
,
id
:
child
.
nodeId
,
name
:
child
.
name
,
avatar
:
child
.
avatar
,
version
:
child
.
version
,
toolConfig
:
child
.
toolConfig
,
params
:
tool
.
config
,
requestSchema
:
formatSchema
({
toolId
:
child
.
nodeId
,
inputs
:
child
.
inputs
,
flowNodeType
:
child
.
flowNodeType
,
name
:
child
.
name
,
toolDescription
:
child
.
toolDescription
,
intro
:
child
.
intro
})
}));
}
else
if
(
mcpToolsetVal
)
{
}
else
if
(
mcpToolsetVal
)
{
const
app
=
await
MongoApp
.
findOne
({
_id
:
toolNode
.
pluginId
}).
lean
();
// MCP toolset 已在当前版本节点保存 toolList;展开时不再触发 DB 查询。
if
(
!
app
)
return
[];
const
toolList
:
McpToolConfigType
[]
=
mcpToolsetVal
.
toolList
??
[];
const
toolList
=
await
getMCPChildren
(
app
);
const
toolSetId
=
mcpToolsetVal
.
toolId
||
toolNode
.
pluginId
;
const
toolSetId
=
mcpToolsetVal
.
toolId
||
toolNode
.
pluginId
||
pluginId
;
const
children
=
toolList
.
map
((
tool
,
index
)
=>
{
const
children
=
toolList
.
map
((
tool
,
index
)
=>
{
const
newToolNode
=
getMCPToolRuntimeNode
({
const
newToolNode
=
getMCPToolRuntimeNode
({
toolSetId
,
toolSetId
,
...
@@ -210,26 +554,9 @@ export const getAgentRuntimeTools = async ({
...
@@ -210,26 +554,9 @@ export const getAgentRuntimeTools = async ({
return
newToolNode
;
return
newToolNode
;
});
});
return
children
.
map
((
child
)
=>
{
return
children
.
map
((
child
)
=>
buildSubApp
(
child
));
return
{
type
:
'tool'
,
id
:
child
.
nodeId
,
name
:
child
.
name
,
avatar
:
child
.
avatar
,
version
:
child
.
version
,
toolConfig
:
child
.
toolConfig
,
params
:
tool
.
config
,
requestSchema
:
formatSchema
({
toolId
:
child
.
nodeId
,
inputs
:
child
.
inputs
,
flowNodeType
:
child
.
flowNodeType
,
name
:
child
.
name
,
toolDescription
:
child
.
toolDescription
,
intro
:
child
.
intro
})
};
});
}
else
if
(
httpToolsetVal
)
{
}
else
if
(
httpToolsetVal
)
{
// HTTP toolset 的 requestSchema 在 getHTTPToolRuntimeNode 中写入 jsonSchema。
const
children
=
httpToolsetVal
.
toolList
.
map
((
tool
:
HttpToolConfigType
,
index
)
=>
{
const
children
=
httpToolsetVal
.
toolList
.
map
((
tool
:
HttpToolConfigType
,
index
)
=>
{
const
newToolNode
=
getHTTPToolRuntimeNode
({
const
newToolNode
=
getHTTPToolRuntimeNode
({
tool
,
tool
,
...
@@ -241,39 +568,12 @@ export const getAgentRuntimeTools = async ({
...
@@ -241,39 +568,12 @@ export const getAgentRuntimeTools = async ({
return
newToolNode
;
return
newToolNode
;
});
});
return
children
.
map
((
child
)
=>
{
return
children
.
map
((
child
)
=>
buildSubApp
(
child
));
return
{
type
:
'tool'
,
id
:
child
.
nodeId
,
name
:
child
.
name
,
avatar
:
child
.
avatar
,
version
:
child
.
version
,
toolConfig
:
child
.
toolConfig
,
params
:
tool
.
config
,
requestSchema
:
formatSchema
({
toolId
:
child
.
nodeId
,
inputs
:
child
.
inputs
,
flowNodeType
:
child
.
flowNodeType
,
name
:
child
.
name
,
toolDescription
:
child
.
toolDescription
,
intro
:
child
.
intro
})
};
});
}
}
return
[];
return
[];
}
}
else
{
// OpenAI function name 不能包含斜杠等字符,runtime map 也使用同一份清洗后的 id。
// else if (source === AppToolSourceEnum.commercial) {
// const systemToolRepo = SystemToolRepo.getInstance();
// const detail = await systemToolRepo.getSystemToolDetail({
// pluginId
// })
// }
else
{
const
cleanedPluginId
=
pluginId
.
replace
(
/
[^
a-zA-Z0-9_-
]
/g
,
''
);
const
cleanedPluginId
=
pluginId
.
replace
(
/
[^
a-zA-Z0-9_-
]
/g
,
''
);
return
[
return
[
...
@@ -291,7 +591,8 @@ export const getAgentRuntimeTools = async ({
...
@@ -291,7 +591,8 @@ export const getAgentRuntimeTools = async ({
flowNodeType
:
toolNode
.
flowNodeType
,
flowNodeType
:
toolNode
.
flowNodeType
,
name
:
toolNode
.
name
,
name
:
toolNode
.
name
,
toolDescription
:
toolNode
.
toolDescription
,
toolDescription
:
toolNode
.
toolDescription
,
intro
:
toolNode
.
intro
intro
:
toolNode
.
intro
,
jsonSchema
:
toolNode
.
jsonSchema
})
})
}
}
];
];
...
...
packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts
0 → 100644
View file @
40a8a809
import
{
beforeEach
,
describe
,
expect
,
it
,
vi
}
from
'vitest'
;
import
{
AppTypeEnum
}
from
'@fastgpt/global/core/app/constants'
;
import
{
FlowNodeTypeEnum
}
from
'@fastgpt/global/core/workflow/node/constant'
;
import
{
getAgentRuntimeTools
}
from
'@fastgpt/service/core/workflow/dispatch/ai/agent/sub/tool/utils'
;
import
type
{
NodeToolConfigType
}
from
'@fastgpt/global/core/workflow/type/node'
;
const
{
authAppByTmbIdMock
,
getAppVersionByIdMock
}
=
vi
.
hoisted
(()
=>
({
authAppByTmbIdMock
:
vi
.
fn
(),
getAppVersionByIdMock
:
vi
.
fn
()
}));
vi
.
mock
(
'@fastgpt/service/support/permission/app/auth'
,
()
=>
({
authAppByTmbId
:
authAppByTmbIdMock
}));
vi
.
mock
(
'@fastgpt/service/core/app/version/controller'
,
()
=>
({
getAppVersionById
:
getAppVersionByIdMock
}));
vi
.
mock
(
'@fastgpt/service/core/app/tool/systemTool/systemTool.repo'
,
()
=>
({
SystemToolRepo
:
{
getInstance
:
vi
.
fn
(()
=>
({
getSystemToolDetail
:
vi
.
fn
()
}))
}
}));
vi
.
mock
(
'@fastgpt/service/common/logger'
,
()
=>
({
LogCategories
:
{
MODULE
:
{
AI
:
{
AGENT
:
'agent'
}
}
},
getLogger
:
vi
.
fn
(()
=>
({
warn
:
vi
.
fn
()
}))
}));
const
mcpInputSchema
=
{
type
:
'object'
,
properties
:
{
query
:
{
type
:
'string'
,
description
:
'Search query'
}
},
required
:
[
'query'
]
};
const
httpInputSchema
=
{
type
:
'object'
,
properties
:
{
keyword
:
{
type
:
'string'
,
description
:
'Keyword'
,
'x-tool-description'
:
'Keyword'
}
},
required
:
[
'keyword'
]
};
const
httpRequestSchema
=
{
type
:
'object'
,
properties
:
{
body
:
{
type
:
'object'
,
description
:
'Raw request body'
}
},
required
:
[
'body'
]
};
const
mcpTool
=
{
name
:
'search'
,
description
:
'Search docs'
,
inputSchema
:
mcpInputSchema
};
const
httpTool
=
{
name
:
'create'
,
description
:
'Create record'
,
path
:
'/records'
,
method
:
'post'
,
inputSchema
:
httpInputSchema
,
requestSchema
:
httpRequestSchema
,
outputSchema
:
{
type
:
'object'
,
properties
:
{}
}
};
const
createToolsetApp
=
({
id
,
type
,
toolConfig
}:
{
id
:
string
;
type
:
AppTypeEnum
.
mcpToolSet
|
AppTypeEnum
.
httpToolSet
;
toolConfig
:
NodeToolConfigType
;
})
=>
({
_id
:
id
,
teamId
:
'team_1'
,
tmbId
:
'tmb_1'
,
type
,
name
:
`
${
id
}
name`
,
avatar
:
`
${
id
}
.png`
,
intro
:
`
${
id
}
intro`
,
updateTime
:
new
Date
(),
modules
:
[
{
nodeId
:
'toolset_node'
,
flowNodeType
:
FlowNodeTypeEnum
.
toolSet
,
name
:
`
${
id
}
node`
,
avatar
:
`
${
id
}
.png`
,
intro
:
`
${
id
}
intro`
,
inputs
:
[],
outputs
:
[],
toolConfig
}
],
edges
:
[],
chatConfig
:
{}
});
describe
(
'getAgentRuntimeTools schema loading'
,
()
=>
{
beforeEach
(()
=>
{
vi
.
clearAllMocks
();
authAppByTmbIdMock
.
mockImplementation
(
async
({
appId
}:
{
appId
:
string
})
=>
{
const
app
=
appMap
[
appId
];
if
(
!
app
)
throw
new
Error
(
`missing app
${
appId
}
`
);
return
{
app
};
});
getAppVersionByIdMock
.
mockImplementation
(
async
({
app
}:
{
app
:
any
})
=>
({
versionId
:
''
,
versionName
:
app
.
name
,
nodes
:
app
.
modules
,
edges
:
app
.
edges
,
chatConfig
:
app
.
chatConfig
}));
});
const
appMap
:
Record
<
string
,
any
>
=
{
mcp_app
:
createToolsetApp
({
id
:
'mcp_app'
,
type
:
AppTypeEnum
.
mcpToolSet
,
toolConfig
:
{
mcpToolSet
:
{
url
:
'https://mcp.example.com'
,
headerSecret
:
{},
toolList
:
[
mcpTool
]
}
}
}),
'123_app'
:
createToolsetApp
({
id
:
'123_app'
,
type
:
AppTypeEnum
.
mcpToolSet
,
toolConfig
:
{
mcpToolSet
:
{
url
:
'https://mcp.example.com'
,
headerSecret
:
{},
toolList
:
[
mcpTool
]
}
}
}),
http_app
:
createToolsetApp
({
id
:
'http_app'
,
type
:
AppTypeEnum
.
httpToolSet
,
toolConfig
:
{
httpToolSet
:
{
baseUrl
:
'https://api.example.com'
,
headerSecret
:
{},
toolList
:
[
httpTool
]
}
}
})
};
it
(
'loads MCP toolset children with their input schema'
,
async
()
=>
{
const
tools
=
await
getAgentRuntimeTools
({
tmbId
:
'tmb_1'
,
tools
:
[{
id
:
'mcp_app'
,
config
:
{}
}]
});
expect
(
tools
).
toHaveLength
(
1
);
expect
(
tools
[
0
].
requestSchema
.
function
.
name
).
toBe
(
'mcp_app0'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
toEqual
(
mcpInputSchema
);
expect
(
tools
[
0
].
toolConfig
?.
mcpTool
?.
toolId
).
toBe
(
'mcp-mcp_app/search'
);
});
it
(
'loads a selected MCP tool with its input schema'
,
async
()
=>
{
const
tools
=
await
getAgentRuntimeTools
({
tmbId
:
'tmb_1'
,
tools
:
[{
id
:
'mcp-mcp_app/search'
,
config
:
{}
}]
});
expect
(
tools
).
toHaveLength
(
1
);
expect
(
tools
[
0
].
id
).
toBe
(
'mcp_appsearch'
);
expect
(
tools
[
0
].
name
).
toBe
(
'search'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
name
).
toBe
(
'mcp_appsearch'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
toEqual
(
mcpInputSchema
);
expect
(
tools
[
0
].
toolConfig
?.
mcpTool
?.
toolId
).
toBe
(
'mcp-mcp_app/search'
);
});
it
(
'prefixes tool function name only when the runtime tool id starts with a number'
,
async
()
=>
{
const
tools
=
await
getAgentRuntimeTools
({
tmbId
:
'tmb_1'
,
tools
:
[{
id
:
'mcp-123_app/search'
,
config
:
{}
}]
});
expect
(
tools
).
toHaveLength
(
1
);
expect
(
tools
[
0
].
id
).
toBe
(
'123_appsearch'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
name
).
toBe
(
't123_appsearch'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
toEqual
(
mcpInputSchema
);
});
it
(
'loads HTTP toolset children with their request schema'
,
async
()
=>
{
const
tools
=
await
getAgentRuntimeTools
({
tmbId
:
'tmb_1'
,
tools
:
[{
id
:
'http_app'
,
config
:
{}
}]
});
expect
(
tools
).
toHaveLength
(
1
);
expect
(
tools
[
0
].
requestSchema
.
function
.
name
).
toBe
(
'http_app0'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
toEqual
(
httpRequestSchema
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
not
.
toEqual
(
httpInputSchema
);
expect
(
tools
[
0
].
toolConfig
?.
httpTool
?.
toolId
).
toBe
(
'http-http_app/create'
);
});
it
(
'loads a selected HTTP tool with its request schema'
,
async
()
=>
{
const
tools
=
await
getAgentRuntimeTools
({
tmbId
:
'tmb_1'
,
tools
:
[{
id
:
'http-http_app/create'
,
config
:
{}
}]
});
expect
(
tools
).
toHaveLength
(
1
);
expect
(
tools
[
0
].
id
).
toBe
(
'http_appcreate'
);
expect
(
tools
[
0
].
name
).
toBe
(
'create'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
name
).
toBe
(
'http_appcreate'
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
toEqual
(
httpRequestSchema
);
expect
(
tools
[
0
].
requestSchema
.
function
.
parameters
).
not
.
toEqual
(
httpInputSchema
);
expect
(
tools
[
0
].
toolConfig
?.
httpTool
?.
toolId
).
toBe
(
'http-http_app/create'
);
});
});
pro
@
5151345e
Subproject commit
35458b567a3b719987bdd4e9a18dc747a8b3449
5
Subproject commit
5151345e0b0c883f3b812194430598dfe3113e6
5
projects/app/src/pages/api/core/app/create.ts
View file @
40a8a809
...
@@ -53,8 +53,19 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) {
...
@@ -53,8 +53,19 @@ async function handler(req: ApiRequestProps<CreateAppBodyType>) {
// 凭证校验
// 凭证校验
const
{
teamId
,
tmbId
,
userId
,
isRoot
}
=
parentId
const
{
teamId
,
tmbId
,
userId
,
isRoot
}
=
parentId
?
await
authApp
({
req
,
appId
:
parentId
,
per
:
WritePermissionVal
,
authToken
:
true
})
?
await
authApp
({
:
await
authUserPer
({
req
,
authToken
:
true
,
per
:
TeamAppCreatePermissionVal
});
req
,
appId
:
parentId
,
authToken
:
true
,
authApiKey
:
true
,
per
:
WritePermissionVal
})
:
await
authUserPer
({
req
,
authToken
:
true
,
authApiKey
:
true
,
per
:
TeamAppCreatePermissionVal
});
// 上限校验
// 上限校验
await
checkTeamAppTypeLimit
({
await
checkTeamAppTypeLimit
({
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment