2025-10-30 11:14:43 +08:00
|
|
|
"""FastAPI应用主入口"""
|
2026-04-24 10:11:23 +08:00
|
|
|
from fastapi import FastAPI, Request, status, HTTPException, Depends
|
2025-10-30 11:14:43 +08:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
from fastapi.responses import JSONResponse, FileResponse
|
|
|
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2025-10-31 17:23:25 +08:00
|
|
|
from app.config import settings as config_settings
|
2025-10-30 11:14:43 +08:00
|
|
|
from app.database import close_db, _session_stats
|
|
|
|
|
from app.logger import setup_logging, get_logger
|
|
|
|
|
from app.middleware import RequestIDMiddleware
|
|
|
|
|
from app.middleware.auth_middleware import AuthMiddleware
|
2026-01-09 17:13:19 +08:00
|
|
|
from app.mcp import mcp_client, register_status_sync
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
setup_logging(
|
2025-10-31 17:23:25 +08:00
|
|
|
level=config_settings.log_level,
|
|
|
|
|
log_to_file=config_settings.log_to_file,
|
|
|
|
|
log_file_path=config_settings.log_file_path,
|
|
|
|
|
max_bytes=config_settings.log_max_bytes,
|
|
|
|
|
backup_count=config_settings.log_backup_count
|
2025-10-30 11:14:43 +08:00
|
|
|
)
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
"""应用生命周期管理"""
|
2026-01-09 17:13:19 +08:00
|
|
|
# 注册MCP状态同步服务
|
|
|
|
|
register_status_sync()
|
2026-04-29 08:31:07 +08:00
|
|
|
|
|
|
|
|
# 安全保障:确保后台任务表存在(兼容未执行Alembic迁移的旧部署)
|
|
|
|
|
try:
|
|
|
|
|
from app.database import get_engine
|
|
|
|
|
from app.models.background_task import BackgroundTask
|
|
|
|
|
_startup_engine = await get_engine("system")
|
|
|
|
|
async with _startup_engine.begin() as conn:
|
|
|
|
|
# 仅创建 background_tasks 表(如果不存在),不影响其他表
|
|
|
|
|
await conn.run_sync(
|
|
|
|
|
lambda sync_conn: BackgroundTask.__table__.create(sync_conn, checkfirst=True)
|
|
|
|
|
)
|
|
|
|
|
logger.info("后台任务表检查完成")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"后台任务表检查失败(不影响启动): {e}")
|
|
|
|
|
|
2025-12-26 15:05:48 +08:00
|
|
|
logger.info("应用启动完成")
|
2025-11-07 22:14:20 +08:00
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
yield
|
2025-11-07 22:14:20 +08:00
|
|
|
|
|
|
|
|
# 清理MCP插件
|
2026-01-09 17:13:19 +08:00
|
|
|
await mcp_client.cleanup()
|
2025-11-22 18:23:30 +08:00
|
|
|
|
|
|
|
|
# 清理HTTP客户端池
|
|
|
|
|
from app.services.ai_service import cleanup_http_clients
|
|
|
|
|
await cleanup_http_clients()
|
|
|
|
|
|
|
|
|
|
# 关闭数据库连接
|
2025-10-30 11:14:43 +08:00
|
|
|
await close_db()
|
2025-11-22 18:23:30 +08:00
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
logger.info("应用已关闭")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
2025-10-31 17:23:25 +08:00
|
|
|
title=config_settings.app_name,
|
|
|
|
|
version=config_settings.app_version,
|
2025-10-30 11:14:43 +08:00
|
|
|
description="AI写小说工具 - 智能小说创作助手",
|
|
|
|
|
lifespan=lifespan
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
|
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
|
|
|
"""处理请求验证错误"""
|
|
|
|
|
logger.error(f"请求验证失败: {exc.errors()}")
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
|
|
|
content={
|
|
|
|
|
"detail": "请求参数验证失败",
|
|
|
|
|
"errors": exc.errors()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
|
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
|
|
|
|
"""处理所有未捕获的异常"""
|
|
|
|
|
logger.error(f"未处理的异常: {type(exc).__name__}: {str(exc)}", exc_info=True)
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
|
|
|
content={
|
|
|
|
|
"detail": "服务器内部错误",
|
2025-10-31 17:23:25 +08:00
|
|
|
"message": str(exc) if config_settings.debug else "请稍后重试"
|
2025-10-30 11:14:43 +08:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.add_middleware(RequestIDMiddleware)
|
|
|
|
|
app.add_middleware(AuthMiddleware)
|
|
|
|
|
|
2025-10-31 17:23:25 +08:00
|
|
|
if config_settings.debug:
|
2025-10-30 11:14:43 +08:00
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
2025-10-31 17:23:25 +08:00
|
|
|
allow_origins=config_settings.cors_origins,
|
2025-10-30 11:14:43 +08:00
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health")
|
|
|
|
|
async def health_check():
|
|
|
|
|
"""健康检查"""
|
|
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health/db-sessions")
|
2026-04-24 10:11:23 +08:00
|
|
|
async def db_session_stats(request: Request):
|
2025-10-30 11:14:43 +08:00
|
|
|
"""
|
|
|
|
|
数据库会话统计(监控连接泄漏)
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
- created: 总创建会话数
|
|
|
|
|
- closed: 总关闭会话数
|
|
|
|
|
- active: 当前活跃会话数(应该接近0)
|
|
|
|
|
- errors: 错误次数
|
|
|
|
|
- generator_exits: SSE断开次数
|
|
|
|
|
- last_check: 最后检查时间
|
|
|
|
|
"""
|
2026-04-24 10:11:23 +08:00
|
|
|
if not getattr(request.state, "is_admin", False):
|
|
|
|
|
raise HTTPException(status_code=403, detail="需要管理员权限")
|
2025-10-30 11:14:43 +08:00
|
|
|
return {
|
|
|
|
|
"status": "ok",
|
|
|
|
|
"session_stats": _session_stats,
|
|
|
|
|
"warning": "活跃会话数过多" if _session_stats["active"] > 10 else None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 14:31:54 +08:00
|
|
|
@app.get("/nanobot/sessions")
|
|
|
|
|
async def nanobot_sessions():
|
|
|
|
|
"""兼容层:为环境监控工具提供空的会话列表"""
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
2025-10-30 11:14:43 +08:00
|
|
|
from app.api import (
|
|
|
|
|
projects, outlines, characters, chapters,
|
|
|
|
|
wizard_stream, relationships, organizations,
|
2025-11-07 22:14:20 +08:00
|
|
|
auth, users, settings, writing_styles, memories,
|
2025-12-06 14:08:20 +08:00
|
|
|
mcp_plugins, admin, inspiration, prompt_templates,
|
2026-03-16 11:34:07 +08:00
|
|
|
changelog, careers, foreshadows, prompt_workshop, book_import,
|
2026-04-29 08:31:07 +08:00
|
|
|
project_covers, tasks
|
2025-10-30 11:14:43 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.include_router(auth.router, prefix="/api")
|
|
|
|
|
app.include_router(users.router, prefix="/api")
|
2025-10-30 16:53:50 +08:00
|
|
|
app.include_router(settings.router, prefix="/api")
|
2025-11-13 11:43:45 +08:00
|
|
|
app.include_router(admin.router, prefix="/api")
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
app.include_router(projects.router, prefix="/api")
|
2026-03-16 11:34:07 +08:00
|
|
|
app.include_router(project_covers.router, prefix="/api")
|
2025-10-30 11:14:43 +08:00
|
|
|
app.include_router(wizard_stream.router, prefix="/api")
|
2025-11-14 17:16:24 +08:00
|
|
|
app.include_router(inspiration.router, prefix="/api")
|
2025-10-30 11:14:43 +08:00
|
|
|
app.include_router(outlines.router, prefix="/api")
|
|
|
|
|
app.include_router(characters.router, prefix="/api")
|
2025-12-22 19:53:31 +08:00
|
|
|
app.include_router(careers.router, prefix="/api") # 职业管理API
|
2025-10-30 11:14:43 +08:00
|
|
|
app.include_router(chapters.router, prefix="/api")
|
|
|
|
|
app.include_router(relationships.router, prefix="/api")
|
|
|
|
|
app.include_router(organizations.router, prefix="/api")
|
2025-10-31 17:23:25 +08:00
|
|
|
app.include_router(writing_styles.router, prefix="/api")
|
2025-11-04 14:38:59 +08:00
|
|
|
app.include_router(memories.router) # 记忆管理API (已包含/api前缀)
|
2026-01-19 17:24:37 +08:00
|
|
|
app.include_router(foreshadows.router) # 伏笔管理API (已包含/api前缀)
|
2025-11-07 22:14:20 +08:00
|
|
|
app.include_router(mcp_plugins.router, prefix="/api") # MCP插件管理API
|
2025-11-29 22:01:02 +08:00
|
|
|
app.include_router(prompt_templates.router, prefix="/api") # 提示词模板管理API
|
2025-12-06 14:08:20 +08:00
|
|
|
app.include_router(changelog.router, prefix="/api") # 更新日志API
|
2026-01-27 13:57:32 +08:00
|
|
|
app.include_router(prompt_workshop.router, prefix="/api") # 提示词工坊API
|
2026-03-04 16:28:16 +08:00
|
|
|
app.include_router(book_import.router, prefix="/api") # 拆书导入API
|
2026-04-29 08:31:07 +08:00
|
|
|
app.include_router(tasks.router, prefix="/api") # 后台任务API
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
static_dir = Path(__file__).parent.parent / "static"
|
2026-03-16 11:34:07 +08:00
|
|
|
generated_assets_root_dir = Path(__file__).parent.parent / "storage"
|
|
|
|
|
generated_covers_dir = generated_assets_root_dir / "generated_covers"
|
|
|
|
|
generated_covers_dir.mkdir(parents=True, exist_ok=True)
|
2025-10-30 11:14:43 +08:00
|
|
|
if static_dir.exists():
|
|
|
|
|
app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
|
2026-03-16 11:34:07 +08:00
|
|
|
app.mount("/generated-assets/covers", StaticFiles(directory=str(generated_covers_dir)), name="generated-covers")
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
@app.get("/{full_path:path}")
|
|
|
|
|
async def serve_spa(full_path: str):
|
|
|
|
|
"""服务单页应用,所有非API路径返回index.html"""
|
|
|
|
|
if full_path.startswith("api/"):
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=404,
|
|
|
|
|
content={"detail": "API路径不存在"}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
file_path = static_dir / full_path
|
2026-04-24 10:11:23 +08:00
|
|
|
try:
|
|
|
|
|
resolved_file = file_path.resolve()
|
|
|
|
|
resolved_static = static_dir.resolve()
|
|
|
|
|
resolved_file.relative_to(resolved_static)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=404,
|
|
|
|
|
content={"detail": "页面不存在"}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if resolved_file.is_file():
|
|
|
|
|
return FileResponse(resolved_file)
|
2025-10-30 11:14:43 +08:00
|
|
|
|
|
|
|
|
index_file = static_dir / "index.html"
|
|
|
|
|
if index_file.exists():
|
|
|
|
|
return FileResponse(index_file)
|
|
|
|
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=404,
|
|
|
|
|
content={"detail": "页面不存在"}
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
logger.warning("静态文件目录不存在,请先构建前端: cd frontend && npm run build")
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
async def root():
|
|
|
|
|
return {
|
2026-05-12 12:19:13 +08:00
|
|
|
"message": "欢迎使用墨木灵思",
|
2025-10-31 17:23:25 +08:00
|
|
|
"version": config_settings.app_version,
|
2025-10-30 11:14:43 +08:00
|
|
|
"docs": "/docs",
|
|
|
|
|
"notice": "请先构建前端: cd frontend && npm run build"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import uvicorn
|
|
|
|
|
uvicorn.run(
|
|
|
|
|
"app.main:app",
|
2025-10-31 17:23:25 +08:00
|
|
|
host=config_settings.app_host,
|
|
|
|
|
port=config_settings.app_port,
|
|
|
|
|
reload=config_settings.debug
|
2026-04-24 10:11:23 +08:00
|
|
|
)
|