mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
@@ -0,0 +1,226 @@
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { Box, Button, Flex, ModalBody, ModalFooter, Spinner, Text } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/v2/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { POST, GET } from '@/web/common/api/request';
|
||||
import QRCode from 'qrcode';
|
||||
import MyLoading from '@fastgpt/web/components/common/MyLoading';
|
||||
import { formatFileSize } from '@fastgpt/global/common/file/tools';
|
||||
|
||||
type QRStatus = 'loading' | 'wait' | 'scanned' | 'confirmed' | 'expired' | 'error';
|
||||
|
||||
const QRLoginModal = ({
|
||||
shareId,
|
||||
onSuccess,
|
||||
onClose
|
||||
}: {
|
||||
shareId: string;
|
||||
onSuccess: () => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [status, setStatus] = useState<QRStatus>('loading');
|
||||
const [qrText, setQrText] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const mountedRef = useRef(true);
|
||||
const pollingRef = useRef(false);
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
pollingRef.current = false;
|
||||
}, []);
|
||||
|
||||
// 串行轮询:等上一个请求完成后再发起下一个
|
||||
const startPolling = useCallback(() => {
|
||||
pollingRef.current = true;
|
||||
|
||||
const poll = async () => {
|
||||
while (pollingRef.current && mountedRef.current) {
|
||||
try {
|
||||
const data = await GET<{ status: string }>('/support/outLink/wechat/qrcode/status', {
|
||||
shareId
|
||||
});
|
||||
if (!mountedRef.current || !pollingRef.current) return;
|
||||
|
||||
switch (data.status) {
|
||||
case 'scaned':
|
||||
setStatus('scanned');
|
||||
break;
|
||||
case 'confirmed':
|
||||
setStatus('confirmed');
|
||||
pollingRef.current = false;
|
||||
toast({
|
||||
title: t('publish:wechat.login_success'),
|
||||
status: 'success'
|
||||
});
|
||||
setTimeout(onSuccess, 1000);
|
||||
return;
|
||||
case 'expired':
|
||||
setStatus('expired');
|
||||
pollingRef.current = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
if (!mountedRef.current) return;
|
||||
setStatus('error');
|
||||
setErrMsg(t('publish:wechat.status_check_failed'));
|
||||
pollingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待 2s 后再发起下一轮
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
}, [shareId, toast, t, onSuccess]);
|
||||
|
||||
// 用 qrcode 库渲染二维码到 canvas
|
||||
const drawQRCode = useCallback((text: string) => {
|
||||
if (!text || !canvasRef.current) return;
|
||||
const canvas = document.createElement('canvas');
|
||||
QRCode.toCanvas(canvas, text, {
|
||||
width: 220,
|
||||
margin: 2,
|
||||
color: { dark: '#000000', light: '#ffffff' }
|
||||
})
|
||||
.then(() => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.innerHTML = '';
|
||||
canvasRef.current.appendChild(canvas);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const generateQR = useCallback(async () => {
|
||||
try {
|
||||
setStatus('loading');
|
||||
setErrMsg('');
|
||||
stopPolling();
|
||||
|
||||
const data = await POST<{
|
||||
qrcode: string;
|
||||
qrcode_img_content: string;
|
||||
}>('/support/outLink/wechat/qrcode/generate', { shareId });
|
||||
|
||||
if (!mountedRef.current) return;
|
||||
|
||||
setQrText(data.qrcode_img_content);
|
||||
setStatus('wait');
|
||||
|
||||
startPolling();
|
||||
} catch {
|
||||
if (!mountedRef.current) return;
|
||||
setStatus('error');
|
||||
setErrMsg(t('publish:wechat.qr_generate_failed'));
|
||||
}
|
||||
}, [shareId, startPolling, stopPolling, t]);
|
||||
|
||||
// qrText 变化时重新渲染二维码
|
||||
useEffect(() => {
|
||||
drawQRCode(qrText);
|
||||
}, [qrText, drawQRCode]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
generateQR();
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
stopPolling();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (status) {
|
||||
case 'loading':
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" minH="350px">
|
||||
<MyLoading fixed={false} text={t('publish:wechat.generating_qr')} />
|
||||
</Flex>
|
||||
);
|
||||
case 'wait':
|
||||
return (
|
||||
<Flex direction="column" align="center">
|
||||
<Box
|
||||
p={4}
|
||||
bg="white"
|
||||
borderRadius="lg"
|
||||
boxShadow="md"
|
||||
border="1px solid"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<Box ref={canvasRef} w="220px" h="220px" display="inline-block" />
|
||||
</Box>
|
||||
<Text mt={4} fontSize="lg" fontWeight="medium">
|
||||
{t('publish:wechat.scan_qr_tip')}
|
||||
</Text>
|
||||
<Text mt={1} fontSize="sm" color="gray.500">
|
||||
{t('publish:wechat.scan_qr_desc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
case 'scanned':
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" minH="350px">
|
||||
<Text fontSize="60px">👀</Text>
|
||||
<Text mt={4} fontSize="lg" fontWeight="medium" color="blue.600">
|
||||
{t('publish:wechat.scanned_tip')}
|
||||
</Text>
|
||||
<Text mt={1} fontSize="sm" color="gray.500">
|
||||
{t('publish:wechat.scanned_desc')}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
case 'confirmed':
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" minH="350px">
|
||||
<Text fontSize="60px">✅</Text>
|
||||
<Text mt={4} fontSize="lg" fontWeight="medium" color="green.600">
|
||||
{t('publish:wechat.confirmed_tip')}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
case 'expired':
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" minH="350px">
|
||||
<Text fontSize="60px">⏰</Text>
|
||||
<Text mt={4} fontSize="lg" fontWeight="medium" color="orange.600">
|
||||
{t('publish:wechat.expired_tip')}
|
||||
</Text>
|
||||
<Button mt={4} colorScheme="blue" onClick={generateQR}>
|
||||
{t('publish:wechat.retry')}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
case 'error':
|
||||
return (
|
||||
<Flex direction="column" align="center" justify="center" minH="350px">
|
||||
<Text fontSize="60px">❌</Text>
|
||||
<Text mt={4} color="red.500">
|
||||
{errMsg}
|
||||
</Text>
|
||||
<Button mt={4} colorScheme="blue" onClick={generateQR}>
|
||||
{t('publish:wechat.retry')}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} title={t('publish:wechat.login_title')} size="md">
|
||||
<ModalBody py={6}>{renderContent()}</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="whiteBase" onClick={onClose}>
|
||||
{t('common:Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default QRLoginModal;
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Flex, Grid, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import type { WechatAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
|
||||
const WechatEditModal = ({
|
||||
appId,
|
||||
defaultData,
|
||||
onClose,
|
||||
onCreate,
|
||||
onEdit,
|
||||
isEdit = false
|
||||
}: {
|
||||
appId: string;
|
||||
defaultData: OutLinkEditType<WechatAppType>;
|
||||
onClose: () => void;
|
||||
onCreate: (shareId: string) => Promise<string | undefined>;
|
||||
onEdit: () => void;
|
||||
isEdit?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, setValue, handleSubmit } = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const { runAsync: onclickCreate, loading: creating } = useRequest(
|
||||
(e) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId,
|
||||
type: PublishChannelEnum.wechat
|
||||
}),
|
||||
{
|
||||
errorToast: t('common:create_failed'),
|
||||
successToast: t('common:create_success'),
|
||||
onSuccess: async (shareId) => {
|
||||
const _id = await onCreate(shareId);
|
||||
if (_id) setValue('_id', _id);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onclickUpdate, loading: updating } = useRequest((e) => updateShareChat(e), {
|
||||
errorToast: t('common:update_failed'),
|
||||
successToast: t('common:update_success'),
|
||||
onSuccess: () => {
|
||||
onEdit();
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc="core/app/publish/wechat"
|
||||
title={isEdit ? t('publish:wechat.edit') : t('publish:wechat.create')}
|
||||
minW={['auto', '500px']}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody fontSize={'14px'} p={8}>
|
||||
<Grid gridTemplateColumns={'1fr'} gap={4}>
|
||||
<Flex flexDir={'column'} gap={2}>
|
||||
<FormLabel required>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={t('publish:wechat.name_placeholder')}
|
||||
maxLength={100}
|
||||
{...register('name', { required: t('common:name_is_empty') })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex flexDir={'column'} gap={2}>
|
||||
<FormLabel>
|
||||
{t('common:support.outlink.Max usage points')}
|
||||
<QuestionTip ml={1} label={t('common:support.outlink.Max usage points tip')} />
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...register('limit.maxUsagePoints', {
|
||||
min: -1,
|
||||
max: 10000000,
|
||||
valueAsNumber: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:Close')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={creating || updating}
|
||||
onClick={handleSubmit((data) => (isEdit ? onclickUpdate(data) : onclickCreate(data)))}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WechatEditModal;
|
||||
@@ -0,0 +1,251 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Link,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
|
||||
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
||||
import { defaultOutLinkForm } from '@/web/core/app/constants';
|
||||
import type { WechatAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import { POST } from '@/web/common/api/request';
|
||||
import type { ColorSchemaType } from '@fastgpt/web/components/common/Tag/index';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
|
||||
const WechatEditModal = dynamic(() => import('./WechatEditModal'));
|
||||
const QRLoginModal = dynamic(() => import('./QRLoginModal'));
|
||||
|
||||
const Wechat = ({ appId }: { appId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const [editData, setEditData] = useState<OutLinkEditType<WechatAppType>>();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [loginShareId, setLoginShareId] = useState<string>();
|
||||
|
||||
const {
|
||||
data: shareChatList = [],
|
||||
loading: isFetching,
|
||||
runAsync: refetch
|
||||
} = useRequest(
|
||||
() => getShareChatList<WechatAppType>({ appId, type: PublishChannelEnum.wechat }),
|
||||
{
|
||||
manual: false,
|
||||
refreshDeps: [appId]
|
||||
}
|
||||
);
|
||||
|
||||
const statusBadge = (status?: string) => {
|
||||
const map: Record<string, { colorSchema: ColorSchemaType; label: string }> = {
|
||||
online: { colorSchema: 'green', label: t('publish:wechat.status.online') },
|
||||
offline: { colorSchema: 'gray', label: t('publish:wechat.status.offline') },
|
||||
error: { colorSchema: 'red', label: t('publish:wechat.status.error') }
|
||||
};
|
||||
const cfg = map[status || 'offline'] ?? map['offline'];
|
||||
return <MyTag colorSchema={cfg.colorSchema}>{cfg.label}</MyTag>;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
|
||||
{t('publish:wechat.title')}
|
||||
</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={getDocPath('/docs/use-cases/external-integration/wechat')}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="book" w={'17px'} h={'17px'} mr="1" />
|
||||
{t('common:read_doc')}
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
size={['sm', 'md']}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
|
||||
{...(shareChatList.length >= 10
|
||||
? { isDisabled: true, title: t('common:core.app.share.Amount limit tip') }
|
||||
: {})}
|
||||
onClick={() => {
|
||||
setEditData(defaultOutLinkForm as any);
|
||||
setIsEdit(false);
|
||||
}}
|
||||
>
|
||||
{t('common:add_new')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<TableContainer mt={3}>
|
||||
<Table variant={'simple'} w={'100%'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:Name')}</Th>
|
||||
<Th>{t('publish:wechat.status')}</Th>
|
||||
<Th>{t('common:support.outlink.Usage points')}</Th>
|
||||
<Th>{t('common:last_use_time')}</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{shareChatList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name}</Td>
|
||||
<Td>{statusBadge(item.app?.status)}</Td>
|
||||
<Td>{Math.round(item.usagePoints)}</Td>
|
||||
<Td>
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
|
||||
: t('common:un_used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
{!item.app?.token ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
mr={3}
|
||||
colorScheme="green"
|
||||
onClick={() => {
|
||||
setLoginShareId(item.shareId);
|
||||
}}
|
||||
>
|
||||
{t('publish:wechat.login')}
|
||||
</Button>
|
||||
) : item.app.status === 'online' ? (
|
||||
<Button
|
||||
size={'sm'}
|
||||
mr={3}
|
||||
variant={'whiteBase'}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await POST('/support/outLink/wechat/logout', {
|
||||
shareId: item.shareId
|
||||
});
|
||||
refetch();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('publish:wechat.logout')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size={'sm'}
|
||||
mr={3}
|
||||
variant={'whitePrimary'}
|
||||
onClick={() => {
|
||||
setLoginShareId(item.shareId);
|
||||
}}
|
||||
>
|
||||
{t('publish:wechat.relogin')}
|
||||
</Button>
|
||||
)}
|
||||
<MyMenu
|
||||
Button={
|
||||
<Button size={'smSquare'} variant={'whiteBase'}>
|
||||
<MyIcon name={'more'} w={'14px'} />
|
||||
</Button>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('common:Edit'),
|
||||
icon: 'edit',
|
||||
onClick: () => {
|
||||
setEditData({
|
||||
_id: item._id,
|
||||
name: item.name,
|
||||
limit: item.limit,
|
||||
app: item.app,
|
||||
defaultResponse: item.defaultResponse,
|
||||
immediateResponse: item.immediateResponse
|
||||
});
|
||||
setIsEdit(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common:Delete'),
|
||||
icon: 'delete',
|
||||
onClick: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetch();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<EmptyTip text={t('common:core.app.share.Not share link')} />
|
||||
)}
|
||||
|
||||
{editData && (
|
||||
<WechatEditModal
|
||||
appId={appId}
|
||||
defaultData={editData}
|
||||
isEdit={isEdit}
|
||||
onCreate={async (shareId) => {
|
||||
const newList = await refetch();
|
||||
return newList?.find((i) => i.shareId === shareId)?._id;
|
||||
}}
|
||||
onEdit={() => refetch()}
|
||||
onClose={() => setEditData(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{loginShareId && (
|
||||
<QRLoginModal
|
||||
shareId={loginShareId}
|
||||
onSuccess={() => {
|
||||
refetch();
|
||||
setLoginShareId(undefined);
|
||||
}}
|
||||
onClose={() => setLoginShareId(undefined)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Wechat);
|
||||
@@ -20,6 +20,7 @@ const FeiShu = dynamic(() => import('./FeiShu'));
|
||||
const DingTalk = dynamic(() => import('./DingTalk'));
|
||||
const Wecom = dynamic(() => import('./Wecom'));
|
||||
const OffiAccount = dynamic(() => import('./OffiAccount'));
|
||||
const Wechat = dynamic(() => import('./Wechat'));
|
||||
const Playground = dynamic(() => import('./Playground'));
|
||||
|
||||
const OutLink = () => {
|
||||
@@ -45,6 +46,13 @@ const OutLink = () => {
|
||||
value: PublishChannelEnum.apikey,
|
||||
isProFn: false
|
||||
},
|
||||
{
|
||||
icon: 'core/app/publish/wechat',
|
||||
title: t('publish:wechat.bot'),
|
||||
desc: t('publish:wechat.bot_desc'),
|
||||
value: PublishChannelEnum.wechat,
|
||||
isProFn: true
|
||||
},
|
||||
...(feConfigs?.show_publish_feishu !== false &&
|
||||
!userInfo?.tags?.includes(UserTagsEnum.enum.wecom)
|
||||
? [
|
||||
@@ -91,6 +99,7 @@ const OutLink = () => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
icon: 'core/chat/sidebar/home',
|
||||
title: t('common:navbar.Chat'),
|
||||
@@ -145,6 +154,7 @@ const OutLink = () => {
|
||||
{linkType === PublishChannelEnum.dingtalk && <DingTalk appId={appId} />}
|
||||
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />}
|
||||
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
|
||||
{linkType === PublishChannelEnum.wechat && <Wechat appId={appId} />}
|
||||
{linkType === PublishChannelEnum.playground && <Playground appId={appId} />}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { type ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { type AuthModeType } from '@fastgpt/service/support/permission/type';
|
||||
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
|
||||
import { authOutLinkInit } from '@/service/support/permission/auth/outLink';
|
||||
import { authOutLinkInit } from '@fastgpt/service/support/outLink/runtime/auth';
|
||||
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
|
||||
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
|
||||
import type { WechatAppType } from '@fastgpt/global/support/outLink/type';
|
||||
|
||||
async function handler(req: ApiRequestProps<{ shareId: string }>): Promise<void> {
|
||||
const { shareId } = req.body;
|
||||
|
||||
await authOutLinkValid<WechatAppType>({ shareId });
|
||||
|
||||
await MongoOutLink.updateOne(
|
||||
{ shareId },
|
||||
{
|
||||
$set: {
|
||||
'app.status': 'offline',
|
||||
'app.token': '',
|
||||
'app.lastError': ''
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ILinkClient } from '@fastgpt/service/support/outLink/wechat/ilinkClient';
|
||||
import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/authLink';
|
||||
import type { WechatAppType } from '@fastgpt/global/support/outLink/type';
|
||||
import { setRedisCache } from '@fastgpt/service/common/redis/cache';
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<{ shareId: string }>
|
||||
): Promise<{ qrcode: string; qrcode_img_content: string; expireTime: number }> {
|
||||
const { shareId } = req.body;
|
||||
|
||||
await authOutLinkValid<WechatAppType>({ shareId });
|
||||
|
||||
const client = new ILinkClient();
|
||||
const qrData = await client.getQRCode();
|
||||
|
||||
await setRedisCache(`publish:wechat:qrcode:${shareId}`, JSON.stringify(qrData), 480);
|
||||
|
||||
return {
|
||||
qrcode: qrData.qrcode,
|
||||
qrcode_img_content: qrData.qrcode_img_content,
|
||||
expireTime: 480
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ILinkClient } from '@fastgpt/service/support/outLink/wechat/ilinkClient';
|
||||
import { getRedisCache, delRedisCache } from '@fastgpt/service/common/redis/cache';
|
||||
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
|
||||
import { startWechatPolling } from '@fastgpt/service/support/outLink/wechat/mq';
|
||||
|
||||
async function handler(req: ApiRequestProps<{}, { shareId: string }>): Promise<{ status: string }> {
|
||||
const { shareId } = req.query;
|
||||
|
||||
const raw = await getRedisCache(`publish:wechat:qrcode:${shareId}`);
|
||||
if (!raw) {
|
||||
return { status: 'expired' };
|
||||
}
|
||||
|
||||
const qrData = JSON.parse(raw);
|
||||
const client = new ILinkClient();
|
||||
const statusData = await client.getQRCodeStatus(qrData.qrcode);
|
||||
|
||||
if (statusData.status === 'confirmed' && statusData.bot_token && statusData.ilink_bot_id) {
|
||||
await MongoOutLink.updateOne(
|
||||
{ shareId },
|
||||
{
|
||||
$set: {
|
||||
'app.token': statusData.bot_token,
|
||||
'app.baseUrl': statusData.baseurl || 'https://ilinkai.weixin.qq.com',
|
||||
'app.accountId': statusData.ilink_bot_id,
|
||||
'app.userId': statusData.ilink_user_id || '',
|
||||
'app.status': 'online',
|
||||
'app.loginTime': new Date().toISOString(),
|
||||
'app.syncBuf': '',
|
||||
'app.lastError': ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await delRedisCache(`publish:wechat:qrcode:${shareId}`);
|
||||
await startWechatPolling(shareId);
|
||||
}
|
||||
|
||||
return { status: statusData.status };
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -4,6 +4,7 @@ import { initDatasetDeleteWorker } from '@fastgpt/service/core/dataset/delete';
|
||||
import { initAppDeleteWorker } from '@fastgpt/service/core/app/delete';
|
||||
import { initTeamDeleteWorker } from '@fastgpt/service/support/user/team/delete';
|
||||
import { initCollectionUpdateWorker } from '@fastgpt/service/core/dataset/collection/mq';
|
||||
import { initWechatPollWorker } from '@fastgpt/service/support/outLink/wechat/mq';
|
||||
|
||||
const logger = getLogger(LogCategories.INFRA.QUEUE);
|
||||
|
||||
@@ -14,6 +15,7 @@ export const initBullMQWorkers = () => {
|
||||
initDatasetDeleteWorker(),
|
||||
initAppDeleteWorker(),
|
||||
initTeamDeleteWorker(),
|
||||
initCollectionUpdateWorker()
|
||||
initCollectionUpdateWorker(),
|
||||
initWechatPollWorker()
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
import type {
|
||||
AuthOutLinkChatProps,
|
||||
AuthOutLinkLimitProps,
|
||||
AuthOutLinkInitProps,
|
||||
AuthOutLinkResponse
|
||||
} from '@fastgpt/global/support/outLink/api';
|
||||
import { type ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
@@ -10,11 +9,8 @@ import { authOutLinkValid } from '@fastgpt/service/support/permission/publish/au
|
||||
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
|
||||
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
|
||||
import { type OutLinkSchema } from '@fastgpt/global/support/outLink/type';
|
||||
import { authOutLinkInit } from '@fastgpt/service/support/outLink/runtime/auth';
|
||||
|
||||
export function authOutLinkInit(data: AuthOutLinkInitProps): Promise<AuthOutLinkResponse> {
|
||||
if (!global.feConfigs?.isPlus) return Promise.resolve({ uid: data.outLinkUid });
|
||||
return POST<AuthOutLinkResponse>('/support/outLink/authInit', data);
|
||||
}
|
||||
export function authOutLinkChatLimit(data: AuthOutLinkLimitProps): Promise<AuthOutLinkResponse> {
|
||||
if (!global.feConfigs?.isPlus) return Promise.resolve({ uid: data.outLinkUid });
|
||||
return POST<AuthOutLinkResponse>('/support/outLink/authChatStart', data);
|
||||
|
||||
Reference in New Issue
Block a user