- 新增 Platform Descriptor 模式完整说明(接口、控件描述符、自注册) - 更新目录结构(src/config/ → src/platforms/) - 合并 Painting/Video 数据流为统一描述 - 更新 dialogBox 说明为通用编排壳 - 修正所有已删除/移动文件的路径引用
13 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
常用命令
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),提交生成任务并轮询结果。
核心架构:Platform Descriptor 模式。 Painting 和 Video 是两个独立的平台包,通过统一的注册表动态加载。dialogBox 是通用编排壳,不包含任何平台特定逻辑。
关键目录
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
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ └── param.js # 参数 store(当前为空)
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关(登录、token 校验、用户信息、验证码)
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│ ├── dialogBox/ # 通用编排壳(核心交互入口)
│ │ └── index.vue # <component :is> 动态渲染平台控件,不含平台分支
│ ├── 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 # 透传层: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)
Platform Descriptor 模式
每个平台通过 defineXxxPlatform() 工厂函数返回标准接口:
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 绑定对:
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 是纯透传:
- 用户选择模型 →
platform.loadConfig(modelName, modelType)加载参数 schema - 参数 schema 驱动
controls渲染 UI,用户填写参数 - 用户点击发送 →
handleStart()→platform.buildTaskBody({ prompt, referenceImages })返回扁平modelParams createTask(data)透传data.body(不再做任何转换)getModelId(type, modelName)查找 UUID → POST/suanli/v1/tasks(X-Session-Idheader)- 20s 间隔轮询直至完成/失败
模型参数配置
Painting 模型参数 schema 在 src/platforms/painting/models/*.js 中,参数通过 ui 字段映射到 UI 控件:
ui 值 |
控件 | 说明 |
|---|---|---|
textarea |
Sender 内置 textarea | prompt 输入框 |
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 模式区分:通过 getDimConfig() 自动检测 ui: 'dimension'(组合)或 ui: 'dimensionWidth'(拆分),两种模式共用 DimensionInput 组件。
条件显示:showWhen: { aspectRatio: 'custom' } 使参数仅在 proportion 选 custom 时显示。
displayNameMap 机制
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 版。当前因 model.value 已是中文 config key 直达 configs[],暂不触发。若后续改为按 display_name 查找,需修复此重复 key。
$attrs 穿透注意
向子组件传递的 prop 如果子组件未声明,会通过 $attrs 穿透到根元素。所有通过 v-model 传递的值,子组件必须声明对应的 prop。
API 层设计原则
src/apis/只做纯 HTTP 调用(service.get/post/delete),不含缓存、localStorage、业务逻辑- 缓存、数据转换等业务逻辑放在
src/utils/中
关键注意事项
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 +pendingRequestsMap 并发去重。 - 平台包预加载:dialogBox 顶层
importPainting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
接口速查
| 函数 | 端点 | 用途 |
|---|---|---|
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 |
删除历史记录 |
任务响应格式
// 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 APIunplugin-vue-components:自动注册src/components/下的组件和 Element Plus 组件- Element Plus 图标通过
unplugin-icons按需加载