From 655e535d96fc7f7edbefee0dd74982252e8069ac Mon Sep 17 00:00:00 2001 From: tt-P607 <68868379+tt-P607@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:21:06 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"feat(chat):=20=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E5=8F=82=E6=95=B0=E5=AE=9E=E7=8E=B0=E4=B8=93?= =?UTF-8?q?=E7=94=A8=E7=9A=84=20@=E7=94=A8=E6=88=B7=20=E5=8A=9F=E8=83=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e5117720c6bb764e69648a0d1a73c58a3f5ee246. --- src/chat/message_receive/message_handler.py | 40 ++--- src/chat/planner_actions/action_manager.py | 48 +----- src/chat/replyer/default_generator.py | 8 - src/plugin_system/apis/send_api.py | 90 ++--------- src/plugins/built_in/core_actions/plugin.py | 2 +- src/plugins/built_in/core_actions/reply.py | 4 - .../src/handlers/to_core/message_handler.py | 67 ++++---- .../src/handlers/to_napcat/send_handler.py | 150 ++++++++++-------- 8 files changed, 152 insertions(+), 257 deletions(-) diff --git a/src/chat/message_receive/message_handler.py b/src/chat/message_receive/message_handler.py index 92ee4f779..a7238c2fd 100644 --- a/src/chat/message_receive/message_handler.py +++ b/src/chat/message_receive/message_handler.py @@ -30,16 +30,16 @@ from __future__ import annotations import os import re import traceback -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any -from mofox_wire import MessageEnvelope +from mofox_wire import MessageEnvelope, MessageRuntime from src.chat.message_manager import message_manager from src.chat.message_receive.storage import MessageStorage from src.chat.utils.utils import is_mentioned_bot_in_message from src.common.data_models.database_data_model import DatabaseGroupInfo, DatabaseMessages, DatabaseUserInfo from src.common.logger import get_logger -from src.config.config import Config, global_config +from src.config.config import global_config from src.mood.mood_manager import mood_manager from src.plugin_system.base import BaseCommand, EventType from src.plugin_system.core import component_registry, event_manager, global_announcement_manager @@ -48,10 +48,6 @@ if TYPE_CHECKING: from src.chat.message_receive.chat_stream import ChatStream from src.common.core_sink_manager import CoreSinkManager -if TYPE_CHECKING: - from mofox_wire import MessageRuntime - global_config: Config | None - logger = get_logger("message_handler") # 项目根目录 @@ -59,13 +55,7 @@ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) def _check_ban_words(text: str, chat: "ChatStream", userinfo) -> bool: """检查消息是否包含过滤词""" - if global_config: - for word in global_config.message_receive.ban_words: - if word in text: - chat_name = chat.group_info.group_name if chat.group_info else "私聊" - logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") - logger.info(f"[过滤词识别]消息中含有{word},filtered") - return True + for word in global_config.message_receive.ban_words: if word in text: chat_name = chat.group_info.group_name if chat.group_info else "私聊" logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") @@ -76,13 +66,7 @@ def _check_ban_words(text: str, chat: "ChatStream", userinfo) -> bool: def _check_ban_regex(text: str, chat: "ChatStream", userinfo) -> bool: """检查消息是否匹配过滤正则表达式""" - if global_config: - for pattern in global_config.message_receive.ban_msgs_regex: - if re.search(pattern, text): - chat_name = chat.group_info.group_name if chat.group_info else "私聊" - logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") - logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") - return True + for pattern in global_config.message_receive.ban_msgs_regex: if re.search(pattern, text): chat_name = chat.group_info.group_name if chat.group_info else "私聊" logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") @@ -116,13 +100,13 @@ class MessageHandler: self._message_manager_started = False self._core_sink_manager: CoreSinkManager | None = None self._shutting_down = False - self._runtime: "MessageRuntime | None" = None + self._runtime: MessageRuntime | None = None def set_core_sink_manager(self, manager: "CoreSinkManager") -> None: """设置 CoreSinkManager 引用""" self._core_sink_manager = manager - def register_handlers(self, runtime: "MessageRuntime") -> None: + def register_handlers(self, runtime: MessageRuntime) -> None: """ 向 MessageRuntime 注册消息处理器和钩子 @@ -297,7 +281,7 @@ class MessageHandler: chat = await get_chat_manager().get_or_create_stream( platform=platform, user_info=DatabaseUserInfo.from_dict(user_info) if user_info else None, # type: ignore - group_info=DatabaseGroupInfo.from_dict(cast(dict, group_info)) if group_info else None, + group_info=DatabaseGroupInfo.from_dict(group_info) if group_info else None, ) # 将消息信封转换为 DatabaseMessages @@ -447,7 +431,7 @@ class MessageHandler: chat = await get_chat_manager().get_or_create_stream( platform=platform, user_info=DatabaseUserInfo.from_dict(user_info) if user_info else None, # type: ignore - group_info=DatabaseGroupInfo.from_dict(cast(dict, group_info)) if group_info else None, + group_info=DatabaseGroupInfo.from_dict(group_info) if group_info else None, ) # 将消息信封转换为 DatabaseMessages @@ -551,7 +535,7 @@ class MessageHandler: text = message.processed_plain_text or "" # 获取配置的命令前缀 - prefixes = global_config.command.command_prefixes if global_config else [] + prefixes = global_config.command.command_prefixes # 检查是否以任何前缀开头 matched_prefix = None @@ -723,7 +707,7 @@ class MessageHandler: # 检查是否需要处理消息 should_process_in_manager = True - if global_config and group_info and str(group_info.group_id) in global_config.message_receive.mute_group_list: + if group_info and str(group_info.group_id) in global_config.message_receive.mute_group_list: is_image_or_emoji = message.is_picid or message.is_emoji if not message.is_mentioned and not is_image_or_emoji: logger.debug( @@ -747,7 +731,7 @@ class MessageHandler: # 情绪系统更新 try: - if global_config and global_config.mood.enable_mood: + if global_config.mood.enable_mood: interest_rate = message.interest_value or 0.0 logger.debug(f"开始更新情绪状态,兴趣度: {interest_rate:.2f}") diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index 58f920237..ef8b24657 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -104,7 +104,7 @@ class ChatterActionManager: log_prefix=log_prefix, shutting_down=shutting_down, plugin_config=plugin_config, - action_message=action_message.flatten() if action_message else None, + action_message=action_message, ) logger.debug(f"创建Action实例成功: {action_name}") @@ -252,7 +252,7 @@ class ChatterActionManager: # 检查目标消息是否为表情包消息以及配置是否允许回复表情包 if target_message and getattr(target_message, "is_emoji", False): # 如果是表情包消息且配置不允许回复表情包,则跳过回复 - if global_config and not getattr(global_config.chat, "allow_reply_to_emoji", True): + if not getattr(global_config.chat, "allow_reply_to_emoji", True): logger.info(f"{log_prefix} 目标消息为表情包且配置不允许回复表情包,跳过回复") return {"action_type": action_name, "success": True, "reply_text": "", "skip_reason": "emoji_not_allowed"} @@ -288,7 +288,7 @@ class ChatterActionManager: reply_message=target_message, action_data=action_data_with_mode, available_actions=current_actions, # type: ignore - enable_tool=global_config.tool.enable_tool if global_config else False, + enable_tool=global_config.tool.enable_tool, request_type="chat.replyer", from_plugin=False, ) @@ -325,7 +325,6 @@ class ChatterActionManager: thinking_id, [], # actions should_quote_reply, # 传递should_quote_reply参数 - action_data=action_data or {} ) # 记录回复动作到目标消息 @@ -493,7 +492,6 @@ class ChatterActionManager: thinking_id, actions, should_quote_reply: bool | None = None, - action_data: dict | None = None ) -> tuple[str, dict[str, float]]: """ 发送并存储回复信息 @@ -511,39 +509,11 @@ class ChatterActionManager: Returns: Tuple[Dict[str, Any], str, Dict[str, float]]: 循环信息, 回复文本, 循环计时器 """ - # 提取回复文本 - reply_text = "" - for reply_seg in response_set: - if isinstance(reply_seg, tuple) and len(reply_seg) >= 2: - _, data = reply_seg - else: - data = str(reply_seg) - if isinstance(data, list): - data = "".join(map(str, data)) - reply_text += data - - # 检查是否需要@用户 - at_user_id = action_data.get("at_user_id") if action_data else None - if at_user_id and chat_stream.group_info: - logger.info(f"检测到需要@用户: {at_user_id},将使用 SEND_AT_MESSAGE 命令发送") - from src.plugins.built_in.napcat_adapter.src.event_models import CommandType - command_payload = { - "name": CommandType.SEND_AT_MESSAGE.name, - "args": { - "qq_id": str(at_user_id), - "text": reply_text - } - } - await send_api.command_to_stream( - command=command_payload, - stream_id=chat_stream.stream_id + # 发送回复 + with Timer("回复发送", cycle_timers): + reply_text = await self.send_response( + chat_stream, response_set, loop_start_time, action_message, should_quote_reply ) - else: - # 正常发送回复 - with Timer("回复发送", cycle_timers): - reply_text = await self.send_response( - chat_stream, response_set, loop_start_time, action_message, should_quote_reply, action_data - ) # 存储reply action信息 person_info_manager = get_person_info_manager() @@ -588,7 +558,7 @@ class ChatterActionManager: return reply_text, cycle_timers async def send_response( - self, chat_stream, reply_set, thinking_start_time, message_data, should_quote_reply: bool | None = None, action_data: dict | None = None + self, chat_stream, reply_set, thinking_start_time, message_data, should_quote_reply: bool | None = None ) -> str: """ 发送回复内容的具体实现 @@ -599,7 +569,6 @@ class ChatterActionManager: thinking_start_time: 思考开始时间 message_data: 消息数据 should_quote_reply: 是否应该引用回复原消息,None表示自动决定 - action_data: 动作数据,用于检查是否需要@ Returns: str: 完整的回复文本 @@ -628,7 +597,6 @@ class ChatterActionManager: logger.debug(f"[send_response] message_data: {message_data}") first_replied = False - for reply_seg in reply_set: # 调试日志:验证reply_seg的格式 logger.debug(f"Processing reply_seg type: {type(reply_seg)}, content: {reply_seg}") diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 55fc23e12..fe9be0494 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -38,10 +38,6 @@ from src.plugin_system.base.component_types import ActionInfo, EventType if TYPE_CHECKING: from src.chat.message_receive.chat_stream import ChatStream - from src.config.config import APIAdapterConfig, Config - - global_config: "Config" - model_config: "APIAdapterConfig" logger = get_logger("replyer") @@ -123,10 +119,6 @@ def init_prompt(): {action_descriptions} -- **关于@功能的重要说明**: - - 如果你需要在一个回复中`@`某个用户,**请不要**在你的回复内容中直接输出`@`符号或`艾特`等文字。 - - 你应该使用`reply`或`respond`动作中的`at_user_id`参数。只需要将目标的QQ号填入该参数,系统就会自动为你完成`@`操作。 - ## 任务 *{chat_scene}* diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 9a596f444..3aac3d76c 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -92,11 +92,10 @@ import traceback import uuid from typing import TYPE_CHECKING, Any -from mofox_wire import MessageEnvelope, MessageInfoPayload, SegPayload +from mofox_wire import MessageEnvelope from src.common.data_models.database_data_model import DatabaseUserInfo if TYPE_CHECKING: from src.common.data_models.database_data_model import DatabaseMessages - from src.config.config import Config # 导入依赖 from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager @@ -104,9 +103,6 @@ from src.chat.message_receive.uni_message_sender import HeartFCSender from src.common.logger import get_logger from src.config.config import global_config -if TYPE_CHECKING: - assert global_config is not None - # 日志记录器 logger = get_logger("send_api") @@ -197,44 +193,32 @@ def _build_message_envelope( ) -> MessageEnvelope: """构建发送的 MessageEnvelope 数据结构""" target_user_info = target_stream.user_info or bot_user_info - message_info: MessageInfoPayload = { + message_info: dict[str, Any] = { "message_id": message_id, "time": timestamp, "platform": target_stream.platform, "user_info": { - "user_id": target_user_info.user_id or "", - "user_nickname": target_user_info.user_nickname or "", - "user_cardname": getattr(target_user_info, "user_cardname", "") or "", - "platform": target_user_info.platform or "", + "user_id": target_user_info.user_id, + "user_nickname": target_user_info.user_nickname, + "user_cardname": getattr(target_user_info, "user_cardname", None), + "platform": target_user_info.platform, }, - "group_info": None, # type: ignore } if target_stream.group_info: message_info["group_info"] = { - "group_id": target_stream.group_info.group_id or "", - "group_name": target_stream.group_info.group_name or "", - "platform": target_stream.group_info.platform or "", + "group_id": target_stream.group_info.group_id, + "group_name": target_stream.group_info.group_name, + "platform": target_stream.group_info.platform, } - # Ensure message_segment is of the correct type - seg_payload: SegPayload - if isinstance(message_segment, list): - seg_payload = {"type": "seglist", "data": message_segment} - elif isinstance(message_segment, dict) and "type" in message_segment and "data" in message_segment: - seg_payload = message_segment # type: ignore - else: - # Fallback for simple string content or other unexpected formats - seg_payload = {"type": "text", "data": str(message_segment)} - - - envelope: MessageEnvelope = { + return { + "id": str(uuid.uuid4()), "direction": "outgoing", "platform": target_stream.platform, "message_info": message_info, - "message_segment": seg_payload, + "message_segment": message_segment, } - return envelope @@ -273,18 +257,9 @@ async def _send_to_target( current_time = time.time() message_id = f"send_api_{int(current_time * 1000)}" - # Use a safer way to get bot config - if not global_config: - logger.error("[SendAPI] Global config is not initialized!") - return False - bot_config = global_config.bot - if not bot_config: - logger.error("[SendAPI] Bot configuration not found!") - return False - bot_user_info = DatabaseUserInfo( - user_id=str(bot_config.qq_account), - user_nickname=bot_config.nickname, + user_id=str(global_config.bot.qq_account), + user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) @@ -437,30 +412,6 @@ async def image_to_stream( ) -async def at_user_to_stream( - user_id: str, - stream_id: str, - display_text: str = "", - storage_message: bool = False, -) -> bool: - """向指定流发送@用户消息 - 这是一个特殊的、独立的段,通常用于在发送文本之前先发送@信息 - - Args: - user_id: 要@的用户的ID - stream_id: 聊天流ID - display_text: 在数据库中存储的显示文本 - storage_message: 是否存储此消息段到数据库 - - Returns: - bool: 是否发送成功 - """ - at_segment = {"qq": user_id} - return await _send_to_target( - "at", at_segment, stream_id, display_text, typing=False, storage_message=storage_message, set_reply=False - ) - - async def command_to_stream( command: str | dict, stream_id: str, @@ -567,18 +518,9 @@ async def adapter_command_to_stream( current_time = time.time() message_id = f"adapter_cmd_{int(current_time * 1000)}" - # Use a safer way to get bot config - if not global_config: - logger.error("[SendAPI] Global config is not initialized!") - return {"status": "error", "message": "Global config is not initialized"} - bot_config = global_config.bot - if not bot_config: - logger.error("[SendAPI] Bot configuration not found!") - return {"status": "error", "message": "Bot configuration not found"} - bot_user_info = DatabaseUserInfo( - user_id=str(bot_config.qq_account), - user_nickname=bot_config.nickname, + user_id=str(global_config.bot.qq_account), + user_nickname=global_config.bot.nickname, platform=target_stream.platform, ) diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index 008e42b09..5baaa3a8e 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -63,7 +63,7 @@ class CoreActionsPlugin(BasePlugin): """返回插件包含的组件列表""" # --- 根据配置注册组件 --- - components = [] + components: ClassVar = [] # 注册 reply 动作 if self.get_config("components.enable_reply", True): diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py index 06d00a23d..9a90f7e33 100644 --- a/src/plugins/built_in/core_actions/reply.py +++ b/src/plugins/built_in/core_actions/reply.py @@ -37,7 +37,6 @@ class ReplyAction(BaseAction): "target_message_id": "要回复的目标消息ID(必需,来自未读消息的 标签)", "content": "回复的具体内容(可选,由LLM生成)", "should_quote_reply": "是否引用原消息(可选,true/false,默认false。群聊中回复较早消息或需要明确指向时使用true)", - "at_user_id": "需要@的用户的QQ号(可选,string)。如果需要在回复中@某个用户,请提供此参数。", } # 动作使用场景 @@ -48,7 +47,6 @@ class ReplyAction(BaseAction): "私聊场景必须使用此动作(不支持 respond)", "群聊中需要明确回应某个特定用户或问题时使用", "关注单条消息的具体内容和上下文细节", - "如果回复时需要@某个用户,请在参数中提供'at_user_id'。", ] # 关联类型 @@ -85,7 +83,6 @@ class RespondAction(BaseAction): # 动作参数定义 action_parameters: ClassVar = { "content": "回复的具体内容(可选,由LLM生成)", - "at_user_id": "需要@的用户的QQ号(可选,string)。如果需要在回复中@某个用户,请提供此参数。", } # 动作使用场景 @@ -96,7 +93,6 @@ class RespondAction(BaseAction): "关注对话流程、话题走向和整体氛围", "适合群聊中的自然对话流,无需精确指向特定消息", "可以同时回应多个话题或参与者", - "如果回复时需要@某个用户,请在参数中提供'at_user_id'。", ] # 关联类型 diff --git a/src/plugins/built_in/napcat_adapter/src/handlers/to_core/message_handler.py b/src/plugins/built_in/napcat_adapter/src/handlers/to_core/message_handler.py index 17f8df00f..0cc74681b 100644 --- a/src/plugins/built_in/napcat_adapter/src/handlers/to_core/message_handler.py +++ b/src/plugins/built_in/napcat_adapter/src/handlers/to_core/message_handler.py @@ -5,7 +5,7 @@ from __future__ import annotations import base64 import time from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple import uuid from mofox_wire import MessageBuilder @@ -224,7 +224,7 @@ class MessageHandler: if not messages: logger.warning("转发消息内容为空或获取失败") return None - return await self.handle_forward_message(cast(list, messages)) + return await self.handle_forward_message(messages) case RealMessageType.json: return await self._handle_json_message(segment) case RealMessageType.file: @@ -331,13 +331,10 @@ class MessageHandler: {"type": seg.get("type", "text"), "data": seg.get("data", "")} for seg in reply_segments ] or [{"type": "text", "data": "[无法获取被引用的消息]"}] - return cast( - SegPayload, - { - "type": "seglist", - "data": [{"type": "text", "data": prefix_text}, *brief_segments, {"type": "text", "data": suffix_text}], - }, - ) + return { + "type": "seglist", + "data": [{"type": "text", "data": prefix_text}, *brief_segments, {"type": "text", "data": suffix_text}], + } async def _handle_record_message(self, segment: dict) -> SegPayload | None: """处理语音消息""" @@ -383,17 +380,14 @@ class MessageHandler: video_base64 = base64.b64encode(video_data).decode("utf-8") logger.debug(f"视频文件大小: {len(video_data) / (1024 * 1024):.2f} MB") - return cast( - SegPayload, - { - "type": "video", - "data": { - "base64": video_base64, - "filename": Path(file_path).name, - "size_mb": len(video_data) / (1024 * 1024), - }, + return { + "type": "video", + "data": { + "base64": video_base64, + "filename": Path(file_path).name, + "size_mb": len(video_data) / (1024 * 1024), }, - ) + } elif video_url: # URL下载处理 from ..video_handler import get_video_downloader @@ -407,18 +401,15 @@ class MessageHandler: video_base64 = base64.b64encode(download_result["data"]).decode("utf-8") logger.debug(f"视频下载成功,大小: {len(download_result['data']) / (1024 * 1024):.2f} MB") - return cast( - SegPayload, - { - "type": "video", - "data": { - "base64": video_base64, - "filename": download_result.get("filename", "video.mp4"), - "size_mb": len(download_result["data"]) / (1024 * 1024), - "url": video_url, - }, + return { + "type": "video", + "data": { + "base64": video_base64, + "filename": download_result.get("filename", "video.mp4"), + "size_mb": len(download_result["data"]) / (1024 * 1024), + "url": video_url, }, - ) + } else: logger.warning("既没有有效的本地文件路径,也没有有效的视频URL") return None @@ -463,14 +454,14 @@ class MessageHandler: processed_message = handled_message forward_hint = {"type": "text", "data": "这是一条转发消息:\n"} - return cast(SegPayload, {"type": "seglist", "data": [forward_hint, processed_message]}) + return {"type": "seglist", "data": [forward_hint, processed_message]} async def _recursive_parse_image_seg(self, seg_data: SegPayload, to_image: bool) -> SegPayload: # sourcery skip: merge-else-if-into-elif if seg_data.get("type") == "seglist": new_seg_list = [] for i_seg in seg_data.get("data", []): - parsed_seg = await self._recursive_parse_image_seg(cast(SegPayload, i_seg), to_image) + parsed_seg = await self._recursive_parse_image_seg(i_seg, to_image) new_seg_list.append(parsed_seg) return {"type": "seglist", "data": new_seg_list} @@ -478,7 +469,7 @@ class MessageHandler: if seg_data.get("type") == "image": image_url = seg_data.get("data") try: - encoded_image = await get_image_base64(cast(str, image_url)) + encoded_image = await get_image_base64(image_url) except Exception as e: logger.error(f"图片处理失败: {str(e)}") return {"type": "text", "data": "[图片]"} @@ -486,7 +477,7 @@ class MessageHandler: if seg_data.get("type") == "emoji": image_url = seg_data.get("data") try: - encoded_image = await get_image_base64(cast(str, image_url)) + encoded_image = await get_image_base64(image_url) except Exception as e: logger.error(f"图片处理失败: {str(e)}") return {"type": "text", "data": "[表情包]"} @@ -501,7 +492,7 @@ class MessageHandler: logger.debug(f"不处理类型: {seg_data.get('type')}") return seg_data - async def _handle_forward_message(self, message_list: list, layer: int) -> Tuple[Optional[SegPayload], int]: + async def _handle_forward_message(self, message_list: list, layer: int) -> Tuple[SegPayload | None, int]: # sourcery skip: low-code-quality """ 递归处理实际转发消息 @@ -539,7 +530,7 @@ class MessageHandler: continue contents = sub_message_data.get("content") seg_data, count = await self._handle_forward_message(contents, layer + 1) - if not seg_data: + if seg_data is None: continue image_count += count head_tip: SegPayload = { @@ -604,7 +595,7 @@ class MessageHandler: "id": file_id, } - return cast(SegPayload, {"type": "file", "data": file_data}) + return {"type": "file", "data": file_data} async def _handle_json_message(self, segment: dict) -> SegPayload | None: """ @@ -632,7 +623,7 @@ class MessageHandler: # 从回声消息中提取文件信息 file_info = self._extract_file_info_from_echo(nested_data) if file_info: - return cast(SegPayload, {"type": "file", "data": file_info}) + return {"type": "file", "data": file_info} # 检查是否是QQ小程序分享消息 if "app" in nested_data and "com.tencent.miniapp" in str(nested_data.get("app", "")): diff --git a/src/plugins/built_in/napcat_adapter/src/handlers/to_napcat/send_handler.py b/src/plugins/built_in/napcat_adapter/src/handlers/to_napcat/send_handler.py index 47d56b996..b832d3ae3 100644 --- a/src/plugins/built_in/napcat_adapter/src/handlers/to_napcat/send_handler.py +++ b/src/plugins/built_in/napcat_adapter/src/handlers/to_napcat/send_handler.py @@ -5,7 +5,7 @@ from __future__ import annotations import random import time import uuid -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast, TypedDict +from typing import TYPE_CHECKING, Any, Dict, List, Optional from mofox_wire import MessageEnvelope, SegPayload, GroupInfoPayload, UserInfoPayload, MessageInfoPayload from src.common.logger import get_logger @@ -19,12 +19,6 @@ if TYPE_CHECKING: from ....plugin import NapcatAdapter -class AtPayload(TypedDict, total=False): - """@ 消息段数据""" - - user_id: str - - class SendHandler: """负责向 Napcat 发送消息""" @@ -47,11 +41,10 @@ class SendHandler: return message_segment = envelope.get("message_segment") - segment: SegPayload if isinstance(message_segment, list): - segment = {"type": "seglist", "data": message_segment} + segment: SegPayload = {"type": "seglist", "data": message_segment} else: - segment = message_segment or {} # type: ignore + segment = message_segment or {} if segment: seg_type = segment.get("type") @@ -73,12 +66,11 @@ class SendHandler: 处理普通消息发送 """ logger.info("处理普通信息中") - message_info: MessageInfoPayload = envelope.get("message_info", {}) or {} - message_segment: Union[SegPayload, List[SegPayload]] = envelope.get("message_segment") or cast(SegPayload, {}) + message_info: MessageInfoPayload = envelope.get("message_info", {}) + message_segment: SegPayload = envelope.get("message_segment", {}) # type: ignore[assignment] - seg_data: SegPayload if isinstance(message_segment, list): - seg_data = {"type": "seglist", "data": message_segment} + seg_data: SegPayload = {"type": "seglist", "data": message_segment} else: seg_data = message_segment @@ -89,9 +81,7 @@ class SendHandler: id_name: Optional[str] = None processed_message: list = [] try: - processed_message = await self.handle_seg_recursive( - seg_data, cast(UserInfoPayload, user_info if user_info is not None else {}) - ) + processed_message = await self.handle_seg_recursive(seg_data, user_info or {}) except Exception as e: logger.error(f"处理消息时发生错误: {e}") return None @@ -133,10 +123,10 @@ class SendHandler: 处理命令类 """ logger.debug("处理命令中") - message_info: MessageInfoPayload = envelope.get("message_info", {}) or {} - group_info: Optional[GroupInfoPayload] = message_info.get("group_info") - segment: SegPayload = envelope.get("message_segment", {}) # type: ignore - seg_data: Dict[str, Any] = segment.get("data", {}) if isinstance(segment, dict) else {} # type: ignore + message_info: Dict[str, Any] = envelope.get("message_info", {}) + group_info: Optional[Dict[str, Any]] = message_info.get("group_info") + segment: SegPayload = envelope.get("message_segment", {}) # type: ignore[assignment] + seg_data: Dict[str, Any] = segment.get("data", {}) if isinstance(segment, dict) else {} command_name: Optional[str] = seg_data.get("name") try: args = seg_data.get("args", {}) @@ -157,10 +147,10 @@ class SendHandler: command, args_dict = self.handle_ai_voice_send_command(args, group_info) elif command_name == CommandType.SET_EMOJI_LIKE.name: command, args_dict = self.handle_set_emoji_like_command(args) - elif command_name == CommandType.SEND_LIKE.name: - command, args_dict = self.handle_send_like_command(args) elif command_name == CommandType.SEND_AT_MESSAGE.name: command, args_dict = self.handle_at_message_command(args, group_info) + elif command_name == CommandType.SEND_LIKE.name: + command, args_dict = self.handle_send_like_command(args) else: logger.error(f"未知命令: {command_name}") return @@ -186,8 +176,8 @@ class SendHandler: 处理适配器命令类 - 用于直接向Napcat发送命令并返回结果 """ logger.info("处理适配器命令中") - segment: SegPayload = envelope.get("message_segment", {}) # type: ignore - seg_data: Dict[str, Any] = segment.get("data", {}) if isinstance(segment, dict) else {} # type: ignore + segment: SegPayload = envelope.get("message_segment", {}) # type: ignore[assignment] + seg_data: Dict[str, Any] = segment.get("data", {}) if isinstance(segment, dict) else {} try: action = seg_data.get("action") @@ -255,9 +245,6 @@ class SendHandler: if not text: return payload new_payload = self.build_payload(payload, self.handle_text_message(str(text)), False) - elif seg_type == "at": - at_data: AtPayload = seg.get("data", {}) # type: ignore - new_payload = self.build_payload(payload, self.handle_at_message(at_data), False) elif seg_type == "face": logger.warning("MoFox-Bot 发送了qq原生表情,暂时不支持") elif seg_type == "image": @@ -312,21 +299,50 @@ class SendHandler: payload.append(addon) return payload - async def handle_reply_message(self, message_id: str, user_info: UserInfoPayload) -> dict: + async def handle_reply_message(self, message_id: str, user_info: UserInfoPayload) -> dict | list: """处理回复消息""" logger.debug(f"开始处理回复消息,消息ID: {message_id}") reply_seg = {"type": "reply", "data": {"id": message_id}} + + # 检查是否启用引用艾特功能 + if not config_api.get_plugin_config(self.plugin_config, "features.enable_reply_at", False): + logger.info("引用艾特功能未启用,仅发送普通回复") + return reply_seg + + try: + msg_info_response = await self.send_message_to_napcat("get_msg", {"message_id": message_id}) + logger.debug(f"获取消息 {message_id} 的详情响应: {msg_info_response}") + + replied_user_id = None + if msg_info_response and msg_info_response.get("status") == "ok": + sender_info = msg_info_response.get("data", {}).get("sender") + if sender_info: + replied_user_id = sender_info.get("user_id") + + if not replied_user_id: + logger.warning(f"无法获取消息 {message_id} 的发送者信息,跳过 @") + logger.debug(f"最终返回的回复段: {reply_seg}") + return reply_seg + + if random.random() < config_api.get_plugin_config(self.plugin_config, "features.reply_at_rate", 0.5): + at_seg = {"type": "at", "data": {"qq": str(replied_user_id)}} + text_seg = {"type": "text", "data": {"text": " "}} + result_seg = [reply_seg, at_seg, text_seg] + logger.debug(f"最终返回的回复段: {result_seg}") + return result_seg + + except Exception as e: + logger.error(f"处理引用回复并尝试@时出错: {e}") + logger.debug(f"最终返回的回复段: {reply_seg}") + return reply_seg + + logger.debug(f"最终返回的回复段: {reply_seg}") return reply_seg def handle_text_message(self, message: str) -> dict: """处理文本消息""" return {"type": "text", "data": {"text": message}} - def handle_at_message(self, at_data: AtPayload) -> dict: - """处理@消息""" - user_id = at_data.get("user_id") - return {"type": "at", "data": {"qq": str(user_id)}} - def handle_image_message(self, encoded_image: str) -> dict: """处理图片消息""" return { @@ -354,8 +370,14 @@ class SendHandler: def handle_voice_message(self, encoded_voice: str) -> dict: """处理语音消息""" + use_tts = False + if self.plugin_config: + use_tts = config_api.get_plugin_config(self.plugin_config, "voice.use_tts", False) + + if not use_tts: + logger.warning("未启用语音消息处理") + return {} if not encoded_voice: - logger.warning("接收到空的语音消息,跳过处理") return {} return { "type": "record", @@ -394,7 +416,7 @@ class SendHandler: """处理删除消息命令""" return "delete_msg", {"message_id": args["message_id"]} - def handle_ban_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: + def handle_ban_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: """处理封禁命令""" duration: int = int(args["duration"]) user_id: int = int(args["qq_id"]) @@ -414,7 +436,7 @@ class SendHandler: }, ) - def handle_whole_ban_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: + def handle_whole_ban_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: """处理全体禁言命令""" enable = args["enable"] assert isinstance(enable, bool), "enable参数必须是布尔值" @@ -429,7 +451,7 @@ class SendHandler: }, ) - def handle_kick_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: + def handle_kick_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: """处理群成员踢出命令""" user_id: int = int(args["qq_id"]) group_id: int = int(group_info["group_id"]) if group_info and group_info.get("group_id") else 0 @@ -446,7 +468,7 @@ class SendHandler: }, ) - def handle_poke_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: + def handle_poke_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: """处理戳一戳命令""" user_id: int = int(args["qq_id"]) group_id: Optional[int] = None @@ -493,7 +515,31 @@ class SendHandler: {"user_id": user_id, "times": times}, ) - def handle_ai_voice_send_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: + def handle_at_message_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: + """处理艾特并发送消息命令""" + at_user_id = args.get("qq_id") + text = args.get("text") + + if not at_user_id or not text: + raise ValueError("艾特消息命令缺少 qq_id 或 text 参数") + + if not group_info or not group_info.get("group_id"): + raise ValueError("艾特消息命令必须在群聊上下文中使用") + + message_payload = [ + {"type": "at", "data": {"qq": str(at_user_id)}}, + {"type": "text", "data": {"text": " " + str(text)}}, + ] + + return ( + "send_group_msg", + { + "group_id": group_info["group_id"], + "message": message_payload, + }, + ) + + def handle_ai_voice_send_command(self, args: Dict[str, Any], group_info: Optional[Dict[str, Any]]) -> tuple[str, Dict[str, Any]]: """ 处理AI语音发送命令的逻辑。 并返回 NapCat 兼容的 (action, params) 元组。 @@ -519,30 +565,6 @@ class SendHandler: }, ) - def handle_at_message_command(self, args: Dict[str, Any], group_info: Optional[GroupInfoPayload]) -> tuple[str, Dict[str, Any]]: - """处理艾特并发送消息命令""" - at_user_id = args.get("qq_id") - text = args.get("text") - - if not at_user_id or not text: - raise ValueError("艾特消息命令缺少 qq_id 或 text 参数") - - if not group_info or not group_info.get("group_id"): - raise ValueError("艾特消息命令必须在群聊上下文中使用") - - message_payload = [ - {"type": "at", "data": {"qq": str(at_user_id)}}, - {"type": "text", "data": {"text": " " + str(text)}}, - ] - - return ( - "send_group_msg", - { - "group_id": group_info["group_id"], - "message": message_payload, - }, - ) - async def send_message_to_napcat(self, action: str, params: dict, timeout: float = 20.0) -> dict: """通过 adapter API 发送到 napcat""" try: