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:
Archer
2025-04-16 22:18:51 +08:00
committed by GitHub
parent ab799e13cd
commit 952412f648
166 changed files with 6318 additions and 1263 deletions

View File

@@ -0,0 +1,469 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Box,
Flex,
Button,
ModalBody,
Input,
Textarea,
TableContainer,
Table,
Thead,
Th,
Tbody,
Tr,
Td,
ModalFooter
} from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
import { HttpPluginImgUrl } from '@fastgpt/global/common/file/image/constants';
import {
postCreateHttpPlugin,
putUpdateHttpPlugin,
getApiSchemaByUrl
} from '@/web/core/app/api/plugin';
import { str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpPlugin/type';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
export type EditHttpPluginProps = {
id?: string;
avatar: string;
name: string;
intro?: string;
pluginData?: AppSchema['pluginData'];
};
export const defaultHttpPlugin: EditHttpPluginProps = {
avatar: HttpPluginImgUrl,
name: '',
intro: '',
pluginData: {
apiSchemaStr: '',
customHeaders: '{"Authorization":"Bearer"}'
}
};
const HttpPluginEditModal = ({
defaultPlugin = defaultHttpPlugin,
onClose
}: {
defaultPlugin?: EditHttpPluginProps;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const isEdit = !!defaultPlugin.id;
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
const [schemaUrl, setSchemaUrl] = useState('');
const [customHeaders, setCustomHeaders] = useState<{ key: string; value: string }[]>(() => {
const keyValue = JSON.parse(defaultPlugin.pluginData?.customHeaders || '{}');
return Object.keys(keyValue).map((key) => ({ key, value: keyValue[key] }));
});
const [updateTrigger, setUpdateTrigger] = useState(false);
const { register, setValue, handleSubmit, watch } = useForm<EditHttpPluginProps>({
defaultValues: defaultPlugin
});
const avatar = watch('avatar');
const apiSchemaStr = watch('pluginData.apiSchemaStr');
const [apiData, setApiData] = useState<OpenApiJsonSchema>({ pathData: [], serverPath: '' });
const { mutate: onCreate, isLoading: isCreating } = useRequest({
mutationFn: async (data: EditHttpPluginProps) => {
return postCreateHttpPlugin({
parentId,
name: data.name,
intro: data.intro,
avatar: data.avatar,
pluginData: {
apiSchemaStr: data.pluginData?.apiSchemaStr,
customHeaders: data.pluginData?.customHeaders
}
});
},
onSuccess() {
loadMyApps();
onClose();
},
successToast: t('common:common.Create Success'),
errorToast: t('common:common.Create Failed')
});
const { mutate: updatePlugins, isLoading: isUpdating } = useRequest({
mutationFn: async (data: EditHttpPluginProps) => {
if (!data.id || !data.pluginData) return Promise.resolve('');
return putUpdateHttpPlugin({
appId: data.id,
name: data.name,
intro: data.intro,
avatar: data.avatar,
pluginData: data.pluginData
});
},
onSuccess() {
loadMyApps();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
});
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: 'image/*',
multiple: false
});
/* load api from url */
const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({
mutationFn: async () => {
if (!schemaUrl || (!schemaUrl.startsWith('https://') && !schemaUrl.startsWith('http://'))) {
return toast({
title: t('common:plugin.Invalid URL'),
status: 'warning'
});
}
const schema = await getApiSchemaByUrl(schemaUrl);
setValue('pluginData.apiSchemaStr', JSON.stringify(schema, null, 2));
},
errorToast: t('common:plugin.Invalid Schema')
});
useEffect(() => {
(async () => {
if (!apiSchemaStr) {
return setApiData({ pathData: [], serverPath: '' });
}
try {
setApiData(await str2OpenApiSchema(apiSchemaStr));
} catch (err) {
toast({
status: 'warning',
title: t('common:plugin.Invalid Schema')
});
setApiData({ pathData: [], serverPath: '' });
}
})();
}, [apiSchemaStr, t, toast]);
return (
<>
<MyModal
isOpen
onClose={onClose}
iconSrc="core/app/type/httpPluginFill"
title={isEdit ? t('common:plugin.Edit Http Plugin') : t('common:plugin.Import Plugin')}
w={['90vw', '600px']}
h={['90vh', '80vh']}
position={'relative'}
>
<ModalBody flex={'1 0 0'} overflow={'auto'}>
<>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('common:plugin.Set Name')}
</Box>
<Flex mt={3} 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.800'} fontWeight={'bold'} mt={3}>
{t('common:plugin.Intro')}
</Box>
<Textarea
{...register('intro')}
bg={'myWhite.600'}
rows={3}
mt={3}
placeholder={t('common:core.plugin.Http plugin intro placeholder')}
/>
</>
</>
{/* import */}
<Box mt={4}>
<Box
color={'myGray.800'}
fontWeight={'bold'}
justifyContent={'space-between'}
display={'flex'}
>
<Box my={'auto'}>{'OpenAPI Schema'}</Box>
<Box>
<Flex alignItems={'center'}>
<Input
mr={2}
placeholder={t('common:plugin.Import from URL')}
h={'30px'}
w={['150px', '250px']}
fontSize={'sm'}
onBlur={(e) => setSchemaUrl(e.target.value)}
/>
<Button
size={'sm'}
variant={'whitePrimary'}
isLoading={isLoadingUrlApi}
onClick={onClickUrlLoadApi}
>
{t('common:common.Import')}
</Button>
</Flex>
</Box>
</Box>
<Textarea
{...register('pluginData.apiSchemaStr')}
bg={'myWhite.600'}
rows={10}
mt={3}
onBlur={(e) => {
const content = e.target.value;
if (!content) return;
setValue('pluginData.apiSchemaStr', content);
}}
/>
</Box>
<>
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
{t('common:core.plugin.Custom headers')}
</Box>
<Box
mt={1}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom={'none'}
>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2} borderRadius="none !important">
{t('common:core.module.http.Props name')}
</Th>
<Th px={2} borderRadius="none !important">
{t('common:core.module.http.Props value')}
</Th>
</Tr>
</Thead>
<Tbody>
{customHeaders.map((item, index) => (
<Tr key={`${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
placeholder={t('common:core.module.http.Props name')}
value={item.key}
onBlur={(val) => {
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, key: val } : item
);
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('common:core.module.http.Props value')}
value={item.value}
onBlur={(val) =>
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, value: val } : item
);
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
})
}
/>
<MyIcon
name={'delete'}
cursor={'pointer'}
_hover={{ color: 'red.600' }}
w={'14px'}
onClick={() =>
setCustomHeaders((prev) => {
const newHeaders = prev.filter((val) => val.key !== item.key);
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
})
}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
placeholder={t('common:core.module.http.Add props')}
value={''}
updateTrigger={updateTrigger}
onBlur={(val) => {
if (!val) return;
setCustomHeaders((prev) => {
const newHeaders = [...prev, { key: val, value: '' }];
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
setUpdateTrigger((prev) => !prev);
}}
/>
</Td>
<Td p={0}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
</Box>
</>
<>
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
{t('common:plugin.Plugin List')}
</Box>
<Box
mt={3}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom={'none'}
>
<TableContainer maxH={400} overflowY={'auto'}>
<Table bg={'white'}>
<Thead bg={'myGray.50'}>
<Th>{t('common:Name')}</Th>
<Th>{t('common:plugin.Description')}</Th>
<Th>{t('common:plugin.Method')}</Th>
<Th>{t('common:plugin.Path')}</Th>
</Thead>
<Tbody>
{apiData.pathData?.map((item, index) => (
<Tr key={index}>
<Td>{item.name}</Td>
<Td
fontSize={'sm'}
textColor={'gray.600'}
w={'auto'}
maxW={80}
whiteSpace={'pre-wrap'}
>
{item.description}
</Td>
<Td>{item.method}</Td>
<Td>{item.path}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</>
</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common:common.Close')}
</Button>
{!isEdit ? (
<Button
isDisabled={apiData.pathData.length === 0}
onClick={handleSubmit((data) => onCreate(data))}
isLoading={isCreating}
>
{t('common:common.Confirm Create')}
</Button>
) : (
<Button
isDisabled={apiData.pathData.length === 0}
isLoading={isUpdating}
onClick={handleSubmit((data) => updatePlugins(data))}
>
{t('common:common.Confirm Update')}
</Button>
)}
</ModalFooter>
</MyModal>
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</>
);
};
export default HttpPluginEditModal;