From 5152121c33122c28a72fdf5d4d8083fb1f7ffe1a Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Tue, 26 May 2026 10:11:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=20API=20=E5=9C=B0=E5=9D=80=E5=B9=B6=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20editModule.vue=20=E8=A1=A8=E5=8D=95=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 editModule.vue 中添加响应字段和请求映射的验证逻辑,确保用户输入有效性,提升表单提交的可靠性。 --- .../modelModule/component/editModule.vue | 125 +++++++++++++++--- 1 file changed, 105 insertions(+), 20 deletions(-) diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index 0d75179..392700e 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -563,6 +563,18 @@ const state = reactive({ callback(new Error('主动拉取时,请填写拉取地址')); return; } + // 验证响应字段至少有一个有效字段 + const validResponseFields = pullConfigForm.responseFields.filter((f) => String(f.value || '').trim() !== ''); + if (validResponseFields.length === 0) { + callback(new Error('主动拉取时,至少需要配置一个响应字段')); + return; + } + // 验证是否设置了返回主体字段 + const hasMainBody = pullConfigForm.responseFields.some((f) => f.isMainBody && String(f.value || '').trim() !== ''); + if (!hasMainBody) { + callback(new Error('主动拉取时,必须设置一个返回主体字段')); + return; + } callback(); }, trigger: 'change', @@ -595,10 +607,47 @@ const state = reactive({ queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }], timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }], expectedSeconds: [{ required: true, message: '请输入预计执行时间', trigger: 'blur' }], + requestMapping: [ + { + validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => { + const emptyKeys = state.requestMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== ''); + if (emptyKeys.length > 0) { + callback(new Error('请求映射字段名不能为空')); + return; + } + callback(); + }, + trigger: 'change', + }, + ], + responseMapping: [ + { + validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => { + // 检查是否有空键名但有值的字段 + const emptyKeys = state.responseMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== ''); + if (emptyKeys.length > 0) { + callback(new Error('响应映射字段名不能为空')); + return; + } + // 检查是否设置了返回主体字段(必填) + const validFields = state.responseMappingFields.filter((x) => String(x.key || '').trim() !== ''); + if (validFields.length > 0) { + const hasMainBody = state.responseMappingFields.some((f) => f.isMainBody && String(f.key || '').trim() !== ''); + if (!hasMainBody) { + callback(new Error('响应映射必须设置一个返回主体字段')); + return; + } + } + callback(); + }, + trigger: 'change', + }, + ], tokenConfig: [ { validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => { - if (!state.tokenConfigFields.length || state.tokenConfigFields.some((x) => !String(x.key || '').trim())) { + const emptyKeys = state.tokenConfigFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== ''); + if (emptyKeys.length > 0) { callback(new Error('Token计算配置字段名不能为空')); return; } @@ -610,7 +659,8 @@ const state = reactive({ extendMapping: [ { validator: (_rule: unknown, _value: unknown, callback: (e?: Error) => void) => { - if (state.extendMappingFields.some((x) => !String(x.key || '').trim())) { + const emptyKeys = state.extendMappingFields.filter((x) => String(x.key || '').trim() === '' && String(x.value || '').trim() !== ''); + if (emptyKeys.length > 0) { callback(new Error('附加映射字段名不能为空')); return; } @@ -650,26 +700,41 @@ const fieldsToObject = (fields: Array<{ key: string; value: string }>) => { }; const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg); -// 解析 form:支持数组 [{ key, value }] 或历史对象 { k: { value } } -const parseFormFields = (form: unknown) => { - if (!form) return []; - if (Array.isArray(form)) { - return (form as ModelFormEntry[]) +// 统一的字段解析函数:支持数组、对象、JSON字符串 +const parseFieldsUnified = (raw: unknown): Array<{ key: string; value: string }> => { + if (!raw) return []; + + // 如果是字符串,尝试解析为JSON + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw); + return parseFieldsUnified(parsed); + } catch { + return []; + } + } + + // 如果是数组格式 [{ key, value }] + if (Array.isArray(raw)) { + return (raw as ModelFormEntry[]) .filter((item) => item && (item.key !== undefined || item.value !== undefined)) .map((item) => ({ key: String(item.key ?? '').trim(), value: String(item.value ?? '').trim(), })); } - if (typeof form === 'object') { + + // 如果是对象格式 { key: value } + if (typeof raw === 'object') { const fields: Array<{ key: string; value: string }> = []; - Object.keys(form as Record).forEach((key) => { - const v = (form as Record)[key]; - const value = String(v || ''); + Object.keys(raw as Record).forEach((key) => { + const v = (raw as Record)[key]; + const value = String(v ?? ''); fields.push({ key, value }); }); return fields; } + return []; }; // 解析 requestMapping 对象为数组 @@ -949,12 +1014,11 @@ const fillFormFromDetailRow = (row: Record) => { tokenConfig: '{}', }; state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || ''))); - state.formFields = ensureKeyValueRows(parseFormFields(row.form)); - // 解析请求映射和响应映射 + state.formFields = ensureKeyValueRows(parseFieldsUnified(row.form)); state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping)); state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping)); - state.extendMappingFields = ensureKeyValueRows(objectToFields((row.extendMapping as Record) || {})); - state.tokenConfigFields = ensureKeyValueRows(objectToFields((row.tokenConfig as Record) || {})); + state.extendMappingFields = ensureKeyValueRows(parseFieldsUnified(row.extendMapping)); + state.tokenConfigFields = ensureKeyValueRows(parseFieldsUnified(row.tokenConfig)); // 根据 responseTokenField 字段设置计费字段标记(单选) const tokenFieldKey = String(row.responseTokenField || '').trim(); @@ -1102,17 +1166,37 @@ const onSubmit = () => { state.dialog.loading = true; try { + // 触发所有自定义字段的验证 if (state.ruleForm.queryResponseType === 'pull') { await editModuleFormRef.value?.validateField?.('queryPullConfig'); } + + // 验证响应映射(如果有配置) + const validResponseFields = state.responseMappingFields.filter((x) => String(x.key || '').trim() !== ''); + if (validResponseFields.length > 0) { + await editModuleFormRef.value?.validateField?.('responseMapping'); + } + + // 验证请求映射(如果有配置) + const validRequestFields = state.requestMappingFields.filter((x) => String(x.key || '').trim() !== ''); + if (validRequestFields.length > 0) { + await editModuleFormRef.value?.validateField?.('requestMapping'); + } + state.ruleForm.headMsg = stringifyHeaders(); - const requestMapping = fieldsToObject(state.requestMappingFields); - const responseMapping = fieldsToObject(state.responseMappingFields); + + // 过滤掉空键名的字段 + const requestMapping = fieldsToObject(state.requestMappingFields.filter((f) => String(f.key || '').trim() !== '')); + const responseMapping = fieldsToObject(state.responseMappingFields.filter((f) => String(f.key || '').trim() !== '')); + // 获取被设置为返回主体的字段 {key: value} - const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody); + const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody && String(f.key || '').trim() !== ''); const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {}; + + // 获取计费字段(可选) const responseTokenField = state.responseMappingFields.find((f) => f.isTokenField)?.key?.trim() || String(state.ruleForm.responseTokenField || '').trim(); + const submitData: CreateModelParams = { modelName: state.ruleForm.modelName, modelType: state.ruleForm.modelType as number | string, @@ -1123,6 +1207,7 @@ const onSubmit = () => { isPrivate: state.ruleForm.isPrivate, enabled: state.ruleForm.enabled, isChatModel: state.ruleForm.isChatModel, + // 确保 API 密钥只在 isPrivate=1 时提交 apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '', form: state.formFields .filter((f) => String(f.key || '').trim() !== '') @@ -1138,9 +1223,9 @@ const onSubmit = () => { retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds, autoCleanSeconds: state.ruleForm.autoCleanSeconds, remark: state.ruleForm.remark || '', - extendMapping: fieldsToUnknownObject(state.extendMappingFields), + extendMapping: fieldsToUnknownObject(state.extendMappingFields.filter((f) => String(f.key || '').trim() !== '')), responseTokenField, - tokenConfig: fieldsToUnknownObject(state.tokenConfigFields), + tokenConfig: fieldsToUnknownObject(state.tokenConfigFields.filter((f) => String(f.key || '').trim() !== '')), queryConfig: buildQueryConfig(), };