Commit 14228b00 by Finley Ge Committed by GitHub

feat: new tool node get the latest version rather than keep latest (#7064)

* fix: workflow system tool use pluginModule

* fix: zip upload cancel && bump plugin sdk version

* Handle latest-version tool previews and confirm keep latest

* chore: update pro submodule

* perf: api

* perf: name

* fix: refine tool preview version selection

* fix(systemTool): sort workflow tool versions by time

---------

Co-authored-by: archer <545436317@qq.com>
parent 4c4384a4
......@@ -13,6 +13,7 @@ import { SystemToolSystemSecretStatusEnum } from '../constants';
export const SystemToolBaseSchema = z.object({
id: z.string(),
version: z.string(),
versionLabel: z.string().optional(),
etag: z.string().optional()
});
......@@ -102,8 +103,14 @@ export const SystemToolDetailSchema = z.object({
export type SystemToolDetailType = z.infer<typeof SystemToolDetailSchema>;
export const SystemToolVersionSchema = z.object({
version: z.string(),
versionDescription: z.string().optional()
version: z.string().meta({
example: '68ad85a7463006c963799a05',
description: '工具版本标识。工作流工具为关联应用版本 ID,普通插件工具为插件版本号'
}),
versionDescription: z.string().optional().meta({
example: 'Workflow v1',
description: '工具版本展示名。工作流工具为关联应用版本名称'
})
});
export type SystemToolVersionType = z.infer<typeof SystemToolVersionSchema>;
import z from 'zod';
import {
FlowNodeTemplateTypeSchema,
NodeTemplateListItemTypeSchema
} from '../../../../core/workflow/type/node';
import { OpenAPIFlowNodeOutputItemTypeSchema } from '../../workflow/node';
import { BoolSchema } from '../../../../common/zod';
const ToolNodeTemplateListItemSchema = NodeTemplateListItemTypeSchema.extend({
toolDescription: z.string().optional().meta({
description: '工具调用描述'
})
}).catchall(z.any());
const ToolPreviewNodeResponseSchema = FlowNodeTemplateTypeSchema.omit({
outputs: true
}).extend({
outputs: z.array(OpenAPIFlowNodeOutputItemTypeSchema)
});
/* ============================================================================
* API: 获取系统工具模板列表
* Route: POST /api/core/app/tool/getSystemToolTemplates
* Method: POST
* Description: 获取可添加到工作流中的系统工具模板列表,支持搜索、标签筛选和工具集子工具查询
* Tags: ['系统工具', 'Read']
* ============================================================================ */
export const GetSystemToolTemplatesBodySchema = z.object({
getAll: z.boolean().optional().meta({
example: false,
description: '是否获取全部工具。当前接口保留该字段用于兼容旧调用'
}),
searchKey: z.string().optional().meta({
example: 'weather',
description: '搜索关键字,会匹配工具名称、简介、工具描述和标签'
}),
parentId: z.string().nullish().meta({
example: 'systemTool-map',
description: '工具集父工具 ID。传入后返回该工具集下的子工具模板'
}),
tags: z
.array(z.string())
.optional()
.meta({
example: ['search'],
description: '工具标签筛选条件'
})
});
export type GetSystemToolTemplatesBodyType = z.infer<typeof GetSystemToolTemplatesBodySchema>;
export const GetSystemToolTemplatesResponseSchema = z.array(ToolNodeTemplateListItemSchema);
export type GetSystemToolTemplatesResponseType = z.infer<
typeof GetSystemToolTemplatesResponseSchema
>;
/* ============================================================================
* API: 获取工具路径
* Route: GET /api/core/app/tool/path
* Method: GET
* Description: 获取系统工具或工具集子工具在工具树中的路径
* Tags: ['系统工具', 'Read']
* ============================================================================ */
export const GetToolPathQuerySchema = z.object({
sourceId: z.string().nullish().meta({
example: 'systemTool-map/geocode',
description: '工具 ID。为空时返回空路径'
}),
type: z.enum(['current', 'parent']).optional().meta({
example: 'current',
description: '路径类型:current 返回当前工具路径,parent 只返回父工具路径'
})
});
export type GetToolPathQueryType = z.infer<typeof GetToolPathQuerySchema>;
export const ToolPathItemSchema = z.object({
parentId: z.string().meta({
example: 'systemTool-map',
description: '路径节点 ID'
}),
parentName: z.string().meta({
example: 'Map',
description: '路径节点名称'
})
});
export const GetToolPathResponseSchema = z.array(ToolPathItemSchema);
export type GetToolPathResponseType = z.infer<typeof GetToolPathResponseSchema>;
/* ============================================================================
* API: 获取工具预览节点
* Route: GET /api/core/app/tool/getPreviewNode
* Method: GET
* Description: 根据工具 ID 和版本配置生成可插入工作流画布的工具节点模板,支持系统工具和我的工具(MCP、HTTP、工作流工具)
* Tags: ['系统工具', 'HTTP 工具管理', 'MCP 工具管理', '团队插件管理', 'Read']
* ============================================================================ */
const GetPreviewNodeBaseQuerySchema = z.object({
appId: z.string().meta({
example: 'systemTool-weather',
description:
'工具 ID,支持系统工具 systemTool/commercial、我的工具 personal/mcp/http 组合 ID 及工具集子工具 ID'
})
});
export const GetPreviewNodeQuerySchema = z.union([
GetPreviewNodeBaseQuerySchema.extend({
versionId: z.string().meta({
example: '68ad85a7463006c963799a05',
description:
'工具版本 ID,与 getLatestVersion 必须二选一。传空字符串时返回最新版节点数据,但响应中的 version 为空'
}),
getLatestVersion: z.undefined().optional()
}),
GetPreviewNodeBaseQuerySchema.extend({
versionId: z.undefined().optional(),
getLatestVersion: BoolSchema.refine((value) => value === true, {
message: 'getLatestVersion must be true when provided'
}).meta({
example: true,
description:
'是否获取最新版本 ID,与 versionId 必须二选一。只能传 true,表示返回最新版节点数据并带上具体 version'
})
})
]);
export type GetPreviewNodeQuery = z.infer<typeof GetPreviewNodeQuerySchema>;
export const GetPreviewNodeResponseSchema = ToolPreviewNodeResponseSchema;
export type GetPreviewNodeResponse = z.infer<typeof GetPreviewNodeResponseSchema>;
import z from 'zod';
export const ToolDetailBodySchema = z.object({});
export const ToolDetailQuerySchema = z.object({
/** 系统工具的 ID
* - systemTool-xxxx
* - systemTool-xxxx/childId
* - comercial-xxxx,
*/
id: z.string().meta({ description: '系统工具的 ID' }),
version: z.string().optional().meta({ description: '系统工具的版本, 如果不填则返回最新版本' }),
source: z.string().optional().meta({ description: '系统工具的来源,默认为 system' })
});
export const ToolDetailResponseSchema = z.object({
// 基本信息
id: z.string(),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
tags: z.array(z.string()).nullish().meta({ description: '系统工具的标签' }),
avatar: z.string().optional().meta({ description: '系统工具的头像' }),
name: z.string().meta({ description: '系统工具的名称' }),
intro: z.string().optional().meta({ description: '系统工具的简介' }),
author: z.string().optional().meta({ description: '系统工具的作者' }),
instructions: z.string().optional().meta({ description: '使用说明 (文字)' }),
courseUrl: z.string().optional().meta({ description: '系统工具的教程链接' }),
readmeUrl: z.string().optional().meta({ description: '系统工具的 README 文档链接' }),
// 输入输出
// 计费相关
/** 这个字段暂时没有用 */
originCost: z.number().optional().meta({ description: '原始价格' }),
currentCost: z.number().optional().meta({ description: '价格' }),
hasTokenFee: z.boolean().optional().meta({ description: '是否配置了系统密钥' }),
systemKeyCost: z.number().optional().meta({ description: '系统密钥费用' })
});
export type ToolDetailBodyType = z.infer<typeof ToolDetailBodySchema>;
export type ToolDetailQueryType = z.infer<typeof ToolDetailQuerySchema>;
export type ToolDetailResponseType = z.infer<typeof ToolDetailResponseSchema>;
import { OpenAPIPath } from '../../../../type';
export const ToolDetailPath = {
'/api/core/app/tool/detail': {
summary: '获取系统工具详情'
}
} satisfies OpenAPIPath;
import { ToolDetailPath } from './detail';
import type { OpenAPIPath } from '../../../type';
import { TagsMap } from '../../../tag';
import {
GetPreviewNodeQuerySchema,
GetPreviewNodeResponseSchema,
GetSystemToolTemplatesBodySchema,
GetSystemToolTemplatesResponseSchema,
GetToolPathQuerySchema,
GetToolPathResponseSchema
} from './api';
export const ToolPath = {
...ToolDetailPath
// TODO
export const ToolPath: OpenAPIPath = {
'/core/app/tool/getSystemToolTemplates': {
post: {
summary: '获取系统工具模板列表',
description: '获取可添加到工作流中的系统工具模板列表,支持搜索、标签筛选和工具集子工具查询',
tags: [TagsMap.appSystemTool],
requestBody: {
content: {
'application/json': {
schema: GetSystemToolTemplatesBodySchema
}
}
},
responses: {
200: {
description: '成功获取系统工具模板列表',
content: {
'application/json': {
schema: GetSystemToolTemplatesResponseSchema
}
}
}
}
}
},
'/core/app/tool/path': {
get: {
summary: '获取工具路径',
description: '获取系统工具或工具集子工具在工具树中的路径',
tags: [TagsMap.appSystemTool],
requestParams: {
query: GetToolPathQuerySchema
},
responses: {
200: {
description: '成功获取工具路径',
content: {
'application/json': {
schema: GetToolPathResponseSchema
}
}
}
}
}
},
'/core/app/tool/getPreviewNode': {
get: {
summary: '获取工具节点信息',
description:
'根据工具 ID 和版本配置生成可插入工作流画布的工具节点模板,支持系统工具和我的工具(MCP、HTTP、工作流工具)',
tags: [TagsMap.appSystemTool, TagsMap.httpTools, TagsMap.mcpTools, TagsMap.pluginTeam],
requestParams: {
query: GetPreviewNodeQuerySchema
},
responses: {
200: {
description: '成功获取工具节点信息',
content: {
'application/json': {
schema: GetPreviewNodeResponseSchema
}
}
}
}
}
}
};
import z from 'zod';
export const SystemToolListItemSchema = z.object({
// 基础信息
id: z.string().meta({ description: '系统工具的 ID' }),
isToolSet: z.boolean().meta({ description: '是否为工具集' }),
avatar: z.string().meta({ description: '工具的图标' }),
name: z.string().meta({ description: '工具的名称' }),
intro: z.string().meta({ description: '工具的简介' }),
author: z.string().meta({ description: '工具的作者' }),
tags: z.array(z.string()).meta({ description: '工具的标签' }),
// 计费相关
currentCost: z.number().meta({ description: '当前使用的费用' }),
systemKeyCost: z.number().meta({ description: '系统密钥的费用' }),
hasTokenFee: z.boolean().meta({ description: '是否有系统密钥费用' })
});
export const SystemToolListBodySchema = z.object({
searchKey: z.string().optional(),
tags: z.array(z.string()).optional()
});
export const SystemToolListQuerySchema = z.object({});
export const SystemToolListResponseSchema = z.array(SystemToolListItemSchema);
export type SystemToolListBodyType = z.infer<typeof SystemToolListBodySchema>;
export type SystemToolListQueryType = z.infer<typeof SystemToolListQuerySchema>;
export type SystemToolListResponseType = z.infer<typeof SystemToolListResponseSchema>;
......@@ -5,7 +5,6 @@ import {
} from '../../../../../core/app/tool/systemTool/type/base';
import type { AdminSystemToolDetailType } from '../../../../../core/app/tool/systemTool/type';
import {
AdminSystemToolChildDetailSchema,
AdminSystemToolDetailSchema,
AdminSystemToolListItemSchema
} from '../../../../../core/app/tool/systemTool/type';
......@@ -28,17 +27,40 @@ export type GetAdminSystemToolsQueryType = z.infer<typeof GetAdminSystemToolsQue
export const GetAdminSystemToolsResponseSchema = z.array(AdminSystemToolListItemSchema);
export type GetAdminSystemToolsResponseType = z.infer<typeof GetAdminSystemToolsResponseSchema>;
// Admin tool detail
/* ============================================================================
* API: 获取系统工具详情
* Route: GET /api/core/plugin/admin/tool/detail
* Method: GET
* Description: 管理员获取系统工具详情,支持按版本查看
* Tags: ['管理员系统工具管理', 'Read']
* ============================================================================ */
export const GetAdminSystemToolDetailQuerySchema = z.object({
toolId: z.string(),
version: z.string().optional()
toolId: z.string().meta({
example: 'systemTool-weather',
description: '系统工具 ID,支持系统工具、商业工具和工具集子工具 ID'
}),
version: z.string().optional().meta({
example: '68ad85a7463006c963799a05',
description: '工具版本 ID。为空时返回最新版本详情'
})
});
export type GetAdminSystemToolDetailQueryType = z.infer<typeof GetAdminSystemToolDetailQuerySchema>;
export type GetAdminSystemToolDetailResponseType = AdminSystemToolDetailType;
// Admin tool versions
/* ============================================================================
* API: 获取系统工具版本列表
* Route: GET /api/core/plugin/admin/tool/versions
* Method: GET
* Description: 管理员获取系统工具版本列表,工作流工具返回关联应用版本 ID 和版本名称
* Tags: ['管理员系统工具管理', 'Read']
* ============================================================================ */
export const GetAdminSystemToolVersionsQuerySchema = z.object({
toolId: z.string()
toolId: z.string().meta({
example: 'systemTool-weather',
description: '系统工具 ID,支持系统工具、商业工具和工具集子工具 ID'
})
});
export type GetAdminSystemToolVersionsQueryType = z.infer<
typeof GetAdminSystemToolVersionsQuerySchema
......
......@@ -5,8 +5,8 @@ import {
GetTeamToolDetailQuerySchema,
GetTeamToolVersionsQuerySchema,
GetTeamToolVersionsResponseSchema,
TeamToolDetailSchema
} from './tool/dto';
OpenAPITeamToolDetailSchema
} from './tool/api';
export const PluginTeamPath: OpenAPIPath = {
'/core/plugin/team/tool/list': {
......@@ -40,7 +40,7 @@ export const PluginTeamPath: OpenAPIPath = {
description: '获取工具卡片详情成功',
content: {
'application/json': {
schema: TeamToolDetailSchema
schema: OpenAPITeamToolDetailSchema
}
}
}
......
......@@ -5,6 +5,15 @@ import {
SystemToolListItemSchema
} from '../../../../../core/app/tool/systemTool/type';
import { SystemToolVersionSchema } from '../../../../../core/app/tool/systemTool/type/base';
import { OpenAPIFlowNodeOutputItemTypeSchema } from '../../../workflow/node';
/* ============================================================================
* API: 获取团队插件列表
* Route: GET /api/core/plugin/team/tool/list
* Method: GET
* Description: 获取当前团队可用的插件和系统工具列表
* Tags: ['团队插件管理', 'Read']
* ============================================================================ */
export const GetTeamSystemPluginListQuerySchema = z.object({});
......@@ -19,10 +28,27 @@ export type GetTeamPluginListResponseType = z.infer<typeof GetTeamPluginListResp
export const GetTeamToolDetailSourceEnum = z.enum(['system', 'team']);
/* ============================================================================
* API: 获取团队工具详情
* Route: GET /api/core/plugin/team/tool/detail
* Method: GET
* Description: 获取当前团队视角下的工具详情,支持系统工具和团队工具来源
* Tags: ['团队插件管理', 'Read']
* ============================================================================ */
export const GetTeamToolDetailQuerySchema = z.object({
toolId: z.string(),
version: z.string().optional(),
source: GetTeamToolDetailSourceEnum.optional()
toolId: z.string().meta({
example: 'systemTool-weather',
description: '工具 ID,支持系统工具、团队工具和工具集子工具 ID'
}),
version: z.string().optional().meta({
example: '68ad85a7463006c963799a05',
description: '工具版本 ID。为空时返回最新版本详情'
}),
source: GetTeamToolDetailSourceEnum.optional().meta({
example: 'system',
description: '工具来源。system 表示系统工具,team 表示当前团队工具'
})
});
export type GetTeamToolDetailQueryType = z.infer<typeof GetTeamToolDetailQuerySchema>;
......@@ -39,9 +65,31 @@ export const TeamToolDetailSchema = z.object({
});
export type GetTeamToolDetailResponseType = z.infer<typeof TeamToolDetailSchema>;
export const GetTeamToolVersionsQuerySchema = z.object({
toolId: z.string(),
source: GetTeamToolDetailSourceEnum.optional()
const OpenAPISystemToolChildDetailSchema = SystemToolChildDetailSchema.omit({
outputs: true
}).extend({
outputs: z.array(OpenAPIFlowNodeOutputItemTypeSchema)
});
export const OpenAPITeamToolDetailSchema = TeamToolDetailSchema.omit({
outputs: true,
children: true
}).extend({
outputs: z.array(OpenAPIFlowNodeOutputItemTypeSchema).optional(),
children: z.array(OpenAPISystemToolChildDetailSchema).optional()
});
/* ============================================================================
* API: 获取团队工具版本列表
* Route: GET /api/core/plugin/team/tool/versions
* Method: GET
* Description: 获取当前团队视角下的工具版本列表,工作流工具返回关联应用版本 ID 和版本名称
* Tags: ['团队插件管理', 'Read']
* ============================================================================ */
export const GetTeamToolVersionsQuerySchema = GetTeamToolDetailQuerySchema.pick({
toolId: true,
source: true
});
export type GetTeamToolVersionsQueryType = z.infer<typeof GetTeamToolVersionsQuerySchema>;
......
import z from 'zod';
export const GetTeamToolDetailQuerySchema = z.object({
toolId: z.string()
});
export type GetTeamToolDetailQueryType = z.infer<typeof GetTeamToolDetailQuerySchema>;
export const ToolDetailItemSchema = z.object({
name: z.string(),
intro: z.string(),
icon: z.string().nullish(),
readme: z.string().nullish(),
versionList: z.array(
z.object({
inputs: z.array(
z.object({
key: z.string(),
label: z.string().nullish(),
description: z.string().nullish(),
valueType: z.string().nullish()
})
),
outputs: z.array(
z.object({
key: z.string(),
label: z.string().nullish(),
description: z.string().nullish(),
valueType: z.string().nullish()
})
)
})
)
});
export const TeamToolDetailSchema = z.object({
tools: z.array(ToolDetailItemSchema),
downloadUrl: z.string()
});
export type GetTeamToolDetailResponseType = z.infer<typeof TeamToolDetailSchema>;
......@@ -17,7 +17,7 @@ const OpenAPIFlowNodeInputItemTypeSchema = FlowNodeInputItemTypeSchema.meta({
// `invalidCondition` in FlowNodeOutputItemTypeSchema is a Zod function schema used only
// by the editor to validate outputs; function schemas cannot be represented in JSON
// Schema, so we strip it before exposing via OpenAPI.
const OpenAPIFlowNodeOutputItemTypeSchema = FlowNodeOutputItemTypeSchema.omit({
export const OpenAPIFlowNodeOutputItemTypeSchema = FlowNodeOutputItemTypeSchema.omit({
invalidCondition: true
}).meta({
description: '工作流节点输出配置'
......
......@@ -33,7 +33,7 @@ export const openAPITagGroups = [
},
{
name: '核心-工具管理',
tags: [TagsMap.httpTools, TagsMap.mcpTools, TagsMap.mcpServer]
tags: [TagsMap.appSystemTool, TagsMap.httpTools, TagsMap.mcpTools, TagsMap.mcpServer]
},
{
name: '核心-AI 相关',
......
......@@ -12,6 +12,7 @@ export const TagsMap = {
mcpTools: 'MCP 工具管理',
httpTools: 'HTTP 工具管理',
mcpServer: 'MCP 发布管理',
appSystemTool: '系统工具',
/* 核心-AI 相关 */
aiSkill: 'AI技能管理',
......
import { describe, expect, it } from 'vitest';
import { GetPreviewNodeQuerySchema } from '@fastgpt/global/openapi/core/app/tool/api';
describe('GetPreviewNodeQuerySchema', () => {
it('accepts versionId, including empty string', () => {
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather',
versionId: ''
}).success
).toBe(true);
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather',
versionId: '68ad85a7463006c963799a05'
}).success
).toBe(true);
});
it('accepts getLatestVersion only when versionId is omitted', () => {
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather',
getLatestVersion: true
}).success
).toBe(true);
});
it('rejects missing or duplicated version selectors', () => {
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather'
}).success
).toBe(false);
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather',
versionId: '',
getLatestVersion: true
}).success
).toBe(false);
});
it('rejects getLatestVersion=false', () => {
expect(
GetPreviewNodeQuerySchema.safeParse({
appId: 'systemTool-weather',
getLatestVersion: false
}).success
).toBe(false);
});
});
......@@ -84,18 +84,26 @@ type AppToolType = WorkflowTemplateType & {
export async function getChildAppPreviewNode({
appId,
versionId,
getLatestVersion,
lang = 'en',
source: toolSource = 'system'
}: {
appId: string;
versionId?: string;
getLatestVersion?: boolean;
lang?: localeType;
source?: string;
}): Promise<FlowNodeTemplateType> {
const { source, pluginId } = splitCombineToolId(appId);
if (source === AppToolSourceEnum.systemTool || source === AppToolSourceEnum.commercial) {
return getToolPreviewNode({ pluginId: appId, versionId, lang, source: toolSource });
return getToolPreviewNode({
pluginId: appId,
versionId,
getLatestVersion,
lang,
source: source === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : toolSource
});
}
// 存在 app 里面的插件的情况
......@@ -106,7 +114,11 @@ export async function getChildAppPreviewNode({
if (!item) return Promise.reject(PluginErrEnum.unExist);
if (AppFolderTypeList.includes(item.type)) return Promise.reject(PluginErrEnum.unExist);
const version = await getAppVersionById({ appId: pluginId, versionId, app: item });
const version = await getAppVersionById({
appId: pluginId,
versionId: versionId || undefined,
app: item
});
const isLatest =
version.versionId && Types.ObjectId.isValid(version.versionId)
......@@ -128,6 +140,8 @@ export async function getChildAppPreviewNode({
};
}
const shouldReturnVersion = versionId ? true : versionId === undefined && getLatestVersion;
return {
id: String(item._id),
teamId: String(item.teamId),
......@@ -142,8 +156,8 @@ export async function getChildAppPreviewNode({
},
templateType: FlowNodeTemplateTypeEnum.teamApp,
version: versionId ? version?.versionId : '',
versionLabel: version?.versionName,
version: shouldReturnVersion ? (version.versionId ?? '') : '',
versionLabel: shouldReturnVersion ? version.versionName : undefined,
isLatestVersion: isLatest,
originCost: 0,
......@@ -159,7 +173,11 @@ export async function getChildAppPreviewNode({
const item = await MongoApp.findById(parentId).lean();
if (!item) return Promise.reject(PluginErrEnum.unExist);
const version = await getAppVersionById({ appId: parentId, versionId, app: item });
const version = await getAppVersionById({
appId: parentId,
versionId: versionId || undefined,
app: item
});
const toolConfig = version.nodes[0].toolConfig?.mcpToolSet;
const tool = await (async () => {
if (toolConfig?.toolList) {
......@@ -201,7 +219,11 @@ export async function getChildAppPreviewNode({
const item = await MongoApp.findById(parentId).lean();
if (!item) return Promise.reject(PluginErrEnum.unExist);
const version = await getAppVersionById({ appId: parentId, versionId, app: item });
const version = await getAppVersionById({
appId: parentId,
versionId: versionId || undefined,
app: item
});
const toolConfig = version.nodes[0].toolConfig?.httpToolSet;
const tool = await (async () => {
if (toolConfig?.toolList) {
......
......@@ -16,21 +16,24 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
export async function getToolPreviewNode({
pluginId,
versionId,
getLatestVersion,
lang = 'en',
source: toolSource = 'system'
}: {
pluginId: string;
versionId?: string;
getLatestVersion?: boolean;
lang?: localeType;
source?: string;
}): Promise<FlowNodeTemplateType> {
const systemToolRepo = SystemToolRepo.getInstance();
const toolDetail = await systemToolRepo.getSystemToolDetail({
pluginId,
version: versionId,
version: versionId || undefined,
lang,
source: toolSource
});
const shouldReturnVersion = versionId ? true : versionId === undefined && getLatestVersion;
const inputs = [
...(toolDetail.secrets?.length
......@@ -65,8 +68,8 @@ export async function getToolPreviewNode({
isTool: true,
catchError: false,
version: versionId, // 为 undefined 时,为保持最新版
versionLabel: versionId,
version: shouldReturnVersion ? toolDetail.version : '',
versionLabel: shouldReturnVersion ? (toolDetail.versionLabel ?? toolDetail.version) : undefined,
isLatestVersion: toolDetail.isLatestVersion,
showSourceHandle: true,
showTargetHandle: true,
......
......@@ -172,7 +172,8 @@ export class SystemToolRepo {
name: dbTool.customConfig.name,
status: dbTool.status,
toolDescription: dbTool.customConfig.toolDescription ?? dbTool.customConfig.intro ?? '',
version: dbTool.customConfig.version ?? '',
version: appVersion.versionId,
versionLabel: appVersion.versionName,
intro: dbTool.customConfig.intro ?? '',
tags: dbTool.customConfig.tags ?? [],
author: dbTool.customConfig.author ?? global.feConfigs.systemTitle ?? '',
......@@ -318,13 +319,8 @@ export class SystemToolRepo {
lang?: `${LangEnum}`;
}): Promise<SystemToolVersionType[]> => {
const { pluginId: rawPluginId, source: pluginSource } = splitCombineToolId(pluginId);
if (pluginSource === AppToolSourceEnum.commercial) {
const tool = await this.getSystemToolRecord(pluginId);
if (!tool || !tool.customConfig?.associatedPluginId) {
return Promise.reject('Plugin is not associated with a app');
}
const tool = await MongoSystemTool.findOne({ pluginId });
if (tool?.customConfig?.associatedPluginId) {
const { associatedPluginId } = tool.customConfig;
const appVersions = await MongoAppVersion.find(
{
......@@ -334,10 +330,11 @@ export class SystemToolRepo {
{
versionName: 1
}
);
).sort({ time: -1, _id: -1 });
return appVersions.map((item) => ({
version: item.versionName
version: String(item._id),
versionDescription: item.versionName
}));
}
......@@ -345,7 +342,7 @@ export class SystemToolRepo {
const versions = await pluginClient.listPluginVersions({
pluginId: parentToolId,
source
source: pluginSource === AppToolSourceEnum.commercial ? AppToolSourceEnum.commercial : source
});
return versions.map((item) => ({
......
......@@ -140,7 +140,10 @@ export async function rewriteAppWorkflowToDetail({
nodes.map(async (node) => {
// Tool node
if (node.pluginId) {
const result = await loadToolNode({ id: node.pluginId, versionId: node.version });
const result = await loadToolNode({
id: node.pluginId,
versionId: node.version ?? ''
});
if (result.success) {
const preview = result.data!;
node.avatar = preview.avatar ?? node.avatar;
......
......@@ -116,6 +116,7 @@ export const getAgentRuntimeTools = async ({
const [toolNode] = await Promise.all([
getChildAppPreviewNode({
appId: tool.id,
versionId: '',
lang
}),
...(authAppId
......
......@@ -78,6 +78,8 @@ describe('getToolPreviewNode', () => {
inputList: secrets
});
expect(result.inputs[1]?.key).toBe('city');
expect(result.version).toBe('1.0.0');
expect(result.versionLabel).toBe('1.0.0');
});
it('keeps inputs unchanged when system tool has no secrets', async () => {
......@@ -116,6 +118,76 @@ describe('getToolPreviewNode', () => {
expect(result.inputs).toHaveLength(1);
expect(result.inputs[0]?.key).toBe('city');
expect(result.version).toBe('');
expect(result.versionLabel).toBeUndefined();
});
it('returns latest version id when requested explicitly', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({
id: 'systemTool-weather',
version: '1.0.0',
status: 1,
source: 'system',
isToolSet: false,
avatar: 'weather.svg',
name: 'Weather',
intro: 'Weather query',
author: 'FastGPT',
tags: [],
toolDescription: 'Weather query',
currentCost: 0,
systemKeyCost: 0,
hasTokenFee: false,
hasSystemSecret: false,
inputs: [],
outputs: []
});
const result = await getToolPreviewNode({
pluginId: 'systemTool-weather',
getLatestVersion: true,
lang: 'en'
});
expect(result.version).toBe('1.0.0');
expect(result.versionLabel).toBe('1.0.0');
});
it('uses latest data but returns empty version when versionId is an empty string', async () => {
mocks.getSystemToolDetail.mockResolvedValueOnce({
id: 'systemTool-weather',
version: '1.0.0',
status: 1,
source: 'system',
isToolSet: false,
avatar: 'weather.svg',
name: 'Weather',
intro: 'Weather query',
author: 'FastGPT',
tags: [],
toolDescription: 'Weather query',
currentCost: 0,
systemKeyCost: 0,
hasTokenFee: false,
hasSystemSecret: false,
inputs: [],
outputs: []
});
const result = await getToolPreviewNode({
pluginId: 'systemTool-weather',
versionId: '',
lang: 'en'
});
expect(mocks.getSystemToolDetail).toHaveBeenCalledWith({
pluginId: 'systemTool-weather',
version: undefined,
lang: 'en',
source: 'system'
});
expect(result.version).toBe('');
expect(result.versionLabel).toBeUndefined();
});
it('returns plugin module preview for commercial workflow tools', async () => {
......@@ -124,6 +196,7 @@ describe('getToolPreviewNode', () => {
version: 'workflow-version',
status: 1,
source: 'system',
versionLabel: 'Workflow v1',
isToolSet: false,
avatar: 'workflow.svg',
name: 'Workflow Tool',
......@@ -159,5 +232,7 @@ describe('getToolPreviewNode', () => {
expect(result.toolConfig).toBeUndefined();
expect(result.isFolder).toBe(false);
expect(result.inputs[0]?.key).toBe('query');
expect(result.version).toBe('workflow-version');
expect(result.versionLabel).toBe('Workflow v1');
});
});
......@@ -3,9 +3,11 @@ import { SystemToolSystemSecretStatusEnum } from '@fastgpt/global/core/app/tool/
const mocks = vi.hoisted(() => ({
listTools: vi.fn(),
listPluginVersions: vi.fn(),
getTool: vi.fn(),
findSystemTools: vi.fn(),
findSystemTool: vi.fn(),
findAppVersions: vi.fn(),
findAppById: vi.fn(),
getAppLatestVersion: vi.fn(),
getAppVersionById: vi.fn(),
......@@ -15,6 +17,7 @@ const mocks = vi.hoisted(() => ({
vi.mock('@fastgpt/service/thirdProvider/fastgptPlugin', () => ({
pluginClient: {
listTools: mocks.listTools,
listPluginVersions: mocks.listPluginVersions,
getTool: mocks.getTool
}
}));
......@@ -40,6 +43,12 @@ vi.mock('@fastgpt/service/core/app/version/controller', () => ({
checkIsLatestVersion: mocks.checkIsLatestVersion
}));
vi.mock('@fastgpt/service/core/app/version/schema', () => ({
MongoAppVersion: {
find: mocks.findAppVersions
}
}));
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
const createPluginTool = ({
......@@ -158,7 +167,11 @@ describe('SystemToolRepo.getSystemToolList', () => {
mocks.listTools.mockResolvedValue([
createPluginTool({ pluginId: 'no-secret', name: 'No secret' }),
createPluginTool({ pluginId: 'need-secret', name: 'Need secret', hasSecret: true }),
createPluginTool({ pluginId: 'configured-secret', name: 'Configured secret', hasSecret: true })
createPluginTool({
pluginId: 'configured-secret',
name: 'Configured secret',
hasSecret: true
})
]);
mocks.findSystemTools.mockResolvedValue([
createToolConfig({ pluginId: 'no-secret', pluginOrder: 1, tags: [] }),
......@@ -186,7 +199,7 @@ describe('SystemToolRepo.getSystemToolList', () => {
});
describe('SystemToolRepo.getSystemToolDetail', () => {
it('returns saved author for workflow tools', async () => {
it('returns app version id and label for workflow tools', async () => {
mocks.findSystemTool.mockResolvedValue({
pluginId: 'systemTool-workflow-tool',
status: 'Normal',
......@@ -210,6 +223,7 @@ describe('SystemToolRepo.getSystemToolDetail', () => {
});
mocks.getAppLatestVersion.mockResolvedValue({
versionId: 'latest-version',
versionName: 'Latest Version',
nodes: []
});
mocks.checkIsLatestVersion.mockResolvedValue(true);
......@@ -220,5 +234,64 @@ describe('SystemToolRepo.getSystemToolDetail', () => {
expect(tool.author).toBe('Custom Author');
expect(tool.hasTokenFee).toBe(true);
expect(tool.version).toBe('latest-version');
expect(tool.versionLabel).toBe('Latest Version');
});
});
describe('SystemToolRepo.getVersions', () => {
it('returns app version ids and names for workflow tools', async () => {
const sortedAppVersions = [
{
_id: '507f1f77bcf86cd799439012',
versionName: 'Workflow v2'
},
{
_id: '507f1f77bcf86cd799439013',
versionName: 'Workflow v1'
}
];
const sortAppVersions = vi.fn().mockResolvedValue(sortedAppVersions);
mocks.findSystemTool.mockResolvedValue({
pluginId: 'commercial-workflow-tool',
customConfig: {
associatedPluginId: '507f1f77bcf86cd799439011'
}
});
mocks.findAppVersions.mockReturnValue({
sort: sortAppVersions
});
const versions = await SystemToolRepo.getInstance().getVersions({
pluginId: 'commercial-workflow-tool'
});
expect(versions).toEqual([
{
version: '507f1f77bcf86cd799439012',
versionDescription: 'Workflow v2'
},
{
version: '507f1f77bcf86cd799439013',
versionDescription: 'Workflow v1'
}
]);
expect(sortAppVersions).toHaveBeenCalledWith({ time: -1, _id: -1 });
});
it('lists commercial plugin versions from commercial source', async () => {
mocks.findSystemTool.mockResolvedValue(undefined);
mocks.listPluginVersions.mockResolvedValue([{ version: '2.0.0' }, { version: '1.0.0' }]);
const versions = await SystemToolRepo.getInstance().getVersions({
pluginId: 'commercial-search'
});
expect(mocks.listPluginVersions).toHaveBeenCalledWith({
pluginId: 'search',
source: 'commercial'
});
expect(versions).toEqual([{ version: '2.0.0' }, { version: '1.0.0' }]);
});
});
......@@ -191,6 +191,7 @@
"invalid_json_format": "JSON format error",
"json_schema_tip": "Example:\n{\n \"name\": \"test\",\n \"description\":\"测试\",\n \"schema\": {\n \"type\":\"object\",\n \"properties\": {\n \"test\": {\n \"type\": \"string\",\n \"description\": \"Text field\"\n }\n },\n \"required\": [\n \"test\"\n ]\n }\n}",
"keep_the_latest": "Keep the latest",
"keep_the_latest_confirm_tip": "Please confirm whether to keep the latest version. Upstream tool updates may make the current configuration or features unavailable. Proceed with caution.",
"llm_multimodal_audio": "Audio",
"llm_multimodal_image": "Image",
"llm_multimodal_select_placeholder": "Select multimodal capabilities",
......
......@@ -191,6 +191,7 @@
"invalid_json_format": "JSON 格式错误",
"json_schema_tip": "示例:\n{\n \"name\": \"test\",\n \"description\":\"测试\",\n \"schema\": {\n \"type\":\"object\",\n \"properties\": {\n \"test\": {\n \"type\": \"string\",\n \"description\": \"Text field\"\n }\n },\n \"required\": [\n \"test\"\n ]\n }\n}",
"keep_the_latest": "保持最新版本",
"keep_the_latest_confirm_tip": "请确认是否保持最新版。上游工具更新可能导致当前配置或功能不可用,请谨慎操作。",
"llm_multimodal_audio": "音频",
"llm_multimodal_image": "图片",
"llm_multimodal_select_placeholder": "请选择多模态能力",
......
......@@ -186,6 +186,7 @@
"invalid_json_format": "JSON 格式錯誤",
"json_schema_tip": "示例:\n{\n \"name\": \"test\",\n \"description\":\"測試\",\n \"schema\": {\n \"type\":\"object\",\n \"properties\": {\n \"test\": {\n \"type\": \"string\",\n \"description\": \"Text field\"\n }\n },\n \"required\": [\n \"test\"\n ]\n }\n}",
"keep_the_latest": "保持最新版本",
"keep_the_latest_confirm_tip": "請確認是否保持最新版。上游工具更新可能導致當前配置或功能不可用,請謹慎操作。",
"llm_multimodal_audio": "音頻",
"llm_multimodal_image": "圖片",
"llm_multimodal_select_placeholder": "請選擇多模態能力",
......
Subproject commit 1ebfaac6a4e6faef2c01d4b6785add3debf7285f
Subproject commit 35458b567a3b719987bdd4e9a18dc747a8b34495
......@@ -189,7 +189,7 @@ export const useSkillManager = ({
return toolId;
}
const toolTemplate = await getToolPreviewNode({ appId: toolId });
const toolTemplate = await getToolPreviewNode({ appId: toolId, versionId: '' });
const toolValid = validateToolConfiguration({
toolTemplate,
......
......@@ -431,7 +431,7 @@ export const loadGeneratedTools = async ({
}
// 新工具,需要与已配置的 tool 进行 input 合并
const tool = await getToolPreviewNode({ appId: toolId });
const tool = await getToolPreviewNode({ appId: toolId, versionId: '' });
// 验证工具配置
const toolValid = validateToolConfiguration({
toolTemplate: tool,
......
......@@ -263,7 +263,7 @@ const RenderList = React.memo(function RenderList({
const { runAsync: onClickAdd, loading: isLoading } = useRequest(
async (template: NodeTemplateListItemType) => {
const res = await getToolPreviewNode({ appId: template.id });
const res = await getToolPreviewNode({ appId: template.id, versionId: '' });
const isToolSetTemplate = template.flowNodeType === FlowNodeTypeEnum.toolSet;
if (!isToolSetTemplate) {
......
......@@ -245,7 +245,10 @@ const NodeTemplateList = ({
].includes(template.flowNodeType);
if (shouldLoadPreviewNode) {
const node = await getToolPreviewNode({ appId: template.id });
const node = await getToolPreviewNode({
appId: template.id,
getLatestVersion: true
});
return {
...node,
flowNodeType:
......
......@@ -23,6 +23,7 @@ import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
import { ConnectionSourceHandle, ConnectionTargetHandle } from './Handle/ConnectionHandle';
import { useDebug } from '../../hooks/useDebug';
import { getToolPreviewNode } from '@/web/core/app/api/tool';
import { getAppVersionList } from '@/web/core/app/api/version';
import { getTeamToolVersions } from '@/web/core/plugin/team/api';
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
......@@ -55,6 +56,8 @@ import { splitCombineToolId, getToolRawId } from '@fastgpt/global/core/app/tool/
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import { getAppPermission } from '@/web/core/app/api';
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { SystemToolVersionType } from '@fastgpt/global/core/app/tool/systemTool/type/base';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
......@@ -220,15 +223,22 @@ const NodeCard = (props: Props) => {
const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType];
const isLoopNode = isNestedParentNodeType(node?.flowNodeType ?? '');
const showVersion = useMemo(() => {
// 1. MCP tool and HTTP tool set do not have version
const source = node?.pluginId ? splitCombineToolId(node.pluginId).source : undefined;
// 1. MCP/HTTP single tools use the latest toolset content and do not expose version selection.
if (source === AppToolSourceEnum.mcp || source === AppToolSourceEnum.http) return false;
// 2. MCP/HTTP tool sets do not have version
if (
isAppNode &&
(node.toolConfig?.mcpToolSet || node.toolConfig?.mcpTool || node?.toolConfig?.httpToolSet)
(node.toolConfig?.mcpToolSet ||
node.toolConfig?.mcpTool ||
node?.toolConfig?.httpToolSet ||
node?.toolConfig?.httpTool)
)
return false;
// 2. Team app/System commercial plugin
// 3. Team app/System commercial plugin
if (isAppNode && node?.pluginId && !node?.pluginData?.error) return true;
// 3. System tool
// 4. System tool
if (isAppNode && node?.toolConfig?.systemTool) return true;
return false;
......@@ -576,6 +586,13 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
const { t } = useTranslation();
const onResetNode = useContextSelector(WorkflowActionsContext, (v) => v.onResetNode);
const { openConfirm: openKeepLatestConfirm, ConfirmModal: KeepLatestConfirmModal } = useConfirm({
content: t('app:keep_the_latest_confirm_tip')
});
const toolSource = useMemo(
() => (node.pluginId ? splitCombineToolId(node.pluginId).source : undefined),
[node.pluginId]
);
const {
runAsync: loadVersions,
......@@ -585,15 +602,35 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
async () => {
if (!node.pluginId) return [];
const { source } = splitCombineToolId(node.pluginId);
const { authAppId } = splitCombineToolId(node.pluginId);
if (toolSource === AppToolSourceEnum.mcp || toolSource === AppToolSourceEnum.http) return [];
if (toolSource === AppToolSourceEnum.personal) {
if (!authAppId) return [];
const { list = [] } = await getAppVersionList({
appId: authAppId,
isPublish: true,
offset: 0,
pageSize: 100
});
return list.map<SystemToolVersionType>((item) => ({
version: item._id,
versionDescription: item.versionName
}));
}
return getTeamToolVersions({
toolId: node.pluginId,
source: source === AppToolSourceEnum.systemTool ? 'system' : 'team'
source:
toolSource === AppToolSourceEnum.systemTool || toolSource === AppToolSourceEnum.commercial
? 'system'
: 'team'
});
},
{
refreshDeps: [node.pluginId]
refreshDeps: [node.pluginId, toolSource]
}
);
......@@ -602,7 +639,10 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
if (!node) return;
if (node.pluginId) {
const template = await getToolPreviewNode({ appId: node.pluginId, version: versionId });
const template = await getToolPreviewNode({
appId: node.pluginId,
versionId
});
if (!!template) {
onResetNode({
......@@ -623,6 +663,19 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
refreshDeps: [node, onResetNode]
}
);
const onSelectVersion = useCallback(
(versionId: string) => {
if (!versionId) {
openKeepLatestConfirm({
onConfirm: () => onUpdateVersion('')
})();
return;
}
return onUpdateVersion(versionId);
},
[onUpdateVersion, openKeepLatestConfirm]
);
const renderVersionList = useCreation(
() => [
......@@ -631,8 +684,7 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
value: ''
},
...versionList.map((item) => ({
label: item.version,
description: item.versionDescription,
label: item.versionDescription || item.version,
value: item.version
}))
],
......@@ -652,18 +704,21 @@ const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeIt
}, [node.isLatestVersion, node.version, node.versionLabel, t]);
return (
<MySelect
className="nowheel"
value={node.version}
onChange={onUpdateVersion}
isLoading={isUpdating || isLoadingVersions}
customOnOpen={loadVersions}
placeholder={node?.versionLabel}
variant={'whitePrimaryOutline'}
size={'sm'}
list={renderVersionList}
valueLabel={valueLabel}
/>
<>
<MySelect
className="nowheel"
value={node.version}
onChange={onSelectVersion}
isLoading={isUpdating || isLoadingVersions}
customOnOpen={loadVersions}
placeholder={node?.versionLabel}
variant={'whitePrimaryOutline'}
size={'sm'}
list={renderVersionList}
valueLabel={valueLabel}
/>
<KeepLatestConfirmModal isLoading={isUpdating} />
</>
);
});
......
......@@ -190,7 +190,7 @@ const RenderList = React.memo(function RenderList({
const { runAsync: onClickAdd, loading: isLoading } = useRequest(
async (template: NodeTemplateListItemType) => {
const res = await getToolPreviewNode({ appId: template.id });
const res = await getToolPreviewNode({ appId: template.id, versionId: '' });
const isToolSetTemplate = template.flowNodeType === FlowNodeTypeEnum.toolSet;
/* Invalid plugin check
......
......@@ -238,7 +238,7 @@ const HomeChatWindow = () => {
const tools: FlowNodeTemplateType[] = await Promise.all(
selectedToolIds.map(async (toolId) => {
const node = await getToolPreviewNode({ appId: toolId });
const node = await getToolPreviewNode({ appId: toolId, versionId: '' });
node.inputs = node.inputs.map((input) => {
const tool = availableTools.find((tool) => tool.pluginId === toolId);
const value = tool?.inputs?.[input.key];
......@@ -433,9 +433,7 @@ const HomeChatWindow = () => {
>
<IconButton
aria-label="Open history"
icon={
<MyIcon name="core/chat/sidebar/menu" w="20px" h="20px" color="currentColor" />
}
icon={<MyIcon name="core/chat/sidebar/menu" w="20px" h="20px" color="currentColor" />}
variant="unstyled"
{...mobileChatHeaderIconButtonStyle}
onClick={onOpenSlider}
......
......@@ -329,7 +329,7 @@ const SystemToolConfigModal = ({
const versionSelectList = useMemo(
() =>
toolVersions.map((item) => ({
label: item.version,
label: item.versionDescription || item.version,
value: item.version
})),
[toolVersions]
......
......@@ -5,26 +5,40 @@ import { getChildAppPreviewNode } from '@fastgpt/service/core/app/tool/controlle
import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import type { NextApiResponse } from 'next';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
export type GetPreviewNodeQuery = { appId: string; version?: string };
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import {
GetPreviewNodeQuerySchema,
GetPreviewNodeResponseSchema,
type GetPreviewNodeQuery
} from '@fastgpt/global/openapi/core/app/tool/api';
async function handler(
req: ApiRequestProps<{}, GetPreviewNodeQuery>,
_res: NextApiResponse<any>
req: ApiRequestProps<Record<string, never>, GetPreviewNodeQuery>
): Promise<FlowNodeTemplateType> {
const { appId, version } = req.query;
const {
query: { appId, versionId, getLatestVersion }
} = parseApiInput({
req,
querySchema: GetPreviewNodeQuerySchema
});
const { authAppId } = splitCombineToolId(appId);
if (authAppId) {
await authApp({ req, authToken: true, appId: authAppId, per: ReadPermissionVal });
}
return getChildAppPreviewNode({ appId, versionId: version, lang: getLocale(req) });
return GetPreviewNodeResponseSchema.parse(
await getChildAppPreviewNode({
appId,
versionId,
getLatestVersion,
lang: getLocale(req)
})
);
}
export default NextAPI(handler);
import { type NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import { NextAPI } from '@/service/middleware/entry';
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
......@@ -9,19 +8,25 @@ import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constant
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
import {
GetSystemToolTemplatesBodySchema,
GetSystemToolTemplatesResponseSchema,
type GetSystemToolTemplatesBodyType
} from '@fastgpt/global/openapi/core/app/tool/api';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
export type GetSystemPluginTemplatesBody = {
getAll?: boolean;
searchKey?: string;
parentId?: ParentIdType;
tags?: string[];
};
export type GetSystemPluginTemplatesBody = GetSystemToolTemplatesBodyType;
export async function handler(
req: ApiRequestProps<GetSystemPluginTemplatesBody>
): Promise<NodeTemplateListItemType[]> {
const { teamId, tmbId, isRoot } = await authCert({ req, authToken: true });
const { tags, parentId, searchKey } = req.body;
const {
body: { tags, parentId, searchKey }
} = parseApiInput({
req,
bodySchema: GetSystemToolTemplatesBodySchema
});
const lang = getLocale(req);
const searchRegex = getSearchRegex(searchKey);
......@@ -56,7 +61,9 @@ export async function handler(
hasTokenFee: parent.hasTokenFee
})) ?? [];
return filterTemplatesBySearchKey(childTemplates, searchRegex);
return GetSystemToolTemplatesResponseSchema.parse(
filterTemplatesBySearchKey(childTemplates, searchRegex)
);
}
// no parentId, get all tools
const tools = await systemToolRepo.getSystemToolList({
......@@ -65,7 +72,7 @@ export async function handler(
tags
});
return tools
const templates = tools
.filter((item) => {
if (isRoot) return true;
if (item.hideTags && item.hideTags.some((tag) => userTags.includes(tag))) return false;
......@@ -82,6 +89,8 @@ export async function handler(
tags: tool.tags
}))
.filter((item) => filterTemplateBySearchKey(item, searchRegex));
return GetSystemToolTemplatesResponseSchema.parse(templates);
}
export default NextAPI(handler);
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type GetPathProps,
type ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import {
GetToolPathQuerySchema,
GetToolPathResponseSchema,
type GetToolPathQueryType,
type GetToolPathResponseType
} from '@fastgpt/global/openapi/core/app/tool/api';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
export type pathQuery = GetPathProps;
export type pathQuery = GetToolPathQueryType;
export type pathBody = {};
export type pathBody = Record<string, never>;
export type pathResponse = Promise<ParentTreePathItemType[]>;
export type pathResponse = Promise<GetToolPathResponseType>;
export async function handler(
req: ApiRequestProps<pathBody, pathQuery>,
res: ApiResponseType<any>
): Promise<pathResponse> {
const { sourceId: pluginId, type = 'current' } = req.query;
export async function handler(req: ApiRequestProps<pathBody, pathQuery>): Promise<pathResponse> {
const {
query: { sourceId: pluginId, type = 'current' }
} = parseApiInput({
req,
querySchema: GetToolPathQuerySchema
});
const lang = getLocale(req);
if (!pluginId) return [];
if (!pluginId) return GetToolPathResponseSchema.parse([]);
const parentToolId = getParentToolId(pluginId);
const pathToolIds = type === 'parent' ? (parentToolId ? [parentToolId] : []) : [];
......@@ -34,7 +39,9 @@ export async function handler(
pathToolIds.push(pluginId);
}
return Promise.all(pathToolIds.map((toolId) => getToolPathItem({ toolId, lang })));
return GetToolPathResponseSchema.parse(
await Promise.all(pathToolIds.map((toolId) => getToolPathItem({ toolId, lang })))
);
}
export default NextAPI(handler);
......@@ -58,7 +65,7 @@ async function getToolPathItem({
}: {
toolId: string;
lang: ReturnType<typeof getLocale>;
}): Promise<ParentTreePathItemType> {
}): Promise<GetToolPathResponseType[number]> {
const systemToolRepo = SystemToolRepo.getInstance();
const { source } = splitCombineToolId(toolId);
const tool = await systemToolRepo.getSystemToolDetail({
......
......@@ -2,7 +2,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
......@@ -23,7 +23,7 @@ async function handler(
}).body;
const { offset, pageSize } = parsePaginationRequest(req);
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
await authApp({ appId, req, per: ReadPermissionVal, authToken: true });
const match = {
appId,
......
......@@ -11,6 +11,7 @@ import {
GetAdminSystemToolVersionsResponseSchema
} from '@fastgpt/global/openapi/core/plugin/admin/tool/api';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
export type getSystemToolVersionsQuery = GetAdminSystemToolVersionsQueryType;
......@@ -22,7 +23,12 @@ async function handler(
req: ApiRequestProps<getSystemToolVersionsBody, getSystemToolVersionsQuery>,
res: ApiResponseType<any>
): Promise<getSystemToolVersionsResponse> {
const { toolId } = GetAdminSystemToolVersionsQuerySchema.parse(req.query);
const {
query: { toolId }
} = parseApiInput({
req,
querySchema: GetAdminSystemToolVersionsQuerySchema
});
const lang = getLocale(req);
await authSystemAdmin({ req });
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
......@@ -7,20 +7,23 @@ import {
TeamToolDetailSchema,
type GetTeamToolDetailQueryType,
type GetTeamToolDetailResponseType
} from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
} from '@fastgpt/global/openapi/core/plugin/team/tool/api';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
export type detailQuery = GetTeamToolDetailQueryType;
export type detailBody = {};
export type detailBody = Record<string, never>;
export type detailResponse = GetTeamToolDetailResponseType;
async function handler(
req: ApiRequestProps<detailBody, detailQuery>,
res: ApiResponseType<any>
): Promise<detailResponse> {
const { toolId, source, version } = GetTeamToolDetailQuerySchema.parse(req.query);
async function handler(req: ApiRequestProps<detailBody, detailQuery>): Promise<detailResponse> {
const {
query: { toolId, source, version }
} = parseApiInput({
req,
querySchema: GetTeamToolDetailQuerySchema
});
const lang = getLocale(req);
const { teamId } = await authCert({ req, authToken: true });
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import {
type GetTeamSystemPluginListQueryType,
type GetTeamPluginListResponseType
} from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
} from '@fastgpt/global/openapi/core/plugin/team/tool/api';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
......@@ -12,7 +12,7 @@ import type { UserTagsType } from '@fastgpt/global/support/user/type';
export type listQuery = GetTeamSystemPluginListQueryType;
export type listBody = {};
export type listBody = Record<string, never>;
export type listResponse = GetTeamPluginListResponseType;
......@@ -26,10 +26,7 @@ const hasMatchedUserTag = ({
return !!targetTags?.some((tag) => userTags.includes(tag));
};
async function handler(
req: ApiRequestProps<listBody, listQuery>,
res: ApiResponseType<any>
): Promise<listResponse> {
async function handler(req: ApiRequestProps<listBody, listQuery>): Promise<listResponse> {
const lang = getLocale(req);
const { teamId, tmbId } = await authCert({ req, authToken: true });
......
......@@ -2,25 +2,30 @@ import { NextAPI } from '@/service/middleware/entry';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { SystemToolRepo } from '@fastgpt/service/core/app/tool/systemTool/systemTool.repo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import {
GetTeamToolVersionsQuerySchema,
GetTeamToolVersionsResponseSchema,
type GetTeamToolVersionsQueryType,
type GetTeamToolVersionsResponseType
} from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
} from '@fastgpt/global/openapi/core/plugin/team/tool/api';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
export type getSystemToolVersionsQuery = GetTeamToolVersionsQueryType;
export type getSystemToolVersionsBody = {};
export type getSystemToolVersionsBody = Record<string, never>;
export type getSystemToolVersionsResponse = GetTeamToolVersionsResponseType;
async function handler(
req: ApiRequestProps<getSystemToolVersionsBody, getSystemToolVersionsQuery>,
res: ApiResponseType<any>
req: ApiRequestProps<getSystemToolVersionsBody, getSystemToolVersionsQuery>
): Promise<getSystemToolVersionsResponse> {
const { toolId, source } = GetTeamToolVersionsQuerySchema.parse(req.query);
const {
query: { toolId, source }
} = parseApiInput({
req,
querySchema: GetTeamToolVersionsQuerySchema
});
const lang = getLocale(req);
const { teamId } = await authCert({ req, authToken: true });
......
......@@ -21,7 +21,7 @@ import ToolDetailDrawer from '@fastgpt/web/components/core/plugin/tool/ToolDetai
import { useUserStore } from '../../../web/support/user/useUserStore';
import { useRouter } from 'next/router';
import { getDocPath } from '@/web/common/system/doc';
import type { GetTeamPluginListResponseType } from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
import type { GetTeamPluginListResponseType } from '@fastgpt/global/openapi/core/plugin/team/tool/api';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import DashboardContainer from '@/pageComponents/dashboard/Container';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
......
......@@ -6,16 +6,18 @@ import type {
import { getAppDetailById, getMyApps } from '../api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import type { GetPreviewNodeQuery } from '@/pages/api/core/app/tool/getPreviewNode';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type {
GetPathProps,
ParentIdType,
ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import type { GetSystemPluginTemplatesBody } from '@/pages/api/core/app/tool/getSystemToolTemplates';
import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants';
import { getMcpChildren } from './mcpTools';
import type {
GetPreviewNodeQuery,
GetSystemToolTemplatesBodyType
} from '@fastgpt/global/openapi/core/app/tool/api';
/* ============ team plugin ============== */
export const getTeamAppTemplates = async (data?: {
......@@ -83,7 +85,7 @@ export const getTeamAppTemplates = async (data?: {
};
/* ============ Tool ============== */
export const getAppToolTemplates = (data: GetSystemPluginTemplatesBody) =>
export const getAppToolTemplates = (data: GetSystemToolTemplatesBodyType) =>
POST<NodeTemplateListItemType[]>('/core/app/tool/getSystemToolTemplates', data);
export const getAppToolPaths = (data: GetPathProps) => {
......
......@@ -2,13 +2,13 @@ import { GET } from '@/web/common/api/request';
import type {
GetTeamSystemPluginListQueryType,
GetTeamPluginListResponseType
} from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
} from '@fastgpt/global/openapi/core/plugin/team/tool/api';
import type {
GetTeamToolDetailQueryType,
GetTeamToolDetailResponseType,
GetTeamToolVersionsQueryType,
GetTeamToolVersionsResponseType
} from '@fastgpt/global/openapi/core/plugin/team/tool/dto';
} from '@fastgpt/global/openapi/core/plugin/team/tool/api';
export const getTeamSystemPluginList = (data: GetTeamSystemPluginListQueryType) =>
GET<GetTeamPluginListResponseType>(`/core/plugin/team/tool/list`, data);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment