Initial commit: Pixel AI comic/video creation platform
- FastAPI backend with SQLModel, Alembic migrations, AgentScope agents - Next.js 15 frontend with React 19, Tailwind, Zustand, React Flow - Multi-provider AI system (DashScope, Kling, MiniMax, Volcengine, OpenAI, etc.) - All HTTP clients migrated from sync requests to async httpx - Admin-managed API keys via environment variables - SSRF vulnerability fixed in ensure_url()
This commit is contained in:
608
docs/API.md
Normal file
608
docs/API.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# Pixel API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了 Pixel 后端 API 的核心端点,重点介绍模型配置和生成 API。
|
||||
|
||||
**基础 URL**: `http://localhost:8000/api/v1`
|
||||
|
||||
**认证**: 目前不需要认证(开发环境)
|
||||
|
||||
---
|
||||
|
||||
## 模型 ID 格式
|
||||
|
||||
所有 API 使用**复合 ID 格式**来标识模型:
|
||||
|
||||
```
|
||||
provider/model_key
|
||||
```
|
||||
|
||||
**示例**:
|
||||
- `dashscope/qwen-image` - DashScope 的 Qwen 图片生成模型
|
||||
- `dashscope/wan2.6-video` - DashScope 的 Wan 2.6 视频生成模型
|
||||
- `volcengine/doubao-tts` - 火山引擎的豆包 TTS 模型
|
||||
- `modelscope/qwen-image` - ModelScope 的 Qwen 图片生成模型
|
||||
|
||||
**格式规则**:
|
||||
- 必须包含一个 `/` 分隔符
|
||||
- `provider` 和 `model_key` 都不能为空
|
||||
- 不支持多个 `/` 分隔符
|
||||
|
||||
---
|
||||
|
||||
## 模型配置 API
|
||||
|
||||
### 获取所有模型配置
|
||||
|
||||
获取系统中所有可用的模型配置,按类型分组。
|
||||
|
||||
**端点**: `GET /api/v1/models`
|
||||
|
||||
**响应格式**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"image": {
|
||||
"dashscope/qwen-image": {
|
||||
"id": "dashscope/qwen-image",
|
||||
"name": "Qwen Image",
|
||||
"type": "image",
|
||||
"provider": "dashscope",
|
||||
"model_key": "qwen-image",
|
||||
"is_default": true,
|
||||
"enabled": true,
|
||||
"capabilities": {
|
||||
"supportsLora": true,
|
||||
"supportsRefImage": true
|
||||
},
|
||||
"resolutions": {
|
||||
"1K": {
|
||||
"16:9": "1280*720",
|
||||
"1:1": "1024*1024"
|
||||
},
|
||||
"2K": {
|
||||
"16:9": "2560*1440",
|
||||
"1:1": "2048*2048"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelscope/qwen-image": {
|
||||
"id": "modelscope/qwen-image",
|
||||
"name": "ModelScope Qwen Image",
|
||||
"type": "image",
|
||||
"provider": "modelscope",
|
||||
"model_key": "qwen-image",
|
||||
"is_default": false,
|
||||
"enabled": true,
|
||||
"capabilities": {
|
||||
"supportsLora": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"dashscope/wan2.6-video": {
|
||||
"id": "dashscope/wan2.6-video",
|
||||
"name": "Wan 2.6 Video",
|
||||
"type": "video",
|
||||
"provider": "dashscope",
|
||||
"model_key": "wan2.6-video",
|
||||
"is_default": true,
|
||||
"enabled": true,
|
||||
"durations": {
|
||||
"5s": "5秒",
|
||||
"10s": "10秒"
|
||||
}
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
"volcengine/doubao-tts": {
|
||||
"id": "volcengine/doubao-tts",
|
||||
"name": "豆包 TTS",
|
||||
"type": "audio",
|
||||
"provider": "volcengine",
|
||||
"model_key": "doubao-tts",
|
||||
"is_default": true,
|
||||
"enabled": true,
|
||||
"voices": [
|
||||
{
|
||||
"id": "zh_female_qingxin",
|
||||
"name": "清新女声",
|
||||
"language": "zh"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"llm": {
|
||||
"dashscope/qwen-plus": {
|
||||
"id": "dashscope/qwen-plus",
|
||||
"name": "Qwen Plus",
|
||||
"type": "llm",
|
||||
"provider": "dashscope",
|
||||
"model_key": "qwen-plus",
|
||||
"is_default": true,
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | string | 复合 ID,格式为 `provider/model_key` |
|
||||
| `name` | string | 模型显示名称 |
|
||||
| `type` | string | 模型类型:`image`、`video`、`audio`、`llm` |
|
||||
| `provider` | string | 提供商名称 |
|
||||
| `model_key` | string | 模型键名 |
|
||||
| `is_default` | boolean | 是否为该类型的默认模型 |
|
||||
| `enabled` | boolean | 是否启用 |
|
||||
| `capabilities` | object | 模型能力(可选) |
|
||||
| `resolutions` | object | 支持的分辨率(图片模型) |
|
||||
| `durations` | object | 支持的时长(视频模型) |
|
||||
| `voices` | array | 支持的音色(音频模型) |
|
||||
|
||||
---
|
||||
|
||||
## 图片生成 API
|
||||
|
||||
### 生成图片
|
||||
|
||||
创建图片生成任务。
|
||||
|
||||
**端点**: `POST /api/v1/generations/image`
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "a beautiful sunset over mountains",
|
||||
"model": "dashscope/qwen-image",
|
||||
"negativePrompt": "blurry, low quality",
|
||||
"resolution": "2K",
|
||||
"aspectRatio": "16:9",
|
||||
"n": 1,
|
||||
"imageInputs": ["https://example.com/ref-image.jpg"],
|
||||
"extraParams": {
|
||||
"loras": [
|
||||
{
|
||||
"model": "dashscope/anime-style",
|
||||
"weight": 0.8
|
||||
}
|
||||
]
|
||||
},
|
||||
"projectId": "proj_123",
|
||||
"source": "canvas",
|
||||
"sourceId": "canvas_456"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `prompt` | string | ✅ | 生成提示词 |
|
||||
| `model` | string | ✅ | 模型复合 ID,格式:`provider/model_key` |
|
||||
| `negativePrompt` | string | ❌ | 负面提示词 |
|
||||
| `resolution` | string | ❌ | 分辨率级别(如 `1K`、`2K`),默认 `1K` |
|
||||
| `aspectRatio` | string | ❌ | 宽高比(如 `16:9`、`1:1`) |
|
||||
| `n` | integer | ❌ | 生成数量,默认 1 |
|
||||
| `imageInputs` | array | ❌ | 参考图片 URL 列表 |
|
||||
| `extraParams` | object | ❌ | 额外参数(如 LoRA 配置) |
|
||||
| `projectId` | string | ❌ | 项目 ID |
|
||||
| `source` | string | ❌ | 来源标识 |
|
||||
| `sourceId` | string | ❌ | 来源对象 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"task_id": "task_abc123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "400",
|
||||
"message": "Model must be in format 'provider/model_key', got: 'qwen-image'. Example: 'dashscope/qwen-image'"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "404",
|
||||
"message": "Model 'invalid/model' not found. Available models can be fetched from /api/v1/models"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 视频生成 API
|
||||
|
||||
### 生成视频
|
||||
|
||||
创建视频生成任务。
|
||||
|
||||
**端点**: `POST /api/v1/generations/video`
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "a cat playing with a ball",
|
||||
"model": "dashscope/wan2.6-video",
|
||||
"negativePrompt": "static, blurry",
|
||||
"duration": "5s",
|
||||
"resolution": "720p",
|
||||
"n": 1,
|
||||
"imageInputs": ["https://example.com/first-frame.jpg"],
|
||||
"projectId": "proj_123"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `prompt` | string | ✅ | 生成提示词 |
|
||||
| `model` | string | ✅ | 模型复合 ID,格式:`provider/model_key` |
|
||||
| `negativePrompt` | string | ❌ | 负面提示词 |
|
||||
| `duration` | string | ❌ | 视频时长(如 `5s`、`10s`) |
|
||||
| `resolution` | string | ❌ | 分辨率(如 `720p`、`1080p`) |
|
||||
| `n` | integer | ❌ | 生成数量,默认 1 |
|
||||
| `imageInputs` | array | ❌ | 参考图片 URL 列表(首帧) |
|
||||
| `projectId` | string | ❌ | 项目 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"task_id": "task_xyz789"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 音频生成 API
|
||||
|
||||
### 生成音频
|
||||
|
||||
创建音频生成任务(TTS)。
|
||||
|
||||
**端点**: `POST /api/v1/generations/audio`
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "Hello, welcome to Pixel AI!",
|
||||
"model": "volcengine/doubao-tts",
|
||||
"voice": "zh_female_qingxin",
|
||||
"speed": 1.0,
|
||||
"projectId": "proj_123"
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `prompt` | string | ✅ | 要转换的文本 |
|
||||
| `model` | string | ✅ | 模型复合 ID,格式:`provider/model_key` |
|
||||
| `voice` | string | ❌ | 音色 ID |
|
||||
| `speed` | float | ❌ | 语速,默认 1.0 |
|
||||
| `projectId` | string | ❌ | 项目 ID |
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"task_id": "task_audio123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务查询 API
|
||||
|
||||
### 获取任务状态
|
||||
|
||||
查询生成任务的状态和结果。
|
||||
|
||||
**端点**: `GET /api/v1/tasks/{task_id}`
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": "task_abc123",
|
||||
"type": "image",
|
||||
"status": "completed",
|
||||
"model": "dashscope/qwen-image",
|
||||
"params": {
|
||||
"prompt": "a beautiful sunset over mountains",
|
||||
"resolution": "2K",
|
||||
"aspectRatio": "16:9"
|
||||
},
|
||||
"result": {
|
||||
"images": [
|
||||
{
|
||||
"url": "https://example.com/generated-image.jpg",
|
||||
"width": 2560,
|
||||
"height": 1440
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": "2026-02-11T10:00:00Z",
|
||||
"updated_at": "2026-02-11T10:00:30Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**任务状态**:
|
||||
- `pending` - 等待处理
|
||||
- `processing` - 处理中
|
||||
- `completed` - 已完成
|
||||
- `failed` - 失败
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
所有错误响应遵循统一格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "400",
|
||||
"message": "详细的错误描述",
|
||||
"request_id": "req_123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| `400` | 请求参数错误(如模型 ID 格式不正确) |
|
||||
| `404` | 资源不存在(如模型不存在) |
|
||||
| `500` | 服务器内部错误 |
|
||||
| `503` | 服务暂时不可用 |
|
||||
|
||||
### 模型 ID 格式错误
|
||||
|
||||
**错误请求**:
|
||||
```json
|
||||
{
|
||||
"prompt": "a cat",
|
||||
"model": "qwen-image"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"code": "400",
|
||||
"message": "Model must be in format 'provider/model_key', got: 'qwen-image'. Example: 'dashscope/qwen-image'"
|
||||
}
|
||||
```
|
||||
|
||||
### 模型不存在
|
||||
|
||||
**错误请求**:
|
||||
```json
|
||||
{
|
||||
"prompt": "a cat",
|
||||
"model": "invalid/model"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"code": "404",
|
||||
"message": "Model 'invalid/model' not found. Available models can be fetched from /api/v1/models"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用复合 ID
|
||||
|
||||
始终使用完整的复合 ID 格式:
|
||||
|
||||
✅ **正确**:
|
||||
```json
|
||||
{
|
||||
"model": "dashscope/qwen-image"
|
||||
}
|
||||
```
|
||||
|
||||
❌ **错误**:
|
||||
```json
|
||||
{
|
||||
"model": "qwen-image"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取可用模型
|
||||
|
||||
在调用生成 API 之前,先获取可用模型列表:
|
||||
|
||||
```javascript
|
||||
// 1. 获取模型配置
|
||||
const response = await fetch('/api/v1/models');
|
||||
const { data } = await response.json();
|
||||
|
||||
// 2. 使用模型 ID
|
||||
const imageModels = data.image;
|
||||
const modelId = Object.keys(imageModels)[0]; // "dashscope/qwen-image"
|
||||
|
||||
// 3. 调用生成 API
|
||||
await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: "a cat",
|
||||
model: modelId
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
始终处理可能的错误:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: "a cat",
|
||||
model: "dashscope/qwen-image"
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error('API Error:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await response.json();
|
||||
console.log('Task ID:', data.task_id);
|
||||
} catch (error) {
|
||||
console.error('Network Error:', error);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 轮询任务状态
|
||||
|
||||
生成任务是异步的,需要轮询状态:
|
||||
|
||||
```javascript
|
||||
async function waitForTask(taskId) {
|
||||
while (true) {
|
||||
const response = await fetch(`/api/v1/tasks/${taskId}`);
|
||||
const { data } = await response.json();
|
||||
|
||||
if (data.status === 'completed') {
|
||||
return data.result;
|
||||
}
|
||||
|
||||
if (data.status === 'failed') {
|
||||
throw new Error('Task failed');
|
||||
}
|
||||
|
||||
// 等待 2 秒后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧格式迁移
|
||||
|
||||
如果你的代码使用了旧的 `provider` 参数,需要进行以下修改:
|
||||
|
||||
**旧代码**:
|
||||
```javascript
|
||||
await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: "a cat",
|
||||
model: "qwen-image",
|
||||
provider: "dashscope" // ❌ 不再支持
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**新代码**:
|
||||
```javascript
|
||||
await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
prompt: "a cat",
|
||||
model: "dashscope/qwen-image" // ✅ 使用复合 ID
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 前端辅助函数
|
||||
|
||||
如果你的前端代码有 `getProviderForModel` 函数,可以直接删除:
|
||||
|
||||
**旧代码**:
|
||||
```typescript
|
||||
const provider = getProviderForModel(model, 'image'); // ❌ 删除
|
||||
await ImageGenerationService.generate({
|
||||
model,
|
||||
provider, // ❌ 删除
|
||||
prompt
|
||||
});
|
||||
```
|
||||
|
||||
**新代码**:
|
||||
```typescript
|
||||
await ImageGenerationService.generate({
|
||||
model, // ✅ 直接使用复合 ID
|
||||
prompt
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 交互式文档
|
||||
|
||||
访问 FastAPI 自动生成的交互式文档:
|
||||
|
||||
- **Swagger UI**: http://localhost:8000/docs
|
||||
- **ReDoc**: http://localhost:8000/redoc
|
||||
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v2.0.0 (2026-02-11)
|
||||
|
||||
- ✅ 统一使用复合 ID 格式(`provider/model_key`)
|
||||
- ✅ 移除冗余的 `provider` 参数
|
||||
- ✅ 模型配置 API 返回按类型分组的 HashMap
|
||||
- ✅ 改进错误消息,提供清晰的格式说明
|
||||
- ✅ 简化前端 API 调用
|
||||
|
||||
---
|
||||
|
||||
## 支持
|
||||
|
||||
如有问题,请联系开发团队或查看:
|
||||
- [GitHub Issues](https://github.com/pixel-ai/pixel/issues)
|
||||
- [开发文档](../README.md)
|
||||
165
docs/FRONTEND_OPTIMIZATION.md
Normal file
165
docs/FRONTEND_OPTIMIZATION.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 前端优化建议
|
||||
|
||||
本文档基于对当前前端代码的检查,给出可执行的优化建议,按优先级和类别组织。
|
||||
|
||||
---
|
||||
|
||||
## 一、已修复问题
|
||||
|
||||
### 1. 项目设置保存 API 路径错误
|
||||
|
||||
- **位置**:`frontend/src/components/canvas/panels/management/project-details/tabs/SettingsTab.tsx`
|
||||
- **问题**:使用 `fetch(\`${OpenAPI.BASE}/api/projects/${projectId}\`)` 请求的是 `/api/projects/:id`,而后端路由为 `/api/v1/projects/:id`,导致 404。
|
||||
- **修复**:改为使用生成的 API 客户端 `DefaultService.updateProject(projectId, updateData)`,保证请求路径为 `/api/v1/projects/:id`。
|
||||
- **说明**:若后端需要持久化 `type`、`status`、`defaultImageModel` 等,需在 `UpdateProjectRequest` 中声明对应字段。
|
||||
|
||||
---
|
||||
|
||||
## 二、配置与一致性
|
||||
|
||||
### 2. API Base URL 与 Rewrites 统一(已落地)
|
||||
|
||||
- **现状**:
|
||||
- `next.config.mjs` 使用 `API_URL`(无 `NEXT_PUBLIC_`)做 rewrites。
|
||||
- `frontend/src/lib/client.ts` 使用 `NEXT_PUBLIC_API_URL || 'http://localhost:8000'` 设置 `OpenAPI.BASE`。
|
||||
- **已做**:
|
||||
- `client.ts` 改为 `OpenAPI.BASE = process.env.NEXT_PUBLIC_API_URL ?? ''`,默认走同源 + rewrites。
|
||||
- `next.config.mjs` 增加注释说明 `API_URL` 与 `NEXT_PUBLIC_API_URL` 的职责。
|
||||
- 新增 `frontend/.env.example`,说明 `API_URL`(服务端 rewrites)与 `NEXT_PUBLIC_API_URL`(浏览器端,不设则用同源)。
|
||||
|
||||
### 3. 依赖声明(已落地)
|
||||
|
||||
- **现状**:`canvasStore.ts` 使用 `lodash-es`(如 `debounce`),但 `package.json` 中只有 `@types/lodash-es`(devDependency),`lodash-es` 为间接依赖。
|
||||
- **已做**:在 `dependencies` 中显式添加 `lodash-es: ^4.17.21`。
|
||||
|
||||
---
|
||||
|
||||
## 三、数据请求与状态
|
||||
|
||||
### 4. 项目页用 React Query 替代手写 fetch(已落地)
|
||||
|
||||
- **位置**:`frontend/src/app/project/[id]/page.tsx`
|
||||
- **已做**:
|
||||
- 新增 `useProjectWorkspace(projectId)`(`useProjects.ts`),内部使用 `useQuery` + `getProject(id, false, true)`,缓存 key 为 `projectKeys.workspace(id)`,与列表/详情隔离。
|
||||
- 项目页改为使用该 hook,无 projectId 时展示「无效的项目」,loading 时展示 `LoadingState`,error 时展示错误文案 +「重试」按钮(调用 `refetch`)。
|
||||
- 数据同步到 `useProjectStore` 在 hook 的 `useEffect` 中完成,依赖仅 `projectId` 与 `query.data`,避免 setter 导致重复请求。
|
||||
|
||||
### 5. 项目页初始化逻辑(已落地)
|
||||
|
||||
- **已做**:在 `useProjectWorkspace` 内用 `initialEpisodeSetRef` 保证「有集数据且当前无选中集时自动选中第一集」只执行一次;切换 `projectId` 时重置该 ref,避免 refetch 覆盖用户已选集。
|
||||
|
||||
---
|
||||
|
||||
## 四、性能与包体积
|
||||
|
||||
### 6. 大组件与动态加载(已落地)
|
||||
|
||||
- **现状**:`Canvas.tsx`、`AppNode.tsx` 已对部分重量级组件使用 `next/dynamic`(ImageCropper、SketchEditor、SettingsModal、ProjectDetailsModal、ExpandedView、RightSidebar、各 Node Renderer),方向正确。
|
||||
- **已做**:
|
||||
- **Canvas**:底部输入栏 `InteractionInput` 改为 `next/dynamic` 按需加载,首屏不阻塞;展示「加载输入栏...」占位。
|
||||
- **InteractionInput**:`ChatDisplay` 改为 `next/dynamic`,仅在用户打开「助手」聊天时加载对应 chunk。
|
||||
- **next.config.mjs**:`optimizePackageImports` 增加 `@radix-ui/react-dialog`、`@radix-ui/react-dropdown-menu`、`@radix-ui/react-select`、`@radix-ui/react-tabs`,与现有 `@lobehub/icons`、`lucide-react`、`framer-motion` 一起做 barrel 优化;可定期检查是否再加大库。
|
||||
|
||||
### 7. 画布相关 memo 与订阅(已落地)
|
||||
|
||||
- **现状**:Canvas 已对 Sidebar、RightSidebar、CanvasContextMenu 使用 `memo`;Zustand 需注意选择器粒度。
|
||||
- **已做**:
|
||||
- **单字段订阅**:画布相关 hooks 与组件中,凡只用 store 单一字段的,一律改为 `useProjectStore((s) => s.xxx)` / `useCanvasStore((s) => s.xxx)`(如 `useNodeExecution`、`useCanvasPersistence`、`useGenerationHistory`、`CanvasManager`、`StoryboardDetails`、`CreateAssetForm`)。
|
||||
- **多字段订阅**:需要多个字段的改用 `useShallow`,避免整 store 订阅导致无关变更触发重渲染。已改:`useNodeActions`、`useCanvasInitialization`、`useAssetSaver`、`StoryboardsTab`、`ScriptTab`、`AssetsTab`、`ProjectDetailsModal`、`EpisodeDetails`、`CanvasRightDock`、`OverviewTab`、`SettingsTab`。
|
||||
- **回调稳定性**:上述从 store 取出的 action(如 `updateAsset`、`setProject`)由 Zustand 提供,引用稳定,子组件不会因回调引用变而多余重渲染。
|
||||
|
||||
---
|
||||
|
||||
## 五、错误处理与健壮性
|
||||
|
||||
### 8. 使用 ErrorBoundary 包裹画布(已落地)
|
||||
|
||||
- **位置**:`frontend/src/app/project/[id]/page.tsx`
|
||||
- **已做**:项目页已用 `CanvasErrorBoundary` 包裹 `<Canvas />`,画布渲染异常会展示 fallback,不再直接白屏。
|
||||
|
||||
### 9. 控制台与类型(进行中)
|
||||
|
||||
- **已做(第一批)**:
|
||||
- `logger` 参数类型从 `any[]` 收紧为 `unknown[]`。
|
||||
- 核心链路改造为 `logger`:`OfflineSyncProvider`、`CanvasPersistenceService`、`useCanvasInitialization`。
|
||||
- **后续**:其余 `console.*` 与 `any` 仍较多,建议按模块持续替换(交互输入、项目管理、设置页优先)。
|
||||
|
||||
---
|
||||
|
||||
## 六、代码结构与可维护性
|
||||
|
||||
### 10. 超大类/文件拆分(进行中)
|
||||
|
||||
- **现状**:`InteractionInput.tsx`、`AutoResizeTextarea.tsx` 等单文件超过 700 行,逻辑多、状态多。
|
||||
- **风险**:
|
||||
- 修改任意一处逻辑都可能影响到完全无关的功能,容易产生回归。
|
||||
- 代码评审成本高,新同学上手困难。
|
||||
- 无法对局部逻辑做针对性单测或 Storybook。
|
||||
- **建议(结构拆分)**:
|
||||
- 按“Tab/模式”(如 text / image / video / audio)、或“编辑态 vs 创建态”拆成子组件或自定义 hooks(如 `useInteractionEditMode`、`useGenerationSubmit`),主文件只做组合与布局。
|
||||
- 将 **通用逻辑**(如输入框自适应高度、回车发送、快捷键、草稿保存)沉淀为 `useXXX` hooks 或 `components/common/` 下的基础组件,避免在多个页面重复实现。
|
||||
- 对于复用度低但逻辑复杂的块(如“生成参数面板”“节点执行状态展示”),放到 `components/canvas/interaction/` 等子目录中,以“目录 + index 组件”形式归类。
|
||||
- **建议(渐进式重构策略)**:
|
||||
- 以“**先抽 hooks 再拆 UI**”为原则:先把明显的纯业务逻辑(如表单状态、提交节流、API 调用)抽到 hooks,再将 View 拆分为多个视觉组件。
|
||||
- 每次 PR 控制拆分粒度(例如一次只拆出一个 Tab 或一个大功能块),并配合简单的回归路径说明(“本次拆分仅影响 xx Tab 的输入与发送”)。
|
||||
- 对已拆分出的 hooks/子组件,在命名与文件路径上保持稳定,避免后续再次大规模移动。
|
||||
|
||||
- **已做(第一步)**:
|
||||
- 将 `AutoResizeTextarea` 中 mention 解析与 chip 构建抽离到 `controls/interaction-input/mentionUtils.ts`,减少主组件体积并提高复用性。
|
||||
|
||||
### 11. API 调用方式统一
|
||||
|
||||
- **现状**:绝大多数使用 `DefaultService.*`,仅 SettingsTab 曾用裸 `fetch`(已改为 DefaultService)。
|
||||
- **风险**:
|
||||
- 手写 URL(如 `/api/v1/...`)容易与后端路由或版本前缀不一致,导致线上才暴露问题。
|
||||
- 缺失统一的错误处理与类型约束,不利于后续做全局 toast、重试等能力。
|
||||
- **建议(编码规范)**:
|
||||
- 新功能 **一律优先使用** 生成的 `DefaultService`/各 Service,不再手写 `fetch` + 字符串路径。
|
||||
- 仅在极少数场景(如第三方 webhook、中转代理)允许使用裸 `fetch`,并在代码上方用注释说明原因。
|
||||
- 不在组件内直接使用 `fetch`,而是封装到 `frontend/src/lib/api/xxx.ts` 或对应 Service wrapper 中,组件只关心“调用哪个方法”。
|
||||
- **建议(基础设施)**:
|
||||
- 如有需要,可在 `lib/client.ts` 或单独文件中,封装统一的 `request`/`mutation` helper(例如整合 React Query、错误提示、埋点等),再由 `DefaultService` 在内部复用。
|
||||
- 为常用的实体(如 Project、Episode、Asset)提供对应 `useProjectQuery`、`useProjectMutation` 等 hooks,对外只暴露 hooks,不暴露底层 Service 调用细节。
|
||||
- 在代码评审 Checklist 中增加一项:“**是否使用了生成的 Service,而不是手写 fetch**”,借此约束新增代码。
|
||||
|
||||
### 12. 国际化 (i18n)
|
||||
|
||||
- **现状**:已接入 i18next,部分文案使用 `useTranslation`,同时存在大量中文硬编码。
|
||||
- **建议**:若计划多语言,逐步将界面文案迁到 i18n key;若短期仅中文,可在文档中说明当前策略,避免混用导致维护成本增加。
|
||||
|
||||
---
|
||||
|
||||
## 七、测试与质量
|
||||
|
||||
### 13. React Query 默认配置(已落地)
|
||||
|
||||
- **已做**:
|
||||
- `QueryProvider` 默认值调整为更保守策略:`staleTime: 1min`、`retry: 1`、`refetchOnReconnect: true`。
|
||||
- 在 `useProjects` / `useProject` / `useProjectWorkspace` 上显式配置 `staleTime`、`retry`、`retryDelay`、`refetchOnReconnect`;其中 workspace 查询单独设置 `staleTime: 60s`,避免沿用全局一刀切。
|
||||
|
||||
### 14. 端到端与关键路径(进行中)
|
||||
|
||||
- **已做(基线)**:
|
||||
- 新增 Playwright 配置:`frontend/playwright.config.ts`。
|
||||
- 新增关键路径 E2E:`frontend/tests/e2e/project-canvas.spec.ts`(进入项目 → 载入画布 → 新增提示词节点 → 编辑内容 → 触发保存并断言保存请求)。
|
||||
- 新增脚本:`frontend/package.json` 中 `e2e` / `e2e:headed`。
|
||||
- **后续**:补充分镜编辑、素材拖拽、失败重试等场景。
|
||||
|
||||
---
|
||||
|
||||
## 八、小结优先级
|
||||
|
||||
| 优先级 | 项 | 说明 |
|
||||
|--------|----|------|
|
||||
| 高 | 项目设置 API 路径 | 已修复 |
|
||||
| 高 | 项目页 loading/error 与 React Query | 已落地 |
|
||||
| 高 | 画布 ErrorBoundary | 避免白屏 |
|
||||
| 中 | API Base 与 env 说明 | 减少部署/联调困惑 |
|
||||
| 中 | lodash-es 显式依赖 | 依赖健康 |
|
||||
| 中 | 大组件与动态加载 | 已落地 |
|
||||
| 中 | 画布 memo 与细粒度订阅 | 已落地 |
|
||||
| 中 | 大组件拆分(超大类拆 hooks/子组件) | 性能与可维护性 |
|
||||
| 低 | console → logger、减少 any | 长期可维护性 |
|
||||
| 低 | i18n 策略统一 | 若有多语言计划 |
|
||||
|
||||
如需对某一项做具体改动(例如贴出 patch 或分步实现),可以指定编号或文件路径继续细化。
|
||||
668
docs/development-guide.md
Normal file
668
docs/development-guide.md
Normal file
@@ -0,0 +1,668 @@
|
||||
# Pixel 开发指南
|
||||
|
||||
本指南提供 Pixel 项目的开发最佳实践、API 使用说明和常见问题解决方案。
|
||||
|
||||
## 目录
|
||||
|
||||
- [模型配置](#模型配置)
|
||||
- [API 调用](#api-调用)
|
||||
- [复合 ID 格式](#复合-id-格式)
|
||||
- [错误处理](#错误处理)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [常见问题](#常见问题)
|
||||
|
||||
---
|
||||
|
||||
## 模型配置
|
||||
|
||||
### 获取可用模型
|
||||
|
||||
使用 `/api/v1/models` 端点获取所有可用的模型配置:
|
||||
|
||||
```typescript
|
||||
// 前端示例
|
||||
const response = await fetch('/api/v1/models');
|
||||
const data = await response.json();
|
||||
|
||||
// 响应格式
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"image": {
|
||||
"dashscope/qwen-image": {
|
||||
"id": "dashscope/qwen-image",
|
||||
"name": "Qwen Image",
|
||||
"type": "image",
|
||||
"provider": "dashscope",
|
||||
"model_key": "qwen-image",
|
||||
"is_default": true,
|
||||
"enabled": true,
|
||||
"capabilities": {
|
||||
"supportsLora": true,
|
||||
"supportsRefImage": true
|
||||
},
|
||||
"resolutions": {
|
||||
"1K": { "16:9": "1280*720", "1:1": "1024*1024" },
|
||||
"2K": { "16:9": "2560*1440", "1:1": "2048*2048" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"video": { ... },
|
||||
"audio": { ... },
|
||||
"llm": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 模型配置结构
|
||||
|
||||
每个模型配置包含以下字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | string | 复合 ID,格式:`provider/model_key` |
|
||||
| `name` | string | 显示名称 |
|
||||
| `type` | string | 模型类型:`image`、`video`、`audio`、`llm` |
|
||||
| `provider` | string | 提供商名称 |
|
||||
| `model_key` | string | 模型键 |
|
||||
| `is_default` | boolean | 是否为默认模型 |
|
||||
| `enabled` | boolean | 是否启用 |
|
||||
| `capabilities` | object | 模型能力(可选) |
|
||||
| `resolutions` | object | 支持的分辨率(图片/视频) |
|
||||
| `durations` | object | 支持的时长(视频) |
|
||||
| `voices` | array | 支持的声音(音频) |
|
||||
|
||||
---
|
||||
|
||||
## API 调用
|
||||
|
||||
### 复合 ID 格式
|
||||
|
||||
**重要**:所有 API 调用必须使用复合 ID 格式:`provider/model_key`
|
||||
|
||||
#### ✅ 正确示例
|
||||
|
||||
```typescript
|
||||
// 图片生成
|
||||
await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: "dashscope/qwen-image", // ✅ 复合 ID 格式
|
||||
prompt: "a beautiful sunset"
|
||||
})
|
||||
});
|
||||
|
||||
// 视频生成
|
||||
await fetch('/api/v1/generations/video', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: "dashscope/wan2.6-video", // ✅ 复合 ID 格式
|
||||
prompt: "a cat playing"
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
#### ❌ 错误示例
|
||||
|
||||
```typescript
|
||||
// ❌ 错误:缺少 provider
|
||||
{
|
||||
model: "qwen-image",
|
||||
prompt: "a cat"
|
||||
}
|
||||
|
||||
// ❌ 错误:使用了已废弃的 provider 参数
|
||||
{
|
||||
model: "dashscope/qwen-image",
|
||||
provider: "dashscope", // 不再需要
|
||||
prompt: "a cat"
|
||||
}
|
||||
```
|
||||
|
||||
### 图片生成 API
|
||||
|
||||
**端点**:`POST /api/v1/generations/image`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// 必需参数
|
||||
model: string; // 复合 ID: "provider/model_key"
|
||||
prompt: string; // 生成提示词
|
||||
|
||||
// 可选参数
|
||||
negativePrompt?: string; // 负面提示词
|
||||
imageInputs?: string[]; // 参考图片 URL 列表
|
||||
resolution?: string; // 分辨率级别: "1K", "2K", "4K"
|
||||
aspectRatio?: string; // 宽高比: "16:9", "1:1", "9:16"
|
||||
n?: number; // 生成数量,默认 1
|
||||
extraParams?: object; // 额外参数
|
||||
|
||||
// 元数据
|
||||
projectId?: string; // 项目 ID
|
||||
source?: string; // 来源
|
||||
sourceId?: string; // 来源 ID
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"task_id": "task_123456"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**完整示例**:
|
||||
|
||||
```typescript
|
||||
const response = await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: "dashscope/qwen-image",
|
||||
prompt: "a beautiful sunset over mountains",
|
||||
negativePrompt: "blurry, low quality",
|
||||
resolution: "2K",
|
||||
aspectRatio: "16:9",
|
||||
n: 1
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Task ID:', result.data.task_id);
|
||||
```
|
||||
|
||||
### 视频生成 API
|
||||
|
||||
**端点**:`POST /api/v1/generations/video`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// 必需参数
|
||||
model: string; // 复合 ID: "provider/model_key"
|
||||
prompt: string; // 生成提示词
|
||||
|
||||
// 可选参数
|
||||
negativePrompt?: string; // 负面提示词
|
||||
imageInputs?: string[]; // 参考图片 URL 列表
|
||||
resolution?: string; // 分辨率级别
|
||||
aspectRatio?: string; // 宽高比
|
||||
duration?: string; // 时长: "5s", "10s"
|
||||
extraParams?: object; // 额外参数
|
||||
|
||||
// 元数据
|
||||
projectId?: string;
|
||||
source?: string;
|
||||
sourceId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 音频生成 API
|
||||
|
||||
**端点**:`POST /api/v1/generations/audio`
|
||||
|
||||
**请求参数**:
|
||||
|
||||
```typescript
|
||||
{
|
||||
// 必需参数
|
||||
model: string; // 复合 ID: "provider/model_key"
|
||||
prompt: string; // 生成提示词或文本
|
||||
|
||||
// 可选参数
|
||||
voice?: string; // 声音 ID
|
||||
speed?: number; // 语速
|
||||
extraParams?: object; // 额外参数
|
||||
|
||||
// 元数据
|
||||
projectId?: string;
|
||||
source?: string;
|
||||
sourceId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 复合 ID 格式
|
||||
|
||||
### 格式规范
|
||||
|
||||
复合 ID 使用 `/` 分隔符连接 provider 和 model_key:
|
||||
|
||||
```
|
||||
格式:provider/model_key
|
||||
示例:dashscope/qwen-image
|
||||
```
|
||||
|
||||
### 格式验证
|
||||
|
||||
**有效格式**:
|
||||
- ✅ `dashscope/qwen-image`
|
||||
- ✅ `modelscope/qwen-image`
|
||||
- ✅ `volcengine/doubao-tts`
|
||||
- ✅ `google/gemini-pro`
|
||||
|
||||
**无效格式**:
|
||||
- ❌ `qwen-image` (缺少 provider)
|
||||
- ❌ `dashscope-qwen-image` (使用了 `-` 而不是 `/`)
|
||||
- ❌ `dashscope/qwen/image` (多个分隔符)
|
||||
- ❌ `/qwen-image` (provider 为空)
|
||||
- ❌ `dashscope/` (model_key 为空)
|
||||
|
||||
### 前端验证
|
||||
|
||||
```typescript
|
||||
function validateModel(model: string): void {
|
||||
if (!model || !model.trim()) {
|
||||
throw new Error('Model cannot be empty');
|
||||
}
|
||||
|
||||
if (!model.includes('/')) {
|
||||
throw new Error(
|
||||
`Model must be in format 'provider/model_key', got: '${model}'`
|
||||
);
|
||||
}
|
||||
|
||||
const parts = model.split('/');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`Model format invalid: '${model}'. Must have exactly one '/' separator.`
|
||||
);
|
||||
}
|
||||
|
||||
const [provider, modelKey] = parts;
|
||||
if (!provider || !modelKey) {
|
||||
throw new Error(
|
||||
`Model format invalid: '${model}'. Both provider and model_key must be non-empty.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
try {
|
||||
validateModel("dashscope/qwen-image"); // ✅ 通过
|
||||
validateModel("qwen-image"); // ❌ 抛出错误
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### 后端验证
|
||||
|
||||
后端会自动验证 model 格式,无效格式会返回 400 错误:
|
||||
|
||||
```python
|
||||
# backend/src/models/schemas/generation.py
|
||||
class ImageGenerationRequest(BaseModel):
|
||||
model: str
|
||||
|
||||
@field_validator('model')
|
||||
@classmethod
|
||||
def validate_model_format(cls, v: str) -> str:
|
||||
if '/' not in v:
|
||||
raise ValueError(
|
||||
f"Model must be in format 'provider/model_key', got: '{v}'"
|
||||
)
|
||||
|
||||
parts = v.split('/')
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f"Model format invalid: '{v}'. Must have exactly one '/' separator."
|
||||
)
|
||||
|
||||
provider, model_key = parts
|
||||
if not provider or not model_key:
|
||||
raise ValueError(
|
||||
f"Model format invalid: '{v}'. Both provider and model_key must be non-empty."
|
||||
)
|
||||
|
||||
return v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误码
|
||||
|
||||
| 错误码 | 说明 | 解决方案 |
|
||||
|--------|------|----------|
|
||||
| 400 | 请求参数错误 | 检查参数格式,特别是 model 格式 |
|
||||
| 404 | 模型不存在 | 使用 `/api/v1/models` 获取可用模型列表 |
|
||||
| 500 | 服务器内部错误 | 联系技术支持 |
|
||||
|
||||
### 错误响应格式
|
||||
|
||||
```typescript
|
||||
{
|
||||
"code": "400",
|
||||
"message": "Model must be in format 'provider/model_key', got: 'qwen-image'. Example: 'dashscope/qwen-image'"
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理示例
|
||||
|
||||
```typescript
|
||||
async function generateImage(params: ImageGenerationParams) {
|
||||
try {
|
||||
// 前端验证
|
||||
validateModel(params.model);
|
||||
|
||||
const response = await fetch('/api/v1/generations/image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error.message.includes('format')) {
|
||||
console.error('Model format error:', error.message);
|
||||
// 提示用户使用正确格式
|
||||
} else if (error.message.includes('not found')) {
|
||||
console.error('Model not found:', error.message);
|
||||
// 提示用户选择其他模型
|
||||
} else {
|
||||
console.error('Unknown error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 使用 ModelStore 管理模型配置
|
||||
|
||||
```typescript
|
||||
// frontend/src/lib/store/modelStore.ts
|
||||
import { useModelStore } from '@/lib/store/modelStore';
|
||||
|
||||
// 获取图片模型
|
||||
const imageModels = useModelStore.getState().imageModels;
|
||||
|
||||
// 获取特定模型
|
||||
const model = imageModels["dashscope/qwen-image"];
|
||||
|
||||
// 获取默认模型
|
||||
const defaultModel = Object.values(imageModels).find(m => m.is_default);
|
||||
```
|
||||
|
||||
### 2. 直接使用复合 ID
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐:直接使用 model.id
|
||||
const modelId = model.id; // "dashscope/qwen-image"
|
||||
|
||||
await ImageGenerationService.generate({
|
||||
model: modelId,
|
||||
prompt: "a cat"
|
||||
});
|
||||
|
||||
// ❌ 不推荐:手动拼接
|
||||
const modelId = `${model.provider}/${model.model_key}`;
|
||||
```
|
||||
|
||||
### 3. 缓存模型配置
|
||||
|
||||
```typescript
|
||||
// 只在首次加载时请求
|
||||
let modelsLoaded = false;
|
||||
|
||||
async function loadModels() {
|
||||
if (modelsLoaded) return;
|
||||
|
||||
const response = await fetch('/api/v1/models');
|
||||
const data = await response.json();
|
||||
useModelStore.getState().setModels(data);
|
||||
modelsLoaded = true;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 验证用户输入
|
||||
|
||||
```typescript
|
||||
// 在发送请求前验证
|
||||
function validateGenerationParams(params: ImageGenerationParams) {
|
||||
validateModel(params.model);
|
||||
|
||||
if (!params.prompt || params.prompt.trim().length === 0) {
|
||||
throw new Error('Prompt cannot be empty');
|
||||
}
|
||||
|
||||
if (params.n && (params.n < 1 || params.n > 4)) {
|
||||
throw new Error('n must be between 1 and 4');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 处理异步任务
|
||||
|
||||
```typescript
|
||||
async function generateAndWait(params: ImageGenerationParams) {
|
||||
// 1. 创建任务
|
||||
const { data } = await ImageGenerationService.generate(params);
|
||||
const taskId = data.task_id;
|
||||
|
||||
// 2. 轮询任务状态
|
||||
while (true) {
|
||||
const task = await fetch(`/api/v1/tasks/${taskId}`).then(r => r.json());
|
||||
|
||||
if (task.data.status === 'completed') {
|
||||
return task.data.result;
|
||||
}
|
||||
|
||||
if (task.data.status === 'failed') {
|
||||
throw new Error(task.data.error);
|
||||
}
|
||||
|
||||
// 等待 1 秒后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么不再需要 `provider` 参数?
|
||||
|
||||
**A**: 复合 ID 已经包含了 provider 信息。例如 `dashscope/qwen-image` 中,`dashscope` 就是 provider。传递单独的 `provider` 参数是冗余的。
|
||||
|
||||
**迁移示例**:
|
||||
|
||||
```typescript
|
||||
// 旧代码(已废弃)
|
||||
await ImageGenerationService.generate({
|
||||
model: "dashscope/qwen-image",
|
||||
provider: "dashscope", // ❌ 冗余
|
||||
prompt: "a cat"
|
||||
});
|
||||
|
||||
// 新代码
|
||||
await ImageGenerationService.generate({
|
||||
model: "dashscope/qwen-image", // ✅ 只需要 model
|
||||
prompt: "a cat"
|
||||
});
|
||||
```
|
||||
|
||||
### Q2: 如何从复合 ID 中提取 provider?
|
||||
|
||||
**A**: 使用字符串分割:
|
||||
|
||||
```typescript
|
||||
const modelId = "dashscope/qwen-image";
|
||||
const [provider, modelKey] = modelId.split('/');
|
||||
|
||||
console.log(provider); // "dashscope"
|
||||
console.log(modelKey); // "qwen-image"
|
||||
```
|
||||
|
||||
但通常不需要手动提取,因为 `ModelConfig` 对象已经包含了 `provider` 字段:
|
||||
|
||||
```typescript
|
||||
const model = imageModels["dashscope/qwen-image"];
|
||||
console.log(model.provider); // "dashscope"
|
||||
```
|
||||
|
||||
### Q3: 如何处理模型不存在的情况?
|
||||
|
||||
**A**: 后端会返回 404 错误,前端应该提示用户选择其他模型:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await ImageGenerationService.generate({
|
||||
model: "invalid/model",
|
||||
prompt: "a cat"
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('not found')) {
|
||||
// 提示用户
|
||||
alert('模型不存在,请选择其他模型');
|
||||
|
||||
// 获取可用模型列表
|
||||
const response = await fetch('/api/v1/models');
|
||||
const { data } = await response.json();
|
||||
console.log('Available models:', Object.keys(data.image));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Q4: 如何选择默认模型?
|
||||
|
||||
**A**: 使用 `is_default` 字段:
|
||||
|
||||
```typescript
|
||||
const imageModels = useModelStore.getState().imageModels;
|
||||
const defaultModel = Object.values(imageModels).find(m => m.is_default);
|
||||
|
||||
if (defaultModel) {
|
||||
console.log('Default model:', defaultModel.id);
|
||||
}
|
||||
```
|
||||
|
||||
### Q5: 如何检查模型是否支持某个功能?
|
||||
|
||||
**A**: 使用 `capabilities` 字段:
|
||||
|
||||
```typescript
|
||||
const model = imageModels["dashscope/qwen-image"];
|
||||
|
||||
if (model.capabilities?.supportsLora) {
|
||||
console.log('Model supports LoRA');
|
||||
}
|
||||
|
||||
if (model.capabilities?.supportsRefImage) {
|
||||
console.log('Model supports reference images');
|
||||
}
|
||||
```
|
||||
|
||||
### Q6: 如何获取模型支持的分辨率?
|
||||
|
||||
**A**: 使用 `resolutions` 字段:
|
||||
|
||||
```typescript
|
||||
const model = imageModels["dashscope/qwen-image"];
|
||||
|
||||
// 获取所有分辨率级别
|
||||
const levels = Object.keys(model.resolutions); // ["1K", "2K", "4K"]
|
||||
|
||||
// 获取特定级别的宽高比
|
||||
const aspectRatios = Object.keys(model.resolutions["2K"]); // ["16:9", "1:1", "9:16"]
|
||||
|
||||
// 获取具体尺寸
|
||||
const size = model.resolutions["2K"]["16:9"]; // "2560*1440"
|
||||
```
|
||||
|
||||
### Q7: 旧代码中的 `getProviderForModel` 函数怎么办?
|
||||
|
||||
**A**: 该函数已被移除,不再需要。直接使用 `ModelConfig.provider` 字段:
|
||||
|
||||
```typescript
|
||||
// 旧代码(已废弃)
|
||||
const provider = getProviderForModel(modelId, 'image'); // ❌
|
||||
|
||||
// 新代码
|
||||
const model = imageModels[modelId];
|
||||
const provider = model.provider; // ✅
|
||||
```
|
||||
|
||||
### Q8: 如何处理多个 provider 提供相同的 model_key?
|
||||
|
||||
**A**: 使用完整的复合 ID 来区分:
|
||||
|
||||
```typescript
|
||||
// 两个不同的模型,但 model_key 相同
|
||||
const dashscopeModel = imageModels["dashscope/qwen-image"];
|
||||
const modelscopeModel = imageModels["modelscope/qwen-image"];
|
||||
|
||||
// 使用时指定完整的复合 ID
|
||||
await ImageGenerationService.generate({
|
||||
model: "dashscope/qwen-image", // 使用 DashScope 的版本
|
||||
prompt: "a cat"
|
||||
});
|
||||
```
|
||||
|
||||
### Q9: 如何测试 API 调用?
|
||||
|
||||
**A**: 使用 curl 或 Postman:
|
||||
|
||||
```bash
|
||||
# 测试图片生成
|
||||
curl -X POST http://localhost:8000/api/v1/generations/image \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "dashscope/qwen-image",
|
||||
"prompt": "a beautiful sunset",
|
||||
"resolution": "2K",
|
||||
"aspectRatio": "16:9"
|
||||
}'
|
||||
|
||||
# 测试模型配置
|
||||
curl http://localhost:8000/api/v1/models
|
||||
```
|
||||
|
||||
### Q10: 如何调试模型查找问题?
|
||||
|
||||
**A**: 检查后端日志:
|
||||
|
||||
```bash
|
||||
# 后端会记录详细的查找过程
|
||||
tail -f logs/app.log | grep "Resolving service"
|
||||
```
|
||||
|
||||
日志示例:
|
||||
```
|
||||
INFO: Resolving service: model='dashscope/qwen-image', type=image
|
||||
INFO: Found service by composite ID: 'dashscope/qwen-image'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [API 文档](./API.md) - 完整的 API 参考
|
||||
- [项目 README](../README.md) - 项目概述和快速开始
|
||||
- [前端优化文档](./FRONTEND_OPTIMIZATION.md) - 前端性能优化策略
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-02-11
|
||||
**版本**:1.0.0
|
||||
Reference in New Issue
Block a user