chore: 将 docs/ 加入 .gitignore,从版本追踪中移除

This commit is contained in:
王佑琳 2026-06-09 18:28:40 +08:00
parent d4ef09247c
commit 0eee8b1f7f
5 changed files with 1 additions and 2280 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ dist-ssr
*.sln
*.sw?
TEST/
docs/

File diff suppressed because it is too large Load Diff

View File

@ -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.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 修复"
```

View File

@ -1,187 +0,0 @@
# 平台化架构设计
## 目标
将 Painting / Video 两套硬编码分支重构为统一的平台描述符架构,使加入新平台只需新建一个文件夹并实现标准接口,零改动 dialogBox。
## 背景
当前 `dialogBox/index.vue`792 行)通过 `v-if="type === 'Painting'"` / `v-if="type === 'Video'"` 承载两套完全不同的逻辑:
- **模型列表**Painting 走后端 APIVideo 走静态 JSON
- **参数 schema**Painting 走本地 JS 文件Video 走远程 workflow JSON
- **UI 控件**Painting 有 proportion/dimension/quality/quantityVideo 有 pattern/proportion/time
- **任务 body**Painting 扁平 modelParamsVideo `{ 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 迁入 descriptordialogBox 切换读取路径 | 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不变

View File

@ -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)`
- 优先读 localStoragekey: `model_config_{modelId}`TTL 60 秒)
- 未命中调 `requestModelConfig()` + 写入缓存
- `pendingRequests` Map 并发去重
新增 `preloadModelConfigs(modelIds)`
- 调用 `requestModelConfigsBatch(modelIds)`
- 逐条写入 localStorage 缓存
## 三、共享工具函数
新建 `src/utils/modelConfigHelper.js`
| 导出函数 | 说明 |
|---------|------|
| `syncDefaults(config, state)` | params → paramValues + 专用 refproportion/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]`reactiveVue 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.vueUI 不变
- `buildTaskBody()` / `fillFromResult()`:逻辑不变