4.7.1-alpha (#1120)
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
@@ -214,9 +214,6 @@ export const FlowProvider = ({
|
||||
if (source?.flowType === FlowNodeTypeEnum.classifyQuestion && !type) {
|
||||
return ModuleIOValueTypeEnum.boolean;
|
||||
}
|
||||
if (source?.flowType === FlowNodeTypeEnum.tools) {
|
||||
return ModuleIOValueTypeEnum.tools;
|
||||
}
|
||||
if (source?.flowType === FlowNodeTypeEnum.pluginInput) {
|
||||
return source?.inputs.find((input) => input.key === connect.sourceHandle)?.valueType;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
const router = useRouter();
|
||||
const [currentParent, setCurrentParent] = useState<RenderListProps['currentParent']>();
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const {
|
||||
basicNodeTemplates,
|
||||
@@ -64,7 +65,12 @@ const ModuleTemplateList = ({ isOpen, onClose }: ModuleTemplateListProps) => {
|
||||
|
||||
const templates = useMemo(() => {
|
||||
const map = {
|
||||
[TemplateTypeEnum.basic]: basicNodeTemplates,
|
||||
[TemplateTypeEnum.basic]: basicNodeTemplates.filter((item) => {
|
||||
if (item.flowType === FlowNodeTypeEnum.lafModule && !feConfigs.lafEnv) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
[TemplateTypeEnum.systemPlugin]: systemNodeTemplates,
|
||||
[TemplateTypeEnum.teamPlugin]: teamPluginNodeTemplates.filter((item) =>
|
||||
searchKey ? item.pluginType !== PluginTypeEnum.folder : true
|
||||
|
||||
@@ -88,7 +88,7 @@ enum TabEnum {
|
||||
headers = 'headers',
|
||||
body = 'body'
|
||||
}
|
||||
type PropsArrType = {
|
||||
export type PropsArrType = {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
@@ -245,7 +245,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
|
||||
);
|
||||
});
|
||||
|
||||
function RenderHttpProps({
|
||||
export function RenderHttpProps({
|
||||
moduleId,
|
||||
inputs
|
||||
}: {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import { FlowModuleItemType } from '@fastgpt/global/core/module/type.d';
|
||||
import Container from '../modules/Container';
|
||||
import { Box, Button, Center, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { ModuleIOValueTypeEnum, ModuleInputKeyEnum } from '@fastgpt/global/core/module/constants';
|
||||
import { onChangeNode, useFlowProviderStore } from '../../FlowProvider';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getLafAppDetail } from '@/web/support/laf/api';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { getApiSchemaByUrl } from '@/web/core/plugin/api';
|
||||
import { str2OpenApiSchema } from '@fastgpt/global/core/plugin/httpPlugin/utils';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import Divider from '../modules/Divider';
|
||||
import RenderToolInput from '../render/RenderToolInput';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
|
||||
|
||||
const NodeLaf = (props: NodeProps<FlowModuleItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { data, selected } = props;
|
||||
const { moduleId, inputs } = data;
|
||||
|
||||
const requestUrl = inputs.find((item) => item.key === ModuleInputKeyEnum.httpReqUrl);
|
||||
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const token = userInfo?.team.lafAccount?.token;
|
||||
const appid = userInfo?.team.lafAccount?.appid;
|
||||
|
||||
// not config laf
|
||||
if (!token || !appid) {
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<ConfigLaf />
|
||||
</NodeCard>
|
||||
);
|
||||
}
|
||||
|
||||
const { data: lafData, isLoading: isLoadingFunctions } = useQuery(
|
||||
['getLafFunctionList'],
|
||||
async () => {
|
||||
// load laf app detail
|
||||
const appDetail = await getLafAppDetail(appid);
|
||||
|
||||
// load laf app functions
|
||||
const schemaUrl = `https://${appDetail?.domain.domain}/_/api-docs?token=${appDetail?.openapi_token}`;
|
||||
|
||||
const schema = await getApiSchemaByUrl(schemaUrl);
|
||||
const openApiSchema = await str2OpenApiSchema(JSON.stringify(schema));
|
||||
const filterPostSchema = openApiSchema.pathData.filter((item) => item.method === 'post');
|
||||
|
||||
return {
|
||||
lafApp: appDetail,
|
||||
lafFunctions: filterPostSchema.map((item) => ({
|
||||
...item,
|
||||
requestUrl: `https://${appDetail?.domain.domain}${item.path}`
|
||||
}))
|
||||
};
|
||||
},
|
||||
{
|
||||
onError(err) {
|
||||
toast({
|
||||
status: 'error',
|
||||
title: getErrText(err, '获取Laf函数列表失败')
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const lafFunctionSelectList = useMemo(
|
||||
() =>
|
||||
lafData?.lafFunctions.map((item) => ({
|
||||
label: item.description ? `${item.name} (${item.description})` : item.name,
|
||||
value: item.requestUrl
|
||||
})) || [],
|
||||
[lafData?.lafFunctions]
|
||||
);
|
||||
|
||||
const selectedFunction = useMemo(
|
||||
() => lafFunctionSelectList.find((item) => item.value === requestUrl?.value)?.value,
|
||||
[lafFunctionSelectList, requestUrl?.value]
|
||||
);
|
||||
|
||||
const onSyncParams = useCallback(() => {
|
||||
const lafFunction = lafData?.lafFunctions.find((item) => item.requestUrl === selectedFunction);
|
||||
|
||||
if (!lafFunction) return;
|
||||
|
||||
const bodyParams =
|
||||
lafFunction?.request?.content?.['application/json']?.schema?.properties || {};
|
||||
|
||||
const requiredParams =
|
||||
lafFunction?.request?.content?.['application/json']?.schema?.required || [];
|
||||
|
||||
const allParams = [
|
||||
...Object.keys(bodyParams).map((key) => ({
|
||||
name: key,
|
||||
desc: bodyParams[key].description,
|
||||
required: requiredParams?.includes(key) || false,
|
||||
value: `{{${key}}}`,
|
||||
type: 'string'
|
||||
}))
|
||||
].filter((item) => !inputs.find((input) => input.key === item.name));
|
||||
|
||||
// add params
|
||||
allParams.forEach((param) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'addInput',
|
||||
key: param.name,
|
||||
value: {
|
||||
key: param.name,
|
||||
valueType: ModuleIOValueTypeEnum.string,
|
||||
label: param.name,
|
||||
type: FlowNodeInputTypeEnum.target,
|
||||
required: param.required,
|
||||
description: param.desc || '',
|
||||
toolDescription: param.desc || '未设置参数描述',
|
||||
edit: true,
|
||||
editField: {
|
||||
key: true,
|
||||
name: true,
|
||||
description: true,
|
||||
required: true,
|
||||
dataType: true,
|
||||
inputType: true,
|
||||
isToolInput: true
|
||||
},
|
||||
connected: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common.Sync success')
|
||||
});
|
||||
}, [inputs, lafData?.lafFunctions, moduleId, selectedFunction, t, toast]);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'350px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
{/* select function */}
|
||||
<MySelect
|
||||
isLoading={isLoadingFunctions}
|
||||
list={lafFunctionSelectList}
|
||||
placeholder={t('core.module.laf.Select laf function')}
|
||||
onchange={(e) => {
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'updateInput',
|
||||
key: ModuleInputKeyEnum.httpReqUrl,
|
||||
value: {
|
||||
...requestUrl,
|
||||
value: e
|
||||
}
|
||||
});
|
||||
}}
|
||||
value={selectedFunction}
|
||||
/>
|
||||
{/* auto set params and go to edit */}
|
||||
{!!selectedFunction && (
|
||||
<Flex justifyContent={'flex-end'} mt={2} gap={2}>
|
||||
{/* <Button variant={'whiteBase'} size={'sm'} onClick={onSyncParams}>
|
||||
{t('core.module.Laf sync params')}
|
||||
</Button> */}
|
||||
<Button
|
||||
variant={'grayBase'}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
const lafFunction = lafData?.lafFunctions.find(
|
||||
(item) => item.requestUrl === selectedFunction
|
||||
);
|
||||
|
||||
if (!lafFunction) return;
|
||||
const url = `${feConfigs.lafEnv}/app/${lafData?.lafApp?.appid}/function${lafFunction?.path}`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
>
|
||||
{t('plugin.go to laf')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Container>
|
||||
{!!selectedFunction && <RenderIO {...props} />}
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeLaf);
|
||||
|
||||
const ConfigLaf = () => {
|
||||
const { t } = useTranslation();
|
||||
const { userInfo } = useUserStore();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const {
|
||||
isOpen: isOpenLafConfig,
|
||||
onOpen: onOpenLafConfig,
|
||||
onClose: onCloseLafConfig
|
||||
} = useDisclosure();
|
||||
|
||||
return !!feConfigs?.lafEnv ? (
|
||||
<Center minH={150}>
|
||||
<Button onClick={onOpenLafConfig} variant={'whitePrimary'}>
|
||||
{t('plugin.Please bind laf accout first')} <ChevronRightIcon />
|
||||
</Button>
|
||||
|
||||
{isOpenLafConfig && feConfigs?.lafEnv && (
|
||||
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLafConfig} />
|
||||
)}
|
||||
</Center>
|
||||
) : (
|
||||
<Box>系统未配置Laf环境</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const RenderIO = ({ data, selected }: NodeProps<FlowModuleItemType>) => {
|
||||
const { t } = useTranslation();
|
||||
const { moduleId, inputs, outputs } = data;
|
||||
const { splitToolInputs, hasToolNode } = useFlowProviderStore();
|
||||
const { commonInputs, toolInputs } = splitToolInputs(inputs, moduleId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasToolNode && (
|
||||
<>
|
||||
<Divider text={t('core.module.tool.Tool input')} />
|
||||
<Container>
|
||||
<RenderToolInput moduleId={moduleId} inputs={toolInputs} canEdit />
|
||||
</Container>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Divider text={t('common.Input')} />
|
||||
<Container>
|
||||
<Box mb={3}>自定义Body参数</Box>
|
||||
<RenderInput moduleId={moduleId} flowInputList={commonInputs} />
|
||||
</Container>
|
||||
</>
|
||||
<>
|
||||
<Divider text={t('common.Output')} />
|
||||
<Container>
|
||||
<RenderOutput moduleId={moduleId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -164,7 +164,7 @@ const NodeCard = (props: Props) => {
|
||||
top={'-20px'}
|
||||
right={0}
|
||||
transform={'translateX(90%)'}
|
||||
pl={'17px'}
|
||||
pl={'20px'}
|
||||
pr={'10px'}
|
||||
pb={'20px'}
|
||||
pt={'20px'}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/module/node/constant';
|
||||
import { ModuleIOValueTypeEnum } from '@fastgpt/global/core/module/constants';
|
||||
|
||||
export const defaultEditFormData: FlowNodeInputItemType = {
|
||||
valueType: 'string',
|
||||
type: FlowNodeInputTypeEnum.hidden,
|
||||
type: FlowNodeInputTypeEnum.target,
|
||||
key: '',
|
||||
label: '',
|
||||
toolDescription: '',
|
||||
required: true
|
||||
required: true,
|
||||
edit: true,
|
||||
editField: {
|
||||
key: true,
|
||||
description: true,
|
||||
dataType: true
|
||||
},
|
||||
defaultEditField: {
|
||||
label: '',
|
||||
key: '',
|
||||
description: '',
|
||||
inputType: FlowNodeInputTypeEnum.target,
|
||||
valueType: ModuleIOValueTypeEnum.string
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,7 +44,8 @@ const nodeTypes: Record<`${FlowNodeTypeEnum}`, any> = {
|
||||
[FlowNodeTypeEnum.tools]: dynamic(() => import('./components/nodes/NodeTools')),
|
||||
[FlowNodeTypeEnum.stopTool]: (data: NodeProps<FlowModuleItemType>) => (
|
||||
<NodeSimple {...data} minW={'100px'} maxW={'300px'} />
|
||||
)
|
||||
),
|
||||
[FlowNodeTypeEnum.lafModule]: dynamic(() => import('./components/nodes/NodeLaf'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
|
||||
185
projects/app/src/components/support/laf/LafAccountModal.tsx
Normal file
185
projects/app/src/components/support/laf/LafAccountModal.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { ModalBody, Box, Flex, Input, ModalFooter, Button, Link } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { putUpdateTeam } from '@/web/support/user/team/api';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import type { LafAccountType } from '@fastgpt/global/support/user/team/type.d';
|
||||
import { postLafPat2Token, getLafApplications } from '@/web/support/laf/api';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
|
||||
const LafAccountModal = ({
|
||||
defaultData = {
|
||||
token: '',
|
||||
appid: ''
|
||||
},
|
||||
onClose
|
||||
}: {
|
||||
defaultData?: LafAccountType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, handleSubmit, setValue, getValues, watch, reset } = useForm({
|
||||
defaultValues: {
|
||||
...defaultData,
|
||||
pat: ''
|
||||
}
|
||||
});
|
||||
|
||||
const lafToken = watch('token');
|
||||
const pat = watch('pat');
|
||||
const appid = watch('appid');
|
||||
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { toast } = useToast();
|
||||
const { userInfo, initUserInfo } = useUserStore();
|
||||
|
||||
const onResetForm = useCallback(() => {
|
||||
reset({
|
||||
token: '',
|
||||
appid: '',
|
||||
pat: ''
|
||||
});
|
||||
}, [reset]);
|
||||
|
||||
const { mutate: authLafPat, isLoading: isPatLoading } = useRequest({
|
||||
mutationFn: async (pat) => {
|
||||
const token = await postLafPat2Token(pat);
|
||||
setValue('token', token);
|
||||
},
|
||||
errorToast: t('plugin.Invalid Env')
|
||||
});
|
||||
|
||||
const { data: appListData = [] } = useQuery(
|
||||
['appList', lafToken],
|
||||
() => {
|
||||
return getLafApplications(lafToken);
|
||||
},
|
||||
{
|
||||
enabled: !!lafToken,
|
||||
onSuccess: (data) => {
|
||||
if (!getValues('appid') && data.length > 0) {
|
||||
setValue('appid', data[0].appid);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
onResetForm();
|
||||
toast({
|
||||
title: getErrText(err, '获取应用列表失败'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: onSubmit, isLoading: isUpdating } = useRequest({
|
||||
mutationFn: async (data: LafAccountType) => {
|
||||
if (!userInfo?.team.teamId) return;
|
||||
return putUpdateTeam({
|
||||
teamId: userInfo?.team.teamId,
|
||||
lafAccount: data
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
initUserInfo();
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common.Update Success'),
|
||||
errorToast: t('common.Update Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen iconSrc="/imgs/module/laf.png" title={t('user.Laf Account Setting')}>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
<Box>{t('support.user.Laf account intro')}</Box>
|
||||
<Box textDecoration={'underline'}>
|
||||
<Link href={`https://doc.laf.run/zh/`} isExternal>
|
||||
{t('support.user.Laf account course')}
|
||||
</Link>
|
||||
</Box>
|
||||
<Box>
|
||||
<Link textDecoration={'underline'} href={`${feConfigs.lafEnv}/`} isExternal>
|
||||
{t('support.user.Go laf env')}
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>PAT:</Box>
|
||||
{!lafToken ? (
|
||||
<>
|
||||
<Input
|
||||
flex={'1 0 0'}
|
||||
size={'sm'}
|
||||
{...register('pat')}
|
||||
placeholder={t('plugin.Enter PAT')}
|
||||
/>
|
||||
<Button
|
||||
ml={2}
|
||||
variant={'whitePrimary'}
|
||||
isDisabled={!pat}
|
||||
onClick={() => {
|
||||
authLafPat(pat);
|
||||
}}
|
||||
isLoading={isPatLoading}
|
||||
>
|
||||
验证
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
onClick={() => {
|
||||
onResetForm();
|
||||
putUpdateTeam({
|
||||
teamId: userInfo?.team.teamId || '',
|
||||
lafAccount: { token: '', appid: '' }
|
||||
});
|
||||
}}
|
||||
>
|
||||
已验证,点击取消绑定
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
{!!lafToken && (
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>{t('plugin.Currentapp')}</Box>
|
||||
<MySelect
|
||||
minW={'200px'}
|
||||
list={
|
||||
appListData
|
||||
.filter((app) => app.state === 'Running')
|
||||
.map((app) => ({
|
||||
label: `${app.name}`,
|
||||
value: app.appid
|
||||
})) || []
|
||||
}
|
||||
placeholder={t('plugin.App')}
|
||||
value={watch('appid')}
|
||||
onchange={(e) => {
|
||||
setValue('appid', e);
|
||||
}}
|
||||
{...(register('appid'), { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
|
||||
{t('common.Close')}
|
||||
</Button>
|
||||
<Button isLoading={isUpdating} onClick={handleSubmit((data) => onSubmit(data))}>
|
||||
{t('common.Update')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LafAccountModal;
|
||||
Reference in New Issue
Block a user