feat: custom domain & wecom bot SaaS integration (#6047)
* feat: custom Domain type define * feat: custom domain * feat: wecom custom domain * chore: i18n * chore: i18n; team auth * feat: wecom multi-model message support * chore: wecom edit modal * chore(doc): custom domain && wecom bot * fix: type * fix: type * fix: file detect * feat: fe * fix: img name
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: 配置自定义域名
|
||||
description: 如何在 FastGPT 中配置自定义域名
|
||||
---
|
||||
|
||||
FastGPT 云服务版自 v4.14.4 后支持配置自定义域名。
|
||||
|
||||
## 如何配置自定义域名
|
||||
|
||||
### 1. 打开“自定义域名”页面
|
||||
|
||||
在侧边栏选择“账号” -> “自定义域名”,打开自定义域名配置页。
|
||||
|
||||
如果您的套餐等级不支持配置,请根据页面的指引升级套餐。
|
||||
|
||||

|
||||
|
||||
### 2. 添加自定义域名
|
||||
|
||||
1. 准备好您的域名。您的域名必须先经过备案,目前支持“阿里云”、“腾讯云”、“火山引擎”三家服务商的备案域名。
|
||||
2. 点击“编辑”按钮,进入编辑状态。
|
||||
3. 填入您的域名,例如 www.example.com
|
||||
4. 在域名服务商的域名解析处,添加界面中提示的 DNS 纪录,注意纪录类型为 CNAME。
|
||||
5. 添加解析纪录后,点击“保存”按钮。系统将自动检查 DNS 解析情况,一般情况下,在一分钟内就可以获取到解析纪录。如果长时间没有获取到纪录,可以重试一次。
|
||||
6. 待状态提示显示为“已生效”后,点击“确认”按钮即可。
|
||||
|
||||

|
||||
|
||||
现在您可以通过您自己的域名访问 fastgpt 服务、调用 fastgpt 的 API 了。
|
||||
|
||||
## 域名解析失效
|
||||
|
||||
系统会每天对 DNS 解析进行检查,如果发现 DNS 解析纪录失效,则会停用该自定义域名,可以在“自定义域名”管理界面中点击“编辑”进行重新解析。
|
||||
|
||||

|
||||
|
||||
如果您需要修改自定义域名、或修改服务商,则需要删除自定义域名配置后进行重新配置。
|
||||
|
||||
|
||||
## 使用案例
|
||||
|
||||
- [接入企业微信智能机器人](/docs/use-cases/external-integration/wecom)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"title": "团队与权限",
|
||||
"description": "团队管理、成员组与权限设置,确保团队协作中的数据安全和权限分配合理。",
|
||||
"pages": ["team_roles_permissions","invitation_link"]
|
||||
}
|
||||
"pages": ["team_roles_permissions","invitation_link", "customDomain"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -3,119 +3,55 @@ title: 接入企微机器人教程
|
|||
description: FastGPT 接入企微机器人教程
|
||||
---
|
||||
|
||||
从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。
|
||||
- 从 4.12.4 版本起,FastGPT 商业版支持直接接入企微机器人,无需额外的 API。
|
||||
- 从 4.14.4 版本起,FastGPT 云服务版支持通过配置自定义域名的方式接入企微智能机器人。
|
||||
|
||||
## 1. 配置可信域名和可信IP
|
||||
## 1. (云服务版必须)配置自定义域名
|
||||
|
||||
点击企微头像,打开管理企业
|
||||
企微要求智能机器人消息推送地址必须使用企业主体域名,因此云服务版本用户必须先配置自定义域名才能使用企微机器人。
|
||||
|
||||

|
||||
- [配置自定义域名](/docs/introduction/guide/team_permissions/customDomain)
|
||||
|
||||
在应用管理中找到"自建"-"创建应用"
|
||||
若您是商业版用户,请继续使用您企业的域名。
|
||||
|
||||

|
||||
## 2. 创建智能机器人
|
||||
|
||||
创建好应用后, 下拉, 依次配置"网页授权及JS-SDK"和"企业可信IP"
|
||||
1. 超级管理员登录 [企业微信管理后台](https://work.weixin.qq.com/)
|
||||
2. 在"安全与管理" - "管理工具"页面点击"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 )
|
||||
|
||||

|
||||

|
||||
|
||||
其中, 网页授权及JS-SDK要求按照企微指引,完成域名归属认证
|
||||
3. 在创建机器人页面, 下拉, 点击 "API模式创建"
|
||||
|
||||

|
||||

|
||||
|
||||
企业可信IP要求为企业服务器IP, 后续企微的回调URL将请求到此IP
|
||||
4. 随机生成或者手动输入 Token 和 Encoding-AESKey,并且纪录下来
|
||||
|
||||

|
||||

|
||||
|
||||
## 2. 创建企业自建应用
|
||||
5. 在 FastGPT 中,选择要使用 Agent,在发布渠道页面,选择“企业微信机器人”,点击“创建”
|
||||
|
||||
前往 FastGPT ,选择想要接入的应用,在 发布渠道 页面,新建一个接入企微智能机器人的发布渠道,填写好基础信息。
|
||||

|
||||
|
||||

|
||||
6. 配置该发布渠道的信息,需要填入 Token 和 AESKey,也就是第四步中纪录下来的 Token 和 Encoding-AESKey
|
||||
|
||||
现在回到企业微信平台,找到 Corp ID, Secret, Agent ID, Token, AES Key 信息并填写回 FastGPT 平台
|
||||

|
||||
|
||||

|
||||
7. 点击“确认”后,选择您配置的自定义域名,复制回调地址,填回企微智能机器人配置页中。
|
||||
|
||||
在"我的企业"里找到企业 ID, 填写到 FastGPT 的 Corp ID 中
|
||||

|
||||
|
||||

|
||||
|
||||
在应用中找到 Agent Id 和 Secret, 并填写回 FastGPT
|
||||
|
||||

|
||||
|
||||
点击"消息接收"-"设置API接收"
|
||||
|
||||

|
||||
|
||||
随机生成或者手动输入 Token 和 Encoding-Key, 分别填写到 FastGPT 的 Token 和 AES Key 中
|
||||
|
||||

|
||||
|
||||
填写完成后确认创建
|
||||
|
||||
然后点击请求地址, 复制页面中的链接
|
||||
|
||||

|
||||
|
||||
回到刚才的配置详情, 将刚才复制的链接填入 URL 框中, 并点击下方的保存 ,即可完成自建应用的创建
|
||||
|
||||
注意: 若复制的链接是以 "http://localhost" 开头, 需要将本地地址改为企业主体域名
|
||||
|
||||
因为企微会给填写的 URL 发送验证密文, 若 URL 为本地地址, 则本地接收不到企微的密文
|
||||
|
||||
若 URL 不是企业主体域名, 则验证会失败
|
||||
|
||||
## 3. 创建智能机器人
|
||||
|
||||
第二步创建企业自建应用是为了验证域名和IP的合规性, 并获取 secret 参数, 下面创建智能机器人才是正式的配置流程
|
||||
|
||||
在"安全与管理" - "管理工具"页面找到"智能机器人" ( 注意: 只有企业创建者或超级管理员才有权限看到这个入口 )
|
||||
|
||||

|
||||
|
||||
创建机器人页面,下拉,找到,点击"API模式创建"
|
||||
|
||||

|
||||
|
||||
与刚才配置自建应用同理, 在 FastGPT 平台再新增一个发布渠道, 并回到企业微信配置参数
|
||||
|
||||

|
||||
|
||||
随机生成或者手动输入 Token 和 Encoding-AESKey, 分别填写到 FastGPT 的 Token 和 AES Key 中
|
||||
|
||||

|
||||
|
||||
Corp ID 和 Secret 这两个参数和刚才的自建应用保持一致
|
||||
|
||||
Agent ID 和自建应用的不同, 需要先填写一个自定义值, 后续会根据企业微信提供的数据重新更改
|
||||
|
||||
在 FastGPT 将Corp ID, Secret, Agent ID, Token, AES Key 等参数都填写完毕后, 点击确认
|
||||
|
||||
然后点击请求地址, 复制页面中的链接
|
||||
|
||||
回到企业微信, 将链接粘贴到智能机器人的 URL 配置栏, 点击创建
|
||||
|
||||
创建完成后, 找到智能机器人的配置详情
|
||||
|
||||

|
||||
|
||||
复制 Bot ID, 填写到 FastGPT 的 Agent ID 中, 即可完成智能机器人配置
|
||||
|
||||

|
||||
|
||||
## 4. 使用智能机器人
|
||||
## 3. 使用智能机器人
|
||||
|
||||
在企业微信平台的"通讯录",即可找到创建的机器人,就可以发送消息了
|
||||
|
||||

|
||||

|
||||
|
||||
## FAQ
|
||||
|
||||
### 发送了消息,没响应
|
||||
|
||||
1. 检查可信域名和可信IP是否配置正确。
|
||||
2. 检查自建应用的 Secret 参数是否与智能机器人一致。
|
||||
3. 查看 FastGPT 对话日志,是否有对应的提问记录
|
||||
1. 检查可信域名是否配置正确。
|
||||
2. 检查 Token 和 Encoding-AESKey 是否正确。
|
||||
3. 查看 FastGPT 对话日志,是否有对应的提问记录。
|
||||
4. 如果没记录,则可能是应用运行报错了,可以先试试最简单的机器人.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-11-14T13:21:17+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-12-08T21:02:38+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-12-09T12:18:15+08:00",
|
||||
"document/content/docs/introduction/development/proxy/cloudflare.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/development/proxy/http_proxy.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/development/proxy/nginx.mdx": "2025-07-23T21:35:03+08:00",
|
||||
|
|
@ -89,6 +89,7 @@
|
|||
"document/content/docs/introduction/guide/plugins/google_search_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/guide/plugins/searxng_plugin_guide.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/guide/plugins/upload_system_tool.mdx": "2025-11-04T16:58:12+08:00",
|
||||
"document/content/docs/introduction/guide/team_permissions/customDomain.mdx": "2025-12-08T22:21:05+08:00",
|
||||
"document/content/docs/introduction/guide/team_permissions/invitation_link.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/guide/team_permissions/team_roles_permissions.mdx": "2025-07-23T21:35:03+08:00",
|
||||
"document/content/docs/introduction/index.en.mdx": "2025-07-23T21:35:03+08:00",
|
||||
|
|
@ -101,7 +102,7 @@
|
|||
"document/content/docs/protocol/terms.en.mdx": "2025-08-03T22:37:45+08:00",
|
||||
"document/content/docs/protocol/terms.mdx": "2025-08-03T22:37:45+08:00",
|
||||
"document/content/docs/toc.en.mdx": "2025-08-04T13:42:36+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-11-29T09:24:47+08:00",
|
||||
"document/content/docs/toc.mdx": "2025-12-08T22:21:05+08:00",
|
||||
"document/content/docs/upgrading/4-10/4100.mdx": "2025-08-02T19:38:37+08:00",
|
||||
"document/content/docs/upgrading/4-10/4101.mdx": "2025-09-08T20:07:20+08:00",
|
||||
"document/content/docs/upgrading/4-11/4110.mdx": "2025-08-05T23:20:39+08:00",
|
||||
|
|
@ -198,6 +199,6 @@
|
|||
"document/content/docs/use-cases/external-integration/feishu.mdx": "2025-07-24T14:23:04+08:00",
|
||||
"document/content/docs/use-cases/external-integration/official_account.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/use-cases/external-integration/openapi.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-09-16T14:22:28+08:00",
|
||||
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-08T22:21:05+08:00",
|
||||
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
|
||||
}
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
|
@ -75,7 +75,7 @@ export const parseUrlToFileType = (url: string): UserChatItemFileItemType | unde
|
|||
};
|
||||
}
|
||||
|
||||
// Default to image type for non-document files
|
||||
// Default to file type for non-extension files
|
||||
return {
|
||||
type: ChatFileTypeEnum.image,
|
||||
name: filename || 'null',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<typeof VerifyFileType>;
|
||||
export type CustomDomainType = z.infer<typeof CustomDomainType>;
|
||||
|
||||
export type CreateCustomDomainBody = {
|
||||
domain: string;
|
||||
provider: ProviderEnum;
|
||||
cnameDomain: string;
|
||||
};
|
||||
export type ProviderEnum = z.infer<typeof ProviderEnum>;
|
||||
export type CustomDomainStatusEnum = z.infer<typeof CustomDomainStatusEnum>;
|
||||
|
||||
export type UpdateDomainVerifyFileBody = {
|
||||
domain: string;
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
/**
|
||||
* @param domain : should be like sealosbja.site (secondary domain)
|
||||
* @returns CNAME domain: fastgpt-<random_string>.<domain>
|
||||
*/
|
||||
export const generateCNAMEDomain = (domain: string): string => {
|
||||
const str = customAlphabet('abcdefghijklmnopqrstuvwxyz', 8);
|
||||
return `fastgpt-${str()}.${domain}`;
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||
import { S3PrivateBucket } from '../../buckets/private';
|
||||
import { S3Sources } from '../../type';
|
||||
import type { UploadFileParams } from './type';
|
||||
import {
|
||||
type CheckChatFileKeys,
|
||||
type DelChatFileByPrefixParams,
|
||||
ChatFileUploadSchema,
|
||||
DelChatFileByPrefixSchema
|
||||
DelChatFileByPrefixSchema,
|
||||
UploadChatFileSchema
|
||||
} from './type';
|
||||
import { differenceInHours } from 'date-fns';
|
||||
import { S3Buckets } from '../../constants';
|
||||
|
|
@ -109,6 +111,30 @@ export class S3ChatSource {
|
|||
deleteChatFileByKey(key: string) {
|
||||
return this.bucket.addDeleteJob({ key });
|
||||
}
|
||||
|
||||
async uploadFile(params: UploadFileParams) {
|
||||
const { appId, chatId, uId, filename, expiredTime, buffer, contentType } =
|
||||
UploadChatFileSchema.parse(params);
|
||||
const { fileKey } = getFileS3Key.chat({
|
||||
appId,
|
||||
chatId,
|
||||
uId,
|
||||
filename
|
||||
});
|
||||
|
||||
console.log('upload to s3, contentType:', contentType);
|
||||
await this.bucket.putObject(fileKey, buffer, undefined, {
|
||||
'Content-Type': contentType || 'application/octet-stream'
|
||||
});
|
||||
|
||||
return {
|
||||
fileKey,
|
||||
accessUrl: await this.bucket.createPreviewUrl({
|
||||
key: fileKey,
|
||||
expiredHours: expiredTime ? differenceInHours(new Date(), expiredTime) : 24
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getS3ChatSource() {
|
||||
|
|
|
|||
|
|
@ -16,3 +16,15 @@ export const DelChatFileByPrefixSchema = z.object({
|
|||
uId: z.string().nonempty().optional()
|
||||
});
|
||||
export type DelChatFileByPrefixParams = z.infer<typeof DelChatFileByPrefixSchema>;
|
||||
|
||||
export const UploadChatFileSchema = z.object({
|
||||
appId: ObjectIdSchema,
|
||||
chatId: z.string().nonempty(),
|
||||
uId: z.string().nonempty(),
|
||||
filename: z.string().nonempty(),
|
||||
expiredTime: z.date().optional(),
|
||||
buffer: z.instanceof(Buffer),
|
||||
contentType: z.string().optional()
|
||||
});
|
||||
|
||||
export type UploadFileParams = z.infer<typeof UploadChatFileSchema>;
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_30360_927)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 0.833336C15.0626 0.833336 19.1667 4.93739 19.1667 10C19.1667 15.0626 15.0626 19.1667 10 19.1667C4.93739 19.1667 0.833336 15.0626 0.833336 10C0.833336 4.93739 4.93739 0.833336 10 0.833336ZM10 17.5C10.0951 17.5 10.7259 17.3391 11.429 15.7921C11.742 15.1036 12.0067 14.2572 12.1946 13.2943H7.80539C7.99334 14.2572 8.25805 15.1036 8.571 15.7921C9.27415 17.3391 9.90487 17.5 10 17.5ZM7.5715 11.6277C7.52492 11.1056 7.5 10.5614 7.5 10C7.5 9.42872 7.5258 8.87537 7.57397 8.34492H12.426C12.4742 8.87537 12.5 9.42872 12.5 10C12.5 10.5614 12.4751 11.1056 12.4285 11.6277H7.5715ZM13.8895 13.2943C13.6355 14.7449 13.2187 16.0175 12.6883 17.0038C14.4614 16.3228 15.9103 14.9879 16.7396 13.2943H13.8895ZM17.3229 11.6277H14.1012C14.1442 11.0994 14.1667 10.5555 14.1667 10C14.1667 9.43491 14.1434 8.88176 14.0989 8.34492H17.3168C17.4367 8.87739 17.5 9.4313 17.5 10C17.5 10.5589 17.4389 11.1036 17.3229 11.6277ZM5.89882 11.6277H2.67711C2.56115 11.1036 2.5 10.5589 2.5 10C2.5 9.4313 2.5633 8.87739 2.68323 8.34492H5.90108C5.85658 8.88176 5.83334 9.43491 5.83334 10C5.83334 10.5555 5.85579 11.0994 5.89882 11.6277ZM3.26036 13.2943H6.1105C6.36451 14.7449 6.78134 16.0175 7.31173 17.0038C5.53862 16.3228 4.08972 14.9879 3.26036 13.2943ZM7.81077 6.67825H12.1892C12.0016 5.72674 11.739 4.88982 11.429 4.20786C10.7259 2.66093 10.0951 2.5 10 2.5C9.90487 2.5 9.27415 2.66093 8.571 4.20786C8.26102 4.88982 7.99838 5.72674 7.81077 6.67825ZM13.8847 6.67825H16.7261C15.8946 4.99771 14.4518 3.6735 12.6883 2.99618C13.2153 3.97628 13.6302 5.23909 13.8847 6.67825ZM7.31173 2.99618C6.78469 3.97628 6.36977 5.23909 6.11533 6.67825H3.27386C4.10539 4.99771 5.5482 3.6735 7.31173 2.99618Z" fill="#485264"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_30360_927">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg viewBox="0 0 91 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M65.1605 15.1515H71.6969V13.1313H65.1605V11.1111H71.4927V0.60606H56.7856V11.1111H63.1178V13.1313H56.5813V15.1515H63.1178V17.3737H56.0707V19.3939H72.3097V17.3737H65.2626V15.1515H65.1605ZM65.3647 2.62626H69.6543V4.84848H65.3647V2.62626ZM65.3647 6.86869H69.6543V8.9899H65.3647V6.86869ZM63.3221 8.9899H59.0325V6.86869H63.3221V8.9899ZM63.3221 4.84848H59.0325V2.62626H63.3221V4.84848ZM10.7239 8.68687H21.4478V11.1111H10.7239V8.68687Z" fill="#FF6A00"/>
|
||||
<path d="M26.7587 0H19.7116L21.4478 2.42424L26.5544 4.0404C27.4736 4.34343 28.0864 5.25253 28.0864 6.16162V13.8384C28.0864 14.7475 27.4736 15.6566 26.5544 15.9596L21.4478 17.5758L19.7116 20H26.7587C29.7205 20 32.0696 17.6768 32.0696 14.7475V5.35354C32.0696 2.42424 29.7205 0 26.7587 0ZM5.51515 15.9596C4.59596 15.6566 3.98316 14.7475 3.98316 13.8384V6.16162C3.98316 5.25253 4.59596 4.34343 5.51515 4.0404L10.6218 2.42424L12.358 0H5.31089C2.34905 0 0 2.42424 0 5.35354V14.6465C0 17.5758 2.34905 19.899 5.31089 19.899H12.358L10.6218 17.4747L5.51515 15.9596ZM49.7385 4.84848H44.5297V15.4545H49.7385V4.84848ZM47.5937 13.3333H46.4703V6.86869H47.5937V13.3333ZM36.8698 19.3939H38.9125V2.62626H41.0572L39.8316 8.18182V10.202H41.0572V14.6465C41.0572 14.9495 40.853 15.1515 40.5466 15.1515H40.0359V17.1717H41.0572C42.1807 17.1717 43.0999 16.2626 43.0999 15.1515V8.08081H41.8743L43.0999 2.52525V0.50505H36.8698V19.3939Z" fill="#FF6A00"/>
|
||||
<path d="M44.019 2.62626H51.0662V15.6566C51.0662 16.5657 50.3513 17.3737 49.3299 17.3737H47.7979V19.3939H49.9427C51.679 19.3939 53.211 17.9798 53.211 16.1616V2.62626H53.9259V0.60606H44.019V2.62626ZM75.0673 0.70707H89.7744V2.72727H75.0673V0.70707ZM90.5914 10.303V8.28283H74.3524V10.303H78.4377L75.1694 17.3737V19.3939H88.9573C89.5701 19.3939 89.9786 18.8889 89.9786 18.3838C89.9786 18.1818 89.9786 18.0808 89.8765 17.9798L88.1403 14.1414H85.8933L87.4253 17.3737H77.5185L80.7867 10.303H90.5914Z" fill="#FF6A00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
<svg viewBox="0 0 77 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M61.9037 7.97203H54.3043V9.01632H56.6884V13.1935H53.8572V14.387H56.6884V21.1002H57.8804V14.387H60.8606V20.9511H63.2447V19.7576H62.2017V7.97203H61.9037ZM57.8804 13.0443V9.01632H60.8606V13.1935H57.8804V13.0443ZM40.5955 17.9674H46.7048V19.1608H40.5955V17.9674ZM36.2743 15.8788C36.2743 17.9674 36.1252 19.4592 35.6782 20.8019C35.6782 20.9511 35.6782 20.9511 35.8272 20.9511H37.0193C37.4663 19.0117 37.4663 17.5198 37.4663 16.3263H39.1054V19.7576H38.0624C37.9133 19.7576 37.9133 19.7576 37.9133 19.9068L38.2114 20.9511H40.2975V7.97203H36.4233L36.2743 15.8788ZM37.6153 15.4312V12.7459H39.2544V15.5804H37.6153V15.4312ZM39.1054 9.01632V11.7016H37.4663V9.01632H39.1054Z" fill="#56606F"/>
|
||||
<path d="M47.152 12.5967H48.6421V11.5524H44.3208C44.3208 11.2541 44.3208 10.8065 44.6188 10.5082H48.4931V9.46387H47.45C47.599 8.86713 47.897 8.12121 47.897 8.12121C47.897 7.97203 47.897 7.97203 47.748 7.97203H46.854L46.4069 9.46387H45.0659C45.2149 8.86713 45.2149 8.2704 45.3639 7.52447C45.3639 7.37529 45.3639 7.37529 45.2149 7.37529H44.3208C44.1718 8.12121 44.1718 8.86713 43.8738 9.46387H43.1288L42.6817 7.97203H41.7877C41.6387 7.97203 41.6387 7.97203 41.6387 8.12121C41.6387 8.27039 41.7877 8.86713 42.0857 9.46387H41.0426V10.5082H43.5758C43.4268 10.8065 43.4268 11.2541 43.1288 11.5524H40.7446V12.5967H42.5327C41.7877 13.641 41.0426 14.0886 40.7446 14.2378V15.4312C40.7446 15.5804 40.8936 15.5804 40.8936 15.4312C41.3407 15.5804 41.6387 15.1329 41.9367 14.8345H46.2579V16.4755H42.8307L42.9798 15.5804C42.9798 15.4312 42.9798 15.4312 42.8307 15.4312H41.7877L41.6387 17.3706C41.6387 17.5198 41.6387 17.5198 41.7877 17.5198H47.599V20.3543H45.0659C44.9169 20.3543 44.9169 20.3543 44.9169 20.5035L45.2149 21.5478H48.7911V16.6247H47.45V14.0886C48.046 14.5361 48.4931 14.9837 49.0891 15.1329C49.2381 15.282 49.2381 15.282 49.2381 15.1329V14.0886C48.6421 13.9394 47.897 13.4918 47.152 12.5967ZM45.8109 12.5967C45.9599 13.0443 46.4069 13.4918 46.705 13.7902H42.9798C43.2778 13.4918 43.5758 13.0443 43.8738 12.5967H45.8109ZM55.0494 17.2214L52.9633 18.8625V11.5524H50.1321V12.7459H51.7712V19.7576C51.3242 20.0559 51.0262 20.2051 51.0262 20.2051L51.7712 21.1002L55.1984 18.5641L55.0494 17.2214ZM51.9202 10.359H53.1123C53.2613 10.359 53.2613 10.2098 53.2613 10.2098L51.4732 7.52447H50.2812L50.1321 7.67366C50.2812 7.82284 51.9202 10.359 51.9202 10.359ZM65.629 8.12121H75.3145V9.31468H65.629V8.12121ZM69.2052 14.0886H76.8046V12.8951H64.4369V14.0886H67.8641C67.1191 16.028 65.331 20.6527 65.331 20.8019C65.331 20.951 65.331 20.951 65.48 20.951H76.2086C76.3576 20.951 76.3576 20.8019 76.3576 20.8019L74.7185 16.6247H73.5264C73.3774 16.6247 73.3774 16.7739 73.3774 16.7739C73.3774 16.9231 74.5695 19.7576 74.5695 19.7576H67.4171C67.2681 19.6084 69.2052 14.0886 69.2052 14.0886Z" fill="#56606F"/>
|
||||
<path d="M28.2277 19.4592C27.7807 19.9068 26.7376 20.8019 24.8005 20.8019H13.7739C17.2011 17.5198 19.8832 14.6853 20.1813 14.5361C20.4793 14.2378 21.0753 13.641 21.6713 13.1935C22.8634 11.8508 23.9065 11.7016 24.9495 11.7016C26.2906 11.7016 27.3336 12.1492 28.2277 13.0443C30.0158 14.6853 30.0158 17.8182 28.2277 19.4592ZM30.4628 10.9557C29.1217 9.61305 27.1846 8.56876 25.2475 8.56876C23.3104 8.56876 21.8203 9.31468 20.4793 10.2098C20.0322 10.6573 19.1382 11.2541 18.5422 12.1492C17.7971 12.5967 6.77051 23.7855 6.77051 23.7855H23.0124C24.0555 23.7855 24.9495 23.7855 25.5455 23.6364C27.1846 23.4872 28.9727 22.7413 30.3138 21.5478C33.145 18.5641 33.145 13.7902 30.4628 10.9557Z" fill="#00A3FF"/>
|
||||
<path d="M12.135 10.2098C10.6449 9.1655 9.3038 8.71795 7.5157 8.71795C5.42958 8.71795 3.49248 9.61305 2.30041 11.1049C-0.530744 14.0886 -0.530744 18.8625 2.44942 21.8462C3.64149 23.0396 4.98256 23.6364 6.47264 23.6364L9.45281 20.8019H7.81372C6.17463 20.6527 5.13157 20.0559 4.38653 19.4592C2.59843 17.669 2.59843 14.8345 4.38653 12.8951C5.28058 12 6.17462 11.5524 7.66471 11.5524C8.55876 11.5524 9.45281 11.7016 10.6449 12.8951C11.0919 13.4918 12.433 14.3869 12.88 14.9837H13.029L14.8171 12.8951C14.2211 12.1492 12.731 10.9557 12.135 10.2098Z" fill="#00C8DC"/>
|
||||
<path d="M25.6948 6.77855C24.3537 2.89977 20.4795 0.214451 16.3073 0.214451C11.241 0.214451 7.21778 3.94405 6.32373 8.71795C6.62175 8.71795 7.06877 8.56876 7.5158 8.56876C7.96282 8.56876 8.70786 8.71795 9.15489 8.71795C9.89993 5.4359 12.7311 3.19813 16.0093 3.19813C18.8404 3.19813 21.3736 4.83916 22.5656 7.37529C22.5656 7.37529 22.7146 7.52447 22.7146 7.37529C23.7577 7.22611 24.8007 6.77855 25.6948 6.77855C25.6948 7.07692 25.6948 7.07692 25.6948 6.77855Z" fill="#006EFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -11,6 +11,25 @@
|
|||
"create_model": "Add new model",
|
||||
"custom_domain": "Custom Domain",
|
||||
"custom_model": "custom model",
|
||||
"custom_domain": "custom domain",
|
||||
"custom_domain.domain": "Domain",
|
||||
"custom_domain.provider": "Domain Provider",
|
||||
"custom_domain.registration_hint": "Please prepare your own domain and complete the ICP filing through <bold>{{provider}}</bold>, 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 <bold>{{domain}}</bold>. After the resolution takes effect, you can bind the custom domain.",
|
||||
"custom_domain.dns_resolved": "Verified",
|
||||
"custom_domain.dns_resolving": "Verifying",
|
||||
"custom_domain.delete_confirm": "Confirm to delete this custom domain?",
|
||||
"custom_domain.status.active": "Active",
|
||||
"custom_domain.status.inactive": "Inactive",
|
||||
"custom_domain.domain_verify": "Domain Verification",
|
||||
"custom_domain.domain_verify.path": "File Path",
|
||||
"custom_domain.domain_verify.content": "File Content",
|
||||
"custom_domain.domain_verify.desc": "After saving, visiting {{domain}}/{{path}} will return {{content}}",
|
||||
"default_model": "Default model",
|
||||
"default_model_config": "Default model configuration",
|
||||
"language": "Language and time zone",
|
||||
|
|
@ -114,5 +133,7 @@
|
|||
"app_registration_count": "App registration count",
|
||||
"audit_log_store_duration": "Audit log storage duration",
|
||||
"ticket_response_time": "Ticket response time",
|
||||
"custom_config_details": "Custom configuration details"
|
||||
"custom_config_details": "Custom configuration details",
|
||||
"upgrade_to_use_custom_domain": "Current plan does not support custom domains, please upgrade first",
|
||||
"upgrade_plan": "Upgrade plan"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,25 @@
|
|||
"custom_config_details": "定制配置详情",
|
||||
"custom_domain": "自定义域名",
|
||||
"custom_model": "自定义模型",
|
||||
"custom_domain": "自定义域名",
|
||||
"custom_domain.domain": "域名",
|
||||
"custom_domain.provider": "域名备案商",
|
||||
"custom_domain.registration_hint": "请自备域名并通过 <bold>{{provider}}</bold> 完成备案后,将域名填入下方输入框中",
|
||||
"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 解析到 <bold>{{domain}}</bold>,解析生效后即可绑定自定义域名。",
|
||||
"custom_domain.dns_resolved": "已验证",
|
||||
"custom_domain.dns_resolving": "验证中",
|
||||
"custom_domain.delete_confirm": "确认删除该自定义域名?",
|
||||
"custom_domain.status.active": "已生效",
|
||||
"custom_domain.status.inactive": "已失效",
|
||||
"custom_domain.domain_verify": "域名校验",
|
||||
"custom_domain.domain_verify.path": "文件路径",
|
||||
"custom_domain.domain_verify.content": "文件内容",
|
||||
"custom_domain.domain_verify.desc": "保存后,访问 {{domain}}/{{path}} 将返回 {{content}}",
|
||||
"day": "天",
|
||||
"default_model": "预设模型",
|
||||
"default_model_config": "默认模型配置",
|
||||
|
|
@ -104,9 +123,6 @@
|
|||
"requests_per_minute": "QPM",
|
||||
"reset_default": "恢复默认配置",
|
||||
"status": "状态",
|
||||
"subscription_mode_month": "时长",
|
||||
"subscription_package": "订阅套餐",
|
||||
"subscription_period": "订阅周期",
|
||||
"support_wallet_amount": "金额",
|
||||
"team": "团队管理",
|
||||
"third_party": "第三方账号",
|
||||
|
|
@ -114,5 +130,11 @@
|
|||
"usage_records": "使用记录",
|
||||
"website_sync_per_dataset": "站点同步最大页数",
|
||||
"yes": "是",
|
||||
"yuan": "{{amount}}元"
|
||||
"yuan": "{{amount}}元",
|
||||
"no": "否",
|
||||
"subscription_period": "订阅周期",
|
||||
"subscription_package": "订阅套餐",
|
||||
"subscription_mode_month": "时长",
|
||||
"upgrade_to_use_custom_domain": "当前套餐不支持自定义域名,请先升级",
|
||||
"upgrade_plan": "升级套餐"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@
|
|||
"pro_modal_title": "商业版专享!",
|
||||
"pro_modal_unlock_button": "去解锁",
|
||||
"publish_channel": "发布渠道",
|
||||
"publish_channel.wecom.empty": "发布到企业微信机器人,请先 <a>绑定自定义域名</a>,并且通过域名校验。",
|
||||
"publish_success": "发布成功",
|
||||
"question_guide_tip": "对话结束后,会为你生成 3 个引导性问题。",
|
||||
"reasoning_response": "输出思考",
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
"Folder": "文件夹",
|
||||
"FullScreen": "全屏",
|
||||
"FullScreenLight": "全屏预览",
|
||||
"refresh": "刷新",
|
||||
"Import": "导入",
|
||||
"Input": "输入",
|
||||
"Instructions": "使用说明",
|
||||
|
|
|
|||
|
|
@ -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": "自定义域名管理"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,25 @@
|
|||
"custom_config_details": "定制配置詳情",
|
||||
"custom_domain": "自定義域名",
|
||||
"custom_model": "自訂模型",
|
||||
"custom_domain": "自訂域名",
|
||||
"custom_domain.domain": "域名",
|
||||
"custom_domain.provider": "域名備案商",
|
||||
"custom_domain.registration_hint": "請自備域名並透過 <bold>{{provider}}</bold> 完成備案後,將域名填入下方輸入框中",
|
||||
"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 解析到 <bold>{{domain}}</bold>,解析生效後即可繫結自訂域名。",
|
||||
"custom_domain.dns_resolved": "已驗證",
|
||||
"custom_domain.dns_resolving": "驗證中",
|
||||
"custom_domain.delete_confirm": "確認刪除該自訂域名?",
|
||||
"custom_domain.status.active": "已生效",
|
||||
"custom_domain.status.inactive": "已失效",
|
||||
"custom_domain.domain_verify": "域名校驗",
|
||||
"custom_domain.domain_verify.path": "檔案路徑",
|
||||
"custom_domain.domain_verify.content": "檔案內容",
|
||||
"custom_domain.domain_verify.desc": "儲存後,訪問 {{domain}}/{{path}} 將傳回 {{content}}",
|
||||
"day": "天",
|
||||
"default_model": "預設模型",
|
||||
"default_model_config": "預設模型設定",
|
||||
|
|
@ -114,5 +133,10 @@
|
|||
"usage_records": "使用記錄",
|
||||
"website_sync_per_dataset": "站點同步最大頁數",
|
||||
"yes": "是",
|
||||
"yuan": "{{amount}}元"
|
||||
"yuan": "{{amount}}元",
|
||||
"month": "月",
|
||||
"extra_dataset_size": "額外知識庫容量",
|
||||
"extra_ai_points": "AI 積分運算標準",
|
||||
"upgrade_to_use_custom_domain": "目前套餐不支援自訂域名,請先升級",
|
||||
"upgrade_plan": "升級套餐"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "自訂域名管理"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const Navbar = ({ unread }: { unread: number }) => {
|
|||
activeLink: [
|
||||
'/account/bill',
|
||||
'/account/info',
|
||||
'/account/customDomain',
|
||||
'/account/team',
|
||||
'/account/usage',
|
||||
'/account/thirdParty',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,331 @@
|
|||
import {
|
||||
ModalBody,
|
||||
Box,
|
||||
Radio,
|
||||
Flex,
|
||||
Text,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Tag,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
IconButton,
|
||||
Button,
|
||||
ModalFooter,
|
||||
Link
|
||||
} from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation, Trans } from 'next-i18next';
|
||||
import Icon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { providerMap } from '@/web/support/customDomain/const';
|
||||
import type { ProviderEnum } from '@fastgpt/global/support/customDomain/type';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { generateCNAMEDomain } from '@fastgpt/global/support/customDomain/utils';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import {
|
||||
activeCustomDomain,
|
||||
checkCustomDomainDNSResolve,
|
||||
createCustomDomain
|
||||
} from '@/web/support/customDomain/api';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
|
||||
const ProviderItem = ({
|
||||
icon,
|
||||
selected,
|
||||
onClick,
|
||||
isDisabled
|
||||
}: {
|
||||
icon: IconNameType;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
isDisabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Flex
|
||||
flex="1"
|
||||
paddingInline="32px"
|
||||
paddingBlock="12px"
|
||||
alignItems="center"
|
||||
justifyContent="start"
|
||||
gap="8px"
|
||||
border="1px solid"
|
||||
borderColor={'blue.600'}
|
||||
borderRadius="sm"
|
||||
{...(selected
|
||||
? {
|
||||
backgroundColor: 'blue.50'
|
||||
}
|
||||
: {})}
|
||||
onClick={isDisabled ? undefined : onClick}
|
||||
cursor={isDisabled ? 'not-allowed' : 'pointer'}
|
||||
>
|
||||
<Radio isChecked={selected} />
|
||||
<Icon name={icon} padding={'11px'} h="20px" w="100px" />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
function CreateCustomDomainModal<T extends 'create' | 'refresh'>({
|
||||
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<ProviderEnum>('aliyun');
|
||||
const [domain, setDomain] = useState<string>('');
|
||||
const [editDomain, setEditDomain] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === 'refresh') {
|
||||
setProvider(data?.provider || 'aliyun');
|
||||
setDomain(data?.domain || '');
|
||||
}
|
||||
}, [data, type]);
|
||||
|
||||
const cnameDomain = useMemo(() => {
|
||||
if (type === 'refresh') {
|
||||
return data?.cnameDomain!;
|
||||
}
|
||||
const domain = feConfigs?.customDomain?.domain?.[provider];
|
||||
if (domain) {
|
||||
return generateCNAMEDomain(domain);
|
||||
}
|
||||
return '';
|
||||
}, [data?.cnameDomain, feConfigs?.customDomain?.domain, provider, type]);
|
||||
|
||||
const [DnsResolved, setDnsResolved] = useState<boolean>(false);
|
||||
const [startDnsResolve, setStartDnsResolve] = useState<boolean>(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 (
|
||||
<MyModal isOpen onClose={onClose} title={t('account:custom_domain')} minW="800px">
|
||||
<ModalBody>
|
||||
<Box fontWeight="500" color="gray.900">
|
||||
{t('account:custom_domain.provider')}
|
||||
</Box>
|
||||
<Flex flexDirection="row" gap="16px" w="100%" marginTop={'10px'}>
|
||||
<ProviderItem
|
||||
icon="support/account/customDomain/provider/aliyun"
|
||||
selected={provider === 'aliyun'}
|
||||
onClick={() => setProvider('aliyun')}
|
||||
isDisabled={!editDomain || type === 'refresh'}
|
||||
/>
|
||||
<ProviderItem
|
||||
icon="support/account/customDomain/provider/tencent"
|
||||
selected={provider === 'tencent'}
|
||||
onClick={() => setProvider('tencent')}
|
||||
isDisabled={!editDomain || type === 'refresh'}
|
||||
/>
|
||||
<ProviderItem
|
||||
icon="support/account/customDomain/provider/volcengine"
|
||||
selected={provider === 'volcengine'}
|
||||
onClick={() => setProvider('volcengine')}
|
||||
isDisabled={!editDomain || type === 'refresh'}
|
||||
/>
|
||||
</Flex>
|
||||
<Box marginTop={'16px'} fontSize={'sm'} color={'gray.600'}>
|
||||
<Trans
|
||||
i18nKey="account:custom_domain.registration_hint"
|
||||
values={{ provider: t(providerMap[provider]) }}
|
||||
components={{ bold: <Text as="span" fontWeight="bold" color="gray.900" /> }}
|
||||
/>
|
||||
</Box>
|
||||
<Flex marginTop={'16px'} alignItems="center">
|
||||
<InputGroup>
|
||||
<Input
|
||||
h="40px"
|
||||
placeholder="www.example.com"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
isDisabled={!editDomain || type === 'refresh'}
|
||||
/>
|
||||
<InputRightElement width="auto" paddingRight={'8px'}>
|
||||
{!editDomain && domain && startDnsResolve ? (
|
||||
DnsResolved ? (
|
||||
<Tag colorScheme="green" size="sm">
|
||||
{t('account:custom_domain.dns_resolved')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag colorScheme="red" size="sm">
|
||||
{t('account:custom_domain.dns_resolving')}
|
||||
</Tag>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
marginLeft={'8px'}
|
||||
onClick={() => {
|
||||
if (type === 'create') {
|
||||
setEditDomain(!editDomain);
|
||||
} else {
|
||||
checkDNSResolve();
|
||||
setStartDnsResolve(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{type === 'create'
|
||||
? editDomain
|
||||
? t('common:Save')
|
||||
: t('common:Edit')
|
||||
: t('common:refresh')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
padding="16px"
|
||||
borderRadius={'12px'}
|
||||
border="1px dashed"
|
||||
borderColor="#A1A1AA"
|
||||
w="full"
|
||||
marginTop="24px"
|
||||
flexDir="column"
|
||||
>
|
||||
<Box fontWeight="500" color="gray.900">
|
||||
{t('account:custom_domain.DNS_record')}
|
||||
</Box>
|
||||
<Box marginTop={'16px'} fontSize={'sm'} color={'gray.600'}>
|
||||
<Trans
|
||||
i18nKey="account:custom_domain.DNS_resolve_hint"
|
||||
values={{ domain: cnameDomain }}
|
||||
components={{ bold: <Text as="span" fontWeight="bold" color="gray.900" /> }}
|
||||
/>
|
||||
</Box>
|
||||
<Table size="sm" marginTop={'16px'} w="full">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('account:custom_domain.DNS_record.type')}</Th>
|
||||
<Th>TTL</Th>
|
||||
<Th>{t('common:value')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td>CNAME</Td>
|
||||
<Td>Auto</Td>
|
||||
<Td>
|
||||
<Flex alignItems="center" gap={2} justifyContent="space-between">
|
||||
<Text>{cnameDomain}</Text>
|
||||
<IconButton
|
||||
icon={<Icon name="copy" w="14px" />}
|
||||
aria-label="copy"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
onClick={() => copyData(cnameDomain || '')}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
<Link
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/introduction/guide/team_permissions/customDomain')
|
||||
}
|
||||
target={'_blank'}
|
||||
mt="2"
|
||||
ml="2"
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<Icon w={'17px'} h={'17px'} name="book" mr="1" />
|
||||
{t('common:read_doc')}
|
||||
</Flex>
|
||||
</Link>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={onClose} colorScheme="gray" variant="outline" mr={3}>
|
||||
{t('common:Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (type === 'create') {
|
||||
return createDomain({
|
||||
cnameDomain,
|
||||
domain,
|
||||
provider
|
||||
});
|
||||
}
|
||||
return activeDomain(domain);
|
||||
}}
|
||||
colorScheme="blue"
|
||||
isLoading={loading}
|
||||
isDisabled={!DnsResolved}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateCustomDomainModal;
|
||||
|
|
@ -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 (
|
||||
<MyModal isOpen onClose={onClose} title={t('account:custom_domain.domain_verify')} minW="800px">
|
||||
<ModalBody>
|
||||
<Grid gridTemplateColumns="1fr 1fr" gap="16px">
|
||||
<Box>
|
||||
<FormLabel required>{t('account:custom_domain.domain_verify.path')}</FormLabel>
|
||||
<Input {...register('path')} />
|
||||
</Box>
|
||||
<Box>
|
||||
<FormLabel required>{t('account:custom_domain.domain_verify.content')}</FormLabel>
|
||||
<Input {...register('content')} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt="2">{t('account:custom_domain.domain_verify.desc', { domain, path, content })}</Box>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={handleSubmit(() => updateVerifyFile({ domain, path, content }))}
|
||||
isLoading={isUpdating}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
}
|
||||
|
||||
export default domainVerifyModal;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Flex, Box, Button, ModalBody, Input, Link } from '@chakra-ui/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Flex, Box, Button, ModalBody, Input, Link, ModalFooter, Grid } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import type { WecomAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type';
|
||||
|
|
@ -7,12 +7,14 @@ import { useTranslation } from 'next-i18next';
|
|||
import { useForm } from 'react-hook-form';
|
||||
import { createShareChat, updateShareChat } from '@/web/support/outLink/api';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import BasicInfo from '../components/BasicInfo';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import { useMyStep } from '@fastgpt/web/hooks/useStep';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { format } from 'date-fns';
|
||||
import { ShareLinkContainer } from '../components/showShareLinkModal';
|
||||
|
||||
const WecomEditModal = ({
|
||||
appId,
|
||||
|
|
@ -25,7 +27,7 @@ const WecomEditModal = ({
|
|||
appId: string;
|
||||
defaultData: OutLinkEditType<WecomAppType>;
|
||||
onClose: () => void;
|
||||
onCreate: (id: string) => void;
|
||||
onCreate: (shareId: string) => Promise<string | undefined>;
|
||||
onEdit: () => void;
|
||||
isEdit?: boolean;
|
||||
}) => {
|
||||
|
|
@ -38,7 +40,11 @@ const WecomEditModal = ({
|
|||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const { runAsync: onclickCreate, loading: creating } = useRequest2(
|
||||
const {
|
||||
runAsync: onclickCreate,
|
||||
loading: creating,
|
||||
data: createShareId
|
||||
} = useRequest2(
|
||||
(e) =>
|
||||
createShareChat({
|
||||
...e,
|
||||
|
|
@ -48,117 +54,215 @@ const WecomEditModal = ({
|
|||
{
|
||||
errorToast: t('common:create_failed'),
|
||||
successToast: t('common:create_success'),
|
||||
onSuccess: onCreate
|
||||
onSuccess: async (shareId) => {
|
||||
const _id = await onCreate(shareId);
|
||||
if (_id) {
|
||||
setValue('_id', _id);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { runAsync: onclickUpdate, loading: updating } = useRequest2((e) => updateShareChat(e), {
|
||||
const {
|
||||
runAsync: onclickUpdate,
|
||||
loading: updating,
|
||||
data: updatedShareId
|
||||
} = useRequest2((e) => updateShareChat(e), {
|
||||
errorToast: t('common:update_failed'),
|
||||
successToast: t('common:update_success'),
|
||||
onSuccess: onEdit
|
||||
});
|
||||
|
||||
const shareId = useMemo(() => createShareId || updatedShareId, [createShareId, updatedShareId]);
|
||||
|
||||
// 判断是否已经创建成功(有 createShareId 说明已经创建)
|
||||
const isCreated = useMemo(() => !!createShareId, [createShareId]);
|
||||
const isEditMode = useMemo(() => isEdit || isCreated, [isEdit, isCreated]);
|
||||
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { MyStep, activeStep, goToNext, goToPrevious } = useMyStep({
|
||||
steps: [
|
||||
{
|
||||
title: t('publish:wecom.create_modal.step.1')
|
||||
},
|
||||
{
|
||||
title: t('publish:wecom.create_modal.step.2')
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const baseUrl = useMemo(
|
||||
() => feConfigs?.customApiDomain || `${location.origin}/api`,
|
||||
[feConfigs?.customApiDomain]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
iconSrc="core/app/publish/wecom"
|
||||
title={isEdit ? t('publish:wecom.edit_modal_title') : t('publish:wecom.create_modal_title')}
|
||||
title={
|
||||
isEditMode ? t('publish:wecom.edit_modal_title') : t('publish:wecom.create_modal_title')
|
||||
}
|
||||
minW={['auto', '60rem']}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalBody display={'grid'} gridTemplateColumns={['1fr', '1fr 1fr']} fontSize={'14px'} p={0}>
|
||||
<Box p={8} minH={['auto', '400px']} borderRight={'base'}>
|
||||
<BasicInfo register={register} setValue={setValue} defaultData={defaultData} />
|
||||
</Box>
|
||||
<Flex p={8} minH={['auto', '400px']} flexDirection="column" gap={6}>
|
||||
<Flex alignItems="center">
|
||||
<Box color="myGray.600">{t('publish:wecom.api')}</Box>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={feConfigs.openAPIDocUrl || getDocPath('/docs/use-cases/wecom-bot')}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="book" w={'17px'} h={'17px'} mr="1" />
|
||||
{t('common:read_doc')}
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Corp ID
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Corp ID"
|
||||
{...register('app.CorpId', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Agent ID
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Agent ID"
|
||||
{...register('app.AgentId', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Secret
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Secret"
|
||||
{...register('app.SuiteSecret', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
Token
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="Token"
|
||||
{...register('app.CallbackToken', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 6.25rem'} required>
|
||||
AES Key
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="AES Key"
|
||||
{...register('app.CallbackEncodingAesKey', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box flex={1}></Box>
|
||||
|
||||
<Flex justifyContent={'end'}>
|
||||
<Button variant={'whiteBase'} mr={3} onClick={onClose}>
|
||||
{t('common:Close')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={creating || updating}
|
||||
onClick={submitShareChat((data) =>
|
||||
isEdit ? onclickUpdate(data) : onclickCreate(data)
|
||||
)}
|
||||
<ModalBody fontSize={'14px'} p={8}>
|
||||
<MyStep />
|
||||
{activeStep === 0 && (
|
||||
<>
|
||||
<Grid
|
||||
gridTemplateColumns={'200px 1fr'}
|
||||
rowGap="4"
|
||||
mt="4"
|
||||
pb="24px"
|
||||
borderBottom="1px solid"
|
||||
borderColor="myGray.200"
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box color="myGray.900" fontWeight={'500'}>
|
||||
{t('publish:basic_info')}
|
||||
</Box>
|
||||
<Grid gridTemplateColumns={'1fr 1fr'} columnGap="4">
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel required>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={t('publish:publish_name')}
|
||||
maxLength={100}
|
||||
{...register('name', {
|
||||
required: t('common:name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel>
|
||||
QPM
|
||||
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
|
||||
</FormLabel>
|
||||
<Input
|
||||
max={1000}
|
||||
{...register('limit.QPM', {
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: t('publish:qpm_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel>
|
||||
{t('common:support.outlink.Max usage points')}
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.Max usage points tip')}
|
||||
></QuestionTip>
|
||||
</FormLabel>
|
||||
<Input
|
||||
{...register('limit.maxUsagePoints', {
|
||||
min: -1,
|
||||
max: 10000000,
|
||||
valueAsNumber: true,
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel>{t('common:expired_time')}</FormLabel>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
defaultValue={
|
||||
defaultData.limit?.expiredTime
|
||||
? format(defaultData.limit?.expiredTime, 'YYYY-MM-DDTHH:mm')
|
||||
: ''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setValue('limit.expiredTime', new Date(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid gridTemplateColumns={'200px 1fr'} rowGap="4" mt="24px">
|
||||
<Flex h="min">
|
||||
<Box color="myGray.900" fontWeight="500">
|
||||
{t('publish:wecom.api')}
|
||||
</Box>
|
||||
<Flex>
|
||||
{feConfigs?.docUrl && (
|
||||
<Link
|
||||
href={
|
||||
feConfigs.openAPIDocUrl ||
|
||||
getDocPath('/docs/use-cases/external-integration/wecom')
|
||||
}
|
||||
target={'_blank'}
|
||||
ml={2}
|
||||
color={'primary.500'}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
<Flex alignItems={'center'}>
|
||||
<MyIcon name="book" w={'17px'} h={'17px'} mr="1" />
|
||||
{t('common:read_doc')}
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Grid gridTemplateColumns={'1fr 1fr'} columnGap="4">
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel required>Token</FormLabel>
|
||||
<Input
|
||||
placeholder="Token"
|
||||
{...register('app.CallbackToken', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex flexDir={'column'} gap="2">
|
||||
<FormLabel required>AES Key</FormLabel>
|
||||
<Input
|
||||
placeholder="AES Key"
|
||||
{...register('app.CallbackEncodingAesKey', { required: true })}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<Box mt="4">
|
||||
<ShareLinkContainer
|
||||
shareLink={`${baseUrl}/support/outLink/wecom/${shareId}`}
|
||||
img="/imgs/outlink/wecom-copylink-instruction.png"
|
||||
></ShareLinkContainer>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{activeStep === 1 && (
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
mr={3}
|
||||
onClick={() => {
|
||||
goToPrevious();
|
||||
}}
|
||||
>
|
||||
{t('common:last_step')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isLoading={creating || updating}
|
||||
onClick={() => {
|
||||
if (activeStep === 0) {
|
||||
submitShareChat((data) =>
|
||||
(isEditMode ? onclickUpdate(data) : onclickCreate(data)).then(() => goToNext())
|
||||
)();
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import {
|
|||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useDisclosure
|
||||
useDisclosure,
|
||||
Link
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
|
|
@ -19,7 +20,7 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
|
|||
import { defaultOutLinkForm } from '@/web/core/app/constants';
|
||||
import type { WecomAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
|
||||
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import dayjs from 'dayjs';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
|
@ -64,25 +65,35 @@ const Wecom = ({ appId }: { appId: string }) => {
|
|||
<Box fontWeight={'bold'} fontSize={['md', 'lg']}>
|
||||
{t('publish:wecom.title')}
|
||||
</Box>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
colorScheme={'blue'}
|
||||
size={['sm', 'md']}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
|
||||
ml={3}
|
||||
{...(shareChatList.length >= 10
|
||||
? {
|
||||
isDisabled: true,
|
||||
title: t('common:core.app.share.Amount limit tip')
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
setEditWecomData(defaultOutLinkForm as any); // HACK
|
||||
setIsEdit(false);
|
||||
}}
|
||||
>
|
||||
{t('common:add_new')}
|
||||
</Button>
|
||||
<Flex gap={3}>
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={['sm', 'md']}
|
||||
onClick={() => {
|
||||
window.open('/account/customDomain', '_blank');
|
||||
}}
|
||||
>
|
||||
{t('publish:custom_domain_management')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
colorScheme={'blue'}
|
||||
size={['sm', 'md']}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w="1.25rem" color="white" />}
|
||||
{...(shareChatList.length >= 10
|
||||
? {
|
||||
isDisabled: true,
|
||||
title: t('common:core.app.share.Amount limit tip')
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
setEditWecomData(defaultOutLinkForm as any); // HACK
|
||||
setIsEdit(false);
|
||||
}}
|
||||
>
|
||||
{t('common:add_new')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<TableContainer mt={3}>
|
||||
<Table variant={'simple'} w={'100%'} overflowX={'auto'} fontSize={'sm'}>
|
||||
|
|
@ -199,14 +210,35 @@ const Wecom = ({ appId }: { appId: string }) => {
|
|||
<WecomEditModal
|
||||
appId={appId}
|
||||
defaultData={editWecomData}
|
||||
onCreate={() => 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 && (
|
||||
<EmptyTip text={t('common:core.app.share.Not share link')}> </EmptyTip>
|
||||
<EmptyTip
|
||||
text={
|
||||
<Trans
|
||||
i18nKey="app:publish_channel.wecom.empty"
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
color="primary.600"
|
||||
key="link"
|
||||
href="/account/customDomain"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
></EmptyTip>
|
||||
)}
|
||||
<Loading loading={isFetching} fixed={false} />
|
||||
{showShareLinkModalOpen && (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
|
|||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { listCustomDomain } from '@/web/support/customDomain/api';
|
||||
import { useState, useMemo } from 'react';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
|
||||
export type ShowShareLinkModalProps = {
|
||||
shareLink: string;
|
||||
|
|
@ -11,38 +16,126 @@ export type ShowShareLinkModalProps = {
|
|||
img: string;
|
||||
};
|
||||
|
||||
function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) {
|
||||
export const ShareLinkContainer = ({ shareLink, img }: { shareLink: string; img: string }) => {
|
||||
const { copyData } = useCopyData();
|
||||
const { t } = useTranslation();
|
||||
const [customDomain, setCustomDomain] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data: customDomainList = [] } = useRequest2(listCustomDomain, {
|
||||
manual: false
|
||||
});
|
||||
|
||||
// 从 shareLink 中提取原始域名
|
||||
const originalDomain = useMemo(() => {
|
||||
try {
|
||||
const url = new URL(shareLink);
|
||||
return url.origin;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [shareLink]);
|
||||
|
||||
// 计算显示的分享链接(使用自定义域名替换原始域名)
|
||||
const displayShareLink = useMemo(() => {
|
||||
if (!customDomain || !originalDomain) {
|
||||
return shareLink;
|
||||
}
|
||||
return shareLink.replace(originalDomain, `https://${customDomain}`);
|
||||
}, [shareLink, customDomain, originalDomain]);
|
||||
|
||||
// 处理域名选择选项
|
||||
const domainOptions = useMemo(() => {
|
||||
const options = [
|
||||
{
|
||||
label: t('publish:use_default_domain') || '使用默认域名',
|
||||
value: ''
|
||||
}
|
||||
];
|
||||
|
||||
// 只显示已激活的自定义域名
|
||||
const activeDomains = customDomainList
|
||||
.filter((item) => item.status === 'active')
|
||||
.map((item) => ({
|
||||
label: item.domain,
|
||||
value: item.domain
|
||||
}));
|
||||
|
||||
return [...options, ...activeDomains];
|
||||
}, [customDomainList, t]);
|
||||
return (
|
||||
<>
|
||||
{/* 自定义域名选择器 */}
|
||||
{domainOptions.length > 1 && (
|
||||
<Box mb={4}>
|
||||
<MySelect
|
||||
value={customDomain || ''}
|
||||
list={domainOptions}
|
||||
onChange={(value) => setCustomDomain(value || undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border="base"
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('publish:copy_link_hint')}</Box>
|
||||
<MyIcon
|
||||
name={'copy'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() => copyData(displayShareLink)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
|
||||
{displayShareLink}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
|
||||
<MyImage src={img} borderRadius="0.5rem" alt="" />
|
||||
</Box>
|
||||
|
||||
{/*<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'} mt="4">
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border="base"
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex="1">{t('publish:ip_whitelist')}</Box>
|
||||
<MyIcon
|
||||
name={'copy'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() => copyData(feConfigs?.ip_whitelist || '')}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box p={3} wordBreak={'break-all'}>
|
||||
{feConfigs.ip_whitelist}
|
||||
</Box>
|
||||
</Box>*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function ShowShareLinkModal({ shareLink, onClose, img }: ShowShareLinkModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MyModal onClose={onClose} title={t('publish:show_share_link_modal_title')}>
|
||||
<ModalBody>
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border="base"
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('publish:copy_link_hint')}</Box>
|
||||
<MyIcon
|
||||
name={'copy'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
onClick={() => copyData(shareLink)}
|
||||
/>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre'} p={3} overflowX={'auto'}>
|
||||
{shareLink}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box mt="4" borderRadius="0.5rem" border="1px" borderStyle="solid" borderColor="myGray.200">
|
||||
<MyImage src={img} borderRadius="0.5rem" alt="" />
|
||||
</Box>
|
||||
<ShareLinkContainer shareLink={shareLink} img={img} />
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
import AccountContainer from '@/pageComponents/account/AccountContainer';
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import { deleteCustomDomain, listCustomDomain } from '@/web/support/customDomain/api';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Thead,
|
||||
Tr,
|
||||
useDisclosure
|
||||
} from '@chakra-ui/react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { providerMap, customDomainStatusMap } from '@/web/support/customDomain/const';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import MyLoading from '@fastgpt/web/components/common/MyLoading';
|
||||
import type { CustomDomainType } from '@fastgpt/global/support/customDomain/type';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants';
|
||||
import { useRouter } from 'next/router';
|
||||
import Tag from '@fastgpt/web/components/common/Tag';
|
||||
|
||||
const CreateCustomDomainModal = dynamic(
|
||||
() => import('@/pageComponents/account/customDomain/createModal')
|
||||
);
|
||||
|
||||
/** unimplemented */
|
||||
// const DomainVerifyModal = dynamic(
|
||||
// () => import('@/pageComponents/account/customDomain/domainVerifyModal')
|
||||
// );
|
||||
|
||||
const CustomDomain = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { teamPlanStatus } = useUserStore();
|
||||
|
||||
const {
|
||||
data: customDomainList,
|
||||
refreshAsync: refreshCustomDomainList,
|
||||
loading: loadingCustomDomainList
|
||||
} = useRequest2(listCustomDomain, {
|
||||
manual: false
|
||||
});
|
||||
const {
|
||||
isOpen: isOpenCreateModal,
|
||||
onOpen: onOpenCreateModal,
|
||||
onClose: onCloseCreateModal
|
||||
} = useDisclosure();
|
||||
|
||||
// const {
|
||||
// isOpen: isOpenDomainVerify,
|
||||
// onOpen: onOpenDomainVerify,
|
||||
// onClose: onCloseDomainVerify
|
||||
// } = useDisclosure();
|
||||
|
||||
const { runAsync: onDelete, loading: loadingDelete } = useRequest2(deleteCustomDomain, {
|
||||
manual: true,
|
||||
successToast: t('common:Success'),
|
||||
onSuccess: () => refreshCustomDomainList()
|
||||
});
|
||||
|
||||
const { ConfirmModal, openConfirm } = useConfirm({
|
||||
content: t('account:custom_domain.delete_confirm')
|
||||
});
|
||||
|
||||
const [editDomain, setEditDomain] = useState<CustomDomainType | undefined>(undefined);
|
||||
|
||||
// 检查用户是否有 advanced 套餐
|
||||
const isAdvancedPlan = useMemo(() => {
|
||||
const currentLevel = teamPlanStatus?.standard?.currentSubLevel;
|
||||
if (!currentLevel) return false;
|
||||
|
||||
return currentLevel === StandardSubLevelEnum.advanced;
|
||||
}, [teamPlanStatus?.standard?.currentSubLevel]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountContainer>
|
||||
<Flex flexDirection="column" h="100%" padding="24px">
|
||||
<TableContainer flex="1" display="flex" flexDirection="column">
|
||||
{loadingCustomDomainList ? <MyLoading /> : null}
|
||||
<Flex justifyContent="space-between" alignItems="center" w="100%">
|
||||
<Box fontSize="20px" fontWeight="500">
|
||||
{t('account:custom_domain')}
|
||||
{customDomainList?.length ? (
|
||||
`: (${customDomainList.length}/${teamPlanStatus?.standardConstants?.customDomain})`
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="whitePrimaryOutline"
|
||||
onClick={onOpenCreateModal}
|
||||
isDisabled={!isAdvancedPlan}
|
||||
>
|
||||
{t('common:Add')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Table marginTop="12px">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Td>{t('account:custom_domain.domain')}</Td>
|
||||
<Td>CNAME</Td>
|
||||
<Td>{t('account:custom_domain.provider')}</Td>
|
||||
<Td>{t('common:Status')}</Td>
|
||||
<Td>{t('common:Action')}</Td>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{customDomainList?.length ? (
|
||||
customDomainList.map((customDomain) => (
|
||||
<Tr key={customDomain.domain}>
|
||||
<Td>{customDomain.domain}</Td>
|
||||
<Td>{customDomain.cnameDomain}</Td>
|
||||
<Td>{t(providerMap[customDomain.provider])}</Td>
|
||||
<Td>
|
||||
{customDomain.status === 'active' ? (
|
||||
<Tag colorSchema="green">
|
||||
{t(customDomainStatusMap[customDomain.status])}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag colorSchema="red">
|
||||
{t(customDomainStatusMap[customDomain.status])}
|
||||
</Tag>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Flex gap="2">
|
||||
<Button
|
||||
variant="whiteDanger"
|
||||
onClick={() => {
|
||||
return openConfirm(() => onDelete(customDomain.domain))();
|
||||
}}
|
||||
>
|
||||
{t('common:Delete')}
|
||||
</Button>
|
||||
{customDomain.status === 'inactive' ? (
|
||||
<Button
|
||||
variant="whitePrimary"
|
||||
onClick={() => {
|
||||
setEditDomain(customDomain);
|
||||
onOpenCreateModal();
|
||||
}}
|
||||
>
|
||||
{t('common:Edit')}
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
// <Button
|
||||
// variant="whitePrimary"
|
||||
// onClick={() => {
|
||||
// setEditDomain(customDomain);
|
||||
// onOpenDomainVerify();
|
||||
// }}
|
||||
// >
|
||||
// {t('account:custom_domain.domain_verify')}
|
||||
// </Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
))
|
||||
) : (
|
||||
<Tr h="100%">
|
||||
<Td colSpan={5} textAlign="center" h="100%">
|
||||
<Flex
|
||||
h="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
minH="400px"
|
||||
flexDirection="column"
|
||||
gap={4}
|
||||
>
|
||||
<EmptyTip
|
||||
text={
|
||||
!isAdvancedPlan && (
|
||||
<Flex flexDir="column" alignItems="center">
|
||||
<Box>{t('account:upgrade_to_use_custom_domain')}</Box>
|
||||
<Button
|
||||
mt="4"
|
||||
variant="primary"
|
||||
onClick={() => router.push('/price')}
|
||||
size="md"
|
||||
>
|
||||
{t('account:upgrade_plan')}
|
||||
</Button>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Flex>
|
||||
</AccountContainer>
|
||||
<ConfirmModal />
|
||||
{isOpenCreateModal && (
|
||||
<CreateCustomDomainModal
|
||||
onClose={() => {
|
||||
onCloseCreateModal();
|
||||
refreshCustomDomainList();
|
||||
setEditDomain(undefined);
|
||||
}}
|
||||
type={editDomain ? 'refresh' : 'create'}
|
||||
data={editDomain!}
|
||||
/>
|
||||
)}
|
||||
{/*{isOpenDomainVerify && editDomain?.domain && (
|
||||
<DomainVerifyModal
|
||||
domain={editDomain?.domain}
|
||||
onClose={() => {
|
||||
onCloseDomainVerify();
|
||||
setEditDomain(undefined);
|
||||
}}
|
||||
/>
|
||||
)}*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomDomain;
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['account']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ export type OutLinkUpdateQuery = {};
|
|||
// }
|
||||
export type OutLinkUpdateBody = OutLinkEditType;
|
||||
|
||||
export type OutLinkUpdateResponse = {};
|
||||
export type OutLinkUpdateResponse = string;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<OutLinkUpdateBody, OutLinkUpdateQuery>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<CustomDomainType[]>('/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
|
||||
);
|
||||
|
|
@ -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<ProviderEnum, Parameters<typeof t>[0]>;
|
||||
|
||||
export const customDomainStatusMap = {
|
||||
active: 'account:custom_domain.status.active',
|
||||
inactive: 'account:custom_domain.status.inactive'
|
||||
} satisfies Record<CustomDomainStatusEnum, Parameters<typeof t>[0]>;
|
||||