diff --git a/document/content/docs/introduction/guide/team_permissions/customDomain.mdx b/document/content/docs/introduction/guide/team_permissions/customDomain.mdx new file mode 100644 index 0000000000..de504a0f48 --- /dev/null +++ b/document/content/docs/introduction/guide/team_permissions/customDomain.mdx @@ -0,0 +1,42 @@ +--- +title: 配置自定义域名 +description: 如何在 FastGPT 中配置自定义域名 +--- + +FastGPT 云服务版自 v4.14.4 后支持配置自定义域名。 + +## 如何配置自定义域名 + +### 1. 打开“自定义域名”页面 + +在侧边栏选择“账号” -> “自定义域名”,打开自定义域名配置页。 + +如果您的套餐等级不支持配置,请根据页面的指引升级套餐。 + +![打开配置页面](/imgs/guide/team_permissions/customDomain/1.png) + +### 2. 添加自定义域名 + +1. 准备好您的域名。您的域名必须先经过备案,目前支持“阿里云”、“腾讯云”、“火山引擎”三家服务商的备案域名。 +2. 点击“编辑”按钮,进入编辑状态。 +3. 填入您的域名,例如 www.example.com +4. 在域名服务商的域名解析处,添加界面中提示的 DNS 纪录,注意纪录类型为 CNAME。 +5. 添加解析纪录后,点击“保存”按钮。系统将自动检查 DNS 解析情况,一般情况下,在一分钟内就可以获取到解析纪录。如果长时间没有获取到纪录,可以重试一次。 +6. 待状态提示显示为“已生效”后,点击“确认”按钮即可。 + +![配置自定义域名](/imgs/guide/team_permissions/customDomain/2.png) + +现在您可以通过您自己的域名访问 fastgpt 服务、调用 fastgpt 的 API 了。 + +## 域名解析失效 + +系统会每天对 DNS 解析进行检查,如果发现 DNS 解析纪录失效,则会停用该自定义域名,可以在“自定义域名”管理界面中点击“编辑”进行重新解析。 + +![编辑](/imgs/guide/team_permissions/customDomain/3.png) + +如果您需要修改自定义域名、或修改服务商,则需要删除自定义域名配置后进行重新配置。 + + +## 使用案例 + +- [接入企业微信智能机器人](/docs/use-cases/external-integration/wecom) diff --git a/document/content/docs/introduction/guide/team_permissions/meta.json b/document/content/docs/introduction/guide/team_permissions/meta.json index e1ec7bdf4e..88edf31f4f 100644 --- a/document/content/docs/introduction/guide/team_permissions/meta.json +++ b/document/content/docs/introduction/guide/team_permissions/meta.json @@ -1,5 +1,5 @@ { "title": "团队与权限", "description": "团队管理、成员组与权限设置,确保团队协作中的数据安全和权限分配合理。", - "pages": ["team_roles_permissions","invitation_link"] -} \ No newline at end of file + "pages": ["team_roles_permissions","invitation_link", "customDomain"] +} diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index df924c944b..37924d06f3 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -92,6 +92,7 @@ description: FastGPT 文档目录 - [/docs/introduction/guide/plugins/google_search_plugin_guide](/docs/introduction/guide/plugins/google_search_plugin_guide) - [/docs/introduction/guide/plugins/searxng_plugin_guide](/docs/introduction/guide/plugins/searxng_plugin_guide) - [/docs/introduction/guide/plugins/upload_system_tool](/docs/introduction/guide/plugins/upload_system_tool) +- [/docs/introduction/guide/team_permissions/customDomain](/docs/introduction/guide/team_permissions/customDomain) - [/docs/introduction/guide/team_permissions/invitation_link](/docs/introduction/guide/team_permissions/invitation_link) - [/docs/introduction/guide/team_permissions/team_roles_permissions](/docs/introduction/guide/team_permissions/team_roles_permissions) - [/docs/introduction/index](/docs/introduction/index) diff --git a/document/content/docs/use-cases/external-integration/wecom.mdx b/document/content/docs/use-cases/external-integration/wecom.mdx index 2d9f9ba8d1..712c24c121 100644 --- a/document/content/docs/use-cases/external-integration/wecom.mdx +++ b/document/content/docs/use-cases/external-integration/wecom.mdx @@ -3,119 +3,55 @@ title: 接入企微机器人教程 description: FastGPT 接入企微机器人教程 --- -从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。 +- 从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。 +- 从 4.14.4 版本起,FastGPT 云服务版支持通过配置自定义域名的方式接入企微智能机器人。 -## 1. 配置可信域名和可信IP +## 1. (云服务版必须)配置自定义域名 -点击企微头像,打开管理企业 +企微要求智能机器人消息推送地址必须使用企业主体域名,因此云服务版本用户必须先配置自定义域名才能使用企微机器人。 -![图片](/imgs/wecom-bot-1.png) +- [配置自定义域名](/docs/introduction/guide/team_permissions/customDomain) -在应用管理中找到"自建"-"创建应用" +若您是商业版用户,请继续使用您企业的域名。 -![图片](/imgs/wecom-bot-2.png) +## 2. 创建智能机器人 -创建好应用后, 下拉, 依次配置"网页授权及JS-SDK"和"企业可信IP" +1. 超级管理员登录 [企业微信管理后台](https://work.weixin.qq.com/) +2. 在"安全与管理" - "管理工具"页面点击"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 ) -![图片](/imgs/wecom-bot-3.png) +![图片](/imgs/use-cases/external-integration/wecom/1.png) -其中, 网页授权及JS-SDK要求按照企微指引,完成域名归属认证 +3. 在创建机器人页面, 下拉, 点击 "API模式创建" -![图片](/imgs/wecom-bot-4.png) +![图片](/imgs/use-cases/external-integration/wecom/2.png) -企业可信IP要求为企业服务器IP, 后续企微的回调URL将请求到此IP +4. 随机生成或者手动输入 Token 和 Encoding-AESKey,并且纪录下来 -![图片](/imgs/wecom-bot-5.png) +![图片](/imgs/use-cases/external-integration/wecom/3.png) -## 2. 创建企业自建应用 +5. 在 FastGPT 中,选择要使用 Agent,在发布渠道页面,选择“企业微信机器人”,点击“创建” -前往 FastGPT ,选择想要接入的应用,在 发布渠道 页面,新建一个接入企微智能机器人的发布渠道,填写好基础信息。 +![图片](/imgs/use-cases/external-integration/wecom/4.png) -![图片](/imgs/wecom-bot-6.png) +6. 配置该发布渠道的信息,需要填入 Token 和 AESKey,也就是第四步中纪录下来的 Token 和 Encoding-AESKey -现在回到企业微信平台,找到 Corp ID, Secret, Agent ID, Token, AES Key 信息并填写回 FastGPT 平台 +![图片](/imgs/use-cases/external-integration/wecom/5.png) -![图片](/imgs/wecom-bot-7.png) +7. 点击“确认”后,选择您配置的自定义域名,复制回调地址,填回企微智能机器人配置页中。 -在"我的企业"里找到企业 ID, 填写到 FastGPT 的 Corp ID 中 +![图片](/imgs/use-cases/external-integration/wecom/6.png) -![图片](/imgs/wecom-bot-8.png) - -在应用中找到 Agent Id 和 Secret, 并填写回 FastGPT - -![图片](/imgs/wecom-bot-9.png) - -点击"消息接收"-"设置API接收" - -![图片](/imgs/wecom-bot-10.png) - -随机生成或者手动输入 Token 和 Encoding-Key, 分别填写到 FastGPT 的 Token 和 AES Key 中 - -![图片](/imgs/wecom-bot-11.png) - -填写完成后确认创建 - -然后点击请求地址, 复制页面中的链接 - -![图片](/imgs/wecom-bot-12.png) - -回到刚才的配置详情, 将刚才复制的链接填入 URL 框中, 并点击下方的保存 ,即可完成自建应用的创建 - -注意: 若复制的链接是以 "http://localhost" 开头, 需要将本地地址改为企业主体域名 - -因为企微会给填写的 URL 发送验证密文, 若 URL 为本地地址, 则本地接收不到企微的密文 - -若 URL 不是企业主体域名, 则验证会失败 - -## 3. 创建智能机器人 - -第二步创建企业自建应用是为了验证域名和IP的合规性, 并获取 secret 参数, 下面创建智能机器人才是正式的配置流程 - -在"安全与管理" - "管理工具"页面找到"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 ) - -![图片](/imgs/wecom-bot-13.png) - -创建机器人页面,下拉,找到,点击"API模式创建" - -![图片](/imgs/wecom-bot-14.png) - -与刚才配置自建应用同理, 在 FastGPT 平台再新增一个发布渠道, 并回到企业微信配置参数 - -![图片](/imgs/wecom-bot-19.png) - -随机生成或者手动输入 Token 和 Encoding-AESKey, 分别填写到 FastGPT 的 Token 和 AES Key 中 - -![图片](/imgs/wecom-bot-15.png) - -Corp ID 和 Secret 这两个参数和刚才的自建应用保持一致 - -Agent ID 和自建应用的不同, 需要先填写一个自定义值, 后续会根据企业微信提供的数据重新更改 - -在 FastGPT 将Corp ID, Secret, Agent ID, Token, AES Key 等参数都填写完毕后, 点击确认 - -然后点击请求地址, 复制页面中的链接 - -回到企业微信, 将链接粘贴到智能机器人的 URL 配置栏, 点击创建 - -创建完成后, 找到智能机器人的配置详情 - -![图片](/imgs/wecom-bot-16.png) - -复制 Bot ID, 填写到 FastGPT 的 Agent ID 中, 即可完成智能机器人配置 - -![图片](/imgs/wecom-bot-17.png) - -## 4. 使用智能机器人 +## 3. 使用智能机器人 在企业微信平台的"通讯录",即可找到创建的机器人,就可以发送消息了 -![图片](/imgs/wecom-bot-18.png) +![图片](/imgs/use-cases/external-integration/wecom/7.png) ## FAQ ### 发送了消息,没响应 -1. 检查可信域名和可信IP是否配置正确。 -2. 检查自建应用的 Secret 参数是否与智能机器人一致。 -3. 查看 FastGPT 对话日志,是否有对应的提问记录 +1. 检查可信域名是否配置正确。 +2. 检查 Token 和 Encoding-AESKey 是否正确。 +3. 查看 FastGPT 对话日志,是否有对应的提问记录。 4. 如果没记录,则可能是应用运行报错了,可以先试试最简单的机器人. diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 69d243dfb0..d17198805e 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -34,7 +34,7 @@ "document/content/docs/introduction/development/openapi/chat.mdx": "2025-11-14T13:21:17+08:00", "document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-29T11:34:11+08:00", "document/content/docs/introduction/development/openapi/intro.mdx": "2025-09-29T11:34:11+08:00", - "document/content/docs/introduction/development/openapi/share.mdx": "2025-12-08T21:02:38+08:00", + "document/content/docs/introduction/development/openapi/share.mdx": "2025-12-09T12:18:15+08:00", "document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/http_proxy.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/development/proxy/nginx.mdx": "2025-07-23T21:35:03+08:00", @@ -89,6 +89,7 @@ "document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-11-04T16:58:12+08:00", + "document/content/docs/introduction/guide/team_permissions/customDomain.mdx": "2025-12-08T22:21:05+08:00", "document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00", @@ -101,7 +102,7 @@ "document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00", "document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00", - "document/content/docs/toc.mdx": "2025-11-29T09:24:47+08:00", + "document/content/docs/toc.mdx": "2025-12-08T22:21:05+08:00", "document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00", "document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00", @@ -198,6 +199,6 @@ "document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00", "document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/use-cases/external-integration/openapi.mdx": "2025-09-29T11:34:11+08:00", - "document/content/docs/use-cases/external-integration/wecom.mdx": "2025-09-16T14:22:28+08:00", + "document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-08T22:21:05+08:00", "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" } \ No newline at end of file diff --git a/document/public/imgs/guide/team_permissions/customDomain/1.png b/document/public/imgs/guide/team_permissions/customDomain/1.png new file mode 100644 index 0000000000..1b3cd1cf3e Binary files /dev/null and b/document/public/imgs/guide/team_permissions/customDomain/1.png differ diff --git a/document/public/imgs/guide/team_permissions/customDomain/2.png b/document/public/imgs/guide/team_permissions/customDomain/2.png new file mode 100644 index 0000000000..7c217d5600 Binary files /dev/null and b/document/public/imgs/guide/team_permissions/customDomain/2.png differ diff --git a/document/public/imgs/guide/team_permissions/customDomain/3.png b/document/public/imgs/guide/team_permissions/customDomain/3.png new file mode 100644 index 0000000000..11c9d81df8 Binary files /dev/null and b/document/public/imgs/guide/team_permissions/customDomain/3.png differ diff --git a/document/public/imgs/wecom-bot-13.png b/document/public/imgs/use-cases/external-integration/wecom/1.png similarity index 100% rename from document/public/imgs/wecom-bot-13.png rename to document/public/imgs/use-cases/external-integration/wecom/1.png diff --git a/document/public/imgs/wecom-bot-14.png b/document/public/imgs/use-cases/external-integration/wecom/2.png similarity index 100% rename from document/public/imgs/wecom-bot-14.png rename to document/public/imgs/use-cases/external-integration/wecom/2.png diff --git a/document/public/imgs/wecom-bot-15.png b/document/public/imgs/use-cases/external-integration/wecom/3.png similarity index 100% rename from document/public/imgs/wecom-bot-15.png rename to document/public/imgs/use-cases/external-integration/wecom/3.png diff --git a/document/public/imgs/use-cases/external-integration/wecom/4.png b/document/public/imgs/use-cases/external-integration/wecom/4.png new file mode 100644 index 0000000000..8cdb9635fc Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/4.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/5.png b/document/public/imgs/use-cases/external-integration/wecom/5.png new file mode 100644 index 0000000000..00def50daa Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/5.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/6.png b/document/public/imgs/use-cases/external-integration/wecom/6.png new file mode 100644 index 0000000000..fa5f909bfe Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/6.png differ diff --git a/document/public/imgs/wecom-bot-18.png b/document/public/imgs/use-cases/external-integration/wecom/7.png similarity index 100% rename from document/public/imgs/wecom-bot-18.png rename to document/public/imgs/use-cases/external-integration/wecom/7.png diff --git a/document/public/imgs/wecom-bot-1.png b/document/public/imgs/wecom-bot-1.png deleted file mode 100644 index 1be6328206..0000000000 Binary files a/document/public/imgs/wecom-bot-1.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-10.png b/document/public/imgs/wecom-bot-10.png deleted file mode 100644 index 8da377f3d5..0000000000 Binary files a/document/public/imgs/wecom-bot-10.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-11.png b/document/public/imgs/wecom-bot-11.png deleted file mode 100644 index 5b49a5bc49..0000000000 Binary files a/document/public/imgs/wecom-bot-11.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-12.png b/document/public/imgs/wecom-bot-12.png deleted file mode 100644 index 136ce94bb6..0000000000 Binary files a/document/public/imgs/wecom-bot-12.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-16.png b/document/public/imgs/wecom-bot-16.png deleted file mode 100644 index e3f06ab339..0000000000 Binary files a/document/public/imgs/wecom-bot-16.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-17.png b/document/public/imgs/wecom-bot-17.png deleted file mode 100644 index c886e3557b..0000000000 Binary files a/document/public/imgs/wecom-bot-17.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-19.png b/document/public/imgs/wecom-bot-19.png deleted file mode 100644 index 814d856324..0000000000 Binary files a/document/public/imgs/wecom-bot-19.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-2.png b/document/public/imgs/wecom-bot-2.png deleted file mode 100644 index d1353c93a1..0000000000 Binary files a/document/public/imgs/wecom-bot-2.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-3.png b/document/public/imgs/wecom-bot-3.png deleted file mode 100644 index ab5bc44207..0000000000 Binary files a/document/public/imgs/wecom-bot-3.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-4.png b/document/public/imgs/wecom-bot-4.png deleted file mode 100644 index 18ab2a9958..0000000000 Binary files a/document/public/imgs/wecom-bot-4.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-5.png b/document/public/imgs/wecom-bot-5.png deleted file mode 100644 index d14d1bfc4b..0000000000 Binary files a/document/public/imgs/wecom-bot-5.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-6.png b/document/public/imgs/wecom-bot-6.png deleted file mode 100644 index 7f5bb961d5..0000000000 Binary files a/document/public/imgs/wecom-bot-6.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-7.png b/document/public/imgs/wecom-bot-7.png deleted file mode 100644 index ba8bab218b..0000000000 Binary files a/document/public/imgs/wecom-bot-7.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-8.png b/document/public/imgs/wecom-bot-8.png deleted file mode 100644 index c164397891..0000000000 Binary files a/document/public/imgs/wecom-bot-8.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-9.png b/document/public/imgs/wecom-bot-9.png deleted file mode 100644 index 439f79df8b..0000000000 Binary files a/document/public/imgs/wecom-bot-9.png and /dev/null differ diff --git a/packages/global/common/file/tools.ts b/packages/global/common/file/tools.ts index fbaa9a8ef7..ca31c2c50a 100644 --- a/packages/global/common/file/tools.ts +++ b/packages/global/common/file/tools.ts @@ -75,7 +75,7 @@ export const parseUrlToFileType = (url: string): UserChatItemFileItemType | unde }; } - // Default to image type for non-document files + // Default to file type for non-extension files return { type: ChatFileTypeEnum.image, name: filename || 'null', diff --git a/packages/global/common/file/utils.ts b/packages/global/common/file/utils.ts index a8118bf2bf..16f378e4bb 100644 --- a/packages/global/common/file/utils.ts +++ b/packages/global/common/file/utils.ts @@ -4,3 +4,25 @@ export const isCSVFile = (filename: string) => { const extension = path.extname(filename).toLowerCase(); return extension === '.csv'; }; + +export function detectImageContentType(buffer: Buffer) { + if (!buffer || buffer.length < 12) return 'text/plain'; + + // JPEG + if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return 'image/jpeg'; + + // PNG + const pngSig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + if (pngSig.every((v, i) => buffer.readUInt8(i) === v)) return 'image/png'; + + // GIF + const gifSig = buffer.subarray(0, 6).toString('ascii'); + if (gifSig === 'GIF87a' || gifSig === 'GIF89a') return 'image/gif'; + + // WEBP + const riff = buffer.subarray(0, 4).toString('ascii'); + const webp = buffer.subarray(8, 12).toString('ascii'); + if (riff === 'RIFF' && webp === 'WEBP') return 'image/webp'; + + return 'text/plain'; +} diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 828e87564f..421c95d85b 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -128,6 +128,17 @@ export type FastGPTFeConfigsType = { alipay?: boolean; bank?: boolean; }; + fileUrlWhitelist?: string[]; + customDomain?: { + enable?: boolean; + domain?: { + aliyun?: string; + tencent?: string; + volcengine?: string; + }; + }; + + ip_whitelist?: string; }; export type SystemEnvType = { @@ -147,6 +158,30 @@ export type SystemEnvType = { customPdfParse?: customPdfParseType; fileUrlWhitelist?: string[]; + customDomain?: customDomainType; +}; + +export type customDomainType = { + kc?: { + aliyun?: string; + tencent?: string; + volcengine?: string; + }; + domain?: { + aliyun?: string; + tencent?: string; + volcengine?: string; + }; + issuerServiceName?: { + aliyun?: string; + tencent?: string; + volcengine?: string; + }; + nginxServiceName?: { + aliyun?: string; + tencent?: string; + volcengine?: string; + }; }; export type customPdfParseType = { diff --git a/packages/global/support/customDomain/type.ts b/packages/global/support/customDomain/type.ts new file mode 100644 index 0000000000..247c332c32 --- /dev/null +++ b/packages/global/support/customDomain/type.ts @@ -0,0 +1,37 @@ +import z from 'zod'; + +export const CustomDomainStatusEnum = z.enum([ + 'active', // confirm to active + 'inactive' // scheduled task find DNS Resolve is down +]); + +export const ProviderEnum = z.enum(['aliyun', 'tencent', 'volcengine']); +export const VerifyFileType = z.object({ + path: z.string(), + content: z.string() +}); +export const CustomDomainType = z.object({ + teamId: z.string(), + domain: z.string(), + cnameDomain: z.string(), + status: CustomDomainStatusEnum, + verifyFile: VerifyFileType.optional(), + provider: ProviderEnum +}); + +export type VerifyFileType = z.infer; +export type CustomDomainType = z.infer; + +export type CreateCustomDomainBody = { + domain: string; + provider: ProviderEnum; + cnameDomain: string; +}; +export type ProviderEnum = z.infer; +export type CustomDomainStatusEnum = z.infer; + +export type UpdateDomainVerifyFileBody = { + domain: string; + path: string; + content: string; +}; diff --git a/packages/global/support/customDomain/utils.ts b/packages/global/support/customDomain/utils.ts new file mode 100644 index 0000000000..996a2f99c9 --- /dev/null +++ b/packages/global/support/customDomain/utils.ts @@ -0,0 +1,10 @@ +import { customAlphabet } from 'nanoid'; + +/** + * @param domain : should be like sealosbja.site (secondary domain) + * @returns CNAME domain: fastgpt-. + */ +export const generateCNAMEDomain = (domain: string): string => { + const str = customAlphabet('abcdefghijklmnopqrstuvwxyz', 8); + return `fastgpt-${str()}.${domain}`; +}; diff --git a/packages/global/support/outLink/type.d.ts b/packages/global/support/outLink/type.d.ts index 2c3f54037b..d2bd692e40 100644 --- a/packages/global/support/outLink/type.d.ts +++ b/packages/global/support/outLink/type.d.ts @@ -20,11 +20,15 @@ export interface DingtalkAppType { } export interface WecomAppType { - AgentId: string; - CorpId: string; - SuiteSecret: string; CallbackToken: string; CallbackEncodingAesKey: string; + + /** @deprecated */ + // AgentId: string; + /** @deprecated */ + // CorpId: string; + /** @deprecated */ + // SuiteSecret: string; } // TODO: unused diff --git a/packages/service/common/s3/sources/chat/index.ts b/packages/service/common/s3/sources/chat/index.ts index 5baf71e462..772548a621 100644 --- a/packages/service/common/s3/sources/chat/index.ts +++ b/packages/service/common/s3/sources/chat/index.ts @@ -1,11 +1,13 @@ import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools'; import { S3PrivateBucket } from '../../buckets/private'; import { S3Sources } from '../../type'; +import type { UploadFileParams } from './type'; import { type CheckChatFileKeys, type DelChatFileByPrefixParams, ChatFileUploadSchema, - DelChatFileByPrefixSchema + DelChatFileByPrefixSchema, + UploadChatFileSchema } from './type'; import { differenceInHours } from 'date-fns'; import { S3Buckets } from '../../constants'; @@ -109,6 +111,30 @@ export class S3ChatSource { deleteChatFileByKey(key: string) { return this.bucket.addDeleteJob({ key }); } + + async uploadFile(params: UploadFileParams) { + const { appId, chatId, uId, filename, expiredTime, buffer, contentType } = + UploadChatFileSchema.parse(params); + const { fileKey } = getFileS3Key.chat({ + appId, + chatId, + uId, + filename + }); + + console.log('upload to s3, contentType:', contentType); + await this.bucket.putObject(fileKey, buffer, undefined, { + 'Content-Type': contentType || 'application/octet-stream' + }); + + return { + fileKey, + accessUrl: await this.bucket.createPreviewUrl({ + key: fileKey, + expiredHours: expiredTime ? differenceInHours(new Date(), expiredTime) : 24 + }) + }; + } } export function getS3ChatSource() { diff --git a/packages/service/common/s3/sources/chat/type.ts b/packages/service/common/s3/sources/chat/type.ts index bccd7942f9..9141b96fbd 100644 --- a/packages/service/common/s3/sources/chat/type.ts +++ b/packages/service/common/s3/sources/chat/type.ts @@ -16,3 +16,15 @@ export const DelChatFileByPrefixSchema = z.object({ uId: z.string().nonempty().optional() }); export type DelChatFileByPrefixParams = z.infer; + +export const UploadChatFileSchema = z.object({ + appId: ObjectIdSchema, + chatId: z.string().nonempty(), + uId: z.string().nonempty(), + filename: z.string().nonempty(), + expiredTime: z.date().optional(), + buffer: z.instanceof(Buffer), + contentType: z.string().optional() +}); + +export type UploadFileParams = z.infer; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index cdc8095080..fdccae986f 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -61,6 +61,7 @@ export const iconPaths = { 'common/gitFill': () => import('./icons/common/gitFill.svg'), 'common/gitInlight': () => import('./icons/common/gitInlight.svg'), 'common/gitLight': () => import('./icons/common/gitLight.svg'), + 'common/globalLine': () => import('./icons/common/globalLine.svg'), 'common/googleFill': () => import('./icons/common/googleFill.svg'), 'common/help': () => import('./icons/common/help.svg'), 'common/importLight': () => import('./icons/common/importLight.svg'), @@ -472,6 +473,12 @@ export const iconPaths = { star: () => import('./icons/star.svg'), stop: () => import('./icons/stop.svg'), 'support/account/coupon': () => import('./icons/support/account/coupon.svg'), + 'support/account/customDomain/provider/aliyun': () => + import('./icons/support/account/customDomain/provider/aliyun.svg'), + 'support/account/customDomain/provider/tencent': () => + import('./icons/support/account/customDomain/provider/tencent.svg'), + 'support/account/customDomain/provider/volcengine': () => + import('./icons/support/account/customDomain/provider/volcengine.svg'), 'support/account/laf': () => import('./icons/support/account/laf.svg'), 'support/account/loginoutLight': () => import('./icons/support/account/loginoutLight.svg'), 'support/account/plans': () => import('./icons/support/account/plans.svg'), diff --git a/packages/web/components/common/Icon/icons/common/globalLine.svg b/packages/web/components/common/Icon/icons/common/globalLine.svg new file mode 100644 index 0000000000..9212327901 --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/globalLine.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/web/components/common/Icon/icons/support/account/customDomain/provider/aliyun.svg b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/aliyun.svg new file mode 100644 index 0000000000..576a575edf --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/aliyun.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/web/components/common/Icon/icons/support/account/customDomain/provider/tencent.svg b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/tencent.svg new file mode 100644 index 0000000000..4a23cd77a0 --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/tencent.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/web/components/common/Icon/icons/support/account/customDomain/provider/volcengine.svg b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/volcengine.svg new file mode 100644 index 0000000000..7310c7b0de --- /dev/null +++ b/packages/web/components/common/Icon/icons/support/account/customDomain/provider/volcengine.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/web/i18n/en/account.json b/packages/web/i18n/en/account.json index 062af0c68f..8d8f05218b 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -11,6 +11,25 @@ "create_model": "Add new model", "custom_domain": "Custom Domain", "custom_model": "custom model", + "custom_domain": "custom domain", + "custom_domain.domain": "Domain", + "custom_domain.provider": "Domain Provider", + "custom_domain.registration_hint": "Please prepare your own domain and complete the ICP filing through {{provider}}, then fill in the domain in the input box below", + "custom_domain.provider.aliyun": "Alibaba Cloud", + "custom_domain.provider.tencent": "Tencent Cloud", + "custom_domain.provider.volcengine": "Volcano Engine", + "custom_domain.DNS_record": "DNS Record", + "custom_domain.DNS_record.type": "Type", + "custom_domain.DNS_resolve_hint": "Please go to your domain service provider and add a CNAME resolution for this domain to {{domain}}. After the resolution takes effect, you can bind the custom domain.", + "custom_domain.dns_resolved": "Verified", + "custom_domain.dns_resolving": "Verifying", + "custom_domain.delete_confirm": "Confirm to delete this custom domain?", + "custom_domain.status.active": "Active", + "custom_domain.status.inactive": "Inactive", + "custom_domain.domain_verify": "Domain Verification", + "custom_domain.domain_verify.path": "File Path", + "custom_domain.domain_verify.content": "File Content", + "custom_domain.domain_verify.desc": "After saving, visiting {{domain}}/{{path}} will return {{content}}", "default_model": "Default model", "default_model_config": "Default model configuration", "language": "Language and time zone", @@ -114,5 +133,7 @@ "app_registration_count": "App registration count", "audit_log_store_duration": "Audit log storage duration", "ticket_response_time": "Ticket response time", - "custom_config_details": "Custom configuration details" + "custom_config_details": "Custom configuration details", + "upgrade_to_use_custom_domain": "Current plan does not support custom domains, please upgrade first", + "upgrade_plan": "Upgrade plan" } diff --git a/packages/web/i18n/en/publish.json b/packages/web/i18n/en/publish.json index 8f808e95db..5fc1aa23d3 100644 --- a/packages/web/i18n/en/publish.json +++ b/packages/web/i18n/en/publish.json @@ -35,11 +35,16 @@ "wecom.bot_desc": "Connect to WeCom Bot directly via API", "wecom.create_modal_title": "Create WeCom Bot", "wecom.edit_modal_title": "Edit WeCom Bot", + "wecom.create_modal.step.1": "Configure Parameters", + "wecom.create_modal.step.2": "Fill in Callback URL", "wecom.title": "Publish to WeCom Bot", "dingtalk.bot": "DingTalk Bot", "dingtalk.bot_desc": "Connect to DingTalk Bot directly via API", "dingtalk.create_modal_title": "Create DingTalk Bot", "dingtalk.edit_modal_title": "Edit DingTalk Bot", "dingtalk.title": "Publish to DingTalk Bot", - "dingtalk.api": "DingTalk API" + "dingtalk.api": "DingTalk API", + "use_default_domain": "Use Default Domain", + "ip_whitelist": "IP Whitelist", + "custom_domain_management": "Custom Domain Management" } diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index f75ebbc79a..fb0d3b5174 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -16,6 +16,25 @@ "custom_config_details": "定制配置详情", "custom_domain": "自定义域名", "custom_model": "自定义模型", + "custom_domain": "自定义域名", + "custom_domain.domain": "域名", + "custom_domain.provider": "域名备案商", + "custom_domain.registration_hint": "请自备域名并通过 {{provider}} 完成备案后,将域名填入下方输入框中", + "custom_domain.provider.aliyun": "阿里云", + "custom_domain.provider.tencent": "腾讯云", + "custom_domain.provider.volcengine": "火山引擎", + "custom_domain.DNS_record": "DNS 记录", + "custom_domain.DNS_record.type": "类型", + "custom_domain.DNS_resolve_hint": "请到您的域名服务商处,添加该域名的 CNAME 解析到 {{domain}},解析生效后即可绑定自定义域名。", + "custom_domain.dns_resolved": "已验证", + "custom_domain.dns_resolving": "验证中", + "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": "默认模型配置", @@ -104,9 +123,6 @@ "requests_per_minute": "QPM", "reset_default": "恢复默认配置", "status": "状态", - "subscription_mode_month": "时长", - "subscription_package": "订阅套餐", - "subscription_period": "订阅周期", "support_wallet_amount": "金额", "team": "团队管理", "third_party": "第三方账号", @@ -114,5 +130,11 @@ "usage_records": "使用记录", "website_sync_per_dataset": "站点同步最大页数", "yes": "是", - "yuan": "{{amount}}元" + "yuan": "{{amount}}元", + "no": "否", + "subscription_period": "订阅周期", + "subscription_package": "订阅套餐", + "subscription_mode_month": "时长", + "upgrade_to_use_custom_domain": "当前套餐不支持自定义域名,请先升级", + "upgrade_plan": "升级套餐" } diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 1b1ed7918f..f58186211d 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/common.json b/packages/web/i18n/zh-CN/common.json index 59d76fc6b5..b82af7c6f5 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -50,6 +50,7 @@ "Folder": "文件夹", "FullScreen": "全屏", "FullScreenLight": "全屏预览", + "refresh": "刷新", "Import": "导入", "Input": "输入", "Instructions": "使用说明", diff --git a/packages/web/i18n/zh-CN/publish.json b/packages/web/i18n/zh-CN/publish.json index fbcbe59fdd..d71a60200b 100644 --- a/packages/web/i18n/zh-CN/publish.json +++ b/packages/web/i18n/zh-CN/publish.json @@ -35,11 +35,16 @@ "wecom.bot_desc": "通过 API 直接接入企业微信机器人", "wecom.create_modal_title": "创建企微机器人", "wecom.edit_modal_title": "编辑企微机器人", + "wecom.create_modal.step.1": "配置参数", + "wecom.create_modal.step.2": "填写回调地址", "wecom.title": "发布到企业微信机器人", "dingtalk.bot": "钉钉机器人", "dingtalk.bot_desc": "通过 API 直接接入钉钉机器人", "dingtalk.create_modal_title": "创建钉钉机器人", "dingtalk.edit_modal_title": "编辑钉钉机器人", "dingtalk.title": "发布到钉钉机器人", - "dingtalk.api": "钉钉 API" + "dingtalk.api": "钉钉 API", + "use_default_domain": "使用默认域名", + "ip_whitelist": "IP 白名单", + "custom_domain_management": "自定义域名管理" } diff --git a/packages/web/i18n/zh-Hant/account.json b/packages/web/i18n/zh-Hant/account.json index a5991bcafe..3bc4bf8b67 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -16,6 +16,25 @@ "custom_config_details": "定制配置詳情", "custom_domain": "自定義域名", "custom_model": "自訂模型", + "custom_domain": "自訂域名", + "custom_domain.domain": "域名", + "custom_domain.provider": "域名備案商", + "custom_domain.registration_hint": "請自備域名並透過 {{provider}} 完成備案後,將域名填入下方輸入框中", + "custom_domain.provider.aliyun": "阿里雲", + "custom_domain.provider.tencent": "騰訊雲", + "custom_domain.provider.volcengine": "火山引擎", + "custom_domain.DNS_record": "DNS 記錄", + "custom_domain.DNS_record.type": "類型", + "custom_domain.DNS_resolve_hint": "請到您的域名服務商處,新增該域名的 CNAME 解析到 {{domain}},解析生效後即可繫結自訂域名。", + "custom_domain.dns_resolved": "已驗證", + "custom_domain.dns_resolving": "驗證中", + "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": "預設模型設定", @@ -114,5 +133,10 @@ "usage_records": "使用記錄", "website_sync_per_dataset": "站點同步最大頁數", "yes": "是", - "yuan": "{{amount}}元" + "yuan": "{{amount}}元", + "month": "月", + "extra_dataset_size": "額外知識庫容量", + "extra_ai_points": "AI 積分運算標準", + "upgrade_to_use_custom_domain": "目前套餐不支援自訂域名,請先升級", + "upgrade_plan": "升級套餐" } diff --git a/packages/web/i18n/zh-Hant/publish.json b/packages/web/i18n/zh-Hant/publish.json index e19c34c9fa..111e6024b0 100644 --- a/packages/web/i18n/zh-Hant/publish.json +++ b/packages/web/i18n/zh-Hant/publish.json @@ -35,11 +35,16 @@ "wecom.bot_desc": "透過 API 直接連結企業微信聊天機器人", "wecom.create_modal_title": "建立企業微信聊天機器人", "wecom.edit_modal_title": "編輯企業微信聊天機器人", + "wecom.create_modal.step.1": "配置參數", + "wecom.create_modal.step.2": "填寫回調地址", "wecom.title": "發布至企業微信聊天機器人", "dingtalk.bot": "釘釘聊天機器人", "dingtalk.bot_desc": "透過 API 直接連結釘釘聊天機器人", "dingtalk.create_modal_title": "建立釘釘聊天機器人", "dingtalk.edit_modal_title": "編輯釘釘聊天機器人", "dingtalk.title": "發布至釘釘聊天機器人", - "dingtalk.api": "釘釘 API" + "dingtalk.api": "釘釘 API", + "use_default_domain": "使用預設域名", + "ip_whitelist": "IP 白名單", + "custom_domain_management": "自訂域名管理" } diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index 4da88ba8c3..f65f49d534 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -85,6 +85,7 @@ const Navbar = ({ unread }: { unread: number }) => { activeLink: [ '/account/bill', '/account/info', + '/account/customDomain', '/account/team', '/account/usage', '/account/thirdParty', diff --git a/projects/app/src/pageComponents/account/AccountContainer.tsx b/projects/app/src/pageComponents/account/AccountContainer.tsx index 6afed1f7d1..6b31608c72 100644 --- a/projects/app/src/pageComponents/account/AccountContainer.tsx +++ b/projects/app/src/pageComponents/account/AccountContainer.tsx @@ -22,7 +22,8 @@ export enum TabEnum { 'apikey' = 'apikey', 'loginout' = 'loginout', 'team' = 'team', - 'model' = 'model' + 'model' = 'model', + 'customDomain' = 'customDomain' } const AccountContainer = ({ @@ -77,6 +78,15 @@ const AccountContainer = ({ label: t('account:third_party'), value: TabEnum.thirdParty }, + ...(feConfigs.isPlus && feConfigs.customDomain?.enable + ? [ + { + icon: 'common/globalLine', + label: t('account:custom_domain'), + value: TabEnum.customDomain + } + ] + : []), { icon: 'common/model', label: t('account:model_provider'), diff --git a/projects/app/src/pageComponents/account/customDomain/createModal.tsx b/projects/app/src/pageComponents/account/customDomain/createModal.tsx new file mode 100644 index 0000000000..2b97199816 --- /dev/null +++ b/projects/app/src/pageComponents/account/customDomain/createModal.tsx @@ -0,0 +1,331 @@ +import { + ModalBody, + Box, + Radio, + Flex, + Text, + Input, + InputGroup, + InputRightElement, + Tag, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + IconButton, + Button, + ModalFooter, + Link +} 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 +} from '@/web/support/customDomain/api'; +import { getDocPath } from '@/web/common/system/doc'; + +const ProviderItem = ({ + icon, + selected, + onClick, + isDisabled +}: { + icon: IconNameType; + selected: boolean; + onClick: () => void; + isDisabled: boolean; +}) => { + return ( + + + + + ); +}; + +function CreateCustomDomainModal({ + onClose, + type, + data +}: { + onClose: () => void; + type: T; + data?: T extends 'refresh' + ? { + domain: string; + provider: ProviderEnum; + cnameDomain: string; + } + : undefined; +}) { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { copyData } = useCopyData(); + + const [provider, setProvider] = useState('aliyun'); + const [domain, setDomain] = useState(''); + const [editDomain, setEditDomain] = useState(false); + + useEffect(() => { + if (type === 'refresh') { + setProvider(data?.provider || 'aliyun'); + setDomain(data?.domain || ''); + } + }, [data, type]); + + const cnameDomain = useMemo(() => { + if (type === 'refresh') { + return data?.cnameDomain!; + } + const domain = feConfigs?.customDomain?.domain?.[provider]; + if (domain) { + return generateCNAMEDomain(domain); + } + return ''; + }, [data?.cnameDomain, feConfigs?.customDomain?.domain, provider, type]); + + const [DnsResolved, setDnsResolved] = useState(false); + const [startDnsResolve, setStartDnsResolve] = useState(type === 'create'); + + const { runAsync: checkDNSResolve } = useRequest2( + () => checkCustomDomainDNSResolve({ cnameDomain, domain }), + { + manual: true, + throttleWait: 4000, + onSuccess: (data) => { + setDnsResolved(data.success === true); + } + } + ); + + const { runAsync: activeDomain } = useRequest2(activeCustomDomain, { + manual: true, + onSuccess: () => onClose(), + successToast: t('common:Success') + }); + + const { runAsync: createDomain, loading: loadingCreatingDomain } = useRequest2( + createCustomDomain, + { + manual: true, + onSuccess: () => onClose(), + successToast: t('common:Success') + } + ); + + // auto trigger checkDNSResolve per 5s + useEffect(() => { + const intervalId = setInterval(() => { + if (domain && !editDomain && !DnsResolved && startDnsResolve) checkDNSResolve(); + }, 5000); + return () => clearInterval(intervalId); + }, [DnsResolved, checkDNSResolve, cnameDomain, domain, editDomain, startDnsResolve]); + + useEffect(() => { + if (domain && provider) { + setDnsResolved(false); + } + }, [domain, provider]); + + const loading = loadingCreatingDomain; + + return ( + + + + {t('account:custom_domain.provider')} + + + setProvider('aliyun')} + isDisabled={!editDomain || type === 'refresh'} + /> + setProvider('tencent')} + isDisabled={!editDomain || type === 'refresh'} + /> + setProvider('volcengine')} + isDisabled={!editDomain || type === 'refresh'} + /> + + + }} + /> + + + + setDomain(e.target.value)} + isDisabled={!editDomain || type === 'refresh'} + /> + + {!editDomain && domain && startDnsResolve ? ( + DnsResolved ? ( + + {t('account:custom_domain.dns_resolved')} + + ) : ( + + {t('account:custom_domain.dns_resolving')} + + ) + ) : ( + <> + )} + + + + + + + + {t('account:custom_domain.DNS_record')} + + + }} + /> + + + + + + + + + + + + + + + + +
{t('account:custom_domain.DNS_record.type')}TTL{t('common:value')}
CNAMEAuto + + {cnameDomain} + } + aria-label="copy" + size="xs" + variant="ghost" + onClick={() => copyData(cnameDomain || '')} + /> + +
+ + + + + {t('common:read_doc')} + + +
+
+ + + + +
+ ); +} + +export default CreateCustomDomainModal; 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 0000000000..145877083b --- /dev/null +++ b/projects/app/src/pageComponents/account/customDomain/domainVerifyModal.tsx @@ -0,0 +1,59 @@ +import { ModalBody, Box, Input, Button, ModalFooter, Grid } from '@chakra-ui/react'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import { useTranslation } from 'next-i18next'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { updateCustomDomainVerifyFile } from '@/web/support/customDomain/api'; +import { useForm } from 'react-hook-form'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; + +function domainVerifyModal({ 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 domainVerifyModal; 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 f1da69ab77..936d6bd195 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Wecom/WecomEditModal.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react'; +import React, { useMemo } from 'react'; +import { Flex, Box, Button, ModalBody, Input, Link, ModalFooter, Grid } 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'; @@ -7,12 +7,14 @@ 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'; +import { useMyStep } from '@fastgpt/web/hooks/useStep'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { format } from 'date-fns'; +import { ShareLinkContainer } from '../components/showShareLinkModal'; const WecomEditModal = ({ appId, @@ -25,7 +27,7 @@ const WecomEditModal = ({ appId: string; defaultData: OutLinkEditType; onClose: () => void; - onCreate: (id: string) => void; + onCreate: (shareId: string) => Promise; onEdit: () => void; isEdit?: boolean; }) => { @@ -38,7 +40,11 @@ const WecomEditModal = ({ defaultValues: defaultData }); - const { runAsync: onclickCreate, loading: creating } = useRequest2( + const { + runAsync: onclickCreate, + loading: creating, + data: createShareId + } = useRequest2( (e) => createShareChat({ ...e, @@ -48,117 +54,215 @@ const WecomEditModal = ({ { errorToast: t('common:create_failed'), successToast: t('common:create_success'), - onSuccess: onCreate + onSuccess: async (shareId) => { + const _id = await onCreate(shareId); + if (_id) { + setValue('_id', _id); + } + } } ); - const { runAsync: onclickUpdate, loading: updating } = useRequest2((e) => updateShareChat(e), { + const { + runAsync: onclickUpdate, + loading: updating, + data: updatedShareId + } = useRequest2((e) => updateShareChat(e), { errorToast: t('common:update_failed'), successToast: t('common:update_success'), onSuccess: onEdit }); + const shareId = useMemo(() => createShareId || updatedShareId, [createShareId, updatedShareId]); + + // 判断是否已经创建成功(有 createShareId 说明已经创建) + const isCreated = useMemo(() => !!createShareId, [createShareId]); + const isEditMode = useMemo(() => isEdit || isCreated, [isEdit, isCreated]); + const { feConfigs } = useSystemStore(); + const { MyStep, activeStep, goToNext, goToPrevious } = useMyStep({ + steps: [ + { + title: t('publish:wecom.create_modal.step.1') + }, + { + title: t('publish:wecom.create_modal.step.2') + } + ] + }); + + const baseUrl = useMemo( + () => feConfigs?.customApiDomain || `${location.origin}/api`, + [feConfigs?.customApiDomain] + ); return ( - - - - - - - {t('publish:wecom.api')} - {feConfigs?.docUrl && ( - - - - {t('common:read_doc')} - - - )} - - - - Corp ID - - - - - - Agent ID - - - - - - Secret - - - - - - Token - - - - - - AES Key - - - - - - - - - - - + + {t('publish:basic_info')} + + + + {t('common:Name')} + + + + + QPM + + + + + + + {t('common:support.outlink.Max usage points')} + + + + + + {t('common:expired_time')} + { + setValue('limit.expiredTime', new Date(e.target.value)); + }} + /> + + + + + + + + {t('publish:wecom.api')} + + + {feConfigs?.docUrl && ( + + + + {t('common:read_doc')} + + + )} + + + + + + Token + + + + AES Key + + + + + + )} + {activeStep === 1 && ( + + + + )} + + {activeStep === 1 && ( + + )} + + ); }; diff --git a/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx index 8840bfcc28..46e35b49d1 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/Wecom/index.tsx @@ -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'; @@ -64,25 +65,35 @@ const Wecom = ({ appId }: { appId: string }) => { {t('publish:wecom.title')} - + + + + @@ -199,14 +210,35 @@ const Wecom = ({ appId }: { appId: string }) => { Promise.all([refetchShareChatList(), setEditWecomData(undefined)])} - onEdit={() => Promise.all([refetchShareChatList(), setEditWecomData(undefined)])} + onCreate={async (shareId: string) => { + const newList = await refetchShareChatList(); + const newItem = newList.find((item) => item.shareId === shareId); + return newItem?._id; + }} + onEdit={() => refetchShareChatList()} onClose={() => setEditWecomData(undefined)} isEdit={isEdit} /> )} {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 5826cb12d8..272aad36ff 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; @@ -11,38 +16,126 @@ export type ShowShareLinkModalProps = { img: string; }; -function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) { +export const ShareLinkContainer = ({ shareLink, img }: { shareLink: string; img: string }) => { const { copyData } = useCopyData(); const { t } = useTranslation(); + 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)} + /> + + )} + + + + {t('publish:copy_link_hint')} + copyData(displayShareLink)} + /> + + + {displayShareLink} + + + + + + + + {/* + + {t('publish:ip_whitelist')} + copyData(feConfigs?.ip_whitelist || '')} + /> + + + + {feConfigs.ip_whitelist} + + */} + + ); +}; + +function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) { + const { t } = useTranslation(); return ( - - - {t('publish:copy_link_hint')} - copyData(shareLink)} - /> - - - {shareLink} - - - - - + ); diff --git a/projects/app/src/pages/account/customDomain/index.tsx b/projects/app/src/pages/account/customDomain/index.tsx new file mode 100644 index 0000000000..d884bdbd70 --- /dev/null +++ b/projects/app/src/pages/account/customDomain/index.tsx @@ -0,0 +1,242 @@ +import AccountContainer from '@/pageComponents/account/AccountContainer'; +import { serviceSideProps } from '@/web/common/i18n/utils'; +import { deleteCustomDomain, listCustomDomain } from '@/web/support/customDomain/api'; +import { + Box, + Button, + Flex, + Table, + TableContainer, + Tbody, + Td, + Thead, + Tr, + useDisclosure +} from '@chakra-ui/react'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useTranslation } from 'next-i18next'; +import dynamic from 'next/dynamic'; +import { providerMap, customDomainStatusMap } from '@/web/support/customDomain/const'; +import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; +import MyLoading from '@fastgpt/web/components/common/MyLoading'; +import type { CustomDomainType } from '@fastgpt/global/support/customDomain/type'; +import { useState, useMemo } from 'react'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants'; +import { useRouter } from 'next/router'; +import Tag from '@fastgpt/web/components/common/Tag'; + +const CreateCustomDomainModal = dynamic( + () => import('@/pageComponents/account/customDomain/createModal') +); + +/** unimplemented */ +// const DomainVerifyModal = dynamic( +// () => import('@/pageComponents/account/customDomain/domainVerifyModal') +// ); + +const CustomDomain = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { teamPlanStatus } = useUserStore(); + + const { + data: customDomainList, + refreshAsync: refreshCustomDomainList, + loading: loadingCustomDomainList + } = useRequest2(listCustomDomain, { + manual: false + }); + const { + isOpen: isOpenCreateModal, + onOpen: onOpenCreateModal, + onClose: onCloseCreateModal + } = useDisclosure(); + + // const { + // isOpen: isOpenDomainVerify, + // onOpen: onOpenDomainVerify, + // onClose: onCloseDomainVerify + // } = useDisclosure(); + + const { runAsync: onDelete, loading: loadingDelete } = useRequest2(deleteCustomDomain, { + manual: true, + successToast: t('common:Success'), + onSuccess: () => refreshCustomDomainList() + }); + + const { ConfirmModal, openConfirm } = useConfirm({ + content: t('account:custom_domain.delete_confirm') + }); + + const [editDomain, setEditDomain] = useState(undefined); + + // 检查用户是否有 advanced 套餐 + const isAdvancedPlan = useMemo(() => { + const currentLevel = teamPlanStatus?.standard?.currentSubLevel; + if (!currentLevel) return false; + + return currentLevel === StandardSubLevelEnum.advanced; + }, [teamPlanStatus?.standard?.currentSubLevel]); + + return ( + <> + + + + {loadingCustomDomainList ? : null} + + + {t('account:custom_domain')} + {customDomainList?.length ? ( + `: (${customDomainList.length}/${teamPlanStatus?.standardConstants?.customDomain})` + ) : ( + <> + )} + + + + + +
+ + + + + + + + + + + {customDomainList?.length ? ( + customDomainList.map((customDomain) => ( + + + + + + + + )) + ) : ( + + + + )} + +
{t('account:custom_domain.domain')}CNAME{t('account:custom_domain.provider')}{t('common:Status')}{t('common:Action')}
{customDomain.domain}{customDomain.cnameDomain}{t(providerMap[customDomain.provider])} + {customDomain.status === 'active' ? ( + + {t(customDomainStatusMap[customDomain.status])} + + ) : ( + + {t(customDomainStatusMap[customDomain.status])} + + )} + + + + {customDomain.status === 'inactive' ? ( + + ) : ( + <> + // + )} + +
+ + + {t('account:upgrade_to_use_custom_domain')} + + + ) + } + /> + +
+
+ + + + {isOpenCreateModal && ( + { + onCloseCreateModal(); + refreshCustomDomainList(); + setEditDomain(undefined); + }} + type={editDomain ? 'refresh' : 'create'} + data={editDomain!} + /> + )} + {/*{isOpenDomainVerify && editDomain?.domain && ( + { + onCloseDomainVerify(); + setEditDomain(undefined); + }} + /> + )}*/} + + ); +}; + +export default CustomDomain; + +export async function getServerSideProps(content: any) { + return { + props: { + ...(await serviceSideProps(content, ['account'])) + } + }; +} diff --git a/projects/app/src/pages/api/support/outLink/update.ts b/projects/app/src/pages/api/support/outLink/update.ts index 853b92be85..73f9fabdb0 100644 --- a/projects/app/src/pages/api/support/outLink/update.ts +++ b/projects/app/src/pages/api/support/outLink/update.ts @@ -21,7 +21,7 @@ export type OutLinkUpdateQuery = {}; // } export type OutLinkUpdateBody = OutLinkEditType; -export type OutLinkUpdateResponse = {}; +export type OutLinkUpdateResponse = string; async function handler( req: ApiRequestProps @@ -44,7 +44,7 @@ async function handler( per: ManagePermissionVal }); - await MongoOutLink.findByIdAndUpdate(_id, { + const doc = await MongoOutLink.findByIdAndUpdate(_id, { name, responseDetail, showRawSource, @@ -66,6 +66,6 @@ async function handler( } }); })(); - return {}; + return doc?.shareId!; } export default NextAPI(handler); diff --git a/projects/app/src/web/support/customDomain/api.ts b/projects/app/src/web/support/customDomain/api.ts new file mode 100644 index 0000000000..20fcbe6f0c --- /dev/null +++ b/projects/app/src/web/support/customDomain/api.ts @@ -0,0 +1,38 @@ +import type { + CreateCustomDomainBody, + CustomDomainType +} from '@fastgpt/global/support/customDomain/type'; +import { DELETE, GET, POST } from '@/web/common/api/request'; + +export const listCustomDomain = () => GET('/proApi/support/customDomain/list'); + +export const checkCustomDomainDNSResolve = (props: { domain: string; cnameDomain: string }) => + POST<{ success: boolean; message: string }>( + '/proApi/support/customDomain/checkDNSResolve', + props + ); + +export const deleteCustomDomain = (domain: string) => + DELETE<{ success: boolean; message: string }>('/proApi/support/customDomain/delete', { + domain + }); + +export const createCustomDomain = (props: CreateCustomDomainBody) => + POST<{ success: boolean; message: string }>('/proApi/support/customDomain/create', props); + +export const activeCustomDomain = (domain: string) => + POST<{ success: boolean; message: string }>('/proApi/support/customDomain/active', { + domain + }); + +// TODO: verify files + +export const updateCustomDomainVerifyFile = (props: { + domain: string; + path: string; + content: string; +}) => + POST<{ success: boolean; message: string }>( + '/proApi/support/customDomain/updateVerifyFile', + props + ); diff --git a/projects/app/src/web/support/customDomain/const.ts b/projects/app/src/web/support/customDomain/const.ts new file mode 100644 index 0000000000..b7f2baaff9 --- /dev/null +++ b/projects/app/src/web/support/customDomain/const.ts @@ -0,0 +1,16 @@ +import type { + ProviderEnum, + CustomDomainStatusEnum +} from '@fastgpt/global/support/customDomain/type'; +import type { t } from 'i18next'; + +export const providerMap = { + aliyun: 'account:custom_domain.provider.aliyun', + tencent: 'account:custom_domain.provider.tencent', + volcengine: 'account:custom_domain.provider.volcengine' +} satisfies Record[0]>; + +export const customDomainStatusMap = { + active: 'account:custom_domain.status.active', + inactive: 'account:custom_domain.status.inactive' +} satisfies Record[0]>;