main.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """PaperGraph 主应用入口 —— FastAPI 实例、中间件、路由挂载与生命周期管理."""
  2. import asyncio
  3. import logging
  4. from contextlib import asynccontextmanager
  5. from fastapi import FastAPI, HTTPException, Request
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from fastapi.responses import JSONResponse
  8. from starlette.middleware.base import BaseHTTPMiddleware
  9. from ..settings import configure_logging, get_settings, print_config, validate_config
  10. from ..services.graph.kg_relations import get_kg_metrics
  11. from .routes import paper_routes, paper_reader_routes, search_routes
  12. settings = get_settings()
  13. logger = logging.getLogger(__name__)
  14. class _MeaningfulActivityMiddleware(BaseHTTPMiddleware):
  15. async def dispatch(self, request: Request, call_next):
  16. response = await call_next(request)
  17. try:
  18. from ..services.daily.daily_auto_refresh import touch_meaningful_activity_if_needed
  19. touch_meaningful_activity_if_needed(request.app, request.method, request.url.path)
  20. except Exception:
  21. pass
  22. return response
  23. @asynccontextmanager
  24. async def lifespan(app: FastAPI):
  25. configure_logging(settings.log_level)
  26. logger.info("%s", "=" * 60)
  27. logger.info("📚 %s v%s", settings.app_name, settings.app_version)
  28. logger.info("%s", "=" * 60)
  29. app.state.last_meaningful_activity_monotonic = None
  30. daily_refresh_task: asyncio.Task | None = None
  31. print_config()
  32. try:
  33. validate_config()
  34. logger.info("✅ 配置验证通过")
  35. except ValueError as e:
  36. logger.error("❌ 配置验证失败: %s", e)
  37. raise
  38. try:
  39. from ..services.daily.daily_auto_refresh import spawn_daily_auto_refresh
  40. daily_refresh_task = spawn_daily_auto_refresh(app)
  41. except Exception as exc:
  42. logger.warning("每日论文后台自动刷新任务未启动: %s", exc)
  43. logger.info("%s", "=" * 60)
  44. yield
  45. logger.info("%s", "=" * 60)
  46. logger.info("👋 应用正在关闭...")
  47. if daily_refresh_task is not None:
  48. daily_refresh_task.cancel()
  49. try:
  50. await daily_refresh_task
  51. except asyncio.CancelledError:
  52. pass
  53. except Exception:
  54. logger.debug("每日论文后台任务结束异常", exc_info=True)
  55. logger.info("%s", "=" * 60)
  56. app = FastAPI(
  57. title=settings.app_name,
  58. version=settings.app_version,
  59. description=settings.description,
  60. docs_url=None,
  61. redoc_url=None,
  62. lifespan=lifespan,
  63. )
  64. app.add_middleware(
  65. CORSMiddleware,
  66. allow_origins=settings.get_cors_origins_list(),
  67. allow_credentials=True,
  68. allow_methods=["*"],
  69. allow_headers=["*"],
  70. )
  71. app.add_middleware(_MeaningfulActivityMiddleware)
  72. app.include_router(paper_routes.router, prefix="/api")
  73. app.include_router(paper_reader_routes.router, prefix="/api")
  74. app.include_router(search_routes.router, prefix="/api")
  75. @app.get("/")
  76. async def root():
  77. return {
  78. "name": settings.app_name,
  79. "version": settings.app_version,
  80. "status": "running",
  81. "docs_enabled": False,
  82. }
  83. @app.get("/health")
  84. async def health():
  85. return {
  86. "status": "healthy",
  87. "service": settings.app_name,
  88. "version": settings.app_version,
  89. "kg_metrics": get_kg_metrics(),
  90. }
  91. @app.exception_handler(Exception)
  92. async def global_exception_handler(request: Request, exc: Exception):
  93. if isinstance(exc, HTTPException):
  94. return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
  95. logger.exception(
  96. "未处理异常: %s %s",
  97. request.method,
  98. request.url.path,
  99. exc_info=exc,
  100. )
  101. return JSONResponse(
  102. status_code=500,
  103. content={"success": False, "message": str(exc)},
  104. )
  105. if __name__ == "__main__":
  106. import uvicorn
  107. uvicorn.run(
  108. "app.api.main:app",
  109. host=settings.host,
  110. port=settings.port,
  111. reload=settings.debug,
  112. )