Files
pixel/docs/development-guide.md
张鹏 f9f4560459 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()
2026-04-29 01:20:12 +08:00

669 lines
15 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.
# 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