fix(security): P1 audio_merger裸subprocess下沉 + ffmpeg_utils架构下沉到packages/shared
CI/CD Pipeline / Unit Tests (pull_request) Failing after 36s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 41s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 23s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1531h41m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1531h41m12s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1531h41m11s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1531h41m13s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1531h41m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1531h41m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1531h41m13s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1531h41m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1531h41m14s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1532h12m51s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1532h12m52s

- 新增 packages/shared/ffmpeg_utils.py: run_ffmpeg/FFMPEG_BIN/FFPROBE_BIN下沉到共享层
- apps/worker/video_processing/ffmpeg_utils.py: 从shared re-export,保持向后兼容
- packages/application/tts_job/audio_merger.py: 改用 shared.ffmpeg_utils.run_ffmpeg
- 新增 test_audio_merger_security.py: 6个单测验证下沉正确性

解决架构分层问题:application层不再需要跨层调用worker层的ffmpeg工具
This commit is contained in:
CI Bot
2026-07-14 19:14:52 +08:00
parent 0346d5ae20
commit f4701f7b39
4 changed files with 250 additions and 76 deletions
+14 -63
View File
@@ -1,23 +1,28 @@
"""FFmpeg 工具函数 — 共享原语.
"""FFmpeg 工具函数 — Worker 层.
提供 FFmpeg / FFprobe 调用、视频信息探测、视频标准化、xfade 转场滤镜构建
等底层能力,供 UnifiedRenderService、VideoComposeService 等复用。
业务相关的滤镜构建、视频探测、视频标准化等能力放在这里;
底层原语(run_ffmpeg / 二进制路径 / 默认超时)已下沉到 packages/shared/ffmpeg_utils.py,
本模块 re-export 保持向后兼容。
"""
from __future__ import annotations
import logging
import shutil
import subprocess # nosec B404
from pathlib import Path
from typing import Any
# 底层原语从 shared 层导入,application 层和 worker 层共用同一份实现
from shared.ffmpeg_utils import ( # noqa: F401
DEFAULT_FFMPEG_TIMEOUT,
FFMPEG_BIN,
FFPROBE_BIN,
run_ffmpeg,
)
logger = logging.getLogger(__name__)
# ── 常量 ──────────────────────────────────────────────────────────────────────
FFMPEG_BIN: str = shutil.which("ffmpeg") or "ffmpeg"
FFPROBE_BIN: str = shutil.which("ffprobe") or "ffprobe"
# ── 常量(Worker 层业务相关) ────────────────────────────────────────────────
DEFAULT_OUTPUT_WIDTH = 1280
DEFAULT_OUTPUT_HEIGHT = 720
@@ -61,62 +66,8 @@ XFADE_TRANSITION_MAP: dict[str, str] = {
DEFAULT_TRANSITION_DURATION = 0.5
# FFmpeg 执行默认超时(秒),防止 FFmpeg hang 住导致 worker 永久阻塞
# 默认 30 分钟,足够处理大部分短视频渲染;超长视频可单独传参覆盖
DEFAULT_FFMPEG_TIMEOUT = 1800
# ── FFmpeg 执行 ───────────────────────────────────────────────────────────────
def run_ffmpeg(
command: list[str],
*,
capture_output: bool = True,
timeout: int | None = DEFAULT_FFMPEG_TIMEOUT,
) -> tuple[str, str]:
"""执行 FFmpeg 命令。
Args:
command: 完整的 ffmpeg 命令列表(含 "ffmpeg" 本身)
capture_output: 是否捕获 stdout/stderr
timeout: 超时时间(秒),默认 1800s(30分钟);None 表示不设超时(不推荐)
Returns:
(stdout, stderr) 元组
Raises:
subprocess.CalledProcessError: 命令执行失败时抛出,
异常信息包含完整 stderr 以便排查。
subprocess.TimeoutExpired: 超时未完成时抛出,FFmpeg 进程会被 kill。
"""
try:
result = subprocess.run( # nosec B603
command,
check=True,
stdout=subprocess.PIPE if capture_output else None,
stderr=subprocess.PIPE if capture_output else None,
text=True,
timeout=timeout,
)
return (result.stdout or "", result.stderr or "")
except subprocess.TimeoutExpired:
logger.error(
"FFmpeg 命令超时 (%ds): command=%s",
timeout or -1,
" ".join(str(c) for c in command[:20]),
)
raise
except subprocess.CalledProcessError as e:
# 把完整 stderr 打到日志,方便排查 exit code 183 等问题
stderr_text = (e.stderr or "").strip()
logger.error(
"FFmpeg 命令失败: exit_code=%d command=%s\nstderr:\n%s",
e.returncode,
" ".join(str(c) for c in command[:20]), # 截断过长的命令
stderr_text[:5000], # 截断过长的 stderr
)
raise
# ── FFprobe 探测 ──────────────────────────────────────────────────────────────
def probe_has_audio(local_path: str | Path) -> bool: