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:
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);
|
||||
Reference in New Issue
Block a user