Date: Fri, 4 Apr 2025 20:24:12 +0800
Subject: [PATCH 03/13] :bug: Refactor chat message handling to improve message
storage and error logging
---
.../chat_history/chat_message.py | 41 +++----------------
1 file changed, 6 insertions(+), 35 deletions(-)
diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py
index d9e58c5a..b3bebb4f 100644
--- a/zhenxun/builtin_plugins/chat_history/chat_message.py
+++ b/zhenxun/builtin_plugins/chat_history/chat_message.py
@@ -1,7 +1,6 @@
from nonebot import on_message
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import UniMsg
-from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
@@ -39,45 +38,17 @@ def rule(message: UniMsg) -> bool:
chat_history = on_message(rule=rule, priority=1, block=False)
-TEMP_LIST = []
-
-
@chat_history.handle()
-async def _(message: UniMsg, session: EventSession):
- # group_id = session.id3 or session.id2
- group_id = session.id2
- TEMP_LIST.append(
- ChatHistory(
+async def handle_message(message: UniMsg, session: EventSession):
+ """处理消息存储"""
+ try:
+ await ChatHistory.create(
user_id=session.id1,
- group_id=group_id,
+ group_id=session.id2,
text=str(message),
plain_text=message.extract_plain_text(),
bot_id=session.bot_id,
platform=session.platform,
)
- )
-
-
-@scheduler.scheduled_job(
- "interval",
- minutes=1,
-)
-async def _():
- try:
- message_list = TEMP_LIST.copy()
- TEMP_LIST.clear()
- if message_list:
- await ChatHistory.bulk_create(message_list)
- logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务")
except Exception as e:
- logger.error("定时批量添加聊天记录", "定时任务", e=e)
-
-
-# @test.handle()
-# async def _(event: MessageEvent):
-# print(await ChatHistory.get_user_msg(event.user_id, "private"))
-# print(await ChatHistory.get_user_msg_count(event.user_id, "private"))
-# print(await ChatHistory.get_user_msg(event.user_id, "group"))
-# print(await ChatHistory.get_user_msg_count(event.user_id, "group"))
-# print(await ChatHistory.get_group_msg(event.group_id))
-# print(await ChatHistory.get_group_msg_count(event.group_id))
+ logger.warning("存储聊天记录失败", "chat_history", e=e)
From 2d8320b5a01484a8383df19360759df12b67d57b Mon Sep 17 00:00:00 2001
From: HibiKier <45528451+HibiKier@users.noreply.github.com>
Date: Sat, 5 Apr 2025 23:19:24 +0800
Subject: [PATCH 04/13] =?UTF-8?q?:ambulance:=20=E4=BF=AE=E5=A4=8Dpydantic2?=
=?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E7=9A=84=E5=95=86=E5=BA=97=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E6=9E=84=E9=80=A0=20(#1883)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
zhenxun/builtin_plugins/shop/_data_source.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py
index 2160238d..0fdd4e53 100644
--- a/zhenxun/builtin_plugins/shop/_data_source.py
+++ b/zhenxun/builtin_plugins/shop/_data_source.py
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
import inspect
import time
from types import MappingProxyType
-from typing import Any, ClassVar, Literal
+from typing import Any, Literal
from nonebot.adapters import Bot, Event
from nonebot.compat import model_dump
@@ -65,15 +65,15 @@ class ShopParam(BaseModel):
"""道具单次使用数量"""
text: str
"""text"""
- send_success_msg: ClassVar[bool] = True
+ send_success_msg: bool = True
"""是否发送使用成功信息"""
- max_num_limit: ClassVar[int] = 1
+ max_num_limit: int = 1
"""单次使用最大次数"""
session: Uninfo | None = None
"""Uninfo"""
message: UniMsg
"""UniMessage"""
- extra_data: ClassVar[dict[str, Any]] = {}
+ extra_data: dict[str, Any] = Field(default_factory=dict)
"""额外数据"""
class Config:
@@ -384,10 +384,10 @@ class ShopManage:
cls.uuid2goods[uuid] = Goods(
model=create_model(
f"{uuid}_model",
- send_success_msg=send_success_msg,
- max_num_limit=max_num_limit,
__base__=ShopParam,
- extra_data=kwargs,
+ send_success_msg=(bool, Field(default=send_success_msg)),
+ max_num_limit=(int, Field(default=max_num_limit)),
+ extra_data=(dict[str, Any], Field(default=kwargs)),
),
params=kwargs,
before_handle=before_handle,
From ccc4f27e3d7a050667e749aba26008371d8a0e5b Mon Sep 17 00:00:00 2001
From: HibiKier <45528451+HibiKier@users.noreply.github.com>
Date: Mon, 7 Apr 2025 17:28:29 +0800
Subject: [PATCH 05/13] =?UTF-8?q?:speech=5Fballoon:=20=E4=BF=AE=E6=94=B9RE?=
=?UTF-8?q?ADME=E4=B8=AD=E6=96=87=E6=A1=A3=E5=9C=B0=E5=9D=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6eb1d5a5..83987f0d 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@
-[文档](https://hibikier.github.io/zhenxun_bot/)
+[文档](https://zhenxun-org.github.io/zhenxun_bot/)
@@ -124,7 +124,7 @@ AccessToken: PUBLIC_ZHENXUN_TEST
- 通过 Config 配置项将所有插件配置统计保存至 config.yaml,利于统一用户修改
- 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
- 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等
-- **更多详细请通过 [传送门](https://hibikier.github.io/zhenxun_bot/) 查看文档!**
+- **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!**
## 🛠️ 简单部署
From b5f101546a1d10c0a6d61de9658664055ee23768 Mon Sep 17 00:00:00 2001
From: BalconyJH
Date: Wed, 9 Apr 2025 15:51:21 +0800
Subject: [PATCH 06/13] :arrow_up: lock multidict != 6.3.2 duo to Memory leak
---
poetry.lock | 194 ++++++++++++++++++++++++-------------------------
pyproject.toml | 1 +
2 files changed, 98 insertions(+), 97 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 8ac16046..21748cd0 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "aiocache"
@@ -258,7 +258,7 @@ description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.8"
groups = ["main"]
-markers = "python_version < \"3.11.0\""
+markers = "python_version == \"3.10\""
files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@@ -2002,104 +2002,104 @@ reference = "aliyun"
[[package]]
name = "multidict"
-version = "6.3.2"
+version = "6.2.0"
description = "multidict implementation"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
- {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3dc0eec9304fa04d84a51ea13b0ec170bace5b7ddeaac748149efd316f1504"},
- {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9534f3d84addd3b6018fa83f97c9d4247aaa94ac917d1ed7b2523306f99f5c16"},
- {file = "multidict-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a003ce1413ae01f0b8789c1c987991346a94620a4d22210f7a8fe753646d3209"},
- {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b43f7384e68b1b982c99f489921a459467b5584bdb963b25e0df57c9039d0ad"},
- {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d142ae84047262dc75c1f92eaf95b20680f85ce11d35571b4c97e267f96fadc4"},
- {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec7e86fbc48aa1d6d686501a8547818ba8d645e7e40eaa98232a5d43ee4380ad"},
- {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe019fb437632b016e6cac67a7e964f1ef827ef4023f1ca0227b54be354da97e"},
- {file = "multidict-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b60cb81214a9da7cfd8ae2853d5e6e47225ece55fe5833142fe0af321c35299"},
- {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32d9e8ef2e0312d4e96ca9adc88e0675b6d8e144349efce4a7c95d5ccb6d88e0"},
- {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:335d584312e3fa43633d63175dfc1a5f137dd7aa03d38d1310237d54c3032774"},
- {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8df917faa6b8cac3d6870fc21cb7e4d169faca68e43ffe568c156c9c6408a4d"},
- {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc060b9b89b701dd8fedef5b99e1f1002b8cb95072693233a63389d37e48212d"},
- {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2ce3be2500658f3c644494b934628bb0c82e549dde250d2119689ce791cc8b8"},
- {file = "multidict-6.3.2-cp310-cp310-win32.whl", hash = "sha256:dbcb4490d8e74b484449abd51751b8f560dd0a4812eb5dacc6a588498222a9ab"},
- {file = "multidict-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:06944f9ced30f8602be873563ed4df7e3f40958f60b2db39732c11d615a33687"},
- {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45a034f41fcd16968c0470d8912d293d7b0d0822fc25739c5c2ff7835b85bc56"},
- {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:352585cec45f5d83d886fc522955492bb436fca032b11d487b12d31c5a81b9e3"},
- {file = "multidict-6.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da9d89d293511fd0a83a90559dc131f8b3292b6975eb80feff19e5f4663647e2"},
- {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fa716592224aa652b9347a586cfe018635229074565663894eb4eb21f8307f"},
- {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0326278a44c56e94792475268e5cd3d47fbc0bd41ee56928c3bbb103ba7f58fe"},
- {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb1ea87f7fe45e5079f6315e95d64d4ca8b43ef656d98bed63a02e3756853a22"},
- {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cff3c5a98d037024a9065aafc621a8599fad7b423393685dc83cf7a32f8b691"},
- {file = "multidict-6.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed99834b053c655d980fb98029003cb24281e47a796052faad4543aa9e01b8e8"},
- {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7048440e505d2b4741e5d0b32bd2f427c901f38c7760fc245918be2cf69b3b85"},
- {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27248c27b563f5889556da8a96e18e98a56ff807ac1a7d56cf4453c2c9e4cd91"},
- {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6323b4ba0e018bd266f776c35f3f0943fc4ee77e481593c9f93bd49888f24e94"},
- {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:81f7ce5ec7c27d0b45c10449c8f0fed192b93251e2e98cb0b21fec779ef1dc4d"},
- {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03bfcf2825b3bed0ba08a9d854acd18b938cab0d2dba3372b51c78e496bac811"},
- {file = "multidict-6.3.2-cp311-cp311-win32.whl", hash = "sha256:f32c2790512cae6ca886920e58cdc8c784bdc4bb2a5ec74127c71980369d18dc"},
- {file = "multidict-6.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b0c15e58e038a2cd75ef7cf7e072bc39b5e0488b165902efb27978984bbad70"},
- {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d1e0ba1ce1b8cc79117196642d95f4365e118eaf5fb85f57cdbcc5a25640b2a4"},
- {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:029bbd7d782251a78975214b78ee632672310f9233d49531fc93e8e99154af25"},
- {file = "multidict-6.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7db41e3b56817d9175264e5fe00192fbcb8e1265307a59f53dede86161b150e"},
- {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcab18e65cc555ac29981a581518c23311f2b1e72d8f658f9891590465383be"},
- {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d50eff89aa4d145a5486b171a2177042d08ea5105f813027eb1050abe91839f"},
- {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643e57b403d3e240045a3681f9e6a04d35a33eddc501b4cbbbdbc9c70122e7bc"},
- {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d17b37b9715b30605b5bab1460569742d0c309e5c20079263b440f5d7746e7e"},
- {file = "multidict-6.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68acd51fa94e63312b8ddf84bfc9c3d3442fe1f9988bbe1b6c703043af8867fe"},
- {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:347eea2852ab7f697cc5ed9b1aae96b08f8529cca0c6468f747f0781b1842898"},
- {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4d3f8e57027dcda84a1aa181501c15c45eab9566eb6fcc274cbd1e7561224f8"},
- {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca57a841ffcf712e47875d026aa49d6e67f9560624d54b51628603700d5d287"},
- {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7cafdafb44c4e646118410368307693e49d19167e5f119cbe3a88697d2d1a636"},
- {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:430120c6ce3715a9c6075cabcee557daccbcca8ba25a9fedf05c7bf564532f2d"},
- {file = "multidict-6.3.2-cp312-cp312-win32.whl", hash = "sha256:13bec31375235a68457ab887ce1bbf4f59d5810d838ae5d7e5b416242e1f3ed4"},
- {file = "multidict-6.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3b6d7620e6e90c6d97eaf3a63bf7fbd2ba253aab89120a4a9c660bf2d675391"},
- {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b9ca24700322816ae0d426aa33671cf68242f8cc85cee0d0e936465ddaee90b5"},
- {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9fbbe23667d596ff4f9f74d44b06e40ebb0ab6b262cf14a284f859a66f86457"},
- {file = "multidict-6.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cb602c5bea0589570ad3a4a6f2649c4f13cc7a1e97b4c616e5e9ff8dc490987"},
- {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93ca81dd4d1542e20000ed90f4cc84b7713776f620d04c2b75b8efbe61106c99"},
- {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18b6310b5454c62242577a128c87df8897f39dd913311cf2e1298e47dfc089eb"},
- {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a6dda57de1fc9aedfdb600a8640c99385cdab59a5716cb714b52b6005797f77"},
- {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8ec42d03cc6b29845552a68151f9e623c541f1708328353220af571e24a247"},
- {file = "multidict-6.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80681969cee2fa84dafeb53615d51d24246849984e3e87fbe4fe39956f2e23bf"},
- {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:01489b0c3592bb9d238e5690e9566db7f77a5380f054b57077d2c4deeaade0eb"},
- {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:522d9f1fd995d04dfedc0a40bca7e2591bc577d920079df50b56245a4a252c1c"},
- {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2014e9cf0b4e9c75bbad49c1758e5a9bf967a56184fc5fcc51527425baf5abba"},
- {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:78ced9fcbee79e446ff4bb3018ac7ba1670703de7873d9c1f6f9883db53c71bc"},
- {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1faf01af972bd01216a107c195f5294f9f393531bc3e4faddc9b333581255d4d"},
- {file = "multidict-6.3.2-cp313-cp313-win32.whl", hash = "sha256:7a699ab13d8d8e1f885de1535b4f477fb93836c87168318244c2685da7b7f655"},
- {file = "multidict-6.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:8666bb0d883310c83be01676e302587834dfd185b52758caeab32ef0eb387bc6"},
- {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d82c95aabee29612b1c4f48b98be98181686eb7d6c0152301f72715705cc787b"},
- {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f47709173ea9e87a7fd05cd7e5cf1e5d4158924ff988a9a8e0fbd853705f0e68"},
- {file = "multidict-6.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c7f9d0276ceaab41b8ae78534ff28ea33d5de85db551cbf80c44371f2b55d13"},
- {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6eab22df44a25acab2e738f882f5ec551282ab45b2bbda5301e6d2cfb323036"},
- {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a947cb7c657f57874021b9b70c7aac049c877fb576955a40afa8df71d01a1390"},
- {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5faa346e8e1c371187cf345ab1e02a75889f9f510c9cbc575c31b779f7df084d"},
- {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6e08d977aebf1718540533b4ba5b351ccec2db093370958a653b1f7f9219cc"},
- {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98eab7acf55275b5bf09834125fa3a80b143a9f241cdcdd3f1295ffdc3c6d097"},
- {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:36863655630becc224375c0b99364978a0f95aebfb27fb6dd500f7fb5fb36e79"},
- {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9c0979c096c0d46a963331b0e400d3a9e560e41219df4b35f0d7a2f28f39710"},
- {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0efc04f70f05e70e5945890767e8874da5953a196f5b07c552d305afae0f3bf6"},
- {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2c519b3b82c34539fae3e22e4ea965869ac6b628794b1eb487780dde37637ab7"},
- {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:329160e301f2afd7b43725d3dda8a7ef8ee41d4ceac2083fc0d8c1cc8a4bd56b"},
- {file = "multidict-6.3.2-cp313-cp313t-win32.whl", hash = "sha256:420e5144a5f598dad8db3128f1695cd42a38a0026c2991091dab91697832f8cc"},
- {file = "multidict-6.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:875faded2861c7af2682c67088e6313fec35ede811e071c96d36b081873cea14"},
- {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2516c5eb5732d6c4e29fa93323bfdc55186895124bc569e2404e3820934be378"},
- {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:be5c8622e665cc5491c13c0fcd52915cdbae991a3514251d71129691338cdfb2"},
- {file = "multidict-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ef33150eea7953cfdb571d862cff894e0ad97ab80d97731eb4b9328fc32d52b"},
- {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b357738ce46e998f1b1bad9c4b79b2a9755915f71b87a8c01ce123a22a4f99"},
- {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c60e059fcd3655a653ba99fec2556cd0260ec57f9cb138d3e6ffc413638a2e"},
- {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:629e7c5e75bde83e54a22c7043ce89d68691d1f103be6d09a1c82b870df3b4b8"},
- {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6c8fc97d893fdf1fff15a619fee8de2f31c9b289ef7594730e35074fa0cefb"},
- {file = "multidict-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52081d2f27e0652265d4637b03f09b82f6da5ce5e1474f07dc64674ff8bfc04c"},
- {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:64529dc395b5fd0a7826ffa70d2d9a7f4abd8f5333d6aaaba67fdf7bedde9f21"},
- {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2b7c3fad827770840f5399348c89635ed6d6e9bba363baad7d3c7f86a9cf1da3"},
- {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:24aa42b1651c654ae9e5273e06c3b7ccffe9f7cc76fbde40c37e9ae65f170818"},
- {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:04ceea01e9991357164b12882e120ce6b4d63a0424bb9f9cd37910aa56d30830"},
- {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:943897a41160945416617db567d867ab34e9258adaffc56a25a4c3f99d919598"},
- {file = "multidict-6.3.2-cp39-cp39-win32.whl", hash = "sha256:76157a9a0c5380aadd3b5ff7b8deee355ff5adecc66c837b444fa633b4d409a2"},
- {file = "multidict-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:d091d123e44035cd5664554308477aff0b58db37e701e7598a67e907b98d1925"},
- {file = "multidict-6.3.2-py3-none-any.whl", hash = "sha256:71409d4579f716217f23be2f5e7afca5ca926aaeb398aa11b72d793bff637a1f"},
- {file = "multidict-6.3.2.tar.gz", hash = "sha256:c1035eea471f759fa853dd6e76aaa1e389f93b3e1403093fa0fd3ab4db490678"},
+ {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"},
+ {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"},
+ {file = "multidict-6.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e"},
+ {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a"},
+ {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de"},
+ {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d"},
+ {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3"},
+ {file = "multidict-6.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32c9b4878f48be3e75808ea7e499d6223b1eea6d54c487a66bc10a1871e3dc6a"},
+ {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a"},
+ {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49"},
+ {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191"},
+ {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb"},
+ {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a"},
+ {file = "multidict-6.2.0-cp310-cp310-win32.whl", hash = "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460"},
+ {file = "multidict-6.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1"},
+ {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46"},
+ {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932"},
+ {file = "multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf"},
+ {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf"},
+ {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc"},
+ {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1"},
+ {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081"},
+ {file = "multidict-6.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98"},
+ {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633"},
+ {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e"},
+ {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d"},
+ {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4"},
+ {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2"},
+ {file = "multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d"},
+ {file = "multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86"},
+ {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b"},
+ {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4"},
+ {file = "multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44"},
+ {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd"},
+ {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e"},
+ {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c"},
+ {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"},
+ {file = "multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29"},
+ {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd"},
+ {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8"},
+ {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df"},
+ {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d"},
+ {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b"},
+ {file = "multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626"},
+ {file = "multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c"},
+ {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80"},
+ {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16"},
+ {file = "multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e"},
+ {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817"},
+ {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc"},
+ {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1"},
+ {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844"},
+ {file = "multidict-6.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48"},
+ {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0"},
+ {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f"},
+ {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de"},
+ {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02"},
+ {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d"},
+ {file = "multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e"},
+ {file = "multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2"},
+ {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7"},
+ {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b"},
+ {file = "multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e"},
+ {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025"},
+ {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd"},
+ {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7"},
+ {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af"},
+ {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331"},
+ {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c"},
+ {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b"},
+ {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151"},
+ {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019"},
+ {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547"},
+ {file = "multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc"},
+ {file = "multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44"},
+ {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a"},
+ {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac"},
+ {file = "multidict-6.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88"},
+ {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133"},
+ {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656"},
+ {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349"},
+ {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f"},
+ {file = "multidict-6.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aed763b6a1b28c46c055692836879328f0b334a6d61572ee4113a5d0c859872"},
+ {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2"},
+ {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27"},
+ {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90"},
+ {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf"},
+ {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2"},
+ {file = "multidict-6.2.0-cp39-cp39-win32.whl", hash = "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d"},
+ {file = "multidict-6.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3"},
+ {file = "multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530"},
+ {file = "multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8"},
]
[package.dependencies]
@@ -4367,7 +4367,7 @@ files = [
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
-markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""}
+markers = {main = "python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\""}
[package.source]
type = "legacy"
@@ -5104,4 +5104,4 @@ reference = "aliyun"
[metadata]
lock-version = "2.1"
python-versions = "^3.10"
-content-hash = "f1d23ae2feff2f5c96a97f1a7e52e1738858817b13281b9253a41851cacf0740"
+content-hash = "48aa6fabc582a0c75b333f9bd3418264a1fd15a5c8c50220b456ba00d03cd35e"
diff --git a/pyproject.toml b/pyproject.toml
index 1b3685a0..621472fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ nonebot-plugin-alconna = "^0.54.0"
tenacity = "^9.0.0"
nonebot-plugin-uninfo = ">0.4.1"
nonebot-plugin-waiter = "^0.8.1"
+multidict = ">=6.0.0,!=6.3.2"
[tool.poetry.group.dev.dependencies]
nonebug = "^0.4"
From bc2e06a9ec4248ca37a7c327fc52a10d6ffc5c74 Mon Sep 17 00:00:00 2001
From: BalconyJH
Date: Wed, 9 Apr 2025 15:52:09 +0800
Subject: [PATCH 07/13] :wrench: Add Prometheus configuration for PostgreSQL
and Redis exporters
---
prometheus.yml | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 prometheus.yml
diff --git a/prometheus.yml b/prometheus.yml
new file mode 100644
index 00000000..88ca15ea
--- /dev/null
+++ b/prometheus.yml
@@ -0,0 +1,12 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: 'postgresql'
+ static_configs:
+ - targets: [ 'postgres-exporter:9187' ]
+
+ - job_name: 'redis'
+ static_configs:
+ - targets: [ 'redis-exporter:9121' ]
\ No newline at end of file
From 5a0af6a64b7d31e9baf4cb996908461331d1f892 Mon Sep 17 00:00:00 2001
From: BalconyJH
Date: Wed, 9 Apr 2025 15:52:28 +0800
Subject: [PATCH 08/13] :wrench: Add Docker Compose configuration for
PostgreSQL, Redis, and monitoring stack
---
docker-compose-dev.yml | 67 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
create mode 100644 docker-compose-dev.yml
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
new file mode 100644
index 00000000..c5375f41
--- /dev/null
+++ b/docker-compose-dev.yml
@@ -0,0 +1,67 @@
+services:
+ db:
+ image: postgres:15
+ ports:
+ - "5432:5432"
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: password
+ POSTGRES_DB: zhenxun
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ labels:
+ - "prometheus.io/scrape=true"
+ - "prometheus.io/port=9187"
+
+ postgres-exporter:
+ image: prometheuscommunity/postgres-exporter
+ environment:
+ DATA_SOURCE_NAME: "postgresql://postgres:password@db:5432/zhenxun?sslmode=disable"
+ ports:
+ - "9187:9187"
+ depends_on:
+ - db
+
+ redis:
+ image: redis:7
+ ports:
+ - "6379:6379"
+ labels:
+ - "prometheus.io/scrape=true"
+ - "prometheus.io/port=9121"
+
+ redis-exporter:
+ image: oliver006/redis_exporter
+ environment:
+ REDIS_ADDR: redis://redis:6379
+ ports:
+ - "9121:9121"
+ depends_on:
+ - redis
+
+ prometheus:
+ image: prom/prometheus
+ ports:
+ - "9090:9090"
+ volumes:
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
+ - prometheus_data:/prometheus
+ command:
+ - '--config.file=/etc/prometheus/prometheus.yml'
+ - '--storage.tsdb.path=/prometheus'
+ - '--web.console.libraries=/etc/prometheus/console_libraries'
+ - '--web.console.templates=/etc/prometheus/consoles'
+
+ grafana:
+ image: grafana/grafana
+ ports:
+ - "3000:3000"
+ volumes:
+ - grafana_data:/var/lib/grafana
+ depends_on:
+ - prometheus
+
+volumes:
+ pgdata:
+ prometheus_data:
+ grafana_data:
From b38509b2f5856f921673b6c7e87731d5f65ca667 Mon Sep 17 00:00:00 2001
From: HibiKier <45528451+HibiKier@users.noreply.github.com>
Date: Thu, 17 Apr 2025 16:58:06 +0800
Subject: [PATCH 09/13] =?UTF-8?q?:bug:=20=E6=8F=92=E4=BB=B6=E8=8E=B7?=
=?UTF-8?q?=E5=8F=96=E9=BB=98=E8=AE=A4=E8=BF=87=E6=BB=A4=E7=88=B6=E6=8F=92?=
=?UTF-8?q?=E4=BB=B6=20(#1894)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* :bug: 插件获取默认过滤父插件
* :bug: 修复插件获取
---
zhenxun/models/plugin_info.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/zhenxun/models/plugin_info.py b/zhenxun/models/plugin_info.py
index b6dfd2ce..862aea8c 100644
--- a/zhenxun/models/plugin_info.py
+++ b/zhenxun/models/plugin_info.py
@@ -60,27 +60,41 @@ class PluginInfo(Model):
table_description = "插件基本信息"
@classmethod
- async def get_plugin(cls, load_status: bool = True, **kwargs) -> Self | None:
+ async def get_plugin(
+ cls, load_status: bool = True, filter_parent: bool = True, **kwargs
+ ) -> Self | None:
"""获取插件列表
参数:
load_status: 加载状态.
+ filter_parent: 过滤父组件
返回:
Self | None: 插件
"""
+ if filter_parent:
+ return await cls.get_or_none(
+ load_status=load_status, plugin_type__not=PluginType.PARENT, **kwargs
+ )
return await cls.get_or_none(load_status=load_status, **kwargs)
@classmethod
- async def get_plugins(cls, load_status: bool = True, **kwargs) -> list[Self]:
+ async def get_plugins(
+ cls, load_status: bool = True, filter_parent: bool = True, **kwargs
+ ) -> list[Self]:
"""获取插件列表
参数:
load_status: 加载状态.
+ filter_parent: 过滤父组件
返回:
list[Self]: 插件列表
"""
+ if filter_parent:
+ return await cls.filter(
+ load_status=load_status, plugin_type__not=PluginType.PARENT, **kwargs
+ ).all()
return await cls.filter(load_status=load_status, **kwargs).all()
@classmethod
From 6769c724cb4dc373f418837b7f9cfdc134d6ff2b Mon Sep 17 00:00:00 2001
From: HibiKier <775757368@qq.com>
Date: Wed, 16 Apr 2025 09:38:40 +0800
Subject: [PATCH 10/13] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E7=BE=A4?=
=?UTF-8?q?=E8=A2=AB=E5=8A=A8=E5=BC=80=E5=85=B3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
zhenxun/builtin_plugins/about.py | 39 ++++++++++++++++----------------
zhenxun/models/group_console.py | 4 ++--
2 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/zhenxun/builtin_plugins/about.py b/zhenxun/builtin_plugins/about.py
index faa0ba0e..31c77bc7 100644
--- a/zhenxun/builtin_plugins/about.py
+++ b/zhenxun/builtin_plugins/about.py
@@ -26,6 +26,21 @@ __plugin_meta__ = PluginMetadata(
_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me())
+QQ_INFO = """
+『绪山真寻Bot』
+版本:{version}
+简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
+""".strip()
+
+INFO = """
+『绪山真寻Bot』
+版本:{version}
+简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
+项目地址:https://github.com/zhenxun-org/zhenxun_bot
+文档地址:https://zhenxun-org.github.io/zhenxun_bot/
+""".strip()
+
+
@_matcher.handle()
async def _(session: Uninfo, arparma: Arparma):
ver_file = Path() / "__version__"
@@ -35,25 +50,11 @@ async def _(session: Uninfo, arparma: Arparma):
if text := await f.read():
version = text.split(":")[-1].strip()
if PlatformUtils.is_qbot(session):
- info: list[str | Path] = [
- f"""
-『绪山真寻Bot』
-版本:{version}
-简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
- """.strip()
- ]
+ result: list[str | Path] = [QQ_INFO.format(version=version)]
path = DATA_PATH / "about.png"
if path.exists():
- info.append(path)
+ result.append(path)
+ await MessageUtils.build_message(result).send() # type: ignore
else:
- info = [
- f"""
-『绪山真寻Bot』
-版本:{version}
-简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
-项目地址:https://github.com/HibiKier/zhenxun_bot
-文档地址:https://hibikier.github.io/zhenxun_bot/
- """.strip()
- ]
- await MessageUtils.build_message(info).send() # type: ignore
- logger.info("查看关于", arparma.header_result, session=session)
+ await MessageUtils.build_message(INFO.format(version=version)).send()
+ logger.info("查看关于", arparma.header_result, session=session)
diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py
index 0a0693d3..08406fa7 100644
--- a/zhenxun/models/group_console.py
+++ b/zhenxun/models/group_console.py
@@ -41,9 +41,9 @@ def convert_module_format(data: str | list[str]) -> str | list[str]:
str | list[str]: 根据输入类型返回转换后的数据。
"""
if isinstance(data, str):
- return [item.strip(",") for item in data.split("<") if item]
+ return [item.strip(",") for item in data.split("<") if item.strip()]
else:
- return "".join(format(item) for item in data)
+ return "".join(add_disable_marker(item) for item in data)
class GroupConsole(Model):
From ff75e2ee92bd5706a8b6fec7eb1f0d5b048d7137 Mon Sep 17 00:00:00 2001
From: Rumio <32546670+webjoin111@users.noreply.github.com>
Date: Sat, 26 Apr 2025 20:15:44 +0800
Subject: [PATCH 11/13] =?UTF-8?q?:sparkles:=20=20=E5=A2=9E=E5=8A=A0webui?=
=?UTF-8?q?=E6=89=B9=E9=87=8F=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
zhenxun/builtin_plugins/web_ui/__init__.py | 9 +-
.../web_ui/api/tabs/plugin_manage/__init__.py | 69 +++++++++-
.../api/tabs/plugin_manage/data_source.py | 127 +++++++++++++++++-
.../web_ui/api/tabs/plugin_manage/model.py | 95 ++++++++-----
4 files changed, 254 insertions(+), 46 deletions(-)
diff --git a/zhenxun/builtin_plugins/web_ui/__init__.py b/zhenxun/builtin_plugins/web_ui/__init__.py
index d8d71025..90772bc5 100644
--- a/zhenxun/builtin_plugins/web_ui/__init__.py
+++ b/zhenxun/builtin_plugins/web_ui/__init__.py
@@ -29,8 +29,7 @@ from .public import init_public
__plugin_meta__ = PluginMetadata(
name="WebUi",
description="WebUi API",
- usage="""
- """.strip(),
+ usage='"""\n """.strip(),',
extra=PluginExtraData(
author="HibiKier",
version="0.1",
@@ -83,7 +82,6 @@ BaseApiRouter.include_router(plugin_router)
BaseApiRouter.include_router(system_router)
BaseApiRouter.include_router(menu_router)
-
WsApiRouter = APIRouter(prefix="/zhenxun/socket")
WsApiRouter.include_router(ws_log_routes)
@@ -94,6 +92,8 @@ WsApiRouter.include_router(chat_routes)
@driver.on_startup
async def _():
try:
+ # 存储任务引用的列表,防止任务被垃圾回收
+ _tasks = []
async def log_sink(message: str):
loop = None
@@ -104,7 +104,8 @@ async def _():
logger.warning("Web Ui log_sink", e=e)
if not loop:
loop = asyncio.new_event_loop()
- loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) # noqa: RUF006
+ # 存储任务引用到外部列表中
+ _tasks.append(loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))))
logger_.add(
log_sink, colorize=True, filter=default_filter, format=default_format
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
index e011e67f..45878880 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
@@ -9,10 +9,13 @@ from ....base_model import Result
from ....utils import authentication
from .data_source import ApiDataSource
from .model import (
+ BatchUpdatePlugins,
+ BatchUpdateResult,
PluginCount,
PluginDetail,
PluginInfo,
PluginSwitch,
+ RenameMenuTypePayload,
UpdatePlugin,
)
@@ -30,9 +33,8 @@ async def _(
plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
) -> Result[list[PluginInfo]]:
try:
- return Result.ok(
- await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!"
- )
+ result = await ApiDataSource.get_plugin_list(plugin_type, menu_type)
+ return Result.ok(result, "拿到信息啦!")
except Exception as e:
logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -144,11 +146,66 @@ async def _() -> Result[list[str]]:
)
async def _(module: str) -> Result[PluginDetail]:
try:
- return Result.ok(
- await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!"
- )
+ detail = await ApiDataSource.get_plugin_detail(module)
+ return Result.ok(detail, "已经帮你写好啦!")
except (ValueError, KeyError):
return Result.fail("插件数据不存在...")
except Exception as e:
logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e)
return Result.fail(f"{type(e)}: {e}")
+
+
+@router.put(
+ "/plugins/batch_update",
+ dependencies=[authentication()],
+ response_model=Result[BatchUpdateResult],
+ response_class=JSONResponse,
+ summary="批量更新插件配置",
+)
+async def batch_update_plugin_config_api(
+ params: BatchUpdatePlugins,
+) -> Result[BatchUpdateResult]:
+ """批量更新插件配置,如开关、类型等"""
+ try:
+ result_dict = await ApiDataSource.batch_update_plugins(params=params)
+ result_model = BatchUpdateResult(
+ success=result_dict["success"],
+ updated_count=result_dict["updated_count"],
+ errors=result_dict["errors"],
+ )
+ return Result.ok(result_model, "插件配置更新完成")
+ except Exception as e:
+ logger.error(f"{router.prefix}/plugins/batch_update 调用错误", "WebUi", e=e)
+ return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
+
+
+# 新增:重命名菜单类型路由
+@router.put(
+ "/menu_type/rename",
+ dependencies=[authentication()],
+ response_model=Result,
+ summary="重命名菜单类型",
+)
+async def rename_menu_type_api(payload: RenameMenuTypePayload) -> Result:
+ try:
+ result = await ApiDataSource.rename_menu_type(
+ old_name=payload.old_name, new_name=payload.new_name
+ )
+ if result.get("success"):
+ return Result.ok(
+ info=result.get(
+ "info",
+ f"成功将 {result.get('updated_count', 0)} 个插件的菜单类型从 "
+ f"'{payload.old_name}' 修改为 '{payload.new_name}'",
+ )
+ )
+ else:
+ return Result.fail(info=result.get("info", "重命名失败"))
+ except ValueError as ve:
+ return Result.fail(info=str(ve))
+ except RuntimeError as re:
+ logger.error(f"{router.prefix}/menu_type/rename 调用错误", "WebUi", e=re)
+ return Result.fail(info=str(re))
+ except Exception as e:
+ logger.error(f"{router.prefix}/menu_type/rename 调用错误", "WebUi", e=e)
+ return Result.fail(info=f"发生未知错误: {type(e).__name__}")
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
index ee0992d6..d525c9bf 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
@@ -2,13 +2,20 @@ import re
import cattrs
from fastapi import Query
+from tortoise.exceptions import DoesNotExist
from zhenxun.configs.config import Config
from zhenxun.configs.utils import ConfigGroup
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
from zhenxun.utils.enum import BlockType, PluginType
-from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin
+from .model import (
+ BatchUpdatePlugins,
+ PluginConfig,
+ PluginDetail,
+ PluginInfo,
+ UpdatePlugin,
+)
class ApiDataSource:
@@ -44,6 +51,7 @@ class ApiDataSource:
level=plugin.level,
status=plugin.status,
author=plugin.author,
+ block_type=plugin.block_type,
)
plugin_list.append(plugin_info)
return plugin_list
@@ -69,7 +77,6 @@ class ApiDataSource:
db_plugin.block_type = param.block_type
db_plugin.status = param.block_type != BlockType.ALL
await db_plugin.save()
- # 配置项
if param.configs and (configs := Config.get(param.module)):
for key in param.configs:
if c := configs.configs.get(key):
@@ -80,6 +87,87 @@ class ApiDataSource:
Config.save(save_simple_data=True)
return db_plugin
+ @classmethod
+ async def batch_update_plugins(cls, params: BatchUpdatePlugins) -> dict:
+ """批量更新插件数据
+
+ 参数:
+ params: BatchUpdatePlugins
+
+ 返回:
+ dict: 更新结果, 例如 {'success': True, 'updated_count': 5, 'errors': []}
+ """
+ plugins_to_update_other_fields = []
+ other_update_fields = set()
+ updated_count = 0
+ errors = []
+
+ for item in params.updates:
+ try:
+ db_plugin = await DbPluginInfo.get(module=item.module)
+ plugin_changed_other = False
+ plugin_changed_block = False
+
+ if db_plugin.block_type != item.block_type:
+ db_plugin.block_type = item.block_type
+ db_plugin.status = item.block_type != BlockType.ALL
+ plugin_changed_block = True
+
+ if item.menu_type is not None and db_plugin.menu_type != item.menu_type:
+ db_plugin.menu_type = item.menu_type
+ other_update_fields.add("menu_type")
+ plugin_changed_other = True
+
+ if (
+ item.default_status is not None
+ and db_plugin.default_status != item.default_status
+ ):
+ db_plugin.default_status = item.default_status
+ other_update_fields.add("default_status")
+ plugin_changed_other = True
+
+ if plugin_changed_block:
+ try:
+ await db_plugin.save(update_fields=["block_type", "status"])
+ updated_count += 1
+ except Exception as e_save:
+ errors.append(
+ {
+ "module": item.module,
+ "error": f"Save block_type failed: {e_save!s}",
+ }
+ )
+ plugin_changed_other = False
+
+ if plugin_changed_other:
+ plugins_to_update_other_fields.append(db_plugin)
+
+ except DoesNotExist:
+ errors.append({"module": item.module, "error": "Plugin not found"})
+ except Exception as e:
+ errors.append({"module": item.module, "error": str(e)})
+
+ bulk_updated_count = 0
+ if plugins_to_update_other_fields and other_update_fields:
+ try:
+ await DbPluginInfo.bulk_update(
+ plugins_to_update_other_fields, list(other_update_fields)
+ )
+ bulk_updated_count = len(plugins_to_update_other_fields)
+ except Exception as e_bulk:
+ errors.append(
+ {
+ "module": "batch_update_other",
+ "error": f"Bulk update failed: {e_bulk!s}",
+ }
+ )
+
+ return {
+ "success": len(errors) == 0,
+ "updated_count": updated_count + bulk_updated_count,
+ "errors": errors,
+ }
+
@classmethod
def __build_plugin_config(
cls, module: str, cfg: str, config: ConfigGroup
@@ -115,6 +203,41 @@ class ApiDataSource:
type_inner=type_inner, # type: ignore
)
+ @classmethod
+ async def rename_menu_type(cls, old_name: str, new_name: str) -> dict:
+ """重命名菜单类型,并更新所有相关插件
+
+ 参数:
+ old_name: 旧菜单类型名称
+ new_name: 新菜单类型名称
+
+ 返回:
+ dict: 更新结果, 例如 {'success': True, 'updated_count': 3}
+ """
+ if not old_name or not new_name:
+ raise ValueError("旧名称和新名称都不能为空")
+ if old_name == new_name:
+ return {
+ "success": True,
+ "updated_count": 0,
+ "info": "新旧名称相同,无需更新",
+ }
+
+ # 检查新名称是否已存在(理论上前端会校验,后端再保险一次)
+ exists = await DbPluginInfo.filter(menu_type=new_name).exists()
+ if exists:
+ raise ValueError(f"新的菜单类型名称 '{new_name}' 已被其他插件使用")
+
+ try:
+ # 使用 filter().update() 进行批量更新
+ updated_count = await DbPluginInfo.filter(menu_type=old_name).update(
+ menu_type=new_name
+ )
+ return {"success": True, "updated_count": updated_count}
+ except Exception as e:
+ # 可以添加更详细的日志记录
+ raise RuntimeError(f"数据库更新菜单类型失败: {e!s}")
+
@classmethod
async def get_plugin_detail(cls, module: str) -> PluginDetail:
"""获取插件详情
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
index 662814c9..c2bcc4bb 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
@@ -1,6 +1,6 @@
from typing import Any
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
from zhenxun.utils.enum import BlockType
@@ -37,19 +37,19 @@ class UpdatePlugin(BaseModel):
module: str
"""模块"""
default_status: bool
- """默认开关"""
+ """是否默认开启"""
limit_superuser: bool
- """限制超级用户"""
- cost_gold: int
- """金币花费"""
- menu_type: str
- """插件菜单类型"""
+ """是否限制超级用户"""
level: int
- """插件所需群权限"""
+ """等级"""
+ cost_gold: int
+ """花费金币"""
+ menu_type: str
+ """菜单类型"""
block_type: BlockType | None = None
"""禁用类型"""
configs: dict[str, Any] | None = None
- """配置项"""
+ """设置项"""
class PluginInfo(BaseModel):
@@ -58,27 +58,26 @@ class PluginInfo(BaseModel):
"""
module: str
- """插件名称"""
+ """模块"""
plugin_name: str
- """插件中文名称"""
+ """插件名称"""
default_status: bool
- """默认开关"""
+ """是否默认开启"""
limit_superuser: bool
- """限制超级用户"""
+ """是否限制超级用户"""
+ level: int
+ """等级"""
cost_gold: int
"""花费金币"""
menu_type: str
- """插件菜单类型"""
+ """菜单类型"""
version: str
- """插件版本"""
- level: int
- """群权限"""
+ """版本"""
status: bool
- """当前状态"""
+ """状态"""
author: str | None = None
"""作者"""
- block_type: BlockType | None = None
- """禁用类型"""
+ block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)")
class PluginConfig(BaseModel):
@@ -86,20 +85,13 @@ class PluginConfig(BaseModel):
插件配置项
"""
- module: str
- """模块"""
- key: str
- """键"""
- value: Any
- """值"""
- help: str | None = None
- """帮助"""
- default_value: Any
- """默认值"""
- type: Any = None
- """值类型"""
- type_inner: list[str] | None = None
- """List Tuple等内部类型检验"""
+ module: str = Field(..., description="模块名")
+ key: str = Field(..., description="键")
+ value: Any = Field(None, description="值")
+ help: str | None = Field(None, description="帮助信息")
+ default_value: Any = Field(None, description="默认值")
+ type: str | None = Field(None, description="类型")
+ type_inner: list[str] | None = Field(None, description="内部类型")
class PluginCount(BaseModel):
@@ -117,6 +109,21 @@ class PluginCount(BaseModel):
"""其他插件"""
+class BatchUpdatePluginItem(BaseModel):
+ module: str = Field(..., description="插件模块名")
+ default_status: bool | None = Field(None, description="默认状态(开关)")
+ menu_type: str | None = Field(None, description="菜单类型")
+ block_type: BlockType | None = Field(
+ None, description="插件禁用状态 (None: 启用, ALL: 禁用)"
+ )
+
+
+class BatchUpdatePlugins(BaseModel):
+ updates: list[BatchUpdatePluginItem] = Field(
+ ..., description="要批量更新的插件列表"
+ )
+
+
class PluginDetail(PluginInfo):
"""
插件详情
@@ -125,6 +132,26 @@ class PluginDetail(PluginInfo):
config_list: list[PluginConfig]
+class RenameMenuTypePayload(BaseModel):
+ old_name: str = Field(..., description="旧菜单类型名称")
+ new_name: str = Field(..., description="新菜单类型名称")
+
+
class PluginIr(BaseModel):
id: int
"""插件id"""
+
+
+class BatchUpdateResult(BaseModel):
+ """
+ 批量更新插件结果
+ """
+
+ success: bool = Field(..., description="是否全部成功")
+ """是否全部成功"""
+ updated_count: int = Field(..., description="更新成功的数量")
+ """更新成功的数量"""
+ errors: list[dict[str, str]] = Field(
+ default_factory=list, description="错误信息列表"
+ )
+ """错误信息列表"""
From 6546eb990b652123f1f4d42f76ba267c1e85f84c Mon Sep 17 00:00:00 2001
From: Rumio <32546670+webjoin111@users.noreply.github.com>
Date: Mon, 12 May 2025 16:15:25 +0800
Subject: [PATCH 12/13] =?UTF-8?q?=E2=9C=A8=20=E5=A2=9E=E5=BC=BA=E5=B9=BF?=
=?UTF-8?q?=E6=92=AD=E6=8F=92=E4=BB=B6=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../superuser/broadcast/__init__.py | 181 +++++-
.../superuser/broadcast/_data_source.py | 72 ---
.../superuser/broadcast/broadcast_manager.py | 490 +++++++++++++++
.../superuser/broadcast/message_processor.py | 584 ++++++++++++++++++
.../superuser/broadcast/models.py | 64 ++
.../superuser/broadcast/utils.py | 175 ++++++
6 files changed, 1466 insertions(+), 100 deletions(-)
delete mode 100644 zhenxun/builtin_plugins/superuser/broadcast/_data_source.py
create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/broadcast_manager.py
create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/message_processor.py
create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/models.py
create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/utils.py
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py
index c025fd0c..3fc08e4c 100644
--- a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py
+++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py
@@ -1,32 +1,77 @@
-from typing import Annotated
-
-from nonebot import on_command
-from nonebot.adapters import Bot
-from nonebot.params import Command
+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
-from nonebot_plugin_alconna import Text as alcText
-from nonebot_plugin_alconna import UniMsg
+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 ._data_source import BroadcastManage
+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="""
- 广播 [消息] [图片]
- 示例:广播 你们好!
+ 广播 [消息内容]
+ - 直接发送消息到除当前群组外的所有群组
+ - 支持文本、图片、@、表情、视频等多种消息类型
+ - 示例:广播 你们好!
+ - 示例:广播 [图片] 新活动开始啦!
+
+ 广播 + 引用消息
+ - 将引用的消息作为广播内容发送
+ - 支持引用普通消息或合并转发消息
+ - 示例:(引用一条消息) 广播
+
+ 广播撤回
+ - 撤回最近一次由您触发的广播消息
+ - 仅能撤回短时间内的消息
+ - 示例:广播撤回
+
+ 特性:
+ - 在群组中使用广播时,不会将消息发送到当前群组
+ - 在私聊中使用广播时,会发送到所有群组
+
+ 别名:
+ - bc (广播的简写)
+ - recall (广播撤回的别名)
""".strip(),
extra=PluginExtraData(
author="HibiKier",
- version="0.1",
+ version="1.2",
plugin_type=PluginType.SUPERUSER,
configs=[
RegisterConfig(
@@ -42,26 +87,106 @@ __plugin_meta__ = PluginMetadata(
).to_dict(),
)
-_matcher = on_command(
- "广播", priority=1, permission=SUPERUSER, block=True, rule=to_me()
+AnySeg = (
+ UnionPattern(
+ [
+ Text,
+ Image,
+ At,
+ AtAll,
+ Audio,
+ Video,
+ File,
+ Emoji,
+ Reply,
+ Reference,
+ Hyper,
+ Button,
+ Keyboard,
+ Voice,
+ ]
+ )
+ @ "AnySeg"
+)
+
+_matcher = on_alconna(
+ Alconna(
+ "广播",
+ Args["content?", AllParam],
+ ),
+ aliases={"bc"},
+ priority=1,
+ permission=SUPERUSER,
+ block=True,
+ rule=to_me(),
+ use_origin=False,
+)
+
+_recall_matcher = on_alconna(
+ Alconna("广播撤回"),
+ aliases={"recall"},
+ priority=1,
+ permission=SUPERUSER,
+ block=True,
+ rule=to_me(),
)
@_matcher.handle()
-async def _(
+async def handle_broadcast(
bot: Bot,
+ event: Event,
session: EventSession,
- message: UniMsg,
- command: Annotated[tuple[str, ...], Command()],
+ arp: alc.Arparma,
):
- for msg in message:
- if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]):
- msg.text = msg.text.replace(command[0], "", 1).strip()
- break
- await MessageUtils.build_message("正在发送..请等一下哦!").send()
- count, error_count = await BroadcastManage.send(bot, message, session)
- result = f"成功广播 {count} 个群组"
- if error_count:
- result += f"\n广播失败 {error_count} 个群组"
- await MessageUtils.build_message(f"发送广播完成!\n{result}").send(reply_to=True)
- logger.info(f"发送广播信息: {message}", "广播", session=session)
+ broadcast_content_msg = await _extract_broadcast_content(bot, event, arp, session)
+ if not broadcast_content_msg:
+ return
+
+ target_groups, enabled_groups = await get_broadcast_target_groups(bot, session)
+ if not target_groups or not enabled_groups:
+ return
+
+ try:
+ await send_broadcast_and_notify(
+ bot, event, broadcast_content_msg, enabled_groups, target_groups, session
+ )
+ except Exception as e:
+ error_msg = "发送广播失败"
+ BroadcastManager.log_error(error_msg, e, session)
+ await MessageUtils.build_message(f"{error_msg}。").send(reply_to=True)
+
+
+@_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)
+ user_id = str(event.get_user_id())
+ await bot.send_private_msg(user_id=user_id, message=f"{error_msg}。")
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py
deleted file mode 100644
index 1ee1a28c..00000000
--- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py
+++ /dev/null
@@ -1,72 +0,0 @@
-import asyncio
-import random
-
-from nonebot.adapters import Bot
-import nonebot_plugin_alconna as alc
-from nonebot_plugin_alconna import Image, UniMsg
-from nonebot_plugin_session import EventSession
-
-from zhenxun.services.log import logger
-from zhenxun.utils.common_utils import CommonUtils
-from zhenxun.utils.message import MessageUtils
-from zhenxun.utils.platform import PlatformUtils
-
-
-class BroadcastManage:
- @classmethod
- async def send(
- cls, bot: Bot, message: UniMsg, session: EventSession
- ) -> tuple[int, int]:
- """发送广播消息
-
- 参数:
- bot: Bot
- message: 消息内容
- session: Session
-
- 返回:
- tuple[int, int]: 发送成功的群组数量, 发送失败的群组数量
- """
- message_list = []
- for msg in message:
- if isinstance(msg, alc.Image) and msg.url:
- message_list.append(Image(url=msg.url))
- elif isinstance(msg, alc.Text):
- message_list.append(msg.text)
- group_list, _ = await PlatformUtils.get_group_list(bot)
- if group_list:
- error_count = 0
- for group in group_list:
- try:
- if not await CommonUtils.task_is_block(
- bot,
- "broadcast", # group.channel_id
- group.group_id,
- ):
- target = PlatformUtils.get_target(
- group_id=group.group_id, channel_id=group.channel_id
- )
- if target:
- await MessageUtils.build_message(message_list).send(
- target, bot
- )
- logger.debug(
- "发送成功",
- "广播",
- session=session,
- target=f"{group.group_id}:{group.channel_id}",
- )
- await asyncio.sleep(random.randint(1, 3))
- else:
- logger.warning("target为空", "广播", session=session)
- except Exception as e:
- error_count += 1
- logger.error(
- "发送失败",
- "广播",
- session=session,
- target=f"{group.group_id}:{group.channel_id}",
- e=e,
- )
- return len(group_list) - error_count, error_count
- return 0, 0
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/broadcast_manager.py b/zhenxun/builtin_plugins/superuser/broadcast/broadcast_manager.py
new file mode 100644
index 00000000..c3d7b5cc
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/broadcast_manager.py
@@ -0,0 +1,490 @@
+import asyncio
+import random
+import traceback
+from typing import ClassVar
+
+from nonebot.adapters import Bot
+from nonebot.adapters.onebot.v11 import Bot as V11Bot
+from nonebot.exception import ActionFailed
+from nonebot_plugin_alconna import UniMessage
+from nonebot_plugin_alconna.uniseg import Receipt, Reference
+from nonebot_plugin_session import EventSession
+
+from zhenxun.models.group_console import GroupConsole
+from zhenxun.services.log import logger
+from zhenxun.utils.common_utils import CommonUtils
+from zhenxun.utils.platform import PlatformUtils
+
+from .models import BroadcastDetailResult, BroadcastResult
+from .utils import custom_nodes_to_v11_nodes, uni_message_to_v11_list_of_dicts
+
+
+class BroadcastManager:
+ """广播管理器"""
+
+ _last_broadcast_msg_ids: ClassVar[dict[str, int]] = {}
+
+ @staticmethod
+ def _get_session_info(session: EventSession | None) -> str:
+ """获取会话信息字符串"""
+ if not session:
+ return ""
+
+ try:
+ platform = getattr(session, "platform", "unknown")
+ session_id = str(session)
+ return f"[{platform}:{session_id}]"
+ except Exception:
+ return "[session-info-error]"
+
+ @staticmethod
+ def log_error(
+ message: str, error: Exception, session: EventSession | None = None, **kwargs
+ ):
+ """记录错误日志"""
+ session_info = BroadcastManager._get_session_info(session)
+ error_type = type(error).__name__
+ stack_trace = traceback.format_exc()
+ error_details = f"\n类型: {error_type}\n信息: {error!s}\n堆栈: {stack_trace}"
+
+ logger.error(
+ f"{session_info} {message}{error_details}", "广播", e=error, **kwargs
+ )
+
+ @staticmethod
+ def log_warning(message: str, session: EventSession | None = None, **kwargs):
+ """记录警告级别日志"""
+ session_info = BroadcastManager._get_session_info(session)
+ logger.warning(f"{session_info} {message}", "广播", **kwargs)
+
+ @staticmethod
+ def log_info(message: str, session: EventSession | None = None, **kwargs):
+ """记录信息级别日志"""
+ session_info = BroadcastManager._get_session_info(session)
+ logger.info(f"{session_info} {message}", "广播", **kwargs)
+
+ @classmethod
+ def get_last_broadcast_msg_ids(cls) -> dict[str, int]:
+ """获取最近广播消息ID"""
+ return cls._last_broadcast_msg_ids.copy()
+
+ @classmethod
+ def clear_last_broadcast_msg_ids(cls) -> None:
+ """清空消息ID记录"""
+ cls._last_broadcast_msg_ids.clear()
+
+ @classmethod
+ async def get_all_groups(cls, bot: Bot) -> tuple[list[GroupConsole], str]:
+ """获取群组列表"""
+ return await PlatformUtils.get_group_list(bot)
+
+ @classmethod
+ async def send(
+ cls, bot: Bot, message: UniMessage, session: EventSession
+ ) -> BroadcastResult:
+ """发送广播到所有群组"""
+ logger.debug(
+ f"开始广播(send - 广播到所有群组),Bot ID: {bot.self_id}",
+ "广播",
+ session=session,
+ )
+
+ logger.debug("清空上一次的广播消息ID记录", "广播", session=session)
+ cls.clear_last_broadcast_msg_ids()
+
+ all_groups, _ = await cls.get_all_groups(bot)
+ return await cls.send_to_specific_groups(bot, message, all_groups, session)
+
+ @classmethod
+ async def send_to_specific_groups(
+ cls,
+ bot: Bot,
+ message: UniMessage,
+ target_groups: list[GroupConsole],
+ session_info: EventSession | str | None = None,
+ ) -> BroadcastResult:
+ """发送广播到指定群组"""
+ log_session = session_info or bot.self_id
+ logger.debug(
+ f"开始广播,目标 {len(target_groups)} 个群组,Bot ID: {bot.self_id}",
+ "广播",
+ session=log_session,
+ )
+
+ if not target_groups:
+ logger.debug("目标群组列表为空,广播结束", "广播", session=log_session)
+ return 0, 0
+
+ platform = PlatformUtils.get_platform(bot)
+ is_forward_broadcast = any(
+ isinstance(seg, Reference) and getattr(seg, "nodes", None)
+ for seg in message
+ )
+
+ if platform == "qq" and isinstance(bot, V11Bot) and is_forward_broadcast:
+ if (
+ len(message) == 1
+ and isinstance(message[0], Reference)
+ and getattr(message[0], "nodes", None)
+ ):
+ nodes_list = getattr(message[0], "nodes", [])
+ v11_nodes = custom_nodes_to_v11_nodes(nodes_list)
+ node_count = len(v11_nodes)
+ logger.debug(
+ f"从 UniMessage 构造转发节点数: {node_count}",
+ "广播",
+ session=log_session,
+ )
+ else:
+ logger.warning(
+ "广播消息包含合并转发段和其他段,将尝试打平成一个节点发送",
+ "广播",
+ session=log_session,
+ )
+ v11_content_list = uni_message_to_v11_list_of_dicts(message)
+ v11_nodes = (
+ [
+ {
+ "type": "node",
+ "data": {
+ "user_id": bot.self_id,
+ "nickname": "广播",
+ "content": v11_content_list,
+ },
+ }
+ ]
+ if v11_content_list
+ else []
+ )
+
+ if not v11_nodes:
+ logger.warning(
+ "构造出的 V11 合并转发节点为空,无法发送",
+ "广播",
+ session=log_session,
+ )
+ return 0, len(target_groups)
+ success_count, error_count, skip_count = await cls._broadcast_forward(
+ bot, log_session, target_groups, v11_nodes
+ )
+ else:
+ if is_forward_broadcast:
+ logger.warning(
+ f"合并转发消息在适配器 ({platform}) 不支持,将作为普通消息发送",
+ "广播",
+ session=log_session,
+ )
+ success_count, error_count, skip_count = await cls._broadcast_normal(
+ bot, log_session, target_groups, message
+ )
+
+ total = len(target_groups)
+ stats = f"成功: {success_count}, 失败: {error_count}"
+ stats += f", 跳过: {skip_count}, 总计: {total}"
+ logger.debug(
+ f"广播统计 - {stats}",
+ "广播",
+ session=log_session,
+ )
+
+ msg_ids = cls.get_last_broadcast_msg_ids()
+ if msg_ids:
+ id_list_str = ", ".join([f"{k}:{v}" for k, v in msg_ids.items()])
+ logger.debug(
+ f"广播结束,记录了 {len(msg_ids)} 条消息ID: {id_list_str}",
+ "广播",
+ session=log_session,
+ )
+ else:
+ logger.warning(
+ "广播结束,但没有记录任何消息ID",
+ "广播",
+ session=log_session,
+ )
+
+ return success_count, error_count
+
+ @classmethod
+ async def _extract_message_id_from_result(
+ cls,
+ result: dict | Receipt,
+ group_key: str,
+ session_info: EventSession | str,
+ msg_type: str = "普通",
+ ) -> None:
+ """提取消息ID并记录"""
+ if isinstance(result, dict) and "message_id" in result:
+ msg_id = result["message_id"]
+ try:
+ msg_id_int = int(msg_id)
+ cls._last_broadcast_msg_ids[group_key] = msg_id_int
+ logger.debug(
+ f"记录群 {group_key} 的{msg_type}消息ID: {msg_id_int}",
+ "广播",
+ session=session_info,
+ )
+ except (ValueError, TypeError):
+ logger.warning(
+ f"{msg_type}结果中的 message_id 不是有效整数: {msg_id}",
+ "广播",
+ session=session_info,
+ )
+ elif isinstance(result, Receipt) and result.msg_ids:
+ try:
+ first_id_info = result.msg_ids[0]
+ msg_id = None
+ if isinstance(first_id_info, dict) and "message_id" in first_id_info:
+ msg_id = first_id_info["message_id"]
+ logger.debug(
+ f"从 Receipt.msg_ids[0] 提取到 ID: {msg_id}",
+ "广播",
+ session=session_info,
+ )
+ elif isinstance(first_id_info, int | str):
+ msg_id = first_id_info
+ logger.debug(
+ f"从 Receipt.msg_ids[0] 提取到原始ID: {msg_id}",
+ "广播",
+ session=session_info,
+ )
+
+ if msg_id is not None:
+ try:
+ msg_id_int = int(msg_id)
+ cls._last_broadcast_msg_ids[group_key] = msg_id_int
+ logger.debug(
+ f"记录群 {group_key} 的消息ID: {msg_id_int}",
+ "广播",
+ session=session_info,
+ )
+ except (ValueError, TypeError):
+ logger.warning(
+ f"提取的ID ({msg_id}) 不是有效整数",
+ "广播",
+ session=session_info,
+ )
+ else:
+ info_str = str(first_id_info)
+ logger.warning(
+ f"无法从 Receipt.msg_ids[0] 提取ID: {info_str}",
+ "广播",
+ session=session_info,
+ )
+ except IndexError:
+ logger.warning("Receipt.msg_ids 为空", "广播", session=session_info)
+ except Exception as e_extract:
+ logger.error(
+ f"从 Receipt 提取 msg_id 时出错: {e_extract}",
+ "广播",
+ session=session_info,
+ e=e_extract,
+ )
+ else:
+ logger.warning(
+ f"发送成功但无法从结果获取消息 ID. 结果: {result}",
+ "广播",
+ session=session_info,
+ )
+
+ @classmethod
+ async def _check_group_availability(cls, bot: Bot, group: GroupConsole) -> bool:
+ """检查群组是否可用"""
+ if not group.group_id:
+ return False
+
+ if await CommonUtils.task_is_block(bot, "broadcast", group.group_id):
+ return False
+
+ return True
+
+ @classmethod
+ async def _broadcast_forward(
+ cls,
+ bot: V11Bot,
+ session_info: EventSession | str,
+ group_list: list[GroupConsole],
+ v11_nodes: list[dict],
+ ) -> BroadcastDetailResult:
+ """发送合并转发"""
+ success_count = 0
+ error_count = 0
+ skip_count = 0
+
+ for _, group in enumerate(group_list):
+ group_key = group.group_id or group.channel_id
+
+ if not await cls._check_group_availability(bot, group):
+ skip_count += 1
+ continue
+
+ try:
+ result = await bot.send_group_forward_msg(
+ group_id=int(group.group_id), messages=v11_nodes
+ )
+
+ logger.debug(
+ f"合并转发消息发送结果: {result}, 类型: {type(result)}",
+ "广播",
+ session=session_info,
+ )
+
+ await cls._extract_message_id_from_result(
+ result, group_key, session_info, "合并转发"
+ )
+
+ success_count += 1
+ await asyncio.sleep(random.randint(1, 3))
+ except ActionFailed as af_e:
+ error_count += 1
+ logger.error(
+ f"发送失败(合并转发) to {group_key}: {af_e}",
+ "广播",
+ session=session_info,
+ e=af_e,
+ )
+ except Exception as e:
+ error_count += 1
+ logger.error(
+ f"发送失败(合并转发) to {group_key}: {e}",
+ "广播",
+ session=session_info,
+ e=e,
+ )
+
+ return success_count, error_count, skip_count
+
+ @classmethod
+ async def _broadcast_normal(
+ cls,
+ bot: Bot,
+ session_info: EventSession | str,
+ group_list: list[GroupConsole],
+ message: UniMessage,
+ ) -> BroadcastDetailResult:
+ """发送普通消息"""
+ success_count = 0
+ error_count = 0
+ skip_count = 0
+
+ for _, group in enumerate(group_list):
+ group_key = (
+ f"{group.group_id}:{group.channel_id}"
+ if group.channel_id
+ else str(group.group_id)
+ )
+
+ if not await cls._check_group_availability(bot, group):
+ skip_count += 1
+ continue
+
+ try:
+ target = PlatformUtils.get_target(
+ group_id=group.group_id, channel_id=group.channel_id
+ )
+
+ if target:
+ receipt: Receipt = await message.send(target, bot=bot)
+
+ logger.debug(
+ f"广播消息发送结果: {receipt}, 类型: {type(receipt)}",
+ "广播",
+ session=session_info,
+ )
+
+ await cls._extract_message_id_from_result(
+ receipt, group_key, session_info
+ )
+
+ success_count += 1
+ await asyncio.sleep(random.randint(1, 3))
+ else:
+ logger.warning(
+ "target为空", "广播", session=session_info, target=group_key
+ )
+ skip_count += 1
+ except Exception as e:
+ error_count += 1
+ logger.error(
+ f"发送失败(普通) to {group_key}: {e}",
+ "广播",
+ session=session_info,
+ e=e,
+ )
+
+ return success_count, error_count, skip_count
+
+ @classmethod
+ async def recall_last_broadcast(
+ cls, bot: Bot, session_info: EventSession | str
+ ) -> BroadcastResult:
+ """撤回最近广播"""
+ msg_ids_to_recall = cls.get_last_broadcast_msg_ids()
+
+ if not msg_ids_to_recall:
+ logger.warning(
+ "没有找到最近的广播消息ID记录", "广播撤回", session=session_info
+ )
+ return 0, 0
+
+ id_list_str = ", ".join([f"{k}:{v}" for k, v in msg_ids_to_recall.items()])
+ logger.debug(
+ f"找到 {len(msg_ids_to_recall)} 条广播消息ID记录: {id_list_str}",
+ "广播撤回",
+ session=session_info,
+ )
+
+ success_count = 0
+ error_count = 0
+
+ logger.info(
+ f"准备撤回 {len(msg_ids_to_recall)} 条广播消息",
+ "广播撤回",
+ session=session_info,
+ )
+
+ for group_key, msg_id in msg_ids_to_recall.items():
+ try:
+ logger.debug(
+ f"尝试撤回消息 (ID: {msg_id}) in {group_key}",
+ "广播撤回",
+ session=session_info,
+ )
+ await bot.call_api("delete_msg", message_id=msg_id)
+ success_count += 1
+ except ActionFailed as af_e:
+ retcode = getattr(af_e, "retcode", None)
+ wording = getattr(af_e, "wording", "")
+ if retcode == 100 and "MESSAGE_NOT_FOUND" in wording.upper():
+ logger.warning(
+ f"消息 (ID: {msg_id}) 可能已被撤回或不存在于 {group_key}",
+ "广播撤回",
+ session=session_info,
+ )
+ elif retcode == 300 and "delete message" in wording.lower():
+ logger.warning(
+ f"消息 (ID: {msg_id}) 可能已被撤回或不存在于 {group_key}",
+ "广播撤回",
+ session=session_info,
+ )
+ else:
+ error_count += 1
+ logger.error(
+ f"撤回消息失败 (ID: {msg_id}) in {group_key}: {af_e}",
+ "广播撤回",
+ session=session_info,
+ e=af_e,
+ )
+ except Exception as e:
+ error_count += 1
+ logger.error(
+ f"撤回消息时发生未知错误 (ID: {msg_id}) in {group_key}: {e}",
+ "广播撤回",
+ session=session_info,
+ e=e,
+ )
+ await asyncio.sleep(0.2)
+
+ logger.debug("撤回操作完成,清空消息ID记录", "广播撤回", session=session_info)
+ cls.clear_last_broadcast_msg_ids()
+
+ return success_count, error_count
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/message_processor.py b/zhenxun/builtin_plugins/superuser/broadcast/message_processor.py
new file mode 100644
index 00000000..809e3645
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/message_processor.py
@@ -0,0 +1,584 @@
+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.services.log import logger
+from zhenxun.utils.common_utils import CommonUtils
+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":
+ video_seg = None
+ if data_dict.get("url"):
+ video_seg = Video(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)
+ video_seg = Video(raw=raw_bytes)
+ else:
+ video_seg = Video(path=file_val)
+ if video_seg:
+ result.append(video_seg)
+ logger.debug(f"[Depth {depth}] 处理视频消息成功", "广播")
+ else:
+ logger.warning(f"[Depth {depth}] V11 视频 {index} 缺少URL/文件", "广播")
+ 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
+) -> tuple[list, list]:
+ """获取广播目标群组和启用了广播功能的群组"""
+ target_groups = []
+ all_groups, _ = await BroadcastManager.get_all_groups(bot)
+
+ current_group_id = None
+ if hasattr(session, "id2") and session.id2:
+ current_group_id = session.id2
+
+ if current_group_id:
+ target_groups = [
+ group for group in all_groups if group.group_id != current_group_id
+ ]
+ logger.info(
+ f"向除当前群组({current_group_id})外的所有群组广播", "广播", session=session
+ )
+ else:
+ target_groups = all_groups
+ logger.info("向所有群组广播", "广播", session=session)
+
+ if not target_groups:
+ await MessageUtils.build_message("没有找到符合条件的广播目标群组。").send(
+ reply_to=True
+ )
+ return [], []
+
+ enabled_groups = []
+ for group in target_groups:
+ if not await CommonUtils.task_is_block(bot, "broadcast", group.group_id):
+ enabled_groups.append(group)
+
+ if not enabled_groups:
+ await MessageUtils.build_message(
+ "没有启用了广播功能的目标群组可供立即发送。"
+ ).send(reply_to=True)
+ return target_groups, []
+
+ return target_groups, enabled_groups
+
+
+async def send_broadcast_and_notify(
+ bot: Bot,
+ event: Event,
+ message: UniMessage,
+ enabled_groups: list,
+ target_groups: list,
+ session: EventSession,
+) -> None:
+ """发送广播并通知结果"""
+ BroadcastManager.clear_last_broadcast_msg_ids()
+ count, error_count = await BroadcastManager.send_to_specific_groups(
+ bot, message, enabled_groups, session
+ )
+
+ result = f"成功广播 {count} 个群组"
+ if error_count:
+ result += f"\n发送失败 {error_count} 个群组"
+ result += f"\n有效: {len(enabled_groups)} / 总计: {len(target_groups)}"
+
+ user_id = str(event.get_user_id())
+ await bot.send_private_msg(user_id=user_id, message=f"发送广播完成!\n{result}")
+
+ BroadcastManager.log_info(
+ f"广播完成,有效/总计: {len(enabled_groups)}/{len(target_groups)}",
+ session,
+ )
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/models.py b/zhenxun/builtin_plugins/superuser/broadcast/models.py
new file mode 100644
index 00000000..4bcdf936
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/models.py
@@ -0,0 +1,64 @@
+from datetime import datetime
+from typing import Any
+
+from nonebot_plugin_alconna import UniMessage
+
+from zhenxun.models.group_console import GroupConsole
+
+GroupKey = str
+MessageID = int
+BroadcastResult = tuple[int, int]
+BroadcastDetailResult = tuple[int, int, int]
+
+
+class BroadcastTarget:
+ """广播目标"""
+
+ def __init__(self, group_id: str, channel_id: str | None = None):
+ self.group_id = group_id
+ self.channel_id = channel_id
+
+ def to_dict(self) -> dict[str, str | None]:
+ """转换为字典格式"""
+ return {"group_id": self.group_id, "channel_id": self.channel_id}
+
+ @classmethod
+ def from_group_console(cls, group: GroupConsole) -> "BroadcastTarget":
+ """从 GroupConsole 对象创建"""
+ return cls(group_id=group.group_id, channel_id=group.channel_id)
+
+ @property
+ def key(self) -> str:
+ """获取群组的唯一标识"""
+ if self.channel_id:
+ return f"{self.group_id}:{self.channel_id}"
+ return str(self.group_id)
+
+
+class BroadcastTask:
+ """广播任务"""
+
+ def __init__(
+ self,
+ bot_id: str,
+ message: UniMessage,
+ targets: list[BroadcastTarget],
+ scheduled_time: datetime | None = None,
+ task_id: str | None = None,
+ ):
+ self.bot_id = bot_id
+ self.message = message
+ self.targets = targets
+ self.scheduled_time = scheduled_time
+ self.task_id = task_id
+
+ def to_dict(self) -> dict[str, Any]:
+ """转换为字典格式,用于序列化"""
+ return {
+ "bot_id": self.bot_id,
+ "targets": [t.to_dict() for t in self.targets],
+ "scheduled_time": self.scheduled_time.isoformat()
+ if self.scheduled_time
+ else None,
+ "task_id": self.task_id,
+ }
diff --git a/zhenxun/builtin_plugins/superuser/broadcast/utils.py b/zhenxun/builtin_plugins/superuser/broadcast/utils.py
new file mode 100644
index 00000000..748559fd
--- /dev/null
+++ b/zhenxun/builtin_plugins/superuser/broadcast/utils.py
@@ -0,0 +1,175 @@
+import base64
+
+import nonebot_plugin_alconna as alc
+from nonebot_plugin_alconna import UniMessage
+from nonebot_plugin_alconna.uniseg import Reference
+from nonebot_plugin_alconna.uniseg.segment import CustomNode, Video
+
+from zhenxun.services.log import logger
+
+
+def uni_segment_to_v11_segment_dict(
+ seg: alc.Segment, depth: int = 0
+) -> dict | list[dict] | None:
+ """UniSeg段转V11字典"""
+ if isinstance(seg, alc.Text):
+ return {"type": "text", "data": {"text": seg.text}}
+ elif isinstance(seg, alc.Image):
+ if getattr(seg, "url", None):
+ return {
+ "type": "image",
+ "data": {"file": seg.url},
+ }
+ elif getattr(seg, "raw", None):
+ raw_data = seg.raw
+ if isinstance(raw_data, str):
+ if len(raw_data) >= 9 and raw_data[:9] == "base64://":
+ return {"type": "image", "data": {"file": raw_data}}
+ elif isinstance(raw_data, bytes):
+ b64_str = base64.b64encode(raw_data).decode()
+ return {"type": "image", "data": {"file": f"base64://{b64_str}"}}
+ else:
+ logger.warning(f"无法处理 Image.raw 的类型: {type(raw_data)}", "广播")
+ elif getattr(seg, "path", None):
+ logger.warning(
+ f"在合并转发中使用了本地图片路径,可能无法显示: {seg.path}", "广播"
+ )
+ return {"type": "image", "data": {"file": f"file:///{seg.path}"}}
+ else:
+ logger.warning(f"alc.Image 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
+ elif isinstance(seg, alc.At):
+ return {"type": "at", "data": {"qq": seg.target}}
+ elif isinstance(seg, alc.AtAll):
+ return {"type": "at", "data": {"qq": "all"}}
+ elif isinstance(seg, Video):
+ if getattr(seg, "url", None):
+ return {
+ "type": "video",
+ "data": {"file": seg.url},
+ }
+ elif getattr(seg, "raw", None):
+ raw_data = seg.raw
+ if isinstance(raw_data, str):
+ if len(raw_data) >= 9 and raw_data[:9] == "base64://":
+ return {"type": "video", "data": {"file": raw_data}}
+ elif isinstance(raw_data, bytes):
+ b64_str = base64.b64encode(raw_data).decode()
+ return {"type": "video", "data": {"file": f"base64://{b64_str}"}}
+ else:
+ logger.warning(f"无法处理 Video.raw 的类型: {type(raw_data)}", "广播")
+ elif getattr(seg, "path", None):
+ logger.warning(
+ f"在合并转发中使用了本地视频路径,可能无法显示: {seg.path}", "广播"
+ )
+ return {"type": "video", "data": {"file": f"file:///{seg.path}"}}
+ else:
+ logger.warning(f"Video 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
+ elif isinstance(seg, Reference) and getattr(seg, "nodes", None):
+ if depth >= 3:
+ logger.warning(
+ f"嵌套转发深度超过限制 (depth={depth}),不再继续解析", "广播"
+ )
+ return {"type": "text", "data": {"text": "[嵌套转发层数过多,内容已省略]"}}
+
+ nested_v11_content_list = []
+ nodes_list = getattr(seg, "nodes", [])
+ for node in nodes_list:
+ if isinstance(node, CustomNode):
+ node_v11_content = []
+ if isinstance(node.content, UniMessage):
+ for nested_seg in node.content:
+ converted_dict = uni_segment_to_v11_segment_dict(
+ nested_seg, depth + 1
+ )
+ if isinstance(converted_dict, list):
+ node_v11_content.extend(converted_dict)
+ elif converted_dict:
+ node_v11_content.append(converted_dict)
+ elif isinstance(node.content, str):
+ node_v11_content.append(
+ {"type": "text", "data": {"text": node.content}}
+ )
+ if node_v11_content:
+ separator = {
+ "type": "text",
+ "data": {
+ "text": f"\n--- 来自 {node.name} ({node.uid}) 的消息 ---\n"
+ },
+ }
+ nested_v11_content_list.insert(0, separator)
+ nested_v11_content_list.extend(node_v11_content)
+ nested_v11_content_list.append(
+ {"type": "text", "data": {"text": "\n---\n"}}
+ )
+
+ return nested_v11_content_list
+
+ else:
+ logger.warning(f"广播时跳过不支持的 UniSeg 段类型: {type(seg)}", "广播")
+ return None
+
+
+def uni_message_to_v11_list_of_dicts(uni_msg: UniMessage | str | list) -> list[dict]:
+ """UniMessage转V11字典列表"""
+ try:
+ if isinstance(uni_msg, str):
+ return [{"type": "text", "data": {"text": uni_msg}}]
+
+ if isinstance(uni_msg, list):
+ if not uni_msg:
+ return []
+
+ if all(isinstance(item, str) for item in uni_msg):
+ return [{"type": "text", "data": {"text": item}} for item in uni_msg]
+
+ result = []
+ for item in uni_msg:
+ if hasattr(item, "__iter__") and not isinstance(item, str | bytes):
+ result.extend(uni_message_to_v11_list_of_dicts(item))
+ elif hasattr(item, "text") and not isinstance(item, str | bytes):
+ text_value = getattr(item, "text", "")
+ result.append({"type": "text", "data": {"text": str(text_value)}})
+ elif hasattr(item, "url") and not isinstance(item, str | bytes):
+ url_value = getattr(item, "url", "")
+ if isinstance(item, Video):
+ result.append(
+ {"type": "video", "data": {"file": str(url_value)}}
+ )
+ else:
+ result.append(
+ {"type": "image", "data": {"file": str(url_value)}}
+ )
+ else:
+ try:
+ result.append({"type": "text", "data": {"text": str(item)}})
+ except Exception as e:
+ logger.warning(f"无法转换列表元素: {item}, 错误: {e}", "广播")
+ return result
+ except Exception as e:
+ logger.warning(f"消息转换过程中出错: {e}", "广播")
+
+ return [{"type": "text", "data": {"text": str(uni_msg)}}]
+
+
+def custom_nodes_to_v11_nodes(custom_nodes: list[CustomNode]) -> list[dict]:
+ """CustomNode列表转V11节点"""
+ v11_nodes = []
+ for node in custom_nodes:
+ v11_content_list = uni_message_to_v11_list_of_dicts(node.content)
+
+ if v11_content_list:
+ v11_nodes.append(
+ {
+ "type": "node",
+ "data": {
+ "user_id": str(node.uid),
+ "nickname": node.name,
+ "content": v11_content_list,
+ },
+ }
+ )
+ else:
+ logger.warning(
+ f"CustomNode (uid={node.uid}) 内容转换后为空,跳过此节点", "广播"
+ )
+ return v11_nodes
From 9cda0e5d8f2a930ca778b1b3b1e7014e9f6a1f44 Mon Sep 17 00:00:00 2001
From: HibiKier <45528451+HibiKier@users.noreply.github.com>
Date: Thu, 15 May 2025 23:52:20 +0800
Subject: [PATCH 13/13] =?UTF-8?q?:sparkles:=20=E9=80=82=E9=85=8D=E6=96=B0?=
=?UTF-8?q?=E7=89=88=E6=9C=ACwebui=20(#1905)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: molanp <104612722+molanp@users.noreply.github.com>
Co-authored-by: BalconyJH <73932916+BalconyJH@users.noreply.github.com>
---
zhenxun/builtin_plugins/help/_data_source.py | 4 +++-
zhenxun/builtin_plugins/help/_utils.py | 10 ++++++++--
zhenxun/builtin_plugins/help/html_help.py | 17 +++++++++++++----
zhenxun/builtin_plugins/help/zhenxun_help.py | 14 ++++++++++++--
.../web_ui/api/menu/data_source.py | 5 ++++-
.../web_ui/api/tabs/main/__init__.py | 3 ++-
.../web_ui/api/tabs/main/data_source.py | 2 +-
.../web_ui/api/tabs/manage/chat.py | 11 +++++++----
.../web_ui/api/tabs/manage/model.py | 2 ++
.../web_ui/api/tabs/plugin_manage/__init__.py | 6 +++++-
.../api/tabs/plugin_manage/data_source.py | 4 ++++
.../web_ui/api/tabs/plugin_manage/model.py | 7 +++++++
.../web_ui/api/tabs/system/__init__.py | 12 ++++++++++++
.../web_ui/api/tabs/system/model.py | 4 ++++
zhenxun/builtin_plugins/web_ui/utils.py | 18 +++++++++++++++++-
15 files changed, 101 insertions(+), 18 deletions(-)
diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py
index cfaa4503..86f42536 100644
--- a/zhenxun/builtin_plugins/help/_data_source.py
+++ b/zhenxun/builtin_plugins/help/_data_source.py
@@ -40,7 +40,9 @@ async def create_help_img(
match help_type:
case "html":
- result = BuildImage.open(await build_html_image(group_id, is_detail))
+ result = BuildImage.open(
+ await build_html_image(session, group_id, is_detail)
+ )
case "zhenxun":
result = BuildImage.open(
await build_zhenxun_image(session, group_id, is_detail)
diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py
index 6c382c7d..0554fc8d 100644
--- a/zhenxun/builtin_plugins/help/_utils.py
+++ b/zhenxun/builtin_plugins/help/_utils.py
@@ -1,5 +1,8 @@
from collections.abc import Callable
+from nonebot_plugin_uninfo import Uninfo
+
+from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import PluginType
@@ -27,13 +30,15 @@ async def sort_type() -> dict[str, list[PluginInfo]]:
async def classify_plugin(
- group_id: str | None, is_detail: bool, handle: Callable
+ session: Uninfo, group_id: str | None, is_detail: bool, handle: Callable
) -> dict[str, list]:
"""对插件进行分类并判断状态
参数:
+ session: Uninfo对象
group_id: 群组id
is_detail: 是否详细帮助
+ handle: 回调方法
返回:
dict[str, list[Item]]: 分类插件数据
@@ -41,9 +46,10 @@ async def classify_plugin(
sort_data = await sort_type()
classify: dict[str, list] = {}
group = await GroupConsole.get_or_none(group_id=group_id) if group_id else None
+ bot = await BotConsole.get_or_none(bot_id=session.self_id)
for menu, value in sort_data.items():
for plugin in value:
if not classify.get(menu):
classify[menu] = []
- classify[menu].append(handle(plugin, group, is_detail))
+ classify[menu].append(handle(bot, plugin, group, is_detail))
return classify
diff --git a/zhenxun/builtin_plugins/help/html_help.py b/zhenxun/builtin_plugins/help/html_help.py
index 1815b99a..7c552a0d 100644
--- a/zhenxun/builtin_plugins/help/html_help.py
+++ b/zhenxun/builtin_plugins/help/html_help.py
@@ -2,9 +2,11 @@ import os
import random
from nonebot_plugin_htmlrender import template_to_pic
+from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel
from zhenxun.configs.path_config import TEMPLATE_PATH
+from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType
@@ -48,11 +50,12 @@ ICON2STR = {
def __handle_item(
- plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
+ bot: BotConsole, plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
) -> Item:
"""构造Item
参数:
+ bot: BotConsole
plugin: PluginInfo
group: 群组
is_detail: 是否详细
@@ -73,10 +76,13 @@ def __handle_item(
]:
sta = 2
if group:
- if f"{plugin.module}:super," in group.block_plugin:
+ if f"{plugin.module}," in group.superuser_block_plugin:
sta = 2
if f"{plugin.module}," in group.block_plugin:
sta = 1
+ if bot:
+ if f"{plugin.module}," in bot.block_plugins:
+ sta = 2
return Item(plugin_name=plugin.name, sta=sta)
@@ -119,14 +125,17 @@ def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
return plugin_list
-async def build_html_image(group_id: str | None, is_detail: bool) -> bytes:
+async def build_html_image(
+ session: Uninfo, group_id: str | None, is_detail: bool
+) -> bytes:
"""构造HTML帮助图片
参数:
+ session: Uninfo
group_id: 群号
is_detail: 是否详细帮助
"""
- classify = await classify_plugin(group_id, is_detail, __handle_item)
+ classify = await classify_plugin(session, group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify)
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "menu").absolute()),
diff --git a/zhenxun/builtin_plugins/help/zhenxun_help.py b/zhenxun/builtin_plugins/help/zhenxun_help.py
index f6d930e6..b96d3c59 100644
--- a/zhenxun/builtin_plugins/help/zhenxun_help.py
+++ b/zhenxun/builtin_plugins/help/zhenxun_help.py
@@ -6,6 +6,7 @@ from pydantic import BaseModel
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.configs.utils import PluginExtraData
+from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType
@@ -21,12 +22,19 @@ class Item(BaseModel):
"""插件命令"""
-def __handle_item(plugin: PluginInfo, group: GroupConsole | None, is_detail: bool):
+def __handle_item(
+ bot: BotConsole | None,
+ plugin: PluginInfo,
+ group: GroupConsole | None,
+ is_detail: bool,
+):
"""构造Item
参数:
+ bot: BotConsole
plugin: PluginInfo
group: 群组
+ is_detail: 是否为详细
返回:
Item: Item
@@ -40,6 +48,8 @@ def __handle_item(plugin: PluginInfo, group: GroupConsole | None, is_detail: boo
plugin.name = f"{plugin.name}(不可用)"
elif group and f"{plugin.module}," in group.block_plugin:
plugin.name = f"{plugin.name}(不可用)"
+ elif bot and f"{plugin.module}," in bot.block_plugins:
+ plugin.name = f"{plugin.name}(不可用)"
commands = []
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
@@ -142,7 +152,7 @@ async def build_zhenxun_image(
group_id: 群号
is_detail: 是否详细帮助
"""
- classify = await classify_plugin(group_id, is_detail, __handle_item)
+ classify = await classify_plugin(session, group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify)
platform = PlatformUtils.get_platform(session)
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py
index 9cfcd244..14f5c928 100644
--- a/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py
+++ b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py
@@ -46,7 +46,10 @@ class MenuManage:
icon="database",
),
MenuItem(
- name="系统信息", module="system", router="/system", icon="system"
+ name="文件管理", module="system", router="/system", icon="system"
+ ),
+ MenuItem(
+ name="关于我们", module="about", router="/about", icon="about"
),
]
self.save()
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py
index 36059101..f93d0ab1 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/main/__init__.py
@@ -16,7 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result
from ....config import QueryDateType
-from ....utils import authentication, get_system_status
+from ....utils import authentication, clear_help_image, get_system_status
from .data_source import ApiDataSource
from .model import (
ActiveGroup,
@@ -234,6 +234,7 @@ async def _(param: BotManageUpdateParam):
bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins)
bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks)
await bot_data.save(update_fields=["block_plugins", "block_tasks"])
+ clear_help_image()
return Result.ok()
except Exception as e:
logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e)
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py
index f9ff6fca..e87647dd 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/main/data_source.py
@@ -92,7 +92,7 @@ class ApiDataSource:
"""
version_file = Path() / "__version__"
if version_file.exists():
- if text := version_file.open().read():
+ if text := version_file.open(encoding="utf-8").read():
return text.replace("__version__: ", "").strip()
return "unknown"
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py
index d20149fb..389546ca 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/chat.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
from fastapi import APIRouter
import nonebot
from nonebot import on_message
@@ -49,13 +51,14 @@ async def message_handle(
message: UniMsg,
group_id: str | None,
):
+ time = str(datetime.now().replace(microsecond=0))
messages = []
for m in message:
if isinstance(m, Text | str):
- messages.append(MessageItem(type="text", msg=str(m)))
+ messages.append(MessageItem(type="text", msg=str(m), time=time))
elif isinstance(m, Image):
if m.url:
- messages.append(MessageItem(type="img", msg=m.url))
+ messages.append(MessageItem(type="img", msg=m.url, time=time))
elif isinstance(m, At):
if group_id:
if m.target == "0":
@@ -72,9 +75,9 @@ async def message_handle(
uname = group_user.user_name
if m.target not in ID2NAME[group_id]:
ID2NAME[group_id][m.target] = uname
- messages.append(MessageItem(type="at", msg=f"@{uname}"))
+ messages.append(MessageItem(type="at", msg=f"@{uname}", time=time))
elif isinstance(m, Hyper):
- messages.append(MessageItem(type="text", msg="[分享消息]"))
+ messages.append(MessageItem(type="text", msg="[分享消息]", time=time))
return messages
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py
index 7149cee1..68772d0f 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/manage/model.py
@@ -237,6 +237,8 @@ class MessageItem(BaseModel):
"""消息类型"""
msg: str
"""内容"""
+ time: str
+ """发送日期"""
class Message(BaseModel):
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
index 45878880..9dd134a4 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/__init__.py
@@ -6,7 +6,7 @@ from zhenxun.services.log import logger
from zhenxun.utils.enum import BlockType, PluginType
from ....base_model import Result
-from ....utils import authentication
+from ....utils import authentication, clear_help_image
from .data_source import ApiDataSource
from .model import (
BatchUpdatePlugins,
@@ -80,6 +80,7 @@ async def _() -> Result[PluginCount]:
async def _(param: UpdatePlugin) -> Result:
try:
await ApiDataSource.update_plugin(param)
+ clear_help_image()
return Result.ok(info="已经帮你写好啦!")
except (ValueError, KeyError):
return Result.fail("插件数据不存在...")
@@ -107,6 +108,7 @@ async def _(param: PluginSwitch) -> Result:
db_plugin.block_type = None
db_plugin.status = True
await db_plugin.save()
+ clear_help_image()
return Result.ok(info="成功改变了开关状态!")
except Exception as e:
logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e)
@@ -173,6 +175,7 @@ async def batch_update_plugin_config_api(
updated_count=result_dict["updated_count"],
errors=result_dict["errors"],
)
+ clear_help_image()
return Result.ok(result_model, "插件配置更新完成")
except Exception as e:
logger.error(f"{router.prefix}/plugins/batch_update 调用错误", "WebUi", e=e)
@@ -192,6 +195,7 @@ async def rename_menu_type_api(payload: RenameMenuTypePayload) -> Result:
old_name=payload.old_name, new_name=payload.new_name
)
if result.get("success"):
+ clear_help_image()
return Result.ok(
info=result.get(
"info",
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
index d525c9bf..0f2c3676 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/data_source.py
@@ -52,6 +52,10 @@ class ApiDataSource:
status=plugin.status,
author=plugin.author,
block_type=plugin.block_type,
+ is_builtin="builtin_plugins" in plugin.module_path
+ or plugin.plugin_type == PluginType.HIDDEN,
+ allow_setting=plugin.plugin_type != PluginType.HIDDEN,
+ allow_switch=plugin.plugin_type != PluginType.HIDDEN,
)
plugin_list.append(plugin_info)
return plugin_list
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
index c2bcc4bb..579f3104 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/plugin_manage/model.py
@@ -78,6 +78,13 @@ class PluginInfo(BaseModel):
author: str | None = None
"""作者"""
block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)")
+ """禁用状态"""
+ is_builtin: bool = False
+ """是否为内置插件"""
+ allow_switch: bool = True
+ """是否允许开关"""
+ allow_setting: bool = True
+ """是否允许设置"""
class PluginConfig(BaseModel):
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py
index aa92306a..ffcd05be 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py
@@ -36,6 +36,8 @@ async def _(path: str | None = None) -> Result[list[DirFile]]:
is_image=is_image,
name=file,
parent=path,
+ size=None if file_path.is_dir() else file_path.stat().st_size,
+ mtime=file_path.stat().st_mtime,
)
)
return Result.ok(data_list)
@@ -215,3 +217,13 @@ async def _(full_path: str) -> Result[str]:
return Result.ok(BuildImage.open(path).pic2bs4())
except Exception as e:
return Result.warning_(f"获取图片失败: {e!s}")
+
+
+@router.get(
+ "/ping",
+ response_model=Result[str],
+ response_class=JSONResponse,
+ description="检查服务器状态",
+)
+async def _() -> Result[str]:
+ return Result.ok("pong")
diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/system/model.py b/zhenxun/builtin_plugins/web_ui/api/tabs/system/model.py
index 3c2357f2..2959a0e1 100644
--- a/zhenxun/builtin_plugins/web_ui/api/tabs/system/model.py
+++ b/zhenxun/builtin_plugins/web_ui/api/tabs/system/model.py
@@ -14,6 +14,10 @@ class DirFile(BaseModel):
"""文件夹或文件名称"""
parent: str | None = None
"""父级"""
+ size: int | None = None
+ """文件大小"""
+ mtime: float | None = None
+ """修改时间"""
class DeleteFile(BaseModel):
diff --git a/zhenxun/builtin_plugins/web_ui/utils.py b/zhenxun/builtin_plugins/web_ui/utils.py
index df2fdd35..a7e22a07 100644
--- a/zhenxun/builtin_plugins/web_ui/utils.py
+++ b/zhenxun/builtin_plugins/web_ui/utils.py
@@ -11,7 +11,7 @@ import psutil
import ujson as json
from zhenxun.configs.config import Config
-from zhenxun.configs.path_config import DATA_PATH
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
from .base_model import SystemFolderSize, SystemStatus, User
@@ -28,6 +28,22 @@ if token_file.exists():
token_data = json.load(open(token_file, encoding="utf8"))
+GROUP_HELP_PATH = DATA_PATH / "group_help"
+SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png"
+SIMPLE_DETAIL_HELP_IMAGE = IMAGE_PATH / "SIMPLE_DETAIL_HELP.png"
+
+
+def clear_help_image():
+ """清理帮助图片"""
+ if SIMPLE_HELP_IMAGE.exists():
+ SIMPLE_HELP_IMAGE.unlink()
+ if SIMPLE_DETAIL_HELP_IMAGE.exists():
+ SIMPLE_DETAIL_HELP_IMAGE.unlink()
+ for file in GROUP_HELP_PATH.iterdir():
+ if file.is_file():
+ file.unlink()
+
+
def get_user(uname: str) -> User | None:
"""获取账号密码