- 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()
669 lines
15 KiB
Markdown
669 lines
15 KiB
Markdown
# 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
|