feat: Token映射配置增强 + 模型详情API + 编辑器布局优化

1. Token 映射下拉:
   - 新增 flattenWrappedFields 函数递归提取 { type, attrs } 结构中的叶子字段
   - 下拉选项显示 路径 — 标签 格式,选中值存完整路径
   - 同步/流式从响应映射取,异步从回调响应映射取

2. 模型详情 API:
   - 新增 getModelManage 详情接口 (/model/manage/getModelManage)
   - openDialog 改为 async,编辑时调详情接口获取完整数据
   - 修复 res.data.modelManage 取值路径

3. JSON 编辑器布局:
   - 弹窗宽度 1000px → 1200px
   - 配置侧栏 320px → 380px
   - config-summary 改用 flex:1 防止滚动条截断文本

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-17 14:45:53 +08:00
co-authored by Claude
parent 51b5008951
commit bf1e9fa89a
6 changed files with 99 additions and 35 deletions
+18
View File
@@ -174,6 +174,24 @@ export interface UpdateModelManageParams extends Partial<CreateModelManageParams
id: string;
}
/** 详情响应 */
export interface ModelManageDetailResponse {
code: number;
message: string;
data: ModelManageItem;
}
/**
* 获取模型配置详情
*/
export function getModelManage(id: string) {
return request({
url: '/model-gateway/model/manage/getModelManage',
method: 'get',
params: { id },
});
}
/**
* 创建模型配置
*/
@@ -241,7 +241,7 @@ function emitChange() { emit('change'); }
}
.node-count { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
.config-summary {
font-size: 11px; color: #64748b; margin-left: auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
font-size: 11px; color: #64748b; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.field-btn-configured {
color: #2563eb !important;
+1 -1
View File
@@ -614,7 +614,7 @@ defineExpose({
}
}
.config-sidebar {
width: 320px;
width: 380px;
flex-shrink: 0;
border-left: 1px solid #e2e8f0;
overflow: hidden;
@@ -129,21 +129,21 @@
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="输入 Token 路径">
<el-select v-model="formData.tokenMapping.promptTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
<el-option v-for="item in tokenMappingOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="输出 Token 路径">
<el-select v-model="formData.tokenMapping.completionTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
<el-option v-for="item in tokenMappingOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="总 Token 路径">
<el-select v-model="formData.tokenMapping.totalTokens" :disabled="!hasMappingConfig" :placeholder="mappingPlaceholder" clearable style="width: 100%">
<el-option v-for="key in tokenMappingOptions" :key="key" :label="key" :value="key" />
<el-option v-for="item in tokenMappingOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
@@ -236,7 +236,7 @@
</el-form>
<!-- JSON 编辑器弹窗 -->
<el-dialog v-model="jsonEditorVisible" :title="jsonEditorTitle" width="1000px" top="5vh" :close-on-click-modal="false" destroy-on-close append-to-body>
<el-dialog v-model="jsonEditorVisible" :title="jsonEditorTitle" width="1200px" top="5vh" :close-on-click-modal="false" destroy-on-close append-to-body>
<template #header>
<span>{{ jsonEditorTitle }}</span>
<div class="json-editor-toolbar">
@@ -276,7 +276,8 @@ import { JsonEditor } from '/@/components/json-schema-editor';
import {
createModelManage,
updateModelManage,
getModelSupplierList,
getModelManage,
getModelSupplierList,
type ModelManageItem,
type ModelTypeTreeNode,
type ModelSupplierItem,
@@ -385,12 +386,48 @@ watch(isReasoningModel, (val) => {
if (!val) formData.chatModel = false;
});
/** 根据返回类型获取 Token 映射下拉选项(响应映射的 key 列表) */
/** 递归提取 { type, attrs/value } 包裹结构中的所有叶子字段 */
function flattenWrappedFields(obj: any, basePath: string = ''): { path: string; label: string }[] {
const results: { path: string; label: string }[] = [];
if (!obj || typeof obj !== 'object') return results;
// 检测 { type, attrs/value } 包裹格式
if (typeof obj.type === 'string' && ['string','number','boolean','null','object','array'].includes(obj.type)) {
const jtype = obj.type;
const dataKey = (jtype === 'object' || jtype === 'array') ? 'attrs' : 'value';
if (jtype === 'object' && dataKey in obj && obj[dataKey] && typeof obj[dataKey] === 'object' && !Array.isArray(obj[dataKey])) {
for (const [key, val] of Object.entries(obj[dataKey])) {
const childPath = basePath ? basePath + '.attrs.' + key : key;
results.push(...flattenWrappedFields(val, childPath));
}
} else if (jtype === 'array' && dataKey in obj && Array.isArray(obj[dataKey])) {
obj[dataKey].forEach((item: any, idx: number) => {
results.push(...flattenWrappedFields(item, basePath + '.attrs[' + idx + ']'));
});
} else if (['string', 'number', 'boolean', 'null'].includes(jtype)) {
results.push({ path: basePath, label: obj.label || '' });
}
return results;
}
// 普通对象(非包裹格式)
if (!Array.isArray(obj)) {
for (const [key, val] of Object.entries(obj)) {
const childPath = basePath ? basePath + '.' + key : key;
results.push(...flattenWrappedFields(val, childPath));
}
}
return results;
}
/** 根据返回类型获取 Token 映射下拉选项(响应映射的 key — 路径) */
const tokenMappingOptions = computed(() => {
const source = formData.responseType === 2
? formData.asyncTaskMapping.responseMapping
: formData.responseMapping;
return source ? Object.keys(source) : [];
return source ? flattenWrappedFields(source) : [];
});
/** 是否有可用的映射配置 */
@@ -514,32 +551,41 @@ const resetForm = () => {
formData.lastFrame = '';
};
const openDialog = (type: 'add' | 'edit', row?: ModelManageItem) => {
const openDialog = async (type: 'add' | 'edit', id?: string) => {
resetForm();
isEdit.value = type === 'edit';
detailLoading.value = false;
detailLoading.value = type === 'edit';
loadSupplierList();
if (type === 'edit' && row) {
editId.value = row.id;
formData.modelName = row.modelName;
formData.modelType = [row.modelType];
formData.modelSupplier = Number(row.modelSupplier) || undefined;
formData.baseUrl = row.baseUrl;
formData.apiKey = row.apiKey || '';
// 兼容新旧字段名
formData.responseType = row.responseType ?? row.invokeType;
formData.enabled = row.enabled ?? true;
formData.chatModel = row.ChatModel ?? false;
formData.maxConcurrency = row.maxConcurrency || 10;
formData.maxTokens = row.maxTokens || 4096;
formData.tokenPredictPrice = row.tokenPredictPrice ?? 0;
formData.requestHeadMapping = row.requestHeadMapping || row.requestMapping || {};
formData.requestBodyMapping = row.requestBodyMapping || {};
formData.responseMapping = row.responseMapping || {};
formData.tokenMapping = row.tokenMapping ? { ...defaultTokenMapping, ...row.tokenMapping } : { ...defaultTokenMapping };
formData.asyncTaskMapping = row.asyncTaskMapping ? { ...defaultAsyncTaskMapping, ...row.asyncTaskMapping } : { ...defaultAsyncTaskMapping };
formData.lastFrame = row.lastFrame || '';
if (type === 'edit' && id) {
editId.value = id;
try {
const res = await getModelManage(id);
const row = res.data.modelManage;
formData.modelName = row.modelName;
formData.modelType = [row.modelType];
formData.modelSupplier = Number(row.modelSupplier) || undefined;
formData.baseUrl = row.baseUrl;
formData.apiKey = row.apiKey || '';
formData.responseType = row.responseType ?? row.invokeType;
formData.enabled = row.enabled ?? true;
formData.chatModel = row.ChatModel ?? false;
formData.maxConcurrency = row.maxConcurrency || 10;
formData.maxTokens = row.maxTokens || 4096;
formData.tokenPredictPrice = row.tokenPredictPrice ?? 0;
formData.requestHeadMapping = row.requestHeadMapping || row.requestMapping || {};
formData.requestBodyMapping = row.requestBodyMapping || {};
formData.responseMapping = row.responseMapping || {};
formData.tokenMapping = row.tokenMapping ? { ...defaultTokenMapping, ...row.tokenMapping } : { ...defaultTokenMapping };
formData.asyncTaskMapping = row.asyncTaskMapping ? { ...defaultAsyncTaskMapping, ...row.asyncTaskMapping } : { ...defaultAsyncTaskMapping };
formData.lastFrame = row.lastFrame || '';
} catch {
// 关闭弹窗,错误由全局处理
dialogVisible.value = false;
return;
} finally {
detailLoading.value = false;
}
}
dialogVisible.value = true;
+2 -2
View File
@@ -180,8 +180,8 @@ const handleSizeChange = (size: number) => {
fetchModelList();
};
const handleEdit = (_row: ModelManageItem) => {
ElMessage.info('修改功能待接入');
const handleEdit = (row: ModelManageItem) => {
editModuleRef.value?.openDialog('edit', row.id)
};
const handleDelete = (row: ModelManageItem) => {