From 36305f226c2f8610b1bce3738864c1e0f2923055 Mon Sep 17 00:00:00 2001 From: Oct-autumn Date: Wed, 4 Jun 2025 21:16:06 +0800 Subject: [PATCH 01/10] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84config?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.py | 93 ---------------------- src/config/__init__.py | 5 ++ src/config/config.py | 140 +++++++++++++++++++++++++++++++++ src/config/config_base.py | 128 ++++++++++++++++++++++++++++++ src/config/official_configs.py | 77 ++++++++++++++++++ src/logger.py | 2 +- src/mmc_com_layer.py | 4 +- src/recv_handler.py | 34 ++++---- src/response_pool.py | 4 +- src/send_handler.py | 2 +- template/template_config.toml | 35 +++++---- 11 files changed, 392 insertions(+), 132 deletions(-) delete mode 100644 src/config.py create mode 100644 src/config/__init__.py create mode 100644 src/config/config.py create mode 100644 src/config/config_base.py create mode 100644 src/config/official_configs.py diff --git a/src/config.py b/src/config.py deleted file mode 100644 index ee13c98..0000000 --- a/src/config.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import sys -import tomli -import shutil -from .logger import logger -from typing import Optional - - -class Config: - platform: str = "qq" - nickname: Optional[str] = None - server_host: str = "localhost" - server_port: int = 8095 - napcat_heartbeat_interval: int = 30 - - def __init__(self): - self._get_config_path() - - def _get_config_path(self): - current_file_path = os.path.abspath(__file__) - src_path = os.path.dirname(current_file_path) - self.root_path = os.path.join(src_path, "..") - self.config_path = os.path.join(self.root_path, "config.toml") - - def load_config(self): # sourcery skip: extract-method, move-assign - include_configs = ["Napcat_Server", "MaiBot_Server", "Chat", "Voice", "Debug"] - if not os.path.exists(self.config_path): - logger.error("配置文件不存在!") - logger.info("正在创建配置文件...") - shutil.copy( - os.path.join(self.root_path, "template", "template_config.toml"), - os.path.join(self.root_path, "config.toml"), - ) - logger.info("配置文件创建成功,请修改配置文件后重启程序。") - sys.exit(1) - with open(self.config_path, "rb") as f: - try: - raw_config = tomli.load(f) - except tomli.TOMLDecodeError as e: - logger.critical(f"配置文件bot_config.toml填写有误,请检查第{e.lineno}行第{e.colno}处:{e.msg}") - sys.exit(1) - for key in include_configs: - if key not in raw_config: - logger.error(f"配置文件中缺少必需的字段: '{key}'") - logger.error("你的配置文件可能过时,请尝试手动更新配置文件。") - sys.exit(1) - - self.server_host = raw_config["Napcat_Server"].get("host", "localhost") - self.server_port = raw_config["Napcat_Server"].get("port", 8095) - self.napcat_heartbeat_interval = raw_config["Napcat_Server"].get("heartbeat", 30) - - self.mai_host = raw_config["MaiBot_Server"].get("host", "localhost") - self.mai_port = raw_config["MaiBot_Server"].get("port", 8000) - self.platform = raw_config["MaiBot_Server"].get("platform_name") - if not self.platform: - logger.critical("请在配置文件中指定平台") - sys.exit(1) - - self.group_list_type: str = raw_config["Chat"].get("group_list_type") - self.group_list: list = raw_config["Chat"].get("group_list", []) - self.private_list_type: str = raw_config["Chat"].get("private_list_type") - self.private_list: list = raw_config["Chat"].get("private_list", []) - self.ban_user_id: list = raw_config["Chat"].get("ban_user_id", []) - self.enable_poke: bool = raw_config["Chat"].get("enable_poke", True) - if self.group_list_type not in ["whitelist", "blacklist"]: - logger.critical("请在配置文件中指定group_list_type或group_list_type填写错误") - sys.exit(1) - if self.private_list_type not in ["whitelist", "blacklist"]: - logger.critical("请在配置文件中指定private_list_type或private_list_type填写错误") - sys.exit(1) - - self.use_tts = raw_config["Voice"].get("use_tts", False) - - self.debug_level = raw_config["Debug"].get("level", "INFO") - if self.debug_level == "DEBUG": - logger.debug("原始配置文件内容:") - logger.debug(raw_config) - logger.debug("读取到的配置内容:") - logger.debug(f"平台: {self.platform}") - logger.debug(f"MaiBot服务器地址: {self.mai_host}:{self.mai_port}") - logger.debug(f"Napcat服务器地址: {self.server_host}:{self.server_port}") - logger.debug(f"心跳间隔: {self.napcat_heartbeat_interval}秒") - logger.debug(f"群聊列表类型: {self.group_list_type}") - logger.debug(f"群聊列表: {self.group_list}") - logger.debug(f"私聊列表类型: {self.private_list_type}") - logger.debug(f"私聊列表: {self.private_list}") - logger.debug(f"禁用用户ID列表: {self.ban_user_id}") - logger.debug(f"是否启用TTS: {self.use_tts}") - logger.debug(f"调试级别: {self.debug_level}") - - -global_config = Config() -global_config.load_config() diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..40ba89a --- /dev/null +++ b/src/config/__init__.py @@ -0,0 +1,5 @@ +from .config import global_config + +__all__ = [ + "global_config", +] diff --git a/src/config/config.py b/src/config/config.py new file mode 100644 index 0000000..a219078 --- /dev/null +++ b/src/config/config.py @@ -0,0 +1,140 @@ +import os +from dataclasses import dataclass + +import tomlkit +import shutil + +from tomlkit import TOMLDocument +from tomlkit.items import Table +from ..logger import logger +from rich.traceback import install + +from src.config.config_base import ConfigBase +from src.config.official_configs import ( + ChatConfig, + DebugConfig, + MaiBotServerConfig, + NapcatServerConfig, + NicknameConfig, + VoiceConfig, +) + +install(extra_lines=3) + +TEMPLATE_DIR = "template" + + +def update_config(): + # 定义文件路径 + template_path = f"{TEMPLATE_DIR}/template_config.toml" + old_config_path = "config.toml" + new_config_path = "config.toml" + + # 检查配置文件是否存在 + if not os.path.exists(old_config_path): + logger.info("配置文件不存在,从模板创建新配置") + shutil.copy2(template_path, old_config_path) # 复制模板文件 + logger.info(f"已创建新配置文件,请填写后重新运行: {old_config_path}") + # 如果是新创建的配置文件,直接返回 + quit() + + # 读取旧配置文件和模板文件 + with open(old_config_path, "r", encoding="utf-8") as f: + old_config = tomlkit.load(f) + with open(template_path, "r", encoding="utf-8") as f: + new_config = tomlkit.load(f) + + # 检查version是否相同 + if old_config and "inner" in old_config and "inner" in new_config: + old_version = old_config["inner"].get("version") + new_version = new_config["inner"].get("version") + if old_version and new_version and old_version == new_version: + logger.info(f"检测到配置文件版本号相同 (v{old_version}),跳过更新") + return + else: + logger.info(f"检测到版本号不同: 旧版本 v{old_version} -> 新版本 v{new_version}") + else: + logger.info("已有配置文件未检测到版本号,可能是旧版本。将进行更新") + + # 备份文件名 + old_backup_path = "config.toml.back" + + # 备份旧配置文件 + shutil.move(old_config_path, old_backup_path) + logger.info(f"已备份旧配置文件到: {old_backup_path}") + + # 复制模板文件到配置目录 + shutil.copy2(template_path, new_config_path) + logger.info(f"已创建新配置文件: {new_config_path}") + + def update_dict(target: TOMLDocument | dict, source: TOMLDocument | dict): + """ + 将source字典的值更新到target字典中(如果target中存在相同的键) + """ + for key, value in source.items(): + # 跳过version字段的更新 + if key == "version": + continue + if key in target: + if isinstance(value, dict) and isinstance(target[key], (dict, Table)): + update_dict(target[key], value) + else: + try: + # 对数组类型进行特殊处理 + if isinstance(value, list): + # 如果是空数组,确保它保持为空数组 + target[key] = tomlkit.array(str(value)) if value else tomlkit.array() + else: + # 其他类型使用item方法创建新值 + target[key] = tomlkit.item(value) + except (TypeError, ValueError): + # 如果转换失败,直接赋值 + target[key] = value + + # 将旧配置的值更新到新配置中 + logger.info("开始合并新旧配置...") + update_dict(new_config, old_config) + + # 保存更新后的配置(保留注释和格式) + with open(new_config_path, "w", encoding="utf-8") as f: + f.write(tomlkit.dumps(new_config)) + logger.info("配置文件更新完成,建议检查新配置文件中的内容,以免丢失重要信息") + quit() + + +@dataclass +class Config(ConfigBase): + """总配置类""" + + nickname: NicknameConfig + napcat_server: NapcatServerConfig + maibot_server: MaiBotServerConfig + chat: ChatConfig + voice: VoiceConfig + debug: DebugConfig + + +def load_config(config_path: str) -> Config: + """ + 加载配置文件 + :param config_path: 配置文件路径 + :return: Config对象 + """ + # 读取配置文件 + with open(config_path, "r", encoding="utf-8") as f: + config_data = tomlkit.load(f) + + # 创建Config对象 + try: + return Config.from_dict(config_data) + except Exception as e: + logger.critical("配置文件解析失败") + raise e + + +# 更新配置 +update_config() + +logger.info("正在品鉴配置文件...") +global_config = load_config(config_path="config.toml") +logger.info("非常的新鲜,非常的美味!") diff --git a/src/config/config_base.py b/src/config/config_base.py new file mode 100644 index 0000000..fbd3dd9 --- /dev/null +++ b/src/config/config_base.py @@ -0,0 +1,128 @@ +from dataclasses import dataclass, fields, MISSING +from typing import TypeVar, Type, Any, get_origin, get_args, Literal + +T = TypeVar("T", bound="ConfigBase") + +TOML_DICT_TYPE = { + int, + float, + str, + bool, + list, + dict, +} + + +@dataclass +class ConfigBase: + """配置类的基类""" + + @classmethod + def from_dict(cls: Type[T], data: dict[str, Any]) -> T: + """从字典加载配置字段""" + if not isinstance(data, dict): + raise TypeError(f"Expected a dictionary, got {type(data).__name__}") + + init_args: dict[str, Any] = {} + + for f in fields(cls): + field_name = f.name + + if field_name.startswith("_"): + # 跳过以 _ 开头的字段 + continue + + if field_name not in data: + if f.default is not MISSING or f.default_factory is not MISSING: + # 跳过未提供且有默认值/默认构造方法的字段 + continue + else: + raise ValueError(f"Missing required field: '{field_name}'") + + value = data[field_name] + field_type = f.type + + try: + init_args[field_name] = cls._convert_field(value, field_type) + except TypeError as e: + raise TypeError(f"Field '{field_name}' has a type error: {e}") from e + except Exception as e: + raise RuntimeError(f"Failed to convert field '{field_name}' to target type: {e}") from e + + return cls(**init_args) + + @classmethod + def _convert_field(cls, value: Any, field_type: Type[Any]) -> Any: + """ + 转换字段值为指定类型 + + 1. 对于嵌套的 dataclass,递归调用相应的 from_dict 方法 + 2. 对于泛型集合类型(list, set, tuple),递归转换每个元素 + 3. 对于基础类型(int, str, float, bool),直接转换 + 4. 对于其他类型,尝试直接转换,如果失败则抛出异常 + """ + + # 如果是嵌套的 dataclass,递归调用 from_dict 方法 + if isinstance(field_type, type) and issubclass(field_type, ConfigBase): + if not isinstance(value, dict): + raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") + return field_type.from_dict(value) + + # 处理泛型集合类型(list, set, tuple) + field_origin_type = get_origin(field_type) + field_type_args = get_args(field_type) + + if field_origin_type in {list, set, tuple}: + # 检查提供的value是否为list + if not isinstance(value, list): + raise TypeError(f"Expected an list for {field_type.__name__}, got {type(value).__name__}") + + if field_origin_type is list: + return [cls._convert_field(item, field_type_args[0]) for item in value] + elif field_origin_type is set: + return {cls._convert_field(item, field_type_args[0]) for item in value} + elif field_origin_type is tuple: + # 检查提供的value长度是否与类型参数一致 + if len(value) != len(field_type_args): + raise TypeError( + f"Expected {len(field_type_args)} items for {field_type.__name__}, got {len(value)}" + ) + return tuple(cls._convert_field(item, arg) for item, arg in zip(value, field_type_args)) + + if field_origin_type is dict: + # 检查提供的value是否为dict + if not isinstance(value, dict): + raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") + + # 检查字典的键值类型 + if len(field_type_args) != 2: + raise TypeError(f"Expected a dictionary with two type arguments for {field_type.__name__}") + key_type, value_type = field_type_args + + return {cls._convert_field(k, key_type): cls._convert_field(v, value_type) for k, v in value.items()} + + # 处理基础类型,例如 int, str 等 + if field_origin_type is type(None) and value is None: # 处理Optional类型 + return None + + # 处理Literal类型 + if field_origin_type is Literal or get_origin(field_type) is Literal: + # 获取Literal的允许值 + allowed_values = get_args(field_type) + if value in allowed_values: + return value + else: + raise TypeError(f"Value '{value}' is not in allowed values {allowed_values} for Literal type") + + if field_type is Any or isinstance(value, field_type): + return value + + # 其他类型,尝试直接转换 + try: + return field_type(value) + except (ValueError, TypeError) as e: + raise TypeError(f"Cannot convert {type(value).__name__} to {field_type.__name__}") from e + + def __str__(self): + """返回配置类的字符串表示""" + return f"{self.__class__.__name__}({', '.join(f'{f.name}={getattr(self, f.name)}' for f in fields(self))})" diff --git a/src/config/official_configs.py b/src/config/official_configs.py new file mode 100644 index 0000000..3119ffb --- /dev/null +++ b/src/config/official_configs.py @@ -0,0 +1,77 @@ +from dataclasses import dataclass, field +from typing import Literal + +from src.config.config_base import ConfigBase + +""" +须知: +1. 本文件中记录了所有的配置项 +2. 所有新增的class都需要继承自ConfigBase +3. 所有新增的class都应在config.py中的Config类中添加字段 +4. 对于新增的字段,若为可选项,则应在其后添加field()并设置default_factory或default +""" + +ADAPTER_PLATFORM = "qq" + + +@dataclass +class NicknameConfig(ConfigBase): + nickname: str + """机器人昵称""" + + +@dataclass +class NapcatServerConfig(ConfigBase): + host: str = "localhost" + """Napcat服务端的主机地址""" + + port: int = 8095 + """Napcat服务端的端口号""" + + heartbeat_interval: int = 30 + """Napcat心跳间隔时间,单位为秒""" + + +@dataclass +class MaiBotServerConfig(ConfigBase): + platform_name: str = field(default=ADAPTER_PLATFORM, init=False) + """平台名称,“qq”""" + + host: str = "localhost" + """MaiMCore的主机地址""" + + port: int = 8000 + """MaiMCore的端口号""" + + +@dataclass +class ChatConfig(ConfigBase): + group_list_type: Literal["whitelist", "blacklist"] = "whitelist" + """群聊列表类型 白名单/黑名单""" + + group_list: list[str] = field(default_factory=[]) + """群聊列表""" + + private_list_type: Literal["whitelist", "blacklist"] = "whitelist" + """私聊列表类型 白名单/黑名单""" + + private_list: list[str] = field(default_factory=[]) + """私聊列表""" + + ban_user_id: list[str] = field(default_factory=[]) + """被封禁的用户ID列表,封禁后将无法与其进行交互""" + + enable_poke: bool = True + """是否启用戳一戳功能""" + + +@dataclass +class VoiceConfig(ConfigBase): + use_tts: bool = False + """是否启用TTS功能""" + + +@dataclass +class DebugConfig(ConfigBase): + level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" + """日志级别,默认为INFO""" diff --git a/src/logger.py b/src/logger.py index 3acba4f..8071ff7 100644 --- a/src/logger.py +++ b/src/logger.py @@ -5,6 +5,6 @@ import sys logger.remove() logger.add( sys.stderr, - level=global_config.debug_level, + level=global_config.debug.level, format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", ) diff --git a/src/mmc_com_layer.py b/src/mmc_com_layer.py index 174ef1f..ab50cca 100644 --- a/src/mmc_com_layer.py +++ b/src/mmc_com_layer.py @@ -5,8 +5,8 @@ from .send_handler import send_handler route_config = RouteConfig( route_config={ - global_config.platform: TargetConfig( - url=f"ws://{global_config.mai_host}:{global_config.mai_port}/ws", + global_config.maibot_server.platform_name: TargetConfig( + url=f"ws://{global_config.maibot_server.host}:{global_config.maibot_server.port}/ws", token=None, ) } diff --git a/src/recv_handler.py b/src/recv_handler.py index 7e031ec..21ff56f 100644 --- a/src/recv_handler.py +++ b/src/recv_handler.py @@ -36,7 +36,7 @@ class RecvHandler: def __init__(self): self.server_connection: Server.ServerConnection = None - self.interval = global_config.napcat_heartbeat_interval + self.interval = global_config.napcat_server.heartbeat_interval async def handle_meta_event(self, message: dict) -> None: event_type = message.get("meta_event_type") @@ -77,20 +77,20 @@ class RecvHandler: """ logger.debug(f"群聊id: {group_id}, 用户id: {user_id}") if group_id: - if global_config.group_list_type == "whitelist" and group_id not in global_config.group_list: + if global_config.chat.group_list_type == "whitelist" and group_id not in global_config.chat.group_list: logger.warning("群聊不在聊天白名单中,消息被丢弃") return False - elif global_config.group_list_type == "blacklist" and group_id in global_config.group_list: + elif global_config.chat.group_list_type == "blacklist" and group_id in global_config.chat.group_list: logger.warning("群聊在聊天黑名单中,消息被丢弃") return False else: - if global_config.private_list_type == "whitelist" and user_id not in global_config.private_list: + if global_config.chat.private_list_type == "whitelist" and user_id not in global_config.chat.private_list: logger.warning("私聊不在聊天白名单中,消息被丢弃") return False - elif global_config.private_list_type == "blacklist" and user_id in global_config.private_list: + elif global_config.chat.private_list_type == "blacklist" and user_id in global_config.chat.private_list: logger.warning("私聊在聊天黑名单中,消息被丢弃") return False - if user_id in global_config.ban_user_id: + if user_id in global_config.chat.ban_user_id: logger.warning("用户在全局黑名单中,消息被丢弃") return False return True @@ -123,7 +123,7 @@ class RecvHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -149,7 +149,7 @@ class RecvHandler: nickname = fetched_member_info.get("nickname") if fetched_member_info else None # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, user_id=sender_info.get("user_id"), user_nickname=nickname, user_cardname=None, @@ -164,7 +164,7 @@ class RecvHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -182,7 +182,7 @@ class RecvHandler: # 发送者用户信息 user_info: UserInfo = UserInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, user_id=sender_info.get("user_id"), user_nickname=sender_info.get("nickname"), user_cardname=sender_info.get("card"), @@ -195,7 +195,7 @@ class RecvHandler: group_name = fetched_group_info.get("group_name") group_info: GroupInfo = GroupInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, group_id=raw_message.get("group_id"), group_name=group_name, ) @@ -205,12 +205,12 @@ class RecvHandler: return None additional_config: dict = {} - if global_config.use_tts: + if global_config.voice.use_tts: additional_config["allow_tts"] = True # 消息信息 message_info: BaseMessageInfo = BaseMessageInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, message_id=message_id, time=message_time, user_info=user_info, @@ -500,7 +500,7 @@ class RecvHandler: sub_type = raw_message.get("sub_type") match sub_type: case NoticeType.Notify.poke: - if global_config.enable_poke: + if global_config.chat.enable_poke: handled_message: Seg = await self.handle_poke_notify(raw_message) else: logger.warning("戳一戳消息被禁用,取消戳一戳处理") @@ -532,7 +532,7 @@ class RecvHandler: source_name = "QQ用户" user_info: UserInfo = UserInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, user_id=user_id, user_nickname=source_name, user_cardname=source_cardname, @@ -547,13 +547,13 @@ class RecvHandler: else: logger.warning("无法获取戳一戳消息所在群的名称") group_info = GroupInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, group_id=group_id, group_name=group_name, ) message_info: BaseMessageInfo = BaseMessageInfo( - platform=global_config.platform, + platform=global_config.maibot_server.platform_name, message_id="notice", time=message_time, user_info=user_info, diff --git a/src/response_pool.py b/src/response_pool.py index 66ded1d..c41ed7f 100644 --- a/src/response_pool.py +++ b/src/response_pool.py @@ -35,10 +35,10 @@ async def check_timeout_response() -> None: cleaned_message_count: int = 0 now_time = time.time() for echo_id, response_time in list(response_time_dict.items()): - if now_time - response_time > global_config.napcat_heartbeat_interval: + if now_time - response_time > global_config.napcat_server.heartbeat_interval: cleaned_message_count += 1 response_dict.pop(echo_id) response_time_dict.pop(echo_id) logger.warning(f"响应消息 {echo_id} 超时,已删除") logger.info(f"已删除 {cleaned_message_count} 条超时响应消息") - await asyncio.sleep(global_config.napcat_heartbeat_interval) + await asyncio.sleep(global_config.napcat_server.heartbeat_interval) diff --git a/src/send_handler.py b/src/send_handler.py index baf6fe7..74646b6 100644 --- a/src/send_handler.py +++ b/src/send_handler.py @@ -209,7 +209,7 @@ class SendHandler: def handle_voice_message(self, encoded_voice: str) -> dict: """处理语音消息""" - if not global_config.use_tts: + if not global_config.voice.use_tts: logger.warning("未启用语音消息处理") return {} if not encoded_voice: diff --git a/template/template_config.toml b/template/template_config.toml index 1d0d830..b4cdce0 100644 --- a/template/template_config.toml +++ b/template/template_config.toml @@ -1,30 +1,33 @@ -[Nickname] # 现在没用 +[inner] +version = "0.1.0" # 版本号 +# 请勿修改版本号,除非你知道自己在做什么 + +[nickname] # 现在没用 nickname = "" -[Napcat_Server] # Napcat连接的ws服务设置 -host = "localhost" # Napcat设定的主机地址 -port = 8095 # Napcat设定的端口 -heartbeat = 30 # 与Napcat设置的心跳相同(按秒计) +[napcat_server] # Napcat连接的ws服务设置 +host = "localhost" # Napcat设定的主机地址 +port = 8095 # Napcat设定的端口 +heartbeat_interval = 30 # 与Napcat设置的心跳相同(按秒计) -[MaiBot_Server] # 连接麦麦的ws服务设置 -platform_name = "qq" # 标识adapter的名称(必填) -host = "localhost" # 麦麦在.env文件中设置的主机地址,即HOST字段 -port = 8000 # 麦麦在.env文件中设置的端口,即PORT字段 +[maibot_server] # 连接麦麦的ws服务设置 +host = "localhost" # 麦麦在.env文件中设置的主机地址,即HOST字段 +port = 8000 # 麦麦在.env文件中设置的端口,即PORT字段 -[Chat] # 黑白名单功能 +[chat] # 黑白名单功能 group_list_type = "whitelist" # 群组名单类型,可选为:whitelist, blacklist -group_list = [] # 群组名单 +group_list = [] # 群组名单 # 当group_list_type为whitelist时,只有群组名单中的群组可以聊天 # 当group_list_type为blacklist时,群组名单中的任何群组无法聊天 private_list_type = "whitelist" # 私聊名单类型,可选为:whitelist, blacklist -private_list = [] # 私聊名单 +private_list = [] # 私聊名单 # 当private_list_type为whitelist时,只有私聊名单中的用户可以聊天 # 当private_list_type为blacklist时,私聊名单中的任何用户无法聊天 -ban_user_id = [] # 全局禁止名单(全局禁止名单中的用户无法进行任何聊天) +ban_user_id = [] # 全局禁止名单(全局禁止名单中的用户无法进行任何聊天) enable_poke = true # 是否启用戳一戳功能 -[Voice] # 发送语音设置 +[voice] # 发送语音设置 use_tts = false # 是否使用tts语音(请确保你配置了tts并有对应的adapter) -[Debug] -level = "INFO" # 日志等级(DEBUG, INFO, WARNING, ERROR) +[debug] +level = "INFO" # 日志等级(DEBUG, INFO, WARNING, ERROR, CRITICAL) From d64670a930b9e81614cc3de3d77a1aaac3043e20 Mon Sep 17 00:00:00 2001 From: Oct-autumn Date: Wed, 4 Jun 2025 23:34:11 +0800 Subject: [PATCH 02/10] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E4=B8=8A?= =?UTF-8?q?=E4=B8=AA=E6=8F=90=E4=BA=A4=E6=BC=8F=E6=8E=89=E7=9A=84=E5=87=A0?= =?UTF-8?q?=E5=A4=84global=5Fconfig=E7=9A=84=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + main.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 6d6652c..267374b 100644 --- a/.gitignore +++ b/.gitignore @@ -270,4 +270,5 @@ $RECYCLE.BIN/ *.lnk config.toml +config.toml.back test \ No newline at end of file diff --git a/main.py b/main.py index 50cf968..c3c6cdb 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ async def message_recv(server_connection: Server.ServerConnection): async for raw_message in server_connection: logger.debug( f"{raw_message[:100]}..." - if (len(raw_message) > 100 and global_config.debug_level != "DEBUG") + if (len(raw_message) > 100 and global_config.debug.level != "DEBUG") else raw_message ) decoded_raw_message: dict = json.loads(raw_message) @@ -52,8 +52,10 @@ async def main(): async def napcat_server(): logger.info("正在启动adapter...") - async with Server.serve(message_recv, global_config.server_host, global_config.server_port) as server: - logger.info(f"Adapter已启动,监听地址: ws://{global_config.server_host}:{global_config.server_port}") + async with Server.serve(message_recv, global_config.napcat_server.host, global_config.napcat_server.port) as server: + logger.info( + f"Adapter已启动,监听地址: ws://{global_config.napcat_server.host}:{global_config.napcat_server.port}" + ) await server.serve_forever() From 81a71af4aa1af278fdf1cc74dc4f4b05dd773783 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sun, 15 Jun 2025 16:40:25 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E4=BF=AE=E5=A4=8DConfig=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=B2=A1=E8=BD=AC=E6=8D=A2=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 16 +++++---- src/config/config_base.py | 66 +++++++++++++++++++--------------- src/config/official_configs.py | 6 ++-- src/recv_handler.py | 2 +- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/main.py b/main.py index 50cf968..2c71ef1 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ async def message_recv(server_connection: Server.ServerConnection): async for raw_message in server_connection: logger.debug( f"{raw_message[:100]}..." - if (len(raw_message) > 100 and global_config.debug_level != "DEBUG") + if (len(raw_message) > 100 and global_config.debug.level != "DEBUG") else raw_message ) decoded_raw_message: dict = json.loads(raw_message) @@ -52,19 +52,23 @@ async def main(): async def napcat_server(): logger.info("正在启动adapter...") - async with Server.serve(message_recv, global_config.server_host, global_config.server_port) as server: - logger.info(f"Adapter已启动,监听地址: ws://{global_config.server_host}:{global_config.server_port}") + async with Server.serve(message_recv, global_config.napcat_server.host, global_config.napcat_server.port) as server: + logger.info( + f"Adapter已启动,监听地址: ws://{global_config.napcat_server.host}:{global_config.napcat_server.port}" + ) await server.serve_forever() async def graceful_shutdown(): try: logger.info("正在关闭adapter...") - await mmc_stop_com() tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] for task in tasks: - task.cancel() - await asyncio.gather(*tasks, return_exceptions=True) + if not task.done(): + task.cancel() + await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), 15) + await mmc_stop_com() # 后置避免神秘exception + logger.info("Adapter已成功关闭") except Exception as e: logger.error(f"Adapter关闭中出现错误: {e}") diff --git a/src/config/config_base.py b/src/config/config_base.py index fbd3dd9..518f99c 100644 --- a/src/config/config_base.py +++ b/src/config/config_base.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, fields, MISSING -from typing import TypeVar, Type, Any, get_origin, get_args, Literal +from typing import TypeVar, Type, Any, get_origin, get_args, Literal, Dict, List, Set, Tuple, Union T = TypeVar("T", bound="ConfigBase") @@ -18,16 +18,16 @@ class ConfigBase: """配置类的基类""" @classmethod - def from_dict(cls: Type[T], data: dict[str, Any]) -> T: + def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: """从字典加载配置字段""" if not isinstance(data, dict): raise TypeError(f"Expected a dictionary, got {type(data).__name__}") - init_args: dict[str, Any] = {} + init_args: Dict[str, Any] = {} for f in fields(cls): field_name = f.name - + field_type = f.type if field_name.startswith("_"): # 跳过以 _ 开头的字段 continue @@ -40,14 +40,12 @@ class ConfigBase: raise ValueError(f"Missing required field: '{field_name}'") value = data[field_name] - field_type = f.type - try: init_args[field_name] = cls._convert_field(value, field_type) except TypeError as e: - raise TypeError(f"Field '{field_name}' has a type error: {e}") from e + raise TypeError(f"字段 '{field_name}' 出现类型错误: {e}") from e except Exception as e: - raise RuntimeError(f"Failed to convert field '{field_name}' to target type: {e}") from e + raise RuntimeError(f"无法将字段 '{field_name}' 转换为目标类型,出现错误: {e}") from e return cls(**init_args) @@ -61,33 +59,30 @@ class ConfigBase: 3. 对于基础类型(int, str, float, bool),直接转换 4. 对于其他类型,尝试直接转换,如果失败则抛出异常 """ - # 如果是嵌套的 dataclass,递归调用 from_dict 方法 if isinstance(field_type, type) and issubclass(field_type, ConfigBase): - if not isinstance(value, dict): - raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") return field_type.from_dict(value) - # 处理泛型集合类型(list, set, tuple) field_origin_type = get_origin(field_type) - field_type_args = get_args(field_type) + field_args_type = get_args(field_type) + # 处理泛型集合类型(list, set, tuple) if field_origin_type in {list, set, tuple}: # 检查提供的value是否为list if not isinstance(value, list): raise TypeError(f"Expected an list for {field_type.__name__}, got {type(value).__name__}") if field_origin_type is list: - return [cls._convert_field(item, field_type_args[0]) for item in value] - elif field_origin_type is set: - return {cls._convert_field(item, field_type_args[0]) for item in value} - elif field_origin_type is tuple: + return [cls._convert_field(item, field_args_type[0]) for item in value] + if field_origin_type is set: + return {cls._convert_field(item, field_args_type[0]) for item in value} + if field_origin_type is tuple: # 检查提供的value长度是否与类型参数一致 - if len(value) != len(field_type_args): + if len(value) != len(field_args_type): raise TypeError( - f"Expected {len(field_type_args)} items for {field_type.__name__}, got {len(value)}" + f"Expected {len(field_args_type)} items for {field_type.__name__}, got {len(value)}" ) - return tuple(cls._convert_field(item, arg) for item, arg in zip(value, field_type_args)) + return tuple(cls._convert_field(item, arg_type) for item, arg_type in zip(value, field_args_type)) if field_origin_type is dict: # 检查提供的value是否为dict @@ -95,18 +90,30 @@ class ConfigBase: raise TypeError(f"Expected a dictionary for {field_type.__name__}, got {type(value).__name__}") # 检查字典的键值类型 - if len(field_type_args) != 2: + if len(field_args_type) != 2: raise TypeError(f"Expected a dictionary with two type arguments for {field_type.__name__}") - key_type, value_type = field_type_args + key_type, value_type = field_args_type return {cls._convert_field(k, key_type): cls._convert_field(v, value_type) for k, v in value.items()} - # 处理基础类型,例如 int, str 等 - if field_origin_type is type(None) and value is None: # 处理Optional类型 - return None + # 处理Optional类型 + if field_origin_type is Union: # assert get_origin(Optional[Any]) is Union + if value is None: + return None + # 如果有数据,检查实际类型 + if type(value) not in field_args_type: + raise TypeError(f"Expected {field_args_type} for {field_type.__name__}, got {type(value).__name__}") + return cls._convert_field(value, field_args_type[0]) + + # 处理int, str, float, bool等基础类型 + if field_origin_type is None: + if isinstance(value, field_type): + return field_type(value) + else: + raise TypeError(f"Expected {field_type.__name__}, got {type(value).__name__}") # 处理Literal类型 - if field_origin_type is Literal or get_origin(field_type) is Literal: + if field_origin_type is Literal: # 获取Literal的允许值 allowed_values = get_args(field_type) if value in allowed_values: @@ -114,14 +121,15 @@ class ConfigBase: else: raise TypeError(f"Value '{value}' is not in allowed values {allowed_values} for Literal type") - if field_type is Any or isinstance(value, field_type): + # 处理其他类型 + if field_type is Any: return value - # 其他类型,尝试直接转换 + # 其他类型直接转换 try: return field_type(value) except (ValueError, TypeError) as e: - raise TypeError(f"Cannot convert {type(value).__name__} to {field_type.__name__}") from e + raise TypeError(f"无法将 {type(value).__name__} 转换为 {field_type.__name__}") from e def __str__(self): """返回配置类的字符串表示""" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 3119ffb..d8928a8 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -49,16 +49,16 @@ class ChatConfig(ConfigBase): group_list_type: Literal["whitelist", "blacklist"] = "whitelist" """群聊列表类型 白名单/黑名单""" - group_list: list[str] = field(default_factory=[]) + group_list: list[int] = field(default_factory=[]) """群聊列表""" private_list_type: Literal["whitelist", "blacklist"] = "whitelist" """私聊列表类型 白名单/黑名单""" - private_list: list[str] = field(default_factory=[]) + private_list: list[int] = field(default_factory=[]) """私聊列表""" - ban_user_id: list[str] = field(default_factory=[]) + ban_user_id: list[int] = field(default_factory=[]) """被封禁的用户ID列表,封禁后将无法与其进行交互""" enable_poke: bool = True diff --git a/src/recv_handler.py b/src/recv_handler.py index 21ff56f..7cc0a07 100644 --- a/src/recv_handler.py +++ b/src/recv_handler.py @@ -697,7 +697,7 @@ class RecvHandler: user_nickname: str = sender_info.get("nickname", "QQ用户") user_nickname_str = f"【{user_nickname}】:" break_seg = Seg(type="text", data="\n") - message_of_sub_message_list: dict = sub_message.get("message") + message_of_sub_message_list: List[Dict[str, Any]] = sub_message.get("message") if not message_of_sub_message_list: logger.warning("转发消息内容为空") continue From e1ab7b69568988fe7a881ecb911af7536e9321a6 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sun, 15 Jun 2025 16:46:13 +0800 Subject: [PATCH 04/10] ruff --- src/config/config_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config_base.py b/src/config/config_base.py index 518f99c..87cb079 100644 --- a/src/config/config_base.py +++ b/src/config/config_base.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, fields, MISSING -from typing import TypeVar, Type, Any, get_origin, get_args, Literal, Dict, List, Set, Tuple, Union +from typing import TypeVar, Type, Any, get_origin, get_args, Literal, Dict, Union T = TypeVar("T", bound="ConfigBase") From d72082989af57ec74119153bd6dac04935b79034 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 17 Jun 2025 16:31:27 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E6=97=B6=E5=B0=9A=E5=B0=8F=E5=9E=83?= =?UTF-8?q?=E5=9C=BE=E4=B9=8B=E7=B3=BB=E7=BB=9F=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/maimai.ico | Bin 0 -> 67715 bytes src/recv_handler.py | 23 ++++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 assets/maimai.ico diff --git a/assets/maimai.ico b/assets/maimai.ico new file mode 100644 index 0000000000000000000000000000000000000000..578b11cdd0c88dc8f590668a718303d939754f67 GIT binary patch literal 67715 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

9q=wX1$>M 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⁢M5M6l!#t$4Iq9u3gs}Q_@_7Zo*5SE}Y^vaGH+ky#ka*McEbX+p0En4|w zJv}*^)jKx$3xBcXbAs;$lLegB94I^%4(4Ouyaezy35Mg#AvWc?FabI#9RRVY@CO%g zs}v_U?N`&dk`R-gHo65uH*zg+J*qrTrN`5mr53jZbD}tat?24fkzu^;2mptc&n~-_ zu#^AlBbnTt0Lm2rKaN#nbCt5NL(?6ybTSRCZX?ApIte1csD2uNdkRBi8{Syz4cj(0fpLIRiJR}Qvl-hQIH#R|C<~$G)%PvALv*kF zf7x~UCq6U%lf8qnPb3y{a;|xBn)QL14cY^GR&wqDhP%*REGgA+Q>6EJzYHd%hg@d%epovrA(zKoXD#1j!?nP<6J4aKuF1)oJ{(KtipOC= z&Nh_*+uMPoRF9YJ_V3qq7bqAFJKb?Y6R7yGkc0k}uM7Zrkh1`Bf585`+oiFTgos}k zltM1oOD-tSbKOpb^rY`0?t=cECd}qSuykTn}IkC^lS}6s8BGT&9JLS3nQ@6c8 z>tI0mv)k^(>T{VY6FrveuS_ z;`HF3-`|6OXhv8Z4oh2DNG?9nmyg=O)3DCa0k4zIo{m9)d0FzMJL{r$p}pX)J)q(m z97J{IHnj9o(xU)?S3O*k1%yCY0roc?fLFskXiICXpl$Z1hS(su0v+$Nnk2sj8aqc_ zOxAVe$e`MBtD{t<^mHA*VZ4&WU>+f(!f{Ya#x%I)b|< zveT>*`6GmugX9Riw(iTBzi_u%cE%tdHf_k<1_xh;UMQoZYk>67R41XXc&_1$1041G z=4eo;$b|rTe+iZ7G+UGv-^U`Z=2{anE?1Y0;t&+qi3nw~HT|T?DMlxC?xqF2OG>4- zS737imSE+7rYgG+1XNMYuFpB z_yB9c@Cu0uWXZ*VOj4OEM-Hjx2;J-(n^p(4P0O6~{@!M}gq`@_2d6XWngeOxVLfrw zj)!8MQrH;2{mqw&lbe?nc(*PNya4MrBwN3{+3sYW#>H0Qxu91>kjsK!RO2ILSS{8QS@p5�Mr;^wE$g-l5zBebV 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 None: event_type = message.get("meta_event_type") @@ -49,6 +52,8 @@ class RecvHandler: asyncio.create_task(self.check_heartbeat(self_id)) elif event_type == MetaEventType.heartbeat: if message["status"].get("online") and message["status"].get("good"): + if not self._interval_checking: + asyncio.create_task(self.check_heartbeat()) self.last_heart_beat = time.time() self.interval = message.get("interval") / 1000 else: @@ -56,10 +61,20 @@ class RecvHandler: logger.warning(f"Bot {self_id} Napcat 端异常!") async def check_heartbeat(self, id: int) -> None: + self._interval_checking = True while True: now_time = time.time() - if now_time - self.last_heart_beat > self.interval + 3: - logger.warning(f"Bot {id} 连接已断开") + if now_time - self.last_heart_beat > self.interval * 2: + logger.error(f"Bot {id} 连接已断开,被下线,或者Napcat卡死!") + current_dir = os.path.dirname(__file__) + icon_path = os.path.join(current_dir, "..", "assets", "maimai.ico") + notification.notify( + title="警告", + message=f"Bot {id} 连接已断开,被下线,或者Napcat卡死!", + app_name="MaiBot Napcat Adapter", + timeout=10, + app_icon=icon_path, + ) break else: logger.debug("心跳正常") @@ -769,7 +784,9 @@ class RecvHandler: async def message_process(self, message_base: MessageBase) -> None: try: - await self.maibot_router.send_message(message_base) + send_status = await self.maibot_router.send_message(message_base) + if not send_status: + raise RuntimeError("发送消息失败,可能是路由未正确配置或连接异常") except Exception as e: logger.error(f"发送消息失败: {str(e)}") logger.error("请检查与MaiBot之间的连接") From 79ef02f19378696dde49ccbba89af2821b67ac9b Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 17 Jun 2025 16:31:56 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E5=8F=88=E5=BF=98=E4=BA=86ruff=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/recv_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recv_handler.py b/src/recv_handler.py index 6189009..7b11fbd 100644 --- a/src/recv_handler.py +++ b/src/recv_handler.py @@ -7,7 +7,7 @@ import json import websockets as Server from typing import List, Tuple, Optional, Dict, Any import uuid -from plyer import notification, facades +from plyer import notification import os from . import MetaEventType, RealMessageType, MessageType, NoticeType From 51cbb2b227c9056255ca8d576710464adbe83441 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Tue, 17 Jun 2025 16:33:04 +0800 Subject: [PATCH 07/10] =?UTF-8?q?requirements.txt=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 41e8eb1..e5964ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ requests maim_message loguru pillow -tomli \ No newline at end of file +tomli +plyer \ No newline at end of file From b5e7316b948305161da2a2224e31af15b4bb6833 Mon Sep 17 00:00:00 2001 From: A0000Xz <122650088+A0000Xz@users.noreply.github.com> Date: Sun, 22 Jun 2025 01:59:42 +0800 Subject: [PATCH 08/10] Add files via upload --- src/__init__.py | 155 +++++------ src/send_handler.py | 655 +++++++++++++++++++++++--------------------- 2 files changed, 420 insertions(+), 390 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index a35ff0e..e12aede 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,77 +1,78 @@ -from enum import Enum - - -class MetaEventType: - lifecycle = "lifecycle" # 生命周期 - - class Lifecycle: - connect = "connect" # 生命周期 - WebSocket 连接成功 - - heartbeat = "heartbeat" # 心跳 - - -class MessageType: # 接受消息大类 - private = "private" # 私聊消息 - - class Private: - friend = "friend" # 私聊消息 - 好友 - group = "group" # 私聊消息 - 群临时 - group_self = "group_self" # 私聊消息 - 群中自身发送 - other = "other" # 私聊消息 - 其他 - - group = "group" # 群聊消息 - - class Group: - normal = "normal" # 群聊消息 - 普通 - anonymous = "anonymous" # 群聊消息 - 匿名消息 - notice = "notice" # 群聊消息 - 系统提示 - - -class NoticeType: # 通知事件 - friend_recall = "friend_recall" # 私聊消息撤回 - group_recall = "group_recall" # 群聊消息撤回 - notify = "notify" - - class Notify: - poke = "poke" # 戳一戳 - - -class RealMessageType: # 实际消息分类 - text = "text" # 纯文本 - face = "face" # qq表情 - image = "image" # 图片 - record = "record" # 语音 - video = "video" # 视频 - at = "at" # @某人 - rps = "rps" # 猜拳魔法表情 - dice = "dice" # 骰子 - shake = "shake" # 私聊窗口抖动(只收) - poke = "poke" # 群聊戳一戳 - share = "share" # 链接分享(json形式) - reply = "reply" # 回复消息 - forward = "forward" # 转发消息 - node = "node" # 转发消息节点 - - -class MessageSentType: - private = "private" - - class Private: - friend = "friend" - group = "group" - - group = "group" - - class Group: - normal = "normal" - - -class CommandType(Enum): - """命令类型""" - - GROUP_BAN = "set_group_ban" # 禁言用户 - GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 - GROUP_KICK = "set_group_kick" # 踢出群聊 - - def __str__(self) -> str: - return self.value +from enum import Enum + + +class MetaEventType: + lifecycle = "lifecycle" # 生命周期 + + class Lifecycle: + connect = "connect" # 生命周期 - WebSocket 连接成功 + + heartbeat = "heartbeat" # 心跳 + + +class MessageType: # 接受消息大类 + private = "private" # 私聊消息 + + class Private: + friend = "friend" # 私聊消息 - 好友 + group = "group" # 私聊消息 - 群临时 + group_self = "group_self" # 私聊消息 - 群中自身发送 + other = "other" # 私聊消息 - 其他 + + group = "group" # 群聊消息 + + class Group: + normal = "normal" # 群聊消息 - 普通 + anonymous = "anonymous" # 群聊消息 - 匿名消息 + notice = "notice" # 群聊消息 - 系统提示 + + +class NoticeType: # 通知事件 + friend_recall = "friend_recall" # 私聊消息撤回 + group_recall = "group_recall" # 群聊消息撤回 + notify = "notify" + + class Notify: + poke = "poke" # 戳一戳 + + +class RealMessageType: # 实际消息分类 + text = "text" # 纯文本 + face = "face" # qq表情 + image = "image" # 图片 + record = "record" # 语音 + video = "video" # 视频 + at = "at" # @某人 + rps = "rps" # 猜拳魔法表情 + dice = "dice" # 骰子 + shake = "shake" # 私聊窗口抖动(只收) + poke = "poke" # 群聊戳一戳 + share = "share" # 链接分享(json形式) + reply = "reply" # 回复消息 + forward = "forward" # 转发消息 + node = "node" # 转发消息节点 + + +class MessageSentType: + private = "private" + + class Private: + friend = "friend" + group = "group" + + group = "group" + + class Group: + normal = "normal" + + +class CommandType(Enum): + """命令类型""" + + GROUP_BAN = "set_group_ban" # 禁言用户 + GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 + GROUP_KICK = "set_group_kick" # 踢出群聊 + SEND_POKE = "send_poke" # 戳一戳 + + def __str__(self) -> str: + return self.value diff --git a/src/send_handler.py b/src/send_handler.py index 74646b6..8aae094 100644 --- a/src/send_handler.py +++ b/src/send_handler.py @@ -1,313 +1,342 @@ -import json -import websockets as Server -import uuid -from maim_message import ( - UserInfo, - GroupInfo, - Seg, - BaseMessageInfo, - MessageBase, -) -from typing import Dict, Any, Tuple - -from . import CommandType -from .config import global_config -from .response_pool import get_response -from .logger import logger -from .utils import get_image_format, convert_image_to_gif - - -class SendHandler: - def __init__(self): - self.server_connection: Server.ServerConnection = None - - async def handle_message(self, raw_message_base_dict: dict) -> None: - raw_message_base: MessageBase = MessageBase.from_dict(raw_message_base_dict) - message_segment: Seg = raw_message_base.message_segment - logger.info("接收到来自MaiBot的消息,处理中") - if message_segment.type == "command": - return await self.send_command(raw_message_base) - else: - return await self.send_normal_message(raw_message_base) - - async def send_normal_message(self, raw_message_base: MessageBase) -> None: - """ - 处理普通消息发送 - """ - logger.info("处理普通信息中") - message_info: BaseMessageInfo = raw_message_base.message_info - message_segment: Seg = raw_message_base.message_segment - group_info: GroupInfo = message_info.group_info - user_info: UserInfo = message_info.user_info - target_id: int = None - action: str = None - id_name: str = None - processed_message: list = [] - try: - processed_message = await self.handle_seg_recursive(message_segment) - except Exception as e: - logger.error(f"处理消息时发生错误: {e}") - return - - if not processed_message: - logger.critical("现在暂时不支持解析此回复!") - return None - - if group_info and user_info: - logger.debug("发送群聊消息") - target_id = group_info.group_id - action = "send_group_msg" - id_name = "group_id" - elif user_info: - logger.debug("发送私聊消息") - target_id = user_info.user_id - action = "send_private_msg" - id_name = "user_id" - else: - logger.error("无法识别的消息类型") - return - logger.info("尝试发送到napcat") - response = await self.send_message_to_napcat( - action, - { - id_name: target_id, - "message": processed_message, - }, - ) - if response.get("status") == "ok": - logger.info("消息发送成功") - else: - logger.warning(f"消息发送失败,napcat返回:{str(response)}") - - async def send_command(self, raw_message_base: MessageBase) -> None: - """ - 处理命令类 - """ - logger.info("处理命令中") - message_info: BaseMessageInfo = raw_message_base.message_info - message_segment: Seg = raw_message_base.message_segment - group_info: GroupInfo = message_info.group_info - seg_data: Dict[str, Any] = message_segment.data - command_name: str = seg_data.get("name") - try: - match command_name: - case CommandType.GROUP_BAN.name: - command, args_dict = self.handle_ban_command(seg_data.get("args"), group_info) - case CommandType.GROUP_WHOLE_BAN.name: - command, args_dict = self.handle_whole_ban_command(seg_data.get("args"), group_info) - case CommandType.GROUP_KICK.name: - command, args_dict = self.handle_kick_command(seg_data.get("args"), group_info) - case _: - logger.error(f"未知命令: {command_name}") - return - except Exception as e: - logger.error(f"处理命令时发生错误: {e}") - return None - - if not command or not args_dict: - logger.error("命令或参数缺失") - return None - - response = await self.send_message_to_napcat(command, args_dict) - if response.get("status") == "ok": - logger.info(f"命令 {command_name} 执行成功") - else: - logger.warning(f"命令 {command_name} 执行失败,napcat返回:{str(response)}") - - def get_level(self, seg_data: Seg) -> int: - if seg_data.type == "seglist": - return 1 + max(self.get_level(seg) for seg in seg_data.data) - else: - return 1 - - async def handle_seg_recursive(self, seg_data: Seg) -> list: - payload: list = [] - if seg_data.type == "seglist": - # level = self.get_level(seg_data) # 给以后可能的多层嵌套做准备,此处不使用 - if not seg_data.data: - return [] - for seg in seg_data.data: - payload = self.process_message_by_type(seg, payload) - else: - payload = self.process_message_by_type(seg_data, payload) - return payload - - def process_message_by_type(self, seg: Seg, payload: list) -> list: - # sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression - new_payload = payload - if seg.type == "reply": - target_id = seg.data - if target_id == "notice": - return payload - new_payload = self.build_payload(payload, self.handle_reply_message(target_id), True) - elif seg.type == "text": - text = seg.data - if not text: - return payload - new_payload = self.build_payload(payload, self.handle_text_message(text), False) - elif seg.type == "face": - logger.warning("MaiBot 发送了qq原生表情,暂时不支持") - elif seg.type == "image": - image = seg.data - new_payload = self.build_payload(payload, self.handle_image_message(image), False) - elif seg.type == "emoji": - emoji = seg.data - new_payload = self.build_payload(payload, self.handle_emoji_message(emoji), False) - elif seg.type == "voice": - voice = seg.data - new_payload = self.build_payload(payload, self.handle_voice_message(voice), False) - return new_payload - - def build_payload(self, payload: list, addon: dict, is_reply: bool = False) -> list: - # sourcery skip: for-append-to-extend, merge-list-append, simplify-generator - """构建发送的消息体""" - if is_reply: - temp_list = [] - temp_list.append(addon) - for i in payload: - if i.get("type") == "reply": - logger.debug("检测到多个回复,使用最新的回复") - continue - temp_list.append(i) - return temp_list - else: - payload.append(addon) - return payload - - def handle_reply_message(self, id: str) -> dict: - """处理回复消息""" - return {"type": "reply", "data": {"id": id}} - - def handle_text_message(self, message: str) -> dict: - """处理文本消息""" - return {"type": "text", "data": {"text": message}} - - def handle_image_message(self, encoded_image: str) -> dict: - """处理图片消息""" - return { - "type": "image", - "data": { - "file": f"base64://{encoded_image}", - "subtype": 0, - }, - } # base64 编码的图片 - - def handle_emoji_message(self, encoded_emoji: str) -> dict: - """处理表情消息""" - encoded_image = encoded_emoji - image_format = get_image_format(encoded_emoji) - if image_format != "gif": - encoded_image = convert_image_to_gif(encoded_emoji) - return { - "type": "image", - "data": { - "file": f"base64://{encoded_image}", - "subtype": 1, - "summary": "[动画表情]", - }, - } - - def handle_voice_message(self, encoded_voice: str) -> dict: - """处理语音消息""" - if not global_config.voice.use_tts: - logger.warning("未启用语音消息处理") - return {} - if not encoded_voice: - return {} - return { - "type": "record", - "data": {"file": f"base64://{encoded_voice}"}, - } - - def handle_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理封禁命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - duration: int = int(args["duration"]) - user_id: int = int(args["qq_id"]) - group_id: int = int(group_info.group_id) - if duration <= 0: - raise ValueError("封禁时间必须大于0") - if not user_id or not group_id: - raise ValueError("封禁命令缺少必要参数") - if duration > 2592000: - raise ValueError("封禁时间不能超过30天") - return ( - CommandType.GROUP_BAN.value, - { - "group_id": group_id, - "user_id": user_id, - "duration": duration, - }, - ) - - def handle_whole_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理全体禁言命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - enable = args["enable"] - assert isinstance(enable, bool), "enable参数必须是布尔值" - group_id: int = int(group_info.group_id) - if group_id <= 0: - raise ValueError("群组ID无效") - return ( - CommandType.GROUP_WHOLE_BAN.value, - { - "group_id": group_id, - "enable": enable, - }, - ) - - def handle_kick_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: - """处理群成员踢出命令 - - Args: - args (Dict[str, Any]): 参数字典 - group_info (GroupInfo): 群聊信息(对应目标群聊) - - Returns: - Tuple[CommandType, Dict[str, Any]] - """ - user_id: int = int(args["qq_id"]) - group_id: int = int(group_info.group_id) - if group_id <= 0: - raise ValueError("群组ID无效") - if user_id <= 0: - raise ValueError("用户ID无效") - return ( - CommandType.GROUP_KICK.value, - { - "group_id": group_id, - "user_id": user_id, - "reject_add_request": False, # 不拒绝加群请求 - }, - ) - - async def send_message_to_napcat(self, action: str, params: dict) -> dict: - request_uuid = str(uuid.uuid4()) - payload = json.dumps({"action": action, "params": params, "echo": request_uuid}) - await self.server_connection.send(payload) - try: - response = await get_response(request_uuid) - except TimeoutError: - logger.error("发送消息超时,未收到响应") - return {"status": "error", "message": "timeout"} - except Exception as e: - logger.error(f"发送消息失败: {e}") - return {"status": "error", "message": str(e)} - return response - - -send_handler = SendHandler() +import json +import websockets as Server +import uuid +from maim_message import ( + UserInfo, + GroupInfo, + Seg, + BaseMessageInfo, + MessageBase, +) +from typing import Dict, Any, Tuple + +from . import CommandType +from .config import global_config +from .response_pool import get_response +from .logger import logger +from .utils import get_image_format, convert_image_to_gif + + +class SendHandler: + def __init__(self): + self.server_connection: Server.ServerConnection = None + + async def handle_message(self, raw_message_base_dict: dict) -> None: + raw_message_base: MessageBase = MessageBase.from_dict(raw_message_base_dict) + message_segment: Seg = raw_message_base.message_segment + logger.info("接收到来自MaiBot的消息,处理中") + if message_segment.type == "command": + return await self.send_command(raw_message_base) + else: + return await self.send_normal_message(raw_message_base) + + async def send_normal_message(self, raw_message_base: MessageBase) -> None: + """ + 处理普通消息发送 + """ + logger.info("处理普通信息中") + message_info: BaseMessageInfo = raw_message_base.message_info + message_segment: Seg = raw_message_base.message_segment + group_info: GroupInfo = message_info.group_info + user_info: UserInfo = message_info.user_info + target_id: int = None + action: str = None + id_name: str = None + processed_message: list = [] + try: + processed_message = await self.handle_seg_recursive(message_segment) + except Exception as e: + logger.error(f"处理消息时发生错误: {e}") + return + + if not processed_message: + logger.critical("现在暂时不支持解析此回复!") + return None + + if group_info and user_info: + logger.debug("发送群聊消息") + target_id = group_info.group_id + action = "send_group_msg" + id_name = "group_id" + elif user_info: + logger.debug("发送私聊消息") + target_id = user_info.user_id + action = "send_private_msg" + id_name = "user_id" + else: + logger.error("无法识别的消息类型") + return + logger.info("尝试发送到napcat") + response = await self.send_message_to_napcat( + action, + { + id_name: target_id, + "message": processed_message, + }, + ) + if response.get("status") == "ok": + logger.info("消息发送成功") + else: + logger.warning(f"消息发送失败,napcat返回:{str(response)}") + + async def send_command(self, raw_message_base: MessageBase) -> None: + """ + 处理命令类 + """ + logger.info("处理命令中") + message_info: BaseMessageInfo = raw_message_base.message_info + message_segment: Seg = raw_message_base.message_segment + group_info: GroupInfo = message_info.group_info + seg_data: Dict[str, Any] = message_segment.data + command_name: str = seg_data.get("name") + try: + match command_name: + case CommandType.GROUP_BAN.name: + command, args_dict = self.handle_ban_command(seg_data.get("args"), group_info) + case CommandType.GROUP_WHOLE_BAN.name: + command, args_dict = self.handle_whole_ban_command(seg_data.get("args"), group_info) + case CommandType.GROUP_KICK.name: + command, args_dict = self.handle_kick_command(seg_data.get("args"), group_info) + case CommandType.SEND_POKE.name: + command, args_dict = self.handle_poke_command(seg_data.get("args"), group_info) + case _: + logger.error(f"未知命令: {command_name}") + return + except Exception as e: + logger.error(f"处理命令时发生错误: {e}") + return None + + if not command or not args_dict: + logger.error("命令或参数缺失") + return None + + response = await self.send_message_to_napcat(command, args_dict) + if response.get("status") == "ok": + logger.info(f"命令 {command_name} 执行成功") + else: + logger.warning(f"命令 {command_name} 执行失败,napcat返回:{str(response)}") + + def get_level(self, seg_data: Seg) -> int: + if seg_data.type == "seglist": + return 1 + max(self.get_level(seg) for seg in seg_data.data) + else: + return 1 + + async def handle_seg_recursive(self, seg_data: Seg) -> list: + payload: list = [] + if seg_data.type == "seglist": + # level = self.get_level(seg_data) # 给以后可能的多层嵌套做准备,此处不使用 + if not seg_data.data: + return [] + for seg in seg_data.data: + payload = self.process_message_by_type(seg, payload) + else: + payload = self.process_message_by_type(seg_data, payload) + return payload + + def process_message_by_type(self, seg: Seg, payload: list) -> list: + # sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression + new_payload = payload + if seg.type == "reply": + target_id = seg.data + if target_id == "notice": + return payload + new_payload = self.build_payload(payload, self.handle_reply_message(target_id), True) + elif seg.type == "text": + text = seg.data + if not text: + return payload + new_payload = self.build_payload(payload, self.handle_text_message(text), False) + elif seg.type == "face": + logger.warning("MaiBot 发送了qq原生表情,暂时不支持") + elif seg.type == "image": + image = seg.data + new_payload = self.build_payload(payload, self.handle_image_message(image), False) + elif seg.type == "emoji": + emoji = seg.data + new_payload = self.build_payload(payload, self.handle_emoji_message(emoji), False) + elif seg.type == "voice": + voice = seg.data + new_payload = self.build_payload(payload, self.handle_voice_message(voice), False) + return new_payload + + def build_payload(self, payload: list, addon: dict, is_reply: bool = False) -> list: + # sourcery skip: for-append-to-extend, merge-list-append, simplify-generator + """构建发送的消息体""" + if is_reply: + temp_list = [] + temp_list.append(addon) + for i in payload: + if i.get("type") == "reply": + logger.debug("检测到多个回复,使用最新的回复") + continue + temp_list.append(i) + return temp_list + else: + payload.append(addon) + return payload + + def handle_reply_message(self, id: str) -> dict: + """处理回复消息""" + return {"type": "reply", "data": {"id": id}} + + def handle_text_message(self, message: str) -> dict: + """处理文本消息""" + return {"type": "text", "data": {"text": message}} + + def handle_image_message(self, encoded_image: str) -> dict: + """处理图片消息""" + return { + "type": "image", + "data": { + "file": f"base64://{encoded_image}", + "subtype": 0, + }, + } # base64 编码的图片 + + def handle_emoji_message(self, encoded_emoji: str) -> dict: + """处理表情消息""" + encoded_image = encoded_emoji + image_format = get_image_format(encoded_emoji) + if image_format != "gif": + encoded_image = convert_image_to_gif(encoded_emoji) + return { + "type": "image", + "data": { + "file": f"base64://{encoded_image}", + "subtype": 1, + "summary": "[动画表情]", + }, + } + + def handle_voice_message(self, encoded_voice: str) -> dict: + """处理语音消息""" + if not global_config.voice.use_tts: + logger.warning("未启用语音消息处理") + return {} + if not encoded_voice: + return {} + return { + "type": "record", + "data": {"file": f"base64://{encoded_voice}"}, + } + + def handle_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: + """处理封禁命令 + + Args: + args (Dict[str, Any]): 参数字典 + group_info (GroupInfo): 群聊信息(对应目标群聊) + + Returns: + Tuple[CommandType, Dict[str, Any]] + """ + duration: int = int(args["duration"]) + user_id: int = int(args["qq_id"]) + group_id: int = int(group_info.group_id) + if duration <= 0: + raise ValueError("封禁时间必须大于0") + if not user_id or not group_id: + raise ValueError("封禁命令缺少必要参数") + if duration > 2592000: + raise ValueError("封禁时间不能超过30天") + return ( + CommandType.GROUP_BAN.value, + { + "group_id": group_id, + "user_id": user_id, + "duration": duration, + }, + ) + + def handle_whole_ban_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: + """处理全体禁言命令 + + Args: + args (Dict[str, Any]): 参数字典 + group_info (GroupInfo): 群聊信息(对应目标群聊) + + Returns: + Tuple[CommandType, Dict[str, Any]] + """ + enable = args["enable"] + assert isinstance(enable, bool), "enable参数必须是布尔值" + group_id: int = int(group_info.group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + return ( + CommandType.GROUP_WHOLE_BAN.value, + { + "group_id": group_id, + "enable": enable, + }, + ) + + def handle_kick_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: + """处理群成员踢出命令 + + Args: + args (Dict[str, Any]): 参数字典 + group_info (GroupInfo): 群聊信息(对应目标群聊) + + Returns: + Tuple[CommandType, Dict[str, Any]] + """ + user_id: int = int(args["qq_id"]) + group_id: int = int(group_info.group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + if user_id <= 0: + raise ValueError("用户ID无效") + return ( + CommandType.GROUP_KICK.value, + { + "group_id": group_id, + "user_id": user_id, + "reject_add_request": False, # 不拒绝加群请求 + }, + ) + + def handle_poke_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: + """处理戳一戳命令 + + Args: + args (Dict[str, Any]): 参数字典 + group_info (GroupInfo): 群聊信息(对应目标群聊) + + Returns: + Tuple[CommandType, Dict[str, Any]] + """ + user_id: int = int(args["qq_id"]) + if group_info == None: + group_id = None + else: + group_id: int = int(group_info.group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + if user_id <= 0: + raise ValueError("用户ID无效") + return ( + CommandType.SEND_POKE.value, + { + "group_id": group_id, + "user_id": user_id, + }, + ) + + async def send_message_to_napcat(self, action: str, params: dict) -> dict: + request_uuid = str(uuid.uuid4()) + payload = json.dumps({"action": action, "params": params, "echo": request_uuid}) + await self.server_connection.send(payload) + try: + response = await get_response(request_uuid) + except TimeoutError: + logger.error("发送消息超时,未收到响应") + return {"status": "error", "message": "timeout"} + except Exception as e: + logger.error(f"发送消息失败: {e}") + return {"status": "error", "message": str(e)} + return response + + +send_handler = SendHandler() From 9ef9dff4a9a223bc744318fbbe31a22f6270d7cf Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sun, 22 Jun 2025 09:49:23 +0800 Subject: [PATCH 09/10] ruff --- src/__init__.py | 4 ++-- src/send_handler.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index e12aede..bfae081 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -72,7 +72,7 @@ class CommandType(Enum): GROUP_BAN = "set_group_ban" # 禁言用户 GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 GROUP_KICK = "set_group_kick" # 踢出群聊 - SEND_POKE = "send_poke" # 戳一戳 - + SEND_POKE = "send_poke" # 戳一戳 + def __str__(self) -> str: return self.value diff --git a/src/send_handler.py b/src/send_handler.py index 8aae094..9085fed 100644 --- a/src/send_handler.py +++ b/src/send_handler.py @@ -296,7 +296,7 @@ class SendHandler: "reject_add_request": False, # 不拒绝加群请求 }, ) - + def handle_poke_command(self, args: Dict[str, Any], group_info: GroupInfo) -> Tuple[str, Dict[str, Any]]: """处理戳一戳命令 @@ -310,7 +310,7 @@ class SendHandler: user_id: int = int(args["qq_id"]) if group_info == None: group_id = None - else: + else: group_id: int = int(group_info.group_id) if group_id <= 0: raise ValueError("群组ID无效") From 3711b2892de6dcc97aefe50ca826250ba6483482 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sun, 22 Jun 2025 10:03:07 +0800 Subject: [PATCH 10/10] command update --- command_args.md | 18 ++++++++++++++---- src/send_handler.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/command_args.md b/command_args.md index 8a73651..cbbb582 100644 --- a/command_args.md +++ b/command_args.md @@ -5,7 +5,7 @@ Seg.type = "command" ## 群聊禁言 ```python Seg.data: Dict[str, Any] = { - "name": "GROUP_BAN" + "name": "GROUP_BAN", "args": { "qq_id": "用户QQ号", "duration": "禁言时长(秒)" @@ -16,7 +16,7 @@ Seg.data: Dict[str, Any] = { ## 群聊全体禁言 ```python Seg.data: Dict[str, Any] = { - "name": "GROUP_WHOLE_BAN" + "name": "GROUP_WHOLE_BAN", "args": { "enable": "是否开启全体禁言(True/False)" }, @@ -28,10 +28,20 @@ Seg.data: Dict[str, Any] = { ## 群聊踢人 ```python Seg.data: Dict[str, Any] = { - "name": "GROUP_KICK" + "name": "GROUP_KICK", "args": { "qq_id": "用户QQ号", }, } ``` -其中,群聊ID将会通过Group_Info.group_id自动获取。 \ No newline at end of file +其中,群聊ID将会通过Group_Info.group_id自动获取。 + +## 戳一戳 +```python +Seg,.data: Dict[str, Any] = { + "name": "SEND_POKE", + "args": { + "qq_id": "目标QQ号" + } +} +``` \ No newline at end of file diff --git a/src/send_handler.py b/src/send_handler.py index 9085fed..37788b8 100644 --- a/src/send_handler.py +++ b/src/send_handler.py @@ -308,7 +308,7 @@ class SendHandler: Tuple[CommandType, Dict[str, Any]] """ user_id: int = int(args["qq_id"]) - if group_info == None: + if group_info is None: group_id = None else: group_id: int = int(group_info.group_id)