更新模型配置和订阅页面

- 修改模型模块的字段名称,从 `keyword` 更改为 `modelName`,以提高一致性。
- 添加模型类型和访问类型的选择功能,增强用户交互体验。
- 移除不必要的调试日志,优化代码整洁性。
- 更新订阅页面的错误处理逻辑,确保用户在加载失败时获得清晰反馈。
This commit is contained in:
2026-05-11 13:48:20 +08:00
parent 76420713fa
commit 0a42e700e2
9 changed files with 617 additions and 249 deletions

View File

@@ -1,7 +1,14 @@
<template>
<div class="system-edit-module-container">
<el-dialog :title="state.dialog.title" v-model="state.dialog.isShowDialog" width="900px">
<el-form ref="editModuleFormRef" :model="state.ruleForm" :rules="state.rules" size="default" label-width="140px">
<el-form
ref="editModuleFormRef"
v-loading="state.dialog.detailLoading"
:model="state.ruleForm"
:rules="state.rules"
size="default"
label-width="140px"
>
<!-- 基础配置 -->
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -12,20 +19,18 @@
<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 label="图片模型" :value="1"></el-option>
<el-option label="语音模型" :value="2"></el-option>
<el-option label="推理模型" :value="3"></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>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="模型地址" prop="baseUrl">
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型地址" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="路由" prop="route">
<el-input v-model="state.ruleForm.route" placeholder="请输入路由" clearable></el-input>
<el-form-item label="模型服务地址" prop="baseUrl">
<el-input v-model="state.ruleForm.baseUrl" placeholder="请输入模型服务地址" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@@ -38,6 +43,33 @@
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="访问类型" prop="isPrivate">
<el-radio-group v-model="state.ruleForm.isPrivate" @change="onIsPrivateChange">
<el-radio :label="0">私有</el-radio>
<el-radio :label="1">公共</el-radio>
</el-radio-group>
</el-form-item>
</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-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="是否对话模型" prop="isChatModel">
<el-radio-group v-model="state.ruleForm.isChatModel">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="请求头绑定" prop="headMsg">
<el-button @click="showHeaderDialog = true" style="width: 100%"> 配置请求头 ({{ state.headers.length }}) </el-button>
@@ -110,13 +142,43 @@
<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-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-form-item>
</el-col>
</el-row>
</el-collapse-transition>
</el-form>
<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">{{ state.dialog.submitTxt }}</el-button>
<el-button
type="primary"
@click="onSubmit"
size="default"
:loading="state.dialog.loading"
:disabled="state.dialog.detailLoading"
>
{{ state.dialog.submitTxt }}
</el-button>
</span>
</template>
</el-dialog>
@@ -161,11 +223,32 @@
</template>
<script setup lang="ts" name="systemEditModule">
import { reactive, ref } from 'vue';
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 } 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 };
const props = withDefaults(
defineProps<{
modelTypes?: ModelTypeOption[];
}>(),
{ modelTypes: () => [] as ModelTypeOption[] }
);
const modelTypeOptions = computed(() => props.modelTypes);
const typeOptionValue = (id: number | string): number | string => {
const n = Number(id);
return Number.isNaN(n) ? id : n;
};
const editModuleFormRef = ref();
const emit = defineEmits(['refresh']);
@@ -175,12 +258,14 @@ const state = reactive({
ruleForm: {
id: '',
modelName: '',
modelsType: 1,
modelsType: null as number | string | null,
baseUrl: '',
route: '',
httpMethod: 'POST',
headMsg: '',
isPrivate: 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
@@ -189,13 +274,67 @@ const state = reactive({
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
},
rules: {
modelName: [{ required: true, message: '请输入模型名称', trigger: 'blur' }],
modelsType: [{ required: true, message: '请选择模型类型', trigger: 'change' }],
baseUrl: [{ required: true, message: '请输入模型地址', trigger: 'blur' }],
route: [{ required: true, message: '请输入路由', trigger: 'blur' }],
modelsType: [
{
validator: (_rule: unknown, value: unknown, callback: (e?: Error) => void) => {
if (value === null || value === undefined || value === '') {
callback(new Error('请选择模型类型'));
} else {
callback();
}
},
trigger: 'change',
},
],
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' }],
@@ -207,58 +346,85 @@ const state = reactive({
title: '',
submitTxt: '',
loading: false,
detailLoading: false,
},
showAdvanced: false, // 是否展开高级配置
headers: [] as Array<{ key: string; value: string }>, // 请求头列表
showAdvanced: false,
headers: [] as Array<{ key: string; value: string }>,
formFields: [] as Array<{ key: string; value: string }>,
});
// 解析 headMsg 字符串为数组
const parseHeaders = (headMsg: string) => {
if (!headMsg) return [];
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[])
.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') {
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) });
}
});
return fields;
}
return [];
};
const buildFormArray = (): ModelFormEntry[] => {
return state.formFields
.filter((field) => field.key?.trim() && field.value?.trim())
.map((field) => ({ key: field.key.trim(), value: field.value.trim() }));
};
const parseKeyValueString = (raw: string) => {
if (!raw) return [];
const headers: Array<{ key: string; value: string }> = [];
const pairs = headMsg.split(',');
const pairs = raw.split(',');
pairs.forEach((pair) => {
const [key, value] = pair.split(':');
if (key && value) {
headers.push({ key: key.trim(), value: value.trim() });
const idx = pair.indexOf(':');
if (idx === -1) return;
const key = pair.slice(0, idx).trim();
const value = pair.slice(idx + 1).trim();
if (key) {
headers.push({ key, value });
}
});
return headers;
};
// 解析 form 对象为数组
const parseFormFields = (form: any) => {
if (!form || typeof form !== 'object') return [];
const fields: Array<{ key: string; value: string }> = [];
Object.keys(form).forEach((key) => {
if (form[key] && typeof form[key] === 'object' && form[key].value !== undefined) {
fields.push({ key, value: form[key].value });
}
});
return fields;
};
// 将数组转换为 form 对象
const stringifyFormFields = () => {
const formObj: Record<string, { value: string }> = {};
state.formFields.forEach((field) => {
const key = field.key?.trim();
const value = field.value?.trim();
if (key && value) {
formObj[key] = { value: value };
}
});
return formObj;
};
// 将数组转换为 headMsg 字符串
const stringifyHeaders = () => {
return state.headers
.filter((h) => h.key && h.value)
.map((h) => `${h.key}:${h.value}`)
.filter((h) => h.key?.trim() && h.value?.trim())
.map((h) => `${h.key.trim()}:${h.value.trim()}`)
.join(',');
};
const onIsPrivateChange = (val: string | number | boolean | undefined) => {
if (val === 0 || val === '0') {
state.ruleForm.apiKey = '';
}
};
const parseJsonObjectField = (raw: string, fallback: Record<string, unknown>) => {
try {
const o = JSON.parse(raw || '{}');
if (o && typeof o === 'object' && !Array.isArray(o)) {
return o as Record<string, unknown>;
}
} catch {
/* ignore */
}
return fallback;
};
// 添加请求头
const addHeader = () => {
state.headers.push({ key: '', value: '' });
@@ -289,43 +455,121 @@ const confirmFormFields = () => {
showFormDialog.value = false;
};
// 打开弹窗
const openDialog = (type: string, row?: any) => {
const ensureKeyValueRows = (rows: Array<{ key: string; value: string }>) => (rows.length ? rows : [{ key: '', value: '' }]);
/** 从 getModel 返回的 data 中取出单条模型对象 */
const unwrapModelDetailPayload = (data: unknown): Record<string, unknown> | null => {
if (data == null) return null;
if (Array.isArray(data)) return null;
if (typeof data !== 'object') return null;
const o = data as Record<string, unknown>;
if (typeof o.modelName === 'string') return o;
if (o.model && typeof o.model === 'object' && !Array.isArray(o.model)) {
return o.model as Record<string, unknown>;
}
if (o.detail && typeof o.detail === 'object' && !Array.isArray(o.detail)) {
return o.detail as Record<string, unknown>;
}
if (o.info && typeof o.info === 'object' && !Array.isArray(o.info)) {
return o.info as Record<string, unknown>;
}
return o;
};
const fillFormFromDetailRow = (row: Record<string, unknown>) => {
const timeoutSeconds =
row.timeoutSeconds != null && row.timeoutSeconds !== ''
? Number(row.timeoutSeconds)
: row.timeoutMs != null
? Math.floor(Number(row.timeoutMs) / 1000)
: 30;
const isPrivate = row.isPrivate !== undefined && row.isPrivate !== null ? Number(row.isPrivate) : 0;
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,
baseUrl: String(row.baseUrl ?? ''),
httpMethod: String(row.httpMethod || 'POST'),
headMsg: String(row.headMsg || ''),
isPrivate,
apiKey: isPrivate === 1 ? String(row.apiKey ?? '') : '',
enabled: Number(row.enabled ?? 1),
isChatModel: row.isChatModel !== undefined && row.isChatModel !== null ? Number(row.isChatModel) : 0,
maxConcurrency: Number(row.maxConcurrency ?? 10),
queueLimit: Number(row.queueLimit ?? 100),
timeoutSeconds,
expectedSeconds: Number(row.expectedSeconds ?? 15),
retryTimes: Number(row.retryTimes ?? 3),
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));
};
// 打开弹窗(编辑时会请求 /model/getModel 详情)
const openDialog = async (type: string, row?: Record<string, unknown>) => {
state.dialog.type = type;
state.dialog.isShowDialog = true;
state.showAdvanced = false;
state.dialog.detailLoading = false;
if (type === 'edit') {
// 编辑时需要转换字段
state.ruleForm = {
id: row.id,
modelName: row.modelName,
modelsType: row.modelsType || 1,
baseUrl: row.baseUrl,
route: row.route,
httpMethod: row.httpMethod,
headMsg: row.headMsg || '',
enabled: row.enabled,
maxConcurrency: row.maxConcurrency,
queueLimit: row.queueLimit,
timeoutSeconds: Math.floor(row.timeoutMs / 1000), // 毫秒转秒
expectedSeconds: row.expectedSeconds || 15,
retryTimes: row.retryTimes,
retryQueueMaxSeconds: row.retryQueueMaxSeconds,
autoCleanSeconds: row.autoCleanSeconds,
remark: row.remark || '',
};
// 解析请求头
state.headers = parseHeaders(row.headMsg || '');
state.formFields = parseFormFields(row.form);
const listRowId = row?.id;
if (listRowId === undefined || listRowId === null || listRowId === '') {
ElMessage.error('缺少模型 ID');
state.dialog.isShowDialog = false;
return;
}
state.dialog.title = '修改模型配置';
state.dialog.submitTxt = '修 改';
state.dialog.detailLoading = true;
try {
const res: any = await getModelModuleDetail(listRowId as string | number);
if (res.code !== 0) {
ElMessage.error(res.message || '获取模型详情失败');
state.dialog.isShowDialog = false;
return;
}
const detail = unwrapModelDetailPayload(res.data) ?? row ?? null;
if (!detail || typeof detail !== 'object') {
ElMessage.error('获取模型详情失败');
state.dialog.isShowDialog = false;
return;
}
fillFormFromDetailRow(detail as Record<string, unknown>);
} catch {
ElMessage.error('获取模型详情失败');
state.dialog.isShowDialog = false;
} finally {
state.dialog.detailLoading = false;
}
} else {
state.ruleForm = {
id: '',
modelName: '',
modelsType: 1,
modelsType: null,
baseUrl: '',
route: '',
httpMethod: 'POST',
headMsg: '',
isPrivate: 0,
apiKey: '',
enabled: 1,
isChatModel: 0,
maxConcurrency: 10,
queueLimit: 100,
timeoutSeconds: 30,
@@ -334,21 +578,20 @@ const openDialog = (type: string, row?: any) => {
retryQueueMaxSeconds: 60,
autoCleanSeconds: 300,
remark: '',
requestMappingJson: '{}',
responseMappingJson: '{}',
};
// 初始化一个空的请求头
state.headers = [{ key: '', value: '' }];
state.formFields = [{ key: '', value: '' }];
state.dialog.title = '新增模型配置';
state.dialog.submitTxt = '新 增';
}
state.dialog.type = type;
state.dialog.isShowDialog = true;
state.showAdvanced = false; // 每次打开弹窗时重置为收起状态
};
// 关闭弹窗
const closeDialog = () => {
state.dialog.isShowDialog = false;
state.dialog.detailLoading = false;
editModuleFormRef.value?.resetFields();
};
@@ -364,19 +607,34 @@ const onSubmit = () => {
state.dialog.loading = true;
try {
// 将 headMsg 转换为 form 的 JSON 格式
// headMsg: "X-API-Key:xxx,operation:true"
// 转换为 form: { "X-API-Key": { "value": "xxx" }, "operation": { "value": "true" } }
const formObj = stringifyFormFields();
// 提交数据
state.ruleForm.headMsg = stringifyHeaders();
const requestMapping = parseJsonObjectField(state.ruleForm.requestMappingJson, {});
const responseMapping = parseJsonObjectField(state.ruleForm.responseMappingJson, {});
const submitData = {
...state.ruleForm,
form: formObj,
modelName: state.ruleForm.modelName,
modelsType: state.ruleForm.modelsType as number | string,
baseUrl: state.ruleForm.baseUrl,
httpMethod: state.ruleForm.httpMethod || 'POST',
headMsg: state.ruleForm.headMsg,
isPrivate: state.ruleForm.isPrivate,
enabled: state.ruleForm.enabled,
isChatModel: state.ruleForm.isChatModel,
apiKey: state.ruleForm.isPrivate === 1 ? String(state.ruleForm.apiKey ?? '').trim() : '',
form: buildFormArray(),
requestMapping,
responseMapping,
maxConcurrency: state.ruleForm.maxConcurrency,
queueLimit: state.ruleForm.queueLimit,
timeoutSeconds: state.ruleForm.timeoutSeconds,
expectedSeconds: state.ruleForm.expectedSeconds,
retryTimes: state.ruleForm.retryTimes,
retryQueueMaxSeconds: state.ruleForm.retryQueueMaxSeconds,
autoCleanSeconds: state.ruleForm.autoCleanSeconds,
remark: state.ruleForm.remark || '',
};
if (state.dialog.type === 'edit') {
await updateModelModule(submitData);
await updateModelModule({ ...submitData, id: state.ruleForm.id });
ElMessage.success('修改成功');
} else {
await addModelModule(submitData);