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 类,提供了所有与群组管理、成员操作
等相关的 OneBot v11 API 封装。
"""
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any
import json
from core.redis_manager import redis_manager
from .base import BaseAPI

View File

@@ -105,7 +105,7 @@ class MessageAPI(BaseAPI):
"""
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。
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]:
"""

View File

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

View File

@@ -6,83 +6,111 @@
- 通过回复一条消息并发送指令,将该消息转发给机器人所在的所有群聊。
- 此插件不写入 __plugin_meta__保持隐藏。
"""
import asyncio
from core.command_manager import matcher
from models import MessageEvent
from models import MessageEvent, PrivateMessageEvent
from core.permission_manager import ADMIN
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)
async def broadcast_message(event: MessageEvent):
async def broadcast_start(event: MessageEvent):
"""
广播指令处理器
:param event: 消息事件对象。
广播指令的入口,启动一个等待用户消息的会话
"""
# 1. 检查是否为私聊消息
# 使用 hasattr 安全地检查 group_id 属性,避免 AttributeError
if hasattr(event, 'group_id') and event.group_id:
# 在群聊中调用时,静默处理,不予响应
# 1. 仅限私聊
if not isinstance(event, PrivateMessageEvent):
return
# 2. 检查是否回复了某条消息
reply = event.reply
if not reply:
await event.reply("请通过“回复”一条您想广播的消息来使用此功能。")
user_id = event.user_id
# 如果上一个会话的超时任务还在,先取消它
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
# 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
try:
group_list = await bot.get_group_list()
if not group_list:
await event.reply("机器人目前没有加入任何群聊。")
return
return True
except Exception as e:
logger.error(f"[Broadcast] 获取群聊列表失败: {e}")
await event.reply(f"获取群聊列表时发生错误,无法广播。错误信息: {e}")
return
await event.reply(f"获取群聊列表时发生错误: {e}")
return True
# 4. 获取被回复的消息内容
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
success_count, failed_count = 0, 0
total_groups = len(group_list)
await event.reply(f"已收到广播内容,准备打包并向 {total_groups} 个群聊广播...")
await event.reply(f"准备向 {total_groups} 个群聊广播消息,请稍候...")
for group in group_list:
group_id = group.group_id
if not group_id:
continue
try:
# 发送消息到群聊
await bot.send_group_msg(
group_id=group_id,
message=message_content
# --- 将管理员发送的消息打包成一个单节点的合并转发消息 ---
try:
nodes_to_send = [
bot.build_forward_node(
user_id=event.user_id,
nickname=event.sender.nickname,
message=message_to_broadcast
)
]
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
logger.info(f"[Broadcast] 已成功将消息发送至群聊: {group_id}")
except Exception as e:
failed_count += 1
logger.error(f"[Broadcast] 发送消息至群聊 {group_id} 失败: {e}")
logger.error(f"[Broadcast] 发至群聊 {group.group_id} 失败: {e}")
# 6. 向管理员报告结果
report_message = (
f"广播任务完成。\n"
f"总群聊数: {total_groups}\n"
f"成功: {success_count}\n"
f"失败: {failed_count}"
)
await event.reply(report_message)
report = f"广播完成。\n总群聊: {total_groups}\n成功: {success_count}\n失败: {failed_count}"
await event.reply(report)
return True # 消费事件,防止其他处理器响应