Files
my-v0-server/app/main.py
T

109 lines
4.7 KiB
Python

"""FastAPI 应用入口"""
import asyncio
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI
from loguru import logger
from app.api.agent import router
from app.api.conversations import router as conversation_router
from app.api.files import UploadSizeLimit
from app.api.files import limits_router as file_limits_router
from app.api.files import router as file_router
from app.api.materials import router as material_router
from app.core.auth import get_current_user
from app.core.exceptions import register_exception_handlers
from app.core.logging import setup_logging
@asynccontextmanager
async def lifespan(application: FastAPI):
setup_logging()
from app.agent.screen_agent import get_screen_agent
from app.clients.decision import close_decision_engine
from app.clients.mcp import start_mcp_servers, stop_mcp_servers
from app.clients.user_center import close_user_center
from app.datafiles.insights import generate_insights, insights_enabled
from app.datafiles.service import DataFileService
from app.datafiles.storage import open_data_file_store
from app.observability.tracing import flush_tracing
from app.processor.graph import build_graph
from app.services.agent import AgentService
from app.services.material_mongo import warm_material_cache
from app.services.persistence import open_persistence
# 按配置选择 MongoDB 或进程内存,会话记录与图检查点使用同一种存储
persistence = await open_persistence()
application.state.conversation_store = persistence.store
mcp_processes = []
sweeper = None
data_files = None
try:
# 数据文件的元数据和会话记录用同一种存储;启动时先清理一遍过期目录,之后每小时一次
data_files = DataFileService(
await open_data_file_store(persistence.db),
persistence.store,
insights=generate_insights if insights_enabled() else None,
)
application.state.data_file_service = data_files
sweeper = asyncio.create_task(data_files.run_sweeper())
# 物料目录启动时载入内存,动作校验与前端接口共用,避免重复查询数据库
await warm_material_cache(persistence.db)
# 拉起 mcp_servers.json 中由应用管理的服务;单个服务失败只记录,用到它的请求执行时报错
mcp_processes = await start_mcp_servers()
# 提前拉取 MCP 工具并组装 agent,问题在启动时暴露;失败只记录,首个请求会重试
try:
await get_screen_agent()
except Exception as error:
logger.opt(exception=error).error("screen agent 预先组装失败")
application.state.agent_service = AgentService(
build_graph(persistence.checkpointer), persistence.store, data_files
)
yield
finally:
if sweeper is not None:
sweeper.cancel()
if data_files is not None:
await data_files.aclose()
# 共享客户端只在应用退出时关闭,追踪刷新放到线程避免阻塞事件循环
try:
await stop_mcp_servers(mcp_processes)
finally:
try:
await persistence.close()
finally:
try:
await close_decision_engine()
finally:
try:
await close_user_center()
finally:
try:
await asyncio.to_thread(flush_tracing)
finally:
await logger.complete()
def create_app() -> FastAPI:
application = FastAPI(title="Screen Agent API", lifespan=lifespan)
# 统一处理请求校验、HTTP 错误和未捕获异常
register_exception_handlers(application)
# 统一 /api 前缀,方便前端代理和反向代理只配置一条规则
# 会话和 agent 接口需登录,物料目录是全局公开数据
authenticated = [Depends(get_current_user)]
application.include_router(router, prefix="/api", dependencies=authenticated)
application.include_router(
conversation_router, prefix="/api", dependencies=authenticated
)
# 上传限制无需登录;要先于 /files/{file_id} 注册,否则会被当成文件 ID
application.include_router(file_limits_router, prefix="/api")
application.include_router(file_router, prefix="/api", dependencies=authenticated)
# 上传在读请求体之前先按 Content-Length 检查大小
application.add_middleware(UploadSizeLimit, path=f"/api{file_router.prefix}")
application.include_router(material_router, prefix="/api")
return application
app = create_app()