From 4284e0f860526b31007c07fbed9cb70e1bd31124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Thu, 20 Nov 2025 19:01:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=8B=AC=E7=AB=8B=E7=9A=84?= =?UTF-8?q?=20WebUI=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=8C=E9=87=8D=E6=9E=84=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E5=90=AF=E5=8A=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 9 +++ src/common/server.py | 16 ----- src/main.py | 43 +++++------ src/webui/manager.py | 109 ---------------------------- src/webui/webui_server.py | 148 ++++++++++++++++++++++++++++++++++++++ template/template.env | 7 +- 6 files changed, 185 insertions(+), 147 deletions(-) delete mode 100644 src/webui/manager.py create mode 100644 src/webui/webui_server.py diff --git a/bot.py b/bot.py index 7ba9af4b..68c6e110 100644 --- a/bot.py +++ b/bot.py @@ -75,6 +75,15 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression try: logger.info("正在优雅关闭麦麦...") + # 关闭 WebUI 服务器 + try: + from src.webui.webui_server import get_webui_server + webui_server = get_webui_server() + if webui_server and webui_server._server: + await webui_server.shutdown() + except Exception as e: + logger.warning(f"关闭 WebUI 服务器时出错: {e}") + from src.plugin_system.core.events_manager import events_manager from src.plugin_system.base.component_types import EventType diff --git a/src/common/server.py b/src/common/server.py index ebdc9fa2..88608677 100644 --- a/src/common/server.py +++ b/src/common/server.py @@ -1,5 +1,4 @@ from fastapi import FastAPI, APIRouter -from fastapi.middleware.cors import CORSMiddleware # 新增导入 from typing import Optional from uvicorn import Config, Server as UvicornServer import asyncio @@ -17,21 +16,6 @@ class Server: self._server: Optional[UvicornServer] = None self.set_address(host, port) - # 配置 CORS - origins = [ - "http://localhost:7999", # 允许的前端源 - "http://127.0.0.1:7999", - # 在生产环境中,您应该添加实际的前端域名 - ] - - self.app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, # 是否支持 cookie - allow_methods=["*"], # 允许所有 HTTP 方法 - allow_headers=["*"], # 允许所有 HTTP 请求头 - ) - def register_router(self, router: APIRouter, prefix: str = ""): """注册路由 diff --git a/src/main.py b/src/main.py index b442f29d..09ead248 100644 --- a/src/main.py +++ b/src/main.py @@ -36,25 +36,13 @@ class MainSystem: # 使用消息API替代直接的FastAPI实例 self.app: MessageServer = get_global_api() self.server: Server = get_global_server() + self.webui_server = None # 独立的 WebUI 服务器 - # 注册 WebUI API 路由 - self._register_webui_routes() + # 设置独立的 WebUI 服务器 + self._setup_webui_server() - # 设置 WebUI(开发/生产模式) - self._setup_webui() - - def _register_webui_routes(self): - """注册 WebUI API 路由""" - try: - from src.webui.routes import router as webui_router - - self.server.register_router(webui_router) - logger.info("WebUI API 路由已注册") - except Exception as e: - logger.warning(f"注册 WebUI API 路由失败: {e}") - - def _setup_webui(self): - """设置 WebUI(根据环境变量决定模式)""" + def _setup_webui_server(self): + """设置独立的 WebUI 服务器""" import os webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true" @@ -65,11 +53,22 @@ class MainSystem: webui_mode = os.getenv("WEBUI_MODE", "production").lower() try: - from src.webui.manager import setup_webui + from src.webui.webui_server import get_webui_server - setup_webui(mode=webui_mode) + self.webui_server = get_webui_server() + + if webui_mode == "development": + logger.info("📝 WebUI 开发模式已启用") + logger.info("🌐 后端 API 将运行在 http://0.0.0.0:8001") + logger.info("💡 请手动启动前端开发服务器: cd MaiBot-Dashboard && bun dev") + logger.info("💡 前端将运行在 http://localhost:7999") + else: + logger.info("✅ WebUI 生产模式已启用") + logger.info(f"🌐 WebUI 将运行在 http://0.0.0.0:8001") + logger.info("💡 请确保已构建前端: cd MaiBot-Dashboard && bun run build") + except Exception as e: - logger.error(f"设置 WebUI 失败: {e}") + logger.error(f"❌ 初始化 WebUI 服务器失败: {e}") async def initialize(self): """初始化系统组件""" @@ -164,6 +163,10 @@ class MainSystem: self.server.run(), ] + # 如果 WebUI 服务器已初始化,添加到任务列表 + if self.webui_server: + tasks.append(self.webui_server.start()) + await asyncio.gather(*tasks) # async def forget_memory_task(self): diff --git a/src/webui/manager.py b/src/webui/manager.py deleted file mode 100644 index 4dc472e2..00000000 --- a/src/webui/manager.py +++ /dev/null @@ -1,109 +0,0 @@ -"""WebUI 管理器 - 处理开发/生产环境的 WebUI 启动""" - -import os -from pathlib import Path -from src.common.logger import get_logger -from .token_manager import get_token_manager - -logger = get_logger("webui") - - -def setup_webui(mode: str = "production") -> bool: - """ - 设置 WebUI - - Args: - mode: 运行模式,"development" 或 "production" - - Returns: - bool: 是否成功设置 - """ - # 初始化 Token 管理器(确保 token 文件存在) - token_manager = get_token_manager() - current_token = token_manager.get_token() - logger.info(f"🔑 WebUI Access Token: {current_token}") - logger.info("💡 请使用此 Token 登录 WebUI") - - if mode == "development": - return setup_dev_mode() - else: - return setup_production_mode() - - -def setup_dev_mode() -> bool: - """设置开发模式 - 仅启用 CORS,前端自行启动""" - from src.common.server import get_global_server - from .logs_ws import router as logs_router - - # 注册 WebSocket 日志路由(开发模式也需要) - server = get_global_server() - server.register_router(logs_router) - logger.info("✅ WebSocket 日志推送路由已注册") - - logger.info("📝 WebUI 开发模式已启用") - logger.info("🌐 请手动启动前端开发服务器: cd webui && npm run dev") - logger.info("💡 前端将运行在 http://localhost:7999") - return True - - -def setup_production_mode() -> bool: - """设置生产模式 - 挂载静态文件""" - try: - from src.common.server import get_global_server - from starlette.responses import FileResponse - from .logs_ws import router as logs_router - import mimetypes - - # 确保正确的 MIME 类型映射 - mimetypes.init() - mimetypes.add_type("application/javascript", ".js") - mimetypes.add_type("application/javascript", ".mjs") - mimetypes.add_type("text/css", ".css") - mimetypes.add_type("application/json", ".json") - - server = get_global_server() - - # 注册 WebSocket 日志路由 - server.register_router(logs_router) - logger.info("✅ WebSocket 日志推送路由已注册") - - base_dir = Path(__file__).parent.parent.parent - static_path = base_dir / "webui" / "dist" - - if not static_path.exists(): - logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}") - logger.warning("💡 请先构建前端: cd webui && npm run build") - return False - - if not (static_path / "index.html").exists(): - logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}") - logger.warning("💡 请确认前端已正确构建") - return False - - # 处理 SPA 路由 - @server.app.get("/{full_path:path}") - async def serve_spa(full_path: str): - """服务单页应用""" - # API 路由不处理 - if full_path.startswith("api/"): - return None - - # 检查文件是否存在 - file_path = static_path / full_path - if file_path.is_file(): - # 自动检测 MIME 类型 - media_type = mimetypes.guess_type(str(file_path))[0] - return FileResponse(file_path, media_type=media_type) - - # 返回 index.html(SPA 路由) - return FileResponse(static_path / "index.html", media_type="text/html") - - host = os.getenv("HOST", "127.0.0.1") - port = os.getenv("PORT", "8000") - logger.info("✅ WebUI 生产模式已挂载") - logger.info(f"🌐 访问 http://{host}:{port} 查看 WebUI") - return True - - except Exception as e: - logger.error(f"挂载 WebUI 静态文件失败: {e}") - return False diff --git a/src/webui/webui_server.py b/src/webui/webui_server.py new file mode 100644 index 00000000..61d279e2 --- /dev/null +++ b/src/webui/webui_server.py @@ -0,0 +1,148 @@ +"""独立的 WebUI 服务器 - 运行在 0.0.0.0:8001""" + +import os +import asyncio +import mimetypes +from pathlib import Path +from fastapi import FastAPI +from fastapi.responses import FileResponse +from uvicorn import Config, Server as UvicornServer +from src.common.logger import get_logger + +logger = get_logger("webui_server") + + +class WebUIServer: + """独立的 WebUI 服务器""" + + def __init__(self, host: str = "0.0.0.0", port: int = 8001): + self.host = host + self.port = port + self.app = FastAPI(title="MaiBot WebUI") + self._server = None + + # 显示 Access Token + self._show_access_token() + + # 重要:先注册 API 路由,再设置静态文件 + self._register_api_routes() + self._setup_static_files() + + def _show_access_token(self): + """显示 WebUI Access Token""" + try: + from src.webui.token_manager import get_token_manager + + token_manager = get_token_manager() + current_token = token_manager.get_token() + logger.info(f"🔑 WebUI Access Token: {current_token}") + logger.info("💡 请使用此 Token 登录 WebUI") + except Exception as e: + logger.error(f"❌ 获取 Access Token 失败: {e}") + + def _setup_static_files(self): + """设置静态文件服务""" + # 确保正确的 MIME 类型映射 + mimetypes.init() + mimetypes.add_type("application/javascript", ".js") + mimetypes.add_type("application/javascript", ".mjs") + mimetypes.add_type("text/css", ".css") + mimetypes.add_type("application/json", ".json") + + base_dir = Path(__file__).parent.parent.parent + static_path = base_dir / "webui" / "dist" + + if not static_path.exists(): + logger.warning(f"❌ WebUI 静态文件目录不存在: {static_path}") + logger.warning("💡 请先构建前端: cd webui && npm run build") + return + + if not (static_path / "index.html").exists(): + logger.warning(f"❌ 未找到 index.html: {static_path / 'index.html'}") + logger.warning("💡 请确认前端已正确构建") + return + + # 处理 SPA 路由 - 注意:这个路由优先级最低 + @self.app.get("/{full_path:path}", include_in_schema=False) + async def serve_spa(full_path: str): + """服务单页应用 - 只处理非 API 请求""" + # 如果是根路径,直接返回 index.html + if not full_path or full_path == "/": + return FileResponse(static_path / "index.html", media_type="text/html") + + # 检查是否是静态文件 + file_path = static_path / full_path + if file_path.is_file() and file_path.exists(): + # 自动检测 MIME 类型 + media_type = mimetypes.guess_type(str(file_path))[0] + return FileResponse(file_path, media_type=media_type) + + # 其他路径返回 index.html(SPA 路由) + return FileResponse(static_path / "index.html", media_type="text/html") + + logger.info(f"✅ WebUI 静态文件服务已配置: {static_path}") + + def _register_api_routes(self): + """注册所有 WebUI API 路由""" + try: + # 导入所有 WebUI 路由 + from src.webui.routes import router as webui_router + from src.webui.logs_ws import router as logs_router + + # 注册路由 + self.app.include_router(webui_router) + self.app.include_router(logs_router) + + logger.info("✅ WebUI API 路由已注册") + except Exception as e: + logger.error(f"❌ 注册 WebUI API 路由失败: {e}") + + async def start(self): + """启动服务器""" + config = Config( + app=self.app, + host=self.host, + port=self.port, + log_config=None, + access_log=False, + ) + self._server = UvicornServer(config=config) + + logger.info("🌐 WebUI 服务器启动中...") + logger.info(f"🌐 访问地址: http://{self.host}:{self.port}") + + try: + await self._server.serve() + except Exception as e: + logger.error(f"❌ WebUI 服务器运行错误: {e}") + raise + + async def shutdown(self): + """关闭服务器""" + if self._server: + logger.info("正在关闭 WebUI 服务器...") + self._server.should_exit = True + try: + await asyncio.wait_for(self._server.shutdown(), timeout=3.0) + logger.info("✅ WebUI 服务器已关闭") + except asyncio.TimeoutError: + logger.warning("⚠️ WebUI 服务器关闭超时") + except Exception as e: + logger.error(f"❌ WebUI 服务器关闭失败: {e}") + finally: + self._server = None + + +# 全局 WebUI 服务器实例 +_webui_server = None + + +def get_webui_server() -> WebUIServer: + """获取全局 WebUI 服务器实例""" + global _webui_server + if _webui_server is None: + # 从环境变量读取配置 + host = os.getenv("WEBUI_HOST", "0.0.0.0") + port = int(os.getenv("WEBUI_PORT", "8001")) + _webui_server = WebUIServer(host=host, port=port) + return _webui_server diff --git a/template/template.env b/template/template.env index d19678e7..b6dd0e5c 100644 --- a/template/template.env +++ b/template/template.env @@ -1,6 +1,9 @@ +# 麦麦主程序配置 HOST=127.0.0.1 PORT=8000 -# WebUI 配置 +# WebUI 独立服务器配置 WEBUI_ENABLED=true -WEBUI_MODE=production # 生产模式 \ No newline at end of file +WEBUI_MODE=production # 模式: development(开发) 或 production(生产) +WEBUI_HOST=0.0.0.0 # WebUI 服务器监听地址 +WEBUI_PORT=8001 # WebUI 服务器端口 \ No newline at end of file