# --- routers/leaf_options.py --- from fastapi import APIRouter, BackgroundTasks, HTTPException, Body from pydantic import BaseModel from typing import List import json, os, asyncio, time, re # 🌟 引入新的 asyncssh 版本爬蟲 from cmts_scraper import sync_cmts_leaves_async from shared import CMTS_DEVICE router = APIRouter(prefix="/api/v1", tags=["Options"]) CACHE_FILE = "cmts_leaf_options_cache.json" scraper_semaphore = asyncio.Semaphore(1) class SyncOptionsRequest(BaseModel): leaf_paths: List[str] async def run_scraper_task_async(leaf_paths: List[str]): if scraper_semaphore.locked(): print("⚠️ [Debug] 爬蟲正在執行中,本次請求被忽略") return async with scraper_semaphore: try: print(f"🚀 [Debug] 背景爬蟲開始啟動,準備抓取: {leaf_paths}") # 🌟 雙重防護:確保傳給 CMTS 的是空白分隔,並在後端也徹底拔除 [0], [1] 等虛擬索引 cmts_query_paths = [] for p in leaf_paths: clean_p = p.replace("::", " ") clean_p = re.sub(r"\s*\[\d+\]", "", clean_p) # 拔除虛擬索引 cmts_query_paths.append(clean_p) # 去除重複的路徑 (例如 alias [0] 和 alias [1] 淨化後都會變成 alias) cmts_query_paths = list(dict.fromkeys(cmts_query_paths)) print(f"⏳ [Debug] 正在連線 CMTS 執行 sync_cmts_leaves_async...") # 🌟 核心修改:直接 await 非同步爬蟲,不再需要 Netmiko 與 to_thread # 從共用的 CMTS_DEVICE 字典中提取連線資訊 result = await sync_cmts_leaves_async( host=CMTS_DEVICE.get("host"), username=CMTS_DEVICE.get("username"), password=CMTS_DEVICE.get("password"), leaf_paths=cmts_query_paths ) print(f"✅ [Debug] CMTS 抓取完成,結果: {result['status']}") if result["status"] == "success": cache_data = {} if os.path.exists(CACHE_FILE): with open(CACHE_FILE, "r", encoding="utf-8") as f: cache_data = json.load(f) current_timestamp = int(time.time()) # 存入快取 for cmts_path, data in result["data"].items(): data["updated_at"] = current_timestamp cache_data[cmts_path] = data with open(CACHE_FILE, "w", encoding="utf-8") as f: json.dump(cache_data, f, indent=4, ensure_ascii=False) print("💾 [Debug] 快取檔案寫入成功!") else: error_msg = result.get('message', '沒有提供錯誤訊息') print(f"❌ [Debug] 抓取失敗,CMTS 拒絕了我們!原因: {error_msg}") except Exception as e: print(f"💥 [Debug] 背景爬蟲發生嚴重錯誤: {e}") @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) @router.post("/cmts-leaf-options/sync") async def sync_leaf_options(request: SyncOptionsRequest, background_tasks: BackgroundTasks): if not request.leaf_paths: raise HTTPException(status_code=400) background_tasks.add_task(run_scraper_task_async, request.leaf_paths) return {"status": "processing"} # 🌟 新增:清除特定選項快取的 API (使用您原本定義的 CACHE_FILE) @router.post("/clear_cache") async def clear_specific_cache(paths_to_clear: list = Body(...)): cleared_count = 0 # 1. 檢查快取檔案是否存在 if not os.path.exists(CACHE_FILE): return {"status": "success", "cleared_count": 0, "message": "快取檔案不存在"} try: # 2. 讀取現有的 JSON 快取資料 with open(CACHE_FILE, "r", encoding="utf-8") as f: cache_data = json.load(f) # 3. 逐一比對並刪除指定的路徑 for path in paths_to_clear: if path in cache_data: del cache_data[path] cleared_count += 1 # 4. 如果有刪除資料,將更新後的內容寫回 JSON 檔案 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)}"}