634ed22dcd
大屏(Vite dev :1420 / Tauri tauri.localhost)与 server-core(:8090) 不同源;app.park 独立 FastAPI 未加 CORS → OPTIONS 预检 405、ACAO 拒绝。加 CORSMiddleware(allow_origins=['*']), TestClient 验证 /park/api/config 与预检均 200 且带 ACAO。
94 lines
3.3 KiB
Python
94 lines
3.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""园区子应用(app/park)—— FastAPI 子应用组装。
|
||
|
||
由 server-core/dispatcher.py 以 `/park` 前缀路由对外;独立 lifespan:
|
||
启动 MQTT hub、数据 tick 线程(sim_engine → publish_tick);s2s 实时语音栈延后。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import logging
|
||
import threading
|
||
from contextlib import asynccontextmanager
|
||
from pathlib import Path
|
||
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from .config import settings
|
||
from .event_bus import bus
|
||
from .mqtt import hub
|
||
from .routers import router
|
||
from .sim_engine import get_engine, sim_engine
|
||
from . import tenants
|
||
|
||
log = logging.getLogger("dpm.park")
|
||
|
||
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# 园区表/种子由 alembic + scripts/db/seed.py 非运行态完成;此处仅启动运行时组件
|
||
bus.set_loop(asyncio.get_running_loop())
|
||
hub.start()
|
||
|
||
stop = threading.Event()
|
||
|
||
# s2s 实时语音栈(可选):后台线程起 RealtimeServer(:8765),仅 S2S_ENABLED=1
|
||
# 依赖 speech_to_speech(vendor/s2s-cloud,需先 `uv pip install -e app/park/vendor/s2s-cloud`)
|
||
if settings.S2S_ENABLED:
|
||
try:
|
||
from .s2s_bridge import start_s2s_backend
|
||
start_s2s_backend(stop)
|
||
except Exception as e: # noqa: BLE001
|
||
log.warning("s2s 语音栈启动失败(S2S_ENABLED=1 但依赖未装?): %s", e)
|
||
|
||
def tick_loop():
|
||
while not stop.is_set():
|
||
try:
|
||
# 多租户:逐园区 tick + 按租户发布(同步读租户列表)
|
||
for t in tenants.list_tenants_sync():
|
||
try:
|
||
snap = get_engine(t["id"]).tick()
|
||
hub.publish_tick(snap, t["id"])
|
||
except Exception as e: # noqa: BLE001
|
||
log.warning("园区 %s tick 异常: %s", t.get("id"), e)
|
||
except Exception as e: # noqa: BLE001
|
||
log.warning("园区数据 tick 异常: %s", e)
|
||
stop.wait(settings.MQTT_TICK_INTERVAL)
|
||
|
||
threading.Thread(target=tick_loop, daemon=True).start()
|
||
log.info("园区子应用 /park 启动(MQTT %s:%s 系 %s)",
|
||
settings.MQTT_HOST, settings.MQTT_PORT,
|
||
"已启用" if settings.MQTT_ENABLED else "已禁用")
|
||
try:
|
||
yield
|
||
finally:
|
||
stop.set()
|
||
hub.stop()
|
||
|
||
|
||
app = FastAPI(title="OPC 智能园区子应用", version="1.0.0", lifespan=lifespan)
|
||
|
||
# 跨域:大屏(Vite dev :1420 / Tauri tauri.localhost)与 server-core(:8090) 不同源,需放行
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
allow_credentials=True,
|
||
)
|
||
|
||
# 关键:内部路由本为 /api/*,挂载到 /park 下 → 外部地址 /park/api/*
|
||
app.include_router(router, prefix="/park")
|
||
|
||
# 媒体静态服务(上传/播放文件)
|
||
app.mount("/park/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="park-media")
|
||
|
||
# 管理后台静态资源(如有)
|
||
_static_dir = Path(__file__).resolve().parent / "static"
|
||
if _static_dir.exists():
|
||
app.mount("/park/static", StaticFiles(directory=str(_static_dir)), name="park-static")
|