diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4bb51fb..41d5d9a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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)" ] } } diff --git a/bug.txt b/bug.txt new file mode 100644 index 0000000..e69de29 diff --git a/components.d.ts b/components.d.ts index 6178c47..0ad0509 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md b/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md new file mode 100644 index 0000000..5ce6719 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md @@ -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} 模型配置对象 + */ +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 修复" +``` diff --git a/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md b/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md new file mode 100644 index 0000000..7458e43 --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md @@ -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)`: + +- 优先读 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()`:逻辑不变 diff --git a/docs/模型参数后端化方案.md b/docs/模型参数后端化方案.md deleted file mode 100644 index ff66fc9..0000000 --- a/docs/模型参数后端化方案.md +++ /dev/null @@ -1,282 +0,0 @@ -# 模型参数后端化方案 - -## 一、当前架构(平台重构后) - -平台重构已采用 **Platform Descriptor 模式**,Painting 和 Video 统一走扁平 `modelParams`: - -``` -src/platforms/ -├── registry.js # 注册表:registerPlatform() + createPlatform() -├── painting/ -│ ├── index.js # Painting descriptor(controls, 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 | 结果 URL(24 小时有效) | -| `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 上传接口三种方式传入图片。 diff --git a/src/platforms/painting/models/flux.js b/src/platforms/painting/models/flux.js deleted file mode 100644 index 4c41f27..0000000 --- a/src/platforms/painting/models/flux.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/gpt-image-i2i.js b/src/platforms/painting/models/gpt-image-i2i.js deleted file mode 100644 index 743b659..0000000 --- a/src/platforms/painting/models/gpt-image-i2i.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/gpt-image.js b/src/platforms/painting/models/gpt-image.js deleted file mode 100644 index 46b73dc..0000000 --- a/src/platforms/painting/models/gpt-image.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/index.js b/src/platforms/painting/models/index.js deleted file mode 100644 index e775b9c..0000000 --- a/src/platforms/painting/models/index.js +++ /dev/null @@ -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 -} diff --git a/src/platforms/painting/models/jimeng.js b/src/platforms/painting/models/jimeng.js deleted file mode 100644 index 042c0b3..0000000 --- a/src/platforms/painting/models/jimeng.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/nano-pro.js b/src/platforms/painting/models/nano-pro.js deleted file mode 100644 index 42e7052..0000000 --- a/src/platforms/painting/models/nano-pro.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/qwen-edit.js b/src/platforms/painting/models/qwen-edit.js deleted file mode 100644 index 0d87bde..0000000 --- a/src/platforms/painting/models/qwen-edit.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/qwen.js b/src/platforms/painting/models/qwen.js deleted file mode 100644 index da2c342..0000000 --- a/src/platforms/painting/models/qwen.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/platforms/painting/models/z-image.js b/src/platforms/painting/models/z-image.js deleted file mode 100644 index e5f7fc5..0000000 --- a/src/platforms/painting/models/z-image.js +++ /dev/null @@ -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', - }, - ], -} diff --git a/src/utils/modelConfig.js b/src/utils/modelConfig.js deleted file mode 100644 index 67d717b..0000000 --- a/src/utils/modelConfig.js +++ /dev/null @@ -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)) -} diff --git a/平台参数管理-上线说明.md b/平台参数管理-上线说明.md new file mode 100644 index 0000000..ad1d17b --- /dev/null +++ b/平台参数管理-上线说明.md @@ -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 插入/更新)不会触发任何副作用,安全