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:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,40 @@
"""User monitoring routers."""
from __future__ import annotations
from fastapi import APIRouter
from starlette.routing import BaseRoute
from .user import router as monitoring_router
_RUST_OWNED_MONITORING_ROUTE_SIGNATURES = frozenset(
{
("GET", "/api/monitoring/my-audit-logs"),
("GET", "/api/monitoring/rate-limit-status"),
}
)
def _route_is_rust_owned(route: BaseRoute) -> bool:
path = getattr(route, "path", None)
methods = getattr(route, "methods", None)
if not isinstance(path, str) or not methods:
return False
return any(
(method, path) in _RUST_OWNED_MONITORING_ROUTE_SIGNATURES
for method in methods
if method not in {"HEAD", "OPTIONS"}
)
def _build_python_monitoring_router() -> APIRouter:
router = APIRouter()
router.include_router(monitoring_router)
router.routes = [route for route in router.routes if not _route_is_rust_owned(route)]
return router
python_monitoring_router = _build_python_monitoring_router()
router = python_monitoring_router
__all__ = ["python_monitoring_router", "router"]

View File

@@ -0,0 +1,237 @@
"""普通用户可访问的监控与审计端点。"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from src.api.base.adapter import ApiAdapter, ApiMode
from src.api.base.context import ApiRequestContext
from src.api.base.pagination import PaginationMeta, build_pagination_payload, paginate_query
from src.api.base.pipeline import get_pipeline
from src.core.logger import logger
from src.database import get_db
from src.models.database import ApiKey, AuditLog
from src.services.rate_limit.user_rpm_limiter import SYSTEM_RPM_CONFIG_KEY, get_user_rpm_limiter
from src.services.system.config import SystemConfigService
router = APIRouter(prefix="/api/monitoring", tags=["Monitoring"])
pipeline = get_pipeline()
@router.get("/my-audit-logs")
async def get_my_audit_logs(
request: Request,
event_type: str | None = Query(None, description="事件类型筛选"),
days: int = Query(30, description="查询天数"),
limit: int = Query(50, description="返回数量限制"),
offset: int = Query(0, ge=0, description="偏移量"),
db: Session = Depends(get_db),
) -> Any:
"""
获取我的审计日志
获取当前用户的审计日志记录。需要登录。
**查询参数**:
- `event_type`: 可选,事件类型筛选
- `days`: 查询最近多少天的日志,默认 30 天
- `limit`: 返回数量限制,默认 50
- `offset`: 分页偏移量,默认 0
**返回字段**:
- `items`: 审计日志列表,每条日志包含:
- `id`: 日志 ID
- `event_type`: 事件类型
- `description`: 事件描述
- `ip_address`: IP 地址
- `status_code`: HTTP 状态码
- `created_at`: 创建时间
- `meta`: 分页元数据total, limit, offset, count
- `filters`: 筛选条件
"""
adapter = UserAuditLogsAdapter(event_type=event_type, days=days, limit=limit, offset=offset)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
@router.get("/rate-limit-status")
async def get_rate_limit_status(request: Request, db: Session = Depends(get_db)) -> Any:
"""
获取速率限制状态
获取当前用户所有活跃 API Key 的速率限制状态。需要登录。
**返回字段**:
- `user_id`: 用户 ID
- `api_keys`: API Key 限流状态列表,每个包含:
- `api_key_name`: API Key 名称
- `limit`: 速率限制上限
- `remaining`: 剩余可用次数
- `reset_time`: 限制重置时间
- `window`: 时间窗口
"""
adapter = UserRateLimitStatusAdapter()
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
class AuthenticatedApiAdapter(ApiAdapter):
"""需要用户登录的适配器基类。"""
mode = ApiMode.USER
def authorize(self, context: ApiRequestContext) -> None: # type: ignore[override]
if not context.user:
raise HTTPException(status_code=401, detail="未登录")
@dataclass
class UserAuditLogsAdapter(AuthenticatedApiAdapter):
event_type: str | None
days: int
limit: int
offset: int
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
user = context.user
if not user:
raise HTTPException(status_code=401, detail="未登录")
query = db.query(AuditLog).filter(AuditLog.user_id == user.id)
if self.event_type:
query = query.filter(AuditLog.event_type == self.event_type)
cutoff_time = datetime.now(timezone.utc) - timedelta(days=self.days)
query = query.filter(AuditLog.created_at >= cutoff_time)
query = query.order_by(AuditLog.created_at.desc())
total, logs = paginate_query(query, self.limit, self.offset)
items = [
{
"id": log.id,
"event_type": log.event_type,
"description": log.description,
"ip_address": log.ip_address,
"status_code": log.status_code,
"created_at": log.created_at.isoformat() if log.created_at else None,
}
for log in logs
]
meta = PaginationMeta(
total=total,
limit=self.limit,
offset=self.offset,
count=len(items),
)
return build_pagination_payload(
items,
meta,
filters={
"event_type": self.event_type,
"days": self.days,
},
)
class UserRateLimitStatusAdapter(AuthenticatedApiAdapter):
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
db = context.db
user = context.user
if not user:
raise HTTPException(status_code=401, detail="未登录")
api_keys = (
db.query(ApiKey)
.filter(ApiKey.user_id == user.id, ApiKey.is_active.is_(True))
.order_by(ApiKey.created_at.desc())
.all()
)
try:
limiter = await get_user_rpm_limiter()
system_default_raw = SystemConfigService.get_config(
db, SYSTEM_RPM_CONFIG_KEY, default=0
)
system_default = max(int(system_default_raw or 0), 0)
reset_at = limiter.get_reset_at()
window = f"{limiter.bucket_seconds}s"
except Exception as exc:
logger.warning("读取新 RPM 限流状态失败,回退插件状态接口: {}", exc)
limiter = None
system_default = 0
reset_at = None
window = None
rate_limit_info = []
for key in api_keys:
if limiter is not None:
if key.is_standalone:
user_limit = key.rate_limit if key.rate_limit is not None else system_default
user_scope_key = limiter.get_standalone_rpm_key(key.id)
key_limit = 0
else:
user_limit = user.rate_limit if user.rate_limit is not None else system_default
user_scope_key = limiter.get_user_rpm_key(user.id)
key_limit = max(int(key.rate_limit or 0), 0)
user_count = (
await limiter.get_scope_count(user_scope_key)
if user_limit and user_limit > 0
else 0
)
key_count = (
await limiter.get_scope_count(limiter.get_key_rpm_key(key.id))
if key_limit > 0
else 0
)
user_remaining = max(user_limit - user_count, 0) if user_limit > 0 else None
key_remaining = max(key_limit - key_count, 0) if key_limit > 0 else None
scoped_statuses: list[tuple[str, int, int]] = []
if user_limit > 0 and user_remaining is not None:
scoped_statuses.append(("user", user_limit, user_remaining))
if key_limit > 0 and key_remaining is not None:
scoped_statuses.append(("key", key_limit, key_remaining))
primary_scope = (
min(scoped_statuses, key=lambda item: item[2]) if scoped_statuses else None
)
rate_limit_info.append(
{
"api_key_name": key.name or f"Key-{key.id}",
"limit": primary_scope[1] if primary_scope else None,
"remaining": primary_scope[2] if primary_scope else None,
"scope": primary_scope[0] if primary_scope else None,
"reset_time": reset_at.isoformat() if reset_at else None,
"window": window,
"user_limit": user_limit,
"user_remaining": user_remaining,
"key_limit": key_limit if key_limit > 0 else None,
"key_remaining": key_remaining,
}
)
continue
rate_limit_info.append(
{
"api_key_name": key.name or f"Key-{key.id}",
"limit": None,
"remaining": None,
"scope": None,
"reset_time": None,
"window": None,
"user_limit": None,
"user_remaining": None,
"key_limit": None,
"key_remaining": None,
}
)
return {"user_id": user.id, "api_keys": rate_limit_info}