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:
@@ -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')
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
5
projects/app/src/pages/plugin/list/component/type.d.ts
vendored
Normal file
5
projects/app/src/pages/plugin/list/component/type.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
|
||||
|
||||
export type EditFormType = CreateOnePluginParams & {
|
||||
id?: string;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user