167 lines
7.3 KiB
Python
167 lines
7.3 KiB
Python
"""FastAPI 应用入口"""
|
|
|
|
import asyncio
|
|
import re
|
|
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.datasource import router as datasource_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.api.public import router as public_router
|
|
from app.api.screens import router as screen_router
|
|
from app.config import settings
|
|
from app.core.body_limit import BodyLimit, BodyLimitRule
|
|
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
|
|
from app.services.screen_mongo import MongoScreenStore
|
|
from app.services.screens import ScreenService
|
|
from app.services.thumbnails import ThumbnailStorage
|
|
|
|
# 按配置选择 MongoDB 或进程内存,会话记录与图检查点使用同一种存储
|
|
persistence = await open_persistence()
|
|
application.state.conversation_store = persistence.store
|
|
mcp_processes = []
|
|
sweeper = None
|
|
trash_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())
|
|
# 大屏存取必须落 MongoDB:没配时不创建服务,screens 相关接口返回 503,而不是存在内存里重启就丢
|
|
screen_service = None
|
|
if persistence.db is not None:
|
|
screen_store = MongoScreenStore(persistence.db)
|
|
await screen_store.ensure_indexes()
|
|
screen_service = ScreenService(
|
|
screen_store,
|
|
ThumbnailStorage(settings.thumbnail_dir),
|
|
data_files,
|
|
settings.trash_retention_days,
|
|
)
|
|
trash_sweeper = asyncio.create_task(screen_service.run_trash_sweeper())
|
|
else:
|
|
logger.warning("未配置 MONGODB_URI,大屏存取接口不可用")
|
|
application.state.screen_service = screen_service
|
|
# 物料目录启动时载入内存,动作校验与前端接口共用,避免重复查询数据库
|
|
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 trash_sweeper is not None:
|
|
trash_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")
|
|
application.include_router(screen_router, prefix="/api", dependencies=authenticated)
|
|
application.include_router(
|
|
datasource_router, prefix="/api", dependencies=authenticated
|
|
)
|
|
# 匿名接口统一放 /api/public 下,不挂鉴权
|
|
application.include_router(public_router, prefix="/api")
|
|
# 保存 / 发布的请求体比普通接口大得多,单独放宽;封面图 1 MB
|
|
application.add_middleware(
|
|
BodyLimit,
|
|
rules=[
|
|
BodyLimitRule(
|
|
frozenset({"POST", "PUT"}),
|
|
re.compile(r"/api/screens(/[^/]+)?"),
|
|
settings.screen_max_mb * 1024 * 1024,
|
|
f"大屏数据不能超过 {settings.screen_max_mb} MB",
|
|
),
|
|
BodyLimitRule(
|
|
frozenset({"PUT"}),
|
|
re.compile(r"/api/screens/[^/]+/thumbnail"),
|
|
1024 * 1024 + 64 * 1024,
|
|
"封面图不能超过 1 MB",
|
|
),
|
|
# 其余带请求体的接口(发布、复制、改名、数据源取数、会话)都只有几个字段
|
|
BodyLimitRule(
|
|
frozenset({"POST", "PUT", "PATCH"}),
|
|
re.compile(r"/api/(screens|datasource|conversations)(/.*)?"),
|
|
64 * 1024,
|
|
"请求体过大",
|
|
),
|
|
],
|
|
)
|
|
return application
|
|
|
|
|
|
app = create_app()
|