- 新增 DimensionInput 组件(Popover + W/H 数字输入 + 比例锁),支持 combined(单字段 W*H)和 split(独立 width/height)两种模式 - 修复 jimeng/qwen/qwen-edit 尺寸参数不显示:改用 dimension/dimensionWidth/dimensionHeight 替代 number/select - 修复 GPT-Image-2/GPT-Image-2 I2I quality 选择器不显示:通过 Select 组件承载 ui: 'select' - 修复 jimeng/GPT-Image-2 误显示 quantity:showQuantity 移除 fallback,仅匹配 ui: 'quantity' - 新增 docs/模型参数后端化方案.md:API 设计、数据库设计、前后端迁移步骤 - 更新 CLAUDE.md:补充新 UI 类型映射、dimension 模式说明、displayNameMap bug 标注 - 删除废弃文件 Vidu Q3-T2V.json、modelConfig 空目录
198 lines
12 KiB
Markdown
198 lines
12 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## 常用命令
|
||
|
||
```bash
|
||
pnpm dev # 启动 Vite 开发服务器
|
||
pnpm build # 生产构建
|
||
pnpm preview # 预览生产构建
|
||
npx eslint . # 代码检查(@antfu/eslint-config,Vue 支持,无 TypeScript)
|
||
```
|
||
|
||
## 技术栈
|
||
|
||
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + `vue-element-plus-x`(多媒体编辑) + Less + pnpm
|
||
|
||
## 架构概览
|
||
|
||
AI 绘画/视频生成前端操作平台,通过 HTTP 接口对接算力调度后端(suanli)和第三方 AI 平台(RunningHub),提交生成任务并轮询结果。
|
||
|
||
**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
|
||
|
||
### 关键目录
|
||
|
||
```
|
||
src/
|
||
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller
|
||
├── router/index.js # 路由定义 + token 验证守卫
|
||
├── stores/ # Pinia 状态管理
|
||
│ ├── user.js # 用户认证、信息(含 sessionId),使用 pinia 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 用于 Painting,video.vue 用于 Video)
|
||
│ │ ├── dimension/ # 尺寸输入 Popover(W/H 数字输入 + 比例锁,支持 split/combined 两种模式)
|
||
│ │ ├── imageUploader/ # 图片上传(Painting)
|
||
│ │ ├── videoImageUploader/ # 视频图片上传(Video)
|
||
│ │ ├── quantity/ # 生成数量选择器(支持 1-6,上限由模型配置派生)
|
||
│ │ ├── Time/ # 视频时长选择器
|
||
│ │ └── pattern/ # 视频模式选择器
|
||
│ ├── Popover/ # 自定义弹出层(Teleport to body,position: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 返回 modelParams,Video 走 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 类型
|
||
```
|
||
|
||
### 模型参数配置(Painting 新架构)
|
||
|
||
`src/config/models/` 下每个模型一个 JS 文件,参数通过 `ui` 字段映射到不同 UI 组件:
|
||
|
||
| `ui` 值 | 组件 | 说明 |
|
||
|---------|------|------|
|
||
| `textarea` | Sender 内置 textarea | prompt 输入框 |
|
||
| `proportion` | `paintingProportion` | 比例选择 Popover(options 含 `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` | 参考图上传 |
|
||
| `hidden` | 无 | 静默写入默认值 |
|
||
|
||
**dimension 模式区分**:dialogBox 通过 `dimConfig` computed 自动检测 —— 找 `ui: 'dimension'`(组合)或 `ui: 'dimensionWidth'`(拆分),两种模式共用同一个 `DimensionInput` 组件。
|
||
|
||
**条件显示**:`showWhen: { aspectRatio: 'custom' }` 使参数仅在 proportion 选 `custom` 时显示(如 `customWidth`/`customHight`)。
|
||
|
||
模型选择器从 API(`fetchPlatformModels`)获取模型列表,按 API 返回的 `tags` 数组字段分组(`text`→生成模型,`edit`→编辑模型,`vision`→视觉理解模型)。
|
||
|
||
### `displayNameMap` 机制
|
||
|
||
`src/config/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()`
|
||
|
||
### `$attrs` 穿透注意
|
||
|
||
向子组件传递的 prop 如果子组件未声明,会通过 `$attrs` 穿透到根元素。例如 `dialogBox` 向 `paintingProportion` 传递 `v-model:width="customWidth"`,若 `paintingProportion` 未声明 `width` prop,值会穿透到 `<Popover>` 的 `width` prop,导致异常宽度。**所有通过 `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` 时预请求,避免首次点击"发送"时才触发。
|
||
|
||
### 接口速查
|
||
|
||
| 函数 | 端点 | 用途 |
|
||
|------|------|------|
|
||
| `requestCreateTask` | POST `/suanli/v1/tasks` | 创建任务(带 `X-Session-Id` header) |
|
||
| `requestTaskStatus` | GET `/suanli/v1/tasks/:id` | 查询单个任务状态 |
|
||
| `requestTaskHistory` | GET `/suanli/v1/tasks/history` | 历史任务列表(支持 `user_id`/`platform_code`/`page`/`pageSize`) |
|
||
| `fetchPlatformModels` | GET `/suanli/v1/platforms/:code/models` | 获取平台模型列表(返回 `{id, display_name, tags, disabled?}`) |
|
||
| `cancelOrCollect` | POST `/collect/toggle` | 收藏/取消收藏 |
|
||
| `deleteGenerateHistory` | DELETE `/taskRecordHistory/delete` | 删除历史记录 |
|
||
|
||
### 任务响应格式
|
||
|
||
```json
|
||
// GET /suanli/v1/tasks/:id 返回结构
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"task_id": "uuid",
|
||
"status": "completed", // queued | processing | completed | failed
|
||
"outputs": [ // ⚠️ 扁平数组,不是 { images: [...] }
|
||
{ "url": "https://...", "type": "png" }
|
||
],
|
||
"vendor_error": "..." // 仅 failed 时有值
|
||
}
|
||
}
|
||
```
|
||
|
||
### 请求拦截器路由
|
||
|
||
拦截器统一设置 `Authorization: <token>`(不带 Bearer 前缀),根据 URL 前缀切换后端:
|
||
|
||
| URL 前缀 | 环境变量 |
|
||
|----------|----------|
|
||
| `/suanli` | `VITE_API_TASK_TARGET` |
|
||
| `/pay` | `VITE_API_PAY_TARGET` |
|
||
| `/aigc` | `VITE_API_AIGC_TARGET` |
|
||
| 其他 | `VITE_API_BASE_URL`(默认) |
|
||
|
||
### 平台编码映射
|
||
|
||
| 类型 | 平台编码 |
|
||
|------|----------|
|
||
| Painting | `ai_painting_talk` |
|
||
| Video | `ai_video_talk` |
|
||
|
||
映射函数 `getPlatformCode()` 位于 `utils/modelApi.js`。
|
||
|
||
### 自动导入
|
||
|
||
- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API
|
||
- `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件
|
||
- Element Plus 图标通过 `unplugin-icons` 按需加载
|