This commit is contained in:
archer 2025-11-07 15:58:16 +08:00
parent f5a636e706
commit 3b9f6a3ee4
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
6 changed files with 22 additions and 1154 deletions

View File

@ -16,10 +16,10 @@ import {
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getChildAppRuntimeById } from '../../../../../../app/plugin/controller';
import { getChildAppRuntimeById } from '../../../../../../app/tool/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
import { serverGetWorkflowToolRunUserQuery } from '../../../../../../app/tool/workflowTool/utils';
import { getWorkflowToolInputsFromStoreNodes } from '@fastgpt/global/core/app/tool/workflowTool/utils';
type Props = ModuleDispatchProps<{}> & {
callParams: {
@ -176,8 +176,8 @@ export const dispatchPlugin = async (props: Props): Promise<DispatchSubAppRespon
isChildApp: true
},
variables: runtimeVariables,
query: getPluginRunUserQuery({
pluginInputs: getPluginInputsFromStoreNodes(plugin.nodes),
query: serverGetWorkflowToolRunUserQuery({
pluginInputs: getWorkflowToolInputsFromStoreNodes(plugin.nodes),
variables: runtimeVariables
}).value,
chatConfig: {},

View File

@ -1,10 +1,10 @@
import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import { SystemToolInputTypeEnum } from '@fastgpt/global/core/app/systemTool/constants';
import { SystemToolSecretInputTypeEnum } from '@fastgpt/global/core/app/tool/systemTool/constants';
import type { DispatchSubAppResponse } from '../../type';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
import { getSystemToolById } from '../../../../../../app/plugin/controller';
import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils';
import { getSystemToolById } from '../../../../../../app/tool/controller';
import { getSecretValue } from '../../../../../../../common/secret/utils';
import { MongoSystemPlugin } from '../../../../../../app/plugin/systemPluginSchema';
import { MongoSystemTool } from '../../../../../../plugin/tool/systemToolSchema';
import { APIRunSystemTool } from '../../../../../../app/tool/api';
import type {
ChatDispatchProps,
@ -18,10 +18,10 @@ import { pushTrack } from '../../../../../../../common/middle/tracks/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { getAppVersionById } from '../../../../../../app/version/controller';
import { MCPClient } from '../../../../../../app/mcp';
import type { McpToolDataType } from '@fastgpt/global/core/app/mcpTools/type';
import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type';
type SystemInputConfigType = {
type: SystemToolInputTypeEnum;
type: SystemToolSecretInputTypeEnum;
value: StoreSecretValueType;
};
type Props = {
@ -50,16 +50,16 @@ export const dispatchTool = async ({
const tool = await getSystemToolById(toolConfig?.systemTool.toolId);
const inputConfigParams = await (async () => {
switch (system_input_config?.type) {
case SystemToolInputTypeEnum.team:
case SystemToolSecretInputTypeEnum.team:
return Promise.reject(new Error('This is not supported yet'));
case SystemToolInputTypeEnum.manual:
case SystemToolSecretInputTypeEnum.manual:
return getSecretValue({
storeSecret: system_input_config.value || {}
});
case SystemToolInputTypeEnum.system:
case SystemToolSecretInputTypeEnum.system:
default:
// read from mongo
const dbPlugin = await MongoSystemPlugin.findOne({
const dbPlugin = await MongoSystemTool.findOne({
pluginId: tool.id
}).lean();
return dbPlugin?.inputListVal || {};
@ -122,7 +122,7 @@ export const dispatchTool = async ({
}
const usagePoints = (() => {
if (params.system_input_config?.type !== SystemToolInputTypeEnum.system) {
if (params.system_input_config?.type !== SystemToolSecretInputTypeEnum.system) {
return 0;
}
return (tool.systemKeyCost ?? 0) + (tool.currentCost ?? 0);
@ -147,7 +147,7 @@ export const dispatchTool = async ({
]
};
} else if (toolConfig?.mcpTool?.toolId) {
const { pluginId } = splitCombinePluginId(toolConfig.mcpTool.toolId);
const { pluginId } = splitCombineToolId(toolConfig.mcpTool.toolId);
const [parentId, toolName] = pluginId.split('/');
const tool = await getAppVersionById({
appId: parentId,
@ -163,7 +163,10 @@ export const dispatchTool = async ({
})
});
const result = await mcpClient.toolCall(toolName, params);
const result = await mcpClient.toolCall({
toolName,
params
});
return {
response: JSON.stringify(result),
usages: []

View File

@ -1,184 +0,0 @@
import { useCallback, useMemo, useState, useEffect, useRef } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import type { SkillOptionType } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillPickerPlugin';
import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
type UseAppManagerProps = {
selectedSkillKey?: string;
currentAppId?: string;
};
type UseAppManagerReturn = {
appSkillOptions: SkillOptionType[];
queryString: string | null;
setQueryString: (value: string | null) => void;
loadFolderContent: (folderId: string) => Promise<void>;
removeFolderContent: (folderId: string) => void;
loadedFolders: Set<string>;
};
export const useAppManager = ({
selectedSkillKey,
currentAppId
}: UseAppManagerProps): UseAppManagerReturn => {
const { t, i18n } = useTranslation();
const lang = i18n?.language as localeType;
const [appSkillOptions, setAppSkillOptions] = useState<SkillOptionType[]>([]);
const [queryString, setQueryString] = useState<string | null>(null);
const [loadedFolders, setLoadedFolders] = useState<Set<string>>(new Set());
const appSkillOptionsRef = useRef<SkillOptionType[]>([]);
const defaultAppSkillOption = useMemo(() => {
return {
key: 'app',
label: t('common:App'),
icon: 'core/workflow/template/runApp'
};
}, [t]);
const buildAppSkillOptions = useCallback(
(teamApps: NodeTemplateListItemType[], parentKey: string = 'app') => {
return teamApps
.filter((app) => app.id !== currentAppId) // 过滤掉当前应用
.map((app) => ({
key: app.id,
label: t(parseI18nString(app.name, lang)),
icon: app.isFolder ? 'common/folderFill' : app.avatar || 'core/workflow/template/runApp',
parentKey,
canOpen: app.isFolder
}));
},
[t, lang, currentAppId]
);
const loadFolderContent = useCallback(
async (folderId: string) => {
if (loadedFolders.has(folderId)) return;
try {
// 先添加 loading 占位符
setAppSkillOptions((prev) => {
const newOptions = [
...prev,
{
key: 'loading',
label: 'Loading...',
icon: '',
parentKey: folderId
}
];
appSkillOptionsRef.current = newOptions;
return newOptions;
});
// 加载文件夹内容
const children = await getTeamPlugTemplates({
parentId: folderId,
searchKey: ''
});
// 构建子项选项
const childOptions = buildAppSkillOptions(children, folderId);
// 替换 loading 占位符为实际内容
setAppSkillOptions((prev) => {
const filteredOptions = prev.filter(
(opt) => !(opt.parentKey === folderId && opt.key === 'loading')
);
const newOptions = [...filteredOptions, ...childOptions];
appSkillOptionsRef.current = newOptions;
return newOptions;
});
// 标记已加载
setLoadedFolders((prev) => new Set([...prev, folderId]));
} catch (error) {
console.error('Failed to load folder content:', error);
// 移除 loading 占位符
setAppSkillOptions((prev) => {
const newOptions = prev.filter(
(opt) => !(opt.parentKey === folderId && opt.key === 'loading')
);
appSkillOptionsRef.current = newOptions;
return newOptions;
});
}
},
[loadedFolders, buildAppSkillOptions]
);
const removeFolderContent = useCallback((folderId: string) => {
// 递归移除文件夹及其所有子项的内容
const removeRecursively = (parentId: string) => {
const children = appSkillOptionsRef.current.filter((opt) => opt.parentKey === parentId);
children.forEach((child) => {
if (child.canOpen) {
removeRecursively(child.key);
}
});
};
// 移除文件夹的所有子项
removeRecursively(folderId);
// 从数据中移除所有子项
setAppSkillOptions((prev) => {
const newOptions = prev.filter((opt) => {
// 检查是否是要移除的文件夹的后代
const isDescendant = (optionKey: string): boolean => {
const option = prev.find((o) => o.key === optionKey);
if (!option?.parentKey) return false;
if (option.parentKey === folderId) return true;
return isDescendant(option.parentKey);
};
return !isDescendant(opt.key);
});
appSkillOptionsRef.current = newOptions;
return newOptions;
});
// 从已加载集合中移除
setLoadedFolders((prevLoaded) => {
const newLoaded = new Set(prevLoaded);
newLoaded.delete(folderId);
return newLoaded;
});
}, []);
useRequest2(
async () => {
try {
return await getTeamPlugTemplates({
parentId: '',
searchKey: queryString?.trim() || ''
});
} catch (error) {
console.error('Failed to load team plugin templates:', error);
return [];
}
},
{
manual: false,
refreshDeps: [queryString],
onSuccess(data) {
const options = buildAppSkillOptions(data);
const newOptions = [defaultAppSkillOption, ...options];
setAppSkillOptions(newOptions);
appSkillOptionsRef.current = newOptions;
}
}
);
return {
appSkillOptions,
queryString,
setQueryString,
loadFolderContent,
removeFolderContent,
loadedFolders
};
};

View File

@ -1,359 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import {
getSystemPlugTemplates,
getPluginGroups,
getPreviewPluginNode
} from '@/web/core/app/api/plugin';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { SkillOptionType } from '@fastgpt/web/components/common/Textarea/PromptEditor/plugins/SkillPickerPlugin';
import type {
FlowNodeTemplateType,
NodeTemplateListItemType
} from '@fastgpt/global/core/workflow/type/node';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { workflowStartNodeId } from '@/web/core/app/constants';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/type';
import type { SystemToolGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
export type ExtendedToolType = FlowNodeTemplateType & {
isUnconfigured?: boolean;
};
type UseToolManagerProps = {
appForm: AppFormEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
setConfigTool: (tool: ExtendedToolType | undefined) => void;
selectedSkillKey?: string;
};
type UseToolManagerReturn = {
toolSkillOptions: SkillOptionType[];
queryString: string | null;
setQueryString: (value: string | null) => void;
handleAddToolFromEditor: (toolKey: string) => Promise<string>;
handleConfigureTool: (toolId: string) => void;
handleRemoveToolFromEditor: (toolId: string) => void;
};
export const useToolManager = ({
appForm,
setAppForm,
setConfigTool,
selectedSkillKey
}: UseToolManagerProps): UseToolManagerReturn => {
const { t, i18n } = useTranslation();
const { toast } = useToast();
const lang = i18n?.language as localeType;
const [toolSkillOptions, setToolSkillOptions] = useState<SkillOptionType[]>([]);
const [queryString, setQueryString] = useState<string | null>(null);
/* get tool skills */
const { data: pluginGroups = [] } = useRequest2(
async () => {
try {
return await getPluginGroups();
} catch (error) {
console.error('Failed to load plugin groups:', error);
return [];
}
},
{
manual: false,
onSuccess(data) {
const primaryOptions: SkillOptionType[] = data.map((item) => ({
key: item.groupId,
label: t(item.groupName),
icon: 'core/workflow/template/toolCall'
}));
setToolSkillOptions(primaryOptions);
}
}
);
const requestParentId = useMemo(() => {
if (queryString?.trim()) {
return '';
}
const selectedOption = toolSkillOptions.find((option) => option.key === selectedSkillKey);
if (!toolSkillOptions.some((option) => option.parentKey) && selectedOption) {
return '';
}
if (selectedOption?.canOpen) {
const hasLoadingPlaceholder = toolSkillOptions.some(
(option) => option.parentKey === selectedSkillKey && option.key === 'loading'
);
if (hasLoadingPlaceholder) {
return selectedSkillKey;
}
}
return null;
}, [toolSkillOptions, selectedSkillKey, queryString]);
const buildToolSkillOptions = useCallback(
(systemPlugins: NodeTemplateListItemType[], pluginGroups: SystemToolGroupSchemaType[]) => {
const skillOptions: SkillOptionType[] = [];
pluginGroups.forEach((group) => {
skillOptions.push({
key: group.groupId,
label: t(group.groupName as any),
icon: 'core/workflow/template/toolCall'
});
});
pluginGroups.forEach((group) => {
const categoryMap = group.groupTypes.reduce<
Record<string, { label: string; type: string }>
>((acc, item) => {
acc[item.typeId] = {
label: t(parseI18nString(item.typeName, lang)),
type: item.typeId
};
return acc;
}, {});
const pluginsByCategory = new Map<string, NodeTemplateListItemType[]>();
systemPlugins.forEach((plugin) => {
if (categoryMap[plugin.templateType]) {
if (!pluginsByCategory.has(plugin.templateType)) {
pluginsByCategory.set(plugin.templateType, []);
}
pluginsByCategory.get(plugin.templateType)!.push(plugin);
}
});
pluginsByCategory.forEach((plugins, categoryType) => {
plugins.forEach((plugin) => {
const canOpen = plugin.flowNodeType === 'toolSet' || plugin.isFolder;
const category = categoryMap[categoryType];
skillOptions.push({
key: plugin.id,
label: t(parseI18nString(plugin.name, lang)),
icon: plugin.avatar || 'core/workflow/template/toolCall',
parentKey: group.groupId,
canOpen,
categoryType: category.type,
categoryLabel: category.label
});
if (canOpen) {
skillOptions.push({
key: 'loading',
label: 'Loading...',
icon: plugin.avatar || 'core/workflow/template/toolCall',
parentKey: plugin.id
});
}
});
});
});
return skillOptions;
},
[t, lang]
);
const buildSearchOptions = useCallback(
(searchResults: NodeTemplateListItemType[]) => {
return searchResults.map((plugin) => ({
key: plugin.id,
label: t(parseI18nString(plugin.name, lang)),
icon: plugin.avatar || 'core/workflow/template/toolCall'
}));
},
[t, lang]
);
const updateTertiaryOptions = useCallback(
(
currentOptions: SkillOptionType[],
parentKey: string | undefined,
subItems: NodeTemplateListItemType[]
) => {
const filteredOptions = currentOptions.filter((option) => !(option.parentKey === parentKey));
const newTertiaryOptions = subItems.map((plugin) => ({
key: plugin.id,
label: t(parseI18nString(plugin.name, lang)),
icon: 'core/workflow/template/toolCall',
parentKey
}));
return [...filteredOptions, ...newTertiaryOptions];
},
[t, lang]
);
useRequest2(
async () => {
try {
return await getSystemPlugTemplates({
parentId: requestParentId || '',
searchKey: queryString?.trim() || ''
});
} catch (error) {
console.error('Failed to load system plugin templates:', error);
return [];
}
},
{
manual: requestParentId === null,
refreshDeps: [requestParentId, queryString],
onSuccess(data) {
if (queryString?.trim()) {
const searchOptions = buildSearchOptions(data);
setToolSkillOptions(searchOptions);
} else if (requestParentId === '') {
const fullOptions = buildToolSkillOptions(data, pluginGroups);
setToolSkillOptions(fullOptions);
} else if (requestParentId === selectedSkillKey) {
setToolSkillOptions((prevOptions) =>
updateTertiaryOptions(prevOptions, requestParentId, data)
);
}
}
}
);
const validateToolConfiguration = useCallback(
(toolTemplate: FlowNodeTemplateType): boolean => {
// 检查文件上传配置
const oneFileInput =
toolTemplate.inputs.filter((input) =>
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
).length === 1;
const canUploadFile =
appForm.chatConfig?.fileSelectConfig?.canSelectFile ||
appForm.chatConfig?.fileSelectConfig?.canSelectImg;
const hasValidFileInput = oneFileInput && !!canUploadFile;
// 检查是否有无效的输入配置
const hasInvalidInput = toolTemplate.inputs.some(
(input) =>
// 引用类型但没有工具描述
(input.renderTypeList.length === 1 &&
input.renderTypeList[0] === FlowNodeInputTypeEnum.reference &&
!input.toolDescription) ||
// 包含数据集选择
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) ||
// 包含动态输入参数
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) ||
// 文件选择但配置无效
(input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !hasValidFileInput)
);
if (hasInvalidInput) {
toast({
title: t('app:simple_tool_tips'),
status: 'warning'
});
return false;
}
return true;
},
[appForm.chatConfig, toast, t]
);
const checkNeedsUserConfiguration = useCallback((toolTemplate: FlowNodeTemplateType): boolean => {
const formRenderTypes = [
FlowNodeInputTypeEnum.input,
FlowNodeInputTypeEnum.textarea,
FlowNodeInputTypeEnum.numberInput,
FlowNodeInputTypeEnum.switch,
FlowNodeInputTypeEnum.select,
FlowNodeInputTypeEnum.JSONEditor
];
return (
toolTemplate.inputs.length > 0 &&
toolTemplate.inputs.some((input) => {
// 有工具描述的不需要配置
if (input.toolDescription) return false;
// 禁用流的不需要配置
if (input.key === NodeInputKeyEnum.forbidStream) return false;
// 系统输入配置需要配置
if (input.key === NodeInputKeyEnum.systemInputConfig) return true;
// 检查是否包含表单类型的输入
return formRenderTypes.some((type) => input.renderTypeList.includes(type));
})
);
}, []);
const handleAddToolFromEditor = useCallback(
async (toolKey: string): Promise<string> => {
try {
const toolTemplate = await getPreviewPluginNode({ appId: toolKey });
if (!validateToolConfiguration(toolTemplate)) {
return '';
}
const needsConfiguration = checkNeedsUserConfiguration(toolTemplate);
const toolId = `tool_${getNanoid(6)}`;
const toolInstance: ExtendedToolType = {
...toolTemplate,
id: toolId,
inputs: toolTemplate.inputs.map((input) => {
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
return {
...input,
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
};
}
return input;
}),
isUnconfigured: needsConfiguration
};
setAppForm((state: any) => ({
...state,
selectedTools: [...state.selectedTools, toolInstance]
}));
return toolId;
} catch (error) {
console.error('Failed to add tool from editor:', error);
return '';
}
},
[validateToolConfiguration, checkNeedsUserConfiguration, setAppForm]
);
const handleRemoveToolFromEditor = useCallback(
(toolId: string) => {
setAppForm((state: any) => ({
...state,
selectedTools: state.selectedTools.filter((tool: ExtendedToolType) => tool.id !== toolId)
}));
},
[setAppForm]
);
const handleConfigureTool = useCallback(
(toolId: string) => {
const tool = appForm.selectedTools.find(
(tool: ExtendedToolType) => tool.id === toolId
) as ExtendedToolType;
if (tool?.isUnconfigured) {
setConfigTool(tool);
}
},
[appForm.selectedTools, setConfigTool]
);
return {
toolSkillOptions,
queryString,
setQueryString,
handleAddToolFromEditor,
handleConfigureTool,
handleRemoveToolFromEditor
};
};

View File

@ -1,592 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
css,
Flex,
Grid
} from '@chakra-ui/react';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import {
type FlowNodeTemplateType,
type NodeTemplateListItemType,
type NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
getPluginGroups,
getPreviewPluginNode,
getSystemPlugTemplates,
getSystemPluginPaths
} from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../../context';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useMemoizedFn } from 'ahooks';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { type AppFormEditFormType } from '@fastgpt/global/core/app/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { workflowStartNodeId } from '@/web/core/app/constants';
import ConfigToolModal from '../../component/ConfigToolModal';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
type Props = {
selectedTools: FlowNodeTemplateType[];
chatConfig: AppFormEditFormType['chatConfig'];
selectedModel: LLMModelItemType;
onAddTool: (tool: FlowNodeTemplateType) => void;
onRemoveTool: (tool: NodeTemplateListItemType) => void;
};
export const childAppSystemKey: string[] = [
NodeInputKeyEnum.forbidStream,
NodeInputKeyEnum.history,
NodeInputKeyEnum.historyMaxAmount,
NodeInputKeyEnum.userChatInput
];
enum TemplateTypeEnum {
'systemPlugin' = 'systemPlugin',
'teamPlugin' = 'teamPlugin'
}
const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void }) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin);
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
const {
data: templates = [],
runAsync: loadTemplates,
loading: isLoading
} = useRequest2(
async ({
type = templateType,
parentId = '',
searchVal = searchKey
}: {
type?: TemplateTypeEnum;
parentId?: ParentIdType;
searchVal?: string;
}) => {
if (type === TemplateTypeEnum.systemPlugin) {
return getSystemPlugTemplates({ parentId, searchKey: searchVal });
} else if (type === TemplateTypeEnum.teamPlugin) {
return getTeamPlugTemplates({
parentId,
searchKey: searchVal
}).then((res) => res.filter((app) => app.id !== appDetail._id));
}
},
{
onSuccess(_, [{ type = templateType, parentId = '' }]) {
setTemplateType(type);
setParentId(parentId);
},
refreshDeps: [templateType, searchKey, parentId],
errorToast: t('common:core.module.templates.Load plugin error')
}
);
const { data: paths = [] } = useRequest2(
() => {
if (templateType === TemplateTypeEnum.teamPlugin)
return getAppFolderPath({ sourceId: parentId, type: 'current' });
return getSystemPluginPaths({ sourceId: parentId, type: 'current' });
},
{
manual: false,
refreshDeps: [parentId]
}
);
const onUpdateParentId = useCallback(
(parentId: ParentIdType) => {
loadTemplates({
parentId
});
},
[loadTemplates]
);
useRequest2(() => loadTemplates({ searchVal: searchKey }), {
manual: false,
throttleWait: 300,
refreshDeps: [searchKey]
});
return (
<MyModal
isOpen
title={t('common:core.app.Tool call')}
iconSrc="core/app/toolCall"
onClose={onClose}
maxW={['90vw', '700px']}
w={'700px'}
h={['90vh', '80vh']}
>
{/* Header: row and search */}
<Box px={[3, 6]} pt={4} display={'flex'} justifyContent={'space-between'} w={'full'}>
<FillRowTabs
list={[
{
icon: 'phoneTabbar/tool',
label: t('common:navbar.Toolkit'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
px={'15px'}
value={templateType}
onChange={(e) =>
loadTemplates({
type: e as TemplateTypeEnum,
parentId: null
})
}
/>
<Box w={300}>
<SearchInput
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={
templateType === TemplateTypeEnum.systemPlugin
? t('common:search_tool')
: t('app:search_app')
}
/>
</Box>
</Box>
{/* route components */}
{!searchKey && parentId && (
<Flex mt={2} px={[3, 6]}>
<FolderPath paths={paths} FirstPathDom={null} onClick={onUpdateParentId} />
</Flex>
)}
<MyBox isLoading={isLoading} mt={2} pb={3} flex={'1 0 0'} h={0}>
<Box px={[3, 6]} overflow={'overlay'} height={'100%'}>
<RenderList
templates={templates}
type={templateType}
setParentId={onUpdateParentId}
{...props}
/>
</Box>
</MyBox>
</MyModal>
);
};
export default React.memo(ToolSelectModal);
const RenderList = React.memo(function RenderList({
templates,
type,
onAddTool,
onRemoveTool,
setParentId,
selectedTools,
chatConfig,
selectedModel
}: Props & {
templates: NodeTemplateListItemType[];
type: TemplateTypeEnum;
setParentId: (parentId: ParentIdType) => any;
}) {
const { t, i18n } = useTranslation();
const lang = i18n.language as localeType;
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
const onCloseConfigTool = useCallback(() => setConfigTool(undefined), []);
const { toast } = useToast();
const { runAsync: onClickAdd, loading: isLoading } = useRequest2(
async (template: NodeTemplateListItemType) => {
const res = await getPreviewPluginNode({ appId: template.id });
/* Invalid plugin check
1. Reference type. but not tool description;
2. Has dataset select
3. Has dynamic external data
*/
const oneFileInput =
res.inputs.filter((input) =>
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
).length === 1;
const canUploadFile =
chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg;
const invalidFileInput = oneFileInput && !!canUploadFile;
if (
res.inputs.some(
(input) =>
(input.renderTypeList.length === 1 &&
input.renderTypeList[0] === FlowNodeInputTypeEnum.reference &&
!input.toolDescription) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) ||
(input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !invalidFileInput)
)
) {
return toast({
title: t('app:simple_tool_tips'),
status: 'warning'
});
}
// 判断是否可以直接添加工具,满足以下任一条件:
// 1. 有工具描述
// 2. 是模型选择类型
// 3. 是文件上传类型且:已开启文件上传、非必填、只有一个文件上传输入
const hasInputForm =
res.inputs.length > 0 &&
res.inputs.some((input) => {
if (input.toolDescription) {
return false;
}
if (input.key === NodeInputKeyEnum.forbidStream) {
return false;
}
if (input.key === NodeInputKeyEnum.systemInputConfig) {
return true;
}
// Check if input has any of the form render types
const formRenderTypes = [
FlowNodeInputTypeEnum.input,
FlowNodeInputTypeEnum.textarea,
FlowNodeInputTypeEnum.numberInput,
FlowNodeInputTypeEnum.switch,
FlowNodeInputTypeEnum.select,
FlowNodeInputTypeEnum.JSONEditor
];
return formRenderTypes.some((type) => input.renderTypeList.includes(type));
});
// 构建默认表单数据
const defaultForm = {
...res,
inputs: res.inputs.map((input) => {
// 如果是文件上传类型,设置为从工作流开始节点获取用户文件
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
return {
...input,
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
};
}
return input;
})
};
if (hasInputForm) {
setConfigTool(defaultForm);
} else {
onAddTool(defaultForm);
}
},
{
errorToast: t('common:core.module.templates.Load plugin error')
}
);
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
manual: false
});
const formatTemplatesArray = useMemo(() => {
const data = (() => {
if (type === TemplateTypeEnum.systemPlugin) {
return pluginGroups.map((group) => {
const map = group.groupTypes.reduce<
Record<
string,
{
list: NodeTemplateListItemType[];
label: string;
}
>
>((acc, item) => {
acc[item.typeId] = {
list: [],
label: t(parseI18nString(item.typeName, i18n.language))
};
return acc;
}, {});
templates.forEach((item) => {
if (map[item.templateType]) {
map[item.templateType].list.push({
...item,
name: t(parseI18nString(item.name, i18n.language)),
intro: t(parseI18nString(item.intro, i18n.language))
});
}
});
return {
label: group.groupName,
list: Object.entries(map)
.map(([type, { list, label }]) => ({
type,
label,
list
}))
.filter((item) => item.list.length > 0)
};
});
}
// Team apps
return [
{
list: [
{
list: templates,
type: '',
label: ''
}
],
label: ''
}
];
})();
return data.filter(({ list }) => list.length > 0);
}, [i18n.language, pluginGroups, t, templates, type]);
const gridStyle = useMemo(() => {
if (type === TemplateTypeEnum.teamPlugin) {
return {
gridTemplateColumns: ['1fr', '1fr'],
py: 2,
avatarSize: '2rem'
};
}
return {
gridTemplateColumns: ['1fr', '1fr 1fr'],
py: 3,
avatarSize: '1.75rem'
};
}, [type]);
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
return (
<>
{list.map((item, i) => {
return (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
>
<Flex>
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
</Flex>
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2} columnGap={3}>
{item.list.map((template) => {
const selected = selectedTools.some((tool) => tool.pluginId === template.id);
return (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<MyAvatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{/* {type === TemplateTypeEnum.systemPlugin && (
<CostTooltip
cost={template.currentCost}
hasTokenFee={template.hasTokenFee}
/>
)} */}
</Box>
}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
>
<MyAvatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
flexShrink={0}
/>
<Box
color={'myGray.900'}
fontWeight={'500'}
fontSize={'sm'}
flex={'1 0 0'}
ml={3}
className="textEllipsis"
>
{t(template.name as any)}
</Box>
{selected ? (
<Button
size={'sm'}
variant={'grayDanger'}
leftIcon={<MyIcon name={'delete'} w={'16px'} mr={-1} />}
onClick={() => onRemoveTool(template)}
px={2}
fontSize={'mini'}
>
{t('common:Remove')}
</Button>
) : template.flowNodeType === 'toolSet' ? (
<Flex gap={2}>
<Button
size={'sm'}
variant={'whiteBase'}
isLoading={isLoading}
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
onClick={() => setParentId(template.id)}
px={2}
fontSize={'mini'}
>
{t('common:Open')}
</Button>
<Button
size={'sm'}
variant={'primaryOutline'}
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
isLoading={isLoading}
onClick={() => onClickAdd(template)}
px={2}
fontSize={'mini'}
>
{t('common:Add')}
</Button>
</Flex>
) : template.isFolder ? (
<Button
size={'sm'}
variant={'whiteBase'}
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
onClick={() => setParentId(template.id)}
px={2}
fontSize={'mini'}
>
{t('common:Open')}
</Button>
) : (
<Button
size={'sm'}
variant={'primaryOutline'}
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
isLoading={isLoading}
onClick={() => onClickAdd(template)}
px={2}
fontSize={'mini'}
>
{t('common:Add')}
</Button>
)}
</Flex>
</MyTooltip>
);
})}
</Grid>
</Box>
);
})}
</>
);
});
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<>
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
{formatTemplatesArray.length > 1 ? (
<>
{formatTemplatesArray.map(({ list, label }, index) => (
<AccordionItem key={index} border={'none'}>
<AccordionButton
fontSize={'sm'}
fontWeight={'500'}
color={'myGray.900'}
justifyContent={'space-between'}
alignItems={'center'}
borderRadius={'md'}
px={3}
>
{t(label as any)}
<AccordionIcon />
</AccordionButton>
<AccordionPanel py={0}>
<PluginListRender list={list} />
</AccordionPanel>
</AccordionItem>
))}
</>
) : (
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
)}
</Accordion>
{!!configTool && (
<ConfigToolModal
configTool={configTool}
onCloseConfigTool={onCloseConfigTool}
onAddTool={onAddTool}
/>
)}
</>
);
});

View File

@ -71,7 +71,7 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
name,
avatar,
intro,
type: 'agent',
type,
modules: await (async () => {
if (modules) {
const myModels = new Set(