diff --git a/packages/global/support/outLink/constant.ts b/packages/global/support/outLink/constant.ts index 44224726c..aaa1bafb6 100644 --- a/packages/global/support/outLink/constant.ts +++ b/packages/global/support/outLink/constant.ts @@ -3,5 +3,6 @@ export enum PublishChannelEnum { iframe = 'iframe', apikey = 'apikey', feishu = 'feishu', - wecom = 'wecom' + wecom = 'wecom', + officialAccount = 'official_account' } diff --git a/packages/global/support/outLink/type.d.ts b/packages/global/support/outLink/type.d.ts index c947cdf69..ec882c00b 100644 --- a/packages/global/support/outLink/type.d.ts +++ b/packages/global/support/outLink/type.d.ts @@ -25,7 +25,18 @@ export interface WecomAppType { // TODO: unused export interface WechatAppType {} -export type OutlinkAppType = FeishuAppType | WecomAppType | undefined; +export interface OffiAccountAppType { + appId: string; + isVerified?: boolean; // if isVerified, we could use '客服接口' to reply + secret: string; + CallbackToken: string; + CallbackEncodingAesKey?: string; + timeoutReply?: string; // if timeout (15s), will reply this content. + // timeout reply is optional, but when isVerified is false, the wechat will reply a default message which is `该公众号暂时无法提供服务,请稍后再试` + // because we can not reply anything in 15s. Thus, the wechat server will treat this request as a failed request. +} + +export type OutlinkAppType = FeishuAppType | WecomAppType | OffiAccountAppType | undefined; export type OutLinkSchema = { _id: string; diff --git a/packages/global/support/tmpData/constant.ts b/packages/global/support/tmpData/constant.ts index f548dcc3b..96ea8fef8 100644 --- a/packages/global/support/tmpData/constant.ts +++ b/packages/global/support/tmpData/constant.ts @@ -1,6 +1,7 @@ export enum TmpDataEnum { FeishuAccessToken = 'feishu_access_token', - WecomAccessToken = 'wecom_access_token' + WecomAccessToken = 'wecom_access_token', + OffiAccountAccessToken = 'offiaccount_access_token' } type _TmpDataMetadata = { @@ -11,6 +12,9 @@ type _TmpDataMetadata = { CorpId: string; AgentId: string; }; + [TmpDataEnum.OffiAccountAccessToken]: { + AppId: string; + }; }; type _TmpDataType = { @@ -20,11 +24,15 @@ type _TmpDataType = { [TmpDataEnum.WecomAccessToken]: { accessToken: string; }; + [TmpDataEnum.OffiAccountAccessToken]: { + accessToken: string; + }; }; export const TmpDataExpireTime = { [TmpDataEnum.FeishuAccessToken]: 1000 * 60 * 60 * 1.5, // 1.5 hours - [TmpDataEnum.WecomAccessToken]: 1000 * 60 * 60 * 2 // 2 hours + [TmpDataEnum.WecomAccessToken]: 1000 * 60 * 60 * 2, // 2 hours + [TmpDataEnum.OffiAccountAccessToken]: 1000 * 60 * 60 * 2 // 2 hours }; export type TmpDataMetadata = _TmpDataMetadata[T]; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index ab8a5bbdc..c64585be8 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -76,6 +76,8 @@ export const iconPaths = { 'core/app/logsLight': () => import('./icons/core/app/logsLight.svg'), 'core/app/markLight': () => import('./icons/core/app/markLight.svg'), 'core/app/publish/lark': () => import('./icons/core/app/publish/lark.svg'), + 'core/app/publish/offiaccount': () => import('./icons/core/app/publish/offiaccount.svg'), + 'core/app/publish/wechat': () => import('./icons/core/app/publish/wechat.svg'), 'core/app/publish/wecom': () => import('./icons/core/app/publish/wecom.svg'), 'core/app/questionGuide': () => import('./icons/core/app/questionGuide.svg'), 'core/app/schedulePlan': () => import('./icons/core/app/schedulePlan.svg'), diff --git a/packages/web/components/common/Icon/icons/core/app/publish/offiaccount.svg b/packages/web/components/common/Icon/icons/core/app/publish/offiaccount.svg new file mode 100644 index 000000000..26afb8dca --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/publish/offiaccount.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/app/publish/wechat.svg b/packages/web/components/common/Icon/icons/core/app/publish/wechat.svg new file mode 100644 index 000000000..af29c77a0 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/app/publish/wechat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/web/i18n/zh/publish.json b/packages/web/i18n/zh/publish.json index d24a34530..ca10b0c8d 100644 --- a/packages/web/i18n/zh/publish.json +++ b/packages/web/i18n/zh/publish.json @@ -31,5 +31,12 @@ "edit_modal_title": "编辑企微机器人", "create_modal_title": "创建企微机器人", "api": "企微 API" + }, + "official_account": { + "name": "微信公众号接入", + "desc": "通过 API 直接接入微信公众号", + "edit_modal_title": "编辑微信公众号接入", + "create_modal_title": "创建微信公众号接入", + "api": "微信公众号 API" } } diff --git a/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png b/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png new file mode 100644 index 000000000..e04324b5c Binary files /dev/null and b/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png differ diff --git a/projects/app/src/pages/api/support/outLink/offiaccount/[token].ts b/projects/app/src/pages/api/support/outLink/offiaccount/[token].ts new file mode 100644 index 000000000..b7470027c --- /dev/null +++ b/projects/app/src/pages/api/support/outLink/offiaccount/[token].ts @@ -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, + res: ApiResponseType +): Promise { + 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; diff --git a/projects/app/src/pages/api/support/outLink/wecom/[token].ts b/projects/app/src/pages/api/support/outLink/wecom/[token].ts index fd2b6e89a..0b46aba8e 100644 --- a/projects/app/src/pages/api/support/outLink/wecom/[token].ts +++ b/projects/app/src/pages/api/support/outLink/wecom/[token].ts @@ -9,6 +9,8 @@ async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { + // WARN: it is not supported yet. + return {}; const { token, type } = req.query; const result = await plusRequest({ url: `support/outLink/wecom/${token}`, diff --git a/projects/app/src/pages/app/detail/components/Publish/OffiAccount/OffiAccountEditModal.tsx b/projects/app/src/pages/app/detail/components/Publish/OffiAccount/OffiAccountEditModal.tsx new file mode 100644 index 000000000..ecca68b3c --- /dev/null +++ b/projects/app/src/pages/app/detail/components/Publish/OffiAccount/OffiAccountEditModal.tsx @@ -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; + 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 ( + + + + + + + + {t('publish:official_account.api')} + {feConfigs?.docUrl && ( + + + + {t('common:common.Read document')} + + + )} + + + + App ID + + + + + + Secret + + + + + + Token + + + + + AES Key + + + + + + + + + + + + + ); +}; + +export default OffiAccountEditModal; diff --git a/projects/app/src/pages/app/detail/components/Publish/OffiAccount/index.tsx b/projects/app/src/pages/app/detail/components/Publish/OffiAccount/index.tsx new file mode 100644 index 000000000..b2e671ad8 --- /dev/null +++ b/projects/app/src/pages/app/detail/components/Publish/OffiAccount/index.tsx @@ -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>(); + const [isEdit, setIsEdit] = useState(false); + + const baseUrl = useMemo( + () => feConfigs?.customApiDomain || `${location.origin}/api`, + [feConfigs?.customApiDomain] + ); + + const { + data: shareChatList = [], + loading: isFetching, + runAsync: refetchShareChatList + } = useRequest2( + () => getShareChatList({ appId, type: PublishChannelEnum.officialAccount }), + { + manual: false + } + ); + + const { + onOpen: openShowShareLinkModal, + isOpen: showShareLinkModalOpen, + onClose: closeShowShareLinkModal + } = useDisclosure(); + + const [showShareLink, setShowShareLink] = useState(null); + + return ( + + + + {t('publish:official_account.name')} + + + + + + + + + + {feConfigs?.isPlus && ( + <> + + + + )} + + + + + + {shareChatList.map((item) => ( + + + + {feConfigs?.isPlus && ( + <> + + + + )} + + + + ))} + +
{t('common:common.Name')} {t('common:support.outlink.Usage points')} {t('common:core.app.share.Ip limit title')} {t('common:common.Expired Time')} {t('common:common.Last use time')}
{item.name} + {Math.round(item.usagePoints)} + {feConfigs?.isPlus + ? `${ + item.limit?.maxUsagePoints && item.limit.maxUsagePoints > -1 + ? ` / ${item.limit.maxUsagePoints}` + : ` / ${t('common:common.Unlimited')}` + }` + : ''} + {item?.limit?.QPM || '-'} + {item?.limit?.expiredTime + ? dayjs(item.limit?.expiredTime).format('YYYY/MM/DD\nHH:mm') + : '-'} + + {item.lastTime + ? t(formatTimeToChatTime(item.lastTime) as any) + : t('common:common.Un used')} + + + + } + 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); + } + } + ] + } + ]} + /> +
+
+ {editOffiAccountData && ( + Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])} + onEdit={() => Promise.all([refetchShareChatList(), setEditOffiAccountData(undefined)])} + onClose={() => setEditOffiAccountData(undefined)} + isEdit={isEdit} + /> + )} + {shareChatList.length === 0 && !isFetching && ( + + )} + + {showShareLinkModalOpen && ( + + )} +
+ ); +}; + +export default React.memo(OffiAccount); diff --git a/projects/app/src/pages/app/detail/components/Publish/Wecom/WecomEditModal.tsx b/projects/app/src/pages/app/detail/components/Publish/Wecom/WecomEditModal.tsx index 478c201a2..839459364 100644 --- a/projects/app/src/pages/app/detail/components/Publish/Wecom/WecomEditModal.tsx +++ b/projects/app/src/pages/app/detail/components/Publish/Wecom/WecomEditModal.tsx @@ -138,9 +138,7 @@ const WecomEditModal = ({ diff --git a/projects/app/src/pages/app/detail/components/Publish/index.tsx b/projects/app/src/pages/app/detail/components/Publish/index.tsx index 5f846cdf9..c6f769cc5 100644 --- a/projects/app/src/pages/app/detail/components/Publish/index.tsx +++ b/projects/app/src/pages/app/detail/components/Publish/index.tsx @@ -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 && } {linkType === PublishChannelEnum.feishu && } - {linkType === PublishChannelEnum.wecom && } + {/* {linkType === PublishChannelEnum.wecom && } */} + {linkType === PublishChannelEnum.officialAccount && } );