diff --git a/command_args.md b/command_args.md index 3c8947d..6bc9319 100644 --- a/command_args.md +++ b/command_args.md @@ -1,8 +1,28 @@ # Command Arguments + ```python Seg.type = "command" ``` -## 群聊禁言 + +所有命令执行后都会通过自定义消息类型 `command_response` 返回响应,格式如下: + +```python +{ + "command_name": "命令名称", + "success": True/False, # 是否执行成功 + "timestamp": 1234567890.123, # 时间戳 + "data": {...}, # 返回数据(成功时) + "error": "错误信息" # 错误信息(失败时) +} +``` + +插件需要注册 `command_response` 自定义消息处理器来接收命令响应。 + +--- + +## 操作类命令 + +### 群聊禁言 ```python Seg.data: Dict[str, Any] = { "name": "GROUP_BAN", @@ -15,7 +35,8 @@ Seg.data: Dict[str, Any] = { 其中,群聊ID将会通过Group_Info.group_id自动获取。 **当`duration`为 0 时相当于解除禁言。** -## 群聊全体禁言 + +### 群聊全体禁言 ```python Seg.data: Dict[str, Any] = { "name": "GROUP_WHOLE_BAN", @@ -27,18 +48,36 @@ Seg.data: Dict[str, Any] = { 其中,群聊ID将会通过Group_Info.group_id自动获取。 `enable`的参数需要为boolean类型,True表示开启全体禁言,False表示关闭全体禁言。 -## 群聊踢人 + +### 群聊踢人 +将指定成员从群聊中踢出,可选拉黑。 + ```python Seg.data: Dict[str, Any] = { "name": "GROUP_KICK", "args": { - "qq_id": "用户QQ号", + "group_id": 123456789, # 可选,如果在群聊上下文中可从 group_info 自动获取 + "user_id": 12345678, # 必需,用户QQ号 + "reject_add_request": False # 可选,是否群拉黑,默认 False }, } ``` -其中,群聊ID将会通过Group_Info.group_id自动获取。 -## 戳一戳 +### 批量踢出群成员 +批量将多个成员从群聊中踢出,可选拉黑。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GROUP_KICK_MEMBERS", + "args": { + "group_id": 123456789, # 可选,如果在群聊上下文中可从 group_info 自动获取 + "user_id": [12345678, 87654321], # 必需,用户QQ号数组 + "reject_add_request": False # 可选,是否群拉黑,默认 False + }, +} +``` + +### 戳一戳 ```python Seg.data: Dict[str, Any] = { "name": "SEND_POKE", @@ -48,7 +87,7 @@ Seg.data: Dict[str, Any] = { } ``` -## 撤回消息 +### 撤回消息 ```python Seg.data: Dict[str, Any] = { "name": "DELETE_MSG", @@ -57,4 +96,381 @@ Seg.data: Dict[str, Any] = { } } ``` -其中message_id是消息的实际qq_id,于新版的mmc中可以从数据库获取(如果工作正常的话) \ No newline at end of file +其中message_id是消息的实际qq_id,于新版的mmc中可以从数据库获取(如果工作正常的话) + +### 给消息贴表情 +```python +Seg.data: Dict[str, Any] = { + "name": "MESSAGE_LIKE", + "args": { + "message_id": "消息ID", + "emoji_id": "表情ID" + } +} +``` + +### 设置群名 +设置指定群的群名称。 + +```python +Seg.data: Dict[str, Any] = { + "name": "SET_GROUP_NAME", + "args": { + "group_id": 123456789, # 可选,如果在群聊上下文中可从 group_info 自动获取 + "group_name": "新群名" # 必需,新的群名称 + } +} +``` + +### 设置账号信息 +设置Bot自己的QQ账号资料。 + +```python +Seg.data: Dict[str, Any] = { + "name": "SET_QQ_PROFILE", + "args": { + "nickname": "新昵称", # 必需,昵称 + "personal_note": "个性签名", # 可选,个性签名 + "sex": "male" # 可选,性别:"male" | "female" | "unknown" + } +} +``` + +**返回数据示例:** +```python +{ + "result": 0, # 结果码,0为成功 + "errMsg": "" # 错误信息 +} +``` + +--- + +## 查询类命令 + +### 获取登录号信息 +获取Bot自身的账号信息。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_LOGIN_INFO", + "args": {} +} +``` + +**返回数据示例:** +```python +{ + "user_id": 12345678, + "nickname": "Bot昵称" +} +``` + +### 获取陌生人信息 +```python +Seg.data: Dict[str, Any] = { + "name": "GET_STRANGER_INFO", + "args": { + "user_id": "用户QQ号" + } +} +``` + +**返回数据示例:** +```python +{ + "user_id": 12345678, + "nickname": "用户昵称", + "sex": "male/female/unknown", + "age": 0 +} +``` + +### 获取好友列表 +获取Bot的好友列表。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_FRIEND_LIST", + "args": { + "no_cache": False # 可选,是否不使用缓存,默认 False + } +} +``` + +**返回数据示例:** +```python +[ + { + "user_id": 12345678, + "nickname": "好友昵称", + "remark": "备注名", + "sex": "male", # "male" | "female" | "unknown" + "age": 18, + "qid": "QID字符串", + "level": 64, + "login_days": 365, + "birthday_year": 2000, + "birthday_month": 1, + "birthday_day": 1, + "phone_num": "电话号码", + "email": "邮箱", + "category_id": 0, # 分组ID + "categoryName": "我的好友", # 分组名称 + "categoryId": 0 + }, + ... +] +``` + +### 获取群信息 +获取指定群的详细信息。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_INFO", + "args": { + "group_id": 123456789 # 可选,如果在群聊上下文中可从 group_info 自动获取 + } +} +``` + +**返回数据示例:** +```python +{ + "group_id": "123456789", # 群号(字符串) + "group_name": "群名称", + "group_remark": "群备注", + "group_all_shut": 0, # 群全员禁言状态(0=未禁言) + "member_count": 100, # 当前成员数量 + "max_member_count": 500 # 最大成员数量 +} +``` + +### 获取群详细信息 +获取指定群的详细信息(与 GET_GROUP_INFO 类似,可能提供更实时的数据)。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_DETAIL_INFO", + "args": { + "group_id": 123456789 # 可选,如果在群聊上下文中可从 group_info 自动获取 + } +} +``` + +**返回数据示例:** +```python +{ + "group_id": 123456789, # 群号(数字) + "group_name": "群名称", + "group_remark": "群备注", + "group_all_shut": 0, # 群全员禁言状态(0=未禁言) + "member_count": 100, # 当前成员数量 + "max_member_count": 500 # 最大成员数量 +} +``` + +### 获取群列表 +获取Bot加入的所有群列表。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_LIST", + "args": { + "no_cache": False # 可选,是否不使用缓存,默认 False + } +} +``` + +**返回数据示例:** +```python +[ + { + "group_id": "123456789", # 群号(字符串) + "group_name": "群名称", + "group_remark": "群备注", + "group_all_shut": 0, # 群全员禁言状态 + "member_count": 100, # 当前成员数量 + "max_member_count": 500 # 最大成员数量 + }, + ... +] +``` + +### 获取群@全体成员剩余次数 +查询指定群的@全体成员剩余使用次数。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_AT_ALL_REMAIN", + "args": { + "group_id": 123456789 # 可选,如果在群聊上下文中可从 group_info 自动获取 + } +} +``` + +**返回数据示例:** +```python +{ + "can_at_all": True, # 是否可以@全体成员 + "remain_at_all_count_for_group": 10, # 群剩余@全体成员次数 + "remain_at_all_count_for_uin": 5 # Bot剩余@全体成员次数 +} +``` + +### 获取群成员信息 +获取指定群成员的详细信息。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_MEMBER_INFO", + "args": { + "group_id": 123456789, # 可选,如果在群聊上下文中可从 group_info 自动获取 + "user_id": 12345678, # 必需,用户QQ号 + "no_cache": False # 可选,是否不使用缓存,默认 False + } +} +``` + +**返回数据示例:** +```python +{ + "group_id": 123456789, + "user_id": 12345678, + "nickname": "昵称", + "card": "群名片", + "sex": "male", # "male" | "female" | "unknown" + "age": 18, + "join_time": 1234567890, # 加群时间戳 + "last_sent_time": 1234567890, # 最后发言时间戳 + "level": 1, # 群等级 + "qq_level": 64, # QQ等级 + "role": "member", # "owner" | "admin" | "member" + "title": "专属头衔", + "area": "地区", + "unfriendly": False, # 是否不友好 + "title_expire_time": 1234567890, # 头衔过期时间 + "card_changeable": True, # 名片是否可修改 + "shut_up_timestamp": 0, # 禁言时间戳 + "is_robot": False, # 是否机器人 + "qage": "10年" # Q龄 +} +``` + +### 获取群成员列表 +获取指定群的所有成员列表。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_GROUP_MEMBER_LIST", + "args": { + "group_id": 123456789, # 可选,如果在群聊上下文中可从 group_info 自动获取 + "no_cache": False # 可选,是否不使用缓存,默认 False + } +} +``` + +**返回数据示例:** +```python +[ + { + "group_id": 123456789, + "user_id": 12345678, + "nickname": "昵称", + "card": "群名片", + "sex": "male", # "male" | "female" | "unknown" + "age": 18, + "join_time": 1234567890, + "last_sent_time": 1234567890, + "level": 1, + "qq_level": 64, + "role": "member", # "owner" | "admin" | "member" + "title": "专属头衔", + "area": "地区", + "unfriendly": False, + "title_expire_time": 1234567890, + "card_changeable": True, + "shut_up_timestamp": 0, + "is_robot": False, + "qage": "10年" + }, + ... +] +``` + +### 获取消息详情 +获取指定消息的完整详情信息。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_MSG", + "args": { + "message_id": 123456 # 必需,消息ID + } +} +``` + +**返回数据示例:** +```python +{ + "self_id": 12345678, # Bot自身ID + "user_id": 87654321, # 发送者ID + "time": 1234567890, # 时间戳 + "message_id": 123456, # 消息ID + "message_seq": 123456, # 消息序列号 + "real_id": 123456, # 真实消息ID + "real_seq": "123456", # 真实序列号(字符串) + "message_type": "group", # "private" | "group" + "sub_type": "normal", # 子类型 + "message_format": "array", # 消息格式 + "post_type": "message", # 事件类型 + "group_id": 123456789, # 群号(群消息时存在) + "sender": { + "user_id": 87654321, + "nickname": "昵称", + "sex": "male", # "male" | "female" | "unknown" + "age": 18, + "card": "群名片", # 群消息时存在 + "level": "1", # 群等级(字符串) + "role": "member" # "owner" | "admin" | "member" + }, + "message": [...], # 消息段数组 + "raw_message": "消息文本内容", # 原始消息文本 + "font": 0 # 字体 +} +``` + +### 获取合并转发消息 +获取合并转发消息的所有子消息内容。 + +```python +Seg.data: Dict[str, Any] = { + "name": "GET_FORWARD_MSG", + "args": { + "message_id": "7123456789012345678" # 必需,合并转发消息ID(字符串) + } +} +``` + +**返回数据示例:** +```python +{ + "messages": [ + { + "sender": { + "user_id": 87654321, + "nickname": "昵称", + "sex": "male", + "age": 18, + "card": "群名片", + "level": "1", + "role": "member" + }, + "time": 1234567890, + "message": [...] # 消息段数组 + }, + ... + ] +} +``` \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index 646d0a9..4deadb0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -7,13 +7,30 @@ from .logger import logger class CommandType(Enum): """命令类型""" + # 操作类命令 GROUP_BAN = "set_group_ban" # 禁言用户 GROUP_WHOLE_BAN = "set_group_whole_ban" # 群全体禁言 GROUP_KICK = "set_group_kick" # 踢出群聊 + GROUP_KICK_MEMBERS = "set_group_kick_members" # 批量踢出群成员 + SET_GROUP_NAME = "set_group_name" # 设置群名 SEND_POKE = "send_poke" # 戳一戳 DELETE_MSG = "delete_msg" # 撤回消息 AI_VOICE_SEND = "send_group_ai_record" # 发送群AI语音 MESSAGE_LIKE = "message_like" # 给消息贴表情 + SET_QQ_PROFILE = "set_qq_profile" # 设置账号信息 + + # 查询类命令 + GET_LOGIN_INFO = "get_login_info" # 获取登录号信息 + GET_STRANGER_INFO = "get_stranger_info" # 获取陌生人信息 + GET_FRIEND_LIST = "get_friend_list" # 获取好友列表 + GET_GROUP_INFO = "get_group_info" # 获取群信息 + GET_GROUP_DETAIL_INFO = "get_group_detail_info" # 获取群详细信息 + GET_GROUP_LIST = "get_group_list" # 获取群列表 + GET_GROUP_AT_ALL_REMAIN = "get_group_at_all_remain" # 获取群@全体成员剩余次数 + GET_GROUP_MEMBER_INFO = "get_group_member_info" # 获取群成员信息 + GET_GROUP_MEMBER_LIST = "get_group_member_list" # 获取群成员列表 + GET_MSG = "get_msg" # 获取消息 + GET_FORWARD_MSG = "get_forward_msg" # 获取合并转发消息 def __str__(self) -> str: return self.value diff --git a/src/recv_handler/message_handler.py b/src/recv_handler/message_handler.py index c54186c..54a5b4b 100644 --- a/src/recv_handler/message_handler.py +++ b/src/recv_handler/message_handler.py @@ -307,9 +307,9 @@ class MessageHandler: else: logger.warning("video处理失败") case RealMessageType.json: - ret_seg = await self.handle_json_message(sub_message) - if ret_seg: - seg_message.append(ret_seg) + ret_segs = await self.handle_json_message(sub_message) + if ret_segs: + seg_message.extend(ret_segs) else: logger.warning("json处理失败") case RealMessageType.file: @@ -486,13 +486,13 @@ class MessageHandler: "url": url }) - async def handle_json_message(self, raw_message: dict) -> Seg | None: + async def handle_json_message(self, raw_message: dict) -> List[Seg] | None: """ 处理JSON卡片消息(小程序、分享、群公告等) Parameters: raw_message: dict: 原始消息 Returns: - seg_data: Seg: 处理后的消息段 + seg_data: List[Seg]: 处理后的消息段列表(可能包含文本和图片) """ message_data: dict = raw_message.get("data") json_data: str = message_data.get("data") @@ -505,90 +505,241 @@ class MessageHandler: # 尝试解析JSON获取详细信息 parsed_json = json.loads(json_data) app = parsed_json.get("app", "") + meta = parsed_json.get("meta", {}) - # 检查是否为群公告 + # 群公告(由于图片URL是加密的,因此无法读取) if app == "com.tencent.mannounce": - meta = parsed_json.get("meta", {}) mannounce = meta.get("mannounce", {}) - title_encoded = mannounce.get("title", "") - text_encoded = mannounce.get("text", "") - - # 解码Base64编码的标题和内容 - title = "" - text = "" - try: - if title_encoded: - title = base64.b64decode(title_encoded).decode("utf-8") - if text_encoded: - text = base64.b64decode(text_encoded).decode("utf-8") - except Exception as e: - logger.warning(f"群公告Base64解码失败: {e}") - # 降级使用原始值 - title = title_encoded - text = text_encoded - - # 构建群公告文本 - announce_text = "[群公告]" - if title: - announce_text += f"\n标题: {title}" - if text: - announce_text += f"\n内容: {text}" - - return Seg(type="text", data=announce_text) + title = mannounce.get("title", "") + text = mannounce.get("text", "") + encode_flag = mannounce.get("encode", 0) + if encode_flag == 1: + try: + if title: + title = base64.b64decode(title).decode("utf-8", errors="ignore") + if text: + text = base64.b64decode(text).decode("utf-8", errors="ignore") + except Exception as e: + logger.warning(f"群公告Base64解码失败: {e}") + if title and text: + content = f"[{title}]:{text}" + elif title: + content = f"[{title}]" + elif text: + content = f"{text}" + else: + content = "[群公告]" + return [Seg(type="text", data=content)] - # 检查是否为音乐卡片 + # 音乐卡片 if app in ("com.tencent.music.lua", "com.tencent.structmsg"): - meta = parsed_json.get("meta", {}) music = meta.get("music", {}) - - # 尝试从music字段提取信息 if music: title = music.get("title", "") singer = music.get("desc", "") or music.get("singer", "") jump_url = music.get("jumpUrl", "") or music.get("jump_url", "") music_url = music.get("musicUrl", "") or music.get("music_url", "") - tag = music.get("tag", "") # 音乐来源标签,如"网易云音乐" - preview = music.get("preview", "") # 封面图URL + tag = music.get("tag", "") + preview = music.get("preview", "") - # 返回结构化的音乐卡片数据 - return Seg(type="music_card", data={ + return [Seg(type="music_card", data={ "title": title, "singer": singer, "jump_url": jump_url, "music_url": music_url, "tag": tag, "preview": preview - }) + })] - # 检查是否为小程序分享(如B站视频分享) + # QQ小程序分享(含预览图) if app == "com.tencent.miniapp_01": - meta = parsed_json.get("meta", {}) detail = meta.get("detail_1", {}) - if detail: - title = detail.get("title", "") # 小程序名称,如"哔哩哔哩" - desc = detail.get("desc", "") # 分享内容描述 - url = detail.get("url", "") # 小程序链接 - qqdocurl = detail.get("qqdocurl", "") # 原始链接(如B站链接) - preview = detail.get("preview", "") # 预览图 - icon = detail.get("icon", "") # 小程序图标 + title = detail.get("title", "") + desc = detail.get("desc", "") + url = detail.get("url", "") + qqdocurl = detail.get("qqdocurl", "") + preview_url = detail.get("preview", "") + icon = detail.get("icon", "") - # 返回结构化的小程序卡片数据 - return Seg(type="miniapp_card", data={ + seg_list = [Seg(type="miniapp_card", data={ "title": title, "desc": desc, "url": url, "source_url": qqdocurl, - "preview": preview, + "preview": preview_url, "icon": icon - }) + })] + + # 下载预览图 + if preview_url: + try: + image_base64 = await get_image_base64(preview_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"QQ小程序预览图下载失败: {e}") + + return seg_list + + # 礼物消息 + if app == "com.tencent.giftmall.giftark": + giftark = meta.get("giftark", {}) + if giftark: + gift_name = giftark.get("title", "礼物") + desc = giftark.get("desc", "") + gift_text = f"[赠送礼物: {gift_name}]" + if desc: + gift_text += f"\n{desc}" + return [Seg(type="text", data=gift_text)] + + # 推荐联系人 + if app == "com.tencent.contact.lua": + contact_info = meta.get("contact", {}) + name = contact_info.get("nickname", "未知联系人") + tag = contact_info.get("tag", "推荐联系人") + return [Seg(type="text", data=f"[{tag}] {name}")] + + # 推荐群聊 + if app == "com.tencent.troopsharecard": + contact_info = meta.get("contact", {}) + name = contact_info.get("nickname", "未知群聊") + tag = contact_info.get("tag", "推荐群聊") + return [Seg(type="text", data=f"[{tag}] {name}")] + + # 图文分享(如 哔哩哔哩HD、网页、群精华等) + if app == "com.tencent.tuwen.lua": + news = meta.get("news", {}) + title = news.get("title", "未知标题") + desc = (news.get("desc", "") or "").replace("[图片]", "").strip() + tag = news.get("tag", "图文分享") + preview_url = news.get("preview", "") + if tag and title and tag in title: + title = title.replace(tag, "", 1).strip(":: -— ") + text_content = f"[{tag}] {title}:{desc}" + seg_list = [Seg(type="text", data=text_content)] + + # 下载预览图 + if preview_url: + try: + image_base64 = await get_image_base64(preview_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"图文预览图下载失败: {e}") + + return seg_list + + # 群相册(含预览图) + if app == "com.tencent.feed.lua": + feed = meta.get("feed", {}) + title = feed.get("title", "群相册") + tag = feed.get("tagName", "群相册") + desc = feed.get("forwardMessage", "") + cover_url = feed.get("cover", "") + if tag and title and tag in title: + title = title.replace(tag, "", 1).strip(":: -— ") + text_content = f"[{tag}] {title}:{desc}" + seg_list = [Seg(type="text", data=text_content)] + + # 下载封面图 + if cover_url: + try: + image_base64 = await get_image_base64(cover_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"群相册封面下载失败: {e}") + + return seg_list + + # QQ收藏分享(含预览图) + if app == "com.tencent.template.qqfavorite.share": + news = meta.get("news", {}) + desc = news.get("desc", "").replace("[图片]", "").strip() + tag = news.get("tag", "QQ收藏") + preview_url = news.get("preview", "") + seg_list = [Seg(type="text", data=f"[{tag}] {desc}")] + + # 下载预览图 + if preview_url: + try: + image_base64 = await get_image_base64(preview_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"QQ收藏预览图下载失败: {e}") + + return seg_list + + # QQ空间分享(含预览图) + if app == "com.tencent.miniapp.lua": + miniapp = meta.get("miniapp", {}) + title = miniapp.get("title", "未知标题") + tag = miniapp.get("tag", "QQ空间") + preview_url = miniapp.get("preview", "") + seg_list = [Seg(type="text", data=f"[{tag}] {title}")] + + # 下载预览图 + if preview_url: + try: + image_base64 = await get_image_base64(preview_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"QQ空间预览图下载失败: {e}") + + return seg_list + + # QQ频道分享(含预览图) + if app == "com.tencent.forum": + detail = meta.get("detail") if isinstance(meta, dict) else None + if detail: + feed = detail.get("feed", {}) + poster = detail.get("poster", {}) + channel_info = detail.get("channel_info", {}) + guild_name = channel_info.get("guild_name", "") + nick = poster.get("nick", "QQ用户") + title = feed.get("title", {}).get("contents", [{}])[0].get("text_content", {}).get("text", "帖子") + face_content = "" + for item in feed.get("contents", {}).get("contents", []): + emoji = item.get("emoji_content") + if emoji: + eid = emoji.get("id") + if eid in qq_face: + face_content += qq_face.get(eid, "") + + seg_list = [Seg(type="text", data=f"[频道帖子] [{guild_name}]{nick}:{title}{face_content}")] + + # 下载帖子中的图片 + pic_urls = [img.get("pic_url") for img in feed.get("images", []) if img.get("pic_url")] + for pic_url in pic_urls: + try: + image_base64 = await get_image_base64(pic_url) + seg_list.append(Seg(type="image", data=image_base64)) + except Exception as e: + logger.error(f"QQ频道图片下载失败: {e}") + + return seg_list + + # QQ地图位置分享 + if app == "com.tencent.map": + location = meta.get("Location.Search", {}) + name = location.get("name", "未知地点") + address = location.get("address", "") + return [Seg(type="text", data=f"[位置] {address} · {name}")] + + # QQ一起听歌 + if app == "com.tencent.together": + invite = (meta or {}).get("invite", {}) + title = invite.get("title") or "一起听歌" + summary = invite.get("summary") or "" + return [Seg(type="text", data=f"[{title}] {summary}")] # 其他卡片消息使用prompt字段 prompt = parsed_json.get("prompt", "[卡片消息]") - return Seg(type="text", data=prompt) + return [Seg(type="text", data=prompt)] except json.JSONDecodeError: logger.warning("JSON消息解析失败") - return Seg(type="text", data="[卡片消息]") + return [Seg(type="text", data="[卡片消息]")] + except Exception as e: + logger.error(f"JSON消息处理异常: {e}") + return [Seg(type="text", data="[卡片消息]")] async def handle_file_message(self, raw_message: dict) -> Seg | None: """ diff --git a/src/recv_handler/message_sending.py b/src/recv_handler/message_sending.py index 6ba7bf4..2d92f02 100644 --- a/src/recv_handler/message_sending.py +++ b/src/recv_handler/message_sending.py @@ -4,6 +4,13 @@ from src.logger import logger from maim_message import MessageBase, Router +# 消息大小限制 (字节) +# WebSocket 服务端限制为 100MB,这里设置 95MB 留一点余量 +MAX_MESSAGE_SIZE_BYTES = 95 * 1024 * 1024 # 95MB +MAX_MESSAGE_SIZE_KB = MAX_MESSAGE_SIZE_BYTES / 1024 +MAX_MESSAGE_SIZE_MB = MAX_MESSAGE_SIZE_KB / 1024 + + class MessageSending: """ 负责把消息发送到麦麦 @@ -24,10 +31,27 @@ class MessageSending: # 计算消息大小用于调试 msg_dict = message_base.to_dict() msg_json = json.dumps(msg_dict, ensure_ascii=False) - msg_size_kb = len(msg_json.encode('utf-8')) / 1024 + msg_size_bytes = len(msg_json.encode('utf-8')) + msg_size_kb = msg_size_bytes / 1024 + msg_size_mb = msg_size_kb / 1024 + logger.debug(f"发送消息大小: {msg_size_kb:.2f} KB") + + # 检查消息是否超过大小限制 + if msg_size_bytes > MAX_MESSAGE_SIZE_BYTES: + logger.error( + f"消息大小 ({msg_size_mb:.2f} MB) 超过限制 ({MAX_MESSAGE_SIZE_MB:.0f} MB)," + f"消息已被丢弃以避免连接断开" + ) + logger.warning( + f"被丢弃的消息来源: platform={message_base.message_info.platform}, " + f"group_id={message_base.message_info.group_info.group_id if message_base.message_info.group_info else 'N/A'}, " + f"user_id={message_base.message_info.user_info.user_id if message_base.message_info.user_info else 'N/A'}" + ) + return False + if msg_size_kb > 1024: # 超过 1MB 时警告 - logger.warning(f"发送的消息较大 ({msg_size_kb:.2f} KB),可能导致传输问题") + logger.warning(f"发送的消息较大 ({msg_size_mb:.2f} MB),可能导致传输延迟") send_status = await self.maibot_router.send_message(message_base) if not send_status: @@ -37,6 +61,7 @@ class MessageSending: except Exception as e: logger.error(f"发送消息失败: {str(e)}") logger.error("请检查与MaiBot之间的连接") + return False async def send_custom_message(self, custom_message: Dict, platform: str, message_type: str) -> bool: """ diff --git a/src/recv_handler/meta_event_handler.py b/src/recv_handler/meta_event_handler.py index 289e551..40f5a1a 100644 --- a/src/recv_handler/meta_event_handler.py +++ b/src/recv_handler/meta_event_handler.py @@ -25,14 +25,26 @@ class MetaEventHandler: logger.success(f"Bot {self_id} 连接成功") asyncio.create_task(self.check_heartbeat(self_id)) elif event_type == MetaEventType.heartbeat: - if message["status"].get("online") and message["status"].get("good"): + self_id = message.get("self_id") + status = message.get("status", {}) + is_online = status.get("online", False) + is_good = status.get("good", False) + + if is_online and is_good: + # 正常心跳 if not self._interval_checking: - asyncio.create_task(self.check_heartbeat()) + asyncio.create_task(self.check_heartbeat(self_id)) self.last_heart_beat = time.time() - self.interval = message.get("interval") / 1000 + self.interval = message.get("interval", 30000) / 1000 else: - self_id = message.get("self_id") - logger.warning(f"Bot {self_id} Napcat 端异常!") + # Bot 离线或状态异常 + if not is_online: + logger.error(f"🔴 Bot {self_id} 已下线 (online=false)") + logger.warning("Bot 可能被踢下线、网络断开或主动退出登录") + elif not is_good: + logger.warning(f"⚠️ Bot {self_id} 状态异常 (good=false)") + else: + logger.warning(f"Bot {self_id} Napcat 端异常!") async def check_heartbeat(self, id: int) -> None: self._interval_checking = True diff --git a/src/recv_handler/notice_handler.py b/src/recv_handler/notice_handler.py index ccfc633..add8913 100644 --- a/src/recv_handler/notice_handler.py +++ b/src/recv_handler/notice_handler.py @@ -10,6 +10,7 @@ from src.database import BanUser, db_manager, is_identical from . import NoticeType, ACCEPT_FORMAT from .message_sending import message_send_instance from .message_handler import message_handler +from .qq_emoji_list import qq_face from maim_message import FormatInfo, UserInfo, GroupInfo, Seg, BaseMessageInfo, MessageBase from src.utils import ( @@ -402,185 +403,13 @@ class NoticeHandler: likes = raw_message.get("likes", []) message_id = raw_message.get("message_id") - # 构建表情文本 + # 构建表情文本,直接使用 qq_face 映射 emoji_texts = [] - # QQ 官方表情映射表 (EmojiType=1 为 QQ 系统表情,EmojiType=2 为 Emoji Unicode) - emoji_map = { - # QQ 系统表情 (Type 1) - "4": "得意", - "5": "流泪", - "8": "睡", - "9": "大哭", - "10": "尴尬", - "12": "调皮", - "14": "微笑", - "16": "酷", - "21": "可爱", - "23": "傲慢", - "24": "饥饿", - "25": "困", - "26": "惊恐", - "27": "流汗", - "28": "憨笑", - "29": "悠闲", - "30": "奋斗", - "32": "疑问", - "33": "嘘", - "34": "晕", - "38": "敲打", - "39": "再见", - "41": "发抖", - "42": "爱情", - "43": "跳跳", - "49": "拥抱", - "53": "蛋糕", - "60": "咖啡", - "63": "玫瑰", - "66": "爱心", - "74": "太阳", - "75": "月亮", - "76": "赞", - "78": "握手", - "79": "胜利", - "85": "飞吻", - "89": "西瓜", - "96": "冷汗", - "97": "擦汗", - "98": "抠鼻", - "99": "鼓掌", - "100": "糗大了", - "101": "坏笑", - "102": "左哼哼", - "103": "右哼哼", - "104": "哈欠", - "106": "委屈", - "109": "左亲亲", - "111": "可怜", - "116": "示爱", - "118": "抱拳", - "120": "拳头", - "122": "爱你", - "123": "NO", - "124": "OK", - "125": "转圈", - "129": "挥手", - "144": "喝彩", - "147": "棒棒糖", - "171": "茶", - "173": "泪奔", - "174": "无奈", - "175": "卖萌", - "176": "小纠结", - "179": "doge", - "180": "惊喜", - "181": "骚扰", - "182": "笑哭", - "183": "我最美", - "201": "点赞", - "203": "托脸", - "212": "托腮", - "214": "啵啵", - "219": "蹭一蹭", - "222": "抱抱", - "227": "拍手", - "232": "佛系", - "240": "喷脸", - "243": "甩头", - "246": "加油抱抱", - "262": "脑阔疼", - "264": "捂脸", - "265": "辣眼睛", - "266": "哦哟", - "267": "头秃", - "268": "问号脸", - "269": "暗中观察", - "270": "emm", - "271": "吃瓜", - "272": "呵呵哒", - "273": "我酸了", - "277": "汪汪", - "278": "汗", - "281": "无眼笑", - "282": "敬礼", - "284": "面无表情", - "285": "摸鱼", - "287": "哦", - "289": "睁眼", - "290": "敲开心", - "293": "摸锦鲤", - "294": "期待", - "297": "拜谢", - "298": "元宝", - "299": "牛啊", - "305": "右亲亲", - "306": "牛气冲天", - "307": "喵喵", - "314": "仔细分析", - "315": "加油", - "318": "崇拜", - "319": "比心", - "320": "庆祝", - "322": "拒绝", - "324": "吃糖", - "326": "生气", - # Unicode Emoji (Type 2) - "9728": "☀", - "9749": "☕", - "9786": "☺", - "10024": "✨", - "10060": "❌", - "10068": "❔", - "127801": "🌹", - "127817": "🍉", - "127822": "🍎", - "127827": "🍓", - "127836": "🍜", - "127838": "🍞", - "127847": "🍧", - "127866": "🍺", - "127867": "🍻", - "127881": "🎉", - "128027": "🐛", - "128046": "🐮", - "128051": "🐳", - "128053": "🐵", - "128074": "👊", - "128076": "👌", - "128077": "👍", - "128079": "👏", - "128089": "👙", - "128102": "👦", - "128104": "👨", - "128147": "💓", - "128157": "💝", - "128164": "💤", - "128166": "💦", - "128168": "💨", - "128170": "💪", - "128235": "📫", - "128293": "🔥", - "128513": "😁", - "128514": "😂", - "128516": "😄", - "128522": "😊", - "128524": "😌", - "128527": "😏", - "128530": "😒", - "128531": "😓", - "128532": "😔", - "128536": "😘", - "128538": "😚", - "128540": "😜", - "128541": "😝", - "128557": "😭", - "128560": "😰", - "128563": "😳", - } - for like in likes: - emoji_id = like.get("emoji_id", "") + emoji_id = str(like.get("emoji_id", "")) count = like.get("count", 1) - emoji = emoji_map.get(emoji_id, f"表情{emoji_id}") + # 使用 qq_face 字典获取表情描述 + emoji = qq_face.get(emoji_id, f"[表情:未知{emoji_id}]") if count > 1: emoji_texts.append(f"{emoji}x{count}") else: diff --git a/src/recv_handler/qq_emoji_list.py b/src/recv_handler/qq_emoji_list.py index 51c3232..3b3c8bb 100644 --- a/src/recv_handler/qq_emoji_list.py +++ b/src/recv_handler/qq_emoji_list.py @@ -31,7 +31,7 @@ qq_face: dict = { "30": "[表情:奋斗]", "31": "[表情:咒骂]", "32": "[表情:疑问]", - "33": "[表情: 嘘]", + "33": "[表情:嘘]", "34": "[表情:晕]", "35": "[表情:折磨]", "36": "[表情:衰]", @@ -117,7 +117,7 @@ qq_face: dict = { "268": "[表情:问号脸]", "269": "[表情:暗中观察]", "270": "[表情:emm]", - "271": "[表情:吃 瓜]", + "271": "[表情:吃瓜]", "272": "[表情:呵呵哒]", "273": "[表情:我酸了]", "277": "[表情:汪汪]", @@ -146,7 +146,7 @@ qq_face: dict = { "314": "[表情:仔细分析]", "317": "[表情:菜汪]", "318": "[表情:崇拜]", - "319": "[表情: 比心]", + "319": "[表情:比心]", "320": "[表情:庆祝]", "323": "[表情:嫌弃]", "324": "[表情:吃糖]", @@ -175,13 +175,65 @@ qq_face: dict = { "355": "[表情:耶]", "356": "[表情:666]", "357": "[表情:裂开]", - "392": "[表情:龙年 快乐]", + "392": "[表情:龙年快乐]", "393": "[表情:新年中龙]", "394": "[表情:新年大龙]", "395": "[表情:略略略]", + "128522": "[表情:嘿嘿]", + "128524": "[表情:羞涩]", + "128538": "[表情:亲亲]", + "128531": "[表情:汗]", + "128560": "[表情:紧张]", + "128541": "[表情:吐舌]", + "128513": "[表情:呲牙]", + "128540": "[表情:淘气]", + "9786": "[表情:可爱]", + "128532": "[表情:失落]", + "128516": "[表情:高兴]", + "128527": "[表情:哼哼]", + "128530": "[表情:不屑]", + "128563": "[表情:瞪眼]", + "128536": "[表情:飞吻]", + "128557": "[表情:大哭]", + "128514": "[表情:激动]", + "128170": "[表情:肌肉]", + "128074": "[表情:拳头]", + "128077": "[表情:厉害]", + "128079": "[表情:鼓掌]", + "128076": "[表情:好的]", + "127836": "[表情:拉面]", + "127847": "[表情:刨冰]", + "127838": "[表情:面包]", + "127866": "[表情:啤酒]", + "127867": "[表情:干杯]", + "9749": "[表情:咖啡]", + "127822": "[表情:苹果]", + "127827": "[表情:草莓]", + "127817": "[表情:西瓜]", + "127801": "[表情:玫瑰]", + "127881": "[表情:庆祝]", + "128157": "[表情:礼物]", + "10024": "[表情:闪光]", + "128168": "[表情:吹气]", + "128166": "[表情:水]", + "128293": "[表情:火]", + "128164": "[表情:睡觉]", + "128235": "[表情:邮箱]", + "128103": "[表情:女孩]", + "128102": "[表情:男孩]", + "128053": "[表情:猴]", + "128046": "[表情:牛]", + "128027": "[表情:虫]", + "128051": "[表情:鲸鱼]", + "9728": "[表情:晴天]", + "10068": "[表情:问号]", + "128147": "[表情:爱心]", + "10060": "[表情:错误]", + "128089": "[表情:内衣]", + "128104": "[表情:爸爸]", "😊": "[表情:嘿嘿]", "😌": "[表情:羞涩]", - "😚": "[ 表情:亲亲]", + "😚": "[表情:亲亲]", "😓": "[表情:汗]", "😰": "[表情:紧张]", "😝": "[表情:吐舌]", @@ -200,7 +252,7 @@ qq_face: dict = { "😂": "[表情:激动]", "💪": "[表情:肌肉]", "👊": "[表情:拳头]", - "👍": "[表情 :厉害]", + "👍": "[表情:厉害]", "👏": "[表情:鼓掌]", "👎": "[表情:鄙视]", "🙏": "[表情:合十]", @@ -245,6 +297,6 @@ qq_face: dict = { "☀": "[表情:晴天]", "❔": "[表情:问号]", "🔫": "[表情:手枪]", - "💓": "[表情:爱 心]", + "💓": "[表情:爱心]", "🏪": "[表情:便利店]", -} +} \ No newline at end of file diff --git a/src/send_handler/main_send_handler.py b/src/send_handler/main_send_handler.py index 8cce8a9..cc2c945 100644 --- a/src/send_handler/main_send_handler.py +++ b/src/send_handler/main_send_handler.py @@ -1,4 +1,5 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional +import time from maim_message import ( UserInfo, GroupInfo, @@ -10,6 +11,7 @@ from src.logger import logger from .send_command_handler import SendCommandHandleClass from .send_message_handler import SendMessageHandleClass from .nc_sending import nc_message_sender +from src.recv_handler.message_sending import message_send_instance class SendHandler: @@ -34,21 +36,89 @@ class SendHandler: 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 = seg_data.get('name', 'UNKNOWN') + try: command, args_dict = SendCommandHandleClass.handle_command(seg_data, group_info) except Exception as e: logger.error(f"处理命令时出错: {str(e)}") + # 发送错误响应给麦麦 + await self._send_command_response( + platform=message_info.platform, + command_name=command_name, + success=False, + error=str(e) + ) return if not command or not args_dict: logger.error("命令或参数缺失") + await self._send_command_response( + platform=message_info.platform, + command_name=command_name, + success=False, + error="命令或参数缺失" + ) return None response = await nc_message_sender.send_message_to_napcat(command, args_dict) + + # 根据响应状态发送结果给麦麦 if response.get("status") == "ok": - logger.info(f"命令 {seg_data.get('name')} 执行成功") + logger.info(f"命令 {command_name} 执行成功") + await self._send_command_response( + platform=message_info.platform, + command_name=command_name, + success=True, + data=response.get("data") + ) else: - logger.warning(f"命令 {seg_data.get('name')} 执行失败,napcat返回:{str(response)}") + logger.warning(f"命令 {command_name} 执行失败,napcat返回:{str(response)}") + await self._send_command_response( + platform=message_info.platform, + command_name=command_name, + success=False, + error=str(response), + data=response.get("data") # 有些错误响应也可能包含部分数据 + ) + + async def _send_command_response( + self, + platform: str, + command_name: str, + success: bool, + data: Optional[Dict] = None, + error: Optional[str] = None + ) -> None: + """发送命令响应回麦麦 + + Args: + platform: 平台标识 + command_name: 命令名称 + success: 是否执行成功 + data: 返回数据(成功时) + error: 错误信息(失败时) + """ + response_data = { + "command_name": command_name, + "success": success, + "timestamp": time.time() + } + + if data is not None: + response_data["data"] = data + if error: + response_data["error"] = error + + try: + await message_send_instance.send_custom_message( + custom_message=response_data, + platform=platform, + message_type="command_response" + ) + logger.debug(f"已发送命令响应: {command_name}, success={success}") + except Exception as e: + logger.error(f"发送命令响应失败: {e}") async def send_normal_message(self, raw_message_base: MessageBase) -> None: """ diff --git a/src/send_handler/send_command_handler.py b/src/send_handler/send_command_handler.py index d7eeec1..e8a37ba 100644 --- a/src/send_handler/send_command_handler.py +++ b/src/send_handler/send_command_handler.py @@ -123,29 +123,108 @@ class SendCommandHandleClass: ) @staticmethod - @register_command(CommandType.GROUP_KICK, require_group=True) + @register_command(CommandType.GROUP_KICK, require_group=False) def handle_kick_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: """处理群成员踢出命令 Args: - args: 参数字典 {"qq_id": int} - group_info: 群聊信息(对应目标群聊) + args: 参数字典 {"group_id": int, "user_id": int, "reject_add_request": bool (可选)} + group_info: 群聊信息(可选,可自动获取 group_id) Returns: Tuple[str, Dict[str, Any]]: (action, params) """ - user_id: int = int(args["qq_id"]) - group_id: int = int(group_info.group_id) + if not args: + raise ValueError("群踢人命令缺少参数") + + # 优先从 args 获取 group_id,否则从 group_info 获取 + group_id = args.get("group_id") + if not group_id and group_info: + group_id = int(group_info.group_id) + + user_id = args.get("user_id") + + if not group_id: + raise ValueError("群踢人命令缺少必要参数: group_id") + if not user_id: + raise ValueError("群踢人命令缺少必要参数: user_id") + + group_id = int(group_id) + user_id = int(user_id) if group_id <= 0: raise ValueError("群组ID无效") if user_id <= 0: raise ValueError("用户ID无效") + + # reject_add_request 是可选参数,默认 False + reject_add_request = args.get("reject_add_request", False) + return ( CommandType.GROUP_KICK.value, { "group_id": group_id, "user_id": user_id, - "reject_add_request": False, # 不拒绝加群请求 + "reject_add_request": bool(reject_add_request), + }, + ) + + @staticmethod + @register_command(CommandType.GROUP_KICK_MEMBERS, require_group=False) + def handle_kick_members_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """处理批量踢出群成员命令 + + Args: + args: 参数字典 {"group_id": int, "user_id": List[int], "reject_add_request": bool (可选)} + group_info: 群聊信息(可选,可自动获取 group_id) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("批量踢人命令缺少参数") + + # 优先从 args 获取 group_id,否则从 group_info 获取 + group_id = args.get("group_id") + if not group_id and group_info: + group_id = int(group_info.group_id) + + user_id = args.get("user_id") + + if not group_id: + raise ValueError("批量踢人命令缺少必要参数: group_id") + if not user_id: + raise ValueError("批量踢人命令缺少必要参数: user_id") + + # 验证 user_id 是数组 + if not isinstance(user_id, list): + raise ValueError("user_id 必须是数组类型") + if len(user_id) == 0: + raise ValueError("user_id 数组不能为空") + + # 转换并验证每个 user_id + user_id_list = [] + for uid in user_id: + try: + uid_int = int(uid) + if uid_int <= 0: + raise ValueError(f"用户ID无效: {uid}") + user_id_list.append(uid_int) + except (ValueError, TypeError) as e: + raise ValueError(f"用户ID格式错误: {uid} - {str(e)}") from None + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + # reject_add_request 是可选参数,默认 False + reject_add_request = args.get("reject_add_request", False) + + return ( + CommandType.GROUP_KICK_MEMBERS.value, + { + "group_id": group_id, + "user_id": user_id_list, + "reject_add_request": bool(reject_add_request), }, ) @@ -178,6 +257,45 @@ class SendCommandHandleClass: }, ) + @staticmethod + @register_command(CommandType.SET_GROUP_NAME, require_group=False) + def handle_set_group_name_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """设置群名 + + Args: + args: 参数字典 {"group_id": int, "group_name": str} + group_info: 群聊信息(可选,可自动获取 group_id) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("设置群名命令缺少参数") + + # 优先从 args 获取 group_id,否则从 group_info 获取 + group_id = args.get("group_id") + if not group_id and group_info: + group_id = int(group_info.group_id) + + group_name = args.get("group_name") + + if not group_id: + raise ValueError("设置群名命令缺少必要参数: group_id") + if not group_name: + raise ValueError("设置群名命令缺少必要参数: group_name") + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + return ( + CommandType.SET_GROUP_NAME.value, + { + "group_id": group_id, + "group_name": str(group_name), + }, + ) + @staticmethod @register_command(CommandType.DELETE_MSG, require_group=False) def delete_msg_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: @@ -199,12 +317,40 @@ class SendCommandHandleClass: except (ValueError, TypeError) as e: raise ValueError(f"消息ID无效: {args['message_id']} - {str(e)}") from None - return ( - CommandType.DELETE_MSG.value, - { - "message_id": message_id, - }, - ) + return (CommandType.DELETE_MSG.value, {"message_id": message_id}) + + @staticmethod + @register_command(CommandType.SET_QQ_PROFILE, require_group=False) + def handle_set_qq_profile_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """设置账号信息 + + Args: + args: 参数字典 {"nickname": str, "personal_note": str (可选), "sex": str (可选)} + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("设置账号信息命令缺少参数") + + nickname = args.get("nickname") + if not nickname: + raise ValueError("设置账号信息命令缺少必要参数: nickname") + + params = {"nickname": str(nickname)} + + # 可选参数 + if "personal_note" in args: + params["personal_note"] = str(args["personal_note"]) + + if "sex" in args: + sex = str(args["sex"]).lower() + if sex not in ["male", "female", "unknown"]: + raise ValueError(f"性别参数无效: {sex},必须为 male/female/unknown 之一") + params["sex"] = sex + + return (CommandType.SET_QQ_PROFILE.value, params) @staticmethod @register_command(CommandType.AI_VOICE_SEND, require_group=True) @@ -276,3 +422,298 @@ class SendCommandHandleClass: "set": True, }, ) + + # ============ 查询类命令处理器 ============ + + @staticmethod + @register_command(CommandType.GET_LOGIN_INFO, require_group=False) + def handle_get_login_info_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取登录号信息(Bot自身信息) + + Args: + args: 参数字典(无需参数) + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + return (CommandType.GET_LOGIN_INFO.value, {}) + + @staticmethod + @register_command(CommandType.GET_STRANGER_INFO, require_group=False) + def handle_get_stranger_info_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取陌生人信息 + + Args: + args: 参数字典 {"user_id": int} + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("获取陌生人信息命令缺少参数") + + user_id = args.get("user_id") + if not user_id: + raise ValueError("获取陌生人信息命令缺少必要参数: user_id") + + user_id = int(user_id) + if user_id <= 0: + raise ValueError("用户ID无效") + + return ( + CommandType.GET_STRANGER_INFO.value, + {"user_id": user_id}, + ) + + @staticmethod + @register_command(CommandType.GET_FRIEND_LIST, require_group=False) + def handle_get_friend_list_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取好友列表 + + Args: + args: 参数字典 {"no_cache": bool} (可选,默认 false) + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # no_cache 参数是可选的,默认为 false + no_cache = args.get("no_cache", False) if args else False + + return (CommandType.GET_FRIEND_LIST.value, {"no_cache": bool(no_cache)}) + + @staticmethod + @register_command(CommandType.GET_GROUP_INFO, require_group=False) + def handle_get_group_info_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群信息 + + Args: + args: 参数字典 {"group_id": int} 或从 group_info 自动获取 + group_info: 群聊信息(可选) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # 优先从 args 获取,否则从 group_info 获取 + group_id = args.get("group_id") if args else None + if not group_id and group_info: + group_id = int(group_info.group_id) + + if not group_id: + raise ValueError("获取群信息命令缺少必要参数: group_id") + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + return ( + CommandType.GET_GROUP_INFO.value, + {"group_id": group_id}, + ) + + @staticmethod + @register_command(CommandType.GET_GROUP_DETAIL_INFO, require_group=False) + def handle_get_group_detail_info_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群详细信息 + + Args: + args: 参数字典 {"group_id": int} 或从 group_info 自动获取 + group_info: 群聊信息(可选) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # 优先从 args 获取,否则从 group_info 获取 + group_id = args.get("group_id") if args else None + if not group_id and group_info: + group_id = int(group_info.group_id) + + if not group_id: + raise ValueError("获取群详细信息命令缺少必要参数: group_id") + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + return ( + CommandType.GET_GROUP_DETAIL_INFO.value, + {"group_id": group_id}, + ) + + @staticmethod + @register_command(CommandType.GET_GROUP_LIST, require_group=False) + def handle_get_group_list_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群列表 + + Args: + args: 参数字典 {"no_cache": bool} (可选,默认 false) + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # no_cache 参数是可选的,默认为 false + no_cache = args.get("no_cache", False) if args else False + + return (CommandType.GET_GROUP_LIST.value, {"no_cache": bool(no_cache)}) + + @staticmethod + @register_command(CommandType.GET_GROUP_AT_ALL_REMAIN, require_group=False) + def handle_get_group_at_all_remain_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群@全体成员剩余次数 + + Args: + args: 参数字典 {"group_id": int} 或从 group_info 自动获取 + group_info: 群聊信息(可选) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # 优先从 args 获取,否则从 group_info 获取 + group_id = args.get("group_id") if args else None + if not group_id and group_info: + group_id = int(group_info.group_id) + + if not group_id: + raise ValueError("获取群@全体成员剩余次数命令缺少必要参数: group_id") + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + return ( + CommandType.GET_GROUP_AT_ALL_REMAIN.value, + {"group_id": group_id}, + ) + + @staticmethod + @register_command(CommandType.GET_GROUP_MEMBER_INFO, require_group=False) + def handle_get_group_member_info_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群成员信息 + + Args: + args: 参数字典 {"group_id": int, "user_id": int, "no_cache": bool} 或 group_id 从 group_info 自动获取 + group_info: 群聊信息(可选) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("获取群成员信息命令缺少参数") + + # 优先从 args 获取,否则从 group_info 获取 + group_id = args.get("group_id") + if not group_id and group_info: + group_id = int(group_info.group_id) + + user_id = args.get("user_id") + no_cache = args.get("no_cache", False) + + if not group_id: + raise ValueError("获取群成员信息命令缺少必要参数: group_id") + if not user_id: + raise ValueError("获取群成员信息命令缺少必要参数: user_id") + + group_id = int(group_id) + user_id = int(user_id) + if group_id <= 0: + raise ValueError("群组ID无效") + if user_id <= 0: + raise ValueError("用户ID无效") + + return ( + CommandType.GET_GROUP_MEMBER_INFO.value, + { + "group_id": group_id, + "user_id": user_id, + "no_cache": bool(no_cache), + }, + ) + + @staticmethod + @register_command(CommandType.GET_GROUP_MEMBER_LIST, require_group=False) + def handle_get_group_member_list_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取群成员列表 + + Args: + args: 参数字典 {"group_id": int, "no_cache": bool} 或 group_id 从 group_info 自动获取 + group_info: 群聊信息(可选) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + # 优先从 args 获取,否则从 group_info 获取 + group_id = args.get("group_id") if args else None + if not group_id and group_info: + group_id = int(group_info.group_id) + + no_cache = args.get("no_cache", False) if args else False + + if not group_id: + raise ValueError("获取群成员列表命令缺少必要参数: group_id") + + group_id = int(group_id) + if group_id <= 0: + raise ValueError("群组ID无效") + + return ( + CommandType.GET_GROUP_MEMBER_LIST.value, + { + "group_id": group_id, + "no_cache": bool(no_cache), + }, + ) + + @staticmethod + @register_command(CommandType.GET_MSG, require_group=False) + def handle_get_msg_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取消息详情 + + Args: + args: 参数字典 {"message_id": int} + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("获取消息命令缺少参数") + + message_id = args.get("message_id") + if not message_id: + raise ValueError("获取消息命令缺少必要参数: message_id") + + message_id = int(message_id) + if message_id <= 0: + raise ValueError("消息ID无效") + + return ( + CommandType.GET_MSG.value, + {"message_id": message_id}, + ) + + @staticmethod + @register_command(CommandType.GET_FORWARD_MSG, require_group=False) + def handle_get_forward_msg_command(args: Dict[str, Any], group_info: Optional[GroupInfo]) -> Tuple[str, Dict[str, Any]]: + """获取合并转发消息 + + Args: + args: 参数字典 {"message_id": str} + group_info: 群聊信息(不使用) + + Returns: + Tuple[str, Dict[str, Any]]: (action, params) + """ + if not args: + raise ValueError("获取合并转发消息命令缺少参数") + + message_id = args.get("message_id") + if not message_id: + raise ValueError("获取合并转发消息命令缺少必要参数: message_id") + + return ( + CommandType.GET_FORWARD_MSG.value, + {"message_id": str(message_id)}, + ) diff --git a/src/send_handler/send_message_handler.py b/src/send_handler/send_message_handler.py index b5c70f5..101ef8d 100644 --- a/src/send_handler/send_message_handler.py +++ b/src/send_handler/send_message_handler.py @@ -216,12 +216,61 @@ class SendMessageHandleClass: } @staticmethod - def handle_file_message(file_path: str) -> dict: - """处理文件消息""" - return { - "type": "file", - "data": {"file": f"file://{file_path}"}, - } + def handle_file_message(file_data) -> dict: + """处理文件消息 + + Args: + file_data: 可以是字符串(文件路径)或字典(完整文件信息) + - 字符串:简单的文件路径 + - 字典:包含 file, name, path, thumb, url 等字段 + + Returns: + NapCat 格式的文件消息段 + """ + # 如果是简单的字符串路径(兼容旧版本) + if isinstance(file_data, str): + return { + "type": "file", + "data": {"file": f"file://{file_data}"}, + } + + # 如果是完整的字典数据 + if isinstance(file_data, dict): + data = {} + + # file 字段是必需的 + if "file" in file_data: + file_value = file_data["file"] + # 如果是本地路径且没有协议前缀,添加 file:// 前缀 + if not any(file_value.startswith(prefix) for prefix in ["file://", "http://", "https://", "base64://"]): + data["file"] = f"file://{file_value}" + else: + data["file"] = file_value + else: + # 没有 file 字段,尝试使用 path 或 url + if "path" in file_data: + data["file"] = f"file://{file_data['path']}" + elif "url" in file_data: + data["file"] = file_data["url"] + else: + logger.warning("文件消息缺少必要的 file/path/url 字段") + return None + + # 添加可选字段 + if "name" in file_data: + data["name"] = file_data["name"] + if "thumb" in file_data: + data["thumb"] = file_data["thumb"] + if "url" in file_data and "file" not in file_data: + data["file"] = file_data["url"] + + return { + "type": "file", + "data": data, + } + + logger.warning(f"不支持的文件数据类型: {type(file_data)}") + return None @staticmethod def handle_imageurl_message(image_url: str) -> dict: