From dec1a43f283b3d02537ec5b9f7beeea33bdfd7c8 Mon Sep 17 00:00:00 2001 From: K2Cr2O1 <2221577113@qq.com> Date: Sun, 8 Mar 2026 12:25:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(bot):=20=E5=AE=9E=E7=8E=B0=20BotManager=20?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E5=96=84=E6=9C=BA=E5=99=A8=E4=BA=BA=E6=B3=A8?= =?UTF-8?q?=E9=94=80=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加全局 BotManager 单例用于统一管理所有 Bot 实例 在 WS 关闭和 ReverseWSManager 清理时调用注销逻辑 修改广播插件使用 BotManager 获取所有活跃 Bot 实例 移除 echo 插件的权限限制并更新文档配置 --- core/managers/bot_manager.py | 57 +++++++++++++++++++++++++++++ core/managers/reverse_ws_manager.py | 4 ++ core/ws.py | 5 +++ docs/getting-started.md | 27 +++++++++++--- main.py | 16 -------- plugins/broadcast.py | 39 +++++++++++++++----- plugins/echo.py | 3 +- requirements.txt | 2 +- 8 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 core/managers/bot_manager.py diff --git a/core/managers/bot_manager.py b/core/managers/bot_manager.py new file mode 100644 index 0000000..a572f6b --- /dev/null +++ b/core/managers/bot_manager.py @@ -0,0 +1,57 @@ +from typing import Dict, List, Optional, TYPE_CHECKING +import threading +from ..utils.logger import ModuleLogger + +if TYPE_CHECKING: + from ..bot import Bot + +class BotManager: + """ + Bot 实例管理器 + + 负责统一管理所有活跃的 Bot 实例(包括正向 WS 和反向 WS 连接的 Bot)。 + 提供注册、注销和获取 Bot 实例的方法。 + """ + def __init__(self): + self._bots: Dict[str, "Bot"] = {} # type: ignore[assignment] # key: bot_id (str), value: Bot instance + self._lock = threading.RLock() + self.logger = ModuleLogger("BotManager") + + def register_bot(self, bot: "Bot") -> None: + """ + 注册一个 Bot 实例 + """ + if not bot or not bot.self_id: + self.logger.warning("尝试注册无效的 Bot 实例") + return + + bot_id = str(bot.self_id) + with self._lock: + self._bots[bot_id] = bot + self.logger.info(f"Bot 实例已注册: {bot_id}") + + def unregister_bot(self, bot_id: str) -> None: + """ + 注销一个 Bot 实例 + """ + with self._lock: + if bot_id in self._bots: + del self._bots[bot_id] + self.logger.info(f"Bot 实例已注销: {bot_id}") + + def get_bot(self, bot_id: str) -> Optional["Bot"]: + """ + 根据 ID 获取 Bot 实例 + """ + with self._lock: + return self._bots.get(str(bot_id)) + + def get_all_bots(self) -> List["Bot"]: + """ + 获取所有活跃的 Bot 实例 + """ + with self._lock: + return list(self._bots.values()) + +# 全局单例实例 +bot_manager = BotManager() diff --git a/core/managers/reverse_ws_manager.py b/core/managers/reverse_ws_manager.py index db611a7..c214695 100644 --- a/core/managers/reverse_ws_manager.py +++ b/core/managers/reverse_ws_manager.py @@ -255,6 +255,10 @@ class ReverseWSManager: del self._client_health[client_id] with self._bots_lock: if client_id in self.bots: + # 从 BotManager 注销 + from .bot_manager import bot_manager + if self.bots[client_id].self_id: + bot_manager.unregister_bot(str(self.bots[client_id].self_id)) del self.bots[client_id] # 清理该客户端的防重复数据 diff --git a/core/ws.py b/core/ws.py index a2d32eb..0734259 100644 --- a/core/ws.py +++ b/core/ws.py @@ -232,6 +232,11 @@ class WS: """ self.logger.info("正在关闭 WebSocket 客户端...") + # 从 BotManager 注销 + if self.bot and self.self_id: + from .managers.bot_manager import bot_manager + bot_manager.unregister_bot(str(self.self_id)) + if self.ws: await self.ws.close() diff --git a/docs/getting-started.md b/docs/getting-started.md index 08690cf..1e80f3b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -63,12 +63,32 @@ python setup_mypyc.py build_ext --inplace # 你的 OneBot 地址 # 我们用的是正向连接,也就是 Bot 主动去连 OneBot uri = "ws://127.0.0.1:3001" -token = "" +token = "" + +#当然你也可以配置逆向连接 +[reverse_ws] +enabled = true # 是否启用 +host = "0.0.0.0" # 监听地址 +port = 3002 # 监听端口 +token = "" [redis] host = "127.0.0.1" port = 6379 db = 0 + +# MySQL 配置 +[mysql] +# MySQL 主机地址 +host = "114.66.61.199" +# MySQL 端口 +port = 42398 +# MySQL 用户名 +user = "neobot" +# MySQL 密码 +password = "neobot" +# MySQL 数据库名称 +db = "neobot" ``` 把 `uri` 改成你自己的 OneBot 地址。 @@ -87,7 +107,4 @@ python -X jit -X gil=0 main.py 如果你看到日志刷出来,最后显示 "连接成功!",恭喜,你成功了! -现在,试着给你的机器人发个 `/help`看看会返回什么东西 - -**多前端支持**: -如果需要同时连接多个 OneBot 实现(如多个 QQ 账号),GIL-free 模式可以确保每个连接真正并行处理事件,不会相互阻塞。 +现在,试着给你的机器人发个 `/help`看看会返回什么东西 \ No newline at end of file diff --git a/main.py b/main.py index ecbe375..5395806 100644 --- a/main.py +++ b/main.py @@ -24,21 +24,6 @@ from core.services.local_file_server import start_local_file_server, stop_local_ -# 尝试使用高性能事件循环 -try: - if sys.platform == 'win32': - # winloop 与 Playwright 存在兼容性问题 (不支持 startupinfo),暂时禁用 - # import winloop - # asyncio.set_event_loop_policy(winloop.EventLoopPolicy()) - # print("已启用 winloop 高性能事件循环") - print("Windows 平台检测到 Playwright,已自动禁用 winloop 以确保兼容性") - else: - import uvloop - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - print("已启用 uvloop 高性能事件循环") -except ImportError: - print("未检测到高性能事件循环库 (uvloop/winloop),将使用默认事件循环") - # 将项目根目录添加到 sys.path ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, ROOT_DIR) @@ -233,7 +218,6 @@ if __name__ == "__main__": asyncio.run(main()) except KeyboardInterrupt: # 捕获 KeyboardInterrupt,不做任何操作,让 asyncio.run 正常结束 - # 这样 main 函数中的 finally 块会被执行 pass except Exception as e: main_logger.exception("程序发生未处理的全局异常") diff --git a/plugins/broadcast.py b/plugins/broadcast.py index 5b3ca46..0b9350f 100644 --- a/plugins/broadcast.py +++ b/plugins/broadcast.py @@ -114,10 +114,21 @@ async def broadcast_subscription_loop(): logger.info(f"[Broadcast] 收到跨机器人广播消息: 来源 {robot_id}") - # 获取当前机器人的实例 - from core.ws import WS - if WS.instance: - await broadcast_message_to_groups(WS.instance, message_data, robot_id) + # 获取所有活跃的 Bot 实例 + from core.managers.bot_manager import bot_manager + all_bots = bot_manager.get_all_bots() + + if not all_bots: + logger.warning("[Broadcast] 没有活跃的 Bot 实例,无法转发广播消息") + continue + + # 遍历所有 Bot 进行广播 + for bot in all_bots: + # 避免重复广播:如果消息来源就是当前 Bot,则跳过 + if str(bot.self_id) == str(robot_id): + continue + + await broadcast_message_to_groups(bot, message_data, robot_id) except json.JSONDecodeError as e: logger.error(f"[Broadcast] 解析广播消息失败: {e}") @@ -178,16 +189,26 @@ async def handle_broadcast_content(event: MessageEvent): await event.reply("捕获到的消息为空,已取消广播。") return True - # 获取当前机器人ID(使用反向WS的机器人ID) - from core.ws import WS + # 获取当前机器人ID robot_id = "unknown" - if WS.instance and hasattr(WS.instance, 'self_id'): - robot_id = str(WS.instance.self_id) + if event.bot and hasattr(event.bot, 'self_id'): + robot_id = str(event.bot.self_id) # --- 执行本地广播 --- + # 1. 先让接收到指令的这个 Bot 进行广播 await broadcast_message_to_groups(event.bot, message_to_broadcast, robot_id) - # --- 通过 Redis 发布消息给其他机器人 --- + # 2. 获取其他所有 Bot 并进行广播(针对同一进程内的其他 Bot) + from core.managers.bot_manager import bot_manager + all_bots = bot_manager.get_all_bots() + + for bot in all_bots: + # 跳过已经广播过的 Bot (即当前接收指令的 Bot) + if str(bot.self_id) == robot_id: + continue + await broadcast_message_to_groups(bot, message_to_broadcast, robot_id) + + # --- 通过 Redis 发布消息给其他进程的机器人 --- try: if redis_manager.redis: broadcast_data = { diff --git a/plugins/echo.py b/plugins/echo.py index d017712..8a700a2 100644 --- a/plugins/echo.py +++ b/plugins/echo.py @@ -6,7 +6,6 @@ Echo 与交互插件 from core.managers.command_manager import matcher from core.bot import Bot from models.events.message import MessageEvent -from core.permission import Permission __plugin_meta__ = { "name": "echo", @@ -14,7 +13,7 @@ __plugin_meta__ = { "usage": "/echo [内容] - 复读内容\n/赞我 - 让机器人给你点赞", } -@matcher.command("echo", permission=Permission.ADMIN) +@matcher.command("echo") async def handle_echo(bot: Bot, event: MessageEvent, args: list[str]): """ 处理 echo 指令,原样回复用户输入的内容 diff --git a/requirements.txt b/requirements.txt index 16b3a15..a534050 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ anyio==4.12.1 astroid==4.0.3 attrs==25.4.0 beautifulsoup4==4.14.3 -bilibili-api-python==2024.12.1 +bilibili-api-python bs4==0.0.2 cachetools==6.2.4 certifi==2026.1.4