mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 22:03:54 +00:00
4.8.8 test fix (#2143)
* perf: transcriptions api * perf: variable picker tip * perf: variable picker tip * perf: chat select app * feat: router to app detail * perf: variable avoid space * perf: variable picker * perf: doc2x icon and params * perf: sandbox support countToken * feat: sandbox support delay and countToken
This commit is contained in:
@@ -35,22 +35,22 @@
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "9.1.7",
|
||||
"hyperdown": "^2.4.29",
|
||||
"i18next": "23.11.5",
|
||||
"immer": "^9.0.19",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mermaid": "^10.2.3",
|
||||
"nanoid": "^4.0.1",
|
||||
"next": "14.2.5",
|
||||
"json5": "^2.2.3",
|
||||
"next-i18next": "15.3.0",
|
||||
"nextjs-node-loader": "^1.1.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "18.3.1",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "7.43.1",
|
||||
"i18next": "23.11.5",
|
||||
"next-i18next": "15.3.0",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
|
@@ -0,0 +1,16 @@
|
||||
import { Box, HStack, StackProps } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
const VariableTip = (props: StackProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<HStack fontSize={'xs'} spacing={1} {...props}>
|
||||
<MyIcon name={'common/info'} w={'0.9rem'} transform={'translateY(1px)'} />
|
||||
<Box>{t('common:textarea_variable_picker_tip')}</Box>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariableTip;
|
@@ -127,6 +127,7 @@ const DatasetParamsModal = ({
|
||||
>
|
||||
<ModalBody flex={'auto'} overflow={'auto'}>
|
||||
<LightRowTabs<SearchSettingTabEnum>
|
||||
width={'100%'}
|
||||
mb={3}
|
||||
list={[
|
||||
{
|
||||
|
@@ -299,6 +299,8 @@ const VariableEdit = ({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitEdit(({ variable }) => {
|
||||
variable.key = variable.key.trim();
|
||||
|
||||
// check select
|
||||
if (variable.type === VariableInputEnum.select) {
|
||||
const enums = variable.enums.filter((item) => item.value);
|
||||
|
@@ -157,6 +157,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
{!hideTabs && (
|
||||
<Box>
|
||||
<LightRowTabs
|
||||
w={'100%'}
|
||||
list={list}
|
||||
value={currentTab}
|
||||
inlineStyles={{ pt: 0 }}
|
||||
|
@@ -3,15 +3,14 @@ import { jsonRes } from '@fastgpt/service/common/response';
|
||||
import { getUploadModel } from '@fastgpt/service/common/file/multer';
|
||||
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
|
||||
import fs from 'fs';
|
||||
import { getAIApi } from '@fastgpt/service/core/ai/config';
|
||||
import { pushWhisperUsage } from '@/service/support/wallet/usage/push';
|
||||
import { authChatCert } from '@/service/support/permission/auth/chat';
|
||||
import { MongoApp } from '@fastgpt/service/core/app/schema';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions';
|
||||
|
||||
const upload = getUploadModel({
|
||||
maxSize: 2
|
||||
maxSize: 20
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
@@ -45,6 +44,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
|
||||
// auth role
|
||||
const { teamId, tmbId } = await authChatCert({ req, authToken: true });
|
||||
|
||||
// auth app
|
||||
// const app = await MongoApp.findById(appId, 'modules').lean();
|
||||
// if (!app) {
|
||||
@@ -54,11 +54,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
// throw new Error('Whisper is not open in the app');
|
||||
// }
|
||||
|
||||
const ai = getAIApi();
|
||||
|
||||
const result = await ai.audio.transcriptions.create({
|
||||
file: fs.createReadStream(file.path),
|
||||
model: global.whisperModel.model
|
||||
const result = await aiTranscriptions({
|
||||
model: global.whisperModel.model,
|
||||
fileStream: fs.createReadStream(file.path)
|
||||
});
|
||||
|
||||
pushWhisperUsage({
|
||||
|
@@ -34,6 +34,7 @@ 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';
|
||||
|
||||
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
|
||||
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
|
||||
@@ -57,7 +58,8 @@ const LabelStyles: BoxProps = {
|
||||
w: ['60px', '100px'],
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
fontSize: 'xs'
|
||||
fontSize: 'sm',
|
||||
color: 'myGray.900'
|
||||
};
|
||||
|
||||
const EditForm = ({
|
||||
@@ -162,10 +164,13 @@ const EditForm = ({
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box mt={3}>
|
||||
<HStack {...LabelStyles}>
|
||||
<Box mt={4}>
|
||||
<HStack {...LabelStyles} w={'100%'}>
|
||||
<Box>{t('common:core.ai.Prompt')}</Box>
|
||||
<QuestionTip label={t('common:core.app.tip.chatNodeSystemPromptTip')} />
|
||||
|
||||
<Box flex={1} />
|
||||
<VariableTip color={'myGray.500'} />
|
||||
</HStack>
|
||||
<Box mt={1}>
|
||||
<PromptEditor
|
||||
|
@@ -309,6 +309,7 @@ export function RenderHttpProps({
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<LightRowTabs<TabEnum>
|
||||
width={'100%'}
|
||||
list={[
|
||||
{ label: <RenderPropsItem text="Params" num={paramsLength} />, value: TabEnum.params },
|
||||
...(!['GET', 'DELETE'].includes(requestMethods)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { Box, Flex, HStack } from '@chakra-ui/react';
|
||||
|
||||
import NodeInputSelect from '@fastgpt/web/components/core/workflow/NodeInputSelect';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
@@ -10,6 +10,10 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pages/app/detail/components/WorkflowComponents/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import VariableTip from '@/components/common/Textarea/MyTextarea/VariableTip';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
@@ -68,6 +72,14 @@ const InputLabel = ({ nodeId, input }: Props) => {
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Variable picker tip */}
|
||||
{input.renderTypeList[input.selectedTypeIndex ?? 0] === FlowNodeInputTypeEnum.textarea && (
|
||||
<>
|
||||
<Box flex={1} />
|
||||
<VariableTip transform={'translateY(2px)'} />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, [
|
||||
|
@@ -27,50 +27,45 @@ const ChatHeader = ({
|
||||
chatData,
|
||||
history,
|
||||
showHistory,
|
||||
onRoute2AppDetail,
|
||||
apps
|
||||
apps,
|
||||
onRouteToAppDetail
|
||||
}: {
|
||||
history: ChatItemType[];
|
||||
showHistory?: boolean;
|
||||
onRoute2AppDetail?: () => void;
|
||||
apps?: AppListItemType[];
|
||||
chatData: InitChatResponse;
|
||||
apps?: AppListItemType[];
|
||||
onRouteToAppDetail?: () => void;
|
||||
}) => {
|
||||
const isPlugin = chatData.app.type === AppTypeEnum.plugin;
|
||||
const { isPc } = useSystem();
|
||||
return (
|
||||
<>
|
||||
{isPc && isPlugin ? null : (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={[3, 5]}
|
||||
minH={['46px', '60px']}
|
||||
borderBottom={'sm'}
|
||||
color={'myGray.900'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{isPc ? (
|
||||
<PcHeader
|
||||
title={chatData.title}
|
||||
chatModels={chatData.app.chatModels}
|
||||
history={history}
|
||||
/>
|
||||
) : (
|
||||
<MobileHeader
|
||||
apps={apps}
|
||||
appId={chatData.appId}
|
||||
go2AppDetail={onRoute2AppDetail}
|
||||
name={chatData.app.name}
|
||||
avatar={chatData.app.avatar}
|
||||
showHistory={showHistory}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* control */}
|
||||
{!isPlugin && <ToolMenu history={history} />}
|
||||
</Flex>
|
||||
return isPc && isPlugin ? null : (
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={[3, 5]}
|
||||
minH={['46px', '60px']}
|
||||
borderBottom={'sm'}
|
||||
color={'myGray.900'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{isPc ? (
|
||||
<>
|
||||
<PcHeader title={chatData.title} chatModels={chatData.app.chatModels} history={history} />
|
||||
<Box flex={1} />
|
||||
</>
|
||||
) : (
|
||||
<MobileHeader
|
||||
apps={apps}
|
||||
appId={chatData.appId}
|
||||
name={chatData.app.name}
|
||||
avatar={chatData.app.avatar}
|
||||
showHistory={showHistory}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
{/* control */}
|
||||
{!isPlugin && <ToolMenu history={history} onRouteToAppDetail={onRouteToAppDetail} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,7 +83,6 @@ const MobileDrawer = ({
|
||||
app = 'app'
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useSystem();
|
||||
const router = useRouter();
|
||||
const isTeamChat = router.pathname === '/chat/team';
|
||||
const [currentTab, setCurrentTab] = useState<TabEnum>(TabEnum.recently);
|
||||
@@ -103,129 +97,120 @@ const MobileDrawer = ({
|
||||
);
|
||||
}, []);
|
||||
const { onChangeAppId } = useContextSelector(ChatContext, (v) => v);
|
||||
const onclickApp = (id: string) => {
|
||||
onChangeAppId(id);
|
||||
onCloseDrawer();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'45px'}
|
||||
w={'100vw'}
|
||||
h={'calc(100% - 45px)'}
|
||||
background={'rgba(0, 0, 0, 0.2)'}
|
||||
left={0}
|
||||
zIndex={5}
|
||||
onClick={() => {
|
||||
onCloseDrawer();
|
||||
}}
|
||||
>
|
||||
{/* menu */}
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'45px'}
|
||||
w={'100vw'}
|
||||
h={'calc(100% - 45px)'}
|
||||
background={'rgba(0, 0, 0, 0.2)'}
|
||||
left={0}
|
||||
zIndex={5}
|
||||
onClick={() => {
|
||||
onCloseDrawer();
|
||||
}}
|
||||
px={[2, 5]}
|
||||
padding={2}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
background={'white'}
|
||||
position={'relative'}
|
||||
>
|
||||
{/* menu */}
|
||||
<Box
|
||||
w={'100vw'}
|
||||
px={[2, 5]}
|
||||
padding={2}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
background={'white'}
|
||||
position={'relative'}
|
||||
>
|
||||
{!isPc && appId && (
|
||||
<LightRowTabs<TabEnum>
|
||||
flex={'1 0 0'}
|
||||
width={isTeamChat ? '30%' : '60%'}
|
||||
mr={10}
|
||||
inlineStyles={{
|
||||
px: 1
|
||||
}}
|
||||
list={[
|
||||
...(isTeamChat
|
||||
? [{ label: t('common:all_apps'), value: TabEnum.recently }]
|
||||
: [
|
||||
{ label: t('common:core.chat.Recent use'), value: TabEnum.recently },
|
||||
{ label: t('common:all_apps'), value: TabEnum.app }
|
||||
])
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
width={'100vw'}
|
||||
height={'auto'}
|
||||
minH={'10vh'}
|
||||
maxH={'60vh'}
|
||||
overflow={'auto'}
|
||||
background={'white'}
|
||||
zIndex={3}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
borderRadius={'0 0 10px 10px'}
|
||||
position={'relative'}
|
||||
padding={3}
|
||||
pt={0}
|
||||
pb={4}
|
||||
>
|
||||
{/* history */}
|
||||
{currentTab === TabEnum.recently && (
|
||||
<>
|
||||
{Array.isArray(apps) &&
|
||||
apps.map((item) => (
|
||||
<Flex justify={'center'} key={item._id}>
|
||||
<Flex
|
||||
py={2.5}
|
||||
px={2}
|
||||
width={'100%'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
{...(item._id === appId
|
||||
? {
|
||||
backgroundColor: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
onClick: () => onChangeAppId(item._id)
|
||||
})}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'24px'} />
|
||||
<Box ml={2} className={'textEllipsis'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{currentTab === TabEnum.app && !isPc && (
|
||||
<>
|
||||
<SelectOneResource
|
||||
value={appId}
|
||||
onSelect={(id) => {
|
||||
if (!id) return;
|
||||
onChangeAppId(id);
|
||||
}}
|
||||
server={getAppList}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<LightRowTabs<TabEnum>
|
||||
gap={3}
|
||||
inlineStyles={{
|
||||
px: 2
|
||||
}}
|
||||
list={[
|
||||
...(isTeamChat
|
||||
? [{ label: t('common:all_apps'), value: TabEnum.recently }]
|
||||
: [
|
||||
{ label: t('common:core.chat.Recent use'), value: TabEnum.recently },
|
||||
{ label: t('common:all_apps'), value: TabEnum.app }
|
||||
])
|
||||
]}
|
||||
value={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
<Box
|
||||
width={'100%'}
|
||||
minH={'10vh'}
|
||||
background={'white'}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
borderRadius={'0 0 10px 10px'}
|
||||
position={'relative'}
|
||||
py={3}
|
||||
pt={0}
|
||||
pb={4}
|
||||
h={'65vh'}
|
||||
>
|
||||
{/* history */}
|
||||
{currentTab === TabEnum.recently && (
|
||||
<Box px={3} overflow={'auto'} h={'100%'}>
|
||||
{Array.isArray(apps) &&
|
||||
apps.map((item) => (
|
||||
<Flex justify={'center'} key={item._id}>
|
||||
<Flex
|
||||
py={2.5}
|
||||
px={2}
|
||||
width={'100%'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
{...(item._id === appId
|
||||
? {
|
||||
backgroundColor: 'primary.50 !important',
|
||||
color: 'primary.600'
|
||||
}
|
||||
: {
|
||||
onClick: () => onclickApp(item._id)
|
||||
})}
|
||||
>
|
||||
<Avatar src={item.avatar} w={'24px'} />
|
||||
<Box ml={2} className={'textEllipsis'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{currentTab === TabEnum.app && (
|
||||
<SelectOneResource
|
||||
value={appId}
|
||||
onSelect={(id) => {
|
||||
if (!id) return;
|
||||
onclickApp(id);
|
||||
}}
|
||||
server={getAppList}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileHeader = ({
|
||||
showHistory,
|
||||
go2AppDetail,
|
||||
name,
|
||||
avatar,
|
||||
appId,
|
||||
apps
|
||||
}: {
|
||||
showHistory?: boolean;
|
||||
go2AppDetail?: () => void;
|
||||
avatar: string;
|
||||
name: string;
|
||||
apps?: AppListItemType[];
|
||||
appId: string;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const router = useRouter();
|
||||
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
|
||||
const { isOpen: isOpenDrawer, onToggle: toggleDrawer, onClose: onCloseDrawer } = useDisclosure();
|
||||
@@ -237,22 +222,21 @@ const MobileHeader = ({
|
||||
<MyIcon name={'menu'} w={'20px'} h={'20px'} color={'myGray.900'} onClick={onOpenSlider} />
|
||||
)}
|
||||
<Flex px={3} alignItems={'center'} flex={'1 0 0'} w={0} justifyContent={'center'}>
|
||||
<Avatar src={avatar} w={'16px'} />
|
||||
<Box ml={1} className="textEllipsis" onClick={go2AppDetail}>
|
||||
{name}
|
||||
</Box>
|
||||
{isShareChat ? null : (
|
||||
<MyIcon
|
||||
_active={{ transform: 'scale(0.9)' }}
|
||||
name={'core/chat/chevronSelector'}
|
||||
w={'20px'}
|
||||
h={'20px'}
|
||||
color={isOpenDrawer ? 'primary.600' : 'myGray.900'}
|
||||
onClick={toggleDrawer}
|
||||
/>
|
||||
)}
|
||||
<Flex alignItems={'center'} onClick={toggleDrawer}>
|
||||
<Avatar src={avatar} w={'1rem'} />
|
||||
<Box ml={1} className="textEllipsis">
|
||||
{name}
|
||||
</Box>
|
||||
{isShareChat ? null : (
|
||||
<MyIcon
|
||||
name={'core/chat/chevronSelector'}
|
||||
w={'1.25rem'}
|
||||
color={isOpenDrawer ? 'primary.600' : 'myGray.900'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{!isPc && isOpenDrawer && !isShareChat && (
|
||||
{isOpenDrawer && !isShareChat && (
|
||||
<MobileDrawer apps={apps} appId={appId} onCloseDrawer={onCloseDrawer} />
|
||||
)}
|
||||
</>
|
||||
@@ -292,7 +276,6 @@ const PcHeader = ({
|
||||
</MyTag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<Box flex={1} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -27,7 +27,6 @@ const ChatHistorySlider = ({
|
||||
appId,
|
||||
appName,
|
||||
appAvatar,
|
||||
apps = [],
|
||||
confirmClearText,
|
||||
onDelHistory,
|
||||
onClearHistory,
|
||||
@@ -37,7 +36,6 @@ const ChatHistorySlider = ({
|
||||
appId?: string;
|
||||
appName: string;
|
||||
appAvatar: string;
|
||||
apps?: AppListItemType[];
|
||||
confirmClearText: string;
|
||||
onDelHistory: (e: { chatId: string }) => void;
|
||||
onClearHistory: () => void;
|
||||
@@ -46,10 +44,9 @@ const ChatHistorySlider = ({
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const isTeamChat = router.pathname === '/chat/team';
|
||||
const isUserChatPage = router.pathname === '/chat';
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo } = useUserStore();
|
||||
@@ -103,7 +100,7 @@ const ChatHistorySlider = ({
|
||||
whiteSpace={'nowrap'}
|
||||
>
|
||||
{isPc && (
|
||||
<MyTooltip label={canRouteToDetail ? appT('app_detail') : ''} offset={[0, 0]}>
|
||||
<MyTooltip label={canRouteToDetail ? t('app:app_detail') : ''} offset={[0, 0]}>
|
||||
<Flex
|
||||
pt={5}
|
||||
pb={2}
|
||||
@@ -136,7 +133,7 @@ const ChatHistorySlider = ({
|
||||
justify={['space-between', '']}
|
||||
alignItems={'center'}
|
||||
>
|
||||
{!isPc && appId && (
|
||||
{!isPc && (
|
||||
<Flex height={'100%'} align={'center'} justify={'center'}>
|
||||
<MyIcon ml={2} name="core/chat/sideLine" />
|
||||
<Box ml={2} fontWeight={'bold'}>
|
||||
@@ -147,8 +144,9 @@ const ChatHistorySlider = ({
|
||||
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
flex={[appId ? '0 0 auto' : 1, 1]}
|
||||
flex={['0 0 auto', 1]}
|
||||
h={'100%'}
|
||||
px={6}
|
||||
color={'primary.600'}
|
||||
borderRadius={'xl'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
@@ -158,7 +156,7 @@ const ChatHistorySlider = ({
|
||||
{t('common:core.chat.New Chat')}
|
||||
</Button>
|
||||
{/* Clear */}
|
||||
{isPc && (
|
||||
{isPc && histories.length > 0 && (
|
||||
<IconButton
|
||||
ml={3}
|
||||
h={'100%'}
|
||||
@@ -288,7 +286,7 @@ const ChatHistorySlider = ({
|
||||
</Box>
|
||||
|
||||
{/* exec */}
|
||||
{!isPc && appId && !isTeamChat && (
|
||||
{!isPc && isUserChatPage && (
|
||||
<Flex
|
||||
mt={2}
|
||||
borderTop={theme.borders.base}
|
||||
|
@@ -7,7 +7,13 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
|
||||
const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
|
||||
const ToolMenu = ({
|
||||
history,
|
||||
onRouteToAppDetail
|
||||
}: {
|
||||
history: ChatItemType[];
|
||||
onRouteToAppDetail?: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { onExportChat } = useChatBox();
|
||||
const router = useRouter();
|
||||
@@ -57,7 +63,20 @@ const ToolMenu = ({ history }: { history: ChatItemType[] }) => {
|
||||
// onClick: () => onExportChat({ type: 'pdf', history })
|
||||
// }
|
||||
]
|
||||
}
|
||||
},
|
||||
...(onRouteToAppDetail
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
icon: 'core/app/aiLight',
|
||||
label: t('app:app_detail'),
|
||||
onClick: onRouteToAppDetail
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
|
@@ -195,7 +195,6 @@ const Chat = ({
|
||||
);
|
||||
})(
|
||||
<ChatHistorySlider
|
||||
apps={myApps}
|
||||
confirmClearText={t('common:core.chat.Confirm to clear history')}
|
||||
appId={appId}
|
||||
appName={chatData.app.name}
|
||||
@@ -229,8 +228,8 @@ const Chat = ({
|
||||
apps={myApps}
|
||||
chatData={chatData}
|
||||
history={chatRecords}
|
||||
onRoute2AppDetail={() => router.push(`/app/detail?appId=${appId}`)}
|
||||
showHistory
|
||||
onRouteToAppDetail={() => router.push(`/app/detail?appId=${appId}`)}
|
||||
/>
|
||||
|
||||
{/* chat box */}
|
||||
@@ -341,7 +340,7 @@ export async function getServerSideProps(context: any) {
|
||||
props: {
|
||||
appId: context?.query?.appId || '',
|
||||
chatId: context?.query?.chatId || '',
|
||||
...(await serviceSideProps(context, ['file']))
|
||||
...(await serviceSideProps(context, ['file', 'app']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -292,7 +292,7 @@ const OutLink = ({ appName, appIntro, appAvatar }: Props) => {
|
||||
{showHead === '1' ? (
|
||||
<ChatHeader
|
||||
chatData={chatData}
|
||||
history={chatData.history}
|
||||
history={chatRecords}
|
||||
showHistory={showHistory === '1'}
|
||||
/>
|
||||
) : null}
|
||||
@@ -396,7 +396,7 @@ export async function getServerSideProps(context: any) {
|
||||
appIntro: app?.appId?.intro ?? 'intro',
|
||||
shareId: shareId ?? '',
|
||||
authToken: authToken ?? '',
|
||||
...(await serviceSideProps(context, ['file']))
|
||||
...(await serviceSideProps(context, ['file', 'app']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -199,7 +199,6 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
})(
|
||||
<ChatHistorySlider
|
||||
appId={appId}
|
||||
apps={myApps}
|
||||
appName={chatData.app.name}
|
||||
appAvatar={chatData.app.avatar}
|
||||
confirmClearText={t('common:core.chat.Confirm to clear history')}
|
||||
@@ -230,7 +229,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
flexDirection={'column'}
|
||||
>
|
||||
{/* header */}
|
||||
<ChatHeader apps={myApps} chatData={chatData} history={chatData.history} showHistory />
|
||||
<ChatHeader apps={myApps} chatData={chatData} history={chatRecords} showHistory />
|
||||
{/* chat box */}
|
||||
<Box flex={1}>
|
||||
{chatData.app.type === AppTypeEnum.plugin ? (
|
||||
@@ -340,7 +339,7 @@ export async function getServerSideProps(context: any) {
|
||||
chatId: context?.query?.chatId || '',
|
||||
teamId: context?.query?.teamId || '',
|
||||
teamToken: context?.query?.teamToken || '',
|
||||
...(await serviceSideProps(context, ['file']))
|
||||
...(await serviceSideProps(context, ['file', 'app']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -128,7 +128,7 @@ const Login = () => {
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: { ...(await serviceSideProps(context)) }
|
||||
props: { ...(await serviceSideProps(context, ['app'])) }
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -439,7 +439,7 @@ export const simpleBotTemplates: TemplateType = [
|
||||
label: 'core.ai.Prompt',
|
||||
description: 'core.app.tip.chatNodeSystemPromptTip',
|
||||
placeholder: 'core.app.tip.chatNodeSystemPromptTip',
|
||||
value: '请直接将我的问题翻译成{{language}},不需要回答问题。'
|
||||
value: '请直接将我的问题翻译成{{$VARIABLE_NODE_ID.language$}},不需要回答问题。'
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
|
@@ -25,7 +25,9 @@
|
||||
"@nestjs/platform-fastify": "^10.3.8",
|
||||
"@nestjs/swagger": "^7.3.1",
|
||||
"fastify": "^4.27.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"isolated-vm": "^4.7.2",
|
||||
"tiktoken": "^1.0.15",
|
||||
"node-gyp": "^10.1.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
|
10
projects/sandbox/src/sandbox/jsFn/delay.ts
Normal file
10
projects/sandbox/src/sandbox/jsFn/delay.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const timeDelay = (time: number) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (time > 10000) {
|
||||
reject('Delay time must be less than 10');
|
||||
}
|
||||
setTimeout(() => {
|
||||
resolve('');
|
||||
}, time);
|
||||
});
|
||||
};
|
8
projects/sandbox/src/sandbox/jsFn/tiktoken/index.ts
Normal file
8
projects/sandbox/src/sandbox/jsFn/tiktoken/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Tiktoken } from 'tiktoken/lite';
|
||||
const cl100k_base = require('tiktoken/encoders/cl100k_base');
|
||||
|
||||
export const countToken = (text: string = '') => {
|
||||
const enc = new Tiktoken(cl100k_base.bpe_ranks, cl100k_base.special_tokens, cl100k_base.pat_str);
|
||||
const encodeText = enc.encode(text);
|
||||
return encodeText.length;
|
||||
};
|
@@ -1,15 +1,14 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { SandboxService } from './sandbox.service';
|
||||
import { RunCodeDto, RunCodeResponse } from './dto/create-sandbox.dto';
|
||||
import { WorkerNameEnum, runWorker } from 'src/worker/utils';
|
||||
import { RunCodeDto } from './dto/create-sandbox.dto';
|
||||
import { runSandbox } from './utils';
|
||||
|
||||
@Controller('sandbox')
|
||||
export class SandboxController {
|
||||
constructor(private readonly sandboxService: SandboxService) {}
|
||||
constructor() {}
|
||||
|
||||
@Post('/js')
|
||||
@HttpCode(200)
|
||||
runJs(@Body() codeProps: RunCodeDto) {
|
||||
return runWorker<RunCodeResponse>(WorkerNameEnum.runJs, codeProps);
|
||||
return runSandbox(codeProps);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RunCodeDto } from './dto/create-sandbox.dto';
|
||||
import { WorkerNameEnum, runWorker } from 'src/worker/utils';
|
||||
|
||||
@Injectable()
|
||||
export class SandboxService {
|
||||
runJs(params: RunCodeDto) {
|
||||
return runWorker(WorkerNameEnum.runJs, params);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
102
projects/sandbox/src/sandbox/utils.ts
Normal file
102
projects/sandbox/src/sandbox/utils.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { RunCodeDto, RunCodeResponse } from 'src/sandbox/dto/create-sandbox.dto';
|
||||
import IsolatedVM, { ExternalCopy, Isolate, Reference } from 'isolated-vm';
|
||||
import { countToken } from './jsFn/tiktoken';
|
||||
import { timeDelay } from './jsFn/delay';
|
||||
|
||||
const CustomLogStr = 'CUSTOM_LOG';
|
||||
|
||||
/*
|
||||
Rewrite code to add custom functions: Promise function; Log.
|
||||
*/
|
||||
function getFnCode(code: string) {
|
||||
const rewriteSystemFn = `
|
||||
const thisDelay = (...args) => global_delay.applySyncPromise(undefined,args)
|
||||
`;
|
||||
|
||||
// rewrite delay
|
||||
code = code.replace(/delay\((.*)\)/g, `thisDelay($1)`);
|
||||
|
||||
// rewrite log
|
||||
code = code.replace(/console\.log/g, `${CustomLogStr}`);
|
||||
|
||||
const runCode = `
|
||||
(async() => {
|
||||
try {
|
||||
${rewriteSystemFn}
|
||||
${code}
|
||||
|
||||
const res = await main(variables, {})
|
||||
return JSON.stringify(res);
|
||||
} catch(err) {
|
||||
return JSON.stringify({ERROR: err?.message ?? err})
|
||||
}
|
||||
})
|
||||
`;
|
||||
return runCode;
|
||||
}
|
||||
|
||||
function registerSystemFn(jail: IsolatedVM.Reference<Record<string | number | symbol, any>>) {
|
||||
return Promise.all([
|
||||
// delay
|
||||
jail.set('global_delay', new Reference(timeDelay)),
|
||||
jail.set('countToken', countToken)
|
||||
]);
|
||||
}
|
||||
|
||||
export const runSandbox = async ({
|
||||
code,
|
||||
variables = {}
|
||||
}: RunCodeDto): Promise<RunCodeResponse> => {
|
||||
const logData = [];
|
||||
|
||||
const isolate = new Isolate({ memoryLimit: 32 });
|
||||
const context = await isolate.createContext();
|
||||
const jail = context.global;
|
||||
|
||||
try {
|
||||
// Add global variables
|
||||
await Promise.all([
|
||||
jail.set('variables', new ExternalCopy(variables).copyInto()),
|
||||
jail.set(CustomLogStr, function (...args) {
|
||||
logData.push(
|
||||
args
|
||||
.map((item) => (typeof item === 'object' ? JSON.stringify(item, null, 2) : item))
|
||||
.join(', ')
|
||||
);
|
||||
}),
|
||||
registerSystemFn(jail)
|
||||
]);
|
||||
|
||||
// Run code
|
||||
const fn = await context.eval(getFnCode(code), { reference: true, timeout: 10000 });
|
||||
|
||||
try {
|
||||
// Get result and parse
|
||||
const value = await fn.apply(undefined, [], { result: { promise: true } });
|
||||
const result = JSON.parse(value.toLocaleString());
|
||||
|
||||
// release memory
|
||||
context.release();
|
||||
isolate.dispose();
|
||||
|
||||
if (result.ERROR) {
|
||||
return Promise.reject(result.ERROR);
|
||||
}
|
||||
|
||||
return {
|
||||
codeReturn: result,
|
||||
log: logData.join('\n')
|
||||
};
|
||||
} catch (error) {
|
||||
context.release();
|
||||
isolate.dispose();
|
||||
return Promise.reject('Not an invalid response.You must return an object');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
context.release();
|
||||
isolate.dispose();
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
@@ -1,53 +0,0 @@
|
||||
import { RunCodeDto, RunCodeResponse } from 'src/sandbox/dto/create-sandbox.dto';
|
||||
import { parentPort } from 'worker_threads';
|
||||
import { workerResponse } from './utils';
|
||||
|
||||
// @ts-ignore
|
||||
const ivm = require('isolated-vm');
|
||||
|
||||
parentPort?.on('message', ({ code, variables = {} }: RunCodeDto) => {
|
||||
const resolve = (data: RunCodeResponse) => workerResponse({ parentPort, type: 'success', data });
|
||||
const reject = (error: any) => workerResponse({ parentPort, type: 'error', data: error });
|
||||
try {
|
||||
const isolate = new ivm.Isolate({ memoryLimit: 32 });
|
||||
const context = isolate.createContextSync();
|
||||
const jail = context.global;
|
||||
|
||||
// custom function
|
||||
const logData = [];
|
||||
const CustomLogStr = 'CUSTOM_LOG';
|
||||
code = code.replace(/console\.log/g, `${CustomLogStr}`);
|
||||
jail.setSync(CustomLogStr, function (...args) {
|
||||
logData.push(
|
||||
args
|
||||
.map((item) => (typeof item === 'object' ? JSON.stringify(item, null, 2) : item))
|
||||
.join(', ')
|
||||
);
|
||||
});
|
||||
|
||||
jail.setSync('responseData', function (args: any): any {
|
||||
if (typeof args === 'object') {
|
||||
resolve({
|
||||
codeReturn: args,
|
||||
log: logData.join('\n')
|
||||
});
|
||||
} else {
|
||||
reject('Not an invalid response, must return an object');
|
||||
}
|
||||
});
|
||||
|
||||
// Add global variables
|
||||
jail.setSync('variables', new ivm.ExternalCopy(variables).copyInto());
|
||||
|
||||
const scriptCode = `
|
||||
${code}
|
||||
responseData(main(variables))`;
|
||||
|
||||
context.evalSync(scriptCode, { timeout: 6000 });
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
});
|
@@ -1,47 +0,0 @@
|
||||
import { type MessagePort, Worker } from 'worker_threads';
|
||||
import * as path from 'path';
|
||||
|
||||
export enum WorkerNameEnum {
|
||||
runJs = 'runJs',
|
||||
runPy = 'runPy'
|
||||
}
|
||||
|
||||
type WorkerResponseType = { type: 'success' | 'error'; data: any };
|
||||
|
||||
export const getWorker = (name: WorkerNameEnum) => {
|
||||
const baseUrl =
|
||||
process.env.NODE_ENV === 'production' ? 'projects/sandbox/dist/worker' : 'dist/worker';
|
||||
const workerPath = path.join(process.cwd(), baseUrl, `${name}.js`);
|
||||
return new Worker(workerPath);
|
||||
};
|
||||
|
||||
export const runWorker = <T = any>(name: WorkerNameEnum, params?: Record<string, any>) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const worker = getWorker(name);
|
||||
|
||||
worker.postMessage(params);
|
||||
|
||||
worker.on('message', (msg: WorkerResponseType) => {
|
||||
if (msg.type === 'error') return reject(msg.data);
|
||||
|
||||
resolve(msg.data);
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
worker.on('error', (err) => {
|
||||
reject(err);
|
||||
worker.terminate();
|
||||
});
|
||||
worker.on('messageerror', (err) => {
|
||||
reject(err);
|
||||
worker.terminate();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const workerResponse = ({
|
||||
parentPort,
|
||||
...data
|
||||
}: WorkerResponseType & { parentPort?: MessagePort }) => {
|
||||
parentPort?.postMessage(data);
|
||||
};
|
Reference in New Issue
Block a user