Ctrl+C 停服时 aiosqlite 后台线程上的连接若未归还池会被 GC 直接丢弃, 表现为 shutdown 时的 non-checked-in connection SAWarning。在 lifespan 关闭 阶段调用 engine.dispose() 主动清理(数据不受影响,纯停机噪音修复)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
73 lines
2.4 KiB
Python
73 lines
2.4 KiB
Python
from contextlib import asynccontextmanager
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
|
||
from app.core.config import settings
|
||
from app.core.database import init_db, Base, engine
|
||
from app.core.security import verify_token
|
||
from app.api.books import router as books_router
|
||
from app.api.chat import router as chat_router
|
||
from app.api.models import router as models_router
|
||
from app.api.mcp import router as mcp_router
|
||
from app.api.auth import router as auth_router
|
||
|
||
from app.models.book import Book, Chapter, Segment
|
||
from app.models.chat import Conversation, Message
|
||
from app.models.ai_model import AiModel
|
||
from app.models.mcp_server import McpServer
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
await init_db()
|
||
try:
|
||
yield
|
||
finally:
|
||
# 优雅停机:主动销毁连接池,避免 SQLite 后台线程的连接被 GC 丢弃
|
||
# (表现为 shutdown 时 SAWarning:non-checked-in connection ... dropped)
|
||
await engine.dispose()
|
||
|
||
|
||
app = FastAPI(title="BookMate", version="0.1.0", lifespan=lifespan)
|
||
|
||
# CORS 先加,站点访问密码中间件后加 → 密码中间件在最外层先跑
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
|
||
@app.middleware("http")
|
||
async def site_auth_middleware(request: Request, call_next):
|
||
"""站点访问密码门:设置 SITE_PASSWORD 后,除登录/健康检查/CORS 预检外全部拦截。"""
|
||
if not settings.site_password:
|
||
return await call_next(request)
|
||
path = request.url.path
|
||
method = request.method
|
||
# CORS 预检、健康检查、登录相关端点放行
|
||
if method == "OPTIONS" or path == "/api/health" or path.startswith("/api/auth/"):
|
||
return await call_next(request)
|
||
token = request.headers.get("x-access-token", "")
|
||
if not token or not verify_token(token, settings.site_password):
|
||
return JSONResponse(status_code=401, content={"detail": "需要访问密码"})
|
||
return await call_next(request)
|
||
|
||
|
||
app.include_router(books_router, prefix="/api")
|
||
app.include_router(chat_router, prefix="/api")
|
||
app.include_router(models_router, prefix="/api")
|
||
app.include_router(mcp_router, prefix="/api")
|
||
app.include_router(auth_router, prefix="/api")
|
||
|
||
|
||
@app.get("/api/health")
|
||
async def health():
|
||
return {"status": "ok"} |