From 0eee8b1f7f49afd15a25b763d7e05de8f9535410 Mon Sep 17 00:00:00 2001 From: WangLeo <690854599@qq.com> Date: Tue, 9 Jun 2026 18:28:40 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=B0=86=20docs/=20=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=20.gitignore=EF=BC=8C=E4=BB=8E=E7=89=88=E6=9C=AC=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E4=B8=AD=E7=A7=BB=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../2026-06-09-platform-architecture-plan.md | 1211 ----------------- .../2026-06-09-模型参数后端化-前端适配.md | 735 ---------- ...2026-06-09-platform-architecture-design.md | 187 --- ...26-06-09-模型参数后端化-前端适配-design.md | 147 -- 5 files changed, 1 insertion(+), 2280 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-09-platform-architecture-plan.md delete mode 100644 docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md delete mode 100644 docs/superpowers/specs/2026-06-09-platform-architecture-design.md delete mode 100644 docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md diff --git a/.gitignore b/.gitignore index e74dd37..897d507 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? TEST/ +docs/ diff --git a/docs/superpowers/plans/2026-06-09-platform-architecture-plan.md b/docs/superpowers/plans/2026-06-09-platform-architecture-plan.md deleted file mode 100644 index 09d9709..0000000 --- a/docs/superpowers/plans/2026-06-09-platform-architecture-plan.md +++ /dev/null @@ -1,1211 +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:** 将 dialogBox 中的 Painting/Video 硬分支重构为平台描述符模式,新平台只需新建文件夹实现标准接口。 - -**Architecture:** 每个平台导出 `definePlatform()` 工厂函数,返回 ModelSelector 组件 + controls 列表 + buildTaskBody 等标准接口。dialogBox 退化为纯渲染引擎,通过 `createPlatform(type)` 获取 descriptor 并委托所有平台特定逻辑。 - -**Tech Stack:** Vue 3 Composition API + Pinia + Vite - ---- - -### Task 1: 创建平台注册表和基础设施 - -**Files:** -- Create: `src/platforms/registry.js` - -- [ ] **Step 1: 创建 registry.js** - -```js -// src/platforms/registry.js - -/** 平台注册表:id → definePlatform 工厂函数 */ -const registry = {} - -/** 注册平台 */ -export function registerPlatform(id, factory) { - registry[id] = factory -} - -/** 根据平台类型创建平台实例 */ -export function createPlatform(type) { - const factory = registry[type] - if (!factory) throw new Error(`未注册的平台: ${type}`) - return factory() -} - -/** 获取所有已注册平台 ID */ -export function getRegisteredPlatforms() { - return Object.keys(registry) -} -``` - -- [ ] **Step 2: 验证文件无语法错误** - -Run: `node -e "import('./src/platforms/registry.js').then(m => console.log('OK'))"` 或在 Vite dev 下验证 - -- [ ] **Step 3: 提交** - -```bash -git add src/platforms/registry.js -git commit -m "feat: 新增平台注册表基础设施" -``` - ---- - -### Task 2: 创建 Painting 平台包 - -**Files:** -- Create: `src/platforms/painting/index.js` -- Create: `src/platforms/painting/modelSelector.vue` -- Create: `src/platforms/painting/controls/proportion.vue` -- Create: `src/platforms/painting/controls/dimension.vue` -- Create: `src/platforms/painting/controls/quality.vue` -- Create: `src/platforms/painting/controls/quantity.vue` -- Create: `src/platforms/painting/imageUploader.vue` - -- [ ] **Step 1: 迁移 modelSelector(从 `dialogBox/model/painting.vue`)** - -Copy `src/components/dialogBox/model/painting.vue` → `src/platforms/painting/modelSelector.vue`,无需修改内容。 - -- [ ] **Step 2: 迁移 proportion(从 `dialogBox/proportion/painting.vue`)** - -Copy `src/components/dialogBox/proportion/painting.vue` → `src/platforms/painting/controls/proportion.vue`,无需修改内容。 - -- [ ] **Step 3: 迁移 dimension(从 `dialogBox/dimension/index.vue`)** - -Copy `src/components/dialogBox/dimension/index.vue` → `src/platforms/painting/controls/dimension.vue`,无需修改内容。 - -- [ ] **Step 4: 提取 quality 为独立组件(原 dialogBox inline)** - -新建 `src/platforms/painting/controls/quality.vue`: - -```vue - - - - - -``` - -- [ ] **Step 5: 迁移 quantity(从 `dialogBox/quantity/index.vue`)** - -Copy `src/components/dialogBox/quantity/index.vue` → `src/platforms/painting/controls/quantity.vue`,无需修改内容。 - -- [ ] **Step 6: 迁移 imageUploader(从 `dialogBox/imageUploader/index.vue`)** - -Copy `src/components/dialogBox/imageUploader/index.vue` → `src/platforms/painting/imageUploader.vue`,无需修改内容。 - -- [ ] **Step 7: 创建 Painting descriptor** - -新建 `src/platforms/painting/index.js`: - -```js -import { ref, reactive, computed, markRaw } from 'vue' -import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi' -import { getModelConfig } from '@/config/models/index.js' -import PaintingModelSelector from './modelSelector.vue' -import PaintingProportion from './controls/proportion.vue' -import DimensionInput from './controls/dimension.vue' -import QualitySelect from './controls/quality.vue' -import Quantity from './controls/quantity.vue' -import ImageUploader from './imageUploader.vue' -import { registerPlatform } from '../registry.js' - -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 -} - -export function definePaintingPlatform() { - const model = ref('Flux 2') - const modelType = ref('text') - const proportion = ref('1:1') - const resolution = ref('2k') - const customWidth = ref(1024) - const customHight = ref(1024) - const dimWidth = ref(1024) - const dimHeight = ref(1024) - const quantity = ref(1) - const quality = ref('medium') - const modelConfig = ref(null) - const promptPlaceholder = ref('描述你想生成的画面和动作。') - const paramValues = reactive({}) - - const state = { - model, modelType, - proportion, resolution, - customWidth, customHight, - dimWidth, dimHeight, - quantity, quality, - paramValues, modelConfig, - } - - function syncDefaults(config) { - modelConfig.value = config - if (!config) return - config.params.forEach(p => { - if (!(p.name in paramValues)) { - paramValues[p.name] = p.default ?? (p.name === 'outputFormat' ? 'png' : '') - } - }) - 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' - 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 - } - } - - // 同步 UI refs → paramValues(由 dialogBox 在合适的时机调用) - function syncParamValues() { - const ratioParam = modelConfig.value?.params?.find(p => p.ui === 'proportion') - if (ratioParam) paramValues[ratioParam.name] = proportion.value - const resParam = modelConfig.value?.params?.find(p => p.ui === 'resolution') - if (resParam) paramValues[resParam.name] = resolution.value - const qtyParam = modelConfig.value?.params?.find(p => p.ui === 'quantity') - if (qtyParam) paramValues[qtyParam.name] = quantity.value - if (modelConfig.value?.params?.find(p => p.name === 'customWidth')) { - paramValues.customWidth = customWidth.value - } - if (modelConfig.value?.params?.find(p => p.name === 'customHight')) { - paramValues.customHight = customHight.value - } - if (modelConfig.value?.params?.find(p => p.name === 'quality')) { - paramValues.quality = quality.value - } - const dc = getDimConfig(modelConfig.value) - 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) - } - } - - const controls = [ - { - name: 'proportion', - component: markRaw(PaintingProportion), - show: (config) => !!config?.params?.find(p => p.ui === 'proportion'), - props: (config) => { - const ratioParam = config?.params?.find(p => p.ui === 'proportion') - const resParam = config?.params?.find(p => p.ui === 'resolution') - return { - modelValue: proportion.value, - 'onUpdate:modelValue': (v) => { proportion.value = v }, - resolution: resolution.value, - 'onUpdate:resolution': (v) => { resolution.value = v }, - width: customWidth.value, - 'onUpdate:width': (v) => { customWidth.value = v }, - height: customHight.value, - 'onUpdate:height': (v) => { customHight.value = v }, - proportionOptions: ratioParam?.options - ?.filter(o => o !== 'custom') - .map(o => ({ value: o, label: o })) || [], - resolutionOptions: resParam?.options - ?.map(o => ({ value: o, label: o.toUpperCase() })) || [], - allowCustom: ratioParam?.options?.includes('custom') || false, - } - }, - }, - { - name: 'dimension', - component: markRaw(DimensionInput), - show: (config) => !!config?.params?.find(p => p.ui === 'dimension' || p.ui === 'dimensionWidth'), - props: (config) => { - const dc = getDimConfig(config) - return { - width: dimWidth.value, - 'onUpdate:width': (v) => { dimWidth.value = v }, - height: dimHeight.value, - 'onUpdate:height': (v) => { dimHeight.value = v }, - minW: dc?.config?.width?.min || dc?.wParam?.min || 256, - maxW: dc?.config?.width?.max || dc?.wParam?.max || 6197, - minH: dc?.config?.height?.min || dc?.hParam?.min || 256, - maxH: dc?.config?.height?.max || dc?.hParam?.max || 4096, - } - }, - }, - { - name: 'quality', - component: markRaw(QualitySelect), - show: (config) => !!config?.params?.find(p => p.name === 'quality'), - props: (config) => { - const q = config?.params?.find(p => p.name === 'quality') - return { - modelValue: quality.value, - 'onUpdate:modelValue': (v) => { quality.value = v }, - options: q?.options?.map(o => ({ value: o, label: o })) || [], - } - }, - }, - { - name: 'quantity', - component: markRaw(Quantity), - show: (config) => !!config?.params?.find(p => p.ui === 'quantity'), - props: (config) => { - const qtyParam = config?.params?.find(p => p.ui === 'quantity') - return { - modelValue: quantity.value, - 'onUpdate:modelValue': (v) => { quantity.value = v }, - max: qtyParam?.options?.length ? Math.max(...qtyParam.options) : 4, - } - }, - }, - ] - - const platform = { - id: 'painting', - label: 'AI绘画2026', - ModelSelector: markRaw(PaintingModelSelector), - modelSelectorProps: null, - controls, - ImageUploader: markRaw(ImageUploader), - state, - model, - modelType, - modelConfig, - promptPlaceholder, - - async loadModels() { - const code = getPlatformCode('Painting') - return fetchPlatformModels(code) - }, - - async loadConfig(modelName) { - const config = getModelConfig(modelName) - syncDefaults(config) - return config - }, - - getDefaultModel() { - return 'Flux 2' - }, - - showImageUploader() { - if (modelType.value !== 'text') return true - return modelConfig.value?.inputType === 'image' || modelConfig.value?.inputType === 'both' - }, - - imageUploadLimit() { - if (!modelConfig.value) return 4 - const imageParam = modelConfig.value.params.find(p => p.ui === 'imageUpload') - return imageParam?.maxCount || modelConfig.value.maxImages || 4 - }, - - isImageRequired() { - return !!(modelConfig.value?.params?.find(p => p.ui === 'imageUpload')) - }, - - buildTaskBody(shared) { - syncParamValues() - const modelParams = { ...paramValues } - if (shared.prompt.value) modelParams.prompt = shared.prompt.value - return modelParams - }, - - // 从历史记录恢复参数到 UI state - fillFromResult(resultData) { - if (resultData.model !== undefined) model.value = resultData.model - if (resultData.modelType !== undefined) modelType.value = resultData.modelType - if (resultData.proportion !== undefined) proportion.value = resultData.proportion - if (resultData.resolution !== undefined) resolution.value = resultData.resolution - if (resultData.customWidth !== undefined) customWidth.value = resultData.customWidth - if (resultData.customHight !== undefined) customHight.value = resultData.customHight - if (resultData.quantity !== undefined) quantity.value = resultData.quantity - if (resultData.modelParams !== undefined) Object.assign(paramValues, resultData.modelParams) - // 从恢复的 modelParams 同步 dimension/quality UI refs - const dc = getDimConfig(modelConfig.value) - if (dc?.type === 'split') { - if (paramValues[dc.wParam.name] !== undefined) dimWidth.value = paramValues[dc.wParam.name] - if (paramValues[dc.hParam.name] !== undefined) dimHeight.value = paramValues[dc.hParam.name] - } else if (dc?.type === 'combined') { - if (paramValues[dc.paramName]) { - const parsed = dc.config.parse(paramValues[dc.paramName]) - dimWidth.value = parsed.width - dimHeight.value = parsed.height - } - } - if (paramValues.quality !== undefined) quality.value = paramValues.quality - }, - } - - return platform -} - -// 自注册 -registerPlatform('Painting', definePaintingPlatform) -``` - -- [ ] **Step 8: 提交** - -```bash -git add src/platforms/painting/ src/platforms/registry.js -git commit -m "feat: 新增 Painting 平台包(descriptor + 控件迁移)" -``` - ---- - -### Task 3: 创建 Video 平台包 - -**Files:** -- Create: `src/platforms/video/index.js` -- Create: `src/platforms/video/modelSelector.vue` -- Create: `src/platforms/video/controls/pattern.vue` -- Create: `src/platforms/video/controls/proportion.vue` -- Create: `src/platforms/video/controls/time.vue` -- Create: `src/platforms/video/imageUploader.vue` - -- [ ] **Step 1: 迁移 modelSelector(从 `dialogBox/model/video.vue`)** - -Copy `src/components/dialogBox/model/video.vue` → `src/platforms/video/modelSelector.vue`,无需修改内容。 - -- [ ] **Step 2: 迁移 pattern(从 `dialogBox/pattern/index.vue`)** - -Copy `src/components/dialogBox/pattern/index.vue` → `src/platforms/video/controls/pattern.vue`,无需修改内容。 - -- [ ] **Step 3: 迁移 proportion(从 `dialogBox/proportion/video.vue`)** - -Copy `src/components/dialogBox/proportion/video.vue` → `src/platforms/video/controls/proportion.vue`,无需修改内容。 - -- [ ] **Step 4: 迁移 time(从 `dialogBox/Time/index.vue`)** - -Copy `src/components/dialogBox/Time/index.vue` → `src/platforms/video/controls/time.vue`,无需修改内容。 - -- [ ] **Step 5: 迁移 imageUploader(从 `dialogBox/videoImageUploader/index.vue`)** - -Copy `src/components/dialogBox/videoImageUploader/index.vue` → `src/platforms/video/imageUploader.vue`,无需修改内容。 - -- [ ] **Step 6: 创建 Video descriptor** - -新建 `src/platforms/video/index.js`: - -```js -import { ref, reactive, markRaw } from 'vue' -import { fetchModelConfig } from '@/utils/modelConfig' -import { fetchPlatformModels, getPlatformCode } from '@/utils/modelApi' -import VideoModelSelector from './modelSelector.vue' -import Pattern from './controls/pattern.vue' -import VideoProportion from './controls/proportion.vue' -import Time from './controls/time.vue' -import VideoImageUploader from './imageUploader.vue' -import { registerPlatform } from '../registry.js' - -export function defineVideoPlatform() { - const model = ref('LTX2.0') - const modelType = ref('text') - const proportion = ref('16:9') - const resolution = ref('1k') - const duration = ref(5) - const videoPattern = ref('文生视频') - - const resolutionOptions = ref([ - { value: '1k', label: '标清 1K' }, - { value: '2k', label: '高清 2K' }, - { value: '4k', label: '超清 4K' }, - ]) - const proportionOptions = ref([ - { value: '智能', label: '智能' }, - { value: '21:9', label: '21:9' }, - { value: '16:9', label: '16:9' }, - { value: '4:3', label: '4:3' }, - { value: '1:1', label: '1:1' }, - { value: '3:4', label: '3:4' }, - { value: '9:16', label: '9:16' }, - ]) - const durationOptions = ref([]) - const modelDisplayConfig = ref(null) - const promptPlaceholder = ref('描述你想生成的画面和动作。') - - const state = { - model, modelType, - proportion, resolution, - duration, videoPattern, - resolutionOptions, proportionOptions, durationOptions, - modelDisplayConfig, - } - - async function loadConfig(modelName, modelTypeVal) { - const config = await fetchModelConfig('Video', modelName, modelTypeVal) - modelDisplayConfig.value = config - if (config?.display) { - const d = config.display - if (d.resolution) { - resolution.value = d.resolution.default || '1k' - resolutionOptions.value = d.resolution.options || [] - } - if (d.proportion) { - proportion.value = d.proportion.default || '16:9' - proportionOptions.value = d.proportion.options || [] - } - if (d.duration) { - duration.value = d.duration.default || 5 - durationOptions.value = d.duration.options || [] - } - if (d.promptPlaceholder) { - promptPlaceholder.value = d.promptPlaceholder.default || '描述你想生成的画面和动作。' - } - } - return config - } - - const controls = [ - { - name: 'pattern', - component: markRaw(Pattern), - show: () => true, - props: () => ({ - modelValue: videoPattern.value, - 'onUpdate:modelValue': (v) => { videoPattern.value = v }, - }), - }, - { - name: 'proportion', - component: markRaw(VideoProportion), - show: () => true, - props: () => ({ - modelValue: proportion.value, - 'onUpdate:modelValue': (v) => { proportion.value = v }, - resolution: resolution.value, - 'onUpdate:resolution': (v) => { resolution.value = v }, - proportionOptions: proportionOptions.value, - resolutionOptions: resolutionOptions.value, - }), - }, - { - name: 'time', - component: markRaw(Time), - show: () => true, - props: () => ({ - modelValue: duration.value, - 'onUpdate:modelValue': (v) => { duration.value = v }, - options: durationOptions.value, - }), - }, - ] - - const platform = { - id: 'video', - label: 'AI视频2026', - ModelSelector: markRaw(VideoModelSelector), - modelSelectorProps: () => ({ videoPattern: videoPattern.value }), - controls, - ImageUploader: markRaw(VideoImageUploader), - state, - model, - modelType, - modelDisplayConfig, - promptPlaceholder, - - async loadModels() { - const code = getPlatformCode('Video') - return fetchPlatformModels(code) - }, - - async loadConfig(modelName, modelTypeVal) { - return loadConfig(modelName, modelTypeVal) - }, - - getDefaultModel() { - return 'LTX2.0' - }, - - showImageUploader() { - return modelType.value !== 'text' - }, - - imageUploadLimit() { - return modelDisplayConfig.value?.display?.images || 1 - }, - - isImageRequired() { - return modelType.value !== 'text' - }, - - buildTaskBody(shared) { - // 与 Painting 统一:直接返回扁平参数 - const modelParams = { - prompt: shared.prompt.value, - proportion: proportion.value, - resolution: resolution.value, - duration: duration.value, - videoPattern: videoPattern.value, - } - return modelParams - }, - - fillFromResult(resultData) { - if (resultData.model !== undefined) model.value = resultData.model - if (resultData.modelType !== undefined) modelType.value = resultData.modelType - if (resultData.proportion !== undefined) proportion.value = resultData.proportion - if (resultData.resolution !== undefined) resolution.value = resultData.resolution - if (resultData.duration !== undefined) duration.value = resultData.duration - if (resultData.videoPattern !== undefined) videoPattern.value = resultData.videoPattern - }, - } - - return platform -} - -registerPlatform('Video', defineVideoPlatform) -``` - -- [ ] **Step 7: 提交** - -```bash -git add src/platforms/video/ -git commit -m "feat: 新增 Video 平台包(descriptor + 控件迁移)" -``` - ---- - -### Task 4: 重写 dialogBox 为通用编排壳 - -**Files:** -- Modify: `src/components/dialogBox/index.vue`(重写) -- Modify: `src/utils/createTask.js` - -- [ ] **Step 1: 简化 createTask.js 为纯透传** - -修改 `src/utils/createTask.js`: - -```js -// src/utils/createTask.js -// 所有平台 descriptor 的 buildTaskBody() 已直接返回扁平 modelParams, -// 此文件仅做透传,后续可直接移除 -export async function createTask(data) { - return data.body -} -``` - -- [ ] **Step 2: 重写 dialogBox/index.vue** - -用以下内容完整替换 `src/components/dialogBox/index.vue`: - -```vue - - - - - -``` - -- [ ] **Step 3: 验证 dialogBox 的 import 路径正确** - -检查所有 import 路径是否存在: -- `@/platforms/registry.js` → `src/platforms/registry.js` -- `@/platforms/painting/index.js` → `src/platforms/painting/index.js` -- `@/platforms/video/index.js` → `src/platforms/video/index.js` - -- [ ] **Step 4: 提交** - -```bash -git add src/components/dialogBox/index.vue src/utils/createTask.js -git commit -m "refactor: dialogBox 重构为通用平台编排壳,委托所有平台特定逻辑" -``` - ---- - -### Task 5: 删除旧代码 - -**Files:** -- Delete: `src/config/models/` 目录 -- Delete: `src/config/runninghub/` 目录 -- Delete: `src/utils/modelConfig.js` -- Delete: `src/components/dialogBox/model/painting.vue` -- Delete: `src/components/dialogBox/model/video.vue` -- Delete: `src/components/dialogBox/proportion/painting.vue` -- Delete: `src/components/dialogBox/proportion/video.vue` -- Delete: `src/components/dialogBox/dimension/index.vue` -- Delete: `src/components/dialogBox/quantity/index.vue` -- Delete: `src/components/dialogBox/pattern/index.vue` -- Delete: `src/components/dialogBox/Time/index.vue` -- Delete: `src/components/dialogBox/imageUploader/index.vue` -- Delete: `src/components/dialogBox/videoImageUploader/index.vue` -- Modify: `src/config/index.js` - -- [ ] **Step 1: 删除 dialogBox 下的旧平台组件** - -```bash -rm -rf src/components/dialogBox/model/ -rm -rf src/components/dialogBox/proportion/ -rm -rf src/components/dialogBox/dimension/ -rm -rf src/components/dialogBox/quantity/ -rm -rf src/components/dialogBox/pattern/ -rm -rf src/components/dialogBox/Time/ -rm -rf src/components/dialogBox/imageUploader/ -rm -rf src/components/dialogBox/videoImageUploader/ -``` - -- [ ] **Step 2: 删除旧 config 目录** - -```bash -rm -rf src/config/models/ -rm -rf src/config/runninghub/ -rm src/config/index.js -# 注意:modelConfig.js 暂时保留,Video descriptor 用 fetchModelConfig() 获取 display 配置驱动 UI, -# 待 Video 后端化(API 返回参数 schema)后可删除 -``` - -- [ ] **Step 3: 删除 config/index.js(不再被引用)** - -```bash -rm src/config/index.js -``` - -原来 `config/index.js` 仅导出 runninghub 适配器供 `createTask.js` 使用,现在 createTask 已简化,此文件无用。 - -- [ ] **Step 4: 搜索并修复残留引用** - -```bash -grep -r "config/models" src/ --include="*.js" --include="*.vue" -grep -r "components/dialogBox/model" src/ --include="*.js" --include="*.vue" -grep -r "components/dialogBox/proportion" src/ --include="*.js" --include="*.vue" -grep -r "utils/modelConfig" src/ --include="*.js" --include="*.vue" -``` - -修复任何残留引用,更新为新的 import 路径。 - -- [ ] **Step 5: 提交** - -```bash -git add -A -git commit -m "chore: 删除旧架构代码(config/models、runninghub、dialogBox 旧组件)" -``` - ---- - -### Task 6: 验证构建与运行 - -- [ ] **Step 1: 启动开发服务器** - -```bash -pnpm dev -``` - -- [ ] **Step 2: 检查 Painting 流程** - -1. 打开首页 → 确认标题显示 "AI绘画2026" -2. 确认模型选择器显示模型列表(从 API 加载) -3. 切换模型 → 确认比例/分辨率/尺寸等控件正确显示/隐藏 -4. 输入 prompt → 点击发送 → 确认请求发出 -5. 确认生成结果在虚拟滚动列表中显示 - -- [ ] **Step 3: 检查 Video 流程** - -1. 切换到 Video 模式 → 确认标题显示 "AI视频2026" -2. 确认 Pattern 选择器 + 模型选择器正常工作 -3. 切换 Pattern → 确认模型列表更新 -4. 输入 prompt → 点击发送 → 确认请求发出 - -- [ ] **Step 4: 检查平台切换** - -1. 从 Painting 切换到 Video → 确认所有控件正确替换 -2. 从 Video 切换到 Painting → 确认控件恢复 - -- [ ] **Step 5: 检查历史回填** - -1. 点击历史记录中的某项 → 确认参数正确回填到当前平台控件 - -- [ ] **Step 6: 修复发现的问题并提交** - -```bash -git add -A -git commit -m "fix: 验证后修复平台切换和历史回填问题" -``` - ---- - -### Task 7: 审查后端化方案文档 - -- [ ] **Step 1: 阅读当前后端化方案** - -读取 `docs/模型参数后端化方案.md` - -- [ ] **Step 2: 对照新架构,指出文档需要变更的部分** - -新架构下,各平台的 `loadConfig()` 在 descriptor 内实现,后端化只需改变 descriptor 的内部实现: - -1. **统一 API 端点**:所有平台通过同一 API 获取模型列表和参数 schema,不再有 Painting 本地 JS / Video 远程 JSON 的差异 -2. **参数元数据格式**:后端返回的模型参数需要包含 `ui` 字段(当前 Painting 在本地 config 中定义),以便 descriptor 的 `show()` 和 `props()` 正确工作 -3. **文档需补充**:后端 API 返回的模型参数 schema 格式约定(每个参数需包含 `name`、`type`、`ui`、`default`、`options`、`min/max` 等字段) -4. **不再需要的内容**:文档中 RunningHub 原始 API 的 workflow/seed/width/height 等概念,在新架构中这些由 descriptor 内部消化,不再暴露给 API 调用方 - -- [ ] **Step 3: 将审查结论写入计划备注或直接修改 doc** - ---- - -### 完成标准 - -- [ ] `pnpm build` 无错误 -- [ ] Painting 流程:模型选择 → 参数调整 → 发送 → 结果展示 -- [ ] Video 流程:Pattern → 模型选择 → 参数调整 → 发送 → 结果展示 -- [ ] 平台切换:控件正确替换,无残留状态 -- [ ] 历史回填:参数正确恢复到控件 -- [ ] 新增平台只需 3 步:新建文件夹 → 实现 descriptor → registry 注册一行 diff --git a/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md b/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md deleted file mode 100644 index 5ce6719..0000000 --- a/docs/superpowers/plans/2026-06-09-模型参数后端化-前端适配.md +++ /dev/null @@ -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} 模型配置对象 - */ -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-platform-architecture-design.md b/docs/superpowers/specs/2026-06-09-platform-architecture-design.md deleted file mode 100644 index 2bc60ac..0000000 --- a/docs/superpowers/specs/2026-06-09-platform-architecture-design.md +++ /dev/null @@ -1,187 +0,0 @@ -# 平台化架构设计 - -## 目标 - -将 Painting / Video 两套硬编码分支重构为统一的平台描述符架构,使加入新平台只需新建一个文件夹并实现标准接口,零改动 dialogBox。 - -## 背景 - -当前 `dialogBox/index.vue`(792 行)通过 `v-if="type === 'Painting'"` / `v-if="type === 'Video'"` 承载两套完全不同的逻辑: - -- **模型列表**:Painting 走后端 API,Video 走静态 JSON -- **参数 schema**:Painting 走本地 JS 文件,Video 走远程 workflow JSON -- **UI 控件**:Painting 有 proportion/dimension/quality/quantity,Video 有 pattern/proportion/time -- **任务 body**:Painting 扁平 modelParams,Video `{ workflowId, nodeInfoList }` - -后续模型参数后端化后,所有平台将统一为"API 获取模型列表 + API 获取参数 schema"模式。 - -## 核心设计:平台描述符模式 - -每个平台封装为一个文件夹,导出 `definePlatform()` 工厂函数,返回标准接口对象。dialogBox 退化为纯渲染引擎。 - -### 平台接口 - -```js -// src/platforms//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. 渲染 `` -2. 遍历 `platform.controls`,`show` 返回 true 的渲染 `` -3. `handleStart()` 委托给 `platform.buildTaskBody(state)` → 调用 `taskPolling.generate(body)` -4. `watch(model)` 委托给 `platform.loadConfig(name)` - -## 数据流 - -```mermaid -flowchart TD - subgraph dialogBox["dialogBox 编排层"] - A["platform.loadModels()"] --> B[模型选择器渲染] - B --> C["watch(model) → platform.loadConfig(name)"] - C --> D[计算 visibleControls] - D --> E["v-for 渲染 controls(自动 v-model)"] - E --> F["handleStart → platform.buildTaskBody()"] - F --> G["taskPolling.generate(body)"] - end - - subgraph platform["platform 包"] - H["loadModels() → API"] - I["loadConfig(name) → API"] - J["buildTaskBody() → 扁平 body"] - end - - G --> K[POST /suanli/v1/tasks] - K --> L[轮询 → displayStore 更新虚拟滚动列表] -``` - -## 目录结构 - -``` -src/ -├── platforms/ # 平台包(新增) -│ ├── painting/ -│ │ ├── index.js # definePlatform() -│ │ ├── modelSelector.vue -│ │ ├── imageUploader.vue -│ │ └── controls/ -│ │ ├── proportion.vue -│ │ ├── dimension.vue -│ │ ├── quality.vue -│ │ └── quantity.vue -│ ├── video/ -│ │ ├── index.js -│ │ ├── modelSelector.vue -│ │ ├── imageUploader.vue -│ │ └── controls/ -│ │ ├── pattern.vue -│ │ ├── proportion.vue -│ │ └── time.vue -│ └── registry.js -│ -├── components/ -│ ├── dialogBox/index.vue # 精简后 ~200 行 -│ ├── Popover/ # 共享基础组件(不变) -│ ├── Select/ # 共享基础组件(不变) -│ ├── Img/ # 共享基础组件(不变) -│ └── virtual-scroller/ # 共享基础组件(不变) -│ -├── apis/ # API 层(不变) -├── utils/ -│ ├── taskPolling.js # 任务轮询(不变) -│ ├── request.js # Axios(不变) -│ └── modelApi.js # 平台 API 封装 -│ -├── config/ -│ ├── models/ # 逐步废弃 -│ ├── runninghub/ # 逐步废弃 -│ └── plugins.js # 不变 -``` - -### 迁移路径 - -| 步骤 | 内容 | 影响范围 | -|------|------|----------| -| 1 | 新建 `src/platforms/` + `registry.js`,不删旧代码 | 纯新增 | -| 2 | Painting 迁入 descriptor,dialogBox 切换读取路径 | dialogBox 精简 | -| 3 | Video 迁入 descriptor | dialogBox 继续精简 | -| 4 | 删除旧代码:`config/models/`、`config/runninghub/`、`modelConfig.js` | 清理 | -| 5 | 后端化:各平台 `loadConfig()` 改为调 API | 仅改 descriptor 内部 | - -每步独立提交,方便回滚。 - -## 不变更的部分 - -- `taskPolling.js`:任务创建和轮询逻辑通用,不变 -- `displayStore`:虚拟滚动列表状态通用,不变 -- `Popover`、`Select`、`Img`、`virtual-scroller`:共享 UI 组件 -- `home/index.vue`:仍然传 `type` prop,不变 -- `apis/`、`request.js`:HTTP 层不变 -- `config/plugins.js`、router、stores:不变 diff --git a/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md b/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md deleted file mode 100644 index 7458e43..0000000 --- a/docs/superpowers/specs/2026-06-09-模型参数后端化-前端适配-design.md +++ /dev/null @@ -1,147 +0,0 @@ -# 模型参数后端化 — 前端适配设计 - -## 概述 - -将 Painting/Video 平台的模型参数配置从**前端代码硬编码**迁移至**后端 API 获取**,实现新增模型或修改参数无需前端发版。 - -方案 A:最小改动,只替换配置来源,保持现有架构不变。 - -## 一、数据流 - -``` -页面加载 → platform.loadModels() - → fetchPlatformModels(code) // 已有 - → 提取所有 modelId - → POST /suanli/v1/models/configs // 新增,批量获取 - → 返回 { "uuid1": { config }, ... } - → 逐条写入 60s localStorage 缓存 - -用户切换模型 → platform.loadConfig(modelName) - → getModelId(type, modelName) // 已有 - → getModelConfig(modelId) // 新增,优先缓存 → fallback 单条 API - → syncDefaults(config) - → modelConfig.value = config - → 遍历 params 初始化 paramValues + 专用 ref - → dimension.separator → 生成 parse/format - → promptPlaceholder 同步 - → visibleControls 更新(含 showWhen 条件判断) - → 用户填写参数 → buildTaskBody() → 扁平 modelParams → POST 创建任务 -``` - -## 二、API 层 - -### 新增 API 函数(`src/apis/display/index.js`) - -```js -// 批量获取模型配置 -export function requestModelConfigsBatch(modelIds) { - return service.post('/suanli/v1/models/configs', { modelIds }) -} - -// 单条查询(缓存未命中 fallback) -export function requestModelConfig(modelId) { - return service.get(`/suanli/v1/models/${modelId}/config`) -} -``` - -### 缓存层(`src/utils/modelApi.js`) - -新增 `getModelConfig(modelId)`: - -- 优先读 localStorage(key: `model_config_{modelId}`,TTL 60 秒) -- 未命中调 `requestModelConfig()` + 写入缓存 -- `pendingRequests` Map 并发去重 - -新增 `preloadModelConfigs(modelIds)`: - -- 调用 `requestModelConfigsBatch(modelIds)` -- 逐条写入 localStorage 缓存 - -## 三、共享工具函数 - -新建 `src/utils/modelConfigHelper.js`: - -| 导出函数 | 说明 | -|---------|------| -| `syncDefaults(config, state)` | params → paramValues + 专用 ref(proportion/resolution/quantity/dimension/quality) | -| `syncParamValues(config, state)` | 专用 ref 回写到 paramValues | -| `getDimConfig(config)` | 检测 combined/split 模式,返回 dimension 配置 | -| `checkShowWhen(param, paramValues)` | 检查 showWhen 条件是否满足 | - -### `syncDefaults` 增强 - -1. dimension.separator → 生成 `parse/format` 函数(替代硬编码的 JS 函数) -2. `config.promptPlaceholder` → 同步到 `promptPlaceholder.value` -3. customWidth/customHight 继续通过 `p.name` 查找(保持现有硬编码兼容) - -## 四、Painting 平台改造 - -`src/platforms/painting/index.js`: - -- `loadConfig()` 改为 `getModelId()` + `getModelConfig()` API 调用 -- `syncDefaults`/`syncParamValues`/`getDimConfig` 改为从 helper 导入 -- 移除 `import { getModelConfig } from './models/index.js'` -- controls 的 `show()` 加入 `checkShowWhen` 判断 - -## 五、Video 平台改造 - -`src/platforms/video/index.js`: - -| 项目 | 当前 | 改造后 | -|------|------|--------| -| 配置来源 | `modelConfig.js` → 远程 JSON | 统一 API | -| 配置存储 | `modelDisplayConfig` | 统一 `modelConfig` | -| 数据结构 | `config.display.*` | 统一 `params[]` 数组 | -| `loadConfig` | `loadInternalConfig` | 改为 API + `syncDefaults` | -| `buildTaskBody` | 硬编码 5 字段 | 改为 params 驱动(与 Painting 一致) | -| `showImageUploader` | `modelType !== 'text'` | 改为 `config.inputType` 驱动 | -| controls | pattern/proportion/time | 改为按 `params[]` 的 `ui` 驱动 | - -Video 现有的 pattern/time 控件对应的 ui 值暂未定义,保留占位,等后端配置数据就绪后再适配。 - -## 六、showWhen 条件显示 - -`checkShowWhen(param, paramValues)` 检查 param 的 `showWhen` 字段: - -```js -// 例如 { aspectRatio: 'custom' } → 仅在 paramValues.aspectRatio === 'custom' 时显示 -showWhen 为空 → 总是显示 -showWhen 存在 → 所有 key-value 匹配才显示 -``` - -controls 的 `show()` 中调用,因为直接读取 `paramValues[key]`(reactive),Vue computed 自动追踪依赖。 - -## 七、文件清理 - -| 路径 | 操作 | -|------|------| -| `src/utils/modelConfigHelper.js` | **新建** | -| `src/platforms/painting/models/`(9 文件) | **删除** | -| `src/utils/modelConfig.js` | **删除** | -| `src/utils/createTask.js` | **删除** | - -删除前需确认 `createTask.js` 无其他文件 import。 - -## 八、API 验证发现 - -已用 token 实测 Painting 全部 8 个模型,结论: - -| 发现 | 结论 | -|------|------| -| 单条 API | `GET /suanli/v1/models/:id/config` → `data` 直接返回 config | -| 批量 API | `POST /suanli/v1/models/configs` → `data` 为 `{ modelId: config, ... }`,不存在的 ID 返回空 `{}` | -| `dimension.separator` | 字符串 `"*"`,需前端 `syncDefaults` 生成 parse/format | -| `dimensionWidth/Height` 的 `min/max` | 在 param 根层,与硬编码结构一致,`getDimConfig` 直接兼容 | -| `imageUpload` 的 `maxCount` | 在 param 根层,`imageUploadLimit()` 直接兼容 | -| `number` 类型的 `min/max` | 在 param 根层,与 dimensionWidth 一致 | -| customWidth/customHight | `ui: 'number'`,`showWhen: {"aspectRatio": "custom"}` | -| hidden 参数 | 不返回 `options` 字段(与硬编码不同),不影响功能 | -| `type` 字段 | API 不返回,前端 `syncDefaults` 不依赖,无影响 | -| Video 平台 | 模型列表为空,改造后无 fallback 将不可用 | - -## 九、不改动的部分 - -- `src/components/dialogBox/index.vue`:通过 `platform.loadConfig()` 多态调用,改动仅在 descriptor 内部 -- `src/utils/modelApi.js`:`getModelId()`/`fetchPlatformModels()` 保持不变,新增两个函数 -- controls 组件(proportion.vue / dimension.vue / quality.vue / quantity.vue):UI 不变 -- `buildTaskBody()` / `fillFromResult()`:逻辑不变