# web/server.py import os import asyncio from pathlib import Path from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel from core.app_manager import AppManager from vs_log_macros import VSC_INFO, VSC_ERROR app = FastAPI(title="Visa Plugin Manager") # === 1. 路径计算 (关键修改) === # 获取 web/server.py 所在的绝对路径目录 CURRENT_DIR = Path(__file__).resolve().parent # 拼接出 static 目录: .../web/static STATIC_DIR = CURRENT_DIR / "static" # 拼接出 index.html 路径 INDEX_FILE = STATIC_DIR / "index.html" VSC_INFO("web", f"Static Directory configured at: {STATIC_DIR}") VSC_INFO("web", f"Index File expected at: {INDEX_FILE}") # 确保目录存在 if not STATIC_DIR.exists(): VSC_INFO("web", "Static directory missing, creating...") STATIC_DIR.mkdir(parents=True, exist_ok=True) # === 2. 挂载静态文件 === # 挂载 /static 路径,用于访问 CSS/JS 等资源 (虽然本例是CDN,但保留以备用) app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # === 3. 根路径路由 === @app.get("/") async def read_index(): # 显式检查文件是否存在,方便调试 if not INDEX_FILE.exists(): return { "error": "Index file not found", "expected_path": str(INDEX_FILE), "tip": "Please ensure 'index.html' exists in the 'web/static' folder." } return FileResponse(str(INDEX_FILE)) # === 数据模型 === class GroupControl(BaseModel): group_id: str class UpgradePluginRequest(BaseModel): plugin_name: str plugin_bin: str class UpdateConfigRequest(BaseModel): group_id: str new_config_str: str # === API 接口 === @app.get("/status") def get_status(): return {"data": AppManager.Instance().get_status()} @app.post("/start") def start_group(payload: GroupControl): if AppManager.Instance().start_group(payload.group_id): return {"message": f"Group {payload.group_id} started"} raise HTTPException(status_code=400, detail="Failed to start group") @app.post("/stop") def stop_group(payload: GroupControl): if AppManager.Instance().stop_group(payload.group_id): return {"message": f"Group {payload.group_id} stopped"} raise HTTPException(status_code=400, detail="Group not running or failed to stop") @app.post("/restart") def restart_group(payload: GroupControl): if AppManager.Instance().restart_group(payload.group_id): return {"message": f"Group {payload.group_id} restarted"} raise HTTPException(status_code=400, detail="Failed to restart") @app.post("/group_config") def get_group_config(payload: GroupControl): config = AppManager.Instance().get_group_config(payload.group_id) if config: return {"message": f"Group {payload.group_id} restarted", "data": config} raise HTTPException(status_code=400, detail="Failed to get group config") @app.post("/ota/upgrade_plugin") def ota_update(payload: UpgradePluginRequest): try: restarted = AppManager.Instance().ota_upgrade_plugin(payload.plugin_name) return { "message": f"Plugin {payload.plugin_name} reloaded", "restarted_groups": restarted } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/ota/update_config") def ota_update(payload: UpdateConfigRequest): try: AppManager.Instance().ota_update_plugin_config(payload.group_id, payload.new_config_str) return { "message": f"Plugin {payload.group_id} config updated" } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.websocket("/ws/logs/{group_id}") async def websocket_logs(ws: WebSocket, group_id: str): await ws.accept() queue = asyncio.Queue() loop = asyncio.get_running_loop() # ⭐ 只在这里拿 def log_callback(msg: str): loop.call_soon_threadsafe(queue.put_nowait, msg) AppManager.Instance().subscribe_executor_logs(group_id, log_callback) try: while True: msg = await queue.get() await ws.send_text(msg) finally: AppManager.Instance().unsubscribe_executor_logs(group_id, log_callback) def run_web_server(host="0.0.0.0", port=8000): import uvicorn # log_level warning 减少控制台刷屏 uvicorn.run(app, host=host, port=port, log_level="info")