from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from app.engine.config import STATIC_DIR from app.platform.registry import TenantRegistry # 路径锚定(不随 CWD 漂移):seed_profiles 与 tenants 都在仓库根 BASE_DIR = Path(__file__).resolve().parents[1] SEED_DIR = BASE_DIR / "seed_profiles" TENANTS_DIR = BASE_DIR / "tenants" # .gitignore 已忽略 tenants/*.db # 过渡兜底(Task 5b→7,中间件上线后随 routes/runtime.py 一并删除): # Host 中间件落地前请求 state 里没有租户,一律落默认租户 jiancai DEFAULT_TENANT_HOST = "chat2.agent.hnxinyouhe.com" @asynccontextmanager async def lifespan(app: FastAPI): # 测试注入缝:conftest 预置过 app.state.registry(临时 data_dir 的独立 # Registry)则沿用,不再加载仓库 seed——测试租户库与生产互不污染 registry = getattr(app.state, "registry", None) if registry is None: registry = TenantRegistry.load_from_dir(SEED_DIR, data_dir=TENANTS_DIR) app.state.registry = registry # 预热默认租户(过渡兜底读 app.state.default_runtime)。engine 建在本 # lifespan 的事件循环里——连接与 loop 绑定,别处另起 asyncio.run 复用 # 同一 engine 会跨 loop 死锁(测试直连造数走 conftest.async_db) app.state.default_runtime = await registry.resolve(DEFAULT_TENANT_HOST) # 询盘推送(设计 8.2): 清扫崩溃遗留的 stale pending → failed。逐租户、 # 用该租户自己的 session_factory;db 文件不存在的租户跳过——全新库 # 无遗留行,保持 engine 懒建纪律(预热的默认租户文件已建,照常扫)。 from app.engine.services.lead_push_service import sweep_stale_pending for rt in registry.runtimes(): if rt.config.status != "active" or not rt.db_path.exists(): continue ready = await registry.ensure_ready(rt) await sweep_stale_pending(session_factory=ready.session_factory) try: yield finally: await registry.dispose_all() # Windows 文件锁: 不关则库文件删不掉 app = FastAPI(title="HESUPIPE AI 客服", lifespan=lifespan) from app.engine.routes.chat import router as chat_router from app.engine.routes.chat_rest import router as chat_rest_router app.include_router(chat_router) app.include_router(chat_rest_router) app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") @app.middleware("http") async def static_no_cache(request, call_next): """静态资源协商缓存(2026-09-30 线上事件): 无 Cache-Control 时浏览器对 widget.js 走启发式缓存,修复部署后用户端仍跑旧脚本。no-cache=每次用 etag 轻量协商,有更新即时生效,未变则 304 几乎零开销。""" resp = await call_next(request) if request.url.path.startswith("/static/"): resp.headers["Cache-Control"] = "no-cache" return resp @app.get("/") async def index(): from fastapi.responses import FileResponse return FileResponse(str(STATIC_DIR / "chat.html")) @app.get("/health") async def health(): return {"status": "ok"}