diff --git a/src/api/settings/modelConfig/modelModule/index.ts b/src/api/settings/modelConfig/modelModule/index.ts index 89741eb..a706c79 100644 --- a/src/api/settings/modelConfig/modelModule/index.ts +++ b/src/api/settings/modelConfig/modelModule/index.ts @@ -112,6 +112,7 @@ export interface CreateModelParams { modelName: string; /** 与 listType 返回的类型 id 一致,可能为数字或字符串 */ modelType: number | string; + operatorName?: string; baseUrl: string; httpMethod?: string; headMsg?: string; @@ -122,6 +123,10 @@ export interface CreateModelParams { form: ModelFormEntry[]; requestMapping?: Record; responseMapping?: Record; + responseBody?: Record; + extendMapping?: Record; + responseTokenField?: string; + tokenConfig?: Record; maxConcurrency?: number; queueLimit?: number; timeoutSeconds: number; diff --git a/src/components/model/ModelSelector.vue b/src/components/model/ModelSelector.vue index 97206fc..7751e58 100644 --- a/src/components/model/ModelSelector.vue +++ b/src/components/model/ModelSelector.vue @@ -92,7 +92,14 @@ import { ref, reactive, watch, onMounted } from 'vue'; import { ElMessage, type FormInstance, type FormRules } from 'element-plus'; import { Search, CircleCheck } from '@element-plus/icons-vue'; -import { getModelModuleList, addModelModule, getModelTypeList, normalizeModelTypeOptions } from '/@/api/settings/modelConfig/modelModule'; +import { + getModelModuleList, + addModelModule, + getModelTypeList, + normalizeModelTypeOptions, + type CreateModelParams, + type ModelFormEntry, +} from '/@/api/settings/modelConfig/modelModule'; import { checkIsSuperAdmin } from '/@/api/system/user/index'; import { getApiErrorMessage } from '/@/utils/request'; import EditModule from '/@/views/settings/modelConfig/modelModule/component/editModule.vue'; @@ -101,7 +108,7 @@ interface ModelItem { id: string; tenantId?: number; modelName: string; - modelType: number; + modelType: number | string; baseUrl: string; route: string; httpMethod: string; @@ -113,9 +120,10 @@ interface ModelItem { operatorName?: string; responseBody?: Record; tokenConfig?: Record | string; - form?: any; - requestMapping?: any; - responseMapping?: any; + extendMapping?: Record | string; + form?: ModelFormEntry[] | Record; + requestMapping?: Record; + responseMapping?: Record; maxConcurrency?: number; queueLimit?: number; timeoutSeconds?: number; @@ -206,13 +214,30 @@ watch( } ); -const getModelTypeName = (type: number) => { +const getModelTypeName = (type: number | string) => { const typeMap: Record = { 1: '推理模型', 2: '图片模型', 3: '音频模型', }; - return typeMap[type] || '未知类型'; + return typeMap[Number(type)] || '未知类型'; +}; + +const parseJsonObjectField = (raw: unknown): Record => { + if (raw && typeof raw === 'object' && !Array.isArray(raw)) { + return raw as Record; + } + if (typeof raw === 'string') { + try { + const parsed = JSON.parse(raw || '{}'); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + return {}; + } + } + return {}; }; const fetchModelList = async () => { @@ -270,7 +295,13 @@ const handleCreatePrivateModel = async () => { creatingModel.value = true; const builtInModel = builtInModelToClone.value; - const createParams = { + const formList: ModelFormEntry[] = Array.isArray(builtInModel.form) + ? (builtInModel.form as ModelFormEntry[]) + : Object.entries((builtInModel.form as Record) || {}).map(([key, value]) => ({ + key: String(key), + value: String(value ?? ''), + })); + const createParams: CreateModelParams = { modelName: apiKeyForm.modelName, modelType: builtInModel.modelType, operatorName: builtInModel.operatorName || '', @@ -281,9 +312,9 @@ const handleCreatePrivateModel = async () => { enabled: builtInModel.enabled ?? 1, isChatModel: builtInModel.isChatModel || 0, apiKey: apiKeyForm.apiKey, - form: builtInModel.form || {}, - requestMapping: builtInModel.requestMapping || {}, - responseMapping: builtInModel.responseMapping || {}, + form: formList, + requestMapping: (builtInModel.requestMapping as Record) || {}, + responseMapping: (builtInModel.responseMapping as Record) || {}, responseBody: builtInModel.responseBody || {}, maxConcurrency: builtInModel.maxConcurrency || 10, queueLimit: builtInModel.queueLimit || 100, @@ -293,8 +324,9 @@ const handleCreatePrivateModel = async () => { retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60, autoCleanSeconds: builtInModel.autoCleanSeconds || 300, remark: builtInModel.remark || '', - tokenMapping: builtInModel.tokenMapping || '', - tokenConfig: builtInModel.tokenConfig || {}, + + extendMapping: parseJsonObjectField(builtInModel.extendMapping), + tokenConfig: parseJsonObjectField(builtInModel.tokenConfig), }; const res: any = await addModelModule(createParams); diff --git a/src/views/settings/modelConfig/modelModule/component/editModule.vue b/src/views/settings/modelConfig/modelModule/component/editModule.vue index 6de6d64..fb95d0a 100644 --- a/src/views/settings/modelConfig/modelModule/component/editModule.vue +++ b/src/views/settings/modelConfig/modelModule/component/editModule.vue @@ -152,9 +152,15 @@ - - - + + + @@ -163,7 +169,7 @@ v-model="state.ruleForm.tokenConfig" type="textarea" :rows="4" - placeholder="请输入 JSON 对象,例如:{promptRate: 1, completionRate: 1}" + placeholder='请输入 JSON 对象,例如:{\"promptRate\": 1, \"completionRate\": 1}' clearable > @@ -240,9 +246,18 @@
- + = + + {{ field.isTokenField ? '✓ 计费字段' : '设置计费字段' }} + {{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }} @@ -271,6 +286,7 @@ import { getModelModuleDetail, getOperatorList, type ModelFormEntry, + type CreateModelParams, } from '/@/api/settings/modelConfig/modelModule/index'; export type ModelTypeOption = { id: number | string; label: string }; @@ -334,7 +350,8 @@ const state = reactive({ retryQueueMaxSeconds: 60, autoCleanSeconds: 300, remark: '', - tokenMapping: '', + extendMapping: '{}', + responseTokenField: '', tokenConfig: '{}', }, rules: { @@ -401,6 +418,27 @@ const state = reactive({ trigger: 'blur', }, ], + extendMapping: [ + { + validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => { + if (value === undefined || value === null || String(value).trim() === '') { + callback(); + return; + } + try { + const parsed = JSON.parse(String(value)); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + callback(); + return; + } + callback(new Error('附加映射必须为 JSON 对象')); + } catch { + callback(new Error('附加映射 JSON 格式不正确')); + } + }, + trigger: 'blur', + }, + ], }, dialog: { isShowDialog: false, @@ -414,7 +452,7 @@ const state = reactive({ headers: [] as Array<{ key: string; value: string }>, formFields: [] as Array<{ key: string; value: string }>, requestMappingFields: [] as Array<{ key: string; value: string }>, - responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean }>, + responseMappingFields: [] as Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>, mainBodyIndex: -1, // 记录哪一行被设置为返回主体 }); @@ -549,11 +587,28 @@ const confirmRequestMappingFields = () => { // 响应映射字段操作 const addResponseMappingField = () => { - state.responseMappingFields.push({ key: '', value: '', isMainBody: false }); + state.responseMappingFields.push({ key: '', value: '', isMainBody: false, isTokenField: false }); }; const removeResponseMappingField = (index: number) => { + const removed = state.responseMappingFields[index]; state.responseMappingFields.splice(index, 1); + if (removed?.isTokenField) { + state.ruleForm.responseTokenField = ''; + } +}; + +const setTokenField = (index: number) => { + state.responseMappingFields.forEach((field, i) => { + field.isTokenField = i === index; + }); + state.ruleForm.responseTokenField = state.responseMappingFields[index]?.key?.trim?.() || ''; +}; + +const syncTokenFieldOnKeyChange = (index: number) => { + const row = state.responseMappingFields[index]; + if (!row?.isTokenField) return; + state.ruleForm.responseTokenField = row.key?.trim?.() || ''; }; // 设置返回主体(单选) @@ -571,9 +626,9 @@ const confirmResponseMappingFields = () => { const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]); -const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean }>) => { - if (!rows.length) return [{ key: '', value: '', isMainBody: false }]; - return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false })); +const ensureResponseMappingRows = (rows: Array<{ key: string; value: string; isMainBody?: boolean; isTokenField?: boolean }>) => { + if (!rows.length) return [{ key: '', value: '', isMainBody: false, isTokenField: false }]; + return rows.map((row) => ({ ...row, isMainBody: row.isMainBody || false, isTokenField: row.isTokenField || false })); }; /** 从 getModel 返回的 data 中取出单条模型对象 */ @@ -623,11 +678,10 @@ const fillFormFromDetailRow = (row: Record) => { retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60), autoCleanSeconds: Number(row.autoCleanSeconds ?? 300), remark: String(row.remark || ''), - tokenMapping: String(row.tokenMapping || ''), - tokenConfig: - typeof row.tokenConfig === 'string' - ? row.tokenConfig - : JSON.stringify((row.tokenConfig as Record) || {}, null, 2), + extendMapping: + typeof row.extendMapping === 'string' ? row.extendMapping : JSON.stringify((row.extendMapping as Record) || {}, null, 2), + responseTokenField: String(row.responseTokenField || ''), + tokenConfig: typeof row.tokenConfig === 'string' ? row.tokenConfig : JSON.stringify((row.tokenConfig as Record) || {}, null, 2), }; state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || ''))); state.formFields = ensureKeyValueRows(parseFormFields(row.form)); @@ -635,6 +689,15 @@ const fillFormFromDetailRow = (row: Record) => { state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping)); state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping)); + // 根据 responseTokenField 字段设置计费字段标记(单选) + const tokenFieldKey = String(row.responseTokenField || '').trim(); + if (tokenFieldKey) { + const tokenFieldIndex = state.responseMappingFields.findIndex((f) => String(f.key || '').trim() === tokenFieldKey); + if (tokenFieldIndex !== -1) { + state.responseMappingFields[tokenFieldIndex].isTokenField = true; + } + } + // 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value}) if (row.responseBody && typeof row.responseBody === 'object') { const responseBodyKey = Object.keys(row.responseBody)[0]; @@ -705,13 +768,14 @@ const openDialog = async (type: string, row?: Record) => { retryQueueMaxSeconds: 60, autoCleanSeconds: 300, remark: '', - tokenMapping: '', + extendMapping: '{}', + responseTokenField: '', tokenConfig: '{}', }; state.headers = [{ key: '', value: '' }]; state.formFields = [{ key: '', value: '' }]; state.requestMappingFields = [{ key: '', value: '' }]; - state.responseMappingFields = [{ key: '', value: '', isMainBody: false }]; + state.responseMappingFields = [{ key: '', value: '', isMainBody: false, isTokenField: false }]; state.dialog.title = '新增模型配置'; state.dialog.submitTxt = '新 增'; } @@ -742,7 +806,9 @@ const onSubmit = () => { // 获取被设置为返回主体的字段 {key: value} const responseBodyField = state.responseMappingFields.find((f) => f.isMainBody); const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {}; - const submitData = { + 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, operatorName: state.ruleForm.operatorName, @@ -753,7 +819,9 @@ const onSubmit = () => { enabled: state.ruleForm.enabled, isChatModel: state.ruleForm.isChatModel, apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '', - form: fieldsToObject(state.formFields), + form: state.formFields + .filter((f) => String(f.key || '').trim() !== '') + .map((f) => ({ key: String(f.key).trim(), value: String(f.value ?? '') })), requestMapping, responseMapping, responseBody, @@ -765,7 +833,8 @@ const onSubmit = () => { retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds, autoCleanSeconds: state.ruleForm.autoCleanSeconds, remark: state.ruleForm.remark || '', - tokenMapping: state.ruleForm.tokenMapping || '', + extendMapping: parseJsonObjectField(state.ruleForm.extendMapping || '{}', {}), + responseTokenField, tokenConfig: parseJsonObjectField(state.ruleForm.tokenConfig || '{}', {}), };