feat(广播): 重构广播插件为会话模式并支持合并转发消息

重构广播功能,从简单的回复转发模式改为更安全的会话模式:
1. 添加会话状态管理,60秒超时自动取消
2. 支持直接发送消息内容而非必须回复
3. 使用合并转发消息格式发送广播内容
4. 改进错误处理和状态报告
5. 添加类型提示和文档注释

同时修改相关API和模型:
1. 在GroupInfo中添加群备注和全员禁言字段
2. 改进get_forward_msg返回类型和兼容性处理
3. 清理不必要的Optional导入
This commit is contained in:
2026-01-07 00:24:47 +08:00
parent f33d31f5fc
commit c708761726
4 changed files with 105 additions and 59 deletions

View File

@@ -4,7 +4,7 @@
该模块定义了 `GroupAPI` Mixin 类,提供了所有与群组管理、成员操作 该模块定义了 `GroupAPI` Mixin 类,提供了所有与群组管理、成员操作
等相关的 OneBot v11 API 封装。 等相关的 OneBot v11 API 封装。
""" """
from typing import List, Dict, Any, Optional from typing import List, Dict, Any
import json import json
from core.redis_manager import redis_manager from core.redis_manager import redis_manager
from .base import BaseAPI from .base import BaseAPI

View File

@@ -105,7 +105,7 @@ class MessageAPI(BaseAPI):
""" """
return await self.call_api("get_msg", {"message_id": message_id}) return await self.call_api("get_msg", {"message_id": message_id})
async def get_forward_msg(self, id: str) -> Dict[str, Any]: async def get_forward_msg(self, id: str) -> List[Dict[str, Any]]:
""" """
获取合并转发消息的内容。 获取合并转发消息的内容。
@@ -113,9 +113,21 @@ class MessageAPI(BaseAPI):
id (str): 合并转发消息的 ID。 id (str): 合并转发消息的 ID。
Returns: Returns:
Dict[str, Any]: OneBot API 的响应数据,包含转发消息的节点列表。 List[Dict[str, Any]]: 转发消息的节点列表。
""" """
return await self.call_api("get_forward_msg", {"id": id}) forward_data = await self.call_api("get_forward_msg", {"id": id})
nodes = forward_data.get("data")
if not isinstance(nodes, list):
# 兼容某些实现可能将节点放在 'messages' 键下
data = forward_data.get('data', {})
if isinstance(data, dict):
nodes = data.get('messages')
if not isinstance(nodes, list):
raise ValueError("在 get_forward_msg 响应中找不到消息节点列表")
return nodes
async def send_group_forward_msg(self, group_id: int, messages: List[Dict[str, Any]]) -> Dict[str, Any]: async def send_group_forward_msg(self, group_id: int, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
""" """

View File

@@ -24,6 +24,12 @@ class GroupInfo:
max_member_count: int = 0 max_member_count: int = 0
"""最大成员数""" """最大成员数"""
group_remark: str = ""
"""群备注"""
group_all_shut: int = 0
"""是否全员禁言"""
@dataclass @dataclass
class GroupMemberInfo: class GroupMemberInfo:

View File

@@ -6,83 +6,111 @@
- 通过回复一条消息并发送指令,将该消息转发给机器人所在的所有群聊。 - 通过回复一条消息并发送指令,将该消息转发给机器人所在的所有群聊。
- 此插件不写入 __plugin_meta__保持隐藏。 - 此插件不写入 __plugin_meta__保持隐藏。
""" """
import asyncio
from core.command_manager import matcher from core.command_manager import matcher
from models import MessageEvent from models import MessageEvent, PrivateMessageEvent
from core.permission_manager import ADMIN from core.permission_manager import ADMIN
from core.logger import logger from core.logger import logger
# --- 会话状态管理 ---
# 结构: {user_id: asyncio.TimerHandle}
broadcast_sessions: dict[int, asyncio.TimerHandle] = {}
def cleanup_session(user_id: int):
"""
清理超时的广播会话。
"""
if user_id in broadcast_sessions:
del broadcast_sessions[user_id]
logger.info(f"[Broadcast] 会话 {user_id} 已超时,自动取消。")
@matcher.command("broadcast", "广播", permission=ADMIN) @matcher.command("broadcast", "广播", permission=ADMIN)
async def broadcast_message(event: MessageEvent): async def broadcast_start(event: MessageEvent):
""" """
广播指令处理器 广播指令的入口,启动一个等待用户消息的会话
:param event: 消息事件对象。
""" """
# 1. 检查是否为私聊消息 # 1. 仅限私聊
# 使用 hasattr 安全地检查 group_id 属性,避免 AttributeError if not isinstance(event, PrivateMessageEvent):
if hasattr(event, 'group_id') and event.group_id:
# 在群聊中调用时,静默处理,不予响应
return return
# 2. 检查是否回复了某条消息 user_id = event.user_id
reply = event.reply
if not reply: # 如果上一个会话的超时任务还在,先取消它
await event.reply("请通过“回复”一条您想广播的消息来使用此功能。") if user_id in broadcast_sessions:
broadcast_sessions[user_id].cancel()
await event.reply("已进入广播模式,请在 60 秒内发送您想要广播的消息内容。")
# 设置 60 秒超时
loop = asyncio.get_running_loop()
timeout_handler = loop.call_later(
60,
cleanup_session,
user_id
)
broadcast_sessions[user_id] = timeout_handler
@matcher.on_message()
async def handle_broadcast_content(event: MessageEvent):
"""
通用消息处理器,用于捕获广播模式下的消息输入。
将捕获到的消息打包成一个新的合并转发消息并广播。
"""
# 仅处理私聊消息,且用户在广播会话中
if not isinstance(event, PrivateMessageEvent) or event.user_id not in broadcast_sessions:
return return
# 3. 获取机器人所在的群聊列表 user_id = event.user_id
# 成功捕获到消息,取消超时任务并清理会话
broadcast_sessions[user_id].cancel()
del broadcast_sessions[user_id]
message_to_broadcast = event.message
if not message_to_broadcast:
await event.reply("捕获到的消息为空,已取消广播。")
return True
# --- 执行广播逻辑 ---
bot = event.bot bot = event.bot
try: try:
group_list = await bot.get_group_list() group_list = await bot.get_group_list()
if not group_list: if not group_list:
await event.reply("机器人目前没有加入任何群聊。") await event.reply("机器人目前没有加入任何群聊。")
return return True
except Exception as e: except Exception as e:
logger.error(f"[Broadcast] 获取群聊列表失败: {e}") logger.error(f"[Broadcast] 获取群聊列表失败: {e}")
await event.reply(f"获取群聊列表时发生错误,无法广播。错误信息: {e}") await event.reply(f"获取群聊列表时发生错误: {e}")
return return True
# 4. 获取被回复的消息内容 success_count, failed_count = 0, 0
try:
message_data = await bot.get_msg(reply.message_id)
message_content = message_data.get("message", "")
if not message_content:
await event.reply("无法获取被回复的消息内容,广播失败。")
return
except Exception as e:
logger.error(f"[Broadcast] 获取消息内容失败: {e}")
await event.reply(f"获取消息内容时发生错误: {e}")
return
# 5. 遍历所有群聊并发送消息
success_count = 0
failed_count = 0
total_groups = len(group_list) total_groups = len(group_list)
await event.reply(f"已收到广播内容,准备打包并向 {total_groups} 个群聊广播...")
await event.reply(f"准备向 {total_groups} 个群聊广播消息,请稍候...") # --- 将管理员发送的消息打包成一个单节点的合并转发消息 ---
try:
for group in group_list: nodes_to_send = [
group_id = group.group_id bot.build_forward_node(
if not group_id: user_id=event.user_id,
continue nickname=event.sender.nickname,
message=message_to_broadcast
try:
# 发送消息到群聊
await bot.send_group_msg(
group_id=group_id,
message=message_content
) )
]
except Exception as e:
logger.error(f"[Broadcast] 构建转发节点失败: {e}")
await event.reply(f"构建转发消息节点时发生错误: {e}")
return True
# --- 向所有群聊发送打包好的合并转发消息 ---
for group in group_list:
try:
await bot.send_group_forward_msg(group.group_id, nodes_to_send)
success_count += 1 success_count += 1
logger.info(f"[Broadcast] 已成功将消息发送至群聊: {group_id}")
except Exception as e: except Exception as e:
failed_count += 1 failed_count += 1
logger.error(f"[Broadcast] 发送消息至群聊 {group_id} 失败: {e}") logger.error(f"[Broadcast] 发至群聊 {group.group_id} 失败: {e}")
# 6. 向管理员报告结果 report = f"广播完成。\n总群聊: {total_groups}\n成功: {success_count}\n失败: {failed_count}"
report_message = ( await event.reply(report)
f"广播任务完成。\n"
f"总群聊数: {total_groups}\n" return True # 消费事件,防止其他处理器响应
f"成功: {success_count}\n"
f"失败: {failed_count}"
)
await event.reply(report_message)