diff --git a/CLAUDE.md b/CLAUDE.md index 0307852..63d3a56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 2743ebb..4cafca7 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -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) diff --git a/backend/utils/seedance_client.py b/backend/utils/seedance_client.py index 22bcce5..f440ff1 100644 --- a/backend/utils/seedance_client.py +++ b/backend/utils/seedance_client.py @@ -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() diff --git a/docs/changelog.md b/docs/changelog.md index 6324817..6f7827e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 视频持久化 **状态**: ✅ 已完成 | **验收**: ✅ 通过(本地端到端测试) diff --git a/web/src/store/generation.ts b/web/src/store/generation.ts index c947bea..eb662b5 100644 --- a/web/src/store/generation.ts +++ b/web/src/store/generation.ts @@ -168,123 +168,7 @@ export const useGenerationStore = create((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 });