Files
NeoBot/models/events/factory.py
baby2016 651d982e19 更新/help (#31)
* 滚木

* 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: 更新依赖项

* refactor(handler): 移除TYPE_CHECKING并直接导入Bot类

简化类型注解,直接导入Bot类而非使用TYPE_CHECKING条件导入,提高代码可读性和维护性

* fix(command_manager): 修复插件卸载时元信息移除不精确的问题

修复 CommandManager 中 unload_plugin 方法移除插件元信息时使用 startswith 导致可能误删其他插件的问题,改为精确匹配
同时调整相关测试用例验证精确匹配行为

* refactor: 清理未使用的导入和更新文档结构

docs: 添加config_models.py到项目结构文档
docs: 调整数据目录位置到core/data下
docs: 更新权限管理器文档描述

* 文档更新

* 更新thpic插件 支持一次返回多张图

* feat: 添加测试覆盖率并修复相关问题

refactor(redis_manager): 移除冗余的ConnectionError处理
refactor(event_handler): 优化Bot类型注解
refactor(factory): 移除未使用的GroupCardNoticeEvent

test: 添加全面的单元测试覆盖
- 添加test_import.py测试模块导入
- 添加test_debug.py测试插件加载调试
- 添加test_plugin_error.py测试错误处理
- 添加test_config_loader.py测试配置加载
- 添加test_redis_manager.py测试Redis管理
- 添加test_bot.py测试Bot功能
- 扩展test_models.py测试消息模型
- 添加test_plugin_manager_coverage.py测试插件管理
- 添加test_executor.py测试代码执行器
- 添加test_ws.py测试WebSocket
- 添加test_api.py测试API接口
- 添加test_core_managers.py测试核心管理模块

fix(plugin_manager): 修复插件加载日志变量问题

覆盖率已到达86%(忽略插件)

* 更新/help指令,现在会发送图片

---------

Co-authored-by: K2cr2O1 <2221577113@qq.com>
Co-authored-by: 镀铬酸钾 <148796996+K2cr2O1@users.noreply.github.com>
2026-01-10 20:39:52 +08:00

362 lines
13 KiB
Python

"""
事件工厂模块
用于根据 JSON 数据创建对应的事件对象。
"""
from typing import Any, Dict
from models.message import MessageSegment
from models.sender import Sender
from .base import OneBotEvent, EventType
from .message import GroupMessageEvent, PrivateMessageEvent, Anonymous
from .notice import (
NoticeEvent, FriendAddNoticeEvent, FriendRecallNoticeEvent,
GroupRecallNoticeEvent, GroupIncreaseNoticeEvent,
GroupDecreaseNoticeEvent, GroupAdminNoticeEvent, GroupBanNoticeEvent,
GroupUploadNoticeEvent, GroupUploadFile,
NotifyNoticeEvent, PokeNotifyEvent, LuckyKingNotifyEvent, HonorNotifyEvent,
GroupCardNoticeEvent, OfflineFileNoticeEvent, OfflineFile,
ClientStatusNoticeEvent, ClientStatus, EssenceNoticeEvent
)
from .request import RequestEvent, FriendRequestEvent, GroupRequestEvent
from .meta import MetaEvent, HeartbeatEvent, LifeCycleEvent, HeartbeatStatus
class EventFactory:
"""
事件工厂类
"""
@staticmethod
def create_event(data: Dict[str, Any]) -> OneBotEvent:
"""
根据数据创建事件对象
:param data: 事件数据字典
:return: 对应的事件对象
:raises ValueError: 如果事件类型未知
"""
post_type = data.get("post_type")
# 提取公共字段
common_args = {
"time": data.get("time", 0),
"self_id": data.get("self_id", 0),
}
if post_type == EventType.MESSAGE or post_type == EventType.MESSAGE_SENT:
return EventFactory._create_message_event(data, common_args)
elif post_type == EventType.NOTICE:
return EventFactory._create_notice_event(data, common_args)
elif post_type == EventType.REQUEST:
return EventFactory._create_request_event(data, common_args)
elif post_type == EventType.META:
return EventFactory._create_meta_event(data, common_args)
else:
# 未知类型的事件,抛出异常
raise ValueError(f"Unknown event type: {post_type}")
@staticmethod
def _create_message_event(data: Dict[str, Any], common_args: Dict[str, Any]) -> OneBotEvent:
"""
创建消息事件
:param data: 事件数据
:param common_args: 公共参数
:return: 消息事件对象
"""
message_type = data.get("message_type")
# 解析消息段
message_list = []
raw_message_list = data.get("message", [])
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", {})))
# 解析发送者
sender_data = data.get("sender", {})
sender = Sender(
user_id=sender_data.get("user_id", 0),
nickname=sender_data.get("nickname", ""),
sex=sender_data.get("sex", "unknown"),
age=sender_data.get("age", 0),
card=sender_data.get("card"),
area=sender_data.get("area"),
level=sender_data.get("level"),
role=sender_data.get("role"),
title=sender_data.get("title"),
)
msg_args = {
**common_args,
"message_type": message_type,
"sub_type": data.get("sub_type", ""),
"message_id": data.get("message_id", 0),
"user_id": data.get("user_id", 0),
"message": message_list,
"raw_message": data.get("raw_message", ""),
"font": data.get("font", 0),
"sender": sender,
}
if message_type == "private":
return PrivateMessageEvent(**msg_args)
elif message_type == "group":
anonymous_data = data.get("anonymous")
anonymous = None
if anonymous_data:
anonymous = Anonymous(
id=anonymous_data.get("id", 0),
name=anonymous_data.get("name", ""),
flag=anonymous_data.get("flag", "")
)
return GroupMessageEvent(
**msg_args,
group_id=data.get("group_id", 0),
anonymous=anonymous,
)
else:
# 未知消息类型,抛出异常
raise ValueError(f"Unknown message type: {message_type}")
@staticmethod
def _create_notice_event(data: Dict[str, Any], common_args: Dict[str, Any]) -> OneBotEvent:
"""
创建通知事件
:param data: 事件数据
:param common_args: 公共参数
:return: 通知事件对象
"""
notice_type = data.get("notice_type", "")
# 好友相关通知
if notice_type == "friend_add":
return FriendAddNoticeEvent(
**common_args,
notice_type=notice_type,
user_id=data.get("user_id", 0)
)
elif notice_type == "friend_recall":
return FriendRecallNoticeEvent(
**common_args,
notice_type=notice_type,
user_id=data.get("user_id", 0),
message_id=data.get("message_id", 0)
)
# 群相关通知
elif notice_type == "group_recall":
return GroupRecallNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
operator_id=data.get("operator_id", 0),
message_id=data.get("message_id", 0)
)
elif notice_type == "group_increase":
return GroupIncreaseNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
operator_id=data.get("operator_id", 0),
sub_type=data.get("sub_type", "")
)
elif notice_type == "group_decrease":
return GroupDecreaseNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
operator_id=data.get("operator_id", 0),
sub_type=data.get("sub_type", "")
)
elif notice_type == "group_admin":
return GroupAdminNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
sub_type=data.get("sub_type", "")
)
elif notice_type == "group_ban":
return GroupBanNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
operator_id=data.get("operator_id", 0),
duration=data.get("duration", 0),
sub_type=data.get("sub_type", "")
)
elif notice_type == "group_upload":
file_data = data.get("file", {})
file = GroupUploadFile(
id=file_data.get("id", ""),
name=file_data.get("name", ""),
size=file_data.get("size", 0),
busid=file_data.get("busid", 0)
)
return GroupUploadNoticeEvent(
**common_args,
notice_type=notice_type,
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
file=file
)
elif notice_type == "notify":
sub_type = data.get("sub_type", "")
if sub_type == "poke":
return PokeNotifyEvent(
**common_args,
notice_type=notice_type,
sub_type=sub_type,
user_id=data.get("user_id", 0),
target_id=data.get("target_id", 0),
group_id=data.get("group_id", 0)
)
elif sub_type == "lucky_king":
return LuckyKingNotifyEvent(
**common_args,
notice_type=notice_type,
sub_type=sub_type,
user_id=data.get("user_id", 0),
group_id=data.get("group_id", 0),
target_id=data.get("target_id", 0)
)
elif sub_type == "honor":
return HonorNotifyEvent(
**common_args,
notice_type=notice_type,
sub_type=sub_type,
user_id=data.get("user_id", 0),
group_id=data.get("group_id", 0),
honor_type=data.get("honor_type", "")
)
else:
return NotifyNoticeEvent(
**common_args,
notice_type=notice_type,
sub_type=sub_type,
user_id=data.get("user_id", 0)
)
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", {})
offline_file = OfflineFile(
name=file_data.get("name", ""),
size=file_data.get("size", 0),
url=file_data.get("url", "")
)
return OfflineFileNoticeEvent(
**common_args,
notice_type=notice_type,
user_id=data.get("user_id", 0),
file=offline_file
)
elif notice_type == "client_status":
client_data = data.get("client", {})
client = ClientStatus(
online=client_data.get("online", False),
status=client_data.get("status", "")
)
return ClientStatusNoticeEvent(
**common_args,
notice_type=notice_type,
client=client
)
elif notice_type == "essence":
return EssenceNoticeEvent(
**common_args,
notice_type=notice_type,
sub_type=data.get("sub_type", ""),
group_id=data.get("group_id", 0),
sender_id=data.get("sender_id", 0),
operator_id=data.get("operator_id", 0),
message_id=data.get("message_id", 0)
)
else:
# 未知通知类型,返回基础通知事件
return NoticeEvent(**common_args, notice_type=notice_type)
@staticmethod
def _create_request_event(data: Dict[str, Any], common_args: Dict[str, Any]) -> OneBotEvent:
"""
创建请求事件
:param data: 事件数据
:param common_args: 公共参数
:return: 请求事件对象
"""
request_type = data.get("request_type", "")
if request_type == "friend":
return FriendRequestEvent(
**common_args,
request_type=request_type,
user_id=data.get("user_id", 0),
comment=data.get("comment", ""),
flag=data.get("flag", "")
)
elif request_type == "group":
return GroupRequestEvent(
**common_args,
request_type=request_type,
sub_type=data.get("sub_type", ""),
group_id=data.get("group_id", 0),
user_id=data.get("user_id", 0),
comment=data.get("comment", ""),
flag=data.get("flag", "")
)
else:
# 未知请求类型,返回基础请求事件
return RequestEvent(**common_args, request_type=request_type)
@staticmethod
def _create_meta_event(data: Dict[str, Any], common_args: Dict[str, Any]) -> OneBotEvent:
"""
创建元事件
:param data: 事件数据
:param common_args: 公共参数
:return: 元事件对象
"""
meta_event_type = data.get("meta_event_type", "")
if meta_event_type == "heartbeat":
status_data = data.get("status", {})
status = HeartbeatStatus(
online=status_data.get("online"),
good=status_data.get("good", True)
)
return HeartbeatEvent(
**common_args,
meta_event_type=meta_event_type,
status=status,
interval=data.get("interval", 0)
)
elif meta_event_type == "lifecycle":
return LifeCycleEvent(
**common_args,
meta_event_type=meta_event_type,
sub_type=data.get("sub_type", "")
)
else:
# 未知元事件类型,返回基础元事件
return MetaEvent(**common_args, meta_event_type=meta_event_type)