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
025b3dac
authored
Apr 18, 2026
by
Archer
Committed by
GitHub
Apr 18, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: http tool schema (#6768)
* fix: http tool schema * perf: del dataset * perf: review * add test
parent
7506a147
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
772 additions
and
50 deletions
+772
-50
document/content/docs/self-host/upgrading/4-14/41410.mdx
+7
-5
document/content/docs/self-host/upgrading/4-14/41411.mdx
+2
-2
document/data/doc-last-modified.json
+10
-10
packages/global/core/app/tool/httpTool/utils.ts
+20
-0
packages/global/core/app/tool/mcpTool/utils.ts
+2
-1
packages/service/core/app/tool/httpTool/entity.ts
+14
-0
packages/service/core/app/tool/mcpTool/entity.ts
+2
-1
packages/service/core/dataset/delete/processor.ts
+25
-18
packages/service/core/workflow/dispatch/utils.ts
+47
-5
projects/app/src/pages/api/core/dataset/delete.ts
+4
-5
test/cases/global/core/app/tool/httpTool/utils.test.ts
+67
-0
test/cases/global/core/app/tool/mcpTool/utils.test.ts
+18
-2
test/cases/service/core/app/tool/httpTool/entity.test.ts
+78
-0
test/cases/service/core/workflow/dispatch/utils.test.ts
+476
-1
No files found.
document/content/docs/self-host/upgrading/4-14/41410.mdx
View file @
025b3dac
...
...
@@ -9,12 +9,13 @@ description: 'FastGPT V4.14.10 更新说明'
以下针对的是 `docker compose` 部署方案的配置调整,使用`sealos`的商业版用户,可私信支持人员,提供在线的沙盒服务方案。
打开[最新 yml 部署文件](https://github.com/labring/FastGPT/blob/main/deploy/docker/cn/docker-compose.pg.yml),调整
以下内容:
参考[最新 yml 部署文件](https://github.com/labring/FastGPT/blob/main/deploy/docker/cn/docker-compose.pg.yml),调整本地 yml 文件,加入
以下内容:
1. 在文件顶部增加 `x-volume-manager-auth-token: &x-volume-manager-auth-token 'vmtoken'` 变量配置。
2. 增加 3 组 services: `opensandbox-server`,`volume-manager`,`agent-sandbox-image`
3. 增加 `configs`, 文件底部可找到该内容,直接复制添加。
4. 修改 `fastgpt-app`/`fastgpt-pro` 环境变量, 增加以下变量:
2. 增加 5 组 services: `opensandbox-server`,`opensandbox-agent-sandbox-image`,`opensandbox-execd-image`,`opensandbox-egress-image`,`fastgpt-volume-manager`
3. 调整 `networks`,可参考最新的 yml 完全修改。
4. 增加 `configs`配置, 文件底部可找到该内容,直接复制添加。
5. 修改 `fastgpt-app`/`fastgpt-pro` 环境变量, 增加以下变量:
```bash
# ==================== Agent sandbox 配置 ====================
...
...
@@ -25,6 +26,7 @@ AGENT_SANDBOX_OPENSANDBOX_API_KEY:
AGENT_SANDBOX_OPENSANDBOX_RUNTIME: docker
AGENT_SANDBOX_OPENSANDBOX_IMAGE_REPO: registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt-agent-sandbox
AGENT_SANDBOX_OPENSANDBOX_IMAGE_TAG: v0.1
AGENT_SANDBOX_OPENSANDBOX_USE_SERVER_PROXY: true
# Volume 持久化配置(opensandbox provider 下可选)
AGENT_SANDBOX_ENABLE_VOLUME: true
AGENT_SANDBOX_VOLUME_MANAGER_URL: http://volume-manager:3000
...
...
@@ -39,7 +41,7 @@ AGENT_SANDBOX_VOLUME_MANAGER_TOKEN: *x-volume-manager-auth-token
### 3. 更新镜像 tag
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.14.10.
2
- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.14.10.
4
- 更新 fastpgt-pro(商业版) 镜像 tag: v4.14.10
- 更新 code-sandbox 镜像 tag: v4.14.10
- 更新 fastgpt-plugin 镜像 tag: v0.5.6
...
...
document/content/docs/self-host/upgrading/4-14/41411.mdx
View file @
025b3dac
...
...
@@ -39,4 +39,4 @@ description: 'FastGPT V4.14.11 更新说明'
8. 工作流代码运行节点,AI 生成代码后,会讲输出值的 id 全部替换,优化成相同 key 的 id 不替换。
9. 工作流中,父级节点受到辅助线自动对齐时候,其子节点位置会偏移。
10. 评估列表权限过滤未覆盖继承权限。
11. MCP 工具 raw schema 未成功保存,导致工具调用时候,schema 不准确。
\ No newline at end of file
11. MCP 工具和 Http 工具 raw schema 未成功保存,导致工具调用时候,schema 不准确。
\ No newline at end of file
document/data/doc-last-modified.json
View file @
025b3dac
...
...
@@ -73,8 +73,8 @@
"document/content/docs/introduction/guide/dashboard/workflow/laf.mdx"
:
"2025-07-23T21:35:03+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/loop.en.mdx"
:
"2026-02-26T22:14:30+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/loop.mdx"
:
"2025-09-17T22:29:56+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/parallel_run.en.mdx"
:
"2026-04-17T
15:12:11
+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/parallel_run.mdx"
:
"2026-04-17T
15:12:11
+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/parallel_run.en.mdx"
:
"2026-04-17T
23:28:43
+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/parallel_run.mdx"
:
"2026-04-17T
23:28:43
+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/question_classify.en.mdx"
:
"2026-02-26T22:14:30+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/question_classify.mdx"
:
"2025-07-23T21:35:03+08:00"
,
"document/content/docs/introduction/guide/dashboard/workflow/reply.en.mdx"
:
"2026-02-26T22:14:30+08:00"
,
...
...
@@ -155,8 +155,8 @@
"document/content/docs/self-host/config/model/minimax.mdx"
:
"2026-03-19T09:32:57-05:00"
,
"document/content/docs/self-host/config/model/siliconCloud.en.mdx"
:
"2026-03-19T14:09:03+08:00"
,
"document/content/docs/self-host/config/model/siliconCloud.mdx"
:
"2026-03-19T14:09:03+08:00"
,
"document/content/docs/self-host/config/object-storage.en.mdx"
:
"2026-04-1
3T17:52:30
+08:00"
,
"document/content/docs/self-host/config/object-storage.mdx"
:
"2026-04-1
3T17:52:30
+08:00"
,
"document/content/docs/self-host/config/object-storage.en.mdx"
:
"2026-04-1
7T23:28:43
+08:00"
,
"document/content/docs/self-host/config/object-storage.mdx"
:
"2026-04-1
7T23:28:43
+08:00"
,
"document/content/docs/self-host/config/signoz.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/config/signoz.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/custom-models/bge-rerank.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
...
...
@@ -175,8 +175,8 @@
"document/content/docs/self-host/custom-models/ollama.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/custom-models/xinference.en.mdx"
:
"2026-03-30T10:05:42+08:00"
,
"document/content/docs/self-host/custom-models/xinference.mdx"
:
"2026-03-30T10:05:42+08:00"
,
"document/content/docs/self-host/deploy/docker.en.mdx"
:
"2026-04-1
6T15:22:51
+08:00"
,
"document/content/docs/self-host/deploy/docker.mdx"
:
"2026-04-1
6T15:22:51
+08:00"
,
"document/content/docs/self-host/deploy/docker.en.mdx"
:
"2026-04-1
7T23:28:43
+08:00"
,
"document/content/docs/self-host/deploy/docker.mdx"
:
"2026-04-1
7T23:28:43
+08:00"
,
"document/content/docs/self-host/deploy/sealos.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/deploy/sealos.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/design/dataset.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
...
...
@@ -223,8 +223,8 @@
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4141.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41410.en.mdx"
:
"2026-03-31T23:15:29+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41410.mdx"
:
"2026-04-
08T16:15:25
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41411.mdx"
:
"2026-04-1
7T17:46:20
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41410.mdx"
:
"2026-04-
18T19:42:07
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/41411.mdx"
:
"2026-04-1
8T19:42:07
+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4142.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
...
...
@@ -385,8 +385,8 @@
"document/content/docs/self-host/upgrading/outdated/499.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx"
:
"2026-03-03T17:39:47+08:00"
,
"document/content/docs/toc.en.mdx"
:
"2026-04-17T
15:12:11
+08:00"
,
"document/content/docs/toc.mdx"
:
"2026-04-17T
15:12:11
+08:00"
,
"document/content/docs/toc.en.mdx"
:
"2026-04-17T
23:28:43
+08:00"
,
"document/content/docs/toc.mdx"
:
"2026-04-17T
23:28:43
+08:00"
,
"document/content/docs/use-cases/app-cases/dalle3.en.mdx"
:
"2026-02-26T22:14:30+08:00"
,
"document/content/docs/use-cases/app-cases/dalle3.mdx"
:
"2025-07-23T21:35:03+08:00"
,
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx"
:
"2026-02-26T22:14:30+08:00"
,
...
...
packages/global/core/app/tool/httpTool/utils.ts
View file @
025b3dac
...
...
@@ -8,6 +8,7 @@ import { type StoreSecretValueType } from '../../../../common/secret/type';
import
{
type
JsonSchemaPropertiesItemType
}
from
'../../jsonschema'
;
import
{
NodeOutputKeyEnum
,
WorkflowIOValueTypeEnum
}
from
'../../../workflow/constants'
;
import
{
i18nT
}
from
'../../../../../web/i18n/utils'
;
import
type
{
NodeToolConfigType
}
from
'../../../workflow/type/node'
;
export
const
getHTTPToolSetRuntimeNode
=
({
name
,
...
...
@@ -89,6 +90,25 @@ export const getHTTPToolRuntimeNode = ({
};
};
export
const
parseHttpToolConfig
=
(
config
:
NonNullable
<
NodeToolConfigType
[
'httpTool'
]
>
):
|
{
toolsetId
:
string
;
toolName
:
string
;
}
|
undefined
=>
{
const
prefix
=
`
${
AppToolSourceEnum
.
http
}
-`
;
if
(
!
config
.
toolId
.
startsWith
(
prefix
))
return
undefined
;
const
[
toolsetId
,
...
rest
]
=
config
.
toolId
.
slice
(
prefix
.
length
).
split
(
'/'
);
const
toolName
=
rest
.
join
(
'/'
);
if
(
!
toolsetId
||
!
toolName
)
return
undefined
;
return
{
toolsetId
,
toolName
};
};
export
const
pathData2ToolList
=
async
(
pathData
:
PathDataType
[]
):
Promise
<
HttpToolConfigType
[]
>
=>
{
...
...
packages/global/core/app/tool/mcpTool/utils.ts
View file @
025b3dac
...
...
@@ -92,7 +92,8 @@ export const parsetMcpToolConfig = (
|
undefined
=>
{
const
prefix
=
`
${
AppToolSourceEnum
.
mcp
}
-`
;
if
(
!
config
.
toolId
.
startsWith
(
prefix
))
return
undefined
;
const
[
toolsetId
,
toolName
]
=
config
.
toolId
.
slice
(
prefix
.
length
).
split
(
'/'
);
const
[
toolsetId
,
...
rest
]
=
config
.
toolId
.
slice
(
prefix
.
length
).
split
(
'/'
);
const
toolName
=
rest
.
join
(
'/'
);
if
(
!
toolsetId
||
!
toolName
)
return
undefined
;
return
{
toolsetId
,
...
...
packages/service/core/app/tool/httpTool/entity.ts
0 → 100644
View file @
025b3dac
import
type
{
AppSchemaType
}
from
'@fastgpt/global/core/app/type'
;
import
{
MongoApp
}
from
'../../schema'
;
export
const
getHttpToolsets
=
({
teamId
,
ids
,
field
}:
{
teamId
:
string
;
ids
:
string
[];
field
?:
Record
<
string
,
boolean
>
;
}):
Promise
<
AppSchemaType
[]
>
=>
{
return
MongoApp
.
find
({
teamId
,
_id
:
{
$in
:
ids
}
},
field
).
lean
();
};
packages/service/core/app/tool/mcpTool/entity.ts
View file @
025b3dac
import
type
{
AppSchemaType
}
from
'@fastgpt/global/core/app/type'
;
import
{
MongoApp
}
from
'../../schema'
;
export
const
getMcpToolsets
=
({
...
...
@@ -8,6 +9,6 @@ export const getMcpToolsets = ({
teamId
:
string
;
ids
:
string
[];
field
?:
Record
<
string
,
boolean
>
;
})
=>
{
})
:
Promise
<
AppSchemaType
[]
>
=>
{
return
MongoApp
.
find
({
teamId
,
_id
:
{
$in
:
ids
}
},
field
).
lean
();
};
packages/service/core/dataset/delete/processor.ts
View file @
025b3dac
...
...
@@ -43,25 +43,32 @@ export const deleteTeamAllDatasets = async (teamId: string) => {
teamId
,
datasetIds
:
datasets
.
map
((
d
)
=>
d
.
_id
)
});
await
MongoDataset
.
updateMany
(
{
teamId
},
{
$set
:
{
deleteTime
:
new
Date
()
await
mongoSessionRun
(
async
(
session
)
=>
{
await
MongoDataset
.
updateMany
(
{
teamId
},
{
$set
:
{
deleteTime
:
new
Date
()
}
},
{
session
}
}
);
await
Promise
.
all
(
datasets
.
map
((
dataset
)
=>
{
if
(
dataset
.
parentId
)
return
;
return
addDatasetDeleteJob
({
teamId
,
datasetId
:
dataset
.
_id
});
})
);
);
await
Promise
.
all
(
datasets
.
map
((
dataset
)
=>
{
// 有 parentId 的忽略,只需要删 root 下的即可。
if
(
dataset
.
parentId
)
return
;
return
addDatasetDeleteJob
({
teamId
,
datasetId
:
dataset
.
_id
});
})
);
});
};
// 批量删除函数
...
...
packages/service/core/workflow/dispatch/utils.ts
View file @
025b3dac
...
...
@@ -25,7 +25,10 @@ import {
import
{
getNanoid
}
from
'@fastgpt/global/common/string/tools'
;
import
{
type
SearchDataResponseItemType
}
from
'@fastgpt/global/core/dataset/type'
;
import
{
getMCPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/mcpTool/utils'
;
import
{
getHTTPToolRuntimeNode
}
from
'@fastgpt/global/core/app/tool/httpTool/utils'
;
import
{
getHTTPToolRuntimeNode
,
parseHttpToolConfig
}
from
'@fastgpt/global/core/app/tool/httpTool/utils'
;
import
{
FlowNodeTypeEnum
}
from
'@fastgpt/global/core/workflow/node/constant'
;
import
{
MongoApp
}
from
'../../../core/app/schema'
;
import
{
getMCPChildren
}
from
'../../../core/app/mcp'
;
...
...
@@ -40,6 +43,7 @@ import { presignVariablesFileUrls } from '../../chat/utils';
import
{
getSystemTime
}
from
'@fastgpt/global/common/time/timezone'
;
import
{
parsetMcpToolConfig
}
from
'@fastgpt/global/core/app/tool/mcpTool/utils'
;
import
{
getMcpToolsets
}
from
'../../app/tool/mcpTool/entity'
;
import
{
getHttpToolsets
}
from
'../../app/tool/httpTool/entity'
;
import
{
getHTTPToolList
}
from
'../../app/http'
;
/* get system variable */
...
...
@@ -396,7 +400,7 @@ export const rewriteRuntimeWorkFlow = async ({
})
=>
{
/* Toolset 展开 */
// TODO: 待性能优化
{
const
parseToolset
=
async
()
=>
{
const
toolSetNodes
=
nodes
.
filter
((
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
toolSet
);
if
(
toolSetNodes
.
length
>
0
)
{
const
nodeIdsToRemove
=
new
Set
<
string
>
();
...
...
@@ -481,10 +485,10 @@ export const rewriteRuntimeWorkFlow = async ({
}
}
}
}
}
;
/* MCP tool 获取原始 schema 加入到 jsonschema 字段里 */
{
const
parseMcpTool
=
async
()
=>
{
const
mcpToolNodes
=
nodes
.
filter
(
(
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
tool
&&
node
.
toolConfig
?.
mcpTool
);
...
...
@@ -517,7 +521,45 @@ export const rewriteRuntimeWorkFlow = async ({
node
.
jsonSchema
=
toolRaw
.
inputSchema
;
node
.
intro
=
toolRaw
.
description
;
});
}
};
/* Http tool 获取原始 schema 加入到 jsonschema 字段里 */
const
parseHttpTool
=
async
()
=>
{
const
httpToolNodes
=
nodes
.
filter
(
(
node
)
=>
node
.
flowNodeType
===
FlowNodeTypeEnum
.
tool
&&
node
.
toolConfig
?.
httpTool
);
const
parseHttpToolConfigs
=
httpToolNodes
.
map
((
node
)
=>
parseHttpToolConfig
(
node
.
toolConfig
?.
httpTool
!
))
.
filter
(
Boolean
)
as
{
toolsetId
:
string
;
toolName
:
string
}[];
// 批量获取 toolset
const
toolsets
=
await
getHttpToolsets
({
teamId
,
ids
:
parseHttpToolConfigs
.
map
((
config
)
=>
config
.
toolsetId
),
field
:
{
_id
:
true
,
modules
:
true
}
});
const
toolsetMap
=
new
Map
<
string
,
(
typeof
toolsets
)[
number
]
>
();
toolsets
.
forEach
((
toolset
)
=>
{
toolsetMap
.
set
(
String
(
toolset
.
_id
),
toolset
);
});
httpToolNodes
.
forEach
((
node
)
=>
{
const
httpTool
=
node
.
toolConfig
?.
httpTool
;
if
(
!
httpTool
)
return
;
const
parseResult
=
parseHttpToolConfig
(
httpTool
);
if
(
!
parseResult
)
return
;
const
toolset
=
toolsetMap
.
get
(
parseResult
.
toolsetId
);
const
toolList
=
toolset
?.
modules
?.[
0
].
toolConfig
?.
httpToolSet
?.
toolList
;
if
(
!
toolList
)
return
;
const
toolRaw
=
toolList
.
find
((
tool
)
=>
tool
.
name
===
parseResult
.
toolName
);
if
(
!
toolRaw
)
return
;
node
.
jsonSchema
=
toolRaw
.
requestSchema
;
node
.
intro
=
toolRaw
.
description
;
});
};
await
Promise
.
all
([
parseToolset
(),
parseMcpTool
(),
parseHttpTool
()]);
};
export
const
getNodeErrResponse
=
({
...
...
projects/app/src/pages/api/core/dataset/delete.ts
View file @
025b3dac
...
...
@@ -30,6 +30,10 @@ async function handler(req: ApiRequestProps) {
fields
:
'_id'
});
const
datasetIds
=
deleteDatasets
.
map
((
d
)
=>
d
.
_id
);
await
deleteDatasetsImmediate
({
teamId
,
datasetIds
});
await
mongoSessionRun
(
async
(
session
)
=>
{
// 1. Mark as deleted
...
...
@@ -46,11 +50,6 @@ async function handler(req: ApiRequestProps) {
}
);
await
deleteDatasetsImmediate
({
teamId
,
datasetIds
});
// 2. Add to delete queue
await
addDatasetDeleteJob
({
teamId
,
...
...
test/cases/global/core/app/tool/httpTool/utils.test.ts
View file @
025b3dac
...
...
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
import
{
getHTTPToolSetRuntimeNode
,
getHTTPToolRuntimeNode
,
parseHttpToolConfig
,
pathData2ToolList
}
from
'@fastgpt/global/core/app/tool/httpTool/utils'
;
import
{
...
...
@@ -159,6 +160,72 @@ describe('httpTool utils', () => {
});
});
describe
(
'parseHttpToolConfig'
,
()
=>
{
it
(
'should parse toolsetId and toolName from a valid toolId'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-toolset-456/someTool'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-456'
,
toolName
:
'someTool'
});
});
it
(
'should return undefined when toolId does not match http- prefix pattern'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'mcp-foo/bar'
});
expect
(
result
).
toBeUndefined
();
});
it
(
'should return undefined when toolId has no slash separator'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-toolset-no-tool'
});
expect
(
result
).
toBeUndefined
();
});
it
(
'should return undefined when toolsetId segment is empty in toolId'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-/toolName'
});
expect
(
result
).
toBeUndefined
();
});
it
(
'should return undefined when toolName segment is empty in toolId'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-toolset-abc/'
});
expect
(
result
).
toBeUndefined
();
});
it
(
'should return undefined when toolId is empty string'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
''
});
expect
(
result
).
toBeUndefined
();
});
it
(
'should preserve slashes inside tool name'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-toolset-abc/namespace/nestedTool'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-abc'
,
toolName
:
'namespace/nestedTool'
});
});
it
(
'should preserve multiple slashes inside tool name'
,
()
=>
{
const
result
=
parseHttpToolConfig
({
toolId
:
'http-toolset-xyz/a/b/c/d'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-xyz'
,
toolName
:
'a/b/c/d'
});
});
});
describe
(
'pathData2ToolList'
,
()
=>
{
it
(
'should convert simple path data to tool list'
,
async
()
=>
{
const
pathData
:
PathDataType
[]
=
[
...
...
test/cases/global/core/app/tool/mcpTool/utils.test.ts
View file @
025b3dac
...
...
@@ -191,12 +191,28 @@ describe('mcpTool utils', () => {
expect
(
result
).
toBeUndefined
();
});
it
(
'should p
arse toolsetId correctly when tool name contains additional slashes
'
,
()
=>
{
it
(
'should p
reserve slashes inside tool name
'
,
()
=>
{
const
result
=
parsetMcpToolConfig
({
toolId
:
'mcp-toolset-abc/namespace/nestedTool'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-abc'
,
toolName
:
'namespace'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-abc'
,
toolName
:
'namespace/nestedTool'
});
});
it
(
'should preserve multiple slashes inside tool name'
,
()
=>
{
const
result
=
parsetMcpToolConfig
({
toolId
:
'mcp-toolset-xyz/a/b/c/d'
});
expect
(
result
).
toEqual
({
toolsetId
:
'toolset-xyz'
,
toolName
:
'a/b/c/d'
});
});
it
(
'should return undefined when toolName segment is empty in toolId'
,
()
=>
{
const
result
=
parsetMcpToolConfig
({
toolId
:
'mcp-toolset-abc/'
});
expect
(
result
).
toBeUndefined
();
});
});
});
test/cases/service/core/app/tool/httpTool/entity.test.ts
0 → 100644
View file @
025b3dac
import
{
describe
,
it
,
expect
,
vi
,
beforeEach
}
from
'vitest'
;
const
{
mockMongoAppFind
}
=
vi
.
hoisted
(()
=>
({
mockMongoAppFind
:
vi
.
fn
()
}));
vi
.
mock
(
'@fastgpt/service/core/app/schema'
,
()
=>
({
MongoApp
:
{
find
:
mockMongoAppFind
}
}));
import
{
getHttpToolsets
}
from
'@fastgpt/service/core/app/tool/httpTool/entity'
;
const
setupFindReturn
=
(
result
:
any
)
=>
{
const
leanFn
=
vi
.
fn
().
mockResolvedValue
(
result
);
mockMongoAppFind
.
mockReturnValue
({
lean
:
leanFn
});
return
{
leanFn
};
};
beforeEach
(()
=>
{
vi
.
clearAllMocks
();
});
describe
(
'getHttpToolsets'
,
()
=>
{
it
(
'should query MongoApp with teamId, ids, and field, and return lean result'
,
async
()
=>
{
const
docs
=
[
{
_id
:
'id1'
,
modules
:
[{
toolConfig
:
{
httpToolSet
:
{
toolList
:
[]
}
}
}]
},
{
_id
:
'id2'
,
modules
:
[]
}
];
const
{
leanFn
}
=
setupFindReturn
(
docs
);
const
field
=
{
_id
:
true
,
modules
:
true
};
const
res
=
await
getHttpToolsets
({
teamId
:
'team1'
,
ids
:
[
'id1'
,
'id2'
],
field
});
expect
(
mockMongoAppFind
).
toHaveBeenCalledTimes
(
1
);
expect
(
mockMongoAppFind
).
toHaveBeenCalledWith
(
{
teamId
:
'team1'
,
_id
:
{
$in
:
[
'id1'
,
'id2'
]
}
},
field
);
expect
(
leanFn
).
toHaveBeenCalledTimes
(
1
);
expect
(
res
).
toBe
(
docs
);
});
it
(
'should pass undefined field when not provided'
,
async
()
=>
{
setupFindReturn
([]);
await
getHttpToolsets
({
teamId
:
'team1'
,
ids
:
[
'id1'
]
});
expect
(
mockMongoAppFind
).
toHaveBeenCalledWith
(
{
teamId
:
'team1'
,
_id
:
{
$in
:
[
'id1'
]
}
},
undefined
);
});
it
(
'should handle empty ids array'
,
async
()
=>
{
const
{
leanFn
}
=
setupFindReturn
([]);
const
res
=
await
getHttpToolsets
({
teamId
:
'team1'
,
ids
:
[]
});
expect
(
mockMongoAppFind
).
toHaveBeenCalledWith
({
teamId
:
'team1'
,
_id
:
{
$in
:
[]
}
},
undefined
);
expect
(
leanFn
).
toHaveBeenCalledTimes
(
1
);
expect
(
res
).
toEqual
([]);
});
it
(
'should propagate rejection from lean()'
,
async
()
=>
{
const
leanFn
=
vi
.
fn
().
mockRejectedValue
(
new
Error
(
'db error'
));
mockMongoAppFind
.
mockReturnValue
({
lean
:
leanFn
});
await
expect
(
getHttpToolsets
({
teamId
:
'team1'
,
ids
:
[
'id1'
],
field
:
{
_id
:
true
}
})
).
rejects
.
toThrow
(
'db error'
);
});
});
test/cases/service/core/workflow/dispatch/utils.test.ts
View file @
025b3dac
...
...
@@ -12,8 +12,11 @@ import {
formatHttpError
,
rewriteRuntimeWorkFlow
,
getNodeErrResponse
,
safePoints
safePoints
,
getSystemVariables
}
from
'@fastgpt/service/core/workflow/dispatch/utils'
;
import
{
encryptSecret
}
from
'@fastgpt/service/common/secret/aes256gcm'
;
import
{
WorkflowIOValueTypeEnum
}
from
'@fastgpt/global/core/workflow/constants'
;
import
{
responseWrite
}
from
'@fastgpt/service/common/response'
;
import
{
ChatRoleEnum
}
from
'@fastgpt/global/core/chat/constants'
;
import
type
{
ChatItemMiniType
}
from
'@fastgpt/global/core/chat/type'
;
...
...
@@ -51,6 +54,11 @@ vi.mock('@fastgpt/service/core/app/http', () => ({
getHTTPToolList
:
(...
args
:
any
[])
=>
mockGetHTTPToolList
(...
args
)
}));
const
mockPresignVariablesFileUrls
=
vi
.
fn
();
vi
.
mock
(
'@fastgpt/service/core/chat/utils'
,
()
=>
({
presignVariablesFileUrls
:
(...
args
:
any
[])
=>
mockPresignVariablesFileUrls
(...
args
)
}));
describe
(
'getWorkflowResponseWrite'
,
()
=>
{
const
mockRes
=
()
=>
{
const
res
:
any
=
{
closed
:
false
};
...
...
@@ -804,6 +812,170 @@ describe('rewriteRuntimeWorkFlow', () => {
expect
(
nodes
.
find
((
n
)
=>
n
.
nodeId
===
'ts41'
)).
toBeDefined
();
expect
(
edges
.
filter
((
e
)
=>
e
.
target
===
'ts40'
||
e
.
target
===
'ts41'
).
length
).
toBe
(
2
);
});
// Helper: route MongoApp.find responses by the toolsetId it queries, since
// parseMcpTool and parseHttpTool may both hit MongoApp.find in parallel.
const
setupFindByIdMap
=
(
idToDoc
:
Record
<
string
,
any
>
)
=>
{
mockMongoAppFind
.
mockImplementation
((
query
:
any
)
=>
{
const
ids
:
string
[]
=
query
?.
_id
?.
$in
??
[];
const
docs
=
ids
.
map
((
id
)
=>
idToDoc
[
id
]).
filter
(
Boolean
);
return
{
lean
:
vi
.
fn
().
mockResolvedValue
(
docs
)
};
});
};
it
(
'should inject jsonSchema and intro for standalone MCP tool nodes'
,
async
()
=>
{
const
mcpToolNode
=
makeNode
(
'mcp1'
,
FlowNodeTypeEnum
.
tool
,
{
toolConfig
:
{
mcpTool
:
{
toolId
:
'mcp-toolset-1/toolA'
}
}
}
as
any
);
const
nodes
=
[
mcpToolNode
];
const
edges
:
RuntimeEdgeItemType
[]
=
[];
const
toolAInputSchema
=
{
type
:
'object'
,
properties
:
{
x
:
{
type
:
'string'
}
}
};
setupFindByIdMap
({
'toolset-1'
:
{
_id
:
'toolset-1'
,
modules
:
[
{
toolConfig
:
{
mcpToolSet
:
{
toolList
:
[
{
name
:
'toolA'
,
description
:
'tool A description'
,
inputSchema
:
toolAInputSchema
}
]
}
}
}
]
}
});
await
rewriteRuntimeWorkFlow
({
teamId
:
'team1'
,
nodes
,
edges
});
expect
(
mcpToolNode
.
jsonSchema
).
toEqual
(
toolAInputSchema
);
expect
(
mcpToolNode
.
intro
).
toBe
(
'tool A description'
);
});
it
(
'should inject jsonSchema and intro for standalone HTTP tool nodes'
,
async
()
=>
{
const
httpToolNode
=
makeNode
(
'http1'
,
FlowNodeTypeEnum
.
tool
,
{
toolConfig
:
{
httpTool
:
{
toolId
:
'http-toolset-1/toolB'
}
}
}
as
any
);
const
nodes
=
[
httpToolNode
];
const
edges
:
RuntimeEdgeItemType
[]
=
[];
const
toolBRequestSchema
=
{
type
:
'object'
,
properties
:
{
y
:
{
type
:
'number'
}
}
};
setupFindByIdMap
({
'toolset-1'
:
{
_id
:
'toolset-1'
,
modules
:
[
{
toolConfig
:
{
httpToolSet
:
{
toolList
:
[
{
name
:
'toolB'
,
description
:
'tool B description'
,
requestSchema
:
toolBRequestSchema
}
]
}
}
}
]
}
});
await
rewriteRuntimeWorkFlow
({
teamId
:
'team1'
,
nodes
,
edges
});
expect
(
httpToolNode
.
jsonSchema
).
toEqual
(
toolBRequestSchema
);
expect
(
httpToolNode
.
intro
).
toBe
(
'tool B description'
);
});
it
(
'should preserve tool names containing slashes when injecting schema'
,
async
()
=>
{
const
httpToolNode
=
makeNode
(
'http1'
,
FlowNodeTypeEnum
.
tool
,
{
toolConfig
:
{
httpTool
:
{
toolId
:
'http-toolset-1/namespace/toolC'
}
}
}
as
any
);
const
nodes
=
[
httpToolNode
];
const
edges
:
RuntimeEdgeItemType
[]
=
[];
setupFindByIdMap
({
'toolset-1'
:
{
_id
:
'toolset-1'
,
modules
:
[
{
toolConfig
:
{
httpToolSet
:
{
toolList
:
[
{
name
:
'namespace/toolC'
,
description
:
'nested tool'
,
requestSchema
:
{
type
:
'object'
}
}
]
}
}
}
]
}
});
await
rewriteRuntimeWorkFlow
({
teamId
:
'team1'
,
nodes
,
edges
});
expect
(
httpToolNode
.
jsonSchema
).
toEqual
({
type
:
'object'
});
expect
(
httpToolNode
.
intro
).
toBe
(
'nested tool'
);
});
it
(
'should skip schema injection when toolset is not found'
,
async
()
=>
{
const
httpToolNode
=
makeNode
(
'http1'
,
FlowNodeTypeEnum
.
tool
,
{
toolConfig
:
{
httpTool
:
{
toolId
:
'http-missing/toolX'
}
},
intro
:
'original'
,
jsonSchema
:
{
type
:
'original'
}
}
as
any
);
const
nodes
=
[
httpToolNode
];
const
edges
:
RuntimeEdgeItemType
[]
=
[];
setupFindByIdMap
({});
await
rewriteRuntimeWorkFlow
({
teamId
:
'team1'
,
nodes
,
edges
});
expect
(
httpToolNode
.
jsonSchema
).
toEqual
({
type
:
'original'
});
expect
(
httpToolNode
.
intro
).
toBe
(
'original'
);
});
it
(
'should skip schema injection when toolId prefix does not match'
,
async
()
=>
{
const
httpToolNode
=
makeNode
(
'http1'
,
FlowNodeTypeEnum
.
tool
,
{
toolConfig
:
{
httpTool
:
{
toolId
:
'mcp-toolset-1/toolA'
}
},
intro
:
'original'
,
jsonSchema
:
{
type
:
'original'
}
}
as
any
);
const
nodes
=
[
httpToolNode
];
const
edges
:
RuntimeEdgeItemType
[]
=
[];
setupFindByIdMap
({});
await
rewriteRuntimeWorkFlow
({
teamId
:
'team1'
,
nodes
,
edges
});
expect
(
httpToolNode
.
jsonSchema
).
toEqual
({
type
:
'original'
});
expect
(
httpToolNode
.
intro
).
toBe
(
'original'
);
});
});
describe
(
'getNodeErrResponse'
,
()
=>
{
...
...
@@ -893,3 +1065,306 @@ describe('safePoints', () => {
expect
(
safePoints
(
undefined
)).
toBe
(
0
);
});
});
describe
(
'getSystemVariables'
,
()
=>
{
const
baseArgs
=
{
timezone
:
'Asia/Shanghai'
,
runningAppInfo
:
{
id
:
'app-123'
}
as
any
,
chatId
:
'chat-1'
,
responseChatItemId
:
'rci-1'
,
histories
:
[],
uid
:
'user-1'
,
chatConfig
:
{}
as
any
,
variables
:
{}
};
it
(
'should return only system variables when chatConfig has no variables'
,
async
()
=>
{
const
result
=
await
getSystemVariables
(
baseArgs
);
expect
(
result
.
userId
).
toBe
(
'user-1'
);
expect
(
result
.
appId
).
toBe
(
'app-123'
);
expect
(
result
.
chatId
).
toBe
(
'chat-1'
);
expect
(
result
.
responseChatItemId
).
toBe
(
'rci-1'
);
expect
(
result
.
histories
).
toEqual
([]);
expect
(
typeof
result
.
cTime
).
toBe
(
'string'
);
});
it
(
'should return only system variables when chatConfig is undefined'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
undefined
as
any
});
expect
(
result
.
userId
).
toBe
(
'user-1'
);
expect
(
result
.
appId
).
toBe
(
'app-123'
);
expect
((
result
as
any
).
custom
).
toBeUndefined
();
});
it
(
'should coerce runningAppInfo.id to string'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
runningAppInfo
:
{
id
:
42
}
as
any
});
expect
(
result
.
appId
).
toBe
(
'42'
);
});
it
(
'should default histories to [] when not provided'
,
async
()
=>
{
const
{
histories
,
...
rest
}
=
baseArgs
;
const
result
=
await
getSystemVariables
(
rest
as
any
);
expect
(
result
.
histories
).
toEqual
([]);
});
it
(
'should preserve provided histories'
,
async
()
=>
{
const
histories
=
[{
obj
:
'Human'
,
value
:
[]
}]
as
any
;
const
result
=
await
getSystemVariables
({
...
baseArgs
,
histories
});
expect
(
result
.
histories
).
toBe
(
histories
);
});
it
(
'should decrypt password variable from item.label'
,
async
()
=>
{
const
encrypted
=
encryptSecret
(
'plain-value'
);
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'pwdKey'
,
label
:
'pwdLabel'
,
type
:
VariableInputEnum
.
password
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
pwdLabel
:
JSON
.
stringify
({
value
:
''
,
secret
:
encrypted
})
}
});
expect
((
result
as
any
).
pwdKey
).
toBe
(
'plain-value'
);
});
it
(
'should decrypt password variable from item.key when label missing'
,
async
()
=>
{
const
encrypted
=
encryptSecret
(
'key-value'
);
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'pwdKey'
,
label
:
'pwdLabel'
,
type
:
VariableInputEnum
.
password
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
pwdKey
:
{
value
:
''
,
secret
:
encrypted
}
}
});
expect
((
result
as
any
).
pwdKey
).
toBe
(
'key-value'
);
});
it
(
'should fall back to defaultValue for password variable'
,
async
()
=>
{
const
encrypted
=
encryptSecret
(
'default-value'
);
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'pwdKey'
,
label
:
'pwdLabel'
,
type
:
VariableInputEnum
.
password
,
valueType
:
WorkflowIOValueTypeEnum
.
string
,
defaultValue
:
{
value
:
''
,
secret
:
encrypted
}
}
]
}
as
any
,
variables
:
{}
});
expect
((
result
as
any
).
pwdKey
).
toBe
(
'default-value'
);
});
it
(
'should map file variables to their url list'
,
async
()
=>
{
mockPresignVariablesFileUrls
.
mockResolvedValueOnce
({
fileKey
:
[
{
key
:
'a'
,
url
:
'http://example.com/a'
},
{
key
:
'b'
,
url
:
'http://example.com/b'
}
]
});
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'fileKey'
,
label
:
'fileLabel'
,
type
:
VariableInputEnum
.
file
,
valueType
:
WorkflowIOValueTypeEnum
.
arrayString
}
]
}
as
any
,
variables
:
{
fileKey
:
[{
key
:
'a'
},
{
key
:
'b'
}]
}
});
expect
((
result
as
any
).
fileKey
).
toEqual
([
'http://example.com/a'
,
'http://example.com/b'
]);
});
it
(
'should return undefined for file variable when presign returns no entry'
,
async
()
=>
{
mockPresignVariablesFileUrls
.
mockResolvedValueOnce
({});
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'fileKey'
,
label
:
'fileLabel'
,
type
:
VariableInputEnum
.
file
,
valueType
:
WorkflowIOValueTypeEnum
.
arrayString
}
]
}
as
any
,
variables
:
{}
});
expect
((
result
as
any
).
fileKey
).
toBeUndefined
();
});
it
(
'should use variables[label] when provided (API input)'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'myKey'
,
label
:
'myLabel'
,
type
:
VariableInputEnum
.
input
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
myLabel
:
'from-label'
}
});
expect
((
result
as
any
).
myKey
).
toBe
(
'from-label'
);
});
it
(
'should use variables[key] when label missing (Web input)'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'myKey'
,
label
:
'myLabel'
,
type
:
VariableInputEnum
.
input
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
myKey
:
'from-key'
}
});
expect
((
result
as
any
).
myKey
).
toBe
(
'from-key'
);
});
it
(
'should fall back to defaultValue when neither label nor key provided'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'myKey'
,
label
:
'myLabel'
,
type
:
VariableInputEnum
.
input
,
valueType
:
WorkflowIOValueTypeEnum
.
string
,
defaultValue
:
'fallback'
}
]
}
as
any
,
variables
:
{}
});
expect
((
result
as
any
).
myKey
).
toBe
(
'fallback'
);
});
it
(
'should format value by valueType (number)'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'n'
,
label
:
'nLabel'
,
type
:
VariableInputEnum
.
numberInput
,
valueType
:
WorkflowIOValueTypeEnum
.
number
}
]
}
as
any
,
variables
:
{
nLabel
:
'42'
}
});
expect
((
result
as
any
).
n
).
toBe
(
42
);
});
it
(
'should prefer label over key when both present'
,
async
()
=>
{
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'dup'
,
label
:
'dupLabel'
,
type
:
VariableInputEnum
.
input
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
dupLabel
:
'label-wins'
,
dup
:
'key-loses'
}
});
expect
((
result
as
any
).
dup
).
toBe
(
'label-wins'
);
});
it
(
'should process multiple variables in the same call'
,
async
()
=>
{
mockPresignVariablesFileUrls
.
mockResolvedValueOnce
({
f
:
[{
key
:
'k'
,
url
:
'http://example.com/k'
}]
});
const
encrypted
=
encryptSecret
(
'pwd-plain'
);
const
result
=
await
getSystemVariables
({
...
baseArgs
,
chatConfig
:
{
variables
:
[
{
key
:
'a'
,
label
:
'aLabel'
,
type
:
VariableInputEnum
.
input
,
valueType
:
WorkflowIOValueTypeEnum
.
string
},
{
key
:
'f'
,
label
:
'fLabel'
,
type
:
VariableInputEnum
.
file
,
valueType
:
WorkflowIOValueTypeEnum
.
arrayString
},
{
key
:
'p'
,
label
:
'pLabel'
,
type
:
VariableInputEnum
.
password
,
valueType
:
WorkflowIOValueTypeEnum
.
string
}
]
}
as
any
,
variables
:
{
aLabel
:
'alpha'
,
f
:
[{
key
:
'k'
}],
pLabel
:
JSON
.
stringify
({
value
:
''
,
secret
:
encrypted
})
}
});
expect
((
result
as
any
).
a
).
toBe
(
'alpha'
);
expect
((
result
as
any
).
f
).
toEqual
([
'http://example.com/k'
]);
expect
((
result
as
any
).
p
).
toBe
(
'pwd-plain'
);
});
});
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