From 5ca06c2928b43570447f9f429f62c650bb317ec1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Sep 2026 16:44:16 +0800 Subject: [PATCH 01/10] fix: improve OpenAPI schemas --- packages/global/common/zod/openapi.ts | 17 ++++ .../core/ai/auxiliaryGeneration/type.ts | 9 +- packages/global/core/ai/llm/type.ts | 11 ++- .../global/core/dataset/workflowTagFilter.ts | 6 +- packages/global/core/workflow/runtime/type.ts | 99 +++++++++++++++---- .../template/system/interactive/type.ts | 43 ++++++-- packages/global/core/workflow/type/io.ts | 41 +++++--- .../openapi/admin/app/templateType/api.ts | 7 ++ packages/global/openapi/admin/dataset/api.ts | 2 + .../openapi/admin/system/inform/index.ts | 28 +----- .../global/openapi/admin/system/model/api.ts | 41 ++++---- packages/global/openapi/admin/user/api.ts | 4 + packages/global/openapi/admin/user/index.ts | 14 +-- .../admin/wallet/bill/invoice/index.ts | 7 +- .../global/openapi/admin/wallet/plan/api.ts | 2 + .../global/openapi/admin/wallet/plan/index.ts | 14 +-- packages/global/openapi/api.ts | 11 ++- packages/global/openapi/common/other/api.ts | 2 +- packages/global/openapi/core/ai/api.ts | 1 + .../global/openapi/core/app/common/api.ts | 10 +- .../global/openapi/core/app/evaluation/api.ts | 2 +- .../global/openapi/core/app/httpTools/api.ts | 12 ++- .../global/openapi/core/app/mcpTools/api.ts | 4 +- .../global/openapi/core/app/template/api.ts | 26 +++-- .../openapi/core/chat/completion/api.ts | 16 ++- .../global/openapi/core/chat/record/api.ts | 6 +- .../core/dataset/collection/createApi.ts | 8 +- .../openapi/core/dataset/synonym/api.ts | 4 +- .../global/openapi/core/plugin/admin/api.ts | 3 +- .../global/openapi/core/plugin/debug/api.ts | 3 + .../openapi/core/plugin/marketplace/api.ts | 2 +- .../openapi/core/plugin/team/pkg/api.ts | 3 +- packages/global/openapi/plugin/invoke.ts | 16 +-- .../global/openapi/support/outLink/api.ts | 13 ++- .../openapi/support/user/account/login/api.ts | 10 +- .../support/wallet/bill/invoice/api.ts | 7 +- .../global/support/wallet/sub/coupon/type.ts | 2 +- .../test/openapi/admin/settings/model.test.ts | 43 +++++++- packages/global/test/openapi/api.test.ts | 18 ++++ packages/global/test/openapi/core/app.test.ts | 6 +- projects/app/src/pages/api/invoke/userInfo.ts | 16 +-- .../test/pages/api/core/ai/model/test.test.ts | 9 +- .../pages/api/core/ai/model/update.test.ts | 12 ++- 43 files changed, 414 insertions(+), 196 deletions(-) create mode 100644 packages/global/test/openapi/api.test.ts diff --git a/packages/global/common/zod/openapi.ts b/packages/global/common/zod/openapi.ts index f340fe392e2c..19aa43749f6e 100644 --- a/packages/global/common/zod/openapi.ts +++ b/packages/global/common/zod/openapi.ts @@ -1,5 +1,22 @@ import type { ZodOpenApiMetadata } from 'zod-openapi'; +/** + * 任意 JSON 值。运行时保持宽松(z.any()),文档里声明 JSON 的六种取值, + * 避免客户端把工作流变量这类动态字段渲染成 null/any。 + */ +export const JsonValueOpenApiMeta = { + type: ['object', 'array', 'string', 'number', 'boolean', 'null'] +} satisfies Pick; + +/** + * 结构随渠道/插件/模板类型变化、但一定是对象:运行时保持宽松,文档里声明为开放对象, + * 避免客户端拿到 null/any。 + */ +export const OpenObjectOpenApiMeta = { + type: 'object', + additionalProperties: true +} satisfies Pick; + /** 显式导出 refine 中的字段组合约束;顺序也是文档默认示例的选择顺序,不改变运行时校验。 */ export const requiredAlternatives = ( branches: string[][], diff --git a/packages/global/core/ai/auxiliaryGeneration/type.ts b/packages/global/core/ai/auxiliaryGeneration/type.ts index a7793cf31811..f375a2a432ac 100644 --- a/packages/global/core/ai/auxiliaryGeneration/type.ts +++ b/packages/global/core/ai/auxiliaryGeneration/type.ts @@ -5,6 +5,7 @@ import { SelectedAgentSkillItemTypeSchema } from '../../app/formEdit/type'; import { ObjectIdSchema } from '../../../common/type/mongo'; import { ChatAgentHelperTypeEnum } from './constants'; import { BoolSchema } from '../../../common/zod'; +import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; export const AuxiliaryGenerationChatFileSchema = z.object({ type: z.enum(ChatFileTypeEnum), @@ -72,7 +73,13 @@ export const ChatAgentHelperCompletionsParamsSchema = z appId: ObjectIdSchema, messages: z.array(ChatCompletionMessageParamSchema), // ChatBox 当前仍复用 workflow interactive 数据结构;辅助生成 API 只透传给 chat round 准备逻辑。 - interactive: z.any().optional(), + interactive: z + .any() + .optional() + .meta({ + ...OpenObjectOpenApiMeta, + description: '交互式响应,直接透传给 chat round 准备逻辑' + }), metadata: z.object({ type: z.literal(ChatAgentHelperTypeEnum.chatAgent), data: ChatAgentHelperMetadataSchema diff --git a/packages/global/core/ai/llm/type.ts b/packages/global/core/ai/llm/type.ts index 1dbec5835248..7596462acbef 100644 --- a/packages/global/core/ai/llm/type.ts +++ b/packages/global/core/ai/llm/type.ts @@ -2,6 +2,7 @@ import type openai from 'openai'; import type { Stream } from 'openai/streaming'; import { audioFileType } from '../../../common/file/constants'; import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; /* 通用类型 */ export const ChatCompletionContentPartTextSchema = z.object({ @@ -184,9 +185,13 @@ export const ChatCompletionAssistantMessageParamSchema = z.object({ }), // FastGPT 自定义扩展。为避免与 workflow/interactive 形成循环依赖,此处用 z.any() 占位, // 真实类型见 packages/global/core/workflow/template/system/interactive/type.ts:WorkflowInteractiveResponseType - interactive: z.any().optional().meta({ - description: '交互式响应(FastGPT 自定义扩展)' - }), + interactive: z + .any() + .optional() + .meta({ + ...OpenObjectOpenApiMeta, + description: '交互式响应(FastGPT 自定义扩展)' + }), // 下面的几个,目前系统没用到 audio: z.object({ id: z.string() }).nullish(), function_call: ChatCompletionMessageToolCallFunctionSchema.nullish().meta({ diff --git a/packages/global/core/dataset/workflowTagFilter.ts b/packages/global/core/dataset/workflowTagFilter.ts index bd0d511f7f0f..381e5a7bc354 100644 --- a/packages/global/core/dataset/workflowTagFilter.ts +++ b/packages/global/core/dataset/workflowTagFilter.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { JsonValueOpenApiMeta } from '../../common/zod/openapi'; import { formatTime2YMDHM } from '../../common/string/time'; import { DatasetCollectionTagTypeEnum } from './constants'; import type { DatasetCollectionTagType, DatasetTagType } from './type'; @@ -78,7 +79,10 @@ export const DatasetTagFilterConditionSchema = z.object({ tagType: WorkflowTagFilterTagTypeSchema.optional(), op: z.string().optional(), valueMode: z.enum(DatasetTagFilterValueModeEnum).optional(), - value: z.unknown().optional() + value: z + .unknown() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '筛选值' }) }); export type DatasetTagFilterCondition = z.infer; diff --git a/packages/global/core/workflow/runtime/type.ts b/packages/global/core/workflow/runtime/type.ts index 5f6f1f8f1e53..1a53e5cb6c41 100644 --- a/packages/global/core/workflow/runtime/type.ts +++ b/packages/global/core/workflow/runtime/type.ts @@ -8,6 +8,7 @@ import { SearchDataResponseQuoteListItemSchema } from '../../dataset/type'; import { DatasetSearchModeEnum } from '../../dataset/constants'; import { ChatRoleEnum } from '../../chat/constants'; import z from 'zod'; +import { JsonValueOpenApiMeta } from '../../../common/zod/openapi'; import type { JSONSchemaInputType } from '../../app/jsonschema'; const AgentPlanNodeStatusSchema = z.enum(['set_plan', 'update_plan', 'ask_question']); @@ -153,12 +154,21 @@ export const DispatchNodeResponseSchema = z // Tool toolInput: z.record(z.string(), z.any()).optional().meta({ description: '工具输入' }), pluginOutput: z.record(z.string(), z.any()).optional().meta({ description: '插件输出' }), - pluginDetail: z.array(z.any()).optional(), + pluginDetail: z + .array(z.any()) + .optional() + .meta({ + items: { type: 'object', additionalProperties: true }, + description: '插件执行详情(递归节点响应)' + }), toolParamsResult: z .record(z.string(), z.any()) .optional() .meta({ description: '工具参数结果' }), - toolRes: z.any().optional().meta({ description: '工具响应' }), + toolRes: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '工具响应' }), // if-else ifElseResult: z.string().optional().meta({ description: '判断器结果' }), @@ -166,7 +176,13 @@ export const DispatchNodeResponseSchema = z // tool call toolCallInputTokens: z.number().optional().meta({ description: '工具调用输入 token' }), toolCallOutputTokens: z.number().optional().meta({ description: '工具调用输出 token' }), - toolDetail: z.array(z.any()).optional(), + toolDetail: z + .array(z.any()) + .optional() + .meta({ + items: { type: 'object', additionalProperties: true }, + description: '工具执行详情(递归节点响应)' + }), toolStop: z.boolean().optional(), // Agent call @@ -195,39 +211,86 @@ export const DispatchNodeResponseSchema = z formInputResult: z.record(z.string(), z.any()).optional().meta({ description: '表单输入结果' }), // update var - updateVarResult: z.array(z.any()).optional().meta({ description: '更新变量结果' }), + updateVarResult: z + .array(z.any()) + .optional() + .meta({ items: JsonValueOpenApiMeta, description: '更新变量结果' }), // loop - loopResult: z.array(z.any()).optional().meta({ description: '循环结果' }), - loopInput: z.array(z.any()).optional().meta({ description: '循环输入' }), - loopDetail: z.array(z.any()).optional().meta({ description: '循环详情' }), - loopInputValue: z.any().optional().meta({ description: '循环输入值' }), - loopOutputValue: z.any().optional().meta({ description: '循环输出值' }), + loopResult: z + .array(z.any()) + .optional() + .meta({ items: JsonValueOpenApiMeta, description: '循环结果' }), + loopInput: z + .array(z.any()) + .optional() + .meta({ items: JsonValueOpenApiMeta, description: '循环输入' }), + loopDetail: z + .array(z.any()) + .optional() + .meta({ + items: { type: 'object', additionalProperties: true }, + description: '循环详情(递归节点响应)' + }), + loopInputValue: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '循环输入值' }), + loopOutputValue: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '循环输出值' }), // parallel run - parallelInput: z.array(z.any()).optional().meta({ description: '并行输入' }), - parallelResult: z.array(z.any()).optional().meta({ description: '并行结果' }), + parallelInput: z + .array(z.any()) + .optional() + .meta({ items: JsonValueOpenApiMeta, description: '并行输入' }), + parallelResult: z + .array(z.any()) + .optional() + .meta({ items: JsonValueOpenApiMeta, description: '并行结果' }), parallelRunDetail: z .array(z.any()) .optional() - .meta({ description: '各任务执行摘要(成功/失败状态)' }), + .meta({ + items: { type: 'object', additionalProperties: true }, + description: '各任务执行摘要(成功/失败状态)' + }), parallelDetail: z .array(z.any()) .optional() - .meta({ description: '成功任务子工作流完整响应列表', deprecated: true }), + .meta({ + items: { type: 'object', additionalProperties: true }, + description: '成功任务子工作流完整响应列表', + deprecated: true + }), // loopRun loopRunInput: z .any() .optional() - .meta({ description: 'loopRun 循环输入(数组或条件模式标记)' }), + .meta({ ...JsonValueOpenApiMeta, description: 'loopRun 循环输入(数组或条件模式标记)' }), loopRunIterations: z.number().optional().meta({ description: 'loopRun 实际执行轮数' }), - loopRunHistory: z.array(z.any()).optional().meta({ description: 'loopRun 每轮快照' }), + loopRunHistory: z + .array(z.any()) + .optional() + .meta({ + items: { type: 'object', additionalProperties: true }, + description: 'loopRun 每轮快照' + }), loopRunDetail: z .array(z.any()) .optional() - .meta({ description: 'loopRun 各轮子工作流节点响应聚合', deprecated: true }), - childrenResponses: z.array(z.any()).optional().meta({ description: '子节点响应' }), + .meta({ + items: { type: 'object', additionalProperties: true }, + description: 'loopRun 各轮子工作流节点响应聚合', + deprecated: true + }), + childrenResponses: z + .array(z.any()) + .optional() + .meta({ items: { type: 'object', additionalProperties: true }, description: '子节点响应' }), // Tools toolId: z.string().optional().meta({ description: '工具 ID' }), @@ -260,6 +323,6 @@ export type DispatchNodeResponseType = Omit< export const NodeOutputItemSchema = z.object({ nodeId: z.string(), key: z.enum(Object.values(NodeOutputKeyEnum)), - value: z.any() + value: z.any().meta({ ...JsonValueOpenApiMeta, description: '节点输出值' }) }); export type NodeOutputItemType = z.infer; diff --git a/packages/global/core/workflow/template/system/interactive/type.ts b/packages/global/core/workflow/template/system/interactive/type.ts index e60ec30bf2ca..837f9c3cf483 100644 --- a/packages/global/core/workflow/template/system/interactive/type.ts +++ b/packages/global/core/workflow/template/system/interactive/type.ts @@ -4,6 +4,7 @@ import { WorkflowIOValueTypeEnum } from '../../../../../core/workflow/constants' import { AppFileSelectConfigTypeSchema } from '../../../../app/type/config.schema'; import { RuntimeEdgeItemTypeSchema } from '../../../type/edge'; import z from 'zod'; +import { JsonValueOpenApiMeta, OpenObjectOpenApiMeta } from '../../../../../common/zod/openapi'; import { ChatCompletionMessageParamSchema } from '../../../../ai/llm/type'; import { AgentAskQuestionSchema } from '../../../../ai/agent/type'; @@ -32,7 +33,11 @@ export type InteractiveNodeType = z.infer; export const ChildrenInteractiveSchema = z.object({ type: z.literal('childrenInteractive'), params: z.object({ - childrenResponse: z.any() + // 递归结构(WorkflowInteractiveResponseType),运行时保持宽松。 + childrenResponse: z.any().meta({ + ...OpenObjectOpenApiMeta, + description: '子工作流交互响应' + }) }) }); export type ChildrenInteractive = InteractiveNodeType & { @@ -45,7 +50,10 @@ export type ChildrenInteractive = InteractiveNodeType & { export const ToolCallChildrenInteractiveSchema = z.object({ type: z.literal('toolChildrenInteractive'), params: z.object({ - childrenResponse: z.any(), + childrenResponse: z.any().meta({ + ...OpenObjectOpenApiMeta, + description: '子工作流交互响应' + }), toolParams: z.object({ // 兼容旧历史:新交互不再持久化完整 messages 快照,恢复时由 chat history 重建。 memoryRequestMessages: z.array(ChatCompletionMessageParamSchema).optional(), @@ -60,8 +68,11 @@ export type ToolCallChildrenInteractive = InteractiveNodeType & export const LoopInteractiveSchema = z.object({ type: z.literal('loopInteractive'), params: z.object({ - loopResult: z.array(z.any()), - childrenResponse: z.any(), + loopResult: z.array(z.any()).meta({ items: JsonValueOpenApiMeta, description: '各轮响应' }), + childrenResponse: z.any().meta({ + ...OpenObjectOpenApiMeta, + description: '子工作流交互响应' + }), currentIndex: z.number() }) }); @@ -77,10 +88,21 @@ export type LoopInteractive = InteractiveNodeType & { export const LoopRunInteractiveSchema = z.object({ type: z.literal('loopRunInteractive'), params: z.object({ - loopHistory: z.array(z.any()), - childrenResponse: z.any(), + loopHistory: z + .array(z.any()) + .meta({ items: { type: 'object', additionalProperties: true }, description: '各轮快照' }), + childrenResponse: z.any().meta({ + ...OpenObjectOpenApiMeta, + description: '子工作流交互响应' + }), iteration: z.number(), - pendingIterationSummary: z.any().optional() + pendingIterationSummary: z + .any() + .optional() + .meta({ + ...OpenObjectOpenApiMeta, + description: '待执行轮次的摘要' + }) }) }); export type LoopRunInteractive = InteractiveNodeType & { @@ -146,10 +168,13 @@ export const UserInputFormItemSchema = AppFileSelectConfigTypeSchema.extend({ type: z.enum(FlowNodeInputTypeEnum), key: z.string(), label: z.string(), - value: z.any(), + value: z.any().meta({ ...JsonValueOpenApiMeta, description: '当前填写值' }), valueType: z.enum(WorkflowIOValueTypeEnum), description: z.string().optional(), - defaultValue: z.any().optional(), + defaultValue: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '默认值' }), required: z.boolean(), maxLength: z.number().optional(), // input & textarea diff --git a/packages/global/core/workflow/type/io.ts b/packages/global/core/workflow/type/io.ts index 28b202cac2a3..79fa458edbb9 100644 --- a/packages/global/core/workflow/type/io.ts +++ b/packages/global/core/workflow/type/io.ts @@ -4,6 +4,7 @@ import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../node/constant' import { SecretValueTypeSchema } from '../../../common/secret/type'; import z from 'zod'; import { BoolSchema, IntSchema, NumSchema } from '../../../common/zod'; +import { JsonValueOpenApiMeta } from '../../../common/zod/openapi'; /* Dataset node */ export const SelectedDatasetSchema = z.object({ @@ -83,9 +84,13 @@ export const InputComponentPropsTypeSchema = z.object({ required: BoolSchema.optional().meta({ description: '该变量是否必填' }), - defaultValue: z.any().optional().meta({ - description: '变量默认值' - }), + defaultValue: z + .any() + .optional() + .meta({ + description: '变量默认值', + ...JsonValueOpenApiMeta + }), // 不同组件的配置嘻嘻 referencePlaceholder: z.string().optional().meta({ @@ -293,9 +298,13 @@ export const FlowNodeInputItemTypeSchema = InputComponentPropsTypeSchema.extend( valueDesc: z.string().optional().meta({ description: '输入值说明,通常用于展示引用值含义' }), // data desc - value: z.any().optional().meta({ - description: '节点输入当前值' - }), + value: z + .any() + .optional() + .meta({ + description: '节点输入当前值', + ...JsonValueOpenApiMeta + }), debugLabel: z.string().optional().meta({ description: '调试模式下展示的输入名称' @@ -363,9 +372,13 @@ export const FlowNodeOutputItemTypeSchema = z.object({ valueDesc: z.string().optional().meta({ description: '输出值说明,通常用于展示引用值含义' }), - value: z.any().optional().meta({ - description: '节点输出默认值或静态值' - }), + value: z + .any() + .optional() + .meta({ + description: '节点输出默认值或静态值', + ...JsonValueOpenApiMeta + }), label: z.string().optional().meta({ description: '节点输出展示名称' @@ -373,9 +386,13 @@ export const FlowNodeOutputItemTypeSchema = z.object({ description: z.string().optional().meta({ description: '节点输出说明' }), - defaultValue: z.any().optional().meta({ - description: '节点输出默认值' - }), + defaultValue: z + .any() + .optional() + .meta({ + description: '节点输出默认值', + ...JsonValueOpenApiMeta + }), required: BoolSchema.optional().meta({ description: '该输出是否为必需输出' }), diff --git a/packages/global/openapi/admin/app/templateType/api.ts b/packages/global/openapi/admin/app/templateType/api.ts index 0b1d809f4f4a..1d90a36e12a5 100644 --- a/packages/global/openapi/admin/app/templateType/api.ts +++ b/packages/global/openapi/admin/app/templateType/api.ts @@ -5,6 +5,12 @@ export const SaveTemplateTypeBodySchema = z.object({ typeName: z.string().meta({ description: '模板类型名称' }), typeOrder: z.number().meta({ description: '模板类型排序值' }) }); +export type SaveTemplateTypeBodyType = z.infer; + +export const DeleteTemplateTypeQuerySchema = z.object({ + typeId: z.string().meta({ description: '模板类型ID' }) +}); +export type DeleteTemplateTypeQueryType = z.infer; export const UpdateTemplateTypeOrderBodySchema = z.object({ types: z @@ -16,3 +22,4 @@ export const UpdateTemplateTypeOrderBodySchema = z.object({ ) .meta({ description: '模板类型排序列表' }) }); +export type UpdateTemplateTypeOrderBodyType = z.infer; diff --git a/packages/global/openapi/admin/dataset/api.ts b/packages/global/openapi/admin/dataset/api.ts index 589a173cf3f5..5216a4e5589b 100644 --- a/packages/global/openapi/admin/dataset/api.ts +++ b/packages/global/openapi/admin/dataset/api.ts @@ -10,5 +10,7 @@ export const DatasetItemSchema = z.object({ totalDatas: z.number().meta({ description: '数据总量' }), totalVectors: z.number().meta({ description: '向量总量' }) }); +export type DatasetItemType = z.infer; export const GetDatasetsResponseSchema = PaginationResponseSchema(DatasetItemSchema); +export type GetDatasetsResponseType = z.infer; diff --git a/packages/global/openapi/admin/system/inform/index.ts b/packages/global/openapi/admin/system/inform/index.ts index fdef58cef802..5311c09b9770 100644 --- a/packages/global/openapi/admin/system/inform/index.ts +++ b/packages/global/openapi/admin/system/inform/index.ts @@ -25,12 +25,7 @@ export const AdminInformPath: OpenAPIPath = { }, responses: { 200: { - description: '成功发送系统通知', - content: { - 'application/json': { - schema: {} - } - } + description: '成功发送系统通知' } } } @@ -66,12 +61,7 @@ export const AdminInformPath: OpenAPIPath = { }, responses: { 200: { - description: '成功更新系统弹窗', - content: { - 'application/json': { - schema: {} - } - } + description: '成功更新系统弹窗' } } } @@ -107,12 +97,7 @@ export const AdminInformPath: OpenAPIPath = { }, responses: { 200: { - description: '成功更新运营广告', - content: { - 'application/json': { - schema: {} - } - } + description: '成功更新运营广告' } } } @@ -148,12 +133,7 @@ export const AdminInformPath: OpenAPIPath = { }, responses: { 200: { - description: '成功更新活动广告', - content: { - 'application/json': { - schema: {} - } - } + description: '成功更新活动广告' } } } diff --git a/packages/global/openapi/admin/system/model/api.ts b/packages/global/openapi/admin/system/model/api.ts index c07a8dcc4f48..9c636b393e4d 100644 --- a/packages/global/openapi/admin/system/model/api.ts +++ b/packages/global/openapi/admin/system/model/api.ts @@ -142,7 +142,19 @@ export const TestAdminSystemModelQuerySchema = AdminSystemModelReferenceSchema.e }) }); export type TestAdminSystemModelQuery = z.infer; -export const TestAdminSystemModelResponseSchema = z.unknown(); +/** 模型连通性测试结果;TTS、STT 和 rerank 测试成功时没有业务返回值。 */ +export const TestAdminSystemModelResponseSchema = z + .union([ + z.string().meta({ description: 'LLM 的回答文本' }), + z.object({ + tokens: z.number().int().nonnegative().meta({ description: 'Embedding 输入 token 数' }), + vectors: z + .array(z.array(z.number())) + .meta({ description: 'Embedding 向量数组,顺序与输入一致' }) + }) + ]) + .optional() + .meta({ description: '模型连通性测试结果' }); export type TestAdminSystemModelResponse = z.infer; /* ============================================================================ @@ -229,23 +241,18 @@ export type GetAdminModelTemplatesResponse = z.infer { - if ( - typeof value === 'object' && - value !== null && - Object.prototype.hasOwnProperty.call(value, 'modelId') - ) { - ctx.addIssue({ - code: 'custom', - path: ['modelId'], - message: 'modelId is not allowed when creating a model' - }); - } - }) - .pipe(SystemModelDocumentDataSchema) - .meta({ description: '不含 modelId 的完整系统模型配置' }); + .discriminatedUnion('type', [ + LLMSystemModelDocumentSchema, + EmbeddingSystemModelDocumentSchema, + TTSSystemModelDocumentSchema, + STTSystemModelDocumentSchema, + RerankSystemModelDocumentSchema + ]) + .meta({ + description: '不含 modelId 的完整系统模型配置;未声明字段会被忽略,modelId 始终由服务端生成' + }); export const CreateSystemModelBodySchema = z .object({ diff --git a/packages/global/openapi/admin/user/api.ts b/packages/global/openapi/admin/user/api.ts index 2cff1f1b4970..5741671eafdf 100644 --- a/packages/global/openapi/admin/user/api.ts +++ b/packages/global/openapi/admin/user/api.ts @@ -37,10 +37,12 @@ export const AddUserBodySchema = z.object({ username: z.string().min(1).meta({ description: '用户名' }), password: z.string().min(1).meta({ description: '密码' }) }); +export type AddUserBodyType = z.infer; export const AddUserResponseSchema = z.object({ userId: z.string().meta({ description: '新创建的用户ID' }), teamId: z.string().meta({ description: '用户的团队ID' }) }); +export type AddUserResponseType = z.infer; // updateUser export const UpdateUserBodySchema = z.object({ @@ -49,8 +51,10 @@ export const UpdateUserBodySchema = z.object({ password: z.string().min(1).optional().meta({ description: '新密码' }), status: z.enum(UserStatusEnum).optional().meta({ description: '用户状态' }) }); +export type UpdateUserBodyType = z.infer; // delete export const DeleteUserBodySchema = z.object({ username: z.string().min(1).meta({ description: '用户名' }) }); +export type DeleteUserBodyType = z.infer; diff --git a/packages/global/openapi/admin/user/index.ts b/packages/global/openapi/admin/user/index.ts index 42f5a336a024..887ca36dbb43 100644 --- a/packages/global/openapi/admin/user/index.ts +++ b/packages/global/openapi/admin/user/index.ts @@ -72,12 +72,7 @@ export const AdminUsersPath: OpenAPIPath = { }, responses: { 200: { - description: '更新成功', - content: { - 'application/json': { - schema: {} - } - } + description: '更新成功' } } } @@ -96,12 +91,7 @@ export const AdminUsersPath: OpenAPIPath = { }, responses: { 200: { - description: '注销成功', - content: { - 'application/json': { - schema: {} - } - } + description: '注销成功' } } } diff --git a/packages/global/openapi/admin/wallet/bill/invoice/index.ts b/packages/global/openapi/admin/wallet/bill/invoice/index.ts index 8ab3bb2352a6..34670d876316 100644 --- a/packages/global/openapi/admin/wallet/bill/invoice/index.ts +++ b/packages/global/openapi/admin/wallet/bill/invoice/index.ts @@ -45,12 +45,7 @@ export const AdminInvoicePath: OpenAPIPath = { }, responses: { 200: { - description: '发票开具成功', - content: { - 'application/json': { - schema: {} - } - } + description: '发票开具成功' } } } diff --git a/packages/global/openapi/admin/wallet/plan/api.ts b/packages/global/openapi/admin/wallet/plan/api.ts index 7ec9c9044e56..332b37798795 100644 --- a/packages/global/openapi/admin/wallet/plan/api.ts +++ b/packages/global/openapi/admin/wallet/plan/api.ts @@ -61,6 +61,7 @@ export const AddPlansBodySchema = z.object({ totalPoints: z.number().optional().meta({ description: '总积分' }), surplusPoints: z.number().optional().meta({ description: '剩余积分' }) }); +export type AddPlansBodyType = z.infer; // updatePlan export const UpdatePlanBodySchema = z.object({ @@ -88,3 +89,4 @@ export const UpdatePlanBodySchema = z.object({ maxUploadFileCount: NumSchema.optional().meta({ description: '最大上传文件数' }), enableSandbox: z.boolean().optional().meta({ description: '是否启用沙盒' }) }); +export type UpdatePlanBodyType = z.infer; diff --git a/packages/global/openapi/admin/wallet/plan/index.ts b/packages/global/openapi/admin/wallet/plan/index.ts index 237fb6b19ae9..5411ba3bcd8b 100644 --- a/packages/global/openapi/admin/wallet/plan/index.ts +++ b/packages/global/openapi/admin/wallet/plan/index.ts @@ -46,12 +46,7 @@ export const AdminPlanPath: OpenAPIPath = { }, responses: { 200: { - description: '添加成功', - content: { - 'application/json': { - schema: {} - } - } + description: '添加成功' } } } @@ -70,12 +65,7 @@ export const AdminPlanPath: OpenAPIPath = { }, responses: { 200: { - description: '更新成功', - content: { - 'application/json': { - schema: {} - } - } + description: '更新成功' } } } diff --git a/packages/global/openapi/api.ts b/packages/global/openapi/api.ts index 1f01a7c7deff..830450ab9c2a 100644 --- a/packages/global/openapi/api.ts +++ b/packages/global/openapi/api.ts @@ -1,5 +1,6 @@ import type { RequireOnlyOne } from '../common/type/utils'; import { IntSchema } from '../common/zod'; +import { JsonValueOpenApiMeta } from '../common/zod/openapi'; import z from 'zod'; /* 按 offset 分页 */ @@ -39,7 +40,10 @@ export const LinkedPaginationSchema = (extraShape? .optional() .default(10) .meta({ example: 15, description: '每页条数' }), - anchor: z.any().optional().meta({ description: '当前锚点(如 chunkIndex)' }), + anchor: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '当前锚点(如 chunkIndex)' }), initialId: z.string().optional().meta({ example: '68ad85a7463006c963799a05', description: '初始定位数据 ID' @@ -71,7 +75,10 @@ export const LinkedListResponseSchema = (itemSchema: T) itemSchema, z.object({ id: z.string().meta({ example: '68ad85a7463006c963799a05', description: '数据 ID' }), - anchor: z.any().optional().meta({ description: '锚点值' }) + anchor: z + .any() + .optional() + .meta({ ...JsonValueOpenApiMeta, description: '锚点值' }) }) ) ) diff --git a/packages/global/openapi/common/other/api.ts b/packages/global/openapi/common/other/api.ts index c691ca1a835b..9c19614aa692 100644 --- a/packages/global/openapi/common/other/api.ts +++ b/packages/global/openapi/common/other/api.ts @@ -35,7 +35,7 @@ export const PushTrackBodySchema = z.object({ example: TrackEnum.useAppTemplate, description: '埋点事件类型' }), - data: z.unknown().meta({ + data: z.json().meta({ example: { id: 'app-template-id', name: '示例模板' }, description: '事件关联数据,结构由事件类型决定' }) diff --git a/packages/global/openapi/core/ai/api.ts b/packages/global/openapi/core/ai/api.ts index ebe0602a5bb9..94ed47e375e0 100644 --- a/packages/global/openapi/core/ai/api.ts +++ b/packages/global/openapi/core/ai/api.ts @@ -112,6 +112,7 @@ export type ResumeStreamRuntimeParams = z.infer export const StreamResumeCompletedRecordsSchema = z.object({ list: z.array(z.any()).meta({ + items: { type: 'object', additionalProperties: true }, description: '最新已落库的聊天记录' }), total: z.number().int().nonnegative().meta({ diff --git a/packages/global/openapi/core/app/common/api.ts b/packages/global/openapi/core/app/common/api.ts index 471afb41edf8..4116476a46d1 100644 --- a/packages/global/openapi/core/app/common/api.ts +++ b/packages/global/openapi/core/app/common/api.ts @@ -502,9 +502,13 @@ export const UpdateAppBodySchema = z }); export type UpdateAppBodyType = z.infer; -export const UpdateAppResponseSchema = z.unknown().nullable().optional().meta({ - description: 'MongoDB 更新结果;移动应用时无返回数据' -}); +export const UpdateAppResponseSchema = z + .union([ + z.looseObject({}).meta({ description: 'MongoDB 更新结果' }), + z.null().meta({ description: '移动应用时无返回数据' }) + ]) + .optional() + .meta({ description: '应用更新结果' }); export type UpdateAppResponseType = z.infer; /* ============================================================================ diff --git a/packages/global/openapi/core/app/evaluation/api.ts b/packages/global/openapi/core/app/evaluation/api.ts index 39758a31fb18..4d256453d524 100644 --- a/packages/global/openapi/core/app/evaluation/api.ts +++ b/packages/global/openapi/core/app/evaluation/api.ts @@ -51,7 +51,7 @@ export const CreateEvaluationBodySchema = z export type CreateEvaluationBodyType = z.infer; export const CreateEvaluationFormSchema = z.object({ - file: z.any().meta({ + file: z.string().meta({ format: 'binary', description: 'CSV 评测文件,包含 *q 问题列和 *a 期望答案列' }), diff --git a/packages/global/openapi/core/app/httpTools/api.ts b/packages/global/openapi/core/app/httpTools/api.ts index f3900023c759..ff15efe7cebd 100644 --- a/packages/global/openapi/core/app/httpTools/api.ts +++ b/packages/global/openapi/core/app/httpTools/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { JsonValueOpenApiMeta, OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; import { ObjectIdSchema } from '../../../../common/type/mongo'; import { StoreSecretValueTypeSchema } from '../../../../common/secret/type'; import { CreateAppBodySchema } from '../common/api'; @@ -93,6 +94,7 @@ export const GetApiSchemaByUrlBodySchema = z export type GetApiSchemaByUrlBodyType = z.infer; export const GetApiSchemaByUrlResponseSchema = z.any().meta({ + ...OpenObjectOpenApiMeta, description: '解析后的 OpenAPI Schema 对象' }); export type GetApiSchemaByUrlResponseType = z.infer; @@ -156,9 +158,13 @@ export type RunHttpToolBodyType = z.infer; export const RunHttpToolResponseSchema = z .object({ - data: z.any().optional().meta({ - description: '工具调用返回结果' - }), + data: z + .any() + .optional() + .meta({ + ...JsonValueOpenApiMeta, + description: '工具调用返回结果' + }), errorMsg: z.string().optional().meta({ example: '请求失败', description: '错误信息' diff --git a/packages/global/openapi/core/app/mcpTools/api.ts b/packages/global/openapi/core/app/mcpTools/api.ts index b287eb13e0ce..39e7d78e4d2b 100644 --- a/packages/global/openapi/core/app/mcpTools/api.ts +++ b/packages/global/openapi/core/app/mcpTools/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; import { ObjectIdSchema } from '../../../../common/type/mongo'; import { StoreSecretValueTypeSchema } from '../../../../common/secret/type'; import { CreateAppBodySchema } from '../common/api'; @@ -155,6 +156,7 @@ export const RunMcpToolBodySchema = z export type RunMcpToolBodyType = z.infer; export const RunMcpToolResponseSchema = z.any().meta({ - description: '工具调用结果' + ...OpenObjectOpenApiMeta, + description: '工具调用结果(MCP CallToolResult 结构,内容随工具变化)' }); export type RunMcpToolResponseType = z.infer; diff --git a/packages/global/openapi/core/app/template/api.ts b/packages/global/openapi/core/app/template/api.ts index 867962fc87e3..63c3926dc4b4 100644 --- a/packages/global/openapi/core/app/template/api.ts +++ b/packages/global/openapi/core/app/template/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; import { BoolSchema, NumSchema } from '../../../../common/zod'; import { AppTypeEnum } from '../../../../core/app/constants'; import { AppTemplateSchema } from '../../../../core/app/type'; @@ -73,18 +74,23 @@ export const GetAppTemplateDetailQuerySchema = z.object({ }); export type GetAppTemplateDetailQueryType = z.infer; -export const GetAppTemplateDetailResponseSchema = AppTemplateSchema.omit({ +/** + * 模板对象在文档中的投影。底层 workflow 是 z.custom(): + * chat agent 模板是另一种结构,且 z.custom 无法映射为 OpenAPI 类型,这里只声明为开放对象。 + */ +export const OpenAPIAppTemplateSchema = AppTemplateSchema.omit({ workflow: true -}) - .extend({ - workflow: z.any().meta({ - description: '模板对应的应用编排配置;不同应用类型可能使用不同结构' - }) +}).extend({ + workflow: z.any().meta({ + ...OpenObjectOpenApiMeta, + description: '模板对应的应用编排配置;不同应用类型可能使用不同结构' }) - .optional() - .meta({ - description: '模板详情;未找到模板时为空' - }); +}); +export type OpenAPIAppTemplateType = z.infer; + +export const GetAppTemplateDetailResponseSchema = OpenAPIAppTemplateSchema.optional().meta({ + description: '模板详情;未找到模板时为空' +}); export type GetAppTemplateDetailResponseType = z.infer; /* ============================================================================ diff --git a/packages/global/openapi/core/chat/completion/api.ts b/packages/global/openapi/core/chat/completion/api.ts index da7bb3d48259..563858fd4c2b 100644 --- a/packages/global/openapi/core/chat/completion/api.ts +++ b/packages/global/openapi/core/chat/completion/api.ts @@ -144,6 +144,10 @@ export type CompletionsProps = z.infer; const ChatCompletionResponseMessageSchema = z.object({ role: z.literal('assistant').meta({ description: '消息角色' }), content: z.any().meta({ + anyOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'object', additionalProperties: true } } + ], description: '消息内容。普通对话为字符串;detail=true 或工作流命中交互节点时,可能为按字段名区分的对象数组(如 text / interactive / tool / file / reasoning)。v1 会额外补充 type 字段,取值为 text / interactive / tool / file / reasoning;v2 不补充 type。当元素包含 interactive 字段时,只返回交互展示配置,不返回 entryNodeIds / memoryEdges / nodeOutputs 等内部运行态字段' }), @@ -176,10 +180,14 @@ export const CompletionsResponseSchema = z.object({ description: 'Token 用量。v1 接口为占位值,需要时请从 responseData 计算' }), choices: z.array(ChatCompletionChoiceSchema).meta({ description: '回复选项列表' }), - responseData: z.array(z.any()).optional().meta({ - description: - '各节点详细响应数据(仅 detail=true 时返回)。每项是一个节点的执行结果,常见字段如 moduleName / moduleType / runningTime / quoteList 等' - }), + responseData: z + .array(z.any()) + .optional() + .meta({ + items: { type: 'object', additionalProperties: true }, + description: + '各节点详细响应数据(仅 detail=true 时返回)。每项是一个节点的执行结果,常见字段如 moduleName / moduleType / runningTime / quoteList 等' + }), newVariables: z .record(z.string(), z.any()) .optional() diff --git a/packages/global/openapi/core/chat/record/api.ts b/packages/global/openapi/core/chat/record/api.ts index e9f1d875a54f..d0d72b830873 100644 --- a/packages/global/openapi/core/chat/record/api.ts +++ b/packages/global/openapi/core/chat/record/api.ts @@ -176,7 +176,9 @@ export type GetPaginationRecordsBodyType = z.infer; export const GetPaginationRecordsResponseSchema = z.object({ - list: z.array(z.any()).meta({ description: '对话列表' }), + list: z + .array(z.any()) + .meta({ items: { type: 'object', additionalProperties: true }, description: '对话列表' }), total: z.number().int().nonnegative().meta({ example: 10, description: '总数' }) }); export type GetPaginationRecordsResponseType = z.infer; @@ -233,7 +235,7 @@ export type AudioTranscriptionsDataType = z.infer; export const AudioTranscriptionsFormRawSchema = z.object({ - file: z.any().meta({ format: 'binary', description: '上传的音频文件(二进制)' }), + file: z.string().meta({ format: 'binary', description: '上传的音频文件(二进制)' }), data: AudioTranscriptionsDataRawSchema.meta({ description: '语音识别参数(JSON 序列化后传入)' }) diff --git a/packages/global/openapi/core/dataset/collection/createApi.ts b/packages/global/openapi/core/dataset/collection/createApi.ts index 7a2eec957734..3f0cd80c9829 100644 --- a/packages/global/openapi/core/dataset/collection/createApi.ts +++ b/packages/global/openapi/core/dataset/collection/createApi.ts @@ -108,7 +108,7 @@ export type CreateCollectionByLocalFileBodyType = z.infer< // OpenAPI 文档专用:描述 multipart/form-data 的实际结构 // file 字段为二进制文件;data 字段为 JSON 序列化的对象(encoding: application/json) export const CreateCollectionByLocalFileFormSchema = z.object({ - file: z.any().meta({ format: 'binary', description: '上传的文件(二进制)' }), + file: z.string().meta({ format: 'binary', description: '上传的文件(二进制)' }), data: CreateCollectionByLocalFileBodySchema.meta({ description: '集合参数(JSON 序列化后传入)' }) @@ -180,7 +180,7 @@ export type CreateImageCollectionFormType = z.infer; export const UploadDatasetSynonymFileFormSchema = z.object({ - file: z.any().meta({ format: 'binary', description: 'CSV、XLS 或 XLSX 同义词文件' }), + file: z.string().meta({ format: 'binary', description: 'CSV、XLS 或 XLSX 同义词文件' }), data: UploadDatasetSynonymFileBodySchema.meta({ description: 'JSON 序列化后的知识库参数' }) }); export const UpdateDatasetSynonymFileFormSchema = z.object({ - file: z.any().meta({ format: 'binary', description: 'CSV、XLS 或 XLSX 同义词文件' }), + file: z.string().meta({ format: 'binary', description: 'CSV、XLS 或 XLSX 同义词文件' }), data: UpdateDatasetSynonymFileBodySchema.meta({ description: 'JSON 序列化后的知识库参数' }) }); diff --git a/packages/global/openapi/core/plugin/admin/api.ts b/packages/global/openapi/core/plugin/admin/api.ts index 78209e74ab12..24f878bb4fa8 100644 --- a/packages/global/openapi/core/plugin/admin/api.ts +++ b/packages/global/openapi/core/plugin/admin/api.ts @@ -10,7 +10,8 @@ import { I18nStringSchema } from '../../../../common/i18n/type'; * ============================================================================ */ export const UploadPkgPluginBodySchema = z.object({ - file: z.any().meta({ + file: z.string().meta({ + format: 'binary', description: 'multipart/form-data file 字段,可重复传入,支持 .pkg 文件或包含多个 .pkg 的 .zip 文件' }) diff --git a/packages/global/openapi/core/plugin/debug/api.ts b/packages/global/openapi/core/plugin/debug/api.ts index a918ef2dd69d..a177ccfc72c3 100644 --- a/packages/global/openapi/core/plugin/debug/api.ts +++ b/packages/global/openapi/core/plugin/debug/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; export const PluginDebugChannelStatusSchema = z.enum([ 'enabled', @@ -118,6 +119,7 @@ export const PluginDebugChannelPluginSchema = z description: '调试插件版本' }), name: z.unknown().meta({ + ...OpenObjectOpenApiMeta, example: { en: 'Get Time', 'zh-CN': '获取时间' @@ -128,6 +130,7 @@ export const PluginDebugChannelPluginSchema = z .unknown() .optional() .meta({ + ...OpenObjectOpenApiMeta, example: { en: 'Get current time', 'zh-CN': '获取当前时间' diff --git a/packages/global/openapi/core/plugin/marketplace/api.ts b/packages/global/openapi/core/plugin/marketplace/api.ts index c4494d3cb335..71a171b55761 100644 --- a/packages/global/openapi/core/plugin/marketplace/api.ts +++ b/packages/global/openapi/core/plugin/marketplace/api.ts @@ -280,7 +280,7 @@ export type GetMarketplaceDownloadUrlsResponseType = z.infer< // Upload marketplace pkg export const UploadMarketplacePkgBodySchema = z.object({ - file: z.any(), + file: z.string().meta({ format: 'binary', description: '上传的 .pkg 文件' }), source: MarketplacePkgSourceSchema.optional().default(MarketplaceOfficialSource) }); export const UploadMarketplacePkgDataSchema = z.object({ diff --git a/packages/global/openapi/core/plugin/team/pkg/api.ts b/packages/global/openapi/core/plugin/team/pkg/api.ts index 1a6e3e6b0433..248da96e7199 100644 --- a/packages/global/openapi/core/plugin/team/pkg/api.ts +++ b/packages/global/openapi/core/plugin/team/pkg/api.ts @@ -11,7 +11,8 @@ import { TeamPluginEmptyResponseSchema } from '../common'; * ============================================================================ */ export const UploadTeamPkgPluginBodySchema = z.object({ - file: z.any().meta({ + file: z.string().meta({ + format: 'binary', description: 'multipart/form-data file 字段,可重复传入,支持 .pkg 文件或包含多个 .pkg 的 .zip 文件' }) diff --git a/packages/global/openapi/plugin/invoke.ts b/packages/global/openapi/plugin/invoke.ts index 437beff3ea21..ddc0778d1b6d 100644 --- a/packages/global/openapi/plugin/invoke.ts +++ b/packages/global/openapi/plugin/invoke.ts @@ -8,10 +8,6 @@ import { ChatFileTypeEnum } from '../../core/chat/constants'; * Description: 通过 invoke token 获取当前运行上下文的用户信息 * Tags: ['通用-反向调用', '插件'] * ============================================================================ */ -export const InvokeUserInfoBodySchema = z.object({}); - -export const InvokeUserInfoQuerySchema = z.object({}); - export const InvokeUserInfoResponseSchema = z.object({ username: z.string().meta({ description: '账号', example: 'user@example.com' }), contact: z.string().nullish().meta({ description: '联系方式', example: '13800138000' }), @@ -27,8 +23,6 @@ export const InvokeUserInfoResponseSchema = z.object({ ) }); -export type InvokeUserInfoBodyType = z.infer; -export type InvokeUserInfoQueryType = z.infer; export type InvokeUserInfoResponseType = z.infer; /* ============================================================================ @@ -38,8 +32,9 @@ export type InvokeUserInfoResponseType = z.infer; +/** @deprecated 仅供当前 Pro 子模块过渡使用。 */ export type InvokeWecomCorpTokenQueryType = z.infer; export type InvokeWecomCorpTokenResponseType = z.infer; @@ -66,7 +63,7 @@ export const InvokeFileUploadBodySchema = z.object({ }); export const InvokeFileUploadFormSchema = InvokeFileUploadBodySchema.extend({ - file: z.any().meta({ + file: z.string().meta({ format: 'binary', description: '待上传的文件(二进制)' }) @@ -79,8 +76,6 @@ export const InvokeAuthorizationHeaderSchema = z.object({ }) }); -export const InvokeFileUploadQuerySchema = z.object({}); - export const InvokeFileUploadResponseSchema = z.object({ url: z.string().meta({ description: '上传后的文件访问 URL', @@ -105,5 +100,4 @@ export const InvokeFileUploadResponseSchema = z.object({ }); export type InvokeFileUploadBodyType = z.infer; -export type InvokeFileUploadQueryType = z.infer; export type InvokeFileUploadResponseType = z.infer; diff --git a/packages/global/openapi/support/outLink/api.ts b/packages/global/openapi/support/outLink/api.ts index a0d6b3e308b4..d8c87c67d154 100644 --- a/packages/global/openapi/support/outLink/api.ts +++ b/packages/global/openapi/support/outLink/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; import { PublishChannelEnum } from '../../../support/outLink/constant'; import { ObjectIdSchema } from '../../../common/type/mongo'; @@ -20,9 +21,15 @@ const OutLinkLimitSchema = z }) .meta({ description: '发布渠道限制配置' }); -const OutLinkAppConfigSchema = z.any().optional().meta({ - description: '第三方平台配置,不同发布渠道结构不同' -}); +// 各发布渠道(公众号/企微/飞书/钉钉…)配置结构不同,且 type 字段是 app 的同级字段, +// 无法在 app 上做 discriminatedUnion,这里声明为开放对象。 +const OutLinkAppConfigSchema = z + .any() + .optional() + .meta({ + ...OpenObjectOpenApiMeta, + description: '第三方平台配置,不同发布渠道结构不同' + }); export const OutLinkEditSchema = z.object({ _id: ObjectIdSchema.optional().meta({ description: '发布渠道 ID,更新时必填' }), diff --git a/packages/global/openapi/support/user/account/login/api.ts b/packages/global/openapi/support/user/account/login/api.ts index a228cd081ce4..d9c6c81aa827 100644 --- a/packages/global/openapi/support/user/account/login/api.ts +++ b/packages/global/openapi/support/user/account/login/api.ts @@ -10,12 +10,14 @@ import { ShortAuthStringSchema } from '../../../../../support/user/account/verification/type'; import { PublicAuthTrackRegisterParamsSchema } from '../common'; +import { OpenObjectOpenApiMeta } from '../../../../../common/zod/openapi'; const OpenAPITeamTmbItemSchema = TeamTmbItemSchema.omit({ permission: true }).extend({ permission: z.any().meta({ - description: '团队权限实例。返回值为服务端权限对象,文档中按任意结构展示。' + ...OpenObjectOpenApiMeta, + description: '团队权限实例。具体权限字段取决于团队角色配置。' }) }); @@ -25,13 +27,17 @@ export const OpenAPIUserSchema = UserSchema.omit({ }).extend({ team: OpenAPITeamTmbItemSchema, permission: z.any().meta({ - description: '用户权限实例。返回值为服务端权限对象,文档中按任意结构展示。' + ...OpenObjectOpenApiMeta, + description: '用户权限实例。具体权限字段取决于团队角色配置。' }) }); export type OpenAPIUserType = z.infer; export const LoginSuccessResponseSchema = z.object({ + // 登录结果长期作为 UserDetailType 使用;在业务类型统一前保持运行时和 TS 兼容, + // 文档明确它是对象,避免客户端生成 null schema。 user: z.any().meta({ + ...OpenObjectOpenApiMeta, description: '用户详情' }), token: z.string().meta({ diff --git a/packages/global/openapi/support/wallet/bill/invoice/api.ts b/packages/global/openapi/support/wallet/bill/invoice/api.ts index f5ed4e2e4d4c..e39c28836420 100644 --- a/packages/global/openapi/support/wallet/bill/invoice/api.ts +++ b/packages/global/openapi/support/wallet/bill/invoice/api.ts @@ -94,7 +94,12 @@ export const InvoiceRecordSchema = z .string() .nullish() .transform((value) => value ?? '-') - .meta({ example: '13800138000', description: '联系人电话;历史记录缺失时返回 -' }), + // transform 后一定是字符串;zod-openapi 在出参位置只能读到 .meta(),必须显式声明类型。 + .meta({ + type: 'string', + example: '13800138000', + description: '联系人电话;历史记录缺失时返回 -' + }), emailAddress: z.string().meta({ example: 'billing@example.com', description: '发票接收邮箱' }) }) .meta({ description: '发票记录;文件内容不在列表接口中返回' }); diff --git a/packages/global/support/wallet/sub/coupon/type.ts b/packages/global/support/wallet/sub/coupon/type.ts index e7673856d5c5..f60d2a6febe5 100644 --- a/packages/global/support/wallet/sub/coupon/type.ts +++ b/packages/global/support/wallet/sub/coupon/type.ts @@ -2,7 +2,7 @@ import z from 'zod'; import { SubTypeEnum, StandardSubLevelEnum } from '../constants'; import { CouponTypeEnum } from './constants'; -const CustomSubConfigSchema = z.object({ +export const CustomSubConfigSchema = z.object({ requestsPerMinute: z.number(), maxTeamMember: z.number(), maxAppAmount: z.number(), diff --git a/packages/global/test/openapi/admin/settings/model.test.ts b/packages/global/test/openapi/admin/settings/model.test.ts index 5589e3b5528d..331fc9fe3c25 100644 --- a/packages/global/test/openapi/admin/settings/model.test.ts +++ b/packages/global/test/openapi/admin/settings/model.test.ts @@ -7,6 +7,7 @@ import { DeleteSystemModelsBodySchema, ImportedSystemModelSchema, ReplaceSystemModelChannelsBodySchema, + TestAdminSystemModelResponseSchema, TestAdminSystemModelQuerySchema, UpdateSystemModelBodySchema, UpdateSystemModelStatusBodySchema @@ -41,6 +42,35 @@ describe('admin system model API schemas', () => { ).not.toThrow(); }); + it('documents the create body modelData as a discriminated union', () => { + // 回归:曾用 z.unknown().pipe(...) 实现,zod-openapi 只能输出 description, + // 生成的客户端会把 modelData 当成 null/any。 + const document = createDocument({ + openapi: '3.1.0', + info: { title: 'Admin model API', version: '1.0.0' }, + paths: AdminSystemModelPath + }); + const body = document.paths?.['/admin/system/model/create']?.post?.requestBody as + | { content: { 'application/json': { schema: { properties: Record } } } } + | undefined; + const modelData = body?.content['application/json'].schema.properties.modelData as + | { type?: string; oneOf?: unknown[] } + | undefined; + + expect(modelData?.type).toBe('object'); + expect(modelData?.oneOf).toHaveLength(5); + }); + + it('describes each model test result without a documentation-only override', () => { + expect(TestAdminSystemModelResponseSchema.parse('Hello')).toBe('Hello'); + expect(TestAdminSystemModelResponseSchema.parse({ tokens: 2, vectors: [[0.1, 0.2]] })).toEqual({ + tokens: 2, + vectors: [[0.1, 0.2]] + }); + expect(TestAdminSystemModelResponseSchema.parse(undefined)).toBeUndefined(); + expect(() => TestAdminSystemModelResponseSchema.parse([[0.1, 0.2]])).toThrow(); + }); + it('validates unique model IDs for batch status and delete operations', () => { const modelIds = ['68ad85a7463006c963799a05', '68ad85a7463006c963799a06']; @@ -85,7 +115,7 @@ describe('admin system model API schemas', () => { ).toThrow(); }); - it('rejects generated model IDs and invalid channel IDs at write boundaries', () => { + it('strips legacy model fields and rejects invalid channel IDs at write boundaries', () => { const modelData = { type: 'llm' as const, provider: 'OpenAI', @@ -96,12 +126,16 @@ describe('admin system model API schemas', () => { config: { maxContext: 16000, maxResponse: 8000, quoteMaxToken: 12000 } }; - expect(() => + expect( CreateSystemModelBodySchema.parse({ - modelData: { ...modelData, modelId: '68ad85a7463006c963799a05' }, + modelData: { + ...modelData, + modelId: '68ad85a7463006c963799a05', + legacyClientField: true + }, channelIds: [] }) - ).toThrow('modelId is not allowed when creating a model'); + ).toEqual({ modelData, channelIds: [] }); expect(() => CreateSystemModelBodySchema.parse({ modelData, channelIds: [0] })).toThrow(); expect(() => ReplaceSystemModelChannelsBodySchema.parse({ @@ -215,3 +249,4 @@ describe('admin system model API schemas', () => { expect(parsed.config).not.toHaveProperty('unknownConfig'); }); }); + diff --git a/packages/global/test/openapi/api.test.ts b/packages/global/test/openapi/api.test.ts new file mode 100644 index 000000000000..c81cc5adff83 --- /dev/null +++ b/packages/global/test/openapi/api.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { LinkedListResponseSchema, LinkedPaginationSchema } from '../../openapi/api'; +import z from 'zod'; + +describe('shared API schemas', () => { + it('keeps legacy JSON cursor anchors compatible', () => { + const anchor = { index: 2, filters: ['active'] }; + + expect(LinkedPaginationSchema().parse({ anchor }).anchor).toEqual(anchor); + expect( + LinkedListResponseSchema(z.object({ name: z.string() })).parse({ + list: [{ id: 'item-1', name: 'First', anchor }], + hasMorePrev: false, + hasMoreNext: true + }).list[0].anchor + ).toEqual(anchor); + }); +}); diff --git a/packages/global/test/openapi/core/app.test.ts b/packages/global/test/openapi/core/app.test.ts index 4404e05d35d8..a67d44121aad 100644 --- a/packages/global/test/openapi/core/app.test.ts +++ b/packages/global/test/openapi/core/app.test.ts @@ -156,8 +156,10 @@ describe('App OpenAPI contracts', () => { appId: objectId, evalModelId: objectId }); - expect(CreateEvaluationFormSchema.parse({ file: {}, data: '{}' })).toEqual({ - file: {}, + expect( + CreateEvaluationFormSchema.parse({ file: 'binary-file-placeholder', data: '{}' }) + ).toEqual({ + file: 'binary-file-placeholder', data: '{}' }); expect(DeleteEvaluationQuerySchema.parse({ evalId: objectId })).toEqual({ evalId: objectId }); diff --git a/projects/app/src/pages/api/invoke/userInfo.ts b/projects/app/src/pages/api/invoke/userInfo.ts index b9c1e73abaa0..333b986f0206 100644 --- a/projects/app/src/pages/api/invoke/userInfo.ts +++ b/projects/app/src/pages/api/invoke/userInfo.ts @@ -1,23 +1,13 @@ import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/next/type'; -import type { - InvokeUserInfoBodyType, - InvokeUserInfoQueryType, - InvokeUserInfoResponseType -} from '@fastgpt/global/openapi/plugin/invoke'; -import { - InvokeUserInfoQuerySchema, - InvokeUserInfoResponseSchema -} from '@fastgpt/global/openapi/plugin/invoke'; +import type { InvokeUserInfoResponseType } from '@fastgpt/global/openapi/plugin/invoke'; +import { InvokeUserInfoResponseSchema } from '@fastgpt/global/openapi/plugin/invoke'; import { InvokeProcessor } from '@fastgpt/service/support/invoke/invoke'; -import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; async function handler( - req: ApiRequestProps, + req: ApiRequestProps, _res: ApiResponseType ): Promise { - parseApiInput({ req, querySchema: InvokeUserInfoQuerySchema }); - const token = req.headers.authorization?.split(' ')[1] || ''; const userInfo = await InvokeProcessor.getInstanceFromToken(token).handleGetUserInfo(); diff --git a/projects/app/test/pages/api/core/ai/model/test.test.ts b/projects/app/test/pages/api/core/ai/model/test.test.ts index fefa52981fb7..2dd9d760cedf 100644 --- a/projects/app/test/pages/api/core/ai/model/test.test.ts +++ b/projects/app/test/pages/api/core/ai/model/test.test.ts @@ -225,19 +225,22 @@ describe('admin model test routing', () => { }); it('tests an embedding model through the selected channel', async () => { - const vectors = [{ embedding: [0.1, 0.2], index: 0 }]; + const embeddingResult = { + tokens: 1, + vectors: [[0.1, 0.2]] + }; mocks.findModelData.mockReturnValue({ ...installedModel, type: ModelTypeEnum.embedding }); - mocks.getVectors.mockResolvedValue(vectors); + mocks.getVectors.mockResolvedValue(embeddingResult); const result = await handler( { query: { modelId: installedModel.modelId, channelId: 11 } } as any, {} as any ); - expect(result).toEqual(vectors); + expect(result).toEqual(embeddingResult); expect(mocks.getVectors).toHaveBeenCalledWith( expect.objectContaining({ inputs: [{ type: 'text', input: 'Hi' }], diff --git a/projects/app/test/pages/api/core/ai/model/update.test.ts b/projects/app/test/pages/api/core/ai/model/update.test.ts index 55fad44739c4..c09ae947cab5 100644 --- a/projects/app/test/pages/api/core/ai/model/update.test.ts +++ b/projects/app/test/pages/api/core/ai/model/update.test.ts @@ -486,15 +486,17 @@ describe('admin settings model create/update api', () => { await expect(MongoAIModel.countDocuments()).resolves.toBe(0); }); - it('rejects modelId anywhere in a create model payload', async () => { + it('ignores a client-provided modelId when creating a model', async () => { + const clientModelId = '68ad85a7463006c963799a05'; const res = await callApi({ handler: createModelApi, - body: { modelData: { ...buildLlmDocument(), modelId: '68ad85a7463006c963799a05' } } + body: { modelData: { ...buildLlmDocument(), modelId: clientModelId } } }); - expect(res.error?.name).toBe('ApiRequestInputParseError'); - await expect(MongoAIModel.countDocuments()).resolves.toBe(0); - expect(configMocks.updatedReloadSystemModel).not.toHaveBeenCalled(); + expect(res.error).toBeUndefined(); + expect(res.data?.modelId).not.toBe(clientModelId); + await expect(MongoAIModel.countDocuments()).resolves.toBe(1); + expect(configMocks.updatedReloadSystemModel).toHaveBeenCalledTimes(1); }); it('rejects the whole template batch when a selected template disappeared', async () => { From faeaefbe9a0028c8ae799d8f6e78e86463cf32e1 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 18:08:10 +0800 Subject: [PATCH 02/10] fix(openapi): export missing admin schemas and align contracts --- .../global/openapi/admin/app/templates/api.ts | 11 ++++++++ packages/global/openapi/admin/system/api.ts | 1 + packages/global/openapi/admin/team/api.ts | 10 ++++++- .../openapi/admin/wallet/bill/invoice/api.ts | 28 +++++++++++++++++-- .../global/openapi/admin/wallet/plan/api.ts | 11 +++++--- pro | 2 +- 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/global/openapi/admin/app/templates/api.ts b/packages/global/openapi/admin/app/templates/api.ts index e4c0a1470acb..7903bac16d0c 100644 --- a/packages/global/openapi/admin/app/templates/api.ts +++ b/packages/global/openapi/admin/app/templates/api.ts @@ -12,6 +12,7 @@ import { OpenAPIStoreNodeItemTypeSchema } from '../../../core/workflow/node'; import z from 'zod'; +import { OpenAPIAppTemplateSchema } from '../../../core/app/template/api'; const adminTemplateTypes = new Set([ AppTypeEnum.simple, @@ -157,3 +158,13 @@ export const UpdateQuickTemplateBodySchema = z.object({ templateIds: z.array(z.string()).meta({ description: '设置为快捷模板的模板ID列表' }) }); export type UpdateQuickTemplateBodyType = z.infer; + +export const DeleteTemplateQuerySchema = z.object({ + id: z.string().meta({ description: '模板ID' }) +}); +export type DeleteTemplateQueryType = z.infer; + +export const GetAdminTemplatesResponseSchema = z + .array(OpenAPIAppTemplateSchema) + .meta({ description: '所有应用模板列表' }); +export type GetAdminTemplatesResponseType = z.infer; diff --git a/packages/global/openapi/admin/system/api.ts b/packages/global/openapi/admin/system/api.ts index 22d6fd4c445b..a6ab40eae5b9 100644 --- a/packages/global/openapi/admin/system/api.ts +++ b/packages/global/openapi/admin/system/api.ts @@ -16,6 +16,7 @@ export const GetConfigResponseSchema = z.object({ .optional() .meta({ description: '系统 FastGPT Pro 商业版配置(不含 license)' }) }); +export type GetConfigResponse = z.infer; /* ============================================================================ * API: 更新系统配置 diff --git a/packages/global/openapi/admin/team/api.ts b/packages/global/openapi/admin/team/api.ts index d7632ff6dcca..140c1350c1e4 100644 --- a/packages/global/openapi/admin/team/api.ts +++ b/packages/global/openapi/admin/team/api.ts @@ -1,4 +1,5 @@ import z from 'zod'; +import { ObjectIdSchema } from '../../../common/type/mongo'; import { PaginationResponseSchema, PaginationSchema } from '../../api'; /* ============================================================================ @@ -37,16 +38,23 @@ export const TeamMemberItemSchema = z.object({ status: z.string().meta({ description: '成员状态' }) }); +export const GetTeamMembersQuerySchema = z.object({ + teamId: z.string().meta({ description: '团队ID' }) +}); +export type GetTeamMembersQueryType = z.infer; + export const GetTeamMembersResponseSchema = z.object({ members: z.array(TeamMemberItemSchema).meta({ description: '团队成员列表' }), team: z .object({ - _id: z.string().meta({ description: '团队ID' }), + _id: ObjectIdSchema.meta({ description: '团队ID' }), name: z.string().meta({ description: '团队名称' }) }) .meta({ description: '团队基本信息' }) }); export type GetTeamMembersResponseType = z.infer; +export type UpdateTeamBodyType = z.infer; +export type UpdateTeamResponseType = z.infer; export const UpdateTeamBodySchema = z.object({ id: z.string().meta({ description: '团队ID' }), diff --git a/packages/global/openapi/admin/wallet/bill/invoice/api.ts b/packages/global/openapi/admin/wallet/bill/invoice/api.ts index 3ac855e0c1f4..ce5feaa3cc82 100644 --- a/packages/global/openapi/admin/wallet/bill/invoice/api.ts +++ b/packages/global/openapi/admin/wallet/bill/invoice/api.ts @@ -1,14 +1,31 @@ import z from 'zod'; +import { ObjectIdSchema } from '../../../../../common/type/mongo'; +import { NumSchema } from '../../../../../common/zod'; +import { InvoiceStatusEnum } from '../../../../../support/wallet/bill/invoice/constants'; import { PaginationResponseSchema } from '../../../../api'; export const InvoiceItemSchema = z.object({ - _id: z.string().meta({ description: '发票ID' }), + _id: ObjectIdSchema.meta({ description: '发票ID' }), + teamId: ObjectIdSchema.meta({ description: '团队ID' }), teamName: z.string().meta({ description: '团队名称' }), + unifiedCreditCode: z.string().optional().meta({ description: '统一社会信用代码' }), + companyAddress: z.string().optional().meta({ description: '公司地址' }), + companyPhone: z.string().optional().meta({ description: '公司电话' }), + bankName: z.string().optional().meta({ description: '开户银行' }), + bankAccount: z.string().optional().meta({ description: '开户账号' }), + needSpecialInvoice: z.boolean().optional().meta({ description: '是否需要专票' }), + contactPhone: z.string().optional().meta({ description: '联系电话' }), emailAddress: z.string().meta({ description: '邮箱地址' }), - status: z.string().meta({ description: '发票状态' }), + amount: NumSchema.meta({ description: '开票金额' }), + status: z.nativeEnum(InvoiceStatusEnum).meta({ + description: '发票状态:1-申请中,2-已完成' + }), + billIdList: z.array(ObjectIdSchema).optional().meta({ description: '关联订单ID列表' }), createTime: z.date().meta({ description: '创建时间' }), - finishTime: z.date().optional().meta({ description: '完成时间' }) + finishTime: z.date().optional().meta({ description: '完成时间' }), + file: z.any().optional().meta({ description: '发票文件' }) }); +export type InvoiceItemType = z.infer; export const InvoiceListBodySchema = z.object({ pageNum: z.number().meta({ description: '页码' }), @@ -16,9 +33,14 @@ export const InvoiceListBodySchema = z.object({ search: z.string().optional().meta({ description: '搜索关键词(团队名称)' }) }); export const InvoiceListResponseSchema = PaginationResponseSchema(InvoiceItemSchema); +export type InvoiceListBodyType = z.infer; +export type InvoiceListResponseType = z.infer; // invoice/finish is multipart/form-data export const InvoiceFinishBodySchema = z.object({ invoiceId: z.string().meta({ description: '发票ID' }), file: z.string().optional().meta({ description: '发票文件(multipart 上传)' }) }); +export const InvoiceFinishDataSchema = InvoiceFinishBodySchema; +export type InvoiceFinishBodyType = z.infer; +export type InvoiceFinishDataType = InvoiceFinishBodyType; diff --git a/packages/global/openapi/admin/wallet/plan/api.ts b/packages/global/openapi/admin/wallet/plan/api.ts index 332b37798795..b3815caf6684 100644 --- a/packages/global/openapi/admin/wallet/plan/api.ts +++ b/packages/global/openapi/admin/wallet/plan/api.ts @@ -56,10 +56,13 @@ export const AddPlansBodySchema = z.object({ startTime: z.string().meta({ description: '开始时间' }), expiredTime: z.string().meta({ description: '结束时间' }), price: NumSchema.meta({ description: '价格' }), - level: z.enum(StandardSubLevelEnum).meta({ description: '套餐等级(仅标准套餐需要)' }), - extraDatasetSize: z.number().optional().meta({ description: '额外知识库容量' }), - totalPoints: z.number().optional().meta({ description: '总积分' }), - surplusPoints: z.number().optional().meta({ description: '剩余积分' }) + level: z + .enum(StandardSubLevelEnum) + .optional() + .meta({ description: '套餐等级(仅标准套餐需要)' }), + extraDatasetSize: NumSchema.optional().meta({ description: '额外知识库容量' }), + totalPoints: NumSchema.optional().meta({ description: '总积分' }), + surplusPoints: NumSchema.optional().meta({ description: '剩余积分' }) }); export type AddPlansBodyType = z.infer; diff --git a/pro b/pro index d8879935a322..ff143df66f27 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit d8879935a32297e648ae5b4187213ae35edc5fc7 +Subproject commit ff143df66f278ebcbbf217a6ecb0e9da6d12cfa9 From 2f59eef4abcc08f2d71edfde33ab13528ebb04bc Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 18:11:21 +0800 Subject: [PATCH 03/10] refactor(openapi): use ObjectIdSchema for dataset item ids --- packages/global/openapi/admin/dataset/api.ts | 5 +++-- pro | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/global/openapi/admin/dataset/api.ts b/packages/global/openapi/admin/dataset/api.ts index 5216a4e5589b..2ec22ad08c91 100644 --- a/packages/global/openapi/admin/dataset/api.ts +++ b/packages/global/openapi/admin/dataset/api.ts @@ -1,9 +1,10 @@ import z from 'zod'; +import { ObjectIdSchema } from '../../../common/type/mongo'; import { PaginationResponseSchema } from '../../api'; export const DatasetItemSchema = z.object({ - id: z.string().meta({ description: '知识库ID' }), - teamId: z.string().meta({ description: '所属团队ID' }), + id: ObjectIdSchema.meta({ description: '知识库ID' }), + teamId: ObjectIdSchema.meta({ description: '所属团队ID' }), name: z.string().meta({ description: '知识库名称' }), intro: z.string().meta({ description: '知识库简介' }), username: z.string().meta({ description: '创建者用户名' }), diff --git a/pro b/pro index ff143df66f27..11074975da6e 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit ff143df66f278ebcbbf217a6ecb0e9da6d12cfa9 +Subproject commit 11074975da6ee8cd2a1d40c02523fafff2c9dab5 From e507f70af599ec72606152ca99000f2f193357e1 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 20:59:35 +0800 Subject: [PATCH 04/10] refactor(openapi): align admin mutation schemas, unify open object meta, and sync pro submodule --- packages/global/core/workflow/runtime/type.ts | 85 +++++------- .../template/system/interactive/type.ts | 4 +- .../openapi/admin/app/templateType/index.ts | 21 +-- .../openapi/admin/app/templates/index.ts | 40 ++---- packages/global/openapi/admin/system/api.ts | 8 +- .../global/openapi/admin/system/model/api.ts | 14 -- .../openapi/admin/system/model/index.ts | 11 +- packages/global/openapi/admin/team/api.ts | 2 +- .../openapi/admin/wallet/bill/invoice/api.ts | 29 ++-- .../admin/wallet/bill/invoice/index.ts | 5 +- .../global/openapi/admin/wallet/plan/api.ts | 85 +++++++----- packages/global/openapi/common/other/api.ts | 2 +- packages/global/openapi/core/ai/api.ts | 3 +- .../global/openapi/core/app/common/api.ts | 8 +- .../openapi/core/chat/completion/api.ts | 20 +-- .../global/openapi/core/chat/record/api.ts | 5 +- .../global/openapi/core/plugin/debug/api.ts | 25 ++-- .../openapi/support/user/account/login/api.ts | 5 +- .../test/openapi/admin/apps/templates.test.ts | 26 +++- .../test/openapi/admin/settings.test.ts | 26 ++++ .../test/openapi/admin/settings/model.test.ts | 12 -- .../test/openapi/admin/team/team.test.ts | 50 +++++++ .../test/openapi/admin/wallet/invoice.test.ts | 55 ++++++++ .../test/openapi/admin/wallet/plan.test.ts | 124 ++++++++++++++++++ packages/global/test/openapi/core/app.test.ts | 2 + .../support/user/account/login/api.test.ts | 19 ++- pro | 2 +- .../src/pages/api/admin/system/model/test.ts | 31 ++--- projects/app/src/pages/api/core/app/update.ts | 9 +- .../test/pages/api/core/ai/model/test.test.ts | 4 +- 30 files changed, 470 insertions(+), 262 deletions(-) create mode 100644 packages/global/test/openapi/admin/team/team.test.ts create mode 100644 packages/global/test/openapi/admin/wallet/invoice.test.ts create mode 100644 packages/global/test/openapi/admin/wallet/plan.test.ts diff --git a/packages/global/core/workflow/runtime/type.ts b/packages/global/core/workflow/runtime/type.ts index 1a53e5cb6c41..41a040f9b9fa 100644 --- a/packages/global/core/workflow/runtime/type.ts +++ b/packages/global/core/workflow/runtime/type.ts @@ -8,7 +8,7 @@ import { SearchDataResponseQuoteListItemSchema } from '../../dataset/type'; import { DatasetSearchModeEnum } from '../../dataset/constants'; import { ChatRoleEnum } from '../../chat/constants'; import z from 'zod'; -import { JsonValueOpenApiMeta } from '../../../common/zod/openapi'; +import { JsonValueOpenApiMeta, OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; import type { JSONSchemaInputType } from '../../app/jsonschema'; const AgentPlanNodeStatusSchema = z.enum(['set_plan', 'update_plan', 'ask_question']); @@ -154,13 +154,10 @@ export const DispatchNodeResponseSchema = z // Tool toolInput: z.record(z.string(), z.any()).optional().meta({ description: '工具输入' }), pluginOutput: z.record(z.string(), z.any()).optional().meta({ description: '插件输出' }), - pluginDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: '插件执行详情(递归节点响应)' - }), + pluginDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: '插件执行详情(递归节点响应)' + }), toolParamsResult: z .record(z.string(), z.any()) .optional() @@ -176,13 +173,10 @@ export const DispatchNodeResponseSchema = z // tool call toolCallInputTokens: z.number().optional().meta({ description: '工具调用输入 token' }), toolCallOutputTokens: z.number().optional().meta({ description: '工具调用输出 token' }), - toolDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: '工具执行详情(递归节点响应)' - }), + toolDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: '工具执行详情(递归节点响应)' + }), toolStop: z.boolean().optional(), // Agent call @@ -225,13 +219,10 @@ export const DispatchNodeResponseSchema = z .array(z.any()) .optional() .meta({ items: JsonValueOpenApiMeta, description: '循环输入' }), - loopDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: '循环详情(递归节点响应)' - }), + loopDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: '循环详情(递归节点响应)' + }), loopInputValue: z .any() .optional() @@ -250,21 +241,15 @@ export const DispatchNodeResponseSchema = z .array(z.any()) .optional() .meta({ items: JsonValueOpenApiMeta, description: '并行结果' }), - parallelRunDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: '各任务执行摘要(成功/失败状态)' - }), - parallelDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: '成功任务子工作流完整响应列表', - deprecated: true - }), + parallelRunDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: '各任务执行摘要(成功/失败状态)' + }), + parallelDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: '成功任务子工作流完整响应列表', + deprecated: true + }), // loopRun loopRunInput: z @@ -272,25 +257,19 @@ export const DispatchNodeResponseSchema = z .optional() .meta({ ...JsonValueOpenApiMeta, description: 'loopRun 循环输入(数组或条件模式标记)' }), loopRunIterations: z.number().optional().meta({ description: 'loopRun 实际执行轮数' }), - loopRunHistory: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: 'loopRun 每轮快照' - }), - loopRunDetail: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: 'loopRun 各轮子工作流节点响应聚合', - deprecated: true - }), + loopRunHistory: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: 'loopRun 每轮快照' + }), + loopRunDetail: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: 'loopRun 各轮子工作流节点响应聚合', + deprecated: true + }), childrenResponses: z .array(z.any()) .optional() - .meta({ items: { type: 'object', additionalProperties: true }, description: '子节点响应' }), + .meta({ items: OpenObjectOpenApiMeta, description: '子节点响应' }), // Tools toolId: z.string().optional().meta({ description: '工具 ID' }), diff --git a/packages/global/core/workflow/template/system/interactive/type.ts b/packages/global/core/workflow/template/system/interactive/type.ts index 837f9c3cf483..1d4464e388d8 100644 --- a/packages/global/core/workflow/template/system/interactive/type.ts +++ b/packages/global/core/workflow/template/system/interactive/type.ts @@ -88,9 +88,7 @@ export type LoopInteractive = InteractiveNodeType & { export const LoopRunInteractiveSchema = z.object({ type: z.literal('loopRunInteractive'), params: z.object({ - loopHistory: z - .array(z.any()) - .meta({ items: { type: 'object', additionalProperties: true }, description: '各轮快照' }), + loopHistory: z.array(z.any()).meta({ items: OpenObjectOpenApiMeta, description: '各轮快照' }), childrenResponse: z.any().meta({ ...OpenObjectOpenApiMeta, description: '子工作流交互响应' diff --git a/packages/global/openapi/admin/app/templateType/index.ts b/packages/global/openapi/admin/app/templateType/index.ts index 5f13277cce76..af925aa65219 100644 --- a/packages/global/openapi/admin/app/templateType/index.ts +++ b/packages/global/openapi/admin/app/templateType/index.ts @@ -18,12 +18,7 @@ export const AdminTemplateTypePath: OpenAPIPath = { }, responses: { 200: { - description: '保存成功', - content: { - 'application/json': { - schema: {} - } - } + description: '保存成功' } } } @@ -40,12 +35,7 @@ export const AdminTemplateTypePath: OpenAPIPath = { }, responses: { 200: { - description: '删除成功', - content: { - 'application/json': { - schema: {} - } - } + description: '删除成功' } } } @@ -64,12 +54,7 @@ export const AdminTemplateTypePath: OpenAPIPath = { }, responses: { 200: { - description: '排序更新成功', - content: { - 'application/json': { - schema: {} - } - } + description: '排序更新成功' } } } diff --git a/packages/global/openapi/admin/app/templates/index.ts b/packages/global/openapi/admin/app/templates/index.ts index 2e4f37936d5d..dac51b7b23d5 100644 --- a/packages/global/openapi/admin/app/templates/index.ts +++ b/packages/global/openapi/admin/app/templates/index.ts @@ -5,7 +5,8 @@ import { CreateTemplateBodySchema, UpdateTemplateBodySchema, UpdateTemplateOrderBodySchema, - UpdateQuickTemplateBodySchema + UpdateQuickTemplateBodySchema, + GetAdminTemplatesResponseSchema } from './api'; export const AdminTemplatePath: OpenAPIPath = { @@ -23,12 +24,7 @@ export const AdminTemplatePath: OpenAPIPath = { }, responses: { 200: { - description: '创建成功', - content: { - 'application/json': { - schema: {} - } - } + description: '创建成功' } } } @@ -43,7 +39,7 @@ export const AdminTemplatePath: OpenAPIPath = { description: '成功获取模板列表', content: { 'application/json': { - schema: {} + schema: GetAdminTemplatesResponseSchema } } } @@ -64,12 +60,7 @@ export const AdminTemplatePath: OpenAPIPath = { }, responses: { 200: { - description: '更新成功', - content: { - 'application/json': { - schema: {} - } - } + description: '更新成功' } } } @@ -86,12 +77,7 @@ export const AdminTemplatePath: OpenAPIPath = { }, responses: { 200: { - description: '删除成功', - content: { - 'application/json': { - schema: {} - } - } + description: '删除成功' } } } @@ -110,12 +96,7 @@ export const AdminTemplatePath: OpenAPIPath = { }, responses: { 200: { - description: '排序更新成功', - content: { - 'application/json': { - schema: {} - } - } + description: '排序更新成功' } } } @@ -134,12 +115,7 @@ export const AdminTemplatePath: OpenAPIPath = { }, responses: { 200: { - description: '设置成功', - content: { - 'application/json': { - schema: {} - } - } + description: '设置成功' } } } diff --git a/packages/global/openapi/admin/system/api.ts b/packages/global/openapi/admin/system/api.ts index a6ab40eae5b9..ecdc1b219aec 100644 --- a/packages/global/openapi/admin/system/api.ts +++ b/packages/global/openapi/admin/system/api.ts @@ -1,5 +1,6 @@ import z from 'zod'; import { SubPlanInputSchema } from '../../../support/wallet/sub/type'; +import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; /* ============================================================================ * API: 获取系统配置 @@ -10,11 +11,14 @@ import { SubPlanInputSchema } from '../../../support/wallet/sub/type'; * ============================================================================ */ export const GetConfigResponseSchema = z.object({ - fastgpt: z.any().optional().meta({ description: '系统 FastGPT 配置' }), + fastgpt: z + .any() + .optional() + .meta({ ...OpenObjectOpenApiMeta, description: '系统 FastGPT 配置' }), fastgptPro: z .any() .optional() - .meta({ description: '系统 FastGPT Pro 商业版配置(不含 license)' }) + .meta({ ...OpenObjectOpenApiMeta, description: '系统 FastGPT Pro 商业版配置(不含 license)' }) }); export type GetConfigResponse = z.infer; diff --git a/packages/global/openapi/admin/system/model/api.ts b/packages/global/openapi/admin/system/model/api.ts index 9c636b393e4d..0621793bdee6 100644 --- a/packages/global/openapi/admin/system/model/api.ts +++ b/packages/global/openapi/admin/system/model/api.ts @@ -142,20 +142,6 @@ export const TestAdminSystemModelQuerySchema = AdminSystemModelReferenceSchema.e }) }); export type TestAdminSystemModelQuery = z.infer; -/** 模型连通性测试结果;TTS、STT 和 rerank 测试成功时没有业务返回值。 */ -export const TestAdminSystemModelResponseSchema = z - .union([ - z.string().meta({ description: 'LLM 的回答文本' }), - z.object({ - tokens: z.number().int().nonnegative().meta({ description: 'Embedding 输入 token 数' }), - vectors: z - .array(z.array(z.number())) - .meta({ description: 'Embedding 向量数组,顺序与输入一致' }) - }) - ]) - .optional() - .meta({ description: '模型连通性测试结果' }); -export type TestAdminSystemModelResponse = z.infer; /* ============================================================================ * API: 测试新增或编辑中的管理员系统模型草稿 diff --git a/packages/global/openapi/admin/system/model/index.ts b/packages/global/openapi/admin/system/model/index.ts index 75b15c031530..7d51ac3df763 100644 --- a/packages/global/openapi/admin/system/model/index.ts +++ b/packages/global/openapi/admin/system/model/index.ts @@ -14,7 +14,6 @@ import { ReplaceSystemModelChannelsBodySchema, TestAdminSystemModelQuerySchema, TestDraftAdminSystemModelBodySchema, - TestAdminSystemModelResponseSchema, UpdateDefaultModelsBodySchema, UpdateSystemModelBodySchema, UpdateSystemModelStatusBodySchema, @@ -118,10 +117,7 @@ export const AdminSystemModelPath: OpenAPIPath = { tags: [DevApiTagsMap.adminSystemModel], requestParams: { query: TestAdminSystemModelQuerySchema }, responses: { - 200: { - description: '模型测试结果', - content: { 'application/json': { schema: TestAdminSystemModelResponseSchema } } - } + 200: { description: '测试成功' } } }, post: { @@ -132,10 +128,7 @@ export const AdminSystemModelPath: OpenAPIPath = { content: { 'application/json': { schema: TestDraftAdminSystemModelBodySchema } } }, responses: { - 200: { - description: '模型测试结果', - content: { 'application/json': { schema: TestAdminSystemModelResponseSchema } } - } + 200: { description: '测试成功' } } } }, diff --git a/packages/global/openapi/admin/team/api.ts b/packages/global/openapi/admin/team/api.ts index 140c1350c1e4..03dd8bcacff4 100644 --- a/packages/global/openapi/admin/team/api.ts +++ b/packages/global/openapi/admin/team/api.ts @@ -32,7 +32,7 @@ export const GetTeamsResponseSchema = PaginationResponseSchema(TeamItemSchema); export type GetTeamsResponseType = z.infer; export const TeamMemberItemSchema = z.object({ - userName: z.string().meta({ description: '成员用户名' }), + userName: z.string().default('').meta({ description: '成员用户名' }), teamId: z.string().meta({ description: '团队ID' }), role: z.string().meta({ description: '成员角色' }), status: z.string().meta({ description: '成员状态' }) diff --git a/packages/global/openapi/admin/wallet/bill/invoice/api.ts b/packages/global/openapi/admin/wallet/bill/invoice/api.ts index ce5feaa3cc82..52f015ae5368 100644 --- a/packages/global/openapi/admin/wallet/bill/invoice/api.ts +++ b/packages/global/openapi/admin/wallet/bill/invoice/api.ts @@ -1,6 +1,6 @@ import z from 'zod'; import { ObjectIdSchema } from '../../../../../common/type/mongo'; -import { NumSchema } from '../../../../../common/zod'; +import { IntSchema, NumSchema } from '../../../../../common/zod'; import { InvoiceStatusEnum } from '../../../../../support/wallet/bill/invoice/constants'; import { PaginationResponseSchema } from '../../../../api'; @@ -20,16 +20,15 @@ export const InvoiceItemSchema = z.object({ status: z.nativeEnum(InvoiceStatusEnum).meta({ description: '发票状态:1-申请中,2-已完成' }), - billIdList: z.array(ObjectIdSchema).optional().meta({ description: '关联订单ID列表' }), + billIdList: z.array(z.string()).optional().meta({ description: '关联订单ID列表' }), createTime: z.date().meta({ description: '创建时间' }), - finishTime: z.date().optional().meta({ description: '完成时间' }), - file: z.any().optional().meta({ description: '发票文件' }) + finishTime: z.date().optional().meta({ description: '完成时间' }) }); export type InvoiceItemType = z.infer; export const InvoiceListBodySchema = z.object({ - pageNum: z.number().meta({ description: '页码' }), - pageSize: z.number().meta({ description: '每页条数' }), + pageNum: IntSchema.positive().optional().default(1).meta({ description: '页码' }), + pageSize: IntSchema.positive().optional().default(10).meta({ description: '每页条数' }), search: z.string().optional().meta({ description: '搜索关键词(团队名称)' }) }); export const InvoiceListResponseSchema = PaginationResponseSchema(InvoiceItemSchema); @@ -37,10 +36,16 @@ export type InvoiceListBodyType = z.infer; export type InvoiceListResponseType = z.infer; // invoice/finish is multipart/form-data -export const InvoiceFinishBodySchema = z.object({ - invoiceId: z.string().meta({ description: '发票ID' }), - file: z.string().optional().meta({ description: '发票文件(multipart 上传)' }) +export const InvoiceFinishDataSchema = z.object({ + invoiceId: z.string().meta({ description: '发票ID' }) }); -export const InvoiceFinishDataSchema = InvoiceFinishBodySchema; -export type InvoiceFinishBodyType = z.infer; -export type InvoiceFinishDataType = InvoiceFinishBodyType; +export type InvoiceFinishDataType = z.infer; + +export const InvoiceFinishFormSchema = z.object({ + file: z.string().meta({ format: 'binary', description: '发票文件(multipart 上传)' }), + data: InvoiceFinishDataSchema.meta({ description: 'JSON 序列化后的参数对象' }) +}); +export type InvoiceFinishFormType = z.infer; + +export const InvoiceFinishBodySchema = InvoiceFinishDataSchema; +export type InvoiceFinishBodyType = InvoiceFinishDataType; diff --git a/packages/global/openapi/admin/wallet/bill/invoice/index.ts b/packages/global/openapi/admin/wallet/bill/invoice/index.ts index 34670d876316..35b0308d28f5 100644 --- a/packages/global/openapi/admin/wallet/bill/invoice/index.ts +++ b/packages/global/openapi/admin/wallet/bill/invoice/index.ts @@ -1,6 +1,6 @@ import type { OpenAPIPath } from '../../../../type'; import { DevApiTagsMap } from '../../../../tag'; -import { InvoiceListBodySchema, InvoiceListResponseSchema, InvoiceFinishBodySchema } from './api'; +import { InvoiceListBodySchema, InvoiceListResponseSchema, InvoiceFinishFormSchema } from './api'; import { InvoiceDownloadFileContentSchema, InvoiceDownloadFileQuerySchema @@ -39,7 +39,8 @@ export const AdminInvoicePath: OpenAPIPath = { requestBody: { content: { 'multipart/form-data': { - schema: InvoiceFinishBodySchema + schema: InvoiceFinishFormSchema, + encoding: { data: { contentType: 'application/json' } } } } }, diff --git a/packages/global/openapi/admin/wallet/plan/api.ts b/packages/global/openapi/admin/wallet/plan/api.ts index b3815caf6684..a278893f9f8f 100644 --- a/packages/global/openapi/admin/wallet/plan/api.ts +++ b/packages/global/openapi/admin/wallet/plan/api.ts @@ -50,46 +50,69 @@ export const GetPlansResponseSchema = PaginationResponseSchema(PlanItemSchema); export type GetPlansResponseType = z.infer; // addPlans -export const AddPlansBodySchema = z.object({ +const AddPlansBaseSchema = z.object({ teamId: z.string().meta({ description: '团队ID' }), - type: z.enum(SubTypeEnum).meta({ description: '套餐类型' }), startTime: z.string().meta({ description: '开始时间' }), expiredTime: z.string().meta({ description: '结束时间' }), - price: NumSchema.meta({ description: '价格' }), - level: z - .enum(StandardSubLevelEnum) - .optional() - .meta({ description: '套餐等级(仅标准套餐需要)' }), - extraDatasetSize: NumSchema.optional().meta({ description: '额外知识库容量' }), - totalPoints: NumSchema.optional().meta({ description: '总积分' }), - surplusPoints: NumSchema.optional().meta({ description: '剩余积分' }) + price: NumSchema.meta({ description: '价格' }) }); + +export const AddPlansBodySchema = z.discriminatedUnion('type', [ + AddPlansBaseSchema.extend({ + type: z.literal(SubTypeEnum.standard).meta({ description: '标准套餐' }), + level: z.enum(StandardSubLevelEnum).meta({ description: '套餐等级' }), + totalPoints: NumSchema.optional().meta({ description: '总积分' }), + surplusPoints: NumSchema.optional().meta({ description: '剩余积分' }) + }), + AddPlansBaseSchema.extend({ + type: z.literal(SubTypeEnum.extraDatasetSize).meta({ description: '额外知识库容量' }), + extraDatasetSize: NumSchema.meta({ description: '额外知识库容量' }) + }), + AddPlansBaseSchema.extend({ + type: z.literal(SubTypeEnum.extraPoints).meta({ description: '额外积分' }), + totalPoints: NumSchema.meta({ description: '总积分' }), + surplusPoints: NumSchema.meta({ description: '剩余积分' }) + }) +]); export type AddPlansBodyType = z.infer; // updatePlan -export const UpdatePlanBodySchema = z.object({ +const UpdatePlanBaseSchema = z.object({ id: z.string().meta({ description: '订阅ID' }), - type: z.enum(SubTypeEnum).meta({ description: '套餐类型' }), startTime: z.string().meta({ description: '开始时间' }), expiredTime: z.string().meta({ description: '结束时间' }), - price: NumSchema.meta({ description: '价格' }), - totalPoints: NumSchema.optional().meta({ description: '总积分' }), - surplusPoints: NumSchema.optional().meta({ description: '剩余积分' }), - extraDatasetSize: NumSchema.optional().meta({ description: '额外知识库容量' }), - level: z.enum(StandardSubLevelEnum).optional().meta({ description: '套餐等级' }), - maxTeamMember: NumSchema.optional().meta({ description: '最大团队成员数' }), - maxApp: NumSchema.optional().meta({ description: '最大应用数' }), - maxDataset: NumSchema.optional().meta({ description: '最大知识库数' }), - maxDatasetSize: NumSchema.optional().meta({ description: '最大知识库容量' }), - requestsPerMinute: NumSchema.optional().meta({ description: '每分钟请求数' }), - websiteSyncPerDataset: NumSchema.optional().meta({ description: '每知识库站点同步数' }), - chatHistoryStoreDuration: NumSchema.optional().meta({ description: '聊天记录保存天数' }), - appRegistrationCount: NumSchema.optional().meta({ description: '应用注册数' }), - auditLogStoreDuration: NumSchema.optional().meta({ description: '审计日志保存天数' }), - ticketResponseTime: NumSchema.optional().meta({ description: '工单响应时间' }), - customDomain: NumSchema.optional().meta({ description: '自定义域名数' }), - maxUploadFileSize: NumSchema.optional().meta({ description: '最大上传文件大小' }), - maxUploadFileCount: NumSchema.optional().meta({ description: '最大上传文件数' }), - enableSandbox: z.boolean().optional().meta({ description: '是否启用沙盒' }) + price: NumSchema.meta({ description: '价格' }) }); + +export const UpdatePlanBodySchema = z.discriminatedUnion('type', [ + UpdatePlanBaseSchema.extend({ + type: z.literal(SubTypeEnum.standard).meta({ description: '标准套餐' }), + level: z.enum(StandardSubLevelEnum).meta({ description: '套餐等级' }), + totalPoints: NumSchema.optional().meta({ description: '总积分' }), + surplusPoints: NumSchema.optional().meta({ description: '剩余积分' }), + maxTeamMember: NumSchema.optional().meta({ description: '最大团队成员数' }), + maxApp: NumSchema.optional().meta({ description: '最大应用数' }), + maxDataset: NumSchema.optional().meta({ description: '最大知识库数' }), + maxDatasetSize: NumSchema.optional().meta({ description: '最大知识库容量' }), + requestsPerMinute: NumSchema.optional().meta({ description: '每分钟请求数' }), + websiteSyncPerDataset: NumSchema.optional().meta({ description: '每知识库站点同步数' }), + chatHistoryStoreDuration: NumSchema.optional().meta({ description: '聊天记录保存天数' }), + appRegistrationCount: NumSchema.optional().meta({ description: '应用注册数' }), + auditLogStoreDuration: NumSchema.optional().meta({ description: '审计日志保存天数' }), + ticketResponseTime: NumSchema.optional().meta({ description: '工单响应时间' }), + customDomain: NumSchema.optional().meta({ description: '自定义域名数' }), + maxUploadFileSize: NumSchema.optional().meta({ description: '最大上传文件大小' }), + maxUploadFileCount: NumSchema.optional().meta({ description: '最大上传文件数' }), + enableSandbox: z.boolean().optional().meta({ description: '是否启用沙盒' }) + }), + UpdatePlanBaseSchema.extend({ + type: z.literal(SubTypeEnum.extraDatasetSize).meta({ description: '额外知识库容量' }), + extraDatasetSize: NumSchema.meta({ description: '额外知识库容量' }) + }), + UpdatePlanBaseSchema.extend({ + type: z.literal(SubTypeEnum.extraPoints).meta({ description: '额外积分' }), + totalPoints: NumSchema.meta({ description: '总积分' }), + surplusPoints: NumSchema.meta({ description: '剩余积分' }) + }) +]); export type UpdatePlanBodyType = z.infer; diff --git a/packages/global/openapi/common/other/api.ts b/packages/global/openapi/common/other/api.ts index 9c19614aa692..9b3940b173fb 100644 --- a/packages/global/openapi/common/other/api.ts +++ b/packages/global/openapi/common/other/api.ts @@ -35,7 +35,7 @@ export const PushTrackBodySchema = z.object({ example: TrackEnum.useAppTemplate, description: '埋点事件类型' }), - data: z.json().meta({ + data: z.record(z.string(), z.any()).meta({ example: { id: 'app-template-id', name: '示例模板' }, description: '事件关联数据,结构由事件类型决定' }) diff --git a/packages/global/openapi/core/ai/api.ts b/packages/global/openapi/core/ai/api.ts index 94ed47e375e0..79991783c7d8 100644 --- a/packages/global/openapi/core/ai/api.ts +++ b/packages/global/openapi/core/ai/api.ts @@ -1,5 +1,6 @@ import { ObjectIdSchema } from '../../../common/type/mongo'; import z from 'zod'; +import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; import { ChatGenerateStatusSchema, createOutLinkChatTargetInputSchema, @@ -112,7 +113,7 @@ export type ResumeStreamRuntimeParams = z.infer export const StreamResumeCompletedRecordsSchema = z.object({ list: z.array(z.any()).meta({ - items: { type: 'object', additionalProperties: true }, + items: OpenObjectOpenApiMeta, description: '最新已落库的聊天记录' }), total: z.number().int().nonnegative().meta({ diff --git a/packages/global/openapi/core/app/common/api.ts b/packages/global/openapi/core/app/common/api.ts index 4116476a46d1..c015417ccac8 100644 --- a/packages/global/openapi/core/app/common/api.ts +++ b/packages/global/openapi/core/app/common/api.ts @@ -502,13 +502,7 @@ export const UpdateAppBodySchema = z }); export type UpdateAppBodyType = z.infer; -export const UpdateAppResponseSchema = z - .union([ - z.looseObject({}).meta({ description: 'MongoDB 更新结果' }), - z.null().meta({ description: '移动应用时无返回数据' }) - ]) - .optional() - .meta({ description: '应用更新结果' }); +export const UpdateAppResponseSchema = z.undefined().meta({ description: '更新成功' }); export type UpdateAppResponseType = z.infer; /* ============================================================================ diff --git a/packages/global/openapi/core/chat/completion/api.ts b/packages/global/openapi/core/chat/completion/api.ts index 563858fd4c2b..357a1ca16042 100644 --- a/packages/global/openapi/core/chat/completion/api.ts +++ b/packages/global/openapi/core/chat/completion/api.ts @@ -1,4 +1,4 @@ -import { requiredAlternatives } from '../../../../common/zod/openapi'; +import { OpenObjectOpenApiMeta, requiredAlternatives } from '../../../../common/zod/openapi'; import z from 'zod'; import { ObjectIdSchema } from '../../../../common/type/mongo'; import { ChatCompletionMessageParamSchema } from '../../../../core/ai/llm/type'; @@ -144,10 +144,7 @@ export type CompletionsProps = z.infer; const ChatCompletionResponseMessageSchema = z.object({ role: z.literal('assistant').meta({ description: '消息角色' }), content: z.any().meta({ - anyOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'object', additionalProperties: true } } - ], + anyOf: [{ type: 'string' }, { type: 'array', items: OpenObjectOpenApiMeta }], description: '消息内容。普通对话为字符串;detail=true 或工作流命中交互节点时,可能为按字段名区分的对象数组(如 text / interactive / tool / file / reasoning)。v1 会额外补充 type 字段,取值为 text / interactive / tool / file / reasoning;v2 不补充 type。当元素包含 interactive 字段时,只返回交互展示配置,不返回 entryNodeIds / memoryEdges / nodeOutputs 等内部运行态字段' }), @@ -180,14 +177,11 @@ export const CompletionsResponseSchema = z.object({ description: 'Token 用量。v1 接口为占位值,需要时请从 responseData 计算' }), choices: z.array(ChatCompletionChoiceSchema).meta({ description: '回复选项列表' }), - responseData: z - .array(z.any()) - .optional() - .meta({ - items: { type: 'object', additionalProperties: true }, - description: - '各节点详细响应数据(仅 detail=true 时返回)。每项是一个节点的执行结果,常见字段如 moduleName / moduleType / runningTime / quoteList 等' - }), + responseData: z.array(z.any()).optional().meta({ + items: OpenObjectOpenApiMeta, + description: + '各节点详细响应数据(仅 detail=true 时返回)。每项是一个节点的执行结果,常见字段如 moduleName / moduleType / runningTime / quoteList 等' + }), newVariables: z .record(z.string(), z.any()) .optional() diff --git a/packages/global/openapi/core/chat/record/api.ts b/packages/global/openapi/core/chat/record/api.ts index d0d72b830873..52dafb15f852 100644 --- a/packages/global/openapi/core/chat/record/api.ts +++ b/packages/global/openapi/core/chat/record/api.ts @@ -1,6 +1,7 @@ import z from 'zod'; import { OutLinkChatAuthSchema } from '../../../../support/permission/chat'; import { ObjectIdSchema } from '../../../../common/type/mongo'; +import { OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; import { DatasetCiteItemSchema } from '../../../../core/dataset/type'; import { LinkedListResponseSchema, LinkedPaginationSchema, PaginationSchema } from '../../../api'; import { ChatItemMiniSchema } from '../../../../core/chat/type'; @@ -176,9 +177,7 @@ export type GetPaginationRecordsBodyType = z.infer; export const GetPaginationRecordsResponseSchema = z.object({ - list: z - .array(z.any()) - .meta({ items: { type: 'object', additionalProperties: true }, description: '对话列表' }), + list: z.array(z.any()).meta({ items: OpenObjectOpenApiMeta, description: '对话列表' }), total: z.number().int().nonnegative().meta({ example: 10, description: '总数' }) }); export type GetPaginationRecordsResponseType = z.infer; diff --git a/packages/global/openapi/core/plugin/debug/api.ts b/packages/global/openapi/core/plugin/debug/api.ts index a177ccfc72c3..c3419d061f04 100644 --- a/packages/global/openapi/core/plugin/debug/api.ts +++ b/packages/global/openapi/core/plugin/debug/api.ts @@ -1,5 +1,5 @@ import z from 'zod'; -import { OpenObjectOpenApiMeta } from '../../../../common/zod/openapi'; +import { I18nStringSchema } from '../../../../core/plugin/type'; export const PluginDebugChannelStatusSchema = z.enum([ 'enabled', @@ -118,25 +118,20 @@ export const PluginDebugChannelPluginSchema = z example: '0.0.1', description: '调试插件版本' }), - name: z.unknown().meta({ - ...OpenObjectOpenApiMeta, + name: I18nStringSchema.meta({ example: { en: 'Get Time', 'zh-CN': '获取时间' }, - description: '调试插件名称,保持 plugin-server 原始 i18n 结构' + description: '调试插件名称' + }), + description: I18nStringSchema.optional().meta({ + example: { + en: 'Get current time', + 'zh-CN': '获取当前时间' + }, + description: '调试插件简介' }), - description: z - .unknown() - .optional() - .meta({ - ...OpenObjectOpenApiMeta, - example: { - en: 'Get current time', - 'zh-CN': '获取当前时间' - }, - description: '调试插件简介,保持 plugin-server 原始 i18n 结构' - }), icon: z.string().optional().meta({ example: 'https://fastgpt.example.com/icon.png', description: '调试插件图标' diff --git a/packages/global/openapi/support/user/account/login/api.ts b/packages/global/openapi/support/user/account/login/api.ts index d9c6c81aa827..f48db85e2124 100644 --- a/packages/global/openapi/support/user/account/login/api.ts +++ b/packages/global/openapi/support/user/account/login/api.ts @@ -34,10 +34,7 @@ export const OpenAPIUserSchema = UserSchema.omit({ export type OpenAPIUserType = z.infer; export const LoginSuccessResponseSchema = z.object({ - // 登录结果长期作为 UserDetailType 使用;在业务类型统一前保持运行时和 TS 兼容, - // 文档明确它是对象,避免客户端生成 null schema。 - user: z.any().meta({ - ...OpenObjectOpenApiMeta, + user: OpenAPIUserSchema.meta({ description: '用户详情' }), token: z.string().meta({ diff --git a/packages/global/test/openapi/admin/apps/templates.test.ts b/packages/global/test/openapi/admin/apps/templates.test.ts index ea433194a027..9bbc037bbab3 100644 --- a/packages/global/test/openapi/admin/apps/templates.test.ts +++ b/packages/global/test/openapi/admin/apps/templates.test.ts @@ -2,8 +2,11 @@ import { describe, expect, it } from 'vitest'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { CreateTemplateBodySchema, - UpdateTemplateBodySchema + UpdateTemplateBodySchema, + GetAdminTemplatesResponseSchema } from '@fastgpt/global/openapi/admin/app/templates/api'; +import { AdminTemplatePath } from '@fastgpt/global/openapi/admin/app/templates'; +import { AdminTemplateTypePath } from '@fastgpt/global/openapi/admin/app/templateType'; const templateBase = { name: 'Template', @@ -128,4 +131,25 @@ describe('admin app template schemas', () => { }).success ).toBe(false); }); + + it('configures proper OpenAPI response schemas for template routes', () => { + const listRoute = AdminTemplatePath['/proApi/admin/app/templates/list']?.get; + expect(listRoute?.responses?.[200]?.content?.['application/json']?.schema).toBe( + GetAdminTemplatesResponseSchema + ); + + for (const route of [ + AdminTemplatePath['/proApi/admin/app/templates/create']?.post, + AdminTemplatePath['/proApi/admin/app/templates/update']?.put, + AdminTemplatePath['/proApi/admin/app/templates/delete']?.delete, + AdminTemplatePath['/proApi/admin/app/templates/updateOrder']?.put, + AdminTemplatePath['/proApi/admin/app/templates/updateQuickTemplate']?.put, + AdminTemplateTypePath['/proApi/admin/app/templateType/save']?.post, + AdminTemplateTypePath['/proApi/admin/app/templateType/delete']?.delete, + AdminTemplateTypePath['/proApi/admin/app/templateType/updateOrder']?.put + ]) { + expect(route?.responses?.[200]).toBeDefined(); + expect(route?.responses?.[200]?.content).toBeUndefined(); + } + }); }); diff --git a/packages/global/test/openapi/admin/settings.test.ts b/packages/global/test/openapi/admin/settings.test.ts index 600df023431e..d654a8110cc8 100644 --- a/packages/global/test/openapi/admin/settings.test.ts +++ b/packages/global/test/openapi/admin/settings.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest'; +import { createDocument } from 'zod-openapi'; import { + GetConfigResponseSchema, UpdateConfigBodySchema, UpdateConfigResponseSchema } from '../../../openapi/admin/system/api'; @@ -69,4 +71,28 @@ describe('UpdateConfigBodySchema', () => { it('uses an empty success response contract', () => { expect(UpdateConfigResponseSchema.parse(undefined)).toBeUndefined(); }); + + it('declares open object schemas for dynamic system configurations', () => { + const parsed = GetConfigResponseSchema.parse({ + fastgpt: { feConfigs: { isPlus: true } }, + fastgptPro: { someProKey: 'val' } + }); + expect(parsed.fastgpt).toEqual({ feConfigs: { isPlus: true } }); + expect(parsed.fastgptPro).toEqual({ someProKey: 'val' }); + + const doc = createDocument({ + openapi: '3.1.0', + info: { title: 'Test', version: '1.0.0' }, + components: { + schemas: { + GetConfigResponse: GetConfigResponseSchema + } + } + }); + const schema = doc.components?.schemas?.GetConfigResponse as any; + expect(schema.properties.fastgpt.type).toBe('object'); + expect(schema.properties.fastgpt.additionalProperties).toBe(true); + expect(schema.properties.fastgptPro.type).toBe('object'); + expect(schema.properties.fastgptPro.additionalProperties).toBe(true); + }); }); diff --git a/packages/global/test/openapi/admin/settings/model.test.ts b/packages/global/test/openapi/admin/settings/model.test.ts index 331fc9fe3c25..f98a12d4e6e5 100644 --- a/packages/global/test/openapi/admin/settings/model.test.ts +++ b/packages/global/test/openapi/admin/settings/model.test.ts @@ -7,7 +7,6 @@ import { DeleteSystemModelsBodySchema, ImportedSystemModelSchema, ReplaceSystemModelChannelsBodySchema, - TestAdminSystemModelResponseSchema, TestAdminSystemModelQuerySchema, UpdateSystemModelBodySchema, UpdateSystemModelStatusBodySchema @@ -61,16 +60,6 @@ describe('admin system model API schemas', () => { expect(modelData?.oneOf).toHaveLength(5); }); - it('describes each model test result without a documentation-only override', () => { - expect(TestAdminSystemModelResponseSchema.parse('Hello')).toBe('Hello'); - expect(TestAdminSystemModelResponseSchema.parse({ tokens: 2, vectors: [[0.1, 0.2]] })).toEqual({ - tokens: 2, - vectors: [[0.1, 0.2]] - }); - expect(TestAdminSystemModelResponseSchema.parse(undefined)).toBeUndefined(); - expect(() => TestAdminSystemModelResponseSchema.parse([[0.1, 0.2]])).toThrow(); - }); - it('validates unique model IDs for batch status and delete operations', () => { const modelIds = ['68ad85a7463006c963799a05', '68ad85a7463006c963799a06']; @@ -249,4 +238,3 @@ describe('admin system model API schemas', () => { expect(parsed.config).not.toHaveProperty('unknownConfig'); }); }); - diff --git a/packages/global/test/openapi/admin/team/team.test.ts b/packages/global/test/openapi/admin/team/team.test.ts new file mode 100644 index 000000000000..b8a1d3f300fa --- /dev/null +++ b/packages/global/test/openapi/admin/team/team.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { + TeamMemberItemSchema, + GetTeamMembersResponseSchema +} from '../../../../openapi/admin/team/api'; + +describe('Admin team schemas', () => { + it('tolerates missing username for orphan members with default empty string', () => { + const normal = TeamMemberItemSchema.parse({ + userName: 'alice', + teamId: '68ad85a7463006c963799a05', + role: 'owner', + status: 'active' + }); + expect(normal.userName).toBe('alice'); + + const orphan = TeamMemberItemSchema.parse({ + teamId: '68ad85a7463006c963799a05', + role: 'member', + status: 'active' + }); + expect(orphan.userName).toBe(''); + }); + + it('parses team members response successfully even with orphan members', () => { + const response = GetTeamMembersResponseSchema.parse({ + members: [ + { + userName: 'bob', + teamId: '68ad85a7463006c963799a05', + role: 'admin', + status: 'active' + }, + { + userName: '', + teamId: '68ad85a7463006c963799a05', + role: 'member', + status: 'active' + } + ], + team: { + _id: '68ad85a7463006c963799a05', + name: 'Engineering' + } + }); + + expect(response.members).toHaveLength(2); + expect(response.members[1].userName).toBe(''); + }); +}); diff --git a/packages/global/test/openapi/admin/wallet/invoice.test.ts b/packages/global/test/openapi/admin/wallet/invoice.test.ts new file mode 100644 index 000000000000..f48db9dc1798 --- /dev/null +++ b/packages/global/test/openapi/admin/wallet/invoice.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { + InvoiceItemSchema, + InvoiceListBodySchema, + InvoiceFinishDataSchema, + InvoiceFinishFormSchema +} from '../../../../openapi/admin/wallet/bill/invoice/api'; +import { AdminInvoicePath } from '../../../../openapi/admin/wallet/bill/invoice'; +import { InvoiceStatusEnum } from '../../../../support/wallet/bill/invoice/constants'; + +describe('Admin invoice schemas', () => { + const baseInvoice = { + _id: '68ad85a7463006c963799a05', + teamId: '68ad85a7463006c963799a06', + teamName: 'Test Team', + emailAddress: 'test@example.com', + amount: 500, + status: InvoiceStatusEnum.completed, + createTime: new Date('2026-01-01T00:00:00.000Z') + }; + + it('accepts both ObjectId and custom string IDs in billIdList', () => { + const withCustomStringBillId = InvoiceItemSchema.parse({ + ...baseInvoice, + billIdList: ['legacy-order-12345', '68ad85a7463006c963799a07'] + }); + expect(withCustomStringBillId.billIdList).toEqual([ + 'legacy-order-12345', + '68ad85a7463006c963799a07' + ]); + }); + + it('provides default pagination values for invoice list requests', () => { + const parsed = InvoiceListBodySchema.parse({}); + expect(parsed.pageNum).toBe(1); + expect(parsed.pageSize).toBe(10); + + const custom = InvoiceListBodySchema.parse({ pageNum: 2, pageSize: 20, search: 'demo' }); + expect(custom.pageNum).toBe(2); + expect(custom.pageSize).toBe(20); + expect(custom.search).toBe('demo'); + }); + + it('validates invoice finish data and multipart form contracts', () => { + const data = InvoiceFinishDataSchema.parse({ invoiceId: '68ad85a7463006c963799a05' }); + expect(data.invoiceId).toBe('68ad85a7463006c963799a05'); + + expect(InvoiceFinishDataSchema.safeParse({}).success).toBe(false); + + const finishRoute = AdminInvoicePath['/proApi/admin/wallet/bill/invoice/finish']?.post; + const multipartContent = finishRoute?.requestBody?.content?.['multipart/form-data']; + expect(multipartContent?.schema).toBe(InvoiceFinishFormSchema); + expect(multipartContent?.encoding?.data?.contentType).toBe('application/json'); + }); +}); diff --git a/packages/global/test/openapi/admin/wallet/plan.test.ts b/packages/global/test/openapi/admin/wallet/plan.test.ts new file mode 100644 index 000000000000..5d81c7cecffa --- /dev/null +++ b/packages/global/test/openapi/admin/wallet/plan.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import { + AddPlansBodySchema, + UpdatePlanBodySchema +} from '../../../../openapi/admin/wallet/plan/api'; +import { AdminPlanPath } from '../../../../openapi/admin/wallet/plan'; +import { StandardSubLevelEnum, SubTypeEnum } from '../../../../support/wallet/sub/constants'; + +describe('Admin plan schemas', () => { + const basePlan = { + startTime: '2026-01-01T00:00:00.000Z', + expiredTime: '2027-01-01T00:00:00.000Z', + price: 100 + }; + + describe('AddPlansBodySchema', () => { + it('validates standard plans with required level', () => { + const valid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.standard, + level: StandardSubLevelEnum.basic, + totalPoints: 1000, + surplusPoints: 1000 + }); + expect(valid.success).toBe(true); + + const invalid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.standard + }); + expect(invalid.success).toBe(false); + }); + + it('validates extraDatasetSize plans with required capacity', () => { + const valid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraDatasetSize, + extraDatasetSize: 1024 + }); + expect(valid.success).toBe(true); + + const invalid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraDatasetSize + }); + expect(invalid.success).toBe(false); + }); + + it('validates extraPoints plans with required points', () => { + const valid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraPoints, + totalPoints: 5000, + surplusPoints: 5000 + }); + expect(valid.success).toBe(true); + + const invalid = AddPlansBodySchema.safeParse({ + ...basePlan, + teamId: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraPoints + }); + expect(invalid.success).toBe(false); + }); + }); + + describe('UpdatePlanBodySchema', () => { + it('validates standard plan updates and preserves zero limits', () => { + const valid = UpdatePlanBodySchema.parse({ + ...basePlan, + id: '68ad85a7463006c963799a05', + type: SubTypeEnum.standard, + level: StandardSubLevelEnum.advanced, + maxTeamMember: 0, + websiteSyncPerDataset: 0 + }); + expect(valid.type).toBe(SubTypeEnum.standard); + if (valid.type === SubTypeEnum.standard) { + expect(valid.maxTeamMember).toBe(0); + expect(valid.websiteSyncPerDataset).toBe(0); + } + + const invalid = UpdatePlanBodySchema.safeParse({ + ...basePlan, + id: '68ad85a7463006c963799a05', + type: SubTypeEnum.standard + }); + expect(invalid.success).toBe(false); + }); + + it('validates extra capacity and point updates', () => { + const validDataset = UpdatePlanBodySchema.safeParse({ + ...basePlan, + id: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraDatasetSize, + extraDatasetSize: 2048 + }); + expect(validDataset.success).toBe(true); + + const validPoints = UpdatePlanBodySchema.safeParse({ + ...basePlan, + id: '68ad85a7463006c963799a05', + type: SubTypeEnum.extraPoints, + totalPoints: 10000, + surplusPoints: 8000 + }); + expect(validPoints.success).toBe(true); + }); + }); + + it('defines clean OpenAPI path responses', () => { + expect( + AdminPlanPath['/proApi/admin/wallet/plan/addPlans']?.post?.responses?.[200]?.content + ).toBeUndefined(); + expect( + AdminPlanPath['/proApi/admin/wallet/plan/updatePlan']?.post?.responses?.[200]?.content + ).toBeUndefined(); + }); +}); diff --git a/packages/global/test/openapi/core/app.test.ts b/packages/global/test/openapi/core/app.test.ts index a67d44121aad..c9f5d6df0f8b 100644 --- a/packages/global/test/openapi/core/app.test.ts +++ b/packages/global/test/openapi/core/app.test.ts @@ -6,6 +6,7 @@ import { ChangeAppOwnerBodySchema, ChangeAppOwnerResponseSchema } from '../../../openapi/core/app/permission/api'; +import { UpdateAppResponseSchema } from '../../../openapi/core/app/common/api'; import { UpdateAppCollaboratorBodySchema } from '../../../openapi/support/permission/api'; import { GetTemplateTypesQuerySchema, @@ -104,6 +105,7 @@ describe('App OpenAPI contracts', () => { ownerId: objectId }); expect(ChangeAppOwnerResponseSchema.parse(undefined)).toBeUndefined(); + expect(UpdateAppResponseSchema.parse(undefined)).toBeUndefined(); expect(() => UpdateAppCollaboratorBodySchema.parse({ appId: objectId, collaborators: [] }) ).toThrow(); diff --git a/packages/global/test/openapi/support/user/account/login/api.test.ts b/packages/global/test/openapi/support/user/account/login/api.test.ts index fa18de4bc935..34e31258e4a7 100644 --- a/packages/global/test/openapi/support/user/account/login/api.test.ts +++ b/packages/global/test/openapi/support/user/account/login/api.test.ts @@ -168,7 +168,24 @@ describe('user account OpenAPI contracts', () => { expect( LoginSuccessResponseSchema.parse({ - user: {}, + user: { + _id: objectIdLike, + username: 'user@example.com', + avatar: '/icon/avatar.svg', + timezone: 'Asia/Shanghai', + hasPassword: true, + team: { + userId: objectIdLike, + teamId: objectIdLike, + teamName: 'FastGPT 团队', + memberName: '普通成员', + avatar: '/icon/avatar.svg', + tmbId: objectIdLike, + status: 'active', + permission: {} + }, + permission: {} + }, token: longToken }).token ).toBe(longToken); diff --git a/pro b/pro index 11074975da6e..3e0ef8ef3b10 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 11074975da6ee8cd2a1d40c02523fafff2c9dab5 +Subproject commit 3e0ef8ef3b10858f38d0e9123d8efba72b90f7d4 diff --git a/projects/app/src/pages/api/admin/system/model/test.ts b/projects/app/src/pages/api/admin/system/model/test.ts index 2b548c9a1390..72d8c88e37f1 100644 --- a/projects/app/src/pages/api/admin/system/model/test.ts +++ b/projects/app/src/pages/api/admin/system/model/test.ts @@ -24,10 +24,8 @@ import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { TestAdminSystemModelQuerySchema, TestDraftAdminSystemModelBodySchema, - TestAdminSystemModelResponseSchema, type TestDraftAdminSystemModelBody, - type TestAdminSystemModelQuery, - type TestAdminSystemModelResponse + type TestAdminSystemModelQuery } from '@fastgpt/global/openapi/admin/system/model/api'; import { ModelErrEnum } from '@fastgpt/global/common/error/code/model'; import { UserError } from '@fastgpt/global/common/error/utils'; @@ -36,7 +34,7 @@ const logger = getLogger(LogCategories.MODULE.AI.MODEL); async function handler( req: ApiRequestProps -): Promise { +): Promise { const { teamId } = await authSystemAdmin({ req }); const { modelData, channelId } = await (async () => { @@ -79,29 +77,24 @@ async function handler( const runTest = async () => { if (modelData.type === 'llm') { - return TestAdminSystemModelResponseSchema.parse( - await testLLMModel({ model: modelData, headers, teamId }) - ); + await testLLMModel({ model: modelData, headers, teamId }); + return; } if (modelData.type === 'embedding') { - return TestAdminSystemModelResponseSchema.parse( - await testEmbeddingModel({ model: modelData, headers }) - ); + await testEmbeddingModel({ model: modelData, headers }); + return; } if (modelData.type === 'tts') { - return TestAdminSystemModelResponseSchema.parse( - await testTTSModel({ model: modelData, headers }) - ); + await testTTSModel({ model: modelData, headers }); + return; } if (modelData.type === 'stt') { - return TestAdminSystemModelResponseSchema.parse( - await testSTTModel({ model: modelData, headers }) - ); + await testSTTModel({ model: modelData, headers }); + return; } if (modelData.type === 'rerank') { - return TestAdminSystemModelResponseSchema.parse( - await testReRankModel({ model: modelData, headers }) - ); + await testReRankModel({ model: modelData, headers }); + return; } return Promise.reject('Model type not supported'); diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 5af636f22dc7..b49a12bd398f 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -52,7 +52,7 @@ async function handler(req: ApiRequestProps { {} as any ); - expect(result).toBe('ok'); + expect(result).toBeUndefined(); expect(mocks.createLLMResponse).toHaveBeenCalledWith( expect.objectContaining({ teamId: 'root-team', @@ -240,7 +240,7 @@ describe('admin model test routing', () => { {} as any ); - expect(result).toEqual(embeddingResult); + expect(result).toBeUndefined(); expect(mocks.getVectors).toHaveBeenCalledWith( expect.objectContaining({ inputs: [{ type: 'text', input: 'Hi' }], From 218f09794215dbe8e5808055804c43fb492ee406 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 21:09:52 +0800 Subject: [PATCH 05/10] refactor(openapi): reuse SystemModelDocumentDataSchema and streamline updateApp response to void --- packages/global/openapi/admin/system/api.ts | 25 +++++++++++-------- .../global/openapi/admin/system/model/api.ts | 14 +++-------- .../global/openapi/core/app/common/api.ts | 3 +-- .../global/openapi/core/app/common/index.ts | 10 ++------ .../test/openapi/admin/settings.test.ts | 4 +-- packages/global/test/openapi/core/app.test.ts | 2 -- projects/app/src/pages/api/core/app/update.ts | 6 ++--- 7 files changed, 25 insertions(+), 39 deletions(-) diff --git a/packages/global/openapi/admin/system/api.ts b/packages/global/openapi/admin/system/api.ts index ecdc1b219aec..a7a615aa3909 100644 --- a/packages/global/openapi/admin/system/api.ts +++ b/packages/global/openapi/admin/system/api.ts @@ -1,6 +1,5 @@ import z from 'zod'; -import { SubPlanInputSchema } from '../../../support/wallet/sub/type'; -import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; +import { SubPlanInputSchema, SubPlanSchema } from '../../../support/wallet/sub/type'; /* ============================================================================ * API: 获取系统配置 @@ -10,15 +9,21 @@ import { OpenObjectOpenApiMeta } from '../../../common/zod/openapi'; * Tags: ['Admin', 'Settings', 'Read'] * ============================================================================ */ +export const FastGPTConfigSchema = z + .looseObject({ + feConfigs: z.looseObject({}).optional().meta({ description: '前端功能和展示配置' }), + systemEnv: z.looseObject({}).optional().meta({ description: '服务端系统运行配置' }), + subPlans: SubPlanSchema.optional().meta({ description: '订阅套餐配置' }) + }) + .meta({ example: { feConfigs: {}, systemEnv: {} }, description: '系统 FastGPT 配置' }); + +export const FastGPTProConfigSchema = z + .looseObject({}) + .meta({ example: {}, description: '系统 FastGPT Pro 商业版配置(不含 license)' }); + export const GetConfigResponseSchema = z.object({ - fastgpt: z - .any() - .optional() - .meta({ ...OpenObjectOpenApiMeta, description: '系统 FastGPT 配置' }), - fastgptPro: z - .any() - .optional() - .meta({ ...OpenObjectOpenApiMeta, description: '系统 FastGPT Pro 商业版配置(不含 license)' }) + fastgpt: FastGPTConfigSchema.optional(), + fastgptPro: FastGPTProConfigSchema.optional() }); export type GetConfigResponse = z.infer; diff --git a/packages/global/openapi/admin/system/model/api.ts b/packages/global/openapi/admin/system/model/api.ts index 0621793bdee6..4dd89044c5d9 100644 --- a/packages/global/openapi/admin/system/model/api.ts +++ b/packages/global/openapi/admin/system/model/api.ts @@ -228,17 +228,9 @@ export type GetAdminModelTemplatesResponse = z.infer; -export const UpdateAppResponseSchema = z.undefined().meta({ description: '更新成功' }); -export type UpdateAppResponseType = z.infer; +export type UpdateAppResponseType = void; /* ============================================================================ * API: 置顶应用 diff --git a/packages/global/openapi/core/app/common/index.ts b/packages/global/openapi/core/app/common/index.ts index 77824b4a0c98..64d50e4d513d 100644 --- a/packages/global/openapi/core/app/common/index.ts +++ b/packages/global/openapi/core/app/common/index.ts @@ -21,8 +21,7 @@ import { TransitionWorkflowBodySchema, TransitionWorkflowResponseSchema, UpdateAppBodySchema, - UpdateAppQuerySchema, - UpdateAppResponseSchema + UpdateAppQuerySchema } from './api'; export const AppCommonPath: OpenAPIPath = { @@ -135,12 +134,7 @@ export const AppCommonPath: OpenAPIPath = { }, responses: { 200: { - description: '成功更新应用', - content: { - 'application/json': { - schema: UpdateAppResponseSchema - } - } + description: '成功更新应用' } } } diff --git a/packages/global/test/openapi/admin/settings.test.ts b/packages/global/test/openapi/admin/settings.test.ts index d654a8110cc8..f9946f0db7dd 100644 --- a/packages/global/test/openapi/admin/settings.test.ts +++ b/packages/global/test/openapi/admin/settings.test.ts @@ -91,8 +91,8 @@ describe('UpdateConfigBodySchema', () => { }); const schema = doc.components?.schemas?.GetConfigResponse as any; expect(schema.properties.fastgpt.type).toBe('object'); - expect(schema.properties.fastgpt.additionalProperties).toBe(true); + expect(schema.properties.fastgpt.additionalProperties).toBeTruthy(); expect(schema.properties.fastgptPro.type).toBe('object'); - expect(schema.properties.fastgptPro.additionalProperties).toBe(true); + expect(schema.properties.fastgptPro.additionalProperties).toBeTruthy(); }); }); diff --git a/packages/global/test/openapi/core/app.test.ts b/packages/global/test/openapi/core/app.test.ts index c9f5d6df0f8b..a67d44121aad 100644 --- a/packages/global/test/openapi/core/app.test.ts +++ b/packages/global/test/openapi/core/app.test.ts @@ -6,7 +6,6 @@ import { ChangeAppOwnerBodySchema, ChangeAppOwnerResponseSchema } from '../../../openapi/core/app/permission/api'; -import { UpdateAppResponseSchema } from '../../../openapi/core/app/common/api'; import { UpdateAppCollaboratorBodySchema } from '../../../openapi/support/permission/api'; import { GetTemplateTypesQuerySchema, @@ -105,7 +104,6 @@ describe('App OpenAPI contracts', () => { ownerId: objectId }); expect(ChangeAppOwnerResponseSchema.parse(undefined)).toBeUndefined(); - expect(UpdateAppResponseSchema.parse(undefined)).toBeUndefined(); expect(() => UpdateAppCollaboratorBodySchema.parse({ appId: objectId, collaborators: [] }) ).toThrow(); diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index b49a12bd398f..fc3b16dc3bfc 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -17,7 +17,6 @@ import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { UpdateAppBodySchema, UpdateAppQuerySchema, - UpdateAppResponseSchema, type UpdateAppBodyType, type UpdateAppQueryType } from '@fastgpt/global/openapi/core/app/common/api'; @@ -28,7 +27,7 @@ import { moveApp } from '@/service/core/app/move'; * 1. 若包含 parentId,则复用 moveApp 服务完成鉴权、层级检查、权限继承与移动操作; * 2. 若包含基础信息(名称、类型、头像、介绍等),则校验写权限并更新。 */ -async function handler(req: ApiRequestProps) { +async function handler(req: ApiRequestProps): Promise { const { query: { appId }, body: { parentId, name, avatar, type, intro } @@ -52,7 +51,7 @@ async function handler(req: ApiRequestProps Date: Tue, 22 Sep 2026 21:49:39 +0800 Subject: [PATCH 06/10] refactor(config): remove unused show_aiproxy and show_emptyChat, enrich OpenAPI meta descriptions, and align admin contracts --- packages/global/common/system/types/index.ts | 569 +++++++++++------- packages/global/openapi/admin/system/api.ts | 14 +- packages/global/openapi/admin/team/api.ts | 12 - packages/global/openapi/admin/team/index.ts | 32 +- packages/global/openapi/common/system/api.ts | 5 +- .../test/openapi/admin/settings.test.ts | 16 +- packages/service/common/system/tools.ts | 2 +- .../service/thirdProvider/aiproxy/config.ts | 3 - packages/web/hooks/useConfirm.tsx | 4 +- packages/web/test/hooks/useConfirm.test.ts | 193 ++++++ pro | 2 +- .../admin/config/ModelProvider.tsx | 14 +- .../pageComponents/admin/config/feature.tsx | 4 - .../src/pageComponents/admin/config/type.ts | 1 - .../admin/teams/components/EditTeamModal.tsx | 96 --- .../src/pageComponents/admin/teams/index.tsx | 11 +- .../app/src/service/common/system/index.ts | 3 - projects/app/src/web/admin/config/adapt.ts | 2 - projects/app/src/web/admin/team/api.ts | 9 - 19 files changed, 594 insertions(+), 398 deletions(-) create mode 100644 packages/web/test/hooks/useConfirm.test.ts delete mode 100644 projects/app/src/pageComponents/admin/teams/components/EditTeamModal.tsx diff --git a/packages/global/common/system/types/index.ts b/packages/global/common/system/types/index.ts index 91ff6957360b..f018d6916f4b 100644 --- a/packages/global/common/system/types/index.ts +++ b/packages/global/common/system/types/index.ts @@ -1,5 +1,6 @@ -import type { SubPlanType } from '../../../support/wallet/sub/type'; -import type { AccountCancellationVerificationCapabilities } from '../../../support/user/account/cancellation/type'; +import { SubPlanSchema } from '../../../support/wallet/sub/type'; +import { z } from 'zod'; +import { NumSchema } from '../../zod'; import type { LicensePayload, LicenseSchemaVersionType, @@ -28,235 +29,399 @@ export { LicenseTypeSchema } from '../license/schema'; -export type NavbarItemType = { - id: string; - name: string; - avatar: string; - url: string; - isActive: boolean; -}; +export const NavbarItemSchema = z.looseObject({ + id: z.string(), + name: z.string(), + avatar: z.string(), + url: z.string(), + isActive: z.boolean() +}); +export type NavbarItemType = z.infer; -export type ExternalProviderWorkflowVarType = { - name: string; - key: string; - intro: string; - isOpen: boolean; - url?: string; -}; +export const ExternalProviderWorkflowVarSchema = z.looseObject({ + name: z.string(), + key: z.string(), + intro: z.string(), + isOpen: z.boolean(), + url: z.string().optional() +}); +export type ExternalProviderWorkflowVarType = z.infer; -export type FastGPTRegisterMethodType = 'email' | 'phone'; -export type FastGPTRegisterMethodCompatType = FastGPTRegisterMethodType | 'sync'; -export type FastGPTTeamModeType = 'multi' | 'single' | 'sync'; +export const FastGPTRegisterMethodSchema = z.enum(['email', 'phone']); +export type FastGPTRegisterMethodType = z.infer; -/* fastgpt main */ -export type FastGPTConfigFileType = { - feConfigs: FastGPTFeConfigsType; - systemEnv: SystemEnvType; - subPlans?: SubPlanType; -}; +export const FastGPTRegisterMethodCompatSchema = z.enum(['email', 'phone', 'sync']); +export type FastGPTRegisterMethodCompatType = z.infer; -export type FastGPTFeConfigsType = { - show_workorder?: boolean; - show_emptyChat?: boolean; - isPlus?: boolean; - /** - * 是否部署了商业版(pro)服务,等价于服务端配置了 PRO_URL。 - * - * 与 isPlus 的区别:isPlus 表示「授权是否有效」,会因未激活/已到期而为 false, - * 但这类部署仍然配置了 pro 服务(需要展示激活/续期入口);本字段只表示服务是否接入, - * 用于区分「商业版部署」与「社区版部署」,后者没有授权概念,不应展示额度、公司名与激活状态。 - */ - isProService?: boolean; - hideChatCopyrightSetting?: boolean; - /** - * 用户自助注册方式。兼容期允许读取旧配置中的 sync,但新配置不再写入 sync。 - */ - register_method?: FastGPTRegisterMethodCompatType[]; - teamMode?: FastGPTTeamModeType; - login_method?: FastGPTRegisterMethodType[]; // Attention: login method is different with oauth - find_password_method?: FastGPTRegisterMethodType[]; - bind_notification_method?: FastGPTRegisterMethodType[]; - /** - * @deprecated MCP SSE 代理地址已迁移到环境变量 SSE_MCP_SERVER_PROXY_ENDPOINT。 - * 运行时配置以环境变量为准,admin 不再支持写入该字段。 - */ - mcpServerProxyEndpoint?: string; +export const FastGPTTeamModeSchema = z.enum(['multi', 'single', 'sync']); +export type FastGPTTeamModeType = z.infer; - chineseRedirectUrl?: string; - botIframeUrl?: string; +export const FastGPTFeConfigsSchema = z.looseObject({ + show_workorder: z.boolean().optional().meta({ description: '是否展示工单入口' }), + isPlus: z + .boolean() + .optional() + .meta({ description: '商业版授权是否有效;未激活或已到期时为 false' }), + isProService: z.boolean().optional().meta({ + description: + '是否部署了商业版(pro)服务(配置了 PRO_URL)。与 isPlus 区别:本字段表示服务是否接入(用于区分商业版部署与开源社区版部署),而 isPlus 表示授权是否有效。' + }), + hideChatCopyrightSetting: z + .boolean() + .optional() + .meta({ description: '是否隐藏对话版权自定义设置' }), + register_method: z.array(FastGPTRegisterMethodCompatSchema).optional().meta({ + description: '用户自助注册方式列表(支持邮箱、手机号,兼容历史 sync 配置)' + }), + teamMode: FastGPTTeamModeSchema.optional().meta({ + description: '团队模式(单团队/多团队/账号同步)' + }), + login_method: z + .array(FastGPTRegisterMethodSchema) + .optional() + .meta({ description: '支持的账号登录方式列表(邮箱、手机号)' }), + find_password_method: z + .array(FastGPTRegisterMethodSchema) + .optional() + .meta({ description: '找回密码验证方式列表' }), + bind_notification_method: z + .array(FastGPTRegisterMethodSchema) + .optional() + .meta({ description: '绑定通知联系方式列表' }), + mcpServerProxyEndpoint: z.string().optional().meta({ + description: 'MCP SSE 代理地址,运行时配置以环境变量 SSE_MCP_SERVER_PROXY_ENDPOINT 为准' + }), - show_appStore?: boolean; - show_git?: boolean; - show_pay?: boolean; - show_openai_account?: boolean; - show_compliance_copywriting?: boolean; - show_aiproxy?: boolean; - show_coupon?: boolean; - show_discount_coupon?: boolean; - show_enterprise_auth?: boolean; - showWecomConfig?: boolean; - wecomLoginAutoRedirect?: boolean; - accountCancellation?: { - enabled?: boolean; - }; + chineseRedirectUrl: z.string().optional().meta({ description: '中国大陆地区访问重定向跳转地址' }), + botIframeUrl: z.string().optional().meta({ description: '嵌入式对话助手 iframe 页面地址' }), + + show_appStore: z.boolean().optional().meta({ description: '是否展示应用市场' }), + show_git: z.boolean().optional().meta({ description: '是否展示 GitHub 仓库入口及 Star 信息' }), + show_pay: z.boolean().optional().meta({ description: '是否展示在线充值/支付相关入口' }), + show_openai_account: z + .boolean() + .optional() + .meta({ description: '是否展示个人/团队自定义 OpenAI 账号配置入口' }), + show_compliance_copywriting: z + .boolean() + .optional() + .meta({ description: '前端是否展示合规提示文案' }), + show_coupon: z.boolean().optional().meta({ description: '是否展示兑换码入口' }), + show_discount_coupon: z.boolean().optional().meta({ description: '是否展示优惠券/折扣券入口' }), + show_enterprise_auth: z.boolean().optional().meta({ description: '是否展示企业实名认证入口' }), + showWecomConfig: z.boolean().optional().meta({ description: '是否展示企业微信集成配置' }), + wecomLoginAutoRedirect: z + .boolean() + .optional() + .meta({ description: '在企业微信内置浏览器中访问时是否自动重定向到企微 OAuth 登录' }), + accountCancellation: z + .looseObject({ + enabled: z.boolean().optional().meta({ description: '是否允许用户自助注销账号' }) + }) + .optional() + .meta({ description: '账号注销配置' }), /** 仅暴露注销验证的布尔能力,不包含任何 Provider 密钥。 */ - accountVerification?: { - accountCancellation?: AccountCancellationVerificationCapabilities; - }; + accountVerification: z + .looseObject({ + accountCancellation: z + .looseObject({ + emailCode: z.boolean().optional().meta({ description: '注销时是否支持邮箱验证码校验' }), + phoneCode: z + .boolean() + .optional() + .meta({ description: '注销时是否支持手机短信验证码校验' }), + accountCancellation: z + .boolean() + .optional() + .meta({ description: '是否开启注销二次确认验证' }), + wechat: z.boolean().optional().meta({ description: '注销时是否支持微信扫码验证' }), + oauth: z + .record(z.string(), z.boolean()) + .optional() + .meta({ description: '注销时支持的第三方 OAuth 验证渠道' }) + }) + .optional() + }) + .optional() + .meta({ description: '注销账号等敏感操作的可用验证方式能力(仅暴露开关,不含密钥)' }), - show_dataset_feishu?: boolean; - show_dataset_yuque?: boolean; - show_dataset_dingtalk?: boolean; - show_publish_feishu?: boolean; - show_publish_dingtalk?: boolean; - show_publish_wecom?: boolean; - show_publish_offiaccount?: boolean; - show_publish_wechat?: boolean; - show_agent_sandbox?: boolean; - pluginRemoteDebug?: boolean; - enable_team_plugin_upload?: boolean; + show_dataset_feishu: z + .boolean() + .optional() + .meta({ description: '创建知识库时是否展示飞书知识库导入选项' }), + show_dataset_yuque: z + .boolean() + .optional() + .meta({ description: '创建知识库时是否展示语雀知识库导入选项' }), + show_dataset_dingtalk: z + .boolean() + .optional() + .meta({ description: '创建知识库时是否展示钉钉知识库导入选项' }), + show_publish_feishu: z + .boolean() + .optional() + .meta({ description: '应用发布渠道中是否展示飞书机器人' }), + show_publish_dingtalk: z + .boolean() + .optional() + .meta({ description: '应用发布渠道中是否展示钉钉机器人' }), + show_publish_wecom: z + .boolean() + .optional() + .meta({ description: '应用发布渠道中是否展示企业微信应用' }), + show_publish_offiaccount: z + .boolean() + .optional() + .meta({ description: '应用发布渠道中是否展示微信公众号' }), + show_publish_wechat: z + .boolean() + .optional() + .meta({ description: '应用发布渠道中是否展示微信个人号客服' }), + show_agent_sandbox: z + .boolean() + .optional() + .meta({ description: '是否开启 Agent 代码沙箱执行环境' }), + pluginRemoteDebug: z.boolean().optional().meta({ description: '是否允许团队远程调试自定义插件' }), + enable_team_plugin_upload: z + .boolean() + .optional() + .meta({ description: '是否允许团队成员上传自定义插件' }), - show_dataset_enhance?: boolean; - show_batch_eval?: boolean; + show_dataset_enhance: z + .boolean() + .optional() + .meta({ description: '是否开启数据集增强处理能力(如文本清洗、改写)' }), + show_batch_eval: z.boolean().optional().meta({ description: '是否开启批量评测模块' }), - concatMd?: string; - docUrl?: string; - loginGuideDocUrl?: string; - openAPIDocUrl?: string; - appTemplateCourse?: string; - marketplaceUrl?: string; - customApiDomain?: string; - customSharePageDomain?: string; + concatMd: z + .string() + .optional() + .meta({ description: '自定义展示的 Markdown 文案(如开源地址、加入交流群说明)' }), + docUrl: z.string().optional().meta({ description: '官方使用文档地址' }), + loginGuideDocUrl: z.string().optional().meta({ description: '登录引导帮助文档地址' }), + openAPIDocUrl: z.string().optional().meta({ description: 'OpenAPI 接口文档地址' }), + appTemplateCourse: z.string().optional().meta({ description: '应用模板使用教程链接' }), + marketplaceUrl: z.string().optional().meta({ description: '插件市场与模板市场服务地址' }), + customApiDomain: z.string().optional().meta({ description: '对外开放的自定义 API 域名' }), + customSharePageDomain: z + .string() + .optional() + .meta({ description: '分享免登对话页面的自定义独立域名' }), - systemTitle?: string; - scripts?: { [key: string]: string }[]; - favicon?: string; + systemTitle: z.string().optional().meta({ description: '系统平台标题名称' }), + scripts: z + .array(z.record(z.string(), z.string())) + .optional() + .meta({ description: '前端注入的外部第三方脚本列表' }), + favicon: z.string().optional().meta({ description: '站点浏览器 Favicon 图标地址' }), - sso?: { - icon?: string; - title?: string; - url?: string; - autoLogin?: boolean; - }; - oauth?: { - github?: string; - google?: string; - wechat?: string; - microsoft?: { - clientId?: string; - tenantId?: string; - customButton?: string; - }; - wecom?: boolean; - }; - limit?: { - exportDatasetLimitMinutes?: number; - websiteSyncLimitMinuted?: number; - agentSandboxMaxEditDebug?: number; - agentSandboxMaxSessionRuntime?: number; - agentSandboxArchiveMaxBytes?: number; - skillSandboxMaxBytes?: number; - agentSandboxMaxFileBytes?: number; - workflowParallelRunMaxConcurrency?: number; - maxFolderDepth?: number; - }; + sso: z + .looseObject({ + icon: z.string().optional().meta({ description: 'SSO 登录方式图标' }), + title: z.string().optional().meta({ description: 'SSO 登录按钮显示标题' }), + url: z.string().optional().meta({ description: 'SSO 认证跳转地址' }), + autoLogin: z + .boolean() + .optional() + .meta({ description: '是否在首次进入页面时自动跳转 SSO 登录' }) + }) + .optional() + .meta({ description: '企业单点登录(SSO)配置' }), + oauth: z + .looseObject({ + github: z.string().optional().meta({ description: 'GitHub OAuth Client ID' }), + google: z.string().optional().meta({ description: 'Google OAuth Client ID' }), + wechat: z.string().optional().meta({ description: '微信开放平台 AppID' }), + microsoft: z + .looseObject({ + clientId: z.string().optional().meta({ description: '微软 Azure AD Client ID' }), + tenantId: z.string().optional().meta({ description: '微软 Azure AD 租户 ID' }), + customButton: z.string().optional().meta({ description: '微软登录自定义按钮文案' }) + }) + .optional() + .meta({ description: '微软登录配置' }), + wecom: z.boolean().optional().meta({ description: '是否启用企业微信扫码/网页登录' }) + }) + .optional() + .meta({ description: '第三方 OAuth 快捷登录配置' }), + limit: z + .looseObject({ + exportDatasetLimitMinutes: NumSchema.optional().meta({ + description: '知识库数据集导出频次限制(分钟)' + }), + websiteSyncLimitMinuted: NumSchema.optional().meta({ + description: '网页数据源自动同步时间间隔限制(分钟)' + }), + agentSandboxMaxEditDebug: NumSchema.optional().meta({ + description: '单团队 Agent 沙箱最大同时在线编辑调试数' + }), + agentSandboxMaxSessionRuntime: NumSchema.optional().meta({ + description: 'Agent 沙箱最大会话运行时间(毫秒)' + }), + agentSandboxArchiveMaxBytes: NumSchema.optional().meta({ + description: '沙箱归档文件大小限制(字节)' + }), + skillSandboxMaxBytes: NumSchema.optional().meta({ + description: '技能沙箱最大体积限制(字节)' + }), + agentSandboxMaxFileBytes: NumSchema.optional().meta({ + description: '沙箱单文件大小限制(字节)' + }), + workflowParallelRunMaxConcurrency: NumSchema.optional().meta({ + description: '工作流并行分支最大并发数' + }), + maxFolderDepth: NumSchema.optional().meta({ + description: '应用与知识库目录支持的最大嵌套深度' + }) + }) + .optional() + .meta({ description: '系统各项资源上限与速率限制' }), - uploadFileMaxAmount: number; - uploadFileMaxSize: number; // MB - evalFileMaxLines?: number; + uploadFileMaxAmount: NumSchema.meta({ description: '单次最多上传文件数量' }), + uploadFileMaxSize: NumSchema.meta({ description: '单文件最大大小限制(MB)' }), + evalFileMaxLines: NumSchema.optional().meta({ description: '评测用例文件最大支持行数' }), // Compute by systemEnv.customPdfParse - showCustomPdfParse?: boolean; - customPdfParsePrice?: number; + showCustomPdfParse: z.boolean().optional().meta({ description: '是否启用高精自定义 PDF 解析器' }), + customPdfParsePrice: NumSchema.optional().meta({ + description: '自定义高精 PDF 解析按页计费单价(积分/页)' + }), // 是否预置了智能分块服务地址(SANGFOR_CHUNK_URL)。未配置时 UI 隐藏「智能分块」入口。 - show_intelligent_chunking?: boolean; + show_intelligent_chunking: z.boolean().optional().meta({ + description: '是否预置了智能分块服务(SANGFOR_CHUNK_URL),未配置时前端隐藏智能分块选项' + }), - navbarItems?: NavbarItemType[]; - externalProviderWorkflowVariables?: ExternalProviderWorkflowVarType[]; + navbarItems: z.array(NavbarItemSchema).optional().meta({ description: '自定义系统导航栏链接项' }), + externalProviderWorkflowVariables: z.array(ExternalProviderWorkflowVarSchema).optional().meta({ + description: '外部提供商工作流全局变量定义' + }), - payConfig?: { - wx?: boolean; - alipay?: boolean; - bank?: boolean; - }; - payFormUrl?: string; - fileUrlWhitelist?: string[]; - customDomain?: { - enable?: boolean; - domain?: { - aliyun?: string; - tencent?: string; - volcengine?: string; - }; - }; + payConfig: z + .looseObject({ + wx: z.boolean().optional().meta({ description: '是否启用微信支付' }), + alipay: z.boolean().optional().meta({ description: '是否启用支付宝支付' }), + bank: z.boolean().optional().meta({ description: '是否启用公对公银行转账' }) + }) + .optional() + .meta({ description: '可用支付方式配置' }), + payFormUrl: z.string().optional().meta({ description: '第三方定制充值页面或工单表单跳转链接' }), + fileUrlWhitelist: z + .array(z.string()) + .optional() + .meta({ description: '允许外部加载的文件资源 URL 域名白名单' }), + customDomain: z + .looseObject({ + enable: z.boolean().optional().meta({ description: '是否开启自定义独立域名功能' }), + domain: z + .looseObject({ + aliyun: z.string().optional().meta({ description: '阿里云 DNS 解析域名' }), + tencent: z.string().optional().meta({ description: '腾讯云 DNS 解析域名' }), + volcengine: z.string().optional().meta({ description: '火山引擎 DNS 解析域名' }) + }) + .optional() + .meta({ description: '不同云厂商 DNS 绑定域名配置' }) + }) + .optional() + .meta({ description: '自定义独立域名绑定与 SSL 证书配置' }), - ip_whitelist?: string; + ip_whitelist: z + .string() + .optional() + .meta({ description: '系统访问限制的 IP 白名单列表(逗号分隔)' }), // tmp - agentSandboxFree?: boolean; - agentSandboxProxyUrl?: string; -}; + agentSandboxFree: z + .boolean() + .optional() + .meta({ description: 'Agent 代码沙箱环境是否处于免费体验期' }), + agentSandboxProxyUrl: z + .string() + .optional() + .meta({ description: 'Agent 代码沙箱服务内网代理地址' }) +}); +export type FastGPTFeConfigsType = z.infer; -export type SystemEnvType = { - openapiPrefix?: string; +export const CustomPdfParseSchema = z.looseObject({ + url: z.string().optional(), + key: z.string().optional(), + somarkApiKey: z.string().optional(), + doc2xKey: z.string().optional(), + textinAppId: z.string().optional(), + textinSecretCode: z.string().optional(), + price: NumSchema.optional() +}); +export type customPdfParseType = z.infer; - datasetParseMaxProcess: number; - vectorMaxProcess: number; - qaMaxProcess: number; - vlmMaxProcess: number; +export const LangfuseConfigSchema = z.looseObject({ + secretKey: z.string().optional(), + publicKey: z.string().optional(), + baseUrl: z.string().optional() +}); +export type LangfuseConfigType = z.infer; - hnswEfSearch: number; - hnswMaxScanTuples: number; +export const CustomDomainSchema = z.looseObject({ + kc: z + .looseObject({ + aliyun: z.string().optional(), + tencent: z.string().optional(), + volcengine: z.string().optional() + }) + .optional(), + domain: z + .looseObject({ + aliyun: z.string().optional(), + tencent: z.string().optional(), + volcengine: z.string().optional() + }) + .optional(), + issuerServiceName: z + .looseObject({ + aliyun: z.string().optional(), + tencent: z.string().optional(), + volcengine: z.string().optional() + }) + .optional(), + nginxServiceName: z + .looseObject({ + aliyun: z.string().optional(), + tencent: z.string().optional(), + volcengine: z.string().optional() + }) + .optional() +}); +export type customDomainType = z.infer; - customPdfParse?: customPdfParseType; - langfuse?: LangfuseConfigType; - fileUrlWhitelist?: string[]; - customDomain?: customDomainType; - workflowHttpNode?: { - /** 是否允许工作流 HTTP 节点忽略 HTTPS 证书校验。 */ - ignoreHttpsCertificate?: boolean; - }; -}; +export const SystemEnvSchema = z.looseObject({ + openapiPrefix: z.string().optional(), -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; - }; -}; + datasetParseMaxProcess: NumSchema.meta({ description: '知识库解析最大并发处理进程数' }), + vectorMaxProcess: NumSchema.meta({ description: '向量化处理最大并发进程数' }), + qaMaxProcess: NumSchema.meta({ description: 'QA 问答对拆分最大并发进程数' }), + vlmMaxProcess: NumSchema.meta({ description: '视觉语言模型处理最大并发进程数' }), -export type customPdfParseType = { - url?: string; - key?: string; - somarkApiKey?: string; - doc2xKey?: string; - textinAppId?: string; - textinSecretCode?: string; - price?: number; -}; + hnswEfSearch: NumSchema.meta({ description: 'HNSW 向量检索 efSearch 参数' }), + hnswMaxScanTuples: NumSchema.meta({ description: 'HNSW 向量检索最大扫描元组数' }), + customPdfParse: CustomPdfParseSchema.optional(), + langfuse: LangfuseConfigSchema.optional(), + fileUrlWhitelist: z.array(z.string()).optional(), + customDomain: CustomDomainSchema.optional(), + workflowHttpNode: z + .looseObject({ + /** 是否允许工作流 HTTP 节点忽略 HTTPS 证书校验。 */ + ignoreHttpsCertificate: z.boolean().optional() + }) + .optional() +}); +export type SystemEnvType = z.infer; -export type LangfuseConfigType = { - secretKey?: string; - publicKey?: string; - baseUrl?: string; -}; +/* fastgpt main */ +export const FastGPTConfigFileSchema = z.looseObject({ + feConfigs: FastGPTFeConfigsSchema.optional(), + systemEnv: SystemEnvSchema.optional(), + subPlans: SubPlanSchema.optional() +}); +export type FastGPTConfigFileType = z.infer; /** * 运行时 License 数据(global.licenseData / 前端展示)= diff --git a/packages/global/openapi/admin/system/api.ts b/packages/global/openapi/admin/system/api.ts index a7a615aa3909..76ffe572e2da 100644 --- a/packages/global/openapi/admin/system/api.ts +++ b/packages/global/openapi/admin/system/api.ts @@ -1,5 +1,6 @@ import z from 'zod'; -import { SubPlanInputSchema, SubPlanSchema } from '../../../support/wallet/sub/type'; +import { SubPlanInputSchema } from '../../../support/wallet/sub/type'; +import { FastGPTConfigFileSchema } from '../../../common/system/types'; /* ============================================================================ * API: 获取系统配置 @@ -9,13 +10,10 @@ import { SubPlanInputSchema, SubPlanSchema } from '../../../support/wallet/sub/t * Tags: ['Admin', 'Settings', 'Read'] * ============================================================================ */ -export const FastGPTConfigSchema = z - .looseObject({ - feConfigs: z.looseObject({}).optional().meta({ description: '前端功能和展示配置' }), - systemEnv: z.looseObject({}).optional().meta({ description: '服务端系统运行配置' }), - subPlans: SubPlanSchema.optional().meta({ description: '订阅套餐配置' }) - }) - .meta({ example: { feConfigs: {}, systemEnv: {} }, description: '系统 FastGPT 配置' }); +export const FastGPTConfigSchema = FastGPTConfigFileSchema.meta({ + example: { feConfigs: {}, systemEnv: {} }, + description: '系统 FastGPT 配置' +}); export const FastGPTProConfigSchema = z .looseObject({}) diff --git a/packages/global/openapi/admin/team/api.ts b/packages/global/openapi/admin/team/api.ts index 03dd8bcacff4..bce3b9f797fe 100644 --- a/packages/global/openapi/admin/team/api.ts +++ b/packages/global/openapi/admin/team/api.ts @@ -53,15 +53,3 @@ export const GetTeamMembersResponseSchema = z.object({ .meta({ description: '团队基本信息' }) }); export type GetTeamMembersResponseType = z.infer; -export type UpdateTeamBodyType = z.infer; -export type UpdateTeamResponseType = z.infer; - -export const UpdateTeamBodySchema = z.object({ - id: z.string().meta({ description: '团队ID' }), - name: z.string().optional().meta({ description: '新团队名称' }), - balance: z.number().optional().meta({ description: '新余额' }) -}); - -export const UpdateTeamResponseSchema = z.object({ - balance: z.number().optional().meta({ description: '更新后的余额' }) -}); diff --git a/packages/global/openapi/admin/team/index.ts b/packages/global/openapi/admin/team/index.ts index 4260a495d3cf..ad61a7b99ef7 100644 --- a/packages/global/openapi/admin/team/index.ts +++ b/packages/global/openapi/admin/team/index.ts @@ -1,13 +1,7 @@ import z from 'zod'; import type { OpenAPIPath } from '../../type'; import { DevApiTagsMap } from '../../tag'; -import { - GetTeamsBodySchema, - GetTeamsResponseSchema, - GetTeamMembersResponseSchema, - UpdateTeamBodySchema, - UpdateTeamResponseSchema -} from './api'; +import { GetTeamsBodySchema, GetTeamsResponseSchema, GetTeamMembersResponseSchema } from './api'; export const AdminTeamsPath: OpenAPIPath = { '/proApi/admin/team/getTeams': { @@ -55,29 +49,5 @@ export const AdminTeamsPath: OpenAPIPath = { } } } - }, - '/proApi/admin/team/updateTeam': { - post: { - summary: '更新团队信息', - description: '管理员修改团队的名称或余额', - tags: [DevApiTagsMap.adminTeams], - requestBody: { - content: { - 'application/json': { - schema: UpdateTeamBodySchema - } - } - }, - responses: { - 200: { - description: '更新成功', - content: { - 'application/json': { - schema: UpdateTeamResponseSchema - } - } - } - } - } } }; diff --git a/packages/global/openapi/common/system/api.ts b/packages/global/openapi/common/system/api.ts index 026398741880..f845230861a5 100644 --- a/packages/global/openapi/common/system/api.ts +++ b/packages/global/openapi/common/system/api.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { SubPlanSchema } from '../../../support/wallet/sub/type'; import type { FastGPTFeConfigsType } from '../../../common/system/types'; +import { NumSchema } from '../../../common/zod'; /* ============================================================================ * API: 获取系统初始化数据 @@ -19,8 +20,8 @@ export const GetSystemInitDataQuerySchema = z.object({ export type GetSystemInitDataQuery = z.infer; const FastGPTFeConfigsSchema = z.looseObject({ - uploadFileMaxAmount: z.number(), - uploadFileMaxSize: z.number(), + uploadFileMaxAmount: NumSchema, + uploadFileMaxSize: NumSchema, marketplaceUrl: z.string().url().optional().meta({ example: 'https://v2.marketplace.fastgpt.cn', description: '插件市场服务地址' diff --git a/packages/global/test/openapi/admin/settings.test.ts b/packages/global/test/openapi/admin/settings.test.ts index f9946f0db7dd..2f19075afe0f 100644 --- a/packages/global/test/openapi/admin/settings.test.ts +++ b/packages/global/test/openapi/admin/settings.test.ts @@ -74,10 +74,22 @@ describe('UpdateConfigBodySchema', () => { it('declares open object schemas for dynamic system configurations', () => { const parsed = GetConfigResponseSchema.parse({ - fastgpt: { feConfigs: { isPlus: true } }, + fastgpt: { + feConfigs: { + isPlus: true, + uploadFileMaxSize: '500', + uploadFileMaxAmount: '15' + } + }, fastgptPro: { someProKey: 'val' } }); - expect(parsed.fastgpt).toEqual({ feConfigs: { isPlus: true } }); + expect(parsed.fastgpt).toEqual({ + feConfigs: { + isPlus: true, + uploadFileMaxSize: 500, + uploadFileMaxAmount: 15 + } + }); expect(parsed.fastgptPro).toEqual({ someProKey: 'val' }); const doc = createDocument({ diff --git a/packages/service/common/system/tools.ts b/packages/service/common/system/tools.ts index 70556636f65c..ce510f7820e6 100644 --- a/packages/service/common/system/tools.ts +++ b/packages/service/common/system/tools.ts @@ -17,7 +17,7 @@ export const SERVICE_LOCAL_HOST = : `${process.env.HOSTNAME || 'localhost'}:${SERVICE_LOCAL_PORT}`; export const initFastGPTConfig = (config?: FastGPTConfigFileType) => { - if (!config) return; + if (!config?.feConfigs || !config?.systemEnv) return; // Special config computed config.feConfigs.showCustomPdfParse = diff --git a/packages/service/thirdProvider/aiproxy/config.ts b/packages/service/thirdProvider/aiproxy/config.ts index ad5aca8d191a..f070fd8e3b4f 100644 --- a/packages/service/thirdProvider/aiproxy/config.ts +++ b/packages/service/thirdProvider/aiproxy/config.ts @@ -3,9 +3,6 @@ import { serviceEnv } from '../../env'; export const aiProxyApiEndpoint = serviceEnv.AIPROXY_API_ENDPOINT; export const aiProxyApiToken = serviceEnv.AIPROXY_API_TOKEN; -/** AI Proxy 是启动必填依赖;保留布尔查询供现有前端配置契约使用。 */ -export const hasAIProxyApiEndpoint = () => true; - export const getAIProxyAdminConfig = () => { if (!aiProxyApiEndpoint || !aiProxyApiToken) { throw new Error('AI Proxy endpoint or token is not set'); diff --git a/packages/web/hooks/useConfirm.tsx b/packages/web/hooks/useConfirm.tsx index 734e40ac3ab9..652b6ebdcd17 100644 --- a/packages/web/hooks/useConfirm.tsx +++ b/packages/web/hooks/useConfirm.tsx @@ -131,6 +131,7 @@ export const useConfirm = (props?: { useEffect(() => { if (isOpen) { + setRequesting(false); setCountDownAmount(countDown); setInputValue(''); timer.current = setInterval(() => { @@ -262,7 +263,8 @@ export const useConfirm = (props?: { setRequesting(false); return; } - // 关闭后不再更新本地状态:弹窗退出动画期间的重渲染会让 portal 残留并持续拦截点击。 + // 确认成功后重置 requesting 并关闭弹窗,避免再次打开时残留 loading 态 + setRequesting(false); onClose(); }} > diff --git a/packages/web/test/hooks/useConfirm.test.ts b/packages/web/test/hooks/useConfirm.test.ts new file mode 100644 index 000000000000..8a37a1651813 --- /dev/null +++ b/packages/web/test/hooks/useConfirm.test.ts @@ -0,0 +1,193 @@ +// @vitest-environment jsdom + +import React, { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useConfirm } from '../../hooks/useConfirm'; + +const reactGlobals = globalThis as typeof globalThis & { + IS_REACT_ACT_ENVIRONMENT?: boolean; +}; +reactGlobals.IS_REACT_ACT_ENVIRONMENT = true; + +vi.mock('next-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }) +})); + +vi.mock('../../components/v2/common/MyModal', () => ({ + default: ({ isOpen, children }: { isOpen: boolean; children: React.ReactNode }) => + isOpen ? React.createElement('div', { 'data-testid': 'mock-modal' }, children) : null +})); + +vi.mock('../../components/common/Icon', () => ({ + default: () => React.createElement('span', { 'data-testid': 'mock-icon' }) +})); + +vi.mock('../../components/common/Avatar', () => ({ + default: () => React.createElement('span', { 'data-testid': 'mock-avatar' }) +})); + +vi.mock('@chakra-ui/react', async () => { + const actual = await vi.importActual('@chakra-ui/react'); + return { + ...actual, + Button: ({ children, isLoading, isDisabled, onClick, ...props }: any) => + React.createElement( + 'button', + { + ...props, + 'data-loading': isLoading ? 'true' : 'false', + disabled: Boolean(isDisabled || isLoading), + onClick + }, + isLoading ? 'loading...' : children + ), + Box: ({ children }: any) => React.createElement('div', null, children), + Flex: ({ children }: any) => React.createElement('div', null, children), + HStack: ({ children }: any) => React.createElement('div', null, children), + VStack: ({ children }: any) => React.createElement('div', null, children) + }; +}); + +type HarnessHandle = { + openConfirm: ReturnType['openConfirm']; +}; + +const TestHarness = ({ onReady }: { onReady: (handle: HarnessHandle) => void }) => { + const { openConfirm, ConfirmModal } = useConfirm({ + content: 'test content' + }); + + React.useEffect(() => { + onReady({ openConfirm }); + }, [onReady, openConfirm]); + + return React.createElement( + 'div', + null, + React.createElement(ConfirmModal, { confirmText: '确认' }) + ); +}; + +describe('useConfirm', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + it('should reset loading state when reopening confirm modal after a successful confirmation', async () => { + let handle: HarnessHandle | undefined; + await act(async () => { + root.render(React.createElement(TestHarness, { onReady: (h) => (handle = h) })); + }); + + expect(container.querySelector('[data-testid="mock-modal"]')).toBeNull(); + + // 第一次打开弹窗 + let resolveConfirm: () => void = () => {}; + const onConfirmFirst = vi.fn( + () => + new Promise((resolve) => { + resolveConfirm = resolve; + }) + ); + + await act(async () => { + handle?.openConfirm({ onConfirm: onConfirmFirst })(); + }); + + expect(container.querySelector('[data-testid="mock-modal"]')).not.toBeNull(); + const buttons = container.querySelectorAll('button'); + const confirmBtn = buttons[buttons.length - 1]; + expect(confirmBtn.getAttribute('data-loading')).toBe('false'); + + // 点击确认,进入 loading 态 + await act(async () => { + confirmBtn.click(); + }); + expect(confirmBtn.getAttribute('data-loading')).toBe('true'); + + // 完成确认,弹窗关闭 + await act(async () => { + resolveConfirm(); + }); + expect(container.querySelector('[data-testid="mock-modal"]')).toBeNull(); + + // 第二次打开弹窗:验证不再残留 loading 态 + let resolveConfirmSecond: () => void = () => {}; + const onConfirmSecond = vi.fn( + () => + new Promise((resolve) => { + resolveConfirmSecond = resolve; + }) + ); + + await act(async () => { + handle?.openConfirm({ onConfirm: onConfirmSecond })(); + }); + + expect(container.querySelector('[data-testid="mock-modal"]')).not.toBeNull(); + const buttonsSecond = container.querySelectorAll('button'); + const confirmBtnSecond = buttonsSecond[buttonsSecond.length - 1]; + expect(confirmBtnSecond.getAttribute('data-loading')).toBe('false'); + expect(confirmBtnSecond.disabled).toBe(false); + + // 第二次点击确认,能正常触发 + await act(async () => { + confirmBtnSecond.click(); + }); + expect(confirmBtnSecond.getAttribute('data-loading')).toBe('true'); + expect(onConfirmSecond).toHaveBeenCalledTimes(1); + + await act(async () => { + resolveConfirmSecond(); + }); + expect(container.querySelector('[data-testid="mock-modal"]')).toBeNull(); + }); + + it('should clear loading state when confirmation fails so user can retry', async () => { + let handle: HarnessHandle | undefined; + await act(async () => { + root.render(React.createElement(TestHarness, { onReady: (h) => (handle = h) })); + }); + + let rejectConfirm: (err: any) => void = () => {}; + const onConfirmFail = vi.fn( + () => + new Promise((_, reject) => { + rejectConfirm = reject; + }) + ); + + await act(async () => { + handle?.openConfirm({ onConfirm: onConfirmFail })(); + }); + + const buttons = container.querySelectorAll('button'); + const confirmBtn = buttons[buttons.length - 1]; + + await act(async () => { + confirmBtn.click(); + }); + expect(confirmBtn.getAttribute('data-loading')).toBe('true'); + + // 失败时保持弹窗打开并解除 loading + await act(async () => { + rejectConfirm(new Error('failed')); + }); + expect(container.querySelector('[data-testid="mock-modal"]')).not.toBeNull(); + expect(confirmBtn.getAttribute('data-loading')).toBe('false'); + expect(confirmBtn.disabled).toBe(false); + }); +}); diff --git a/pro b/pro index 3e0ef8ef3b10..1c6db23422ec 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 3e0ef8ef3b10858f38d0e9123d8efba72b90f7d4 +Subproject commit 1c6db23422eca12d4f907e9b096b4ccd7db0e744 diff --git a/projects/app/src/pageComponents/admin/config/ModelProvider.tsx b/projects/app/src/pageComponents/admin/config/ModelProvider.tsx index a34f3b1cb13f..ba86465ea167 100644 --- a/projects/app/src/pageComponents/admin/config/ModelProvider.tsx +++ b/projects/app/src/pageComponents/admin/config/ModelProvider.tsx @@ -6,7 +6,6 @@ import { useRouter } from 'next/router'; import AdminContainer from '@/pageComponents/admin/AdminContainer'; import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import { useClientTranslation } from '@fastgpt/web/i18n/useClientTranslation'; -import { useSystemStore } from '@/web/common/system/useSystemStore'; import { accountPageRootStyles } from '@/pageComponents/account/styles'; const ModelConfigTable = dynamic(() => import('@/pageComponents/model/ModelConfigTable')); @@ -18,21 +17,16 @@ type TabType = 'config' | 'channel' | 'channel_log' | 'account_model'; const ModelProvider = () => { const { t } = useClientTranslation(['config_model', 'config']); - const { feConfigs } = useSystemStore(); const router = useRouter(); const modelTabList = useMemo<{ label: string; value: TabType }[]>( () => [ { label: t('config_model:config_model'), value: 'config' }, - ...(feConfigs.show_aiproxy - ? [ - { label: t('config_model:channel'), value: 'channel' as const }, - { label: t('config_model:log'), value: 'channel_log' as const }, - { label: t('config_model:monitoring'), value: 'account_model' as const } - ] - : []) + { label: t('config_model:channel'), value: 'channel' as const }, + { label: t('config_model:log'), value: 'channel_log' as const }, + { label: t('config_model:monitoring'), value: 'account_model' as const } ], - [feConfigs.show_aiproxy, t] + [t] ); const queryModelTab = router.query.modelTab; const modelTab = modelTabList.find((item) => item.value === queryModelTab)?.value ?? 'config'; diff --git a/projects/app/src/pageComponents/admin/config/feature.tsx b/projects/app/src/pageComponents/admin/config/feature.tsx index c3ff7dcadb14..821b258b760d 100644 --- a/projects/app/src/pageComponents/admin/config/feature.tsx +++ b/projects/app/src/pageComponents/admin/config/feature.tsx @@ -72,10 +72,6 @@ export const Settings = () => { <> - - - - diff --git a/projects/app/src/pageComponents/admin/config/type.ts b/projects/app/src/pageComponents/admin/config/type.ts index fa8567e0055c..093bd80d9922 100644 --- a/projects/app/src/pageComponents/admin/config/type.ts +++ b/projects/app/src/pageComponents/admin/config/type.ts @@ -150,7 +150,6 @@ export type ConfigFormType = { feConfigs: { show_workorder: boolean; appTemplateCourse: string; - show_emptyChat: boolean; show_openai_account: boolean; show_compliance_copywriting: boolean; show_dataset_feishu: boolean; diff --git a/projects/app/src/pageComponents/admin/teams/components/EditTeamModal.tsx b/projects/app/src/pageComponents/admin/teams/components/EditTeamModal.tsx deleted file mode 100644 index cbc206f83d91..000000000000 --- a/projects/app/src/pageComponents/admin/teams/components/EditTeamModal.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Box, Button, FormControl, FormLabel, Input, useDisclosure } from '@chakra-ui/react'; -import React from 'react'; -import { useForm } from 'react-hook-form'; -import { updateTeam } from '@/web/admin/team/api'; -import { useToast } from '@fastgpt/web/hooks/useToast'; -import MyModal from '@fastgpt/web/components/v2/common/MyModal'; - -export default function EditTeamModal(props: { data: any; updateData: any }) { - const { isOpen, onOpen, onClose } = useDisclosure(); - const { data, updateData } = props; - const { toast } = useToast(); - - const { - register, - handleSubmit, - reset, - formState: { errors } - } = useForm({ - defaultValues: data - }); - - const onSubmit = async (formData: any) => { - updateTeam(formData) - .then(() => { - toast({ - title: '变更成功', - status: 'success' - }); - updateData(); - onClose(); - }) - .catch((err) => { - toast({ - title: err.message, - status: 'error' - }); - }); - }; - - return ( - <> - - - - - - - } - > - - - 团队名 - - - - - - 余额 - {errors && !!errors?.balance && ( - - *必填 - - )} - - - - - - ); -} diff --git a/projects/app/src/pageComponents/admin/teams/index.tsx b/projects/app/src/pageComponents/admin/teams/index.tsx index 2722c05efc75..9cb5d395931e 100644 --- a/projects/app/src/pageComponents/admin/teams/index.tsx +++ b/projects/app/src/pageComponents/admin/teams/index.tsx @@ -17,7 +17,6 @@ import dayjs from 'dayjs'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { usePagination } from '@fastgpt/web/hooks/usePagination'; import DetailTeamModal from './components/DetailTeamModal'; -import EditTeamModal from './components/EditTeamModal'; import { getTeams } from '@/web/admin/team/api'; import BoxPageRoot from '@/components/admin/BoxContainer/PageRoot'; import { FixedTableContainer } from '@fastgpt/web/components/common/FixedTable'; @@ -107,15 +106,7 @@ const TeamTable = () => { {item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'} - - - { - getData(1); - }} - /> - + ))} diff --git a/projects/app/src/service/common/system/index.ts b/projects/app/src/service/common/system/index.ts index 53d227994d2e..cf23d95b1f7e 100644 --- a/projects/app/src/service/common/system/index.ts +++ b/projects/app/src/service/common/system/index.ts @@ -28,7 +28,6 @@ import { getAgentSandboxSkillMaxBytes } from '@fastgpt/service/core/ai/sandbox/interface/config'; import { serviceEnv } from '@fastgpt/service/env'; -import { hasAIProxyApiEndpoint } from '@fastgpt/service/thirdProvider/aiproxy/config'; import { appEnv } from '@/env'; import { pluginTagList } from '@fastgpt/global/sdk/fastgpt-plugin'; import { pluginClient } from '@fastgpt/service/thirdProvider/fastgptPlugin'; @@ -95,7 +94,6 @@ export async function getInitConfig() { } const defaultFeConfigs: FastGPTFeConfigsType = { - show_emptyChat: true, show_git: true, docUrl: 'https://doc.fastgpt.io', openAPIDocUrl: 'https://doc.fastgpt.io/openapi/intro', @@ -158,7 +156,6 @@ export async function initSystemConfig() { isProService: !!serviceEnv.PRO_URL, hideChatCopyrightSetting: appEnv.HIDE_CHAT_COPYRIGHT_SETTING, wecomLoginAutoRedirect: appEnv.WECOM_LOGIN_AUTO_REDIRECT, - show_aiproxy: hasAIProxyApiEndpoint(), show_coupon: appEnv.SHOW_COUPON, show_discount_coupon: appEnv.SHOW_DISCOUNT_COUPON, show_dataset_enhance: licenseData?.functions?.datasetEnhance, diff --git a/projects/app/src/web/admin/config/adapt.ts b/projects/app/src/web/admin/config/adapt.ts index 904bb67903f3..5f0ae486ec4d 100644 --- a/projects/app/src/web/admin/config/adapt.ts +++ b/projects/app/src/web/admin/config/adapt.ts @@ -29,7 +29,6 @@ export function formatConfigStore2FormSchema({ // 初始化配置 const { - show_emptyChat = false, show_openai_account = false, show_workorder = false, favicon = '', @@ -81,7 +80,6 @@ export function formatConfigStore2FormSchema({ siteSettings: { feConfigs: { show_workorder, - show_emptyChat, show_openai_account, show_dataset_feishu, show_dataset_yuque, diff --git a/projects/app/src/web/admin/team/api.ts b/projects/app/src/web/admin/team/api.ts index ae04b708f209..6ccff93fab14 100644 --- a/projects/app/src/web/admin/team/api.ts +++ b/projects/app/src/web/admin/team/api.ts @@ -5,17 +5,8 @@ import type { } from '@fastgpt/global/openapi/admin/team/api'; import type { PaginationProps } from '@fastgpt/global/openapi/api'; -export type AdminUpdateTeamData = { - id: string; - name?: string; - balance?: number; -}; - export const getTeams = (data: PaginationProps<{ search?: string }>) => POST('/proApi/admin/team/getTeams', data, { maxQuantity: 1 }); export const getTeamMembers = (teamId: string) => GET('/proApi/admin/team/getTeamMembers', { teamId }); - -export const updateTeam = (data: AdminUpdateTeamData) => - POST('/proApi/admin/team/updateTeam', data); From efc230eab80e1cfb6892530d478a7e709972bda1 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 22:00:00 +0800 Subject: [PATCH 07/10] fix(system): restore required containers on FastGPTConfigFileSchema and decouple openapi FastGPTConfigSchema --- packages/global/common/system/types/index.ts | 22 +++++++++++--------- packages/global/openapi/admin/system/api.ts | 15 +++++++------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/global/common/system/types/index.ts b/packages/global/common/system/types/index.ts index f018d6916f4b..e5d44dd9cc49 100644 --- a/packages/global/common/system/types/index.ts +++ b/packages/global/common/system/types/index.ts @@ -277,8 +277,8 @@ export const FastGPTFeConfigsSchema = z.looseObject({ .optional() .meta({ description: '系统各项资源上限与速率限制' }), - uploadFileMaxAmount: NumSchema.meta({ description: '单次最多上传文件数量' }), - uploadFileMaxSize: NumSchema.meta({ description: '单文件最大大小限制(MB)' }), + uploadFileMaxAmount: NumSchema.default(1000).meta({ description: '单次最多上传文件数量' }), + uploadFileMaxSize: NumSchema.default(1000).meta({ description: '单文件最大大小限制(MB)' }), evalFileMaxLines: NumSchema.optional().meta({ description: '评测用例文件最大支持行数' }), // Compute by systemEnv.customPdfParse @@ -395,13 +395,15 @@ export type customDomainType = z.infer; export const SystemEnvSchema = z.looseObject({ openapiPrefix: z.string().optional(), - datasetParseMaxProcess: NumSchema.meta({ description: '知识库解析最大并发处理进程数' }), - vectorMaxProcess: NumSchema.meta({ description: '向量化处理最大并发进程数' }), - qaMaxProcess: NumSchema.meta({ description: 'QA 问答对拆分最大并发进程数' }), - vlmMaxProcess: NumSchema.meta({ description: '视觉语言模型处理最大并发进程数' }), + datasetParseMaxProcess: NumSchema.default(10).meta({ + description: '知识库解析最大并发处理进程数' + }), + vectorMaxProcess: NumSchema.default(10).meta({ description: '向量化处理最大并发进程数' }), + qaMaxProcess: NumSchema.default(10).meta({ description: 'QA 问答对拆分最大并发进程数' }), + vlmMaxProcess: NumSchema.default(10).meta({ description: '视觉语言模型处理最大并发进程数' }), - hnswEfSearch: NumSchema.meta({ description: 'HNSW 向量检索 efSearch 参数' }), - hnswMaxScanTuples: NumSchema.meta({ description: 'HNSW 向量检索最大扫描元组数' }), + hnswEfSearch: NumSchema.default(100).meta({ description: 'HNSW 向量检索 efSearch 参数' }), + hnswMaxScanTuples: NumSchema.default(100000).meta({ description: 'HNSW 向量检索最大扫描元组数' }), customPdfParse: CustomPdfParseSchema.optional(), langfuse: LangfuseConfigSchema.optional(), fileUrlWhitelist: z.array(z.string()).optional(), @@ -417,8 +419,8 @@ export type SystemEnvType = z.infer; /* fastgpt main */ export const FastGPTConfigFileSchema = z.looseObject({ - feConfigs: FastGPTFeConfigsSchema.optional(), - systemEnv: SystemEnvSchema.optional(), + feConfigs: FastGPTFeConfigsSchema, + systemEnv: SystemEnvSchema, subPlans: SubPlanSchema.optional() }); export type FastGPTConfigFileType = z.infer; diff --git a/packages/global/openapi/admin/system/api.ts b/packages/global/openapi/admin/system/api.ts index 76ffe572e2da..f82a2431fd56 100644 --- a/packages/global/openapi/admin/system/api.ts +++ b/packages/global/openapi/admin/system/api.ts @@ -1,6 +1,6 @@ import z from 'zod'; -import { SubPlanInputSchema } from '../../../support/wallet/sub/type'; -import { FastGPTConfigFileSchema } from '../../../common/system/types'; +import { SubPlanInputSchema, SubPlanSchema } from '../../../support/wallet/sub/type'; +import { FastGPTFeConfigsSchema, SystemEnvSchema } from '../../../common/system/types'; /* ============================================================================ * API: 获取系统配置 @@ -10,10 +10,13 @@ import { FastGPTConfigFileSchema } from '../../../common/system/types'; * Tags: ['Admin', 'Settings', 'Read'] * ============================================================================ */ -export const FastGPTConfigSchema = FastGPTConfigFileSchema.meta({ - example: { feConfigs: {}, systemEnv: {} }, - description: '系统 FastGPT 配置' -}); +export const FastGPTConfigSchema = z + .looseObject({ + feConfigs: FastGPTFeConfigsSchema.optional().meta({ description: '前端功能和展示配置' }), + systemEnv: SystemEnvSchema.optional().meta({ description: '服务端系统运行配置' }), + subPlans: SubPlanSchema.optional().meta({ description: '订阅套餐配置' }) + }) + .meta({ example: { feConfigs: {}, systemEnv: {} }, description: '系统 FastGPT 配置' }); export const FastGPTProConfigSchema = z .looseObject({}) From db5287fd03a2a2feeaf3f13b72ff02da62504b43 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 22:02:24 +0800 Subject: [PATCH 08/10] feat(system): validate runtime config with safeParse and log errors on mount --- packages/service/common/system/tools.ts | 20 +++++++++++++++---- .../service/test/common/system/tools.test.ts | 16 +++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/service/common/system/tools.ts b/packages/service/common/system/tools.ts index ce510f7820e6..30254f2cfb7d 100644 --- a/packages/service/common/system/tools.ts +++ b/packages/service/common/system/tools.ts @@ -1,4 +1,7 @@ -import { type FastGPTConfigFileType } from '@fastgpt/global/common/system/types'; +import { + FastGPTConfigFileSchema, + type FastGPTConfigFileType +} from '@fastgpt/global/common/system/types'; import { isIPv6 } from 'net'; import { getLogger, LogCategories } from '../logger'; import { @@ -38,9 +41,18 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => { maxFolderDepth: serviceEnv.MAX_FOLDER_DEPTH }; - global.feConfigs = config.feConfigs; - global.systemEnv = config.systemEnv; - global.subPlans = config.subPlans; + const parseResult = FastGPTConfigFileSchema.safeParse(config); + if (!parseResult.success) { + logger.error('FastGPT system config validation failed', { + error: parseResult.error + }); + } + + const safeConfig = parseResult.success ? parseResult.data : config; + + global.feConfigs = safeConfig.feConfigs || config.feConfigs; + global.systemEnv = safeConfig.systemEnv || config.systemEnv; + global.subPlans = safeConfig.subPlans ?? config.subPlans; }; export const systemStartCb = () => { diff --git a/packages/service/test/common/system/tools.test.ts b/packages/service/test/common/system/tools.test.ts index 6b47d2741f3e..7b2b03b8a588 100644 --- a/packages/service/test/common/system/tools.test.ts +++ b/packages/service/test/common/system/tools.test.ts @@ -35,5 +35,21 @@ describe('initFastGPTConfig', () => { expect(global.feConfigs.showCustomPdfParse).toBe(true); expect(global.systemEnv.customPdfParse?.somarkApiKey).toBe('sk-test'); + expect(global.systemEnv.datasetParseMaxProcess).toBe(10); + expect(global.systemEnv.hnswEfSearch).toBe(100); + }); + + it('配置校验失败时不中断,仍可降级挂载全局状态', () => { + initFastGPTConfig({ + feConfigs: { + uploadFileMaxSize: 'invalid_size' as any + }, + systemEnv: { + datasetParseMaxProcess: 'not_a_number' as any + } + } as any); + + // 校验失败时不崩溃,保留原属性兜底挂载 + expect(global.systemEnv.datasetParseMaxProcess).toBe('not_a_number'); }); }); From 946efb201c5ba69c48366ebe19037df9dd96870c Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 22:06:15 +0800 Subject: [PATCH 09/10] docs(system): add signoz system config validation alert runbook --- .../system/signoz-system-config-alerts.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .agents/design/common/system/signoz-system-config-alerts.md diff --git a/.agents/design/common/system/signoz-system-config-alerts.md b/.agents/design/common/system/signoz-system-config-alerts.md new file mode 100644 index 000000000000..ba362860178e --- /dev/null +++ b/.agents/design/common/system/signoz-system-config-alerts.md @@ -0,0 +1,60 @@ +# SigNoz 系统配置校验失败告警手册 + +> 本文面向 SigNoz 管理员,记录针对 FastGPT 运行时系统配置校验失败(FastGPT system config validation failed)的日志告警规则配置。 + +## 1. 告警背景 + +在 FastGPT 启动和全局配置挂载(`initFastGPTConfig`)阶段,系统会通过 `FastGPTConfigFileSchema.safeParse` 对合并后的系统配置(`feConfigs`、`systemEnv`)进行严格校验。 +当数据库或环境变量中存在非预期的非法类型数据时,系统会触发降级兜底挂载,并通过 `logger.error` 打印带有稳定标识的错误日志: + +```text +FastGPT system config validation failed +``` + +该告警用于在系统配置格式异常时第一时间通知运维与开发人员介入,避免脏配置引发下游业务隐患。 + +## 2. 告警规则详细配置 + +在 SigNoz 控制台(**Alerts** -> **New Alert**)中选择 **Logs** 告警类型,按以下参数配置: + +### 2.1 基本信息 +- **Alert Name**: `FastGPT System Config Validation Failed` +- **Severity**: `Critical` / `P1` +- **Description**: `FastGPT 运行时系统配置结构校验失败,已触发兜底降级挂载,请立即检查 DB 或环境变量配置` + +### 2.2 查询条件 (Query Builder) + +| 配置项 | 推荐值 | 说明 | +| --- | --- | --- | +| **Data Source** | `Logs` | 基于日志流告警 | +| **Filter** | `body.__log_message = 'FastGPT system config validation failed'` | 匹配稳定错误消息 | +| **Log Level Filter** | `severity_text = 'error'` | 仅匹配 error 等级 | +| **Service Name Filter** | `service.name = 'fastgpt-client'` (或生产实际 `OTEL_SERVICE_NAME`) | 区分服务边界(可选) | +| **Aggregate** | `count()` | 统计发生频次 | +| **Group By** | `body.__log_message`, `service.name` | 按消息和实例聚合 | + +#### 原始查询语句 (ClickHouse / LogQL 参考) + +```sql +SELECT + count() as count +FROM signoz_logs.distributed_logs +WHERE + severity_text = 'error' + AND body ILIKE '%FastGPT system config validation failed%' +``` + +### 2.3 触发条件 (Alert Condition) + +- **Evaluation Window**: `1 minute` (1分钟滚动窗口) +- **Evaluation Frequency**: `1 minute` (每1分钟检查一次) +- **Condition**: `IS ABOVE OR EQUALS (>=) 1` (只要出现1次即触发) +- **No-data Behavior**: `Keep State` 或 `OK` + +### 2.4 通知与排查处理 + +- **Repeat Notification**: `15 分钟` +- **排查步骤**: + 1. 在 SigNoz **Logs** 页面中搜索 `body.__log_message: "FastGPT system config validation failed"`; + 2. 展开对应日志行,查看 `body.error` 字段获取详细的 Zod Schema 校验报错路径(如 `systemEnv.datasetParseMaxProcess` 或 `feConfigs.xxx`); + 3. 确认是数据库中的 `system_configs` 集合数据格式异常,还是容器环境变量传入了非法值并修复。 From 9c6f32ad9eae0a83386ce35e69d187fdc90a092c Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Tue, 22 Sep 2026 22:16:27 +0800 Subject: [PATCH 10/10] fix(types): use z.object for NavbarItemSchema and ExternalProviderWorkflowVarSchema --- packages/global/common/system/types/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/global/common/system/types/index.ts b/packages/global/common/system/types/index.ts index e5d44dd9cc49..d6558016b5ad 100644 --- a/packages/global/common/system/types/index.ts +++ b/packages/global/common/system/types/index.ts @@ -29,7 +29,7 @@ export { LicenseTypeSchema } from '../license/schema'; -export const NavbarItemSchema = z.looseObject({ +export const NavbarItemSchema = z.object({ id: z.string(), name: z.string(), avatar: z.string(), @@ -38,7 +38,7 @@ export const NavbarItemSchema = z.looseObject({ }); export type NavbarItemType = z.infer; -export const ExternalProviderWorkflowVarSchema = z.looseObject({ +export const ExternalProviderWorkflowVarSchema = z.object({ name: z.string(), key: z.string(), intro: z.string(),