"""应用入口。""" from __future__ import annotations import logging from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles import app.models # noqa: F401 注册全部表 from app.api import router from app.database import Base, engine, session_scope from app.errors import AppError from app.scheduler import start_scheduler, stop_scheduler from app.schema_upgrade import upgrade_schema from app.security import ensure_jwt_secret from app.services.auth import ensure_admin logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") @asynccontextmanager async def lifespan(_: FastAPI): Base.metadata.create_all(bind=engine) upgrade_schema() with session_scope() as session: ensure_jwt_secret(session) ensure_admin(session) start_scheduler() try: yield finally: stop_scheduler() WEB_DIR = Path(__file__).resolve().parent.parent / "web" app = FastAPI(title="HyperNode-AI", version="1.0.0", lifespan=lifespan) app.include_router(router) _assets = WEB_DIR / "assets" if _assets.is_dir(): app.mount("/assets", StaticFiles(directory=_assets), name="assets") @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/") def console() -> FileResponse: return _spa("") @app.get("/{full_path:path}") def console_routes(full_path: str) -> FileResponse: return _spa(full_path) def _spa(full_path: str) -> FileResponse: root = WEB_DIR.resolve() index = root / "index.html" if full_path: candidate = (root / full_path).resolve() if candidate.is_file() and (candidate == root or root in candidate.parents): return FileResponse(candidate) return FileResponse(index) @app.exception_handler(AppError) async def handle_app_error(_: Request, exc: AppError) -> JSONResponse: return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})