docs: 更新 CLAUDE.md 以反映 Platform Descriptor 架构重构

- 新增 Platform Descriptor 模式完整说明(接口、控件描述符、自注册)
- 更新目录结构(src/config/ → src/platforms/)
- 合并 Painting/Video 数据流为统一描述
- 更新 dialogBox 说明为通用编排壳
- 修正所有已删除/移动文件的路径引用
This commit is contained in:
王佑琳 2026-06-09 14:53:07 +08:00
parent ac7a592618
commit 72e4acf956

198
CLAUDE.md
View File

@ -17,12 +17,9 @@ Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + `vue-elem
## 架构概览
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端suanli和第三方 AI 平台RunningHub,提交生成任务并轮询结果。
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端suanli提交生成任务并轮询结果。
**Painting 和 Video 走两套不同的任务构造路径:**
- **Painting新架构**:本地模型参数 schema → 专用控件 + 动态表单 → `X-Session-Id` header + 扁平 API body
- **Video旧架构**:远程 workflow JSON → RunningHub Playload 适配器 → `{ workflowId, nodeInfoList }` body。Video 模型列表从远程静态 JSON`VITE_API_MODEL_RESOURCE/.../video.json`获取workflow 配置按日缓存于 localStorage
**核心架构Platform Descriptor 模式。** Painting 和 Video 是两个独立的平台包通过统一的注册表动态加载。dialogBox 是通用编排壳,不包含任何平台特定逻辑。
### 关键目录
@ -30,117 +27,164 @@ AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度
src/
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
├── router/index.js # 路由定义 + token 验证守卫
├── platforms/ # 平台包(独立、可插拔)
│ ├── registry.js # 注册表registerPlatform(id, factory) + createPlatform(type)
│ ├── painting/ # Painting 平台
│ │ ├── index.js # definePaintingPlatform()controls、state、loadConfig、buildTaskBody 等
│ │ ├── modelSelector.vue # 模型选择器(按 API tags 分组)
│ │ ├── imageUploader.vue # 图片上传组件
│ │ ├── models/ # 模型参数 schema本地 JS待后端化
│ │ │ └── index.js # getModelConfig(modelName) → 查找 config
│ │ └── controls/ # 平台专用控件
│ │ ├── proportion.vue
│ │ ├── dimension.vue
│ │ ├── quality.vue
│ │ └── quantity.vue
│ └── video/ # Video 平台
│ ├── index.js # defineVideoPlatform()
│ ├── modelSelector.vue
│ ├── imageUploader.vue
│ └── controls/
│ ├── pattern.vue
│ ├── proportion.vue
│ └── time.vue
├── stores/ # Pinia 状态管理
│ ├── user.js # 用户认证、信息(含 sessionId使用 pinia persist 持久化 token
│ ├── user.js # 用户认证、信息(含 sessionIdpinia persist 持久化 token
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ └── param.js # 参数 store当前为空
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关登录、token 校验、用户信息、验证码)
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│ ├── dialogBox/ # 生成参数输入面板(核心交互入口)
│ │ ├── index.vue # 编排中心:组装所有控件,处理 handleStart()、模型配置加载、参数回填
│ │ ├── model/ # 模型选择器(按 API 返回的 tags 分组value 编码为 tag::display_name
│ │ ├── proportion/ # 比例/分辨率选择器painting.vue 用于 Paintingvideo.vue 用于 Video
│ │ ├── dimension/ # 尺寸输入 PopoverW/H 数字输入 + 比例锁,支持 split/combined 两种模式)
│ │ ├── imageUploader/ # 图片上传Painting
│ │ ├── videoImageUploader/ # 视频图片上传Video
│ │ ├── quantity/ # 生成数量选择器(支持 1-6上限由模型配置派生
│ │ ├── Time/ # 视频时长选择器
│ │ └── pattern/ # 视频模式选择器
│ ├── dialogBox/ # 通用编排壳(核心交互入口)
│ │ └── index.vue # <component :is> 动态渲染平台控件,不含平台分支
│ ├── Popover/ # 自定义弹出层Teleport to bodyposition:fixed + fit-content 宽度)
│ ├── Select/ # 自定义下拉选择器(分组选项,与 Popover 协调互斥开关)
│ ├── Img/ # 图片包装组件点击全屏查看Teleport 实现)
│ ├── virtual-scroller/# 虚拟滚动列表组件自定义实现reverse 模式)
│ └── canvas/ # 图片画布编辑(圆/矩形选区局部重绘undo/redo
├── views/ # 页面home、login
├── utils/
│ ├── request.js # Axios 实例 + 拦截器:统一 Auth不带 Bearer+ 按前缀路由 baseURL
│ ├── taskPolling.js # 任务生成入口:组装参数 → POST 创建任务 → 20s HTTP 轮询直至完成/失败
│ ├── modelApi.js # 模型业务层localStorage 30s 缓存 + pendingRequests 并发去重 + 平台编码映射
│ ├── createTask.js # 任务 body 构造Painting 返回 modelParamsVideo 走 Playload 适配器
│ ├── modelConfig.js # Video 旧架构:从远程 JSON 加载 workflow 配置(含 localStorage 每日缓存)
│ ├── downloadImage.js # 图片/视频下载fetch → Blob → 自动文件名下载
│ ├── tokenError.js # 认证失败处理:提示后 5 秒刷新页面
│ ├── encrypt.ts # 加密工具Base64/MD5/RSA/AES依赖 crypto-js、jsencrypt
│ └── auth.ts # token 存取工具localStorage
├── config/
│ ├── plugins.js # Vite 插件配置unplugin-auto-import + unplugin-vue-components
│ ├── index.js # 平台配置入口(目前仅导出 runninghub 供 Video 使用)
│ ├── runninghub/ # RunningHub 平台适配器Playload() 构造和 result() 解析Video 专用)
│ └── models/ # Painting 模型参数 schema每模型一个 JS 文件,定义 params 和各字段的 ui 类型
└── utils/
├── request.js # Axios 实例 + 拦截器:统一 Auth不带 Bearer+ 按前缀路由 baseURL
├── taskPolling.js # 任务生成入口:组装参数 → POST 创建任务 → 20s HTTP 轮询直至完成/失败
├── modelApi.js # 模型业务层localStorage 30s 缓存 + pendingRequests 并发去重 + 平台编码映射
├── createTask.js # 透传层return data.body各平台 buildTaskBody() 已返回扁平 modelParams
├── modelConfig.js # Video 专用:从远程 JSON 加载 workflow 配置(含 localStorage 每日缓存)
├── downloadImage.js # 图片/视频下载fetch → Blob → 自动文件名下载
├── uploadImage.js # 图片上传工具
├── tokenError.js # 认证失败处理:提示后 5 秒刷新页面
├── encrypt.ts # 加密工具Base64/MD5/RSA/AES依赖 crypto-js、jsencrypt
└── auth.ts # token 存取工具localStorage
```
### 模型参数配置Painting 新架构)
### Platform Descriptor 模式
`src/config/models/` 下每个模型一个 JS 文件,参数通过 `ui` 字段映射到不同 UI 组件
每个平台通过 `defineXxxPlatform()` 工厂函数返回标准接口
| `ui` 值 | 组件 | 说明 |
```js
const platform = {
id: 'painting', // 平台唯一标识
label: 'AI绘画2026', // 显示名称
ModelSelector: markRaw(Component), // 模型选择器组件
modelSelectorProps: null, // 模型选择器的额外 props可为函数
controls: [{ // 控件描述符数组
name: 'proportion',
component: markRaw(Component),
show: (config) => boolean, // 根据 model config 决定是否显示
props: (config) => ({ ... }) // 根据 model config 生成 v-model props
}],
ImageUploader: markRaw(Component), // 图片上传组件(可为 null
state: { ... }, // 平台所有响应式状态
model, modelType, // 当前模型/类型ref
modelConfig, // 当前模型参数配置ref
promptPlaceholder, // 提示词占位文本ref
async loadModels() { ... }, // 获取模型列表
async loadConfig(modelName, modelType) { ... }, // 加载模型参数配置
getDefaultModel() { ... }, // 返回默认模型名
validateBeforeSubmit() { ... }, // 提交前校验,返回 null 表示通过
getUploaderBindings() { ... }, // 图片上传组件的绑定参数
showImageUploader() { ... }, // 是否显示图片上传区域
isImageRequired() { ... }, // 图片是否必填
buildTaskBody({ prompt, referenceImages }) { ... }, // 构造扁平 modelParams
fillFromResult(resultData) { ... }, // 从历史结果回填参数
}
```
控件通过 `ctrl.props(config)` 接收 v-model 绑定对:
```js
props: (config) => ({
modelValue: proportion.value,
'onUpdate:modelValue': (v) => { proportion.value = v },
// ...
})
```
**自注册:** 每个平台文件底部调用 `registerPlatform('Painting', definePaintingPlatform)`,在 import 时自动注册。dialogBox 通过 `createPlatform(props.type)` 获取实例。
### dialogBox 通用编排壳
`src/components/dialogBox/index.vue` 是纯编排组件,不含任何平台分支:
- **平台切换**`const platform = computed(() => createPlatform(props.type))`,切换时重置默认模型并加载模型列表
- **控件渲染**`visibleControls = platform.controls.filter(c => c.show(getCurrentConfig()))`,用 `<component :is>` + `v-bind="ctrl.props(...)"` 渲染
- **配置获取**`getCurrentConfig()` 返回 `platform.modelConfig?.value ?? platform.modelDisplayConfig?.value`,兼容两种配置来源
- **模型切换**:监听 `model + modelType`,调用 `await platform.loadConfig(newModel, newModelType)`
- **任务发起**`handleStart()` 调用 `platform.validateBeforeSubmit()``platform.buildTaskBody()``generate()`
- **参数回填**`fillParamsFromResult()` 委托给 `platform.fillFromResult()`
### 统一数据流Painting + Video
两个平台现已统一,`createTask.js` 是纯透传:
1. 用户选择模型 → `platform.loadConfig(modelName, modelType)` 加载参数 schema
2. 参数 schema 驱动 `controls` 渲染 UI用户填写参数
3. 用户点击发送 → `handleStart()``platform.buildTaskBody({ prompt, referenceImages })` 返回扁平 `modelParams`
4. `createTask(data)` 透传 `data.body`(不再做任何转换)
5. `getModelId(type, modelName)` 查找 UUID → POST `/suanli/v1/tasks``X-Session-Id` header
6. 20s 间隔轮询直至完成/失败
### 模型参数配置
Painting 模型参数 schema 在 `src/platforms/painting/models/*.js` 中,参数通过 `ui` 字段映射到 UI 控件:
| `ui` 值 | 控件 | 说明 |
|---------|------|------|
| `textarea` | Sender 内置 textarea | prompt 输入框 |
| `proportion` | `paintingProportion` | 比例选择 Popoveroptions 含 `custom` 时可自定义 W/H |
| `resolution` | `paintingProportion` 内部 | 分辨率子选项,与 proportion 共用一个 Popover |
| `dimension` | `DimensionInput` | **组合模式**:单个 `"W*H"` 字符串参数,通过 `dimension.parse/format` 序列化 |
| `dimensionWidth` + `dimensionHeight` | `DimensionInput` | **拆分模式**:两个独立 number 参数,共享同一个 Popover 和比例锁 |
| `select` | `Select` | 通用下拉选择(如 quality |
| `quantity` | `Quantity` | 生成张数(上限由 `options` 最大值派生) |
| `imageUpload` | `ImageUploader` | 参考图上传 |
| `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 模式区分**dialogBox 通过 `dimConfig` computed 自动检测 —— 找 `ui: 'dimension'`(组合)或 `ui: 'dimensionWidth'`(拆分),两种模式共用同一个 `DimensionInput` 组件。
**dimension 模式区分**通过 `getDimConfig()` 自动检测 `ui: 'dimension'`(组合)或 `ui: 'dimensionWidth'`(拆分),两种模式共用 `DimensionInput` 组件。
**条件显示**`showWhen: { aspectRatio: 'custom' }` 使参数仅在 proportion 选 `custom` 时显示(如 `customWidth`/`customHight`)。
模型选择器从 API`fetchPlatformModels`)获取模型列表,按 API 返回的 `tags` 数组字段分组(`text`→生成模型,`edit`→编辑模型,`vision`→视觉理解模型)。
**条件显示**`showWhen: { aspectRatio: 'custom' }` 使参数仅在 proportion 选 `custom` 时显示。
### `displayNameMap` 机制
`src/config/models/index.js``displayNameMap` 负责将 API 返回的 `display_name` 映射到 config key。因为同一模型在不同 tag 下可能共用一个 `display_name`(如 `GPT-Image-2``GPT-image-2` 分别对应编辑/生成config key 采用内部中文名区分。
`src/platforms/painting/models/index.js` 中 `displayNameMap` 负责将 API 返回的 `display_name` 映射到 config key。同一模型在不同 tag 下可能共用一个 `display_name`(如 `GPT-Image-2``GPT-image-2`config key 采用内部中文名区分。
**已知 bug**`displayNameMap` 存在重复 key `'GPT-Image-2'`,第二条会覆盖第一条,导致文字生图版 GPT-Image-2 查找走 `displayNameMap` 时映射到 I2I 版的 config。当前因 `model.value` 已经是中文 config key直达 `configs[]` 而非走 map暂时不触发。若后续改为按 `display_name` 查找,需修复此重复 key。
### dialogBox 编排中心
`src/components/dialogBox/index.vue` 是核心编排组件,负责:
- **模型选择切换**:监听 `model` + `modelType` 变化,调用 `loadModelConfig()` 加载模型参数 schema
- **派生 UI 配置**:从 model config 计算 `showProportion`、`showDimension`、`showQuality`、`showQuantity`(各自检查对应 `ui` 字段是否存在);`hasCustomSize`proportion options 含 `custom``dimConfig`(自动识别 combined/split 模式)
- **状态管理**`dimWidth`/`dimHeight` 通过 `v-model:width`/`v-model:height` 与 `DimensionInput` 双向绑定;`qualityValue` 通过 `v-model` 与 Select 组件双向绑定
- **参数回填**`fillParamsFromResult()` 供历史记录重编辑使用
- **任务发起**`handleStart()` 收集所有参数 → 构造 `data` → 调用 `taskPolling.js:generate()`
**已知 bug**`displayNameMap` 存在重复 key `'GPT-Image-2'`,第二条会覆盖第一条,导致文字生图版 GPT-Image-2 查找走 `displayNameMap` 时映射到 I2I 版。当前因 `model.value` 已是中文 config key 直达 `configs[]`,暂不触发。若后续改为按 `display_name` 查找,需修复此重复 key。
### `$attrs` 穿透注意
向子组件传递的 prop 如果子组件未声明,会通过 `$attrs` 穿透到根元素。例如 `dialogBox``paintingProportion` 传递 `v-model:width="customWidth"`,若 `paintingProportion` 未声明 `width` prop值会穿透到 `<Popover>``width` prop导致异常宽度。**所有通过 `v-model` 传递的值,子组件必须声明对应的 prop。**
向子组件传递的 prop 如果子组件未声明,会通过 `$attrs` 穿透到根元素。**所有通过 `v-model` 传递的值,子组件必须声明对应的 prop。**
### API 层设计原则
- `src/apis/` 只做纯 HTTP 调用(`service.get/post/delete`不含缓存、localStorage、业务逻辑
- 缓存、数据转换等业务逻辑放在 `src/utils/`
### 核心数据流
**Painting新架构**
1. 用户设置参数 → `dialogBox` 从 model config 派生 proportion/resolution 选项、hasCustomSize、quantityMax
2. `handleStart()` 收集参数 → 组装 `{ modelParams, request }`
3. `taskPolling.js:generate()``createTask(data)` → Painting 直接返回 `data.modelParams`
4. `getModelId(type, modelName)` 查找 UUID内部调用 `fetchPlatformModels` 走缓存)
5. `requestCreateTask(body, sessionId)` → POST `/suanli/v1/tasks`,携带 `X-Session-Id` header
6. 返回 task_id → 20s 间隔轮询直至完成/失败
**Video旧架构保留**
1. 用户设置 Pattern、videoModel、比例、时长
2. `createTask(data)``runninghub.Playload(data)``fetchModelConfig()` 获取 workflow JSON → 返回 `{ workflowId, nodeInfoList }`
3. 后续同 Painting 步骤 4-6
### 关键注意事项
- **`sessionId`** 来自登录接口返回的 `userInfo.sessionId`,存储在 `useUserStore().userInfo` 中。`taskPolling.js` 必须使用该值,禁止随机生成。
- **`X-Session-Id`** 自定义 header 需要 nginx 在 `/suanli/` location 的 `Access-Control-Allow-Headers` 中加入,否则 POST 请求会触发 CORS 预检失败。
- **模型列表缓存**`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重,避免重复请求。
- **页面加载预请求**:平台模型列表在 `dialogBox onMounted` 时预请求,避免首次点击"发送"时才触发。
- **模型列表缓存**`modelApi.js` 中 `fetchPlatformModels` 使用 localStorage 30 秒 TTL + `pendingRequests` Map 并发去重。
- **平台包预加载**dialogBox 顶层 `import` Painting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
### 接口速查