4.7-alpha2 (#1027)

* feat: stop toolCall and rename some field. (#46)

* perf: node delete tip;pay tip

* fix: toolCall cannot save child answer

* feat: stop tool

* fix: team modal

* fix feckbackMoal  auth bug (#47)

* 简单的支持提示词运行tool。优化workflow模板 (#49)

* remove templates

* fix: request body undefined

* feat: prompt tool run

* feat: workflow tamplates modal

* perf: plugin start

* 4.7 (#50)

* fix docker-compose download url (#994)

original code is a bad url with '404 NOT FOUND' return.
fix docker-compose download url, add 'v' before docker-compose version

* Update ai_settings.md (#1000)

* Update configuration.md

* Update configuration.md

* Fix history in classifyQuestion and extract modules (#1012)

* Fix history in classifyQuestion and extract modules

* Add chatValue2RuntimePrompt import and update text formatting

* flow controller to packages

* fix: rerank select

* modal ui

* perf: modal code path

* point not sufficient

* feat: http url support variable

* fix http key

* perf: prompt

* perf: ai setting modal

* simple edit ui

---------

Co-authored-by: entorick <entorick11@qq.com>
Co-authored-by: liujianglc <liujianglc@163.com>
Co-authored-by: Fengrui Liu <liufengrui.work@bytedance.com>

* fix team share redirect to login (#51)

* feat: support openapi import plugins (#48)

* feat: support openapi import plugins

* feat: import from url

* fix: add body params parse

* fix build

* fix

* fix

* fix

* tool box ui (#52)

* fix: training queue

* feat: simple edit tool select

* perf: simple edit dataset prompt

* fix: chatbox tool ux

* feat: quote prompt module

* perf: plugin tools sign

* perf: model avatar

* tool selector ui

* feat: max histories

* perf: http plugin import (#53)

* perf: plugin http import

* chatBox ui

* perf: name

* fix: Node template card (#54)

* fix: ts

* setting modal

* package

* package

* feat: add plugins search (#57)

* feat: add plugins search

* perf: change http plugin header input

* Yjl (#56)

* perf: prompt tool call

* perf: chat box ux

* doc

* doc

* price tip

* perf: tool selector

* ui'

* fix: vector queue

* fix: empty tool and empty response

* fix: empty msg

* perf: pg index

* perf: ui tip

* doc

* tool tip

---------

Co-authored-by: yst <77910600+yu-and-liu@users.noreply.github.com>
Co-authored-by: entorick <entorick11@qq.com>
Co-authored-by: liujianglc <liujianglc@163.com>
Co-authored-by: Fengrui Liu <liufengrui.work@bytedance.com>
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-03-21 13:32:31 +08:00
committed by GitHub
parent 6d4b331db9
commit 9d27de154b
322 changed files with 9282 additions and 6498 deletions

View File

@@ -13,6 +13,7 @@ import { putUpdatePlugin } from '@/web/core/plugin/api';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { ModuleItemType } from '@fastgpt/global/core/module/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
const ImportSettings = dynamic(() => import('@/components/core/module/Flow/ImportSettings'));
const PreviewPlugin = dynamic(() => import('./Preview'));
@@ -41,7 +42,12 @@ const Header = ({ plugin, onClose }: Props) => {
item.inputs.forEach((item) => {
item.connected = true;
});
if (item.outputs.find((output) => output.targets.length === 0)) {
if (
item.outputs.find(
(output) =>
output.key !== ModuleOutputKeyEnum.pluginStart && output.targets.length === 0
)
) {
toast({
status: 'warning',
title: t('module.Plugin input must connect')

View File

@@ -4,7 +4,7 @@ import { FlowModuleItemType, ModuleItemType } from '@fastgpt/global/core/module/
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import dynamic from 'next/dynamic';
import { plugin2ModuleIO } from '@fastgpt/global/core/module/utils';
import MyModal from '@/components/MyModal';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { PluginItemSchema } from '@fastgpt/global/core/plugin/type';

View File

@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
import Header from './Header';
import Flow from '@/components/core/module/Flow';
import FlowProvider, { useFlowProviderStore } from '@/components/core/module/Flow/FlowProvider';
import { FlowModuleTemplateType } from '@fastgpt/global/core/module/type.d';
import { FlowNodeTemplateType } from '@fastgpt/global/core/module/type.d';
import { pluginSystemModuleTemplates } from '@fastgpt/global/core/module/template/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/module/node/constant';
import { serviceSideProps } from '@/web/common/utils/i18n';
@@ -13,7 +13,7 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { usePluginStore } from '@/web/core/plugin/store/plugin';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
type Props = { pluginId: string };
@@ -22,13 +22,30 @@ const Render = ({ pluginId }: Props) => {
const router = useRouter();
const { toast } = useToast();
const { nodes, initData } = useFlowProviderStore();
const { pluginModuleTemplates, loadPluginTemplates } = usePluginStore();
const { setBasicNodeTemplates } = useWorkflowStore();
const moduleTemplates = useMemo(() => {
const pluginTemplates = pluginModuleTemplates.filter((item) => item.id !== pluginId);
const concatTemplates = [...pluginSystemModuleTemplates, ...pluginTemplates];
const { data: pluginDetail } = useQuery(
['getOnePlugin', pluginId],
() => getOnePlugin(pluginId),
{
onError: (error) => {
toast({
status: 'warning',
title: getErrText(error, t('plugin.Load Plugin Failed'))
});
router.replace('/plugin/list');
}
}
);
const copyTemplates: FlowModuleTemplateType[] = JSON.parse(JSON.stringify(concatTemplates));
useEffect(() => {
initData(JSON.parse(JSON.stringify(pluginDetail?.modules || [])));
}, [pluginDetail?.modules]);
useEffect(() => {
const concatTemplates = [...pluginSystemModuleTemplates];
const copyTemplates: FlowNodeTemplateType[] = JSON.parse(JSON.stringify(concatTemplates));
const filterType: Record<string, 1> = {
[FlowNodeTypeEnum.userGuide]: 1,
@@ -52,34 +69,11 @@ const Render = ({ pluginId }: Props) => {
template.inputs = template.inputs.filter((input) => !input.hideInPlugin);
});
return copyTemplates;
}, [nodes, pluginId, pluginModuleTemplates]);
const { data: pluginDetail } = useQuery(
['getOnePlugin', pluginId],
() => getOnePlugin(pluginId),
{
onError: (error) => {
toast({
status: 'warning',
title: getErrText(error, t('plugin.Load Plugin Failed'))
});
router.replace('/plugin/list');
}
}
);
useQuery(['getPlugTemplates'], () => loadPluginTemplates());
useEffect(() => {
initData(JSON.parse(JSON.stringify(pluginDetail?.modules || [])));
}, [pluginDetail?.modules]);
setBasicNodeTemplates(copyTemplates);
}, [nodes, setBasicNodeTemplates]);
return pluginDetail ? (
<Flow
templates={moduleTemplates}
Header={<Header plugin={pluginDetail} onClose={() => router.back()} />}
/>
<Flow Header={<Header plugin={pluginDetail} onClose={() => router.back()} />} />
) : (
<Loading />
);

View File

@@ -11,22 +11,23 @@ import { useRequest } from '@/web/common/hooks/useRequest';
import { delOnePlugin, postCreatePlugin, putUpdatePlugin } from '@/web/core/plugin/api';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import MyModal from '@/components/MyModal';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { customAlphabet } from 'nanoid';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import { useWorkflowStore } from '@/web/core/workflow/store/workflow';
import { EditFormType } from './type';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
export type FormType = CreateOnePluginParams & {
id?: string;
};
export const defaultForm: FormType = {
export const defaultForm: EditFormType = {
avatar: '/icon/logo.svg',
name: '',
intro: '',
parentId: null,
type: PluginTypeEnum.custom,
modules: [
{
moduleId: nanoid(),
@@ -63,7 +64,7 @@ const CreateModal = ({
onSuccess,
onDelete
}: {
defaultValue?: FormType;
defaultValue?: EditFormType;
onClose: () => void;
onSuccess: () => void;
onDelete: () => void;
@@ -72,14 +73,17 @@ const CreateModal = ({
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const router = useRouter();
const { parentId } = router.query as { parentId: string };
const { isPc } = useSystemStore();
const { loadTeamPluginNodeTemplates } = useWorkflowStore();
const { openConfirm, ConfirmModal } = useConfirm({
title: t('common.Delete Tip'),
content: t('plugin.Confirm Delete')
});
const { register, setValue, getValues, handleSubmit } = useForm<FormType>({
defaultValues: defaultValue
const { register, setValue, getValues, handleSubmit } = useForm<EditFormType>({
defaultValues: { ...defaultValue, parentId: parentId || null }
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
@@ -111,19 +115,20 @@ const CreateModal = ({
);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (data: FormType) => {
mutationFn: async (data: EditFormType) => {
return postCreatePlugin(data);
},
onSuccess(id: string) {
router.push(`/plugin/edit?pluginId=${id}`);
onSuccess();
onClose();
loadTeamPluginNodeTemplates();
},
successToast: t('common.Create Success'),
errorToast: t('common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: FormType) => {
mutationFn: async (data: EditFormType) => {
if (!data.id) return Promise.resolve('');
// @ts-ignore
return putUpdatePlugin(data);
@@ -163,34 +168,41 @@ const CreateModal = ({
isCentered={!isPc}
>
<ModalBody>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('plugin.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
<>
<Box color={'myGray.800'} fontWeight={'bold'}>
{t('plugin.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}
borderRadius={'md'}
onClick={onOpenSelectFile}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
autoFocus={!defaultValue.id}
bg={'myWhite.600'}
{...register('name', {
required: t("common.Name Can't Be Empty")
})}
/>
</MyTooltip>
<Input
flex={1}
ml={4}
autoFocus={!defaultValue.id}
bg={'myWhite.600'}
{...register('name', {
required: t("common.Name Can't Be Empty")
})}
/>
</Flex>
</Flex>
</>
<Box mt={3}>
<Box mb={1}>{t('plugin.Intro')}</Box>
<Textarea {...register('intro')} bg={'myWhite.600'} rows={5} />
<Textarea
{...register('intro')}
bg={'myWhite.600'}
rows={5}
placeholder={t('core.plugin.Intro placeholder')}
/>
</Box>
</ModalBody>

View File

@@ -0,0 +1,539 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Box,
Flex,
Button,
ModalBody,
Input,
Textarea,
TableContainer,
Table,
Thead,
Th,
Tbody,
Tr,
Td,
IconButton,
useDisclosure
} from '@chakra-ui/react';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRequest } from '@/web/common/hooks/useRequest';
import Avatar from '@/components/Avatar';
import MyTooltip from '@/components/MyTooltip';
import { useTranslation } from 'next-i18next';
import { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import {
delOnePlugin,
getApiSchemaByUrl,
postCreatePlugin,
putUpdatePlugin
} from '@/web/core/plugin/api';
import { str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useConfirm } from '@/web/common/hooks/useConfirm';
import { AddIcon } from '@chakra-ui/icons';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { EditFormType } from './type';
import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import { HttpHeaders } from '@/components/core/module/Flow/components/nodes/NodeHttp';
export const defaultHttpPlugin: CreateOnePluginParams = {
avatar: FolderImgUrl,
name: '',
intro: '',
parentId: null,
type: PluginTypeEnum.folder,
modules: [],
metadata: {
apiSchemaStr: '',
customHeaders: ''
}
};
const HttpPluginEditModal = ({
defaultPlugin = defaultHttpPlugin,
onClose,
onSuccess,
onDelete
}: {
defaultPlugin?: EditFormType;
onClose: () => void;
onSuccess: () => void;
onDelete: () => void;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const isEdit = !!defaultPlugin.id;
const [refresh, setRefresh] = useState(false);
const [schemaUrl, setSchemaUrl] = useState('');
const [customHeaders, setCustomHeaders] = useState<{ key: string; value: string }[]>(() => {
const keyValue = JSON.parse(defaultPlugin.metadata?.customHeaders || '{}');
return Object.keys(keyValue).map((key) => ({ key, value: keyValue[key] }));
});
const [updateTrigger, setUpdateTrigger] = useState(false);
const { register, setValue, getValues, handleSubmit, watch } = useForm<CreateOnePluginParams>({
defaultValues: defaultPlugin
});
const apiSchemaStr = watch('metadata.apiSchemaStr');
const apiData = useMemo(() => {
if (!apiSchemaStr) {
return { pathData: [], serverPath: '' };
}
try {
return str2OpenApiSchema(apiSchemaStr);
} catch (err) {
toast({
status: 'warning',
title: t('plugin.Invalid Schema')
});
return { pathData: [], serverPath: '' };
}
}, [apiSchemaStr, t, toast]);
const {
isOpen: isOpenUrlImport,
onOpen: onOpenUrlImport,
onClose: onCloseUrlImport
} = useDisclosure();
const { mutate: onCreate, isLoading: isCreating } = useRequest({
mutationFn: async (data: CreateOnePluginParams) => {
return postCreatePlugin(data);
},
onSuccess() {
onSuccess();
onClose();
},
successToast: t('common.Create Success'),
errorToast: t('common.Create Failed')
});
const { mutate: updatePlugins, isLoading: isUpdating } = useRequest({
mutationFn: async (data: EditFormType) => {
if (!data.id) return Promise.resolve('');
return putUpdatePlugin({
id: data.id,
name: data.name,
avatar: data.avatar,
intro: data.intro,
metadata: data.metadata
});
},
onSuccess() {
onClose();
onSuccess();
},
successToast: t('common.Update Success'),
errorToast: t('common.Update Failed')
});
const { openConfirm, ConfirmModal } = useConfirm({
title: t('common.Delete Tip'),
content: t('core.plugin.Delete http plugin')
});
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: 'image/*',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.pluginAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.Select File Failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
const { mutate: onclickDelPlugin, isLoading: isDeleting } = useRequest({
mutationFn: async () => {
if (!defaultPlugin.id) return;
await delOnePlugin(defaultPlugin.id);
onDelete();
onClose();
},
successToast: t('common.Delete Success'),
errorToast: t('common.Delete Failed')
});
/* load api from url */
const { mutate: onClickUrlLoadApi, isLoading: isLoadingUrlApi } = useRequest({
mutationFn: async () => {
if (!schemaUrl || !schemaUrl.startsWith('https://')) {
return toast({
title: t('plugin.Invalid URL'),
status: 'warning'
});
}
const schema = await getApiSchemaByUrl(schemaUrl);
setValue('metadata.apiSchemaStr', JSON.stringify(schema, null, 2));
onCloseUrlImport();
},
errorToast: t('plugin.Invalid Schema')
});
const leftVariables = useMemo(
() =>
HttpHeaders.filter((variable) => {
const existVariables = customHeaders.map((item) => item.key);
return !existVariables.includes(variable.key);
}),
[customHeaders]
);
return (
<>
<MyModal
isOpen
onClose={onClose}
iconSrc="/imgs/module/http.png"
title={isEdit ? t('plugin.Edit Http Plugin') : t('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('plugin.Set Name')}
</Box>
<Flex mt={3} alignItems={'center'}>
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('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.Name Can't Be Empty")
})}
/>
</Flex>
</>
<>
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
{t('plugin.Intro')}
</Box>
<Textarea {...register('intro')} bg={'myWhite.600'} rows={3} mt={3} />
</>
<Box mt={4}>
<Box
color={'myGray.800'}
fontWeight={'bold'}
justifyContent={'space-between'}
display={'flex'}
>
<Box my={'auto'}>{'OpenAPI Schema'}</Box>
<Box>
{isOpenUrlImport ? (
<Flex alignItems={'center'}>
<Input
mr={2}
placeholder={'https://...'}
h={'30px'}
onBlur={(e) => setSchemaUrl(e.target.value)}
/>
<Button size={'sm'} isLoading={isLoadingUrlApi} onClick={onClickUrlLoadApi}>
{t('common.Confirm')}
</Button>
<Button ml={2} variant={'whiteBase'} size={'sm'} onClick={onCloseUrlImport}>
{t('common.Cancel')}
</Button>
</Flex>
) : (
<Button
variant={'whiteBase'}
size={'sm'}
fontSize={'xs'}
leftIcon={<AddIcon fontSize={'xs'} />}
onClick={onOpenUrlImport}
>
{t('plugin.Import from URL')}
</Button>
)}
</Box>
</Box>
<Textarea
{...register('metadata.apiSchemaStr')}
bg={'myWhite.600'}
rows={10}
mt={3}
onBlur={(e) => {
const content = e.target.value;
if (!content) return;
setValue('metadata.apiSchemaStr', content);
}}
/>
</Box>
<>
<Box color={'myGray.800'} fontWeight={'bold'} mt={3}>
{t('core.plugin.Custom headers')}
</Box>
<Box mt={1}>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
<Tr>
<Th px={2}>{t('core.module.http.Props name')}</Th>
<Th px={2}>{t('core.module.http.Props value')}</Th>
</Tr>
</Thead>
<Tbody>
{customHeaders.map((item, index) => (
<Tr key={`${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={true}
setDropdownValue={(val) => {
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, key: val } : item
);
setValue(
'metadata.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, key: val } : item
);
setValue(
'metadata.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('core.module.http.Props value')}
hasVariablePlugin={false}
value={item.value}
onBlur={(val) =>
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, value: val } : item
);
setValue(
'metadata.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(
'metadata.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
})
}
/>
</Box>
</Td>
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={true}
setDropdownValue={(val) => {
setCustomHeaders((prev) => {
const newHeaders = [...prev, { key: val, value: '' }];
setValue(
'metadata.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('core.module.http.Add props')}
value={''}
variables={leftVariables}
updateTrigger={updateTrigger}
onBlur={(val) => {
if (!val) return;
setCustomHeaders((prev) => {
const newHeaders = [...prev, { key: val, value: '' }];
setValue(
'metadata.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('plugin.Plugin List')}
</Box>
<TableContainer maxH={400} overflowY={'auto'} mt={3}>
<Table border={'1px solid'} borderColor={'myGray.200'}>
<Thead>
<Th>{t('Name')}</Th>
<Th>{t('plugin.Description')}</Th>
<Th>{t('plugin.Method')}</Th>
<Th>{t('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>
</>
</ModalBody>
<Flex px={5} py={4} alignItems={'center'}>
{isEdit && (
<IconButton
className="delete"
size={'xsSquare'}
icon={<MyIcon name={'delete'} w={'14px'} />}
variant={'whiteDanger'}
aria-label={'delete'}
_hover={{
bg: 'red.100'
}}
isLoading={isDeleting}
onClick={(e) => {
e.stopPropagation();
openConfirm(onclickDelPlugin)();
}}
/>
)}
<Box flex={1} />
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
{t('common.Close')}
</Button>
{!isEdit ? (
<Button onClick={handleSubmit((data) => onCreate(data))} isLoading={isCreating}>
{t('common.Confirm Create')}
</Button>
) : (
<Button isLoading={isUpdating} onClick={handleSubmit((data) => updatePlugins(data))}>
{t('common.Confirm Update')}
</Button>
)}
</Flex>
</MyModal>
<File onSelect={onSelectFile} />
<ConfirmModal />
</>
);
};
export default HttpPluginEditModal;

View File

@@ -0,0 +1,5 @@
import { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
export type EditFormType = CreateOnePluginParams & {
id?: string;
};

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Box, Grid, Card, useTheme, Flex, IconButton, Button, Image } from '@chakra-ui/react';
import { Box, Grid, useTheme, Flex, IconButton, Button, Image } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import { AddIcon } from '@chakra-ui/icons';
@@ -9,44 +9,102 @@ import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import PageContainer from '@/components/PageContainer';
import Avatar from '@/components/Avatar';
import EditModal, { defaultForm, FormType } from './component/EditModal';
import { getUserPlugins } from '@/web/core/plugin/api';
import EditModal, { defaultForm } from './component/EditModal';
import { getPluginPaths, getUserPlugins } from '@/web/core/plugin/api';
import EmptyTip from '@/components/EmptyTip';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyMenu from '@/components/MyMenu';
import HttpPluginEditModal, { defaultHttpPlugin } from './component/HttpPluginEditModal';
import { PluginTypeEnum } from '@fastgpt/global/core/plugin/constants';
import ParentPaths from '@/components/common/ParentPaths';
import { EditFormType } from './component/type';
const MyModules = () => {
const TeamPlugins = () => {
const { t } = useTranslation();
const theme = useTheme();
const { userInfo } = useUserStore();
const router = useRouter();
const [editModalData, setEditModalData] = useState<FormType>();
const { parentId } = router.query as { parentId: string };
const [editModalData, setEditModalData] = useState<EditFormType>();
const [httpPluginEditModalData, setHttpPluginModalData] = useState<EditFormType>();
/* load plugins */
const {
data = [],
isLoading,
refetch
} = useQuery(['loadModules'], () => getUserPlugins(), {
refetchOnMount: true
});
} = useQuery(
['loadModules', parentId],
() => {
return Promise.all([
getUserPlugins({ parentId: parentId === undefined ? '' : parentId }),
getPluginPaths(parentId)
]);
},
{
refetchOnMount: true
}
);
const paths = data?.[1] || [];
const plugins = data?.[0] || [];
return (
<PageContainer isLoading={isLoading} insertProps={{ px: [5, '48px'] }}>
<Flex pt={[4, '30px']} alignItems={'center'} justifyContent={'space-between'}>
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={['20px', '24px']} fontWeight={'bold'}>
{t('plugin.My Plugins')}({t('common.Beta')})
</Box>
</Flex>
<ParentPaths
paths={paths.map((path, i) => ({
parentId: path.parentId,
parentName: path.parentName
}))}
FirstPathDom={
<Flex flex={1} alignItems={'center'}>
<Image src={'/imgs/module/plugin.svg'} alt={''} mr={2} h={'24px'} />
<Box className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
{t('plugin.My Plugins')}({t('common.Beta')})
</Box>
</Flex>
}
onClick={(e) => {
router.push({
query: {
parentId: e
}
});
}}
/>
{userInfo?.team?.canWrite && (
<Button
leftIcon={<AddIcon />}
variant={'primaryOutline'}
onClick={() => setEditModalData(defaultForm)}
>
{t('common.New Create')}
</Button>
<MyMenu
offset={[-30, 5]}
width={120}
Button={
<Button variant={'primaryOutline'} px={0}>
<Flex alignItems={'center'} px={'20px'}>
<AddIcon mr={2} />
<Box>{t('common.Create New')}</Box>
</Flex>
</Button>
}
menuList={[
{
label: (
<Flex>
<Image src={'/imgs/module/plugin.svg'} alt={''} w={'18px'} mr={1} />
{t('plugin.Custom Plugin')}
</Flex>
),
onClick: () => setEditModalData(defaultForm)
},
{
label: (
<Flex display={'flex'} alignItems={'center'}>
<Image src={'/imgs/module/http.png'} alt={''} w={'18px'} h={'14px'} mr={1} />
{t('plugin.HTTP Plugin')}
</Flex>
),
onClick: () => setHttpPluginModalData(defaultHttpPlugin)
}
]}
/>
)}
</Flex>
<Grid
@@ -54,7 +112,7 @@ const MyModules = () => {
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
gridGap={5}
>
{data.map((plugin) => (
{plugins.map((plugin) => (
<Box
key={plugin._id}
py={3}
@@ -74,7 +132,22 @@ const MyModules = () => {
display: 'flex'
}
}}
onClick={() => router.push(`/plugin/edit?pluginId=${plugin._id}`)}
onClick={() => {
if (plugin.type === PluginTypeEnum.folder) {
router.push({
query: {
parentId: plugin._id
}
});
} else {
router.push({
pathname: '/plugin/edit',
query: {
pluginId: plugin._id
}
});
}
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={plugin.avatar} borderRadius={'md'} w={'28px'} />
@@ -94,12 +167,20 @@ const MyModules = () => {
}}
onClick={(e) => {
e.stopPropagation();
setEditModalData({
const data = {
id: plugin._id,
parentId: plugin.parentId,
name: plugin.name,
avatar: plugin.avatar,
intro: plugin.intro
});
intro: plugin.intro,
modules: [],
type: plugin.type,
metadata: plugin.metadata
};
if (plugin.type === PluginTypeEnum.folder) {
return setHttpPluginModalData(data);
}
setEditModalData(data);
}}
/>
</Flex>
@@ -124,6 +205,14 @@ const MyModules = () => {
onDelete={refetch}
/>
)}
{!!httpPluginEditModalData && (
<HttpPluginEditModal
defaultPlugin={httpPluginEditModalData}
onClose={() => setHttpPluginModalData(undefined)}
onSuccess={refetch}
onDelete={refetch}
/>
)}
</PageContainer>
);
};
@@ -136,4 +225,4 @@ export async function getServerSideProps(content: any) {
};
}
export default MyModules;
export default TeamPlugins;