refactor: 删除旧模型配置文件

- 删除 src/platforms/painting/models/(9 个硬编码 JS)
- 删除 src/utils/modelConfig.js(Video 旧远程 JSON 加载)
配置已全部迁移至后端 API。
This commit is contained in:
王佑琳 2026-06-09 18:09:25 +08:00
parent 2cd3f8fad6
commit 5c24de354b
17 changed files with 1026 additions and 760 deletions

View File

@ -2,7 +2,12 @@
"permissions": {
"allow": [
"Bash(git add *)",
"Bash(git commit *)"
"Bash(git commit *)",
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --oneline --all -- src/views/home/display/index.vue)",
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --oneline --all -- src/stores/display.js)",
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --oneline --all -- src/components/dialogBox/index.vue)",
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --all --oneline --follow -p -- src/stores/display.js)",
"Bash(git -C \"D:/WebUI/Kexue/操作平台/AI_Painting_V2.0\" log --all --oneline -p -- src/components/dialogBox/index.vue)"
]
}
}

0
bug.txt Normal file
View File

1
components.d.ts vendored
View File

@ -12,6 +12,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Canvas: typeof import('./src/components/canvas/index.vue')['default']
copy: typeof import('./src/components/virtual-scroller/VirtualScroller copy.vue')['default']
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']

View File

@ -0,0 +1,735 @@
# 模型参数后端化 — 前端适配实现计划
> **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

@ -0,0 +1,147 @@
# 模型参数后端化 — 前端适配设计
## 概述
将 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()`:逻辑不变

View File

@ -1,282 +0,0 @@
# 模型参数后端化方案
## 一、当前架构(平台重构后)
平台重构已采用 **Platform Descriptor 模式**Painting 和 Video 统一走扁平 `modelParams`
```
src/platforms/
├── registry.js # 注册表registerPlatform() + createPlatform()
├── painting/
│ ├── index.js # Painting descriptorcontrols, state, loadConfig 等)
│ ├── modelSelector.vue
│ ├── imageUploader.vue
│ ├── models/ # 模型参数 schema本地 JS 文件,待后端化)
│ │ ├── index.js # getModelConfig(modelName) → 查找本地 config
│ │ └── flux-2.js # 单个模型的 params 定义
│ └── controls/
│ ├── proportion.vue
│ ├── dimension.vue
│ ├── quality.vue
│ └── quantity.vue
└── video/
├── index.js # Video descriptor仍用 fetchModelConfig 拉远程 JSON
├── modelSelector.vue
├── imageUploader.vue
└── controls/
├── pattern.vue
├── proportion.vue
└── time.vue
```
**核心数据流(两个平台统一):**
1. 用户选择模型 → `dialogBox` 调用 `platform.loadConfig(modelName, modelType)`
2. `loadConfig` 获取参数 schema → 驱动 `controls` 数组渲染对应 UI 控件
3. 用户点击发送 → `platform.buildTaskBody({ prompt, referenceImages })` 返回扁平 `modelParams`
4. `createTask.js` 透传 `data.body`(不再做任何转换)
5. POST `/suanli/v1/tasks`,携带 `X-Session-Id` header
**当前参数配置来源(待统一):**
| 平台 | 参数配置来源 | 位置 |
|------|-------------|------|
| Painting | 本地 JS 文件(硬编码) | `src/platforms/painting/models/*.js` |
| Video | 远程静态 JSON每日 localStorage 缓存) | `fetchModelConfig()``resources.xueai.art/AIGC/static/public/Platform/Video/workflows/...` |
两个平台的 `buildTaskBody()` 均已返回扁平 `modelParams`,不再构造 `{ workflowId, nodeInfoList }` 格式。
---
## 二、目标架构
将模型参数配置从**前端代码**迁移到**后端 API**,实现:
- 新增模型/修改参数无需前端发版
- Painting 和 Video 使用统一的 API 获取配置
- 前端只保留 UI 控件库,参数 schema 完全由后端驱动
```
后端 API
├── GET /api/v1/platforms/:code/models # 模型列表(已有)
│ └── 响应: [{ id, display_name, tags, config? }]
├── GET /api/v1/models/:id/config # 模型参数配置(新增)
│ └── 响应: { params: [...], inputType, maxImages, ... }
└── POST /api/v1/tasks # 创建任务(已有)
└── 请求体: 扁平 modelParams与 RunningHub API 格式对齐)
```
**改造后的数据流:**
```
用户选择模型
→ platform.loadConfig(modelName)
→ GET /api/v1/models/:id/config
→ 返回 { params: [{ name, ui, default, options, ... }] }
→ 前端根据 ui 字段渲染对应控件
→ 用户填写参数
→ buildTaskBody() 返回扁平 { aspectRatio, resolution, ... }
→ POST /api/v1/tasks
```
---
## 三、模型配置 API 格式
### 3.1 请求
```
GET /api/v1/models/:modelId/config
```
`modelId` 来自模型列表接口返回的 `id` 字段UUID
### 3.2 响应格式
```json
{
"code": 0,
"data": {
"inputType": "text",
"maxImages": 4,
"promptPlaceholder": "描述你想生成的画面和动作。",
"params": [
{
"name": "prompt",
"ui": "textarea",
"label": "提示词",
"required": true,
"default": ""
},
{
"name": "aspectRatio",
"ui": "proportion",
"label": "比例",
"default": "1:1",
"options": ["1:1", "3:4", "4:3", "9:16", "16:9", "custom"]
},
{
"name": "resolution",
"ui": "resolution",
"label": "分辨率",
"default": "2k",
"options": ["1k", "2k", "4k"]
},
{
"name": "size",
"ui": "dimension",
"label": "尺寸",
"default": "1024*1024",
"dimension": {
"separator": "*",
"width": { "min": 256, "max": 6197 },
"height": { "min": 256, "max": 4096 }
}
},
{
"name": "quality",
"ui": "select",
"label": "画质",
"default": "medium",
"options": ["low", "medium", "high"]
},
{
"name": "quantity",
"ui": "quantity",
"label": "生成数量",
"default": 1,
"options": [1, 2, 3, 4]
},
{
"name": "imageUrl",
"ui": "imageUpload",
"label": "参考图",
"maxCount": 4
}
]
}
}
```
### 3.3 `ui` 字段与前端控件映射
| `ui` 值 | 前端控件 | 说明 |
|---------|---------|------|
| `textarea` | Sender 内置 textarea | 提示词输入框,不需要额外渲染控件 |
| `proportion` | `PaintingProportion` / `VideoProportion` | 比例选择 Popover`options` 含 `custom` 时允许自定义宽高 |
| `resolution` | `proportion` 控件内部 | 分辨率子选项,与 proportion 共用 Popover |
| `dimension` | `DimensionInput` | **组合模式**:单字段 `"W*H"` 格式,通过 `dimension.parse/format` 序列化 |
| `dimensionWidth` + `dimensionHeight` | `DimensionInput` | **拆分模式**:两个独立字段,共享同一个 Popover 和比例锁 |
| `select` | `Select` | 通用下拉选择(如 quality |
| `quantity` | `Quantity` | 生成数量,上限由 `options` 最大值派生 |
| `imageUpload` | `ImageUploader` | 参考图上传,`maxCount` 控制数量上限 |
| `hidden` | 无 | 静默写入默认值,不渲染控件 |
> **关于 `dimension.separator`** 当前前端 Painting 配置中 `parse/format` 为 JS 函数(`s.split('*')` / `` `${w}*${h}` ``),后端返回 JSON 无法携带函数。因此 `dimension` 类型的配置需包含 `separator` 字段,前端据此在运行时生成等价的 parse/format 逻辑,无需硬编码分隔符。拆分模式(`dimensionWidth` + `dimensionHeight`)无此字段。
### 3.4 条件显示
```json
{
"name": "customWidth",
"ui": "dimensionWidth",
"showWhen": { "aspectRatio": "custom" }
}
```
`showWhen` 字段使参数仅在指定条件满足时显示。当前支持的条件:`aspectRatio` 值为 `custom`
---
## 四、迁移步骤
### 阶段一:后端实现模型配置 API
- [ ] 设计并实现 `GET /api/v1/models/:id/config` 接口
- [ ] 将 Painting 的 9 个模型配置(`src/platforms/painting/models/*.js`)迁移到数据库/配置文件
- [ ] 将 Video 的 workflow 配置(`resources.xueai.art/AIGC/static/public/Platform/Video/workflows/...`)迁移到同一接口
- [ ] 响应格式对齐 3.2 节的 schema
### 阶段二:前端适配
- [ ] `src/platforms/painting/models/` 目录删除,`getModelConfig()` 改为调用 API
- [ ] `src/utils/modelConfig.js`Video 旧架构遗留删除Video descriptor 改为调用同一 API
- [ ] `src/utils/createTask.js`(当前仅透传)删除,`dialogBox` 直接使用 `buildTaskBody()` 返回值
- [ ] Painting descriptor 的 `loadConfig()` 改为 `fetch` API + 本地缓存30s TTL与模型列表一致
### 阶段三:清理
- [ ] 删除 `src/utils/modelConfig.js`
- [ ] 删除 `src/utils/createTask.js`
- [ ] 确认前端不再包含任何硬编码的模型参数
---
## 五、与 RunningHub API 的关系
后端创建任务的请求体格式应**直接对齐 RunningHub 标准模型 API**,前端 `buildTaskBody()` 返回的扁平 `modelParams` 即是 API body
```json
{
"prompt": "...",
"resolution": "720p",
"aspectRatio": "16:9",
"duration": 5
}
```
不再需要中间层转换(旧架构的 `src/config/runninghub/` 已删除)。
---
## 附录RunningHub API 参考
以下为 RunningHub 标准模型 API 的原始文档,作为后端任务创建接口的设计参考。
### A.1 提交任务
```curl
curl --location --request POST 'https://www.runninghub.cn/openapi/v2/rhart-video/ltx-2.3/text-to-video' \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${RUNNINGHUB_API_KEY}" \
--data-raw '{
"prompt": "...",
"resolution": "720p",
"aspectRatio": "16:9",
"duration": 5
}'
```
| 参数 | 类型 | 必填/可选 | 说明 |
| --- | --- | --- | --- |
| `prompt` | String | 必填 | 提示词 |
| `resolution` | String | 必填 | 枚举值: [1080p, 720p, 480p] |
| `aspectRatio` | String | 必填 | 枚举值: [16:9, 9:16] |
| `duration` | Int | 必填 | 输入范围值: 5 - 15 |
### A.2 查询结果
```curl
curl --location --request POST 'https://www.runninghub.cn/openapi/v2/query' \
--header "Content-Type: application/json" \
--header "Authorization: Bearer ${RUNNINGHUB_API_KEY}" \
--data-raw '{"taskId": "${RUNNINGHUB_TASKID}"}'
```
响应字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | String | 任务 ID |
| `status` | String | QUEUED / RUNNING / SUCCESS / FAILED |
| `results` | List | 生成结果列表 |
| `results[].url` | String | 结果 URL24 小时有效) |
| `results[].nodeId` | String | 工作流节点 ID |
| `results[].outputType` | String | 文件扩展名 (png, mp4, txt) |
| `results[].text` | String | 纯文本输出内容 |
### A.3 文件上传
**上传接口:** `POST https://www.runninghub.cn/openapi/v2/media/upload/binary`
支持 `imageUrls`(公共 URL、Base64 Data URI、RH 上传接口三种方式传入图片。

View File

@ -1,51 +0,0 @@
// Flux 2 Dev — 文生图
export default {
name: 'Flux 2',
tag: '文生图',
inputType: 'text',
params: [
{
name: 'prompt',
label: '提示词',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'aspectRatio',
label: '比例',
type: 'select',
default: '1:1',
options: ['1:1', '3:4', '4:3', '9:16', '16:9', '2:3', '3:2', 'custom'],
ui: 'proportion',
},
{
name: 'customWidth',
label: '宽度',
type: 'number',
default: 1024,
min: 256,
max: 1536,
ui: 'number',
showWhen: { aspectRatio: 'custom' },
},
{
name: 'customHight',
label: '高度',
type: 'number',
default: 1024,
min: 256,
max: 1536,
ui: 'number',
showWhen: { aspectRatio: 'custom' },
},
{
name: 'outputFormat',
label: '输出格式',
type: 'string',
default: 'png',
options: ['png', 'jpeg', 'webp(lossless)', 'webp(lossy)'],
ui: 'hidden',
},
],
}

View File

@ -1,48 +0,0 @@
// GPT-Image-2 I2I — 图片编辑
export default {
name: 'GPT-Image-2 I2I',
tag: '图片编辑',
inputType: 'image',
maxImages: 10,
params: [
{
name: 'imageUrls',
label: '参考图片',
type: 'image',
required: true,
ui: 'imageUpload',
maxCount: 10,
},
{
name: 'prompt',
label: '编辑指令',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'aspectRatio',
label: '比例',
type: 'select',
default: '1:1',
options: ['1:1', '1:2', '2:1', '1:3', '3:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '21:9', '9:21', '16:9'],
ui: 'proportion',
},
{
name: 'resolution',
label: '分辨率',
type: 'select',
default: '2k',
options: ['1k', '2k', '4k'],
ui: 'resolution',
},
{
name: 'quality',
label: '画质',
type: 'select',
default: 'medium',
options: ['low', 'medium', 'high'],
ui: 'select',
},
],
}

View File

@ -1,39 +0,0 @@
// GPT-Image-2 — 文生图
export default {
name: 'GPT-Image-2',
tag: '文生图',
inputType: 'text',
params: [
{
name: 'prompt',
label: '提示词',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'aspectRatio',
label: '比例',
type: 'select',
default: '1:1',
options: ['1:1', '1:2', '2:1', '1:3', '3:1', '2:3', '3:2', '3:4', '4:3', '4:5', '5:4', '9:16', '21:9', '9:21', '16:9'],
ui: 'proportion',
},
{
name: 'resolution',
label: '分辨率',
type: 'select',
default: '2k',
options: ['1k', '2k', '4k'],
ui: 'resolution',
},
{
name: 'quality',
label: '画质',
type: 'select',
default: 'medium',
options: ['low', 'medium', 'high'],
ui: 'select',
},
],
}

View File

@ -1,45 +0,0 @@
// 模型配置注册表 — 按模型名称查找参数 schema
import flux from './flux.js'
import zImage from './z-image.js'
import jimeng from './jimeng.js'
import qwen from './qwen.js'
import gptImage from './gpt-image.js'
import nanoPro from './nano-pro.js'
import qwenEdit from './qwen-edit.js'
import gptImageI2i from './gpt-image-i2i.js'
const configs = {
'Flux 2': flux,
'Z-Image Turbo': zImage,
'即梦4.6': jimeng,
'通义万相2.0': qwen,
'GPT-Image-2': gptImage,
'Nano Pro': nanoPro,
'通义万相2.0 Pro': qwenEdit,
'GPT-Image-2 I2i': gptImageI2i,
}
// API display_name → config key 映射API 返回的 display_name 可能与 config 的 name 不同)
const displayNameMap = {
'flux': 'Flux 2',
'Z-image': 'Z-Image Turbo',
'Jimeng4.6': '即梦4.6',
'QwenImage2.0': '通义万相2.0',
'GPT-image-2': 'GPT-Image-2',
'Banana-Pro': 'Nano Pro',
'QwenImage2.0-Pro': '通义万相2.0 Pro',
'GPT-Image-2': 'GPT-Image-2 I2I',
}
/** 根据模型名称获取参数配置,支持 API display_name 和 config key 两种方式查找 */
export function getModelConfig(modelName) {
if (configs[modelName]) return configs[modelName]
const mappedKey = displayNameMap[modelName]
if (mappedKey && configs[mappedKey]) return configs[mappedKey]
return null
}
/** 获取所有模型配置 */
export function getAllModelConfigs() {
return configs
}

View File

@ -1,40 +0,0 @@
// 即梦 4.6 — 文生图(直接指定宽高像素)
export default {
name: '即梦4.6',
tag: '文生图',
inputType: 'text',
params: [
{
name: 'prompt',
label: '提示词',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'width',
label: '宽度',
type: 'number',
default: 1024,
min: 900,
max: 6197,
ui: 'dimensionWidth',
},
{
name: 'height',
label: '高度',
type: 'number',
default: 1024,
min: 768,
max: 4096,
ui: 'dimensionHeight',
},
{
name: 'forceSingle',
label: '强制单张',
type: 'boolean',
default: false,
ui: 'hidden',
},
],
}

View File

@ -1,40 +0,0 @@
// Nano Pro — 图片编辑
export default {
name: 'Nano Pro',
tag: '图片编辑',
inputType: 'image',
maxImages: 10,
params: [
{
name: 'imageUrls',
label: '参考图片',
type: 'image',
required: true,
ui: 'imageUpload',
maxCount: 10,
},
{
name: 'prompt',
label: '编辑指令',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'aspectRatio',
label: '比例',
type: 'select',
default: '1:1',
options: ['1:1', '3:2', '2:3', '3:4', '4:3', '4:5', '5:4', '9:16', '16:9', '21:9'],
ui: 'proportion',
},
{
name: 'resolution',
label: '分辨率',
type: 'select',
default: '2k',
options: ['1k', '2k', '4k'],
ui: 'resolution',
},
],
}

View File

@ -1,48 +0,0 @@
// 通义万相 2.0 Pro — 图片编辑
export default {
name: '通义万相2.0 Pro',
tag: '图片编辑',
inputType: 'image',
maxImages: 3,
params: [
{
name: 'imageUrls',
label: '参考图片',
type: 'image',
required: true,
ui: 'imageUpload',
maxCount: 3,
},
{
name: 'prompt',
label: '提示词',
type: 'string',
default: '',
ui: 'textarea',
},
{
name: 'size',
label: '尺寸',
type: 'string',
default: '1024*1024',
ui: 'dimension',
dimension: {
parse: (val) => {
const parts = (val || '1024*1024').split('*')
return { width: parseInt(parts[0]) || 1024, height: parseInt(parts[1]) || 1024 }
},
format: (w, h) => `${w}*${h}`,
width: { min: 512, max: 2048 },
height: { min: 512, max: 2048 },
},
},
{
name: 'imageNum',
label: '生成张数',
type: 'select',
default: 1,
options: [1, 2, 3, 4, 5, 6],
ui: 'quantity',
},
],
}

View File

@ -1,40 +0,0 @@
// 通义万相 2.0 — 文生图
export default {
name: '通义万相2.0',
tag: '文生图',
inputType: 'text',
maxImages: 6,
params: [
{
name: 'prompt',
label: '提示词',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'size',
label: '尺寸',
type: 'string',
default: '1024*1024',
ui: 'dimension',
dimension: {
parse: (val) => {
const parts = (val || '1024*1024').split('*')
return { width: parseInt(parts[0]) || 1024, height: parseInt(parts[1]) || 1024 }
},
format: (w, h) => `${w}*${h}`,
width: { min: 512, max: 2048 },
height: { min: 512, max: 2048 },
},
},
{
name: 'imageNum',
label: '生成张数',
type: 'select',
default: 1,
options: [1, 2, 3, 4, 5, 6],
ui: 'quantity',
},
],
}

View File

@ -1,31 +0,0 @@
// Z-Image Turbo — 文生图
export default {
name: 'Z-Image Turbo',
tag: '文生图',
inputType: 'text',
params: [
{
name: 'prompt',
label: '提示词',
type: 'string',
required: true,
ui: 'textarea',
},
{
name: 'aspectRatio',
label: '比例',
type: 'select',
default: '1:1',
options: ['1:1', '3:4', '4:3', '9:16', '16:9', '2:3', '3:2'],
ui: 'proportion',
},
{
name: 'outputFormat',
label: '输出格式',
type: 'string',
default: 'png',
options: ['png', 'jpeg', 'webp(lossless)', 'webp(lossy)'],
ui: 'hidden',
},
],
}

View File

@ -1,95 +0,0 @@
const STORAGE_PREFIX = 'model_config_'
function getTodayDateString() {
const today = new Date()
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
}
function getStorageKey(modelName, modelType) {
return `${STORAGE_PREFIX}${modelType}_${modelName}`
}
function getConfigFromStorage(modelName, modelType) {
try {
const key = getStorageKey(modelName, modelType)
const stored = localStorage.getItem(key)
if (!stored) {
return null
}
const data = JSON.parse(stored)
const todayStr = getTodayDateString()
if (data.storageDate !== todayStr) {
localStorage.removeItem(key)
return null
}
return data.config
} catch (error) {
console.error('从localStorage读取配置失败:', error)
return null
}
}
function saveConfigToStorage(modelName, modelType, config) {
try {
const key = getStorageKey(modelName, modelType)
const data = {
config,
storageDate: getTodayDateString(),
timestamp: Date.now()
}
localStorage.setItem(key, JSON.stringify(data))
} catch (error) {
console.error('保存配置到localStorage失败:', error)
}
}
export async function fetchModelConfig(type, modelName, modelType) {
const cachedConfig = getConfigFromStorage(modelName, modelType)
if (cachedConfig) {
console.log(`从缓存加载模型配置: ${modelName}`)
return cachedConfig
}
try {
const url = `${import.meta.env.VITE_API_MODEL_RESOURCE}/static/public/Platform/${type}/workflows/${modelType}/${modelName}.json`
console.log(`从远程获取模型配置: ${url}`)
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const config = await response.json()
saveConfigToStorage(modelName, modelType, config)
return config
} catch (error) {
console.error('获取模型配置失败:', error)
throw error
}
}
export function clearModelConfigCache(modelName, modelType) {
const key = getStorageKey(modelName, modelType)
localStorage.removeItem(key)
}
export function clearAllModelConfigCache() {
const keysToRemove = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(STORAGE_PREFIX)) {
keysToRemove.push(key)
}
}
keysToRemove.forEach(key => localStorage.removeItem(key))
}

View File

@ -0,0 +1,137 @@
# 平台参数管理 — 功能上线说明
## 概述
绘画/视频平台的模型参数配置已从**前端代码硬编码**迁移至**管理后台数据库管理**。新增模型或修改参数无需前端发版,在管理后台配置即可生效。
## 变更内容
### 1. 新增数据库表
`suanli.model_param_configs` — 模型参数配置表
| 字段 | 类型 | 说明 |
|------|------|------|
| `model_id` | CHAR(36) | 关联模型 UUID |
| `platform_id` | CHAR(36) | 关联平台 UUID如 ai_painting_talk / ai_video_talk |
| `input_type` | VARCHAR(32) | text文生图/ image图片编辑 |
| `max_images` | INT | 最大上传图片数0=不支持 |
| `prompt_placeholder` | VARCHAR(500) | 提示词输入框占位文本 |
| `params` | JSON | 参数列表,每项含 name/ui/label/default/options 等 |
唯一约束:`(model_id, platform_id)` — 同一模型在同一平台下仅一份配置。
### 2. 新增后端接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/suanli/v1/admin/model-params` | 列表(分页,支持 `?platform_id=` 筛选) |
| GET | `/suanli/v1/admin/model-params/:id` | 详情 |
| POST | `/suanli/v1/admin/model-params` | 新建 |
| PUT | `/suanli/v1/admin/model-params/:id` | 更新 |
| DELETE | `/suanli/v1/admin/model-params/:id` | 删除 |
| GET | `/suanli/v1/models/:modelId/config` | **公共 API**,供客户端拉取参数 schema |
公共 API 响应示例(含各 ui 类型的完整结构):
```json
{
"code": 0,
"data": {
"inputType": "text",
"maxImages": 0,
"promptPlaceholder": "描述你想生成的画面。",
"params": [
{ "name": "prompt", "ui": "textarea", "label": "提示词", "required": true, "default": "" },
{ "name": "aspectRatio", "ui": "proportion", "label": "比例", "default": "1:1", "options": ["1:1","16:9","9:16","custom"] },
{ "name": "resolution", "ui": "resolution", "label": "分辨率", "default": "2k", "options": ["1k","2k","4k"] },
{
"name": "size", "ui": "dimension", "label": "尺寸", "default": "1024*1024",
"dimension": {
"separator": "*",
"width": { "min": 512, "max": 2048 },
"height": { "min": 512, "max": 2048 }
}
},
{ "name": "quality", "ui": "select", "label": "画质", "default": "medium", "options": ["low","medium","high"] },
{ "name": "imageNum", "ui": "quantity", "label": "生成张数", "default": 1, "options": [1, 2, 3, 4, 5, 6] },
{ "name": "imageUrls", "ui": "imageUpload", "label": "参考图", "required": true, "maxCount": 10 }
]
}
}
```
### 3. 管理后台页面
路径:侧边栏 → **平台参数管理** → **模型参数配置**
- 进入页面后需先选择平台,才会显示该平台下已配置的模型参数
- 新建时:选择平台 → 手动填入模型 UUID从模型管理页复制→ 配置参数 JSON → 保存
- params 字段提供 JSON 编辑器和格式化按钮
## params 字段说明
`ui` 值与前端控件映射:
| ui | 控件 | 必需字段 | 说明 |
|----|------|---------|------|
| `textarea` | 文本输入框 | name, default | 提示词 |
| `proportion` | 比例选择器 | options | `options``custom` 时允许自定义宽高 |
| `resolution` | 分辨率选择器 | options | 与 proportion 共用弹窗 |
| `dimension` | 组合尺寸 | `dimension: { separator, width: {min,max}, height: {min,max} }` | 单字段 `"W*H"` 格式,前端根据 separator 生成 parse/format |
| `dimensionWidth` / `dimensionHeight` | 分离尺寸 | min, max | 两个独立字段,共享比例锁。必须传 min/max 作为输入校验边界 |
| `select` | 下拉选择 | options | 通用 |
| `quantity` | 生成数量 | options**数字数组** | `options` 必须为数字类型 `[1,2,3]`,前端 `Math.max()` 计算上限 |
| `imageUpload` | 图片上传 | maxCount | 控制上传数量上限。顶层 `maxImages` 仅作 fallback |
| `number` | 数字输入 | min, max | 自定义宽高(如 Flux 的 customWidth/customHight支持 `showWhen` 条件显示 |
| `hidden` | 不渲染 | default | 静默写入默认值 |
**`inputType` 枚举值**`text`(文生图)/ `image`(图生图/图片编辑)/ `both`(同时支持,可传图也可不传)
**条件显示**`"showWhen": { "aspectRatio": "custom" }` 使参数仅在比例为自定义时显示。
**字段类型速查**
| 字段 | 类型 | 适用 ui | 前端读取位置 |
|------|------|---------|-------------|
| `name` | string | 全部 | 各处以 `p.name` 查找 |
| `ui` | string | 全部 | `getDimConfig`、controls `show()` |
| `default` | any | 全部 | `syncDefaults` |
| `options` | array | proportion/resolution/select/quantity | 控件 props、`Math.max` |
| `min` / `max` | number | dimensionWidth/dimensionHeight/number | `getDimConfig` 校验边界 |
| `maxCount` | number | imageUpload | `imageUploadLimit` |
| `dimension.separator` | string | dimension | `syncDefaults` 解析 W*H |
| `dimension.width.min/max` | number | dimension | `getDimConfig` 宽度校验 |
| `dimension.height.min/max` | number | dimension | `getDimConfig` 高度校验 |
| `showWhen` | object | 任意 | 条件显示(需前端改造) |
## 已配置数据
`ai_painting_talk`AI绘画new平台下 8 个模型已完成参数配置:
| 模型 | 输入类型 | 参数数 |
|------|---------|--------|
| Flux 2 | text | 5 |
| Z-Image Turbo | text | 3 |
| 即梦4.6 | text | 4 |
| 通义万相2.0 | text | 3 |
| GPT-Image-2 | text | 4 |
| Nano Pro | image | 4 |
| 通义万相2.0 Pro | image | 4 |
| GPT-Image-2 I2I | image | 5 |
`ai_video_talk`AI视频new暂未配置。
## 客户端接入
AI_Painting_V2.0 前端可调用 `GET /suanli/v1/models/{modelId}/config` 获取参数配置,替代当前的本地 JS 硬编码和远程 JSON 拉取。接入后删除:
- `src/platforms/painting/models/` 目录
- `src/utils/modelConfig.js`
- `src/utils/createTask.js`
## 注意事项
- 参数配置**不同步到转发层**,仅用于驱动前端 UI 渲染,不影响任务执行
- 模型 ID 为 UUID 格式,需从「模型管理」页面复制
- 直接操作数据库(手动 SQL 插入/更新)不会触发任何副作用,安全