Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6839406
fix: 修改加载逻辑,修复在部分情况下无法正确加载插件和若被依赖插件提前被加载且因为某些原因无法加载成功后无法找到被依赖插件的问题
xiaosuyyds Mar 14, 2025
6d59106
feat(CommandRule): 增加无参数命令模式
xiaosuyyds Mar 14, 2025
bb90ff6
refactor: 将 finalize_and_cleanup 函数移至 common 模块
xiaosuyyds Mar 15, 2025
7ea7ed3
style: 优化代码样式
xiaosuyyds Mar 15, 2025
4d5615a
fix: 修复PluginConfig无法正常获取插件名称的问题
xiaosuyyds Mar 15, 2025
eb87823
feat(debug): 添加使用coredumpy的异常保存功能
xiaosuyyds Mar 15, 2025
031cf6c
调整异常存储位置并创建相关目录
xiaosuyyds Mar 15, 2025
67ed7b6
删除遗漏的调试语句
xiaosuyyds Mar 15, 2025
5a5b603
fix(EventClassifier): 修复退群和踢出群聊时的用户昵称获取很容易出错的问题,并修改部分log
xiaosuyyds Mar 16, 2025
a4d6b10
更新项目依赖版本
xiaosuyyds Mar 16, 2025
bb6abe1
docs(README): 更新问题反馈说明和版本协议信息
xiaosuyyds Mar 16, 2025
07d5bcb
feat(utils): 新增 AnyRule 和 AllRule 类
xiaosuyyds Mar 17, 2025
6be7293
feat(QQRichText): 支持全体成员at并优化代码结构
xiaosuyyds Mar 17, 2025
e66225b
docs(config): 更新 dump 文件配置说明并调整依赖
xiaosuyyds Mar 17, 2025
53f7053
feat: 允许不安装coredumpy的情况下也能运行框架,优化线程池的异常日志记录
xiaosuyyds Mar 17, 2025
0270667
feat(core): 在 Plugin 类中添加 extra 字段用于存储附加信息
xiaosuyyds Mar 21, 2025
4e86f7d
feat(Onebot通信): 支持HTTP的access_token与HTTP POST的签名密钥验证
xiaosuyyds Mar 22, 2025
ccc0bea
feat(common): 添加 BytesIO 对象转文件功能
xiaosuyyds Mar 22, 2025
751d96c
fix(ListenerServer): 修复缺少签名的非法请求导致监听服务器报错的问题
xiaosuyyds Mar 24, 2025
328433b
fix(core): 修复 Onebot API 请求中的access_token字段名错误
xiaosuyyds Mar 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*.log
/.idea
*.pyc
*.dump
test.py
*.zip
/MURainBot2
/MURainBot2
107 changes: 107 additions & 0 deletions Lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import shutil
import sys
import time
import traceback
import uuid
from collections import OrderedDict
from io import BytesIO

import requests

Expand All @@ -20,6 +22,7 @@ class LimitedSizeDict(OrderedDict):
"""
带有限制大小的字典
"""

def __init__(self, max_size):
self._max_size = max_size
super().__init__()
Expand Down Expand Up @@ -155,6 +158,7 @@ def cache_decorator(func):
Returns:
None
"""

def wrapper(*args, **kwargs):
key = str(func.__name__) + str(args) + str(kwargs)
if key in cache and (expiration_time == -1 or time.time() - cache[key][1] < expiration_time):
Expand All @@ -181,3 +185,106 @@ def original_func(*args, **kwargs):
return wrapper

return cache_decorator


def finalize_and_cleanup():
"""
结束运行
@return:
"""
logger.info("MuRainBot即将关闭,正在删除缓存")

clean_cache()

logger.warning("MuRainBot结束运行!")
logger.info("再见!\n")


def save_exc_dump(description: str = None, path: str = None):
"""
保存异常堆栈
Args:
description: 保存的dump描述,为空则默认
path: 保存的路径,为空则自动根据错误生成
"""
try:
import coredumpy
except ImportError:
logger.warning("coredumpy未安装,无法保存异常堆栈")
return

try:
exc_type, exc_value, exc_traceback = sys.exc_info()
if not exc_traceback:
raise Exception("No traceback found")

# 遍历 traceback 链表,找到最后一个 frame (异常最初发生的位置)
current_tb = exc_traceback
frame = current_tb.tb_frame
while current_tb:
frame = current_tb.tb_frame
current_tb = current_tb.tb_next

i = 0
while True:
if i > 0:
path_ = os.path.join(DUMPS_PATH,
f"coredumpy_"
f"{time.strftime('%Y%m%d%H%M%S')}_"
f"{frame.f_code.co_name}_{i}.dump")
else:
path_ = os.path.join(DUMPS_PATH,
f"coredumpy_"
f"{time.strftime('%Y%m%d%H%M%S')}_"
f"{frame.f_code.co_name}.dump")
if not os.path.exists(path_):
break
i += 1

kwargs = {
"frame": frame,
"path": path_
}
if description:
kwargs["description"] = description
if path:
kwargs["path"] = path

coredumpy.dump(**kwargs)
except Exception as e:
logger.error(f"保存异常堆栈时发生错误: {repr(e)}\n"
f"{traceback.format_exc()}")
return None

return kwargs["path"]


def bytes_io_to_file(
io_bytes: BytesIO,
file_name: str | None = None,
file_type: str | None = None,
save_dir: str = CACHE_PATH
):
"""
将BytesIO对象保存成文件,并返回路径
Args:
io_bytes: BytesIO对象
file_name: 要保存的文件名,与file_type选一个填即可
file_type: 文件类型(扩展名),与file_name选一个填即可
save_dir: 保存的文件夹

Returns:
保存的文件路径
"""
if not isinstance(io_bytes, BytesIO):
raise TypeError("bytes_io_to_file: 输入类型错误")
if file_name is None:
if file_type is None:
file_type = "cache"
file_name = uuid.uuid4().hex + "." + file_type
if not os.path.exists(save_dir):
os.makedirs(save_dir)

with open(os.path.join(save_dir, file_name), "wb") as f:
f.write(io_bytes.getvalue())
return os.path.join(save_dir, file_name)
4 changes: 4 additions & 0 deletions Lib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
WORK_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_PATH = os.path.join(WORK_PATH, "data")
LOGS_PATH = os.path.join(WORK_PATH, "logs")
DUMPS_PATH = os.path.join(WORK_PATH, "exc_dumps")
PLUGINS_PATH = os.path.join(WORK_PATH, "plugins")
CONFIG_PATH = os.path.join(WORK_PATH, "config.yml")
PLUGIN_CONFIGS_PATH = os.path.join(WORK_PATH, "plugin_configs")
Expand All @@ -22,3 +23,6 @@

if not os.path.exists(LOGS_PATH):
os.makedirs(LOGS_PATH)

if not os.path.exists(DUMPS_PATH):
os.makedirs(DUMPS_PATH)
20 changes: 14 additions & 6 deletions Lib/core/ConfigManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class Api:
"""
host: str
port: int
access_token: str

@dataclasses.dataclass
class Server:
Expand All @@ -127,6 +128,7 @@ class Server:
port: int
server: str
max_works: int
secret: str

@dataclasses.dataclass
class ThreadPool:
Expand All @@ -150,6 +152,7 @@ class Debug:
调试模式,若启用框架的日志等级将被设置为debug,不建议在生产环境开启
"""
enable: bool
save_dump: bool

@dataclasses.dataclass
class AutoRestartOnebot:
Expand All @@ -171,15 +174,17 @@ class Command:
nick_name: "" # 昵称(留空则自动获取)
bot_admin: []

api: # Api设置
api: # Api设置(Onebot HTTP通信)
host: '127.0.0.1'
port: 5700
access_token: "" # HTTP的Access Token,为空则不使用(详见https://github.com/botuniverse/onebot-11/blob/master/communication/authorization.md#http-%E5%92%8C%E6%AD%A3%E5%90%91-websocket)

server: # 监听服务器设置
server: # 监听服务器设置(Onebot HTTP POST通信)
host: '127.0.0.1'
port: 5701
server: 'werkzeug' # 使用的服务器(werkzeug或waitress,使用waitress需先pip install waitress)
max_works: 4 # 最大工作线程数
secret: "" # 上报数据签名密钥(详见https://github.com/botuniverse/onebot-11/blob/master/communication/http-post.md#%E7%AD%BE%E5%90%8D)

thread_pool: # 线程池相关
max_workers: 10 # 线程池最大线程数
Expand All @@ -189,9 +194,9 @@ class Command:
expire_time: 300 # 缓存过期时间(秒)
max_cache_size: 500 # 最大缓存数量(设置过大可能会导致报错)


debug: # 调试模式,若启用框架的日志等级将被设置为debug,不建议在生产环境开启
enable: false # 是否启用调试模式
save_dump: true # 是否在发生异常的同时保存一个dump错误文件(不受debug.enable约束,独立开关,若要使用请先安装coredumpy库,不使用可不安装)

auto_restart_onebot: # 在Onebot实现端状态异常时自动重启Onebot实现端(需开启心跳包)
enable: true # 是否启用自动重启
Expand Down Expand Up @@ -229,13 +234,15 @@ def init(self):
)
self.api = self.Api(
host=self.get("api", {}).get("host", ""),
port=self.get("api", {}).get("port", 5700)
port=self.get("api", {}).get("port", 5700),
access_token=self.get("api", {}).get("access_token", "")
)
self.server = self.Server(
host=self.get("server", {}).get("host", ""),
port=self.get("server", {}).get("port", 5701),
server=self.get("server", {}).get("server", "werkzeug").lower(),
max_works=self.get("server", {}).get("max_works", 4)
max_works=self.get("server", {}).get("max_works", 4),
secret=self.get("server", {}).get("secret", "")
)
self.thread_pool = self.ThreadPool(
max_workers=self.get("thread_pool", {}).get("max_workers", 10)
Expand All @@ -246,7 +253,8 @@ def init(self):
max_cache_size=self.get("qq_data_cache", {}).get("max_cache_size", 500)
)
self.debug = self.Debug(
enable=self.get("debug", {}).get("enable", False)
enable=self.get("debug", {}).get("enable", False),
save_dump=self.get("debug", {}).get("save_dump", True)
)
self.auto_restart_onebot = self.AutoRestartOnebot(
enable=self.get("auto_restart_onebot", {}).get("enable", True)
Expand Down
15 changes: 13 additions & 2 deletions Lib/core/ListenerServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from wsgiref.simple_server import WSGIServer

from flask import Flask, request
import hmac

logger = Logger.get_logger()
app = Flask(__name__)
Expand All @@ -29,6 +30,16 @@ def post_data():
"""
上报处理
"""
if GlobalConfig().server.secret:
sig = hmac.new(GlobalConfig().server.secret.encode("utf-8"), request.get_data(), 'sha1').hexdigest()
try:
received_sig = request.headers['X-Signature'][len('sha1='):]
except KeyError:
logger.warning("收到非法请求(缺少签名),拒绝访问")
return "", 401
if sig != received_sig:
logger.warning("收到非法请求(签名不匹配),拒绝访问")
return "", 401
data = request.get_json()
logger.debug("收到上报: %s" % data)
if "self" in data and GlobalConfig().account.user_id != 0 and data.get("self") != GlobalConfig().account.user_id:
Expand Down Expand Up @@ -83,8 +94,8 @@ def handle(self):


server = ThreadPoolWSGIServer((config.server.host, config.server.port),
app=app,
max_workers=config.server.max_works)
app=app,
max_workers=config.server.max_works)
server.RequestHandlerClass = ThreadPoolWSGIRequestHandler
start_server = lambda: server.serve_forever()
elif config.server.server == "waitress":
Expand Down
20 changes: 16 additions & 4 deletions Lib/core/OnebotAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import requests

from . import EventManager, ConfigManager
from ..common import save_exc_dump
from ..utils import Logger

logger = Logger.get_logger()
Expand Down Expand Up @@ -99,12 +100,16 @@ def get(self, node, data: dict = None, original: bool = None):
logger.debug(f"调用 API: {node} data: {data} by: {traceback.extract_stack()[-3].filename}")
else:
logger.debug(f"调用 API: {node} data: {data} by: {traceback.extract_stack()[-2].filename}")

headers = {
"Content-Type": "application/json"
}
if config.api.access_token:
headers["Authorization"] = f"Bearer {config.api.access_token}"
# 发起get请求
try:
response = requests.post(
url,
headers={"Content-Type": "application/json"},
headers=headers,
data=json.dumps(data if data is not None else {})
)
if response.status_code != 200 or (response.json()['status'] != 'ok' or response.json()['retcode'] != 0):
Expand All @@ -116,8 +121,15 @@ def get(self, node, data: dict = None, original: bool = None):
else:
return response.json()['data']
except Exception as e:
logger.error(f"调用 API: {node} data: {data} 异常: {repr(e)}\n"
f"{traceback.format_exc()}")
if ConfigManager.GlobalConfig().debug.save_dump:
dump_path = save_exc_dump(f"调用 API: {node} data: {data} 异常")
else:
dump_path = None
logger.error(
f"调用 API: {node} data: {data} 异常: {repr(e)}\n"
f"{traceback.format_exc()}"
f"{f"\n已保存异常到 {dump_path}" if dump_path else ""}"
)
raise e

def send_private_msg(self, user_id: int, message: str | list[dict]):
Expand Down
Loading