拆镜复刻开发完成

This commit is contained in:
2026-06-11 17:54:40 +08:00
parent a6b6e5f822
commit 74266a126c
34 changed files with 7105 additions and 470 deletions
@@ -1,10 +1,14 @@
from __future__ import annotations
from types import SimpleNamespace
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db
from app.models.user import User
from app.enums.hot_opening_replicate import ModuleCodeEnum
from app.schemas.hot_opening_replicate import (
HotOpeningActionOut,
HotOpeningDeleteOut,
@@ -35,14 +39,94 @@ from app.services.hot_opening_replicate_service import (
update_hot_opening_material_input,
update_hot_opening_video_prompt_schema,
)
from app.services.module_generation_log_service import log_module_error
from app.tasks.celery_app import celery_app
MODULE = ModuleCodeEnum.HOT_OPENING_REPLICATE.value
def _safe_user_id(user: object | None) -> str | None:
"""从 ORM 对象中安全取用户ID,避免 rollback/commit 后访问过期属性触发 MissingGreenlet。"""
if user is None:
return None
try:
value = getattr(user, "__dict__", {}).get("id")
if value is not None:
return str(value)
except Exception:
pass
try:
identity = sa_inspect(user).identity
if identity:
return str(identity[0])
except Exception:
pass
return None
def _safe_user_is_admin(user: object | None) -> bool:
"""安全判断管理员身份;如果对象属性已过期,保守按普通用户处理。"""
if user is None:
return False
try:
data = getattr(user, "__dict__", {})
if "is_admin" in data:
return bool(data.get("is_admin"))
except Exception:
pass
return False
def _user_context(user: object | None) -> SimpleNamespace:
return SimpleNamespace(id=_safe_user_id(user), is_admin=_safe_user_is_admin(user))
router = APIRouter(
prefix="/hot-opening-replications",
tags=["hot-opening-replications"],
)
def _log_api_error(
*,
event_type: str,
current_user: User | None = None,
project_id: str | None = None,
step_id: str | None = None,
message: str | None = None,
exc: BaseException | None = None,
detail: dict | None = None,
) -> None:
log_module_error(
module=MODULE,
event_type=event_type,
project_id=project_id,
step_id=step_id,
user_id=_safe_user_id(current_user),
message=message,
detail=detail,
exc=exc,
)
def _log_api_exception_from_locals(exc: BaseException, local_values: dict, message: str) -> None:
current_user = local_values.get("current_user")
project_id = local_values.get("project_id_value") or local_values.get("project_id")
step_id = local_values.get("step_id_value") or local_values.get("step_id")
req = local_values.get("req")
detail = {"request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None}
_log_api_error(
event_type="API_REQUEST_FAILED",
current_user=current_user if isinstance(current_user, User) else None,
project_id=str(project_id) if project_id else None,
step_id=str(step_id) if step_id else None,
message=message,
detail=detail,
exc=exc,
)
async def _reload_project_detail(
db: AsyncSession,
current_user: User,
@@ -52,7 +136,7 @@ async def _reload_project_detail(
project = await _get_project_for_user(
db,
project_id=project_id,
user=current_user,
user=_user_context(current_user),
for_update=False,
populate_existing=True,
)
@@ -72,14 +156,33 @@ async def _mark_dispatch_failed_and_raise(
try:
await mark_hot_opening_step_dispatch_failed(
db,
current_user=current_user,
current_user=_user_context(current_user),
project_id=project_id,
step_id=step_id,
error_message=message,
)
await db.commit()
except Exception:
except Exception as exc:
await db.rollback()
_log_api_error(
event_type="CELERY_DISPATCH_MARK_FAILED",
current_user=current_user,
project_id=project_id,
step_id=step_id,
message="Celery 投递失败后标记步骤失败也失败",
detail={"dispatch_error": message},
exc=exc,
)
log_module_error(
module=MODULE,
event_type="CELERY_DISPATCH_FAILED",
project_id=project_id,
step_id=step_id,
user_id=_safe_user_id(current_user),
message=message,
detail={"reason": "celery_dispatch_failed"},
error=message,
)
raise HTTPException(status_code=503, detail=message)
@@ -118,6 +221,7 @@ async def create_task(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"创建爆款开头复刻项目失败: {exc}")
raise HTTPException(status_code=500, detail=f"创建爆款开头复刻项目失败: {exc}")
return await _reload_project_detail(db, current_user, project_id_value)
@@ -185,6 +289,7 @@ async def update_material(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"修改素材输入失败: {exc}")
raise HTTPException(status_code=500, detail=f"修改素材输入失败: {exc}")
return HotOpeningActionOut(
@@ -228,6 +333,7 @@ async def update_image_prompt(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"修改图片 AI 提词失败: {exc}")
raise HTTPException(status_code=500, detail=f"修改图片 AI 提词失败: {exc}")
return HotOpeningActionOut(
@@ -300,6 +406,14 @@ async def generate_image_prompt(
):
_ = req
if celery_app is None:
_log_api_error(
event_type="CELERY_DISABLED",
current_user=current_user,
project_id=project_id,
step_id=step_id,
message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker",
detail={"api": "hot_opening_replicate"},
)
raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker")
try:
@@ -312,6 +426,7 @@ async def generate_image_prompt(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"图片提词任务创建失败: {exc}")
raise HTTPException(status_code=500, detail=f"图片提词任务创建失败: {exc}")
from app.tasks.hot_opening_replicate_tasks import start_image_prompt_optimize
@@ -354,6 +469,14 @@ async def generate_image(
db: AsyncSession = Depends(get_db),
):
if celery_app is None:
_log_api_error(
event_type="CELERY_DISABLED",
current_user=current_user,
project_id=project_id,
step_id=step_id,
message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker",
detail={"api": "hot_opening_replicate"},
)
raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker")
try:
@@ -369,6 +492,7 @@ async def generate_image(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"图片生成任务创建失败: {exc}")
raise HTTPException(status_code=500, detail=f"图片生成任务创建失败: {exc}")
from app.tasks.generation_create_tasks import chatapi_create_generation_task
@@ -411,6 +535,14 @@ async def generate_video_prompt(
db: AsyncSession = Depends(get_db),
):
if celery_app is None:
_log_api_error(
event_type="CELERY_DISABLED",
current_user=current_user,
project_id=project_id,
step_id=step_id,
message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker",
detail={"api": "hot_opening_replicate"},
)
raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker")
try:
@@ -423,6 +555,7 @@ async def generate_video_prompt(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"视频提词任务创建失败: {exc}")
raise HTTPException(status_code=500, detail=f"视频提词任务创建失败: {exc}")
from app.tasks.hot_opening_replicate_tasks import start_video_prompt_optimize
@@ -466,6 +599,14 @@ async def generate_video(
db: AsyncSession = Depends(get_db),
):
if celery_app is None:
_log_api_error(
event_type="CELERY_DISABLED",
current_user=current_user,
project_id=project_id,
step_id=step_id,
message="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker",
detail={"api": "hot_opening_replicate"},
)
raise HTTPException(status_code=503, detail="Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker")
try:
@@ -481,6 +622,7 @@ async def generate_video(
raise
except Exception as exc:
await db.rollback()
_log_api_exception_from_locals(exc, locals(), f"视频生成任务创建失败: {exc}")
raise HTTPException(status_code=500, detail=f"视频生成任务创建失败: {exc}")
from app.tasks.generation_create_tasks import chatapi_create_generation_task