feat: Host 中间件租户路由(纯ASGI覆盖WS/未知404/停用503暂停页/health豁免)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 20:30:30 +08:00
parent 1824bb1590
commit eaa5bf7e51
5 changed files with 338 additions and 50 deletions
+9 -10
View File
@@ -6,6 +6,7 @@ from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.engine.config import STATIC_DIR
from app.platform.host_middleware import TenantHostMiddleware
from app.platform.registry import TenantRegistry
logger = logging.getLogger(__name__)
@@ -14,9 +15,6 @@ logger = logging.getLogger(__name__)
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
@@ -29,15 +27,10 @@ async def lifespan(app: FastAPI):
app.state.registry = registry
try:
# 预热默认租户(过渡兜底读 app.state.default_runtime)。engine 建在
# 本 lifespan 的事件循环里——连接与 loop 绑定,别处另起 asyncio.run
# 复用同一 engine 会跨 loop 死锁(测试直连造数走 conftest.async_db)。
# 预热/加载保持 fail-fast:默认租户起不来 = 过渡期全站无租户。
app.state.default_runtime = await registry.resolve(DEFAULT_TENANT_HOST)
# 询盘推送(设计 8.2): 清扫崩溃遗留的 stale pending → failed。逐租户、
# 用该租户自己的 session_factory;db 文件不存在的租户跳过——全新库
# 无遗留行,保持 engine 懒建纪律(预热的默认租户文件已建,照常扫)。
# 无遗留行,保持 engine 懒建纪律(租户 engine 由 Host 中间件按请求
# 懒建,启动期只有已有库文件的租户会被扫描)。
# 清扫失败只记日志继续:一个租户的坏库不炸整个平台启动(其余租户
# 照常服务),该租户下轮重启还会重扫。
from app.engine.services.lead_push_service import sweep_stale_pending
@@ -80,6 +73,12 @@ async def static_no_cache(request, call_next):
return resp
# 租户路由中间件(Task 7): 最后 add = 最外层——租户解析先于一切用户中间件。
# 纯 ASGI 类(http+websocket 双覆盖),registry 经 scope["app"] 延迟取,
# 因此可在 lifespan 挂 Registry 之前于模块级注册。
app.add_middleware(TenantHostMiddleware)
@app.get("/")
async def index():
from fastapi.responses import FileResponse