from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from app.config import settings, ensure_dirs, STATIC_DIR from app.models.database import Base # 后台推送任务的独立连接写结果可能撞上 SMTP 窗口内持有的写锁,默认 5s # 忙等上限低于 SMTP 最坏时长,抬到 30s;等待发生在 aiosqlite 工作线程,不阻塞事件循环 engine = create_async_engine(settings.DATABASE_URL, echo=settings.APP_DEBUG, connect_args={"timeout": 30}) async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async def _migrate(conn): result = await conn.execute(text("PRAGMA table_info(conversations)")) columns = {row[1] for row in result} if "page_token" not in columns: await conn.execute(text( "ALTER TABLE conversations ADD COLUMN page_token VARCHAR(64)" )) result = await conn.execute(text("PRAGMA table_info(evaluations)")) columns = {row[1] for row in result} if "correction_count" not in columns: await conn.execute(text( "ALTER TABLE evaluations ADD COLUMN correction_count INTEGER DEFAULT 0" )) @asynccontextmanager async def lifespan(app: FastAPI): ensure_dirs() async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await _migrate(conn) # 询盘推送(设计 8.2): 清扫崩溃遗留的 stale pending → failed # 函数内导入: lead_push_service 顶部 from app.main import async_session, # 顶层互相导入会循环 from app.services.lead_push_service import sweep_stale_pending await sweep_stale_pending() yield app = FastAPI(title="HESUPIPE AI 客服", lifespan=lifespan) from app.api.routes.chat import router as chat_router from app.api.routes.admin import router as admin_router from app.api.routes.chat_rest import router as chat_rest_router app.include_router(chat_router) app.include_router(admin_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("/admin") async def admin_page(): from fastapi.responses import FileResponse return FileResponse(str(STATIC_DIR / "admin.html")) @app.get("/health") async def health(): return {"status": "ok"}