Files
ros-dpi/backend/app/main.py
T
Su YijunandClaude Opus 5.5 f3e9fdb2af feat: ROS-DPI v0.1.0:RouterOS 终端流量与应用统计面板
- IPFIX / NetFlow v9 采集 + TZSP DNS 嗅探识别应用,可选 ROS 只读 REST 轮询
- 内置 100+ 应用规则库,支持自定义域名 / IP 段 / 端口规则
- 按分钟 / 小时 / 天汇总写入 MySQL
- 全网概览、主机监控、应用统计、流量流向(桑基图)、识别规则、系统设置
- 中英双语,深浅色主题,T568B 线色设计语言
- Docker 多架构镜像,GitHub Actions 测试与发布

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:51:02 +08:00

90 lines
3.1 KiB
Python

import asyncio
import os
from contextlib import asynccontextmanager
from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from app import __version__
from app.collector import start_listeners
from app.config import get_settings
from app.database import SessionLocal, init_db
from app.engine import engine
from app.models import Setting
from app.ros_poller import poller
from app.routers import admin, auth, stats
from app.security import ensure_admin
FRONTEND_DIR = os.environ.get("FRONTEND_DIR", os.path.join(os.path.dirname(__file__), "..", "frontend_dist"))
def _safe(fn, name):
def run():
try:
fn()
except Exception as e: # noqa: BLE001
print(f"[job] {name} 失败:{type(e).__name__}: {e}")
return run
@asynccontextmanager
async def lifespan(app: FastAPI):
s = get_settings()
await asyncio.to_thread(init_db)
ensure_admin()
engine.load_state()
with SessionLocal() as db:
wan = db.get(Setting, "wan_interface")
poller.wan_override = wan.value if wan and wan.value else ""
transports = await start_listeners(engine, s.ipfix_port, s.tzsp_port)
sched = BackgroundScheduler(timezone=s.tz)
sched.add_job(_safe(engine.flush, "flush"), "cron", second=0, max_instances=1, coalesce=True)
sched.add_job(_safe(engine.cleanup, "cleanup"), "cron", minute=17, max_instances=1)
if poller.enabled:
sched.add_job(_safe(poller.poll, "ros-poll"), "interval", seconds=s.ros_poll_seconds,
max_instances=1, coalesce=True)
await asyncio.to_thread(poller.poll)
sched.start()
print(f"[startup] ROS-DPI {__version__} 已启动,Web 端口 {s.http_port}")
yield
sched.shutdown(wait=False)
for t in transports:
t.close()
_safe(engine.flush, "flush")()
app = FastAPI(title="ROS-DPI", version=__version__, lifespan=lifespan)
@app.exception_handler(Exception)
async def _unhandled(request: Request, exc: Exception):
print(f"[error] {request.method} {request.url.path}: {exc!r}")
msg = (str(exc).splitlines() or [""])[0][:300]
return JSONResponse(status_code=500, content={"detail": f"服务器内部错误:{type(exc).__name__}: {msg}"})
for r in (auth.router, stats.router, admin.router):
app.include_router(r)
@app.get("/api/health")
def health():
return {"ok": True, "version": __version__}
if os.path.isdir(FRONTEND_DIR):
app.mount("/assets", StaticFiles(directory=os.path.join(FRONTEND_DIR, "assets")), name="assets")
@app.get("/{path:path}", include_in_schema=False)
def spa(path: str):
root = os.path.realpath(FRONTEND_DIR)
f = os.path.realpath(os.path.join(root, path))
if path and f.startswith(root + os.sep) and os.path.isfile(f):
return FileResponse(f)
if path.startswith("api/"):
return JSONResponse(status_code=404, content={"detail": "Not Found"})
return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))