V4.8.17 feature (#3485)

* feat: add third party account config (#3443)

* temp

* editor workflow variable style

* add team to dispatch

* i18n

* delete console

* change openai account position

* fix

* fix

* fix

* fix

* fix

* 4.8.17 test (#3461)

* perf: external provider config

* perf: ui

* feat: add template config (#3434)

* change template position

* template config

* delete console

* delete

* fix

* fix

* perf: Mongo visutal field (#3464)

* remve invalid code

* perf: team member visutal code

* perf: virtual search; perf: search test data

* fix: ts

* fix: image response headers

* perf: template code

* perf: auth layout;perf: auto save (#3472)

* perf: auth layout

* perf: auto save

* perf: auto save

* fix: template guide display & http input support external variables (#3475)

* fix: template guide display

* http editor support external workflow variables

* perf: auto save;fix: ifelse checker line break; (#3478)

* perf: auto save

* perf: auto save

* fix: ifelse checker line break

* perf: doc

* perf: doc

* fix: update var type error

* 4.8.17 test (#3479)

* perf: auto save

* perf: auto save

* perf: template code

* 4.8.17 test (#3480)

* perf: auto save

* perf: auto save

* perf: model price model

* feat: add react memo

* perf: model provider filter

* fix: ts (#3481)

* perf: auto save

* perf: auto save

* fix: ts

* simple app tool select (#3473)

* workflow plugin userguide & simple tool ui

* simple tool filter

* reuse component

* change component to hook

* fix

* perf: too selector modal (#3484)

* perf: auto save

* perf: auto save

* perf: markdown render

* perf: too selector

* fix: app version require tmbId

* perf: templates refresh

* perf: templates refresh

* hide auto save error tip

* perf: toolkit guide

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-12-27 20:05:12 +08:00
committed by GitHub
parent a209856d48
commit b520988c64
207 changed files with 2943 additions and 1378 deletions

View File

@@ -17,7 +17,7 @@ const unAuthPage: { [key: string]: boolean } = {
'/price': true
};
const Auth = ({ children }: { children: JSX.Element }) => {
const Auth = ({ children }: { children: JSX.Element | React.ReactNode }) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();

View File

@@ -80,14 +80,14 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{isHideNavbar ? (
<Auth>{children}</Auth>
) : (
<>
<Auth>
<Box h={'100%'} position={'fixed'} left={0} top={0} w={navbarWidth}>
<Navbar unread={unread} />
</Box>
<Box h={'100%'} ml={navbarWidth} overflow={'overlay'}>
<Auth>{children}</Auth>
{children}
</Box>
</>
</Auth>
)}
</>
)}
@@ -96,14 +96,16 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{phoneUnShowLayoutRoute[router.pathname] || isChatPage ? (
<Auth>{children}</Auth>
) : (
<Flex h={'100%'} flexDirection={'column'}>
<Box flex={'1 0 0'} h={0}>
<Auth>{children}</Auth>
</Box>
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
<NavbarPhone unread={unread} />
</Box>
</Flex>
<Auth>
<Flex h={'100%'} flexDirection={'column'}>
<Box flex={'1 0 0'} h={0}>
{children}
</Box>
<Box h={'50px'} borderTop={'1px solid rgba(0,0,0,0.1)'}>
<NavbarPhone unread={unread} />
</Box>
</Flex>
</Auth>
)}
</>
)}

View File

@@ -82,6 +82,7 @@ const Navbar = ({ unread }: { unread: number }) => {
'/account/info',
'/account/team',
'/account/usage',
'/account/thirdParty',
'/account/apikey',
'/account/setting',
'/account/inform',

View File

@@ -28,17 +28,22 @@ const IframeHtmlCodeBlock = dynamic(() => import('./codeBlock/iframe-html'), { s
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
const Markdown = ({
source = '',
showAnimation = false,
isDisabled = false,
forbidZhFormat = false
}: {
type Props = {
source?: string;
showAnimation?: boolean;
isDisabled?: boolean;
forbidZhFormat?: boolean;
}) => {
};
const Markdown = (props: Props) => {
const source = props.source || '';
if (source.length < 200000) {
return <MarkdownRender {...props} />;
}
return <Box whiteSpace={'pre-wrap'}>{source}</Box>;
};
const MarkdownRender = ({ source = '', showAnimation, isDisabled, forbidZhFormat }: Props) => {
const components = useMemo<any>(
() => ({
img: Image,

View File

@@ -24,12 +24,6 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
const { t } = useTranslation();
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const avatarSize = useMemo(() => {
const size = {
sm: '1rem',
@@ -74,17 +68,6 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
: avatarList;
}, [feConfigs.show_pay, avatarList, avatarSize, t]);
const onSelect = useCallback(
(e: string) => {
if (e === 'price') {
onOpenAiPointsModal();
return;
}
return onchange?.(e);
},
[onOpenAiPointsModal, onchange]
);
return (
<Box
css={{
@@ -94,30 +77,32 @@ const OneRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
}}
>
<MyTooltip label={disableTip}>
<MySelect
className="nowheel"
isDisabled={!!disableTip}
list={expandList}
{...props}
onchange={onSelect}
/>
<ModelPriceModal>
{({ onOpen }) => (
<MySelect
className="nowheel"
isDisabled={!!disableTip}
list={expandList}
{...props}
onchange={(e) => {
if (e === 'price') {
onOpen();
return;
}
return onchange?.(e);
}}
/>
)}
</ModelPriceModal>
</MyTooltip>
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
</Box>
);
};
const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) => {
const { t } = useTranslation();
const { feConfigs, llmModelList, vectorModelList } = useSystemStore();
const { llmModelList, vectorModelList } = useSystemStore();
const [value, setValue] = useState<string[]>([]);
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const avatarSize = useMemo(() => {
const size = {
sm: '1rem',
@@ -211,8 +196,6 @@ const MultipleRowSelector = ({ list, onchange, disableTip, ...props }: Props) =>
}}
/>
</MyTooltip>
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
</Box>
);
};

View File

@@ -0,0 +1,46 @@
import { Box, ModalBody, useDisclosure } from '@chakra-ui/react';
import Markdown from '@/components/Markdown';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { getDocPath } from '@/web/common/system/doc';
import React from 'react';
const UseGuideModal = ({
children,
title,
iconSrc,
text,
link
}: {
children: ({ onClick }: { onClick: () => void }) => React.ReactNode;
title?: string;
iconSrc?: string;
text?: string;
link?: string;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const onClick = () => {
if (link) {
return window.open(getDocPath(link), '_blank');
}
if (text) {
return onOpen();
}
};
return (
<>
{children({ onClick })}
{isOpen && (
<MyModal isOpen iconSrc={iconSrc} title={title} onClose={onClose} minW={'600px'}>
<ModalBody>
<Box border={'base'} borderRadius={'10px'} p={4} minH={'500px'}>
<Markdown source={text} />
</Box>
</ModalBody>
</MyModal>
)}
</>
);
};
export default React.memo(UseGuideModal);

View File

@@ -100,12 +100,6 @@ const AIChatSettingsModal = ({
setRefresh(!refresh);
};
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
return (
<MyModal
isOpen
@@ -160,10 +154,11 @@ const AIChatSettingsModal = ({
<Th fontSize={'mini'} pb={2}>
<HStack spacing={1}>
<Box> {t('app:ai_point_price')}</Box>
<QuestionTip
label={t('app:look_ai_point_price')}
onClick={onOpenAiPointsModal}
/>
<ModelPriceModal>
{({ onOpen }) => (
<QuestionTip label={t('app:look_ai_point_price')} onClick={onOpen} />
)}
</ModelPriceModal>
</HStack>
</Th>
<Th fontSize={'mini'} pb={2}>
@@ -327,8 +322,6 @@ const AIChatSettingsModal = ({
{t('common:common.Confirm')}
</Button>
</ModalFooter>
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
</MyModal>
);
};

View File

@@ -9,9 +9,9 @@ import {
Td,
Th,
Thead,
Tr
Tr,
useDisclosure
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useRef, useState } from 'react';
import {
@@ -26,6 +26,9 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal'));
const ModelTable = () => {
const { t } = useTranslation();
@@ -156,6 +159,19 @@ const ModelTable = () => {
search
]);
const filterProviderList = useMemo(() => {
const allProviderIds: string[] = [
...llmModelList,
...vectorModelList,
...audioSpeechModelList,
whisperModel
].map((model) => model.provider);
return providerList.current.filter(
(item) => allProviderIds.includes(item.value) || item.value === ''
);
}, [audioSpeechModelList, llmModelList, vectorModelList, whisperModel]);
return (
<Flex flexDirection={'column'} h={'100%'}>
<Flex>
@@ -168,7 +184,7 @@ const ModelTable = () => {
bg={'myGray.50'}
value={provider}
onchange={setProvider}
list={providerList.current}
list={filterProviderList}
/>
</HStack>
<HStack flexShrink={0} ml={6}>
@@ -228,24 +244,34 @@ const ModelTable = () => {
export default ModelTable;
export const ModelPriceModal = ({ onClose }: { onClose: () => void }) => {
export const ModelPriceModal = ({
children
}: {
children: ({ onOpen }: { onOpen: () => void }) => React.ReactNode;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<MyModal
isCentered
iconSrc="/imgs/modal/bill.svg"
title={t('common:support.wallet.subscription.Ai points')}
isOpen
onClose={onClose}
w={'100%'}
h={'100%'}
maxW={'90vw'}
maxH={'90vh'}
>
<ModalBody flex={'1 0 0'}>
<ModelTable />
</ModalBody>
</MyModal>
<>
{children({ onOpen })}
{isOpen && (
<MyModal
isCentered
iconSrc="/imgs/modal/bill.svg"
title={t('common:support.wallet.subscription.Ai points')}
isOpen
onClose={onClose}
w={'100%'}
h={'100%'}
maxW={'90vw'}
maxH={'90vh'}
>
<ModalBody flex={'1 0 0'}>
<ModelTable />
</ModalBody>
</MyModal>
)}
</>
);
};

View File

@@ -226,8 +226,8 @@ const DatasetParamsModal = ({
{limit !== undefined && (
<Box display={['block', 'flex']}>
<Flex flex={'0 0 120px'} alignItems={'center'} mb={[5, 0]}>
<FormLabel>{t('app:max_quote_tokens')}</FormLabel>
<QuestionTip label={t('app:max_quote_tokens_tips')} />
<FormLabel>{t('common:max_quote_tokens')}</FormLabel>
<QuestionTip label={t('common:max_quote_tokens_tips')} />
</Flex>
<Box flex={'1 0 0'}>
<InputSlider
@@ -245,8 +245,8 @@ const DatasetParamsModal = ({
)}
<Box display={['block', 'flex']} mt={[6, 10]} mb={4}>
<Flex flex={'0 0 120px'} alignItems={'center'} mb={[5, 0]}>
<FormLabel>{t('app:min_similarity')}</FormLabel>
<QuestionTip label={t('app:min_similarity_tip')} />
<FormLabel>{t('common:min_similarity')}</FormLabel>
<QuestionTip label={t('common:min_similarity_tip')} />
</Flex>
<Box flex={'1 0 0'}>
{showSimilarity ? (

View File

@@ -1,4 +1,4 @@
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
import { Box, Button, Flex, Switch, Textarea, useDisclosure } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
@@ -20,6 +20,8 @@ import { isEqual } from 'lodash';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { PluginRunContext } from '../context';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import AIModelSelector from '@/components/Select/AIModelSelector';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
@@ -152,6 +154,7 @@ const RenderPluginInput = ({
}) => {
const { t } = useTranslation();
const inputType = input.renderTypeList[0];
const { llmModelList } = useSystemStore();
const render = (() => {
if (inputType === FlowNodeInputTypeEnum.customVariable) {
@@ -167,7 +170,19 @@ const RenderPluginInput = ({
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
);
}
if (inputType === FlowNodeInputTypeEnum.selectLLMModel) {
return (
<AIModelSelector
w={'100%'}
value={value}
list={llmModelList.map((item) => ({
value: item.model,
label: item.name
}))}
onchange={onChange}
/>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea

View File

@@ -62,8 +62,8 @@ const SearchParamsTip = ({
<Thead>
<Tr bg={'transparent !important'}>
<Th fontSize={'mini'}>{t('common:core.dataset.search.search mode')}</Th>
<Th fontSize={'mini'}>{t('app:max_quote_tokens')}</Th>
<Th fontSize={'mini'}>{t('app:min_similarity')}</Th>
<Th fontSize={'mini'}>{t('common:max_quote_tokens')}</Th>
<Th fontSize={'mini'}>{t('common:min_similarity')}</Th>
{hasReRankModel && <Th fontSize={'mini'}>{t('common:core.dataset.search.ReRank')}</Th>}
<Th fontSize={'mini'}>{t('common:core.module.template.Query extension')}</Th>
{hasEmptyResponseMode && (

View File

@@ -96,7 +96,7 @@ const LafAccountModal = ({
});
return (
<MyModal isOpen iconSrc="/imgs/workflow/laf.png" title={t('common:user.Laf Account Setting')}>
<MyModal isOpen iconSrc="support/account/laf" title={t('common:user.Laf Account Setting')}>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
<Box>{t('common:support.user.Laf account intro')}</Box>
@@ -107,7 +107,7 @@ const LafAccountModal = ({
</Box>
<Box>
<Link textDecoration={'underline'} href={`${feConfigs.lafEnv}/`} isExternal>
{t('support.user.Go laf env', {
{t('common:support.user.Go laf env', {
env: feConfigs.lafEnv?.split('//')[1]
})}
</Link>

View File

@@ -2,7 +2,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { StandardSubLevelEnum, SubModeEnum } from '@fastgpt/global/support/wallet/sub/constants';
import React, { useMemo } from 'react';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { Box, Flex, Grid, useDisclosure } from '@chakra-ui/react';
import { Box, Flex, Grid } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
@@ -22,12 +22,6 @@ const StandardPlanContentList = ({
const { t } = useTranslation();
const { subPlans } = useSystemStore();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const planContent = useMemo(() => {
const plan = subPlans?.standard?.[level];
@@ -100,11 +94,15 @@ const StandardPlanContentList = ({
amount: planContent.totalPoints
})}
</Box>
<QuestionTip
ml={1}
label={t('common:support.wallet.subscription.AI points click to read tip')}
onClick={onOpenAiPointsModal}
></QuestionTip>
<ModelPriceModal>
{({ onOpen }) => (
<QuestionTip
ml={1}
label={t('common:support.wallet.subscription.AI points click to read tip')}
onClick={onOpen}
/>
)}
</ModelPriceModal>
</Flex>
</Flex>
<Flex alignItems={'center'}>
@@ -127,7 +125,6 @@ const StandardPlanContentList = ({
<Box color={'myGray.600'}>{t('common:support.wallet.subscription.web_site_sync')}</Box>
</Flex>
)}
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
</Grid>
) : null;
};

View File

@@ -20,6 +20,7 @@ export type PostPublishAppProps = {
chatConfig: AppSchema['chatConfig'];
isPublish?: boolean;
versionName?: string;
autoSave?: boolean; // If it is automatically saved, only one copy of the entire app will be stored, overwriting the old version
};
export type PostRevertAppProps = {

View File

@@ -10,7 +10,7 @@ export async function register() {
const [
{ connectMongo },
{ systemStartCb },
{ initGlobalVariables, getInitConfig, initSystemPlugins },
{ initGlobalVariables, getInitConfig, initSystemPluginGroups, initAppTemplateTypes },
{ initVectorStore },
{ initRootUser },
{ getSystemPluginCb },
@@ -37,8 +37,10 @@ export async function register() {
await connectMongo();
//init system configinit vector databaseinit root user
await Promise.all([getInitConfig(), initVectorStore(), initRootUser(), initSystemPlugins()]);
await Promise.all([getInitConfig(), initVectorStore(), initRootUser()]);
initSystemPluginGroups();
initAppTemplateTypes();
getSystemPluginCb();
startMongoWatch();
startCron();

View File

@@ -17,6 +17,8 @@ export enum TabEnum {
'bill' = 'bill',
'inform' = 'inform',
'setting' = 'setting',
'thirdParty' = 'thirdParty',
'individuation' = 'individuation',
'apikey' = 'apikey',
'loginout' = 'loginout',
'team' = 'team',
@@ -70,6 +72,11 @@ const AccountContainer = ({
}
]
: []),
{
icon: 'common/thirdParty',
label: t('account:third_party'),
value: TabEnum.thirdParty
},
{
icon: 'common/model',
label: t('account:model_provider'),

View File

@@ -92,7 +92,7 @@ const TeamSelector = ({
: []),
...teamList
];
}, [showManage, teamList, router]);
}, [showManage, t, teamList, router]);
return (
<Box w={'100%'}>

View File

@@ -38,10 +38,8 @@ import { formatTime2YMD } from '@fastgpt/global/common/string/time';
import { getExtraPlanCardRoute } from '@/web/support/wallet/sub/constants';
import StandardPlanContentList from '@/components/support/wallet/StandardPlanContentList';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import AccountContainer from '../components/AccountContainer';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
@@ -52,8 +50,6 @@ const StandDetailModal = dynamic(() => import('./components/standardDetailModal'
const ConversionModal = dynamic(() => import('./components/ConversionModal'));
const UpdatePswModal = dynamic(() => import('./components/UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./components/UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const CommunityModal = dynamic(() => import('@/components/CommunityModal'));
const ModelPriceModal = dynamic(() =>
@@ -144,8 +140,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
async (data: UserType) => {
await updateUserInfo({
avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount
timezone: data.timezone
});
reset(data);
toast({
@@ -353,11 +348,6 @@ const PlanUsage = () => {
onClose: onCloseStandardModal,
onOpen: onOpenStandardModal
} = useDisclosure();
const {
isOpen: isOpenAiPointsModal,
onClose: onCloseAiPointsModal,
onOpen: onOpenAiPointsModal
} = useDisclosure();
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
@@ -443,9 +433,13 @@ const PlanUsage = () => {
<MyIcon mr={2} name={'support/account/plans'} w={'20px'} />
{t('account_info:package_and_usage')}
</Flex>
<Button ml={4} size={'sm'} onClick={onOpenAiPointsModal}>
{t('account_info:billing_standard')}
</Button>
<ModelPriceModal>
{({ onOpen }) => (
<Button ml={4} size={'sm'} onClick={onOpen}>
{t('account_info:billing_standard')}
</Button>
)}
</ModelPriceModal>
<Button ml={4} variant={'whitePrimary'} size={'sm'} onClick={onOpenStandardModal}>
{t('account_info:package_details')}
</Button>
@@ -584,14 +578,24 @@ const PlanUsage = () => {
</Box>
</Box>
{isOpenStandardModal && <StandDetailModal onClose={onCloseStandardModal} />}
{isOpenAiPointsModal && <ModelPriceModal onClose={onCloseAiPointsModal} />}
</Box>
) : null;
};
const ButtonStyles = {
bg: 'white',
py: 3,
px: 6,
border: 'sm',
borderWidth: '1.5px',
borderRadius: 'md',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none' as any,
fontSize: 'sm'
};
const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { isPc } = useSystem();
@@ -600,56 +604,16 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { isOpen: isOpenLaf, onClose: onCloseLaf, onOpen: onOpenLaf } = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
const onclickSave = useCallback(
async (data: UserType) => {
await updateUserInfo({
avatar: data.avatar,
timezone: data.timezone,
openaiAccount: data.openaiAccount
});
reset(data);
toast({
title: t('account_info:update_success_tip'),
status: 'success'
});
},
[reset, t, toast, updateUserInfo]
);
const buttonStyles = useRef<FlexProps>({
bg: 'white',
py: 3,
px: 6,
border: theme.borders.sm,
borderWidth: '1.5px',
borderRadius: 'md',
alignItems: 'center',
cursor: 'pointer',
userSelect: 'none',
fontSize: 'sm'
});
return (
<Box>
<Grid gridGap={4} mt={3}>
{feConfigs?.docUrl && (
<Link
bg={'white'}
href={getDocPath('/docs/intro')}
target="_blank"
display={'flex'}
py={3}
px={6}
border={theme.borders.sm}
borderWidth={'1.5px'}
borderRadius={'md'}
alignItems={'center'}
userSelect={'none'}
textDecoration={'none !important'}
fontSize={'sm'}
{...ButtonStyles}
>
<MyIcon name={'common/courseLight'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
@@ -662,76 +626,22 @@ const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
feConfigs?.navbarItems
?.filter((item) => item.isActive)
.map((item) => (
<Flex
key={item.id}
{...buttonStyles.current}
onClick={() => window.open(item.url, '_blank')}
>
<Flex key={item.id} {...ButtonStyles} onClick={() => window.open(item.url, '_blank')}>
<Avatar src={item.avatar} w={'18px'} />
<Box ml={2} flex={1}>
{item.name}
</Box>
</Flex>
))}
{feConfigs?.lafEnv && userInfo?.team.role === TeamMemberRoleEnum.owner && (
<Flex {...buttonStyles.current} onClick={onOpenLaf}>
<MyImage src="/imgs/workflow/laf.png" w={'18px'} alt="laf" />
<Box ml={2} flex={1}>
{'laf' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.team.lafAccount?.token ? '#67c13b' : 'myGray.500'}
/>
</Flex>
)}
{feConfigs?.show_openai_account && (
<Flex {...buttonStyles.current} onClick={onOpenOpenai}>
<MyIcon name={'common/openai'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{'OpenAI / OneAPI' + t('account_info:account_duplicate')}
</Box>
<Box
w={'9px'}
h={'9px'}
borderRadius={'50%'}
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
/>
</Flex>
)}
{feConfigs?.concatMd && (
<Button
variant={'whiteBase'}
justifyContent={'flex-start'}
leftIcon={<MyIcon name={'modal/concat'} w={'18px'} color={'myGray.600'} />}
onClick={onOpenContact}
h={'48px'}
fontSize={'sm'}
>
{t('account_info:contact_us')}
</Button>
<Flex onClick={onOpenContact} {...ButtonStyles}>
<MyIcon name={'modal/concat'} w={'18px'} color={'myGray.600'} />
<Box ml={2} flex={1}>
{t('account_info:contact_us')}
</Box>
</Flex>
)}
</Grid>
{isOpenLaf && userInfo && (
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLaf} />
)}
{isOpenOpenai && userInfo && (
<OpenAIAccountModal
defaultData={userInfo?.openaiAccount}
onSuccess={(data) =>
onclickSave({
...userInfo,
openaiAccount: data
})
}
onClose={onCloseOpenai}
/>
)}
</Box>
);
};

View File

@@ -130,7 +130,7 @@ const Team = () => {
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team.role === TeamMemberRoleEnum.owner && (
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"

View File

@@ -3,41 +3,51 @@ import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/rea
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 type { UserType } from '@fastgpt/global/support/user/type.d';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import { putUpdateTeam } from '@/web/support/user/team/api';
const OpenAIAccountModal = ({
defaultData,
onSuccess,
onClose
}: {
defaultData: UserType['openaiAccount'];
onSuccess: (e: UserType['openaiAccount']) => Promise<any>;
defaultData?: OpenaiAccountType;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const { register, handleSubmit } = useForm({
defaultValues: defaultData
});
const { mutate: onSubmit, isLoading } = useRequest({
mutationFn: async (data: UserType['openaiAccount']) => onSuccess(data),
onSuccess(res) {
onClose();
const { runAsync: onSubmit, loading } = useRequest2(
async (data: OpenaiAccountType) => {
if (!userInfo?.team.teamId) return;
return putUpdateTeam({
openaiAccount: data
});
},
errorToast: t('account_info:openai_account_setting_exception')
});
{
onSuccess: () => {
initUserInfo();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
}
);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="common/openai"
title={t('account_info:openai_account_configuration')}
title={t('account_thirdParty:openai_account_configuration')}
>
<ModalBody>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('account_info:open_api_notice')}
{t('account_thirdParty:open_api_notice')}
</Box>
<Flex alignItems={'center'} mt={5}>
<Box flex={'0 0 65px'}>API Key:</Box>
@@ -48,16 +58,16 @@ const OpenAIAccountModal = ({
<Input
flex={1}
{...register('baseUrl')}
placeholder={t('account_info:request_address_notice')}
></Input>
placeholder={t('account_thirdParty:request_address_notice')}
/>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('account_info:cancel')}
{t('common:common.Cancel')}
</Button>
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
{t('account_info:confirm')}
<Button isLoading={loading} onClick={handleSubmit(onSubmit)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@@ -0,0 +1,81 @@
import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { ThirdPartyAccountType } from '../index';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useUserStore } from '@/web/support/user/useUserStore';
import { putUpdateTeam } from '@/web/support/user/team/api';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
const WorkflowVariableModal = ({
defaultData,
onClose
}: {
defaultData: ThirdPartyAccountType;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const { register, handleSubmit } = useForm({
defaultValues: {
value: '',
key: defaultData.key || ''
}
});
const { runAsync: onSubmit, loading } = useRequest2(
async (data: { key: string; value: string }) => {
if (!userInfo?.team.teamId) return;
await putUpdateTeam({
externalWorkflowVariable: data
});
},
{
onSuccess: () => {
initUserInfo();
onClose();
},
successToast: t('common:common.Update Success'),
errorToast: t('common:common.Update Failed')
}
);
return (
<MyModal title={`${defaultData.name} 配置`} iconSrc={'edit'} iconColor={'primary.600'}>
<ModalBody w={'420px'}>
<Box fontSize={'14px'} color={'myGray.900'}>
{defaultData.intro}
</Box>
<Box h={'1px'} bg={'myGray.150'} my={4}></Box>
<Flex alignItems={'center'}>
<Box fontSize={'14px'} color={'myGray.900'} fontWeight={'medium'}>
{t('common:core.workflow.value')}
</Box>
<Input
ml={8}
bg={'myGray.50'}
placeholder={t('account_thirdParty:value_placeholder')}
flex={1}
{...register('value')}
/>
</Flex>
<Box mt={1} color={'myGray.500'} fontSize={'xs'}>
{t('account_thirdParty:value_not_return_tip')}
</Box>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={loading} onClick={handleSubmit(onSubmit)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(WorkflowVariableModal);

View File

@@ -0,0 +1,263 @@
import AccountContainer from '../components/AccountContainer';
import { Box, Flex, Grid, Progress, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import dynamic from 'next/dynamic';
import { useState, useMemo } from 'react';
import WorkflowVariableModal from './components/WorkflowVariableModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { serviceSideProps } from '@fastgpt/web/common/system/nextjs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { GET } from '@/web/common/api/request';
import type { checkUsageResponse } from '@/pages/api/support/user/team/thirtdParty/checkUsage';
import MyBox from '@fastgpt/web/components/common/MyBox';
const LafAccountModal = dynamic(() => import('@/components/support/laf/LafAccountModal'));
const OpenAIAccountModal = dynamic(() => import('./components/OpenAIAccountModal'));
export type ThirdPartyAccountType = {
name: string;
icon: string;
iconColor?: string;
key?: string;
intro: string;
onClick?: () => void;
isOpen?: boolean;
active: boolean;
usage?: {
used: number;
total: number;
};
};
const ThirdParty = () => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const { isOpen: isOpenLaf, onClose: onCloseLaf, onOpen: onOpenLaf } = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
const [workflowVariable, setWorkflowVariable] = useState<ThirdPartyAccountType>();
const { userInfo } = useUserStore();
const isOwner = userInfo?.team?.role === TeamMemberRoleEnum.owner;
const defaultAccountList: ThirdPartyAccountType[] = useMemo(
() => [
{
name: t('account_thirdParty:laf_account'),
icon: 'support/account/laf',
intro: t('common:support.user.Laf account intro'),
onClick: onOpenLaf,
isOpen: !!feConfigs?.lafEnv,
active: !!userInfo?.team?.lafAccount?.appid
},
{
name: t('account_thirdParty:openai_account_configuration'),
iconColor: 'black',
icon: 'common/openai',
intro: t('account_thirdParty:open_api_notice'),
onClick: onOpenOpenai,
isOpen: feConfigs?.show_openai_account,
active: userInfo?.team?.openaiAccount?.key !== undefined
}
],
[
feConfigs?.lafEnv,
feConfigs?.show_openai_account,
onOpenLaf,
onOpenOpenai,
t,
userInfo?.team?.lafAccount?.appid,
userInfo?.team?.openaiAccount?.key
]
);
const { data: workflowVariables = [], loading } = useRequest2(
async (): Promise<ThirdPartyAccountType[]> => {
return Promise.all(
(feConfigs?.externalProviderWorkflowVariables || []).map(async (item) => {
const usage = await (async () => {
try {
return await GET<checkUsageResponse>('/support/user/team/thirtdParty/checkUsage', {
key: item.key
});
} catch (err) {
return;
}
})();
const account = {
key: item.key,
name: item.name,
active: userInfo?.team?.externalWorkflowVariables?.[item.key] !== undefined,
icon: 'common/variable',
iconColor: 'primary.600',
intro: item.intro || t('account_thirdParty:no_intro')
};
return {
...account,
usage,
onClick: () => setWorkflowVariable(account),
isOpen: item.isOpen
};
})
);
},
{
manual: false,
refreshDeps: [
feConfigs?.externalProviderWorkflowVariables,
userInfo?.team?.externalWorkflowVariables
]
}
);
const accountList = useMemo(
() => [...defaultAccountList, ...workflowVariables],
[defaultAccountList, workflowVariables]
);
return (
<AccountContainer>
<MyBox isLoading={loading} px={[4, 8]} py={[4, 6]} bg={'white'} h={'full'}>
<Flex>
<MyIcon name={'common/thirdParty'} w={'24px'} color={'myGray.900'} />
<Box ml={3}>
<Box fontSize={'md'} color={'myGray.900'}>
{t('account_thirdParty:third_party_account')}
</Box>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('account_thirdParty:third_party_account_desc')}
</Box>
</Box>
</Flex>
<Grid
gridTemplateColumns={[
'1fr',
'repeat(2,1fr)',
'repeat(3,1fr)',
'repeat(3,1fr)',
'repeat(4,1fr)'
]}
gridGap={4}
alignItems={'stretch'}
mt={5}
pb={5}
>
{accountList
.filter((item) => item.isOpen)
.map((item) => (
<Flex
key={item.name}
flexDirection={'column'}
border={'1px solid'}
borderColor={'myGray.200'}
pt={4}
px={5}
borderRadius={'10px'}
h={'146px'}
cursor={'pointer'}
_hover={{
borderColor: 'primary.600'
}}
onClick={
isOwner
? item.onClick
: () =>
toast({
title: t('account_thirdParty:error.no_permission'),
status: 'warning'
})
}
position={'relative'}
>
<Flex>
<MyIcon name={item.icon as any} w={'24px'} color={item.iconColor} />
<Box ml={2} flex={1} fontWeight={'medium'} fontSize={'16px'} color={'myGray.900'}>
{item.name}
</Box>
<Box
color={item.active ? 'green.600' : 'myGray.700'}
bg={item.active ? 'green.50' : 'myGray.100'}
px={2}
py={1}
borderRadius={'sm'}
fontSize={'10px'}
>
{item.active
? t('account_thirdParty:configured')
: t('account_thirdParty:not_configured')}
</Box>
</Flex>
<Box
className="textEllipsis2"
mt={3}
fontSize={'mini'}
color={'myGray.500'}
lineHeight={'18px'}
>
{item.intro}
</Box>
<Box flex={1} />
{item.active && item.usage && (
<Box w={'full'} mb={4}>
<Flex fontSize={'mini'} color={'myGray.500'}>
<Box>{t('account_thirdParty:usage')}</Box>
{item.usage?.total ? (
<Box ml={1}>
{item.usage.used}/{item.usage.total}
</Box>
) : (
<Box ml={1}>{t('account_thirdParty:unavailable')}</Box>
)}
</Flex>
<Box mt={1} w={'full'}>
<Progress
size={'sm'}
value={(item.usage.used / item.usage.total) * 100}
colorScheme={'blue'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'low'}
isAnimated
hasStripe
/>
</Box>
</Box>
)}
</Flex>
))}
</Grid>
</MyBox>
{isOpenLaf && userInfo && (
<LafAccountModal defaultData={userInfo?.team?.lafAccount} onClose={onCloseLaf} />
)}
{isOpenOpenai && userInfo && (
<OpenAIAccountModal defaultData={userInfo?.team?.openaiAccount} onClose={onCloseOpenai} />
)}
{workflowVariable && (
<WorkflowVariableModal
defaultData={workflowVariable}
onClose={() => setWorkflowVariable(undefined)}
/>
)}
</AccountContainer>
);
};
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account', 'account_thirdParty']))
}
};
}
export default ThirdParty;

View File

@@ -0,0 +1,39 @@
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { NextApiRequest, NextApiResponse } from 'next';
async function handler(req: NextApiRequest, res: NextApiResponse) {
await authCert({ req, authRoot: true });
const users = await MongoUser.find(
{ openaiAccount: { $exists: true, $ne: null } },
'_id openaiAccount'
);
console.log(`${users.length} 个用户需要更新`);
let count = 0;
for (const user of users) {
await mongoSessionRun(async (session) => {
await MongoTeam.updateOne(
{ ownerId: user._id },
{
$set: { openaiAccount: (user as any).openaiAccount }
},
{ session }
);
// @ts-ignore
user.openaiAccount = undefined;
await user.save({ session });
});
count++;
console.log(`已更新 ${count} 个用户`);
}
return { success: true };
}
export default NextAPI(handler);

View File

@@ -94,6 +94,7 @@ async function initHttp(teamId?: string): Promise<any> {
await MongoAppVersion.create(
[
{
tmbId: plugin.tmbId,
appId: newPluginId,
nodes: item.modules,
edges: item.edges
@@ -166,6 +167,7 @@ async function initPlugin(teamId?: string): Promise<any> {
await MongoAppVersion.create(
[
{
tmbId: plugin.tmbId,
appId: newPluginId,
nodes: plugin.modules,
edges: plugin.edges

View File

@@ -40,6 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
res.setHeader('Content-Type', `${file.contentType}; charset=${encoding}`);
res.setHeader('Cache-Control', 'public, max-age=31536000');
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(file.filename)}"`);
res.setHeader('Content-Length', file.length);
stream.pipe(res);

View File

@@ -40,6 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
res.setHeader('Content-Type', `${file.contentType}; charset=${encoding}`);
res.setHeader('Cache-Control', 'public, max-age=31536000');
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(filename)}"`);
res.setHeader('Content-Length', file.length);
stream.pipe(res);

View File

@@ -45,8 +45,9 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
// 上限校验
await checkTeamAppLimit(teamId);
const tmb = await MongoTeamMember.findById({ _id: tmbId });
const user = await MongoUser.findById({ _id: tmb?.userId });
const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId').populate<{
user: { avatar: string; username: string };
}>('user', 'avatar username');
// 创建app
const appId = await onCreateApp({
@@ -59,8 +60,8 @@ async function handler(req: ApiRequestProps<CreateAppBody>) {
chatConfig,
teamId,
tmbId,
userAvatar: user?.avatar,
username: user?.username
userAvatar: tmb?.user?.avatar,
username: tmb?.user?.username
});
pushTrack.createApp({
@@ -132,6 +133,7 @@ export const onCreateApp = async ({
await MongoAppVersion.create(
[
{
tmbId,
appId,
nodes: modules,
edges,

View File

@@ -45,7 +45,8 @@ async function handler(
currentCost: plugin.currentCost,
hasTokenFee: plugin.hasTokenFee,
author: plugin.author,
instructions: plugin.userGuide
instructions: plugin.userGuide,
courseUrl: plugin.courseUrl
}))
.filter((item) => {
if (searchKey) {

View File

@@ -1,8 +1,8 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextAPI } from '@/service/middleware/entry';
import { TemplateMarketItemType } from '@fastgpt/global/core/workflow/type';
import { getTemplateMarketItemDetail } from '@/service/core/app/template';
import { AppTemplateSchemaType } from '@fastgpt/global/core/app/type';
import { getAppTemplatesAndLoadThem } from '@fastgpt/templates/register';
type Props = {
templateId: string;
@@ -11,11 +11,13 @@ type Props = {
async function handler(
req: NextApiRequest,
res: NextApiResponse<any>
): Promise<TemplateMarketItemType | undefined> {
): Promise<AppTemplateSchemaType | undefined> {
await authCert({ req, authToken: true });
const { templateId } = req.query as Props;
return getTemplateMarketItemDetail(templateId);
const templateMarketItems: AppTemplateSchemaType[] = await getAppTemplatesAndLoadThem();
return templateMarketItems.find((item) => item.templateId === templateId);
}
export default NextAPI(handler);

View File

@@ -1,16 +1,53 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextAPI } from '@/service/middleware/entry';
import { TemplateMarketListItemType } from '@fastgpt/global/core/workflow/type';
import { getTemplateMarketItemList } from '@/service/core/app/template';
import { getAppTemplatesAndLoadThem } from '@fastgpt/templates/register';
import { AppTemplateSchemaType } from '@fastgpt/global/core/app/type';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { ApiRequestProps } from '@fastgpt/service/type/next';
export type ListParams = {
isQuickTemplate?: boolean;
type?: AppTypeEnum | 'all';
};
async function handler(
req: NextApiRequest,
req: ApiRequestProps<ListParams>,
res: NextApiResponse<any>
): Promise<TemplateMarketListItemType[]> {
): Promise<AppTemplateSchemaType[]> {
await authCert({ req, authToken: true });
return getTemplateMarketItemList();
const { isQuickTemplate = false, type = 'all' } = req.query;
const templateMarketItems = await getAppTemplatesAndLoadThem();
let filteredItems = templateMarketItems.filter((item) => {
if (!item.isActive) return false;
if (type === 'all') return true;
return item.type === type;
});
if (isQuickTemplate) {
if (filteredItems.some((item) => item.isQuickTemplate !== undefined)) {
filteredItems = filteredItems.filter((item) => item.isQuickTemplate);
} else {
filteredItems = filteredItems.slice(0, 3);
}
}
return filteredItems.map((item) => {
return {
templateId: item.templateId,
name: item.name,
intro: item.intro,
avatar: item.avatar,
tags: item.tags,
type: item.type,
author: item.author,
userGuide: item.userGuide,
workflow: {}
};
});
}
export default NextAPI(handler);

View File

@@ -11,12 +11,9 @@ import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
async function handler(
req: ApiRequestProps<PostPublishAppProps>,
res: NextApiResponse<any>
): Promise<{}> {
async function handler(req: ApiRequestProps<PostPublishAppProps>, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], chatConfig, isPublish, versionName } = req.body;
const { nodes = [], edges = [], chatConfig, isPublish, versionName, autoSave } = req.body;
const { app, tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
@@ -25,6 +22,15 @@ async function handler(
isPlugin: app.type === AppTypeEnum.plugin
});
if (autoSave) {
return MongoApp.findByIdAndUpdate(appId, {
modules: formatNodes,
edges,
chatConfig,
updateTime: new Date()
});
}
await mongoSessionRun(async (session) => {
// create version histories
const [{ _id }] = await MongoAppVersion.create(
@@ -70,8 +76,6 @@ async function handler(
}
);
});
return {};
}
export default NextAPI(handler);

View File

@@ -10,7 +10,7 @@ import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import {
concatHistories,
@@ -115,7 +115,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
})();
const limit = getMaxHistoryLimitFromNodes(nodes);
const [{ histories }, chatDetail, { user }] = await Promise.all([
const [{ histories }, chatDetail, { timezone, externalProvider }] = await Promise.all([
getChatItems({
appId,
chatId,
@@ -158,7 +158,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res,
requestOrigin: req.headers.origin,
mode: 'test',
user,
timezone,
externalProvider,
uid: tmbId,
runningAppInfo: {
@@ -204,7 +205,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userQuestion.value);
const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone)
? variables.cTime ?? getSystemTime(timezone)
: getChatTitleFromChatMessage(userQuestion);
const aiResponse: AIChatItemType & { dataId?: string } = {

View File

@@ -11,6 +11,7 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NextAPI } from '@/service/middleware/entry';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
async function handler(req: NextApiRequest, res: NextApiResponse) {
let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps;
@@ -20,7 +21,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// auth app permission
const [tmb, chat, app] = await Promise.all([
MongoTeamMember.findById(outLinkConfig.tmbId, '_id userId').populate('userId', 'avatar').lean(),
MongoTeamMember.findById(outLinkConfig.tmbId, '_id userId')
.populate<{ user: UserModelSchema }>('user', 'avatar')
.lean(),
MongoChat.findOne({ appId, chatId, shareId }).lean(),
MongoApp.findById(appId).lean()
]);
@@ -45,8 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
chatId,
appId: app._id,
title: chat?.title,
//@ts-ignore
userAvatar: tmb?.userId?.avatar,
userAvatar: tmb?.user?.avatar,
variables: chat?.variables,
app: {
chatConfig: getAppChatConfig({

View File

@@ -66,9 +66,9 @@ async function handler(
return {
type: DatasetSourceReadTypeEnum.apiFile,
sourceId: collection.apiFileId,
apiServer: collection.datasetId.apiServer,
feishuServer: collection.datasetId.feishuServer,
yuqueServer: collection.datasetId.yuqueServer
apiServer: collection.dataset.apiServer,
feishuServer: collection.dataset.feishuServer,
yuqueServer: collection.dataset.yuqueServer
};
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
@@ -90,12 +90,12 @@ async function handler(
return mongoSessionRun(async (session) => {
const { collectionId } = await createCollectionAndInsertData({
dataset: collection.datasetId,
dataset: collection.dataset,
rawText,
createCollectionParams: {
teamId: collection.teamId,
tmbId: collection.tmbId,
datasetId: collection.datasetId._id,
datasetId: collection.dataset._id,
name: collection.name,
type: collection.type,

View File

@@ -25,7 +25,7 @@ async function handler(req: NextApiRequest) {
// find all delete id
const collections = await findCollectionAndChild({
teamId,
datasetId: collection.datasetId._id,
datasetId: collection.datasetId,
collectionId,
fields: '_id teamId datasetId fileId metadata'
});

View File

@@ -37,7 +37,7 @@ async function handler(req: NextApiRequest): Promise<DatasetCollectionItemType>
...collection,
...getCollectionSourceData(collection),
tags: await collectionTagsToTagLabel({
datasetId: collection.datasetId._id,
datasetId: collection.datasetId,
tags: collection.tags
}),
permission,

View File

@@ -148,9 +148,9 @@ async function handler(
return collection.rawLink;
}
if (collection.type === DatasetCollectionTypeEnum.apiFile && collection.apiFileId) {
const apiServer = collection.datasetId.apiServer;
const feishuServer = collection.datasetId.feishuServer;
const yuqueServer = collection.datasetId.yuqueServer;
const apiServer = collection.dataset.apiServer;
const feishuServer = collection.dataset.feishuServer;
const yuqueServer = collection.dataset.yuqueServer;
if (apiServer) {
return useApiDatasetRequest({ apiServer }).getFilePreviewUrl({
@@ -170,11 +170,8 @@ async function handler(
return '';
}
if (collection.type === DatasetCollectionTypeEnum.externalFile) {
if (collection.externalFileId && collection.datasetId.externalReadUrl) {
return collection.datasetId.externalReadUrl.replace(
'{{fileId}}',
collection.externalFileId
);
if (collection.externalFileId && collection.dataset.externalReadUrl) {
return collection.dataset.externalReadUrl.replace('{{fileId}}', collection.externalFileId);
}
if (collection.externalFileUrl) {
return collection.externalFileUrl;

View File

@@ -100,7 +100,7 @@ async function handler(req: ApiRequestProps<UpdateDatasetCollectionParams>) {
const collectionTags = await createOrGetCollectionTags({
tags,
teamId,
datasetId: collection.datasetId._id,
datasetId: collection.datasetId,
session
});

View File

@@ -45,7 +45,7 @@ async function handler(req: NextApiRequest) {
// auth collection and get dataset
const [
{
datasetId: { _id: datasetId, vectorModel }
dataset: { _id: datasetId, vectorModel }
}
] = await Promise.all([getCollectionWithDataset(collectionId)]);

View File

@@ -31,7 +31,7 @@ async function handler(
const queryReg = new RegExp(`${replaceRegChars(searchText)}`, 'i');
const match = {
teamId,
datasetId: collection.datasetId._id,
datasetId: collection.datasetId,
collectionId,
...(searchText.trim()
? {

View File

@@ -1,9 +1,6 @@
/* push data to training queue */
import type { NextApiRequest, NextApiResponse } from 'next';
import type {
PushDatasetDataProps,
PushDatasetDataResponse
} from '@fastgpt/global/core/dataset/api.d';
import type { PushDatasetDataProps } from '@fastgpt/global/core/dataset/api.d';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { checkDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { predictDataLimitLength } from '@fastgpt/global/core/dataset/utils';
@@ -42,9 +39,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
...body,
teamId,
tmbId,
datasetId: collection.datasetId._id,
agentModel: collection.datasetId.agentModel,
vectorModel: collection.datasetId.vectorModel
datasetId: collection.datasetId,
agentModel: collection.dataset.agentModel,
vectorModel: collection.dataset.vectorModel
});
}

View File

@@ -12,7 +12,7 @@ async function handler(req: ApiRequestProps<UpdateDatasetDataProps>) {
// auth data permission
const {
collection: {
datasetId: { vectorModel }
dataset: { vectorModel }
},
teamId,
tmbId

View File

@@ -32,7 +32,7 @@ async function handler(
const queryReg = new RegExp(`${replaceRegChars(searchText)}`, 'i');
const match = {
teamId,
datasetId: collection.datasetId._id,
datasetId: collection.datasetId,
collectionId,
...(searchText.trim()
? {

View File

@@ -4,7 +4,7 @@ import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -37,7 +37,7 @@ async function handler(
]);
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(tmbId);
/* start process */
const { flowUsages, flowResponses, debugResponse, newVariables } = await dispatchWorkFlow({
@@ -50,7 +50,8 @@ async function handler(
tmbId
},
uid: tmbId,
user,
timezone,
externalProvider,
runtimeNodes: nodes,
runtimeEdges: edges,
variables,

View File

@@ -2,14 +2,39 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { UserType } from '@fastgpt/global/support/user/type';
export type TokenLoginQuery = {};
export type TokenLoginBody = {};
export type TokenLoginResponse = {};
export type TokenLoginResponse = UserType;
async function handler(
req: ApiRequestProps<TokenLoginBody, TokenLoginQuery>,
_res: ApiResponseType<any>
): Promise<TokenLoginResponse> {
const { tmbId } = await authCert({ req, authToken: true });
return getUserDetail({ tmbId });
const user = await getUserDetail({ tmbId });
// Remove sensitive information
if (user.team.lafAccount) {
user.team.lafAccount = {
appid: user.team.lafAccount.appid,
token: '',
pat: ''
};
}
if (user.team.openaiAccount) {
user.team.openaiAccount = {
key: '',
baseUrl: user.team.openaiAccount.baseUrl
};
}
if (user.team.externalWorkflowVariables) {
user.team.externalWorkflowVariables = Object.fromEntries(
Object.entries(user.team.externalWorkflowVariables).map(([key, value]) => [key, ''])
);
}
return user;
}
export default NextAPI(handler);

View File

@@ -1,7 +1,6 @@
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { UserUpdateParams } from '@/types/user';
import { getAIApi, openaiBaseUrl } from '@fastgpt/service/core/ai/config';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
/* update user info */
@@ -14,7 +13,7 @@ async function handler(
req: ApiRequestProps<UserAccountUpdateBody, UserAccountUpdateQuery>,
_res: ApiResponseType<any>
): Promise<UserAccountUpdateResponse> {
const { avatar, timezone, openaiAccount, lafAccount } = req.body;
const { avatar, timezone } = req.body;
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
@@ -22,25 +21,6 @@ async function handler(
throw new Error('can not find it');
}
const userId = tmb.userId;
// auth key
if (openaiAccount?.key) {
console.log('auth user openai key', openaiAccount?.key);
const baseUrl = openaiAccount?.baseUrl || openaiBaseUrl;
openaiAccount.baseUrl = baseUrl;
const ai = getAIApi({
userKey: openaiAccount
});
const response = await ai.chat.completions.create({
model: 'gpt-4o-mini',
max_tokens: 1,
messages: [{ role: 'user', content: 'hi' }]
});
if (response?.choices?.[0]?.message?.content === undefined) {
throw new Error('Key response is empty');
}
}
// 更新对应的记录
await MongoUser.updateOne(
@@ -49,9 +29,7 @@ async function handler(
},
{
...(avatar && { avatar }),
...(timezone && { timezone }),
openaiAccount: openaiAccount?.key ? openaiAccount : null,
lafAccount: lafAccount?.token ? lafAccount : null
...(timezone && { timezone })
}
);

View File

@@ -0,0 +1,50 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import axios from 'axios';
import { addLog } from '@fastgpt/service/common/system/log';
export type checkUsageQuery = { key: string };
export type checkUsageBody = {};
export type checkUsageResponse =
| {
total: number;
used: number;
}
| undefined;
async function handler(
req: ApiRequestProps<checkUsageBody, checkUsageQuery>,
res: ApiResponseType<any>
): Promise<checkUsageResponse> {
try {
const { key } = req.query;
const { tmb } = await authUserPer({ req, authToken: true, per: ReadPermissionVal });
const url = global.feConfigs.externalProviderWorkflowVariables?.find(
(item) => item.key === key
)?.url;
if (!url || !tmb.externalWorkflowVariables?.[key]) return undefined;
const { data } = await axios.get<checkUsageResponse>(url, {
headers: {
Authorization: `Bearer ${tmb.externalWorkflowVariables[key]}`
}
});
if (!data) return undefined;
return {
total: data.total || 0,
used: data.used || 0
};
} catch (error) {
addLog.debug('checkUsage error', { error });
}
}
export default NextAPI(handler);

View File

@@ -33,7 +33,7 @@ import {
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { updateApiKeyUsage } from '@fastgpt/service/support/openapi/tools';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { UserModelSchema } from '@fastgpt/global/support/user/type';
@@ -59,6 +59,7 @@ import { rewriteNodeOutputByHistories } from '@fastgpt/global/core/workflow/runt
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
import { ExternalProviderType } from '@fastgpt/global/core/workflow/runtime/type';
type FastGptWebChatProps = {
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
@@ -80,7 +81,8 @@ export type Props = ChatCompletionCreateParams &
type AuthResponseType = {
teamId: string;
tmbId: string;
user: UserModelSchema;
timezone: string;
externalProvider: ExternalProviderType;
app: AppSchema;
responseDetail?: boolean;
showNodeStatus?: boolean;
@@ -154,7 +156,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
teamId,
tmbId,
user,
timezone,
externalProvider,
app,
responseDetail,
authType,
@@ -269,7 +272,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
res,
requestOrigin: req.headers.origin,
mode: 'chat',
user,
timezone,
externalProvider,
runningAppInfo: {
id: String(app._id),
@@ -314,7 +318,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userQuestion.value);
const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone)
? variables.cTime ?? getSystemTime(timezone)
: getChatTitleFromChatMessage(userQuestion);
const aiResponse: AIChatItemType & { dataId?: string } = {
@@ -459,8 +463,18 @@ const authShareChat = async ({
shareId: string;
chatId?: string;
}): Promise<AuthResponseType> => {
const { teamId, tmbId, user, appId, authType, responseDetail, showNodeStatus, uid, sourceName } =
await authOutLinkChatStart(data);
const {
teamId,
tmbId,
timezone,
externalProvider,
appId,
authType,
responseDetail,
showNodeStatus,
uid,
sourceName
} = await authOutLinkChatStart(data);
const app = await MongoApp.findById(appId).lean();
if (!app) {
@@ -477,8 +491,9 @@ const authShareChat = async ({
sourceName,
teamId,
tmbId,
user,
app,
timezone,
externalProvider,
apikey: '',
authType,
responseAllData: false,
@@ -508,7 +523,7 @@ const authTeamSpaceChat = async ({
return Promise.reject('app is empty');
}
const [chat, { user }] = await Promise.all([
const [chat, { timezone, externalProvider }] = await Promise.all([
MongoChat.findOne({ appId, chatId }).lean(),
getUserChatInfoAndAuthTeamPoints(app.tmbId)
]);
@@ -520,8 +535,9 @@ const authTeamSpaceChat = async ({
return {
teamId,
tmbId: app.tmbId,
user,
app,
timezone,
externalProvider,
authType: AuthUserTypeEnum.outLink,
apikey: '',
responseAllData: false,
@@ -588,7 +604,7 @@ const authHeaderRequest = async ({
}
})();
const [{ user }, chat] = await Promise.all([
const [{ timezone, externalProvider }, chat] = await Promise.all([
getUserChatInfoAndAuthTeamPoints(tmbId),
MongoChat.findOne({ appId, chatId }).lean()
]);
@@ -605,7 +621,8 @@ const authHeaderRequest = async ({
return {
teamId,
tmbId,
user,
timezone,
externalProvider,
app,
apikey,
authType,

View File

@@ -9,7 +9,6 @@ import {
Button,
HStack
} from '@chakra-ui/react';
import { SmallAddIcon } from '@chakra-ui/icons';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
@@ -25,20 +24,18 @@ import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/ut
import SearchParamsTip from '@/components/core/dataset/SearchParamsTip';
import SettingLLMModel from '@/components/core/ai/SettingLLMModel';
import type { SettingAIDataType } from '@fastgpt/global/core/app/type.d';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import { TTSTypeEnum } from '@/web/core/app/constants';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { useI18n } from '@/web/context/I18n';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from './components/ToolSelect';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
const ToolSelectModal = dynamic(() => import('./components/ToolSelectModal'));
const TTSSelect = dynamic(() => import('@/components/core/app/TTSSelect'));
const QGConfig = dynamic(() => import('@/components/core/app/QGConfig'));
const WhisperConfig = dynamic(() => import('@/components/core/app/WhisperConfig'));
@@ -70,7 +67,6 @@ const EditForm = ({
const theme = useTheme();
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail } = useContextSelector(AppContext, (v) => v);
@@ -95,11 +91,6 @@ const EditForm = ({
onOpen: onOpenDatasetParams,
onClose: onCloseDatasetParams
} = useDisclosure();
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const formatVariables = useMemo(
() =>
@@ -278,67 +269,7 @@ const EditForm = ({
{/* tool choice */}
<Box {...BoxStyles}>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{appT('plugin_dispatch')}</FormLabel>
<QuestionTip ml={1} label={appT('plugin_dispatch_tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common:common.Choose')}
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => (
<MyTooltip key={item.id} label={item.intro}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2.5}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
<DeleteIcon
onClick={() => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
}}
/>
</Flex>
</MyTooltip>
))}
</Grid>
<ToolSelect appForm={appForm} setAppForm={setAppForm} />
</Box>
{/* File select */}
@@ -494,24 +425,6 @@ const EditForm = ({
}}
/>
)}
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
}));
}}
onClose={onCloseToolsSelect}
/>
)}
</>
);
};

View File

@@ -76,10 +76,12 @@ const Header = ({
const { runAsync: onClickSave, loading } = useRequest2(
async ({
isPublish,
versionName = formatTime2YMDHMS(new Date())
versionName = formatTime2YMDHMS(new Date()),
autoSave
}: {
isPublish?: boolean;
versionName?: string;
autoSave?: boolean;
}) => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
@@ -87,7 +89,8 @@ const Header = ({
edges,
chatConfig: appForm.chatConfig,
isPublish,
versionName
versionName,
autoSave
});
setPast((prevPast) =>
prevPast.map((item, index) =>
@@ -157,7 +160,7 @@ const Header = ({
if (isSaved) return;
try {
console.log('Leave auto save');
return onClickSave({ isPublish: false, versionName: t('app:auto_save') });
return onClickSave({ isPublish: false, autoSave: true });
} catch (error) {
console.error(error);
}

View File

@@ -0,0 +1,130 @@
import { Button, HStack, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box } from '@chakra-ui/react';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { childAppSystemKey } from './ToolSelectModal';
import { Controller, useForm } from 'react-hook-form';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import RenderPluginInput from '@/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
const ConfigToolModal = ({
configTool,
onCloseConfigTool,
onAddTool
}: {
configTool: AppSimpleEditFormType['selectedTools'][number];
onCloseConfigTool: () => void;
onAddTool: (tool: AppSimpleEditFormType['selectedTools'][number]) => void;
}) => {
const { t } = useTranslation();
const {
handleSubmit,
control,
formState: { errors }
} = useForm({
defaultValues: configTool
? configTool.inputs.reduce(
(acc, input) => {
acc[input.key] = input.value || input.defaultValue;
return acc;
},
{} as Record<string, any>
)
: {}
});
return (
<MyModal
isOpen
isCentered
title={t('common:core.app.ToolCall.Parameter setting')}
iconSrc="core/app/toolCall"
overflow={'auto'}
>
<ModalBody>
<HStack mb={4} spacing={1} fontSize={'sm'}>
<MyIcon name={'common/info'} w={'1.25rem'} />
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
{!!(configTool?.courseUrl || configTool?.userGuide) && (
<UseGuideModal
title={configTool?.name}
iconSrc={configTool?.avatar}
text={configTool?.userGuide}
link={configTool?.courseUrl}
>
{({ onClick }) => (
<Box cursor={'pointer'} color={'primary.500'} onClick={onClick}>
{t('app:workflow.Input guide')}
</Box>
)}
</UseGuideModal>
)}
</HStack>
{configTool.inputs
.filter(
(input) =>
!input.toolDescription &&
!childAppSystemKey.includes(input.key) &&
!input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) &&
!input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
)
.map((input) => {
return (
<Controller
key={input.key}
control={control}
name={input.key}
rules={{
validate: (value) => {
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
return value !== undefined;
}
return !!value;
}
}}
render={({ field: { onChange, value } }) => {
return (
<RenderPluginInput
value={value}
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}
/>
);
})}
</ModalBody>
<ModalFooter gap={6}>
<Button onClick={onCloseConfigTool} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button
variant={'primary'}
onClick={handleSubmit((data) => {
onAddTool({
...configTool,
inputs: configTool.inputs.map((input) => ({
...input,
value: data[input.key] ?? input.value
}))
});
onCloseConfigTool();
})}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ConfigToolModal);

View File

@@ -0,0 +1,157 @@
import { Box, Button, Flex, Grid, useDisclosure } from '@chakra-ui/react';
import React, { useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { SmallAddIcon } from '@chakra-ui/icons';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { theme } from '@fastgpt/web/styles/theme';
import DeleteIcon, { hoverDeleteStyles } from '@fastgpt/web/components/common/Icon/delete';
import ToolSelectModal, { childAppSystemKey } from './ToolSelectModal';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import ConfigToolModal from './ConfigToolModal';
import { getWebLLMModel } from '@/web/common/system/utils';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const ToolSelect = ({
appForm,
setAppForm
}: {
appForm: AppSimpleEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppSimpleEditFormType>>;
}) => {
const { t } = useTranslation();
const [configTool, setConfigTool] = useState<
AppSimpleEditFormType['selectedTools'][number] | null
>(null);
const {
isOpen: isOpenToolsSelect,
onOpen: onOpenToolsSelect,
onClose: onCloseToolsSelect
} = useDisclosure();
const selectedModel = getWebLLMModel(appForm.aiSettings.model);
return (
<>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} flex={1}>
<MyIcon name={'core/app/toolCall'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.app.Tool call')}</FormLabel>
<QuestionTip ml={1} label={t('app:plugin_dispatch_tip')} />
</Flex>
<Button
variant={'transparentBase'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
mr={'-5px'}
size={'sm'}
fontSize={'sm'}
onClick={onOpenToolsSelect}
>
{t('common:common.Choose')}
</Button>
</Flex>
<Grid
mt={appForm.selectedTools.length > 0 ? 2 : 0}
gridTemplateColumns={'repeat(2, minmax(0, 1fr))'}
gridGap={[2, 4]}
>
{appForm.selectedTools.map((item) => (
<MyTooltip key={item.id} label={item.intro}>
<Flex
overflow={'hidden'}
alignItems={'center'}
p={2.5}
bg={'white'}
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'}
borderRadius={'md'}
border={theme.borders.base}
_hover={{
...hoverDeleteStyles,
borderColor: 'primary.300'
}}
cursor={'pointer'}
onClick={() => {
if (
item.inputs
.filter((input) => !childAppSystemKey.includes(input.key))
.every(
(input) =>
input.toolDescription ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
)
) {
return;
}
setConfigTool(item);
}}
>
<Avatar src={item.avatar} w={'1.5rem'} h={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
<DeleteIcon
onClick={(e) => {
e.stopPropagation();
setAppForm((state: AppSimpleEditFormType) => ({
...state,
selectedTools: state.selectedTools.filter((tool) => tool.id !== item.id)
}));
}}
/>
</Flex>
</MyTooltip>
))}
</Grid>
{isOpenToolsSelect && (
<ToolSelectModal
selectedTools={appForm.selectedTools}
chatConfig={appForm.chatConfig}
selectedModel={selectedModel}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: [...state.selectedTools, e]
}));
}}
onRemoveTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.filter((item) => item.pluginId !== e.id)
}));
}}
onClose={onCloseToolsSelect}
/>
)}
{configTool && (
<ConfigToolModal
configTool={configTool}
onCloseConfigTool={() => setConfigTool(null)}
onAddTool={(e) => {
setAppForm((state) => ({
...state,
selectedTools: state.selectedTools.map((item) =>
item.pluginId === configTool.pluginId ? e : item
)
}));
}}
/>
)}
</>
);
};
export default React.memo(ToolSelect);

View File

@@ -1,54 +1,63 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
css,
Flex,
HStack,
Input,
InputGroup,
InputLeftElement,
ModalBody,
ModalFooter
Grid
} from '@chakra-ui/react';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import {
FlowNodeTemplateType,
NodeTemplateListItemType
NodeTemplateListItemType,
NodeTemplateListType
} from '@fastgpt/global/core/workflow/type/node.d';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { AddIcon } from '@chakra-ui/icons';
import {
getPluginGroups,
getPreviewPluginNode,
getSystemPlugTemplates,
getSystemPluginPaths
} from '@/web/core/app/api/plugin';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { Controller, useForm } from 'react-hook-form';
import { getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { getAppFolderPath } from '@/web/core/app/api/app';
import FolderPath from '@/components/common/folder/Path';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import CostTooltip from '@/components/core/app/plugin/CostTooltip';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import RenderPluginInput from '@/components/core/chat/ChatContainer/PluginRunBox/components/renderPluginInput';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../../context';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useMemoizedFn } from 'ahooks';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.d';
import { workflowStartNodeId } from '@/web/core/app/constants';
import ConfigToolModal from './ConfigToolModal';
type Props = {
selectedTools: FlowNodeTemplateType[];
chatConfig: AppSimpleEditFormType['chatConfig'];
selectedModel: LLMModelItemType;
onAddTool: (tool: FlowNodeTemplateType) => void;
onRemoveTool: (tool: NodeTemplateListItemType) => void;
};
const childAppSystemKey: string[] = [
export const childAppSystemKey: string[] = [
NodeInputKeyEnum.forbidStream,
NodeInputKeyEnum.history,
NodeInputKeyEnum.historyMaxAmount,
@@ -64,7 +73,7 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.teamPlugin);
const [templateType, setTemplateType] = useState(TemplateTypeEnum.systemPlugin);
const [parentId, setParentId] = useState<ParentIdType>('');
const [searchKey, setSearchKey] = useState('');
@@ -142,14 +151,14 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
<FillRowTabs
list={[
{
icon: 'core/modules/teamPlugin',
label: t('common:core.app.ToolCall.Team'),
value: TemplateTypeEnum.teamPlugin
icon: 'phoneTabbar/tool',
label: t('common:navbar.Toolkit'),
value: TemplateTypeEnum.systemPlugin
},
{
icon: 'core/modules/systemPlugin',
label: t('common:core.app.ToolCall.System'),
value: TemplateTypeEnum.systemPlugin
icon: 'core/modules/teamPlugin',
label: t('common:core.module.template.Team app'),
value: TemplateTypeEnum.teamPlugin
}
]}
py={'5px'}
@@ -162,35 +171,29 @@ const ToolSelectModal = ({ onClose, ...props }: Props & { onClose: () => void })
})
}
/>
<InputGroup w={300}>
<InputLeftElement h={'full'} alignItems={'center'} display={'flex'}>
<MyIcon name={'common/searchLight'} w={'16px'} color={'myGray.500'} ml={3} />
</InputLeftElement>
<Input
bg={'myGray.50'}
placeholder={t('common:plugin.Search plugin')}
<Box w={300}>
<SearchInput
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
placeholder={
templateType === TemplateTypeEnum.systemPlugin
? t('common:plugin.Search plugin')
: t('app:search_app')
}
/>
</InputGroup>
</Box>
</Box>
{/* route components */}
{!searchKey && parentId && (
<Flex mt={2} px={[3, 6]}>
<FolderPath
paths={paths}
FirstPathDom={null}
onClick={() => {
onUpdateParentId(null);
}}
/>
<FolderPath paths={paths} FirstPathDom={null} onClick={() => onUpdateParentId(null)} />
</Flex>
)}
<MyBox isLoading={isLoading} mt={2} px={[3, 6]} pb={3} flex={'1 0 0'} overflowY={'auto'}>
<RenderList
templates={templates}
isLoadingData={isLoading}
type={templateType}
setParentId={onUpdateParentId}
showCost={templateType === TemplateTypeEnum.systemPlugin}
{...props}
/>
</MyBox>
@@ -202,55 +205,116 @@ export default React.memo(ToolSelectModal);
const RenderList = React.memo(function RenderList({
templates,
selectedTools,
isLoadingData,
type,
onAddTool,
onRemoveTool,
setParentId,
showCost
selectedTools,
chatConfig,
selectedModel
}: Props & {
templates: NodeTemplateListItemType[];
isLoadingData: boolean;
type: TemplateTypeEnum;
setParentId: (parentId: ParentIdType) => any;
showCost?: boolean;
}) {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const [configTool, setConfigTool] = useState<FlowNodeTemplateType>();
const onCloseConfigTool = useCallback(() => setConfigTool(undefined), []);
const {
handleSubmit,
reset,
control,
formState: { errors }
} = useForm();
useEffect(() => {
if (configTool) {
const defaultValues = configTool.inputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
reset(defaultValues);
}
}, [configTool, reset]);
const { toast } = useToast();
const { runAsync: onClickAdd, loading: isLoading } = useRequest2(
async (template: NodeTemplateListItemType) => {
const res = await getPreviewPluginNode({ appId: template.id });
// All input is tool params
/* Invalid plugin check
1. Reference type. but not tool description;
2. Has dataset select
3. Has dynamic external data
*/
const oneFileInput =
res.inputs.filter((input) =>
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)
).length === 1;
const canUploadFile =
chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg;
const invalidFileInput = oneFileInput && !!canUploadFile;
if (
res.inputs.every((input) => childAppSystemKey.includes(input.key) || input.toolDescription)
res.inputs.some(
(input) =>
(input.renderTypeList.length === 1 &&
input.renderTypeList[0] === FlowNodeInputTypeEnum.reference &&
!input.toolDescription) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.selectDataset) ||
input.renderTypeList.includes(FlowNodeInputTypeEnum.addInputParam) ||
(input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) && !invalidFileInput)
)
) {
onAddTool(res);
return toast({
title: t('app:simple_tool_tips'),
status: 'warning'
});
}
// 判断是否可以直接添加工具,满足以下任一条件:
// 1. 有工具描述
// 2. 是模型选择类型
// 3. 是文件上传类型且:已开启文件上传、非必填、只有一个文件上传输入
const hasInputForm =
res.inputs.length > 0 &&
res.inputs.some((input) => {
if (input.toolDescription) {
return false;
}
if (input.key === NodeInputKeyEnum.forbidStream) {
return false;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.input)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.textarea)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.numberInput)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.switch)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.select)) {
return true;
}
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.JSONEditor)) {
return true;
}
return false;
});
// 构建默认表单数据
const defaultForm = {
...res,
inputs: res.inputs.map((input) => {
// 如果是模型选择类型,使用当前选中的模型
// if (input.renderTypeList.includes(FlowNodeInputTypeEnum.selectLLMModel)) {
// return {
// ...input,
// value: selectedModel.model
// };
// }
// 如果是文件上传类型,设置为从工作流开始节点获取用户文件
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect)) {
return {
...input,
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
};
}
return input;
})
};
if (hasInputForm) {
setConfigTool(defaultForm);
} else {
reset();
setConfigTool(res);
onAddTool(defaultForm);
}
},
{
@@ -258,165 +322,229 @@ const RenderList = React.memo(function RenderList({
}
);
return templates.length === 0 && !isLoadingData ? (
<EmptyTip text={t('common:core.app.ToolCall.No plugin')} />
) : (
<MyBox>
{templates.map((item, i) => {
const selected = selectedTools.some((tool) => tool.pluginId === item.id);
const { data: pluginGroups = [] } = useRequest2(getPluginGroups, {
manual: false
});
return (
<MyTooltip
key={item.id}
placement={'bottom'}
shouldWrapChildren={false}
label={
<Box>
<Flex alignItems={'center'}>
<Avatar
src={item.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={2} color={'myGray.900'}>
{t(item.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
{t(item.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{showCost && <CostTooltip cost={item.currentCost} />}
</Box>
const formatTemplatesArray = useMemo(() => {
const data = (() => {
if (type === TemplateTypeEnum.systemPlugin) {
return pluginGroups.map((group) => {
const copy: NodeTemplateListType = group.groupTypes.map((type) => ({
list: [],
type: type.typeId,
label: type.typeName
}));
templates.forEach((item) => {
const index = copy.findIndex((template) => template.type === item.templateType);
if (index === -1) return;
copy[index].list.push(item);
});
return {
label: group.groupName,
list: copy.filter((item) => item.list.length > 0)
};
});
}
return [
{
list: [
{
list: templates,
type: '',
label: ''
}
>
<Flex
alignItems={'center'}
position={'relative'}
p={[4, 5]}
_notLast={{
borderBottomWidth: '1px',
borderBottomColor: 'myGray.150'
}}
_hover={{
bg: 'myGray.50'
}}
],
label: ''
}
];
})();
return data.filter(({ list }) => list.length > 0);
}, [pluginGroups, templates, type]);
const gridStyle = useMemo(() => {
if (type === TemplateTypeEnum.teamPlugin) {
return {
gridTemplateColumns: ['1fr', '1fr'],
py: 2,
avatarSize: '2rem'
};
}
return {
gridTemplateColumns: ['1fr', '1fr 1fr'],
py: 3,
avatarSize: '1.75rem'
};
}, [type]);
const PluginListRender = useMemoizedFn(({ list = [] }: { list: NodeTemplateListType }) => {
return (
<>
{list.map((item, i) => {
return (
<Box
key={item.type}
css={css({
span: {
display: 'block'
}
})}
>
<Avatar src={item.avatar} w={'2rem'} objectFit={'contain'} borderRadius={'md'} />
<Box ml={3} flex={'1 0 0'} color={'myGray.900'}>
{t(item.name as any)}
</Box>
{item.author !== undefined && (
<Box fontSize={'xs'} mr={3}>
{`By ${item.author || feConfigs.systemTitle}`}
<Flex>
<Box fontSize={'sm'} my={2} fontWeight={'500'} flex={1} color={'myGray.900'}>
{t(item.label as any)}
</Box>
)}
{selected ? (
<Button
size={'sm'}
variant={'grayDanger'}
leftIcon={<MyIcon name={'delete'} w={'14px'} />}
onClick={() => onRemoveTool(item)}
>
{t('common:common.Remove')}
</Button>
) : item.isFolder ? (
<Button size={'sm'} variant={'whiteBase'} onClick={() => setParentId(item.id)}>
{t('common:common.Open')}
</Button>
) : (
<Button
size={'sm'}
variant={'whiteBase'}
leftIcon={<AddIcon fontSize={'10px'} />}
isLoading={isLoading}
onClick={() => onClickAdd(item)}
>
{t('common:common.Add')}
</Button>
)}
</Flex>
</MyTooltip>
);
})}
</Flex>
<Grid gridTemplateColumns={gridStyle.gridTemplateColumns} rowGap={2} columnGap={3}>
{item.list.map((template) => {
const selected = selectedTools.some((tool) => tool.pluginId === template.id);
{/* Plugin input config */}
{!!configTool && (
<MyModal
isOpen
isCentered
title={t('common:core.app.ToolCall.Parameter setting')}
iconSrc="core/app/toolCall"
overflow={'auto'}
>
<ModalBody>
<HStack mb={4} spacing={1} fontSize={'sm'}>
<MyIcon name={'common/info'} w={'1.25rem'} />
<Box flex={1}>{t('app:tool_input_param_tip')}</Box>
{configTool.courseUrl && (
<Box
cursor={'pointer'}
color={'primary.500'}
onClick={() => window.open(configTool.courseUrl, '_blank')}
>
{t('app:workflow.Input guide')}
</Box>
)}
</HStack>
{configTool.inputs
.filter((item) => !item.toolDescription && !childAppSystemKey.includes(item.key))
.map((input) => {
return (
<Controller
key={input.key}
control={control}
name={input.key}
rules={{
validate: (value) => {
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
return value !== undefined;
}
return !!value;
return (
<MyTooltip
key={template.id}
placement={'right'}
label={
<Box py={2}>
<Flex alignItems={'center'}>
<MyAvatar
src={template.avatar}
w={'1.75rem'}
objectFit={'contain'}
borderRadius={'sm'}
/>
<Box fontWeight={'bold'} ml={3} color={'myGray.900'}>
{t(template.name as any)}
</Box>
</Flex>
<Box mt={2} color={'myGray.500'} maxH={'100px'} overflow={'hidden'}>
{t(template.intro as any) || t('common:core.workflow.Not intro')}
</Box>
{type === TemplateTypeEnum.systemPlugin && (
<CostTooltip
cost={template.currentCost}
hasTokenFee={template.hasTokenFee}
/>
)}
</Box>
}
}}
render={({ field: { onChange, value } }) => {
return (
<RenderPluginInput
value={value}
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
>
<Flex
alignItems={'center'}
py={gridStyle.py}
px={3}
_hover={{ bg: 'myWhite.600' }}
borderRadius={'sm'}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
>
<MyAvatar
src={template.avatar}
w={gridStyle.avatarSize}
objectFit={'contain'}
borderRadius={'sm'}
flexShrink={0}
/>
);
}}
/>
);
})}
</ModalBody>
<ModalFooter gap={6}>
<Button onClick={onCloseConfigTool} variant={'whiteBase'}>
{t('common:common.Cancel')}
</Button>
<Button
variant={'primary'}
onClick={handleSubmit((data) => {
onAddTool({
...configTool,
inputs: configTool.inputs.map((input) => ({
...input,
value: data[input.key] ?? input.value
}))
});
onCloseConfigTool();
})}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
<Box
color={'myGray.900'}
fontWeight={'500'}
fontSize={'sm'}
flex={'1 0 0'}
ml={3}
className="textEllipsis"
>
{t(template.name as any)}
</Box>
{selected ? (
<Button
size={'sm'}
variant={'grayDanger'}
leftIcon={<MyIcon name={'delete'} w={'16px'} mr={-1} />}
onClick={() => onRemoveTool(template)}
px={2}
fontSize={'mini'}
>
{t('common:common.Remove')}
</Button>
) : template.isFolder ? (
<Button
size={'sm'}
variant={'whiteBase'}
leftIcon={<MyIcon name={'common/arrowRight'} w={'16px'} mr={-1.5} />}
onClick={() => setParentId(template.id)}
px={2}
fontSize={'mini'}
>
{t('common:common.Open')}
</Button>
) : (
<Button
size={'sm'}
variant={'primaryOutline'}
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} mr={-1.5} />}
isLoading={isLoading}
onClick={() => onClickAdd(template)}
px={2}
fontSize={'mini'}
>
{t('common:common.Add')}
</Button>
)}
</Flex>
</MyTooltip>
);
})}
</Grid>
</Box>
);
})}
</>
);
});
return templates.length === 0 ? (
<EmptyTip text={t('app:module.No Modules')} />
) : (
<Box flex={'1 0 0'} overflow={'overlay'}>
<Accordion defaultIndex={[0]} allowMultiple reduceMotion>
{formatTemplatesArray.length > 1 ? (
<>
{formatTemplatesArray.map(({ list, label }, index) => (
<AccordionItem key={index} border={'none'}>
<AccordionButton
fontSize={'sm'}
fontWeight={'500'}
color={'myGray.900'}
justifyContent={'space-between'}
alignItems={'center'}
borderRadius={'md'}
px={3}
>
{t(label as any)}
<AccordionIcon />
</AccordionButton>
<AccordionPanel py={0}>
<PluginListRender list={list} />
</AccordionPanel>
</AccordionItem>
))}
</>
) : (
<PluginListRender list={formatTemplatesArray?.[0]?.list} />
)}
</Accordion>
{!!configTool && (
<ConfigToolModal
configTool={configTool}
onCloseConfigTool={onCloseConfigTool}
onAddTool={onAddTool}
/>
)}
</MyBox>
</Box>
);
});

View File

@@ -50,6 +50,7 @@ import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/consta
import { getEditorVariables } from '../../../utils';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
const defaultFormBody = {
@@ -87,6 +88,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { feConfigs } = useSystemStore();
const { isOpen: isOpenCurl, onOpen: onOpenCurl, onClose: onCloseCurl } = useDisclosure();
const requestMethods = inputs.find(
@@ -168,6 +170,15 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
});
}, [nodeId, nodeList, edges, appDetail, t]);
const externalProviderWorkflowVariables = useMemo(() => {
return (
feConfigs?.externalProviderWorkflowVariables?.map((item) => ({
key: item.key,
label: item.name
})) || []
);
}, [feConfigs?.externalProviderWorkflowVariables]);
return (
<Box>
<Box mb={2} display={'flex'} justifyContent={'space-between'}>
@@ -235,7 +246,7 @@ const RenderHttpMethodAndUrl = React.memo(function RenderHttpMethodAndUrl({
}
value={requestUrl?.value || ''}
variableLabels={variables}
variables={variables}
variables={externalProviderWorkflowVariables}
onBlur={onBlurUrl}
onChange={onChangeUrl}
minH={40}
@@ -263,6 +274,7 @@ export function RenderHttpProps({
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { feConfigs } = useSystemStore();
const requestMethods = inputs.find((item) => item.key === NodeInputKeyEnum.httpMethod)?.value;
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
@@ -276,6 +288,15 @@ export function RenderHttpProps({
const headersLength = headers?.value?.length || 0;
// get variable
const externalProviderWorkflowVariables = useMemo(() => {
return (
feConfigs?.externalProviderWorkflowVariables?.map((item) => ({
key: item.key,
label: item.name
})) || []
);
}, [feConfigs?.externalProviderWorkflowVariables]);
const variables = useCreation(() => {
return getEditorVariables({
nodeId,
@@ -298,13 +319,15 @@ export function RenderHttpProps({
params,
headers,
jsonBody,
variables
variables,
externalProviderWorkflowVariables
}),
[headers, jsonBody, params, variables]
[externalProviderWorkflowVariables, headers, jsonBody, params, variables]
);
const Render = useMemo(() => {
const { params, headers, jsonBody, variables } = JSON.parse(stringifyVariables);
const { params, headers, jsonBody, variables, externalProviderWorkflowVariables } =
JSON.parse(stringifyVariables);
return (
<Box>
<Flex alignItems={'center'} mb={2} fontWeight={'medium'} color={'myGray.600'}>
@@ -347,18 +370,31 @@ export function RenderHttpProps({
headers &&
jsonBody &&
{
[TabEnum.params]: <RenderForm nodeId={nodeId} input={params} variables={variables} />,
[TabEnum.params]: (
<RenderForm
nodeId={nodeId}
input={params}
variables={variables}
externalProviderWorkflowVariables={externalProviderWorkflowVariables}
/>
),
[TabEnum.body]: (
<RenderBody
nodeId={nodeId}
variables={variables}
externalProviderWorkflowVariables={externalProviderWorkflowVariables}
jsonBody={jsonBody}
formBody={formBody}
typeInput={contentType}
/>
),
[TabEnum.headers]: (
<RenderForm nodeId={nodeId} input={headers} variables={variables} />
<RenderForm
nodeId={nodeId}
input={headers}
variables={variables}
externalProviderWorkflowVariables={externalProviderWorkflowVariables}
/>
)
}[selectedTab]}
</Box>
@@ -436,11 +472,16 @@ const RenderHttpTimeout = ({
const RenderForm = ({
nodeId,
input,
variables
variables,
externalProviderWorkflowVariables
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariableLabelPickerType[];
externalProviderWorkflowVariables: {
key: string;
label: string;
}[];
}) => {
const { t } = useTranslation();
const { toast } = useToast();
@@ -547,7 +588,7 @@ const RenderForm = ({
placeholder={t('common:textarea_variable_picker_tip')}
value={item.key}
variableLabels={variables}
variables={variables}
variables={externalProviderWorkflowVariables}
onBlur={(val) => {
handleKeyChange(index, val);
@@ -565,7 +606,7 @@ const RenderForm = ({
<HttpInput
placeholder={t('common:textarea_variable_picker_tip')}
value={item.value}
variables={variables}
variables={externalProviderWorkflowVariables}
variableLabels={variables}
onBlur={(val) => {
setList((prevList) =>
@@ -597,7 +638,16 @@ const RenderForm = ({
</TableContainer>
</Box>
);
}, [handleAddNewProps, handleKeyChange, input.key, list, t, updateTrigger, variables]);
}, [
externalProviderWorkflowVariables,
handleAddNewProps,
handleKeyChange,
input.key,
list,
t,
updateTrigger,
variables
]);
return Render;
};
@@ -606,13 +656,18 @@ const RenderBody = ({
jsonBody,
formBody,
typeInput,
variables
variables,
externalProviderWorkflowVariables
}: {
nodeId: string;
jsonBody: FlowNodeInputItemType;
formBody: FlowNodeInputItemType;
typeInput: FlowNodeInputItemType | undefined;
variables: EditorVariableLabelPickerType[];
externalProviderWorkflowVariables: {
key: string;
label: string;
}[];
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
@@ -684,7 +739,12 @@ const RenderBody = ({
</Flex>
{(typeInput?.value === ContentTypes.formData ||
typeInput?.value === ContentTypes.xWwwFormUrlencoded) && (
<RenderForm nodeId={nodeId} input={formBody} variables={variables} />
<RenderForm
nodeId={nodeId}
input={formBody}
variables={variables}
externalProviderWorkflowVariables={externalProviderWorkflowVariables}
/>
)}
{typeInput?.value === ContentTypes.json && (
<PromptEditor
@@ -729,7 +789,16 @@ const RenderBody = ({
)}
</Box>
);
}, [typeInput?.value, t, nodeId, formBody, variables, jsonBody, onChangeNode]);
}, [
typeInput?.value,
nodeId,
formBody,
variables,
externalProviderWorkflowVariables,
jsonBody,
t,
onChangeNode
]);
return Render;
};

View File

@@ -54,8 +54,8 @@ const NodeLaf = (props: NodeProps<FlowNodeItemType>) => {
const { userInfo, initUserInfo } = useUserStore();
const token = userInfo?.team.lafAccount?.token;
const appid = userInfo?.team.lafAccount?.appid;
const token = userInfo?.team?.lafAccount?.token;
const appid = userInfo?.team?.lafAccount?.appid;
const {
data: lafData,
@@ -322,7 +322,7 @@ const ConfigLaf = () => {
</Button>
{isOpenLafConfig && feConfigs?.lafEnv && (
<LafAccountModal defaultData={userInfo?.team.lafAccount} onClose={onCloseLafConfig} />
<LafAccountModal defaultData={userInfo?.team?.lafAccount} onClose={onCloseLafConfig} />
)}
</Center>
) : (

View File

@@ -190,6 +190,19 @@ const FieldEditModal = ({
}
}
if (data.renderTypeList[0] === FlowNodeInputTypeEnum.addInputParam) {
if (
!data.customInputConfig?.selectValueTypeList ||
!data.customInputConfig?.selectValueTypeList.length
) {
toast({
status: 'warning',
title: t('common:core.module.edit.Field Value Type Cannot Be Empty')
});
return;
}
}
// Get toolDescription and removes the types of some unusable tools
if (data.toolDescription && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
data.toolDescription = data.description;

View File

@@ -351,12 +351,6 @@ const InputTypeConfig = ({
{inputType === FlowNodeInputTypeEnum.addInputParam && (
<>
{/* <Flex alignItems={'center'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('common:core.module.Input Type')}
</FormLabel>
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
</Flex> */}
<Box>
<HStack mb={1}>
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import NodeCard from './render/NodeCard';
import { NodeProps } from 'reactflow';
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
@@ -35,6 +35,8 @@ import { useCreation, useMemoizedFn } from 'ahooks';
import { getEditorVariables } from '../../utils';
import { isArray } from 'lodash';
import { WorkflowNodeEdgeContext } from '../../context/workflowInitContext';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { inputs = [], nodeId } = data;
@@ -67,7 +69,17 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
t
});
}, [nodeId, nodeList, edges, appDetail, t]);
const { feConfigs } = useSystemStore();
const externalProviderWorkflowVariables = useMemo(() => {
return (
feConfigs?.externalProviderWorkflowVariables?.map((item) => ({
key: item.key,
label: item.name
})) || []
);
}, [feConfigs?.externalProviderWorkflowVariables]);
// Node inputs
const updateList = useMemo(
() =>
(inputs.find((input) => input.key === NodeInputKeyEnum.updateList)
@@ -104,7 +116,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
(item) => item.renderType === updateItem.renderType
);
const handleUpdate = (newValue?: ReferenceValueType | string) => {
const onUpdateNewValue = (newValue?: ReferenceValueType | string) => {
if (typeof newValue === 'string') {
onUpdateList(
updateList.map((update, i) =>
@@ -134,7 +146,11 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
return {
...update,
value: ['', ''],
valueType,
valueType: getRefData({
variable: value as ReferenceItemValueType,
nodeList,
chatConfig: appDetail.chatConfig
}).valueType,
variable: value as ReferenceItemValueType
};
}
@@ -206,7 +222,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
nodeId={nodeId}
variable={updateItem.value}
valueType={valueType}
onSelect={handleUpdate}
onSelect={onUpdateNewValue}
/>
);
}
@@ -218,9 +234,10 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
<Box w={'300px'}>
<PromptEditor
value={inputValue || ''}
onChange={handleUpdate}
onChange={onUpdateNewValue}
showOpenModal={false}
variableLabels={variables}
variables={[...variables, ...externalProviderWorkflowVariables]}
minH={100}
/>
</Box>
@@ -228,20 +245,18 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput value={Number(inputValue) || 0}>
<NumberInputField bg="white" onChange={(e) => handleUpdate(e.target.value)} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
<MyNumberInput
bg={'white'}
value={Number(inputValue) || 0}
onChange={(e) => onUpdateNewValue(String(e || 0))}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
defaultChecked={inputValue === 'true'}
onChange={(e) => handleUpdate(String(e.target.checked))}
onChange={(e) => onUpdateNewValue(String(e.target.checked))}
/>
);
}
@@ -250,9 +265,10 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
<Box w={'300px'}>
<PromptEditor
value={inputValue || ''}
onChange={handleUpdate}
onChange={onUpdateNewValue}
showOpenModal={false}
variableLabels={variables}
variables={[...variables, ...externalProviderWorkflowVariables]}
minH={100}
/>
</Box>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { Box, Button, Card, Flex, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
@@ -24,11 +24,11 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils';
import { WholeResponseContent } from '@/components/core/chat/components/WholeResponseModal';
import { getDocPath } from '@/web/common/system/doc';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -136,6 +136,7 @@ const NodeCard = (props: Props) => {
} = useConfirm({
content: t('workflow:Confirm_sync_node')
});
const hasNewVersion = nodeTemplate && nodeTemplate.version !== node?.version;
const { runAsync: onClickSyncVersion } = useRequest2(
@@ -166,7 +167,7 @@ const NodeCard = (props: Props) => {
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{/* avatar and name */}
<Flex alignItems={'center'} mb={intro ? 1 : 0}>
<Flex alignItems={'center'} mb={1}>
{node?.flowNodeType !== FlowNodeTypeEnum.stopTool && (
<Flex
alignItems={'center'}
@@ -271,15 +272,19 @@ const NodeCard = (props: Props) => {
{!!nodeTemplate?.diagram && node?.courseUrl && (
<Box bg={'myGray.300'} w={'1px'} h={'12px'} ml={1} mr={0.5} />
)}
{node?.courseUrl && !hasNewVersion && (
<MyTooltip label={t('workflow:Node.Open_Node_Course')}>
<MyIconButton
ml={1}
icon="book"
color={'primary.600'}
onClick={() => window.open(getDocPath(node.courseUrl || ''), '_blank')}
/>
</MyTooltip>
{!!(node?.courseUrl || nodeTemplate?.userGuide) && !hasNewVersion && (
<UseGuideModal
title={nodeTemplate?.name}
iconSrc={nodeTemplate?.avatar}
text={nodeTemplate?.userGuide}
link={nodeTemplate?.courseUrl}
>
{({ onClick }) => (
<MyTooltip label={t('workflow:Node.Open_Node_Course')}>
<MyIconButton ml={1} icon="book" color={'primary.600'} onClick={onClick} />
</MyTooltip>
)}
</UseGuideModal>
)}
</Flex>
<NodeIntro nodeId={nodeId} intro={intro} />
@@ -291,6 +296,7 @@ const NodeCard = (props: Props) => {
);
}, [
node?.flowNodeType,
node?.courseUrl,
showToolHandle,
nodeId,
isFolded,
@@ -301,7 +307,10 @@ const NodeCard = (props: Props) => {
onOpenConfirmSync,
onClickSyncVersion,
nodeTemplate?.diagram,
node?.courseUrl,
nodeTemplate?.userGuide,
nodeTemplate?.name,
nodeTemplate?.avatar,
nodeTemplate?.courseUrl,
intro,
menuForbid,
nodeList,

View File

@@ -8,8 +8,9 @@ import { useCreation } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context';
import { getEditorVariables } from '../../../../../utils';
import { WorkflowNodeEdgeContext } from '../../../../../context/workflowInitContext';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const TextareaRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const edges = useContextSelector(WorkflowNodeEdgeContext, (v) => v.edges);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
@@ -17,6 +18,8 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { feConfigs } = useSystemStore();
// get variable
const variables = useCreation(() => {
return getEditorVariables({
@@ -28,6 +31,15 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
});
}, [nodeId, nodeList, edges, appDetail, t]);
const externalProviderWorkflowVariables = useMemo(() => {
return (
feConfigs?.externalProviderWorkflowVariables?.map((item) => ({
key: item.key,
label: item.name
})) || []
);
}, [feConfigs?.externalProviderWorkflowVariables]);
const onChange = useCallback(
(e: string) => {
onChangeNode({
@@ -47,7 +59,7 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
return (
<PromptEditor
variableLabels={variables}
variables={variables}
variables={[...variables, ...externalProviderWorkflowVariables]}
title={t(item.label as any)}
maxLength={item.maxLength}
minH={100}
@@ -57,7 +69,16 @@ const TextareaRender = ({ inputs = [], item, nodeId }: RenderInputProps) => {
onChange={onChange}
/>
);
}, [item.label, item.maxLength, item.placeholder, item.value, onChange, t, variables]);
}, [
externalProviderWorkflowVariables,
item.label,
item.maxLength,
item.placeholder,
item.value,
onChange,
t,
variables
]);
return Render;
};

View File

@@ -1,5 +1,5 @@
import { useDebounceEffect, useMemoizedFn } from 'ahooks';
import React, { ReactNode, useMemo, useRef, useState } from 'react';
import { useDebounceEffect, useLockFn, useMemoizedFn } from 'ahooks';
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import { WorkflowInitContext, WorkflowNodeEdgeContext } from './workflowInitContext';
import { WorkflowContext } from '.';
@@ -14,6 +14,7 @@ import {
Input_Template_Node_Height,
Input_Template_Node_Width
} from '@fastgpt/global/core/workflow/template/input';
import { isProduction } from '@fastgpt/global/common/system/constants';
type WorkflowStatusContextType = {
isSaved: boolean;
@@ -67,20 +68,28 @@ const WorkflowStatusContextProvider = ({ children }: { children: ReactNode }) =>
// Lead check before unload
const flowData2StoreData = useContextSelector(WorkflowContext, (v) => v.flowData2StoreData);
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const autoSaveFn = useLockFn(async () => {
if (isSaved || !leaveSaveSign.current) return;
console.log('Leave auto save');
const data = flowData2StoreData();
if (!data || data.nodes.length === 0) return;
await onSaveApp({
...data,
isPublish: false,
chatConfig: appDetail.chatConfig,
autoSave: true
});
});
useEffect(() => {
return () => {
if (isProduction) {
autoSaveFn();
}
};
}, []);
useBeforeunload({
tip: t('common:core.common.tip.leave page'),
callback: async () => {
if (isSaved || !leaveSaveSign.current) return;
console.log('Leave auto save');
const data = flowData2StoreData();
if (!data || data.nodes.length === 0) return;
await onSaveApp({
...data,
isPublish: false,
versionName: t('app:unusual_leave_auto_save'),
chatConfig: appDetail.chatConfig
});
}
callback: autoSaveFn
});
const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (state) => state.onNodesChange);

View File

@@ -13,6 +13,7 @@ import { useDisclosure } from '@chakra-ui/react';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
const InfoModal = dynamic(() => import('./InfoModal'));
const TagsEditModal = dynamic(() => import('./TagsEditModal'));
@@ -150,13 +151,20 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
});
const { runAsync: onSaveApp } = useRequest2(async (data: PostPublishAppProps) => {
await postPublishApp(appId, data);
setAppDetail((state) => ({
...state,
...data,
modules: data.nodes || state.modules
}));
reloadAppLatestVersion();
try {
await postPublishApp(appId, data);
setAppDetail((state) => ({
...state,
...data,
modules: data.nodes || state.modules
}));
reloadAppLatestVersion();
} catch (error: any) {
if (error.statusText == AppErrEnum.unExist) {
return;
}
return Promise.reject(error);
}
});
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef } from 'react';
import { Box, Flex, Button, ModalFooter, ModalBody, Input, Grid, Card } from '@chakra-ui/react';
import { Box, Flex, Button, ModalBody, Input, Grid, Card } 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';
@@ -25,6 +25,7 @@ import {
getTemplateMarketItemList
} from '@/web/core/app/api/template';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppTemplateSchemaType } from '@fastgpt/global/core/app/type';
type FormType = {
avatar: string;
@@ -70,13 +71,12 @@ const CreateModal = ({
}
});
const typeData = typeMap.current[type];
const { data: templateList = [] } = useRequest2(getTemplateMarketItemList, {
manual: false
});
const filterTemplates = useMemo(() => {
return templateList.filter((item) => item.type === type).slice(0, 3);
}, [templateList, type]);
const { data: templateList = [], loading: isRequestTemplates } = useRequest2(
() => getTemplateMarketItemList({ isQuickTemplate: true, type }),
{
manual: false
}
);
const { register, setValue, watch, handleSubmit } = useForm<FormType>({
defaultValues: {
@@ -127,13 +127,12 @@ const CreateModal = ({
});
}
const templateDetail = await getTemplateMarketItemDetail({ templateId: templateId });
const templateDetail = await getTemplateMarketItemDetail(templateId);
return postCreateApp({
parentId,
avatar: data.avatar || templateDetail.avatar,
name: data.name,
type: templateDetail.type,
type: templateDetail.type as AppTypeEnum,
modules: templateDetail.workflow.nodes || [],
edges: templateDetail.workflow.edges || [],
chatConfig: templateDetail.workflow.chatConfig
@@ -158,7 +157,7 @@ const CreateModal = ({
onClose={onClose}
isCentered={!isPc}
maxW={['90vw', '40rem']}
isLoading={isCreating}
isLoading={isCreating || isRequestTemplates}
>
<ModalBody px={9} pb={8}>
<Box color={'myGray.800'} fontWeight={'bold'}>
@@ -205,7 +204,7 @@ const CreateModal = ({
</Flex>
<Grid
userSelect={'none'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)']}
gridTemplateColumns={templateList.length > 0 ? ['repeat(1,1fr)', 'repeat(2,1fr)'] : '1fr'}
gridGap={[2, 4]}
>
<Card
@@ -231,9 +230,9 @@ const CreateModal = ({
{typeData.emptyCreateText}
</Box>
</Card>
{filterTemplates.map((item) => (
{templateList.map((item) => (
<Card
key={item.id}
key={item.templateId}
p={4}
borderRadius={'md'}
borderWidth={'1px'}
@@ -271,17 +270,17 @@ const CreateModal = ({
h={'full'}
left={0}
right={0}
bottom={0}
bottom={1}
height={'40px'}
bg={'white'}
zIndex={1}
>
<Button
variant={'whiteBase'}
h={'1.75rem'}
borderRadius={'xl'}
h={6}
borderRadius={'sm'}
w={'40%'}
onClick={handleSubmit((data) => onclickCreate(data, item.id))}
onClick={handleSubmit((data) => onclickCreate(data, item.templateId))}
>
{t('app:templateMarket.Use')}
</Button>

View File

@@ -12,16 +12,16 @@ import {
ModalOverlay
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import MyBox from '@fastgpt/web/components/common/MyBox';
import AppTypeTag from './TypeTag';
import { AppTemplateTypeEnum, AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
getTemplateMarketItemDetail,
getTemplateMarketItemList
getTemplateMarketItemList,
getTemplateTagList
} from '@/web/core/app/api/template';
import { TemplateMarketListItemType } from '@fastgpt/global/core/workflow/type';
import { postCreateApp } from '@/web/core/app/api';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
@@ -34,9 +34,18 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput/index'
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { webPushTrack } from '@/web/common/middle/tracks/utils';
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
import { i18nT } from '@fastgpt/web/i18n/utils';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
type TemplateAppType = AppTypeEnum | 'all';
const recommendTag: TemplateTypeSchemaType = {
typeId: AppTemplateTypeEnum.recommendation,
typeName: i18nT('app:templateMarket.templateTags.Recommendation'),
typeOrder: 0
};
const TemplateMarketModal = ({
defaultType = 'all',
onClose
@@ -46,63 +55,57 @@ const TemplateMarketModal = ({
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const templateTags = [
{
id: AppTemplateTypeEnum.recommendation,
label: t('app:templateMarket.templateTags.Recommendation')
},
{
id: AppTemplateTypeEnum.writing,
label: t('app:templateMarket.templateTags.Writing')
},
{
id: AppTemplateTypeEnum.imageGeneration,
label: t('app:templateMarket.templateTags.Image_generation')
},
{
id: AppTemplateTypeEnum.webSearch,
label: t('app:templateMarket.templateTags.Web_search')
},
{
id: AppTemplateTypeEnum.roleplay,
label: t('app:templateMarket.templateTags.Roleplay')
},
{
id: AppTemplateTypeEnum.officeServices,
label: t('app:templateMarket.templateTags.Office_services')
}
];
const { parentId } = useContextSelector(AppListContext, (v) => v);
const router = useRouter();
const { isPc } = useSystem();
const [currentTag, setCurrentTag] = useState(templateTags[0].id);
const [currentTag, setCurrentTag] = useState<string>(AppTemplateTypeEnum.recommendation);
const [currentAppType, setCurrentAppType] = useState<TemplateAppType>(defaultType);
const [currentSearch, setCurrentSearch] = useState('');
const { data: templateList = [], loading: isLoadingTemplates } = useRequest2(
getTemplateMarketItemList,
() => getTemplateMarketItemList({ type: currentAppType }),
{
manual: false,
refreshDeps: [currentAppType]
}
);
const { data: templateTags = [], loading: isLoadingTags } = useRequest2(
() => getTemplateTagList().then((res) => [recommendTag, ...res]),
{
manual: false
}
);
// Batch by tags
const filterTemplateTags = useMemo(() => {
return templateTags
.map((tag) => {
const templates = templateList.filter((template) => template.tags.includes(tag.typeId));
return {
...tag,
templates
};
})
.filter((item) => item.templates.length > 0);
}, [templateList, templateTags]);
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
async (id: string) => {
const templateDetail = await getTemplateMarketItemDetail({ templateId: id });
async (template: AppTemplateSchemaType) => {
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
return postCreateApp({
parentId,
avatar: templateDetail.avatar,
name: templateDetail.name,
type: templateDetail.type,
avatar: template.avatar,
name: template.name,
type: template.type as AppTypeEnum,
modules: templateDetail.workflow.nodes || [],
edges: templateDetail.workflow.edges || [],
chatConfig: templateDetail.workflow.chatConfig
}).then((res) => {
webPushTrack.useAppTemplate({
id,
name: templateDetail.name
id: res,
name: template.name
});
return res;
@@ -122,9 +125,9 @@ const TemplateMarketModal = ({
async () => {
let firstVisibleTitle: any = null;
templateTags
.map((type) => type.id)
.forEach((type: string) => {
filterTemplateTags
.map((type) => type.typeId)
.forEach((type) => {
const element = document.getElementById(type);
if (!element) return;
@@ -144,17 +147,18 @@ const TemplateMarketModal = ({
}
},
{
throttleWait: 100
throttleWait: 100,
refreshDeps: [filterTemplateTags.length]
}
);
const TemplateCard = useCallback(
({ item }: { item: TemplateMarketListItemType }) => {
({ item }: { item: AppTemplateSchemaType }) => {
const { t } = useTranslation();
return (
<MyBox
key={item.id}
key={item.templateId}
lineHeight={1.5}
h="100%"
pt={4}
@@ -181,7 +185,7 @@ const TemplateMarketModal = ({
{item.name}
</Box>
<Box mr={'-1rem'}>
<AppTypeTag type={item.type} />
<AppTypeTag type={item.type as AppTypeEnum} />
</Box>
</HStack>
<Box
@@ -198,7 +202,7 @@ const TemplateMarketModal = ({
<Box w={'full'} fontSize={'mini'}>
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
<Box
<Flex
className="buttons"
display={'none'}
justifyContent={'center'}
@@ -209,32 +213,49 @@ const TemplateMarketModal = ({
h={'full'}
left={0}
right={0}
bottom={0}
bottom={1}
height={'40px'}
bg={'white'}
zIndex={1}
gap={2}
>
{((item.userGuide?.type === 'markdown' && item.userGuide?.content) ||
(item.userGuide?.type === 'link' && item.userGuide?.link)) && (
<UseGuideModal
title={item.name}
iconSrc={item.avatar}
text={item.userGuide?.content}
link={item.userGuide?.link}
>
{({ onClick }) => (
<Button variant={'whiteBase'} h={6} rounded={'sm'} onClick={onClick}>
{t('app:templateMarket.template_guide')}
</Button>
)}
</UseGuideModal>
)}
<Button
variant={'whiteBase'}
h={'1.75rem'}
borderRadius={'xl'}
w={'40%'}
onClick={() => onUseTemplate(item.id)}
h={6}
rounded={'sm'}
onClick={() => onUseTemplate(item)}
>
{t('app:templateMarket.Use')}
</Button>
</Box>
</Flex>
</Box>
</MyBox>
);
},
[onUseTemplate]
[feConfigs.systemTitle, onUseTemplate]
);
const isLoading = isLoadingTags || isLoadingTemplates || isCreating;
return (
<Modal
isOpen={true}
onClose={() => onClose && onClose()}
onClose={onClose}
autoFocus={false}
blockScrollOnMount={false}
closeOnOverlayClick={false}
@@ -263,11 +284,11 @@ const TemplateMarketModal = ({
<Box flex={'1'} />
<MySelect
<MySelect<TemplateAppType>
h={'8'}
value={currentAppType}
onchange={(value) => {
setCurrentAppType(value as AppTypeEnum | 'all');
setCurrentAppType(value);
}}
bg={'myGray.100'}
minW={'7rem'}
@@ -302,25 +323,24 @@ const TemplateMarketModal = ({
</Box>
)}
</ModalHeader>
<MyBox isLoading={isCreating || isLoadingTemplates} flex={'1 0 0'} overflow={'overlay'}>
<MyBox isLoading={isLoading} flex={'1 0 0'} h="0">
<ModalBody
h={'100%'}
display={'flex'}
bg={'myGray.100'}
overflow={'auto'}
gap={5}
onScroll={handleScroll}
px={0}
pt={5}
>
{isPc && (
<Flex pl={5} flexDirection={'column'} gap={3}>
{templateTags.map((item) => {
{filterTemplateTags.map((item) => {
return (
<Box
key={item.id}
key={item.typeId}
cursor={'pointer'}
{...(item.id === currentTag && !currentSearch
{...(item.typeId === currentTag && !currentSearch
? {
bg: 'primary.1',
color: 'primary.600'
@@ -336,14 +356,14 @@ const TemplateMarketModal = ({
fontSize={'sm'}
fontWeight={500}
onClick={() => {
setCurrentTag(item.id);
const anchor = document.getElementById(item.id);
setCurrentTag(item.typeId);
const anchor = document.getElementById(item.typeId);
if (anchor) {
anchor.scrollIntoView({ behavior: 'auto', block: 'start' });
}
}}
>
{item.label}
{t(item.typeName as any)}
</Box>
);
})}
@@ -370,7 +390,15 @@ const TemplateMarketModal = ({
</Flex>
)}
<Box pl={[3, 0]} pr={[3, 5]} pt={1} flex={'1'} h={'100%'} overflow={'auto'}>
<Box
pl={[3, 0]}
pr={[3, 5]}
pt={1}
flex={'1'}
h={'100%'}
overflow={'auto'}
onScroll={handleScroll}
>
{currentSearch ? (
<>
<Box fontSize={'lg'} color={'myGray.900'} mb={4}>
@@ -396,7 +424,7 @@ const TemplateMarketModal = ({
pb={5}
>
{templates.map((item) => (
<TemplateCard key={item.id} item={item} />
<TemplateCard key={item.templateId} item={item} />
))}
</Grid>
);
@@ -407,26 +435,17 @@ const TemplateMarketModal = ({
</>
) : (
<>
{templateTags.map((item) => {
const currentTemplates = templateList
?.filter((template) => template.tags.includes(item.id))
.filter((template) => {
if (currentAppType === 'all') return true;
return template.type === currentAppType;
});
if (currentTemplates.length === 0) return null;
{filterTemplateTags.map((item) => {
return (
<Box key={item.id}>
<Box key={item.typeId}>
<Box
id={item.id}
id={item.typeId}
fontSize={['md', 'lg']}
color={'myGray.900'}
mb={4}
fontWeight={500}
>
{item.label}
{t(item.typeName as any)}
</Box>
<Grid
gridTemplateColumns={[
@@ -440,8 +459,8 @@ const TemplateMarketModal = ({
alignItems={'stretch'}
pb={5}
>
{currentTemplates.map((item) => (
<TemplateCard key={item.id} item={item} />
{item.templates.map((item) => (
<TemplateCard key={item.templateId} item={item} />
))}
</Grid>
</Box>

View File

@@ -16,7 +16,6 @@ import { useTranslation } from 'next-i18next';
import { getInitOutLinkChatInfo } from '@/web/core/chat/api';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { OutLinkWithAppType } from '@fastgpt/global/support/outLink/type';
import { addLog } from '@fastgpt/service/common/system/log';
import { connectToDatabase } from '@/service/mongo';
import NextHead from '@/components/common/NextHead';
@@ -37,6 +36,7 @@ import ChatRecordContextProvider, {
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
import { AppSchema } from '@fastgpt/global/core/app/type';
const CustomPluginRunBox = dynamic(() => import('./components/CustomPluginRunBox'));
@@ -361,15 +361,14 @@ export async function getServerSideProps(context: any) {
const app = await (async () => {
try {
await connectToDatabase();
const app = (await MongoOutLink.findOne(
return MongoOutLink.findOne(
{
shareId
},
'appId showRawSource showNodeStatus'
)
.populate('appId', 'name avatar intro')
.lean()) as OutLinkWithAppType;
return app;
.populate<{ associatedApp: AppSchema }>('associatedApp', 'name avatar intro')
.lean();
} catch (error) {
addLog.error('getServerSideProps', error);
return undefined;
@@ -378,10 +377,10 @@ export async function getServerSideProps(context: any) {
return {
props: {
appId: String(app?.appId?._id) ?? '',
appName: app?.appId?.name ?? 'AI',
appAvatar: app?.appId?.avatar ?? '',
appIntro: app?.appId?.intro ?? 'AI',
appId: String(app?.appId) ?? '',
appName: app?.associatedApp?.name ?? 'AI',
appAvatar: app?.associatedApp?.avatar ?? '',
appIntro: app?.associatedApp?.intro ?? 'AI',
showRawSource: app?.showRawSource ?? false,
showNodeStatus: app?.showNodeStatus ?? false,
shareId: shareId ?? '',

View File

@@ -158,11 +158,11 @@ const InputDataModal = ({
const maxToken = useMemo(() => {
const vectorModel =
vectorModelList.find((item) => item.model === collection.datasetId.vectorModel) ||
vectorModelList.find((item) => item.model === collection.dataset.vectorModel) ||
vectorModelList[0];
return vectorModel?.maxToken || 3000;
}, [collection.datasetId.vectorModel, vectorModelList]);
}, [collection.dataset.vectorModel, vectorModelList]);
// import new data
const { mutate: sureImportData, isLoading: isImporting } = useRequest({

View File

@@ -1,14 +1,13 @@
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react';
import { Box, Flex, HStack } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import React, { useState } from 'react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Markdown from '@/components/Markdown';
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
import { PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
const PluginCard = ({
item,
@@ -20,8 +19,6 @@ const PluginCard = ({
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const [currentPlugin, setCurrentPlugin] = useState<NodeTemplateListItemType | null>(null);
const type = groups.reduce<string | undefined>((acc, group) => {
const foundType = group.groupTypes.find((type) => type.typeId === item.templateType);
return foundType ? foundType.typeName : acc;
@@ -82,51 +79,33 @@ const PluginCard = ({
<Flex w={'full'} fontSize={'mini'}>
<Flex flex={1}>
{item.instructions && (
<Flex
color={'primary.700'}
alignItems={'center'}
gap={1}
cursor={'pointer'}
onClick={() => setCurrentPlugin(item)}
_hover={{ bg: 'myGray.100' }}
{(item.instructions || item.courseUrl) && (
<UseGuideModal
title={item.name}
iconSrc={item.avatar}
text={item.instructions}
link={item.courseUrl}
>
<MyIcon name={'book'} w={'14px'} />
{t('app:plugin.Instructions')}
</Flex>
{({ onClick }) => (
<Flex
color={'primary.700'}
alignItems={'center'}
gap={1}
cursor={'pointer'}
onClick={onClick}
_hover={{ bg: 'myGray.100' }}
>
<MyIcon name={'book'} w={'14px'} />
{t('app:plugin.Instructions')}
</Flex>
)}
</UseGuideModal>
)}
</Flex>
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
</Flex>
{currentPlugin && (
<InstructionModal currentPlugin={currentPlugin} onClose={() => setCurrentPlugin(null)} />
)}
</MyBox>
);
};
const InstructionModal = ({
currentPlugin,
onClose
}: {
currentPlugin: NodeTemplateListItemType;
onClose: () => void;
}) => {
return (
<MyModal
isOpen
iconSrc={currentPlugin.avatar}
title={currentPlugin.name}
onClose={onClose}
minW={'600px'}
>
<ModalBody>
<Box border={'base'} borderRadius={'10px'} p={4} minH={'500px'}>
<Markdown source={currentPlugin.instructions} />
</Box>
</ModalBody>
</MyModal>
);
};
export default React.memo(PluginCard);

View File

@@ -4,14 +4,14 @@ import type { FastGPTFeConfigsType } from '@fastgpt/global/common/system/types/i
import type { FastGPTConfigFileType } from '@fastgpt/global/common/system/types/index.d';
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
import { getFastGPTConfigFromDB } from '@fastgpt/service/common/system/config/controller';
import { PluginTemplateType } from '@fastgpt/global/core/plugin/type';
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
import { isProduction } from '@fastgpt/global/common/system/constants';
import { initFastGPTConfig } from '@fastgpt/service/common/system/tools';
import json5 from 'json5';
import { SystemPluginTemplateItemType } from '@fastgpt/global/core/workflow/type';
import { defaultGroup } from '@fastgpt/web/core/workflow/constants';
import { defaultGroup, defaultTemplateTypes } from '@fastgpt/web/core/workflow/constants';
import { MongoPluginGroups } from '@fastgpt/service/core/app/plugin/pluginGroupSchema';
import { MongoTemplateTypes } from '@fastgpt/service/core/app/templates/templateTypeSchema';
export const readConfigData = (name: string) => {
const splitName = name.split('.');
@@ -164,7 +164,7 @@ function getSystemPlugin() {
global.communityPlugins = fileTemplates;
}
export async function initSystemPlugins() {
export async function initSystemPluginGroups() {
try {
const { groupOrder, ...restDefaultGroup } = defaultGroup;
await MongoPluginGroups.updateOne(
@@ -182,3 +182,27 @@ export async function initSystemPlugins() {
console.error('Error initializing system plugins:', error);
}
}
export async function initAppTemplateTypes() {
try {
await Promise.all(
defaultTemplateTypes.map((templateType) => {
const { typeOrder, ...rest } = templateType;
return MongoTemplateTypes.updateOne(
{
typeId: templateType.typeId
},
{
$set: rest
},
{
upsert: true
}
);
})
);
} catch (error) {
console.error('Error initializing system templates:', error);
}
}

View File

@@ -4,11 +4,14 @@ import { createDatasetTrainingMongoWatch } from '@/service/core/dataset/training
import { MongoSystemConfigs } from '@fastgpt/service/common/system/config/schema';
import { MongoSystemPlugin } from '@fastgpt/service/core/app/plugin/systemPluginSchema';
import { debounce } from 'lodash';
import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema';
import { getAppTemplatesAndLoadThem } from '@fastgpt/templates/register';
export const startMongoWatch = async () => {
reloadConfigWatch();
refetchSystemPlugins();
createDatasetTrainingMongoWatch();
refetchAppTemplates();
};
const reloadConfigWatch = () => {
@@ -38,3 +41,18 @@ const refetchSystemPlugins = () => {
}, 500)
);
};
const refetchAppTemplates = () => {
const changeStream = MongoAppTemplate.watch();
changeStream.on(
'change',
debounce(async (change) => {
setTimeout(() => {
try {
getAppTemplatesAndLoadThem(true);
} catch (error) {}
}, 5000);
}, 500)
);
};

View File

@@ -1,48 +0,0 @@
import { isProduction } from '@fastgpt/global/common/system/constants';
import { readdirSync, readFileSync } from 'fs';
import path from 'path';
// Get template from memory or file system
const loadTemplateMarketItems = async () => {
if (isProduction && global.appMarketTemplates) return global.appMarketTemplates;
const templatesDir = path.join(process.cwd(), 'public', 'appMarketTemplates');
const templateNames = readdirSync(templatesDir);
global.appMarketTemplates = templateNames.map((name) => {
try {
const filePath = path.join(templatesDir, name, 'template.json');
const fileContent = readFileSync(filePath, 'utf-8');
const data = JSON.parse(fileContent);
return {
id: name,
...data
};
} catch (error) {
console.error(`Error fetching template ${name}:`, error);
return null;
}
});
global.appMarketTemplates.sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0));
return global.appMarketTemplates;
};
export const getTemplateMarketItemDetail = async (id: string) => {
const templateMarketItems = await loadTemplateMarketItems();
return templateMarketItems.find((item) => item.id === id);
};
export const getTemplateMarketItemList = async () => {
const templateMarketItems = await loadTemplateMarketItems();
return templateMarketItems.map((item) => ({
id: item.id,
name: item.name,
avatar: item.avatar,
intro: item.intro,
author: item.author,
tags: item.tags,
type: item.type
}));
};

View File

@@ -1,4 +1,4 @@
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -39,7 +39,7 @@ export const getScheduleTriggerApp = async () => {
if (!app.scheduledTriggerConfig) return;
// random delay 0 ~ 60s
await delay(Math.floor(Math.random() * 60 * 1000));
const { user } = await getUserChatInfoAndAuthTeamPoints(app.tmbId);
const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(app.tmbId);
// Get app latest version
const { nodes, edges, chatConfig } = await getAppLatestVersion(app._id, app);
@@ -57,7 +57,8 @@ export const getScheduleTriggerApp = async () => {
const { flowUsages, assistantResponses, flowResponses } = await retryFn(() => {
return dispatchWorkFlow({
chatId,
user,
timezone,
externalProvider,
mode: 'chat',
runningAppInfo: {
id: String(app._id),

View File

@@ -7,7 +7,7 @@ import type {
} from '@fastgpt/global/support/outLink/api.d';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
@@ -57,7 +57,7 @@ export async function authOutLinkChatStart({
const { outLinkConfig, appId } = await authOutLinkValid({ shareId });
// check ai points and chat limit
const [{ user }, { uid }] = await Promise.all([
const [{ timezone, externalProvider }, { uid }] = await Promise.all([
getUserChatInfoAndAuthTeamPoints(outLinkConfig.tmbId),
authOutLinkChatLimit({ outLink: outLinkConfig, ip, outLinkUid, question })
]);
@@ -69,7 +69,8 @@ export async function authOutLinkChatStart({
authType: AuthUserTypeEnum.token,
responseDetail: outLinkConfig.responseDetail,
showNodeStatus: outLinkConfig.showNodeStatus,
user,
timezone,
externalProvider,
appId,
uid
};

View File

@@ -1,7 +1,4 @@
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
import { TeamMemberWithUserSchema } from '@fastgpt/global/support/user/team/type';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { checkTeamAIPoints } from '@fastgpt/service/support/permission/teamLimit';
import { GET } from '@fastgpt/service/common/api/plusRequest';
import {
AuthTeamTagTokenProps,
@@ -9,20 +6,6 @@ import {
} from '@fastgpt/global/support/user/team/tag';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
export async function getUserChatInfoAndAuthTeamPoints(tmbId: string) {
const tmb = (await MongoTeamMember.findById(tmbId, 'teamId userId').populate(
'userId',
'timezone openaiAccount'
)) as TeamMemberWithUserSchema;
if (!tmb) return Promise.reject(UserErrEnum.unAuthUser);
await checkTeamAIPoints(tmb.teamId);
return {
user: tmb.userId
};
}
export function authTeamTagToken(data: AuthTeamTagTokenProps) {
return GET<AuthTokenFromTeamDomainResponse['data']>('/support/user/team/tag/authTeamToken', data);
}

View File

@@ -1,11 +1,8 @@
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { UserModelSchema } from '@fastgpt/global/support/user/type';
import { LafAccountType } from '@fastgpt/global/support/user/team/type.d';
export interface UserUpdateParams {
balance?: number;
avatar?: string;
timezone?: string;
openaiAccount?: UserModelSchema['openaiAccount'];
lafAccount?: LafAccountType;
}

View File

@@ -39,7 +39,7 @@ function checkMaxQuantity({ url, maxQuantity }: { url: string; maxQuantity?: num
if (item) {
if (item.amount >= maxQuantity) {
item.sign?.abort?.();
!item.sign?.signal?.aborted && item.sign?.abort?.();
maxQuantityMap[url] = {
amount: 1,
sign: controller

View File

@@ -8,7 +8,7 @@ import { AppLogsListItemType } from '@/types/app';
import { PagingData } from '@/types';
/**
* 获取模型列表
* 获取应用列表
*/
export const getMyApps = (data?: ListAppBody) =>
POST<AppListItemType[]>('/core/app/list', data, {
@@ -16,23 +16,23 @@ export const getMyApps = (data?: ListAppBody) =>
});
/**
* 创建一个模型
* 创建一个应用
*/
export const postCreateApp = (data: CreateAppBody) => POST<string>('/core/app/create', data);
export const getMyAppsByTags = (data: {}) => POST(`/proApi/core/chat/team/getApps`, data);
/**
* 根据 ID 删除模型
* 根据 ID 删除应用
*/
export const delAppById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
/**
* 根据 ID 获取模型
* 根据 ID 获取应用
*/
export const getAppDetailById = (id: string) => GET<AppDetailType>(`/core/app/detail?appId=${id}`);
/**
* 根据 ID 更新模型
* 根据 ID 更新应用
*/
export const putAppById = (id: string, data: AppUpdateParams) =>
PUT(`/core/app/update?appId=${id}`, data);

View File

@@ -1,11 +1,17 @@
import { ListParams } from '@/pages/api/core/app/template/list';
import { GET } from '@/web/common/api/request';
import {
TemplateMarketItemType,
TemplateMarketListItemType
} from '@fastgpt/global/core/workflow/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
import { defaultTemplateTypes } from '@fastgpt/web/core/workflow/constants';
export const getTemplateMarketItemList = () =>
GET<TemplateMarketListItemType[]>('/core/app/template/list');
export const getTemplateMarketItemList = (data: ListParams) =>
GET<AppTemplateSchemaType[]>(`/core/app/template/list`, data);
export const getTemplateMarketItemDetail = (data: { templateId: string }) =>
GET<TemplateMarketItemType>(`/core/app/template/detail`, data);
export const getTemplateMarketItemDetail = (templateId: string) =>
GET<AppTemplateSchemaType>(`/core/app/template/detail?templateId=${templateId}`);
export const getTemplateTagList = () => {
return useSystemStore.getState()?.feConfigs?.isPlus
? GET<TemplateTypeSchemaType[]>('/proApi/core/app/template/getTemplateTypes')
: Promise.resolve(defaultTemplateTypes);
};

View File

@@ -56,3 +56,5 @@ export enum TTSTypeEnum {
web = 'web',
model = 'model'
}
export const workflowStartNodeId = 'workflowStartNodeId';

View File

@@ -39,6 +39,7 @@ import {
Input_Template_File_Link_Prompt,
Input_Template_UserChatInput
} from '@fastgpt/global/core/workflow/template/input';
import { workflowStartNodeId } from './constants';
type WorkflowType = {
nodes: StoreNodeItemType[];
@@ -50,7 +51,6 @@ export function form2AppWorkflow(
): WorkflowType & {
chatConfig: AppChatConfigType;
} {
const workflowStartNodeId = 'workflowStartNodeId';
const datasetNodeId = 'iKBoX2vIzETU';
const aiChatNodeId = '7BdojPlukIQw';

View File

@@ -32,7 +32,8 @@ export const defaultCollectionDetail: DatasetCollectionItemType = {
_id: '',
teamId: '',
tmbId: '',
datasetId: {
datasetId: '',
dataset: {
_id: '',
parentId: '',
userId: '',