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,125 @@
"""Admin error stats routes."""
from __future__ import annotations
from datetime import date, timezone
from typing import Any
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import func
from sqlalchemy.orm import Session
from src.api.base.admin_adapter import AdminApiAdapter
from src.api.base.context import ApiRequestContext
from src.config.constants import CacheTTL
from src.database import get_db
from src.models.database import StatsDailyError, Usage
from src.services.system.time_range import TimeRangeParams
from src.utils.cache_decorator import cache_result
from .common import _apply_admin_default_range, _build_time_range_params, pipeline
router = APIRouter()
class AdminErrorDistributionAdapter(AdminApiAdapter):
def __init__(self, time_range: TimeRangeParams | None) -> None:
self.time_range = _apply_admin_default_range(time_range)
@cache_result(
key_prefix="admin:stats:errors:distribution",
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
user_specific=False,
vary_by=[
"time_range.start_date",
"time_range.end_date",
"time_range.preset",
"time_range.timezone",
"time_range.tz_offset_minutes",
],
)
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
if not self.time_range:
return {"distribution": [], "trend": []}
time_range = self.time_range
is_utc = (time_range.timezone in {None, "UTC"}) and time_range.tz_offset_minutes == 0
distribution: dict[str, int] = {}
trend: dict[str, dict[str, int]] = {}
if is_utc:
start_utc, end_utc = time_range.to_utc_datetime_range()
rows = (
context.db.query(StatsDailyError)
.filter(StatsDailyError.date >= start_utc, StatsDailyError.date < end_utc)
.all()
)
for row in rows:
date_str = (
row.date.astimezone(timezone.utc).date().isoformat()
if row.date.tzinfo
else row.date.date().isoformat()
)
distribution[row.error_category] = distribution.get(row.error_category, 0) + int(
row.count or 0
)
trend.setdefault(date_str, {})
trend[date_str][row.error_category] = trend[date_str].get(
row.error_category, 0
) + int(row.count or 0)
else:
for local_date, day_start_utc, day_end_utc in time_range.get_local_day_hours():
rows = (
context.db.query(
Usage.error_category,
func.count(Usage.id).label("count"),
)
.filter(
Usage.created_at >= day_start_utc,
Usage.created_at < day_end_utc,
Usage.error_category.isnot(None),
)
.group_by(Usage.error_category)
.all()
)
date_str = local_date.isoformat()
for row in rows:
distribution[row.error_category] = distribution.get(
row.error_category, 0
) + int(row.count or 0)
trend.setdefault(date_str, {})
trend[date_str][row.error_category] = trend[date_str].get(
row.error_category, 0
) + int(row.count or 0)
trend_items = []
for day in sorted(trend.keys()):
counts = trend[day]
total = sum(counts.values())
trend_items.append({"date": day, "total": total, "categories": counts})
distribution_items = [
{"category": category, "count": count}
for category, count in sorted(
distribution.items(), key=lambda item: item[1], reverse=True
)
]
return {"distribution": distribution_items, "trend": trend_items}
@router.get("/errors/distribution")
async def get_error_distribution(
request: Request,
db: Session = Depends(get_db),
start_date: date | None = Query(None),
end_date: date | None = Query(None),
preset: str | None = Query(None),
timezone_name: str | None = Query(None, alias="timezone"),
tz_offset_minutes: int | None = Query(0),
) -> Any:
time_range = _build_time_range_params(
start_date, end_date, preset, timezone_name, tz_offset_minutes
)
adapter = AdminErrorDistributionAdapter(time_range=time_range)
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)