Files
DPM/backend/app/main.py
T
Pine 107727ea5e feat: Refactor DpmOverlays to use ShowCard for rendering cards and add ToolStatusToast for operation status notifications
- Moved card rendering logic from DpmOverlays to a new ShowCard component for better reusability.
- Introduced ToolStatusToast to display real-time operation statuses in the top right corner.
- Updated PageHeader to conditionally render credits based on the current path.
- Modified PromptPanel to change tool names and update prompt titles.
- Enhanced ScreenLayout to include ToolStatusToast.
- Updated styles for new components and adjusted existing styles for consistency.
- Implemented statusBus utility for dispatching tool status events.
- Updated useMqttControl to integrate tool status notifications during navigation and card display actions.
2026-08-19 12:28:35 +08:00

140 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""OPC 智能园区后端 —— FastAPI 入口
启动:uvicorn app.main:app --host 0.0.0.0 --port 8000
或:python main.py
"""
import asyncio
import logging
import threading
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from .admin import admin_login, admin_logout, admin_page
from .config import settings
from .event_bus import bus
from .mqtt import hub
from .routers import router
from .sim_engine import sim_engine
from .s2s_bridge import start_s2s_backend
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
log = logging.getLogger("dpm.main")
DIST = Path(settings.DIST_DIR)
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动准备
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
bus.set_loop(asyncio.get_running_loop())
hub.start()
stop = threading.Event()
# s2s 实时语音栈(可选):后台线程构建管线池并起 RealtimeServer(:8765)
if settings.S2S_ENABLED:
start_s2s_backend(stop)
def tick_loop():
"""数据引擎每 2.2s 推进一次;MQTT 连接时同步推送快照"""
while not stop.is_set():
try:
snap = sim_engine.tick()
hub.publish_tick(snap)
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("OPC 智能园区后端启动于 http://%s:%s", settings.HOST, settings.PORT)
if settings.MQTT_ENABLED:
log.info("MQTT Broker: %s:%s(前端 WS: %s", settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_WS_URL)
try:
yield
finally:
stop.set()
hub.stop()
log.info("OPC 智能园区后端已停止")
app = FastAPI(title="OPC 智能园区后端", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def no_cache_admin_static(request: Request, call_next):
"""管理后台页面与静态资源禁用缓存,避免旧 JS/CSS 残留导致按钮无响应"""
response = await call_next(request)
if request.url.path.startswith(("/static/", "/admin")):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
app.include_router(router)
# 媒体资源静态服务(上传/播放的文件)
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="media")
# 管理后台静态资源(admin.css / admin.js
_STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# 管理后台(FastAPI + Jinja2 服务端渲染)—— 必须在 SPA 回退之前注册
app.add_api_route("/admin", admin_page, methods=["GET"], include_in_schema=False)
app.add_api_route("/admin/login", admin_login, methods=["POST"], include_in_schema=False)
app.add_api_route("/admin/logout", admin_logout, methods=["GET"], include_in_schema=False)
# ---------- 客户端安装包下载(必须在 SPA 回退之前注册) ----------
_DOWNLOAD_ROOT = Path(__file__).resolve().parent.parent # backend/
@app.get("/download", include_in_schema=False)
@app.get("/11", include_in_schema=False)
async def download_installer():
"""下载 Windows 客户端安装包(backend/ 下 *.setup.exe,如 云超服..._x64-setup.exe)。"""
for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe")):
if f.is_file():
return FileResponse(
path=str(f),
filename=f.name,
media_type="application/octet-stream",
)
return JSONResponse({"detail": "安装包不存在"}, status_code=404)
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
@app.get("/{path:path}", include_in_schema=False)
async def spa_fallback(path: str):
if not DIST.exists():
return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404)
dist = DIST.resolve()
if not path:
target = dist / "index.html"
else:
target = (dist / path).resolve()
if target.is_file() and target.is_relative_to(dist):
return FileResponse(target)
index = dist / "index.html"
if index.exists():
return FileResponse(index)
return JSONResponse({"detail": "Not Found"}, status_code=404)