# 模型参数配置后端化方案 ## 1. 背景与目标 当前模型参数配置硬编码在前端 `src/config/models/*.js` 中,新增模型或修改参数需要前端发版,业务层无法感知参数定义。 目标:将参数配置迁移到业务层,管理员后台配置模型参数,前端通过 API 按模型 ID 动态获取,实现**零发版上线新模型配置**。 ``` 页面加载: GET /suanli/v1/platforms/{code}/models → 模型列表(现有接口,不变) GET /suanli/v1/platforms/{code}/models/params → 批量拉全部模型参数(新接口) 前端存入 Map 切换模型: 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.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 格式: ```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) → 请求冷却检查(localStorage,30s 冷却期) → 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` 扁平提交格式不变