feat: 添加消息大小限制和消息发送处理

- 在 message_sending.py 中引入了 95MB 的最大消息大小限制,以防止连接中断。
- 为超出大小限制的消息添加了调试和错误日志记录。
- 增强了 meta_event_handler.py 中的心跳处理,提供了更详细的机器人状态日志。
- 重构了 notice_handler.py 中的表情符号处理,使用集中式 qq_face 映射代替硬编码值。
- 更新了 qq_emoji_list.py 以修正格式并添加新的表情符号映射。
- 改进了 main_send_handler.py 中的命令处理,以便向平台发送结构化响应。
- 扩展了 send_command_handler.py 中的命令处理,添加了用于设置组名和管理组成员的新命令。
- 增强了 send_message_handler.py 中的文件消息处理,以支持文件路径和详细的文件信息。
pull/76/head
墨梓柒 2026-01-03 19:59:02 +08:00
parent 616ab2b9d6
commit 7b6b0d9593
No known key found for this signature in database
GPG Key ID: 4A65B9DBA35F7635
10 changed files with 1338 additions and 276 deletions

View File

@ -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中可以从数据库获取如果工作正常的话
其中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": [...] # 消息段数组
},
...
]
}
```

View File

@ -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

View File

@ -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:
"""

View File

@ -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:
"""

View File

@ -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

View File

@ -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:

View File

@ -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 = {
"": "[表情:晴天]",
"": "[表情:问号]",
"🔫": "[表情:手枪]",
"💓": "[表情:爱 心]",
"💓": "[表情:爱心]",
"🏪": "[表情:便利店]",
}
}

View File

@ -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:
"""

View File

@ -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)},
)

View File

@ -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: