- 删除 src/platforms/painting/models/(9 个硬编码 JS) - 删除 src/utils/modelConfig.js(Video 旧远程 JSON 加载) 配置已全部迁移至后端 API。
20 KiB
模型参数后端化 — 前端适配实现计划
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: 写入完整文件
// 模型配置共享工具函数
// 供 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
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 函数
// 批量获取模型配置(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
git add src/apis/display/index.js
git commit -m "feat: 新增模型配置 API(批量 + 单条)"
Task 3: 添加缓存层
Files:
-
Modify:
src/utils/modelApi.js -
Step 1: 在文件末尾追加缓存相关函数
在 clearPlatformModelCache 函数之前插入以下代码:
// ==================== 模型配置缓存 ====================
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<Object|null>} 模型配置对象
*/
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:
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
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
将:
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
import { getModelConfig } from './models/index.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 的调用:
function syncDefaults(config) {
syncDefaults_internal(config, paintingState)
}
实际上,直接用 helper 版本替换原来的内部函数。由于 helper 的 syncDefaults 接受 (config, state),在 descriptor 内部创建一个包装:
删除原 syncDefaults 函数(第 46-77 行),改为:
// state 对象供 helper 函数使用
const paintingState = {
modelConfig, paramValues, proportion, resolution, quantity, quality,
customWidth, customHight, dimWidth, dimHeight, promptPlaceholder,
}
function syncDefaults(config) {
_syncDefaults(config, paintingState)
}
同时将 helper 的 syncDefaults 以别名导入(避免与本地函数重名):
import { getDimConfig, checkShowWhen, syncDefaults as _syncDefaults, syncParamValues as _syncParamValues } from '@/utils/modelConfigHelper.js'
- Step 4: 替换
syncParamValues函数
将第 79-102 行的 syncParamValues 函数替换为:
function syncParamValues() {
_syncParamValues(modelConfig.value, paintingState)
}
- Step 5: 替换
loadConfig函数
将第 194-198 行的:
async loadConfig(modelName, _modelType) {
const config = getModelConfig(modelName)
syncDefaults(config)
return config
},
替换为:
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 行的:
async loadModels() {
const code = getPlatformCode('Painting')
return fetchPlatformModels(code)
},
替换为:
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 行)改为:
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
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
将:
import { fetchModelConfig } from '@/utils/modelConfig'
import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi'
替换为:
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 行之后),追加:
// 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 行)替换为:
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 行替换为:
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 行的:
showImageUploader() {
return modelType.value !== 'text'
},
替换为:
showImageUploader() {
return modelConfig.value?.inputType === 'image' || modelConfig.value?.inputType === 'both'
},
将第 149-151 行的:
isImageRequired() {
return modelType.value !== 'text'
},
替换为:
isImageRequired() {
return !!(modelConfig.value?.params?.find(p => p.ui === 'imageUpload'))
},
- Step 6: 替换
imageUploadLimit
将第 145-147 行替换为:
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 行替换为:
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
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:
import { createTask } from '@/utils/createTask'
将第 93-94 行的:
// 通过 createTask 获取 body 内容(RunningHub workflow payload)
const body = await createTask(data)
替换为:
const body = data.body
- Step 3: 删除 createTask.js
rm src/utils/createTask.js
- Step 4: 验证语法
Run: npx eslint src/utils/taskPolling.js --fix
Expected: 无错误
- Step 5: Commit
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: 删除文件
rm -rf src/platforms/painting/models/
rm src/utils/modelConfig.js
- Step 3: Commit
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 修复)
git add -A
git commit -m "chore: ESLint 修复"