2026-08-24 16:57:27 +08:00
|
|
|
|
# -*- 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
|
2026-08-24 20:38:32 +08:00
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2026-08-24 16:57:27 +08:00
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
|
|
|
|
|
from .config import settings
|
|
|
|
|
|
from .event_bus import bus
|
|
|
|
|
|
from .mqtt import hub
|
|
|
|
|
|
from .routers import router
|
2026-08-24 18:06:51 +08:00
|
|
|
|
from .sim_engine import get_engine, sim_engine
|
|
|
|
|
|
from . import tenants
|
2026-08-24 16:57:27 +08:00
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("dpm.park")
|
|
|
|
|
|
|
|
|
|
|
|
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
2026-08-24 20:01:03 +08:00
|
|
|
|
# 园区表/种子由 alembic + scripts/db/seed.py 非运行态完成;此处仅启动运行时组件
|
2026-08-24 16:57:27 +08:00
|
|
|
|
bus.set_loop(asyncio.get_running_loop())
|
|
|
|
|
|
hub.start()
|
|
|
|
|
|
|
|
|
|
|
|
stop = threading.Event()
|
|
|
|
|
|
|
2026-08-24 17:29:37 +08:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2026-08-24 16:57:27 +08:00
|
|
|
|
def tick_loop():
|
|
|
|
|
|
while not stop.is_set():
|
|
|
|
|
|
try:
|
2026-08-24 20:01:03 +08:00
|
|
|
|
# 多租户:逐园区 tick + 按租户发布(同步读租户列表)
|
|
|
|
|
|
for t in tenants.list_tenants_sync():
|
2026-08-24 18:06:51 +08:00
|
|
|
|
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)
|
2026-08-24 16:57:27 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-08-24 20:38:32 +08:00
|
|
|
|
# 跨域:大屏(Vite dev :1420 / Tauri tauri.localhost)与 server-core(:8090) 不同源,需放行
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-24 16:57:27 +08:00
|
|
|
|
# 关键:内部路由本为 /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")
|