""" Vidu 回调路由 ============ 仅保留 Vidu 视频生成任务完成回调接口。 视频生成任务统一走 /tasks/video 创建,/tasks/{task_id} 轮询。 Vidu 任务完成后主动 POST 通知此接口。 回调更新 Async Engine TaskRegistry,供前端统一轮询。 """ import logging from fastapi import APIRouter, HTTPException, Request from app.config import get_settings from app.core.exceptions import PlatformError from app.core.redis_client import get_redis_client from app.platform_gateway import PlatformGateway from app.schemas.common import success_response from app.utils.content_fingerprint import ( extract_vidu_error_code, is_vidu_audit_error, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/vidu", tags=["Vidu"]) def get_platform_gateway(request: Request) -> PlatformGateway: """从 app.state 获取 PlatformGateway""" return request.app.state.platform_gateway @router.post("/callback") async def vidu_callback(request: Request): """ Vidu 视频生成任务完成回调 Vidu 任务完成后主动 POST 通知此接口。 无需登录校验(Vidu 外部调用),统一走 PlatformGateway 处理。 回调结果写入 Async Engine TaskRegistry,前端通过 /tasks/{task_id} 统一轮询。 """ # 1. 统一走 PlatformGateway 处理回调(签名验证 + nonce 防重放) gateway = request.app.state.platform_gateway body_bytes = await request.body() headers_dict = dict(request.headers) # 使用 APP_BASE_URL 构建 callback_url,确保与提交任务时传给 Vidu 的一致 # (Nginx 反向代理可能导致 request.url 的 scheme 为 http,与 Vidu 签名时的 https 不一致) app_base_url = get_settings().app_base_url callback_url = f"{app_base_url}/api/v1/vidu/callback" if app_base_url else str(request.url) logger.info( f"[Vidu] 收到回调: request_url={request.url}, callback_url={callback_url}, body={body_bytes.decode('utf-8', errors='replace')[:500]}" ) try: task_status = await gateway.handle_webhook( platform="vidu", headers=headers_dict, body=body_bytes, secret=get_settings().VIDU_API_KEY, callback_url=callback_url, ) except PlatformError as e: logger.warning(f"[Vidu] 回调验证失败: {e}") raise HTTPException(status_code=401, detail="回调签名验证失败") except Exception as e: logger.error(f"[Vidu] 回调处理失败: {e}") raise HTTPException(status_code=500, detail="回调处理失败,请稍后重试") logger.info( f"[Vidu] 回调解析完成: state={task_status.state}, result={task_status.result}, error={task_status.error_message}" ) # 2. 通过 platform_task_id 反查 Async Engine 内部 task_id,更新 TaskRegistry platform_task_id = task_status.result.get("task_id") if task_status.result else None video_url = task_status.result.get("video_url") if task_status.result else None logger.info(f"[Vidu] 准备反查 internal_task_id: platform_task_id={platform_task_id}") internal_task_id = None if platform_task_id: internal_task_id = await gateway.get_internal_task_id_by_platform_task_id( "vidu", platform_task_id ) if internal_task_id: # 更新 Async Engine TaskRegistry,供前端统一轮询 /tasks/{task_id} # # ⚠️ 注意:此处仅更新任务状态,不执行积分扣费。 # 视频生成的扣费在前端 VideoGeneration.tsx 拼接完成后执行。 # 如果用户在 Vidu 任务提交后关闭应用,前端不会执行扣费,形成漏扣。 # 当前接受此风险,详情见前端注释。 from app.scheduler.registry import TaskRegistry registry = TaskRegistry(get_redis_client()) task_record = await registry.get(internal_task_id) if task_record: if task_status.state == "completed": await registry.update( internal_task_id, status="completed", progress=100, message="视频生成完成", completed=1, total=1, result={"video_url": video_url, "state": "success"}, ) elif task_status.state == "failed": error_message = task_status.error_message or "视频生成失败" err_code = extract_vidu_error_code(error_message) is_audit = err_code and is_vidu_audit_error(err_code) update_kwargs = { "status": "failed", "message": ( "人物分镜台词未通过安全审核,请修改后重试" if is_audit else "视频生成失败" ), "error": error_message, } if is_audit: update_kwargs["error_code"] = "content_violation" await registry.update(internal_task_id, **update_kwargs) logger.info( f"[Vidu] 回调已更新 TaskRegistry: task={internal_task_id}, " f"state={task_status.state}, video_url={video_url}" ) else: logger.warning( f"[Vidu] 回调找不到对应任务记录: internal={internal_task_id}, " f"platform={platform_task_id}" ) else: logger.warning(f"[Vidu] 回调无法反查内部 task_id: platform={platform_task_id}") return success_response(message="回调已接收")