- 新增 DimensionInput 组件(Popover + W/H 数字输入 + 比例锁),支持 combined(单字段 W*H)和 split(独立 width/height)两种模式 - 修复 jimeng/qwen/qwen-edit 尺寸参数不显示:改用 dimension/dimensionWidth/dimensionHeight 替代 number/select - 修复 GPT-Image-2/GPT-Image-2 I2I quality 选择器不显示:通过 Select 组件承载 ui: 'select' - 修复 jimeng/GPT-Image-2 误显示 quantity:showQuantity 移除 fallback,仅匹配 ui: 'quantity' - 新增 docs/模型参数后端化方案.md:API 设计、数据库设计、前后端迁移步骤 - 更新 CLAUDE.md:补充新 UI 类型映射、dimension 模式说明、displayNameMap bug 标注 - 删除废弃文件 Vidu Q3-T2V.json、modelConfig 空目录
21 KiB
模型参数配置后端化方案
1. 背景与目标
当前模型参数配置硬编码在前端 src/config/models/*.js 中,新增模型或修改参数需要前端发版,业务层无法感知参数定义。
目标:将参数配置迁移到业务层,管理员后台配置模型参数,前端通过 API 按模型 ID 动态获取,实现零发版上线新模型配置。
页面加载:
GET /suanli/v1/platforms/{code}/models → 模型列表(现有接口,不变)
GET /suanli/v1/platforms/{code}/models/params → 批量拉全部模型参数(新接口)
前端存入 Map<modelId, config>
切换模型:
config = paramsMap.get(modelId) → 内存读取,0 网络请求
兜底(缓存 miss 时):
GET /suanli/v1/models/{model_id}/params → 单个模型参数
第一部分:后端方案
2. API 设计
2.1 批量获取平台下所有模型参数(主接口)
GET /suanli/v1/platforms/{platform_code}/models/params
Response:
{
"code": 0,
"data": {
"models": [
{
"id": "uuid-of-flux-2",
"input_type": "text",
"max_images": 4,
"params": [
{
"name": "prompt",
"label": "提示词",
"type": "string",
"required": true,
"ui": "textarea"
},
{
"name": "aspectRatio",
"label": "比例",
"type": "select",
"default": "1:1",
"ui": "proportion",
"options": ["1:1", "4:3", "3:2", "16:9", "3:4", "2:3", "9:16", "custom"],
"show_when": null
},
{
"name": "customWidth",
"label": "自定义宽度",
"type": "number",
"default": 1024,
"min": 512,
"max": 2048,
"ui": "hidden",
"show_when": { "aspectRatio": "custom" }
},
{
"name": "resolution",
"label": "分辨率",
"type": "select",
"default": "1k",
"ui": "resolution",
"options": ["1k", "2k", "4k"]
}
]
},
{
"id": "uuid-of-qwen-2.0",
"input_type": "text",
"params": [
{
"name": "prompt",
"label": "提示词",
"type": "string",
"required": true,
"ui": "textarea"
},
{
"name": "size",
"label": "尺寸",
"type": "string",
"default": "1024*1024",
"ui": "dimension",
"dimension": {
"delimiter": "*",
"width": { "min": 512, "max": 2048 },
"height": { "min": 512, "max": 2048 }
}
},
{
"name": "imageNum",
"label": "生成张数",
"type": "select",
"default": 1,
"ui": "quantity",
"options": [1, 2, 3, 4, 5, 6]
}
]
}
]
}
}
2.2 单个模型参数(兜底接口)
GET /suanli/v1/models/{model_id}/params
Response: data 为单个模型对象(不含 models 数组包装),其余结构同 2.1。
2.3 参数字段规范
每个 param 对象的字段定义:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | 是 | 参数 key,提交任务时作为 body 字段名 |
label |
string | 是 | 中文显示名 |
type |
string | 是 | string / number / boolean / select / image |
default |
any | 是 | 默认值,类型必须与 type 一致 |
required |
bool | 否 | 是否必填,默认 false |
ui |
string | 是 | UI 组件标识(见 2.4 映射表) |
options |
array | 否 | type=select 时的可选项列表 |
min |
number | 否 | type=number 时的最小值 |
max |
number | 否 | type=number 时的最大值 |
max_count |
number | 否 | ui=imageUpload 时的最大上传张数 |
dimension |
object | 否 | ui=dimension 时的尺寸配置 |
show_when |
object | 否 | 条件显示,如 {"aspectRatio": "custom"} |
dimension 对象(仅 ui=dimension 时出现):
| 字段 | 类型 | 说明 |
|---|---|---|
delimiter |
string | W/H 分隔符,固定 * |
width.min |
number | 宽度最小值 |
width.max |
number | 宽度最大值 |
height.min |
number | 高度最小值 |
height.max |
number | 高度最大值 |
2.4 ui 字段枚举
ui 值 |
前端渲染组件 | 说明 |
|---|---|---|
textarea |
Sender 内置 textarea | 提示词输入框 |
proportion |
paintingProportion |
比例选择 Popover(含 resolution + custom 尺寸) |
resolution |
paintingProportion 内部 |
分辨率子选项 |
dimension |
DimensionInput |
组合模式 W×H(如 1024*1024) |
dimensionWidth |
DimensionInput |
拆分模式:独立宽度(须与 dimensionHeight 配对) |
dimensionHeight |
DimensionInput |
拆分模式:独立高度(须与 dimensionWidth 配对) |
select |
Select |
通用下拉选择(如 quality) |
quantity |
Quantity |
生成张数选择器 |
imageUpload |
ImageUploader |
参考图上传 |
hidden |
无 | 不渲染,静默写入默认值 |
dimensionWidth+dimensionHeight必须成对出现,前端自动关联到同一个 DimensionInput 组件。
3. 数据库设计
3.1 表结构
model_params 是独立的配置表,与 models 表分离,通过 model_id 关联:
┌─────────────────────┐ ┌──────────────────────────┐
│ models(平台模型表) │ │ model_params(参数配置表) │
│ │ 1:N │ │
│ id (UUID) │◄────────│ model_id (FK) │
│ display_name │ │ name, label, type │
│ platform_code │ │ ui, default_val │
│ tags │ │ options, min, max... │
│ input_type │ │ sort_order │
│ max_images │ │ created_at / updated_at │
└─────────────────────┘ └──────────────────────────┘
CREATE TABLE model_params (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
model_id VARCHAR(64) NOT NULL, -- 关联 models.id(UUID)
name VARCHAR(64) NOT NULL, -- 参数 key
label VARCHAR(64) NOT NULL, -- 中文显示名
type VARCHAR(16) NOT NULL, -- string | number | boolean | select | image
default_val JSON NOT NULL, -- 默认值
required TINYINT DEFAULT 0,
ui VARCHAR(32) NOT NULL, -- UI 组件标识
options JSON DEFAULT NULL,
min_val INT DEFAULT NULL,
max_val INT DEFAULT NULL,
max_count INT DEFAULT NULL,
show_when JSON DEFAULT NULL,
sort_order INT DEFAULT 0,
dimension JSON DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_model_id (model_id)
);
dimension JSON 格式:
{
"delimiter": "*",
"width": { "min": 512, "max": 2048 },
"height": { "min": 512, "max": 2048 }
}
3.2 关键约束
model_params与models完全解耦,各自独立维护- 一个
model_id可有多条参数行,sort_order控制前端渲染顺序 - 模型可以没有参数配置(
model_params中无记录),前端兼容处理(不渲染额外 UI) model_id由管理员手动关联,不是自动生成
3.3 初始数据迁移
将当前 src/config/models/*.js 中 8 个模型的参数转为 INSERT 语句。model_id 须对应 models 表中该模型的 UUID。完整示例见第 7 节。
4. 管理员配置后台
操作流程:
┌──────────────────────────────────────────────────────────┐
│ 1. 进入「模型参数配置」页面 │
│ 2. 选择平台 + 模型(从 models 表读取,按 platform_code 过滤) │
│ 3. 为该模型添加/编辑参数行,每行配置: │
│ - 参数名 (name) - 中文标签 (label) │
│ - 数据类型 (type) - UI 组件 (ui) │
│ - 默认值 (default) - 可选项 (options) │
│ - 数值范围 (min/max) - 尺寸配置 (dimension) │
│ - 条件显示 (show_when) │
│ 4. 拖拽调整参数排序 (sort_order) │
│ 5. 保存后前端缓存 TTL 过期自动生效,也可通知用户手动刷新 │
└──────────────────────────────────────────────────────────┘
5. 接口实现伪代码
# GET /suanli/v1/platforms/{platform_code}/models/params
def get_platform_model_params(platform_code):
models = db.query(
"SELECT id, input_type, max_images FROM models WHERE platform_code = ?",
platform_code
)
result = []
for m in models:
params = db.query(
"SELECT * FROM model_params WHERE model_id = ? ORDER BY sort_order",
m.id
)
result.append({
"id": m.id,
"input_type": m.input_type,
"max_images": m.max_images,
"params": [format_param(p) for p in params],
})
return {"code": 0, "data": {"models": result}}
# GET /suanli/v1/models/{model_id}/params
def get_model_params(model_id):
m = db.query("SELECT id, input_type, max_images FROM models WHERE id = ?", model_id)
if not m:
return {"code": 404, "msg": "模型不存在"}
params = db.query(
"SELECT * FROM model_params WHERE model_id = ? ORDER BY sort_order",
model_id
)
return {
"code": 0,
"data": {
"id": m.id,
"input_type": m.input_type,
"max_images": m.max_images,
"params": [format_param(p) for p in params],
}
}
def format_param(p):
return {
"name": p.name,
"label": p.label,
"type": p.type,
"default": json.loads(p.default_val),
"required": bool(p.required),
"ui": p.ui,
"options": json.loads(p.options) if p.options else None,
"min": p.min_val,
"max": p.max_val,
"max_count": p.max_count,
"show_when": json.loads(p.show_when) if p.show_when else None,
"dimension": json.loads(p.dimension) if p.dimension else None,
}
6. 响应示例汇总
6.1 即梦 4.6(拆分维度 dimensionWidth + dimensionHeight)
{
"id": "jimeng-uuid",
"input_type": "text",
"params": [
{ "name": "prompt", "label": "提示词", "type": "string", "required": true, "ui": "textarea" },
{ "name": "width", "label": "宽度", "type": "number", "default": 1024, "min": 900, "max": 6197, "ui": "dimensionWidth" },
{ "name": "height", "label": "高度", "type": "number", "default": 1024, "min": 768, "max": 4096, "ui": "dimensionHeight" },
{ "name": "forceSingle", "label": "强制单张", "type": "boolean", "default": false, "ui": "hidden" }
]
}
6.2 通义万相 2.0(组合维度 dimension + quantity)
{
"id": "qwen-uuid",
"input_type": "text",
"params": [
{ "name": "prompt", "label": "提示词", "type": "string", "required": true, "ui": "textarea" },
{
"name": "size", "label": "尺寸", "type": "string", "default": "1024*1024", "ui": "dimension",
"dimension": { "delimiter": "*", "width": { "min": 512, "max": 2048 }, "height": { "min": 512, "max": 2048 } }
},
{ "name": "imageNum", "label": "生成张数", "type": "select", "default": 1, "ui": "quantity", "options": [1, 2, 3, 4, 5, 6] }
]
}
6.3 GPT-Image-2(proportion + resolution + select)
{
"id": "gpt-image-uuid",
"input_type": "text",
"params": [
{ "name": "prompt", "label": "提示词", "type": "string", "required": true, "ui": "textarea" },
{
"name": "aspectRatio", "label": "比例", "type": "select", "default": "1:1", "ui": "proportion",
"options": ["1:1", "4:3", "3:2", "16:9", "3:4", "2:3", "9:16", "custom"]
},
{ "name": "customWidth", "label": "自定义宽度", "type": "number", "default": 1024, "min": 512, "max": 2048, "ui": "hidden", "show_when": { "aspectRatio": "custom" } },
{ "name": "customHight", "label": "自定义高度", "type": "number", "default": 1024, "min": 512, "max": 2048, "ui": "hidden", "show_when": { "aspectRatio": "custom" } },
{ "name": "resolution", "label": "分辨率", "type": "select", "default": "1k", "ui": "resolution", "options": ["1k", "2k", "4k"] },
{ "name": "quality", "label": "质量", "type": "select", "default": "medium", "ui": "select", "options": ["low", "medium", "high"] }
]
}
第二部分:前端改动
7. 缓存策略
三级防护,全部位于客户端浏览器:
请求流程:
用户操作
→ L1 内存缓存(Map,会话级,10min TTL)
→ L2 localStorage 缓存(持久化,10min TTL)
→ 请求冷却检查(localStorage,30s 冷却期)
→ API 请求
| 层级 | 存储位置 | 生命周期 | 说明 |
|---|---|---|---|
| L1 | JS 内存 Map | 标签页关闭即销毁 | 同会话内切换模型 0 延迟 |
| L2 | localStorage | 持久化,跨会话/刷新 | 刷新页面后直接命中,无需请求 |
| 冷却期 | localStorage | 独立于缓存 | 限制 API 调用频率最低 30s/次 |
正常场景:首次访问 → API 请求 → 写入 L1 + L2。刷新页面 → L2 命中,0 请求。
极端场景:清掉 localStorage 连刷 10 次 → 请求冷却生效,实际只有 1 次请求到达后端。
8. 新增文件
8.1 API 层
// src/apis/display/index.js(追加以下两个函数)
// 批量获取平台所有模型参数
export const fetchPlatformModelParams = (platformCode) =>
service.get(`/suanli/v1/platforms/${platformCode}/models/params`)
// 获取单个模型参数(兜底)
export const fetchModelParams = (modelId) =>
service.get(`/suanli/v1/models/${modelId}/params`)
8.2 缓存层
// src/utils/modelParams.js(新文件)
import { fetchPlatformModelParams } from '@/apis/display'
const CACHE_TTL = 10 * 60 * 1000 // 缓存有效期:10 分钟
const COOLDOWN = 30 * 1000 // 请求冷却期:30 秒
const STORAGE_PREFIX = 'model_params_'
const memoryCache = new Map() // L1: { platformCode → { data, timestamp } }
const pendingRequests = new Map() // 并发去重
function getStorageCache(platformCode) {
try {
const raw = localStorage.getItem(STORAGE_PREFIX + platformCode)
if (!raw) return null
const { data, timestamp } = JSON.parse(raw)
if (Date.now() - timestamp < CACHE_TTL) {
return new Map(data)
}
} catch { /* ignore */ }
return null
}
function setStorageCache(platformCode, map) {
try {
localStorage.setItem(STORAGE_PREFIX + platformCode, JSON.stringify({
data: [...map],
timestamp: Date.now(),
}))
} catch { /* ignore */ }
}
function getCooldownRemaining(platformCode) {
try {
const raw = localStorage.getItem(STORAGE_PREFIX + platformCode + '_lastFetch')
if (!raw) return 0
const elapsed = Date.now() - parseInt(raw)
return elapsed < COOLDOWN ? COOLDOWN - elapsed : 0
} catch { return 0 }
}
function setFetchTimestamp(platformCode) {
try {
localStorage.setItem(STORAGE_PREFIX + platformCode + '_lastFetch', Date.now().toString())
} catch { /* ignore */ }
}
export function clearModelParamsCache(platformCode) {
memoryCache.delete(platformCode)
localStorage.removeItem(STORAGE_PREFIX + platformCode)
}
export async function getModelParamsMap(platformCode) {
// 1. L1 内存缓存
const mem = memoryCache.get(platformCode)
if (mem && Date.now() - mem.timestamp < CACHE_TTL) {
return mem.data
}
// 2. L2 localStorage 缓存
const storage = getStorageCache(platformCode)
if (storage) {
memoryCache.set(platformCode, { data: storage, timestamp: Date.now() })
return storage
}
// 3. 并发去重
if (pendingRequests.has(platformCode)) {
return pendingRequests.get(platformCode)
}
// 4. 请求冷却
const cooldown = getCooldownRemaining(platformCode)
if (cooldown > 0) {
const promise = new Promise(resolve => {
setTimeout(() => {
pendingRequests.delete(platformCode)
resolve(getModelParamsMap(platformCode))
}, cooldown)
})
pendingRequests.set(platformCode, promise)
return promise
}
// 5. 发起请求
setFetchTimestamp(platformCode)
const promise = fetchPlatformModelParams(platformCode)
.then(res => {
const map = new Map()
for (const m of res.data.models) {
map.set(m.id, m)
}
memoryCache.set(platformCode, { data: map, timestamp: Date.now() })
setStorageCache(platformCode, map)
pendingRequests.delete(platformCode)
return map
})
.catch(err => {
pendingRequests.delete(platformCode)
throw err
})
pendingRequests.set(platformCode, promise)
return promise
}
9. 改造文件
9.1 dialogBox/index.vue
数据流变化:
当前:
model.value (display_name)
→ getModelConfig(modelName) // 静态 JS 文件查找
→ modelConfig (computed)
→ UI 渲染
新方案:
modelId (选中模型的 UUID)
→ modelConfig = paramsMap.get(modelId) // 从预取 Map 查找
→ UI 渲染
核心代码改造:
// 当前
const modelConfig = computed(() => {
return props.type === 'Painting' ? getModelConfig(model.value) : null
})
// 新方案
const paramsMap = ref(new Map())
const modelConfig = computed(() => {
if (props.type !== 'Painting') return null
return paramsMap.value.get(currentModelId.value) || null
})
onMounted(async () => {
const map = await getModelParamsMap('ai_painting_talk')
paramsMap.value = map
})
其他改动点:
modelConfigwatcher 中维度初始化改用统一parseDimension()替代dimension.parse()fillParamsFromResult中维度恢复同理displayNameMap逻辑移除
9.2 model/painting.vue(模型选择器)
- 选中值从
display_name改为model_id - emit 从
update:modelValue(displayName)改为update:modelValue(modelId) - 同时 emit
update:typeValue(inputType)保持不变
9.3 dimension 处理统一化
移除每个模型配置中自定义的 parse/format 函数,前端统一处理:
// src/components/dialogBox/index.vue 中集中定义
function parseDimension(raw, delimiter = '*') {
const parts = (raw || '1024*1024').split(delimiter)
return {
width: parseInt(parts[0]) || 1024,
height: parseInt(parts[1]) || 1024,
}
}
function formatDimension(w, h, delimiter = '*') {
return `${w}${delimiter}${h}`
}
10. 迁移步骤
| 阶段 | 负责方 | 内容 |
|---|---|---|
| 1. 后端准备 | 后端 | 创建 model_params 表 → 录入 8 个模型配置 → 实现批量+单个接口 → 联调验证 |
| 2. 前端适配 | 前端 | 新增 API 函数 + 缓存层 → 改造 dialogBox + model 选择器 → 保留静态配置为 fallback |
| 3. 清理 | 前端 | 确认全量走新接口 → 删除 src/config/models/*.js → 删除 displayNameMap、getModelConfig |
11. 边界情况
11.1 接口失败降级
迁移期间接口失败时回退静态配置:
const modelConfig = computed(() => {
if (paramsMap.value.size > 0) {
return paramsMap.value.get(currentModelId.value) || null
}
return getModelConfig(model.value) // fallback
})
11.2 缓存主动刷新
管理员修改配置后,前端提供「刷新配置」按钮调用 clearModelParamsCache() 立即生效。
11.3 模型无参数配置
若 model_id 在 model_params 表中无记录,前端仅渲染 prompt 输入框(textarea),不显示其他 UI 组件。
11.4 show_when 条件显示
当前仅支持 { "aspectRatio": "custom" } 条件。后续如需扩展,前后端同步约定新的条件字段和取值。
11.5 向后兼容
- 新增接口路径
models/params,不影响现有模型列表接口 - Video 路径不受影响(继续使用
runninghub.Playload()适配器) - Painting 的
modelParams扁平提交格式不变