chore: 将 docs/ 加入 .gitignore,从版本追踪中移除
This commit is contained in:
parent
d4ef09247c
commit
0eee8b1f7f
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
TEST/
|
||||
docs/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,735 +0,0 @@
|
||||
# 模型参数后端化 — 前端适配实现计划
|
||||
|
||||
> **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.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 修复"
|
||||
```
|
||||
@ -1,187 +0,0 @@
|
||||
# 平台化架构设计
|
||||
|
||||
## 目标
|
||||
|
||||
将 Painting / Video 两套硬编码分支重构为统一的平台描述符架构,使加入新平台只需新建一个文件夹并实现标准接口,零改动 dialogBox。
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `dialogBox/index.vue`(792 行)通过 `v-if="type === 'Painting'"` / `v-if="type === 'Video'"` 承载两套完全不同的逻辑:
|
||||
|
||||
- **模型列表**:Painting 走后端 API,Video 走静态 JSON
|
||||
- **参数 schema**:Painting 走本地 JS 文件,Video 走远程 workflow JSON
|
||||
- **UI 控件**:Painting 有 proportion/dimension/quality/quantity,Video 有 pattern/proportion/time
|
||||
- **任务 body**:Painting 扁平 modelParams,Video `{ workflowId, nodeInfoList }`
|
||||
|
||||
后续模型参数后端化后,所有平台将统一为"API 获取模型列表 + API 获取参数 schema"模式。
|
||||
|
||||
## 核心设计:平台描述符模式
|
||||
|
||||
每个平台封装为一个文件夹,导出 `definePlatform()` 工厂函数,返回标准接口对象。dialogBox 退化为纯渲染引擎。
|
||||
|
||||
### 平台接口
|
||||
|
||||
```js
|
||||
// src/platforms/<name>/index.js
|
||||
export function definePlatform() {
|
||||
// 响应式状态(各平台自定义)
|
||||
const model = ref(defaultValue)
|
||||
const modelType = ref(defaultValue)
|
||||
const state = reactive({ ... })
|
||||
|
||||
// 模型选择器组件
|
||||
const ModelSelector = markRaw(Component)
|
||||
|
||||
// 参数控件列表(有序,决定渲染顺序)
|
||||
const controls = [
|
||||
{
|
||||
name: 'proportion',
|
||||
component: markRaw(ProportionComponent),
|
||||
show: (config) => config?.params?.some(p => p.ui === 'proportion'),
|
||||
props: (config) => ({ /* 额外 props */ }),
|
||||
},
|
||||
// ...
|
||||
]
|
||||
|
||||
// 图片上传器(可选)
|
||||
const ImageUploader = markRaw(Component) | null
|
||||
|
||||
return {
|
||||
id: 'painting', // 平台标识
|
||||
label: 'AI绘画2026', // 显示标题
|
||||
ModelSelector, // 模型选择器组件
|
||||
controls, // 有序控件列表
|
||||
ImageUploader, // 图片上传器组件(可选)
|
||||
state, // 平台自定义响应式状态
|
||||
model, // 当前模型 ref
|
||||
modelType, // 当前模型类型 ref
|
||||
|
||||
async loadModels() { }, // 获取模型列表
|
||||
async loadConfig(modelName) { }, // 获取模型参数配置
|
||||
buildTaskBody(state) { }, // 构造请求 body
|
||||
getDefaultModel() { }, // 默认模型名称
|
||||
isImageRequired(state) { }, // 是否必须上传图片
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控件绑定约定
|
||||
|
||||
dialogBox 渲染控件时自动处理 `name` 与 `state` 的 v-model 绑定:
|
||||
|
||||
- `modelValue` → `state[name]`
|
||||
- `onUpdate:modelValue` → `state[name] = v`
|
||||
|
||||
控件 descriptor 仅需定义 `show` 条件(基于 modelConfig 上下文)和额外 `props`。
|
||||
|
||||
### 平台注册表
|
||||
|
||||
```js
|
||||
// src/platforms/registry.js
|
||||
import { definePaintingPlatform } from './painting/index.js'
|
||||
import { defineVideoPlatform } from './video/index.js'
|
||||
|
||||
const registry = { Painting, Video }
|
||||
|
||||
export function createPlatform(type) {
|
||||
const factory = registry[type]
|
||||
if (!factory) throw new Error(`未找到平台: ${type}`)
|
||||
return factory()
|
||||
}
|
||||
```
|
||||
|
||||
### dialogBox 角色变化
|
||||
|
||||
dialogBox 接收 `type` prop → 调用 `createPlatform(type)` → 获得 descriptor → 据 descriptor 渲染一切:
|
||||
|
||||
1. 渲染 `<component :is="platform.ModelSelector">`
|
||||
2. 遍历 `platform.controls`,`show` 返回 true 的渲染 `<component :is>`
|
||||
3. `handleStart()` 委托给 `platform.buildTaskBody(state)` → 调用 `taskPolling.generate(body)`
|
||||
4. `watch(model)` 委托给 `platform.loadConfig(name)`
|
||||
|
||||
## 数据流
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph dialogBox["dialogBox 编排层"]
|
||||
A["platform.loadModels()"] --> B[模型选择器渲染]
|
||||
B --> C["watch(model) → platform.loadConfig(name)"]
|
||||
C --> D[计算 visibleControls]
|
||||
D --> E["v-for 渲染 controls(自动 v-model)"]
|
||||
E --> F["handleStart → platform.buildTaskBody()"]
|
||||
F --> G["taskPolling.generate(body)"]
|
||||
end
|
||||
|
||||
subgraph platform["platform 包"]
|
||||
H["loadModels() → API"]
|
||||
I["loadConfig(name) → API"]
|
||||
J["buildTaskBody() → 扁平 body"]
|
||||
end
|
||||
|
||||
G --> K[POST /suanli/v1/tasks]
|
||||
K --> L[轮询 → displayStore 更新虚拟滚动列表]
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── platforms/ # 平台包(新增)
|
||||
│ ├── painting/
|
||||
│ │ ├── index.js # definePlatform()
|
||||
│ │ ├── modelSelector.vue
|
||||
│ │ ├── imageUploader.vue
|
||||
│ │ └── controls/
|
||||
│ │ ├── proportion.vue
|
||||
│ │ ├── dimension.vue
|
||||
│ │ ├── quality.vue
|
||||
│ │ └── quantity.vue
|
||||
│ ├── video/
|
||||
│ │ ├── index.js
|
||||
│ │ ├── modelSelector.vue
|
||||
│ │ ├── imageUploader.vue
|
||||
│ │ └── controls/
|
||||
│ │ ├── pattern.vue
|
||||
│ │ ├── proportion.vue
|
||||
│ │ └── time.vue
|
||||
│ └── registry.js
|
||||
│
|
||||
├── components/
|
||||
│ ├── dialogBox/index.vue # 精简后 ~200 行
|
||||
│ ├── Popover/ # 共享基础组件(不变)
|
||||
│ ├── Select/ # 共享基础组件(不变)
|
||||
│ ├── Img/ # 共享基础组件(不变)
|
||||
│ └── virtual-scroller/ # 共享基础组件(不变)
|
||||
│
|
||||
├── apis/ # API 层(不变)
|
||||
├── utils/
|
||||
│ ├── taskPolling.js # 任务轮询(不变)
|
||||
│ ├── request.js # Axios(不变)
|
||||
│ └── modelApi.js # 平台 API 封装
|
||||
│
|
||||
├── config/
|
||||
│ ├── models/ # 逐步废弃
|
||||
│ ├── runninghub/ # 逐步废弃
|
||||
│ └── plugins.js # 不变
|
||||
```
|
||||
|
||||
### 迁移路径
|
||||
|
||||
| 步骤 | 内容 | 影响范围 |
|
||||
|------|------|----------|
|
||||
| 1 | 新建 `src/platforms/` + `registry.js`,不删旧代码 | 纯新增 |
|
||||
| 2 | Painting 迁入 descriptor,dialogBox 切换读取路径 | dialogBox 精简 |
|
||||
| 3 | Video 迁入 descriptor | dialogBox 继续精简 |
|
||||
| 4 | 删除旧代码:`config/models/`、`config/runninghub/`、`modelConfig.js` | 清理 |
|
||||
| 5 | 后端化:各平台 `loadConfig()` 改为调 API | 仅改 descriptor 内部 |
|
||||
|
||||
每步独立提交,方便回滚。
|
||||
|
||||
## 不变更的部分
|
||||
|
||||
- `taskPolling.js`:任务创建和轮询逻辑通用,不变
|
||||
- `displayStore`:虚拟滚动列表状态通用,不变
|
||||
- `Popover`、`Select`、`Img`、`virtual-scroller`:共享 UI 组件
|
||||
- `home/index.vue`:仍然传 `type` prop,不变
|
||||
- `apis/`、`request.js`:HTTP 层不变
|
||||
- `config/plugins.js`、router、stores:不变
|
||||
@ -1,147 +0,0 @@
|
||||
# 模型参数后端化 — 前端适配设计
|
||||
|
||||
## 概述
|
||||
|
||||
将 Painting/Video 平台的模型参数配置从**前端代码硬编码**迁移至**后端 API 获取**,实现新增模型或修改参数无需前端发版。
|
||||
|
||||
方案 A:最小改动,只替换配置来源,保持现有架构不变。
|
||||
|
||||
## 一、数据流
|
||||
|
||||
```
|
||||
页面加载 → platform.loadModels()
|
||||
→ fetchPlatformModels(code) // 已有
|
||||
→ 提取所有 modelId
|
||||
→ POST /suanli/v1/models/configs // 新增,批量获取
|
||||
→ 返回 { "uuid1": { config }, ... }
|
||||
→ 逐条写入 60s localStorage 缓存
|
||||
|
||||
用户切换模型 → platform.loadConfig(modelName)
|
||||
→ getModelId(type, modelName) // 已有
|
||||
→ getModelConfig(modelId) // 新增,优先缓存 → fallback 单条 API
|
||||
→ syncDefaults(config)
|
||||
→ modelConfig.value = config
|
||||
→ 遍历 params 初始化 paramValues + 专用 ref
|
||||
→ dimension.separator → 生成 parse/format
|
||||
→ promptPlaceholder 同步
|
||||
→ visibleControls 更新(含 showWhen 条件判断)
|
||||
→ 用户填写参数 → buildTaskBody() → 扁平 modelParams → POST 创建任务
|
||||
```
|
||||
|
||||
## 二、API 层
|
||||
|
||||
### 新增 API 函数(`src/apis/display/index.js`)
|
||||
|
||||
```js
|
||||
// 批量获取模型配置
|
||||
export function requestModelConfigsBatch(modelIds) {
|
||||
return service.post('/suanli/v1/models/configs', { modelIds })
|
||||
}
|
||||
|
||||
// 单条查询(缓存未命中 fallback)
|
||||
export function requestModelConfig(modelId) {
|
||||
return service.get(`/suanli/v1/models/${modelId}/config`)
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存层(`src/utils/modelApi.js`)
|
||||
|
||||
新增 `getModelConfig(modelId)`:
|
||||
|
||||
- 优先读 localStorage(key: `model_config_{modelId}`,TTL 60 秒)
|
||||
- 未命中调 `requestModelConfig()` + 写入缓存
|
||||
- `pendingRequests` Map 并发去重
|
||||
|
||||
新增 `preloadModelConfigs(modelIds)`:
|
||||
|
||||
- 调用 `requestModelConfigsBatch(modelIds)`
|
||||
- 逐条写入 localStorage 缓存
|
||||
|
||||
## 三、共享工具函数
|
||||
|
||||
新建 `src/utils/modelConfigHelper.js`:
|
||||
|
||||
| 导出函数 | 说明 |
|
||||
|---------|------|
|
||||
| `syncDefaults(config, state)` | params → paramValues + 专用 ref(proportion/resolution/quantity/dimension/quality) |
|
||||
| `syncParamValues(config, state)` | 专用 ref 回写到 paramValues |
|
||||
| `getDimConfig(config)` | 检测 combined/split 模式,返回 dimension 配置 |
|
||||
| `checkShowWhen(param, paramValues)` | 检查 showWhen 条件是否满足 |
|
||||
|
||||
### `syncDefaults` 增强
|
||||
|
||||
1. dimension.separator → 生成 `parse/format` 函数(替代硬编码的 JS 函数)
|
||||
2. `config.promptPlaceholder` → 同步到 `promptPlaceholder.value`
|
||||
3. customWidth/customHight 继续通过 `p.name` 查找(保持现有硬编码兼容)
|
||||
|
||||
## 四、Painting 平台改造
|
||||
|
||||
`src/platforms/painting/index.js`:
|
||||
|
||||
- `loadConfig()` 改为 `getModelId()` + `getModelConfig()` API 调用
|
||||
- `syncDefaults`/`syncParamValues`/`getDimConfig` 改为从 helper 导入
|
||||
- 移除 `import { getModelConfig } from './models/index.js'`
|
||||
- controls 的 `show()` 加入 `checkShowWhen` 判断
|
||||
|
||||
## 五、Video 平台改造
|
||||
|
||||
`src/platforms/video/index.js`:
|
||||
|
||||
| 项目 | 当前 | 改造后 |
|
||||
|------|------|--------|
|
||||
| 配置来源 | `modelConfig.js` → 远程 JSON | 统一 API |
|
||||
| 配置存储 | `modelDisplayConfig` | 统一 `modelConfig` |
|
||||
| 数据结构 | `config.display.*` | 统一 `params[]` 数组 |
|
||||
| `loadConfig` | `loadInternalConfig` | 改为 API + `syncDefaults` |
|
||||
| `buildTaskBody` | 硬编码 5 字段 | 改为 params 驱动(与 Painting 一致) |
|
||||
| `showImageUploader` | `modelType !== 'text'` | 改为 `config.inputType` 驱动 |
|
||||
| controls | pattern/proportion/time | 改为按 `params[]` 的 `ui` 驱动 |
|
||||
|
||||
Video 现有的 pattern/time 控件对应的 ui 值暂未定义,保留占位,等后端配置数据就绪后再适配。
|
||||
|
||||
## 六、showWhen 条件显示
|
||||
|
||||
`checkShowWhen(param, paramValues)` 检查 param 的 `showWhen` 字段:
|
||||
|
||||
```js
|
||||
// 例如 { aspectRatio: 'custom' } → 仅在 paramValues.aspectRatio === 'custom' 时显示
|
||||
showWhen 为空 → 总是显示
|
||||
showWhen 存在 → 所有 key-value 匹配才显示
|
||||
```
|
||||
|
||||
controls 的 `show()` 中调用,因为直接读取 `paramValues[key]`(reactive),Vue computed 自动追踪依赖。
|
||||
|
||||
## 七、文件清理
|
||||
|
||||
| 路径 | 操作 |
|
||||
|------|------|
|
||||
| `src/utils/modelConfigHelper.js` | **新建** |
|
||||
| `src/platforms/painting/models/`(9 文件) | **删除** |
|
||||
| `src/utils/modelConfig.js` | **删除** |
|
||||
| `src/utils/createTask.js` | **删除** |
|
||||
|
||||
删除前需确认 `createTask.js` 无其他文件 import。
|
||||
|
||||
## 八、API 验证发现
|
||||
|
||||
已用 token 实测 Painting 全部 8 个模型,结论:
|
||||
|
||||
| 发现 | 结论 |
|
||||
|------|------|
|
||||
| 单条 API | `GET /suanli/v1/models/:id/config` → `data` 直接返回 config |
|
||||
| 批量 API | `POST /suanli/v1/models/configs` → `data` 为 `{ modelId: config, ... }`,不存在的 ID 返回空 `{}` |
|
||||
| `dimension.separator` | 字符串 `"*"`,需前端 `syncDefaults` 生成 parse/format |
|
||||
| `dimensionWidth/Height` 的 `min/max` | 在 param 根层,与硬编码结构一致,`getDimConfig` 直接兼容 |
|
||||
| `imageUpload` 的 `maxCount` | 在 param 根层,`imageUploadLimit()` 直接兼容 |
|
||||
| `number` 类型的 `min/max` | 在 param 根层,与 dimensionWidth 一致 |
|
||||
| customWidth/customHight | `ui: 'number'`,`showWhen: {"aspectRatio": "custom"}` |
|
||||
| hidden 参数 | 不返回 `options` 字段(与硬编码不同),不影响功能 |
|
||||
| `type` 字段 | API 不返回,前端 `syncDefaults` 不依赖,无影响 |
|
||||
| Video 平台 | 模型列表为空,改造后无 fallback 将不可用 |
|
||||
|
||||
## 九、不改动的部分
|
||||
|
||||
- `src/components/dialogBox/index.vue`:通过 `platform.loadConfig()` 多态调用,改动仅在 descriptor 内部
|
||||
- `src/utils/modelApi.js`:`getModelId()`/`fetchPlatformModels()` 保持不变,新增两个函数
|
||||
- controls 组件(proportion.vue / dimension.vue / quality.vue / quantity.vue):UI 不变
|
||||
- `buildTaskBody()` / `fillFromResult()`:逻辑不变
|
||||
Loading…
Reference in New Issue
Block a user