AI_Painting_V2.0/docs/模型参数后端化方案.md
WangLeo a1134d85ad 新增 DimensionInput 共享组件,修复多个模型参数 UI 渲染缺陷,补充后端化方案文档
- 新增 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 空目录
2026-06-08 18:36:53 +08:00

649 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 模型参数配置后端化方案
## 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:**
```json
{
"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 │
└─────────────────────┘ └──────────────────────────┘
```
```sql
CREATE TABLE model_params (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
model_id VARCHAR(64) NOT NULL, -- 关联 models.idUUID
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 格式:
```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. 接口实现伪代码
```python
# 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`
```json
{
"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`
```json
{
"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`
```json
{
"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
→ 请求冷却检查localStorage30s 冷却期)
→ API 请求
```
| 层级 | 存储位置 | 生命周期 | 说明 |
|------|---------|---------|------|
| L1 | JS 内存 Map | 标签页关闭即销毁 | 同会话内切换模型 0 延迟 |
| L2 | localStorage | 持久化,跨会话/刷新 | 刷新页面后直接命中,无需请求 |
| 冷却期 | localStorage | 独立于缓存 | 限制 API 调用频率最低 30s/次 |
**正常场景**:首次访问 → API 请求 → 写入 L1 + L2。刷新页面 → L2 命中0 请求。
**极端场景**:清掉 localStorage 连刷 10 次 → 请求冷却生效,实际只有 1 次请求到达后端。
---
## 8. 新增文件
### 8.1 API 层
```js
// 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 缓存层
```js
// 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 渲染
```
**核心代码改造:**
```js
// 当前
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
})
```
**其他改动点:**
- `modelConfig` watcher 中维度初始化改用统一 `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` 函数,前端统一处理:
```js
// 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 接口失败降级
迁移期间接口失败时回退静态配置:
```js
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` 扁平提交格式不变