# 模型参数后端化 — 前端适配实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将 Painting/Video 平台的模型参数配置从硬编码迁移至后端 API 获取 **Architecture:** 新建 modelConfigHelper.js 共享工具函数,在 modelApi.js 加缓存层,Painting/Video 双平台统一走 API + params 驱动。方案 A 最小改动。 **Tech Stack:** Vue 3 Composition API + Vite 7 + Pinia + Axios --- ### Task 1: 新建 `src/utils/modelConfigHelper.js` **Files:** - Create: `src/utils/modelConfigHelper.js` - [ ] **Step 1: 写入完整文件** ```js // 模型配置共享工具函数 // 供 Painting / Video descriptor 使用 /** * 检测 dimension 配置模式 * @param {Object|null} config - 模型配置对象 * @returns {Object|null} * - combined: { type: 'combined', config: dimension子对象, paramName: string } * - split: { type: 'split', wParam: 宽度参数, hParam: 高度参数 } * - null: 无 dimension 参数 */ export function getDimConfig(config) { if (!config) return null const dimParam = config.params.find(p => p.ui === 'dimension') if (dimParam) return { type: 'combined', config: dimParam.dimension, paramName: dimParam.name } const wParam = config.params.find(p => p.ui === 'dimensionWidth') const hParam = config.params.find(p => p.ui === 'dimensionHeight') if (wParam && hParam) return { type: 'split', wParam, hParam } return null } /** * 检查 showWhen 条件是否满足 * @param {Object} param - 参数定义(可能含 showWhen) * @param {Object} paramValues - 当前所有参数值 * @returns {boolean} */ export function checkShowWhen(param, paramValues) { if (!param.showWhen) return true return Object.entries(param.showWhen).every(([key, expected]) => { return paramValues[key] === expected }) } /** * 将 API 返回的 config 同步到响应式 state * * state 对象需包含以下属性(均为 ref 或 reactive): * modelConfig, paramValues, proportion, resolution, quantity, quality, * customWidth, customHight, dimWidth, dimHeight, promptPlaceholder */ export function syncDefaults(config, state) { const { modelConfig, paramValues, proportion, resolution, quantity, quality, customWidth, customHight, dimWidth, dimHeight, promptPlaceholder, } = state modelConfig.value = config if (!config) return // 1. dimension.separator → 生成 parse/format(在遍历 params 之前完成) config.params.forEach(p => { if (p.ui === 'dimension' && p.dimension?.separator && !p.dimension.parse) { const sep = p.dimension.separator p.dimension.parse = (val) => { const parts = (val || '').split(sep) return { width: parseInt(parts[0]) || 0, height: parseInt(parts[1]) || 0 } } p.dimension.format = (w, h) => `${w}${sep}${h}` } }) // 2. 初始化 paramValues(已存在的 key 保留,避免切换模型时丢失值) config.params.forEach(p => { if (!(p.name in paramValues)) { paramValues[p.name] = p.default ?? (p.name === 'outputFormat' ? 'png' : '') } }) // 3. 同步专用 ref const ratioParam = config.params.find(p => p.ui === 'proportion') if (ratioParam) proportion.value = ratioParam.default || '1:1' const resParam = config.params.find(p => p.ui === 'resolution') if (resParam) resolution.value = resParam.default || '2k' const qtyParam = config.params.find(p => p.ui === 'quantity') if (qtyParam) quantity.value = qtyParam.default || 1 const cwParam = config.params.find(p => p.name === 'customWidth') if (cwParam) customWidth.value = cwParam.default || 1024 const chParam = config.params.find(p => p.name === 'customHight') if (chParam) customHight.value = chParam.default || 1024 const qualityParam = config.params.find(p => p.name === 'quality') if (qualityParam) quality.value = qualityParam.default || 'medium' // 4. dimension 初始化 const dc = getDimConfig(config) if (dc?.type === 'split') { dimWidth.value = dc.wParam.default || 1024 dimHeight.value = dc.hParam.default || 1024 } else if (dc?.type === 'combined') { const dimParam = config.params.find(p => p.name === dc.paramName) const raw = dimParam?.default || '' const parsed = dc.config.parse(raw) dimWidth.value = parsed.width dimHeight.value = parsed.height } // 5. promptPlaceholder 同步 if (config.promptPlaceholder) { promptPlaceholder.value = config.promptPlaceholder } } /** * 将专用 ref 的当前值回写到 paramValues * (在 buildTaskBody 之前调用) */ export function syncParamValues(config, state) { const { paramValues, proportion, resolution, quantity, customWidth, customHight, dimWidth, dimHeight, quality, } = state const ratioParam = config?.params?.find(p => p.ui === 'proportion') if (ratioParam) paramValues[ratioParam.name] = proportion.value const resParam = config?.params?.find(p => p.ui === 'resolution') if (resParam) paramValues[resParam.name] = resolution.value const qtyParam = config?.params?.find(p => p.ui === 'quantity') if (qtyParam) paramValues[qtyParam.name] = quantity.value if (config?.params?.find(p => p.name === 'customWidth')) { paramValues.customWidth = customWidth.value } if (config?.params?.find(p => p.name === 'customHight')) { paramValues.customHight = customHight.value } if (config?.params?.find(p => p.name === 'quality')) { paramValues.quality = quality.value } const dc = getDimConfig(config) if (dc?.type === 'split') { paramValues[dc.wParam.name] = dimWidth.value paramValues[dc.hParam.name] = dimHeight.value } else if (dc?.type === 'combined') { paramValues[dc.paramName] = dc.config.format(dimWidth.value, dimHeight.value) } } ``` - [ ] **Step 2: 验证语法** Run: `npx eslint src/utils/modelConfigHelper.js --fix` Expected: 无错误 - [ ] **Step 3: Commit** ```bash git add src/utils/modelConfigHelper.js git commit -m "feat: 新增 modelConfigHelper 共享工具函数 提取 getDimConfig / checkShowWhen / syncDefaults / syncParamValues, 供 Painting 和 Video 平台共用。syncDefaults 新增 dimension.separator 解析和 promptPlaceholder 同步能力。" ``` --- ### Task 2: 新增 API 函数 **Files:** - Modify: `src/apis/display/index.js` - [ ] **Step 1: 在文件末尾追加两个 API 函数** ```js // 批量获取模型配置(POST /suanli/v1/models/configs) export function requestModelConfigsBatch(modelIds) { return service.post('/suanli/v1/models/configs', { modelIds }) } // 单条查询模型配置(GET /suanli/v1/models/:modelId/config) export function requestModelConfig(modelId) { return service.get(`/suanli/v1/models/${modelId}/config`) } ``` - [ ] **Step 2: 验证语法** Run: `npx eslint src/apis/display/index.js --fix` Expected: 无错误 - [ ] **Step 3: Commit** ```bash git add src/apis/display/index.js git commit -m "feat: 新增模型配置 API(批量 + 单条)" ``` --- ### Task 3: 添加缓存层 **Files:** - Modify: `src/utils/modelApi.js` - [ ] **Step 1: 在文件末尾追加缓存相关函数** 在 `clearPlatformModelCache` 函数之前插入以下代码: ```js // ==================== 模型配置缓存 ==================== const CONFIG_CACHE_PREFIX = 'model_config_' const CONFIG_CACHE_TTL = 60 * 1000 // 60 秒 const pendingConfigRequests = new Map() // 导入 API 函数(在文件顶部添加) // import { requestModelConfigsBatch, requestModelConfig } from '@/apis/display/index.js' /** * 批量预加载模型配置到缓存 * @param {string[]} modelIds - 模型 UUID 列表 */ export async function preloadModelConfigs(modelIds) { if (!modelIds.length) return const result = await requestModelConfigsBatch(modelIds) const data = result?.data || {} const now = Date.now() modelIds.forEach(id => { const config = data[id] if (config) { const cacheEntry = { config, timestamp: now } try { localStorage.setItem(CONFIG_CACHE_PREFIX + id, JSON.stringify(cacheEntry)) } catch { /* localStorage 满时静默失败 */ } } }) } /** * 获取单个模型配置(优先读缓存,未命中调 API) * @param {string} modelId - 模型 UUID * @returns {Promise} 模型配置对象 */ export async function getModelConfig(modelId) { if (!modelId) return null // 1. 读缓存 try { const cached = localStorage.getItem(CONFIG_CACHE_PREFIX + modelId) if (cached) { const { config, timestamp } = JSON.parse(cached) if (Date.now() - timestamp < CONFIG_CACHE_TTL) { return config } } } catch { /* 缓存解析失败,走 API */ } // 2. 并发去重 if (pendingConfigRequests.has(modelId)) { return pendingConfigRequests.get(modelId) } // 3. 调单条 API const promise = (async () => { try { const result = await requestModelConfig(modelId) const config = result?.data if (config) { const cacheEntry = { config, timestamp: Date.now() } try { localStorage.setItem(CONFIG_CACHE_PREFIX + modelId, JSON.stringify(cacheEntry)) } catch { /* 静默 */ } } return config || null } catch { return null } finally { pendingConfigRequests.delete(modelId) } })() pendingConfigRequests.set(modelId, promise) return promise } ``` - [ ] **Step 2: 在文件顶部追加 import** 在 `import { fetchPlatformModels as _fetchPlatformModels } from '@/apis/display/index.js'` 所在行(或其附近),改为同时导入新 API 函数。如果是按需导入,在已有 import 语句中加入 `requestModelConfigsBatch, requestModelConfig`: ```js import { fetchPlatformModels as _fetchPlatformModels, requestModelConfigsBatch, requestModelConfig } from '@/apis/display/index.js' ``` - [ ] **Step 3: 验证语法** Run: `npx eslint src/utils/modelApi.js --fix` Expected: 无错误 - [ ] **Step 4: Commit** ```bash git add src/utils/modelApi.js git commit -m "feat: 新增模型配置缓存层(60s TTL + 并发去重)" ``` --- ### Task 4: Painting 平台接入 API **Files:** - Modify: `src/platforms/painting/index.js` - [ ] **Step 1: 替换 import** 将: ```js import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi' import { getModelConfig } from './models/index.js' ``` 替换为: ```js import { fetchPlatformModels, getPlatformCode, getModelId, getModelConfig, preloadModelConfigs } from '@/utils/modelApi' import { getDimConfig, checkShowWhen, syncDefaults as _syncDefaults, syncParamValues as _syncParamValues } from '@/utils/modelConfigHelper.js' ``` - [ ] **Step 2: 新增 paintingState + 删除 getDimConfig** 删除 painting/index.js 中第 12-20 行的 `getDimConfig` 函数(已在 helper 中)。 - [ ] **Step 3: 替换 `syncDefaults` 函数** 将第 46-77 行的 `syncDefaults` 函数替换为对 helper 的调用: ```js function syncDefaults(config) { syncDefaults_internal(config, paintingState) } ``` 实际上,直接用 helper 版本替换原来的内部函数。由于 helper 的 `syncDefaults` 接受 `(config, state)`,在 descriptor 内部创建一个包装: 删除原 `syncDefaults` 函数(第 46-77 行),改为: ```js // state 对象供 helper 函数使用 const paintingState = { modelConfig, paramValues, proportion, resolution, quantity, quality, customWidth, customHight, dimWidth, dimHeight, promptPlaceholder, } function syncDefaults(config) { _syncDefaults(config, paintingState) } ``` 同时将 helper 的 `syncDefaults` 以别名导入(避免与本地函数重名): ```js import { getDimConfig, checkShowWhen, syncDefaults as _syncDefaults, syncParamValues as _syncParamValues } from '@/utils/modelConfigHelper.js' ``` - [ ] **Step 4: 替换 `syncParamValues` 函数** 将第 79-102 行的 `syncParamValues` 函数替换为: ```js function syncParamValues() { _syncParamValues(modelConfig.value, paintingState) } ``` - [ ] **Step 5: 替换 `loadConfig` 函数** 将第 194-198 行的: ```js async loadConfig(modelName, _modelType) { const config = getModelConfig(modelName) syncDefaults(config) return config }, ``` 替换为: ```js async loadConfig(modelName, _modelType) { const modelId = await getModelId('Painting', modelName) if (!modelId) return null const config = await getModelConfig(modelId) syncDefaults(config) return config }, ``` - [ ] **Step 6: 替换 `loadModels` 函数,加入批量预加载** 将第 189-192 行的: ```js async loadModels() { const code = getPlatformCode('Painting') return fetchPlatformModels(code) }, ``` 替换为: ```js async loadModels() { const code = getPlatformCode('Painting') const models = await fetchPlatformModels(code) if (models?.length) { const modelIds = models.map(m => m.id) await preloadModelConfigs(modelIds) } return models }, ``` - [ ] **Step 7: 在 controls 的 `show()` 中加入 showWhen 判断** dimension control 的 `show`(第 133 行)改为: ```js show: (config) => { const hasDim = config?.params?.find(p => (p.ui === 'dimension' || p.ui === 'dimensionWidth') && checkShowWhen(p, paramValues) ) return !!hasDim }, ``` - [ ] **Step 8: 验证语法** Run: `npx eslint src/platforms/painting/index.js --fix` Expected: 无错误 - [ ] **Step 9: Commit** ```bash git add src/platforms/painting/index.js git commit -m "feat: Painting 平台接入模型配置 API - loadModels 增加批量预加载模型配置 - loadConfig 改为 API 获取(替代硬编码 getModelConfig) - syncDefaults/syncParamValues/getDimConfig 迁移至 helper - controls show() 加入 showWhen 条件判断" ``` --- ### Task 5: Video 平台接入 API **Files:** - Modify: `src/platforms/video/index.js` - [ ] **Step 1: 替换 import** 将: ```js import { fetchModelConfig } from '@/utils/modelConfig' import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi' ``` 替换为: ```js import { fetchPlatformModels, getPlatformCode, getModelId, getModelConfig, preloadModelConfigs } from '@/utils/modelApi' import { getDimConfig, checkShowWhen, syncDefaults as _syncDefaults, syncParamValues as _syncParamValues } from '@/utils/modelConfigHelper.js' ``` - [ ] **Step 2: 增加 params 驱动的 state** 在 `defineVideoPlatform` 函数内部,原有 state ref 定义之后(第 33-35 行之后),追加: ```js // params 驱动(与 Painting 统一) const paramValues = reactive({}) const modelConfig = ref(null) const quality = ref('medium') const customWidth = ref(1024) const customHight = ref(1024) const dimWidth = ref(1024) const dimHeight = ref(1024) const quantity = ref(1) const paintingCompatState = { modelConfig, paramValues, proportion, resolution, quantity, quality, customWidth, customHight, dimWidth, dimHeight, promptPlaceholder, } function syncDefaults(config) { _syncDefaults(config, paintingCompatState) } function syncParamValues() { _syncParamValues(modelConfig.value, paintingCompatState) } ``` - [ ] **Step 3: 替换 `loadInternalConfig` 和 `loadConfig`** 删除 `loadInternalConfig` 函数(第 45-67 行)。 将 `loadConfig`(第 122-124 行)替换为: ```js async loadConfig(modelName, modelTypeVal) { const modelId = await getModelId('Video', modelName) if (!modelId) return null const config = await getModelConfig(modelId) syncDefaults(config) return config }, ``` - [ ] **Step 4: 替换 `loadModels` 加入批量预加载** 将第 117-120 行替换为: ```js async loadModels() { const code = getPlatformCode('Video') const models = await fetchPlatformModels(code) if (models?.length) { const modelIds = models.map(m => m.id) await preloadModelConfigs(modelIds) } return models }, ``` - [ ] **Step 5: 替换 `showImageUploader` 和 `isImageRequired`** 将第 141-143 行的: ```js showImageUploader() { return modelType.value !== 'text' }, ``` 替换为: ```js showImageUploader() { return modelConfig.value?.inputType === 'image' || modelConfig.value?.inputType === 'both' }, ``` 将第 149-151 行的: ```js isImageRequired() { return modelType.value !== 'text' }, ``` 替换为: ```js isImageRequired() { return !!(modelConfig.value?.params?.find(p => p.ui === 'imageUpload')) }, ``` - [ ] **Step 6: 替换 `imageUploadLimit`** 将第 145-147 行替换为: ```js imageUploadLimit() { if (!modelConfig.value) return 4 const imageParam = modelConfig.value.params.find(p => p.ui === 'imageUpload') return imageParam?.maxCount || modelConfig.value.maxImages || 4 }, ``` - [ ] **Step 7: 替换 `buildTaskBody`** 将第 153-162 行替换为: ```js buildTaskBody(shared) { syncParamValues() const modelParams = { ...paramValues } if (shared.prompt.value) modelParams.prompt = shared.prompt.value return modelParams }, ``` - [ ] **Step 8: 替换 `modelDisplayConfig` → `modelConfig`** 在 platform 对象中,将 `modelDisplayConfig` 替换为 `modelConfig`(第 114 行附近)。 在 `fillFromResult` 中保持不变(不涉及 config 字段)。 - [ ] **Step 9: 验证语法** Run: `npx eslint src/platforms/video/index.js --fix` Expected: 无错误 - [ ] **Step 10: Commit** ```bash git add src/platforms/video/index.js git commit -m "feat: Video 平台接入模型配置 API - loadModels 增加批量预加载 - loadConfig 改为 API 获取(替代 modelConfig.js) - buildTaskBody 改为 params 驱动 - showImageUploader/isImageRequired 改为 inputType 驱动 - modelDisplayConfig 统一为 modelConfig" ``` --- ### Task 6: 移除 createTask 依赖 **Files:** - Modify: `src/utils/taskPolling.js` - Delete: `src/utils/createTask.js` - [ ] **Step 1: 确认 createTask 的引用点** `taskPolling.js:4` import + `taskPolling.js:93-94` 调用。`createTask(data)` 只返回 `data.body`(纯透传)。 - [ ] **Step 2: 修改 taskPolling.js** 删除第 4 行的 import: ```js import { createTask } from '@/utils/createTask' ``` 将第 93-94 行的: ```js // 通过 createTask 获取 body 内容(RunningHub workflow payload) const body = await createTask(data) ``` 替换为: ```js const body = data.body ``` - [ ] **Step 3: 删除 createTask.js** ```bash rm src/utils/createTask.js ``` - [ ] **Step 4: 验证语法** Run: `npx eslint src/utils/taskPolling.js --fix` Expected: 无错误 - [ ] **Step 5: Commit** ```bash git add src/utils/taskPolling.js git rm src/utils/createTask.js git commit -m "refactor: 移除 createTask 透传层,taskPolling 直接读 data.body" ``` --- ### Task 7: 删除旧配置文件 **Files:** - Delete: `src/platforms/painting/models/`(整个目录,9 个文件) - Delete: `src/utils/modelConfig.js` - [ ] **Step 1: 确认无其他引用** `src/platforms/painting/models/index.js` — 仅被 painting/index.js 引用(已在 Task 4 移除 import) `src/utils/modelConfig.js` — 仅被 video/index.js 引用(已在 Task 5 移除 import) - [ ] **Step 2: 删除文件** ```bash rm -rf src/platforms/painting/models/ rm src/utils/modelConfig.js ``` - [ ] **Step 3: Commit** ```bash git add -A git commit -m "refactor: 删除 Painting 硬编码模型配置和 Video 旧 config 加载 - 删除 src/platforms/painting/models/(9 个硬编码 JS) - 删除 src/utils/modelConfig.js(Video 旧远程 JSON 加载) 配置已全部迁移至后端 API。" ``` --- ### Task 8: 验证 - [ ] **Step 1: ESLint 全量检查** Run: `npx eslint src/ --fix` Expected: 无错误 - [ ] **Step 2: 启动开发服务器** Run: `pnpm dev` Expected: Vite 启动成功,无编译错误 - [ ] **Step 3: 功能冒烟** 在浏览器中验证: - Painting 平台模型列表正常加载 - 切换模型后控件正常渲染(参数来自 API) - 生成任务提交正常 - Video 平台(暂无模型)不报错 - [ ] **Step 4: Commit(如有 lint 修复)** ```bash git add -A git commit -m "chore: ESLint 修复" ```