From 9914bac5a678f534af3db5b0ac56f8ae44bd8869 Mon Sep 17 00:00:00 2001 From: Alnnt <1138745158@qq.com> Date: Wed, 31 Dec 2025 23:00:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0WEBUI=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=85=81=E8=AE=B8=E5=AE=9E=E6=97=B6=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E7=BE=A4=E8=81=8A/=E7=A7=81=E8=81=8A=E7=9A=84?= =?UTF-8?q?=E9=BB=91=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 4 +- src/webui/__init__.py | 8 + src/webui/app.py | 45 +++++ src/webui/config_manager.py | 89 +++++++++ src/webui/routes.py | 102 ++++++++++ src/webui/static.py | 359 ++++++++++++++++++++++++++++++++++++ 6 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 src/webui/__init__.py create mode 100644 src/webui/app.py create mode 100644 src/webui/config_manager.py create mode 100644 src/webui/routes.py create mode 100644 src/webui/static.py diff --git a/main.py b/main.py index 6f824a6..b88d014 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,7 @@ from src.send_handler.nc_sending import nc_message_sender from src.config import global_config from src.mmc_com_layer import mmc_start_com, mmc_stop_com, router from src.response_pool import put_response, check_timeout_response +from src.webui import start_webui, stop_webui message_queue = asyncio.Queue() @@ -48,7 +49,7 @@ async def message_process(): async def main(): message_send_instance.maibot_router = router - _ = await asyncio.gather(napcat_server(), mmc_start_com(), message_process(), check_timeout_response()) + _ = await asyncio.gather(napcat_server(), mmc_start_com(), message_process(), check_timeout_response(), start_webui()) def check_napcat_server_token(conn, request): token = global_config.napcat_server.token @@ -80,6 +81,7 @@ async def graceful_shutdown(): if not task.done(): task.cancel() await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), 15) + await stop_webui() # 停止 WebUI await mmc_stop_com() # 后置避免神秘exception logger.info("Adapter已成功关闭") except Exception as e: diff --git a/src/webui/__init__.py b/src/webui/__init__.py new file mode 100644 index 0000000..3ebb8fc --- /dev/null +++ b/src/webui/__init__.py @@ -0,0 +1,8 @@ +""" +WebUI 包 - 用于实时修改配置 +""" + +from .app import start_webui, stop_webui, webui_router + +__all__ = ["start_webui", "stop_webui", "webui_router"] + diff --git a/src/webui/app.py b/src/webui/app.py new file mode 100644 index 0000000..16c3880 --- /dev/null +++ b/src/webui/app.py @@ -0,0 +1,45 @@ +""" +WebUI 应用 - 基于 aiohttp 的 Web 服务器 +""" + +import asyncio +from aiohttp import web +from typing import Optional +from src.logger import logger +from src.config import global_config +from .routes import setup_routes + +_runner: Optional[web.AppRunner] = None +_site: Optional[web.TCPSite] = None + +# WebUI 配置 +WEBUI_HOST = "0.0.0.0" +WEBUI_PORT = 8096 + + +async def start_webui(): + """启动 WebUI 服务""" + global _runner, _site + + app = web.Application() + setup_routes(app) + + _runner = web.AppRunner(app) + await _runner.setup() + _site = web.TCPSite(_runner, WEBUI_HOST, WEBUI_PORT) + await _site.start() + + logger.info(f"WebUI 已启动,访问地址: http://{WEBUI_HOST}:{WEBUI_PORT}") + + +async def stop_webui(): + """停止 WebUI 服务""" + global _runner + if _runner: + await _runner.cleanup() + logger.info("WebUI 已停止") + + +# 兼容 router 接口 +webui_router = None + diff --git a/src/webui/config_manager.py b/src/webui/config_manager.py new file mode 100644 index 0000000..9f44d9e --- /dev/null +++ b/src/webui/config_manager.py @@ -0,0 +1,89 @@ +""" +配置管理器 - 处理 ChatConfig 的读写 +""" + +import tomlkit +from typing import Literal, List, Dict, Any +from src.config import global_config +from src.logger import logger + +CONFIG_FILE_PATH = "config.toml" + + +def get_chat_config() -> Dict[str, Any]: + """获取当前 ChatConfig 配置""" + chat = global_config.chat + return { + "group_list_type": chat.group_list_type, + "group_list": list(chat.group_list), + "private_list_type": chat.private_list_type, + "private_list": list(chat.private_list), + } + + +def update_group_list_type(value: Literal["whitelist", "blacklist"]) -> bool: + """更新群聊列表类型""" + try: + global_config.chat.group_list_type = value + return True + except Exception as e: + logger.error(f"更新 group_list_type 失败: {e}") + return False + + +def update_group_list(value: List[int]) -> bool: + """更新群聊列表""" + try: + global_config.chat.group_list = value + return True + except Exception as e: + logger.error(f"更新 group_list 失败: {e}") + return False + + +def update_private_list_type(value: Literal["whitelist", "blacklist"]) -> bool: + """更新私聊列表类型""" + try: + global_config.chat.private_list_type = value + return True + except Exception as e: + logger.error(f"更新 private_list_type 失败: {e}") + return False + + +def update_private_list(value: List[int]) -> bool: + """更新私聊列表""" + try: + global_config.chat.private_list = value + return True + except Exception as e: + logger.error(f"更新 private_list 失败: {e}") + return False + + +def save_config_to_file() -> bool: + """将当前配置保存到文件""" + try: + # 读取现有配置文件以保留注释和格式 + with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: + config_doc = tomlkit.load(f) + + # 更新 chat 配置 + if "chat" not in config_doc: + config_doc["chat"] = tomlkit.table() + + config_doc["chat"]["group_list_type"] = global_config.chat.group_list_type + config_doc["chat"]["group_list"] = global_config.chat.group_list + config_doc["chat"]["private_list_type"] = global_config.chat.private_list_type + config_doc["chat"]["private_list"] = global_config.chat.private_list + + # 写回文件 + with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f: + f.write(tomlkit.dumps(config_doc)) + + logger.info("[WebUI] 配置已保存到文件") + return True + except Exception as e: + logger.error(f"[WebUI] 保存配置到文件失败: {e}") + return False + diff --git a/src/webui/routes.py b/src/webui/routes.py new file mode 100644 index 0000000..791593f --- /dev/null +++ b/src/webui/routes.py @@ -0,0 +1,102 @@ +""" +WebUI 路由定义 +""" + +import json +from aiohttp import web +from src.config import global_config +from src.logger import logger +from .config_manager import ( + get_chat_config, + update_group_list_type, + update_group_list, + update_private_list_type, + update_private_list, + save_config_to_file, +) +from .static import get_index_html + + +async def index_handler(request: web.Request) -> web.Response: + """返回主页面""" + return web.Response(text=get_index_html(), content_type="text/html") + + +async def get_config_handler(request: web.Request) -> web.Response: + """获取当前配置""" + config = get_chat_config() + return web.json_response(config) + + +async def update_config_handler(request: web.Request) -> web.Response: + """更新配置""" + try: + data = await request.json() + + field = data.get("field") + value = data.get("value") + + if field is None or value is None: + return web.json_response({"success": False, "error": "缺少 field 或 value 参数"}, status=400) + + success = False + message = "" + + if field == "group_list_type": + if value not in ["whitelist", "blacklist"]: + return web.json_response({"success": False, "error": "group_list_type 必须是 whitelist 或 blacklist"}, status=400) + success = update_group_list_type(value) + message = f"群聊列表类型已更新为: {value}" + + elif field == "group_list": + if not isinstance(value, list): + return web.json_response({"success": False, "error": "group_list 必须是数组"}, status=400) + # 确保所有元素都是整数 + try: + value = [int(v) for v in value] + except (ValueError, TypeError): + return web.json_response({"success": False, "error": "group_list 中的元素必须是数字"}, status=400) + success = update_group_list(value) + message = f"群聊列表已更新,共 {len(value)} 个群组" + + elif field == "private_list_type": + if value not in ["whitelist", "blacklist"]: + return web.json_response({"success": False, "error": "private_list_type 必须是 whitelist 或 blacklist"}, status=400) + success = update_private_list_type(value) + message = f"私聊列表类型已更新为: {value}" + + elif field == "private_list": + if not isinstance(value, list): + return web.json_response({"success": False, "error": "private_list 必须是数组"}, status=400) + # 确保所有元素都是整数 + try: + value = [int(v) for v in value] + except (ValueError, TypeError): + return web.json_response({"success": False, "error": "private_list 中的元素必须是数字"}, status=400) + success = update_private_list(value) + message = f"私聊列表已更新,共 {len(value)} 个用户" + + else: + return web.json_response({"success": False, "error": f"未知的字段: {field}"}, status=400) + + if success: + # 保存配置到文件 + save_config_to_file() + logger.info(f"[WebUI] {message}") + return web.json_response({"success": True, "message": message, "config": get_chat_config()}) + else: + return web.json_response({"success": False, "error": "更新配置失败"}, status=500) + + except json.JSONDecodeError: + return web.json_response({"success": False, "error": "无效的 JSON 数据"}, status=400) + except Exception as e: + logger.error(f"[WebUI] 更新配置时发生错误: {e}") + return web.json_response({"success": False, "error": str(e)}, status=500) + + +def setup_routes(app: web.Application): + """设置路由""" + app.router.add_get("/", index_handler) + app.router.add_get("/api/config", get_config_handler) + app.router.add_post("/api/config", update_config_handler) + diff --git a/src/webui/static.py b/src/webui/static.py new file mode 100644 index 0000000..5deb59d --- /dev/null +++ b/src/webui/static.py @@ -0,0 +1,359 @@ +""" +静态资源 - 提供 HTML 页面 +""" + + +def get_index_html() -> str: + """返回主页面 HTML""" + return ''' + + + + + MaiBot Adapter 配置管理 + + + +
+

🤖 MaiBot Adapter 配置管理

+ +
+

📋 群聊设置

+
+ + +
+
+ +
+
+
加载中...
+
+
+ + +
+
+
+
+ +
+

💬 私聊设置

+
+ + +
+
+ +
+
+
加载中...
+
+
+ + +
+
+
+
+
+ +
+ + + +''' +