Publish app - feishu and wecom (#2375)

* feat(app publish): feishu bot (#2290)

* feat: feishu publish channel fe

* feat: enable feishu fe,
feat: feishu token api

* feat: feishu bot

* chore: extract saveChat from projects/app

* chore: remove debug log output

* feat: Basic Info

* chore: feishu bot fe adjusting

* feat: feishu bot docs

* feat: new tmpData collection for all tmpdata

* chore: compress the image

* perf: feishu config

* feat: source name

* perf: text desc

* perf: load system plugins

* perf: chat source

* feat(publish): Wecom bot (#2343)

* chore: Wecom Config

* feat(fe): wecom config fe

* feat: wecom fe

* chore: uses the newest editmodal

* feat: update png; adjust the fe

* chore: adjust fe

* perf: publish app ui

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer
2024-08-13 21:52:18 +08:00
committed by GitHub
parent 7417de74da
commit 0f3418daf5
71 changed files with 1301 additions and 498 deletions

View File

@@ -2,7 +2,7 @@ import type { NextApiResponse } from 'next';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
import { getSystemPluginTemplates } from '@fastgpt/plugins/register';
import { getSystemPlugins } from '@/service/core/app/plugin';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
@@ -24,7 +24,7 @@ async function handler(
const formatParentId = parentId || null;
return getSystemPluginTemplates().then((res) =>
return getSystemPlugins().then((res) =>
res
// Just show the active plugins
.filter((item) => item.isActive)

View File

@@ -1,7 +1,7 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { ParentIdType, ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { getSystemPluginTemplates } from '@fastgpt/plugins/register';
import { getSystemPlugins } from '@/service/core/app/plugin';
export type pathQuery = {
parentId: ParentIdType;
@@ -19,7 +19,7 @@ async function handler(
if (!parentId) return [];
const plugins = await getSystemPluginTemplates();
const plugins = await getSystemPlugins();
const plugin = plugins.find((item) => item.id === parentId);
if (!plugin) return [];

View File

@@ -0,0 +1,20 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { POST } from '@fastgpt/service/common/api/plusRequest';
export type OutLinkFeishuQuery = any;
export type OutLinkFeishuBody = any;
export type OutLinkFeishuResponse = {};
async function handler(
req: ApiRequestProps<OutLinkFeishuBody, OutLinkFeishuQuery>,
res: ApiResponseType<any>
): Promise<void> {
// send to pro
const { token } = req.query;
const result = await POST<any>(`support/outLink/feishu/${token}`, req.body, {
headers: req.headers as any
});
res.json(result);
}
export default handler;

View File

@@ -13,7 +13,7 @@ export type OutLinkUpdateResponse = {};
async function handler(
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
): Promise<OutLinkUpdateResponse> {
const { _id, name, responseDetail, limit } = req.body;
const { _id, name, responseDetail, limit, app } = req.body;
if (!_id) {
return Promise.reject(CommonErrEnum.missingParams);
@@ -24,7 +24,8 @@ async function handler(
await MongoOutLink.findByIdAndUpdate(_id, {
name,
responseDetail,
limit
limit,
app
});
return {};
}

View File

@@ -0,0 +1,31 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { plusRequest } from '@fastgpt/service/common/api/plusRequest';
export type OutLinkWecomQuery = any;
export type OutLinkWecomBody = any;
export type OutLinkWecomResponse = {};
async function handler(
req: ApiRequestProps<OutLinkWecomBody, OutLinkWecomQuery>,
res: ApiResponseType<any>
): Promise<any> {
const { token, type } = req.query;
const result = await plusRequest({
url: `support/outLink/wecom/${token}`,
params: {
...req.query,
type
},
data: req.body
});
if (result.data?.data?.message) {
// chanllege
res.send(result.data.data.message);
res.end();
}
res.send('success');
res.end();
}
export default handler;

View File

@@ -21,7 +21,7 @@ import {
} from '@fastgpt/global/core/workflow/runtime/utils';
import { GPTMessages2Chats, chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { saveChat } from '@/service/utils/chat/saveChat';
import { saveChat } from '@fastgpt/service/core/chat/saveChat';
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink';

View File

@@ -71,35 +71,33 @@ const Logs = () => {
const [detailLogsId, setDetailLogsId] = useState<string>();
return (
<>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
{isPc && (
<>
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
{appT('chat_logs')}
<Flex flexDirection={'column'} h={'100%'}>
{isPc && (
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<Box fontWeight={'bold'} fontSize={['md', 'lg']} mb={2}>
{appT('chat_logs')}
</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
{appT('chat_logs_tips')},{' '}
<Box
as={'span'}
mr={2}
textDecoration={'underline'}
cursor={'pointer'}
onClick={onOpenMarkDesc}
>
{t('common:core.chat.Read Mark Description')}
</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
{appT('chat_logs_tips')},{' '}
<Box
as={'span'}
mr={2}
textDecoration={'underline'}
cursor={'pointer'}
onClick={onOpenMarkDesc}
>
{t('common:core.chat.Read Mark Description')}
</Box>
</Box>
</>
)}
</Box>
</Box>
</Box>
)}
{/* table */}
<Flex
flexDirection={'column'}
{...cardStyles}
boxShadow={3.5}
mt={4}
mt={[0, 4]}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
@@ -214,7 +212,7 @@ const Logs = () => {
>
<ModalBody whiteSpace={'pre-wrap'}>{t('common:core.chat.Mark Description')}</ModalBody>
</MyModal>
</>
</Flex>
);
};

View File

@@ -1,32 +1,35 @@
import React, { useMemo } from 'react';
import { Flex, Box, Button, ModalFooter, ModalBody, Input } from '@chakra-ui/react';
import React from 'react';
import { Flex, Box, Button, ModalFooter, ModalBody, Input, Link, Grid } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import type { FeishuType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest } from '@/web/common/hooks/useRequest';
import dayjs from 'dayjs';
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
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 { useSystem } from '@fastgpt/web/hooks/useSystem';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const FeiShuEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<FeishuType>;
defaultData: OutLinkEditType<FeishuAppType>;
onClose: () => void;
onCreate: (id: string) => void;
onEdit: () => void;
isEdit?: boolean;
}) => {
const { t } = useTranslation();
const { publishT } = useI18n();
const {
register,
setValue,
@@ -35,174 +38,101 @@ const FeiShuEditModal = ({
defaultValues: defaultData
});
const isEdit = useMemo(() => !!defaultData?._id, [defaultData]);
const { mutate: onclickCreate, isLoading: creating } = useRequest({
mutationFn: async (e: OutLinkEditType<FeishuType>) => {
const { runAsync: onclickCreate, loading: creating } = useRequest2(
(e) =>
createShareChat({
...e,
appId,
type: PublishChannelEnum.feishu
});
},
errorToast: t('common:common.Create Failed'),
onSuccess: onCreate
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: (e: OutLinkEditType<FeishuType>) => {
return updateShareChat(e);
},
}),
{
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();
const { isPc } = useSystem();
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/shareFill.svg"
title={isEdit ? publishT('edit_link') : publishT('create_link')}
iconSrc="core/app/publish/lark"
title={isEdit ? t('publish:edit_feishu_bot') : t('publish:new_feishu_bot')}
minW={['auto', '60rem']}
>
<ModalBody>
<Flex alignItems={'center'}>
<Box flex={'0 0 90px'}>{t('common:Name')}</Box>
<Input
placeholder={publishT('feishu_name') || 'link_name'} // TODO: i18n
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
QPM
<QuestionTip ml={1} label={publishT('qpm_tips' || '')}></QuestionTip>
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
<Box p={8} h={['auto', '400px']} borderRight={'base'}>
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
</Box>
<Flex p={8} h={['auto', '400px']} flexDirection="column" gap={6}>
<Flex alignItems="center">
<Box color="myGray.600">{t('publish:feishu_api')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/feishu-bot')}
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>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: publishT('qpm_is_empty') || ''
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common:support.outlink.Max usage points')}
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
App ID
</FormLabel>
<Input
placeholder={t('common:core.module.http.AppId')}
{...register('app.appId', {
required: true
})}
/>
</Flex>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common:common.Expired Time')}
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
App Secret
</FormLabel>
<Input
placeholder={'App Secret'}
{...register('app.appSecret', {
required: true
})}
/>
</Flex>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common:default_reply')}
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'}>Encrypt Key</FormLabel>
<Input placeholder="Encrypt Key" {...register('app.encryptKey')} />
</Flex>
<Input
placeholder={publishT('default_response') || 'link_name'}
maxLength={20}
{...register('defaultResponse', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Flex flex={'0 0 90px'} alignItems={'center'}>
{t('common:reply_now')}
<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>
<Input
placeholder={publishT('default_response') || 'link_name'}
maxLength={20}
{...register('immediateResponse', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 90px'}>{t('common:core.module.http.AppId')}</Box>
<Input
placeholder={t('common:core.module.http.AppId') || 'link_name'}
// maxLength={20}
{...register('app.appId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 90px'}>{t('common:core.module.http.AppSecret' as any)}</Box>
<Input
placeholder={'App Secret'}
// maxLength={20}
{...register('app.appSecret', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 90px'}>Encrypt Key</Box>
<Input
placeholder="Encrypt Key"
// maxLength={20}
{...register('app.encryptKey', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
<Flex alignItems={'center'} mt={4}>
<Box flex={'0 0 90px'}>Verification Token</Box>
<Input
placeholder="Verification Token"
// maxLength={20}
{...register('app.verificationToken', {
required: t('common:common.name_is_empty') || 'name_is_empty'
})}
/>
</Flex>
{/* <Flex alignItems={'center'} mt={4}> */}
{/* <Flex flex={'0 0 90px'} alignItems={'center'}> */}
{/* 限制回复 */}
{/* </Flex> */}
{/* <Switch {...register('wecomConfig.ReplyLimit')} size={'lg'} /> */}
{/* </Flex> */}
</ModalBody>
<ModalFooter>
<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>
</ModalFooter>
</MyModal>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
@@ -9,61 +9,82 @@ import {
Tr,
Th,
Td,
Tbody
Tbody,
useDisclosure
} from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import { getShareChatList, delShareChatById } from '@/web/support/outLink/api';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { defaultFeishuOutLinkForm } from '@/web/core/app/constants';
import type { FeishuType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
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 FeiShuEditModal = dynamic(() => import('./FeiShuEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const FeiShu = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const { copyData } = useCopyData();
const [editFeiShuLinkData, setEditFeiShuLinkData] = useState<OutLinkEditType<FeishuType>>();
const { toast } = useToast();
const {
isFetching,
data: shareChatList = [],
refetch: refetchShareChatList
} = useQuery(['initShareChatList', appId], () =>
getShareChatList<FeishuType>({ appId, type: PublishChannelEnum.feishu })
const [editFeiShuLinkData, setEditFeiShuLinkData] = useState<OutLinkEditType<FeishuAppType>>();
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<FeishuAppType>({ appId, type: PublishChannelEnum.feishu }),
{
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'}>
<Flex justifyContent={'space-between'} flexDirection="row">
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
{t('common:core.app.publish.Fei shu bot publish')}
</Box>
<Button
variant={'whitePrimary'}
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={() => setEditFeiShuLinkData(defaultFeishuOutLinkForm)}
onClick={() => {
setEditFeiShuLinkData(defaultFeishuOutLinkForm);
setIsEdit(false);
}}
>
{t('common:core.app.share.Create link')}
{t('common:add_new')}
</Button>
</Flex>
<TableContainer mt={3}>
@@ -112,11 +133,22 @@ const FeiShu = ({ appId }: { appId: string }) => {
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button
onClick={() => {
setShowShareLink(`${baseUrl}/support/outLink/feishu/${item.shareId}`);
openShowShareLinkModal();
}}
size={'sm'}
mr={3}
variant={'whitePrimary'}
>
{t('publish:request_address')}
</Button>
<MyMenu
Button={
<MyIcon
name={'more'}
_hover={{ bg: 'myGray.100 ' }}
_hover={{ bg: 'myGray.100' }}
cursor={'pointer'}
borderRadius={'md'}
w={'14px'}
@@ -129,7 +161,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
{
label: t('common:common.Edit'),
icon: 'edit',
onClick: () =>
onClick: () => {
setEditFeiShuLinkData({
_id: item._id,
name: item.name,
@@ -138,7 +170,9 @@ const FeiShu = ({ appId }: { appId: string }) => {
responseDetail: item.responseDetail,
defaultResponse: item.defaultResponse,
immediateResponse: item.immediateResponse
})
});
setIsEdit(true);
}
},
{
label: t('common:common.Delete'),
@@ -167,27 +201,24 @@ const FeiShu = ({ appId }: { appId: string }) => {
{editFeiShuLinkData && (
<FeiShuEditModal
appId={appId}
// type={'feishu' as PublishChannelEnum}
defaultData={editFeiShuLinkData}
onCreate={(id) => {
refetchShareChatList();
setEditFeiShuLinkData(undefined);
}}
onEdit={() => {
toast({
status: 'success',
title: t('common:common.Update Successful')
});
refetchShareChatList();
setEditFeiShuLinkData(undefined);
}}
onCreate={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditFeiShuLinkData(undefined)])}
onClose={() => setEditFeiShuLinkData(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/feishu-copylink-instruction.png"
/>
)}
</Box>
);
};

View File

@@ -0,0 +1,168 @@
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 { WecomAppType, 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 { useSystem } from '@fastgpt/web/hooks/useSystem';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const WecomEditModal = ({
appId,
defaultData,
onClose,
onCreate,
onEdit,
isEdit = false
}: {
appId: string;
defaultData: OutLinkEditType<WecomAppType>;
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.wecom
}),
{
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="core/app/publish/wecom"
title={isEdit ? t('publish:wecom.edit_modal_title') : t('publish:wecom.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:wecom.api')}</Box>
{feConfigs?.docUrl && (
<Link
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/wecom-bot')}
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>
Corp ID
</FormLabel>
<Input
placeholder="Corp ID"
{...register('app.CorpId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Agent ID
</FormLabel>
<Input
placeholder="Agent ID"
{...register('app.AgentId', {
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} required>
Secret
</FormLabel>
<Input
placeholder="Secret"
{...register('app.SuiteSecret', {
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'} required>
AES Key
</FormLabel>
<Input
placeholder="AES Key"
{...register('app.CallbackEncodingAesKey', {
required: true
})}
/>
</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 WecomEditModal;

View File

@@ -0,0 +1,223 @@
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 { WecomAppType, OutLinkEditType } 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 WecomEditModal = dynamic(() => import('./WecomEditModal'));
const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal'));
const Wecom = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
const { Loading, setIsLoading } = useLoading();
const { feConfigs } = useSystemStore();
const [editWecomData, setEditWecomData] = useState<OutLinkEditType<WecomAppType>>();
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<WecomAppType>({ appId, type: PublishChannelEnum.wecom }), {
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:wecom.title')}
</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={() => {
setEditWecomData(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/wecom/${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: () => {
setEditWecomData({
_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>
{editWecomData && (
<WecomEditModal
appId={appId}
defaultData={editWecomData}
onCreate={() => Promise.all([refetchShareChatList(), setEditWecomData(undefined)])}
onEdit={() => Promise.all([refetchShareChatList(), setEditWecomData(undefined)])}
onClose={() => setEditWecomData(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/wecom-copylink-instruction.png"
/>
)}
</Box>
);
};
export default React.memo(Wecom);

View File

@@ -0,0 +1,87 @@
import React from 'react';
import { Box, Flex, Input } from '@chakra-ui/react';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { OutLinkEditType } from '@fastgpt/global/support/outLink/type';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
function BasicInfo({
register,
setValue,
defaultData
}: {
register: UseFormRegister<OutLinkEditType<any>>;
setValue: UseFormSetValue<OutLinkEditType<any>>;
defaultData: OutLinkEditType<any>;
}) {
const { t } = useTranslation();
return (
<Flex flexDirection="column" gap={6}>
<Box color="myGray.600">{t('publish:basic_info')}</Box>
<Flex alignItems={'center'}>
<FormLabel required flex={'0 0 6.25rem'}>
{t('common:Name')}
</FormLabel>
<Input
placeholder={t('publish:publish_name')}
maxLength={20}
{...register('name', {
required: t('common:common.name_is_empty')
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
QPM
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
</FormLabel>
<Input
max={1000}
{...register('limit.QPM', {
min: 0,
max: 1000,
valueAsNumber: true,
required: t('publish:qpm_is_empty')
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
{t('common:support.outlink.Max usage points')}
<QuestionTip
ml={1}
label={t('common:support.outlink.Max usage points tip')}
></QuestionTip>
</FormLabel>
<Input
{...register('limit.maxUsagePoints', {
min: -1,
max: 10000000,
valueAsNumber: true,
required: true
})}
/>
</Flex>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 6.25rem'} alignItems={'center'}>
{t('common:common.Expired Time')}
</FormLabel>
<Input
type="datetime-local"
defaultValue={
defaultData.limit?.expiredTime
? dayjs(defaultData.limit?.expiredTime).format('YYYY-MM-DDTHH:mm')
: ''
}
onChange={(e) => {
setValue('limit.expiredTime', new Date(e.target.value));
}}
/>
</Flex>
</Flex>
);
}
export default BasicInfo;

View File

@@ -0,0 +1,50 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { Box, Image, Flex, ModalBody } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
export type ShowShareLinkModalProps = {
shareLink: string;
onClose: () => void;
img: string;
};
function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) {
const { copyData } = useCopyData();
const { t } = useTranslation();
return (
<MyModal onClose={onClose} title={t('publish:show_share_link_modal_title')}>
<ModalBody>
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
<Flex
p={3}
bg={'myWhite.500'}
border="base"
borderTopLeftRadius={'md'}
borderTopRightRadius={'md'}
>
<Box flex={1}>{t('publish:copy_link_hint')}</Box>
<MyIcon
name={'copy'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'primary.500' }}
onClick={() => copyData(shareLink)}
/>
</Flex>
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
{shareLink}
</Box>
</Box>
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
<Image src={img} borderRadius="0.5rem" alt="" />
</Box>
</ModalBody>
</MyModal>
);
}
export default ShowShareLinkModal;

View File

@@ -1,5 +1,5 @@
import React, { useRef, useState } from 'react';
import { Box, Flex, useTheme } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import dynamic from 'next/dynamic';
@@ -10,14 +10,18 @@ import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import Link from './Link';
const Link = dynamic(() => import('./Link'));
const API = dynamic(() => import('./API'));
const FeiShu = dynamic(() => import('./FeiShu'));
const Wecom = dynamic(() => import('./Wecom'));
const OutLink = () => {
const { t } = useTranslation();
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const appId = useContextSelector(AppContext, (v) => v.appId);
@@ -26,33 +30,65 @@ const OutLink = () => {
icon: '/imgs/modal/shareFill.svg',
title: t('common:core.app.Share link'),
desc: t('common:core.app.Share link desc'),
value: PublishChannelEnum.share
value: PublishChannelEnum.share,
isProFn: false
},
{
icon: 'support/outlink/apikeyFill',
title: t('common:core.app.Api request'),
desc: t('common:core.app.Api request desc'),
value: PublishChannelEnum.apikey
value: PublishChannelEnum.apikey,
isProFn: false
},
{
icon: 'core/app/publish/lark',
title: t('publish:feishu_bot'),
desc: t('publish:feishu_bot_desc'),
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/lark',
// title: t('common:core.app.publish.Fei shu bot'),
// desc: t('common:core.app.publish.Fei Shu Bot Desc'),
// value: PublishChannelEnum.feishu
// }
]);
const [linkType, setLinkType] = useState<PublishChannelEnum>(PublishChannelEnum.share);
return (
<>
<Box
display={['block', 'flex']}
overflowY={'auto'}
overflowX={'hidden'}
h={'100%'}
flexDirection={'column'}
>
<Box {...cardStyles} boxShadow={2} px={[4, 8]} py={[4, 6]}>
<MyRadio
gridTemplateColumns={['repeat(1,1fr)', 'repeat(auto-fill, minmax(0, 400px))']}
gridTemplateColumns={[
'repeat(1,1fr)',
'repeat(2, 1fr)',
'repeat(3, 1fr)',
'repeat(3, 1fr)',
'repeat(4, 1fr)'
]}
iconSize={'20px'}
list={publishList.current}
value={linkType}
onChange={(e) => setLinkType(e as PublishChannelEnum)}
onChange={(e) => {
const config = publishList.current.find((v) => v.value === e)!;
if (!feConfigs.isPlus && config.isProFn) {
toast({
status: 'warning',
title: t('common:common.system.Commercial version function')
});
} else {
setLinkType(e as PublishChannelEnum);
}
}}
/>
</Box>
@@ -63,15 +99,16 @@ const OutLink = () => {
mt={4}
px={[4, 8]}
py={[4, 6]}
flex={'1 0 0'}
flex={1}
>
{linkType === PublishChannelEnum.share && (
<Link appId={appId} type={PublishChannelEnum.share} />
)}
{linkType === PublishChannelEnum.apikey && <API appId={appId} />}
{linkType === PublishChannelEnum.feishu && <FeiShu appId={appId} />}
{linkType === PublishChannelEnum.wecom && <Wecom appId={appId} />}
</Flex>
</>
</Box>
);
};

View File

@@ -6,7 +6,7 @@ import Edit from './Edit';
import { useContextSelector } from 'use-context-selector';
import { AppContext, TabEnum } from '../context';
import dynamic from 'next/dynamic';
import { Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useTranslation } from 'next-i18next';
@@ -29,10 +29,10 @@ const SimpleEdit = () => {
{currentTab === TabEnum.appEdit ? (
<Edit appForm={appForm} setAppForm={setAppForm} />
) : (
<Flex h={'100%'} flexDirection={'column'} mt={4}>
<Box flex={'1 0 0'} h={0} mt={4}>
{currentTab === TabEnum.publish && <PublishChannel />}
{currentTab === TabEnum.logs && <Logs />}
</Flex>
</Box>
)}
</Flex>
);