V4.9.6 feature (#4565)
* Dashboard submenu (#4545) * add app submenu (#4452) * add app submenu * fix * width & i18n * optimize submenu code (#4515) * optimize submenu code * fix * fix * fix * fix ts * perf: dashboard sub menu * doc --------- Co-authored-by: heheer <heheer@sealos.io> * feat: value format test * doc * Mcp export (#4555) * feat: mcp server * feat: mcp server * feat: mcp server build * update doc * perf: path selector (#4556) * perf: path selector * fix: docker file path * perf: add image endpoint to dataset search (#4557) * perf: add image endpoint to dataset search * fix: mcp_server url * human in loop (#4558) * Support interactive nodes for loops, and enhance the function of merging nested and loop node history messages. (#4552) * feat: add LoopInteractive definition * feat: Support LoopInteractive type and update related logic * fix: Refactor loop handling logic and improve output value initialization * feat: Add mergeSignId to dispatchLoop and dispatchRunAppNode responses * feat: Enhance mergeChatResponseData to recursively merge plugin details and improve response handling * refactor: Remove redundant comments in mergeChatResponseData for clarity * perf: loop interactive * perf: human in loop --------- Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com> * mcp server ui * integrate mcp (#4549) * integrate mcp * delete unused code * fix ts * bug fix * fix * support whole mcp tools * add try catch * fix * fix * fix ts * fix test * fix ts * fix: interactive in v1 completions * doc * fix: router path * fix mcp integrate (#4563) * fix mcp integrate * fix ui * fix: mcp ux * feat: mcp call title * remove repeat loading * fix mcp tools avatar (#4564) * fix * fix avatar * fix update version * update doc * fix: value format * close server and remove cache * perf: avatar --------- Co-authored-by: heheer <heheer@sealos.io> Co-authored-by: Theresa <63280168+sd0ric4@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const WorkOrderShowRouter: { [key: string]: boolean } = {
|
||||
'/app/list': true,
|
||||
'/dashboard/apps': true,
|
||||
'/dataset/list': true,
|
||||
'/toolkit': true
|
||||
};
|
||||
|
||||
@@ -55,8 +55,14 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
label: t('common:navbar.Studio'),
|
||||
icon: 'core/app/aiLight',
|
||||
activeIcon: 'core/app/aiFill',
|
||||
link: `/app/list`,
|
||||
activeLink: ['/app/list', '/app/detail']
|
||||
link: `/dashboard/apps`,
|
||||
activeLink: [
|
||||
'/dashboard/apps',
|
||||
'/app/detail',
|
||||
'/dashboard/templateMarket',
|
||||
'/dashboard/[pluginGroupId]',
|
||||
'/dashboard/mcpServer'
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('common:navbar.Datasets'),
|
||||
@@ -65,13 +71,6 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
link: `/dataset/list`,
|
||||
activeLink: ['/dataset/list', '/dataset/detail']
|
||||
},
|
||||
{
|
||||
label: t('common:navbar.Toolkit'),
|
||||
icon: 'phoneTabbar/tool',
|
||||
activeIcon: 'phoneTabbar/toolFill',
|
||||
link: `/toolkit`,
|
||||
activeLink: ['/toolkit']
|
||||
},
|
||||
{
|
||||
label: t('common:navbar.Account'),
|
||||
icon: 'support/user/userLight',
|
||||
@@ -125,6 +124,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
||||
<Box flex={1}>
|
||||
{navbarList.map((item) => {
|
||||
const isActive = item.activeLink.includes(router.pathname);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={item.link}
|
||||
|
||||
@@ -24,8 +24,14 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
label: t('common:navbar.Studio'),
|
||||
icon: 'core/app/aiLight',
|
||||
activeIcon: 'core/app/aiFill',
|
||||
link: `/app/list`,
|
||||
activeLink: ['/app/list', '/app/detail'],
|
||||
link: `/dashboard/apps`,
|
||||
activeLink: [
|
||||
'/dashboard/apps',
|
||||
'/app/detail',
|
||||
'/dashboard/templateMarket',
|
||||
'/dashboard/[pluginGroupId]',
|
||||
'/dashboard/mcpServer'
|
||||
],
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
@@ -36,14 +42,6 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
|
||||
activeLink: ['/dataset/list', '/dataset/detail'],
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
label: t('common:navbar.Toolkit'),
|
||||
icon: 'phoneTabbar/tool',
|
||||
activeIcon: 'phoneTabbar/toolFill',
|
||||
link: `/toolkit`,
|
||||
activeLink: ['/toolkit'],
|
||||
unread: 0
|
||||
},
|
||||
{
|
||||
label: t('common:navbar.Account'),
|
||||
icon: 'support/user/userLight',
|
||||
|
||||
@@ -11,6 +11,7 @@ const FolderPath = (props: {
|
||||
onClick: (parentId: string) => void;
|
||||
fontSize?: string;
|
||||
hoverStyle?: BoxProps;
|
||||
forbidLastClick?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -19,7 +20,8 @@ const FolderPath = (props: {
|
||||
FirstPathDom,
|
||||
onClick,
|
||||
fontSize,
|
||||
hoverStyle
|
||||
hoverStyle,
|
||||
forbidLastClick = false
|
||||
} = props;
|
||||
|
||||
const concatPaths = useMemo(
|
||||
@@ -37,41 +39,46 @@ const FolderPath = (props: {
|
||||
<>{FirstPathDom}</>
|
||||
) : (
|
||||
<Flex flex={1}>
|
||||
{concatPaths.map((item, i) => (
|
||||
<Flex key={item.parentId || i} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={['xs', fontSize || 'sm']}
|
||||
py={0.5}
|
||||
px={1.5}
|
||||
borderRadius={'md'}
|
||||
maxW={'45vw'}
|
||||
className={'textEllipsis'}
|
||||
{...(i === concatPaths.length - 1 && concatPaths.length > 1
|
||||
? {
|
||||
cursor: 'default',
|
||||
color: 'myGray.700',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'medium',
|
||||
color: 'myGray.500',
|
||||
_hover: {
|
||||
bg: 'myGray.100',
|
||||
...hoverStyle
|
||||
},
|
||||
onClick: () => {
|
||||
onClick(item.parentId);
|
||||
{concatPaths.map((item, i) => {
|
||||
const clickStyles = {
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'myGray.100',
|
||||
...hoverStyle
|
||||
},
|
||||
onClick: () => {
|
||||
onClick(item.parentId);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex key={item.parentId || i} alignItems={'center'}>
|
||||
<Box
|
||||
fontSize={['xs', fontSize || 'sm']}
|
||||
py={0.5}
|
||||
px={1.5}
|
||||
borderRadius={'md'}
|
||||
maxW={'45vw'}
|
||||
className={'textEllipsis'}
|
||||
{...(i === concatPaths.length - 1 && concatPaths.length > 1
|
||||
? {
|
||||
color: 'myGray.700',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
})}
|
||||
>
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
: {
|
||||
fontWeight: 'medium',
|
||||
color: 'myGray.500',
|
||||
...clickStyles
|
||||
})}
|
||||
{...(i === concatPaths.length - 1 && !forbidLastClick && clickStyles)}
|
||||
>
|
||||
{item.parentName}
|
||||
</Box>
|
||||
{i !== concatPaths.length - 1 && (
|
||||
<MyIcon name={'common/line'} color={'myGray.500'} mx={1} width={'5px'} />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const useChatBox = () => {
|
||||
`;
|
||||
} else if (item.type === ChatItemValueTypeEnum.tool) {
|
||||
return `
|
||||
\`\`\`Toll
|
||||
\`\`\`Tool
|
||||
${JSON.stringify(item.tools, null, 2)}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
@@ -66,6 +66,7 @@ import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import TimeBox from './components/TimeBox';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
|
||||
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
|
||||
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
|
||||
@@ -440,12 +441,13 @@ const ChatBox = ({
|
||||
// Only declared variables are kept
|
||||
const requestVariables: Record<string, any> = {};
|
||||
allVariableList?.forEach((item) => {
|
||||
requestVariables[item.key] =
|
||||
const val =
|
||||
variables[item.key] === '' ||
|
||||
variables[item.key] === undefined ||
|
||||
variables[item.key] === null
|
||||
? item.defaultValue
|
||||
: variables[item.key];
|
||||
requestVariables[item.key] = valueTypeFormat(val, item.valueType);
|
||||
});
|
||||
|
||||
const responseChatId = getNanoid(24);
|
||||
|
||||
@@ -438,6 +438,9 @@ export const WholeResponseContent = ({
|
||||
label={t('workflow:tool_params.tool_params_result')}
|
||||
value={activeModule?.toolParamsResult}
|
||||
/>
|
||||
|
||||
{/* tool */}
|
||||
<Row label={t('workflow:tool.tool_result')} value={activeModule?.toolRes} />
|
||||
</Box>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -73,7 +73,6 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<MyBox
|
||||
isLoading={isFetching}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
zIndex={3}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Box, Button, Flex, HStack, IconButton } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { AppContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import TagsEditModal from '../TagsEditModal';
|
||||
|
||||
const AppCard = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
|
||||
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
|
||||
|
||||
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box px={[4, 6]} py={4} position={'relative'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'28px'} />
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={'md'} flex={'1 0 0'} color={'myGray.900'}>
|
||||
{appDetail.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box
|
||||
flex={1}
|
||||
mt={3}
|
||||
mb={4}
|
||||
className={'textEllipsis3'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
minH={'46px'}
|
||||
>
|
||||
{appDetail.intro || t('common:core.app.tip.Add a intro to app')}
|
||||
</Box>
|
||||
<HStack alignItems={'center'}>
|
||||
{appDetail.permission.hasManagePer && (
|
||||
<Button
|
||||
size={['sm', 'md']}
|
||||
variant={'whitePrimary'}
|
||||
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
|
||||
onClick={onOpenInfoEdit}
|
||||
>
|
||||
{t('common:common.Setting')}
|
||||
</Button>
|
||||
)}
|
||||
{appDetail.permission.isOwner && (
|
||||
<MyMenu
|
||||
size={'xs'}
|
||||
Button={
|
||||
<IconButton
|
||||
variant={'whitePrimary'}
|
||||
size={['smSquare', 'mdSquare']}
|
||||
icon={<MyIcon name={'more'} w={'1rem'} />}
|
||||
aria-label={''}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'delete',
|
||||
type: 'danger',
|
||||
label: t('common:common.Delete'),
|
||||
onClick: onDelApp
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
</HStack>
|
||||
</Box>
|
||||
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppCard);
|
||||
241
projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx
Normal file
241
projects/app/src/pageComponents/app/detail/MCPTools/ChatTest.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
|
||||
import { cardStyles } from '../constants';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { postRunMCPTools } from '@/web/core/app/api/plugin';
|
||||
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
|
||||
const ChatTest = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [output, setOutput] = useState<string>('');
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
reset({});
|
||||
setOutput('');
|
||||
}, [currentTool, reset]);
|
||||
|
||||
const { runAsync: runTool, loading: isRunning } = useRequest2(
|
||||
async (data: Record<string, any>) => {
|
||||
if (!currentTool) return;
|
||||
return await postRunMCPTools({
|
||||
params: data,
|
||||
url,
|
||||
toolName: currentTool.name
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
try {
|
||||
const resStr = JSON.stringify(res, null, 2);
|
||||
setOutput(resStr);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex h={'full'} gap={2}>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
py={4}
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]} pb={4}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} color={'myGray.900'} mr={3}>
|
||||
{t('app:chat_debug')}
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
</Flex>
|
||||
|
||||
<Box flex={1} px={[2, 5]} overflow={'auto'}>
|
||||
{Object.keys(currentTool?.inputSchema.properties || {}).length > 0 && (
|
||||
<>
|
||||
<Box color={'myGray.900'} fontSize={'16px'} fontWeight={'medium'} mb={3}>
|
||||
{t('common:common.Input')}
|
||||
</Box>
|
||||
<Box border={'1px solid'} borderColor={'myGray.200'} borderRadius={'8px'} p={3}>
|
||||
{Object.entries(currentTool?.inputSchema.properties || {}).map(
|
||||
([paramName, paramInfo]) => (
|
||||
<Controller
|
||||
key={paramName}
|
||||
control={control}
|
||||
name={paramName}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (!currentTool?.inputSchema.required?.includes(paramName)) return true;
|
||||
return !!value;
|
||||
}
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<RenderToolInput
|
||||
paramName={paramName}
|
||||
paramInfo={paramInfo}
|
||||
toolData={currentTool}
|
||||
isInvalid={errors && Object.keys(errors).includes(paramName)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button mt={3} isLoading={isRunning} onClick={handleSubmit(runTool)}>
|
||||
{t('common:common.Run')}
|
||||
</Button>
|
||||
|
||||
{output && (
|
||||
<>
|
||||
<Box color={'myGray.900'} fontSize={'16px'} fontWeight={'medium'} mb={3} mt={8}>
|
||||
{t('common:common.Output')}
|
||||
</Box>
|
||||
<Box>
|
||||
<Markdown source={`~~~json\n${output}`} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ currentTool, url }: { currentTool: ToolType | null; url: string }) => {
|
||||
const { chatId } = useChatStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
const chatRecordProviderParams = useMemo(
|
||||
() => ({
|
||||
chatId: chatId,
|
||||
appId: appDetail._id
|
||||
}),
|
||||
[appDetail._id, chatId]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChatItemContextProvider
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest currentTool={currentTool} url={url} />
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Render);
|
||||
|
||||
const RenderToolInput = ({
|
||||
paramName,
|
||||
paramInfo,
|
||||
toolData,
|
||||
value,
|
||||
onChange,
|
||||
isInvalid
|
||||
}: {
|
||||
paramName: string;
|
||||
paramInfo: {
|
||||
type: string;
|
||||
description?: string;
|
||||
};
|
||||
toolData: ToolType | null;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
isInvalid: boolean;
|
||||
}) => {
|
||||
const render = (() => {
|
||||
if (paramInfo.type === 'string') {
|
||||
return (
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={paramInfo.description}
|
||||
bg={'myGray.50'}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (paramInfo.type === 'number') {
|
||||
return (
|
||||
<MyNumberInput
|
||||
step={1}
|
||||
bg={'myGray.50'}
|
||||
isInvalid={isInvalid}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (paramInfo.type === 'boolean') {
|
||||
return <Switch isChecked={value} onChange={onChange} isInvalid={isInvalid} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
placeholder={paramInfo.description}
|
||||
resize
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box _notLast={{ mb: 4 }}>
|
||||
<Flex alignItems="center" mb={1}>
|
||||
{toolData?.inputSchema.required?.includes(paramName) && (
|
||||
<Box mr={1} color="red.500">
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<FormLabel fontSize="14px" fontWeight={'normal'} color="myGray.900">
|
||||
{paramName}
|
||||
<QuestionTip label={paramInfo.description} ml={1} />
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
|
||||
{render}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
71
projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx
Normal file
71
projects/app/src/pageComponents/app/detail/MCPTools/Edit.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import React from 'react';
|
||||
import styles from '../SimpleApp/styles.module.scss';
|
||||
import { cardStyles } from '../constants';
|
||||
import AppCard from './AppCard';
|
||||
import ChatTest from './ChatTest';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import EditForm from './EditForm';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
const Edit = ({
|
||||
url,
|
||||
setUrl,
|
||||
toolList,
|
||||
setToolList,
|
||||
currentTool,
|
||||
setCurrentTool
|
||||
}: {
|
||||
url: string;
|
||||
setUrl: (url: string) => void;
|
||||
toolList: ToolType[];
|
||||
setToolList: (toolList: ToolType[]) => void;
|
||||
currentTool: ToolType | null;
|
||||
setCurrentTool: (tool: ToolType) => void;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
display={['block', 'flex']}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
mt={[4, 0]}
|
||||
gap={1}
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
minW={['auto', '580px']}
|
||||
mb={3}
|
||||
flex={'1 0 0'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} {...cardStyles} flex={'1 0 0'} overflow={'auto'} boxShadow={'2'}>
|
||||
<EditForm
|
||||
toolList={toolList}
|
||||
setToolList={setToolList}
|
||||
currentTool={currentTool}
|
||||
setCurrentTool={setCurrentTool}
|
||||
url={url}
|
||||
setUrl={setUrl}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
{isPc && (
|
||||
<Box flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest currentTool={currentTool} url={url} />
|
||||
</Box>
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Edit);
|
||||
264
projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx
Normal file
264
projects/app/src/pageComponents/app/detail/MCPTools/EditForm.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getMCPTools } from '@/web/core/app/api/plugin';
|
||||
import { AppContext } from '../context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { getMCPToolsBody } from '@/pages/api/core/app/mcpTools/getMCPTools';
|
||||
|
||||
const EditForm = ({
|
||||
url,
|
||||
setUrl,
|
||||
toolList,
|
||||
setToolList,
|
||||
currentTool,
|
||||
setCurrentTool
|
||||
}: {
|
||||
url: string;
|
||||
setUrl: (url: string) => void;
|
||||
toolList: ToolType[];
|
||||
setToolList: (toolList: ToolType[]) => void;
|
||||
currentTool: ToolType | null;
|
||||
setCurrentTool: (tool: ToolType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [toolDetail, setToolDetail] = useState<ToolType | null>(null);
|
||||
|
||||
const { runAsync: runGetMCPTools, loading: isGettingTools } = useRequest2(
|
||||
async (data: getMCPToolsBody) => await getMCPTools(data),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setToolList(res);
|
||||
setCurrentTool(res[0]);
|
||||
},
|
||||
errorToast: t('app:MCP_tools_parse_failed')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box p={6}>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name={'common/linkBlue'} w={'20px'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{t('app:MCP_tools_url')}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} gap={2} mt={3}>
|
||||
<Input
|
||||
h={8}
|
||||
placeholder={t('app:MCP_tools_url_placeholder')}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
h={8}
|
||||
isLoading={isGettingTools}
|
||||
onClick={() => {
|
||||
runGetMCPTools({ url });
|
||||
}}
|
||||
>
|
||||
{t('common:common.Parse')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems={'center'} mt={6}>
|
||||
<MyIcon name={'common/list'} w={'20px'} color={'primary.600'} />
|
||||
<FormLabel ml={2} flex={1}>
|
||||
{t('app:MCP_tools_list_with_number', {
|
||||
total: toolList.length || 0
|
||||
})}
|
||||
</FormLabel>
|
||||
</Flex>
|
||||
|
||||
<Box mt={3}>
|
||||
{toolList.map((tool, index) => {
|
||||
return (
|
||||
<MyBox
|
||||
key={tool.name}
|
||||
role="group"
|
||||
position="relative"
|
||||
border={'1px solid'}
|
||||
{...(currentTool?.name === tool.name
|
||||
? {
|
||||
borderRadius: '8px',
|
||||
borderColor: 'primary.600',
|
||||
borderBottomColor: 'primary.600',
|
||||
boxShadow:
|
||||
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
: {
|
||||
borderRadius: 'none',
|
||||
borderColor: 'transparent',
|
||||
borderBottomColor: 'myGray.150',
|
||||
boxShadow: 'none'
|
||||
})}
|
||||
_hover={{
|
||||
borderRadius: '8px',
|
||||
boxShadow:
|
||||
'0px 4px 4px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} py={2} px={3}>
|
||||
<Box w={'20px'} fontSize={'14px'} color={'myGray.500'} fontWeight={'medium'}>
|
||||
{index + 1 < 10 ? `0${index + 1}` : index + 1}
|
||||
</Box>
|
||||
<Box maxW={'full'} pl={2} position="relative" width="calc(100% - 30px)">
|
||||
<Box
|
||||
fontSize={'14px'}
|
||||
color={'myGray.900'}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{tool.name}
|
||||
</Box>
|
||||
<Box
|
||||
fontSize={'12px'}
|
||||
color={'myGray.500'}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{tool.description || t('app:tools_no_description')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
right={3}
|
||||
top="50%"
|
||||
transform="translateY(-50%)"
|
||||
gap={2}
|
||||
display="none"
|
||||
_groupHover={{ display: 'flex' }}
|
||||
background="linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 15%, rgba(255,255,255,1) 100%)"
|
||||
paddingLeft="20px"
|
||||
>
|
||||
<MyIconButton
|
||||
size={'16px'}
|
||||
icon={'common/detail'}
|
||||
p={2}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.250'}
|
||||
hoverBg={'rgba(51, 112, 255, 0.10)'}
|
||||
hoverBorderColor={'primary.300'}
|
||||
tip={t('app:MCP_tools_detail')}
|
||||
onClick={() => {
|
||||
setToolDetail(tool);
|
||||
}}
|
||||
/>
|
||||
<MyIconButton
|
||||
size={'16px'}
|
||||
icon={'core/workflow/debug'}
|
||||
p={2}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.250'}
|
||||
hoverBg={'rgba(51, 112, 255, 0.10)'}
|
||||
hoverBorderColor={'primary.300'}
|
||||
tip={t('app:MCP_tools_debug')}
|
||||
onClick={() => {
|
||||
setCurrentTool(tool);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{toolDetail && <ToolDetailModal tool={toolDetail} onClose={() => setToolDetail(null)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditForm);
|
||||
|
||||
const ToolDetailModal = ({ tool, onClose }: { tool: ToolType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
iconSrc="common/detail"
|
||||
iconColor={'primary.600'}
|
||||
title={t('app:tool_detail')}
|
||||
onClose={onClose}
|
||||
w={'530px'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex pb={6} borderBottom={'1px solid'} borderColor={'myGray.200'}>
|
||||
<Avatar src={appDetail.avatar} borderRadius={'md'} w={'40px'} />
|
||||
<Box ml={'14px'}>
|
||||
<Box fontSize={'16px'} color={'myGray.900'}>
|
||||
{tool.name}
|
||||
</Box>
|
||||
<Box fontSize={'12px'} color={'myGray.500'}>
|
||||
{tool.description}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={6} color={'myGray.900'} fontWeight={'medium'}>
|
||||
{t('common:common.Params')}
|
||||
</Box>
|
||||
|
||||
<Box mt={3}>
|
||||
{Object.entries(tool.inputSchema.properties || {}).map(
|
||||
([paramName, paramInfo]: [string, any]) => (
|
||||
<Box key={paramName} py={2} borderBottom={'1px solid'} borderColor={'myGray.150'}>
|
||||
<Flex alignItems="center">
|
||||
{tool.inputSchema.required?.includes(paramName) && (
|
||||
<Box mr={1} color="red.500">
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
<Box fontSize="14px" color="myGray.900">
|
||||
{paramName}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
ml={1}
|
||||
fontSize={'12px'}
|
||||
color={'myGray.600'}
|
||||
px={1}
|
||||
bg={'myGray.25'}
|
||||
borderRadius={'sm'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
>
|
||||
{paramInfo.type}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={1} fontSize="13px" color="myGray.600">
|
||||
{paramInfo.description}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size={'md'} onClick={onClose}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import { useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { postUpdateMCPTools } from '@/web/core/app/api/plugin';
|
||||
|
||||
const Header = ({ url, toolList }: { url: string; toolList: ToolType[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const appId = useContextSelector(AppContext, (v) => v.appId);
|
||||
const router = useRouter();
|
||||
|
||||
const { lastAppListRouteType } = useSystemStore();
|
||||
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => getAppFolderPath({ sourceId: appId, type: 'parent' }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
}
|
||||
);
|
||||
|
||||
const onClickRoute = useCallback(
|
||||
(parentId: string) => {
|
||||
router.push({
|
||||
pathname: '/dashboard/apps',
|
||||
query: {
|
||||
parentId,
|
||||
type: lastAppListRouteType
|
||||
}
|
||||
});
|
||||
},
|
||||
[router, lastAppListRouteType]
|
||||
);
|
||||
|
||||
const { runAsync: saveMCPTools, loading: isSavingMCPTools } = useRequest2(
|
||||
async () => {
|
||||
return await postUpdateMCPTools({ appId, url, toolList });
|
||||
},
|
||||
{
|
||||
successToast: t('common:common.Update Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Box h={14}>
|
||||
{/* {!isPc && (
|
||||
<Flex justifyContent={'center'}>
|
||||
<RouteTab />
|
||||
</Flex>
|
||||
)} */}
|
||||
<Flex w={'full'} alignItems={'center'} position={'relative'} h={'full'}>
|
||||
<Box flex={'1'}>
|
||||
<FolderPath
|
||||
rootName={t('app:all_apps')}
|
||||
paths={paths}
|
||||
hoverStyle={{ color: 'primary.600' }}
|
||||
onClick={onClickRoute}
|
||||
fontSize={'14px'}
|
||||
/>
|
||||
</Box>
|
||||
{/* {isPc && (
|
||||
<Box position={'absolute'} left={'50%'} transform={'translateX(-50%)'}>
|
||||
<RouteTab />
|
||||
</Box>
|
||||
)} */}
|
||||
<Button size={'sm'} isLoading={isSavingMCPTools} onClick={() => saveMCPTools()}>
|
||||
{t('common:common.Save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import Header from './Header';
|
||||
import Edit from './Edit';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { MCPToolSetData } from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
|
||||
|
||||
const MCPTools = () => {
|
||||
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
|
||||
const toolSetData = useMemo(() => {
|
||||
const toolSetNode = appDetail.modules.find(
|
||||
(item) => item.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
);
|
||||
return toolSetNode?.inputs[0].value as MCPToolSetData;
|
||||
}, [appDetail.modules]);
|
||||
|
||||
const [url, setUrl] = useState(toolSetData?.url || '');
|
||||
const [toolList, setToolList] = useState<ToolType[]>(toolSetData?.toolList || []);
|
||||
const [currentTool, setCurrentTool] = useState<ToolType | null>(toolSetData?.toolList[0] || null);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
|
||||
<Header url={url} toolList={toolList} />
|
||||
<Edit
|
||||
url={url}
|
||||
setUrl={setUrl}
|
||||
toolList={toolList}
|
||||
setToolList={setToolList}
|
||||
currentTool={currentTool}
|
||||
setCurrentTool={setCurrentTool}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MCPTools);
|
||||
@@ -103,7 +103,7 @@ const Header = () => {
|
||||
const onBack = useCallback(async () => {
|
||||
leaveSaveSign.current = false;
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
pathname: '/dashboard/apps',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { Box, Flex, IconButton } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
@@ -10,7 +10,6 @@ import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
@@ -57,7 +56,6 @@ const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
<MyBox
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
|
||||
@@ -71,7 +71,7 @@ const Header = ({
|
||||
const onClickRoute = useCallback(
|
||||
(parentId: string) => {
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
pathname: '/dashboard/apps',
|
||||
query: {
|
||||
parentId,
|
||||
type: lastAppListRouteType
|
||||
|
||||
@@ -9,7 +9,10 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { theme } from '@fastgpt/web/styles/theme';
|
||||
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
|
||||
import ToolSelectModal, { childAppSystemKey } from './ToolSelectModal';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import ConfigToolModal from './ConfigToolModal';
|
||||
import { getWebLLMModel } from '@/web/common/system/utils';
|
||||
@@ -90,7 +93,9 @@ const ToolSelect = ({
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
|
||||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
|
||||
) ||
|
||||
hasError
|
||||
hasError ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.tool ||
|
||||
item.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -471,6 +471,31 @@ const RenderList = React.memo(function RenderList({
|
||||
>
|
||||
{t('common: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: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:common.Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
) : template.isFolder ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
|
||||
@@ -107,7 +107,7 @@ const Header = () => {
|
||||
const onBack = useCallback(async () => {
|
||||
leaveSaveSign.current = false;
|
||||
router.push({
|
||||
pathname: '/app/list',
|
||||
pathname: '/dashboard/apps',
|
||||
query: {
|
||||
parentId: appDetail.parentId,
|
||||
type: lastAppListRouteType
|
||||
|
||||
@@ -65,7 +65,6 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
}}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={isPlugin && loading}
|
||||
zIndex={300}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
|
||||
@@ -364,7 +364,7 @@ const RenderHeader = React.memo(function RenderHeader({
|
||||
color: 'primary.600'
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
onClick={() => router.push('/app/list')}
|
||||
onClick={() => router.push('/dashboard/apps')}
|
||||
gap={1}
|
||||
>
|
||||
<Box>{t('common:create')}</Box>
|
||||
@@ -474,7 +474,8 @@ const RenderList = React.memo(function RenderList({
|
||||
// get plugin preview module
|
||||
if (
|
||||
template.flowNodeType === FlowNodeTypeEnum.pluginModule ||
|
||||
template.flowNodeType === FlowNodeTypeEnum.appModule
|
||||
template.flowNodeType === FlowNodeTypeEnum.appModule ||
|
||||
template.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
setLoading(true);
|
||||
const res = await getPreviewPluginNode({ appId: template.id });
|
||||
@@ -614,11 +615,9 @@ const RenderList = React.memo(function RenderList({
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Flex>
|
||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
|
||||
{t(item.label as any)}
|
||||
</Box>
|
||||
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2}>
|
||||
{item.list.map((template) => {
|
||||
return (
|
||||
@@ -655,9 +654,16 @@ const RenderList = React.memo(function RenderList({
|
||||
py={gridStyle.py}
|
||||
px={3}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
_hover={{
|
||||
bg: 'myWhite.600',
|
||||
'& .arrowIcon': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
borderRadius={'sm'}
|
||||
draggable={!template.isFolder}
|
||||
draggable={
|
||||
!template.isFolder || template.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
}
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < sliderWidth) return;
|
||||
onAddNode({
|
||||
@@ -666,7 +672,10 @@ const RenderList = React.memo(function RenderList({
|
||||
});
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (template.isFolder) {
|
||||
if (
|
||||
template.isFolder &&
|
||||
template.flowNodeType !== FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
return setParentId(template.id);
|
||||
}
|
||||
if (isPc) {
|
||||
@@ -703,6 +712,26 @@ const RenderList = React.memo(function RenderList({
|
||||
{t(template.name as any)}
|
||||
</Box>
|
||||
|
||||
{template.isFolder && (
|
||||
<Box
|
||||
color={'myGray.500'}
|
||||
_hover={{
|
||||
bg: 'var(--light-general-surface-opacity-005, rgba(17, 24, 36, 0.05))',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
p={1}
|
||||
rounded={'sm'}
|
||||
className="arrowIcon"
|
||||
display="none"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
return setParentId(template.id);
|
||||
}}
|
||||
>
|
||||
<MyIcon name="common/arrowRight" w={'24px'} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{gridStyle.authorInRight && template.authorAvatar && template.author && (
|
||||
<HStack spacing={1} maxW={'120px'} flexShrink={0}>
|
||||
<MyAvatar src={template.authorAvatar} w={'1rem'} borderRadius={'50%'} />
|
||||
|
||||
@@ -47,6 +47,8 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowNodeItemType>) => (
|
||||
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
|
||||
),
|
||||
[FlowNodeTypeEnum.tool]: dynamic(() => import('./nodes/NodeTool')),
|
||||
[FlowNodeTypeEnum.toolSet]: dynamic(() => import('./nodes/NodeToolSet')),
|
||||
[FlowNodeTypeEnum.toolParams]: dynamic(() => import('./nodes/NodeToolParams')),
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./nodes/NodeLaf')),
|
||||
[FlowNodeTypeEnum.ifElseNode]: dynamic(() => import('./nodes/NodeIfElse')),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import Container from '../components/Container';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import RenderToolInput from './render/RenderToolInput';
|
||||
|
||||
const NodeTool = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const splitToolInputs = useContextSelector(WorkflowContext, (v) => v.splitToolInputs);
|
||||
const { commonInputs, isTool } = splitToolInputs(inputs, nodeId);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
{isTool && (
|
||||
<>
|
||||
<Container>
|
||||
<RenderToolInput nodeId={nodeId} inputs={inputs} />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={commonInputs} />
|
||||
</Container>
|
||||
</>
|
||||
<>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput flowOutputList={outputs} nodeId={nodeId} />
|
||||
</Container>
|
||||
</>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeTool);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from './render/NodeCard';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import Container from '../components/Container';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
|
||||
const NodeToolSet = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { inputs } = data;
|
||||
const toolList: ToolType[] = inputs.find((item) => item.key === 'toolSetData')?.value?.toolList;
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
<IOTitle text={t('app:MCP_tools_list')} />
|
||||
<Box maxH={'500px'} overflowY={'auto'} className="nowheel">
|
||||
{toolList?.map((tool, index) => (
|
||||
<Flex
|
||||
key={index}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
alignItems={'center'}
|
||||
py={2}
|
||||
px={3}
|
||||
>
|
||||
<Box w={'20px'} fontSize={'14px'} color={'myGray.500'} fontWeight={'medium'}>
|
||||
{index + 1 < 10 ? `0${index + 1}` : index + 1}
|
||||
</Box>
|
||||
<Box maxW={'full'} pl={2} position="relative" width="400px">
|
||||
<Box
|
||||
fontSize={'14px'}
|
||||
color={'myGray.900'}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{tool.name}
|
||||
</Box>
|
||||
<Box
|
||||
fontSize={'12px'}
|
||||
color={'myGray.500'}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
{tool.description || t('app:tools_no_description')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} />
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeToolSet);
|
||||
@@ -105,7 +105,9 @@ const NodeCard = (props: Props) => {
|
||||
|
||||
if (
|
||||
node?.flowNodeType === FlowNodeTypeEnum.pluginModule ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.appModule
|
||||
node?.flowNodeType === FlowNodeTypeEnum.appModule ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.tool ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
return { ...node, ...node.pluginData };
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { UserInputInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import {
|
||||
getLastInteractiveValue,
|
||||
initWorkflowEdgeStatus
|
||||
storeEdges2RuntimeEdges
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
@@ -137,7 +137,7 @@ const NodeDebugResponse = ({ nodeId, debugResult }: NodeDebugResponseProps) => {
|
||||
onNextNodeDebug({
|
||||
...workflowDebugData,
|
||||
// Rewrite runtimeEdges
|
||||
runtimeEdges: initWorkflowEdgeStatus(workflowDebugData.runtimeEdges, lastInteractive),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(workflowDebugData.runtimeEdges, lastInteractive),
|
||||
query: updatedQuery,
|
||||
history: mockHistory
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
refreshDeps: [appId],
|
||||
errorToast: t('common:core.app.error.Get app failed'),
|
||||
onError(err: any) {
|
||||
router.replace('/app/list');
|
||||
router.replace('/dashboard/apps');
|
||||
},
|
||||
onSuccess(res) {
|
||||
setAppDetail(res);
|
||||
@@ -189,7 +189,7 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
router.replace(`/app/list`);
|
||||
router.replace(`/dashboard/apps`);
|
||||
},
|
||||
successToast: t('common:common.Delete Success'),
|
||||
errorToast: t('common:common.Delete Failed')
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import AppTypeTag from './TypeTag';
|
||||
import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
getTemplateMarketItemDetail,
|
||||
getTemplateMarketItemList,
|
||||
getTemplateTagList
|
||||
} from '@/web/core/app/api/template';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppListContext } from './context';
|
||||
import { useRouter } from 'next/router';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput/index';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { webPushTrack } from '@/web/common/middle/tracks/utils';
|
||||
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
|
||||
type TemplateAppType = AppTypeEnum | 'all';
|
||||
|
||||
const recommendTag: TemplateTypeSchemaType = {
|
||||
typeId: AppTemplateTypeEnum.recommendation,
|
||||
typeName: i18nT('app:templateMarket.templateTags.Recommendation'),
|
||||
typeOrder: 0
|
||||
};
|
||||
|
||||
const TemplateMarketModal = ({
|
||||
defaultType = 'all',
|
||||
onClose
|
||||
}: {
|
||||
defaultType?: TemplateAppType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { parentId } = useContextSelector(AppListContext, (v) => v);
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const [currentTag, setCurrentTag] = useState<string>(AppTemplateTypeEnum.recommendation);
|
||||
const [currentAppType, setCurrentAppType] = useState<TemplateAppType>(defaultType);
|
||||
const [currentSearch, setCurrentSearch] = useState('');
|
||||
|
||||
const { data: templateList = [], loading: isLoadingTemplates } = useRequest2(
|
||||
() => getTemplateMarketItemList({ type: currentAppType }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [currentAppType]
|
||||
}
|
||||
);
|
||||
|
||||
const { data: templateTags = [], loading: isLoadingTags } = useRequest2(
|
||||
() => getTemplateTagList().then((res) => [recommendTag, ...res]),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
// Batch by tags
|
||||
const filterTemplateTags = useMemo(() => {
|
||||
return templateTags
|
||||
.map((tag) => {
|
||||
const templates = templateList.filter((template) => template.tags.includes(tag.typeId));
|
||||
return {
|
||||
...tag,
|
||||
templates
|
||||
};
|
||||
})
|
||||
.filter((item) => item.templates.length > 0);
|
||||
}, [templateList, templateTags]);
|
||||
|
||||
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
|
||||
async (template: AppTemplateSchemaType) => {
|
||||
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: template.avatar,
|
||||
name: template.name,
|
||||
type: template.type as AppTypeEnum,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
}).then((res) => {
|
||||
webPushTrack.useAppTemplate({
|
||||
id: res,
|
||||
name: template.name
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess(id: string) {
|
||||
onClose();
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { run: handleScroll } = useRequest2(
|
||||
async () => {
|
||||
let firstVisibleTitle: any = null;
|
||||
|
||||
filterTemplateTags
|
||||
.map((type) => type.typeId)
|
||||
.forEach((type) => {
|
||||
const element = document.getElementById(type);
|
||||
if (!element) return;
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
if (elementRect.top <= window.innerHeight && elementRect.bottom >= 0) {
|
||||
if (
|
||||
!firstVisibleTitle ||
|
||||
elementRect.top < firstVisibleTitle.getBoundingClientRect().top
|
||||
) {
|
||||
firstVisibleTitle = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (firstVisibleTitle) {
|
||||
setCurrentTag(firstVisibleTitle.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
throttleWait: 100,
|
||||
refreshDeps: [filterTemplateTags.length]
|
||||
}
|
||||
);
|
||||
|
||||
const TemplateCard = useCallback(
|
||||
({ item }: { item: AppTemplateSchemaType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
key={item.templateId}
|
||||
lineHeight={1.5}
|
||||
h="100%"
|
||||
pt={4}
|
||||
pb={3}
|
||||
px={4}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
bg={'white'}
|
||||
borderRadius={'10px'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Box mr={'-1rem'}>
|
||||
<AppTypeTag type={item.type as AppTypeEnum} />
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
flex={['1 0 48px', '1 0 56px']}
|
||||
mt={3}
|
||||
pr={1}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
|
||||
</Box>
|
||||
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
<Flex
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={1}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
gap={2}
|
||||
>
|
||||
{((item.userGuide?.type === 'markdown' && item.userGuide?.content) ||
|
||||
(item.userGuide?.type === 'link' && item.userGuide?.link)) && (
|
||||
<UseGuideModal
|
||||
title={item.name}
|
||||
iconSrc={item.avatar}
|
||||
text={item.userGuide?.content}
|
||||
link={item.userGuide?.link}
|
||||
>
|
||||
{({ onClick }) => (
|
||||
<Button variant={'whiteBase'} h={6} rounded={'sm'} onClick={onClick}>
|
||||
{t('app:templateMarket.template_guide')}
|
||||
</Button>
|
||||
)}
|
||||
</UseGuideModal>
|
||||
)}
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={6}
|
||||
rounded={'sm'}
|
||||
onClick={() => onUseTemplate(item)}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
},
|
||||
[feConfigs.systemTitle, onUseTemplate]
|
||||
);
|
||||
|
||||
const isLoading = isLoadingTags || isLoadingTemplates || isCreating;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
autoFocus={false}
|
||||
blockScrollOnMount={false}
|
||||
closeOnOverlayClick={false}
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
w={['90vw', '80vw']}
|
||||
maxW={'90vw'}
|
||||
position={'relative'}
|
||||
h={['90vh']}
|
||||
boxShadow={'7'}
|
||||
overflow={'hidden'}
|
||||
>
|
||||
<ModalHeader
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
py={'10px'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'600'}
|
||||
gap={2}
|
||||
position={'relative'}
|
||||
>
|
||||
<Avatar src={'/imgs/app/templateFill.svg'} w={'2rem'} objectFit={'fill'} />
|
||||
<Box color={'myGray.900'}>{t('app:template_market')}</Box>
|
||||
|
||||
<Box flex={'1'} />
|
||||
|
||||
<MySelect<TemplateAppType>
|
||||
h={'8'}
|
||||
value={currentAppType}
|
||||
onChange={(value) => {
|
||||
setCurrentAppType(value);
|
||||
}}
|
||||
bg={'myGray.100'}
|
||||
minW={'7rem'}
|
||||
borderRadius={'sm'}
|
||||
list={[
|
||||
{ label: t('app:type.All'), value: 'all' },
|
||||
{ label: t('app:type.Simple bot'), value: AppTypeEnum.simple },
|
||||
{ label: t('app:type.Workflow bot'), value: AppTypeEnum.workflow },
|
||||
{ label: t('app:type.Plugin'), value: AppTypeEnum.plugin }
|
||||
]}
|
||||
/>
|
||||
<ModalCloseButton position={'relative'} fontSize={'xs'} top={0} right={0} />
|
||||
|
||||
{isPc && (
|
||||
<Box
|
||||
width="15rem"
|
||||
position={'absolute'}
|
||||
top={'50%'}
|
||||
left={'50%'}
|
||||
transform={'translate(-50%,-50%)'}
|
||||
>
|
||||
<SearchInput
|
||||
pl={7}
|
||||
placeholder={t('app:templateMarket.Search_template')}
|
||||
value={currentSearch}
|
||||
onChange={(e) => setCurrentSearch(e.target.value)}
|
||||
h={8}
|
||||
bg={'myGray.50'}
|
||||
maxLength={100}
|
||||
borderRadius={'sm'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<MyBox isLoading={isLoading} flex={'1 0 0'} h="0">
|
||||
<ModalBody
|
||||
h={'100%'}
|
||||
display={'flex'}
|
||||
bg={'myGray.100'}
|
||||
overflow={'auto'}
|
||||
gap={5}
|
||||
px={0}
|
||||
pt={5}
|
||||
>
|
||||
{isPc && (
|
||||
<Flex pl={5} flexDirection={'column'} gap={3}>
|
||||
{filterTemplateTags.map((item) => {
|
||||
return (
|
||||
<Box
|
||||
key={item.typeId}
|
||||
cursor={'pointer'}
|
||||
{...(item.typeId === currentTag && !currentSearch
|
||||
? {
|
||||
bg: 'primary.1',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
_hover: { bg: 'primary.1' },
|
||||
color: 'myGray.600'
|
||||
})}
|
||||
w={'9.5rem'}
|
||||
px={4}
|
||||
py={2}
|
||||
rounded={'sm'}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
onClick={() => {
|
||||
setCurrentTag(item.typeId);
|
||||
const anchor = document.getElementById(item.typeId);
|
||||
if (anchor) {
|
||||
anchor.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t(item.typeName as any)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Box flex={1} />
|
||||
|
||||
{feConfigs?.appTemplateCourse && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'primary.600'
|
||||
}}
|
||||
py={2}
|
||||
fontWeight={500}
|
||||
rounded={'sm'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => window.open(feConfigs.appTemplateCourse)}
|
||||
gap={1}
|
||||
>
|
||||
<MyIcon name={'common/upRightArrowLight'} w={'1rem'} />
|
||||
<Box>{t('common:contribute_app_template')}</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Box
|
||||
pl={[3, 0]}
|
||||
pr={[3, 5]}
|
||||
pt={1}
|
||||
flex={'1'}
|
||||
h={'100%'}
|
||||
overflow={'auto'}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{currentSearch ? (
|
||||
<>
|
||||
<Box fontSize={'lg'} color={'myGray.900'} mb={4}>
|
||||
{t('common:xx_search_result', { key: currentSearch })}
|
||||
</Box>
|
||||
{(() => {
|
||||
const templates = templateList.filter((template) =>
|
||||
`${template.name}${template.intro}`.includes(currentSearch)
|
||||
);
|
||||
|
||||
if (templates.length > 0) {
|
||||
return (
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{templates.map((item) => (
|
||||
<TemplateCard key={item.templateId} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyTip text={t('app:template_market_empty_data')} />;
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{filterTemplateTags.map((item) => {
|
||||
return (
|
||||
<Box key={item.typeId}>
|
||||
<Box
|
||||
id={item.typeId}
|
||||
fontSize={['md', 'lg']}
|
||||
color={'myGray.900'}
|
||||
mb={4}
|
||||
fontWeight={500}
|
||||
>
|
||||
{t(item.typeName as any)}
|
||||
</Box>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{item.templates.map((item) => (
|
||||
<TemplateCard key={item.templateId} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyBox>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateMarketModal;
|
||||
@@ -299,7 +299,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
p={3}
|
||||
onClick={() => router.push('/app/list')}
|
||||
onClick={() => router.push('/dashboard/apps')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
|
||||
@@ -64,7 +64,7 @@ const SliderApps = ({ apps, activeAppId }: { apps: AppListItemType[]; activeAppI
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.200' }}
|
||||
onClick={() => router.push('/app/list')}
|
||||
onClick={() => router.push('/dashboard/apps')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
|
||||
339
projects/app/src/pageComponents/dashboard/Container.tsx
Normal file
339
projects/app/src/pageComponents/dashboard/Container.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import { Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { navbarWidth } from '@/components/Layout';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTemplateMarketItemList, getTemplateTagList } from '@/web/core/app/api/template';
|
||||
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
|
||||
import { getPluginGroups } from '@/web/core/app/api/plugin';
|
||||
import { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
|
||||
|
||||
export enum TabEnum {
|
||||
apps = 'apps',
|
||||
app_templates = 'templateMarket',
|
||||
mcp_server = 'mcpServer'
|
||||
}
|
||||
type TabEnumType = `${keyof typeof TabEnum}` | string;
|
||||
|
||||
const DashboardContainer = ({
|
||||
children
|
||||
}: {
|
||||
children: (e: {
|
||||
templateTags: TemplateTypeSchemaType[];
|
||||
templateList: AppTemplateSchemaType[];
|
||||
pluginGroups: PluginGroupSchemaType[];
|
||||
MenuIcon: JSX.Element;
|
||||
}) => React.ReactNode;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { isOpen: isOpenSidebar, onOpen: onOpenSidebar, onClose: onCloseSidebar } = useDisclosure();
|
||||
|
||||
// First tab
|
||||
const currentTab = useMemo(() => {
|
||||
const path = router.asPath.split('?')[0]; // 移除查询参数
|
||||
const segments = path.split('/').filter(Boolean); // 过滤空字符串
|
||||
|
||||
return (segments.pop() as TabEnumType) || TabEnum.apps;
|
||||
}, [router.asPath]);
|
||||
|
||||
// Sub tab
|
||||
const { type: currentType, appType } = router.query as {
|
||||
type: string;
|
||||
appType?: AppTypeEnum | 'all';
|
||||
};
|
||||
|
||||
// Template market
|
||||
const { data: templateTags = [], loading: isLoadingTemplatesTags } = useRequest2(
|
||||
() =>
|
||||
currentTab === TabEnum.app_templates
|
||||
? getTemplateTagList().then((res) => [
|
||||
{
|
||||
typeId: AppTemplateTypeEnum.recommendation,
|
||||
typeName: t('app:templateMarket.templateTags.Recommendation'),
|
||||
typeOrder: 0
|
||||
},
|
||||
...res
|
||||
])
|
||||
: Promise.resolve([]),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [currentTab]
|
||||
}
|
||||
);
|
||||
const { data: templateList = [], loading: isLoadingTemplates } = useRequest2(
|
||||
() =>
|
||||
currentTab === TabEnum.app_templates
|
||||
? getTemplateMarketItemList({ type: appType })
|
||||
: Promise.resolve([]),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [currentTab, appType]
|
||||
}
|
||||
);
|
||||
|
||||
// System tools
|
||||
const { data: pluginGroups = [], loading: isLoadingToolGroups } = useRequest2(
|
||||
() =>
|
||||
getPluginGroups().then((res) =>
|
||||
res.map((item) => ({
|
||||
...item,
|
||||
groupTypes: [
|
||||
{
|
||||
typeId: 'all',
|
||||
typeName: t('app:type.All')
|
||||
},
|
||||
...item.groupTypes
|
||||
]
|
||||
}))
|
||||
),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const groupList = useMemo<
|
||||
{
|
||||
groupId: string;
|
||||
groupAvatar: string;
|
||||
groupName: string;
|
||||
children: {
|
||||
typeId: string;
|
||||
typeName: string;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
}[];
|
||||
}[]
|
||||
>(() => {
|
||||
return [
|
||||
{
|
||||
groupId: TabEnum.apps,
|
||||
groupAvatar: 'common/app',
|
||||
groupName: t('common:core.module.template.Team app'),
|
||||
children: [
|
||||
{
|
||||
isActive: !currentType,
|
||||
typeId: 'all',
|
||||
typeName: t('app:type.All')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.simple,
|
||||
typeName: t('app:type.Simple bot')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.workflow,
|
||||
typeName: t('app:type.Workflow bot')
|
||||
},
|
||||
{
|
||||
typeId: AppTypeEnum.plugin,
|
||||
typeName: t('app:type.Plugin')
|
||||
}
|
||||
]
|
||||
},
|
||||
...pluginGroups.map((group) => ({
|
||||
groupId: group.groupId,
|
||||
groupAvatar: group.groupAvatar,
|
||||
groupName: t(group.groupName as any),
|
||||
children: group.groupTypes.map((type, index) => ({
|
||||
typeId: type.typeId,
|
||||
typeName: t(type.typeName as any),
|
||||
isActive: index === 0 && !currentType
|
||||
}))
|
||||
})),
|
||||
{
|
||||
groupId: TabEnum.app_templates,
|
||||
groupAvatar: 'common/templateMarket',
|
||||
groupName: t('common:template_market'),
|
||||
children: [
|
||||
...templateTags
|
||||
.map((tag) => {
|
||||
const templates = templateList.filter((template) =>
|
||||
template.tags.includes(tag.typeId)
|
||||
);
|
||||
return {
|
||||
...tag,
|
||||
templates
|
||||
};
|
||||
})
|
||||
.filter((tag) => tag.templates.length > 0)
|
||||
.map((tag, index) => ({
|
||||
typeId: tag.typeId,
|
||||
typeName: t(tag.typeName as any),
|
||||
isActive: index === 0 && !currentType
|
||||
})),
|
||||
...(feConfigs?.appTemplateCourse
|
||||
? [
|
||||
{
|
||||
typeId: AppTemplateTypeEnum.contribute,
|
||||
typeName: t('common:contribute_app_template'),
|
||||
onClick: () => {
|
||||
window.open(feConfigs.appTemplateCourse);
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
...(feConfigs?.mcpServerProxyEndpoint
|
||||
? [
|
||||
{
|
||||
groupId: TabEnum.mcp_server,
|
||||
groupAvatar: 'key',
|
||||
groupName: t('common:mcp_server'),
|
||||
children: []
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
}, [currentType, feConfigs.appTemplateCourse, pluginGroups, t, templateList, templateTags]);
|
||||
|
||||
const MenuIcon = useMemo(
|
||||
() => (
|
||||
<Flex alignItems={'center'}>
|
||||
{isOpenSidebar && (
|
||||
<Box
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="blackAlpha.600"
|
||||
onClick={onCloseSidebar}
|
||||
zIndex={99}
|
||||
/>
|
||||
)}
|
||||
<MyIcon name="menu" w={'1.25rem'} mr={1.5} onClick={onOpenSidebar} />
|
||||
</Flex>
|
||||
),
|
||||
[isOpenSidebar, onCloseSidebar, onOpenSidebar]
|
||||
);
|
||||
|
||||
const isLoading = isLoadingTemplatesTags || isLoadingTemplates || isLoadingToolGroups;
|
||||
|
||||
return (
|
||||
<Box h={'100%'}>
|
||||
{/* Side bar */}
|
||||
{(isPc || isOpenSidebar) && (
|
||||
<MyBox
|
||||
isLoading={isLoading}
|
||||
position={'fixed'}
|
||||
left={isPc ? navbarWidth : 0}
|
||||
top={0}
|
||||
bg={'myGray.25'}
|
||||
w={`220px`}
|
||||
h={'full'}
|
||||
borderLeft={'1px solid'}
|
||||
borderRight={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
pt={4}
|
||||
px={2.5}
|
||||
pb={2.5}
|
||||
zIndex={100}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{groupList.map((group) => {
|
||||
const selected = currentTab === group.groupId;
|
||||
|
||||
return (
|
||||
<Box key={group.groupId}>
|
||||
<Flex
|
||||
p={2}
|
||||
fontSize={'sm'}
|
||||
rounded={'md'}
|
||||
color={'myGray.700'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'primary.50'
|
||||
}}
|
||||
mb={0.5}
|
||||
onClick={() => {
|
||||
router.push(`/dashboard/${group.groupId}`);
|
||||
onCloseSidebar();
|
||||
}}
|
||||
{...(group.children.length === 0 &&
|
||||
selected && { bg: 'primary.100', color: 'primary.600' })}
|
||||
>
|
||||
<Avatar src={group.groupAvatar} w={'1rem'} mr={1.5} />
|
||||
<Box fontWeight={'medium'}>{group.groupName}</Box>
|
||||
<Box flex={1} />
|
||||
{group.children.length > 0 && (
|
||||
<MyIcon
|
||||
name={selected ? 'core/chat/chevronDown' : 'core/chat/chevronUp'}
|
||||
w={'1rem'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{selected && (
|
||||
<Box>
|
||||
{group.children.map((child) => {
|
||||
const isActive = child.isActive || child.typeId === currentType;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={child.typeId}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
rounded={'md'}
|
||||
py={2}
|
||||
pl={'30px'}
|
||||
cursor={'pointer'}
|
||||
mb={0.5}
|
||||
_hover={{ bg: 'primary.50' }}
|
||||
{...(isActive
|
||||
? {
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
bg: 'transparent',
|
||||
color: 'myGray.500'
|
||||
})}
|
||||
onClick={() => {
|
||||
if (child.onClick) {
|
||||
child.onClick();
|
||||
} else {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
type: child.typeId
|
||||
}
|
||||
});
|
||||
onCloseSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{child.typeName}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</MyBox>
|
||||
)}
|
||||
|
||||
<Box h={'100%'} pl={isPc ? `220px` : 0} position={'relative'}>
|
||||
{children({
|
||||
templateTags,
|
||||
templateList,
|
||||
pluginGroups,
|
||||
MenuIcon
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardContainer;
|
||||
@@ -42,15 +42,7 @@ type FormType = {
|
||||
|
||||
export type CreateAppType = AppTypeEnum.simple | AppTypeEnum.workflow | AppTypeEnum.plugin;
|
||||
|
||||
const CreateModal = ({
|
||||
onClose,
|
||||
type,
|
||||
onOpenTemplateModal
|
||||
}: {
|
||||
type: CreateAppType;
|
||||
onClose: () => void;
|
||||
onOpenTemplateModal: (type: AppTypeEnum) => void;
|
||||
}) => {
|
||||
const CreateModal = ({ onClose, type }: { type: CreateAppType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
@@ -194,14 +186,23 @@ const CreateModal = ({
|
||||
<Box flex={1} />
|
||||
{isTemplateMode && (
|
||||
<Flex
|
||||
onClick={() => onOpenTemplateModal(type)}
|
||||
onClick={() => {
|
||||
router.push({
|
||||
pathname: '/dashboard/templateMarket',
|
||||
query: {
|
||||
appType: type,
|
||||
parentId
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
fontSize={'xs'}
|
||||
_hover={{ color: 'blue.700' }}
|
||||
>
|
||||
{t('common:core.app.more')}
|
||||
{t('common:core.app.switch_to_template_market')}
|
||||
<ChevronRightIcon w={4} h={4} />
|
||||
</Flex>
|
||||
)}
|
||||
@@ -51,8 +51,10 @@ const ListItem = () => {
|
||||
content: t('app:move.hint')
|
||||
});
|
||||
|
||||
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail, setSearchKey } =
|
||||
useContextSelector(AppListContext, (v) => v);
|
||||
const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector(
|
||||
AppListContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const [editedApp, setEditedApp] = useState<EditResourceInfoFormType>();
|
||||
const [editHttpPlugin, setEditHttpPlugin] = useState<EditHttpPluginProps>();
|
||||
@@ -132,7 +134,7 @@ const ListItem = () => {
|
||||
gridTemplateColumns={
|
||||
folderDetail
|
||||
? ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']
|
||||
: ['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
|
||||
: ['1fr', 'repeat(2,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']
|
||||
}
|
||||
gridGap={5}
|
||||
alignItems={'stretch'}
|
||||
@@ -176,7 +178,6 @@ const ListItem = () => {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (AppFolderTypeList.includes(app.type)) {
|
||||
setSearchKey('');
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
@@ -347,7 +348,9 @@ const ListItem = () => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(AppFolderTypeList.includes(app.type)
|
||||
...(app.type === AppTypeEnum.toolSet ||
|
||||
app.type === AppTypeEnum.folder ||
|
||||
app.type === AppTypeEnum.httpPlugin
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@@ -0,0 +1,253 @@
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { getMCPTools, postCreateMCPTools } from '@/web/core/app/api/plugin';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AppListContext } from './context';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { getMCPToolsBody } from '@/pages/api/core/app/mcpTools/getMCPTools';
|
||||
|
||||
export type MCPToolSetData = {
|
||||
url: string;
|
||||
toolList: ToolType[];
|
||||
};
|
||||
|
||||
export type EditMCPToolsProps = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
mcpData: MCPToolSetData;
|
||||
};
|
||||
|
||||
const MCPToolsEditModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
|
||||
|
||||
const { register, setValue, handleSubmit, watch } = useForm<EditMCPToolsProps>({
|
||||
defaultValues: {
|
||||
avatar: 'core/app/type/mcpToolsFill',
|
||||
name: '',
|
||||
mcpData: {
|
||||
url: '',
|
||||
toolList: []
|
||||
}
|
||||
}
|
||||
});
|
||||
const avatar = watch('avatar');
|
||||
const mcpData = watch('mcpData');
|
||||
|
||||
const { runAsync: onCreate, loading: isCreating } = useRequest2(
|
||||
async (data: EditMCPToolsProps) => {
|
||||
return postCreateMCPTools({
|
||||
name: data.name,
|
||||
avatar: data.avatar,
|
||||
toolList: data.mcpData.toolList,
|
||||
url: data.mcpData.url,
|
||||
parentId
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
onClose();
|
||||
loadMyApps();
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: runGetMCPTools, loading: isGettingTools } = useRequest2(
|
||||
(data: getMCPToolsBody) => getMCPTools(data),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
setValue('mcpData.toolList', res);
|
||||
},
|
||||
errorToast: t('app:MCP_tools_parse_failed')
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
File,
|
||||
onOpen: onOpenSelectFile,
|
||||
onSelectImage
|
||||
} = useSelectFile({
|
||||
fileType: 'image/*',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="core/app/type/mcpToolsFill"
|
||||
title={t('app:type.MCP tools')}
|
||||
w={['90vw', '530px']}
|
||||
position={'relative'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'}>
|
||||
{t('common:common.Set Name')}
|
||||
</Box>
|
||||
<Flex mt={2} alignItems={'center'}>
|
||||
<MyTooltip label={t('common:common.Set Avatar')}>
|
||||
<Avatar
|
||||
flexShrink={0}
|
||||
src={avatar}
|
||||
w={['28px', '32px']}
|
||||
h={['28px', '32px']}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
onClick={onOpenSelectFile}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Input
|
||||
flex={1}
|
||||
ml={4}
|
||||
bg={'myWhite.600'}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'} mt={6}>
|
||||
{t('app:MCP_tools_url')}
|
||||
</Box>
|
||||
<Flex alignItems={'center'} gap={2} mt={2}>
|
||||
<Input
|
||||
h={8}
|
||||
placeholder={t('app:MCP_tools_url_placeholder')}
|
||||
{...register('mcpData.url', {
|
||||
required: t('app:MCP_tools_url_is_empty')
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'whitePrimary'}
|
||||
h={8}
|
||||
isLoading={isGettingTools}
|
||||
onClick={() => {
|
||||
runGetMCPTools({ url: mcpData.url });
|
||||
}}
|
||||
>
|
||||
{t('common:common.Parse')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Box color={'myGray.900'} fontSize={'14px'} fontWeight={'medium'} mt={6}>
|
||||
{t('app:MCP_tools_list')}
|
||||
</Box>
|
||||
<Box
|
||||
mt={2}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
borderWidth={'1px'}
|
||||
position={'relative'}
|
||||
>
|
||||
<TableContainer maxH={360} minH={200} overflowY={'auto'}>
|
||||
<Table bg={'white'}>
|
||||
<Thead bg={'myGray.50'}>
|
||||
<Th fontSize={'mini'} py={0} h={'34px'}>
|
||||
{t('common:Name')}
|
||||
</Th>
|
||||
<Th fontSize={'mini'} py={0} h={'34px'}>
|
||||
{t('common:plugin.Description')}
|
||||
</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{mcpData.toolList.map((item) => (
|
||||
<Tr key={item.name} height={'28px'}>
|
||||
<Td
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
py={2}
|
||||
maxW={1 / 2}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{item.name}
|
||||
</Td>
|
||||
<Td
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
py={2}
|
||||
maxW={1 / 2}
|
||||
overflow={'hidden'}
|
||||
textOverflow={'ellipsis'}
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{item.description}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{mcpData.toolList.length === 0 && (
|
||||
<Center
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{t('app:no_mcp_tools_list')}
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter gap={2}>
|
||||
<Button variant={'whitePrimary'} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={mcpData.toolList.length === 0}
|
||||
isLoading={isCreating}
|
||||
onClick={handleSubmit(onCreate)}
|
||||
>
|
||||
{t('common:common.Confirm Create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
<File
|
||||
onSelect={(e) =>
|
||||
onSelectImage(e, {
|
||||
maxH: 300,
|
||||
maxW: 300,
|
||||
callback: (e) => setValue('avatar', e)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPToolsEditModal;
|
||||
@@ -33,6 +33,13 @@ const AppTypeTag = ({ type }: { type: AppTypeEnum }) => {
|
||||
bg: '#FFE4EE',
|
||||
color: '#E82F72'
|
||||
},
|
||||
[AppTypeEnum.toolSet]: {
|
||||
label: t('app:type.MCP tools'),
|
||||
icon: 'core/app/type/mcpTools',
|
||||
bg: '',
|
||||
color: ''
|
||||
},
|
||||
[AppTypeEnum.tool]: undefined,
|
||||
[AppTypeEnum.folder]: undefined
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal'));
|
||||
|
||||
type AppListContextType = {
|
||||
parentId?: string | null;
|
||||
appType: AppTypeEnum | 'ALL';
|
||||
appType: AppTypeEnum | 'all';
|
||||
myApps: AppListItemType[];
|
||||
loadMyApps: () => Promise<AppListItemType[]>;
|
||||
isFetchingApps: boolean;
|
||||
@@ -47,7 +47,7 @@ export const AppListContext = createContext<AppListContextType>({
|
||||
setMoveAppId: function (value: React.SetStateAction<string | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
appType: 'ALL',
|
||||
appType: 'all',
|
||||
refetchFolderDetail: async function (): Promise<AppDetailType | null> {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
@@ -60,7 +60,7 @@ export const AppListContext = createContext<AppListContextType>({
|
||||
const AppListContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { parentId = null, type = 'ALL' } = router.query as {
|
||||
const { parentId = null, type = 'all' } = router.query as {
|
||||
parentId?: string | null;
|
||||
type: AppTypeEnum;
|
||||
};
|
||||
@@ -73,7 +73,7 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
} = useRequest2(
|
||||
() => {
|
||||
const formatType = (() => {
|
||||
if (!type || type === 'ALL') return undefined;
|
||||
if (!type || type === 'all') return undefined;
|
||||
if (type === AppTypeEnum.plugin)
|
||||
return [AppTypeEnum.folder, AppTypeEnum.plugin, AppTypeEnum.httpPlugin];
|
||||
|
||||
414
projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx
Normal file
414
projects/app/src/pageComponents/dashboard/mcp/EditModal.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Flex,
|
||||
Grid,
|
||||
HStack,
|
||||
Input,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { McpAppType } from '@fastgpt/global/support/mcp/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import Path from '@/components/common/folder/Path';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getAppBasicInfoByIds, getMyApps } from '@/web/core/app/api';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { getAppFolderPath } from '@/web/core/app/api/app';
|
||||
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { postCreateMcpServer, putUpdateMcpServer } from '../../../web/support/mcp/api';
|
||||
|
||||
export type EditMcForm = {
|
||||
id?: string;
|
||||
name: string;
|
||||
apps: McpAppType[];
|
||||
};
|
||||
|
||||
export const defaultForm: EditMcForm = {
|
||||
name: '',
|
||||
apps: []
|
||||
};
|
||||
|
||||
const SelectAppModal = ({
|
||||
selectedApps,
|
||||
onClose,
|
||||
onConfirm
|
||||
}: {
|
||||
selectedApps: McpAppType[];
|
||||
onClose: () => void;
|
||||
onConfirm: (e: McpAppType[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedList, setSelectedList] = useState<
|
||||
{
|
||||
appId: string;
|
||||
toolName: string;
|
||||
avatar: string;
|
||||
description: string;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
// Load selected app
|
||||
useRequest2(() => getAppBasicInfoByIds(selectedApps.map((item) => item.appId)), {
|
||||
manual: false,
|
||||
onSuccess: (data) => {
|
||||
setSelectedList(
|
||||
data.map((item) => ({
|
||||
appId: item.id,
|
||||
toolName: item.name,
|
||||
avatar: item.avatar,
|
||||
description: selectedApps.find((app) => app.appId === item.id)?.description || ''
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Load all apps
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [parentId, setParentId] = useState<ParentIdType>('');
|
||||
|
||||
const { data: apps = [], loading: loadingApps } = useRequest2(
|
||||
() =>
|
||||
getMyApps({
|
||||
searchKey,
|
||||
parentId
|
||||
}),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [searchKey, parentId],
|
||||
throttleWait: 200
|
||||
}
|
||||
);
|
||||
const { data: paths = [] } = useRequest2(
|
||||
() => getAppFolderPath({ sourceId: parentId, type: 'current' }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [parentId]
|
||||
}
|
||||
);
|
||||
|
||||
const isLoading = loadingApps;
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
iconSrc={'modal/AddClb'}
|
||||
title={t('dashboard_mcp:select_app')}
|
||||
minW="800px"
|
||||
maxW={'60vw'}
|
||||
h={'100%'}
|
||||
maxH={'90vh'}
|
||||
isCentered
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<ModalBody flex={'1'}>
|
||||
<Grid
|
||||
border="1px solid"
|
||||
borderColor="myGray.200"
|
||||
borderRadius="0.5rem"
|
||||
gridTemplateColumns="1fr 1fr"
|
||||
h={'100%'}
|
||||
>
|
||||
<Flex
|
||||
h={'100%'}
|
||||
flexDirection="column"
|
||||
borderRight="1px solid"
|
||||
borderColor="myGray.200"
|
||||
p="4"
|
||||
>
|
||||
<SearchInput
|
||||
placeholder={t('dashboard_mcp:search_app')}
|
||||
bgColor="myGray.50"
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
|
||||
{paths.length > 0 && !searchKey && (
|
||||
<Box mt={3}>
|
||||
<Path paths={paths} hoverStyle={{ bg: 'myGray.200' }} onClick={setParentId} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{apps.map((item) => {
|
||||
const selected = selectedList.some((app) => app.appId === item._id);
|
||||
const isFolder = AppFolderTypeList.includes(item.type);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
key={item._id}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isFolder) {
|
||||
setParentId(item._id);
|
||||
} else if (selected) {
|
||||
setSelectedList((state) => state.filter((app) => app.appId !== item._id));
|
||||
} else {
|
||||
setSelectedList((state) => [
|
||||
...state,
|
||||
{
|
||||
appId: item._id,
|
||||
toolName: item.name,
|
||||
avatar: item.avatar,
|
||||
description: item.intro
|
||||
}
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} w={'1.25rem'}>
|
||||
{!isFolder && <Checkbox isChecked={selected} />}
|
||||
</Flex>
|
||||
<Avatar src={item.avatar} w="1.5rem" borderRadius={'sm'} />
|
||||
<Box>{item.name}</Box>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Flex h={'100%'} p="4" flexDirection="column">
|
||||
<Box>
|
||||
{`${t('dashboard_mcp:has_chosen')}: `}
|
||||
{selectedList.length}
|
||||
</Box>
|
||||
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
|
||||
{selectedList.map((item) => {
|
||||
return (
|
||||
<HStack
|
||||
key={item.appId}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.avatar} w="1.5rem" borderRadius={'sm'} />
|
||||
<Box ml="2" flex={'1 0 0'}>
|
||||
{item.toolName}
|
||||
</Box>
|
||||
<MyIcon
|
||||
name="common/closeLight"
|
||||
w="1rem"
|
||||
cursor={'pointer'}
|
||||
_hover={{
|
||||
color: 'red.600'
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedList((state) => state.filter((app) => app.appId !== item.appId));
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button ml="4" h={'32px'} onClick={() => onConfirm(selectedList)}>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
const EditMcpModal = ({
|
||||
editMcp,
|
||||
onClose,
|
||||
onSuccess
|
||||
}: {
|
||||
editMcp: EditMcForm;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = !!editMcp.id;
|
||||
console.log(editMcp);
|
||||
const {
|
||||
isOpen: isOpenSelectApp,
|
||||
onOpen: onOpenSelectApp,
|
||||
onClose: onCloseSelectApp
|
||||
} = useDisclosure();
|
||||
|
||||
const { register, handleSubmit, control } = useForm({
|
||||
defaultValues: editMcp
|
||||
});
|
||||
|
||||
const {
|
||||
fields: apps,
|
||||
replace: replaceSelectedApps,
|
||||
remove
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'apps'
|
||||
});
|
||||
|
||||
const { runAsync: createMcp, loading: loadingCreate } = useRequest2(
|
||||
(data: EditMcForm) =>
|
||||
postCreateMcpServer({
|
||||
name: data.name,
|
||||
apps: data.apps.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.Create Success'),
|
||||
onSuccess
|
||||
}
|
||||
);
|
||||
const { runAsync: updateMcp, loading: loadingUpdate } = useRequest2(
|
||||
(data: EditMcForm) =>
|
||||
putUpdateMcpServer({
|
||||
id: data.id!,
|
||||
name: data.name,
|
||||
apps: data.apps.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
}),
|
||||
{
|
||||
manual: true,
|
||||
successToast: t('common:common.Update Success'),
|
||||
onSuccess
|
||||
}
|
||||
);
|
||||
const isConfirming = loadingCreate || loadingUpdate;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
iconSrc="key"
|
||||
title={isEdit ? '编辑MCP' : '创建MCP'}
|
||||
w={'100%'}
|
||||
maxW={['90vw', '600px']}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel required mb={0.5}>
|
||||
{t('common:common.Input name')}
|
||||
</FormLabel>
|
||||
<Input {...register('name', { required: true })} bg={'myGray.50'} />
|
||||
</Box>
|
||||
<Box mt={6}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel>{t('dashboard_mcp:apps')}</FormLabel>
|
||||
<Button variant={'whiteBase'} size={'sm'} onClick={onOpenSelectApp}>
|
||||
{t('dashboard_mcp:manage_app')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer mt={2} position={'relative'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('dashboard_mcp:app_name')}</Th>
|
||||
<Th>{t('dashboard_mcp:app_description')}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{apps.map((app, index) => {
|
||||
return (
|
||||
<Tr key={app.id} fontWeight={500} fontSize={'mini'} color={'myGray.900'}>
|
||||
<Td>{app.toolName}</Td>
|
||||
<Td>
|
||||
<Input
|
||||
{...register(`apps.${index}.description`, { required: true })}
|
||||
bg={'myGray.50'}
|
||||
w={'100%'}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Flex justifyContent={'flex-end'}>
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
hoverColor={'red.600'}
|
||||
onClick={() => remove(index)}
|
||||
color={'myGray.600'}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{apps.length === 0 && <EmptyTip />}
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
|
||||
{t('common:common.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={isConfirming}
|
||||
variant={'primary'}
|
||||
isDisabled={apps.length === 0}
|
||||
onClick={handleSubmit((data) => {
|
||||
if (isEdit) {
|
||||
return updateMcp(data);
|
||||
}
|
||||
return createMcp(data);
|
||||
})}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
|
||||
{isOpenSelectApp && (
|
||||
<SelectAppModal
|
||||
selectedApps={apps}
|
||||
onClose={onCloseSelectApp}
|
||||
onConfirm={(e) => {
|
||||
replaceSelectedApps(
|
||||
e.map((item) => ({
|
||||
appId: item.appId,
|
||||
toolName: item.toolName,
|
||||
description: item.description
|
||||
}))
|
||||
);
|
||||
onCloseSelectApp();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditMcpModal;
|
||||
62
projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx
Normal file
62
projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
|
||||
const UsageWay = ({ mcp, onClose }: { mcp: McpKeyType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const sseUrl = `${feConfigs?.mcpServerProxyEndpoint}/${mcp.key}/sse`;
|
||||
const jsonConfig = `{
|
||||
"mcpServers": {
|
||||
"${feConfigs?.systemTitle}-mcp-${mcp._id}": {
|
||||
"url": "${sseUrl}"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
return (
|
||||
<MyModal isOpen title={t('dashboard_mcp:usage_way')} onClose={onClose}>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel>{t('dashboard_mcp:mcp_endpoints')}</FormLabel>
|
||||
<HStack mt={0.5} bg={'myGray.50'} px={2} py={1} borderRadius={'md'} fontSize={'sm'}>
|
||||
<Box userSelect={'all'} flex={'1 0 0'} whiteSpace={'pre-wrap'} wordBreak={'break-all'}>
|
||||
{sseUrl}
|
||||
</Box>
|
||||
<CopyBox value={sseUrl}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border={'base'}
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('dashboard_mcp:mcp_json_config')}</Box>
|
||||
<CopyBox value={jsonConfig}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre-wrap'} wordBreak={'break-all'} p={3} overflowX={'auto'}>
|
||||
{jsonConfig}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UsageWay);
|
||||
@@ -162,6 +162,7 @@ const CustomAPIFileInput = () => {
|
||||
<Flex flexDirection={'column'} h="full">
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<FolderPath
|
||||
forbidLastClick
|
||||
paths={paths}
|
||||
onClick={(parentId) => {
|
||||
const index = paths.findIndex((item) => item.parentId === parentId);
|
||||
|
||||
@@ -37,7 +37,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
|
||||
const { setLoginStore, feConfigs } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { lastRoute = '/app/list' } = router.query as { lastRoute: string };
|
||||
const { lastRoute = '/dashboard/apps' } = router.query as { lastRoute: string };
|
||||
const state = useRef(getNanoid(8));
|
||||
const redirectUri = `${location.origin}/login/provider`;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
|
||||
const NonePage = () => {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push('/app/list');
|
||||
router.push('/dashboard/apps');
|
||||
}, [router]);
|
||||
|
||||
return <div></div>;
|
||||
|
||||
@@ -48,7 +48,7 @@ function Error() {
|
||||
if (modelError) {
|
||||
router.push('/account/model');
|
||||
} else {
|
||||
router.push('/app/list');
|
||||
router.push('/dashboard/apps');
|
||||
}
|
||||
}, 2000);
|
||||
}, []);
|
||||
|
||||
33
projects/app/src/pages/api/core/app/getBasicInfo.ts
Normal file
33
projects/app/src/pages/api/core/app/getBasicInfo.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { getAppBasicInfoByIds } from '@fastgpt/service/core/app/controller';
|
||||
|
||||
export type getBasicInfoQuery = {};
|
||||
|
||||
export type getBasicInfoBody = {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
export type getBasicInfoResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
}[];
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getBasicInfoBody, getBasicInfoQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<getBasicInfoResponse> {
|
||||
const { ids } = req.body;
|
||||
const { teamId } = await authCert({ req, authToken: true });
|
||||
|
||||
const apps = await getAppBasicInfoByIds({
|
||||
teamId,
|
||||
ids
|
||||
});
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
67
projects/app/src/pages/api/core/app/mcpTools/create.ts
Normal file
67
projects/app/src/pages/api/core/app/mcpTools/create.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { CreateAppBody, onCreateApp } from '../create';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import {
|
||||
getMCPToolRuntimeNode,
|
||||
getMCPToolSetRuntimeNode
|
||||
} from '@fastgpt/global/core/app/mcpTools/utils';
|
||||
|
||||
export type createMCPToolsQuery = {};
|
||||
|
||||
export type createMCPToolsBody = Omit<
|
||||
CreateAppBody,
|
||||
'type' | 'modules' | 'edges' | 'chatConfig'
|
||||
> & {
|
||||
url: string;
|
||||
toolList: ToolType[];
|
||||
};
|
||||
|
||||
export type createMCPToolsResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<createMCPToolsBody, createMCPToolsQuery>,
|
||||
res: ApiResponseType<createMCPToolsResponse>
|
||||
): Promise<createMCPToolsResponse> {
|
||||
const { name, avatar, toolList, url, parentId } = req.body;
|
||||
|
||||
const { teamId, tmbId, userId } = parentId
|
||||
? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true })
|
||||
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
const mcpToolsId = await onCreateApp({
|
||||
name,
|
||||
avatar,
|
||||
parentId,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: AppTypeEnum.toolSet,
|
||||
modules: [getMCPToolSetRuntimeNode({ url, toolList, name, avatar })],
|
||||
session
|
||||
});
|
||||
|
||||
for (const tool of toolList) {
|
||||
await onCreateApp({
|
||||
name: tool.name,
|
||||
avatar,
|
||||
parentId: mcpToolsId,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: AppTypeEnum.tool,
|
||||
intro: tool.description,
|
||||
modules: [getMCPToolRuntimeNode({ tool, url })],
|
||||
session
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
43
projects/app/src/pages/api/core/app/mcpTools/getMCPTools.ts
Normal file
43
projects/app/src/pages/api/core/app/mcpTools/getMCPTools.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
|
||||
export type getMCPToolsQuery = {};
|
||||
|
||||
export type getMCPToolsBody = { url: string };
|
||||
|
||||
export type getMCPToolsResponse = ToolType[];
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<getMCPToolsBody, getMCPToolsQuery>,
|
||||
res: ApiResponseType<getMCPToolsResponse[]>
|
||||
): Promise<getMCPToolsResponse> {
|
||||
const { url } = req.body;
|
||||
|
||||
const client = new Client({
|
||||
name: 'FastGPT-MCP-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const tools = await (async () => {
|
||||
try {
|
||||
const transport = new SSEClientTransport(new URL(url));
|
||||
await client.connect(transport);
|
||||
|
||||
const response = await client.listTools();
|
||||
|
||||
return response.tools || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching MCP tools:', error);
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
})();
|
||||
|
||||
return tools as ToolType[];
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
45
projects/app/src/pages/api/core/app/mcpTools/runTest.ts
Normal file
45
projects/app/src/pages/api/core/app/mcpTools/runTest.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
|
||||
export type RunToolTestQuery = {};
|
||||
export type RunToolTestBody = {
|
||||
params: Record<string, any>;
|
||||
url: string;
|
||||
toolName: string;
|
||||
};
|
||||
export type RunToolTestResponse = any;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<RunToolTestBody, RunToolTestQuery>,
|
||||
res: ApiResponseType<RunToolTestResponse>
|
||||
): Promise<RunToolTestResponse> {
|
||||
const { params, url, toolName } = req.body;
|
||||
|
||||
const client = new Client({
|
||||
name: 'FastGPT-MCP-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const result = await (async () => {
|
||||
try {
|
||||
const transport = new SSEClientTransport(new URL(url));
|
||||
await client.connect(transport);
|
||||
|
||||
return await client.callTool({
|
||||
name: toolName,
|
||||
arguments: params
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running MCP tool test:', error);
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
})();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
138
projects/app/src/pages/api/core/app/mcpTools/update.ts
Normal file
138
projects/app/src/pages/api/core/app/mcpTools/update.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { AppDetailType, ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ClientSession } from 'mongoose';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { onDelOneApp } from '../del';
|
||||
import { onCreateApp } from '../create';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
import {
|
||||
getMCPToolRuntimeNode,
|
||||
getMCPToolSetRuntimeNode
|
||||
} from '@fastgpt/global/core/app/mcpTools/utils';
|
||||
import { MCPToolSetData } from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
|
||||
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
|
||||
|
||||
export type updateMCPToolsQuery = {};
|
||||
|
||||
export type updateMCPToolsBody = {
|
||||
appId: string;
|
||||
url: string;
|
||||
toolList: ToolType[];
|
||||
};
|
||||
|
||||
export type updateMCPToolsResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<updateMCPToolsBody, updateMCPToolsQuery>,
|
||||
res: ApiResponseType<updateMCPToolsResponse>
|
||||
): Promise<updateMCPToolsResponse> {
|
||||
const { appId, url, toolList } = req.body;
|
||||
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
|
||||
|
||||
const toolSetNode = app.modules.find((item) => item.flowNodeType === FlowNodeTypeEnum.toolSet);
|
||||
const toolSetData = toolSetNode?.inputs[0].value as MCPToolSetData;
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
if (
|
||||
!isEqual(toolSetData, {
|
||||
url,
|
||||
toolList
|
||||
})
|
||||
) {
|
||||
await updateMCPChildrenTool({
|
||||
parentApp: app,
|
||||
toolSetData: {
|
||||
url,
|
||||
toolList
|
||||
},
|
||||
session
|
||||
});
|
||||
}
|
||||
|
||||
await MongoApp.findByIdAndUpdate(
|
||||
appId,
|
||||
{
|
||||
modules: [getMCPToolSetRuntimeNode({ url, toolList, name: app.name, avatar: app.avatar })]
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
|
||||
await MongoAppVersion.updateOne(
|
||||
{
|
||||
appId
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
nodes: [getMCPToolSetRuntimeNode({ url, toolList, name: app.name, avatar: app.avatar })]
|
||||
}
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
const updateMCPChildrenTool = async ({
|
||||
parentApp,
|
||||
toolSetData,
|
||||
session
|
||||
}: {
|
||||
parentApp: AppDetailType;
|
||||
toolSetData: MCPToolSetData;
|
||||
session: ClientSession;
|
||||
}) => {
|
||||
const { teamId, tmbId } = parentApp;
|
||||
const dbTools = await MongoApp.find({
|
||||
parentId: parentApp._id,
|
||||
teamId
|
||||
});
|
||||
|
||||
for await (const tool of dbTools) {
|
||||
if (!toolSetData.toolList.find((t) => t.name === tool.name)) {
|
||||
await onDelOneApp({
|
||||
teamId,
|
||||
appId: tool._id,
|
||||
session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const tool of toolSetData.toolList) {
|
||||
if (!dbTools.find((t) => t.name === tool.name)) {
|
||||
await onCreateApp({
|
||||
name: tool.name,
|
||||
avatar: parentApp.avatar,
|
||||
parentId: parentApp._id,
|
||||
teamId,
|
||||
tmbId,
|
||||
type: AppTypeEnum.tool,
|
||||
intro: tool.description,
|
||||
modules: [getMCPToolRuntimeNode({ tool, url: toolSetData.url })],
|
||||
session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for await (const tool of toolSetData.toolList) {
|
||||
const dbTool = dbTools.find((t) => t.name === tool.name);
|
||||
if (dbTool) {
|
||||
await MongoApp.findByIdAndUpdate(
|
||||
dbTool._id,
|
||||
{
|
||||
modules: [getMCPToolRuntimeNode({ tool, url: toolSetData.url })]
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -99,6 +99,16 @@ async function handler(req: ApiRequestProps<AppUpdateBody, AppUpdateQuery>) {
|
||||
|
||||
await refreshSourceAvatar(avatar, app.avatar, session);
|
||||
|
||||
if (app.type === AppTypeEnum.toolSet && avatar) {
|
||||
await MongoApp.updateMany(
|
||||
{ parentId: appId, teamId: app.teamId },
|
||||
{
|
||||
avatar
|
||||
},
|
||||
{ session }
|
||||
);
|
||||
}
|
||||
|
||||
return MongoApp.findByIdAndUpdate(
|
||||
appId,
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
getLastInteractiveValue,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
rewriteNodeOutputByHistories,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse
|
||||
@@ -43,7 +43,11 @@ import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/u
|
||||
import { getChatItems } from '@fastgpt/service/core/chat/controller';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
|
||||
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
ChatRoleEnum,
|
||||
ChatSourceEnum
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat';
|
||||
|
||||
export type Props = {
|
||||
@@ -97,6 +101,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
|
||||
const isPlugin = app.type === AppTypeEnum.plugin;
|
||||
const isTool = app.type === AppTypeEnum.tool;
|
||||
|
||||
const userQuestion: UserChatItemType = await (async () => {
|
||||
if (isPlugin) {
|
||||
@@ -106,6 +111,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
files: variables.files
|
||||
});
|
||||
}
|
||||
if (isTool) {
|
||||
return {
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: [
|
||||
{
|
||||
type: ChatItemValueTypeEnum.text,
|
||||
text: { content: 'tool test' }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const latestHumanChat = chatMessages.pop() as UserChatItemType;
|
||||
if (!latestHumanChat) {
|
||||
@@ -175,7 +191,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
chatId,
|
||||
responseChatItemId,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(edges, interactive),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
lastInteractive: interactive,
|
||||
|
||||
77
projects/app/src/pages/api/support/mcp/create.ts
Normal file
77
projects/app/src/pages/api/support/mcp/create.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
import { McpAppType } from '@fastgpt/global/support/mcp/type';
|
||||
|
||||
export type createQuery = {};
|
||||
|
||||
export type createBody = {
|
||||
name: string;
|
||||
apps: McpAppType[];
|
||||
};
|
||||
|
||||
export type createResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<createBody, createQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<createResponse> {
|
||||
const { teamId, tmbId, permission } = await authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
if (!permission.hasApikeyCreatePer) {
|
||||
return Promise.reject(TeamErrEnum.unPermission);
|
||||
}
|
||||
|
||||
let { name, apps } = req.body;
|
||||
|
||||
if (!apps.length) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
|
||||
// Count mcp length
|
||||
const totalMcp = await MongoMcpKey.countDocuments({ teamId });
|
||||
if (totalMcp >= 100) {
|
||||
return Promise.reject('暂时只支持100个MCP服务');
|
||||
}
|
||||
|
||||
// 对 apps 中的 id 进行去重,确保每个应用只出现一次
|
||||
const uniqueAppIds = new Set();
|
||||
apps = apps.filter((app) => {
|
||||
if (uniqueAppIds.has(app.appId)) {
|
||||
return false; // 过滤掉重复的 app id
|
||||
}
|
||||
uniqueAppIds.add(app.appId);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Check app read permission
|
||||
await Promise.all(
|
||||
apps.map((app) =>
|
||||
authAppByTmbId({
|
||||
tmbId,
|
||||
appId: app.appId,
|
||||
per: ReadPermissionVal
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await MongoMcpKey.create({
|
||||
teamId,
|
||||
tmbId,
|
||||
name,
|
||||
apps
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
34
projects/app/src/pages/api/support/mcp/delete.ts
Normal file
34
projects/app/src/pages/api/support/mcp/delete.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authMcp } from '@fastgpt/service/support/permission/mcp/auth';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
|
||||
export type deleteQuery = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type deleteBody = {};
|
||||
|
||||
export type deleteResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<deleteBody, deleteQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<deleteResponse> {
|
||||
const { id } = req.query;
|
||||
|
||||
await authMcp({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
mcpId: id,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
await MongoMcpKey.deleteOne({ _id: id });
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
34
projects/app/src/pages/api/support/mcp/list.ts
Normal file
34
projects/app/src/pages/api/support/mcp/list.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
|
||||
|
||||
export type listQuery = {};
|
||||
|
||||
export type listBody = {};
|
||||
|
||||
export type listResponse = McpKeyType[];
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<listBody, listQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<listResponse> {
|
||||
const { teamId, tmbId, permission } = await authUserPer({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true
|
||||
});
|
||||
|
||||
const list = await (async () => {
|
||||
if (permission.hasManagePer) {
|
||||
return await MongoMcpKey.find({ teamId }).lean().sort({ _id: -1 });
|
||||
}
|
||||
return await MongoMcpKey.find({ teamId, tmbId }).lean().sort({ _id: -1 });
|
||||
})();
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
197
projects/app/src/pages/api/support/mcp/server/toolCall.ts
Normal file
197
projects/app/src/pages/api/support/mcp/server/toolCall.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
|
||||
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import {
|
||||
getPluginRunUserQuery,
|
||||
updatePluginInputByVariables
|
||||
} from '@fastgpt/global/core/workflow/utils';
|
||||
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
ChatRoleEnum,
|
||||
ChatSourceEnum
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
|
||||
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
|
||||
import { getChatTitleFromChatMessage, removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
|
||||
import { saveChat } from '@fastgpt/service/core/chat/saveChat';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
export type toolCallQuery = {};
|
||||
|
||||
export type toolCallBody = {
|
||||
key: string;
|
||||
toolName: string;
|
||||
inputs: Record<string, any>;
|
||||
};
|
||||
|
||||
export type toolCallResponse = {};
|
||||
|
||||
const dispatchApp = async (app: AppSchema, variables: Record<string, any>) => {
|
||||
const isPlugin = app.type === AppTypeEnum.plugin;
|
||||
|
||||
const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(app.tmbId);
|
||||
// Get app latest version
|
||||
const { nodes, edges, chatConfig } = await getAppLatestVersion(app._id, app);
|
||||
|
||||
const userQuestion: UserChatItemType = (() => {
|
||||
if (isPlugin) {
|
||||
return getPluginRunUserQuery({
|
||||
pluginInputs: getPluginInputsFromStoreNodes(nodes || app.modules),
|
||||
variables
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
obj: ChatRoleEnum.Human,
|
||||
value: [
|
||||
{
|
||||
type: ChatItemValueTypeEnum.text,
|
||||
text: {
|
||||
content: variables.question
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
})();
|
||||
|
||||
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes));
|
||||
if (isPlugin) {
|
||||
// Assign values to runtimeNodes using variables
|
||||
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
|
||||
// Plugin runtime does not need global variables(It has been injected into the pluginInputNode)
|
||||
variables = {};
|
||||
} else {
|
||||
delete variables.question;
|
||||
variables.system_fileUrlList = variables.fileUrlList;
|
||||
delete variables.fileUrlList;
|
||||
}
|
||||
|
||||
const chatId = getNanoid();
|
||||
|
||||
const { flowUsages, assistantResponses, newVariables, flowResponses } = await dispatchWorkFlow({
|
||||
chatId,
|
||||
timezone,
|
||||
externalProvider,
|
||||
mode: 'chat',
|
||||
runningAppInfo: {
|
||||
id: String(app._id),
|
||||
teamId: String(app.teamId),
|
||||
tmbId: String(app.tmbId)
|
||||
},
|
||||
runningUserInfo: {
|
||||
teamId: String(app.teamId),
|
||||
tmbId: String(app.tmbId)
|
||||
},
|
||||
uid: String(app.tmbId),
|
||||
runtimeNodes,
|
||||
runtimeEdges: storeEdges2RuntimeEdges(edges),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
chatConfig,
|
||||
histories: [],
|
||||
stream: false,
|
||||
maxRunTimes: WORKFLOW_MAX_RUN_TIMES
|
||||
});
|
||||
|
||||
// Save chat
|
||||
const aiResponse: AIChatItemType & { dataId?: string } = {
|
||||
obj: ChatRoleEnum.AI,
|
||||
value: assistantResponses,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses
|
||||
};
|
||||
const newTitle = isPlugin ? 'Mcp call' : getChatTitleFromChatMessage(userQuestion);
|
||||
await saveChat({
|
||||
chatId,
|
||||
appId: app._id,
|
||||
teamId: app.teamId,
|
||||
tmbId: app.tmbId,
|
||||
nodes,
|
||||
appChatConfig: chatConfig,
|
||||
variables: newVariables,
|
||||
isUpdateUseTime: false, // owner update use time
|
||||
newTitle,
|
||||
source: ChatSourceEnum.mcp,
|
||||
content: [userQuestion, aiResponse]
|
||||
});
|
||||
|
||||
// Push usage
|
||||
createChatUsage({
|
||||
appName: app.name,
|
||||
appId: app._id,
|
||||
teamId: app.teamId,
|
||||
tmbId: app.tmbId,
|
||||
source: UsageSourceEnum.mcp,
|
||||
flowUsages
|
||||
});
|
||||
|
||||
// Get MCP response type
|
||||
const responseContent = (() => {
|
||||
if (isPlugin) {
|
||||
const output = flowResponses.find(
|
||||
(item) => item.moduleType === FlowNodeTypeEnum.pluginOutput
|
||||
);
|
||||
if (output) {
|
||||
return JSON.stringify(output.pluginOutput);
|
||||
} else {
|
||||
return 'Can not get response from plugin';
|
||||
}
|
||||
}
|
||||
|
||||
return assistantResponses
|
||||
.map((item) => item?.text?.content)
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
})();
|
||||
|
||||
return responseContent;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<toolCallBody, toolCallQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<toolCallResponse> {
|
||||
const { key, toolName, inputs } = req.body;
|
||||
|
||||
const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean();
|
||||
|
||||
if (!mcp) {
|
||||
return Promise.reject(CommonErrEnum.invalidResource);
|
||||
}
|
||||
|
||||
// Get app list
|
||||
const appList = await MongoApp.find({
|
||||
_id: { $in: mcp.apps.map((app) => app.appId) },
|
||||
type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] }
|
||||
}).lean();
|
||||
|
||||
const app = appList.find((app) => {
|
||||
const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!;
|
||||
|
||||
return toolName === mcpApp.toolName;
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return Promise.reject(CommonErrEnum.missingParams);
|
||||
}
|
||||
|
||||
return await dispatchApp(app, inputs);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
152
projects/app/src/pages/api/support/mcp/server/toolList.ts
Normal file
152
projects/app/src/pages/api/support/mcp/server/toolList.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
|
||||
import { Tool } from '@modelcontextprotocol/sdk/types';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
|
||||
export type listToolsQuery = { key: string };
|
||||
|
||||
export type listToolsBody = {};
|
||||
|
||||
export type listToolsResponse = {};
|
||||
|
||||
const pluginNodes2InputSchema = (nodes: StoreNodeItemType[]) => {
|
||||
const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput);
|
||||
|
||||
const schema: Tool['inputSchema'] = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
pluginInput?.inputs.forEach((input) => {
|
||||
const jsonSchema = (
|
||||
toolValueTypeList.find((type) => type.value === input.valueType) || toolValueTypeList[0]
|
||||
)?.jsonSchema;
|
||||
|
||||
schema.properties![input.key] = {
|
||||
...jsonSchema,
|
||||
description: input.description,
|
||||
enum: input.enum?.split('\n').filter(Boolean) || undefined
|
||||
};
|
||||
|
||||
if (input.required) {
|
||||
// @ts-ignore
|
||||
schema.required.push(input.key);
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
const workflow2InputSchema = (chatConfig?: AppChatConfigType) => {
|
||||
const schema: Tool['inputSchema'] = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: {
|
||||
type: 'string',
|
||||
description: 'Question from user'
|
||||
},
|
||||
...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg
|
||||
? {
|
||||
fileUrlList: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
description: 'File linkage'
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
required: ['question']
|
||||
};
|
||||
|
||||
chatConfig?.variables?.forEach((item) => {
|
||||
const jsonSchema = (
|
||||
toolValueTypeList.find((type) => type.value === item.valueType) || toolValueTypeList[0]
|
||||
)?.jsonSchema;
|
||||
|
||||
schema.properties![item.key] = {
|
||||
...jsonSchema,
|
||||
description: item.description,
|
||||
enum: item.enums?.map((enumItem) => enumItem.value) || undefined
|
||||
};
|
||||
|
||||
if (item.required) {
|
||||
// @ts-ignore
|
||||
schema.required!.push(item.key);
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<listToolsBody, listToolsQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<Tool[]> {
|
||||
const { key } = req.query;
|
||||
|
||||
const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean();
|
||||
|
||||
if (!mcp) {
|
||||
return Promise.reject(CommonErrEnum.invalidResource);
|
||||
}
|
||||
|
||||
// Get app list
|
||||
const appList = await MongoApp.find(
|
||||
{
|
||||
_id: { $in: mcp.apps.map((app) => app.appId) },
|
||||
type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] }
|
||||
},
|
||||
{ name: 1, intro: 1 }
|
||||
).lean();
|
||||
|
||||
// Filter not permission app
|
||||
const permissionAppList = await Promise.all(
|
||||
appList.filter(async (app) => {
|
||||
try {
|
||||
await authAppByTmbId({ tmbId: mcp.tmbId, appId: app._id, per: ReadPermissionVal });
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Get latest version
|
||||
const versionList = await Promise.all(
|
||||
permissionAppList.map((app) => getAppLatestVersion(app._id, app))
|
||||
);
|
||||
|
||||
// Compute mcp tools
|
||||
const tools = versionList.map<Tool>((version, index) => {
|
||||
const app = permissionAppList[index];
|
||||
const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!;
|
||||
|
||||
const isPlugin = !!version.nodes.find(
|
||||
(node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput
|
||||
);
|
||||
|
||||
return {
|
||||
name: mcpApp.toolName,
|
||||
description: mcpApp.description,
|
||||
inputSchema: isPlugin
|
||||
? pluginNodes2InputSchema(version.nodes)
|
||||
: workflow2InputSchema(version.chatConfig)
|
||||
};
|
||||
});
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
66
projects/app/src/pages/api/support/mcp/update.ts
Normal file
66
projects/app/src/pages/api/support/mcp/update.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authMcp } from '../../../../../../../packages/service/support/permission/mcp/auth';
|
||||
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema';
|
||||
import { McpAppType } from '@fastgpt/global/support/mcp/type';
|
||||
|
||||
export type updateQuery = {};
|
||||
|
||||
export type updateBody = {
|
||||
id: string;
|
||||
name: string;
|
||||
apps: McpAppType[];
|
||||
};
|
||||
|
||||
export type updateResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<updateBody, updateQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<updateResponse> {
|
||||
let { id: mcpId, name, apps } = req.body;
|
||||
const { tmbId } = await authMcp({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
mcpId,
|
||||
per: WritePermissionVal
|
||||
});
|
||||
|
||||
// 对 apps 中的 id 进行去重,确保每个应用只出现一次
|
||||
const uniqueAppIds = new Set();
|
||||
apps = apps.filter((app) => {
|
||||
if (uniqueAppIds.has(app.appId)) {
|
||||
return false; // 过滤掉重复的 app id
|
||||
}
|
||||
uniqueAppIds.add(app.appId);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Check app read permission
|
||||
await Promise.all(
|
||||
apps.map((app) =>
|
||||
authAppByTmbId({
|
||||
tmbId,
|
||||
appId: app.appId,
|
||||
per: ReadPermissionVal
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await MongoMcpKey.updateOne(
|
||||
{ _id: mcpId },
|
||||
{
|
||||
$set: {
|
||||
...(name && { name }),
|
||||
apps
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -11,7 +11,7 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse,
|
||||
getLastInteractiveValue
|
||||
@@ -289,9 +289,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
chatId,
|
||||
responseChatItemId,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(edges, interactive),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
lastInteractive: interactive,
|
||||
chatConfig,
|
||||
histories: newHistories,
|
||||
stream,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d'
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
getMaxHistoryLimitFromNodes,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes,
|
||||
textAdaptGptResponse,
|
||||
getLastInteractiveValue
|
||||
@@ -288,7 +288,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
chatId,
|
||||
responseChatItemId,
|
||||
runtimeNodes,
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges, interactive),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(edges, interactive),
|
||||
variables,
|
||||
query: removeEmptyUserInput(userQuestion.value),
|
||||
lastInteractive: interactive,
|
||||
|
||||
@@ -21,6 +21,10 @@ const Plugin = dynamic(() => import('@/pageComponents/app/detail/Plugin'), {
|
||||
ssr: false,
|
||||
loading: () => <Loading fixed={false} />
|
||||
});
|
||||
const MCPTools = dynamic(() => import('@/pageComponents/app/detail/MCPTools'), {
|
||||
ssr: false,
|
||||
loading: () => <Loading fixed={false} />
|
||||
});
|
||||
|
||||
const AppDetail = () => {
|
||||
const { setAppId, setSource } = useChatStore();
|
||||
@@ -42,6 +46,7 @@ const AppDetail = () => {
|
||||
{appDetail.type === AppTypeEnum.simple && <SimpleEdit />}
|
||||
{appDetail.type === AppTypeEnum.workflow && <Workflow />}
|
||||
{appDetail.type === AppTypeEnum.plugin && <Plugin />}
|
||||
{appDetail.type === AppTypeEnum.toolSet && <MCPTools />}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -89,7 +89,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
// reset all chat tore
|
||||
if (e?.code === 501) {
|
||||
setLastChatAppId('');
|
||||
router.replace('/app/list');
|
||||
router.replace('/dashboard/apps');
|
||||
} else {
|
||||
router.replace({
|
||||
query: {
|
||||
@@ -259,7 +259,7 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
|
||||
status: 'error',
|
||||
title: t('common:core.chat.You need to a chat app')
|
||||
});
|
||||
router.replace('/app/list');
|
||||
router.replace('/dashboard/apps');
|
||||
} else {
|
||||
router.replace({
|
||||
query: {
|
||||
|
||||
113
projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx
Normal file
113
projects/app/src/pages/dashboard/[pluginGroupId]/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import DashboardContainer from '@/pageComponents/dashboard/Container';
|
||||
|
||||
import PluginCard from '@/pageComponents/dashboard/SystemPlugin/ToolCard';
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import { getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
|
||||
const SystemTools = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { type, pluginGroupId } = router.query as { type?: string; pluginGroupId?: string };
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const { data: plugins = [], loading: isLoading } = useRequest2(getSystemPlugTemplates, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const currentPlugins = useMemo(() => {
|
||||
return plugins
|
||||
.filter((plugin) => {
|
||||
if (!type || type === 'all') return true;
|
||||
return plugin.templateType === type;
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!searchKey) return true;
|
||||
const regx = new RegExp(searchKey, 'i');
|
||||
return regx.test(`${item.name}${item.intro}${item.instructions}`);
|
||||
});
|
||||
}, [plugins, searchKey, type]);
|
||||
|
||||
return (
|
||||
<DashboardContainer>
|
||||
{({ pluginGroups, MenuIcon }) => {
|
||||
const currentGroup = pluginGroups.find((group) => group.groupId === pluginGroupId);
|
||||
const groupTemplateTypeIds =
|
||||
currentGroup?.groupTypes
|
||||
?.map((type) => type.typeId)
|
||||
.reduce(
|
||||
(acc, cur) => {
|
||||
acc[cur] = true;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
) || {};
|
||||
const filterPluginsByGroup = currentPlugins.filter((plugin) => {
|
||||
if (!currentGroup) return true;
|
||||
return groupTemplateTypeIds[plugin.templateType];
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox isLoading={isLoading} h={'100%'}>
|
||||
<Box p={6} h={'100%'} overflowY={'auto'}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
{isPc ? (
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{t('common:core.module.template.System Plugin')}
|
||||
</Box>
|
||||
) : (
|
||||
MenuIcon
|
||||
)}
|
||||
|
||||
<Box flex={'0 0 200px'}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder={t('common:plugin.Search plugin')}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
py={5}
|
||||
>
|
||||
{filterPluginsByGroup.map((item) => (
|
||||
<PluginCard key={item.id} item={item} groups={pluginGroups} />
|
||||
))}
|
||||
</Grid>
|
||||
{filterPluginsByGroup.length === 0 && <EmptyTip />}
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
}}
|
||||
</DashboardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemTools;
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['app']))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { postCreateAppFolder } from '@/web/core/app/api/app';
|
||||
import type { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditFolderModal';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import AppListContextProvider, { AppListContext } from '@/pageComponents/app/list/context';
|
||||
import AppListContextProvider, { AppListContext } from '@/pageComponents/dashboard/apps/context';
|
||||
import FolderPath from '@/components/common/folder/Path';
|
||||
import { useRouter } from 'next/router';
|
||||
import FolderSlideCard from '@/components/common/folder/SlideCard';
|
||||
@@ -22,25 +22,25 @@ import {
|
||||
getCollaboratorList,
|
||||
postUpdateAppCollaborators
|
||||
} from '@/web/core/app/api/collaborator';
|
||||
import type { CreateAppType } from '@/pageComponents/app/list/CreateModal';
|
||||
import type { CreateAppType } from '@/pageComponents/dashboard/apps/CreateModal';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import TemplateMarketModal from '@/pageComponents/app/list/TemplateMarketModal';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import JsonImportModal from '@/pageComponents/app/list/JsonImportModal';
|
||||
import JsonImportModal from '@/pageComponents/dashboard/apps/JsonImportModal';
|
||||
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
|
||||
import DashboardContainer from '@/pageComponents/dashboard/Container';
|
||||
import List from '@/pageComponents/dashboard/apps/List';
|
||||
import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
|
||||
|
||||
const CreateModal = dynamic(() => import('@/pageComponents/app/list/CreateModal'));
|
||||
const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal'));
|
||||
const EditFolderModal = dynamic(
|
||||
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
|
||||
);
|
||||
const HttpEditModal = dynamic(() => import('@/pageComponents/app/list/HttpPluginEditModal'));
|
||||
const List = dynamic(() => import('@/pageComponents/app/list/List'));
|
||||
const HttpEditModal = dynamic(() => import('@/pageComponents/dashboard/apps/HttpPluginEditModal'));
|
||||
|
||||
const MyApps = () => {
|
||||
const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
@@ -66,13 +66,17 @@ const MyApps = () => {
|
||||
onOpen: onOpenCreateHttpPlugin,
|
||||
onClose: onCloseCreateHttpPlugin
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenCreateMCPTools,
|
||||
onOpen: onOpenCreateMCPTools,
|
||||
onClose: onCloseCreateMCPTools
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenJsonImportModal,
|
||||
onOpen: onOpenJsonImportModal,
|
||||
onClose: onCloseJsonImportModal
|
||||
} = useDisclosure();
|
||||
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
|
||||
const [templateModalType, setTemplateModalType] = useState<AppTypeEnum | 'all'>();
|
||||
|
||||
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
|
||||
onSuccess() {
|
||||
@@ -91,6 +95,19 @@ const MyApps = () => {
|
||||
errorToast: 'Error'
|
||||
});
|
||||
|
||||
const appTypeName = useMemo(() => {
|
||||
const map: Record<AppTypeEnum | 'all', string> = {
|
||||
all: t('common:core.module.template.Team app'),
|
||||
[AppTypeEnum.simple]: t('app:type.Simple bot'),
|
||||
[AppTypeEnum.workflow]: t('app:type.Workflow bot'),
|
||||
[AppTypeEnum.plugin]: t('app:type.Plugin'),
|
||||
[AppTypeEnum.httpPlugin]: t('app:type.Http plugin'),
|
||||
[AppTypeEnum.folder]: t('common:Folder'),
|
||||
[AppTypeEnum.toolSet]: t('app:type.MCP tools'),
|
||||
[AppTypeEnum.tool]: t('app:type.MCP tools')
|
||||
};
|
||||
return map[appType] || map['all'];
|
||||
}, [appType, t]);
|
||||
const RenderSearchInput = useMemo(
|
||||
() => (
|
||||
<InputGroup maxW={['auto', '250px']} position={'relative'}>
|
||||
@@ -120,10 +137,11 @@ const MyApps = () => {
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
{paths.length > 0 && (
|
||||
<Box pt={[4, 6]} pl={3}>
|
||||
<Box pt={[4, 6]} pl={5}>
|
||||
<FolderPath
|
||||
paths={paths}
|
||||
hoverStyle={{ bg: 'myGray.200' }}
|
||||
forbidLastClick
|
||||
onClick={(parentId) => {
|
||||
router.push({
|
||||
query: {
|
||||
@@ -140,78 +158,23 @@ const MyApps = () => {
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
pr={folderDetail ? [3, 2] : [3, 8]}
|
||||
pl={3}
|
||||
pr={folderDetail ? [3, 2] : [3, 6]}
|
||||
pl={6}
|
||||
overflowY={'auto'}
|
||||
overflowX={'hidden'}
|
||||
>
|
||||
<Flex pt={paths.length > 0 ? 3 : [4, 6]} alignItems={'center'} gap={3}>
|
||||
<LightRowTabs
|
||||
list={[
|
||||
{
|
||||
label: t('app:type.All'),
|
||||
value: 'ALL'
|
||||
},
|
||||
{
|
||||
label: t('app:type.Simple bot'),
|
||||
value: AppTypeEnum.simple
|
||||
},
|
||||
{
|
||||
label: t('app:type.Workflow bot'),
|
||||
value: AppTypeEnum.workflow
|
||||
},
|
||||
{
|
||||
label: t('app:type.Plugin'),
|
||||
value: AppTypeEnum.plugin
|
||||
}
|
||||
]}
|
||||
value={appType}
|
||||
inlineStyles={{ px: 0.5 }}
|
||||
gap={5}
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
fontSize={['sm', 'md']}
|
||||
flexShrink={0}
|
||||
onChange={(e) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
type: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isPc ? (
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{appTypeName}
|
||||
</Box>
|
||||
) : (
|
||||
MenuIcon
|
||||
)}
|
||||
<Box flex={1} />
|
||||
|
||||
{isPc && RenderSearchInput}
|
||||
|
||||
{isPc && (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
gap={1.5}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.250'}
|
||||
h={9}
|
||||
px={4}
|
||||
fontSize={'14px'}
|
||||
fontWeight={'medium'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
_hover={{
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
onClick={() => setTemplateModalType('all')}
|
||||
>
|
||||
<MyImage src={'/imgs/app/templateFill.svg'} w={'18px'} />
|
||||
{t('app:template_market')}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{(folderDetail
|
||||
? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin
|
||||
: userInfo?.team.permission.hasAppCreatePer) && (
|
||||
@@ -248,6 +211,12 @@ const MyApps = () => {
|
||||
label: t('app:type.Http plugin'),
|
||||
description: t('app:type.Create http plugin tip'),
|
||||
onClick: onOpenCreateHttpPlugin
|
||||
},
|
||||
{
|
||||
icon: 'core/app/type/mcpToolsFill',
|
||||
label: t('app:type.MCP tools'),
|
||||
description: t('app:type.Create mcp tools tip'),
|
||||
onClick: onOpenCreateMCPTools
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -261,20 +230,6 @@ const MyApps = () => {
|
||||
}
|
||||
]
|
||||
},
|
||||
...(isPc
|
||||
? []
|
||||
: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: '/imgs/app/templateFill.svg',
|
||||
label: t('app:template_market'),
|
||||
description: t('app:template_market_description'),
|
||||
onClick: () => setTemplateModalType('all')
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
{
|
||||
children: [
|
||||
{
|
||||
@@ -379,19 +334,10 @@ const MyApps = () => {
|
||||
/>
|
||||
)}
|
||||
{!!createAppType && (
|
||||
<CreateModal
|
||||
type={createAppType}
|
||||
onClose={() => setCreateAppType(undefined)}
|
||||
onOpenTemplateModal={setTemplateModalType}
|
||||
/>
|
||||
<CreateModal type={createAppType} onClose={() => setCreateAppType(undefined)} />
|
||||
)}
|
||||
{isOpenCreateHttpPlugin && <HttpEditModal onClose={onCloseCreateHttpPlugin} />}
|
||||
{!!templateModalType && (
|
||||
<TemplateMarketModal
|
||||
onClose={() => setTemplateModalType(undefined)}
|
||||
defaultType={templateModalType}
|
||||
/>
|
||||
)}
|
||||
{isOpenCreateMCPTools && <MCPToolsEditModal onClose={onCloseCreateMCPTools} />}
|
||||
{isOpenJsonImportModal && <JsonImportModal onClose={onCloseJsonImportModal} />}
|
||||
</Flex>
|
||||
);
|
||||
@@ -399,9 +345,13 @@ const MyApps = () => {
|
||||
|
||||
function ContextRender() {
|
||||
return (
|
||||
<AppListContextProvider>
|
||||
<MyApps />
|
||||
</AppListContextProvider>
|
||||
<DashboardContainer>
|
||||
{({ MenuIcon }) => (
|
||||
<AppListContextProvider>
|
||||
<MyApps MenuIcon={MenuIcon} />
|
||||
</AppListContextProvider>
|
||||
)}
|
||||
</DashboardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
179
projects/app/src/pages/dashboard/mcpServer/index.tsx
Normal file
179
projects/app/src/pages/dashboard/mcpServer/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import React, { useState } from 'react';
|
||||
import DashboardContainer from '@/pageComponents/dashboard/Container';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { deleteMcpServer, getMcpServerList } from '@/web/support/mcp/api';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import EditMcpModal, { defaultForm, EditMcForm } from '@/pageComponents/dashboard/mcp/EditModal';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { McpKeyType } from '@fastgpt/global/support/mcp/type';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
|
||||
const UsageWay = dynamic(() => import('@/pageComponents/dashboard/mcp/usageWay'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const McpServer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const {
|
||||
data: mcpServerList = [],
|
||||
loading: loadingList,
|
||||
refresh: loadMcpList
|
||||
} = useRequest2(getMcpServerList, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
const [editMcp, setEditMcp] = useState<EditMcForm>();
|
||||
const [usageWay, setUsageWay] = useState<McpKeyType>();
|
||||
|
||||
const { openConfirm: openDelConfirm, ConfirmModal: DelConfirmModal } = useConfirm({
|
||||
type: 'delete',
|
||||
content: t('dashboard_mcp:delete_mcp_server_confirm_tip')
|
||||
});
|
||||
const { runAsync: onDeleteMcpServer } = useRequest2(deleteMcpServer, {
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
loadMcpList();
|
||||
}
|
||||
});
|
||||
|
||||
const isLoading = loadingList;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardContainer>
|
||||
{({ MenuIcon }) => (
|
||||
<MyBox isLoading={isLoading} h={'100%'} p={6}>
|
||||
{isPc ? (
|
||||
<Flex alignItems={'flex-end'} justifyContent={'space-between'}>
|
||||
<Box>
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{t('dashboard_mcp:mcp_server')}
|
||||
</Box>
|
||||
<Box fontSize={'xs'} color={'myGray.500'}>
|
||||
{t('dashboard_mcp:mcp_server_description')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button onClick={() => setEditMcp(defaultForm)}>
|
||||
{t('dashboard_mcp:create_mcp_server')}
|
||||
</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<HStack>
|
||||
<Box>{MenuIcon}</Box>
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{t('dashboard_mcp:mcp_server')}
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box fontSize={'xs'} color={'myGray.500'}>
|
||||
{t('dashboard_mcp:mcp_server_description')}
|
||||
</Box>
|
||||
<Flex mt={2} justifyContent={'flex-end'}>
|
||||
<Button onClick={() => setEditMcp(defaultForm)}>
|
||||
{t('dashboard_mcp:create_mcp_server')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* table */}
|
||||
<TableContainer mt={4} bg={'white'} borderRadius={'md'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr borderBottom={'base'}>
|
||||
<Th bg={'white'}>{t('dashboard_mcp:mcp_name')}</Th>
|
||||
<Th bg={'white'}>{t('dashboard_mcp:mcp_apps')}</Th>
|
||||
<Th bg={'white'}></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{mcpServerList.map((mcp) => {
|
||||
return (
|
||||
<Tr key={mcp._id} fontWeight={500} fontSize={'sm'} color={'myGray.900'}>
|
||||
<Td>{mcp.name}</Td>
|
||||
<Td>{mcp.apps.length}</Td>
|
||||
<Td>
|
||||
<HStack>
|
||||
<Button
|
||||
mr={4}
|
||||
variant={'whiteBase'}
|
||||
size={'sm'}
|
||||
onClick={() => setUsageWay(mcp)}
|
||||
>
|
||||
{t('dashboard_mcp:start_use')}
|
||||
</Button>
|
||||
<MyIconButton
|
||||
icon="edit"
|
||||
onClick={() =>
|
||||
setEditMcp({
|
||||
id: mcp._id,
|
||||
name: mcp.name,
|
||||
apps: mcp.apps
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
hoverColor={'red.600'}
|
||||
onClick={() => openDelConfirm(() => onDeleteMcpServer(mcp._id))()}
|
||||
/>
|
||||
</HStack>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{mcpServerList.length === 0 && <EmptyTip />}
|
||||
</TableContainer>
|
||||
</MyBox>
|
||||
)}
|
||||
</DashboardContainer>
|
||||
|
||||
<DelConfirmModal />
|
||||
{!!usageWay && <UsageWay mcp={usageWay} onClose={() => setUsageWay(undefined)} />}
|
||||
{!!editMcp && (
|
||||
<EditMcpModal
|
||||
editMcp={editMcp}
|
||||
onClose={() => setEditMcp(undefined)}
|
||||
onSuccess={() => {
|
||||
setEditMcp(undefined);
|
||||
loadMcpList();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpServer;
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['dashboard_mcp']))
|
||||
}
|
||||
};
|
||||
}
|
||||
348
projects/app/src/pages/dashboard/templateMarket/index.tsx
Normal file
348
projects/app/src/pages/dashboard/templateMarket/index.tsx
Normal file
@@ -0,0 +1,348 @@
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import DashboardContainer from '@/pageComponents/dashboard/Container';
|
||||
import { Box, Button, Flex, Grid, HStack } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getTemplateMarketItemDetail } from '@/web/core/app/api/template';
|
||||
import { postCreateApp } from '@/web/core/app/api';
|
||||
import { webPushTrack } from '@/web/common/middle/tracks/utils';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import AppTypeTag from '@/pageComponents/dashboard/apps/TypeTag';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
const UseGuideModal = dynamic(() => import('@/components/common/Modal/UseGuideModal'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const TemplateMarket = ({
|
||||
templateList,
|
||||
templateTags,
|
||||
MenuIcon
|
||||
}: {
|
||||
templateList: AppTemplateSchemaType[];
|
||||
templateTags: TemplateTypeSchemaType[];
|
||||
MenuIcon: JSX.Element;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
parentId,
|
||||
type,
|
||||
appType = 'all'
|
||||
} = router.query as { parentId?: ParentIdType; type?: string; appType?: AppTypeEnum | 'all' };
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
const filterTemplateTags = useMemo(() => {
|
||||
return templateTags
|
||||
.map((tag) => {
|
||||
const templates = templateList.filter((template) => template.tags.includes(tag.typeId));
|
||||
return {
|
||||
...tag,
|
||||
templates
|
||||
};
|
||||
})
|
||||
.filter((item) => item.templates.length > 0);
|
||||
}, [templateList, templateTags]);
|
||||
|
||||
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
|
||||
async (template: AppTemplateSchemaType) => {
|
||||
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
|
||||
|
||||
return postCreateApp({
|
||||
parentId,
|
||||
avatar: template.avatar,
|
||||
name: template.name,
|
||||
type: template.type as AppTypeEnum,
|
||||
modules: templateDetail.workflow.nodes || [],
|
||||
edges: templateDetail.workflow.edges || [],
|
||||
chatConfig: templateDetail.workflow.chatConfig
|
||||
}).then((res) => {
|
||||
webPushTrack.useAppTemplate({
|
||||
id: res,
|
||||
name: template.name
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess(id: string) {
|
||||
router.push(`/app/detail?appId=${id}`);
|
||||
},
|
||||
successToast: t('common:common.Create Success'),
|
||||
errorToast: t('common:common.Create Failed')
|
||||
}
|
||||
);
|
||||
|
||||
const TemplateCard = useCallback(
|
||||
({ item }: { item: AppTemplateSchemaType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
key={item.templateId}
|
||||
lineHeight={1.5}
|
||||
h="100%"
|
||||
pt={4}
|
||||
pb={3}
|
||||
px={4}
|
||||
border={'base'}
|
||||
boxShadow={'2'}
|
||||
bg={'white'}
|
||||
borderRadius={'10px'}
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5',
|
||||
'& .buttons': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Box mr={'-1rem'}>
|
||||
<AppTypeTag type={item.type as AppTypeEnum} />
|
||||
</Box>
|
||||
</HStack>
|
||||
<Box
|
||||
flex={['1 0 48px', '1 0 56px']}
|
||||
mt={3}
|
||||
pr={1}
|
||||
textAlign={'justify'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<Box className={'textEllipsis2'}>{item.intro || t('app:templateMarket.no_intro')}</Box>
|
||||
</Box>
|
||||
|
||||
<Box w={'full'} fontSize={'mini'}>
|
||||
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
<Flex
|
||||
className="buttons"
|
||||
display={'none'}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
position={'absolute'}
|
||||
borderRadius={'lg'}
|
||||
w={'full'}
|
||||
h={'full'}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={1}
|
||||
height={'40px'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
gap={2}
|
||||
>
|
||||
{((item.userGuide?.type === 'markdown' && item.userGuide?.content) ||
|
||||
(item.userGuide?.type === 'link' && item.userGuide?.link)) && (
|
||||
<UseGuideModal
|
||||
title={item.name}
|
||||
iconSrc={item.avatar}
|
||||
text={item.userGuide?.content}
|
||||
link={item.userGuide?.link}
|
||||
>
|
||||
{({ onClick }) => (
|
||||
<Button variant={'whiteBase'} h={6} rounded={'sm'} onClick={onClick}>
|
||||
{t('app:templateMarket.template_guide')}
|
||||
</Button>
|
||||
)}
|
||||
</UseGuideModal>
|
||||
)}
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
h={6}
|
||||
rounded={'sm'}
|
||||
onClick={() => onUseTemplate(item)}
|
||||
>
|
||||
{t('app:templateMarket.Use')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</MyBox>
|
||||
);
|
||||
},
|
||||
[onUseTemplate, feConfigs.systemTitle]
|
||||
);
|
||||
|
||||
// Scroll to the selected template type
|
||||
useEffect(() => {
|
||||
if (type) {
|
||||
const typeElement = document.getElementById(type as string);
|
||||
if (typeElement) {
|
||||
typeElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<MyBox ref={containerRef} h={'100%'} isLoading={isCreating}>
|
||||
<Flex flexDirection={'column'} h={'100%'} py={5}>
|
||||
<Flex mb={4} alignItems={'center'} px={5}>
|
||||
{isPc ? (
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{t('app:template_market')}
|
||||
</Box>
|
||||
) : (
|
||||
MenuIcon
|
||||
)}
|
||||
|
||||
<Box flex={1} />
|
||||
<Box mr={3}>
|
||||
<SearchInput
|
||||
h={'34px'}
|
||||
bg={'white'}
|
||||
placeholder={t('app:templateMarket.Search_template')}
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<MySelect
|
||||
h={'34px'}
|
||||
bg={'white'}
|
||||
value={appType}
|
||||
list={[
|
||||
{
|
||||
value: 'all',
|
||||
label: t('app:type.All')
|
||||
},
|
||||
{
|
||||
value: AppTypeEnum.simple,
|
||||
label: t('app:type.Simple bot')
|
||||
},
|
||||
{
|
||||
value: AppTypeEnum.workflow,
|
||||
label: t('app:type.Workflow bot')
|
||||
},
|
||||
{
|
||||
value: AppTypeEnum.plugin,
|
||||
label: t('app:type.Plugin')
|
||||
}
|
||||
]}
|
||||
onChange={(e) => {
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
type: '',
|
||||
appType: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box flex={'1 0 0'} px={5} overflow={'auto'}>
|
||||
{searchKey ? (
|
||||
<>
|
||||
<Box fontSize={'lg'} color={'myGray.900'} mb={4}>
|
||||
{t('common:xx_search_result', { key: searchKey })}
|
||||
</Box>
|
||||
{(() => {
|
||||
const templates = templateList.filter((template) =>
|
||||
`${template.name}${template.intro}`.includes(searchKey)
|
||||
);
|
||||
|
||||
if (templates.length > 0) {
|
||||
return (
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{templates.map((item) => (
|
||||
<TemplateCard key={item.templateId} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyTip text={t('app:template_market_empty_data')} />;
|
||||
})()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{filterTemplateTags.map((item) => {
|
||||
return (
|
||||
<Box key={item.typeId}>
|
||||
<Box id={item.typeId} color={'myGray.900'} mb={4} fontWeight={500} pt={2}>
|
||||
{t(item.typeName as any)}
|
||||
</Box>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
pb={5}
|
||||
>
|
||||
{item.templates.map((item) => (
|
||||
<TemplateCard key={item.templateId} item={item} />
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
};
|
||||
|
||||
const TemplateMarketContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<DashboardContainer>
|
||||
{({ templateTags, templateList, MenuIcon }) => (
|
||||
<TemplateMarket
|
||||
templateTags={templateTags}
|
||||
templateList={templateList}
|
||||
MenuIcon={MenuIcon}
|
||||
/>
|
||||
)}
|
||||
</DashboardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateMarketContainer;
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['app']))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from 'next/router';
|
||||
const index = () => {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push('/app/list');
|
||||
router.push('/dashboard/apps');
|
||||
}, [router]);
|
||||
return <Loading></Loading>;
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function getServerSideProps(content: any) {
|
||||
props: {
|
||||
code: content?.query?.code || '',
|
||||
token: content?.query?.token || '',
|
||||
callbackUrl: content?.query?.callbackUrl || '/app/list',
|
||||
callbackUrl: content?.query?.callbackUrl || '/dashboard/apps',
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +66,9 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
|
||||
const decodeLastRoute = decodeURIComponent(lastRoute);
|
||||
// 检查是否是当前的 route
|
||||
const navigateTo =
|
||||
decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute : '/app/list';
|
||||
decodeLastRoute && !decodeLastRoute.includes('/login')
|
||||
? decodeLastRoute
|
||||
: '/dashboard/apps';
|
||||
router.push(navigateTo);
|
||||
},
|
||||
[setUserInfo, lastRoute, router]
|
||||
@@ -129,7 +131,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
|
||||
|
||||
useMount(() => {
|
||||
clearToken();
|
||||
router.prefetch('/app/list');
|
||||
router.prefetch('/dashboard/apps');
|
||||
|
||||
ChineseRedirectUrl && showRedirect && checkIpInChina();
|
||||
localCookieVersion !== cookieVersion && onOpenCookiesDrawer();
|
||||
|
||||
@@ -26,7 +26,9 @@ const provider = () => {
|
||||
(res: ResLogin) => {
|
||||
setUserInfo(res.user);
|
||||
|
||||
router.push(loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/app/list');
|
||||
router.push(
|
||||
loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/dashboard/apps'
|
||||
);
|
||||
},
|
||||
[setUserInfo, router, loginStore?.lastRoute]
|
||||
);
|
||||
@@ -95,7 +97,7 @@ const provider = () => {
|
||||
|
||||
(async () => {
|
||||
await clearToken();
|
||||
router.prefetch('/app/list');
|
||||
router.prefetch('/dashboard/apps');
|
||||
|
||||
if (loginStore && loginStore.provider !== 'sso' && state !== loginStore.state) {
|
||||
toast({
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import { getPluginGroups, getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import { Box, Flex, Grid, useDisclosure } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useMemo, useState } from 'react';
|
||||
import PluginCard from '@/pageComponents/toolkit/PluginCard';
|
||||
import { i18nT } from '@fastgpt/web/i18n/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import { navbarWidth } from '@/components/Layout';
|
||||
|
||||
const Toolkit = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const { data: plugins = [] } = useRequest2(getSystemPlugTemplates, {
|
||||
manual: false
|
||||
});
|
||||
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
|
||||
manual: false
|
||||
});
|
||||
const isOneGroup = pluginGroups.length === 1;
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const { group: selectedGroup = pluginGroups?.[0]?.groupId, type: selectedType = 'all' } =
|
||||
router.query;
|
||||
|
||||
const pluginGroupTypes = useMemo(() => {
|
||||
const allTypes = [
|
||||
{
|
||||
typeId: 'all',
|
||||
typeName: i18nT('common:common.All')
|
||||
}
|
||||
];
|
||||
const currentTypes =
|
||||
pluginGroups?.find((group) => group.groupId === selectedGroup)?.groupTypes ?? [];
|
||||
|
||||
return [
|
||||
...allTypes,
|
||||
...currentTypes.filter((type) =>
|
||||
plugins.find((plugin) => plugin.templateType === type.typeId)
|
||||
)
|
||||
];
|
||||
}, [pluginGroups, plugins, selectedGroup]);
|
||||
|
||||
const currentPlugins = useMemo(() => {
|
||||
const typeArray = pluginGroupTypes?.map((type) => type.typeId);
|
||||
return plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
(selectedType === 'all' && typeArray?.includes(plugin.templateType)) ||
|
||||
selectedType === plugin.templateType
|
||||
)
|
||||
.filter((plugin) => {
|
||||
const str = `${plugin.name}${plugin.intro}${plugin.instructions}`;
|
||||
const regx = new RegExp(search, 'gi');
|
||||
return regx.test(str);
|
||||
});
|
||||
}, [pluginGroupTypes, plugins, selectedType, search]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'} overflow={'auto'}>
|
||||
{/* Mask */}
|
||||
{!isPc && isOpen && (
|
||||
<Box
|
||||
position="fixed"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="blackAlpha.600"
|
||||
onClick={onClose}
|
||||
zIndex={99}
|
||||
/>
|
||||
)}
|
||||
{/* Sidebar */}
|
||||
{(isPc || isOpen) && (
|
||||
<Box
|
||||
position={'fixed'}
|
||||
left={isPc ? navbarWidth : 0}
|
||||
top={0}
|
||||
bg={'myGray.25'}
|
||||
w={['60vw', '200px']}
|
||||
h={'full'}
|
||||
borderLeft={'1px solid'}
|
||||
borderRight={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
pt={4}
|
||||
px={2.5}
|
||||
pb={2.5}
|
||||
zIndex={100}
|
||||
userSelect={'none'}
|
||||
>
|
||||
{pluginGroups.map((group) => {
|
||||
const selected = group.groupId === selectedGroup;
|
||||
return (
|
||||
<Box key={group.groupId}>
|
||||
<Flex
|
||||
p={2}
|
||||
mb={0.5}
|
||||
fontSize={'sm'}
|
||||
rounded={'md'}
|
||||
color={'myGray.900'}
|
||||
{...(!isOneGroup && {
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'primary.50'
|
||||
},
|
||||
onClick: () => {
|
||||
router.push({
|
||||
query: { group: group.groupId, type: 'all' }
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Avatar src={group.groupAvatar} w={'1rem'} mr={1.5} color={'primary.600'} />
|
||||
<Box>{t(group.groupName as any)}</Box>
|
||||
<Box flex={1} />
|
||||
{!isOneGroup && (
|
||||
<MyIcon
|
||||
color={'myGray.600'}
|
||||
name={selected ? 'core/chat/chevronDown' : 'core/chat/chevronUp'}
|
||||
w={'1rem'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{/* group types */}
|
||||
{selected &&
|
||||
pluginGroupTypes.map((type) => {
|
||||
return (
|
||||
<Flex
|
||||
key={type.typeId}
|
||||
fontSize={'14px'}
|
||||
fontWeight={500}
|
||||
rounded={'md'}
|
||||
py={2}
|
||||
pl={'30px'}
|
||||
cursor={'pointer'}
|
||||
mb={0.5}
|
||||
_hover={{ bg: 'primary.50' }}
|
||||
{...(type.typeId === selectedType
|
||||
? {
|
||||
bg: 'primary.50',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
bg: 'transparent',
|
||||
color: 'myGray.500'
|
||||
})}
|
||||
onClick={() => {
|
||||
router.push({
|
||||
query: { group: selectedGroup, type: type.typeId }
|
||||
});
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t(type.typeName as any)}
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Box ml={[0, '200px']} p={[5, 6]}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Flex flex={1} fontSize={'xl'} fontWeight={'medium'} color={'myGray.900'}>
|
||||
{isPc ? (
|
||||
<Box>
|
||||
{t(
|
||||
pluginGroups?.find((group) => group.groupId === selectedGroup)?.groupName as any
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<MyIcon name="menu" w={'20px'} mr={1.5} onClick={onOpen} />
|
||||
)}
|
||||
</Flex>
|
||||
<Box w={['60vw', '260px']}>
|
||||
<SearchInput
|
||||
value={search}
|
||||
bg={'white'}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder={t('common:plugin.Search plugin')}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
'1fr',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={4}
|
||||
alignItems={'stretch'}
|
||||
py={5}
|
||||
>
|
||||
{currentPlugins.map((item) => (
|
||||
<PluginCard key={item.id} item={item} groups={pluginGroups} />
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toolkit;
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(context, ['app', 'user']))
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
getWorkflowEntryNodeIds,
|
||||
initWorkflowEdgeStatus,
|
||||
storeEdges2RuntimeEdges,
|
||||
storeNodes2RuntimeNodes
|
||||
} from '@fastgpt/global/core/workflow/runtime/utils';
|
||||
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
|
||||
@@ -79,7 +79,7 @@ export const getScheduleTriggerApp = async () => {
|
||||
},
|
||||
uid: String(app.tmbId),
|
||||
runtimeNodes: storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)),
|
||||
runtimeEdges: initWorkflowEdgeStatus(edges),
|
||||
runtimeEdges: storeEdges2RuntimeEdges(edges),
|
||||
variables: {},
|
||||
query: userQuery,
|
||||
chatConfig,
|
||||
|
||||
@@ -77,7 +77,7 @@ export const useSystemStore = create<State>()(
|
||||
state.initd = true;
|
||||
});
|
||||
},
|
||||
lastRoute: '/app/list',
|
||||
lastRoute: '/dashboard/apps',
|
||||
setLastRoute(e) {
|
||||
set((state) => {
|
||||
state.lastRoute = e;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CreateAppBody } from '@/pages/api/core/app/create';
|
||||
import type { ListAppBody } from '@/pages/api/core/app/list';
|
||||
import type { AppLogsListItemType } from '@/types/app';
|
||||
import type { PaginationResponse } from '@fastgpt/web/common/fetch/type';
|
||||
import type { getBasicInfoResponse } from '@/pages/api/core/app/getBasicInfo';
|
||||
|
||||
/**
|
||||
* 获取应用列表
|
||||
@@ -37,6 +38,12 @@ export const getAppDetailById = (id: string) => GET<AppDetailType>(`/core/app/de
|
||||
export const putAppById = (id: string, data: AppUpdateParams) =>
|
||||
PUT(`/core/app/update?appId=${id}`, data);
|
||||
|
||||
/**
|
||||
* Get app basic info by ids
|
||||
*/
|
||||
export const getAppBasicInfoByIds = (ids: string[]) =>
|
||||
POST<getBasicInfoResponse>(`/core/app/getBasicInfo`, { ids });
|
||||
|
||||
// =================== chat logs
|
||||
export const getAppChatLogs = (data: GetAppChatLogsParams) =>
|
||||
POST<PaginationResponse<AppLogsListItemType>>(`/core/app/getChatLogs`, data, { maxQuantity: 1 });
|
||||
|
||||
@@ -13,13 +13,17 @@ import type { GetPreviewNodeQuery } from '@/pages/api/core/app/plugin/getPreview
|
||||
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/plugin/getSystemPluginTemplates';
|
||||
import type { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { defaultGroup } from '@fastgpt/web/core/workflow/constants';
|
||||
import { createMCPToolsBody } from '@/pages/api/core/app/mcpTools/create';
|
||||
import { ToolType } from '@fastgpt/global/core/app/type';
|
||||
import { getMCPToolsBody } from '@/pages/api/core/app/mcpTools/getMCPTools';
|
||||
import { RunToolTestBody } from '@/pages/api/core/app/mcpTools/runTest';
|
||||
import { updateMCPToolsBody } from '@/pages/api/core/app/mcpTools/update';
|
||||
|
||||
/* ============ team plugin ============== */
|
||||
export const getTeamPlugTemplates = (data?: ListAppBody) =>
|
||||
@@ -28,12 +32,17 @@ export const getTeamPlugTemplates = (data?: ListAppBody) =>
|
||||
tmbId: app.tmbId,
|
||||
id: app._id,
|
||||
pluginId: app._id,
|
||||
isFolder: app.type === AppTypeEnum.folder || app.type === AppTypeEnum.httpPlugin,
|
||||
isFolder:
|
||||
app.type === AppTypeEnum.folder ||
|
||||
app.type === AppTypeEnum.httpPlugin ||
|
||||
app.type === AppTypeEnum.toolSet,
|
||||
templateType: FlowNodeTemplateTypeEnum.teamApp,
|
||||
flowNodeType:
|
||||
app.type === AppTypeEnum.workflow
|
||||
? FlowNodeTypeEnum.appModule
|
||||
: FlowNodeTypeEnum.pluginModule,
|
||||
: app.type === AppTypeEnum.toolSet
|
||||
? FlowNodeTypeEnum.toolSet
|
||||
: FlowNodeTypeEnum.pluginModule,
|
||||
avatar: app.avatar,
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
@@ -62,6 +71,18 @@ export const getSystemPluginPaths = (data: GetPathProps) => {
|
||||
export const getPreviewPluginNode = (data: GetPreviewNodeQuery) =>
|
||||
GET<FlowNodeTemplateType>('/core/app/plugin/getPreviewNode', data);
|
||||
|
||||
/* ============ mcp tools ============== */
|
||||
export const getMCPTools = (data: getMCPToolsBody) =>
|
||||
POST<ToolType[]>('/core/app/mcpTools/getMCPTools', data);
|
||||
|
||||
export const postCreateMCPTools = (data: createMCPToolsBody) =>
|
||||
POST('/core/app/mcpTools/create', data);
|
||||
|
||||
export const postUpdateMCPTools = (data: updateMCPToolsBody) =>
|
||||
POST('/core/app/mcpTools/update', data);
|
||||
|
||||
export const postRunMCPTools = (data: RunToolTestBody) => POST('/core/app/mcpTools/runTest', data);
|
||||
|
||||
/* ============ http plugin ============== */
|
||||
export const postCreateHttpPlugin = (data: createHttpPluginBody) =>
|
||||
POST('/core/app/httpPlugin/create', data);
|
||||
|
||||
20
projects/app/src/web/support/mcp/api.ts
Normal file
20
projects/app/src/web/support/mcp/api.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { updateBody } from '@/pages/api/support/mcp/update';
|
||||
import { GET, POST, DELETE, PUT } from '../../common/api/request';
|
||||
import type { createBody } from '@/pages/api/support/mcp/create';
|
||||
import type { listResponse } from '@/pages/api/support/mcp/list';
|
||||
|
||||
export const getMcpServerList = () => {
|
||||
return GET<listResponse>('/support/mcp/list');
|
||||
};
|
||||
|
||||
export const postCreateMcpServer = (data: createBody) => {
|
||||
return POST('/support/mcp/create', data);
|
||||
};
|
||||
|
||||
export const putUpdateMcpServer = (data: updateBody) => {
|
||||
return PUT('/support/mcp/update', data);
|
||||
};
|
||||
|
||||
export const deleteMcpServer = (id: string) => {
|
||||
return DELETE(`/support/mcp/delete`, { id });
|
||||
};
|
||||
Reference in New Issue
Block a user