fix(scripts): extract-from-douyin 异常路径全部返回业务码,杜绝 500 INTERNAL_ERROR
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m43s
AI Code Review / AI Code Review (pull_request) Successful in 6m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 12m6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 10s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 36s

问题:POST /api/v1/scripts/extract-from-douyin 在抖音短链无效/网络异常/ASR 兜底异常等
场景下冒泡未捕获 Exception,被全局 general_exception_handler 吞成 500 INTERNAL_ERROR,
前端无法区分业务错误。

修复(apps/api/app/api/routes/scripts_ai.py):
1. yt-dlp 导入失败(缺依赖)→ 503(提示功能不可用)
2. yt_dlp.utils.DownloadError 单独捕获:
   - 含 404/not found/unable to download/unsupported url → 400「无法解析该抖音链接」
   - 其它网络/上游异常 → 502「视频下载失败」
3. 下载 info 成功但文件未落地(isfile False / size 0)→ 502「未获取到有效文件」
4. duration 非数字(TypeError/ValueError)→ 安全降级为 0
5. ASR 转写兜底 catch Exception → 502「语音识别失败」(原只 catch 两个自定义异常)
6. 最外层兜底 catch Exception → 500 但带具体 detail(不再是通用 INTERNAL_ERROR)
7. text/duration 提前初始化,避免异常路径 UnboundLocalError

测试:
- 新增 tests/unit/test_extract_from_douyin_errors.py,覆盖 8 个失败场景
- 更新 tests/unit/test_scripts_ai.py 三个成功/ASR 路径测试,mock os.path.isfile/getsize
- 全量单测 15531 passed
This commit is contained in:
xiaoxia-agent
2026-09-16 13:16:45 +08:00
parent 3eb2fcf3ec
commit fca943428b
3 changed files with 261 additions and 6 deletions
+53 -6
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
import logging
import os
import re
import tempfile
@@ -80,10 +81,20 @@ def extract_from_douyin(
if not re.match(r"^https?://", url_for_download, re.IGNORECASE):
url_for_download = "https://" + url_for_download
# 使用临时目录下载视频,退出时自动清理
text: str = ""
duration: float = 0.0
try:
with tempfile.TemporaryDirectory(prefix="douyin_extract_") as temp_dir:
import yt_dlp
# 延迟导入 yt-dlp,避免模块缺失时影响其他路由启动
try:
import yt_dlp
except ImportError as exc:
logger.error("yt-dlp 未安装,抖音提取功能不可用: %s", exc)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="抖音提取功能暂不可用(缺少依赖 yt-dlp)",
) from exc
ydl_opts = {
"format": "best[ext=mp4]/best",
@@ -96,11 +107,23 @@ def extract_from_douyin(
try:
ydl = yt_dlp.YoutubeDL(ydl_opts)
info = ydl.extract_info(url_for_download, download=True)
except yt_dlp.utils.DownloadError as exc:
# yt-dlp 官方异常类型:HTTP 错误、短链失效、视频下架等
msg = str(exc)
logger.warning("抖音下载失败: url=%s error=%s", source_url, msg)
# 404/视频不存在/不可下载 → 400;网络问题/上游异常 → 502
is_bad_url = any(
kw in msg.lower() for kw in ("404", "not found", "unable to download webpage", "unsupported url", "no video formats")
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST if is_bad_url else status.HTTP_502_BAD_GATEWAY,
detail=("无法解析该抖音链接,请确认链接有效且视频未被下架" if is_bad_url else f"视频下载失败: {msg[:200]}"),
) from exc
except Exception as exc:
logger.error("抖音视频下载失败: url=%s error=%s", source_url, exc)
logger.exception("抖音视频下载异常: url=%s", source_url)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"视频下载失败: {exc}",
detail=f"视频下载失败: {str(exc)[:200]}",
) from exc
if info is None:
@@ -110,9 +133,20 @@ def extract_from_douyin(
)
video_path = ydl.prepare_filename(info)
duration = float(info.get("duration") or 0)
try:
duration = float(info.get("duration") or 0)
except (TypeError, ValueError):
duration = 0.0
# ASR 转写
# 校验下载的文件是否真的存在(某些 yt-dlp 版本可能 info 成功但未下载到文件)
if not os.path.isfile(video_path) or os.path.getsize(video_path) == 0:
logger.error("yt-dlp 未产生有效视频文件: path=%s", video_path)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="视频下载异常:未获取到有效文件",
)
# ASR 转写(兜底捕获所有异常,避免 500)
try:
text = transcribe_to_text(video_path)
except ASRNotConfiguredError as exc:
@@ -125,9 +159,22 @@ def extract_from_douyin(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=str(exc),
) from exc
except Exception as exc:
logger.exception("ASR 转写异常: path=%s", video_path)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"语音识别失败: {str(exc)[:200]}",
) from exc
except HTTPException:
raise
except Exception as exc:
# 最后兜底:任何未捕获异常都转成 502/400,不允许冒泡成 500
logger.exception("抖音文案提取未预期异常: url=%s", source_url)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"抖音文案提取失败: {str(exc)[:200]}",
) from exc
return ExtractFromDouyinResponse(
text=text,