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:
Finley Ge
2024-08-15 12:06:13 +08:00
committed by GitHub
parent 5bbaa8264a
commit f8b8fcc172
14 changed files with 486 additions and 13 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -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;

View File

@@ -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}`,

View File

@@ -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;

View File

@@ -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);

View File

@@ -138,9 +138,7 @@ const WecomEditModal = ({
</FormLabel>
<Input
placeholder="AES Key"
{...register('app.CallbackEncodingAesKey', {
required: true
})}
{...(register('app.CallbackEncodingAesKey'), { required: true })}
/>
</Flex>

View File

@@ -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>
);