From b22ba1aa5314d2eaacb767227495420f00587736 Mon Sep 17 00:00:00 2001 From: Finley Ge <32237950+FinleyGe@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:28:03 +0800 Subject: [PATCH] chore: customDomain openapi doc && new nextapi code snippets (#6082) * perf: faq * index * delete dataset * delete dataset * perf: delete dataset * init * fix: faq * refresh * empty tip * chore: customDomain openapi doc && new nextapi code snippets * chore: update doc * remove ivalid code * snippets --------- Co-authored-by: archer <545436317@qq.com> --- .vscode/nextapi.code-snippets | 50 +++--- document/content/docs/upgrading/4-14/4144.mdx | 4 + document/data/doc-last-modified.json | 10 +- packages/global/openapi/index.ts | 8 +- .../openapi/support/customDomain/api.ts | 84 ++++++++++ .../openapi/support/customDomain/index.ts | 153 ++++++++++++++++++ packages/global/openapi/tag.ts | 1 + packages/service/common/middle/entry.ts | 13 ++ 8 files changed, 291 insertions(+), 32 deletions(-) create mode 100644 packages/global/openapi/support/customDomain/api.ts create mode 100644 packages/global/openapi/support/customDomain/index.ts diff --git a/.vscode/nextapi.code-snippets b/.vscode/nextapi.code-snippets index 701732b0e..a27b15b56 100644 --- a/.vscode/nextapi.code-snippets +++ b/.vscode/nextapi.code-snippets @@ -1,35 +1,33 @@ { - // Place your FastGPT 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and - // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope - // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is - // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: - // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Place your FastGPT 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // Placeholders with the same ids are connected. // Example: "Next api template": { "scope": "javascript,typescript", "prefix": "nextapi", "body": [ - "import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';", - "import { NextAPI } from '@/service/middleware/entry';", - "", - "export type ${TM_FILENAME_BASE}Query = {};", - "", - "export type ${TM_FILENAME_BASE}Body = {};", - "", - "export type ${TM_FILENAME_BASE}Response = {};", - "", - "async function handler(", - " req: ApiRequestProps<${TM_FILENAME_BASE}Body, ${TM_FILENAME_BASE}Query>,", - " res: ApiResponseType", - "): Promise<${TM_FILENAME_BASE}Response> {", - " $1", - " return {}", - "}", - "", - "export default NextAPI(handler);" + "import { NextAPI } from '@/service/middleware/entry';", + "import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';", + "", + "async function handler(", + " req: ApiRequestProps,", + " res: ApiResponseType<${1}ResponseType>", + "): Promise<${1}ResponseType> {", + " const body = ${1}BodySchema.parse(req.body);", + " const query = ${1}QuerySchema.parse(req.query);", + "", + " ${2}", + "", + " return {};", + "}", + "", + "export default NextAPI(handler);" ], - "description": "FastGPT Next API template" + "description": "FastGPT Next API template with Zod validation" }, "use context template": { "scope": "typescriptreact", @@ -38,7 +36,7 @@ "import React, { ReactNode } from 'react';", "import { createContext } from 'use-context-selector';", "", - "type ContextType = {$1};", + "type ContextType = {${1}};", "", "export const Context = createContext({});", "", @@ -65,4 +63,4 @@ "});" ] } -} \ No newline at end of file +} diff --git a/document/content/docs/upgrading/4-14/4144.mdx b/document/content/docs/upgrading/4-14/4144.mdx index 30e64b69d..e4d5f0da5 100644 --- a/document/content/docs/upgrading/4-14/4144.mdx +++ b/document/content/docs/upgrading/4-14/4144.mdx @@ -68,3 +68,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \ 1. 新增 GLM4.6 与 DS3.2 系列模型预设。 2. 修复 MinerU SaaS 插件模型版本不能选择 vlm 的问题 +3. 修复微信公众号插件批量上传 markdown 参数传递问题 +4. 新增获取微信公众号草稿箱列表工具 +5. markdown 转文件支持自定义文件名 +6. 修复 import cache 导致的插件无法被更新的问题 diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index b462d5834..5e22ad811 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -2,7 +2,7 @@ "document/content/docs/faq/app.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/chat.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/dataset.mdx": "2025-08-02T19:38:37+08:00", - "document/content/docs/faq/error.mdx": "2025-12-10T13:24:24+08:00", + "document/content/docs/faq/error.mdx": "2025-12-10T20:07:05+08:00", "document/content/docs/faq/external_channel_integration.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/index.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/faq/other.mdx": "2025-08-04T22:07:52+08:00", @@ -89,7 +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-10T13:24:24+08:00", + "document/content/docs/introduction/guide/team_permissions/customDomain.mdx": "2025-12-10T20:07: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", @@ -119,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-10T13:28:04+08:00", + "document/content/docs/upgrading/4-14/4144.mdx": "2025-12-10T20:07:05+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", @@ -191,7 +191,7 @@ "document/content/docs/use-cases/app-cases/feishu_webhook.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/fixingEvidence.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/google_search.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2025-12-10T13:24:24+08:00", + "document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2025-12-10T20:07:05+08:00", "document/content/docs/use-cases/app-cases/multi_turn_translation_bot.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/submit_application_template.mdx": "2025-08-05T23:20:39+08:00", "document/content/docs/use-cases/app-cases/translate-subtitle-using-gpt.mdx": "2025-07-23T21:35:03+08:00", @@ -199,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-12-10T13:24:24+08:00", + "document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-10T20:07:05+08:00", "document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00" } \ No newline at end of file diff --git a/packages/global/openapi/index.ts b/packages/global/openapi/index.ts index 770332266..66b337f07 100644 --- a/packages/global/openapi/index.ts +++ b/packages/global/openapi/index.ts @@ -4,6 +4,7 @@ import { ApiKeyPath } from './support/openapi'; import { TagsMap } from './tag'; import { PluginPath } from './core/plugin'; import { WalletPath } from './support/wallet'; +import { CustomDomainPath } from './support/customDomain'; export const openAPIDocument = createDocument({ openapi: '3.1.0', @@ -16,7 +17,8 @@ export const openAPIDocument = createDocument({ ...ChatPath, ...ApiKeyPath, ...PluginPath, - ...WalletPath + ...WalletPath, + ...CustomDomainPath }, servers: [{ url: '/api' }], 'x-tagGroups': [ @@ -39,6 +41,10 @@ export const openAPIDocument = createDocument({ { name: '支付', tags: [TagsMap.walletBill, TagsMap.walletDiscountCoupon] + }, + { + name: '自定义域名', + tags: [TagsMap.customDomain] } ] }); diff --git a/packages/global/openapi/support/customDomain/api.ts b/packages/global/openapi/support/customDomain/api.ts new file mode 100644 index 000000000..42fc37946 --- /dev/null +++ b/packages/global/openapi/support/customDomain/api.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { CustomDomainType, ProviderEnum } from '../../../support/customDomain/type'; + +// Create custom domain +export const CreateCustomDomainBodySchema = z.object({ + domain: z.string().meta({ example: 'chat.example.com', description: '自定义域名' }), + provider: ProviderEnum.meta({ + example: 'aliyun', + description: 'DNS 提供商:aliyun, tencent, volcengine' + }), + cnameDomain: z.string().meta({ example: 'lb.example.com', description: 'CNAME 目标域名' }) +}); +export type CreateCustomDomainBodyType = z.infer; + +export const CreateCustomDomainResponseSchema = z.object({ + success: z.boolean().meta({ example: true, description: '创建是否成功' }) +}); +export type CreateCustomDomainResponseType = z.infer; + +// List custom domains +export const CustomDomainListResponseSchema = z.array( + CustomDomainType.extend({ + _id: z + .string() + .optional() + .meta({ example: '68ad85a7463006c963799a05', description: '域名记录 ID' }) + }) +); +export type CustomDomainListResponseType = z.infer; + +// Delete custom domain +export const DeleteCustomDomainQuerySchema = z.object({ + domain: z.string().meta({ example: 'chat.example.com', description: '要删除的域名' }) +}); +export type DeleteCustomDomainQueryType = z.infer; + +export const DeleteCustomDomainResponseSchema = z.object({ + success: z.boolean().meta({ example: true, description: '删除是否成功' }) +}); +export type DeleteCustomDomainResponseType = z.infer; + +// Check DNS resolve +export const CheckDNSResolveBodySchema = z.object({ + domain: z.string().meta({ example: 'chat.example.com', description: '要检查的域名' }), + cnameDomain: z.string().meta({ example: 'lb.example.com', description: 'CNAME 目标域名' }) +}); +export type CheckDNSResolveBodyType = z.infer; + +export const CheckDNSResolveResponseSchema = z.object({ + success: z.boolean().meta({ example: true, description: 'DNS 解析是否成功' }), + message: z + .string() + .optional() + .meta({ example: 'CNAME record not resolved', description: '错误信息' }) +}); +export type CheckDNSResolveResponseType = z.infer; + +// Active custom domain +export const ActiveCustomDomainBodySchema = z.object({ + domain: z.string().meta({ example: 'chat.example.com', description: '要激活的域名' }) +}); +export type ActiveCustomDomainBodyType = z.infer; + +export const ActiveCustomDomainResponseSchema = z.object({ + success: z.boolean().meta({ example: true, description: '激活是否成功' }) +}); +export type ActiveCustomDomainResponseType = z.infer; + +// Update domain verify file +export const UpdateDomainVerifyFileBodySchema = z.object({ + domain: z.string().meta({ example: 'chat.example.com', description: '域名' }), + path: z + .string() + .meta({ example: '/.well-known/pki-validation/fileauth.txt', description: '验证文件路径' }), + content: z.string().meta({ example: '202312121234567890abcdef', description: '验证文件内容' }) +}); +export type UpdateDomainVerifyFileBodyType = z.infer; + +export const UpdateDomainVerifyFileResponseSchema = z.object({ + success: z.boolean().meta({ example: true, description: '更新是否成功' }) +}); +export type UpdateDomainVerifyFileResponseType = z.infer< + typeof UpdateDomainVerifyFileResponseSchema +>; diff --git a/packages/global/openapi/support/customDomain/index.ts b/packages/global/openapi/support/customDomain/index.ts new file mode 100644 index 000000000..2652ecb44 --- /dev/null +++ b/packages/global/openapi/support/customDomain/index.ts @@ -0,0 +1,153 @@ +import type { OpenAPIPath } from '../../type'; +import { + CreateCustomDomainBodySchema, + CreateCustomDomainResponseSchema, + CustomDomainListResponseSchema, + DeleteCustomDomainQuerySchema, + DeleteCustomDomainResponseSchema, + CheckDNSResolveBodySchema, + CheckDNSResolveResponseSchema, + ActiveCustomDomainBodySchema, + ActiveCustomDomainResponseSchema, + UpdateDomainVerifyFileBodySchema, + UpdateDomainVerifyFileResponseSchema +} from './api'; +import { TagsMap } from '../../tag'; + +export const CustomDomainPath: OpenAPIPath = { + '/proApi/support/customDomain/create': { + post: { + summary: '创建自定义域名', + description: + '创建一个新的自定义域名配置,需要高级套餐权限。创建后域名会自动部署到 K8s 集群中', + tags: [TagsMap.customDomain], + requestBody: { + content: { + 'application/json': { + schema: CreateCustomDomainBodySchema + } + } + }, + responses: { + 200: { + description: '成功创建自定义域名', + content: { + 'application/json': { + schema: CreateCustomDomainResponseSchema + } + } + } + } + } + }, + '/proApi/support/customDomain/list': { + get: { + summary: '获取自定义域名列表', + description: '获取当前团队的所有自定义域名配置列表', + tags: [TagsMap.customDomain], + responses: { + 200: { + description: '成功获取自定义域名列表', + content: { + 'application/json': { + schema: CustomDomainListResponseSchema + } + } + } + } + } + }, + '/proApi/support/customDomain/delete': { + delete: { + summary: '删除自定义域名', + description: '删除指定的自定义域名配置,同时会从 K8s 集群中移除相关资源', + tags: [TagsMap.customDomain], + requestParams: { + query: DeleteCustomDomainQuerySchema + }, + responses: { + 200: { + description: '成功删除自定义域名', + content: { + 'application/json': { + schema: DeleteCustomDomainResponseSchema + } + } + } + } + } + }, + '/proApi/support/customDomain/checkDNSResolve': { + post: { + summary: '检查 DNS 解析', + description: '检查自定义域名的 CNAME 记录是否正确配置和解析', + tags: [TagsMap.customDomain], + requestBody: { + content: { + 'application/json': { + schema: CheckDNSResolveBodySchema + } + } + }, + responses: { + 200: { + description: 'DNS 解析检查结果', + content: { + 'application/json': { + schema: CheckDNSResolveResponseSchema + } + } + } + } + } + }, + '/proApi/support/customDomain/active': { + post: { + summary: '激活自定义域名', + description: '将自定义域名状态设置为激活,并重新部署到 K8s 集群', + tags: [TagsMap.customDomain], + requestBody: { + content: { + 'application/json': { + schema: ActiveCustomDomainBodySchema + } + } + }, + responses: { + 200: { + description: '成功激活自定义域名', + content: { + 'application/json': { + schema: ActiveCustomDomainResponseSchema + } + } + } + } + } + }, + '/proApi/support/customDomain/updateVerifyFile': { + post: { + summary: '更新域名验证文件', + description: + '更新域名验证文件配置,用于 SSL 证书验证。更新后会在 K8s 中创建或更新对应的 Ingress', + tags: [TagsMap.customDomain], + requestBody: { + content: { + 'application/json': { + schema: UpdateDomainVerifyFileBodySchema + } + } + }, + responses: { + 200: { + description: '成功更新域名验证文件', + content: { + 'application/json': { + schema: UpdateDomainVerifyFileResponseSchema + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/tag.ts b/packages/global/openapi/tag.ts index 12f153b21..708afe242 100644 --- a/packages/global/openapi/tag.ts +++ b/packages/global/openapi/tag.ts @@ -9,6 +9,7 @@ export const TagsMap = { apiKey: 'APIKey', walletBill: '订单', walletDiscountCoupon: '优惠券', + customDomain: '自定义域名', adminDashboard: '管理员仪表盘' }; diff --git a/packages/service/common/middle/entry.ts b/packages/service/common/middle/entry.ts index e2bbb4c53..09a26342e 100644 --- a/packages/service/common/middle/entry.ts +++ b/packages/service/common/middle/entry.ts @@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { withNextCors } from './cors'; import { type ApiRequestProps } from '../../type/next'; import { addLog } from '../system/log'; +import { ZodError } from 'zod'; export type NextApiHandler = ( req: ApiRequestProps, @@ -49,6 +50,18 @@ export const NextEntry = ({ }); } } catch (error) { + // Handle Zod validation errors + if (error instanceof ZodError) { + return jsonRes(res, { + code: 400, + error: { + message: 'Validation error', + details: error.message + }, + url: req.url + }); + } + return jsonRes(res, { code: 500, error,