from contextlib import asynccontextmanager from pathlib import Path from uuid import uuid4 from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware from app.api.main import api_router from app.api.v1 import websocket from app.core.config import settings from app.core.logging import configure_logging from app.core.request_context import set_request_id from app.core.websocket.broadcaster import broadcaster from app.db.session import init_db from app.services.scheduler import ( cleanup_stale_running_tasks, start_scheduler, stop_scheduler, sync_scheduler_with_datasources, ) from app.services.earth_news_worker import ( start_earth_news_target_worker, stop_earth_news_target_worker, ) configure_logging() class WebSocketCORSMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): if request.url.path.startswith("/ws") and request.method == "GET": response = await call_next(request) response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "*" return response return await call_next(request) class RequestContextMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): request_id = request.headers.get("X-Request-ID") or uuid4().hex set_request_id(request_id) try: response = await call_next(request) finally: set_request_id(None) response.headers["X-Request-ID"] = request_id return response @asynccontextmanager async def lifespan(app: FastAPI): await init_db() await cleanup_stale_running_tasks() start_scheduler() await sync_scheduler_with_datasources() broadcaster.start() start_earth_news_target_worker() yield await stop_earth_news_target_worker() broadcaster.stop() stop_scheduler() app = FastAPI( title=settings.PROJECT_NAME, version=settings.VERSION, description="智能星球计划 - 态势感知系统\n\n## 功能模块\n\n- **用户认证**: JWT-based authentication\n- **数据源管理**: 多源数据采集器管理\n- **任务调度**: 定时任务调度与监控\n- **实时更新**: WebSocket实时数据推送\n- **告警系统**: 多级告警管理\n\n## 数据层级\n\n- **L1**: 核心数据 (TOP500, Epoch AI GPU)\n- **L2**: 扩展数据 (HuggingFace, PeeringDB, 海缆)\n- **L3**: 分析数据\n- **L4**: 决策支持", lifespan=lifespan, docs_url=None, redoc_url="/docs", openapi_url="/openapi.json", ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(RequestContextMiddleware) app.add_middleware(WebSocketCORSMiddleware) app.include_router(api_router, prefix="/api/v1") app.include_router(websocket.router) EARTH_BRAND_ASSET_DIR = Path(__file__).resolve().parents[2] / "data" / "earth-brand" EARTH_BRAND_ASSET_DIR.mkdir(parents=True, exist_ok=True) app.mount( "/earth-brand-assets", StaticFiles(directory=str(EARTH_BRAND_ASSET_DIR)), name="earth-brand-assets", ) @app.get("/health") async def health_check(): return {"status": "healthy", "version": settings.VERSION} @app.get("/") async def root(): return { "name": settings.PROJECT_NAME, "version": settings.VERSION, "docs": "/docs", "redoc": "/redoc", } @app.get("/api/v1/scheduler/jobs") async def get_scheduler_jobs(): from app.services.scheduler import get_scheduler_jobs return {"jobs": get_scheduler_jobs()}