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

15 KiB
Raw Permalink Blame History

Pixel 开发指南

本指南提供 Pixel 项目的开发最佳实践、API 使用说明和常见问题解决方案。

目录


模型配置

获取可用模型

使用 /api/v1/models 端点获取所有可用的模型配置:

// 前端示例
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 模型类型:imagevideoaudiollm
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

正确示例

// 图片生成
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"
  })
});

错误示例

// ❌ 错误:缺少 provider
{
  model: "qwen-image",
  prompt: "a cat"
}

// ❌ 错误:使用了已废弃的 provider 参数
{
  model: "dashscope/qwen-image",
  provider: "dashscope",  // 不再需要
  prompt: "a cat"
}

图片生成 API

端点POST /api/v1/generations/image

请求参数

{
  // 必需参数
  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
}

响应

{
  "code": "200",
  "data": {
    "task_id": "task_123456"
  }
}

完整示例

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

请求参数

{
  // 必需参数
  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

请求参数

{
  // 必需参数
  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 为空)

前端验证

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 错误:

# 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 服务器内部错误 联系技术支持

错误响应格式

{
  "code": "400",
  "message": "Model must be in format 'provider/model_key', got: 'qwen-image'. Example: 'dashscope/qwen-image'"
}

错误处理示例

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 管理模型配置

// 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

// ✅ 推荐:直接使用 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. 缓存模型配置

// 只在首次加载时请求
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. 验证用户输入

// 在发送请求前验证
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. 处理异步任务

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 参数是冗余的。

迁移示例

// 旧代码(已废弃)
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: 使用字符串分割:

const modelId = "dashscope/qwen-image";
const [provider, modelKey] = modelId.split('/');

console.log(provider);   // "dashscope"
console.log(modelKey);   // "qwen-image"

但通常不需要手动提取,因为 ModelConfig 对象已经包含了 provider 字段:

const model = imageModels["dashscope/qwen-image"];
console.log(model.provider);  // "dashscope"

Q3: 如何处理模型不存在的情况?

A: 后端会返回 404 错误,前端应该提示用户选择其他模型:

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 字段:

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 字段:

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 字段:

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 字段:

// 旧代码(已废弃)
const provider = getProviderForModel(modelId, 'image');  // ❌

// 新代码
const model = imageModels[modelId];
const provider = model.provider;  // ✅

Q8: 如何处理多个 provider 提供相同的 model_key

A: 使用完整的复合 ID 来区分:

// 两个不同的模型,但 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

# 测试图片生成
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: 检查后端日志:

# 后端会记录详细的查找过程
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'

相关文档


最后更新2026-02-11
版本1.0.0