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

20 KiB
Raw Blame History

模型参数后端化 — 前端适配实现计划

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: 替换 loadInternalConfigloadConfig

删除 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: 替换 showImageUploaderisImageRequired

将第 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: 替换 modelDisplayConfigmodelConfig

在 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.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 修复)

git add -A
git commit -m "chore: ESLint 修复"