AI_Painting_V2.0/CLAUDE.md
WangLeo 4d76899488 chore: 清理 env 废弃变量、移除旧模型配置,补充 Music 平台文档与大小写归一化修复
- .env 文件移除未使用的 VITE_API_PAY_* 变量,更新生产环境 URL
- 删除 5 个已废弃的 model-configs JSON 文件
- CLAUDE.md 新增 Music 平台架构说明与计费类型映射
- getPlatformCode / getChargeType 增加输入大小写归一化
2026-06-15 16:55:34 +08:00

409 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 常用命令
```bash
pnpm install # 安装依赖
pnpm dev # 启动 Vite 开发服务器(默认 http://localhost:5173
pnpm build # 生产构建
pnpm preview # 预览生产构建
npx eslint . # 代码检查(@antfu/eslint-configVue 支持,无 TypeScript
```
## 技术栈
Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + `vue-element-plus-x`(提供 Sender 输入框组件) + Less + pnpm
## 架构概览
AI 绘画/视频/音乐生成前端操作平台,通过 HTTP 接口对接算力调度后端suanli提交生成任务并轮询结果。
**核心架构Platform Descriptor 模式。** Painting、Video 和 Music 是三个独立的平台包通过统一的注册表动态加载。dialogBox 是通用编排壳,不包含任何平台特定逻辑。
### 关键目录
```
├── model-configs/ # 运维参考:模型参数配置 JSON 文件hailuo, ltx, vidu 等,位于项目根目录)
├── config/
│ └── plugins.js # Vite 插件配置unplugin-auto-import + unplugin-vue-components resolver
├── vite.config.js # 构建配置alias@→src, ~→根目录、envPrefix: ['VITE','FILE']、optimizeDeps
src/
├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/vue-virtual-scroller
├── router/index.js # 路由定义 + token 验证守卫
├── assets/ # 静态资源
│ ├── dialog/ # 控件区域 SVG 图标model, proportion, quantity, time, videoPattern 等)
│ └── display/ # 结果展示区 SVG 图标download, collection, delete, back, brush 等)
├── platforms/ # 平台包(独立、可插拔)
│ ├── registry.js # 注册表registerPlatform(id, factory) + createPlatform(type)
│ ├── painting/ # Painting 平台
│ │ ├── index.js # definePaintingPlatform()controls、state、loadConfig、buildTaskBody 等
│ │ ├── modelSelector.vue # 模型选择器(按 API tags 分组)
│ │ ├── imageUploader.vue # 图片上传组件
│ │ └── 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
│ └── music/ # Music 平台
│ ├── index.js # defineMusicPlatform()
│ ├── modelSelector.vue
│ ├── imageUploader.vue
│ └── controls/
│ ├── modeSelector.vue # 模式选择(常用/专业/Remix/编辑)
│ ├── pureMusicGroup.vue # 纯音乐开关 + 歌词输入弹窗
│ ├── lyricsInput.vue # 专业模式歌词输入
│ └── timeControl.vue # 时长滑块(常用模式)
├── stores/ # Pinia 状态管理
│ ├── user.js # 用户认证、信息(含 sessionIdpinia persist 持久化 token
│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等)
│ └── param.js # 参数 store当前为空
├── apis/ # HTTP API 层:纯请求封装,不含业务逻辑
│ ├── auth/ # 认证相关登录、token 校验、用户信息、验证码)
│ └── display/ # 任务创建/轮询/历史、平台模型、收藏/删除
├── components/
│ ├── dialogBox/ # 通用编排壳(核心交互入口)
│ │ └── index.vue # <component :is> 动态渲染平台控件,不含平台分支
│ ├── Popover/ # 自定义弹出层Teleport to bodyposition:fixed + fit-content 宽度)
│ ├── Select/ # 自定义下拉选择器(分组选项,与 Popover 协调互斥开关)
│ ├── ParamGroup/ # 动态参数容器:遍历 config.params 中未被专用控件处理的 select/switch 参数
│ ├── SwitchControl/ # 纯 CSS 布尔开关(不用外部组件库)
│ ├── Img/ # 图片包装组件点击全屏查看Teleport 实现)
│ ├── virtual-scroller/ # 虚拟滚动列表自定义实现。reverse 模式用 180deg 旋转实现底部锚定slot 内容须反旋转
│ └── canvas/ # 图片画布编辑(圆/矩形选区局部重绘undo/redo
├── views/ # 页面home、login
└── utils/
├── request.js # Axios 实例 + 拦截器:统一 Auth不带 Bearer+ 按前缀路由 baseURL
├── taskPolling.js # 任务生成入口:组装参数 → POST 创建任务 → 首次 5s + 后续 20s HTTP 轮询直至完成/失败
├── modelApi.js # 模型业务层localStorage 缓存 + 并发去重。平台模型列表(30s TTL) + 模型参数配置(60s TTL)
├── modelConfigHelper.js # 模型配置共享工具syncDefaults / syncParamValues / getDimConfig / checkShowWhen
├── downloadImage.js # 图片/视频下载fetch → Blob → 自动文件名下载
├── uploadImage.js # 图片上传工具
├── tokenError.js # 认证失败处理:提示后 5 秒刷新页面
├── encrypt.ts # 加密工具(⚠️ 死代码:依赖 crypto-js/jsencrypt 未安装,无任何 import 引用)
└── auth.ts # token 存取工具localStorage
```
### Platform Descriptor 模式
每个平台通过 `defineXxxPlatform()` 工厂函数返回标准接口:
```js
const platform = {
id: 'painting', // 平台唯一标识
label: 'AI绘画2026', // 显示名称
ModelSelector: markRaw(Component), // 模型选择器组件
modelSelectorProps: null, // 模型选择器的额外 props可为函数
controls: [{ // 控件描述符数组
name: 'proportion',
component: markRaw(Component),
beforeModel: false, // 设为 true 则渲染在 ModelSelector 之前
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) { ... }, // 加载模型参数配置modelName 可以是 UUID/name/display_name
getDefaultModel() { ... }, // 返回默认模型标识(可返回 '' 交由 modelSelector 自动纠错)
imageUploadLimit() { ... }, // 返回图片上传槽位数(应累加所有 imageUpload 参数的 maxCount
validateBeforeSubmit() { ... }, // 提交前校验,返回 null 表示通过
getUploaderBindings() { ... }, // 图片上传组件的绑定参数modelType + imagesCount
showImageUploader() { ... }, // 是否显示图片上传区域
isImageRequired() { ... }, // 图片是否必填
buildTaskBody({ prompt, referenceImages }) { ... }, // 构造扁平 modelParams需将 referenceImages 映射到 imageUpload 参数
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)` 获取实例。注册表内部将 key 统一转为小写,因此 `'Painting'``'painting'` 等效。
### dialogBox 通用编排壳
`src/components/dialogBox/index.vue` 是纯编排组件,不含任何平台分支:
- **平台切换**`const platform = computed(() => createPlatform(props.type))`,切换时重置默认模型并加载模型列表
- **控件渲染**`visibleControls = platform.controls.filter(c => c.show(getCurrentConfig()))`,按 `beforeModel` 拆分为两组,`beforeModel` 控件 → ModelSelector → 其余控件,用 `<component :is>` + `v-bind="ctrl.props(...)"` 渲染
- **配置获取**`getCurrentConfig()` 返回 `platform.modelConfig?.value`
- **模型切换**`watch([model, modelType])` → `platform.loadConfig()``syncDefaults()` 将 schema 写入响应式 state → controls 的 `show()`/`props()` 读取 state → `visibleControls` 自动更新 → UI 重渲染
- **任务发起**`handleStart()` 调用 `platform.validateBeforeSubmit()``platform.buildTaskBody()``generate()`
- **参数回填**`fillParamsFromResult()` 委托给 `platform.fillFromResult()`
### 统一数据流Painting + Video
两个平台现已统一,通过后端 API 获取模型参数配置:
1. 用户选择模型 → `platform.loadConfig(modelName, modelType)` → 调用 `GET /suanli/v1/models/:id/config`(优先 60s 缓存)加载参数 schema
2. 参数 schema 驱动 `controls` 渲染 UI用户填写参数
3. 用户点击发送 → `handleStart()``platform.buildTaskBody({ prompt, referenceImages })` 返回扁平 `modelParams`
4. `taskPolling.js` 直接读取 `data.body``getModelId(type, modelName)` 查找 UUID → POST `/suanli/v1/tasks``X-Session-Id` header
5. 20s 间隔轮询直至完成/失败
### 模型标识与查找
API 返回的模型对象包含三个标识字段:
| 字段 | 示例 | 用途 |
|------|------|------|
| `id` | `e7e1e743-7621-403d-b8fb-2b7f4fa1b4fc` | UUID 主键,**唯一且稳定**,用于 API 调用和内部追踪 |
| `name` | `vidu-text-to-video-q3-turbo` | 内部标识名,通常也唯一 |
| `display_name` | `Vidu q3-turbo` | 用户可见的显示名,**可能重复**(不同 pattern 下的同名模型) |
**Video 平台使用 `id`UUID作为 `model.value`**,避免 `display_name` 冲突导致的模型查找错误。`modelSelector.vue` 中 `modelGroups``value` 设为 `m.id``label` 仍用 `display_name` 显示。
`getModelId(type, modelName)` 查找优先级:`m.id === modelName` → `m.name === modelName``m.display_name === modelName`,向下兼容旧的 name/display_name 调用。
Video 的 `getDefaultModel()` 返回 `''`,不硬编码 UUID模型列表加载后由 `modelSelector` 的 watcher 自动选择第一个可用模型。
### 模型参数配置
模型参数配置通过后端 API `GET /suanli/v1/models/:id/config` 获取60s localStorage 缓存),替代了旧的本地硬编码。参数通过 `ui` 字段映射到前端控件:
| `ui` 值 | 控件 | 说明 |
|---------|------|------|
| `textarea` | Sender 内置 textarea | prompt 输入框 |
| `proportion` | `PaintingProportion` / `VideoProportion` | 比例选择 Popover`options` 含 `custom` 时可自定义宽高) |
| `resolution` | proportion 控件内部 | 分辨率子选项,与 proportion 共用 Popover |
| `dimension` | `DimensionInput` | **组合模式**:单字段 `"W*H"` 格式,后端传 `dimension.separator`,前端生成 parse/format |
| `dimensionWidth` + `dimensionHeight` | `DimensionInput` | **拆分模式**:两个独立字段(含 `min`/`max`),共享比例锁 |
| `number` | proportion 控件内部 | 自定义宽高(如 Flux 的 customWidth/customHight配合 `showWhen` 条件显示 |
| `select` | `Select`(通过 ParamGroup 或专用控件) | 通用下拉。专用控件直接使用 Select其余由 ParamGroup 动态渲染 |
| `switch` | `SwitchControl`(通过 ParamGroup | 布尔开关,纯 CSS 实现,不用外部组件库 |
| `quantity` | `Quantity` | 生成数量,`options` 必须为数字数组,上限由 `Math.max()` 派生 |
| `imageUpload` | `ImageUploader` | 参考图上传,`maxCount` 控制上限 |
| `hidden` | 无 | 静默写入默认值 |
**dimension 模式区分**`getDimConfig()`(来自 `modelConfigHelper.js`)自动检测 `ui: 'dimension'`(组合)或 `ui: 'dimensionWidth'`(拆分),两种模式共用 `DimensionInput` 组件。
**条件显示**`showWhen: { aspectRatio: 'custom' }` 使参数仅在 proportion 选 `custom` 时显示。`checkShowWhen()` 在 controls 的 `show()` 回调中调用,因直接读取 reactive 的 `paramValues[key]`computed 自动追踪依赖。
### modelConfigHelper 共享工具
`src/utils/modelConfigHelper.js` 提供 Painting/Video 双平台共用的 4 个纯函数:
| 函数 | 说明 |
|------|------|
| `syncDefaults(config, state)` | 将 API 返回的 config 同步到响应式 state。处理 `paramValues` 初始化、专用 ref 同步proportion/resolution/quantity/quality/duration/dimension、`promptPlaceholder` 同步。`resolution` 同时匹配 `ui:"resolution"``ui:"select"` |
| `syncParamValues(config, state)` | 在 `buildTaskBody` 前将专用 refproportion/resolution/quantity/quality/duration/dimension回写到 `paramValues` |
| `getDimConfig(config)` | 检测 combined/split 模式 |
| `checkShowWhen(param, paramValues)` | 检查 `showWhen` 条件 |
`state` 参数对象需包含 `modelConfig, paramValues, proportion, resolution, quantity, quality, customWidth, customHight, dimWidth, dimHeight, promptPlaceholder`。Video 平台额外包含 `duration`(用于 syncDefaults 同步时长默认值)。各平台通过包装函数适配。
### ParamGroup 动态参数渲染
当 API 返回的 params 中包含无专用控件的类型(如 `select`、`switch`),由 `ParamGroup` 统一处理:
- **容器定位**:作为平台 controls 数组的最后一项,`show()` 检查是否存在未被 `handledUis` 覆盖的参数
- **`handledUis`**`['textarea', 'proportion', 'resolution', 'dimension', 'dimensionWidth', 'dimensionHeight', 'quantity', 'imageUpload', 'hidden', 'number']` — 这些类型的参数由专用控件处理ParamGroup 跳过
- **`excludeNames` prop**:额外按参数名排除(如 `resolution`、`duration`),因为 VideoProportion/Time 已处理这些参数(即使其 `ui` 类型不在 handledUis 中)
- **双层过滤**:平台 `show()` 做第一层(判断是否渲染 ParamGroupParamGroup 内部 `dynamicParams` 做第二层(判断具体渲染哪些参数)。**两层必须保持一致**,否则会出现重复控件
```js
// Video 平台的 ParamGroup 配置
{
name: 'paramGroup',
component: markRaw(ParamGroup),
show: (config) => {
// 第一层:有未被处理的参数才显示
return config.params.some((p) => {
if (handledUis.includes(p.ui)) return false
if (p.name === 'resolution') return false
if (p.name === 'duration') return false
return true
})
},
props: (config) => ({
config,
paramValues,
excludeNames: ['resolution', 'duration'] // 第二层过滤
})
}
```
### Music 平台特有行为
Music 平台与 Painting/Video 的关键差异:
- **模式驱动控件显隐**`mode` ref常用模式/专业模式/Remix模式/编辑模式)决定大部分控件的 `show()`。切换模式时 `visibleControls` 自动更新。
- **纯音乐/歌词互斥**:常用模式下 `pureMusicGroup` 控件内含纯音乐开关 + 歌词输入弹窗。开关开启时清空歌词;关闭时弹出歌词输入框。`buildTaskBody` 中 `pureMusic` 为 true 时 `lyrics` 为空字符串。
- **参考音频**`imageUploader` 用于上传音频文件(非图片),仅在专业模式下显示(`showImageUploader()` → `mode === '专业模式'`),且为必填(`isImageRequired()` → true
- **数量限制**:专业模式下 `quantity` 强制为 1`props()` 中做 `Math.min(mode === '专业模式' ? 1 : maxQty)`)。
- **Music 独有控件**
| 控件 | `name` | `beforeModel` | 作用 |
|------|--------|---------------|------|
| `ModeSelector` | `modeSelector` | `true`(在模型选择器前) | 常用/专业/Remix/编辑 四选一 |
| `PureMusicGroup` | `pureMusicGroup` | `false` | 纯音乐开关 + 歌词输入(仅常用模式显示) |
| `LyricsInput` | `lyricsInput` | `false` | 歌词输入(仅专业模式显示) |
| `TimeControl` | `timeControl` | `false` | 时长滑块 min~max仅常用模式显示 |
- **`handledUis`**Music ParamGroup 过滤用):`['textarea', 'proportion', 'imageUpload', 'hidden', 'quantity']`,额外按名称排除 `['mode', 'pureMusic', 'lyrics', 'duration', 'quantity']`
### 计费类型映射(`getChargeType`
`taskPolling.js` 中的 `getChargeType(chargeType)` 将平台类型映射为计费数字:
| 平台 | 计费码 |
|------|--------|
| Painting | 1 |
| Video | 4 |
| Music | 5 |
| 其他 | 2 |
### 组件规范
**禁止在项目组件中使用外部 UI 库**Element Plus 等),图标除外。自定义组件使用项目自研的 `Select`、`Popover` 或纯 CSS 实现。`SwitchControl` 即为一例——纯 CSS 滑动开关,不依赖 `el-switch`
### `$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 + `pendingRequests` Map 并发去重。
- **模型配置缓存**`modelApi.js` 中 `getModelConfig` 使用 localStorage 60 秒 TTL + `pendingConfigRequests` Map 并发去重。`loadModels()` 会在获取模型列表后调用 `preloadModelConfigs` 批量预加载。
- **平台包预加载**dialogBox 顶层 `import` Painting 和 Video 平台包,触发自注册,确保首次使用时 registry 已就绪。
- **VirtualScroller 实现说明**:虽然 `package.json` 安装了 `vue-virtual-scroller`npm 包)并在 `main.js` 中全局注册,但 `display/index.vue``<VirtualScroller>` 实际使用的是 `src/components/virtual-scroller/VirtualScroller.vue` 自定义实现(通过 `unplugin-vue-components` 自动注册的同名组件遮蔽 npm 包)。`vue-virtual-scroller` npm 包当前为冗余依赖。
- **VirtualScroller 反旋转**:组件用外层 `transform: rotate(180deg)` 实现 reverse 底部锚定。所有作为 slot 插入的内容必须在根元素加 `transform: rotate(180deg)` 反旋转,否则文字/图片会颠倒显示。参考 `src/views/home/display/components/set.vue:2`
- **VirtualScroller 存在独立测试页**`src/components/virtual-scroller/test.html` + `test-data.js`,可用于验证虚拟滚动行为。
- **VirtualScroller 残留备份文件**`src/components/virtual-scroller/VirtualScroller copy.vue` 是备份副本,不应被引用或修改。
- **Element Plus `<el-input type="textarea">` 不响应 `autosize` 动态变化**`ElInput` 只在 mount 时读取 `autosize`prop 更新时不会重新计算高度。因此 `dialogBox``Sender` 必须绑定 `:key="useDisplay.Sender_variant"`,通过强制重挂载来使新的 `autosize``minRows`/`maxRows`)生效。
- **Video modelSelector pattern→modelType 映射**`getModelType()` 在 `modelSelector.vue` 中将 pattern tag 映射为 modelType——"文生视频"→`text`"图生视频"→`imageToVideo`"首尾帧"→`image`"数字人"→`digitalHuman`"全能参考"→`allReference`"主体参考"→`subjectReference`。该值用于 imageUploader 的标签文本和上传槽位数量。
- **Video `imageUploadLimit()` 累加逻辑**:对于有多个 `imageUpload` 参数的模型(如首尾帧模型的 `firstImageUrl` + `lastImageUrl`),应累加所有 `imageUpload` 参数的 `maxCount`,而非只取第一个。否则首尾帧模型只显示一个上传槽位,尾帧上传无法触发。
- **`buildTaskBody` 参考图映射**Video 平台在 `buildTaskBody` 中需将 `referenceImages` 按索引顺序写入 `imageUpload` 参数(`referenceImages[0]` → `firstImageUrl``referenceImages[1]` → `lastImageUrl`),否则图片数据不会包含在任务请求中。
### VirtualScroller 坐标系统
`VirtualScroller` 使用 180deg 旋转实现 reverse 模式。`handleScroll` 内部将容器物理坐标映射为"页面逻辑坐标"后通过 `emit('scroll', {...})` 发出:
| 字段 | 计算 | 含义 |
|------|------|------|
| `distanceToPageTop` | `scrollHeight - scrollTop - clientHeight` | 距离页面**顶部**(旧内容端)的距离 |
| `distanceToPageBottom` | `scrollTop` | 距离页面**底部**(新内容端)的距离 |
| `isAtPageTop` | `distanceToPageTop <= 0` | 滚动到页面最顶部 |
| `isAtPageBottom` | `distanceToPageBottom <= 0` | 滚动到页面最底部 |
**注意**`distanceToPageBottom = scrollTop`(不是 `distanceToPageTop`),表示从底部向上滚动的距离。判断"用户向上滚动了多少"应使用此字段。
### 输入框滚动收缩机制
滚动列表时提示词输入框自动收缩为一行,这是跨 4 个文件的响应式链路:
1. `VirtualScroller` 发出 `scroll` 事件(含 `isAtPageBottom`、`distanceToPageBottom` 等)
2. `display/index.vue``handleScroll` 根据滚动位置切换 `useDisplay.Sender_variant`
- `isAtPageBottom`(底部)→ `'updown'`展开5-9 行)
- `distanceToPageBottom >= 350`(已滚离底部 350px`'default'`收缩1 行)
- 中间状态 → `'updown'`(展开)
3. `display.js` store 中 `Sender_variant` 是响应式 ref
4. `dialogBox/index.vue``autoSizeConfig` 计算属性读取 `Sender_variant`,返回 `{ minRows, maxRows }`
5. `Sender` 组件通过 `:key="useDisplay.Sender_variant"` **强制重挂载**(因为 Element Plus `ElInput` 不支持 `autosize` 动态更新),新实例以正确的行数初始化
滚动阈值 350px 对应 `VirtualScroller``bottomPlaceholderHeight`
### 接口速查
| 函数 | 端点 | 用途 |
|------|------|------|
| `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(UUID), name, display_name, tags, disabled?}`)。`id`=UUID 主键,`name`=内部标识,`display_name`=用户可见名 |
| `requestModelConfigsBatch` | POST `/suanli/v1/models/configs` | 批量获取模型配置body: `{ modelIds: [...] }` |
| `requestModelConfig` | GET `/suanli/v1/models/:id/config` | 单条模型配置60s 缓存优先) |
| `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 前缀(由环境变量 `VITE_API_TASK_PREFIX` / `VITE_API_PAY_PREFIX` / `VITE_API_AIGC_PREFIX` 定义)切换后端:
| URL 前缀(由环境变量定义) | 目标环境变量 |
|---------------------------|-------------|
| `VITE_API_TASK_PREFIX` 对应前缀 | `VITE_API_TASK_TARGET` |
| `VITE_API_PAY_PREFIX` 对应前缀 | `VITE_API_PAY_TARGET` |
| `VITE_API_AIGC_PREFIX` 对应前缀 | `VITE_API_AIGC_TARGET` |
| 其他 | `VITE_API_BASE_URL`(默认) |
**注意**:前缀字符串本身来自环境变量(如 `VITE_API_TASK_PREFIX=/suanli`),不是硬编码。`request.js` 在初始化时读取这些变量,构建 prefix→target 映射表。
### 环境变量速查
```bash
# .env.development
VITE_BASE = '/' # 应用基础路径
VITE_API_PREFIX = '/api' # 主服务前缀
VITE_API_BASE_URL = 'http://...' # 主服务(默认目标)
VITE_API_PAY_PREFIX = '/pay' # 支付服务前缀
VITE_API_PAY_TARGET = 'http://...' # 支付服务目标
VITE_API_TASK_PREFIX = '/suanli' # 任务服务前缀
VITE_API_TASK_TARGET = 'http://...' # 任务服务目标
VITE_API_WORKFLOW_UPLOAD = 'http://...' # 图片上传地址imageUploader 组件 action
VITE_OPEN_DEVTOOLS = false # 是否开启开发者工具
FILE_OPEN_PREVIEW = true # 是否开启 KKFileView 预览
```
`vite.config.js``envPrefix: ['VITE', 'FILE']`,因此只有以 `VITE_``FILE_` 开头的变量会被暴露给客户端代码。
### 平台编码映射
| 类型 | 平台编码 |
|------|----------|
| Painting | `ai_painting_talk` |
| Video | `ai_video_talk` |
| Music | `ai_music_talk` |
映射函数 `getPlatformCode()` 位于 `utils/modelApi.js`
### 自动导入
- `unplugin-auto-import`:自动导入 Vue/Router/Pinia API
- `unplugin-vue-components`:自动注册 `src/components/` 下的组件和 Element Plus 组件,**生成 `components.d.ts`(勿手动编辑)**
- Element Plus 图标通过 `unplugin-icons` 按需加载