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]
|
del self._client_health[client_id]
|
||||||
with self._bots_lock:
|
with self._bots_lock:
|
||||||
if client_id in self.bots:
|
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]
|
del self.bots[client_id]
|
||||||
|
|
||||||
# 清理该客户端的防重复数据
|
# 清理该客户端的防重复数据
|
||||||
|
|||||||
@@ -232,6 +232,11 @@ class WS:
|
|||||||
"""
|
"""
|
||||||
self.logger.info("正在关闭 WebSocket 客户端...")
|
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:
|
if self.ws:
|
||||||
await self.ws.close()
|
await self.ws.close()
|
||||||
|
|
||||||
|
|||||||
@@ -65,10 +65,30 @@ python setup_mypyc.py build_ext --inplace
|
|||||||
uri = "ws://127.0.0.1:3001"
|
uri = "ws://127.0.0.1:3001"
|
||||||
token = ""
|
token = ""
|
||||||
|
|
||||||
|
#当然你也可以配置逆向连接
|
||||||
|
[reverse_ws]
|
||||||
|
enabled = true # 是否启用
|
||||||
|
host = "0.0.0.0" # 监听地址
|
||||||
|
port = 3002 # 监听端口
|
||||||
|
token = ""
|
||||||
|
|
||||||
[redis]
|
[redis]
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 6379
|
port = 6379
|
||||||
db = 0
|
db = 0
|
||||||
|
|
||||||
|
# MySQL 配置
|
||||||
|
[mysql]
|
||||||
|
# MySQL 主机地址
|
||||||
|
host = "114.66.61.199"
|
||||||
|
# MySQL 端口
|
||||||
|
port = 42398
|
||||||
|
# MySQL 用户名
|
||||||
|
user = "neobot"
|
||||||
|
# MySQL 密码
|
||||||
|
password = "neobot"
|
||||||
|
# MySQL 数据库名称
|
||||||
|
db = "neobot"
|
||||||
```
|
```
|
||||||
把 `uri` 改成你自己的 OneBot 地址。
|
把 `uri` 改成你自己的 OneBot 地址。
|
||||||
|
|
||||||
@@ -88,6 +108,3 @@ python -X jit -X gil=0 main.py
|
|||||||
如果你看到日志刷出来,最后显示 "连接成功!",恭喜,你成功了!
|
如果你看到日志刷出来,最后显示 "连接成功!",恭喜,你成功了!
|
||||||
|
|
||||||
现在,试着给你的机器人发个 `/help`看看会返回什么东西
|
现在,试着给你的机器人发个 `/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
|
# 将项目根目录添加到 sys.path
|
||||||
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, ROOT_DIR)
|
sys.path.insert(0, ROOT_DIR)
|
||||||
@@ -233,7 +218,6 @@ if __name__ == "__main__":
|
|||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# 捕获 KeyboardInterrupt,不做任何操作,让 asyncio.run 正常结束
|
# 捕获 KeyboardInterrupt,不做任何操作,让 asyncio.run 正常结束
|
||||||
# 这样 main 函数中的 finally 块会被执行
|
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
main_logger.exception("程序发生未处理的全局异常")
|
main_logger.exception("程序发生未处理的全局异常")
|
||||||
|
|||||||
@@ -114,10 +114,21 @@ async def broadcast_subscription_loop():
|
|||||||
|
|
||||||
logger.info(f"[Broadcast] 收到跨机器人广播消息: 来源 {robot_id}")
|
logger.info(f"[Broadcast] 收到跨机器人广播消息: 来源 {robot_id}")
|
||||||
|
|
||||||
# 获取当前机器人的实例
|
# 获取所有活跃的 Bot 实例
|
||||||
from core.ws import WS
|
from core.managers.bot_manager import bot_manager
|
||||||
if WS.instance:
|
all_bots = bot_manager.get_all_bots()
|
||||||
await broadcast_message_to_groups(WS.instance, message_data, robot_id)
|
|
||||||
|
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:
|
except json.JSONDecodeError as e:
|
||||||
logger.error(f"[Broadcast] 解析广播消息失败: {e}")
|
logger.error(f"[Broadcast] 解析广播消息失败: {e}")
|
||||||
@@ -178,16 +189,26 @@ async def handle_broadcast_content(event: MessageEvent):
|
|||||||
await event.reply("捕获到的消息为空,已取消广播。")
|
await event.reply("捕获到的消息为空,已取消广播。")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 获取当前机器人ID(使用反向WS的机器人ID)
|
# 获取当前机器人ID
|
||||||
from core.ws import WS
|
|
||||||
robot_id = "unknown"
|
robot_id = "unknown"
|
||||||
if WS.instance and hasattr(WS.instance, 'self_id'):
|
if event.bot and hasattr(event.bot, 'self_id'):
|
||||||
robot_id = str(WS.instance.self_id)
|
robot_id = str(event.bot.self_id)
|
||||||
|
|
||||||
# --- 执行本地广播 ---
|
# --- 执行本地广播 ---
|
||||||
|
# 1. 先让接收到指令的这个 Bot 进行广播
|
||||||
await broadcast_message_to_groups(event.bot, message_to_broadcast, robot_id)
|
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:
|
try:
|
||||||
if redis_manager.redis:
|
if redis_manager.redis:
|
||||||
broadcast_data = {
|
broadcast_data = {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Echo 与交互插件
|
|||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from core.bot import Bot
|
from core.bot import Bot
|
||||||
from models.events.message import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
from core.permission import Permission
|
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "echo",
|
"name": "echo",
|
||||||
@@ -14,7 +13,7 @@ __plugin_meta__ = {
|
|||||||
"usage": "/echo [内容] - 复读内容\n/赞我 - 让机器人给你点赞",
|
"usage": "/echo [内容] - 复读内容\n/赞我 - 让机器人给你点赞",
|
||||||
}
|
}
|
||||||
|
|
||||||
@matcher.command("echo", permission=Permission.ADMIN)
|
@matcher.command("echo")
|
||||||
async def handle_echo(bot: Bot, event: MessageEvent, args: list[str]):
|
async def handle_echo(bot: Bot, event: MessageEvent, args: list[str]):
|
||||||
"""
|
"""
|
||||||
处理 echo 指令,原样回复用户输入的内容
|
处理 echo 指令,原样回复用户输入的内容
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ anyio==4.12.1
|
|||||||
astroid==4.0.3
|
astroid==4.0.3
|
||||||
attrs==25.4.0
|
attrs==25.4.0
|
||||||
beautifulsoup4==4.14.3
|
beautifulsoup4==4.14.3
|
||||||
bilibili-api-python==2024.12.1
|
bilibili-api-python
|
||||||
bs4==0.0.2
|
bs4==0.0.2
|
||||||
cachetools==6.2.4
|
cachetools==6.2.4
|
||||||
certifi==2026.1.4
|
certifi==2026.1.4
|
||||||
|
|||||||
Reference in New Issue
Block a user