diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 3de253c0a..42e3715a2 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -137,6 +137,8 @@ export type FastGPTFeConfigsType = { volcengine?: string; }; }; + + ip_whitelist?: string; }; export type SystemEnvType = { diff --git a/packages/global/support/customDomain/type.ts b/packages/global/support/customDomain/type.ts index 90d40c06f..247c332c3 100644 --- a/packages/global/support/customDomain/type.ts +++ b/packages/global/support/customDomain/type.ts @@ -29,3 +29,9 @@ export type CreateCustomDomainBody = { }; export type ProviderEnum = z.infer; export type CustomDomainStatusEnum = z.infer; + +export type UpdateDomainVerifyFileBody = { + domain: string; + path: string; + content: string; +}; diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index 25405e180..1c8d2de68 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -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": "默认模型配置", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 1b1ed7918..f58186211 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -301,6 +301,7 @@ "pro_modal_title": "商业版专享!", "pro_modal_unlock_button": "去解锁", "publish_channel": "发布渠道", + "publish_channel.wecom.empty": "发布到企业微信机器人,请先 绑定自定义域名,并且通过域名校验。", "publish_success": "发布成功", "question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。", "reasoning_response": "输出思考", diff --git a/packages/web/i18n/zh-CN/publish.json b/packages/web/i18n/zh-CN/publish.json index fbcbe59fd..3ce6153ea 100644 --- a/packages/web/i18n/zh-CN/publish.json +++ b/packages/web/i18n/zh-CN/publish.json @@ -41,5 +41,6 @@ "dingtalk.create_modal_title": "创建钉钉机器人", "dingtalk.edit_modal_title": "编辑钉钉机器人", "dingtalk.title": "发布到钉钉机器人", - "dingtalk.api": "钉钉 API" + "dingtalk.api": "钉钉 API", + "use_default_domain": "使用默认域名" } diff --git a/projects/app/src/pageComponents/account/customDomain/domainVerifyModal.tsx b/projects/app/src/pageComponents/account/customDomain/domainVerifyModal.tsx new file mode 100644 index 000000000..aefea7dfe --- /dev/null +++ b/projects/app/src/pageComponents/account/customDomain/domainVerifyModal.tsx @@ -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({ + 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 ( + + + + + {t('account:custom_domain.domain_verify.path')} + + + + {t('account:custom_domain.domain_verify.content')} + + + + {t('account:custom_domain.domain_verify.desc', { domain, path, content })} + + + + + + ); +} + +export default CreateCustomDomainModal; diff --git a/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx b/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx index f1da69ab7..b1f2f1045 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx @@ -75,7 +75,10 @@ const WecomEditModal = ({ {t('publish:wecom.api')} {feConfigs?.docUrl && ( { /> )} {shareChatList.length === 0 && !isFetching && ( - + + ) + }} + /> + } + > )} {showShareLinkModalOpen && ( diff --git a/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx b/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx index 5826cb12d..df3d13e8f 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx @@ -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(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 ( + {/* 自定义域名选择器 */} + {domainOptions.length > 1 && ( + + setCustomDomain(value || undefined)} + /> + + )} + copyData(shareLink)} + onClick={() => copyData(displayShareLink)} /> - {shareLink} + {displayShareLink} + + + + + IP 白名单 + copyData(feConfigs?.ip_whitelist || '')} + /> + + + + {feConfigs.ip_whitelist} + + ); diff --git a/projects/app/src/pages/account/customDomain/index.tsx b/projects/app/src/pages/account/customDomain/index.tsx index 2d2aa7c6d..a1c65bebb 100644 --- a/projects/app/src/pages/account/customDomain/index.tsx +++ b/projects/app/src/pages/account/customDomain/index.tsx @@ -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)` : <>} - @@ -90,23 +99,37 @@ const CustomDomain = () => { {t(providerMap[customDomain.provider])} {t(customDomainStatusMap[customDomain.status])} - {customDomain.status === 'inactive' ? ( - + {customDomain.status === 'inactive' ? ( + + ) : ( + + )} + + )) @@ -136,6 +159,15 @@ const CustomDomain = () => { data={editDomain!} /> )} + {isOpenDomainVerify && editDomain?.domain && ( + { + onCloseDomainVerify(); + setEditDomain(undefined); + }} + /> + )} ); }; diff --git a/projects/app/src/web/support/customDomain/api.ts b/projects/app/src/web/support/customDomain/api.ts index 7bcf1255a..20fcbe6f0 100644 --- a/projects/app/src/web/support/customDomain/api.ts +++ b/projects/app/src/web/support/customDomain/api.ts @@ -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 + );