scm-harmonic-cmts-admin/routers/leaf_options.py

149 lines
5.2 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.

# --- routers/leaf_options.py ---
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Body
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List
import json, os, asyncio, re
from cmts_scraper import sync_cmts_leaves_async
from shared import CMTS_DEVICE
router = APIRouter(tags=["Options"])
CACHE_FILE = "cmts_leaf_options_cache.json"
# ==========================================
# 🌟 全域廣播機制 (SSE)
# ==========================================
IS_SCANNING = False
active_clients = set()
async def broadcast_message(message_dict: dict):
"""將訊息推播給所有連線中的前端"""
dead_clients = set()
# 🌟 關鍵修復 1SSE 協定嚴格要求必須以 "data: " 開頭,並以 "\n\n" 結尾
message_str = f"data: {json.dumps(message_dict)}\n\n"
for client_queue in active_clients:
try:
await client_queue.put(message_str)
except Exception:
dead_clients.add(client_queue)
for dead in dead_clients:
active_clients.discard(dead)
@router.get("/cmts-leaf-options/stream")
async def sse_stream(request: Request):
"""前端一載入就會連上這個路由,持續監聽廣播"""
client_queue = asyncio.Queue()
active_clients.add(client_queue)
async def event_generator():
try:
while True:
try:
message = await asyncio.wait_for(client_queue.get(), timeout=2.0)
yield message
except asyncio.TimeoutError:
if await request.is_disconnected():
break
# 🌟 關鍵修復 2每 2 秒發送一個空註解,防止瀏覽器因閒置而自動斷線
yield ": keepalive\n\n"
finally:
active_clients.discard(client_queue)
return StreamingResponse(event_generator(), media_type="text/event-stream")
@router.get("/scan-status")
async def get_scan_status():
global IS_SCANNING
return {"is_scanning": IS_SCANNING}
# ==========================================
# 🌟 快取讀取與背景掃描任務
# ==========================================
class SyncOptionsRequest(BaseModel):
leaf_paths: List[str]
@router.get("/cmts-leaf-options")
async def get_leaf_options():
if not os.path.exists(CACHE_FILE): return {}
with open(CACHE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
async def run_scan_task(paths: List[str]):
"""在背景執行的爬蟲任務,並將進度廣播出去"""
global IS_SCANNING
try:
await broadcast_message({"event": "scan_start"})
# 呼叫爬蟲 (帶入設備連線資訊)
async for chunk in sync_cmts_leaves_async(
host=CMTS_DEVICE.get("host"),
username=CMTS_DEVICE.get("username"),
password=CMTS_DEVICE.get("password"),
leaf_paths=paths
):
if isinstance(chunk, str):
await broadcast_message(json.loads(chunk))
else:
await broadcast_message(chunk)
await broadcast_message({"event": "done"})
except Exception as e:
await broadcast_message({"event": "error", "message": str(e)})
finally:
IS_SCANNING = False
@router.post("/cmts-leaf-options/sync")
async def sync_leaf_options(request: SyncOptionsRequest, background_tasks: BackgroundTasks):
"""前端點擊掃描時,只負責觸發背景任務並立即回傳 JSON"""
global IS_SCANNING
if not request.leaf_paths:
raise HTTPException(status_code=400)
if IS_SCANNING:
return {"status": "busy", "message": "⚠️ 掃描任務正在執行中,請勿重複點擊!"}
IS_SCANNING = True
cmts_query_paths = []
for p in request.leaf_paths:
clean_p = p.replace("::", " ")
clean_p = re.sub(r"\s*\[\d+\]", "", clean_p)
cmts_query_paths.append(clean_p)
cmts_query_paths = list(dict.fromkeys(cmts_query_paths))
# 將任務丟入背景執行,並立刻回傳 JSON 給前端
background_tasks.add_task(run_scan_task, cmts_query_paths)
return {"status": "started"}
# ==========================================
# 🌟 清除快取 API (對應 api.js 的 POST 請求)
# ==========================================
@router.post("/clear_cache")
async def clear_specific_cache(paths_to_clear: list = Body(...)):
cleared_count = 0
if not os.path.exists(CACHE_FILE):
return {"status": "success", "cleared_count": 0, "message": "快取檔案不存在"}
try:
with open(CACHE_FILE, "r", encoding="utf-8") as f:
cache_data = json.load(f)
for path in paths_to_clear:
if path in cache_data:
del cache_data[path]
cleared_count += 1
if cleared_count > 0:
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(cache_data, f, ensure_ascii=False, indent=4)
return {"status": "success", "cleared_count": cleared_count}
except Exception as e:
return {"status": "error", "message": f"清除快取時發生錯誤: {str(e)}"}