mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 22:03:54 +00:00
feat(publish): Wechat OffiAccount (#2386)
* feat: OffiAccount fe * feat: offiaccount * fix: wecom requires AES key * fix: OffiAccountEditModal * chore: change wechat svg icon * chore: add offiaccount svg * chore: hide unimplemented wecom entries
This commit is contained in:
Binary file not shown.
After Width: | Height: | Size: 210 KiB |
@@ -0,0 +1,29 @@
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { plusRequest } from '@fastgpt/service/common/api/plusRequest';
|
||||
|
||||
export type OutLinkOffiAccountQuery = any;
|
||||
export type OutLinkOffiAccountBody = any;
|
||||
export type OutLinkOffiAccountResponse = {};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<OutLinkOffiAccountBody, OutLinkOffiAccountQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<any> {
|
||||
const { token, type } = req.query;
|
||||
const result = await plusRequest({
|
||||
url: `support/outLink/offiaccount/${token}`,
|
||||
params: {
|
||||
...req.query,
|
||||
type
|
||||
},
|
||||
data: req.body
|
||||
});
|
||||
|
||||
if (result.data?.data?.message) {
|
||||
res.send(result.data.data.message);
|
||||
}
|
||||
|
||||
res.send('');
|
||||
}
|
||||
|
||||
export default handler;
|
@@ -9,6 +9,8 @@ async function handler(
|
||||
req: ApiRequestProps<OutLinkWecomBody, OutLinkWecomQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<any> {
|
||||
// WARN: it is not supported yet.
|
||||
return {};
|
||||
const { token, type } = req.query;
|
||||
const result = await plusRequest({
|
||||
url: `support/outLink/wecom/${token}`,
|
||||
|
@@ -0,0 +1,153 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import type { OffiAccountAppType, 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 { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import BasicInfo from '../components/BasicInfo';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
const OffiAccountEditModal = ({
|
||||
appId,
|
||||
defaultData,
|
||||
onClose,
|
||||
onCreate,
|
||||
onEdit,
|
||||
isEdit = false
|
||||
}: {
|
||||
appId: string;
|
||||
defaultData: OutLinkEditType<OffiAccountAppType>;
|
||||
onClose: () => void;
|
||||
onCreate: (id: string) => void;
|
||||
onEdit: () => void;
|
||||
isEdit?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit: submitShareChat
|
||||
} = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const { runAsync: onclickCreate, loading: creating } = useRequest2(
|
||||
(e) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
appId,
|
||||
type: PublishChannelEnum.officialAccount
|
||||
}),
|
||||
{
|
||||
errorToast: t('common:common.Create Failed'),
|
||||
successToast: t('common:common.Create Success'),
|
||||
onSuccess: onCreate
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onclickUpdate, loading: updating } = useRequest2((e) => updateShareChat(e), {
|
||||
errorToast: t('common:common.Update Failed'),
|
||||
successToast: t('common:common.Update Success'),
|
||||
onSuccess: onEdit
|
||||
});
|
||||
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc="/imgs/modal/shareFill.svg"
|
||||
title={
|
||||
isEdit
|
||||
? t('publish:official_account.edit_modal_title')
|
||||
: t('publish:official_account.create_modal_title')
|
||||
}
|
||||
minW={['auto', '60rem']}
|
||||
>
|
||||
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
|
||||
<Box p={8} minH={['auto', '400px']} borderRight={'base'}>
|
||||
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
|
||||
</Box>
|
||||
<Flex p={8} minH={['auto', '400px']} flexDirection="column" gap={6}>
|
||||
<Flex alignItems="center">
|
||||
<Box color="myGray.600">{t('publish:official_account.api')}</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/official_account')}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="book" mr="1" />
|
||||
{t('common:common.Read document')}
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
App ID
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="App ID"
|
||||
{...register('app.appId', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Secret
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Secret"
|
||||
{...register('app.secret', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Token
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Token"
|
||||
{...register('app.CallbackToken', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'}>AES Key</FormLabel>
|
||||
<Input placeholder="AES Key" {...register('app.CallbackEncodingAesKey')} />
|
||||
</Flex>
|
||||
|
||||
<Box flex={1}></Box>
|
||||
|
||||
<Flex justifyContent={'end'}>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={creating || updating}
|
||||
onClick={submitShareChat((data) =>
|
||||
isEdit ? onclickUpdate(data) : onclickCreate(data)
|
||||
)}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OffiAccountEditModal;
|
@@ -0,0 +1,227 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure
|
||||
} 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 { OutLinkEditType, OffiAccountAppType } from '@fastgpt/global/support/outLink/type.d';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import dayjs from 'dayjs';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
|
||||
const OffiAccountEditModal = dynamic(() => import('./OffiAccountEditModal'));
|
||||
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
|
||||
|
||||
const OffiAccount = ({ appId }: { appId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const [editOffiAccountData, setEditOffiAccountData] =
|
||||
useState<OutLinkEditType<OffiAccountAppType>>();
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
|
||||
const baseUrl = useMemo(
|
||||
() => feConfigs?.customApiDomain || `${location.origin}/api`,
|
||||
[feConfigs?.customApiDomain]
|
||||
);
|
||||
|
||||
const {
|
||||
data: shareChatList = [],
|
||||
loading: isFetching,
|
||||
runAsync: refetchShareChatList
|
||||
} = useRequest2(
|
||||
() => getShareChatList<OffiAccountAppType>({ appId, type: PublishChannelEnum.officialAccount }),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
onOpen: openShowShareLinkModal,
|
||||
isOpen: showShareLinkModalOpen,
|
||||
onClose: closeShowShareLinkModal
|
||||
} = useDisclosure();
|
||||
|
||||
const [showShareLink, setShowShareLink] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<Box position={'relative'} pt={3} px={5} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'} flexDirection="row">
|
||||
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
|
||||
{t('publish:official_account.name')}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
colorScheme={'blue'}
|
||||
size={['sm', 'md']}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
|
||||
ml={3}
|
||||
{...(shareChatList.length >= 10
|
||||
? {
|
||||
isDisabled: true,
|
||||
title: t('common:core.app.share.Amount limit tip')
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
setEditOffiAccountData(defaultOutLinkForm as any); // HACK
|
||||
setIsEdit(false);
|
||||
}}
|
||||
>
|
||||
{t('common:add_new')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<TableContainer mt={3}>
|
||||
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('common:common.Name')} </Th>
|
||||
<Th> {t('common:support.outlink.Usage points')} </Th>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
<Th>{t('common:core.app.share.Ip limit title')} </Th>
|
||||
<Th> {t('common:common.Expired Time')} </Th>
|
||||
</>
|
||||
)}
|
||||
<Th>{t('common:common.Last use time')} </Th>
|
||||
<Th> </Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{shareChatList.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.name} </Td>
|
||||
<Td>
|
||||
{Math.round(item.usagePoints)}
|
||||
{feConfigs?.isPlus
|
||||
? `${
|
||||
item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1
|
||||
? ` / ${item.limit.maxUsagePoints}`
|
||||
: ` / ${t('common:common.Unlimited')}`
|
||||
}`
|
||||
: ''}
|
||||
</Td>
|
||||
{feConfigs?.isPlus && (
|
||||
<>
|
||||
<Td>{item?.limit?.QPM || '-'} </Td>
|
||||
<Td>
|
||||
{item?.limit?.expiredTime
|
||||
? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm')
|
||||
: '-'}
|
||||
</Td>
|
||||
</>
|
||||
)}
|
||||
<Td>
|
||||
{item.lastTime
|
||||
? t(formatTimeToChatTime(item.lastTime) as any)
|
||||
: t('common:common.Un used')}
|
||||
</Td>
|
||||
<Td display={'flex'} alignItems={'center'}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowShareLink(`${baseUrl}/support/outLink/offiaccount/${item.shareId}`);
|
||||
openShowShareLinkModal();
|
||||
}}
|
||||
size={'sm'}
|
||||
mr={3}
|
||||
variant={'whitePrimary'}
|
||||
>
|
||||
{t('publish:request_address')}
|
||||
</Button>
|
||||
<MyMenu
|
||||
Button={
|
||||
<MyIcon
|
||||
name={'more'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
cursor={'pointer'}
|
||||
borderRadius={'md'}
|
||||
w={'14px'}
|
||||
p={2}
|
||||
/>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('common:common.Edit'),
|
||||
icon: 'edit',
|
||||
onClick: () => {
|
||||
setEditOffiAccountData({
|
||||
_id: item._id,
|
||||
name: item.name,
|
||||
limit: item.limit,
|
||||
app: item.app,
|
||||
responseDetail: item.responseDetail,
|
||||
defaultResponse: item.defaultResponse,
|
||||
immediateResponse: item.immediateResponse
|
||||
});
|
||||
setIsEdit(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common:common.Delete'),
|
||||
icon: 'delete',
|
||||
onClick: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await delShareChatById(item._id);
|
||||
refetchShareChatList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{editOffiAccountData && (
|
||||
<OffiAccountEditModal
|
||||
appId={appId}
|
||||
defaultData={editOffiAccountData}
|
||||
onCreate={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
|
||||
onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])}
|
||||
onClose={() => setEditOffiAccountData(undefined)}
|
||||
isEdit={isEdit}
|
||||
/>
|
||||
)}
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<EmptyTip text={t('common:core.app.share.Not share link')}> </EmptyTip>
|
||||
)}
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
{showShareLinkModalOpen && (
|
||||
<ShowShareLinkModal
|
||||
shareLink={showShareLink ?? ''}
|
||||
onClose={closeShowShareLinkModal}
|
||||
img="/imgs/outlink/offiaccount-copylink-instruction.png"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(OffiAccount);
|
@@ -138,9 +138,7 @@ const WecomEditModal = ({
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="AES Key"
|
||||
{...register('app.CallbackEncodingAesKey', {
|
||||
required: true
|
||||
})}
|
||||
{...(register('app.CallbackEncodingAesKey'), { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
|
@@ -16,7 +16,8 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
const Link = dynamic(() => import('./Link'));
|
||||
const API = dynamic(() => import('./API'));
|
||||
const FeiShu = dynamic(() => import('./FeiShu'));
|
||||
const Wecom = dynamic(() => import('./Wecom'));
|
||||
// const Wecom = dynamic(() => import('./Wecom'));
|
||||
const OffiAccount = dynamic(() => import('./OffiAccount'));
|
||||
|
||||
const OutLink = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -47,11 +48,18 @@ const OutLink = () => {
|
||||
value: PublishChannelEnum.feishu,
|
||||
isProFn: true
|
||||
},
|
||||
// {
|
||||
// icon: 'core/app/publish/wecom',
|
||||
// title: t('publish:wecom.bot'),
|
||||
// desc: t('publish:wecom.bot_desc'),
|
||||
// value: PublishChannelEnum.wecom,
|
||||
// isProFn: true
|
||||
// },
|
||||
{
|
||||
icon: 'core/app/publish/wecom',
|
||||
title: t('publish:wecom.bot'),
|
||||
desc: t('publish:wecom.bot_desc'),
|
||||
value: PublishChannelEnum.wecom,
|
||||
icon: 'core/app/publish/offiaccount',
|
||||
title: t('publish:official_account.name'),
|
||||
desc: t('publish:official_account.desc'),
|
||||
value: PublishChannelEnum.officialAccount,
|
||||
isProFn: true
|
||||
}
|
||||
]);
|
||||
@@ -106,7 +114,8 @@ const OutLink = () => {
|
||||
)}
|
||||
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
|
||||
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
|
||||
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />}
|
||||
{/* {linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />} */}
|
||||
{linkType === PublishChannelEnum.officialAccount && <OffiAccount appId={appId} />}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
|
Reference in New Issue
Block a user