AI_Painting_V2.0/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md
WangLeo 5c24de354b refactor: 删除旧模型配置文件
- 删除 src/platforms/painting/models/(9 个硬编码 JS)
- 删除 src/utils/modelConfig.js(Video 旧远程 JSON 加载)
配置已全部迁移至后端 API。
2026-06-09 18:09:25 +08:00

736 lines
20 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.

# 模型参数后端化 — 前端适配实现计划
> **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<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`
```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.jsVideo 旧远程 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 修复"
```