mirror of https://github.com/Mai-with-u/MaiBot.git
添加独立的 WebUI 服务器支持,重构相关配置和启动逻辑
parent
5d9bf243d2
commit
4284e0f860
9
bot.py
9
bot.py
|
|
@ -75,6 +75,15 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
|
||||||
try:
|
try:
|
||||||
logger.info("正在优雅关闭麦麦...")
|
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.core.events_manager import events_manager
|
||||||
from src.plugin_system.base.component_types import EventType
|
from src.plugin_system.base.component_types import EventType
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from fastapi import FastAPI, APIRouter
|
from fastapi import FastAPI, APIRouter
|
||||||
from fastapi.middleware.cors import CORSMiddleware # 新增导入
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uvicorn import Config, Server as UvicornServer
|
from uvicorn import Config, Server as UvicornServer
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -17,21 +16,6 @@ class Server:
|
||||||
self._server: Optional[UvicornServer] = None
|
self._server: Optional[UvicornServer] = None
|
||||||
self.set_address(host, port)
|
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 = ""):
|
def register_router(self, router: APIRouter, prefix: str = ""):
|
||||||
"""注册路由
|
"""注册路由
|
||||||
|
|
||||||
|
|
|
||||||
43
src/main.py
43
src/main.py
|
|
@ -36,25 +36,13 @@ class MainSystem:
|
||||||
# 使用消息API替代直接的FastAPI实例
|
# 使用消息API替代直接的FastAPI实例
|
||||||
self.app: MessageServer = get_global_api()
|
self.app: MessageServer = get_global_api()
|
||||||
self.server: Server = get_global_server()
|
self.server: Server = get_global_server()
|
||||||
|
self.webui_server = None # 独立的 WebUI 服务器
|
||||||
|
|
||||||
# 注册 WebUI API 路由
|
# 设置独立的 WebUI 服务器
|
||||||
self._register_webui_routes()
|
self._setup_webui_server()
|
||||||
|
|
||||||
# 设置 WebUI(开发/生产模式)
|
def _setup_webui_server(self):
|
||||||
self._setup_webui()
|
"""设置独立的 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(根据环境变量决定模式)"""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true"
|
webui_enabled = os.getenv("WEBUI_ENABLED", "false").lower() == "true"
|
||||||
|
|
@ -65,11 +53,22 @@ class MainSystem:
|
||||||
webui_mode = os.getenv("WEBUI_MODE", "production").lower()
|
webui_mode = os.getenv("WEBUI_MODE", "production").lower()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from src.webui.manager import setup_webui
|
from src.webui.webui_server import get_webui_server
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
setup_webui(mode=webui_mode)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"设置 WebUI 失败: {e}")
|
logger.error(f"❌ 初始化 WebUI 服务器失败: {e}")
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""初始化系统组件"""
|
"""初始化系统组件"""
|
||||||
|
|
@ -164,6 +163,10 @@ class MainSystem:
|
||||||
self.server.run(),
|
self.server.run(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 如果 WebUI 服务器已初始化,添加到任务列表
|
||||||
|
if self.webui_server:
|
||||||
|
tasks.append(self.webui_server.start())
|
||||||
|
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
# async def forget_memory_task(self):
|
# async def forget_memory_task(self):
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
# 麦麦主程序配置
|
||||||
HOST=127.0.0.1
|
HOST=127.0.0.1
|
||||||
PORT=8000
|
PORT=8000
|
||||||
|
|
||||||
# WebUI 配置
|
# WebUI 独立服务器配置
|
||||||
WEBUI_ENABLED=true
|
WEBUI_ENABLED=true
|
||||||
WEBUI_MODE=production # 生产模式
|
WEBUI_MODE=production # 模式: development(开发) 或 production(生产)
|
||||||
|
WEBUI_HOST=0.0.0.0 # WebUI 服务器监听地址
|
||||||
|
WEBUI_PORT=8001 # WebUI 服务器端口
|
||||||
Loading…
Reference in New Issue