mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
5
_deprecated_py_src/api/admin/video_tasks/__init__.py
Normal file
5
_deprecated_py_src/api/admin/video_tasks/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""视频任务管理 API 模块。"""
|
||||
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
539
_deprecated_py_src/api/admin/video_tasks/routes.py
Normal file
539
_deprecated_py_src/api/admin/video_tasks/routes.py
Normal file
@@ -0,0 +1,539 @@
|
||||
"""视频任务管理 API 路由。
|
||||
|
||||
管理员可以查看所有视频任务,用户只能查看自己的任务。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.base.pipeline import get_pipeline
|
||||
from src.api.dashboard.routes import DashboardAdapter
|
||||
from src.config.constants import CacheTTL
|
||||
from src.core.enums import UserRole
|
||||
from src.database import get_db
|
||||
from src.models.database import Provider, ProviderEndpoint, User, VideoTask
|
||||
from src.utils.cache_decorator import cache_result
|
||||
|
||||
router = APIRouter(prefix="/api/admin/video-tasks", tags=["Admin - Video Tasks"])
|
||||
pipeline = get_pipeline()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_video_tasks(
|
||||
request: Request,
|
||||
status: str | None = Query(None, description="Filter by status"),
|
||||
user_id: str | None = Query(None, description="Filter by user ID (admin only)"),
|
||||
model: str | None = Query(None, description="Filter by model"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
获取视频任务列表
|
||||
|
||||
管理员可以查看所有用户的任务,普通用户只能查看自己的任务。
|
||||
|
||||
**查询参数**:
|
||||
- `status`: 按状态筛选(pending/submitted/processing/completed/failed/cancelled)
|
||||
- `user_id`: 按用户 ID 筛选(仅管理员)
|
||||
- `model`: 按模型筛选
|
||||
- `page`: 页码,默认 1
|
||||
- `page_size`: 每页数量,默认 20,最大 100
|
||||
|
||||
**返回字段**:
|
||||
- `items`: 任务列表
|
||||
- `total`: 总数
|
||||
- `page`: 当前页码
|
||||
- `page_size`: 每页数量
|
||||
- `pages`: 总页数
|
||||
"""
|
||||
adapter = VideoTaskListAdapter(
|
||||
status=status,
|
||||
user_id=user_id,
|
||||
model=model,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_video_task_stats(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
获取视频任务统计
|
||||
|
||||
**返回字段**:
|
||||
- `total`: 总任务数
|
||||
- `by_status`: 按状态分组的数量
|
||||
- `by_model`: 按模型分组的数量(前 10)
|
||||
- `today_count`: 今日任务数
|
||||
"""
|
||||
adapter = VideoTaskStatsAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/{task_id}")
|
||||
async def get_video_task_detail(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
获取视频任务详情
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID
|
||||
|
||||
**返回字段**:
|
||||
- 任务的完整信息,包括请求体、响应、状态等
|
||||
"""
|
||||
adapter = VideoTaskDetailAdapter(task_id=task_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/{task_id}/cancel")
|
||||
async def cancel_video_task(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
取消视频任务
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID
|
||||
|
||||
**返回**:
|
||||
- 更新后的任务信息
|
||||
"""
|
||||
adapter = VideoTaskCancelAdapter(task_id=task_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/{task_id}/video")
|
||||
async def proxy_video_stream(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
token: str | None = Query(None, description="JWT access token"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
代理视频流(用于需要认证的视频链接)
|
||||
|
||||
**路径参数**:
|
||||
- `task_id`: 任务 ID
|
||||
|
||||
**查询参数**:
|
||||
- `token`: JWT access token(用于 video 标签请求)
|
||||
|
||||
**返回**:
|
||||
- 视频流
|
||||
"""
|
||||
adapter = VideoTaskProxyVideoAdapter(task_id=task_id, token=token)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
async def _proxy_video_stream_response(
|
||||
*,
|
||||
task_id: str,
|
||||
request: Request,
|
||||
token: str | None,
|
||||
db: Session,
|
||||
) -> Any:
|
||||
"""代理视频流(用于需要认证的视频链接)"""
|
||||
from src.utils.auth_utils import authenticate_user_from_bearer_token
|
||||
|
||||
# 尝试从多个来源获取 token:query param > cookie > header
|
||||
auth_token = token
|
||||
if not auth_token:
|
||||
auth_token = request.cookies.get("access_token")
|
||||
if not auth_token:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
auth_token = auth_header[7:]
|
||||
|
||||
if not auth_token:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
try:
|
||||
user = await authenticate_user_from_bearer_token(auth_token, db, request)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
|
||||
# 查询任务
|
||||
query = db.query(VideoTask).filter(VideoTask.id == task_id)
|
||||
if user.role != UserRole.ADMIN:
|
||||
query = query.filter(VideoTask.user_id == user.id)
|
||||
|
||||
task = query.first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Video task not found")
|
||||
|
||||
if not task.video_url:
|
||||
raise HTTPException(status_code=404, detail="Video not available")
|
||||
|
||||
# 检查是否需要代理(Google API 链接需要认证)
|
||||
video_url = task.video_url
|
||||
needs_proxy = "generativelanguage.googleapis.com" in video_url
|
||||
|
||||
if not needs_proxy:
|
||||
# 不需要代理,重定向到原始 URL
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
return RedirectResponse(url=video_url)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Admin video proxy requires Rust/public download path",
|
||||
)
|
||||
|
||||
|
||||
# ==================== Adapters ====================
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoTaskListAdapter(DashboardAdapter):
|
||||
"""视频任务列表适配器"""
|
||||
|
||||
status: str | None
|
||||
user_id: str | None
|
||||
model: str | None
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
@cache_result(
|
||||
key_prefix="admin:video_tasks:list",
|
||||
ttl=min(5, CacheTTL.ADMIN_USAGE_RECORDS),
|
||||
user_specific=True,
|
||||
vary_by=["status", "user_id", "model", "page", "page_size"],
|
||||
)
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
db = context.db
|
||||
user = context.user
|
||||
is_admin = user.role == UserRole.ADMIN
|
||||
|
||||
query = db.query(VideoTask)
|
||||
|
||||
# 权限过滤:普通用户只能看自己的任务
|
||||
if not is_admin:
|
||||
query = query.filter(VideoTask.user_id == user.id)
|
||||
elif self.user_id:
|
||||
# 管理员可以按用户筛选
|
||||
query = query.filter(VideoTask.user_id == self.user_id)
|
||||
|
||||
# 状态筛选
|
||||
if self.status:
|
||||
query = query.filter(VideoTask.status == self.status)
|
||||
|
||||
# 模型筛选
|
||||
if self.model:
|
||||
escaped = self.model.replace("%", "\\%").replace("_", "\\_")
|
||||
query = query.filter(VideoTask.model.ilike(f"%{escaped}%"))
|
||||
|
||||
# 统计总数(避免 Query.count() 生成大子查询)
|
||||
total = int(query.with_entities(func.count(VideoTask.id)).scalar() or 0)
|
||||
|
||||
# 分页
|
||||
offset = (self.page - 1) * self.page_size
|
||||
tasks = (
|
||||
query.order_by(VideoTask.created_at.desc()).offset(offset).limit(self.page_size).all()
|
||||
)
|
||||
|
||||
# 获取用户信息映射
|
||||
user_ids = list(set(t.user_id for t in tasks if t.user_id))
|
||||
users_map = {}
|
||||
if user_ids:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
users_map = {u.id: u.username for u in users}
|
||||
|
||||
# 获取 Provider 信息映射
|
||||
provider_ids = list(set(t.provider_id for t in tasks if t.provider_id))
|
||||
providers_map = {}
|
||||
if provider_ids:
|
||||
providers = db.query(Provider).filter(Provider.id.in_(provider_ids)).all()
|
||||
providers_map = {p.id: p.name for p in providers}
|
||||
|
||||
items = []
|
||||
for task in tasks:
|
||||
items.append(
|
||||
{
|
||||
"id": task.id,
|
||||
"external_task_id": task.external_task_id,
|
||||
"user_id": task.user_id,
|
||||
"username": users_map.get(task.user_id, "Unknown"),
|
||||
"model": task.model,
|
||||
"prompt": (
|
||||
task.prompt[:100] + "..."
|
||||
if task.prompt and len(task.prompt) > 100
|
||||
else task.prompt
|
||||
),
|
||||
"status": task.status,
|
||||
"progress_percent": task.progress_percent,
|
||||
"progress_message": task.progress_message,
|
||||
"provider_id": task.provider_id,
|
||||
"provider_name": providers_map.get(task.provider_id, "Unknown"),
|
||||
"duration_seconds": task.duration_seconds,
|
||||
"resolution": task.resolution,
|
||||
"aspect_ratio": task.aspect_ratio,
|
||||
"video_url": task.video_url,
|
||||
"error_code": task.error_code,
|
||||
"error_message": task.error_message,
|
||||
"poll_count": task.poll_count,
|
||||
"max_poll_count": task.max_poll_count,
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
|
||||
"submitted_at": task.submitted_at.isoformat() if task.submitted_at else None,
|
||||
}
|
||||
)
|
||||
|
||||
pages = (total + self.page_size - 1) // self.page_size
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": self.page,
|
||||
"page_size": self.page_size,
|
||||
"pages": pages,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoTaskStatsAdapter(DashboardAdapter):
|
||||
"""视频任务统计适配器"""
|
||||
|
||||
@cache_result(
|
||||
key_prefix="admin:video_tasks:stats",
|
||||
ttl=min(5, CacheTTL.ADMIN_USAGE_RECORDS),
|
||||
user_specific=True,
|
||||
)
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
db = context.db
|
||||
user = context.user
|
||||
is_admin = user.role == UserRole.ADMIN
|
||||
|
||||
base_query = db.query(VideoTask)
|
||||
if not is_admin:
|
||||
base_query = base_query.filter(VideoTask.user_id == user.id)
|
||||
|
||||
# 总数(避免 Query.count() 生成大子查询)
|
||||
total = int(base_query.with_entities(func.count(VideoTask.id)).scalar() or 0)
|
||||
|
||||
# 按状态分组
|
||||
status_stats = (
|
||||
base_query.with_entities(
|
||||
VideoTask.status,
|
||||
func.count(VideoTask.id).label("count"),
|
||||
)
|
||||
.group_by(VideoTask.status)
|
||||
.all()
|
||||
)
|
||||
by_status = {stat.status: stat.count for stat in status_stats}
|
||||
|
||||
# 按模型分组(前 10)
|
||||
model_stats = (
|
||||
base_query.with_entities(
|
||||
VideoTask.model,
|
||||
func.count(VideoTask.id).label("count"),
|
||||
)
|
||||
.group_by(VideoTask.model)
|
||||
.order_by(func.count(VideoTask.id).desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
by_model = {stat.model: stat.count for stat in model_stats}
|
||||
|
||||
# 今日任务数
|
||||
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_count = int(
|
||||
base_query.filter(VideoTask.created_at >= today)
|
||||
.with_entities(func.count(VideoTask.id))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
# 管理员额外统计
|
||||
result = {
|
||||
"total": total,
|
||||
"by_status": by_status,
|
||||
"by_model": by_model,
|
||||
"today_count": today_count,
|
||||
}
|
||||
|
||||
if is_admin:
|
||||
# 活跃用户数(有视频任务的用户)
|
||||
active_users = db.query(func.count(func.distinct(VideoTask.user_id))).scalar() or 0
|
||||
result["active_users"] = active_users
|
||||
|
||||
# 处理中的任务数
|
||||
processing_count = (
|
||||
db.query(func.count(VideoTask.id))
|
||||
.filter(VideoTask.status.in_(["submitted", "queued", "processing"]))
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
result["processing_count"] = processing_count
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoTaskDetailAdapter(DashboardAdapter):
|
||||
"""视频任务详情适配器"""
|
||||
|
||||
task_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
db = context.db
|
||||
user = context.user
|
||||
is_admin = user.role == UserRole.ADMIN
|
||||
|
||||
query = db.query(VideoTask).filter(VideoTask.id == self.task_id)
|
||||
if not is_admin:
|
||||
query = query.filter(VideoTask.user_id == user.id)
|
||||
|
||||
task = query.first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Video task not found")
|
||||
|
||||
# 获取用户信息
|
||||
task_user = db.query(User).filter(User.id == task.user_id).first()
|
||||
username = task_user.username if task_user else "Unknown"
|
||||
|
||||
# 获取 Provider 信息
|
||||
provider = db.query(Provider).filter(Provider.id == task.provider_id).first()
|
||||
provider_name = provider.name if provider else "Unknown"
|
||||
|
||||
# 获取 Endpoint 信息
|
||||
endpoint = (
|
||||
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == task.endpoint_id).first()
|
||||
)
|
||||
endpoint_info = None
|
||||
if endpoint:
|
||||
endpoint_info = {
|
||||
"id": endpoint.id,
|
||||
"base_url": endpoint.base_url,
|
||||
"api_format": str(endpoint.api_format),
|
||||
}
|
||||
|
||||
return {
|
||||
"id": task.id,
|
||||
"external_task_id": task.external_task_id,
|
||||
"user_id": task.user_id,
|
||||
"username": username,
|
||||
"api_key_id": task.api_key_id,
|
||||
"provider_id": task.provider_id,
|
||||
"provider_name": provider_name,
|
||||
"endpoint_id": task.endpoint_id,
|
||||
"endpoint": endpoint_info,
|
||||
"key_id": task.key_id,
|
||||
"client_api_format": task.client_api_format,
|
||||
"provider_api_format": task.provider_api_format,
|
||||
"format_converted": task.format_converted,
|
||||
"model": task.model,
|
||||
"prompt": task.prompt,
|
||||
"original_request_body": task.original_request_body,
|
||||
"converted_request_body": task.converted_request_body,
|
||||
"duration_seconds": task.duration_seconds,
|
||||
"resolution": task.resolution,
|
||||
"aspect_ratio": task.aspect_ratio,
|
||||
"size": task.size,
|
||||
"status": task.status,
|
||||
"progress_percent": task.progress_percent,
|
||||
"progress_message": task.progress_message,
|
||||
"video_url": task.video_url,
|
||||
"video_urls": task.video_urls,
|
||||
"thumbnail_url": task.thumbnail_url,
|
||||
"video_size_bytes": task.video_size_bytes,
|
||||
"video_duration_seconds": task.video_duration_seconds,
|
||||
"video_expires_at": (
|
||||
task.video_expires_at.isoformat() if task.video_expires_at else None
|
||||
),
|
||||
"stored_video_path": task.stored_video_path,
|
||||
"storage_provider": task.storage_provider,
|
||||
"error_code": task.error_code,
|
||||
"error_message": task.error_message,
|
||||
"retry_count": task.retry_count,
|
||||
"max_retries": task.max_retries,
|
||||
"poll_interval_seconds": task.poll_interval_seconds,
|
||||
"next_poll_at": task.next_poll_at.isoformat() if task.next_poll_at else None,
|
||||
"poll_count": task.poll_count,
|
||||
"max_poll_count": task.max_poll_count,
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||
"updated_at": task.updated_at.isoformat() if task.updated_at else None,
|
||||
"submitted_at": task.submitted_at.isoformat() if task.submitted_at else None,
|
||||
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
|
||||
"request_metadata": task.request_metadata,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoTaskCancelAdapter(DashboardAdapter):
|
||||
"""视频任务取消适配器"""
|
||||
|
||||
task_id: str
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
from src.core.api_format.conversion.internal_video import VideoStatus
|
||||
|
||||
db = context.db
|
||||
user = context.user
|
||||
is_admin = user.role == UserRole.ADMIN
|
||||
|
||||
query = db.query(VideoTask).filter(VideoTask.id == self.task_id)
|
||||
if not is_admin:
|
||||
query = query.filter(VideoTask.user_id == user.id)
|
||||
|
||||
task = query.first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Video task not found")
|
||||
|
||||
# 只能取消进行中的任务
|
||||
if task.status in [
|
||||
VideoStatus.COMPLETED.value,
|
||||
VideoStatus.FAILED.value,
|
||||
VideoStatus.CANCELLED.value,
|
||||
]:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Cannot cancel task with status: {task.status}"
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
task.status = VideoStatus.CANCELLED.value
|
||||
task.updated_at = datetime.now(timezone.utc)
|
||||
task.completed_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"id": task.id,
|
||||
"status": task.status,
|
||||
"message": "Task cancelled successfully",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class VideoTaskProxyVideoAdapter(DashboardAdapter):
|
||||
task_id: str
|
||||
token: str | None
|
||||
|
||||
def authorize(self, context: ApiRequestContext) -> None: # type: ignore[override]
|
||||
del context
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any:
|
||||
return await _proxy_video_stream_response(
|
||||
task_id=self.task_id,
|
||||
request=context.request,
|
||||
token=self.token,
|
||||
db=context.db,
|
||||
)
|
||||
Reference in New Issue
Block a user