From fafaa28fc8ca1a2b94d8b5cd3d35d8800de3e927 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Thu, 14 May 2026 18:14:05 +0800 Subject: [PATCH] fix: schema parse,default config --- packages/global/common/string/url.ts | 1 + packages/global/common/zod/index.ts | 22 ++++++ packages/global/core/workflow/type/io.ts | 73 +++++++++++-------- packages/service/core/ai/config/utils.ts | 26 ++++--- projects/app/package.json | 2 +- .../app/src/pages/api/core/ai/model/update.ts | 17 ++--- .../pages/api/core/ai/model/updateWithJson.ts | 14 ++-- 7 files changed, 96 insertions(+), 59 deletions(-) create mode 100644 packages/global/common/string/url.ts create mode 100644 packages/global/common/zod/index.ts diff --git a/packages/global/common/string/url.ts b/packages/global/common/string/url.ts new file mode 100644 index 0000000000..1b1f4a4743 --- /dev/null +++ b/packages/global/common/string/url.ts @@ -0,0 +1 @@ +export const stripUrlTrailingSlash = (value?: string) => value?.replace(/\/+$/, '') || ''; diff --git a/packages/global/common/zod/index.ts b/packages/global/common/zod/index.ts new file mode 100644 index 0000000000..6be608e06f --- /dev/null +++ b/packages/global/common/zod/index.ts @@ -0,0 +1,22 @@ +import z from 'zod'; +import { stripUrlTrailingSlash } from '../string/url'; + +const truthyBoolStrs = ['true', '1', 'yes', 'y', 'on']; +export const BoolSchema = z.preprocess((val) => { + if (typeof val === 'boolean') return val; + + if (typeof val === 'string') { + return truthyBoolStrs.includes(val.trim().toLowerCase()); + } + + if (typeof val === 'number') { + if (val === 1) return true; + if (val === 0) return false; + } + + return val; +}, z.boolean()); + +export const NumSchema = z.coerce.number(); +export const IntSchema = NumSchema.int().nonnegative(); +export const UrlSchema = z.string().url().transform(stripUrlTrailingSlash); diff --git a/packages/global/core/workflow/type/io.ts b/packages/global/core/workflow/type/io.ts index 66e3dee2a0..22d53aeee6 100644 --- a/packages/global/core/workflow/type/io.ts +++ b/packages/global/core/workflow/type/io.ts @@ -3,6 +3,7 @@ import { WorkflowIOValueTypeEnum, NodeInputKeyEnum, NodeOutputKeyEnum } from '.. import { FlowNodeInputTypeEnum, FlowNodeOutputTypeEnum } from '../node/constant'; import { SecretValueTypeSchema } from '../../../common/secret/type'; import z from 'zod'; +import { BoolSchema, IntSchema, NumSchema } from '../../../common/zod'; /* Dataset node */ export const SelectedDatasetSchema = z.object({ @@ -19,8 +20,9 @@ export type SelectedDatasetType = z.infer; export const CustomFieldConfigTypeSchema = z.object({ // reference selectValueTypeList: z.array(z.enum(WorkflowIOValueTypeEnum)).optional(), // 可以选哪个数据类型, 只有1个的话,则默认选择 - showDefaultValue: z.boolean().optional(), - showDescription: z.boolean().optional() + showDefaultValue: BoolSchema.optional(), + showDescription: BoolSchema.optional(), + hideBottomDivider: BoolSchema.optional() }); export type CustomFieldConfigType = z.infer; @@ -29,31 +31,40 @@ export const InputComponentPropsTypeSchema = z.object({ label: z.string(), valueType: z.enum(WorkflowIOValueTypeEnum).optional(), - required: z.boolean().optional(), + required: BoolSchema.optional(), defaultValue: z.any().optional(), // 不同组件的配置嘻嘻 referencePlaceholder: z.string().optional(), - isRichText: z.boolean().optional(), // Prompt editor + isRichText: BoolSchema.optional(), // Prompt editor placeholder: z.string().optional(), // input,textarea - maxLength: z.number().optional(), // input,textarea - minLength: z.number().optional(), // password - list: z.array(z.object({ label: z.string(), value: z.string() })).optional(), // select - markList: z.array(z.object({ label: z.string(), value: z.number() })).optional(), // slider - step: z.number().optional(), // slider - max: z.number().optional(), // slider, number input - min: z.number().optional(), // slider, number input - precision: z.number().optional(), // number input + maxLength: IntSchema.optional(), // input,textarea + minLength: IntSchema.optional(), // password + list: z + .array( + z.object({ + label: z.string(), + value: z.string(), + icon: z.string().optional(), + description: z.string().optional() + }) + ) + .optional(), // select + markList: z.array(z.object({ label: z.string(), value: NumSchema })).optional(), // slider + step: NumSchema.optional(), // slider + max: NumSchema.optional(), // slider, number input + min: NumSchema.optional(), // slider, number input + precision: NumSchema.optional(), // number input - canSelectFile: z.boolean().optional(), // file select - canSelectImg: z.boolean().optional(), // file select - canSelectVideo: z.boolean().optional(), // file select - canSelectAudio: z.boolean().optional(), // file select - canSelectCustomFileExtension: z.boolean().optional(), // file select + canSelectFile: BoolSchema.optional(), // file select + canSelectImg: BoolSchema.optional(), // file select + canSelectVideo: BoolSchema.optional(), // file select + canSelectAudio: BoolSchema.optional(), // file select + canSelectCustomFileExtension: BoolSchema.optional(), // file select customFileExtensionList: z.array(z.string()).optional(), // file select - canLocalUpload: z.boolean().optional(), // file select - canUrlUpload: z.boolean().optional(), // file select - maxFiles: z.number().optional(), // file select + canLocalUpload: BoolSchema.optional(), // file select + canUrlUpload: BoolSchema.optional(), // file select + maxFiles: IntSchema.optional(), // file select // Time timeGranularity: z.enum(['day', 'hour', 'minute', 'second']).optional(), // time point select, time range select @@ -76,7 +87,7 @@ export const InputConfigTypeSchema = z.object({ key: z.string(), label: z.string(), description: z.string().optional(), - required: z.boolean().optional(), + required: BoolSchema.optional(), inputType: z.enum(['input', 'numberInput', 'secret', 'switch', 'select']), value: SecretValueTypeSchema.optional(), @@ -87,7 +98,7 @@ export type InputConfigType = z.infer; // Workflow node input export const FlowNodeInputItemTypeSchema = InputComponentPropsTypeSchema.extend({ - selectedTypeIndex: z.number().optional(), + selectedTypeIndex: IntSchema.optional(), renderTypeList: z.array(z.enum(FlowNodeInputTypeEnum)), // Node Type. Decide on a render style valueDesc: z.string().optional(), // data desc value: z.any().optional(), @@ -101,11 +112,11 @@ export const FlowNodeInputItemTypeSchema = InputComponentPropsTypeSchema.extend( inputList: z.array(InputConfigTypeSchema).optional(), // when key === 'system_input_config', this field is used // render components params - canEdit: z.boolean().optional(), // dynamic inputs - isPro: z.boolean().optional(), // Pro version field - isToolOutput: z.boolean().optional(), + canEdit: BoolSchema.optional(), // dynamic inputs + isPro: BoolSchema.optional(), // Pro version field + isToolOutput: BoolSchema.optional(), - deprecated: z.boolean().optional() // node deprecated + deprecated: BoolSchema.optional() // node deprecated }); export type FlowNodeInputItemType = z.infer; @@ -121,18 +132,18 @@ export const FlowNodeOutputItemTypeSchema = z.object({ label: z.string().optional(), description: z.string().optional(), defaultValue: z.any().optional(), - required: z.boolean().optional(), + required: BoolSchema.optional(), - invalid: z.boolean().optional(), + invalid: BoolSchema.optional(), invalidCondition: z .function({ input: z.tuple([ z.object({ - inputs: z.array(FlowNodeInputItemTypeSchema), + inputs: z.custom(), llmModelMap: z.record(z.string(), LLMModelItemSchema) }) ]), - output: z.boolean() + output: BoolSchema }) .optional() .meta({ @@ -143,7 +154,7 @@ export const FlowNodeOutputItemTypeSchema = z.object({ }), customFieldConfig: CustomFieldConfigTypeSchema.optional(), - deprecated: z.boolean().optional() + deprecated: BoolSchema.optional() }); export type FlowNodeOutputItemType = z.infer; diff --git a/packages/service/core/ai/config/utils.ts b/packages/service/core/ai/config/utils.ts index 356efe63f7..c2fcb295db 100644 --- a/packages/service/core/ai/config/utils.ts +++ b/packages/service/core/ai/config/utils.ts @@ -35,14 +35,14 @@ export const loadSystemModels = async (init = false, language = 'en') => { return Promise.reject(error); } - let _systemModelList: SystemModelItemType[] = []; - let _systemActiveModelList: SystemModelItemType[] = []; - let _llmModelMap = new Map(); - let _embeddingModelMap = new Map(); - let _ttsModelMap = new Map(); - let _sttModelMap = new Map(); - let _reRankModelMap = new Map(); - let _systemDefaultModel: SystemDefaultModelType = {}; + const _systemModelList: SystemModelItemType[] = []; + const _systemActiveModelList: SystemModelItemType[] = []; + const _llmModelMap = new Map(); + const _embeddingModelMap = new Map(); + const _ttsModelMap = new Map(); + const _sttModelMap = new Map(); + const _reRankModelMap = new Map(); + const _systemDefaultModel: SystemDefaultModelType = {}; if (!global.systemModelList) { global.systemModelList = []; @@ -144,8 +144,14 @@ export const loadSystemModels = async (init = false, language = 'en') => { ...(model.type === ModelTypeEnum.llm && dbModel?.metadata?.type === ModelTypeEnum.llm ? { maxResponse: dbModel?.metadata?.maxResponse ?? model.maxTokens ?? 8000, - defaultConfig: mergeObject(model.defaultConfig, dbModel?.metadata?.defaultConfig), - fieldMap: mergeObject(model.fieldMap, dbModel?.metadata?.fieldMap), + defaultConfig: + typeof dbModel?.metadata?.defaultConfig === 'object' + ? dbModel?.metadata?.defaultConfig + : model.defaultConfig, + fieldMap: + typeof dbModel?.metadata?.fieldMap === 'object' + ? dbModel?.metadata?.fieldMap + : model.fieldMap, /** @deprecated */ maxTokens: undefined } diff --git a/projects/app/package.json b/projects/app/package.json index acb80b7bf6..d61d72c352 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "@fastgpt/app", - "version": "4.14.19", + "version": "4.14.20", "private": false, "browserslist": [ "Chrome >= 80", diff --git a/projects/app/src/pages/api/core/ai/model/update.ts b/projects/app/src/pages/api/core/ai/model/update.ts index 11ebaf4ec8..26bead4611 100644 --- a/projects/app/src/pages/api/core/ai/model/update.ts +++ b/projects/app/src/pages/api/core/ai/model/update.ts @@ -6,22 +6,16 @@ import { findModelFromAlldata } from '@fastgpt/service/core/ai/model'; import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils'; import { ModelTypeEnum } from '@fastgpt/global/core/ai/constants'; -export type updateQuery = {}; - export type updateBody = { model: string; metadata?: Record; }; -export type updateResponse = {}; - -async function handler( - req: ApiRequestProps, - res: ApiResponseType -): Promise { +async function handler(req: ApiRequestProps, res: ApiResponseType) { await authSystemAdmin({ req }); - let { model, metadata } = req.body; + const metadata = req.body.metadata; + let { model } = req.body; if (!model) return Promise.reject(new Error('model is required')); model = model.trim(); @@ -60,6 +54,11 @@ async function handler( } }); + // 强制更新 defaultConfig 数据类型 + if ('defaultConfig' in metadataConcat && typeof metadataConcat.defaultConfig !== 'object') { + metadataConcat.defaultConfig = {}; + } + await MongoSystemModel.updateOne( { model }, { diff --git a/projects/app/src/pages/api/core/ai/model/updateWithJson.ts b/projects/app/src/pages/api/core/ai/model/updateWithJson.ts index 72aa5fb104..8f7dbf09dc 100644 --- a/projects/app/src/pages/api/core/ai/model/updateWithJson.ts +++ b/projects/app/src/pages/api/core/ai/model/updateWithJson.ts @@ -6,18 +6,11 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema'; import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils'; -export type updateWithJsonQuery = {}; - export type updateWithJsonBody = { config: string; }; -export type updateWithJsonResponse = {}; - -async function handler( - req: ApiRequestProps, - res: ApiResponseType -): Promise { +async function handler(req: ApiRequestProps, res: ApiResponseType) { await authSystemAdmin({ req }); const { config } = req.body; @@ -41,6 +34,11 @@ async function handler( if (!item.metadata.name) { item.metadata.name = item.model; } + if ('defaultConfig' in item.metadata && typeof item.metadata.defaultConfig !== 'object') { + { + item.metadata.defaultConfig = {}; + } + } } await mongoSessionRun(async (session) => {