diff --git a/Lib/common.py b/Lib/common.py
index cb42e4d..b72c0c8 100644
--- a/Lib/common.py
+++ b/Lib/common.py
@@ -1,7 +1,7 @@
"""
工具
"""
-
+import os.path
import shutil
import sys
import time
@@ -241,9 +241,12 @@ def save_exc_dump(description: str = None, path: str = None):
break
i += 1
+ for _ in ['?', '*', '"', '<', '>']:
+ path_ = path_.replace(_, "")
+
kwargs = {
"frame": frame,
- "path": path_
+ "path": os.path.normpath(path_)
}
if description:
kwargs["description"] = description
diff --git a/Lib/core/OnebotAPI.py b/Lib/core/OnebotAPI.py
index 710be8e..9b635c4 100644
--- a/Lib/core/OnebotAPI.py
+++ b/Lib/core/OnebotAPI.py
@@ -199,14 +199,14 @@ def get_msg(self, message_id: int):
}
return self.get("/get_msg", data)
- def get_forward_msg(self, message_id: int):
+ def get_forward_msg(self, id: int):
"""
获取合并转发消息
Args:
- message_id: 消息id
+ id: 合并转发id
"""
data = {
- "message_id": message_id
+ "id": id
}
return self.get("/get_forward_msg", data)
diff --git a/Lib/core/PluginManager.py b/Lib/core/PluginManager.py
index 9ae4042..a1f30cb 100644
--- a/Lib/core/PluginManager.py
+++ b/Lib/core/PluginManager.py
@@ -4,6 +4,7 @@
import dataclasses
import importlib
+import inspect
import sys
import traceback
@@ -221,3 +222,31 @@ def run_plugin_main_wrapper(event):
event: 事件
"""
run_plugin_main(event.event_data)
+
+
+def get_caller_plugin_data():
+ """
+ 获取调用者的插件数据
+ :return:
+ plugin_data: dict | None
+ """
+
+ stack = inspect.stack()[1:]
+ for frame_info in stack:
+ filename = frame_info.filename
+
+ normalized_filename = os.path.normpath(filename)
+ normalized_plugins_path = os.path.normpath(PLUGINS_PATH)
+
+ if normalized_filename.startswith(normalized_plugins_path):
+ for plugin in found_plugins:
+ normalized_plugin_file_path = os.path.normpath(plugin["file_path"])
+ plugin_dir, plugin_file = os.path.split(normalized_plugin_file_path)
+
+ if plugin_dir == normalized_plugins_path:
+ if normalized_plugin_file_path == normalized_filename:
+ return plugin
+ else:
+ if normalized_filename.startswith(plugin_dir):
+ return plugin
+ return None
diff --git a/Lib/utils/Actions.py b/Lib/utils/Actions.py
index 938de94..47cba1a 100644
--- a/Lib/utils/Actions.py
+++ b/Lib/utils/Actions.py
@@ -335,16 +335,16 @@ class GetForwardMsg(Action):
call_func = api.get_forward_msg
- def __init__(self, message_id: int, callback: Callable[[Result], ...] = None):
+ def __init__(self, id: int, callback: Callable[[Result], ...] = None):
"""
Args:
- message_id (int): 合并转发 ID
+ id (int): 合并转发 ID
callback (Callable[[Result], ...], optional): 回调函数. Defaults to None.
"""
- super().__init__(message_id=message_id, callback=callback)
+ super().__init__(id=id, callback=callback)
- def logger(self, result, message_id: int):
- logger.debug(f"获取合并转发消息 {message_id}")
+ def logger(self, result, id: int):
+ logger.debug(f"获取合并转发消息 {id}")
class SendLike(Action):
diff --git a/Lib/utils/EventHandlers.py b/Lib/utils/EventHandlers.py
index 897a3d2..f37bbb3 100644
--- a/Lib/utils/EventHandlers.py
+++ b/Lib/utils/EventHandlers.py
@@ -5,8 +5,8 @@
import traceback
from Lib.common import save_exc_dump
-from Lib.core import EventManager, ConfigManager
-from Lib.utils import EventClassifier, Logger, QQRichText
+from Lib.core import EventManager, ConfigManager, PluginManager
+from Lib.utils import EventClassifier, Logger, QQRichText, StateManager
import inspect
from typing import Literal, Callable, Any, Type
@@ -313,18 +313,51 @@ def wrapper(func):
return wrapper
- def match(self, event_data: EventClassifier.Event):
+ def match(self, event_data: EventClassifier.Event, plugin_data: dict):
"""
匹配事件处理器
Args:
event_data: 事件数据
+ plugin_data: 插件数据
"""
for priority, rules, handler, args, kwargs in sorted(self.handlers, key=lambda x: x[0], reverse=True):
try:
- if all(rule.match(event_data) for rule in rules):
- if handler(event_data, *args, **kwargs) is True:
- logger.debug(f"处理器 {handler.__name__} 阻断了事件 {event_data} 的传播")
- return
+ if not all(rule(event_data) for rule in rules):
+ continue
+
+ # 检测依赖注入
+ handler_kwargs = kwargs.copy() # 复制静态 kwargs
+
+ sig = inspect.signature(handler)
+
+ for name, param in sig.parameters.items():
+ if name == "state":
+ if (isinstance(event_data, EventClassifier.MessageEvent) or
+ isinstance(event_data, EventClassifier.PrivateMessageEvent)):
+ state_id = f"u{event_data.user_id}"
+ elif isinstance(event_data, EventClassifier.GroupMessageEvent):
+ state_id = f"g{event_data.group_id}_u{event_data.user_id}"
+ else:
+ raise TypeError("event_data must be a MessageEvent")
+ handler_kwargs[name] = StateManager.get_state(state_id, plugin_data)
+ elif name == "user_state":
+ if isinstance(event_data, EventClassifier.MessageEvent):
+ state_id = f"u{event_data.user_id}"
+ else:
+ raise TypeError("event_data must be a MessageEvent")
+ handler_kwargs[name] = StateManager.get_state(state_id, plugin_data)
+ elif name == "group_state":
+ if isinstance(event_data, EventClassifier.GroupMessageEvent):
+ state_id = f"g{event_data.group_id}"
+ else:
+ raise TypeError("event_data must be a MessageEvent")
+ handler_kwargs[name] = StateManager.get_state(state_id, plugin_data)
+
+ result = handler(event_data, *args, **handler_kwargs)
+
+ if result is True:
+ logger.debug(f"处理器 {handler.__name__} 阻断了事件 {event_data} 的传播")
+ return # 阻断同一 Matcher 内的传播
except Exception as e:
if ConfigManager.GlobalConfig().debug.save_dump:
dump_path = save_exc_dump(f"执行匹配事件或执行处理器时出错 {event_data}")
@@ -340,12 +373,12 @@ def match(self, event_data: EventClassifier.Event):
events_matchers: dict[str, dict[Type[EventClassifier.Event], list[tuple[int, list[Rule], Matcher]]]] = {}
-def _on_event(event_data, path, event_type):
+def _on_event(event_data, path, event_type, plugin_data):
matchers = events_matchers[path][event_type]
for priority, rules, matcher in sorted(matchers, key=lambda x: x[0], reverse=True):
matcher_event_data = event_data.__class__(event_data.event_data)
if all(rule.match(matcher_event_data) for rule in rules):
- matcher.match(matcher_event_data)
+ matcher.match(matcher_event_data, plugin_data)
def on_event(event: Type[EventClassifier.Event], priority: int = 0, rules: list[Rule] = None):
@@ -364,12 +397,13 @@ def on_event(event: Type[EventClassifier.Event], priority: int = 0, rules: list[
raise TypeError("rules must be a list of Rule")
if not issubclass(event, EventClassifier.Event):
raise TypeError("event must be an instance of EventClassifier.Event")
- path = inspect.stack()[1].filename
+ plugin_data = PluginManager.get_caller_plugin_data()
+ path = plugin_data["path"]
if path not in events_matchers:
events_matchers[path] = {}
if event not in events_matchers[path]:
events_matchers[path][event] = []
- EventManager.event_listener(event, path=path, event_type=event)(_on_event)
+ EventManager.event_listener(event, path=path, event_type=event, plugin_data=plugin_data)(_on_event)
events_matcher = Matcher()
events_matchers[path][event].append((priority, rules, events_matcher))
return events_matcher
diff --git a/Lib/utils/QQRichText.py b/Lib/utils/QQRichText.py
index ccb79b1..4c18810 100644
--- a/Lib/utils/QQRichText.py
+++ b/Lib/utils/QQRichText.py
@@ -117,7 +117,7 @@ def cq_2_array(cq: str) -> list[dict[str, dict[str, Any]]]:
segment_data[now_key] += c # 继续拼接参数值
if "text" in segment_data and len(segment_data["text"]): # 处理末尾可能存在的文本内容
- cq_array.append({"type": "text", "data": {"text": segment_data["text"]}})
+ cq_array.append({"type": "text", "data": {"text": cq_decode(segment_data["text"])}})
return cq_array
@@ -961,8 +961,8 @@ class XML(Segment):
segment_type = "xml"
def __init__(self, data):
- self.data = data
- super().__init__({"type": "xml", "data": {"data": str(self.data)}})
+ self.xml_data = data
+ super().__init__({"type": "xml", "data": {"data": str(self.xml_data)}})
def set_xml_data(self, data):
"""
@@ -970,8 +970,8 @@ def set_xml_data(self, data):
Args:
data: xml数据
"""
- self.data = data
- self.array["data"]["data"] = str(self.data)
+ self.xml_data = data
+ self.array["data"]["data"] = str(self.xml_data)
class JSON(Segment):
@@ -985,8 +985,8 @@ def __init__(self, data):
Args:
data: JSON 内容
"""
- self.data = data
- super().__init__({"type": "json", "data": {"data": str(self.data)}})
+ self.json_data = data
+ super().__init__({"type": "json", "data": {"data": str(self.json_data)}})
def set_json(self, data):
"""
@@ -994,8 +994,8 @@ def set_json(self, data):
Args:
data: json 内容
"""
- self.data = data
- self.array["data"]["data"] = str(self.data)
+ self.json_data = data
+ self.array["data"]["data"] = str(self.json_data)
def get_json(self):
"""
@@ -1003,7 +1003,7 @@ def get_json(self):
Returns:
json: json数据
"""
- return json.loads(self.data)
+ return json.loads(self.json_data)
class QQRichText:
diff --git a/Lib/utils/StateManager.py b/Lib/utils/StateManager.py
new file mode 100644
index 0000000..bd1b6b2
--- /dev/null
+++ b/Lib/utils/StateManager.py
@@ -0,0 +1,68 @@
+"""
+状态管理器
+注意,数据存储于内存中,重启丢失,需要持久化保存的数据请勿放在里面
+"""
+
+from Lib.core import PluginManager
+
+states = {}
+
+
+def get_state(state_id: str, plugin_data: dict = None):
+ """
+ 获取状态数据
+ Args:
+ state_id: 状态ID
+ plugin_data: 插件数据
+
+ Returns:
+ 状态数据,结构类似
+ {
+ "state_id": state_id,
+ "data": {
+ k1: v1,
+ k2: v2
+ },
+ "other_plugin_data": {
+ plugin1_path: {
+ "data": {
+ k1: v1,
+ k2: v2
+ },
+ "meta": {
+ "plugin_data": plugin_data
+ }
+ },
+ plugin2_path: {
+ "data": {
+ k1: v1,
+ k2: v2
+ },
+ "meta": {
+ "plugin_data": plugin_data
+ }
+ }
+ }
+ }
+ """
+ if plugin_data is None:
+ plugin_data = PluginManager.get_caller_plugin_data()
+
+ if state_id not in states:
+ states[state_id] = {}
+
+ if plugin_data["path"] not in states[state_id]:
+ states[state_id][plugin_data["path"]] = {
+ "data": {},
+ "meta": {
+ "plugin_data": plugin_data
+ }
+ }
+
+ return {
+ "state_id": state_id,
+ "data": states[state_id][plugin_data["path"]]["data"],
+ "other_plugin_data": {
+ k: v for k, v in states[state_id].items() if k != plugin_data["path"]
+ }
+ }
diff --git a/Lib/utils/__init__.py b/Lib/utils/__init__.py
index a700ff0..ddc8a6e 100644
--- a/Lib/utils/__init__.py
+++ b/Lib/utils/__init__.py
@@ -3,6 +3,7 @@
"""
from . import Logger
from . import EventClassifier
+from . import StateManager
from . import QQRichText
from . import QQDataCacher
from . import Actions
diff --git a/README.md b/README.md
index 590b999..8f234ff 100644
--- a/README.md
+++ b/README.md
@@ -2,74 +2,72 @@

-
+
-
-
+
+
-
+
+## 🤔 概述
-## 🤔概述
+MuRainBot2 (MRB2) 是一个基于 Python、适配 OneBot v11 协议的**轻量级** QQ 机器人框架。
-### 这是什么?
+它专注于提供稳定高效的核心事件处理与 API 调用能力,所有具体功能(如关键词回复、群管理等)均通过插件实现,赋予开发者高度的灵活性。
-#### 这是一个基于python适配onebot11协议的轻量级QQBot框架
-
-切记!
-MRB2本身**不具备任何**实际功能
-
-具体的功能(如通过关键词回复特定消息等)都需要插件来实现,有任何功能的需要可以自行阅读文档编写。
-
-同时,对于具体的操作(如监听消息、发送消息、批准加群请求之类的)请先明确:
+对于具体的操作(如监听消息、发送消息、批准加群请求等)请先明确:
- 什么是[OneBot11协议](https://11.onebot.dev);
- - 什么是Onebot实现端,什么是Onebot框架;
- - Onebot实现端具有哪些features
-
-~~作者自己写着用的,有一些写的不好的地方还请见谅~~
-
-~~*什么?你问我为什么要叫MRB2,因为这个框架最初是给我的一个叫做沐雨的qqbot写的,然后之前还有[一个写的很垃圾](https://github.com/xiaosuyyds/PyQQbot)的版本,所以就叫做MRB2*~~
+ - 什么是Onebot实现端,什么是Onebot开发框架;
+ - Onebot实现端具有哪些功能
+## ✨ 核心特性
-### 关于本Readme以及MRB2文档的一些术语
-#### 首先推荐在阅读本Readme以及MRB2文档中先了解Onebot的一些基本属于以方便理解[我是链接](https://12.onebot.dev/glossary)
-(上述的术语表中是Onebot12的定义,对于Onebot11可能略有偏差但是大体相通)
-* MRB2:MuRainBot2,缩写MRB2
-* onebot11协议:[OneBot v11](https://11.onebot.dev/),一个用于QQ机器人的协议,本项目就是基于此协议开发的框架
-* 框架:根据[onebot12的定义](https://12.onebot.dev/glossary/#onebot-sdk)(~~我也不知道为什么,但是onebot11就是没有定义什么是框架和sdk之类乱七八糟的术语表~~)
-MRB2就是这样一个SDK(也可称为框架),可以让插件不需要自行实现http通讯和事件、操作、消息等内容的事件分发与解析,同时将实际的功能分为一个一个插件,方便管理。
-* 插件:MRB2本身不具备任何实际上的功能,一切都需要编写插件来实现功能;
-MRB2的插件,统一放在`plugins`文件夹中,每个插件都是一个python文件,也可以是一个文件夹,文件夹中必须包含一个`__init__.py`文件用于初始化插件
+* **🪶 轻量高效:** 没有太多冗杂的功能,使用简单,内存占用较低。
+* **🧩 轻松扩展:** 灵活的插件系统,让您能够轻松、快速地添加、移除或定制所需功能。
+* **🔁 基于线程池:** 基于内置线程池实现并发处理,没有异步的较为复杂的语法,直接编写同步代码。
-### 关于一些提醒
+## 🚨 重要提醒:关于重构与兼容性
-本项目在2024年12月4日在dev分支对框架进行了重构,主要重构了目录结构与一些Lib的实现,不支持过去的插件,如果你有旧版本的插件,可以尝试使用新的框架的文档来进行适配(放心,差别不会很大)。
+> [!CAUTION]
+> **请注意:** 本项目在 2024 年底至 2025 年初进行了一次 **彻底的框架重构**(主要涉及 `dev` 分支并在 2025年1月29日 合并至 `master`)。
+>
+> **当前的 MRB2 版本与重构前的旧版本插件完全不兼容。** 如果您是旧版本用户或拥有旧插件,请参考 **[最新文档](https://mrb2.xiaosu.icu)** 进行适配迁移。
-并在2025年1月29日将重构后的dev分支合并到了master分支。
+## 📖 背景与术语
----
+* **MRB2:** MuRainBot2 的缩写。
+* **OneBot v11 协议:** 一个广泛应用于即时通讯软件中的聊天机器人的应用层协议标准,本项目基于此标准开发。详情请见 [OneBot v11](https://11.onebot.dev/)。
+* **框架 (SDK):** MRB2 作为一个 OneBot SDK(或称开发框架),负责处理与 OneBot 实现端的通信、事件分发、API 调用封装等底层工作,以及提供插件系统,让开发者可以专注于插件功能的实现。更多通用术语可参考 [OneBot v12 术语表](https://12.onebot.dev/glossary/) (v11 与 v12 大体相通)。
+* **插件:** MRB2 的所有功能都由插件提供。插件通常是放置在 `plugins` 目录下的 Python 文件或包含 `__init__.py` 的 Python 包。
-如果使用时遇到问题,请将 `config.yml` 的`debug.enable`设置为`true`,然后复现 bug,
-并检查该问题是否是你使用的 Onebot 实现端的问题(可查看实现端的日志检查是否有异常)
+~~*什么?你问我为什么要叫MRB2,因为这个框架最初是给我的一个叫做沐雨的qqbot写的,然后之前还有[一个写的很垃圾](https://github.com/xiaosuyyds/PyQQbot)的版本,所以就叫做MRB2*~~
-如果是,请自行在你使用的 Onebot 实现端进行反馈。
+## 🐛 问题反馈
-如果不是,将完整 完整 完整的将日志信息(部分对于问题排查不重要的敏感信息(如 QQ 群号、 QQ 号等)可自行遮挡) 和错误描述发到 [issues](https://github.com/MuRainBot/MuRainBot2/issues/new/choose)。
-如果你开启了 `config.yml` 的`debug.save_dump` 也可以把出错的 dump 文件也一起上传以辅助排查问题。
+如果使用时遇到问题,请按以下步骤操作:
-### 目录结构
+1. 将框架版本更新到 [`dev`](https://github.com/MuRainBot/MuRainBot2/tree/dev) 分支
+2. 将 `config.yml` 中的 `debug.enable` 设置为 `true`。
+3. 复现您遇到的 Bug。
+4. **检查 Onebot 实现端的日志**,确认问题是否源于实现端本身。如果是,请向您使用的实现端反馈。
+5. 如果问题确认在 MRB2 框架:
+ * 请准备**完整**的 MRB2 日志文件 (`logs` 目录下)。您可以自行遮挡日志中的 QQ 号、群号等敏感信息。
+ * 提供清晰的错误描述、复现步骤。
+ * 如果开启了 `save_dump` 且生成了 dump 文件,可以一并提供。(不强制,但是推荐提供,不过需要注意可以检查一下是否包含apikey等敏感信息)
+ * 将以上信息提交到项目的 [**Issues**](https://github.com/MuRainBot/MuRainBot2/issues/new/choose) 页面。
+## 📁 目录结构
-查看基本看目录结构
+查看基本目录结构
```
├─ data MRB2及插件的临时/缓存文件
@@ -87,7 +85,7 @@ MRB2的插件,统一放在`plugins`文件夹中,每个插件都是一个pyth
│ ...
├─ plugins
│ ├─ xxx.py xxx插件代码
-│ ├─ yyy.py yyy插件代码
+│ ├─ yyy.py yyy插件代码
│ ...
├─ plugin_configs
│ ├─ xxx.yml xxx插件的配置文件
@@ -100,33 +98,41 @@ MRB2的插件,统一放在`plugins`文件夹中,每个插件都是一个pyth
+## 💻 如何部署?
+
+**本项目使用 Python 3.12+ 开发,并利用了其部分新特性 (如 [PEP 701](https://docs.python.org/zh-cn/3/whatsnew/3.12.html#whatsnew312-pep701))。推荐使用 Python 3.12 或更高版本运行,如果使用 Python 3.12 以下版本,由于未经测试,可能会导致部分代码出现问题。**
-## 💻如何部署?
-**作者在python3.12编写,由于使用了一些高版本python添加的特性
-(例如[PEP701中取消部分f-string语法限制(这真的超方便的好不好()](https://docs.python.org/zh-cn/3.13/whatsnew/3.12.html#whatsnew312-pep701)),
-推荐在3.12.X或以上版本部署和运行**
+详细的部署步骤、配置说明和插件开发指南,请查阅:
-### 具体可查看本项目的[`文档`](https://mrb2.xiaosu.icu)
+### ➡️ [**MRB2 官方文档**](https://mrb2.xiaosu.icu)
-## 📕关于版本
-* 目前MRB2版本为1.0.0-dev
+## 📕 关于版本
-* 关于版本号的说明:
- * 版本号格式为`<主版本>.<次版本>.<修订版本>-<特殊提醒/版本(如果有)>` 例如`1.0.0`
- * 开发版版本号后统一添加`-dev`后缀 例如`1.0.0-dev`
+* 当前版本:`1.0.0-dev`
+* 版本号格式:`<主版本>.<次版本>.<修订版本>[-<特殊标识>]` (例如 `1.0.0`, `1.0.1-beta`, `1.1.0-dev`)。
-## ❤️鸣谢❤️
+## ❤️ 鸣谢 ❤️
-### 请勿直接提交到[`master`](https://github.com/MuRainBot/MuRainBot2)分支,请先提交到[`dev`](https://github.com/MuRainBot/MuRainBot2/tree/dev)分支,每隔一段时间我们会合并到[`master`](https://github.com/MuRainBot/MuRainBot2)分支
+**贡献指南:** 我们欢迎各种形式的贡献!请将您的 Pull Request 提交到 `dev` 分支。我们会定期将 `dev` 分支的稳定更新合并到 `master` 分支。
+
+**感谢所有为 MRB2 付出努力的贡献者!**
-### 感谢所有为此项目做出贡献的大大,你们的存在,让社区变得更加美好~!
-
+
-### 以及特别鸣谢[HarcicYang](https://github.com/HarcicYang)和[kaokao221](https://github.com/kaokao221)为此项目提供了许多的帮助~
-
+**特别感谢 [HarcicYang](https://github.com/HarcicYang)、[kaokao221](https://github.com/kaokao221) 和 [BigCookie233](https://github.com/BigCookie233) 在项目开发过程中提供的宝贵帮助!**
-## ⭐StarHistory⭐
+## ⭐ Star History ⭐
[](https://github.com/MuRainBot/MuRainBot2/stargazers)
+
+
+## 🚀 关于性能
+
+本项目在正常使用,默认配置,多群聊,6-8个中等复杂度(如签到、图片绘制(如视频信息展示等)、AI聊天(基于API接口调用的))的插件情况下内存占用稳定在 100-160MB 左右
+(具体取决于插件和群聊数量以及配置文件,也可能超过这个范围)
+
+仅安装默认插件,默认配置,情况下内存占用稳定在 40MB-60MB 左右
+
+如果实在内存不够用可调小缓存(配置文件中的 `qq_data_cache.max_cache_size`)(尽管这个也占不了多少内存)