mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-14 13:42:56 +08:00
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled
* ⚡️ perf(image_utils): 优化图片哈希获取避免阻塞异步 * ✨ feat(llm): 增强 LLM 管理功能,支持纯文本列表输出,优化模型能力识别并新增提供商 - 【LLM 管理器】为 `llm list` 命令添加 `--text` 选项,支持以纯文本格式输出模型列表。 - 【LLM 配置】新增 `OpenRouter` LLM 提供商的默认配置。 - 【模型能力】增强 `get_model_capabilities` 函数的查找逻辑,支持模型名称分段匹配和更灵活的通配符匹配。 - 【模型能力】为 `Gemini` 模型能力注册表使用更通用的通配符模式。 - 【模型能力】新增 `GPT` 系列模型的详细能力定义,包括多模态输入输出和工具调用支持。 * ✨ feat(renderer): 添加 Jinja2 `inline_asset` 全局函数 - 新增 `RendererService._inline_asset_global` 方法,并注册为 Jinja2 全局函数 `inline_asset`。 - 允许模板通过 `{{ inline_asset('@namespace/path/to/asset.svg') }}` 直接内联已注册命名空间下的资源文件内容。 - 主要用于解决内联 SVG 时可能遇到的跨域安全问题。 - 【重构】优化 `ResourceResolver.resolve_asset_uri` 中对命名空间资源 (以 `@` 开头) 的解析逻辑,确保能够正确获取文件绝对路径并返回 URI。 - 改进 `RenderableComponent.get_extra_css`,使其在组件定义 `component_css` 时自动返回该 CSS 内容。 - 清理 `Renderable` 协议和 `RenderableComponent` 基类中已存在方法的 `[新增]` 标记。 * ✨ feat(tag): 添加标签克隆功能 - 新增 `tag clone <源标签名> <新标签名>` 命令,用于复制现有标签。 - 【优化】在 `tag create`, `tag edit --add`, `tag edit --set` 命令中,自动去重传入的群组ID,避免重复关联。 * ✨ feat(broadcast): 实现标签定向广播、强制发送及并发控制 - 【新功能】 - 新增标签定向广播功能,支持通过 `-t <标签名>` 或 `广播到 <标签名>` 命令向指定标签的群组发送消息 - 引入广播强制发送模式,允许绕过群组的任务阻断设置 - 实现广播并发控制,通过配置限制同时发送任务数量,避免API速率限制 - 优化视频消息处理,支持从URL下载视频内容并作为原始数据发送,提高跨平台兼容性 - 【配置】 - 添加 `DEFAULT_BROADCAST` 配置项,用于设置群组进群时广播功能的默认开关状态 - 添加 `BROADCAST_CONCURRENCY_LIMIT` 配置项,用于控制广播时的最大并发任务数 * ✨ feat(renderer): 支持组件变体样式收集 * ✨ feat(tag): 实现群组标签自动清理及手动清理功能 * 🐛 fix(gemini): 增加响应验证以处理内容过滤(promptFeedback) * 🐛 fix(codeql): 移除对 JavaScript 和 TypeScript 的分析支持 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
654 lines
24 KiB
Python
654 lines
24 KiB
Python
import base64
|
|
import json
|
|
from typing import Any
|
|
|
|
from nonebot.adapters import Bot, Event
|
|
from nonebot.adapters.onebot.v11 import Message as V11Message
|
|
from nonebot.adapters.onebot.v11 import MessageSegment as V11MessageSegment
|
|
from nonebot.exception import ActionFailed
|
|
import nonebot_plugin_alconna as alc
|
|
from nonebot_plugin_alconna import UniMessage
|
|
from nonebot_plugin_alconna.uniseg.segment import (
|
|
At,
|
|
AtAll,
|
|
CustomNode,
|
|
Image,
|
|
Reference,
|
|
Reply,
|
|
Text,
|
|
Video,
|
|
)
|
|
from nonebot_plugin_alconna.uniseg.tools import reply_fetch
|
|
from nonebot_plugin_session import EventSession
|
|
|
|
from zhenxun.models.group_console import GroupConsole
|
|
from zhenxun.services.log import logger
|
|
from zhenxun.services.tags import tag_manager as TagManager
|
|
from zhenxun.utils.common_utils import CommonUtils
|
|
from zhenxun.utils.http_utils import AsyncHttpx
|
|
from zhenxun.utils.message import MessageUtils
|
|
|
|
from .broadcast_manager import BroadcastManager
|
|
|
|
MAX_FORWARD_DEPTH = 3
|
|
|
|
|
|
async def _process_forward_content(
|
|
forward_content: Any, forward_id: str | None, bot: Bot, depth: int
|
|
) -> list[CustomNode]:
|
|
"""处理转发消息内容"""
|
|
nodes_for_alc = []
|
|
content_parsed = False
|
|
|
|
if forward_content:
|
|
nodes_from_content = None
|
|
if isinstance(forward_content, list):
|
|
nodes_from_content = forward_content
|
|
elif isinstance(forward_content, str):
|
|
try:
|
|
parsed_content = json.loads(forward_content)
|
|
if isinstance(parsed_content, list):
|
|
nodes_from_content = parsed_content
|
|
except Exception as json_e:
|
|
logger.debug(
|
|
f"[Depth {depth}] JSON解析失败: {json_e}",
|
|
"广播",
|
|
)
|
|
|
|
if nodes_from_content is not None:
|
|
logger.debug(
|
|
f"[D{depth}] 节点数: {len(nodes_from_content)}",
|
|
"广播",
|
|
)
|
|
content_parsed = True
|
|
for node_data in nodes_from_content:
|
|
node = await _create_custom_node_from_data(node_data, bot, depth + 1)
|
|
if node:
|
|
nodes_for_alc.append(node)
|
|
|
|
if not content_parsed and forward_id:
|
|
logger.debug(
|
|
f"[D{depth}] 尝试API调用ID: {forward_id}",
|
|
"广播",
|
|
)
|
|
try:
|
|
forward_data = await bot.call_api("get_forward_msg", id=forward_id)
|
|
nodes_list = None
|
|
|
|
if isinstance(forward_data, dict) and "messages" in forward_data:
|
|
nodes_list = forward_data["messages"]
|
|
elif (
|
|
isinstance(forward_data, dict)
|
|
and "data" in forward_data
|
|
and isinstance(forward_data["data"], dict)
|
|
and "message" in forward_data["data"]
|
|
):
|
|
nodes_list = forward_data["data"]["message"]
|
|
elif isinstance(forward_data, list):
|
|
nodes_list = forward_data
|
|
|
|
if nodes_list:
|
|
node_count = len(nodes_list)
|
|
logger.debug(
|
|
f"[D{depth + 1}] 节点:{node_count}",
|
|
"广播",
|
|
)
|
|
for node_data in nodes_list:
|
|
node = await _create_custom_node_from_data(
|
|
node_data, bot, depth + 1
|
|
)
|
|
if node:
|
|
nodes_for_alc.append(node)
|
|
else:
|
|
logger.warning(
|
|
f"[D{depth + 1}] ID:{forward_id}无节点",
|
|
"广播",
|
|
)
|
|
nodes_for_alc.append(
|
|
CustomNode(
|
|
uid="0",
|
|
name="错误",
|
|
content="[嵌套转发消息获取失败]",
|
|
)
|
|
)
|
|
except ActionFailed as af_e:
|
|
logger.error(
|
|
f"[D{depth + 1}] API失败: {af_e}",
|
|
"广播",
|
|
e=af_e,
|
|
)
|
|
nodes_for_alc.append(
|
|
CustomNode(
|
|
uid="0",
|
|
name="错误",
|
|
content="[嵌套转发消息获取失败]",
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[D{depth + 1}] 处理出错: {e}",
|
|
"广播",
|
|
e=e,
|
|
)
|
|
nodes_for_alc.append(
|
|
CustomNode(
|
|
uid="0",
|
|
name="错误",
|
|
content="[处理嵌套转发时出错]",
|
|
)
|
|
)
|
|
elif not content_parsed and not forward_id:
|
|
logger.warning(
|
|
f"[D{depth}] 转发段无内容也无ID",
|
|
"广播",
|
|
)
|
|
nodes_for_alc.append(
|
|
CustomNode(
|
|
uid="0",
|
|
name="错误",
|
|
content="[嵌套转发消息无法解析]",
|
|
)
|
|
)
|
|
elif content_parsed and not nodes_for_alc:
|
|
logger.warning(
|
|
f"[D{depth}] 解析成功但无有效节点",
|
|
"广播",
|
|
)
|
|
nodes_for_alc.append(
|
|
CustomNode(
|
|
uid="0",
|
|
name="信息",
|
|
content="[嵌套转发内容为空]",
|
|
)
|
|
)
|
|
|
|
return nodes_for_alc
|
|
|
|
|
|
async def _create_custom_node_from_data(
|
|
node_data: dict, bot: Bot, depth: int
|
|
) -> CustomNode | None:
|
|
"""从节点数据创建CustomNode"""
|
|
node_content_raw = node_data.get("message") or node_data.get("content")
|
|
if not node_content_raw:
|
|
logger.warning(f"[D{depth}] 节点缺少消息内容", "广播")
|
|
return None
|
|
|
|
sender = node_data.get("sender", {})
|
|
uid = str(sender.get("user_id", "10000"))
|
|
name = sender.get("nickname", f"用户{uid[:4]}")
|
|
|
|
extracted_uni_msg = await _extract_content_from_message(
|
|
node_content_raw, bot, depth
|
|
)
|
|
if not extracted_uni_msg:
|
|
return None
|
|
|
|
return CustomNode(uid=uid, name=name, content=extracted_uni_msg)
|
|
|
|
|
|
async def _extract_broadcast_content(
|
|
bot: Bot,
|
|
event: Event,
|
|
arp: alc.Arparma,
|
|
session: EventSession,
|
|
) -> UniMessage | None:
|
|
"""从命令参数或引用消息中提取广播内容"""
|
|
broadcast_content_msg: UniMessage | None = None
|
|
|
|
command_content_list = arp.all_matched_args.get("content", [])
|
|
|
|
processed_command_list = []
|
|
has_command_content = False
|
|
|
|
if command_content_list:
|
|
for item in command_content_list:
|
|
if isinstance(item, alc.Segment):
|
|
processed_command_list.append(item)
|
|
if not (isinstance(item, Text) and not item.text.strip()):
|
|
has_command_content = True
|
|
elif isinstance(item, str):
|
|
if item.strip():
|
|
processed_command_list.append(Text(item.strip()))
|
|
has_command_content = True
|
|
else:
|
|
logger.warning(
|
|
f"Unexpected type in command content: {type(item)}", "广播"
|
|
)
|
|
|
|
if has_command_content:
|
|
logger.debug("检测到命令参数内容,优先使用参数内容", "广播", session=session)
|
|
broadcast_content_msg = UniMessage(processed_command_list)
|
|
|
|
if not broadcast_content_msg.filter(
|
|
lambda x: not (isinstance(x, Text) and not x.text.strip())
|
|
):
|
|
logger.warning(
|
|
"命令参数内容解析后为空或只包含空白", "广播", session=session
|
|
)
|
|
broadcast_content_msg = None
|
|
|
|
if not broadcast_content_msg:
|
|
reply_segment_obj: Reply | None = await reply_fetch(event, bot)
|
|
if (
|
|
reply_segment_obj
|
|
and hasattr(reply_segment_obj, "msg")
|
|
and reply_segment_obj.msg
|
|
):
|
|
logger.debug(
|
|
"未检测到有效命令参数,检测到引用消息", "广播", session=session
|
|
)
|
|
raw_quoted_content = reply_segment_obj.msg
|
|
is_forward = False
|
|
forward_id = None
|
|
|
|
if isinstance(raw_quoted_content, V11Message):
|
|
for seg in raw_quoted_content:
|
|
if isinstance(seg, V11MessageSegment):
|
|
if seg.type == "forward":
|
|
forward_id = seg.data.get("id")
|
|
is_forward = bool(forward_id)
|
|
break
|
|
elif seg.type == "json":
|
|
try:
|
|
json_data_str = seg.data.get("data", "{}")
|
|
if isinstance(json_data_str, str):
|
|
import json
|
|
|
|
json_data = json.loads(json_data_str)
|
|
if (
|
|
json_data.get("app") == "com.tencent.multimsg"
|
|
or json_data.get("view") == "Forward"
|
|
) and json_data.get("meta", {}).get(
|
|
"detail", {}
|
|
).get("resid"):
|
|
forward_id = json_data["meta"]["detail"][
|
|
"resid"
|
|
]
|
|
is_forward = True
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
if is_forward and forward_id:
|
|
logger.info(
|
|
f"尝试获取并构造合并转发内容 (ID: {forward_id})",
|
|
"广播",
|
|
session=session,
|
|
)
|
|
nodes_to_forward: list[CustomNode] = []
|
|
try:
|
|
forward_data = await bot.call_api("get_forward_msg", id=forward_id)
|
|
nodes_list = None
|
|
if isinstance(forward_data, dict) and "messages" in forward_data:
|
|
nodes_list = forward_data["messages"]
|
|
elif (
|
|
isinstance(forward_data, dict)
|
|
and "data" in forward_data
|
|
and isinstance(forward_data["data"], dict)
|
|
and "message" in forward_data["data"]
|
|
):
|
|
nodes_list = forward_data["data"]["message"]
|
|
elif isinstance(forward_data, list):
|
|
nodes_list = forward_data
|
|
|
|
if nodes_list is not None:
|
|
for node_data in nodes_list:
|
|
node_sender = node_data.get("sender", {})
|
|
node_user_id = str(node_sender.get("user_id", "10000"))
|
|
node_nickname = node_sender.get(
|
|
"nickname", f"用户{node_user_id[:4]}"
|
|
)
|
|
node_content_raw = node_data.get(
|
|
"message"
|
|
) or node_data.get("content")
|
|
if node_content_raw:
|
|
extracted_node_uni_msg = (
|
|
await _extract_content_from_message(
|
|
node_content_raw, bot
|
|
)
|
|
)
|
|
if extracted_node_uni_msg:
|
|
nodes_to_forward.append(
|
|
CustomNode(
|
|
uid=node_user_id,
|
|
name=node_nickname,
|
|
content=extracted_node_uni_msg,
|
|
)
|
|
)
|
|
if nodes_to_forward:
|
|
broadcast_content_msg = UniMessage(
|
|
Reference(nodes=nodes_to_forward)
|
|
)
|
|
except ActionFailed:
|
|
await MessageUtils.build_message(
|
|
"获取合并转发消息失败,可能不支持此 API。"
|
|
).send(reply_to=True)
|
|
return None
|
|
except Exception as api_e:
|
|
logger.error(f"处理合并转发时出错: {api_e}", "广播", e=api_e)
|
|
await MessageUtils.build_message(
|
|
"处理合并转发消息时发生内部错误。"
|
|
).send(reply_to=True)
|
|
return None
|
|
else:
|
|
broadcast_content_msg = await _extract_content_from_message(
|
|
raw_quoted_content, bot
|
|
)
|
|
else:
|
|
logger.debug("未检测到命令参数和引用消息", "广播", session=session)
|
|
await MessageUtils.build_message("请提供广播内容或引用要广播的消息").send(
|
|
reply_to=True
|
|
)
|
|
return None
|
|
|
|
if not broadcast_content_msg:
|
|
logger.error(
|
|
"未能从命令参数或引用消息中获取有效的广播内容", "广播", session=session
|
|
)
|
|
await MessageUtils.build_message("错误:未能获取有效的广播内容。").send(
|
|
reply_to=True
|
|
)
|
|
return None
|
|
|
|
return broadcast_content_msg
|
|
|
|
|
|
async def _process_v11_segment(
|
|
seg_obj: V11MessageSegment | dict, depth: int, index: int, bot: Bot
|
|
) -> list[alc.Segment]:
|
|
"""处理V11消息段"""
|
|
result = []
|
|
seg_type = None
|
|
data_dict = None
|
|
|
|
if isinstance(seg_obj, V11MessageSegment):
|
|
seg_type = seg_obj.type
|
|
data_dict = seg_obj.data
|
|
elif isinstance(seg_obj, dict):
|
|
seg_type = seg_obj.get("type")
|
|
data_dict = seg_obj.get("data")
|
|
else:
|
|
return result
|
|
|
|
if not (seg_type and data_dict is not None):
|
|
logger.warning(f"[D{depth}] 跳过无效数据: {type(seg_obj)}", "广播")
|
|
return result
|
|
|
|
if seg_type == "text":
|
|
text_content = data_dict.get("text", "")
|
|
if isinstance(text_content, str) and text_content.strip():
|
|
result.append(Text(text_content))
|
|
elif seg_type == "image":
|
|
img_seg = None
|
|
if data_dict.get("url"):
|
|
img_seg = Image(url=data_dict["url"])
|
|
elif data_dict.get("file"):
|
|
file_val = data_dict["file"]
|
|
if isinstance(file_val, str) and file_val.startswith("base64://"):
|
|
b64_data = file_val[9:]
|
|
raw_bytes = base64.b64decode(b64_data)
|
|
img_seg = Image(raw=raw_bytes)
|
|
else:
|
|
img_seg = Image(path=file_val)
|
|
if img_seg:
|
|
result.append(img_seg)
|
|
else:
|
|
logger.warning(f"[Depth {depth}] V11 图片 {index} 缺少URL/文件", "广播")
|
|
elif seg_type == "at":
|
|
target_qq = data_dict.get("qq", "")
|
|
if target_qq.lower() == "all":
|
|
result.append(AtAll())
|
|
elif target_qq:
|
|
result.append(At(flag="user", target=target_qq))
|
|
elif seg_type == "video":
|
|
if url := data_dict.get("url"):
|
|
try:
|
|
logger.debug(f"[D{depth}] 正在下载视频用于广播: {url}", "广播")
|
|
video_bytes = await AsyncHttpx.get_content(url)
|
|
video_seg = Video(raw=video_bytes)
|
|
logger.debug(
|
|
f"[D{depth}] 视频下载成功, 大小: {len(video_bytes)} bytes",
|
|
"广播",
|
|
)
|
|
result.append(video_seg)
|
|
except Exception as e:
|
|
logger.error(f"[D{depth}] 广播时下载视频失败: {url}", "广播", e=e)
|
|
result.append(Text(f"[视频下载失败: {url}]"))
|
|
elif file_val := data_dict.get("file"):
|
|
if isinstance(file_val, str) and file_val.startswith("base64://"):
|
|
b64_data = file_val[9:]
|
|
raw_bytes = base64.b64decode(b64_data)
|
|
video_seg = Video(raw=raw_bytes)
|
|
result.append(video_seg)
|
|
else:
|
|
video_seg = Video(path=file_val)
|
|
result.append(video_seg)
|
|
return result
|
|
elif seg_type == "forward":
|
|
nested_forward_id = data_dict.get("id") or data_dict.get("resid")
|
|
nested_forward_content = data_dict.get("content")
|
|
|
|
logger.debug(f"[D{depth}] 嵌套转发ID: {nested_forward_id}", "广播")
|
|
|
|
nested_nodes = await _process_forward_content(
|
|
nested_forward_content, nested_forward_id, bot, depth
|
|
)
|
|
|
|
if nested_nodes:
|
|
result.append(Reference(nodes=nested_nodes))
|
|
else:
|
|
logger.warning(f"[D{depth}] 跳过类型: {seg_type}", "广播")
|
|
|
|
return result
|
|
|
|
|
|
async def _extract_content_from_message(
|
|
message_content: Any, bot: Bot, depth: int = 0
|
|
) -> UniMessage:
|
|
"""提取消息内容到UniMessage"""
|
|
temp_msg = UniMessage()
|
|
input_type_str = str(type(message_content))
|
|
|
|
if depth >= MAX_FORWARD_DEPTH:
|
|
logger.warning(
|
|
f"[Depth {depth}] 达到最大递归深度 {MAX_FORWARD_DEPTH},停止解析嵌套转发。",
|
|
"广播",
|
|
)
|
|
temp_msg.append(Text("[嵌套转发层数过多,内容已省略]"))
|
|
return temp_msg
|
|
|
|
segments_to_process = []
|
|
|
|
if isinstance(message_content, UniMessage):
|
|
segments_to_process = list(message_content)
|
|
elif isinstance(message_content, V11Message):
|
|
segments_to_process = list(message_content)
|
|
elif isinstance(message_content, list):
|
|
segments_to_process = message_content
|
|
elif (
|
|
isinstance(message_content, dict)
|
|
and "type" in message_content
|
|
and "data" in message_content
|
|
):
|
|
segments_to_process = [message_content]
|
|
elif isinstance(message_content, str):
|
|
if message_content.strip():
|
|
temp_msg.append(Text(message_content))
|
|
return temp_msg
|
|
else:
|
|
logger.warning(f"[Depth {depth}] 无法处理的输入类型: {input_type_str}", "广播")
|
|
return temp_msg
|
|
|
|
if segments_to_process:
|
|
for index, seg_obj in enumerate(segments_to_process):
|
|
try:
|
|
if isinstance(seg_obj, Text):
|
|
text_content = getattr(seg_obj, "text", None)
|
|
if isinstance(text_content, str) and text_content.strip():
|
|
temp_msg.append(seg_obj)
|
|
elif isinstance(seg_obj, Image):
|
|
if (
|
|
getattr(seg_obj, "url", None)
|
|
or getattr(seg_obj, "path", None)
|
|
or getattr(seg_obj, "raw", None)
|
|
):
|
|
temp_msg.append(seg_obj)
|
|
elif isinstance(seg_obj, At):
|
|
temp_msg.append(seg_obj)
|
|
elif isinstance(seg_obj, AtAll):
|
|
temp_msg.append(seg_obj)
|
|
elif isinstance(seg_obj, Video):
|
|
if (
|
|
getattr(seg_obj, "url", None)
|
|
or getattr(seg_obj, "path", None)
|
|
or getattr(seg_obj, "raw", None)
|
|
):
|
|
temp_msg.append(seg_obj)
|
|
logger.debug(f"[D{depth}] 处理Video对象成功", "广播")
|
|
else:
|
|
processed_segments = await _process_v11_segment(
|
|
seg_obj, depth, index, bot
|
|
)
|
|
temp_msg.extend(processed_segments)
|
|
except Exception as e_conv_seg:
|
|
logger.warning(
|
|
f"[D{depth}] 处理段 {index} 出错: {e_conv_seg}",
|
|
"广播",
|
|
e=e_conv_seg,
|
|
)
|
|
|
|
if not temp_msg and message_content:
|
|
logger.warning(f"未能从类型 {input_type_str} 中提取内容", "广播")
|
|
|
|
return temp_msg
|
|
|
|
|
|
async def get_broadcast_target_groups(
|
|
bot: Bot,
|
|
session: EventSession,
|
|
tag_name: str | None = None,
|
|
force_send: bool = False,
|
|
) -> tuple[list, list]:
|
|
"""获取广播目标群组和启用了广播功能的群组"""
|
|
target_groups_console: list[GroupConsole] = []
|
|
|
|
current_group_raw = getattr(session, "id2", None) or getattr(
|
|
session, "group_id", None
|
|
)
|
|
current_group_id = str(current_group_raw) if current_group_raw else None
|
|
|
|
logger.debug(f"当前群组ID: {current_group_id}", "广播")
|
|
|
|
if tag_name:
|
|
tagged_group_ids = await TagManager.resolve_tag_to_group_ids(tag_name, bot=bot)
|
|
if not tagged_group_ids:
|
|
return [], []
|
|
|
|
valid_groups = await GroupConsole.filter(group_id__in=tagged_group_ids)
|
|
|
|
if current_group_id:
|
|
target_groups_console = [
|
|
group
|
|
for group in valid_groups
|
|
if str(group.group_id) != current_group_id
|
|
]
|
|
excluded_msg = (
|
|
f",已排除当前群组({current_group_id})"
|
|
if any(
|
|
str(group.group_id) == current_group_id for group in valid_groups
|
|
)
|
|
else ""
|
|
)
|
|
broadcast_msg = (
|
|
f"向标签 '{tag_name}' 中的 {len(target_groups_console)} 个群组广播 "
|
|
f"(ForceSend: {force_send}){excluded_msg}"
|
|
)
|
|
logger.info(broadcast_msg, "广播", session=session)
|
|
else:
|
|
target_groups_console = valid_groups
|
|
broadcast_msg = (
|
|
f"向标签 '{tag_name}' 中的 {len(target_groups_console)} 个群组广播 "
|
|
f"(ForceSend: {force_send})"
|
|
)
|
|
logger.info(broadcast_msg, "广播", session=session)
|
|
else:
|
|
all_groups, _ = await BroadcastManager.get_all_groups(bot)
|
|
|
|
if current_group_id:
|
|
target_groups_console = [
|
|
group for group in all_groups if str(group.group_id) != current_group_id
|
|
]
|
|
logger.info(
|
|
(
|
|
f"向除当前群组({current_group_id})外的所有群组广播 "
|
|
f"(ForceSend: {force_send})"
|
|
),
|
|
"广播",
|
|
session=session,
|
|
)
|
|
else:
|
|
target_groups_console = all_groups
|
|
logger.info(
|
|
f"向所有群组广播 (ForceSend: {force_send})", "广播", session=session
|
|
)
|
|
|
|
if not target_groups_console:
|
|
if not tag_name:
|
|
await MessageUtils.build_message("没有找到符合条件的广播目标群组。").send(
|
|
reply_to=True
|
|
)
|
|
return [], []
|
|
|
|
groups_to_actually_send = []
|
|
if force_send:
|
|
groups_to_actually_send = target_groups_console
|
|
logger.debug(
|
|
f"强制发送模式,将向 {len(groups_to_actually_send)} 个目标群组尝试发送。",
|
|
"广播",
|
|
)
|
|
else:
|
|
for group in target_groups_console:
|
|
if not await CommonUtils.task_is_block(bot, "broadcast", group.group_id):
|
|
groups_to_actually_send.append(group)
|
|
logger.debug(
|
|
f"普通发送模式,筛选后将向 {len(groups_to_actually_send)} "
|
|
f"个目标群组尝试发送",
|
|
"广播",
|
|
)
|
|
|
|
return target_groups_console, groups_to_actually_send
|
|
|
|
|
|
async def send_broadcast_and_notify(
|
|
bot: Bot,
|
|
event: Event,
|
|
message: UniMessage,
|
|
groups_to_send: list,
|
|
all_target_groups_for_stats: list,
|
|
session: EventSession,
|
|
force_send: bool = False,
|
|
) -> None:
|
|
"""发送广播并通知结果"""
|
|
BroadcastManager.clear_last_broadcast_msg_ids()
|
|
count, error_count = await BroadcastManager.send_to_specific_groups(
|
|
bot, message, groups_to_send, session, force_send
|
|
)
|
|
|
|
result = f"成功广播 {count} 个群组"
|
|
if error_count:
|
|
result += f"\n发送失败 {error_count} 个群组"
|
|
|
|
effective_sent_count = len(groups_to_send)
|
|
total_considered_count = len(all_target_groups_for_stats)
|
|
|
|
result += f"\n有效: {effective_sent_count} / 总计目标: {total_considered_count}"
|
|
|
|
user_id = str(event.get_user_id())
|
|
await bot.send_private_msg(user_id=user_id, message=f"发送广播完成!\n{result}")
|
|
|
|
BroadcastManager.log_info(
|
|
f"广播完成,有效/总计目标: {effective_sent_count}/{total_considered_count}",
|
|
session,
|
|
)
|