diff --git a/document/content/docs/introduction/development/openapi/share.mdx b/document/content/docs/introduction/development/openapi/share.mdx index 2ab48f32f..fd6fa7a29 100644 --- a/document/content/docs/introduction/development/openapi/share.mdx +++ b/document/content/docs/introduction/development/openapi/share.mdx @@ -28,7 +28,7 @@ description: FastGPT 分享链接身份鉴权 `FastGPT` 将会判断`success`是否为`true`决定是允许用户继续操作。`message`与`msg`是等同的,你可以选择返回其中一个,当`success`不为`true`时,将会提示这个错误。 -`uid` 是用户的唯一凭证,必须返回该 ID 且 ID 的格式为不包含 "|"、"/“、"\" 字符的、长度小于等于 255 的字符串,否则会返回 `Invalid UID` 的错误。`uid` 将会用于拉取对话记录以及保存对话记录,可参考下方实践案例。 +`uid` 是用户的唯一凭证,必须返回该 ID 且 ID 的格式为不包含 "|"、"/“、"\\" 字符的、小于等于 255 **字节长度**的字符串,否则会返回 `Invalid UID` 的错误。`uid` 将会用于拉取对话记录以及保存对话记录,可参考下方实践案例。 ### 触发流程 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 000000000..de504a0f4 --- /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 e1ec7bdf4..88edf31f4 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 df924c944..37924d06f 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/upgrading/4-14/4144.mdx b/document/content/docs/upgrading/4-14/4144.mdx index 0da38638d..a131f498d 100644 --- a/document/content/docs/upgrading/4-14/4144.mdx +++ b/document/content/docs/upgrading/4-14/4144.mdx @@ -31,6 +31,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \ 5. 新版订阅套餐逻辑。 6. 支持配置对话文件白名单。 7. S3 支持 pathStyle 配置。 +8. 支持通过 Sealos 来进行多租户自定义域名配置。 ## ⚙️ 优化 @@ -56,3 +57,4 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \ ## 插件 +1. 新增 GLM4.6 与 DS3.2 系列模型预设。 diff --git a/document/content/docs/use-cases/external-integration/wecom.mdx b/document/content/docs/use-cases/external-integration/wecom.mdx index 2d9f9ba8d..8bd2f9ec4 100644 --- a/document/content/docs/use-cases/external-integration/wecom.mdx +++ b/document/content/docs/use-cases/external-integration/wecom.mdx @@ -3,119 +3,70 @@ 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" +### 2.1 超级管理员登录 -![图片](/imgs/wecom-bot-3.png) +[点击打开企业微信管理后台](https://work.weixin.qq.com/) -其中, 网页授权及JS-SDK要求按照企微指引,完成域名归属认证 +### 2.2 找到智能机器人入口 -![图片](/imgs/wecom-bot-4.png) +在"安全与管理" - "管理工具"页面点击"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 ) -企业可信IP要求为企业服务器IP, 后续企微的回调URL将请求到此IP +![图片](/imgs/use-cases/external-integration/wecom/1.png) -![图片](/imgs/wecom-bot-5.png) +### 2.3 选择 “API模式创建” 智能机器人 -## 2. 创建企业自建应用 +在创建机器人页面, 下拉, 点击 "API模式创建" -前往 FastGPT ,选择想要接入的应用,在 发布渠道 页面,新建一个接入企微智能机器人的发布渠道,填写好基础信息。 +![图片](/imgs/use-cases/external-integration/wecom/2.png) -![图片](/imgs/wecom-bot-6.png) +### 2.4 获取关键密钥 -现在回到企业微信平台,找到 Corp ID, Secret, Agent ID, Token, AES Key 信息并填写回 FastGPT 平台 +随机生成或者手动输入 Token 和 Encoding-AESKey,并且纪录下来 -![图片](/imgs/wecom-bot-7.png) +![图片](/imgs/use-cases/external-integration/wecom/3.png) -在"我的企业"里找到企业 ID, 填写到 FastGPT 的 Corp ID 中 +### 2.5 创建企微机器人发布渠道 -![图片](/imgs/wecom-bot-8.png) +在 FastGPT 中,选择要使用 Agent,在发布渠道页面,选择“企业微信机器人”,点击“创建” -在应用中找到 Agent Id 和 Secret, 并填写回 FastGPT +![图片](/imgs/use-cases/external-integration/wecom/4.png) -![图片](/imgs/wecom-bot-9.png) +### 2.6 配置发布渠道信息 -点击"消息接收"-"设置API接收" +配置该发布渠道的信息,需要填入 Token 和 AESKey,也就是第四步中纪录下来的 Token 和 Encoding-AESKey -![图片](/imgs/wecom-bot-10.png) +![图片](/imgs/use-cases/external-integration/wecom/5.png) -随机生成或者手动输入 Token 和 Encoding-Key, 分别填写到 FastGPT 的 Token 和 AES Key 中 +### 2.7 复制回调地址 -![图片](/imgs/wecom-bot-11.png) +点击“确认”后,选择您配置的自定义域名,复制回调地址,填回企微智能机器人配置页中。 -填写完成后确认创建 +![图片](/imgs/use-cases/external-integration/wecom/6.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 f2246f543..86afd48fd 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-08T16:10:51+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-09T18:14:22+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-09T18:14:22+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", @@ -118,7 +119,7 @@ "document/content/docs/upgrading/4-14/4141.mdx": "2025-11-19T10:15:27+08:00", "document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00", "document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00", - "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-08T17:57:59+08:00", + "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-08T21:45:21+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+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-09T20:03:29+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/package-lock.json b/document/package-lock.json index ef58aa2c1..a0051acbe 100644 --- a/document/package-lock.json +++ b/document/package-lock.json @@ -2543,7 +2543,6 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2554,7 +2553,6 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2576,7 +2574,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3244,7 +3241,6 @@ "resolved": "https://registry.npmjs.org/fumadocs-core/-/fumadocs-core-15.6.3.tgz", "integrity": "sha512-71IPC6Y0ZLPHlavYormnF1r2uX/lNrTFTYCEh6Akll8hWxRNbKG9Hk4xpFJDTkU83c8eLtHk2iow/ccQkcV6Hw==", "license": "MIT", - "peer": true, "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.9", @@ -5177,7 +5173,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.7", "@swc/helpers": "0.5.15", @@ -5353,7 +5348,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5469,7 +5463,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5479,7 +5472,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6101,8 +6093,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.2", @@ -6198,7 +6189,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" 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 000000000..1b3cd1cf3 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 000000000..7c217d560 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 000000000..11c9d81df Binary files /dev/null and b/document/public/imgs/guide/team_permissions/customDomain/3.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/1.png b/document/public/imgs/use-cases/external-integration/wecom/1.png new file mode 100644 index 000000000..9e328bb2e Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/1.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/2.png b/document/public/imgs/use-cases/external-integration/wecom/2.png new file mode 100644 index 000000000..ca3dc6342 Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/2.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/3.png b/document/public/imgs/use-cases/external-integration/wecom/3.png new file mode 100644 index 000000000..fd27476c7 Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/3.png differ 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 000000000..9c0326ab5 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 000000000..715e9e12e 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 000000000..8bf2576b2 Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/6.png differ diff --git a/document/public/imgs/use-cases/external-integration/wecom/7.png b/document/public/imgs/use-cases/external-integration/wecom/7.png new file mode 100644 index 000000000..068270321 Binary files /dev/null and b/document/public/imgs/use-cases/external-integration/wecom/7.png differ diff --git a/document/public/imgs/wecom-bot-1.png b/document/public/imgs/wecom-bot-1.png deleted file mode 100644 index 1be632820..000000000 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 8da377f3d..000000000 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 5b49a5bc4..000000000 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 136ce94bb..000000000 Binary files a/document/public/imgs/wecom-bot-12.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-13.png b/document/public/imgs/wecom-bot-13.png deleted file mode 100644 index feb7668c2..000000000 Binary files a/document/public/imgs/wecom-bot-13.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-14.png b/document/public/imgs/wecom-bot-14.png deleted file mode 100644 index 0a956f2ad..000000000 Binary files a/document/public/imgs/wecom-bot-14.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-15.png b/document/public/imgs/wecom-bot-15.png deleted file mode 100644 index a67f2f1ec..000000000 Binary files a/document/public/imgs/wecom-bot-15.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 e3f06ab33..000000000 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 c886e3557..000000000 Binary files a/document/public/imgs/wecom-bot-17.png and /dev/null differ diff --git a/document/public/imgs/wecom-bot-18.png b/document/public/imgs/wecom-bot-18.png deleted file mode 100644 index 249144a06..000000000 Binary files a/document/public/imgs/wecom-bot-18.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 814d85632..000000000 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 d1353c93a..000000000 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 ab5bc4420..000000000 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 18ab2a995..000000000 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 d14d1bfc4..000000000 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 7f5bb961d..000000000 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 ba8bab218..000000000 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 c16439789..000000000 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 439f79df8..000000000 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 fbaa9a8ef..7f2cb981e 100644 --- a/packages/global/common/file/tools.ts +++ b/packages/global/common/file/tools.ts @@ -1,5 +1,5 @@ import { detect } from 'jschardet'; -import { documentFileType } from './constants'; +import { imageFileType } from './constants'; import { ChatFileTypeEnum } from '../../core/chat/constants'; import { type UserChatItemFileItemType } from '../../core/chat/type'; import * as fs from 'fs'; @@ -66,18 +66,17 @@ export const parseUrlToFileType = (url: string): UserChatItemFileItemType | unde })(); const extension = filename?.split('.').pop()?.toLowerCase() || ''; - // If it's a document type, return as file, otherwise treat as image - if (extension && documentFileType.includes(extension)) { + if (extension && imageFileType.includes(extension)) { + // Default to file type for non-extension files return { - type: ChatFileTypeEnum.file, + type: ChatFileTypeEnum.image, name: filename || 'null', url }; } - - // Default to image type for non-document files + // If it's a document type, return as file, otherwise treat as image return { - type: ChatFileTypeEnum.image, + type: ChatFileTypeEnum.file, name: filename || 'null', url }; diff --git a/packages/global/common/file/utils.ts b/packages/global/common/file/utils.ts index a8118bf2b..16f378e4b 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 828e87564..421c95d85 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/openapi/admin.ts b/packages/global/openapi/admin.ts new file mode 100644 index 000000000..a24436edb --- /dev/null +++ b/packages/global/openapi/admin.ts @@ -0,0 +1,22 @@ +import { createDocument } from 'zod-openapi'; +import { DashboardPath } from './admin/core/dashboard'; +import { TagsMap } from './tag'; + +export const adminOpenAPIDocument = createDocument({ + openapi: '3.1.0', + info: { + title: 'FastGPT Admin API', + version: '0.1.0', + description: 'FastGPT Admin API 文档' + }, + paths: { + ...DashboardPath + }, + servers: [{ url: '/api' }], + 'x-tagGroups': [ + { + name: '仪表盘', + tags: [TagsMap.adminDashboard] + } + ] +}); diff --git a/packages/global/openapi/admin/core/dashboard/api.ts b/packages/global/openapi/admin/core/dashboard/api.ts new file mode 100644 index 000000000..2674fb13f --- /dev/null +++ b/packages/global/openapi/admin/core/dashboard/api.ts @@ -0,0 +1,105 @@ +import { z } from 'zod'; +import { UsageSourceEnum } from '../../../../support/wallet/usage/constants'; + +// Common query schema +export const GetDataChartsQuerySchema = z.object({ + startTime: z.string().meta({ description: '查询起始时间(ISO 8601 格式)' }), + sources: z.array(z.enum(UsageSourceEnum)).optional().meta({ description: '使用来源筛选' }) +}); +export type GetDataChartsQueryType = z.infer; + +// Get user form data response +export const RegisteredUserCountSchema = z.object({ + date: z.string().meta({ description: '注册日期' }), + count: z.number().meta({ description: '该日期注册的用户数' }) +}); +export const GetUserFormDataResponseSchema = z.object({ + startUserCount: z.number().meta({ description: '起始时间之前的用户总数' }), + registeredUserCount: z.array(RegisteredUserCountSchema).meta({ description: '用户注册时间序列' }) +}); +export type GetUserFormDataResponseType = z.infer; + +// Get Pays Form Data Response +export const OrderAmountSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '订单总数' }), + successCount: z.number().meta({ description: '成功订单数' }) +}); +export const PayAmountSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '支付总金额' }) +}); +export const PayTeamSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '支付团队数' }) +}); +export const GetPaysFormDataResponseSchema = z.object({ + orderAmounts: z.array(OrderAmountSchema).meta({ description: '订单数量时间序列' }), + payAmounts: z.array(PayAmountSchema).meta({ description: '支付金额时间序列' }), + payTeams: z.array(PayTeamSchema).meta({ description: '支付团队时间序列' }) +}); +export type GetPaysFormDataResponseType = z.infer; + +// Get chat form data response +export const ChatAmountSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '对话总数' }) +}); +export const ChatItemAmountSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '对话消息总数' }), + averageCount: z.number().meta({ description: '每个对话的平均消息数' }) +}); +export const GetChatFormDataResponseSchema = z.object({ + chatAmounts: z.array(ChatAmountSchema).meta({ description: '对话数量时间序列' }), + chatItemAmounts: z.array(ChatItemAmountSchema).meta({ description: '对话消息数量时间序列' }) +}); +export type GetChatFormDataResponseType = z.infer; + +// Get QPM range distribution response +export const QpmRangeSchema = z.object({ + range: z.string().meta({ description: 'QPM 范围标签' }), + count: z.number().meta({ description: '范围内的团队数量' }) +}); +export const GetQpmRangeResponseSchema = z.object({ + ranges: z.array(QpmRangeSchema).meta({ description: 'QPM 范围统计列表' }) +}); +export type GetQpmRangeResponseType = z.infer; + +// Get cost form data response +export const PointUsageSchema = z.object({ + date: z.string().meta({ description: '数据点日期' }), + totalCount: z.number().meta({ description: '积分使用总数' }) +}); +export const GetCostFormDataResponseSchema = z.object({ + pointUsages: z.array(PointUsageSchema).meta({ description: '积分使用时间序列' }) +}); +export type GetCostFormDataResponseType = z.infer; + +// Get user stats response +export const GetUserStatsResponseSchema = z.object({ + usersCount: z.number().meta({ description: '用户总数' }), + rechargeCount: z.number().meta({ description: '充值总数' }) +}); +export type GetUserStatsResponseType = z.infer; + +// Get app stats response +export const GetAppStatsResponseSchema = z.object({ + workflowCount: z.number().meta({ description: '工作流总数' }), + simpleAppCount: z.number().meta({ description: '简易应用总数' }), + workflowToolCount: z.number().meta({ description: '工作流工具总数' }), + httpToolCount: z.number().meta({ description: 'HTTP 工具总数' }), + mcpToolCount: z.number().meta({ description: 'MCP 工具总数' }) +}); +export type GetAppStatsResponseType = z.infer; + +// Get dataset stats response +export const GetDatasetStatsResponseSchema = z.object({ + commonDatasetCount: z.number().meta({ description: '通用知识库总数' }), + websiteDatasetCount: z.number().meta({ description: 'Web 站点同步总数' }), + apiDatasetCount: z.number().meta({ description: 'API 知识库总数' }), + yuqueDatasetCount: z.number().meta({ description: '语雀知识库总数' }), + feishuDatasetCount: z.number().meta({ description: '飞书知识库总数' }), + totalIndexCount: z.number().meta({ description: '索引总量' }) +}); +export type GetDatasetStatsResponseType = z.infer; diff --git a/packages/global/openapi/admin/core/dashboard/index.ts b/packages/global/openapi/admin/core/dashboard/index.ts new file mode 100644 index 000000000..cd6061e92 --- /dev/null +++ b/packages/global/openapi/admin/core/dashboard/index.ts @@ -0,0 +1,190 @@ +import { z } from 'zod'; +import type { OpenAPIPath } from '../../../type'; +import { + GetDataChartsQuerySchema, + GetChatFormDataResponseSchema, + GetCostFormDataResponseSchema, + GetPaysFormDataResponseSchema, + GetUserFormDataResponseSchema, + GetQpmRangeResponseSchema, + GetUserStatsResponseSchema, + GetAppStatsResponseSchema, + GetDatasetStatsResponseSchema +} from './api'; +import { TagsMap } from '../../../tag'; + +export * from './api'; + +export const DashboardPath: OpenAPIPath = { + '/admin/core/dashboard/getUserStats': { + get: { + summary: '获取用户全局统计', + description: '获取用户总数和充值总数', + tags: [TagsMap.adminDashboard], + responses: { + 200: { + description: '成功获取用户统计', + content: { + 'application/json': { + schema: GetUserStatsResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getAppStats': { + get: { + summary: '获取应用全局统计', + description: '获取工作流、简易应用、工作流工具、HTTP 工具和 MCP 工具的总数', + tags: [TagsMap.adminDashboard], + responses: { + 200: { + description: '成功获取应用统计', + content: { + 'application/json': { + schema: GetAppStatsResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getDatasetStats': { + get: { + summary: '获取知识库全局统计', + description: '获取通用知识库、Web 站点同步、API、语雀、飞书知识库的总数以及索引总量', + tags: [TagsMap.adminDashboard], + responses: { + 200: { + description: '成功获取知识库统计', + content: { + 'application/json': { + schema: GetDatasetStatsResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getChatFormData': { + get: { + summary: '获取对话统计数据', + description: '获取对话数量和对话消息数量的时间序列统计数据', + tags: [TagsMap.adminDashboard], + requestParams: { + query: z.object({ + startTime: z.string().meta({ + description: '查询起始时间(ISO 8601 格式)' + }) + }) + }, + responses: { + 200: { + description: '成功获取对话统计数据', + content: { + 'application/json': { + schema: GetChatFormDataResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getWorkflowQpmRange': { + get: { + summary: '获取工作流 QPM 范围分布', + description: '按团队最大 QPM 统计各范围的团队数量', + tags: [TagsMap.adminDashboard], + requestParams: { + query: z.object({ + startTime: z.string().meta({ + description: '查询起始时间(ISO 8601 格式)' + }) + }) + }, + responses: { + 200: { + description: '成功获取 QPM 范围分布', + content: { + 'application/json': { + schema: GetQpmRangeResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getCostFormData': { + post: { + summary: '获取消费统计数据', + description: '获取积分消耗的时间序列统计数据', + tags: [TagsMap.adminDashboard], + requestBody: { + content: { + 'application/json': { + schema: GetDataChartsQuerySchema + } + } + }, + responses: { + 200: { + description: '成功获取消费统计数据', + content: { + 'application/json': { + schema: GetCostFormDataResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getPaysFormData': { + get: { + summary: '获取支付统计数据', + description: '获取订单和支付金额的时间序列统计数据', + tags: [TagsMap.adminDashboard], + requestParams: { + query: z.object({ + startTime: z.string().meta({ + description: '查询起始时间(ISO 8601 格式)' + }) + }) + }, + responses: { + 200: { + description: '成功获取支付统计数据', + content: { + 'application/json': { + schema: GetPaysFormDataResponseSchema + } + } + } + } + } + }, + '/admin/core/dashboard/getUserFormData': { + get: { + summary: '获取用户注册统计数据', + description: '获取用户注册数量的时间序列统计数据', + tags: [TagsMap.adminDashboard], + requestParams: { + query: z.object({ + startTime: z.string().meta({ + description: '查询起始时间(ISO 8601 格式)' + }) + }) + }, + responses: { + 200: { + description: '成功获取用户统计数据', + content: { + 'application/json': { + schema: GetUserFormDataResponseSchema + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index 853894194..12f153b21 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -8,5 +8,7 @@ export const TagsMap = { pluginTeam: '团队插件管理', apiKey: 'APIKey', walletBill: '订单', - walletDiscountCoupon: '优惠券' + walletDiscountCoupon: '优惠券', + + adminDashboard: '管理员仪表盘' }; diff --git a/packages/global/support/customDomain/type.ts b/packages/global/support/customDomain/type.ts new file mode 100644 index 000000000..247c332c3 --- /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 000000000..996a2f99c --- /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 2c3f54037..d2bd692e4 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/global/support/wallet/sub/coupon/type.d.ts b/packages/global/support/wallet/sub/coupon/type.d.ts index 0afcda182..aa64cc455 100644 --- a/packages/global/support/wallet/sub/coupon/type.d.ts +++ b/packages/global/support/wallet/sub/coupon/type.d.ts @@ -12,6 +12,7 @@ export type CustomSubConfig = { appRegistrationCount: number; auditLogStoreDuration: number; ticketResponseTime: number; + customDomain: number; }; export type TeamCouponSub = { diff --git a/packages/global/support/wallet/sub/type.d.ts b/packages/global/support/wallet/sub/type.d.ts index a575ddf7e..cb8789f2e 100644 --- a/packages/global/support/wallet/sub/type.d.ts +++ b/packages/global/support/wallet/sub/type.d.ts @@ -20,6 +20,7 @@ export type TeamStandardSubPlanItemType = { websiteSyncPerDataset?: number; auditLogStoreDuration?: number; ticketResponseTime?: number; + customDomain?: number; // Custom plan specific fields priceDescription?: string; @@ -73,6 +74,7 @@ export type TeamSubSchema = { appRegistrationCount?: number; auditLogStoreDuration?: number; ticketResponseTime?: number; + customDomain?: number; totalPoints: number; surplusPoints: number; diff --git a/packages/service/common/middle/tracks/schema.ts b/packages/service/common/middle/tracks/schema.ts index 2a34a126f..97acafe55 100644 --- a/packages/service/common/middle/tracks/schema.ts +++ b/packages/service/common/middle/tracks/schema.ts @@ -11,19 +11,24 @@ const TrackSchema = new Schema({ data: Object }); -try { - TrackSchema.index({ event: 1 }); - - TrackSchema.index( - { event: 1, teamId: 1, 'data.datasetId': 1, createTime: -1 }, - { - partialFilterExpression: { - 'data.datasetId': { $exists: true } - } +TrackSchema.index({ event: 1 }); +// Dataset search index +TrackSchema.index( + { event: 1, teamId: 1, 'data.datasetId': 1, createTime: -1 }, + { + partialFilterExpression: { + event: TrackEnum.datasetSearch } - ); -} catch (error) { - console.log(error); -} + } +); +// QPM index +TrackSchema.index( + { event: 1, createTime: -1, 'data.requestCount': 1 }, + { + partialFilterExpression: { + event: TrackEnum.teamChatQPM + } + } +); -export const TrackModel = getMongoModel('track', TrackSchema); +export const TrackModel = getMongoModel('tracks', TrackSchema); diff --git a/packages/service/common/s3/buckets/base.ts b/packages/service/common/s3/buckets/base.ts index cdb03901b..34f44c3e4 100644 --- a/packages/service/common/s3/buckets/base.ts +++ b/packages/service/common/s3/buckets/base.ts @@ -10,11 +10,11 @@ import { import { defaultS3Options, getSystemMaxFileSize, Mimes } from '../constants'; import path from 'node:path'; import { MongoS3TTL } from '../schema'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; import { addHours, addMinutes } from 'date-fns'; import { addLog } from '../../system/log'; import { addS3DelJob } from '../mq'; import { type Readable } from 'node:stream'; +import { type UploadFileByBufferParams, UploadFileByBufferSchema } from '../type'; export class S3BaseBucket { private _client: Client; @@ -62,7 +62,9 @@ export class S3BaseBucket { await this.options.afterInit?.(); console.log(`S3 init success: ${this.name}`); }; - init(); + if (this.options.init) { + init(); + } } get name(): string { @@ -251,4 +253,25 @@ export class S3BaseBucket { return await this.client.presignedGetObject(this.name, key, expires); } + + async uploadFileByBuffer(params: UploadFileByBufferParams) { + const { key, buffer, contentType } = UploadFileByBufferSchema.parse(params); + + await MongoS3TTL.create({ + minioKey: key, + bucketName: this.name, + expiredTime: addHours(new Date(), 1) + }); + await this.putObject(key, buffer, undefined, { + 'Content-Type': contentType || 'application/octet-stream' + }); + + return { + key, + accessUrl: await this.createExternalUrl({ + key, + expiredHours: 2 + }) + }; + } } diff --git a/packages/service/common/s3/constants.ts b/packages/service/common/s3/constants.ts index b9017293c..65be2f896 100644 --- a/packages/service/common/s3/constants.ts +++ b/packages/service/common/s3/constants.ts @@ -28,6 +28,7 @@ export const Mimes = { export const defaultS3Options: { externalBaseURL?: string; afterInit?: () => Promise | void; + init?: boolean; } & ClientOptions = { useSSL: process.env.S3_USE_SSL === 'true', endPoint: process.env.S3_ENDPOINT || 'localhost', @@ -53,8 +54,4 @@ export const getSystemMaxFileSize = () => { return config; // bytes }; -export const S3_KEY_PATH_INVALID_CHARS_MAP: Record = { - '/': true, - '\\': true, - '|': true -}; +export const S3_KEY_PATH_INVALID_CHARS = /[|\\/]/; diff --git a/packages/service/common/s3/index.ts b/packages/service/common/s3/index.ts index 6e6729efd..012d31bf3 100644 --- a/packages/service/common/s3/index.ts +++ b/packages/service/common/s3/index.ts @@ -4,8 +4,8 @@ import { addLog } from '../system/log'; import { startS3DelWorker } from './mq'; export function initS3Buckets() { - const publicBucket = new S3PublicBucket(); - const privateBucket = new S3PrivateBucket(); + const publicBucket = new S3PublicBucket({ init: true }); + const privateBucket = new S3PrivateBucket({ init: true }); global.s3BucketMap = { [publicBucket.name]: publicBucket, diff --git a/packages/service/common/s3/sources/avatar.ts b/packages/service/common/s3/sources/avatar.ts index bfa179e32..6661d504e 100644 --- a/packages/service/common/s3/sources/avatar.ts +++ b/packages/service/common/s3/sources/avatar.ts @@ -7,16 +7,11 @@ import { getFileS3Key } from '../utils'; class S3AvatarSource { private bucket: S3PublicBucket; - private static instance: S3AvatarSource; constructor() { this.bucket = new S3PublicBucket(); } - static getInstance() { - return (this.instance ??= new S3AvatarSource()); - } - get prefix(): string { return imageBaseUrl; } @@ -89,5 +84,13 @@ class S3AvatarSource { } export function getS3AvatarSource() { - return S3AvatarSource.getInstance(); + if (global.avatarBucket) { + return global.avatarBucket; + } + global.avatarBucket = new S3AvatarSource(); + return global.avatarBucket; +} + +declare global { + var avatarBucket: S3AvatarSource; } diff --git a/packages/service/common/s3/sources/chat/index.ts b/packages/service/common/s3/sources/chat/index.ts index 5baf71e46..22463baf2 100644 --- a/packages/service/common/s3/sources/chat/index.ts +++ b/packages/service/common/s3/sources/chat/index.ts @@ -5,7 +5,9 @@ import { type CheckChatFileKeys, type DelChatFileByPrefixParams, ChatFileUploadSchema, - DelChatFileByPrefixSchema + DelChatFileByPrefixSchema, + UploadChatFileSchema, + type UploadFileParams } from './type'; import { differenceInHours } from 'date-fns'; import { S3Buckets } from '../../constants'; @@ -14,16 +16,11 @@ import { getFileS3Key } from '../../utils'; export class S3ChatSource { private bucket: S3PrivateBucket; - private static instance: S3ChatSource; constructor() { this.bucket = new S3PrivateBucket(); } - static getInstance() { - return (this.instance ??= new S3ChatSource()); - } - static parseChatUrl(url: string | URL) { try { const parseUrl = new URL(url); @@ -95,7 +92,7 @@ export class S3ChatSource { const { fileKey } = getFileS3Key.chat({ appId, chatId, uId, filename }); return await this.bucket.createPostPresignedUrl( { rawKey: fileKey, filename }, - { expiredHours: expiredTime ? differenceInHours(new Date(), expiredTime) : 24 } + { expiredHours: expiredTime ? differenceInHours(expiredTime, new Date()) : 24 } ); } @@ -109,8 +106,33 @@ export class S3ChatSource { deleteChatFileByKey(key: string) { return this.bucket.addDeleteJob({ key }); } + + async uploadChatFileByBuffer(params: UploadFileParams) { + const { appId, chatId, uId, filename, buffer, contentType } = + UploadChatFileSchema.parse(params); + const { fileKey } = getFileS3Key.chat({ + appId, + chatId, + uId, + filename + }); + + return this.bucket.uploadFileByBuffer({ + key: fileKey, + buffer, + contentType + }); + } } export function getS3ChatSource() { - return S3ChatSource.getInstance(); + if (global.chatBucket) { + return global.chatBucket; + } + global.chatBucket = new S3ChatSource(); + return global.chatBucket; +} + +declare global { + var chatBucket: S3ChatSource; } diff --git a/packages/service/common/s3/sources/chat/type.ts b/packages/service/common/s3/sources/chat/type.ts index bccd7942f..0be39b0f2 100644 --- a/packages/service/common/s3/sources/chat/type.ts +++ b/packages/service/common/s3/sources/chat/type.ts @@ -16,3 +16,14 @@ 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(), + buffer: z.instanceof(Buffer), + contentType: z.string().optional() +}); + +export type UploadFileParams = z.infer; diff --git a/packages/service/common/s3/sources/dataset/index.ts b/packages/service/common/s3/sources/dataset/index.ts index e8367b769..2acd2cd7e 100644 --- a/packages/service/common/s3/sources/dataset/index.ts +++ b/packages/service/common/s3/sources/dataset/index.ts @@ -29,16 +29,11 @@ import { S3Error } from 'minio'; export class S3DatasetSource { public bucket: S3PrivateBucket; - private static instance: S3DatasetSource; constructor() { this.bucket = new S3PrivateBucket(); } - static getInstance() { - return (this.instance ??= new S3DatasetSource()); - } - // 下载链接 async createGetDatasetFileURL(params: CreateGetDatasetFileURLParams) { const { key, expiredHours, external } = CreateGetDatasetFileURLParamsSchema.parse(params); @@ -260,5 +255,13 @@ export class S3DatasetSource { } export function getS3DatasetSource() { - return S3DatasetSource.getInstance(); + if (global.datasetBucket) { + return global.datasetBucket; + } + global.datasetBucket = new S3DatasetSource(); + return global.datasetBucket; +} + +declare global { + var datasetBucket: S3DatasetSource; } diff --git a/packages/service/common/s3/type.ts b/packages/service/common/s3/type.ts index 6d4af6398..ac638ad77 100644 --- a/packages/service/common/s3/type.ts +++ b/packages/service/common/s3/type.ts @@ -56,6 +56,13 @@ export const UploadImage2S3BucketParamsSchema = z.object({ }); export type UploadImage2S3BucketParams = z.infer; +export const UploadFileByBufferSchema = z.object({ + buffer: z.instanceof(Buffer), + contentType: z.string().optional(), + key: z.string().nonempty() +}); +export type UploadFileByBufferParams = z.infer; + declare global { var s3BucketMap: { [key: string]: S3BaseBucket; diff --git a/packages/service/common/s3/utils.ts b/packages/service/common/s3/utils.ts index cdc68b766..59bfa69d9 100644 --- a/packages/service/common/s3/utils.ts +++ b/packages/service/common/s3/utils.ts @@ -11,7 +11,6 @@ import { getNanoid } from '@fastgpt/global/common/string/tools'; import path from 'node:path'; import type { ParsedFileContentS3KeyParams } from './sources/dataset/type'; import { EndpointUrl } from '@fastgpt/global/common/file/constants'; -import type { NextApiRequest } from 'next'; // S3文件名最大长度配置 export const S3_FILENAME_MAX_LENGTH = 50; @@ -246,130 +245,3 @@ export function isS3ObjectKey( ): key is `${T}/${string}` { return typeof key === 'string' && key.startsWith(`${S3Sources[source]}/`); } - -// export const multer = { -// _storage: multer.diskStorage({ -// filename: (_, file, cb) => { -// if (!file?.originalname) { -// cb(new Error('File not found'), ''); -// } else { -// const ext = path.extname(decodeURIComponent(file.originalname)); -// cb(null, `${getNanoid()}${ext}`); -// } -// } -// }), - -// singleStore(maxFileSize: number = 500) { -// const fileSize = maxFileSize * 1024 * 1024; - -// return multer({ -// limits: { -// fileSize -// }, -// preservePath: true, -// storage: this._storage -// }).single('file'); -// }, - -// multipleStore(maxFileSize: number = 500) { -// const fileSize = maxFileSize * 1024 * 1024; - -// return multer({ -// limits: { -// fileSize -// }, -// preservePath: true, -// storage: this._storage -// }).array('file', global.feConfigs?.uploadFileMaxSize); -// }, - -// resolveFormData({ request, maxFileSize }: { request: NextApiRequest; maxFileSize?: number }) { -// return new Promise<{ -// data: Record; -// fileMetadata: Express.Multer.File; -// getBuffer: () => Buffer; -// getReadStream: () => fs.ReadStream; -// }>((resolve, reject) => { -// const handler = this.singleStore(maxFileSize); - -// // @ts-expect-error it can accept a NextApiRequest -// handler(request, null, (error) => { -// if (error) { -// return reject(error); -// } - -// // @ts-expect-error `file` will be injected by multer -// const file = request.file as Express.Multer.File; - -// if (!file) { -// return reject(new Error('File not found')); -// } - -// const data = (() => { -// if (!request.body?.data) return {}; -// try { -// return JSON.parse(request.body.data); -// } catch { -// return {}; -// } -// })(); - -// resolve({ -// data, -// fileMetadata: file, -// getBuffer: () => fs.readFileSync(file.path), -// getReadStream: () => fs.createReadStream(file.path) -// }); -// }); -// }); -// }, - -// resolveMultipleFormData({ -// request, -// maxFileSize -// }: { -// request: NextApiRequest; -// maxFileSize?: number; -// }) { -// return new Promise<{ -// data: Record; -// fileMetadata: Array; -// }>((resolve, reject) => { -// const handler = this.multipleStore(maxFileSize); - -// // @ts-expect-error it can accept a NextApiRequest -// handler(request, null, (error) => { -// if (error) { -// return reject(error); -// } - -// // @ts-expect-error `files` will be injected by multer -// const files = request.files as Array; - -// if (!files || files.length === 0) { -// return reject(new Error('File not found')); -// } - -// const data = (() => { -// if (!request.body?.data) return {}; -// try { -// return JSON.parse(request.body.data); -// } catch { -// return {}; -// } -// })(); - -// resolve({ -// data, -// fileMetadata: files -// }); -// }); -// }); -// }, - -// clearDiskTempFiles(filepaths: string[]) { -// for (const filepath of filepaths) { -// fs.unlink(filepath, (_) => {}); -// } -// } -// }; diff --git a/packages/service/common/vectorDB/controller.ts b/packages/service/common/vectorDB/controller.ts index 3ada3a48d..492ded73d 100644 --- a/packages/service/common/vectorDB/controller.ts +++ b/packages/service/common/vectorDB/controller.ts @@ -64,13 +64,14 @@ export const recallFromVectorStore = (props: EmbeddingRecallCtrlProps) => retryFn(() => Vector.embRecall(props)); export const getVectorDataByTime = Vector.getVectorDataByTime; +// Count vector export const getVectorCountByTeamId = async (teamId: string) => { const cacheCount = await teamVectorCache.get(teamId); if (cacheCount !== undefined) { return cacheCount; } - const count = await Vector.getVectorCountByTeamId(teamId); + const count = await Vector.getVectorCount({ teamId }); teamVectorCache.set({ teamId, @@ -79,9 +80,7 @@ export const getVectorCountByTeamId = async (teamId: string) => { return count; }; - -export const getVectorCountByDatasetId = Vector.getVectorCountByDatasetId; -export const getVectorCountByCollectionId = Vector.getVectorCountByCollectionId; +export const getVectorCount = Vector.getVectorCount; export const insertDatasetDataVector = async ({ model, diff --git a/packages/service/common/vectorDB/milvus/index.ts b/packages/service/common/vectorDB/milvus/index.ts index 6da807b56..463f6dece 100644 --- a/packages/service/common/vectorDB/milvus/index.ts +++ b/packages/service/common/vectorDB/milvus/index.ts @@ -257,43 +257,36 @@ export class MilvusCtrl { }; }; - getVectorCountByTeamId = async (teamId: string) => { + getVectorCount = async (props: { + teamId?: string; + datasetId?: string; + collectionId?: string; + }) => { + const { teamId, datasetId, collectionId } = props; const client = await this.getClient(); + // Build filter conditions dynamically (each condition wrapped in parentheses) + const filterConditions: string[] = []; + + if (teamId) { + filterConditions.push(`(teamId == "${String(teamId)}")`); + } + + if (datasetId) { + filterConditions.push(`(datasetId == "${String(datasetId)}")`); + } + + if (collectionId) { + filterConditions.push(`(collectionId == "${String(collectionId)}")`); + } + + // If no conditions provided, count all (empty filter) + const filter = filterConditions.length > 0 ? filterConditions.join(' and ') : ''; + const result = await client.query({ collection_name: DatasetVectorTableName, output_fields: ['count(*)'], - filter: `teamId == "${String(teamId)}"` - }); - - const total = result.data?.[0]?.['count(*)'] as number; - - return total; - }; - getVectorCountByDatasetId = async (teamId: string, datasetId: string) => { - const client = await this.getClient(); - - const result = await client.query({ - collection_name: DatasetVectorTableName, - output_fields: ['count(*)'], - filter: `(teamId == "${String(teamId)}") and (dataset == "${String(datasetId)}")` - }); - - const total = result.data?.[0]?.['count(*)'] as number; - - return total; - }; - getVectorCountByCollectionId = async ( - teamId: string, - datasetId: string, - collectionId: string - ) => { - const client = await this.getClient(); - - const result = await client.query({ - collection_name: DatasetVectorTableName, - output_fields: ['count(*)'], - filter: `(teamId == "${String(teamId)}") and (datasetId == "${String(datasetId)}") and (collectionId == "${String(collectionId)}")` + filter: filter || undefined }); const total = result.data?.[0]?.['count(*)'] as number; diff --git a/packages/service/common/vectorDB/oceanbase/index.ts b/packages/service/common/vectorDB/oceanbase/index.ts index bd55a2f2c..647d7784a 100644 --- a/packages/service/common/vectorDB/oceanbase/index.ts +++ b/packages/service/common/vectorDB/oceanbase/index.ts @@ -180,33 +180,34 @@ export class ObVectorCtrl { datasetId: item.dataset_id })); }; - getVectorCountByTeamId = async (teamId: string) => { - const total = await ObClient.count(DatasetVectorTableName, { - where: [['team_id', String(teamId)]] - }); - return total; - }; - getVectorCountByDatasetId = async (teamId: string, datasetId: string) => { - const total = await ObClient.count(DatasetVectorTableName, { - where: [['team_id', String(teamId)], 'and', ['dataset_id', String(datasetId)]] - }); + getVectorCount = async (props: { + teamId?: string; + datasetId?: string; + collectionId?: string; + }) => { + const { teamId, datasetId, collectionId } = props; - return total; - }; - getVectorCountByCollectionId = async ( - teamId: string, - datasetId: string, - collectionId: string - ) => { + // Build where conditions dynamically + const whereConditions: any[] = []; + + if (teamId) { + whereConditions.push(['team_id', String(teamId)]); + } + + if (datasetId) { + if (whereConditions.length > 0) whereConditions.push('and'); + whereConditions.push(['dataset_id', String(datasetId)]); + } + + if (collectionId) { + if (whereConditions.length > 0) whereConditions.push('and'); + whereConditions.push(['collection_id', String(collectionId)]); + } + + // If no conditions provided, count all const total = await ObClient.count(DatasetVectorTableName, { - where: [ - ['team_id', String(teamId)], - 'and', - ['dataset_id', String(datasetId)], - 'and', - ['collection_id', String(collectionId)] - ] + where: whereConditions.length > 0 ? whereConditions : undefined }); return total; diff --git a/packages/service/common/vectorDB/pg/index.ts b/packages/service/common/vectorDB/pg/index.ts index abe51f4ca..0a3694d31 100644 --- a/packages/service/common/vectorDB/pg/index.ts +++ b/packages/service/common/vectorDB/pg/index.ts @@ -204,33 +204,34 @@ export class PgVectorCtrl { datasetId: item.dataset_id })); }; - getVectorCountByTeamId = async (teamId: string) => { - const total = await PgClient.count(DatasetVectorTableName, { - where: [['team_id', String(teamId)]] - }); - return total; - }; - getVectorCountByDatasetId = async (teamId: string, datasetId: string) => { - const total = await PgClient.count(DatasetVectorTableName, { - where: [['team_id', String(teamId)], 'and', ['dataset_id', String(datasetId)]] - }); + getVectorCount = async (props: { + teamId?: string; + datasetId?: string; + collectionId?: string; + }) => { + const { teamId, datasetId, collectionId } = props; - return total; - }; - getVectorCountByCollectionId = async ( - teamId: string, - datasetId: string, - collectionId: string - ) => { + // Build where conditions dynamically + const whereConditions: any[] = []; + + if (teamId) { + whereConditions.push(['team_id', String(teamId)]); + } + + if (datasetId) { + if (whereConditions.length > 0) whereConditions.push('and'); + whereConditions.push(['dataset_id', String(datasetId)]); + } + + if (collectionId) { + if (whereConditions.length > 0) whereConditions.push('and'); + whereConditions.push(['collection_id', String(collectionId)]); + } + + // If no conditions provided, count all const total = await PgClient.count(DatasetVectorTableName, { - where: [ - ['team_id', String(teamId)], - 'and', - ['dataset_id', String(datasetId)], - 'and', - ['collection_id', String(collectionId)] - ] + where: whereConditions.length > 0 ? whereConditions : undefined }); return total; diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 42659811f..2516ba945 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -126,7 +126,6 @@ const AppSchema = new Schema( } ); -AppSchema.index({ type: 1 }); AppSchema.index({ teamId: 1, updateTime: -1 }); AppSchema.index({ teamId: 1, type: 1 }); AppSchema.index( @@ -137,5 +136,7 @@ AppSchema.index( } } ); +// Admin count +AppSchema.index({ type: 1 }); export const MongoApp = getMongoModel(AppCollectionName, AppSchema); diff --git a/packages/service/core/dataset/controller.ts b/packages/service/core/dataset/controller.ts index d16d3afee..6beb7f382 100644 --- a/packages/service/core/dataset/controller.ts +++ b/packages/service/core/dataset/controller.ts @@ -96,40 +96,20 @@ export async function delDatasetRelevantData({ datasetId: { $in: datasetIds } }); - // Delete dataset_data_texts in batches by datasetId for (const datasetId of datasetIds) { + // Delete dataset_data_texts in batches by datasetId await MongoDatasetDataText.deleteMany({ teamId, datasetId }).maxTimeMS(300000); // Reduce timeout for single batch - } - // Delete dataset_datas in batches by datasetId - for (const datasetId of datasetIds) { - await MongoDatasetData.deleteMany({ - teamId, - datasetId - }).maxTimeMS(300000); - } - - await delCollectionRelatedSource({ collections }); - // Delete vector data - await deleteDatasetDataVector({ teamId, datasetIds }); - - // Delete dataset_data_texts in batches by datasetId - for (const datasetId of datasetIds) { - await MongoDatasetDataText.deleteMany({ - teamId, - datasetId - }).maxTimeMS(300000); // Reduce timeout for single batch - } - // Delete dataset_datas in batches by datasetId - for (const datasetId of datasetIds) { + // Delete dataset_datas in batches by datasetId await MongoDatasetData.deleteMany({ teamId, datasetId }).maxTimeMS(300000); } + // Delete source: 兼容旧版的图片 await delCollectionRelatedSource({ collections }); // Delete vector data await deleteDatasetDataVector({ teamId, datasetIds }); diff --git a/packages/service/core/dataset/schema.ts b/packages/service/core/dataset/schema.ts index 452b009d5..822d898d8 100644 --- a/packages/service/core/dataset/schema.ts +++ b/packages/service/core/dataset/schema.ts @@ -148,7 +148,7 @@ const DatasetSchema = new Schema({ try { DatasetSchema.index({ teamId: 1 }); - DatasetSchema.index({ type: 1 }); + DatasetSchema.index({ type: 1 }); // Admin count DatasetSchema.index({ deleteTime: 1 }); // 添加软删除字段索引 } catch (error) { console.log(error); diff --git a/packages/service/core/workflow/dispatch/tools/readFiles.ts b/packages/service/core/workflow/dispatch/tools/readFiles.ts index 62f79911d..38b9df006 100644 --- a/packages/service/core/workflow/dispatch/tools/readFiles.ts +++ b/packages/service/core/workflow/dispatch/tools/readFiles.ts @@ -179,13 +179,13 @@ export const getFileContentFromLinks = async ({ sourceId: url, customPdfParse }); - if (rawTextBuffer) { - return formatResponseObject({ - filename: rawTextBuffer.filename || url, - url, - content: rawTextBuffer.text - }); - } + // if (rawTextBuffer) { + // return formatResponseObject({ + // filename: rawTextBuffer.filename || url, + // url, + // content: rawTextBuffer.text + // }); + // } try { if (isInternalAddress(url)) { @@ -207,23 +207,39 @@ export const getFileContentFromLinks = async ({ // Get file name const { filename, extension, imageParsePrefix } = (() => { - const contentDisposition = response.headers['content-disposition']; - if (contentDisposition) { - const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; - const matches = filenameRegex.exec(contentDisposition); - if (matches != null && matches[1]) { - const filename = decodeURIComponent(matches[1].replace(/['"]/g, '')); - return { - filename, - extension: path.extname(filename).replace('.', ''), - imageParsePrefix: `` // TODO: 需要根据是否是聊天对话里面的外部链接来决定 - }; - } - } - if (isChatExternalUrl) { - const filename = urlObj.pathname.split('/').pop() || 'file'; + const contentDisposition = response.headers['content-disposition'] || ''; + + // Priority: filename* (RFC 5987, UTF-8 encoded) > filename (traditional) + const extractFilename = (contentDisposition: string): string => { + // Try RFC 5987 filename* first (e.g., filename*=UTF-8''encoded-name) + const filenameStarRegex = /filename\*=([^']*)'([^']*)'([^;\n]*)/i; + const starMatches = filenameStarRegex.exec(contentDisposition); + if (starMatches && starMatches[3]) { + const charset = starMatches[1].toLowerCase(); + const encodedFilename = starMatches[3]; + // Decode percent-encoded UTF-8 filename + try { + return decodeURIComponent(encodedFilename); + } catch (error) { + addLog.warn('Failed to decode filename*', { encodedFilename, error }); + } + } + + // Fallback to traditional filename parameter + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i; + const matches = filenameRegex.exec(contentDisposition); + if (matches && matches[1]) { + return matches[1].replace(/['"]/g, ''); + } + + return ''; + }; + + const matchFilename = extractFilename(contentDisposition); + const filename = matchFilename || urlObj.pathname.split('/').pop() || 'file'; const extension = path.extname(filename).replace('.', ''); + return { filename, extension, diff --git a/packages/service/support/wallet/sub/schema.ts b/packages/service/support/wallet/sub/schema.ts index 8585ff636..d0a435aa2 100644 --- a/packages/service/support/wallet/sub/schema.ts +++ b/packages/service/support/wallet/sub/schema.ts @@ -64,6 +64,7 @@ const SubSchema = new Schema({ appRegistrationCount: Number, auditLogStoreDuration: Number, ticketResponseTime: Number, + customDomain: Number, // stand sub and extra points sub. Plan total points totalPoints: { diff --git a/packages/service/support/wallet/sub/utils.ts b/packages/service/support/wallet/sub/utils.ts index e1bafdec7..434d258d8 100644 --- a/packages/service/support/wallet/sub/utils.ts +++ b/packages/service/support/wallet/sub/utils.ts @@ -74,7 +74,8 @@ export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => { standard?.appRegistrationCount ?? standardConstants.appRegistrationCount, auditLogStoreDuration: standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, - ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime + ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime, + customDomain: standard?.customDomain ?? standardConstants.customDomain } : undefined }; @@ -210,7 +211,8 @@ export const getTeamPlanStatus = async ({ auditLogStoreDuration: standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration, ticketResponseTime: - standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime + standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime, + customDomain: standardPlan?.customDomain ?? standardConstants.customDomain } : undefined, diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index cdc809508..fdccae986 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 000000000..921232790 --- /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 000000000..576a575ed --- /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 000000000..4a23cd77a --- /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 000000000..7310c7b0d --- /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 ec07e6f1b..51678eb97 100644 --- a/packages/web/i18n/en/account.json +++ b/packages/web/i18n/en/account.json @@ -9,7 +9,26 @@ "confirm_logout": "Confirm to log out?", "create_channel": "Add new channel", "create_model": "Add new model", + "custom_domain": "Custom Domain", "custom_model": "custom model", + "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", @@ -113,5 +132,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/common.json b/packages/web/i18n/en/common.json index 9e6d832bd..a404eb816 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -776,11 +776,11 @@ "create_success": "Created Successfully", "create_time": "Creation Time", "cron_job_run_app": "Scheduled Task", - "custom_plan_price": "Custom Billing", "custom_plan_feature_1": "Priority Deep Technical Support", "custom_plan_feature_2": "Dedicated Account Manager", "custom_plan_feature_3": "Flexible Resource Configuration", "custom_plan_feature_4": "Secure and Controllable", + "custom_plan_price": "Custom Billing", "custom_title": "Custom Title", "data_index_custom": "Custom index", "data_index_default": "Default index", @@ -1216,6 +1216,7 @@ "support.wallet.subscription.Upgrade plan": "Upgrade Package", "support.wallet.subscription.ai_model": "AI Language Model", "support.wallet.subscription.function.Audit log store duration": "{{amount}} days team operation log records", + "support.wallet.subscription.function.Custom domain": "{{amount}} Custom domains", "support.wallet.subscription.function.History store": "{{amount}} Days of Chat History Retention", "support.wallet.subscription.function.Max app": "{{amount}} Agent limit", "support.wallet.subscription.function.Max dataset": "{{amount}} Dataset limit", @@ -1224,6 +1225,7 @@ "support.wallet.subscription.function.Points": "{{amount}} points", "support.wallet.subscription.function.Requests per minute": "{{amount}} QPM", "support.wallet.subscription.function.Website sync per dataset": "Single knowledge base {{amount}} web pages synchronized", + "support.wallet.subscription.function.custom domain tip": "The number of custom domain names that the team can configure, which can currently be used to access Wecom intelligent robots", "support.wallet.subscription.mode.Month": "Month", "support.wallet.subscription.mode.Period": "Subscription Period", "support.wallet.subscription.mode.Year": "Year", diff --git a/packages/web/i18n/en/publish.json b/packages/web/i18n/en/publish.json index 8f808e95d..5fc1aa23d 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 6e6ff57be..87b68b7a1 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -14,7 +14,26 @@ "create_channel": "新增渠道", "create_model": "新增模型", "custom_config_details": "定制配置详情", + "custom_domain": "自定义域名", "custom_model": "自定义模型", + "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": "默认模型配置", @@ -103,9 +122,6 @@ "requests_per_minute": "QPM", "reset_default": "恢复默认配置", "status": "状态", - "subscription_mode_month": "时长", - "subscription_package": "订阅套餐", - "subscription_period": "订阅周期", "support_wallet_amount": "金额", "team": "团队管理", "third_party": "第三方账号", @@ -113,5 +129,10 @@ "usage_records": "使用记录", "website_sync_per_dataset": "站点同步最大页数", "yes": "是", - "yuan": "{{amount}}元" + "yuan": "{{amount}}元", + "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 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/common.json b/packages/web/i18n/zh-CN/common.json index 8a75af271..3785e5a83 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -781,11 +781,11 @@ "create_success": "创建成功", "create_time": "创建时间", "cron_job_run_app": "定时任务", - "custom_plan_price": "定制化计费", "custom_plan_feature_1": "优先深度技术支持", "custom_plan_feature_2": "专属客户经理", "custom_plan_feature_3": "弹性资源配置", "custom_plan_feature_4": "安全可控", + "custom_plan_price": "定制化计费", "custom_title": "自定义标题", "data_index_custom": "自定义索引", "data_index_default": "默认索引", @@ -1052,6 +1052,7 @@ "read_quote": "查看引用", "redo_tip": "恢复 ctrl shift z", "redo_tip_mac": "恢复 ⌘ shift z", + "refresh": "刷新", "request_end": "已加载全部", "request_error": "请求异常", "request_more": "点击加载更多", @@ -1224,6 +1225,7 @@ "support.wallet.subscription.eval_items_count": "单次评测数据条数: {{count}} 条", "support.wallet.subscription.function.App registration count": "{{amount}} 个应用备案", "support.wallet.subscription.function.Audit log store duration": "{{amount}} 天团队操作日志记录", + "support.wallet.subscription.function.Custom domain": "{{amount}} 个自定义域名", "support.wallet.subscription.function.History store": "{{amount}} 天对话记录保留", "support.wallet.subscription.function.Max app": "{{amount}} 个 Agent", "support.wallet.subscription.function.Max dataset": "{{amount}} 个知识库", @@ -1233,6 +1235,7 @@ "support.wallet.subscription.function.Requests per minute": "{{amount}} QPM", "support.wallet.subscription.function.Ticket response time": "{{amount}} 小时工单支持响应", "support.wallet.subscription.function.Website sync per dataset": "站点同步最大 {{amount}} 页", + "support.wallet.subscription.function.custom domain tip": "团队可以配置的自定义域名数量,目前可用于接入企微智能机器人", "support.wallet.subscription.function.qpm tip": "主要指团队每分钟请求 Agent 的最大次数,与单个 Agent 复杂度无关。其他 OpenAPI 接口也受此影响,每个接口单独计算", "support.wallet.subscription.mode.Month": "按月", "support.wallet.subscription.mode.Period": "订阅周期", diff --git a/packages/web/i18n/zh-CN/publish.json b/packages/web/i18n/zh-CN/publish.json index fbcbe59fd..d71a60200 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 02c7c5204..92c8b12b2 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -15,6 +15,25 @@ "create_model": "新增模型", "custom_config_details": "定制配置詳情", "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": "預設模型設定", @@ -113,5 +132,10 @@ "usage_records": "使用記錄", "website_sync_per_dataset": "站點同步最大頁數", "yes": "是", - "yuan": "{{amount}}元" -} \ No newline at end of file + "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/common.json b/packages/web/i18n/zh-Hant/common.json index 5e619e30b..36d6938ce 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -775,11 +775,11 @@ "create_success": "建立成功", "create_time": "建立時間", "cron_job_run_app": "排程任務", - "custom_plan_price": "定制化計費", "custom_plan_feature_1": "優先深度技術支援", "custom_plan_feature_2": "專屬客戶經理", "custom_plan_feature_3": "彈性資源配置", "custom_plan_feature_4": "安全可控", + "custom_plan_price": "定制化計費", "custom_title": "自訂標題", "data_index_custom": "自定義索引", "data_index_default": "預設索引", @@ -1213,6 +1213,7 @@ "support.wallet.subscription.Upgrade plan": "升級方案", "support.wallet.subscription.ai_model": "AI 語言模型", "support.wallet.subscription.function.Audit log store duration": "{{amount}} 天團隊操作日誌記錄", + "support.wallet.subscription.function.Custom domain": "{{amount}} 個自定義域名", "support.wallet.subscription.function.History store": "{{amount}} 天對話紀錄保留", "support.wallet.subscription.function.Max app": "{{amount}} 個 Agent", "support.wallet.subscription.function.Max dataset": "{{amount}} 個知識庫", @@ -1221,6 +1222,7 @@ "support.wallet.subscription.function.Points": "{{amount}} 積分", "support.wallet.subscription.function.Requests per minute": "{{amount}} QPM", "support.wallet.subscription.function.Website sync per dataset": "單知識庫 {{amount}} 個網頁同步", + "support.wallet.subscription.function.custom domain tip": "團隊可以配置的自定義域名數量,目前可用於接入企微智能機器人", "support.wallet.subscription.mode.Month": "按月", "support.wallet.subscription.mode.Period": "訂閱週期", "support.wallet.subscription.mode.Year": "按年", diff --git a/packages/web/i18n/zh-Hant/publish.json b/packages/web/i18n/zh-Hant/publish.json index e19c34c9f..111e6024b 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/Dockerfile b/projects/app/Dockerfile index ce033e038..7c5285de4 100644 --- a/projects/app/Dockerfile +++ b/projects/app/Dockerfile @@ -76,6 +76,8 @@ COPY --from=maindeps /app/node_modules/@zilliz/milvus2-sdk-node ./node_modules/@ COPY --from=builder /app/projects/app/package.json ./package.json # copy config COPY ./projects/app/data/config.json /app/data/config.json +# copy test.mp3 +COPY ./projects/app/data/test.mp3 /app/data/test.mp3 # copy GeoLite2-City.mmdb COPY ./projects/app/data/GeoLite2-City.mmdb /app/data/GeoLite2-City.mmdb diff --git a/projects/app/public/icon/loginLeft.svg b/projects/app/public/icon/loginLeft.svg deleted file mode 100644 index e943b9c8d..000000000 --- a/projects/app/public/icon/loginLeft.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/app/createButton.jpg b/projects/app/public/imgs/app/createButton.jpg new file mode 100644 index 000000000..31a12245a Binary files /dev/null and b/projects/app/public/imgs/app/createButton.jpg differ diff --git a/projects/app/public/imgs/app/createButton.png b/projects/app/public/imgs/app/createButton.png deleted file mode 100644 index 189d1260a..000000000 Binary files a/projects/app/public/imgs/app/createButton.png and /dev/null differ diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram.png index 0386ed2ce..f937778a8 100644 Binary files a/projects/app/public/imgs/chat/fastgpt_chat_diagram.png and b/projects/app/public/imgs/chat/fastgpt_chat_diagram.png differ diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png index 3100121b6..b5e3b56a9 100644 Binary files a/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png and b/projects/app/public/imgs/chat/fastgpt_chat_diagram_en.png differ diff --git a/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png b/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png index 203186785..b19cf1795 100644 Binary files a/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png and b/projects/app/public/imgs/chat/fastgpt_chat_diagram_zh-Hant.png differ diff --git a/projects/app/public/imgs/errImg.png b/projects/app/public/imgs/errImg.png index ee39efa4d..58e41fb28 100644 Binary files a/projects/app/public/imgs/errImg.png and b/projects/app/public/imgs/errImg.png differ diff --git a/projects/app/public/imgs/home/advanced_settings.png b/projects/app/public/imgs/home/advanced_settings.png deleted file mode 100644 index 3d3fe356d..000000000 Binary files a/projects/app/public/imgs/home/advanced_settings.png and /dev/null differ diff --git a/projects/app/public/imgs/home/ai_assiatant.png b/projects/app/public/imgs/home/ai_assiatant.png deleted file mode 100644 index df6b04c87..000000000 Binary files a/projects/app/public/imgs/home/ai_assiatant.png and /dev/null differ diff --git a/projects/app/public/imgs/home/dataset_import.png b/projects/app/public/imgs/home/dataset_import.png deleted file mode 100644 index 7d12bf11e..000000000 Binary files a/projects/app/public/imgs/home/dataset_import.png and /dev/null differ diff --git a/projects/app/public/imgs/home/icon_0.svg b/projects/app/public/imgs/home/icon_0.svg deleted file mode 100644 index b3f10ddab..000000000 --- a/projects/app/public/imgs/home/icon_0.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_1.svg b/projects/app/public/imgs/home/icon_1.svg deleted file mode 100644 index 531df28c9..000000000 --- a/projects/app/public/imgs/home/icon_1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_2.svg b/projects/app/public/imgs/home/icon_2.svg deleted file mode 100644 index e82d077c9..000000000 --- a/projects/app/public/imgs/home/icon_2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_3.svg b/projects/app/public/imgs/home/icon_3.svg deleted file mode 100644 index 36101bdd0..000000000 --- a/projects/app/public/imgs/home/icon_3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_4.svg b/projects/app/public/imgs/home/icon_4.svg deleted file mode 100644 index 5eb44c9d4..000000000 --- a/projects/app/public/imgs/home/icon_4.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_5.svg b/projects/app/public/imgs/home/icon_5.svg deleted file mode 100644 index b5aad98ce..000000000 --- a/projects/app/public/imgs/home/icon_5.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/icon_6.svg b/projects/app/public/imgs/home/icon_6.svg deleted file mode 100644 index 7ec64ab3b..000000000 --- a/projects/app/public/imgs/home/icon_6.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/projects/app/public/imgs/home/openapi.png b/projects/app/public/imgs/home/openapi.png deleted file mode 100644 index 02bdc5e7a..000000000 Binary files a/projects/app/public/imgs/home/openapi.png and /dev/null differ diff --git a/projects/app/public/imgs/home/test.png b/projects/app/public/imgs/home/test.png deleted file mode 100644 index f735a7670..000000000 Binary files a/projects/app/public/imgs/home/test.png and /dev/null differ diff --git a/projects/app/public/imgs/home/videobgpc.png b/projects/app/public/imgs/home/videobgpc.png deleted file mode 100644 index 9c5bfa454..000000000 Binary files a/projects/app/public/imgs/home/videobgpc.png and /dev/null differ diff --git a/projects/app/public/imgs/home/videobgphone.png b/projects/app/public/imgs/home/videobgphone.png deleted file mode 100644 index a3208ee1d..000000000 Binary files a/projects/app/public/imgs/home/videobgphone.png and /dev/null differ diff --git a/projects/app/public/imgs/outlink/dingtalk-copylink-instruction.png b/projects/app/public/imgs/outlink/dingtalk-copylink-instruction.png index 4f73d14d0..0d913ec4d 100644 Binary files a/projects/app/public/imgs/outlink/dingtalk-copylink-instruction.png and b/projects/app/public/imgs/outlink/dingtalk-copylink-instruction.png differ diff --git a/projects/app/public/imgs/outlink/feishu-copylink-instruction.png b/projects/app/public/imgs/outlink/feishu-copylink-instruction.png index 025c7d148..3b5113bdc 100644 Binary files a/projects/app/public/imgs/outlink/feishu-copylink-instruction.png and b/projects/app/public/imgs/outlink/feishu-copylink-instruction.png differ diff --git a/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.jpg b/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.jpg new file mode 100644 index 000000000..eacecd1a7 Binary files /dev/null and b/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.jpg differ diff --git a/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png b/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png deleted file mode 100644 index e04324b5c..000000000 Binary files a/projects/app/public/imgs/outlink/offiaccount-copylink-instruction.png and /dev/null differ diff --git a/projects/app/public/imgs/outlink/wecom-copylink-instruction.png b/projects/app/public/imgs/outlink/wecom-copylink-instruction.png index 88f44df5d..79bc90e96 100644 Binary files a/projects/app/public/imgs/outlink/wecom-copylink-instruction.png and b/projects/app/public/imgs/outlink/wecom-copylink-instruction.png differ diff --git a/projects/app/public/imgs/proModalBg.png b/projects/app/public/imgs/proModalBg.png index a54fe3d99..d4009cff1 100644 Binary files a/projects/app/public/imgs/proModalBg.png and b/projects/app/public/imgs/proModalBg.png differ diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index 4da88ba8c..f65f49d53 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/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index 6ec2ef46a..73b040a67 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -527,7 +527,7 @@ const ChatBox = ({ file: { type: file.type, name: file.name, - url: file.key ? undefined : file.url || '', + url: file.url, icon: file.icon || '', key: file.key || '' } diff --git a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx index 6a2d8599e..a79fd48b0 100644 --- a/projects/app/src/components/support/wallet/StandardPlanContentList.tsx +++ b/projects/app/src/components/support/wallet/StandardPlanContentList.tsx @@ -46,7 +46,8 @@ const StandardPlanContentList = ({ standplan?.chatHistoryStoreDuration ?? plan.chatHistoryStoreDuration, auditLogStoreDuration: standplan?.auditLogStoreDuration ?? plan.auditLogStoreDuration, appRegistrationCount: standplan?.appRegistrationCount ?? plan.appRegistrationCount, - ticketResponseTime: standplan?.ticketResponseTime ?? plan.ticketResponseTime + ticketResponseTime: standplan?.ticketResponseTime ?? plan.ticketResponseTime, + customDomain: standplan?.customDomain ?? plan.customDomain }; }, [ subPlans?.standard, @@ -62,7 +63,8 @@ const StandardPlanContentList = ({ standplan?.chatHistoryStoreDuration, standplan?.auditLogStoreDuration, standplan?.appRegistrationCount, - standplan?.ticketResponseTime + standplan?.ticketResponseTime, + standplan?.customDomain ]); return planContent ? ( @@ -175,6 +177,20 @@ const StandardPlanContentList = ({ )} + {planContent.customDomain !== undefined && ( + + + + {t('common:support.wallet.subscription.function.Custom domain', { + amount: planContent.customDomain + })} + + + + )} ) : null; }; diff --git a/projects/app/src/pageComponents/account/AccountContainer.tsx b/projects/app/src/pageComponents/account/AccountContainer.tsx index 6afed1f7d..6b31608c7 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/bill/BillDetailModal.tsx b/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx index 5bc963ffd..67ac99b4b 100644 --- a/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx +++ b/projects/app/src/pageComponents/account/bill/BillDetailModal.tsx @@ -109,6 +109,13 @@ const BillDetailModal = ({ billId, onClose }: BillDetailModalProps) => { unit: 'h' }); } + if (config.customDomain !== undefined) { + items.push({ + key: i18nT('account:custom_domain'), + value: config.customDomain, + unit: '' + }); + } return items; }, [bill?.couponDetail?.subscriptions]); 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 000000000..4b8baa92d --- /dev/null +++ b/projects/app/src/pageComponents/account/customDomain/createModal.tsx @@ -0,0 +1,338 @@ +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('tencent'); + const [domain, setDomain] = useState(''); + const [editDomain, setEditDomain] = useState(true); + + useEffect(() => { + if (type === 'refresh') { + setProvider(data?.provider || 'tencent'); + 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('tencent')} + isDisabled={!editDomain || type === 'refresh'} + /> + setProvider('aliyun')} + 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 000000000..145877083 --- /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/OffiAccount/index.tsx b/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx index 989a09fd5..f95fa3e01 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/OffiAccount/index.tsx @@ -240,7 +240,7 @@ const OffiAccount = ({ appId }: { appId: string }) => { )} 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..340bfc812 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,15 @@ 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'; +import { formatTime2YMDHM } from '@fastgpt/global/common/string/time'; const WecomEditModal = ({ appId, @@ -25,7 +28,7 @@ const WecomEditModal = ({ appId: string; defaultData: OutLinkEditType; onClose: () => void; - onCreate: (id: string) => void; + onCreate: (shareId: string) => Promise; onEdit: () => void; isEdit?: boolean; }) => { @@ -38,7 +41,11 @@ const WecomEditModal = ({ defaultValues: defaultData }); - const { runAsync: onclickCreate, loading: creating } = useRequest2( + const { + runAsync: onclickCreate, + loading: creating, + data: createShareId + } = useRequest2( (e) => createShareChat({ ...e, @@ -48,117 +55,214 @@ 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 8840bfcc2..1f8015523 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,13 +20,15 @@ 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'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getDocPath } from '@/web/common/system/doc'; +import { listCustomDomain } from '@/web/support/customDomain/api'; const WecomEditModal = dynamic(() => import('./WecomEditModal')); const ShowShareLinkModal = dynamic(() => import('../components/showShareLinkModal')); @@ -58,31 +61,64 @@ const Wecom = ({ appId }: { appId: string }) => { const [showShareLink, setShowShareLink] = useState(null); + const { data: customDomains = [] } = useRequest2(listCustomDomain, { + manual: false, + refreshOnWindowFocus: true + }); + return ( - - {t('publish:wecom.title')} - - + + + {t('publish:wecom.title')} + + {feConfigs?.docUrl && ( + + + + {t('common:read_doc')} + + + )} + + + {feConfigs.customDomain?.enable && ( + + )} + + @@ -199,14 +235,39 @@ 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 && ( - + 0 + ? { text: '' } + : { + text: ( + + ) + }} + /> + ) + })} + /> )} {showShareLinkModalOpen && ( @@ -214,6 +275,8 @@ const Wecom = ({ appId }: { appId: string }) => { shareLink={showShareLink ?? ''} onClose={closeShowShareLinkModal} img="/imgs/outlink/wecom-copylink-instruction.png" + defaultDomain={false} + showCustomDomainSelector={true} /> )} 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..27412b1ae 100644 --- a/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx +++ b/projects/app/src/pageComponents/app/detail/Publish/components/showShareLinkModal.tsx @@ -4,45 +4,171 @@ 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, useEffect } from 'react'; +import MySelect from '@fastgpt/web/components/common/MySelect'; export type ShowShareLinkModalProps = { shareLink: string; onClose: () => void; img: string; + defaultDomain?: boolean; + showCustomDomainSelector?: boolean; }; -function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) { +export const ShareLinkContainer = ({ + shareLink, + img, + defaultDomain = true, + showCustomDomainSelector = false +}: { + shareLink: string; + img: string; + defaultDomain?: boolean; + showCustomDomainSelector?: boolean; +}) => { const { copyData } = useCopyData(); const { t } = useTranslation(); + const [customDomain, setCustomDomain] = useState(undefined); + + const { data: customDomainList = [] } = useRequest2(listCustomDomain, { + manual: !showCustomDomainSelector + }); + + // 从 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 defaultOption = [ + { + label: t('publish:use_default_domain'), + value: '' + } + ]; + + // 只显示已激活的自定义域名 + const activeDomains = customDomainList + .filter((item) => item.status === 'active') + .map((item) => ({ + label: item.domain, + value: item.domain + })); + + return activeDomains.length === 0 + ? [...defaultOption] + : [...(defaultDomain ? defaultOption : []), ...activeDomains]; + }, [customDomainList, defaultDomain, t]); + + // 当 defaultDomain=false 时,自动选择第一个自定义域名 + useEffect(() => { + if (!defaultDomain && domainOptions.length > 0 && customDomain === undefined) { + setCustomDomain(domainOptions[0].value || undefined); + } + }, [defaultDomain, domainOptions, customDomain]); + + return ( + <> + {/* 自定义域名选择器 */} + {showCustomDomainSelector && 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, + defaultDomain, + showCustomDomainSelector +}: ShowShareLinkModalProps) { + const { t } = useTranslation(); return ( - - - {t('publish:copy_link_hint')} - copyData(shareLink)} - /> - - - {shareLink} - - - - - + ); diff --git a/projects/app/src/pageComponents/dashboard/agent/List.tsx b/projects/app/src/pageComponents/dashboard/agent/List.tsx index f718833ec..e36b804d1 100644 --- a/projects/app/src/pageComponents/dashboard/agent/List.tsx +++ b/projects/app/src/pageComponents/dashboard/agent/List.tsx @@ -528,7 +528,7 @@ const CreateButton = ({ appType }: { appType: AppTypeEnum | 'all' }) => { > 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/core/dataset/collection/detail.ts b/projects/app/src/pages/api/core/dataset/collection/detail.ts index 5009db159..2e266e818 100644 --- a/projects/app/src/pages/api/core/dataset/collection/detail.ts +++ b/projects/app/src/pages/api/core/dataset/collection/detail.ts @@ -9,7 +9,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { type DatasetCollectionItemType } from '@fastgpt/global/core/dataset/type'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { collectionTagsToTagLabel } from '@fastgpt/service/core/dataset/collection/utils'; -import { getVectorCountByCollectionId } from '@fastgpt/service/common/vectorDB/controller'; +import { getVectorCount } from '@fastgpt/service/common/vectorDB/controller'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; import { getS3DatasetSource } from '@fastgpt/service/common/s3/sources/dataset'; @@ -38,7 +38,11 @@ async function handler(req: NextApiRequest): Promise const [file, indexAmount, errorCount] = await Promise.all([ fileId ? getS3DatasetSource().getFileMetadata(fileId) : undefined, - getVectorCountByCollectionId(collection.teamId, collection.datasetId, collection._id), + getVectorCount({ + teamId: collection.teamId, + datasetId: collection.datasetId, + collectionId: collection._id + }), MongoDatasetTraining.countDocuments( { teamId: collection.teamId, diff --git a/projects/app/src/pages/api/support/outLink/update.ts b/projects/app/src/pages/api/support/outLink/update.ts index 853b92be8..73f9fabdb 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/common/system/useSystemStore.ts b/projects/app/src/web/common/system/useSystemStore.ts index abdd79ae2..80ac350ab 100644 --- a/projects/app/src/web/common/system/useSystemStore.ts +++ b/projects/app/src/web/common/system/useSystemStore.ts @@ -125,7 +125,7 @@ export const useSystemStore = create()( return null; }, - gitStar: 25000, + gitStar: 26500, async loadGitStar() { if (!get().feConfigs?.show_git) return; try { @@ -255,6 +255,8 @@ export const useSystemStore = create()( { name: 'globalStore', partialize: (state) => ({ + gitStar: state.gitStar, + loginStore: state.loginStore, initDataBufferId: state.initDataBufferId, feConfigs: state.feConfigs, 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 000000000..20fcbe6f0 --- /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 000000000..b7f2baaff --- /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]>; diff --git a/test/cases/service/common/vectorDB/controller.test.ts b/test/cases/service/common/vectorDB/controller.test.ts index 5e79e42ce..7582d71bb 100644 --- a/test/cases/service/common/vectorDB/controller.test.ts +++ b/test/cases/service/common/vectorDB/controller.test.ts @@ -6,8 +6,7 @@ import { mockVectorInit, mockGetVectorDataByTime, mockGetVectorCountByTeamId, - mockGetVectorCountByDatasetId, - mockGetVectorCountByCollectionId, + mockGetVectorCount, resetVectorMocks } from '@test/mocks/common/vector'; import { mockGetVectorsByText } from '@test/mocks/core/ai/embedding'; @@ -18,8 +17,7 @@ import { recallFromVectorStore, getVectorDataByTime, getVectorCountByTeamId, - getVectorCountByDatasetId, - getVectorCountByCollectionId, + getVectorCount, insertDatasetDataVector, deleteDatasetDataVector } from '@fastgpt/service/common/vectorDB/controller'; @@ -123,48 +121,42 @@ describe('VectorDB Controller', () => { const result = await getVectorCountByTeamId('team_123'); expect(result).toBe(150); - expect(mockGetVectorCountByTeamId).not.toHaveBeenCalled(); + expect(mockGetVectorCount).not.toHaveBeenCalled(); }); it('should fetch from Vector and cache if no cache exists', async () => { mockGetRedisCache.mockResolvedValue(null); - mockGetVectorCountByTeamId.mockResolvedValue(200); + mockGetVectorCount.mockResolvedValue(200); const result = await getVectorCountByTeamId('team_456'); expect(result).toBe(200); - expect(mockGetVectorCountByTeamId).toHaveBeenCalledWith('team_456'); + expect(mockGetVectorCount).toHaveBeenCalledWith({ teamId: 'team_456' }); }); it('should handle undefined cache value', async () => { mockGetRedisCache.mockResolvedValue(undefined); - mockGetVectorCountByTeamId.mockResolvedValue(50); + mockGetVectorCount.mockResolvedValue(50); const result = await getVectorCountByTeamId('team_789'); expect(result).toBe(50); - expect(mockGetVectorCountByTeamId).toHaveBeenCalled(); + expect(mockGetVectorCount).toHaveBeenCalledWith({ teamId: 'team_789' }); }); }); - describe('getVectorCountByDatasetId', () => { - it('should call Vector.getVectorCountByDatasetId', async () => { - const result = await getVectorCountByDatasetId('team_1', 'dataset_1'); + describe('getVectorCount', () => { + it('should call Vector.getVectorCount', async () => { + const result = await getVectorCount({ teamId: 'team_1', datasetId: 'dataset_1' }); - expect(mockGetVectorCountByDatasetId).toHaveBeenCalledWith('team_1', 'dataset_1'); + expect(mockGetVectorCount).toHaveBeenCalledWith({ + teamId: 'team_1', + datasetId: 'dataset_1' + }); expect(result).toBe(50); }); }); - describe('getVectorCountByCollectionId', () => { - it('should call Vector.getVectorCountByCollectionId', async () => { - const result = await getVectorCountByCollectionId('team_1', 'dataset_1', 'col_1'); - - expect(mockGetVectorCountByCollectionId).toHaveBeenCalledWith('team_1', 'dataset_1', 'col_1'); - expect(result).toBe(25); - }); - }); - describe('insertDatasetDataVector', () => { const mockModel = { model: 'text-embedding-ada-002', diff --git a/test/mocks/common/vector.ts b/test/mocks/common/vector.ts index f1a8b8df6..0d427014c 100644 --- a/test/mocks/common/vector.ts +++ b/test/mocks/common/vector.ts @@ -26,9 +26,7 @@ export const mockGetVectorDataByTime = vi.fn().mockResolvedValue([ export const mockGetVectorCountByTeamId = vi.fn().mockResolvedValue(100); -export const mockGetVectorCountByDatasetId = vi.fn().mockResolvedValue(50); - -export const mockGetVectorCountByCollectionId = vi.fn().mockResolvedValue(25); +export const mockGetVectorCount = vi.fn().mockResolvedValue(50); const MockVectorCtrl = vi.fn().mockImplementation(() => ({ init: mockVectorInit, @@ -37,8 +35,7 @@ const MockVectorCtrl = vi.fn().mockImplementation(() => ({ embRecall: mockVectorEmbRecall, getVectorDataByTime: mockGetVectorDataByTime, getVectorCountByTeamId: mockGetVectorCountByTeamId, - getVectorCountByDatasetId: mockGetVectorCountByDatasetId, - getVectorCountByCollectionId: mockGetVectorCountByCollectionId + getVectorCount: mockGetVectorCount })); // Mock PgVectorCtrl @@ -74,6 +71,5 @@ export const resetVectorMocks = () => { mockVectorInit.mockClear(); mockGetVectorDataByTime.mockClear(); mockGetVectorCountByTeamId.mockClear(); - mockGetVectorCountByDatasetId.mockClear(); - mockGetVectorCountByCollectionId.mockClear(); + mockGetVectorCount.mockClear(); };