diff --git a/apps/api/app/api/routes/generation_preview.py b/apps/api/app/api/routes/generation_preview.py index 5e9414a70..eeebe3ed1 100755 --- a/apps/api/app/api/routes/generation_preview.py +++ b/apps/api/app/api/routes/generation_preview.py @@ -8,6 +8,7 @@ from __future__ import annotations import logging from app.auth import AuthenticatedUser, get_current_user +from app.core.storage import get_storage_service from app.core.task_enqueue import ( GLOBAL_PENDING_LIMIT, USER_PENDING_LIMIT, @@ -63,6 +64,22 @@ def _mark_task_failed(repo, task, reason: str) -> None: logger.exception("[预览生成] 标记任务失败时异常: task_id=%s", task.id) +def _sign_video_url(raw_url: str) -> str: + """为私有 OSS bucket 的视频 URL 生成预签名下载链接。 + + 有效期 2 小时,签名失败时降级返回原始 URL。 + """ + if not raw_url: + return "" + try: + storage = get_storage_service() + signed = storage.get_download_url(raw_url, expires_seconds=7200) + return signed or raw_url + except Exception: + logger.warning("[预览] URL签名失败,降级返回原始URL: %s", raw_url[:100], exc_info=True) + return raw_url + + def _to_preview_response(task, generated_videos: list | None = None) -> PreviewGenerationTaskResponse: """将领域任务对象转换为预览响应 DTO。 @@ -78,7 +95,9 @@ def _to_preview_response(task, generated_videos: list | None = None) -> PreviewG file_size = 0 if generated_videos: first_video = generated_videos[0] - video_url = getattr(first_video, "file_url", "") or "" + raw_url = getattr(first_video, "file_url", "") or "" + # P0 修复:私有 bucket 需要预签名 URL,否则前端 403 → 黑屏 + video_url = _sign_video_url(raw_url) duration = float(getattr(first_video, "duration", 0.0) or 0.0) file_size = int(getattr(first_video, "file_size", 0) or 0)