优化模型管理的添加表单,完善数据源

This commit is contained in:
2026-07-18 16:50:16 +08:00
parent 3099f65109
commit cac2dd019c
4 changed files with 158 additions and 49 deletions
+2 -2
View File
@@ -3,6 +3,6 @@ ENV = 'development'
# 统一后端服务地址前缀(网关服务名:admin-go)
# 开发环境走本地代理,避免 CORS
# VITE_API_URL = 'http://116.204.74.41:8000'
VITE_API_URL = 'http://192.168.3.30:8000'
VITE_API_URL = 'http://116.204.74.41:8000'
# VITE_API_URL = 'http://192.168.3.30:8000'
# VITE_API_URL = 'http://192.168.3.135:8000'
+12
View File
@@ -98,6 +98,18 @@ This is the **GFast UI** project (`gfast-ui`), a Vue 3 admin management system b
These rules capture long-term repository preferences confirmed by the user and should be applied in future sessions unless the user explicitly overrides them.
### 最高优先级规则(覆盖以下所有规则)
以下规则优先级高于本文件任何其他规则:
1. **修改前必须确认**:任何代码修改前必须先向用户确认,用户明确同意后才能执行
2. **反常问题必须上报**:遇到 Edit 反复失败、渲染异常、非预期行为等反常问题,必须立即向用户报告问题原因、可行方案和推荐方向,由用户选择后再继续
3. **不确定必须询问**:不确定、不了解、不清楚的事情一律先问用户,不自行假设或绕路
4. **改后影响检查**:代码修改完成后,必须逐一检查对其他代码的影响(渲染条件、接口字段、样式覆盖、其他引用处)
5. **改动评分与总结**:每次修改完成后,对本次改动按 1-10 分打分,并附上总结和改进建议
6. **中文沟通**:所有与用户的沟通、说明、思考展示必须使用中文
7. **操作后清理**:每次文件操作(拆分、替换、脚本执行等)完成后,必须检查并清理产生的临时文件,不得遗留在项目目录中
### Pagination Rules
- Prefer the project global `pagination` component by default.
+6
View File
@@ -54,9 +54,12 @@ export interface ModelManageItem {
requestHeadMapping: Record<string, unknown>;
requestBodyMapping: Record<string, unknown>;
responseMapping: Record<string, unknown>;
responseBodyMapping: string[];
tokenMapping?: TokenMapping;
asyncTaskMapping?: AsyncTaskMapping;
lastFrame?: string;
maxDuration: number;
tokenPredictPriceUnit: string;
// 以下旧字段兼容
httpMethod?: string;
systemModel?: boolean;
@@ -164,9 +167,12 @@ export interface CreateModelManageParams {
requestHeadMapping?: Record<string, unknown>;
requestBodyMapping?: Record<string, unknown>;
responseMapping?: Record<string, unknown>;
responseBodyMapping?: string[];
tokenMapping?: TokenMapping;
asyncTaskMapping?: AsyncTaskMapping;
lastFrame?: string;
maxDuration?: number;
tokenPredictPriceUnit?: string;
}
/** 更新模型配置参数 */
@@ -94,7 +94,13 @@
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="Token 预估价格" prop="tokenPredictPrice">
<el-input-number v-model="formData.tokenPredictPrice" :min="0" :precision="4" :step="0.0001" style="width: 100%" />
<el-input-number v-model="formData.tokenPredictPrice" :min="0" :precision="4" :step="0.0001" style="width: calc(100% - 148px); margin-right: 8px" />
<el-select v-model="formData.tokenPredictPriceUnit" placeholder="单位" clearable style="width: 140px">
<el-option label="秒" value="second" />
<el-option label="百万 Token" value="per_million_tokens" />
<el-option label="千 Token" value="per_kilo_tokens" />
<el-option label="字数" value="per_word" />
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -122,49 +128,30 @@
</el-form-item>
</el-col>
</el-row>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20" v-if="formData.responseType !== 2">
<el-form-item label="响应主体映射">
<el-select
v-model="formData.responseBodyMapping"
multiple
:disabled="!hasMappingConfig"
:placeholder="mappingPlaceholder"
clearable
style="width: 100%"
>
<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>
<!-- ========== Token 映射配置 ========== -->
<el-divider content-position="left">Token 映射配置</el-divider>
<el-row :gutter="20">
<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="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="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="item in tokenMappingOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- ========== 异步任务配置 ========== -->
<!-- ========== 异步专属 ========== -->
<template v-if="formData.responseType === 2">
<el-divider content-position="left">异步任务配置</el-divider>
<el-divider content-position="left">异步映射配置</el-divider>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="回调 URL">
<el-input v-model="formData.asyncTaskMapping.url" placeholder="请输入异步回调地址" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="回调请求头映射">
<KeyValueEditor v-model="formData.asyncTaskMapping.requestHeadMapping" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="回调响应映射">
<el-button @click="openMappingEditor('asyncResponseMapping')" style="width: 100%">
@@ -172,17 +159,53 @@
</el-button>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="回调 URL">
<el-input v-model="formData.asyncTaskMapping.url" placeholder="请输入异步回调地址" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="回调请求头映射">
<KeyValueEditor v-model="formData.asyncTaskMapping.requestHeadMapping" />
</el-form-item>
</el-col>
</el-row>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8" class="mb20">
<el-form-item label="响应主体映射">
<el-select
v-model="formData.responseBodyMapping"
multiple
:disabled="!hasMappingConfig"
:placeholder="mappingPlaceholder"
clearable
style="width: 100%"
>
<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-divider content-position="left" class="sub-divider">任务状态字段路径</el-divider>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="Task ID 路径">
<el-input v-model="formData.asyncTaskMapping.taskId" placeholder="如 data.task_id" clearable />
<el-select v-model="formData.asyncTaskMapping.taskId" :disabled="!hasTaskIdConfig" :placeholder="hasTaskIdConfig ? '请选择 Task ID 路径' : '请先配置响应映射'" clearable style="width: 100%">
<el-option v-for="item in taskIdOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</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="任务状态路径">
<el-input v-model="formData.asyncTaskMapping.taskStatus" placeholder="如 data.status" clearable />
<el-select v-model="formData.asyncTaskMapping.taskStatus" :disabled="!hasTaskStatusConfig" :placeholder="hasTaskStatusConfig ? '请选择任务状态路径' : '请先配置回调响应映射'" clearable style="width: 100%">
<el-option v-for="item in taskStatusOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -222,6 +245,31 @@
</el-row>
</template>
<el-divider content-position="left">Token 映射配置</el-divider>
<el-row :gutter="20">
<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="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="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="item in tokenMappingOptions" :key="item.path" :label="`${item.path} — ${item.label}`" :value="item.path" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- ========== 视频配置 ========== -->
<template v-if="isVideoModel">
<el-divider content-position="left">视频配置</el-divider>
@@ -231,6 +279,11 @@
<el-input v-model="formData.lastFrame" placeholder="视频的尾帧图像 URL" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="最大视频时长(秒)">
<el-input-number v-model="formData.maxDuration" :min="0" :max="999999" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</template>
</el-form>
@@ -278,7 +331,6 @@ import {
updateModelManage,
getModelManage,
getModelSupplierList,
type ModelManageItem,
type ModelTypeTreeNode,
type ModelSupplierItem,
type TokenMapping,
@@ -348,12 +400,15 @@ const formData = reactive({
requestHeadMapping: {} as Record<string, unknown>,
requestBodyMapping: {} as Record<string, unknown>,
responseMapping: {} as Record<string, unknown>,
responseBodyMapping: [] as string[],
// Token 映射
tokenMapping: { ...defaultTokenMapping },
// 异步任务映射
asyncTaskMapping: { ...defaultAsyncTaskMapping },
// 视频
lastFrame: '',
maxDuration: 0,
tokenPredictPriceUnit: '',
});
/** 获取选中模型类型路径上的所有标签 */
@@ -434,9 +489,28 @@ const tokenMappingOptions = computed(() => {
const hasMappingConfig = computed(() => tokenMappingOptions.value.length > 0);
/** 下拉占位文本 */
const mappingPlaceholder = computed(() =>
hasMappingConfig.value ? '请选择' : '请先配置响应映射'
);
const mappingPlaceholder = computed(() => {
if (hasMappingConfig.value) return '请选择';
return formData.responseType === 2 ? '请先配置回调响应映射' : '请先配置响应映射';
});
/** Task ID 路径下拉选项(来源响应映射) */
const taskIdOptions = computed(() => {
const source = formData.responseMapping;
return source ? flattenWrappedFields(source) : [];
});
/** 是否有 Task ID 映射配置 */
const hasTaskIdConfig = computed(() => taskIdOptions.value.length > 0);
/** 任务状态路径下拉选项(来源回调响应映射) */
const taskStatusOptions = computed(() => {
const source = formData.asyncTaskMapping.responseMapping;
return source ? flattenWrappedFields(source) : [];
});
/** 是否有任务状态映射配置 */
const hasTaskStatusConfig = computed(() => taskStatusOptions.value.length > 0);
/** 切换返回类型时清空 token mapping(初始加载时跳过) */
let _skipTokenClear = false;
@@ -548,9 +622,12 @@ const resetForm = () => {
formData.requestHeadMapping = {};
formData.requestBodyMapping = {};
formData.responseMapping = {};
formData.responseBodyMapping = [];
formData.tokenMapping = { ...defaultTokenMapping };
formData.asyncTaskMapping = { ...defaultAsyncTaskMapping };
formData.lastFrame = '';
formData.maxDuration = 0;
formData.tokenPredictPriceUnit = '';
};
const openDialog = async (type: 'add' | 'edit', id?: string) => {
@@ -579,9 +656,12 @@ const openDialog = async (type: 'add' | 'edit', id?: string) => {
formData.requestHeadMapping = row.requestHeadMapping || row.requestMapping || {};
formData.requestBodyMapping = row.requestBodyMapping || {};
formData.responseMapping = row.responseMapping || {};
formData.responseBodyMapping = row.responseBodyMapping || [];
formData.tokenMapping = row.tokenMapping ? { ...defaultTokenMapping, ...row.tokenMapping } : { ...defaultTokenMapping };
formData.asyncTaskMapping = row.asyncTaskMapping ? { ...defaultAsyncTaskMapping, ...row.asyncTaskMapping } : { ...defaultAsyncTaskMapping };
formData.lastFrame = row.lastFrame || '';
formData.maxDuration = row.maxDuration ?? 0;
formData.tokenPredictPriceUnit = row.tokenPredictPriceUnit || '';
} catch {
// 关闭弹窗,错误由全局处理
dialogVisible.value = false;
@@ -616,6 +696,7 @@ const handleSubmit = async () => {
requestHeadMapping: Object.keys(formData.requestHeadMapping).length > 0 ? formData.requestHeadMapping : undefined,
requestBodyMapping: Object.keys(formData.requestBodyMapping).length > 0 ? formData.requestBodyMapping : undefined,
responseMapping: Object.keys(formData.responseMapping).length > 0 ? formData.responseMapping : undefined,
responseBodyMapping: formData.responseBodyMapping.length > 0 ? formData.responseBodyMapping : undefined,
};
// 根据返回类型附加对应配置
@@ -636,12 +717,22 @@ const handleSubmit = async () => {
if (formData.lastFrame) {
lastFrame = formData.lastFrame;
}
let maxDuration: number | undefined;
let tokenPredictPriceUnit: string | undefined;
if (isVideoModel.value) {
maxDuration = formData.maxDuration || undefined;
}
if (formData.tokenPredictPriceUnit) {
tokenPredictPriceUnit = formData.tokenPredictPriceUnit;
}
const params: Record<string, unknown> = {
...baseParams,
...(tokenMapping ? { tokenMapping } : {}),
...(asyncTaskMapping ? { asyncTaskMapping } : {}),
...(lastFrame ? { lastFrame } : {}),
...(maxDuration ? { maxDuration } : {}),
...(tokenPredictPriceUnit ? { tokenPredictPriceUnit } : {}),
};
if (isEdit.value) {