feat: v0.8.1 — Seedance API 友好错误提示 + 前端 Mock 数据清理

- 新增 SeedanceAPIError 异常类 + ERROR_MESSAGES 错误码中文映射
- views.py 异常处理区分 SeedanceAPIError,存储友好错误信息
- 移除 DEV 环境 7 个 mock 任务,消除 404 轮询错误

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-15 22:31:14 +08:00
parent 32f0ee58f4
commit f803a1ba71
5 changed files with 64 additions and 119 deletions

View File

@ -364,6 +364,7 @@ npx tsx src/index.ts --resume /Users/maidong/Desktop/zyc/研究openclaw/视频
| 2026-03-13 | CLAUDE.md 增量开发指南更新为 agent-auto替换 Autonomous Skill | Documentation |
| 2026-03-15 | v0.8.0: 音频引用支持 + 视频 TOS 持久化 + 移除硬编码密钥 + 渐进式轮询 | Full stack |
| 2026-03-15 | TOS 桶切换到 airdrama-media (cn-beijing)K8s Secret 注入 TOS 密钥 | Infra |
| 2026-03-15 | v0.8.1: Seedance API 友好错误提示 (SeedanceAPIError) + 前端 Mock 数据清理 | Full stack |
### Phase 4 Details (2026-03-13)

View File

@ -273,7 +273,11 @@ def video_generate_view(request):
except Exception as e:
logger.exception('Seedance API create task failed')
record.status = 'failed'
record.error_message = str(e)
from utils.seedance_client import SeedanceAPIError
if isinstance(e, SeedanceAPIError):
record.error_message = e.user_message
else:
record.error_message = str(e)
record.save(update_fields=['status', 'error_message'])
# Refund: API call failed, Seedance didn't charge
_refund_quota(record, duration)

View File

@ -4,6 +4,28 @@ import requests
from django.conf import settings
# Seedance API error code → user-friendly Chinese message
ERROR_MESSAGES = {
'InputImageSensitiveContentDetected.PrivacyInformation': '参考图片中检测到真实人脸Seedance 不允许处理包含真人面部的图片',
'InputImageSensitiveContentDetected': '参考图片包含敏感内容,请更换图片后重试',
'InputVideoSensitiveContentDetected': '参考视频包含敏感内容,请更换视频后重试',
'InvalidParameter': '请求参数无效,请检查输入',
'RateLimitExceeded': 'API 调用频率超限,请稍后重试',
'InsufficientBalance': '账户余额不足,请联系管理员充值',
}
class SeedanceAPIError(Exception):
"""Raised when Seedance API returns an error response."""
def __init__(self, code, message, status_code=400):
self.code = code
self.api_message = message
self.status_code = status_code
# Use friendly message if available, otherwise use API message
self.user_message = ERROR_MESSAGES.get(code, message)
super().__init__(self.user_message)
MODEL_MAP = {
'seedance_2.0': 'doubao-seedance-2-0-260128',
'seedance_2.0_fast': 'doubao-seedance-2-0-fast-260128',
@ -48,7 +70,15 @@ def create_task(prompt, model, content_items, aspect_ratio, duration, generate_a
}
resp = requests.post(url, json=payload, headers=_headers(), timeout=60)
resp.raise_for_status()
if resp.status_code != 200:
# Extract human-readable error from Seedance API response
try:
err = resp.json().get('error', {})
code = err.get('code', '')
message = err.get('message', resp.text)
except Exception:
code, message = '', resp.text
raise SeedanceAPIError(code, message, resp.status_code)
return resp.json()

View File

@ -4,6 +4,32 @@
---
## 2026-03-15 — v0.8.1: Seedance API 友好错误提示 + Mock 数据清理
**状态**: ✅ 已完成 | **验收**: ✅ 通过(本地端到端测试)
### 变更内容
1. **Seedance API 友好错误提示**`seedance_client.py` 新增 `SeedanceAPIError` 异常类 + `ERROR_MESSAGES` 错误码映射表API 报错时返回中文友好提示(如"参考图片中检测到真实人脸")而非原始英文错误
2. **views.py 错误传递优化**`video_generate_view` 异常处理识别 `SeedanceAPIError`,将 `user_message` 存入 `error_message` 字段,前端直接展示具体原因
3. **移除前端 Mock 数据**`generation.ts` 删除 DEV 环境下的 7 个硬编码 mock 任务,消除页面加载时的 404 轮询错误
### 变更文件
| 文件 | 改动 |
|------|------|
| `backend/utils/seedance_client.py` | 新增 `SeedanceAPIError` 异常类 + `ERROR_MESSAGES` 映射 + `create_task` 错误解析 |
| `backend/apps/generation/views.py` | 异常处理区分 `SeedanceAPIError`,存储友好错误信息 |
| `web/src/store/generation.ts` | 删除 DEV mock 数据7 个假任务),消除 404 轮询 |
### 触发原因
- 本地测试上传含真人面部的图片Seedance 返回 400 但前端只显示"生成失败,请重试",用户无法理解失败原因
- DEV 环境 mock 数据的假 taskId 触发持续 404 轮询错误
### 备注
- 已覆盖错误码:隐私人脸、敏感图片/视频、参数无效、频率超限、余额不足
- 未匹配的错误码会直接展示 API 原始 message
---
## 2026-03-15 — v0.8.0: Seedance API 全流程修复 + TOS 视频持久化
**状态**: ✅ 已完成 | **验收**: ✅ 通过(本地端到端测试)

View File

@ -168,123 +168,7 @@ export const useGenerationStore = create<GenerationState>((set, get) => ({
const { data } = await videoApi.getTasks();
tasks = data.results.map(backendToFrontend).reverse();
} catch {
// API unavailable — tasks stays empty, mocks will fill in below
}
// Dev-only mock tasks for previewing all card states
if (import.meta.env.DEV) {
tasks.push(
// ① 已完成 — 16:9 城市航拍
{
id: 'mock_completed_169',
taskId: 'demo-169',
prompt: '航拍镜头从城市上空缓缓下降,金色夕阳照亮整个天际线,镜头缓慢推进穿过云层',
editorHtml: '航拍镜头从城市上空缓缓下降,金色夕阳照亮整个天际线,镜头缓慢推进穿过云层',
mode: 'universal' as const,
model: 'seedance_2.0' as const,
aspectRatio: '16:9' as const,
duration: 10 as const,
references: [],
status: 'completed' as const,
progress: 100,
resultUrl: '/demo/demo-16-9.mp4',
createdAt: Date.now() - 3600000,
},
// ② 已完成 — 21:9 爆炸场景
{
id: 'mock_completed_219',
taskId: 'demo-219',
prompt: '0-3s手持近景镜头 + 轻微晃动 + 缓慢推近,爆炸后的烟尘缓缓落下,环境沉闷压抑',
editorHtml: '0-3s手持近景镜头 + 轻微晃动 + 缓慢推近,爆炸后的烟尘缓缓落下',
mode: 'universal' as const,
model: 'seedance_2.0' as const,
aspectRatio: '21:9' as const,
duration: 15 as const,
references: [],
status: 'completed' as const,
progress: 100,
resultUrl: '/demo/demo-21-9.mp4',
createdAt: Date.now() - 7200000,
},
// ③ 已完成 — 9:16 人物出场
{
id: 'mock_completed_916',
taskId: 'demo-916',
prompt: '出场人物张磊、队员1-8紧张的救援场面烟雾弥漫中队员们有序前进',
editorHtml: '出场人物张磊、队员1-8紧张的救援场面烟雾弥漫中队员们有序前进',
mode: 'universal' as const,
model: 'seedance_2.0_fast' as const,
aspectRatio: '9:16' as const,
duration: 4 as const,
references: [],
status: 'completed' as const,
progress: 100,
resultUrl: '/demo/demo-9-16.mp4',
createdAt: Date.now() - 6000000,
},
// ④ 生成中 — 刚开始 (5%)
{
id: 'mock_generating_low',
taskId: 'demo-gen-low',
prompt: '微距镜头拍摄雨滴落在花瓣上的慢动作,水珠在花瓣表面缓缓滑落',
editorHtml: '微距镜头拍摄雨滴落在花瓣上的慢动作,水珠在花瓣表面缓缓滑落',
mode: 'universal' as const,
model: 'seedance_2.0' as const,
aspectRatio: '16:9' as const,
duration: 10 as const,
references: [],
status: 'generating' as const,
progress: 5,
createdAt: Date.now() - 60000,
},
// ⑤ 生成中 — 进行中 (60%)
{
id: 'mock_generating_mid',
taskId: 'demo-gen-mid',
prompt: '水墨风格的山水画卷缓缓展开,远处群山叠嶂,近处溪流潺潺',
editorHtml: '水墨风格的山水画卷缓缓展开,远处群山叠嶂,近处溪流潺潺',
mode: 'universal' as const,
model: 'seedance_2.0_fast' as const,
aspectRatio: '1:1' as const,
duration: 5 as const,
references: [],
status: 'generating' as const,
progress: 60,
createdAt: Date.now() - 120000,
},
// ⑥ 失败 — 参数错误
{
id: 'mock_failed_param',
taskId: 'demo-fail-param',
prompt: '深海探索镜头,潜水艇灯光照亮周围的珊瑚礁和鱼群',
editorHtml: '深海探索镜头,潜水艇灯光照亮周围的珊瑚礁和鱼群',
mode: 'universal' as const,
model: 'seedance_2.0' as const,
aspectRatio: '16:9' as const,
duration: 10 as const,
references: [],
status: 'failed' as const,
progress: 0,
errorMessage: '请求参数有误,请检查输入内容',
createdAt: Date.now() - 5000000,
},
// ⑦ 失败 — 服务器错误
{
id: 'mock_failed_server',
taskId: 'demo-fail-server',
prompt: '星空延时摄影,银河缓缓转动,前景是雪山湖泊的倒影',
editorHtml: '星空延时摄影,银河缓缓转动,前景是雪山湖泊的倒影',
mode: 'universal' as const,
model: 'seedance_2.0' as const,
aspectRatio: '4:3' as const,
duration: 8 as const,
references: [],
status: 'failed' as const,
progress: 0,
errorMessage: '服务器繁忙,请稍后重试',
createdAt: Date.now() - 4000000,
},
);
// API unavailable — tasks stays empty
}
set({ tasks, isLoading: false });