feat(bot): 实现 BotManager 并完善机器人注销逻辑
添加全局 BotManager 单例用于统一管理所有 Bot 实例 在 WS 关闭和 ReverseWSManager 清理时调用注销逻辑 修改广播插件使用 BotManager 获取所有活跃 Bot 实例 移除 echo 插件的权限限制并更新文档配置
This commit is contained in:
57
core/managers/bot_manager.py
Normal file
57
core/managers/bot_manager.py
Normal file
@@ -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()
|
||||
@@ -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]
|
||||
|
||||
# 清理该客户端的防重复数据
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -65,10 +65,30 @@ python setup_mypyc.py build_ext --inplace
|
||||
uri = "ws://127.0.0.1:3001"
|
||||
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 地址。
|
||||
|
||||
@@ -88,6 +108,3 @@ python -X jit -X gil=0 main.py
|
||||
如果你看到日志刷出来,最后显示 "连接成功!",恭喜,你成功了!
|
||||
|
||||
现在,试着给你的机器人发个 `/help`看看会返回什么东西
|
||||
|
||||
**多前端支持**:
|
||||
如果需要同时连接多个 OneBot 实现(如多个 QQ 账号),GIL-free 模式可以确保每个连接真正并行处理事件,不会相互阻塞。
|
||||
|
||||
16
main.py
16
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("程序发生未处理的全局异常")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 指令,原样回复用户输入的内容
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user