From af3f0678b8293ef108d30a66e2b5f1523720c19a Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Thu, 4 Jun 2026 10:16:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E7=9B=B8?= =?UTF-8?q?=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/modelConfig/modelModule/index.ts | 8 +- src/components/model/ModelSelector.vue | 35 +++++++- .../modelModule/component/editModule.vue | 89 ++++++++++++++++++- .../modelConfig/modelModule/index.vue | 35 +++++++- 4 files changed, 159 insertions(+), 8 deletions(-) diff --git a/src/api/settings/modelConfig/modelModule/index.ts b/src/api/settings/modelConfig/modelModule/index.ts index 0d2f2b3..d930467 100644 --- a/src/api/settings/modelConfig/modelModule/index.ts +++ b/src/api/settings/modelConfig/modelModule/index.ts @@ -119,6 +119,8 @@ export interface ModelModuleItem { /** 会话开关状态(列表接口返回,0 关 1 开;会话开关接口就绪后生效) */ chatSessionEnabled?: number; enabled: number; + /** 是否异步 0-同步 1-异步,依赖requestMapping */ + isAsync?: number; maxConcurrency: number; queueLimit: number; timeoutMs?: number; @@ -128,7 +130,7 @@ export interface ModelModuleItem { retryQueueMaxSeconds: number; autoCleanSeconds: number; remark?: string; - headMsg?: string; + headMsg?: string | Record; form?: ModelFormEntry[] | Record; requestMapping?: Record; responseMapping?: Record; @@ -150,10 +152,12 @@ export interface CreateModelParams { operatorName?: string; baseUrl: string; httpMethod?: string; - headMsg?: string; + headMsg?: Record; isPrivate: number; enabled: number; isChatModel: number; + /** 是否异步 0-同步 1-异步,依赖requestMapping,默认0 */ + isAsync: number; apiKey?: string; form: ModelFormEntry[]; requestMapping?: Record; diff --git a/src/components/model/ModelSelector.vue b/src/components/model/ModelSelector.vue index a59faea..68d9860 100644 --- a/src/components/model/ModelSelector.vue +++ b/src/components/model/ModelSelector.vue @@ -402,16 +402,49 @@ const handleCreatePrivateModel = async () => { key: String(key), value: String(value ?? ''), })); + + // Parse headMsg to Record - it might be stored as string or already as object + let headMsgRecord: Record = {}; + if (builtInModel.headMsg && typeof builtInModel.headMsg === 'string') { + // Try to parse as JSON first (new format stored as string) + try { + const parsed = JSON.parse(builtInModel.headMsg); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + Object.entries(parsed).forEach(([k, v]) => { + headMsgRecord[k] = String(v); + }); + } + } catch { + // If JSON parse fails, parse as old format "key1:value1,key2:value2" + const pairs = builtInModel.headMsg.split(','); + pairs.forEach((pair) => { + const idx = pair.indexOf(':'); + if (idx === -1) return; + const key = pair.slice(0, idx).trim(); + const value = pair.slice(idx + 1).trim(); + if (key) { + headMsgRecord[key] = value; + } + }); + } + } else if (builtInModel.headMsg && typeof builtInModel.headMsg === 'object' && !Array.isArray(builtInModel.headMsg)) { + // Already an object + Object.entries(builtInModel.headMsg).forEach(([k, v]) => { + headMsgRecord[k] = String(v); + }); + } + const createParams: CreateModelParams = { modelName: apiKeyForm.modelName, modelType: builtInModel.modelType, operatorName: builtInModel.operatorName || '', baseUrl: builtInModel.baseUrl, httpMethod: builtInModel.httpMethod || 'POST', - headMsg: builtInModel.headMsg || '', + headMsg: headMsgRecord, isPrivate: builtInModel.isPrivate ?? 1, enabled: builtInModel.enabled ?? 1, isChatModel: builtInModel.isChatModel || 0, + isAsync: builtInModel.isAsync ?? 0, apiKey: apiKeyForm.apiKey, form: formList, requestMapping: (builtInModel.requestMapping as Record) || {}, diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index ad53e1a..6038b3b 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -83,6 +83,14 @@ + + + + 同步 + 异步 + + + 配置请求头 ({{ state.headers.length }}) @@ -648,6 +656,7 @@ const state = reactive({ apiKey: '', enabled: 1, isChatModel: 0, + isAsync: 0, // 0-同步 1-异步,默认0 maxConcurrency: 10, queueLimit: 100, timeoutSeconds: 30, @@ -859,7 +868,58 @@ const fieldsToObject = (fields: Array<{ key: string; value: string }>) => { return obj; }; -const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg); +// 解析headers,支持多种格式: +// 1. 旧格式:逗号分隔的"key1:value1,key2:value2"字符串 +// 2. JSON字符串:格式化为JSON对象的字符串 +// 3. 新格式:直接是Record对象 +const parseHeaders = (raw: unknown): Array<{ key: string; value: string }> => { + if (!raw) return []; + + // 如果是字符串,先尝试解析为JSON,如果失败则尝试解析为旧格式key:value,key2:value2 + if (typeof raw === 'string') { + const trimmed = raw.trim(); + if (!trimmed) return []; + // 试试JSON解析,如果开头是{或者[说明是JSON + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed); + return parseHeaders(parsed); + } catch { + // JSON解析失败,回退到旧格式解析 + return parseKeyValueString(trimmed); + } + } + // 不是JSON格式,按旧格式解析 + return parseKeyValueString(trimmed); + } + + // 如果是数组格式 [{ key, value }] + if (Array.isArray(raw)) { + return raw + .filter((item) => item && (item.key !== undefined || item.value !== undefined)) + .map((item) => ({ + key: String(item.key ?? '').trim(), + value: String(item.value ?? '').trim(), + })); + } + + // 如果是对象格式 { key: value } + if (typeof raw === 'object') { + const fields: Array<{ key: string; value: string }> = []; + Object.keys(raw as Record).forEach((key) => { + let v = (raw as Record)[key]; + // 处理 { key: { value: value } } 格式(后端可能返回这种结构) + if (v && typeof v === 'object' && !Array.isArray(v) && 'value' in v) { + v = (v as { value: unknown }).value; + } + const value = String(v ?? ''); + fields.push({ key: key.trim(), value }); + }); + return fields; + } + + return []; +}; // 统一的字段解析函数:支持数组、对象、JSON字符串 const parseFieldsUnified = (raw: unknown): Array<{ key: string; value: string }> => { if (!raw) return []; @@ -1198,6 +1258,16 @@ const fillFormFromDetailRow = (row: Record) => { ? Math.floor(Number(row.timeoutMs) / 1000) : 30; const isPrivate = row.isPrivate !== undefined && row.isPrivate !== null ? Number(row.isPrivate) : 0; + // headMsg might already be a Record object (new format) or a string (old format) + let ruleFormHeadMsg = ''; + if (typeof row.headMsg === 'string') { + ruleFormHeadMsg = row.headMsg; + } else if (row.headMsg && typeof row.headMsg === 'object') { + // If it's already an object, stringify for compatibility + ruleFormHeadMsg = JSON.stringify(row.headMsg); + } else { + ruleFormHeadMsg = ''; + } state.ruleForm = { id: row.id as string, modelName: String(row.modelName ?? ''), @@ -1207,11 +1277,12 @@ const fillFormFromDetailRow = (row: Record) => { httpMethod: String(row.httpMethod || 'POST'), queryResponseType: String((row.queryConfig as Record)?.responseType || 'sync'), queryCallbackUrl: String((row.queryConfig as Record)?.callbackUrl || ''), - headMsg: String(row.headMsg || ''), + headMsg: ruleFormHeadMsg, isPrivate, apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '', enabled: Number(row.enabled ?? 1), isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0, + isAsync: row.isAsync !== undefined && row.isAsync !== null ? Number(row.isAsync) : 0, maxConcurrency: Number(row.maxConcurrency ?? 10), queueLimit: Number(row.queueLimit ?? 100), timeoutSeconds, @@ -1224,7 +1295,7 @@ const fillFormFromDetailRow = (row: Record) => { responseTokenField: String(row.responseTokenField || ''), tokenConfig: '{}', }; - state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || ''))); + state.headers = ensureKeyValueRows(parseHeaders(row.headMsg)); state.formFields = parseFormFieldsUnified(row.form); state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping)); state.responseMappingFields = ensureKeyValueRows(parseResponseMappingFields(row.responseMapping)); @@ -1329,6 +1400,7 @@ const openDialog = async (type: string, row?: Record) => { apiKey: '', enabled: 1, isChatModel: 0, + isAsync: 0, maxConcurrency: 10, queueLimit: 100, timeoutSeconds: 30, @@ -1430,16 +1502,25 @@ const onSubmit = () => { : undefined, })) as unknown as ModelFormEntry[]; + // 将headers转换为JSON对象格式 + const headMsgObj: Record = {}; + state.headers + .filter((h) => h.key?.trim() && h.value?.trim()) + .forEach((h) => { + headMsgObj[h.key.trim()] = h.value.trim(); + }); + const submitData: CreateModelParams = { modelName: state.ruleForm.modelName, modelType: state.ruleForm.modelType as number | string, operatorName: state.ruleForm.operatorName, baseUrl: state.ruleForm.baseUrl, httpMethod: state.ruleForm.httpMethod || 'POST', - headMsg: state.ruleForm.headMsg, + headMsg: headMsgObj, isPrivate: state.ruleForm.isPrivate, enabled: state.ruleForm.enabled, isChatModel: state.ruleForm.isChatModel, + isAsync: state.ruleForm.isAsync, // 确保 API 密钥只在 isPrivate=1 时提交 apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '', form: processedFormFields, diff --git a/src/views/settings/modelConfig/modelModule/index.vue b/src/views/settings/modelConfig/modelModule/index.vue index 5763da6..0285e0c 100644 --- a/src/views/settings/modelConfig/modelModule/index.vue +++ b/src/views/settings/modelConfig/modelModule/index.vue @@ -240,15 +240,48 @@ const handleCreatePrivateModelAndSetChat = async () => { // 基于内置模型创建新模型(继承原模型的所有配置,只替换 apiKey) const builtInModel = builtInModelToClone.value; + + // Parse headMsg to Record - it might be stored as string or already as object + let headMsgRecord: Record = {}; + if (builtInModel.headMsg && typeof builtInModel.headMsg === 'string') { + // Try to parse as JSON first (new format stored as string) + try { + const parsed = JSON.parse(builtInModel.headMsg); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + Object.entries(parsed).forEach(([k, v]) => { + headMsgRecord[k] = String(v); + }); + } + } catch { + // If JSON parse fails, parse as old format "key1:value1,key2:value2" + const pairs = builtInModel.headMsg.split(','); + pairs.forEach((pair: string) => { + const idx = pair.indexOf(':'); + if (idx === -1) return; + const key = pair.slice(0, idx).trim(); + const value = pair.slice(idx + 1).trim(); + if (key) { + headMsgRecord[key] = value; + } + }); + } + } else if (builtInModel.headMsg && typeof builtInModel.headMsg === 'object' && !Array.isArray(builtInModel.headMsg)) { + // Already an object + Object.entries(builtInModel.headMsg).forEach(([k, v]) => { + headMsgRecord[k] = String(v); + }); + } + const createParams = { modelName: apiKeyForm.modelName, modelType: builtInModel.modelType, baseUrl: builtInModel.baseUrl, httpMethod: builtInModel.httpMethod || 'POST', - headMsg: builtInModel.headMsg || '', + headMsg: headMsgRecord, isPrivate: builtInModel.isPrivate ?? 1, enabled: builtInModel.enabled ?? 1, isChatModel: 1, // 设置为会话模型 + isAsync: builtInModel.isAsync ?? 0, apiKey: apiKeyForm.apiKey, form: builtInModel.form || {}, requestMapping: builtInModel.requestMapping || {},