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

21 KiB
Raw Blame History

模型参数配置后端化方案

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×H1024*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.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 格式:

{
  "delimiter": "*",
  "width": { "min": 512, "max": 2048 },
  "height": { "min": 512, "max": 2048 }
}

3.2 关键约束

  • model_paramsmodels 完全解耦,各自独立维护
  • 一个 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-2proportion + 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
    → 请求冷却检查localStorage30s 冷却期)
    → 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
})

其他改动点:

  • 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 函数,前端统一处理:

// 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 → 删除 displayNameMapgetModelConfig

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_idmodel_params 表中无记录,前端仅渲染 prompt 输入框textarea不显示其他 UI 组件。

11.4 show_when 条件显示

当前仅支持 { "aspectRatio": "custom" } 条件。后续如需扩展,前后端同步约定新的条件字段和取值。

11.5 向后兼容

  • 新增接口路径 models/params,不影响现有模型列表接口
  • Video 路径不受影响(继续使用 runninghub.Playload() 适配器)
  • Painting 的 modelParams 扁平提交格式不变