server.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # web/server.py
  2. import os
  3. import asyncio
  4. from pathlib import Path
  5. from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
  6. from fastapi.staticfiles import StaticFiles
  7. from fastapi.responses import FileResponse
  8. from pydantic import BaseModel
  9. from core.app_manager import AppManager
  10. from vs_log_macros import VSC_INFO, VSC_ERROR
  11. app = FastAPI(title="Visa Plugin Manager")
  12. # === 1. 路径计算 (关键修改) ===
  13. # 获取 web/server.py 所在的绝对路径目录
  14. CURRENT_DIR = Path(__file__).resolve().parent
  15. # 拼接出 static 目录: .../web/static
  16. STATIC_DIR = CURRENT_DIR / "static"
  17. # 拼接出 index.html 路径
  18. INDEX_FILE = STATIC_DIR / "index.html"
  19. VSC_INFO("web", f"Static Directory configured at: {STATIC_DIR}")
  20. VSC_INFO("web", f"Index File expected at: {INDEX_FILE}")
  21. # 确保目录存在
  22. if not STATIC_DIR.exists():
  23. VSC_INFO("web", "Static directory missing, creating...")
  24. STATIC_DIR.mkdir(parents=True, exist_ok=True)
  25. # === 2. 挂载静态文件 ===
  26. # 挂载 /static 路径,用于访问 CSS/JS 等资源 (虽然本例是CDN,但保留以备用)
  27. app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
  28. # === 3. 根路径路由 ===
  29. @app.get("/")
  30. async def read_index():
  31. # 显式检查文件是否存在,方便调试
  32. if not INDEX_FILE.exists():
  33. return {
  34. "error": "Index file not found",
  35. "expected_path": str(INDEX_FILE),
  36. "tip": "Please ensure 'index.html' exists in the 'web/static' folder."
  37. }
  38. return FileResponse(str(INDEX_FILE))
  39. # === 数据模型 ===
  40. class GroupControl(BaseModel):
  41. group_id: str
  42. class UpgradePluginRequest(BaseModel):
  43. plugin_name: str
  44. plugin_bin: str
  45. class UpdateConfigRequest(BaseModel):
  46. group_id: str
  47. new_config_str: str
  48. # === API 接口 ===
  49. @app.get("/status")
  50. def get_status():
  51. return {"data": AppManager.Instance().get_status()}
  52. @app.post("/start")
  53. def start_group(payload: GroupControl):
  54. if AppManager.Instance().start_group(payload.group_id):
  55. return {"message": f"Group {payload.group_id} started"}
  56. raise HTTPException(status_code=400, detail="Failed to start group")
  57. @app.post("/stop")
  58. def stop_group(payload: GroupControl):
  59. if AppManager.Instance().stop_group(payload.group_id):
  60. return {"message": f"Group {payload.group_id} stopped"}
  61. raise HTTPException(status_code=400, detail="Group not running or failed to stop")
  62. @app.post("/restart")
  63. def restart_group(payload: GroupControl):
  64. if AppManager.Instance().restart_group(payload.group_id):
  65. return {"message": f"Group {payload.group_id} restarted"}
  66. raise HTTPException(status_code=400, detail="Failed to restart")
  67. @app.post("/group_config")
  68. def get_group_config(payload: GroupControl):
  69. config = AppManager.Instance().get_group_config(payload.group_id)
  70. if config:
  71. return {"message": f"Group {payload.group_id} restarted", "data": config}
  72. raise HTTPException(status_code=400, detail="Failed to get group config")
  73. @app.post("/ota/upgrade_plugin")
  74. def ota_update(payload: UpgradePluginRequest):
  75. try:
  76. restarted = AppManager.Instance().ota_upgrade_plugin(payload.plugin_name)
  77. return {
  78. "message": f"Plugin {payload.plugin_name} reloaded",
  79. "restarted_groups": restarted
  80. }
  81. except Exception as e:
  82. raise HTTPException(status_code=500, detail=str(e))
  83. @app.post("/ota/update_config")
  84. def ota_update(payload: UpdateConfigRequest):
  85. try:
  86. AppManager.Instance().ota_update_plugin_config(payload.group_id, payload.new_config_str)
  87. return {
  88. "message": f"Plugin {payload.group_id} config updated"
  89. }
  90. except Exception as e:
  91. raise HTTPException(status_code=500, detail=str(e))
  92. @app.websocket("/ws/logs/{group_id}")
  93. async def websocket_logs(ws: WebSocket, group_id: str):
  94. await ws.accept()
  95. queue = asyncio.Queue()
  96. loop = asyncio.get_running_loop() # ⭐ 只在这里拿
  97. def log_callback(msg: str):
  98. loop.call_soon_threadsafe(queue.put_nowait, msg)
  99. AppManager.Instance().subscribe_executor_logs(group_id, log_callback)
  100. try:
  101. while True:
  102. msg = await queue.get()
  103. await ws.send_text(msg)
  104. finally:
  105. AppManager.Instance().unsubscribe_executor_logs(group_id, log_callback)
  106. def run_web_server(host="0.0.0.0", port=8000):
  107. import uvicorn
  108. # log_level warning 减少控制台刷屏
  109. uvicorn.run(app, host=host, port=port, log_level="info")