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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user