from __future__ import annotations from dataclasses import dataclass from fastapi import HTTPException from app.enums.celery_queue import CeleryQueue from app.enums.common import ModuleEventTypeEnum from app.services.module_async_recovery_service import ( TASK_MODULE_V2_VIDEO_PROMPT, register_module_step_task, ) from app.services.module_generation_log_service import log_module_error, log_module_event_file from app.services.llm_billing import ( LlmBillingContext, log_celery_dispatch_failure, log_celery_dispatch_start, log_celery_dispatch_success, ) from app.services.module_generation_v2.config import VIDEO_PROMPT_OPTIMIZE, ModuleGenerationV2Config from app.tasks.celery_app import celery_app from app.tasks.module_generation_v2_tasks import start_video_prompt_optimize_v2 @dataclass(frozen=True) class VideoPromptDispatchResult: registry_success: bool celery_success: bool registry_error: str | None = None celery_error: str | None = None @property def recoverable(self) -> bool: return self.registry_success or self.celery_success def ensure_v2_celery_enabled() -> None: if celery_app is None: raise HTTPException(status_code=503, detail="Celery未启用") async def dispatch_video_prompt_v2( *, config: ModuleGenerationV2Config, project_id: str, step_id: str, billing_context: LlmBillingContext, ) -> VideoPromptDispatchResult: """注册并投递 V2 视频提词任务。 Redis 注册成功但 Celery 直投失败时,由周期恢复任务补投;Celery 成功但 Redis 注册失败时任务仍可正常执行。只有两个通道都失败时由 API 补偿落库为失败。 """ log_celery_dispatch_start(billing_context) registry_error: Exception | None = None try: await register_module_step_task( module=config.module, project_id=project_id, step_id=step_id, step_code=VIDEO_PROMPT_OPTIMIZE, task_name=TASK_MODULE_V2_VIDEO_PROMPT, ) except Exception as exc: registry_error = exc log_module_error( module=config.module, event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_REGISTRY_FAILED.value, project_id=project_id, step_id=step_id, message="V2 视频提词 Redis 活跃注册失败,将继续尝试 Celery 直投", exc=exc, ) celery_error: Exception | None = None try: start_video_prompt_optimize_v2.apply_async( args=[project_id, step_id], queue=CeleryQueue.GEN_CHATAPI_CREATE.value, countdown=0, task_id=f"module-v2-video-prompt:{step_id}", ) except Exception as exc: celery_error = exc log_module_error( module=config.module, event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_DISPATCH_FAILED.value, project_id=project_id, step_id=step_id, message="V2 视频提词 Celery 投递失败", detail={"redis_registry_available": registry_error is None}, exc=exc, ) result = VideoPromptDispatchResult( registry_success=registry_error is None, celery_success=celery_error is None, registry_error=str(registry_error) if registry_error else None, celery_error=str(celery_error) if celery_error else None, ) if result.celery_success: log_celery_dispatch_success(billing_context) log_module_event_file( module=config.module, event_type=ModuleEventTypeEnum.V2_VIDEO_PROMPT_DISPATCHED.value, project_id=project_id, step_id=step_id, message="V2 视频提词任务已投递", detail={ "queue": CeleryQueue.GEN_CHATAPI_CREATE.value, "redis_registry_available": result.registry_success, }, ) if not result.celery_success: log_celery_dispatch_failure( billing_context, error=result.celery_error or "Celery direct dispatch failed; waiting for registry recovery", ) return result