mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
feat: wecom custom domain
This commit is contained in:
parent
237c6eaa21
commit
edb83bc2b9
|
|
@ -137,6 +137,8 @@ export type FastGPTFeConfigsType = {
|
|||
volcengine?: string;
|
||||
};
|
||||
};
|
||||
|
||||
ip_whitelist?: string;
|
||||
};
|
||||
|
||||
export type SystemEnvType = {
|
||||
|
|
|
|||
|
|
@ -29,3 +29,9 @@ export type CreateCustomDomainBody = {
|
|||
};
|
||||
export type ProviderEnum = z.infer<typeof ProviderEnum>;
|
||||
export type CustomDomainStatusEnum = z.infer<typeof CustomDomainStatusEnum>;
|
||||
|
||||
export type UpdateDomainVerifyFileBody = {
|
||||
domain: string;
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@
|
|||
"custom_domain.delete_confirm": "确认删除该自定义域名?",
|
||||
"custom_domain.status.active": "已生效",
|
||||
"custom_domain.status.inactive": "已失效",
|
||||
"custom_domain.domain_verify": "域名校验",
|
||||
"custom_domain.domain_verify.path": "文件路径",
|
||||
"custom_domain.domain_verify.content": "文件内容",
|
||||
"custom_domain.domain_verify.desc": "保存后,访问 {{domain}}/{{path}} 将返回 {{content}}",
|
||||
"day": "天",
|
||||
"default_model": "预设模型",
|
||||
"default_model_config": "默认模型配置",
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@
|
|||
"pro_modal_title": "商业版专享!",
|
||||
"pro_modal_unlock_button": "去解锁",
|
||||
"publish_channel": "发布渠道",
|
||||
"publish_channel.wecom.empty": "发布到企业微信机器人,请先 <a>绑定自定义域名</a>,并且通过域名校验。",
|
||||
"publish_success": "发布成功",
|
||||
"question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。",
|
||||
"reasoning_response": "输出思考",
|
||||
|
|
|
|||
|
|
@ -41,5 +41,6 @@
|
|||
"dingtalk.create_modal_title": "创建钉钉机器人",
|
||||
"dingtalk.edit_modal_title": "编辑钉钉机器人",
|
||||
"dingtalk.title": "发布到钉钉机器人",
|
||||
"dingtalk.api": "钉钉 API"
|
||||
"dingtalk.api": "钉钉 API",
|
||||
"use_default_domain": "使用默认域名"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import {
|
||||
ModalBody,
|
||||
Box,
|
||||
Radio,
|
||||
Flex,
|
||||
Text,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Tag,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
IconButton,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Link,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation, Trans } from 'next-i18next';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { providerMap } from '@/web/support/customDomain/const';
|
||||
import type { ProviderEnum } from '@fastgpt/global/support/customDomain/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { generateCNAMEDomain } from '@fastgpt/global/support/customDomain/utils';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
activeCustomDomain,
|
||||
checkCustomDomainDNSResolve,
|
||||
createCustomDomain,
|
||||
updateCustomDomainVerifyFile
|
||||
} from '@/web/support/customDomain/api';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
|
||||
function CreateCustomDomainModal<T extends 'create' | 'refresh'>({
|
||||
onClose,
|
||||
domain
|
||||
}: {
|
||||
onClose: () => void;
|
||||
domain: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { watch, handleSubmit, register } = useForm({
|
||||
defaultValues: {
|
||||
path: '',
|
||||
content: ''
|
||||
}
|
||||
});
|
||||
|
||||
const path = watch('path');
|
||||
const content = watch('content');
|
||||
|
||||
const { runAsync: updateVerifyFile, loading: isUpdating } = useRequest2(
|
||||
updateCustomDomainVerifyFile,
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
successToast: t('common:Success')
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} title={t('account:custom_domain.domain_verify')} minW="800px">
|
||||
<ModalBody>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap="16px">
|
||||
<Box>
|
||||
<FormLabel required>{t('account:custom_domain.domain_verify.path')}</FormLabel>
|
||||
<Input {...register('path')} />
|
||||
</Box>
|
||||
<Box>
|
||||
<FormLabel required>{t('account:custom_domain.domain_verify.content')}</FormLabel>
|
||||
<Input {...register('content')} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt="2">{t('account:custom_domain.domain_verify.desc', { domain, path, content })}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={handleSubmit(() => updateVerifyFile({ domain, path, content }))}
|
||||
isLoading={isUpdating}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateCustomDomainModal;
|
||||
|
|
@ -75,7 +75,10 @@ const WecomEditModal = ({
|
|||
<Box color="myGray.600">{t('publish:wecom.api')}</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/wecom-bot')}
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/wecom')
|
||||
}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure
|
||||
useDisclosure,
|
||||
Link
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
|
|
@ -19,7 +20,7 @@ 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 { Trans, useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import dayjs from 'dayjs';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
|
@ -206,7 +207,24 @@ const Wecom = ({ appId }: { appId: string }) => {
|
|||
/>
|
||||
)}
|
||||
{shareChatList.length === 0 && !isFetching && (
|
||||
<EmptyTip text={t('common:core.app.share.Not share link')}> </EmptyTip>
|
||||
<EmptyTip
|
||||
text={
|
||||
<Trans
|
||||
i18nKey="app:publish_channel.wecom.empty"
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
color="primary.600"
|
||||
key="link"
|
||||
href="/account/customDomain"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></EmptyTip>
|
||||
)}
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
{showShareLinkModalOpen && (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
|
|||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { listCustomDomain } from '@/web/support/customDomain/api';
|
||||
import { useState, useMemo } from 'react';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
|
||||
export type ShowShareLinkModalProps = {
|
||||
shareLink: string;
|
||||
|
|
@ -14,10 +19,66 @@ export type ShowShareLinkModalProps = {
|
|||
function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) {
|
||||
const { copyData } = useCopyData();
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const [customDomain, setCustomDomain] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data: customDomainList = [] } = useRequest2(listCustomDomain, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
// 从 shareLink 中提取原始域名
|
||||
const originalDomain = useMemo(() => {
|
||||
try {
|
||||
const url = new URL(shareLink);
|
||||
return url.origin;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [shareLink]);
|
||||
|
||||
// 计算显示的分享链接(使用自定义域名替换原始域名)
|
||||
const displayShareLink = useMemo(() => {
|
||||
if (!customDomain || !originalDomain) {
|
||||
return shareLink;
|
||||
}
|
||||
return shareLink.replace(originalDomain, `https://${customDomain}`);
|
||||
}, [shareLink, customDomain, originalDomain]);
|
||||
|
||||
// 处理域名选择选项
|
||||
const domainOptions = useMemo(() => {
|
||||
const options = [
|
||||
{
|
||||
label: t('publish:use_default_domain') || '使用默认域名',
|
||||
value: ''
|
||||
}
|
||||
];
|
||||
|
||||
// 只显示已激活的自定义域名
|
||||
const activeDomains = customDomainList
|
||||
.filter((item) => item.status === 'active')
|
||||
.map((item) => ({
|
||||
label: item.domain,
|
||||
value: item.domain
|
||||
}));
|
||||
|
||||
return [...options, ...activeDomains];
|
||||
}, [customDomainList, t]);
|
||||
|
||||
return (
|
||||
<MyModal onClose={onClose} title={t('publish:show_share_link_modal_title')}>
|
||||
<ModalBody>
|
||||
{/* 自定义域名选择器 */}
|
||||
{domainOptions.length > 1 && (
|
||||
<Box mb={4}>
|
||||
<MySelect
|
||||
value={customDomain || ''}
|
||||
list={domainOptions}
|
||||
onChange={(value) => setCustomDomain(value || undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
|
|
@ -33,16 +94,41 @@ function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps
|
|||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() => copyData(shareLink)}
|
||||
onClick={() => copyData(displayShareLink)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
|
||||
{shareLink}
|
||||
{displayShareLink}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
|
||||
<MyImage src={img} borderRadius="0.5rem" alt="" />
|
||||
</Box>
|
||||
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'} mt="4">
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border="base"
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex="1">IP 白名单</Box>
|
||||
<MyIcon
|
||||
name={'copy'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() => copyData(feConfigs?.ip_whitelist || '')}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box p={3} wordBreak={'break-all'}>
|
||||
{feConfigs.ip_whitelist}
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import AccountContainer from '@/pageComponents/account/AccountContainer';
|
||||
import IconButton from '@/pageComponents/account/team/OrgManage/IconButton';
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import { deleteCustomDomain, listCustomDomain } from '@/web/support/customDomain/api';
|
||||
import {
|
||||
|
|
@ -28,6 +27,10 @@ const CreateCustomDomainModal = dynamic(
|
|||
() => import('@/pageComponents/account/customDomain/createModal')
|
||||
);
|
||||
|
||||
const DomainVerifyModal = dynamic(
|
||||
() => import('@/pageComponents/account/customDomain/domainVerifyModal')
|
||||
);
|
||||
|
||||
const CustomDomain = () => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
|
@ -43,6 +46,12 @@ const CustomDomain = () => {
|
|||
onClose: onCloseCreateModal
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isOpenDomainVerify,
|
||||
onOpen: onOpenDomainVerify,
|
||||
onClose: onCloseDomainVerify
|
||||
} = useDisclosure();
|
||||
|
||||
const { runAsync: onDelete, loading: loadingDelete } = useRequest2(deleteCustomDomain, {
|
||||
manual: true,
|
||||
successToast: t('common:Success'),
|
||||
|
|
@ -66,7 +75,7 @@ const CustomDomain = () => {
|
|||
{t('account:custom_domain')}
|
||||
{customDomainList?.length ? `: (${customDomainList.length}/3)` : <></>}
|
||||
</Box>
|
||||
<Button variant="outline" onClick={onOpenCreateModal}>
|
||||
<Button variant="whitePrimaryOutline" onClick={onOpenCreateModal}>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
|
@ -90,23 +99,37 @@ const CustomDomain = () => {
|
|||
<Td>{t(providerMap[customDomain.provider])}</Td>
|
||||
<Td>{t(customDomainStatusMap[customDomain.status])}</Td>
|
||||
<Td>
|
||||
{customDomain.status === 'inactive' ? (
|
||||
<IconButton
|
||||
name="edit"
|
||||
<Flex gap="2">
|
||||
{customDomain.status === 'inactive' ? (
|
||||
<Button
|
||||
variant="whitePrimary"
|
||||
onClick={() => {
|
||||
setEditDomain(customDomain);
|
||||
onOpenCreateModal();
|
||||
}}
|
||||
>
|
||||
{t('common:Edit')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="whitePrimary"
|
||||
onClick={() => {
|
||||
setEditDomain(customDomain);
|
||||
onOpenDomainVerify();
|
||||
}}
|
||||
>
|
||||
{t('account:custom_domain.domain_verify')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="whiteDanger"
|
||||
onClick={() => {
|
||||
setEditDomain(customDomain);
|
||||
onOpenCreateModal();
|
||||
return openConfirm(() => onDelete(customDomain.domain))();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<IconButton
|
||||
name="delete"
|
||||
onClick={() => {
|
||||
return openConfirm(() => onDelete(customDomain.domain))();
|
||||
}}
|
||||
></IconButton>
|
||||
>
|
||||
{t('common:Delete')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
|
|
@ -136,6 +159,15 @@ const CustomDomain = () => {
|
|||
data={editDomain!}
|
||||
/>
|
||||
)}
|
||||
{isOpenDomainVerify && editDomain?.domain && (
|
||||
<DomainVerifyModal
|
||||
domain={editDomain?.domain}
|
||||
onClose={() => {
|
||||
onCloseDomainVerify();
|
||||
setEditDomain(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,3 +26,13 @@ export const activeCustomDomain = (domain: string) =>
|
|||
});
|
||||
|
||||
// TODO: verify files
|
||||
|
||||
export const updateCustomDomainVerifyFile = (props: {
|
||||
domain: string;
|
||||
path: string;
|
||||
content: string;
|
||||
}) =>
|
||||
POST<{ success: boolean; message: string }>(
|
||||
'/proApi/support/customDomain/updateVerifyFile',
|
||||
props
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue