Dev (#28)
* 滚木 * feat: 重构核心架构,增强类型安全与插件管理 本次提交对核心模块进行了深度重构,引入 Pydantic 增强配置管理的类型安全性,并全面优化了插件管理系统。 主要变更详情: 1. 核心架构与配置 - 重构配置加载模块:引入 Pydantic 模型 (`core/config_models.py`),提供严格的配置项类型检查、验证及默认值管理。 - 统一模块结构:规范化模块导入路径,移除冗余的 `__init__.py` 文件,提升项目结构的清晰度。 - 性能优化:集成 Redis 缓存支持 (`RedisManager`),有效降低高频 API 调用开销,提升响应速度。 2. 插件系统升级 - 实现热重载机制:新增插件文件变更监听功能,支持开发过程中自动重载插件,提升开发效率。 - 优化生命周期管理:改进插件加载与卸载逻辑,支持精确卸载指定插件及其关联的命令、事件处理器和定时任务。 3. 功能特性增强 - 新增媒体 API:引入 `MediaAPI` 模块,封装图片、语音等富媒体资源的获取与处理接口。 - 完善权限体系:重构权限管理系统,实现管理员与操作员的分级控制,支持更细粒度的命令权限校验。 4. 代码质量与稳定性 - 全面类型修复:解决 `mypy` 静态类型检查发现的大量类型错误(包括 `CommandManager`、`EventFactory` 及 `Bot` API 签名不匹配问题)。 - 增强错误处理:优化消息处理管道的异常捕获机制,完善关键路径的日志记录,提升系统运行稳定性。 * feat: 添加测试用例并优化代码结构 refactor(permission_manager): 调整初始化顺序和逻辑 fix(admin_manager): 修复初始化逻辑和目录创建问题 feat(ws): 优化Bot实例初始化条件 feat(message): 增强MessageSegment功能并添加测试 feat(events): 支持字符串格式的消息解析 test: 添加核心功能测试用例 refactor(plugin_manager): 改进插件路径处理 style: 清理无用导入和代码 chore: 更新依赖项
This commit is contained in:
@@ -1,97 +1,23 @@
|
||||
from .events.base import OneBotEvent
|
||||
from .events.factory import EventFactory
|
||||
from .events.message import (
|
||||
GroupMessageEvent,
|
||||
MessageEvent,
|
||||
MessageSegment,
|
||||
PrivateMessageEvent,
|
||||
)
|
||||
from .events.meta import HeartbeatEvent, HeartbeatStatus, LifeCycleEvent, MetaEvent
|
||||
from .events.notice import (
|
||||
ClientStatus,
|
||||
ClientStatusNoticeEvent,
|
||||
EssenceNoticeEvent,
|
||||
FriendAddNoticeEvent,
|
||||
FriendRecallNoticeEvent,
|
||||
GroupAdminNoticeEvent,
|
||||
GroupBanNoticeEvent,
|
||||
GroupCardNoticeEvent,
|
||||
GroupDecreaseNoticeEvent,
|
||||
GroupIncreaseNoticeEvent,
|
||||
GroupRecallNoticeEvent,
|
||||
GroupUploadFile,
|
||||
GroupUploadNoticeEvent,
|
||||
HonorNotifyEvent,
|
||||
LuckyKingNotifyEvent,
|
||||
NoticeEvent,
|
||||
NotifyNoticeEvent,
|
||||
OfflineFile,
|
||||
OfflineFileNoticeEvent,
|
||||
PokeNotifyEvent,
|
||||
)
|
||||
from .events.request import FriendRequestEvent, GroupRequestEvent, RequestEvent
|
||||
from .objects import (
|
||||
CurrentTalkative,
|
||||
EssenceMessage,
|
||||
FriendInfo,
|
||||
GroupHonorInfo,
|
||||
GroupInfo,
|
||||
GroupMemberInfo,
|
||||
HonorInfo,
|
||||
LoginInfo,
|
||||
Status,
|
||||
StrangerInfo,
|
||||
VersionInfo,
|
||||
)
|
||||
"""
|
||||
Models 包
|
||||
|
||||
# Alias for backward compatibility
|
||||
Event = OneBotEvent
|
||||
导出常用的模型类,方便插件导入。
|
||||
"""
|
||||
|
||||
from .events.base import OneBotEvent
|
||||
from .events.message import MessageEvent, GroupMessageEvent, PrivateMessageEvent
|
||||
from .events.notice import NoticeEvent
|
||||
from .events.request import RequestEvent
|
||||
from .message import MessageSegment
|
||||
from .sender import Sender
|
||||
|
||||
__all__ = [
|
||||
"OneBotEvent",
|
||||
"MessageEvent",
|
||||
"GroupMessageEvent",
|
||||
"PrivateMessageEvent",
|
||||
"NoticeEvent",
|
||||
"RequestEvent",
|
||||
"MessageSegment",
|
||||
"Sender",
|
||||
"OneBotEvent",
|
||||
"Event",
|
||||
"MessageEvent",
|
||||
"PrivateMessageEvent",
|
||||
"GroupMessageEvent",
|
||||
"NoticeEvent",
|
||||
"FriendAddNoticeEvent",
|
||||
"FriendRecallNoticeEvent",
|
||||
"GroupRecallNoticeEvent",
|
||||
"GroupIncreaseNoticeEvent",
|
||||
"GroupDecreaseNoticeEvent",
|
||||
"GroupAdminNoticeEvent",
|
||||
"GroupBanNoticeEvent",
|
||||
"GroupUploadNoticeEvent",
|
||||
"GroupUploadFile",
|
||||
"NotifyNoticeEvent",
|
||||
"PokeNotifyEvent",
|
||||
"LuckyKingNotifyEvent",
|
||||
"HonorNotifyEvent",
|
||||
"GroupCardNoticeEvent",
|
||||
"OfflineFileNoticeEvent",
|
||||
"OfflineFile",
|
||||
"ClientStatusNoticeEvent",
|
||||
"ClientStatus",
|
||||
"EssenceNoticeEvent",
|
||||
"RequestEvent",
|
||||
"FriendRequestEvent",
|
||||
"GroupRequestEvent",
|
||||
"MetaEvent",
|
||||
"HeartbeatEvent",
|
||||
"LifeCycleEvent",
|
||||
"HeartbeatStatus",
|
||||
"EventFactory",
|
||||
"GroupInfo",
|
||||
"GroupMemberInfo",
|
||||
"FriendInfo",
|
||||
"StrangerInfo",
|
||||
"LoginInfo",
|
||||
"VersionInfo",
|
||||
"Status",
|
||||
"EssenceMessage",
|
||||
"GroupHonorInfo",
|
||||
"CurrentTalkative",
|
||||
"HonorInfo",
|
||||
]
|
||||
|
||||
@@ -70,7 +70,11 @@ class EventFactory:
|
||||
# 解析消息段
|
||||
message_list = []
|
||||
raw_message_list = data.get("message", [])
|
||||
if isinstance(raw_message_list, list):
|
||||
|
||||
if isinstance(raw_message_list, str):
|
||||
# 如果消息是字符串,将其视为纯文本消息段
|
||||
message_list.append(MessageSegment.text(raw_message_list))
|
||||
elif isinstance(raw_message_list, list):
|
||||
for item in raw_message_list:
|
||||
if isinstance(item, dict):
|
||||
message_list.append(MessageSegment(type=item.get("type", ""), data=item.get("data", {})))
|
||||
@@ -252,9 +256,18 @@ class EventFactory:
|
||||
card_new=data.get("card_new", ""),
|
||||
card_old=data.get("card_old", "")
|
||||
)
|
||||
elif notice_type == "group_card":
|
||||
return GroupCardNoticeEvent(
|
||||
**common_args,
|
||||
notice_type=notice_type,
|
||||
group_id=data.get("group_id", 0),
|
||||
user_id=data.get("user_id", 0),
|
||||
card_new=data.get("card_new", ""),
|
||||
card_old=data.get("card_old", "")
|
||||
)
|
||||
elif notice_type == "offline_file":
|
||||
file_data = data.get("file", {})
|
||||
file = OfflineFile(
|
||||
offline_file = OfflineFile(
|
||||
name=file_data.get("name", ""),
|
||||
size=file_data.get("size", 0),
|
||||
url=file_data.get("url", "")
|
||||
@@ -263,7 +276,7 @@ class EventFactory:
|
||||
**common_args,
|
||||
notice_type=notice_type,
|
||||
user_id=data.get("user_id", 0),
|
||||
file=file
|
||||
file=offline_file
|
||||
)
|
||||
elif notice_type == "client_status":
|
||||
client_data = data.get("client", {})
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
定义了消息相关的事件类,包括 MessageEvent, PrivateMessageEvent, GroupMessageEvent。
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from core.managers.permission_manager import ADMIN, OP, USER
|
||||
from core.permission import Permission
|
||||
from models.message import MessageSegment
|
||||
from models.sender import Sender
|
||||
from .base import OneBotEvent, EventType
|
||||
@@ -34,9 +34,9 @@ class MessageEvent(OneBotEvent):
|
||||
"""
|
||||
|
||||
# 权限级别常量,用于装饰器参数
|
||||
ADMIN = ADMIN
|
||||
OP = OP
|
||||
USER = USER
|
||||
ADMIN = Permission.ADMIN
|
||||
OP = Permission.OP
|
||||
USER = Permission.USER
|
||||
|
||||
message_type: str
|
||||
"""消息类型: private (私聊), group (群聊)"""
|
||||
@@ -70,7 +70,7 @@ class MessageEvent(OneBotEvent):
|
||||
def post_type(self) -> str:
|
||||
return EventType.MESSAGE
|
||||
|
||||
async def reply(self, message: str, auto_escape: bool = False):
|
||||
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||
"""
|
||||
回复消息(抽象方法,由子类实现)
|
||||
|
||||
@@ -86,7 +86,7 @@ class PrivateMessageEvent(MessageEvent):
|
||||
私聊消息事件
|
||||
"""
|
||||
|
||||
async def reply(self, message: str, auto_escape: bool = False):
|
||||
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||
"""
|
||||
回复私聊消息
|
||||
|
||||
@@ -110,7 +110,7 @@ class GroupMessageEvent(MessageEvent):
|
||||
anonymous: Optional[Anonymous] = None
|
||||
"""匿名信息"""
|
||||
|
||||
async def reply(self, message: str, auto_escape: bool = False):
|
||||
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||
"""
|
||||
回复群聊消息
|
||||
|
||||
|
||||
@@ -63,5 +63,5 @@ class LifeCycleEvent(MetaEvent):
|
||||
meta_event_type: str = 'lifecycle'
|
||||
"""元事件类型:生命周期事件"""
|
||||
|
||||
sub_type: LifeCycleSubType = LifeCycleSubType.ENABLE
|
||||
sub_type: str = LifeCycleSubType.ENABLE
|
||||
"""子类型:启用、禁用、连接"""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -23,7 +23,7 @@ class MessageSegment:
|
||||
data: Dict[str, Any]
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
def plain_text(self) -> str:
|
||||
"""
|
||||
当消息段类型为 'text' 时,快速获取其文本内容。
|
||||
|
||||
@@ -32,6 +32,19 @@ class MessageSegment:
|
||||
"""
|
||||
return self.data.get("text", "") if self.type == "text" else ""
|
||||
|
||||
@staticmethod
|
||||
def text(text: str) -> "MessageSegment":
|
||||
"""
|
||||
创建一个文本消息段。
|
||||
|
||||
Args:
|
||||
text (str): 文本内容。
|
||||
|
||||
Returns:
|
||||
MessageSegment: 一个类型为 'text' 的消息段对象。
|
||||
"""
|
||||
return MessageSegment(type="text", data={"text": text})
|
||||
|
||||
@property
|
||||
def image_url(self) -> str:
|
||||
"""
|
||||
@@ -76,7 +89,7 @@ class MessageSegment:
|
||||
return self.data.get("file", "")
|
||||
return ""
|
||||
|
||||
def is_at(self, user_id: int = None) -> bool:
|
||||
def is_at(self, user_id: Optional[int] = None) -> bool:
|
||||
"""
|
||||
检查当前消息段是否是一个 'at' (提及) 消息段。
|
||||
|
||||
@@ -93,16 +106,52 @@ class MessageSegment:
|
||||
return True
|
||||
return str(self.data.get("qq")) == str(user_id)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
返回消息段的 CQ 码字符串表示。
|
||||
"""
|
||||
if self.type == "text":
|
||||
return self.data.get("text", "")
|
||||
|
||||
params = ",".join([f"{k}={v}" for k, v in self.data.items()])
|
||||
if params:
|
||||
return f"[CQ:{self.type},{params}]"
|
||||
return f"[CQ:{self.type}]"
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
返回消息段对象的字符串表示形式,便于调试。
|
||||
"""
|
||||
return f"[MS:{self.type}:{self.data}]"
|
||||
|
||||
def __add__(self, other: Any) -> "List[MessageSegment]":
|
||||
"""
|
||||
支持消息段相加,返回消息段列表。
|
||||
"""
|
||||
if isinstance(other, MessageSegment):
|
||||
return [self, other]
|
||||
elif isinstance(other, str):
|
||||
return [self, MessageSegment.text(other)]
|
||||
elif isinstance(other, list):
|
||||
return [self] + other
|
||||
return NotImplemented
|
||||
|
||||
def __radd__(self, other: Any) -> "List[MessageSegment]":
|
||||
"""
|
||||
支持反向相加。
|
||||
"""
|
||||
if isinstance(other, MessageSegment):
|
||||
return [other, self]
|
||||
elif isinstance(other, str):
|
||||
return [MessageSegment.text(other), self]
|
||||
elif isinstance(other, list):
|
||||
return other + [self]
|
||||
return NotImplemented
|
||||
|
||||
# --- 快捷构造方法 ---
|
||||
|
||||
@staticmethod
|
||||
def text(text: str) -> "MessageSegment": # noqa: F811
|
||||
def from_text(text: str) -> "MessageSegment":
|
||||
"""
|
||||
创建一个文本消息段。
|
||||
|
||||
@@ -115,7 +164,7 @@ class MessageSegment:
|
||||
return MessageSegment(type="text", data={"text": text})
|
||||
|
||||
@staticmethod
|
||||
def at(user_id: int | str, name: str = None) -> "MessageSegment":
|
||||
def at(user_id: int | str, name: Optional[str] = None) -> "MessageSegment":
|
||||
"""
|
||||
创建一个 @某人 的消息段。
|
||||
|
||||
@@ -132,7 +181,7 @@ class MessageSegment:
|
||||
return MessageSegment(type="at", data=data)
|
||||
|
||||
@staticmethod
|
||||
def image(file: str, image_type: str = None, cache: bool = True, proxy: bool = True, timeout: int = None, sub_type: int = None) -> "MessageSegment":
|
||||
def image(file: str, image_type: Optional[str] = None, cache: bool = True, proxy: bool = True, timeout: Optional[int] = None, sub_type: Optional[int] = None) -> "MessageSegment":
|
||||
"""
|
||||
创建一个图片消息段。
|
||||
|
||||
@@ -194,7 +243,7 @@ class MessageSegment:
|
||||
"""
|
||||
return MessageSegment(type="xml", data={"data": data})
|
||||
@staticmethod
|
||||
def share(url: str, title: str, content: str = None, image: str = None) -> "MessageSegment":
|
||||
def share(url: str, title: str, content: Optional[str] = None, image: Optional[str] = None) -> "MessageSegment":
|
||||
"""
|
||||
创建一个分享消息段。
|
||||
|
||||
@@ -227,7 +276,7 @@ class MessageSegment:
|
||||
"""
|
||||
return MessageSegment(type="music", data={"type": type, "id": id})
|
||||
@staticmethod
|
||||
def music_custom(url: str, audio: str, title: str, content: str = None, image: str = None) -> "MessageSegment":
|
||||
def music_custom(url: str, audio: str, title: str, content: Optional[str] = None, image: Optional[str] = None) -> "MessageSegment":
|
||||
"""
|
||||
创建一个自定义音乐消息段。
|
||||
|
||||
@@ -248,7 +297,7 @@ class MessageSegment:
|
||||
data["image"] = image
|
||||
return MessageSegment(type="music", data={"type": "custom", **data})
|
||||
@staticmethod
|
||||
def record(file: str, magic: bool = False, cache: bool = True, proxy: bool = True, timeout: int = None) -> "MessageSegment":
|
||||
def record(file: str, magic: bool = False, cache: bool = True, proxy: bool = True, timeout: Optional[int] = None) -> "MessageSegment":
|
||||
"""
|
||||
创建一个语音消息段。
|
||||
|
||||
@@ -267,7 +316,7 @@ class MessageSegment:
|
||||
data["timeout"] = str(timeout)
|
||||
return MessageSegment(type="record", data=data)
|
||||
@staticmethod
|
||||
def video(file: str, cover: str = None, c: int = 2) -> "MessageSegment":
|
||||
def video(file: str, cover: Optional[str] = None, c: int = 2) -> "MessageSegment":
|
||||
"""
|
||||
创建一个视频消息段。
|
||||
|
||||
@@ -297,17 +346,17 @@ class MessageSegment:
|
||||
return MessageSegment(type="file", data={"file": file})
|
||||
|
||||
@staticmethod
|
||||
def reply(message_id: str) -> "MessageSegment":
|
||||
def reply(message_id: str | int) -> "MessageSegment":
|
||||
"""
|
||||
创建一个回复消息段。
|
||||
|
||||
Args:
|
||||
message_id (str): 被回复的消息 ID。
|
||||
message_id (str | int): 被回复的消息 ID。
|
||||
|
||||
Returns:
|
||||
MessageSegment: 一个类型为 'reply' 的消息段对象。
|
||||
"""
|
||||
return MessageSegment(type="reply", data={"id": message_id})
|
||||
return MessageSegment(type="reply", data={"id": str(message_id)})
|
||||
|
||||
@staticmethod
|
||||
def rps() -> "MessageSegment":
|
||||
|
||||
Reference in New Issue
Block a user