mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-14 21:52: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>
244 lines
6.7 KiB
Python
244 lines
6.7 KiB
Python
from arclet.alconna import AllParam
|
||
from nepattern import UnionPattern
|
||
from nonebot.adapters import Bot, Event
|
||
from nonebot.permission import SUPERUSER
|
||
from nonebot.plugin import PluginMetadata
|
||
from nonebot.rule import to_me
|
||
import nonebot_plugin_alconna as alc
|
||
from nonebot_plugin_alconna import (
|
||
Alconna,
|
||
Args,
|
||
on_alconna,
|
||
)
|
||
from nonebot_plugin_alconna.uniseg.segment import (
|
||
At,
|
||
AtAll,
|
||
Audio,
|
||
Button,
|
||
Emoji,
|
||
File,
|
||
Hyper,
|
||
Image,
|
||
Keyboard,
|
||
Reference,
|
||
Reply,
|
||
Text,
|
||
Video,
|
||
Voice,
|
||
)
|
||
from nonebot_plugin_session import EventSession
|
||
|
||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||
from zhenxun.services.log import logger
|
||
from zhenxun.utils.enum import PluginType
|
||
from zhenxun.utils.message import MessageUtils
|
||
|
||
from .broadcast_manager import BroadcastManager
|
||
from .message_processor import (
|
||
_extract_broadcast_content,
|
||
get_broadcast_target_groups,
|
||
send_broadcast_and_notify,
|
||
)
|
||
|
||
BROADCAST_SEND_DELAY_RANGE = (1, 3)
|
||
|
||
__plugin_meta__ = PluginMetadata(
|
||
name="广播",
|
||
description="昭告天下!",
|
||
usage="""
|
||
向所有群组或指定标签的群组发送广播消息。
|
||
|
||
**基础用法**
|
||
- `广播 [消息内容]`:向所有群组发送广播。
|
||
- `广播` (并引用一条消息):将引用的消息作为内容进行广播。
|
||
|
||
**高级定向广播**
|
||
- `广播 -t <标签名> [消息内容]`:向指定标签下的所有群组广播。
|
||
- `广播到 <标签名> [消息内容]`:与 `-t` 等效的快捷方式。
|
||
|
||
**标签可以是静态的,也可以是动态的,例如:**
|
||
- `广播到 核心群 通知:...`
|
||
- `广播到 成员数>500的群 通知:...`
|
||
|
||
**其他命令**
|
||
- `广播撤回` (别名: `recall`):撤回最近一次发送的广播。
|
||
|
||
特性:
|
||
- 在群组中使用广播时,不会将消息发送到当前群组
|
||
- 在私聊中使用广播时,会发送到所有群组
|
||
|
||
别名:
|
||
- bc (广播的简写)
|
||
- recall (广播撤回的别名)
|
||
""".strip(),
|
||
extra=PluginExtraData(
|
||
author="HibiKier",
|
||
version="1.3",
|
||
plugin_type=PluginType.SUPERUSER,
|
||
configs=[
|
||
RegisterConfig(
|
||
module="_task",
|
||
key="DEFAULT_BROADCAST",
|
||
value=True,
|
||
help="被动 广播 进群默认开关状态",
|
||
default_value=True,
|
||
type=bool,
|
||
),
|
||
RegisterConfig(
|
||
module="_task",
|
||
key="BROADCAST_CONCURRENCY_LIMIT",
|
||
value=10,
|
||
help="广播时的最大并发任务数,以避免API速率限制",
|
||
default_value=10,
|
||
),
|
||
],
|
||
tasks=[Task(module="broadcast", name="广播")],
|
||
).to_dict(),
|
||
)
|
||
|
||
AnySeg = (
|
||
UnionPattern(
|
||
[
|
||
Text,
|
||
Image,
|
||
At,
|
||
AtAll,
|
||
Audio,
|
||
Video,
|
||
File,
|
||
Emoji,
|
||
Reply,
|
||
Reference,
|
||
Hyper,
|
||
Button,
|
||
Keyboard,
|
||
Voice,
|
||
]
|
||
)
|
||
@ "AnySeg"
|
||
)
|
||
|
||
_matcher = on_alconna(
|
||
Alconna(
|
||
"广播",
|
||
Args["content?", AllParam],
|
||
alc.Option(
|
||
"-t|--tag", Args["tag_name_bc", str], help_text="向指定标签的群组广播"
|
||
),
|
||
),
|
||
aliases={"bc"},
|
||
priority=1,
|
||
permission=SUPERUSER,
|
||
block=True,
|
||
rule=to_me(),
|
||
use_origin=False,
|
||
)
|
||
|
||
_matcher.shortcut("广播到 {tag}", command="广播 -t {tag} {%*}")
|
||
|
||
_recall_matcher = on_alconna(
|
||
Alconna("广播撤回"),
|
||
aliases={"recall"},
|
||
priority=1,
|
||
permission=SUPERUSER,
|
||
block=True,
|
||
rule=to_me(),
|
||
)
|
||
|
||
|
||
@_matcher.handle()
|
||
async def handle_broadcast(
|
||
bot: Bot,
|
||
event: Event,
|
||
session: EventSession,
|
||
arp: alc.Arparma,
|
||
tag_name_match: alc.Match[str] = alc.AlconnaMatch("tag_name_bc"),
|
||
):
|
||
broadcast_content_msg = await _extract_broadcast_content(bot, event, arp, session)
|
||
if not broadcast_content_msg:
|
||
return
|
||
|
||
tag_name_to_broadcast = None
|
||
force_send = False
|
||
|
||
if tag_name_match.available:
|
||
tag_name_to_broadcast = tag_name_match.result
|
||
force_send = True
|
||
|
||
mode_desc = "强制发送到标签" if force_send else "普通发送"
|
||
logger.debug(
|
||
f"广播模式: {mode_desc}, 标签名: {tag_name_to_broadcast}",
|
||
"广播",
|
||
)
|
||
|
||
target_groups_console, groups_to_actually_send = await get_broadcast_target_groups(
|
||
bot, session, tag_name_to_broadcast, force_send
|
||
)
|
||
|
||
if not target_groups_console:
|
||
if tag_name_to_broadcast:
|
||
await MessageUtils.build_message(
|
||
f"标签 '{tag_name_to_broadcast}' 中没有群组或标签不存在。"
|
||
).send(reply_to=True)
|
||
return
|
||
|
||
if not groups_to_actually_send:
|
||
if not force_send and target_groups_console:
|
||
await MessageUtils.build_message(
|
||
"没有启用了广播功能的目标群组可供立即发送。"
|
||
).send(reply_to=True)
|
||
return
|
||
|
||
try:
|
||
await send_broadcast_and_notify(
|
||
bot,
|
||
event,
|
||
broadcast_content_msg,
|
||
groups_to_actually_send,
|
||
target_groups_console,
|
||
session,
|
||
force_send,
|
||
)
|
||
except Exception as e:
|
||
error_msg = "发送广播失败"
|
||
BroadcastManager.log_error(error_msg, e, session)
|
||
await bot.send_private_msg(
|
||
user_id=str(event.get_user_id()), message=f"{error_msg}。"
|
||
)
|
||
|
||
|
||
@_recall_matcher.handle()
|
||
async def handle_broadcast_recall(
|
||
bot: Bot,
|
||
event: Event,
|
||
session: EventSession,
|
||
):
|
||
"""处理广播撤回命令"""
|
||
await MessageUtils.build_message("正在尝试撤回最近一次广播...").send()
|
||
|
||
try:
|
||
success_count, error_count = await BroadcastManager.recall_last_broadcast(
|
||
bot, session
|
||
)
|
||
|
||
user_id = str(event.get_user_id())
|
||
if success_count == 0 and error_count == 0:
|
||
await bot.send_private_msg(
|
||
user_id=user_id,
|
||
message="没有找到最近的广播消息记录,可能已经撤回或超过可撤回时间。",
|
||
)
|
||
else:
|
||
result = f"广播撤回完成!\n成功撤回 {success_count} 条消息"
|
||
if error_count:
|
||
result += f"\n撤回失败 {error_count} 条消息 (可能已过期或无权限)"
|
||
await bot.send_private_msg(user_id=user_id, message=result)
|
||
BroadcastManager.log_info(
|
||
f"广播撤回完成: 成功 {success_count}, 失败 {error_count}", session
|
||
)
|
||
except Exception as e:
|
||
error_msg = "撤回广播消息失败"
|
||
BroadcastManager.log_error(error_msg, e, session)
|
||
await bot.send_private_msg(
|
||
user_id=str(event.get_user_id()), message=f"{error_msg}。"
|
||
)
|