添加会话模型和API Key配置功能

- 在模型模块中新增会话开关状态字段,支持会话模型的管理。
- 更新模型选择器,增加系统模型的API Key配置弹窗,提升用户体验。
- 优化错误处理逻辑,确保接口错误由全局拦截器处理,减少冗余提示。
- 更新相关样式以增强界面可读性和美观性。
This commit is contained in:
2026-05-11 20:01:03 +08:00
parent 0a42e700e2
commit 29838b030f
19 changed files with 1296 additions and 274 deletions

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form
@@ -19,12 +19,7 @@
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="模型类型" prop="modelsType">
<el-select v-model="state.ruleForm.modelsType" placeholder="请选择模型类型" clearable style="width: 100%">
<el-option
v-for="t in modelTypeOptions"
:key="String(t.id)"
:label="t.label"
:value="typeOptionValue(t.id)"
></el-option>
<el-option v-for="t in modelTypeOptions" :key="String(t.id)" :label="t.label" :value="typeOptionValue(t.id)"></el-option>
</el-select>
</el-form-item>
</el-col>
@@ -53,13 +48,7 @@
</el-col>
<el-col v-if="state.ruleForm.isPrivate === 1" :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="API 密钥" prop="apiKey">
<el-input
v-model="state.ruleForm.apiKey"
type="password"
show-password
placeholder="请输入 API 密钥字符串"
clearable
></el-input>
<el-input v-model="state.ruleForm.apiKey" type="password" show-password placeholder="请输入 API 密钥字符串" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -139,29 +128,21 @@
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="自动清理间隔(秒)" prop="autoCleanSeconds">
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"></el-input-number>
<el-input-number v-model="state.ruleForm.autoCleanSeconds" :min="0" :max="86400" style="width: 100%"> </el-input-number>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="请求映射" prop="requestMappingJson">
<el-input
v-model="state.ruleForm.requestMappingJson"
type="textarea"
:rows="4"
placeholder='JSON 对象,例如 {}'
clearable
></el-input>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求映射" prop="requestMapping">
<el-button @click="showRequestMappingDialog = true" style="width: 100%">
配置请求映射 ({{ state.requestMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="响应映射" prop="responseMappingJson">
<el-input
v-model="state.ruleForm.responseMappingJson"
type="textarea"
:rows="4"
placeholder='JSON 对象,例如 {}'
clearable
></el-input>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="响应映射" prop="responseMapping">
<el-button @click="showResponseMappingDialog = true" style="width: 100%">
配置响应映射 ({{ state.responseMappingFields.length }})
</el-button>
</el-form-item>
</el-col>
</el-row>
@@ -170,13 +151,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button
type="primary"
@click="onSubmit"
size="default"
:loading="state.dialog.loading"
:disabled="state.dialog.detailLoading"
>
<el-button type="primary" @click="onSubmit" size="default" :loading="state.dialog.loading" :disabled="state.dialog.detailLoading">
{{ state.dialog.submitTxt }}
</el-button>
</span>
@@ -219,6 +194,51 @@
</span>
</template>
</el-dialog>
<!-- 请求映射配置弹窗 -->
<el-dialog v-model="showRequestMappingDialog" title="配置请求映射" width="600px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in state.requestMappingFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 40%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 40%" clearable></el-input>
<el-button type="danger" link @click="removeRequestMappingField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addRequestMappingField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showRequestMappingDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmRequestMappingFields" size="default"> </el-button>
</span>
</template>
</el-dialog>
<!-- 响应映射配置弹窗 -->
<el-dialog v-model="showResponseMappingDialog" title="配置响应映射" width="700px" :close-on-click-modal="false">
<div class="mapping-config-container">
<div v-for="(field, index) in state.responseMappingFields" :key="index" class="mapping-field-item">
<el-input v-model="field.key" placeholder="请输入字段名 (Key)" style="width: 30%" clearable></el-input>
<span class="separator">=</span>
<el-input v-model="field.value" placeholder="请输入字段值 (Value)" style="width: 30%" clearable></el-input>
<el-button
:type="field.isMainBody ? 'success' : 'primary'"
:plain="!field.isMainBody"
@click="setMainBody(index)"
size="small"
>
{{ field.isMainBody ? '✓ 返回主体' : '设置返回主体' }}
</el-button>
<el-button type="danger" link @click="removeResponseMappingField(index)">删除</el-button>
</div>
<el-button type="primary" link @click="addResponseMappingField" style="margin-top: 10px">+ 添加字段</el-button>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showResponseMappingDialog = false" size="default"> </el-button>
<el-button type="primary" @click="confirmResponseMappingFields" size="default"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@@ -227,12 +247,7 @@ import { reactive, ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
import {
addModelModule,
updateModelModule,
getModelModuleDetail,
type ModelFormEntry,
} from '/@/api/digitalHuman/modelConfig/modelModule/index';
import { addModelModule, updateModelModule, getModelModuleDetail, type ModelFormEntry } from '/@/api/digitalHuman/modelConfig/modelModule/index';
export type ModelTypeOption = { id: number | string; label: string };
@@ -254,6 +269,8 @@ const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
const showHeaderDialog = ref(false);
const showFormDialog = ref(false);
const showRequestMappingDialog = ref(false);
const showResponseMappingDialog = ref(false);
const state = reactive({
ruleForm: {
id: '',
@@ -274,8 +291,6 @@ const state = reactive({
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
},
rules: {
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
@@ -293,48 +308,6 @@ const state = reactive({
],
baseUrl: [{ required: true, message: '请输入模型服务地址', trigger: 'blur' }],
httpMethod: [{ required: true, message: '请选择请求方式', trigger: 'change' }],
requestMappingJson: [
{
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请输入请求映射 JSON'));
return;
}
try {
const o = JSON.parse(value);
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
callback(new Error('请求映射须为 JSON 对象'));
return;
}
callback();
} catch {
callback(new Error('请求映射 JSON 格式无效'));
}
},
trigger: 'blur',
},
],
responseMappingJson: [
{
validator: (_rule: unknown, value: string, callback: (e?: Error) => void) => {
if (!value || !String(value).trim()) {
callback(new Error('请输入响应映射 JSON'));
return;
}
try {
const o = JSON.parse(value);
if (o === null || typeof o !== 'object' || Array.isArray(o)) {
callback(new Error('响应映射须为 JSON 对象'));
return;
}
callback();
} catch {
callback(new Error('响应映射 JSON 格式无效'));
}
},
trigger: 'blur',
},
],
maxConcurrency: [{ required: true, message: '请输入最大并发数', trigger: 'blur' }],
queueLimit: [{ required: true, message: '请输入排队队列上限', trigger: 'blur' }],
timeoutSeconds: [{ required: true, message: '请输入请求超时时间', trigger: 'blur' }],
@@ -351,8 +324,22 @@ const state = reactive({
showAdvanced: false,
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 }>,
mainBodyIndex: -1, // 记录哪一行被设置为返回主体
});
// 将数组转换为对象
const fieldsToObject = (fields: Array<{ key: string; value: string }>) => {
const obj: Record<string, string> = {};
fields.forEach((f) => {
if (f.key && f.key.trim()) {
obj[f.key.trim()] = f.value || '';
}
});
return obj;
};
const parseHeaders = (headMsg: string) => parseKeyValueString(headMsg);
// 解析 form支持数组 [{ key, value }] 或历史对象 { k: { value } }
const parseFormFields = (form: unknown) => {
@@ -368,15 +355,25 @@ const parseFormFields = (form: unknown) => {
if (typeof form === 'object') {
const fields: Array<{ key: string; value: string }> = [];
Object.keys(form as Record<string, unknown>).forEach((key) => {
const v = (form as Record<string, { value?: string }>)[key];
if (v && typeof v === 'object' && v.value !== undefined) {
fields.push({ key, value: String(v.value) });
}
const v = (form as Record<string, unknown>)[key];
const value = String(v || '');
fields.push({ key, value });
});
return fields;
}
return [];
};
// 解析 requestMapping 对象为数组
const parseRequestMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
// 解析 responseMapping 对象为数组
const parseResponseMappingFields = (mapping: unknown) => {
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping)) return [];
return Object.entries(mapping).map(([key, value]) => ({ key, value: String(value) }));
};
const buildFormArray = (): ModelFormEntry[] => {
return state.formFields
@@ -454,9 +451,48 @@ const removeFormField = (index: number) => {
const confirmFormFields = () => {
showFormDialog.value = false;
};
// 请求映射字段操作
const addRequestMappingField = () => {
state.requestMappingFields.push({ key: '', value: '' });
};
const removeRequestMappingField = (index: number) => {
state.requestMappingFields.splice(index, 1);
};
const confirmRequestMappingFields = () => {
showRequestMappingDialog.value = false;
};
// 响应映射字段操作
const addResponseMappingField = () => {
state.responseMappingFields.push({ key: '', value: '', isMainBody: false });
};
const removeResponseMappingField = (index: number) => {
state.responseMappingFields.splice(index, 1);
};
// 设置返回主体(单选)
const setMainBody = (index: number) => {
// 清除所有字段的返回主体标记
state.responseMappingFields.forEach((field, i) => {
field.isMainBody = i === index;
});
state.mainBodyIndex = index;
};
const confirmResponseMappingFields = () => {
showResponseMappingDialog.value = false;
};
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 }));
};
/** 从 getModel 返回的 data 中取出单条模型对象 */
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
if (data == null) return null;
@@ -487,10 +523,7 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
state.ruleForm = {
id: row.id as string,
modelName: String(row.modelName ?? ''),
modelsType:
row.modelsType !== undefined && row.modelsType !== null
? typeOptionValue(row.modelsType as number | string)
: null,
modelsType: row.modelsType !== undefined && row.modelsType !== null ? typeOptionValue(row.modelsType as number | string) : null,
baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'),
headMsg: String(row.headMsg || ''),
@@ -506,21 +539,25 @@ const fillFormFromDetailRow = (row: Record<string, unknown>) => {
retryQueueMaxSeconds: Number(row.retryQueueMaxSeconds ?? 60),
autoCleanSeconds: Number(row.autoCleanSeconds ?? 300),
remark: String(row.remark || ''),
requestMappingJson: JSON.stringify(
row.requestMapping && typeof row.requestMapping === 'object' ? row.requestMapping : {},
null,
2
),
responseMappingJson: JSON.stringify(
row.responseMapping && typeof row.responseMapping === 'object' ? row.responseMapping : {},
null,
2
),
};
state.headers = ensureKeyValueRows(parseHeaders(String(row.headMsg || '')));
state.formFields = ensureKeyValueRows(parseFormFields(row.form));
// 解析请求映射和响应映射
state.requestMappingFields = ensureKeyValueRows(parseRequestMappingFields(row.requestMapping));
state.responseMappingFields = ensureResponseMappingRows(parseResponseMappingFields(row.responseMapping));
// 根据 responseBody 字段设置返回主体标记 (responseBody 是对象 {key: value})
if (row.responseBody && typeof row.responseBody === 'object') {
const responseBodyKey = Object.keys(row.responseBody)[0];
if (responseBodyKey) {
const mainBodyIndex = state.responseMappingFields.findIndex(f => f.key === responseBodyKey);
if (mainBodyIndex !== -1) {
state.responseMappingFields[mainBodyIndex].isMainBody = true;
state.mainBodyIndex = mainBodyIndex;
}
}
}
};
// 打开弹窗(编辑时会请求 /model/getModel 详情)
const openDialog = async (type: string, row?: Record<string, unknown>) => {
state.dialog.type = type;
@@ -553,7 +590,7 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
}
fillFormFromDetailRow(detail as Record<string, unknown>);
} catch {
ElMessage.error('获取模型详情失败');
// 接口错误由 request 全局提示后端 message
state.dialog.isShowDialog = false;
} finally {
state.dialog.detailLoading = false;
@@ -578,11 +615,11 @@ const openDialog = async (type: string, row?: Record<string, unknown>) => {
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
};
state.headers = [{ key: '', value: '' }];
state.formFields = [{ key: '', value: '' }];
state.requestMappingFields = [{ key: '', value: '' }];
state.responseMappingFields = [{ key: '', value: '', isMainBody: false }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
@@ -608,8 +645,11 @@ const onSubmit = () => {
state.dialog.loading = true;
try {
state.ruleForm.headMsg = stringifyHeaders();
const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
const requestMapping = fieldsToObject(state.requestMappingFields);
const responseMapping = fieldsToObject(state.responseMappingFields);
// 获取被设置为返回主体的字段 {key: value}
const responseBodyField = state.responseMappingFields.find(f => f.isMainBody);
const responseBody = responseBodyField ? { [responseBodyField.key.trim()]: responseBodyField.value } : {};
const submitData = {
modelName: state.ruleForm.modelName,
modelsType: state.ruleForm.modelsType as number | string,
@@ -620,9 +660,10 @@ const onSubmit = () => {
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: buildFormArray(),
form: fieldsToObject(state.formFields),
requestMapping,
responseMapping,
responseBody,
maxConcurrency: state.ruleForm.maxConcurrency,
queueLimit: state.ruleForm.queueLimit,
timeoutSeconds: state.ruleForm.timeoutSeconds,
@@ -642,8 +683,8 @@ const onSubmit = () => {
}
closeDialog();
emit('refresh');
} catch (error) {
ElMessage.error('保存失败');
} catch {
// 接口错误由 request 全局提示后端 message
} finally {
state.dialog.loading = false;
}
@@ -657,6 +698,19 @@ defineExpose({
</script>
<style scoped lang="scss">
.mapping-config-container {
.mapping-field-item {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
.separator {
font-weight: bold;
color: #606266;
}
}
}
.form-config-container {
max-height: 400px;
overflow-y: auto;
@@ -689,3 +743,50 @@ defineExpose({
color: #606266;
}
</style>