diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 16a910c86..0f43458e7 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -239,6 +239,12 @@ class MessageRecv(Message): } """ return "" + elif segment.type == "file": + if isinstance(segment.data, dict): + file_name = segment.data.get('name', '未知文件') + file_size = segment.data.get('size', '未知大小') + return f"[文件:{file_name} ({file_size}字节)]" + return "[收到一个文件]" elif segment.type == "video": self.is_picid = False self.is_emoji = False @@ -296,8 +302,8 @@ class MessageRecv(Message): else: return "" else: - logger.info("未启用视频识别") - return "[视频]" + logger.warning(f"未知的消息段类型: {segment.type}") + return f"[{segment.type} 消息]" except Exception as e: logger.error(f"处理消息段失败: {e!s}, 类型: {segment.type}, 数据: {segment.data}") return f"[处理失败的{segment.type}消息]" @@ -433,6 +439,12 @@ class MessageRecvS4U(MessageRecv): self.is_screen = True self.screen_info = segment.data return "屏幕信息" + elif segment.type == "file": + if isinstance(segment.data, dict): + file_name = segment.data.get('name', '未知文件') + file_size = segment.data.get('size', '未知大小') + return f"[文件:{file_name} ({file_size}字节)]" + return "[收到一个文件]" elif segment.type == "video": self.is_voice = False self.is_picid = False @@ -490,8 +502,8 @@ class MessageRecvS4U(MessageRecv): else: return "" else: - logger.info("未启用视频识别") - return "[视频]" + logger.warning(f"未知的消息段类型: {segment.type}") + return f"[{segment.type} 消息]" except Exception as e: logger.error(f"处理消息段失败: {e!s}, 类型: {segment.type}, 数据: {segment.data}") return f"[处理失败的{segment.type}消息]" diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 27e59f0b9..eae512dfb 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -27,6 +27,67 @@ """ +from pathlib import Path + + +async def file_to_stream( + file_path: str, + stream_id: str, + file_name: str | None = None, + storage_message: bool = True, + set_reply: bool = True +) -> bool: + """向指定流发送文件 + + Args: + file_path: 文件的本地路径 + stream_id: 聊天流ID + file_name: 发送到对方时显示的文件名,如果为 None 则使用原始文件名 + storage_message: 是否存储消息到数据库 + + Returns: + bool: 是否发送成功 + """ + target_stream = await get_chat_manager().get_stream(stream_id) + if not target_stream: + logger.error(f"[SendAPI] 未找到聊天流: {stream_id}") + return False + + if not file_name: + file_name = Path(file_path).name + + # 临时的WSL路径转换方案 + if file_path.startswith("E:"): + original_path = file_path + file_path = "/mnt/e/" + file_path[3:].replace("\\", "/") + logger.info(f"WSL路径转换: {original_path} -> {file_path}") + + params = { + "file": file_path, + "name": file_name, + } + + action = "" + if target_stream.group_info: + action = "upload_group_file" + params["group_id"] = target_stream.group_info.group_id + else: + action = "upload_private_file" + params["user_id"] = target_stream.user_info.user_id + + response = await adapter_command_to_stream( + action=action, + params=params, + stream_id=stream_id, + timeout=300.0 # 文件上传可能需要更长时间 + ) + + if response.get("status") == "ok": + logger.info(f"文件 {file_name} 已成功发送到 {stream_id}") + return True + else: + logger.error(f"文件 {file_name} 发送到 {stream_id} 失败: {response.get('message')}") + return False import asyncio import time diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py index 231c0ce39..d016a5be4 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/__init__.py @@ -33,7 +33,8 @@ class NoticeType: # 通知事件 notify = "notify" group_ban = "group_ban" # 群禁言 group_msg_emoji_like = "group_msg_emoji_like" # 群聊表情回复 - + group_upload = "group_upload" # 群文件上传 + class Notify: poke = "poke" # 戳一戳 input_status = "input_status" # 正在输入 @@ -59,6 +60,7 @@ class RealMessageType: # 实际消息分类 forward = "forward" # 转发消息 node = "node" # 转发消息节点 json = "json" # json消息 + file = "file" # 文件 class MessageSentType: diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py index e0950e0b0..6992dbc8b 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/message_handler.py @@ -178,10 +178,6 @@ class MessageHandler: message_time: float = time.time() # 应可乐要求,现在是float了 template_info: TemplateInfo = None # 模板信息,暂时为空,等待启用 - format_info: FormatInfo = FormatInfo( - content_format=["text", "image", "emoji", "voice"], - accept_format=ACCEPT_FORMAT, - ) # 格式化信息 if message_type == MessageType.private: sub_type = raw_message.get("sub_type") if sub_type == MessageType.Private.friend: @@ -275,6 +271,25 @@ class MessageHandler: logger.warning(f"群聊消息类型 {sub_type} 不支持") return None + # 处理实际信息 + if not raw_message.get("message"): + logger.warning("原始消息内容为空") + return None + + # 获取Seg列表 + seg_message: List[Seg] = await self.handle_real_message(raw_message) + if not seg_message: + logger.warning("处理后消息内容为空") + return None + + # 动态生成 content_format + content_formats = sorted(list(set(seg.type for seg in seg_message))) + logger.debug(f"动态生成 content_format: {content_formats}") + format_info: FormatInfo = FormatInfo( + content_format=content_formats, + accept_format=ACCEPT_FORMAT, + ) + additional_config: dict = {} if config_api.get_plugin_config(self.plugin_config, "voice.use_tts"): additional_config["allow_tts"] = True @@ -291,17 +306,6 @@ class MessageHandler: additional_config=additional_config, ) - # 处理实际信息 - if not raw_message.get("message"): - logger.warning("原始消息内容为空") - return None - - # 获取Seg列表 - seg_message: List[Seg] = await self.handle_real_message(raw_message) - if not seg_message: - logger.warning("处理后消息内容为空") - return None - # 消息缓冲功能已移除,直接处理消息 logger.debug(f"准备发送消息到MoFox-Bot,消息段数量: {len(seg_message)}") @@ -482,6 +486,13 @@ class MessageHandler: seg_message.append(ret_seg) else: logger.warning("json处理失败") + case RealMessageType.file: + ret_seg = await self.handle_file_message(sub_message) + if ret_seg: + # NapcatEvent doesn't have a FILE event yet, so we won't trigger one for now. + seg_message.append(ret_seg) + else: + logger.warning("file处理失败") case _: logger.warning(f"未知消息类型: {sub_message_type}") @@ -773,8 +784,19 @@ class MessageHandler: return Seg(type="json", data=json.dumps(message_data)) try: + # 尝试将json_data解析为Python对象 nested_data = json.loads(json_data) + # 检查是否是机器人自己上传文件的回声 + if self._is_file_upload_echo(nested_data): + logger.info("检测到机器人发送文件的回声消息,将作为文件消息处理") + # 从回声消息中提取文件信息 + file_info = self._extract_file_info_from_echo(nested_data) + if file_info: + # 构建一个与普通文件消息格式相同的字典 + file_message_dict = {"type": "file", "data": file_info} + return await self.handle_file_message(file_message_dict) + # 检查是否是QQ小程序分享消息 if "app" in nested_data and "com.tencent.miniapp" in str(nested_data.get("app", "")): logger.debug("检测到QQ小程序分享消息,开始提取信息") @@ -888,13 +910,88 @@ class MessageHandler: # 如果没有提取到关键信息,返回None return None - except json.JSONDecodeError as e: - logger.error(f"解析JSON消息失败: {e}") + except json.JSONDecodeError: + # 如果解析失败,我们假设它不是我们关心的任何一种结构化JSON, + # 而是普通的文本或者无法解析的格式。 + logger.debug(f"无法将data字段解析为JSON: {json_data}") return None except Exception as e: - logger.error(f"处理JSON消息时出错: {e}") + logger.error(f"处理JSON消息时发生未知错误: {e}") return None + async def handle_file_message(self, raw_message: dict) -> Seg | None: + """ + 处理文件消息 + Parameters: + raw_message: dict: 原始消息 + Returns: + seg_data: Seg: 处理后的消息段 + """ + message_data: dict = raw_message.get("data") + if not message_data: + logger.warning("文件消息缺少 data 字段") + return None + + # 提取文件信息 + file_name = message_data.get("file") + file_size = message_data.get("file_size") + file_id = message_data.get("file_id") + + logger.info(f"收到文件消息: name={file_name}, size={file_size}, id={file_id}") + + # 将文件信息打包成字典 + file_data = { + "name": file_name, + "size": file_size, + "id": file_id, + } + + return Seg(type="file", data=file_data) + + def _is_file_upload_echo(self, nested_data: Any) -> bool: + """检查一个JSON对象是否是机器人自己上传文件的回声消息""" + if not isinstance(nested_data, dict): + return False + + # 检查 'app' 和 'meta' 字段是否存在 + if "app" not in nested_data or "meta" not in nested_data: + return False + + # 检查 'app' 字段是否包含 'com.tencent.miniapp' + if "com.tencent.miniapp" not in str(nested_data.get("app", "")): + return False + + # 检查 'meta' 内部的 'detail_1' 的 'busi_id' 是否为 '1014' + meta = nested_data.get("meta", {}) + detail_1 = meta.get("detail_1", {}) + if detail_1.get("busi_id") == "1014": + return True + + return False + + def _extract_file_info_from_echo(self, nested_data: dict) -> Optional[dict]: + """从文件上传的回声消息中提取文件信息""" + try: + meta = nested_data.get("meta", {}) + detail_1 = meta.get("detail_1", {}) + + # 文件名在 'desc' 字段 + file_name = detail_1.get("desc") + + # 文件大小在 'summary' 字段,格式为 "大小:1.7MB" + summary = detail_1.get("summary", "") + file_size_str = summary.replace("大小:", "").strip() # 移除前缀和空格 + + # QQ API有时返回的大小不标准,这里我们只提取它给的字符串 + # 实际大小已经由Napcat在发送时记录,这里主要是为了保持格式一致 + + if file_name and file_size_str: + return {"file": file_name, "file_size": file_size_str, "file_id": None} # file_id在回声中不可用 + except Exception as e: + logger.error(f"从文件回声中提取信息失败: {e}") + + return None + async def handle_rps_message(self, raw_message: dict) -> Seg: message_data: dict = raw_message.get("data", {}) res = message_data.get("result", "") diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py index 13cea03c0..0f08b32f9 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/notice_handler.py @@ -159,6 +159,11 @@ class NoticeHandler: system_notice = True case _: logger.warning(f"不支持的group_ban类型: {notice_type}.{sub_type}") + case NoticeType.group_upload: + logger.info("群文件上传") + if not await message_handler.check_allow_to_chat(user_id, group_id, False, False): + return None + handled_message, user_info = await self.handle_group_upload_notify(raw_message, group_id, user_id) case _: logger.warning(f"不支持的notice类型: {notice_type}") return None @@ -211,6 +216,9 @@ class NoticeHandler: elif notice_type == NoticeType.group_msg_emoji_like: notice_config["notice_type"] = "emoji_like" notice_config["is_notice"] = True # 表情回复也是notice + elif notice_type == NoticeType.group_upload: + notice_config["notice_type"] = "group_upload" + notice_config["is_notice"] = True # 文件上传也是notice message_info: BaseMessageInfo = BaseMessageInfo( platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), @@ -373,6 +381,38 @@ class NoticeHandler: ) seg_data = Seg(type="text",data=f"{user_name}使用Emoji表情{QQ_FACE.get(like_emoji_id,"")}回复了你的消息[{target_message_text}]") return seg_data, user_info + + async def handle_group_upload_notify(self, raw_message: dict, group_id: int, user_id: int): + if not group_id: + logger.error("群ID不能为空,无法处理群文件上传通知") + return None, None + + user_qq_info: dict = await get_member_info(self.get_server_connection(), group_id, user_id) + if user_qq_info: + user_name = user_qq_info.get("nickname") + user_cardname = user_qq_info.get("card") + else: + user_name = "QQ用户" + user_cardname = "QQ用户" + logger.debug("无法获取上传文件的用户昵称") + + file_info = raw_message.get("file") + if not file_info: + logger.error("群文件上传通知中缺少文件信息") + return None, None + + user_info: UserInfo = UserInfo( + platform=config_api.get_plugin_config(self.plugin_config, "maibot_server.platform_name", "qq"), + user_id=user_id, + user_nickname=user_name, + user_cardname=user_cardname, + ) + + file_name = file_info.get("name", "未知文件") + file_size = file_info.get("size", 0) + + seg_data = Seg(type="text", data=f"{user_name} 上传了文件: {file_name} (大小: {file_size} 字节)") + return seg_data, user_info async def handle_ban_notify(self, raw_message: dict, group_id: int) -> Tuple[Seg, UserInfo] | Tuple[None, None]: if not group_id: diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py index 53e193443..8426147d8 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/send_handler.py @@ -411,6 +411,12 @@ class SendHandler: def handle_file_message(self, file_path: str) -> dict: """处理文件消息""" + # 临时的WSL路径转换方案 + if file_path.startswith("E:"): + original_path = file_path + file_path = "/mnt/e/" + file_path[3:].replace("\\", "/") + logger.info(f"WSL路径转换: {original_path} -> {file_path}") + return { "type": "file", "data": {"file": f"file://{file_path}"},