"""随身助手后端入口。""" from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from .api import agents, auth, chat, config, files, projects, tasks, ws from .config import settings from .core.migrate import migrate from .core.response import fail from .database import Base, SessionLocal, engine from .services.seed import seed_admin_user, seed_builtin_agents, seed_default_config STATIC_DIR = Path(__file__).resolve().parent.parent / "static" STATIC_DIR.mkdir(exist_ok=True) @asynccontextmanager async def lifespan(app: FastAPI): settings.ensure_dirs() Base.metadata.create_all(bind=engine) db = SessionLocal() try: migrate(db) seed_builtin_agents(db) seed_admin_user(db) seed_default_config(db) finally: db.close() yield app = FastAPI( title=settings.APP_NAME, version="0.1.0", description="随身助手:对话 / 智能体 / 多类型项目开发 / 视频分析", lifespan=lifespan, ) # CORS:开发阶段放开,生产环境按需收紧 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception): return JSONResponse(status_code=500, content={"code": 500, "data": None, "message": f"服务器错误:{exc}"}) # 路由注册(统一 /api 前缀) app.include_router(auth.router, prefix=settings.API_PREFIX) app.include_router(chat.router, prefix=settings.API_PREFIX) app.include_router(agents.router, prefix=settings.API_PREFIX) app.include_router(projects.router, prefix=settings.API_PREFIX) app.include_router(files.router, prefix=settings.API_PREFIX) app.include_router(tasks.router, prefix=settings.API_PREFIX) app.include_router(config.router, prefix=settings.API_PREFIX) app.include_router(ws.router, prefix=settings.API_PREFIX) # 静态:上传文件访问(视频抽帧 URL 等) settings.ensure_dirs() app.mount("/uploads", StaticFiles(directory=str(settings.UPLOAD_DIR)), name="uploads") @app.exception_handler(404) async def spa_fallback(request: Request, exc: Exception): """SPA 兜底:非 API 路径一律返回 index.html(前端 hash 路由,实际无需,但稳妥)。""" if request.url.path.startswith("/api") or request.url.path.startswith("/uploads"): return JSONResponse(status_code=404, content={"code": 404, "data": None, "message": "接口不存在"}) index = STATIC_DIR / "index.html" if index.exists(): return FileResponse(index) return JSONResponse(status_code=404, content={"code": 404, "data": None, "message": "前端未构建"}) @app.get("/api/health") def health(): return {"code": 0, "data": {"status": "ok", "app": settings.APP_NAME}, "message": "ok"} # 前端静态托管(最后挂载,避免抢占 API 路由) app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static") if __name__ == "__main__": import uvicorn uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)