Skip to content

Commit 68460d1

Browse files
webjoin111webjoin111pre-commit-ci[bot]
authored
✨ Feat: 增强 LLM、渲染与广播功能并优化性能 (#2071)
* ⚡️ 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>
1 parent c839b44 commit 68460d1

File tree

18 files changed

+803
-232
lines changed

18 files changed

+803
-232
lines changed

.github/workflows/codeql.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,9 @@ jobs:
4545
include:
4646
- language: python
4747
build-mode: none
48-
- language: javascript-typescript
49-
build-mode: none
5048
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
5149
# Use `c-cpp` to analyze code written in C, C++ or both
5250
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
53-
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
5451
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
5552
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
5653
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how

zhenxun/builtin_plugins/llm_manager/__init__.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import defaultdict
2+
13
from nonebot.permission import SUPERUSER
24
from nonebot.plugin import PluginMetadata
35
from nonebot_plugin_alconna import (
@@ -58,7 +60,12 @@
5860
llm_cmd = on_alconna(
5961
Alconna(
6062
"llm",
61-
Subcommand("list", alias=["ls"], help_text="查看模型列表"),
63+
Subcommand(
64+
"list",
65+
Option("--text", action=store_true, help_text="以纯文本格式输出模型列表"),
66+
alias=["ls"],
67+
help_text="查看模型列表",
68+
),
6269
Subcommand("info", Args["model_name", str], help_text="查看模型详情"),
6370
Subcommand("default", Args["model_name?", str], help_text="查看或设置默认模型"),
6471
Subcommand(
@@ -80,13 +87,36 @@
8087

8188

8289
@llm_cmd.assign("list")
83-
async def handle_list(arp: Arparma, show_all: Query[bool] = Query("all")):
90+
async def handle_list(
91+
arp: Arparma,
92+
show_all: Query[bool] = Query("all"),
93+
text_mode: Query[bool] = Query("list.text.value", False),
94+
):
8495
"""处理 'llm list' 命令"""
8596
logger.info("获取LLM模型列表", command="LLM Manage", session=arp.header_result)
8697
models = await DataSource.get_model_list(show_all=show_all.result)
8798

88-
image = await Presenters.format_model_list_as_image(models, show_all.result)
89-
await llm_cmd.finish(MessageUtils.build_message(image))
99+
if text_mode.result:
100+
if not models:
101+
await llm_cmd.finish("当前没有配置任何LLM模型。")
102+
103+
grouped_models = defaultdict(list)
104+
for model in models:
105+
grouped_models[model["provider_name"]].append(model)
106+
107+
response_parts = ["可用的LLM模型列表:"]
108+
for provider, model_list in grouped_models.items():
109+
response_parts.append(f"\n{provider}:")
110+
for model in model_list:
111+
response_parts.append(
112+
f" {model['provider_name']}/{model['model_name']}"
113+
)
114+
115+
response_text = "\n".join(response_parts)
116+
await llm_cmd.finish(response_text)
117+
else:
118+
image = await Presenters.format_model_list_as_image(models, show_all.result)
119+
await llm_cmd.finish(MessageUtils.build_message(image))
90120

91121

92122
@llm_cmd.assign("info")
@@ -114,7 +144,7 @@ async def handle_default(arp: Arparma, model_name: Match[str]):
114144
command="LLM Manage",
115145
session=arp.header_result,
116146
)
117-
success, message = await DataSource.set_default_model(model_name.result)
147+
_success, message = await DataSource.set_default_model(model_name.result)
118148
await llm_cmd.finish(message)
119149
else:
120150
logger.info("查看默认模型", command="LLM Manage", session=arp.header_result)
@@ -132,7 +162,7 @@ async def handle_test(arp: Arparma, model_name: Match[str]):
132162
)
133163
await llm_cmd.send(f"正在测试模型 '{model_name.result}',请稍候...")
134164

135-
success, message = await DataSource.test_model_connectivity(model_name.result)
165+
_success, message = await DataSource.test_model_connectivity(model_name.result)
136166
await llm_cmd.finish(message)
137167

138168

@@ -167,5 +197,5 @@ async def handle_reset_key(
167197
)
168198
logger.info(log_msg, command="LLM Manage", session=arp.header_result)
169199

170-
success, message = await DataSource.reset_key(provider_name.result, key_to_reset)
200+
_success, message = await DataSource.reset_key(provider_name.result, key_to_reset)
171201
await llm_cmd.finish(message)

zhenxun/builtin_plugins/platform/qq/group_handle/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from zhenxun.models.event_log import EventLog
1818
from zhenxun.models.group_console import GroupConsole
1919
from zhenxun.services.cache import CacheRoot
20+
from zhenxun.services.log import logger
21+
from zhenxun.services.tags import tag_manager
2022
from zhenxun.utils.common_utils import CommonUtils
2123
from zhenxun.utils.enum import EventLogType, PluginType
2224
from zhenxun.utils.platform import PlatformUtils
@@ -135,6 +137,11 @@ async def _(
135137
await EventLog.create(
136138
user_id=user_id, group_id=group_id, event_type=EventLogType.KICK_BOT
137139
)
140+
await tag_manager.remove_group_from_all_tags(group_id)
141+
logger.info(
142+
f"机器人被移出群聊,已自动从所有静态标签中移除群组 {group_id}",
143+
"群组标签管理",
144+
)
138145
elif event.sub_type in ["leave", "kick"]:
139146
if event.sub_type == "leave":
140147
"""主动退群"""

zhenxun/builtin_plugins/scheduler/auto_update_group.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from nonebot_plugin_apscheduler import scheduler
33

44
from zhenxun.services.log import logger
5+
from zhenxun.services.tags import tag_manager
56
from zhenxun.utils.platform import PlatformUtils
67

78

@@ -37,3 +38,20 @@ async def _():
3738
f"Bot: {bot.self_id} 自动更新好友信息错误", "自动更新好友", e=e
3839
)
3940
logger.info("自动更新好友信息成功...")
41+
42+
43+
# 自动清理静态标签中的无效群组
44+
@scheduler.scheduled_job(
45+
"cron",
46+
hour=23,
47+
minute=30,
48+
)
49+
async def _prune_stale_tags():
50+
deleted_count = await tag_manager.prune_stale_group_links()
51+
if deleted_count > 0:
52+
logger.info(
53+
f"定时任务:成功清理了 {deleted_count} 个无效的群组标签" f"关联。",
54+
"群组标签管理",
55+
)
56+
else:
57+
logger.debug("定时任务:未发现无效的群组标签关联。", "群组标签管理")

zhenxun/builtin_plugins/superuser/broadcast/__init__.py

Lines changed: 92 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
)
2929
from nonebot_plugin_session import EventSession
3030

31-
from zhenxun.configs.utils import PluginExtraData, Task
31+
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
32+
from zhenxun.services.log import logger
3233
from zhenxun.utils.enum import PluginType
3334
from zhenxun.utils.message import MessageUtils
3435

@@ -45,34 +46,52 @@
4546
name="广播",
4647
description="昭告天下!",
4748
usage="""
48-
广播 [消息内容]
49-
- 直接发送消息到除当前群组外的所有群组
50-
- 支持文本、图片、@、表情、视频等多种消息类型
51-
- 示例:广播 你们好!
52-
- 示例:广播 [图片] 新活动开始啦!
53-
54-
广播 + 引用消息
55-
- 将引用的消息作为广播内容发送
56-
- 支持引用普通消息或合并转发消息
57-
- 示例:(引用一条消息) 广播
58-
59-
广播撤回
60-
- 撤回最近一次由您触发的广播消息
61-
- 仅能撤回短时间内的消息
62-
- 示例:广播撤回
63-
64-
特性:
65-
- 在群组中使用广播时,不会将消息发送到当前群组
66-
- 在私聊中使用广播时,会发送到所有群组
67-
68-
别名:
69-
- bc (广播的简写)
70-
- recall (广播撤回的别名)
49+
向所有群组或指定标签的群组发送广播消息。
50+
51+
**基础用法**
52+
- `广播 [消息内容]`:向所有群组发送广播。
53+
- `广播` (并引用一条消息):将引用的消息作为内容进行广播。
54+
55+
**高级定向广播**
56+
- `广播 -t <标签名> [消息内容]`:向指定标签下的所有群组广播。
57+
- `广播到 <标签名> [消息内容]`:与 `-t` 等效的快捷方式。
58+
59+
**标签可以是静态的,也可以是动态的,例如:**
60+
- `广播到 核心群 通知:...`
61+
- `广播到 成员数>500的群 通知:...`
62+
63+
**其他命令**
64+
- `广播撤回` (别名: `recall`):撤回最近一次发送的广播。
65+
66+
特性:
67+
- 在群组中使用广播时,不会将消息发送到当前群组
68+
- 在私聊中使用广播时,会发送到所有群组
69+
70+
别名:
71+
- bc (广播的简写)
72+
- recall (广播撤回的别名)
7173
""".strip(),
7274
extra=PluginExtraData(
7375
author="HibiKier",
74-
version="1.2",
76+
version="1.3",
7577
plugin_type=PluginType.SUPERUSER,
78+
configs=[
79+
RegisterConfig(
80+
module="_task",
81+
key="DEFAULT_BROADCAST",
82+
value=True,
83+
help="被动 广播 进群默认开关状态",
84+
default_value=True,
85+
type=bool,
86+
),
87+
RegisterConfig(
88+
module="_task",
89+
key="BROADCAST_CONCURRENCY_LIMIT",
90+
value=10,
91+
help="广播时的最大并发任务数,以避免API速率限制",
92+
default_value=10,
93+
),
94+
],
7695
tasks=[Task(module="broadcast", name="广播")],
7796
).to_dict(),
7897
)
@@ -103,6 +122,9 @@
103122
Alconna(
104123
"广播",
105124
Args["content?", AllParam],
125+
alc.Option(
126+
"-t|--tag", Args["tag_name_bc", str], help_text="向指定标签的群组广播"
127+
),
106128
),
107129
aliases={"bc"},
108130
priority=1,
@@ -112,6 +134,8 @@
112134
use_origin=False,
113135
)
114136

137+
_matcher.shortcut("广播到 {tag}", command="广播 -t {tag} {%*}")
138+
115139
_recall_matcher = on_alconna(
116140
Alconna("广播撤回"),
117141
aliases={"recall"},
@@ -128,23 +152,59 @@ async def handle_broadcast(
128152
event: Event,
129153
session: EventSession,
130154
arp: alc.Arparma,
155+
tag_name_match: alc.Match[str] = alc.AlconnaMatch("tag_name_bc"),
131156
):
132157
broadcast_content_msg = await _extract_broadcast_content(bot, event, arp, session)
133158
if not broadcast_content_msg:
134159
return
135160

136-
target_groups, enabled_groups = await get_broadcast_target_groups(bot, session)
137-
if not target_groups or not enabled_groups:
161+
tag_name_to_broadcast = None
162+
force_send = False
163+
164+
if tag_name_match.available:
165+
tag_name_to_broadcast = tag_name_match.result
166+
force_send = True
167+
168+
mode_desc = "强制发送到标签" if force_send else "普通发送"
169+
logger.debug(
170+
f"广播模式: {mode_desc}, 标签名: {tag_name_to_broadcast}",
171+
"广播",
172+
)
173+
174+
target_groups_console, groups_to_actually_send = await get_broadcast_target_groups(
175+
bot, session, tag_name_to_broadcast, force_send
176+
)
177+
178+
if not target_groups_console:
179+
if tag_name_to_broadcast:
180+
await MessageUtils.build_message(
181+
f"标签 '{tag_name_to_broadcast}' 中没有群组或标签不存在。"
182+
).send(reply_to=True)
183+
return
184+
185+
if not groups_to_actually_send:
186+
if not force_send and target_groups_console:
187+
await MessageUtils.build_message(
188+
"没有启用了广播功能的目标群组可供立即发送。"
189+
).send(reply_to=True)
138190
return
139191

140192
try:
141193
await send_broadcast_and_notify(
142-
bot, event, broadcast_content_msg, enabled_groups, target_groups, session
194+
bot,
195+
event,
196+
broadcast_content_msg,
197+
groups_to_actually_send,
198+
target_groups_console,
199+
session,
200+
force_send,
143201
)
144202
except Exception as e:
145203
error_msg = "发送广播失败"
146204
BroadcastManager.log_error(error_msg, e, session)
147-
await MessageUtils.build_message(f"{error_msg}。").send(reply_to=True)
205+
await bot.send_private_msg(
206+
user_id=str(event.get_user_id()), message=f"{error_msg}。"
207+
)
148208

149209

150210
@_recall_matcher.handle()
@@ -178,5 +238,6 @@ async def handle_broadcast_recall(
178238
except Exception as e:
179239
error_msg = "撤回广播消息失败"
180240
BroadcastManager.log_error(error_msg, e, session)
181-
user_id = str(event.get_user_id())
182-
await bot.send_private_msg(user_id=user_id, message=f"{error_msg}。")
241+
await bot.send_private_msg(
242+
user_id=str(event.get_user_id()), message=f"{error_msg}。"
243+
)

0 commit comments

Comments
 (0)