Compare commits

..
Author SHA1 Message Date
HibiKier 094a8701f6 🩹 修正踢出bot时参数 2025-04-05 21:50:21 +08:00
106 changed files with 1152 additions and 4621 deletions
+32 -90
View File
@@ -44,7 +44,7 @@
<div align=center> <div align=center>
[文档](https://zhenxun-org.github.io/zhenxun_bot/) [文档](https://hibikier.github.io/zhenxun_bot/)
</div> </div>
@@ -112,7 +112,7 @@ AccessToken: PUBLIC_ZHENXUN_TEST
| [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 | | [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 |
| [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 | | [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 | | [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
| [WebUi](https://github.com/zhenxun-org/zhenxun_bot) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) | | [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
| [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 | | [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
</div> </div>
@@ -121,32 +121,10 @@ AccessToken: PUBLIC_ZHENXUN_TEST
- 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件 - 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件
- 拥有完善可用的 webui - 拥有完善可用的 webui
- 通过 Config 配置项将所有插件配置统一保存至 config.yaml,利于统一用户修改 - 通过 Config 配置项将所有插件配置统计保存至 config.yaml,利于统一用户修改
- 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息` - 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
- 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等 - 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等
- **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!** - **更多详细请通过 [传送门](https://hibikier.github.io/zhenxun_bot/) 查看文档!**
## 🐣 小白整合
如果你系统是 **Windows** 且不想下载 Python
可以使用整合包(Python3.10+zhenxun+webui)
文档地址:[整合包文档](https://hibikier.github.io/zhenxun_bot/beginner/)
<details>
<summary>下载地址</summary>
- **百度云:**
https://pan.baidu.com/s/1ph4yzx1vdNbkxm9VBKDdgQ?pwd=971j
- **天翼云:**
https://cloud.189.cn/web/share?code=jq67r2i2E7Fb
访问码:8wxm
- **Google Drive:**
https://drive.google.com/file/d/1cc3Dqjk0x5hWGLNeMkrFwWl8BvsK6KfD/view?usp=drive_link
</details>
## 🛠️ 简单部署 ## 🛠️ 简单部署
@@ -172,7 +150,7 @@ poetry run python bot.py
1.在 .env.dev 文件中填写你的机器人配置项 1.在 .env.dev 文件中填写你的机器人配置项
2.在 data/config.yaml 文件中修改你需要修改的插件配置项 2.在 configs/config.yaml 文件中修改你需要修改的插件配置项
<details> <details>
<summary>数据库地址(DB_URL)配置说明</summary> <summary>数据库地址(DB_URL)配置说明</summary>
@@ -294,12 +272,12 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
## ❔ 需要帮助? ## ❔ 需要帮助?
> [!TIP] > [!TIP]
> 发起 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md) > 发起 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 前,我们希望你能够阅读过或者了解 [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
> >
> - 善用[搜索引擎](https://www.google.com/) > - 善用[搜索引擎](https://www.google.com/)
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue > - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue
欢迎前往 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系 欢迎前往 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
## 🛠️ 进度追踪 ## 🛠️ 进度追踪
@@ -309,8 +287,6 @@ Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss) 首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
## 🙏 感谢 ## 🙏 感谢
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议 [botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
@@ -350,68 +326,34 @@ LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
<img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/> <img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
</a> </a>
## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端) ## 📸 WebUI 界面展示
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;"> <div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui00.png" alt="webui00" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui01.png" alt="webui01" style="width: 100%; height: auto;">
</div>
#### 登录界面 <div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui02.png" alt="webui02" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui03.png" alt="webui03" style="width: 100%; height: auto;">
</div>
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-login.jpg) <div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui04.png" alt="webui04" style="width: 100%; height: auto;">
#### API 设置 </div>
<div style="width: 48%; margin-bottom: 10px;">
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-api.jpg) <img src="./docs_image/webui05.png" alt="webui05" style="width: 100%; height: auto;">
</div>
#### 仪表盘
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard.jpg)
#### 仪表盘(展开)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard1.jpg)
#### 控制台
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-command.jpg)
#### 插件列表
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin.jpg)
#### 插件列表(配置项)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin1.jpg)
#### 插件商店
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-store.jpg)
#### 好友/群组管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage.jpg)
#### 请求管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage1.jpg)
#### 数据库管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-database.jpg)
### 文件管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system.jpg)
### 文件管理(文本查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system1.jpg)
### 文件管理(图片查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system2.jpg)
### 关于
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-about.jpg)
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui06.png" alt="webui06" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui07.png" alt="webui07" style="width: 100%; height: auto;">
</div>
</div> </div>
+2 -2
View File
@@ -14,9 +14,9 @@ driver.register_adapter(OneBotV11Adapter)
# driver.register_adapter(DoDoAdapter) # driver.register_adapter(DoDoAdapter)
# driver.register_adapter(DiscordAdapter) # driver.register_adapter(DiscordAdapter)
from zhenxun.services.db_context import disconnect from zhenxun.services.db_context import disconnect, init
# driver.on_startup(init) driver.on_startup(init)
driver.on_shutdown(disconnect) driver.on_shutdown(disconnect)
# nonebot.load_builtin_plugins("echo") # nonebot.load_builtin_plugins("echo")
-67
View File
@@ -1,67 +0,0 @@
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:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Generated
+97 -97
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]] [[package]]
name = "aiocache" name = "aiocache"
@@ -258,7 +258,7 @@ description = "Timeout context manager for asyncio programs"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main"]
markers = "python_version == \"3.10\"" markers = "python_version < \"3.11.0\""
files = [ files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@@ -2002,104 +2002,104 @@ reference = "aliyun"
[[package]] [[package]]
name = "multidict" name = "multidict"
version = "6.2.0" version = "6.3.2"
description = "multidict implementation" description = "multidict implementation"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "dev"] groups = ["main", "dev"]
files = [ files = [
{file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"}, {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3dc0eec9304fa04d84a51ea13b0ec170bace5b7ddeaac748149efd316f1504"},
{file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"}, {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9534f3d84addd3b6018fa83f97c9d4247aaa94ac917d1ed7b2523306f99f5c16"},
{file = "multidict-6.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e"}, {file = "multidict-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a003ce1413ae01f0b8789c1c987991346a94620a4d22210f7a8fe753646d3209"},
{file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b43f7384e68b1b982c99f489921a459467b5584bdb963b25e0df57c9039d0ad"},
{file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d142ae84047262dc75c1f92eaf95b20680f85ce11d35571b4c97e267f96fadc4"},
{file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec7e86fbc48aa1d6d686501a8547818ba8d645e7e40eaa98232a5d43ee4380ad"},
{file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3"}, {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe019fb437632b016e6cac67a7e964f1ef827ef4023f1ca0227b54be354da97e"},
{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.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b60cb81214a9da7cfd8ae2853d5e6e47225ece55fe5833142fe0af321c35299"},
{file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32d9e8ef2e0312d4e96ca9adc88e0675b6d8e144349efce4a7c95d5ccb6d88e0"},
{file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:335d584312e3fa43633d63175dfc1a5f137dd7aa03d38d1310237d54c3032774"},
{file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8df917faa6b8cac3d6870fc21cb7e4d169faca68e43ffe568c156c9c6408a4d"},
{file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc060b9b89b701dd8fedef5b99e1f1002b8cb95072693233a63389d37e48212d"},
{file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a"}, {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2ce3be2500658f3c644494b934628bb0c82e549dde250d2119689ce791cc8b8"},
{file = "multidict-6.2.0-cp310-cp310-win32.whl", hash = "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460"}, {file = "multidict-6.3.2-cp310-cp310-win32.whl", hash = "sha256:dbcb4490d8e74b484449abd51751b8f560dd0a4812eb5dacc6a588498222a9ab"},
{file = "multidict-6.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1"}, {file = "multidict-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:06944f9ced30f8602be873563ed4df7e3f40958f60b2db39732c11d615a33687"},
{file = "multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46"}, {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45a034f41fcd16968c0470d8912d293d7b0d0822fc25739c5c2ff7835b85bc56"},
{file = "multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932"}, {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:352585cec45f5d83d886fc522955492bb436fca032b11d487b12d31c5a81b9e3"},
{file = "multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf"}, {file = "multidict-6.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da9d89d293511fd0a83a90559dc131f8b3292b6975eb80feff19e5f4663647e2"},
{file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fa716592224aa652b9347a586cfe018635229074565663894eb4eb21f8307f"},
{file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0326278a44c56e94792475268e5cd3d47fbc0bd41ee56928c3bbb103ba7f58fe"},
{file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb1ea87f7fe45e5079f6315e95d64d4ca8b43ef656d98bed63a02e3756853a22"},
{file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081"}, {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cff3c5a98d037024a9065aafc621a8599fad7b423393685dc83cf7a32f8b691"},
{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.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed99834b053c655d980fb98029003cb24281e47a796052faad4543aa9e01b8e8"},
{file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7048440e505d2b4741e5d0b32bd2f427c901f38c7760fc245918be2cf69b3b85"},
{file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27248c27b563f5889556da8a96e18e98a56ff807ac1a7d56cf4453c2c9e4cd91"},
{file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6323b4ba0e018bd266f776c35f3f0943fc4ee77e481593c9f93bd49888f24e94"},
{file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:81f7ce5ec7c27d0b45c10449c8f0fed192b93251e2e98cb0b21fec779ef1dc4d"},
{file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2"}, {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03bfcf2825b3bed0ba08a9d854acd18b938cab0d2dba3372b51c78e496bac811"},
{file = "multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d"}, {file = "multidict-6.3.2-cp311-cp311-win32.whl", hash = "sha256:f32c2790512cae6ca886920e58cdc8c784bdc4bb2a5ec74127c71980369d18dc"},
{file = "multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86"}, {file = "multidict-6.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b0c15e58e038a2cd75ef7cf7e072bc39b5e0488b165902efb27978984bbad70"},
{file = "multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b"}, {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d1e0ba1ce1b8cc79117196642d95f4365e118eaf5fb85f57cdbcc5a25640b2a4"},
{file = "multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4"}, {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:029bbd7d782251a78975214b78ee632672310f9233d49531fc93e8e99154af25"},
{file = "multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44"}, {file = "multidict-6.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7db41e3b56817d9175264e5fe00192fbcb8e1265307a59f53dede86161b150e"},
{file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcab18e65cc555ac29981a581518c23311f2b1e72d8f658f9891590465383be"},
{file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d50eff89aa4d145a5486b171a2177042d08ea5105f813027eb1050abe91839f"},
{file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643e57b403d3e240045a3681f9e6a04d35a33eddc501b4cbbbdbc9c70122e7bc"},
{file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"}, {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d17b37b9715b30605b5bab1460569742d0c309e5c20079263b440f5d7746e7e"},
{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.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68acd51fa94e63312b8ddf84bfc9c3d3442fe1f9988bbe1b6c703043af8867fe"},
{file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:347eea2852ab7f697cc5ed9b1aae96b08f8529cca0c6468f747f0781b1842898"},
{file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4d3f8e57027dcda84a1aa181501c15c45eab9566eb6fcc274cbd1e7561224f8"},
{file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca57a841ffcf712e47875d026aa49d6e67f9560624d54b51628603700d5d287"},
{file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7cafdafb44c4e646118410368307693e49d19167e5f119cbe3a88697d2d1a636"},
{file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b"}, {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:430120c6ce3715a9c6075cabcee557daccbcca8ba25a9fedf05c7bf564532f2d"},
{file = "multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626"}, {file = "multidict-6.3.2-cp312-cp312-win32.whl", hash = "sha256:13bec31375235a68457ab887ce1bbf4f59d5810d838ae5d7e5b416242e1f3ed4"},
{file = "multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c"}, {file = "multidict-6.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3b6d7620e6e90c6d97eaf3a63bf7fbd2ba253aab89120a4a9c660bf2d675391"},
{file = "multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80"}, {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b9ca24700322816ae0d426aa33671cf68242f8cc85cee0d0e936465ddaee90b5"},
{file = "multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16"}, {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9fbbe23667d596ff4f9f74d44b06e40ebb0ab6b262cf14a284f859a66f86457"},
{file = "multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e"}, {file = "multidict-6.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cb602c5bea0589570ad3a4a6f2649c4f13cc7a1e97b4c616e5e9ff8dc490987"},
{file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93ca81dd4d1542e20000ed90f4cc84b7713776f620d04c2b75b8efbe61106c99"},
{file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18b6310b5454c62242577a128c87df8897f39dd913311cf2e1298e47dfc089eb"},
{file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a6dda57de1fc9aedfdb600a8640c99385cdab59a5716cb714b52b6005797f77"},
{file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844"}, {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8ec42d03cc6b29845552a68151f9e623c541f1708328353220af571e24a247"},
{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.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80681969cee2fa84dafeb53615d51d24246849984e3e87fbe4fe39956f2e23bf"},
{file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:01489b0c3592bb9d238e5690e9566db7f77a5380f054b57077d2c4deeaade0eb"},
{file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:522d9f1fd995d04dfedc0a40bca7e2591bc577d920079df50b56245a4a252c1c"},
{file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2014e9cf0b4e9c75bbad49c1758e5a9bf967a56184fc5fcc51527425baf5abba"},
{file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:78ced9fcbee79e446ff4bb3018ac7ba1670703de7873d9c1f6f9883db53c71bc"},
{file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d"}, {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1faf01af972bd01216a107c195f5294f9f393531bc3e4faddc9b333581255d4d"},
{file = "multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e"}, {file = "multidict-6.3.2-cp313-cp313-win32.whl", hash = "sha256:7a699ab13d8d8e1f885de1535b4f477fb93836c87168318244c2685da7b7f655"},
{file = "multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2"}, {file = "multidict-6.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:8666bb0d883310c83be01676e302587834dfd185b52758caeab32ef0eb387bc6"},
{file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d82c95aabee29612b1c4f48b98be98181686eb7d6c0152301f72715705cc787b"},
{file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f47709173ea9e87a7fd05cd7e5cf1e5d4158924ff988a9a8e0fbd853705f0e68"},
{file = "multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e"}, {file = "multidict-6.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c7f9d0276ceaab41b8ae78534ff28ea33d5de85db551cbf80c44371f2b55d13"},
{file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6eab22df44a25acab2e738f882f5ec551282ab45b2bbda5301e6d2cfb323036"},
{file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a947cb7c657f57874021b9b70c7aac049c877fb576955a40afa8df71d01a1390"},
{file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5faa346e8e1c371187cf345ab1e02a75889f9f510c9cbc575c31b779f7df084d"},
{file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af"}, {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6e08d977aebf1718540533b4ba5b351ccec2db093370958a653b1f7f9219cc"},
{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.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98eab7acf55275b5bf09834125fa3a80b143a9f241cdcdd3f1295ffdc3c6d097"},
{file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:36863655630becc224375c0b99364978a0f95aebfb27fb6dd500f7fb5fb36e79"},
{file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9c0979c096c0d46a963331b0e400d3a9e560e41219df4b35f0d7a2f28f39710"},
{file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0efc04f70f05e70e5945890767e8874da5953a196f5b07c552d305afae0f3bf6"},
{file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2c519b3b82c34539fae3e22e4ea965869ac6b628794b1eb487780dde37637ab7"},
{file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547"}, {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:329160e301f2afd7b43725d3dda8a7ef8ee41d4ceac2083fc0d8c1cc8a4bd56b"},
{file = "multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc"}, {file = "multidict-6.3.2-cp313-cp313t-win32.whl", hash = "sha256:420e5144a5f598dad8db3128f1695cd42a38a0026c2991091dab91697832f8cc"},
{file = "multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44"}, {file = "multidict-6.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:875faded2861c7af2682c67088e6313fec35ede811e071c96d36b081873cea14"},
{file = "multidict-6.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a"}, {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2516c5eb5732d6c4e29fa93323bfdc55186895124bc569e2404e3820934be378"},
{file = "multidict-6.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac"}, {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:be5c8622e665cc5491c13c0fcd52915cdbae991a3514251d71129691338cdfb2"},
{file = "multidict-6.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88"}, {file = "multidict-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ef33150eea7953cfdb571d862cff894e0ad97ab80d97731eb4b9328fc32d52b"},
{file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b357738ce46e998f1b1bad9c4b79b2a9755915f71b87a8c01ce123a22a4f99"},
{file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c60e059fcd3655a653ba99fec2556cd0260ec57f9cb138d3e6ffc413638a2e"},
{file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:629e7c5e75bde83e54a22c7043ce89d68691d1f103be6d09a1c82b870df3b4b8"},
{file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f"}, {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6c8fc97d893fdf1fff15a619fee8de2f31c9b289ef7594730e35074fa0cefb"},
{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.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52081d2f27e0652265d4637b03f09b82f6da5ce5e1474f07dc64674ff8bfc04c"},
{file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:64529dc395b5fd0a7826ffa70d2d9a7f4abd8f5333d6aaaba67fdf7bedde9f21"},
{file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2b7c3fad827770840f5399348c89635ed6d6e9bba363baad7d3c7f86a9cf1da3"},
{file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:24aa42b1651c654ae9e5273e06c3b7ccffe9f7cc76fbde40c37e9ae65f170818"},
{file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:04ceea01e9991357164b12882e120ce6b4d63a0424bb9f9cd37910aa56d30830"},
{file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2"}, {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:943897a41160945416617db567d867ab34e9258adaffc56a25a4c3f99d919598"},
{file = "multidict-6.2.0-cp39-cp39-win32.whl", hash = "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d"}, {file = "multidict-6.3.2-cp39-cp39-win32.whl", hash = "sha256:76157a9a0c5380aadd3b5ff7b8deee355ff5adecc66c837b444fa633b4d409a2"},
{file = "multidict-6.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3"}, {file = "multidict-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:d091d123e44035cd5664554308477aff0b58db37e701e7598a67e907b98d1925"},
{file = "multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530"}, {file = "multidict-6.3.2-py3-none-any.whl", hash = "sha256:71409d4579f716217f23be2f5e7afca5ca926aaeb398aa11b72d793bff637a1f"},
{file = "multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8"}, {file = "multidict-6.3.2.tar.gz", hash = "sha256:c1035eea471f759fa853dd6e76aaa1e389f93b3e1403093fa0fd3ab4db490678"},
] ]
[package.dependencies] [package.dependencies]
@@ -4367,7 +4367,7 @@ files = [
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
] ]
markers = {main = "python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\""} markers = {main = "python_version < \"3.11\"", dev = "python_full_version <= \"3.11.0a6\""}
[package.source] [package.source]
type = "legacy" type = "legacy"
@@ -5104,4 +5104,4 @@ reference = "aliyun"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "48aa6fabc582a0c75b333f9bd3418264a1fd15a5c8c50220b456ba00d03cd35e" content-hash = "f1d23ae2feff2f5c96a97f1a7e52e1738858817b13281b9253a41851cacf0740"
-12
View File
@@ -1,12 +0,0 @@
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' ]
-1
View File
@@ -45,7 +45,6 @@ nonebot-plugin-alconna = "^0.54.0"
tenacity = "^9.0.0" tenacity = "^9.0.0"
nonebot-plugin-uninfo = ">0.4.1" nonebot-plugin-uninfo = ">0.4.1"
nonebot-plugin-waiter = "^0.8.1" nonebot-plugin-waiter = "^0.8.1"
multidict = ">=6.0.0,!=6.3.2"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
nonebug = "^0.4" nonebug = "^0.4"
+1 -2
View File
@@ -16,7 +16,6 @@ from zhenxun.models.sign_user import SignUser
from zhenxun.models.user_console import UserConsole from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.decorator.shop import shop_register from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.manager.resource_manager import ResourceManager from zhenxun.utils.manager.resource_manager import ResourceManager
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
@@ -71,7 +70,7 @@ from public.bag_users t1
""" """
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
await ResourceManager.init_resources() await ResourceManager.init_resources()
"""签到与用户的数据迁移""" """签到与用户的数据迁移"""
+19 -20
View File
@@ -26,21 +26,6 @@ __plugin_meta__ = PluginMetadata(
_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me()) _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() @_matcher.handle()
async def _(session: Uninfo, arparma: Arparma): async def _(session: Uninfo, arparma: Arparma):
ver_file = Path() / "__version__" ver_file = Path() / "__version__"
@@ -50,11 +35,25 @@ async def _(session: Uninfo, arparma: Arparma):
if text := await f.read(): if text := await f.read():
version = text.split(":")[-1].strip() version = text.split(":")[-1].strip()
if PlatformUtils.is_qbot(session): if PlatformUtils.is_qbot(session):
result: list[str | Path] = [QQ_INFO.format(version=version)] info: list[str | Path] = [
f"""
『绪山真寻Bot』
版本:{version}
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
""".strip()
]
path = DATA_PATH / "about.png" path = DATA_PATH / "about.png"
if path.exists(): if path.exists():
result.append(path) info.append(path)
await MessageUtils.build_message(result).send() # type: ignore
else: else:
await MessageUtils.build_message(INFO.format(version=version)).send() info = [
logger.info("查看关于", arparma.header_result, session=session) 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)
+2 -24
View File
@@ -14,19 +14,13 @@ from nonebot_plugin_alconna import (
from nonebot_plugin_session import EventSession from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig, Config from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.utils import ( from zhenxun.configs.utils import PluginExtraData, RegisterConfig
AICallableParam,
AICallableProperties,
AICallableTag,
PluginExtraData,
RegisterConfig,
)
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import admin_check from zhenxun.utils.rules import admin_check
from ._data_source import BanManage, call_ban from ._data_source import BanManage
base_config = Config.get("ban") base_config = Config.get("ban")
@@ -84,22 +78,6 @@ __plugin_meta__ = PluginMetadata(
type=int, type=int,
) )
], ],
smart_tools=[
AICallableTag(
name="call_ban",
description="某人多次(至少三次)辱骂你,调用此方法进行封禁",
parameters=AICallableParam(
type="object",
properties={
"user_id": AICallableProperties(
type="string", description="用户的id"
),
},
required=["user_id"],
),
func=call_ban,
)
],
).to_dict(), ).to_dict(),
) )
@@ -5,20 +5,9 @@ from nonebot_plugin_session import EventSession
from zhenxun.models.ban_console import BanConsole from zhenxun.models.ban_console import BanConsole
from zhenxun.models.level_user import LevelUser from zhenxun.models.level_user import LevelUser
from zhenxun.services.log import logger
from zhenxun.utils.image_utils import BuildImage, ImageTemplate from zhenxun.utils.image_utils import BuildImage, ImageTemplate
async def call_ban(user_id: str):
"""调用ban
参数:
user_id: 用户id
"""
await BanConsole.ban(user_id, None, 9, 60 * 12)
logger.info("辱骂次数过多,已将用户加入黑名单...", "ban", session=user_id)
class BanManage: class BanManage:
@classmethod @classmethod
async def build_ban_image( async def build_ban_image(
@@ -14,7 +14,6 @@ from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage from zhenxun.utils._build_image import BuildImage
from zhenxun.utils._image_template import ImageTemplate from zhenxun.utils._image_template import ImageTemplate
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
BASE_PATH = DATA_PATH / "welcome_message" BASE_PATH = DATA_PATH / "welcome_message"
@@ -92,7 +91,7 @@ def migrate(path: Path):
json.dump(new_data, f, ensure_ascii=False, indent=4) json.dump(new_data, f, ensure_ascii=False, indent=4)
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
def _(): def _():
"""数据迁移 """数据迁移
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import ( from nonebot_plugin_alconna import (
@@ -15,38 +14,35 @@ from nonebot_plugin_alconna import (
from nonebot_plugin_session import EventSession from nonebot_plugin_session import EventSession
import pytz import pytz
from zhenxun.configs.config import Config from zhenxun.configs.utils import Command, PluginExtraData
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from zhenxun.models.chat_history import ChatHistory from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.group_member_info import GroupInfoUser
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.image_utils import BuildImage, ImageTemplate from zhenxun.utils.image_utils import ImageTemplate
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="消息统计", name="消息统计",
description="消息统计查询", description="消息统计查询",
usage=""" usage="""
格式: 格式:
消息排行 ?[type [日,周,月,季,年]] ?[--des] 消息排行 ?[type [日,周,月,年]] ?[--des]
快捷: 快捷:
[日,周,月,季,年]消息排行 ?[数量] [日,周,月,年]消息排行 ?[数量]
示例: 示例:
消息排行 : 所有记录排行 消息排行 : 所有记录排行
日消息排行 : 今日记录排行 日消息排行 : 今日记录排行
周消息排行 : 本周记录排行 周消息排行 : 今日记录排行
月消息排行 : 本月记录排行 月消息排行 : 今日记录排行
季消息排行 : 本季度记录排行 年消息排行 : 今日记录排行
年消息排行 : 本年记录排行
消息排行 周 --des : 逆序周记录排行 消息排行 周 --des : 逆序周记录排行
""".strip(), """.strip(),
extra=PluginExtraData( extra=PluginExtraData(
author="HibiKier", author="HibiKier",
version="0.2", version="0.1",
plugin_type=PluginType.NORMAL, plugin_type=PluginType.NORMAL,
menu_type="数据统计", menu_type="数据统计",
commands=[ commands=[
@@ -54,19 +50,8 @@ __plugin_meta__ = PluginMetadata(
Command(command="日消息统计"), Command(command="日消息统计"),
Command(command="周消息排行"), Command(command="周消息排行"),
Command(command="月消息排行"), Command(command="月消息排行"),
Command(command="季消息排行"),
Command(command="年消息排行"), Command(command="年消息排行"),
], ],
configs=[
RegisterConfig(
module="chat_history",
key="SHOW_QUIT_MEMBER",
value=True,
help="是否在消息排行中显示已退群用户",
default_value=True,
type=bool,
)
],
).to_dict(), ).to_dict(),
) )
@@ -75,7 +60,7 @@ _matcher = on_alconna(
Alconna( Alconna(
"消息排行", "消息排行",
Option("--des", action=store_true, help_text="逆序"), Option("--des", action=store_true, help_text="逆序"),
Args["type?", ["日", "周", "月", "季", "年"]]["count?", int, 10], Args["type?", ["日", "周", "月", "年"]]["count?", int, 10],
), ),
aliases={"消息统计"}, aliases={"消息统计"},
priority=5, priority=5,
@@ -83,7 +68,7 @@ _matcher = on_alconna(
) )
_matcher.shortcut( _matcher.shortcut(
r"(?P<type>['日', '周', '月', '季', '年'])?消息(排行|统计)\s?(?P<cnt>\d+)?", r"(?P<type>['日', '周', '月', '年'])?消息(排行|统计)\s?(?P<cnt>\d+)?",
command="消息排行", command="消息排行",
arguments=["{type}", "{cnt}"], arguments=["{type}", "{cnt}"],
prefix=True, prefix=True,
@@ -111,57 +96,20 @@ async def _(
date_scope = (time_now - timedelta(days=7), time_now) date_scope = (time_now - timedelta(days=7), time_now)
elif date in ["月"]: elif date in ["月"]:
date_scope = (time_now - timedelta(days=30), time_now) date_scope = (time_now - timedelta(days=30), time_now)
elif date in ["季"]: column_name = ["名次", "昵称", "发言次数"]
date_scope = (time_now - timedelta(days=90), time_now)
column_name = ["名次", "头像", "昵称", "发言次数"]
show_quit_member = Config.get_config("chat_history", "SHOW_QUIT_MEMBER", True)
fetch_count = count.result
if not show_quit_member:
fetch_count = count.result * 2
if rank_data := await ChatHistory.get_group_msg_rank( if rank_data := await ChatHistory.get_group_msg_rank(
group_id, fetch_count, "DES" if arparma.find("des") else "DESC", date_scope group_id, count.result, "DES" if arparma.find("des") else "DESC", date_scope
): ):
idx = 1 idx = 1
data_list = [] data_list = []
for uid, num in rank_data: for uid, num in rank_data:
if len(data_list) >= count.result: if user := await GroupInfoUser.filter(
break
user_in_group = await GroupInfoUser.filter(
user_id=uid, group_id=group_id user_id=uid, group_id=group_id
).first() ).first():
user_name = user.user_name
if not user_in_group and not show_quit_member:
continue
if user_in_group:
user_name = user_in_group.user_name
else: else:
user_name = f"{uid}(已退群)" user_name = uid
data_list.append([idx, user_name, num])
avatar_size = 40
try:
avatar_bytes = await PlatformUtils.get_user_avatar(str(uid), "qq")
if avatar_bytes:
avatar_img = BuildImage(
avatar_size, avatar_size, background=BytesIO(avatar_bytes)
)
await avatar_img.circle()
avatar_tuple = (avatar_img, avatar_size, avatar_size)
else:
avatar_img = BuildImage(avatar_size, avatar_size, color="#CCCCCC")
await avatar_img.circle()
avatar_tuple = (avatar_img, avatar_size, avatar_size)
except Exception as e:
logger.warning(f"获取用户头像失败: {e}", "chat_history")
avatar_img = BuildImage(avatar_size, avatar_size, color="#CCCCCC")
await avatar_img.circle()
avatar_tuple = (avatar_img, avatar_size, avatar_size)
data_list.append([idx, avatar_tuple, user_name, num])
idx += 1 idx += 1
if not date_scope: if not date_scope:
if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id): if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id):
@@ -184,3 +132,13 @@ async def _(
) )
await MessageUtils.build_message(A).finish(reply_to=True) await MessageUtils.build_message(A).finish(reply_to=True)
await MessageUtils.build_message("群组消息记录为空...").finish() await MessageUtils.build_message("群组消息记录为空...").finish()
# # @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))
+2 -2
View File
@@ -37,8 +37,8 @@ __plugin_meta__ = PluginMetadata(
configs=[ configs=[
RegisterConfig( RegisterConfig(
key="type", key="type",
value="zhenxun", value="normal",
help="帮助图片样式 [normal, HTML, zhenxun]", help="帮助图片样式 ['normal', 'HTML', 'zhenxun']",
default_value="zhenxun", default_value="zhenxun",
) )
], ],
+1 -3
View File
@@ -40,9 +40,7 @@ async def create_help_img(
match help_type: match help_type:
case "html": case "html":
result = BuildImage.open( result = BuildImage.open(await build_html_image(group_id, is_detail))
await build_html_image(session, group_id, is_detail)
)
case "zhenxun": case "zhenxun":
result = BuildImage.open( result = BuildImage.open(
await build_zhenxun_image(session, group_id, is_detail) await build_zhenxun_image(session, group_id, is_detail)
+2 -8
View File
@@ -1,8 +1,5 @@
from collections.abc import Callable 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.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
@@ -30,15 +27,13 @@ async def sort_type() -> dict[str, list[PluginInfo]]:
async def classify_plugin( async def classify_plugin(
session: Uninfo, group_id: str | None, is_detail: bool, handle: Callable group_id: str | None, is_detail: bool, handle: Callable
) -> dict[str, list]: ) -> dict[str, list]:
"""对插件进行分类并判断状态 """对插件进行分类并判断状态
参数: 参数:
session: Uninfo对象
group_id: 群组id group_id: 群组id
is_detail: 是否详细帮助 is_detail: 是否详细帮助
handle: 回调方法
返回: 返回:
dict[str, list[Item]]: 分类插件数据 dict[str, list[Item]]: 分类插件数据
@@ -46,10 +41,9 @@ async def classify_plugin(
sort_data = await sort_type() sort_data = await sort_type()
classify: dict[str, list] = {} classify: dict[str, list] = {}
group = await GroupConsole.get_or_none(group_id=group_id) if group_id else None 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 menu, value in sort_data.items():
for plugin in value: for plugin in value:
if not classify.get(menu): if not classify.get(menu):
classify[menu] = [] classify[menu] = []
classify[menu].append(handle(bot, plugin, group, is_detail)) classify[menu].append(handle(plugin, group, is_detail))
return classify return classify
+4 -13
View File
@@ -2,11 +2,9 @@ import os
import random import random
from nonebot_plugin_htmlrender import template_to_pic from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel from pydantic import BaseModel
from zhenxun.configs.path_config import TEMPLATE_PATH 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.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType from zhenxun.utils.enum import BlockType
@@ -50,12 +48,11 @@ ICON2STR = {
def __handle_item( def __handle_item(
bot: BotConsole, plugin: PluginInfo, group: GroupConsole | None, is_detail: bool plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
) -> Item: ) -> Item:
"""构造Item """构造Item
参数: 参数:
bot: BotConsole
plugin: PluginInfo plugin: PluginInfo
group: 群组 group: 群组
is_detail: 是否详细 is_detail: 是否详细
@@ -76,13 +73,10 @@ def __handle_item(
]: ]:
sta = 2 sta = 2
if group: if group:
if f"{plugin.module}," in group.superuser_block_plugin: if f"{plugin.module}:super," in group.block_plugin:
sta = 2 sta = 2
if f"{plugin.module}," in group.block_plugin: if f"{plugin.module}," in group.block_plugin:
sta = 1 sta = 1
if bot:
if f"{plugin.module}," in bot.block_plugins:
sta = 2
return Item(plugin_name=plugin.name, sta=sta) return Item(plugin_name=plugin.name, sta=sta)
@@ -125,17 +119,14 @@ def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
return plugin_list return plugin_list
async def build_html_image( async def build_html_image(group_id: str | None, is_detail: bool) -> bytes:
session: Uninfo, group_id: str | None, is_detail: bool
) -> bytes:
"""构造HTML帮助图片 """构造HTML帮助图片
参数: 参数:
session: Uninfo
group_id: 群号 group_id: 群号
is_detail: 是否详细帮助 is_detail: 是否详细帮助
""" """
classify = await classify_plugin(session, group_id, is_detail, __handle_item) classify = await classify_plugin(group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify) plugin_list = build_plugin_data(classify)
return await template_to_pic( return await template_to_pic(
template_path=str((TEMPLATE_PATH / "menu").absolute()), template_path=str((TEMPLATE_PATH / "menu").absolute()),
+2 -12
View File
@@ -6,7 +6,6 @@ from pydantic import BaseModel
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import TEMPLATE_PATH from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.configs.utils import PluginExtraData from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.bot_console import BotConsole
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.utils.enum import BlockType from zhenxun.utils.enum import BlockType
@@ -22,19 +21,12 @@ class Item(BaseModel):
"""插件命令""" """插件命令"""
def __handle_item( def __handle_item(plugin: PluginInfo, group: GroupConsole | None, is_detail: bool):
bot: BotConsole | None,
plugin: PluginInfo,
group: GroupConsole | None,
is_detail: bool,
):
"""构造Item """构造Item
参数: 参数:
bot: BotConsole
plugin: PluginInfo plugin: PluginInfo
group: 群组 group: 群组
is_detail: 是否为详细
返回: 返回:
Item: Item Item: Item
@@ -48,8 +40,6 @@ def __handle_item(
plugin.name = f"{plugin.name}(不可用)" plugin.name = f"{plugin.name}(不可用)"
elif group and f"{plugin.module}," in group.block_plugin: elif group and f"{plugin.module}," in group.block_plugin:
plugin.name = f"{plugin.name}(不可用)" plugin.name = f"{plugin.name}(不可用)"
elif bot and f"{plugin.module}," in bot.block_plugins:
plugin.name = f"{plugin.name}(不可用)"
commands = [] commands = []
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path) 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: if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
@@ -152,7 +142,7 @@ async def build_zhenxun_image(
group_id: 群号 group_id: 群号
is_detail: 是否详细帮助 is_detail: 是否详细帮助
""" """
classify = await classify_plugin(session, group_id, is_detail, __handle_item) classify = await classify_plugin(group_id, is_detail, __handle_item)
plugin_list = build_plugin_data(classify) plugin_list = build_plugin_data(classify)
platform = PlatformUtils.get_platform(session) platform = PlatformUtils.get_platform(session)
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
+1 -1
View File
@@ -21,7 +21,7 @@ from zhenxun.utils.message import MessageUtils
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="笨蛋检测", name="笨蛋检测",
description="功能名称当命令检测", description="功能名称当命令检测",
usage="""当一些笨蛋直接输入功能名称时,提示笨蛋使用帮助指令查看功能帮助""".strip(), usage="""被动""".strip(),
extra=PluginExtraData( extra=PluginExtraData(
author="HibiKier", author="HibiKier",
version="0.1", version="0.1",
-10
View File
@@ -49,14 +49,4 @@ Config.add_plugin_config(
type=bool, type=bool,
) )
Config.add_plugin_config(
"hook",
"RECORD_BOT_SENT_MESSAGES",
True,
help="记录bot消息发送",
default_value=True,
type=bool,
)
nonebot.load_plugins(str(Path(__file__).parent.resolve())) nonebot.load_plugins(str(Path(__file__).parent.resolve()))
+11 -73
View File
@@ -1,85 +1,23 @@
from typing import Any from typing import Any
from nonebot.adapters import Bot, Message from nonebot.adapters import Bot
from zhenxun.configs.config import Config
from zhenxun.models.bot_message_store import BotMessageStore
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import BotSentType
from zhenxun.utils.manager.message_manager import MessageManager from zhenxun.utils.manager.message_manager import MessageManager
from zhenxun.utils.platform import PlatformUtils
def replace_message(message: Message) -> str:
"""将消息中的at、image、record、face替换为字符串
参数:
message: Message
返回:
str: 文本消息
"""
result = ""
for msg in message:
if isinstance(msg, str):
result += msg
elif msg.type == "at":
result += f"@{msg.data['qq']}"
elif msg.type == "image":
result += "[image]"
elif msg.type == "record":
result += "[record]"
elif msg.type == "face":
result += f"[face:{msg.data['id']}]"
elif msg.type == "reply":
result += ""
else:
result += str(msg)
return result
@Bot.on_called_api @Bot.on_called_api
async def handle_api_result( async def handle_api_result(
bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any
): ):
if exception or api != "send_msg": if not exception and api == "send_msg":
return try:
user_id = data.get("user_id") if (uid := data.get("user_id")) and (msg_id := result.get("message_id")):
group_id = data.get("group_id") MessageManager.add(str(uid), str(msg_id))
message_id = result.get("message_id") logger.debug(
message: Message = data.get("message", "") f"收集消息id,user_id: {uid}, msg_id: {msg_id}", "msg_hook"
message_type = data.get("message_type") )
try: except Exception as e:
# 记录消息id logger.warning(
if user_id and message_id: f"收集消息id发生错误...data: {data}, result: {result}", "msg_hook", e=e
MessageManager.add(str(user_id), str(message_id))
logger.debug(
f"收集消息id,user_id: {user_id}, msg_id: {message_id}", "msg_hook"
) )
except Exception as e:
logger.warning(
f"收集消息id发生错误...data: {data}, result: {result}", "msg_hook", e=e
)
if not Config.get_config("hook", "RECORD_BOT_SENT_MESSAGES"):
return
try:
await BotMessageStore.create(
bot_id=bot.self_id,
user_id=user_id,
group_id=group_id,
sent_type=BotSentType.GROUP
if message_type == "group"
else BotSentType.PRIVATE,
text=replace_message(message),
plain_text=message.extract_plain_text()
if isinstance(message, Message)
else replace_message(message),
platform=PlatformUtils.get_platform(bot),
)
logger.debug(f"消息发送记录,message: {message}")
except Exception as e:
logger.warning(
f"消息发送记录发生错误...data: {data}, result: {result}",
"msg_hook",
e=e,
)
+1 -3
View File
@@ -11,7 +11,6 @@ from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH from zhenxun.configs.path_config import DATA_PATH
from zhenxun.configs.utils import RegisterConfig from zhenxun.configs.utils import RegisterConfig
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
_yaml = YAML(pure=True) _yaml = YAML(pure=True)
_yaml.allow_unicode = True _yaml.allow_unicode = True
@@ -103,7 +102,7 @@ def _generate_simple_config(exists_module: list[str]):
temp_file.unlink() temp_file.unlink()
@PriorityLifecycle.on_startup(priority=0) @driver.on_startup
def _(): def _():
""" """
初始化插件数据配置 初始化插件数据配置
@@ -126,4 +125,3 @@ def _():
with plugins2config_file.open("w", encoding="utf8") as wf: with plugins2config_file.open("w", encoding="utf8") as wf:
_yaml.dump(_data, wf) _yaml.dump(_data, wf)
_generate_simple_config(exists_module) _generate_simple_config(exists_module)
Config.reload()
+1 -2
View File
@@ -20,7 +20,6 @@ from zhenxun.utils.enum import (
PluginLimitType, PluginLimitType,
PluginType, PluginType,
) )
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .manager import manager from .manager import manager
@@ -96,7 +95,7 @@ async def _handle_setting(
) )
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
""" """
初始化插件数据配置 初始化插件数据配置
+1 -2
View File
@@ -10,7 +10,6 @@ from zhenxun.models.group_console import GroupConsole
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.common_utils import CommonUtils from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@@ -133,7 +132,7 @@ async def create_schedule(task: Task):
logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e) logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e)
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
""" """
初始化插件数据配置 初始化插件数据配置
@@ -1,252 +0,0 @@
from datetime import datetime
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Subcommand, on_alconna
from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from nonebot_plugin_waiter import prompt_until
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.depends import UserName
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.utils import is_number
from .data_source import BankManager
__plugin_meta__ = PluginMetadata(
name="小真寻银行",
description="""
小真寻银行,提供高品质的存款!当好感度等级达到指初识时,小真寻会偷偷的帮助你哦。
存款额度与好感度有关,每日存款次数有限制。
基础存款提供基础利息
每日存款提供高额利息
""".strip(),
usage="""
指令:
存款 [金额]
取款 [金额]
银行信息
我的银行信息
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
menu_type="群内小游戏",
configs=[
RegisterConfig(
key="sign_max_deposit",
value=100,
help="好感度换算存款金额比例,当值是100时,最大存款金额=好感度*100,存款的最低金额是100(强制)",
default_value=100,
type=int,
),
RegisterConfig(
key="max_daily_deposit_count",
value=3,
help="每日最大存款次数",
default_value=3,
type=int,
),
RegisterConfig(
key="rate_range",
value=[0.0005, 0.001],
help="小时利率范围",
default_value=[0.0005, 0.001],
type=list[float],
),
RegisterConfig(
key="impression_event",
value=25,
help="到达指定好感度时随机提高或降低利率",
default_value=25,
type=int,
),
RegisterConfig(
key="impression_event_range",
value=[0.00001, 0.0003],
help="到达指定好感度时随机提高或降低利率",
default_value=[0.00001, 0.0003],
type=list[float],
),
RegisterConfig(
key="impression_event_prop",
value=0.3,
help="到达指定好感度时随机提高或降低利率触发概率",
default_value=0.3,
type=float,
),
],
).to_dict(),
)
_matcher = on_alconna(
Alconna(
"mahiro-bank",
Subcommand("deposit", Args["amount?", int]),
Subcommand("withdraw", Args["amount?", int]),
Subcommand("user-info"),
Subcommand("bank-info"),
# Subcommand("loan", Args["amount?", int]),
# Subcommand("repayment", Args["amount?", int]),
),
priority=5,
block=True,
)
_matcher.shortcut(
r"存款\s*(?P<amount>\d+)?",
command="mahiro-bank",
arguments=["deposit", "{amount}"],
prefix=True,
)
_matcher.shortcut(
r"取款\s*(?P<withdraw>\d+)?",
command="mahiro-bank",
arguments=["withdraw", "{withdraw}"],
prefix=True,
)
_matcher.shortcut(
r"我的银行信息",
command="mahiro-bank",
arguments=["user-info"],
prefix=True,
)
_matcher.shortcut(
r"银行信息",
command="mahiro-bank",
arguments=["bank-info"],
prefix=True,
)
async def get_amount(handle_type: str) -> int:
amount_num = await prompt_until(
f"请输入{handle_type}金币数量",
lambda msg: is_number(msg.extract_plain_text()),
timeout=60,
retry=3,
retry_prompt="输入错误,请输入数字。剩余次数:{count}",
)
if not amount_num:
await MessageUtils.build_message(
"输入超时了哦,小真寻柜员以取消本次存款操作..."
).finish()
return int(amount_num.extract_plain_text())
@_matcher.assign("deposit")
async def _(session: Uninfo, arparma: Arparma, amount: Match[int]):
amount_num = amount.result if amount.available else await get_amount("存款")
if result := await BankManager.deposit_check(session.user.id, amount_num):
await MessageUtils.build_message(result).finish(reply_to=True)
_, rate, event_rate = await BankManager.deposit(session.user.id, amount_num)
result = (
f"存款成功!\n此次存款金额为: {amount.result}\n"
f"当前小时利率为: {rate * 100:.2f}%"
)
effective_hour = int(24 - datetime.now().hour)
if event_rate:
result += f"(小真寻偷偷将小时利率给你增加了 {event_rate:.2f}% 哦)"
result += (
f"\n预计总收益为: {int(amount.result * rate * effective_hour) or 1} 金币。"
)
logger.info(
f"小真寻银行存款:{amount_num},当前存款数:{amount.result},存款小时利率: {rate}",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(result).finish(at_sender=True)
@_matcher.assign("withdraw")
async def _(session: Uninfo, arparma: Arparma, amount: Match[int]):
amount_num = amount.result if amount.available else await get_amount("取款")
if result := await BankManager.withdraw_check(session.user.id, amount_num):
await MessageUtils.build_message(result).finish(reply_to=True)
try:
user = await BankManager.withdraw(session.user.id, amount_num)
result = (
f"取款成功!\n当前取款金额为: {amount_num}\n当前存款金额为: {user.amount}"
)
logger.info(
f"小真寻银行取款:{amount_num}, 当前存款数:{user.amount},"
f" 存款小时利率:{user.rate}",
arparma.header_result,
session=session,
)
await MessageUtils.build_message(result).finish(reply_to=True)
except ValueError:
await MessageUtils.build_message("你的银行内的存款数量不足哦...").finish(
reply_to=True
)
@_matcher.assign("user-info")
async def _(session: Uninfo, arparma: Arparma, uname: str = UserName()):
result = await BankManager.get_user_info(session, uname)
await MessageUtils.build_message(result).send()
logger.info("查看银行个人信息", arparma.header_result, session=session)
@_matcher.assign("bank-info")
async def _(session: Uninfo, arparma: Arparma):
result = await BankManager.get_bank_info()
await MessageUtils.build_message(result).send()
logger.info("查看银行信息", arparma.header_result, session=session)
# @_matcher.assign("loan")
# async def _(session: Uninfo, arparma: Arparma, amount: Match[int]):
# amount_num = amount.result if amount.available else await get_amount("贷款")
# if amount_num <= 0:
# await MessageUtils.build_message("贷款数量必须大于 0 啊笨蛋!").finish()
# try:
# user, event_rate = await BankManager.loan(session.user.id, amount_num)
# result = (
# f"贷款成功!\n当前贷金额为: {user.loan_amount}"
# f"\n当前利率为: {user.loan_rate * 100}%"
# )
# if event_rate:
# result += f"(小真寻偷偷将利率给你降低了 {event_rate}% 哦)"
# result += f"\n预计每小时利息为:{int(user.loan_amount * user.loan_rate)}金币。"
# logger.info(
# f"小真寻银行贷款: {amount_num}, 当前贷款数: {user.loan_amount}, "
# f"贷款利率: {user.loan_rate}",
# arparma.header_result,
# session=session,
# )
# except ValueError:
# await MessageUtils.build_message(
# "贷款数量超过最大限制,请签到提升好感度获取更多额度吧..."
# ).finish(reply_to=True)
# @_matcher.assign("repayment")
# async def _(session: Uninfo, arparma: Arparma, amount: Match[int]):
# amount_num = amount.result if amount.available else await get_amount("还款")
# if amount_num <= 0:
# await MessageUtils.build_message("还款数量必须大于 0 啊笨蛋!").finish()
# user = await BankManager.repayment(session.user.id, amount_num)
# result = (f"还款成功!\n当前还款金额为: {amount_num}\n"
# f"当前贷款金额为: {user.loan_amount}")
# logger.info(
# f"小真寻银行还款:{amount_num},当前贷款数:{user.amount}, 贷款利率:{user.rate}",
# arparma.header_result,
# session=session,
# )
# await MessageUtils.build_message(result).finish(at_sender=True)
@scheduler.scheduled_job(
"cron",
hour=0,
minute=0,
)
async def _():
await BankManager.settlement()
logger.info("小真寻银行结算", "定时任务")
@@ -1,450 +0,0 @@
import asyncio
from datetime import datetime, timedelta
import random
from nonebot_plugin_htmlrender import template_to_pic
from nonebot_plugin_uninfo import Uninfo
from tortoise.expressions import RawSQL
from tortoise.functions import Count, Sum
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import TEMPLATE_PATH
from zhenxun.models.mahiro_bank import MahiroBank
from zhenxun.models.mahiro_bank_log import MahiroBankLog
from zhenxun.models.sign_user import SignUser
from zhenxun.models.user_console import UserConsole
from zhenxun.utils.enum import BankHandleType, GoldHandle
from zhenxun.utils.platform import PlatformUtils
base_config = Config.get("mahiro_bank")
class BankManager:
@classmethod
async def random_event(cls, impression: float):
"""随机事件"""
impression_event = base_config.get("impression_event")
impression_event_prop = base_config.get("impression_event_prop")
impression_event_range = base_config.get("impression_event_range")
if impression >= impression_event and random.random() < impression_event_prop:
"""触发好感度事件"""
return random.uniform(impression_event_range[0], impression_event_range[1])
return None
@classmethod
async def deposit_check(cls, user_id: str, amount: int) -> str | None:
"""检查存款是否合法
参数:
user_id: 用户id
amount: 存款金额
返回:
str | None: 存款信息
"""
if amount <= 0:
return "存款数量必须大于 0 啊笨蛋!"
user, sign_user, bank_user = await asyncio.gather(
*[
UserConsole.get_user(user_id),
SignUser.get_user(user_id),
cls.get_user(user_id),
]
)
sign_max_deposit: int = base_config.get("sign_max_deposit")
max_deposit = max(int(float(sign_user.impression) * sign_max_deposit), 100)
if user.gold < amount:
return f"金币数量不足,当前你的金币为:{user.gold}."
if bank_user.amount + amount > max_deposit:
return (
f"存款超过上限,存款上限为:{max_deposit},"
f"当前你的还可以存款金额:{max_deposit - bank_user.amount}。"
)
max_daily_deposit_count: int = base_config.get("max_daily_deposit_count")
today_deposit_count = len(await cls.get_user_deposit(user_id))
if today_deposit_count >= max_daily_deposit_count:
return f"存款次数超过上限,每日存款次数上限为:{max_daily_deposit_count}。"
return None
@classmethod
async def withdraw_check(cls, user_id: str, amount: int) -> str | None:
"""检查取款是否合法
参数:
user_id: 用户id
amount: 取款金额
返回:
str | None: 取款信息
"""
if amount <= 0:
return "取款数量必须大于 0 啊笨蛋!"
user = await cls.get_user(user_id)
data_list = await cls.get_user_deposit(user_id)
lock_amount = sum(data.amount for data in data_list)
if user.amount - lock_amount < amount:
return (
"取款金额不足,当前你的存款为:"
f"{user.amount}({lock_amount}已被锁定)!"
)
return None
@classmethod
async def get_user_deposit(
cls, user_id: str, is_completed: bool = False
) -> list[MahiroBankLog]:
"""获取用户今日存款次数
参数:
user_id: 用户id
返回:
list[MahiroBankLog]: 存款列表
"""
return await MahiroBankLog.filter(
user_id=user_id,
handle_type=BankHandleType.DEPOSIT,
is_completed=is_completed,
)
@classmethod
async def get_user(cls, user_id: str) -> MahiroBank:
"""查询余额
参数:
user_id: 用户id
返回:
MahiroBank
"""
user, _ = await MahiroBank.get_or_create(user_id=user_id)
return user
@classmethod
async def get_user_data(
cls,
user_id: str,
data_type: BankHandleType,
is_completed: bool = False,
count: int = 5,
) -> list[MahiroBankLog]:
return (
await MahiroBankLog.filter(
user_id=user_id, handle_type=data_type, is_completed=is_completed
)
.order_by("-id")
.limit(count)
.all()
)
@classmethod
async def complete_projected_revenue(cls, user_id: str) -> int:
"""预计收益
参数:
user_id: 用户id
返回:
int: 预计收益金额
"""
deposit_list = await cls.get_user_deposit(user_id)
if not deposit_list:
return 0
return int(
sum(
deposit.rate * deposit.amount * deposit.effective_hour
for deposit in deposit_list
)
)
@classmethod
async def get_user_info(cls, session: Uninfo, uname: str) -> bytes:
"""获取用户数据
参数:
session: Uninfo
uname: 用户id
返回:
bytes: 图片数据
"""
user_id = session.user.id
user = await cls.get_user(user_id=user_id)
(
rank,
deposit_count,
user_today_deposit,
projected_revenue,
sum_data,
) = await asyncio.gather(
*[
MahiroBank.filter(amount__gt=user.amount).count(),
MahiroBankLog.filter(user_id=user_id).count(),
cls.get_user_deposit(user_id),
cls.complete_projected_revenue(user_id),
MahiroBankLog.filter(
user_id=user_id, handle_type=BankHandleType.INTEREST
)
.annotate(sum=Sum("amount"))
.values("sum"),
]
)
now = datetime.now()
end_time = (
now
+ timedelta(days=1)
- timedelta(hours=now.hour, minutes=now.minute, seconds=now.second)
)
today_deposit_amount = sum(deposit.amount for deposit in user_today_deposit)
deposit_list = [
{
"id": deposit.id,
"date": now.date(),
"start_time": str(deposit.create_time).split(".")[0],
"end_time": end_time.replace(microsecond=0),
"amount": deposit.amount,
"rate": f"{deposit.rate * 100:.2f}",
"projected_revenue": int(
deposit.amount * deposit.rate * deposit.effective_hour
)
or 1,
}
for deposit in user_today_deposit
]
platform = PlatformUtils.get_platform(session)
data = {
"name": uname,
"rank": rank + 1,
"avatar_url": PlatformUtils.get_user_avatar_url(
user_id, platform, session.self_id
),
"amount": user.amount,
"deposit_count": deposit_count,
"today_deposit_count": len(user_today_deposit),
"cumulative_gain": sum_data[0]["sum"] or 0,
"projected_revenue": projected_revenue,
"today_deposit_amount": today_deposit_amount,
"deposit_list": deposit_list,
"create_time": now.replace(microsecond=0),
}
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "mahiro_bank").absolute()),
template_name="user.html",
templates={"data": data},
pages={
"viewport": {"width": 386, "height": 700},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)
@classmethod
async def get_bank_info(cls) -> bytes:
now = datetime.now()
now_start = now - timedelta(
hours=now.hour, minutes=now.minute, seconds=now.second
)
(
bank_data,
today_count,
interest_amount,
active_user_count,
date_data,
) = await asyncio.gather(
*[
MahiroBank.annotate(
amount_sum=Sum("amount"), user_count=Count("id")
).values("amount_sum", "user_count"),
MahiroBankLog.filter(
create_time__gt=now_start, handle_type=BankHandleType.DEPOSIT
).count(),
MahiroBankLog.filter(handle_type=BankHandleType.INTEREST)
.annotate(amount_sum=Sum("amount"))
.values("amount_sum"),
MahiroBankLog.filter(
create_time__gte=now_start - timedelta(days=7),
handle_type=BankHandleType.DEPOSIT,
)
.annotate(count=Count("user_id", distinct=True))
.values("count"),
MahiroBankLog.filter(
create_time__gte=now_start - timedelta(days=7),
handle_type=BankHandleType.DEPOSIT,
)
.annotate(date=RawSQL("DATE(create_time)"), total_amount=Sum("amount"))
.group_by("date")
.values("date", "total_amount"),
]
)
date2cnt = {str(date["date"]): date["total_amount"] for date in date_data}
date = now.date()
e_date, e_amount = [], []
for _ in range(7):
if str(date) in date2cnt:
e_amount.append(date2cnt[str(date)])
else:
e_amount.append(0)
e_date.append(str(date)[5:])
date -= timedelta(days=1)
e_date.reverse()
e_amount.reverse()
date = 1
lasted_log = await MahiroBankLog.annotate().order_by("create_time").first()
if lasted_log:
date = now.date() - lasted_log.create_time.date()
date = (date.days or 1) + 1
data = {
"amount_sum": bank_data[0]["amount_sum"],
"user_count": bank_data[0]["user_count"],
"today_count": today_count,
"day_amount": int(bank_data[0]["amount_sum"] / date),
"interest_amount": interest_amount[0]["amount_sum"] or 0,
"active_user_count": active_user_count[0]["count"] or 0,
"e_data": e_date,
"e_amount": e_amount,
"create_time": now.replace(microsecond=0),
}
return await template_to_pic(
template_path=str((TEMPLATE_PATH / "mahiro_bank").absolute()),
template_name="bank.html",
templates={"data": data},
pages={
"viewport": {"width": 450, "height": 750},
"base_url": f"file://{TEMPLATE_PATH}",
},
wait=2,
)
@classmethod
async def deposit(
cls, user_id: str, amount: int
) -> tuple[MahiroBank, float, float | None]:
"""存款
参数:
user_id: 用户id
amount: 存款数量
返回:
tuple[MahiroBank, float, float]: MahiroBank,利率,增加的利率
"""
rate_range = base_config.get("rate_range")
rate = random.uniform(rate_range[0], rate_range[1])
sign_user = await SignUser.get_user(user_id)
random_add_rate = await cls.random_event(float(sign_user.impression))
if random_add_rate:
rate += random_add_rate
await UserConsole.reduce_gold(user_id, amount, GoldHandle.PLUGIN, "bank")
return await MahiroBank.deposit(user_id, amount, rate), rate, random_add_rate
@classmethod
async def withdraw(cls, user_id: str, amount: int) -> MahiroBank:
"""取款
参数:
user_id: 用户id
amount: 取款数量
返回:
MahiroBank
"""
await UserConsole.add_gold(user_id, amount, "bank")
return await MahiroBank.withdraw(user_id, amount)
@classmethod
async def loan(cls, user_id: str, amount: int) -> tuple[MahiroBank, float | None]:
"""贷款
参数:
user_id: 用户id
amount: 贷款数量
返回:
tuple[MahiroBank, float]: MahiroBank,贷款利率
"""
rate_range = base_config.get("rate_range")
rate = random.uniform(rate_range[0], rate_range[1])
sign_user = await SignUser.get_user(user_id)
user, _ = await MahiroBank.get_or_create(user_id=user_id)
if user.loan_amount + amount > sign_user.impression * 150:
raise ValueError("贷款数量超过最大限制,请签到提升好感度获取更多额度吧...")
random_reduce_rate = await cls.random_event(float(sign_user.impression))
if random_reduce_rate:
rate -= random_reduce_rate
await UserConsole.add_gold(user_id, amount, "bank")
return await MahiroBank.loan(user_id, amount, rate), random_reduce_rate
@classmethod
async def repayment(cls, user_id: str, amount: int) -> MahiroBank:
"""还款
参数:
user_id: 用户id
amount: 还款数量
返回:
MahiroBank
"""
await UserConsole.reduce_gold(user_id, amount, GoldHandle.PLUGIN, "bank")
return await MahiroBank.repayment(user_id, amount)
@classmethod
async def settlement(cls):
"""结算每日利率"""
bank_user_list = await MahiroBank.filter(amount__gt=0).all()
log_list = await MahiroBankLog.filter(
is_completed=False, handle_type=BankHandleType.DEPOSIT
).all()
user_list = await UserConsole.filter(
user_id__in=[user.user_id for user in bank_user_list]
).all()
user_data = {user.user_id: user for user in user_list}
bank_data: dict[str, list[MahiroBankLog]] = {}
for log in log_list:
if log.user_id not in bank_data:
bank_data[log.user_id] = []
bank_data[log.user_id].append(log)
log_create_list = []
log_update_list = []
# 计算每日默认金币
for bank_user in bank_user_list:
if user := user_data.get(bank_user.user_id):
amount = bank_user.amount
if logs := bank_data.get(bank_user.user_id):
amount -= sum(log.amount for log in logs)
if not amount:
continue
# 计算每日默认金币
gold = int(amount * bank_user.rate)
user.gold += gold
log_create_list.append(
MahiroBankLog(
user_id=bank_user.user_id,
amount=gold,
rate=bank_user.rate,
handle_type=BankHandleType.INTEREST,
is_completed=True,
)
)
# 计算每日存款金币
for user_id, logs in bank_data.items():
if user := user_data.get(user_id):
for log in logs:
gold = int(log.amount * log.rate * log.effective_hour) or 1
user.gold += gold
log.is_completed = True
log_update_list.append(log)
log_create_list.append(
MahiroBankLog(
user_id=user_id,
amount=gold,
rate=log.rate,
handle_type=BankHandleType.INTEREST,
is_completed=True,
)
)
if log_create_list:
await MahiroBankLog.bulk_create(log_create_list, 10)
if log_update_list:
await MahiroBankLog.bulk_update(log_update_list, ["is_completed"], 10)
await UserConsole.bulk_update(user_list, ["gold"], 10)
@@ -1,4 +1,4 @@
from nonebot import on_notice from nonebot import on_notice, on_request
from nonebot.adapters import Bot from nonebot.adapters import Bot
from nonebot.adapters.onebot.v11 import ( from nonebot.adapters.onebot.v11 import (
GroupDecreaseNoticeEvent, GroupDecreaseNoticeEvent,
@@ -14,10 +14,9 @@ from nonebot_plugin_uninfo import Uninfo
from zhenxun.builtin_plugins.platform.qq.exception import ForceAddGroupError from zhenxun.builtin_plugins.platform.qq.exception import ForceAddGroupError
from zhenxun.configs.config import BotConfig, Config from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
from zhenxun.models.event_log import EventLog
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.utils.common_utils import CommonUtils from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import EventLogType, PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.rules import notice_rule from zhenxun.utils.rules import notice_rule
@@ -107,6 +106,8 @@ group_decrease_handle = on_notice(
rule=notice_rule([GroupMemberDecreaseEvent, GroupDecreaseNoticeEvent]), rule=notice_rule([GroupMemberDecreaseEvent, GroupDecreaseNoticeEvent]),
) )
"""群员减少处理""" """群员减少处理"""
add_group = on_request(priority=1, block=False)
"""加群同意请求"""
@group_increase_handle.handle() @group_increase_handle.handle()
@@ -141,20 +142,7 @@ async def _(
if event.sub_type == "kick_me": if event.sub_type == "kick_me":
"""踢出Bot""" """踢出Bot"""
await GroupManager.kick_bot(bot, group_id, str(event.operator_id)) await GroupManager.kick_bot(bot, group_id, str(event.operator_id))
await EventLog.create(
user_id=user_id, group_id=group_id, event_type=EventLogType.KICK_BOT
)
elif event.sub_type in ["leave", "kick"]: elif event.sub_type in ["leave", "kick"]:
if event.sub_type == "leave":
"""主动退群"""
await EventLog.create(
user_id=user_id, group_id=group_id, event_type=EventLogType.LEAVE_MEMBER
)
else:
"""被踢出群"""
await EventLog.create(
user_id=user_id, group_id=group_id, event_type=EventLogType.KICK_MEMBER
)
result = await GroupManager.run_user( result = await GroupManager.run_user(
bot, user_id, group_id, str(event.operator_id), event.sub_type bot, user_id, group_id, str(event.operator_id), event.sub_type
) )
@@ -1,100 +0,0 @@
import asyncio
from datetime import datetime
import random
from nonebot.adapters import Bot
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import Alconna, Args, Arparma, Field, on_alconna
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import PluginCdBlock, PluginExtraData
from zhenxun.models.fg_request import FgRequest
from zhenxun.services.log import logger
from zhenxun.utils.depends import UserName
from zhenxun.utils.enum import RequestHandleType, RequestType
from zhenxun.utils.platform import PlatformUtils
__plugin_meta__ = PluginMetadata(
name="群组申请",
description="""
一些小群直接邀请入群导致无法正常生成审核请求,需要用该方法手动生成审核请求。
当管理员同意同意时会发送消息进行提示,之后再进行拉群不会退出。
该消息会发送至管理员,多次发送不存在的群组id或相同群组id可能导致ban。
""".strip(),
usage="""
指令:
申请入群 [群号]
示例: 申请入群 123123123
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1",
menu_type="其他",
limits=[PluginCdBlock(cd=300, result="每5分钟只能申请一次哦~")],
).to_dict(),
)
_matcher = on_alconna(
Alconna(
"申请入群",
Args[
"group_id",
int,
Field(
missing_tips=lambda: "请在命令后跟随群组id!",
unmatch_tips=lambda _: "群组id必须为数字!",
),
],
),
skip_for_unmatch=False,
priority=5,
block=True,
rule=to_me(),
)
@_matcher.handle()
async def _(
bot: Bot, session: Uninfo, arparma: Arparma, group_id: int, uname: str = UserName()
):
# 旧请求全部设置为过期
await FgRequest.filter(
request_type=RequestType.GROUP,
user_id=session.user.id,
group_id=str(group_id),
handle_type__isnull=True,
).update(handle_type=RequestHandleType.EXPIRE)
f = await FgRequest.create(
request_type=RequestType.GROUP,
platform=PlatformUtils.get_platform(session),
bot_id=bot.self_id,
flag="0",
user_id=session.user.id,
nickname=uname,
group_id=str(group_id),
)
results = await PlatformUtils.send_superuser(
bot,
f"*****一份入群申请*****\n"
f"ID:{f.id}\n"
f"申请人:{uname}({session.user.id})\n群聊:"
f"{group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}\n"
"注:该请求为手动申请入群",
)
if message_ids := [
str(r[1].msg_ids[0]["message_id"]) for r in results if r[1] and r[1].msg_ids
]:
f.message_ids = ",".join(message_ids)
await f.save(update_fields=["message_ids"])
await asyncio.sleep(random.randint(1, 5))
await bot.send_private_msg(
user_id=int(session.user.id),
message=f"已发送申请,请等待管理员审核,ID:{f.id}。",
)
logger.info(
f"用户 {uname}({session.user.id}) 申请入群 {group_id},ID:{f.id}。",
arparma.header_result,
session=session,
)
@@ -18,12 +18,6 @@ from zhenxun.utils.utils import is_number
from .config import BASE_PATH, DEFAULT_GITHUB_URL, EXTRA_GITHUB_URL from .config import BASE_PATH, DEFAULT_GITHUB_URL, EXTRA_GITHUB_URL
BAT_FILE = Path() / "win启动.bat"
WIN_COMMAND = ["./Python310/python.exe", "-m", "pip", "install", "-r"]
DEFAULT_COMMAND = ["poetry", "run", "pip", "install", "-r"]
def row_style(column: str, text: str) -> RowStyle: def row_style(column: str, text: str) -> RowStyle:
"""被动技能文本风格 """被动技能文本风格
@@ -56,10 +50,8 @@ def install_requirement(plugin_path: Path):
return return
try: try:
command = WIN_COMMAND if BAT_FILE.exists() else DEFAULT_COMMAND
command.append(str(existing_requirements))
result = subprocess.run( result = subprocess.run(
command, ["poetry", "run", "pip", "install", "-r", str(existing_requirements)],
check=True, check=True,
capture_output=True, capture_output=True,
text=True, text=True,
@@ -99,8 +91,8 @@ class ShopManage:
"plugins.json" "plugins.json"
) )
extra_github_url = await extra_github_repo.get_raw_download_urls("plugins.json") extra_github_url = await extra_github_repo.get_raw_download_urls("plugins.json")
res = await AsyncHttpx.get(default_github_url, check_status_code=200) res = await AsyncHttpx.get(default_github_url)
res2 = await AsyncHttpx.get(extra_github_url, check_status_code=200) res2 = await AsyncHttpx.get(extra_github_url)
# 检查请求结果 # 检查请求结果
if res.status_code != 200 or res2.status_code != 200: if res.status_code != 200 or res2.status_code != 200:
+6 -29
View File
@@ -17,12 +17,11 @@ from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig, Config from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.models.event_log import EventLog
from zhenxun.models.fg_request import FgRequest from zhenxun.models.fg_request import FgRequest
from zhenxun.models.friend_user import FriendUser from zhenxun.models.friend_user import FriendUser
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import EventLogType, PluginType, RequestHandleType, RequestType from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
base_config = Config.get("invite_manager") base_config = Config.get("invite_manager")
@@ -113,29 +112,21 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi
nickname=nickname, nickname=nickname,
comment=comment, comment=comment,
) )
results = await PlatformUtils.send_superuser( await PlatformUtils.send_superuser(
bot, bot,
f"*****一份好友申请*****\n" f"*****一份好友申请*****\n"
f"ID: {f.id}\n" f"ID: {f.id}\n"
f"昵称:{nickname}({event.user_id})\n" f"昵称:{nickname}({event.user_id})\n"
f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n" f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n"
f"日期:{datetime.now().replace(microsecond=0)}\n" f"日期:{str(datetime.now()).split('.')[0]}\n"
f"备注:{event.comment}", f"备注:{event.comment}",
) )
if message_ids := [
str(r[1].msg_ids[0]["message_id"])
for r in results
if r[1] and r[1].msg_ids
]:
f.message_ids = ",".join(message_ids)
await f.save(update_fields=["message_ids"])
else: else:
logger.debug("好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) logger.debug("好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id)
@group_req.handle() @group_req.handle()
async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession): async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession):
# sourcery skip: low-code-quality
if event.sub_type != "invite": if event.sub_type != "invite":
return return
if str(event.user_id) in bot.config.superusers or base_config.get("AUTO_ADD_GROUP"): if str(event.user_id) in bot.config.superusers or base_config.get("AUTO_ADD_GROUP"):
@@ -195,7 +186,7 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
group_id=str(event.group_id), group_id=str(event.group_id),
handle_type=RequestHandleType.APPROVE, handle_type=RequestHandleType.APPROVE,
) )
results = await PlatformUtils.send_superuser( await PlatformUtils.send_superuser(
bot, bot,
f"*****一份入群申请*****\n" f"*****一份入群申请*****\n"
f"ID:{f.id}\n" f"ID:{f.id}\n"
@@ -239,27 +230,13 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
nickname=nickname, nickname=nickname,
group_id=str(event.group_id), group_id=str(event.group_id),
) )
kick_count = await EventLog.filter( await PlatformUtils.send_superuser(
group_id=str(event.group_id), event_type=EventLogType.KICK_BOT
).count()
kick_message = (
f"\n该群累计踢出{BotConfig.self_nickname} <{kick_count}>次"
if kick_count
else ""
)
results = await PlatformUtils.send_superuser(
bot, bot,
f"*****一份入群申请*****\n" f"*****一份入群申请*****\n"
f"ID:{f.id}\n" f"ID:{f.id}\n"
f"申请人:{nickname}({event.user_id})\n群聊:" f"申请人:{nickname}({event.user_id})\n群聊:"
f"{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}" f"{event.group_id}\n邀请日期:{datetime.now().replace(microsecond=0)}",
f"{kick_message}",
) )
if message_ids := [
str(r[1].msg_ids[0]["message_id"]) for r in results if r[1] and r[1].msg_ids
]:
f.message_ids = ",".join(message_ids)
await f.save(update_fields=["message_ids"])
else: else:
logger.debug( logger.debug(
"群聊请求五分钟内重复, 已忽略", "群聊请求五分钟内重复, 已忽略",
+61 -2
View File
@@ -1,8 +1,67 @@
from asyncio.exceptions import TimeoutError
import aiofiles
import nonebot
from nonebot.drivers import Driver
from nonebot_plugin_apscheduler import scheduler
import ujson as json
from zhenxun.configs.path_config import TEXT_PATH
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.utils.manager.priority_manager import PriorityLifecycle from zhenxun.services.log import logger
from zhenxun.utils.http_utils import AsyncHttpx
driver: Driver = nonebot.get_driver()
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def update_city():
"""
部分插件需要中国省份城市
这里直接更新,避免插件内代码重复
"""
china_city = TEXT_PATH / "china_city.json"
if not china_city.exists():
data = {}
try:
logger.debug("开始更新城市列表...")
res = await AsyncHttpx.get(
"http://www.weather.com.cn/data/city3jdata/china.html", timeout=5
)
res.encoding = "utf8"
provinces_data = json.loads(res.text)
for province in provinces_data.keys():
data[provinces_data[province]] = []
res = await AsyncHttpx.get(
f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html",
timeout=5,
)
res.encoding = "utf8"
city_data = json.loads(res.text)
for city in city_data.keys():
data[provinces_data[province]].append(city_data[city])
async with aiofiles.open(china_city, "w", encoding="utf8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
logger.info("自动更新城市列表完成.....")
except TimeoutError as e:
logger.warning("自动更新城市列表超时...", e=e)
except ValueError as e:
logger.warning("自动城市列表失败.....", e=e)
except Exception as e:
logger.error("自动城市列表未知错误", e=e)
# 自动更新城市列表
@scheduler.scheduled_job(
"cron",
hour=6,
minute=1,
)
async def _():
await update_city()
@driver.on_startup
async def _(): async def _():
"""开启/禁用插件格式修改""" """开启/禁用插件格式修改"""
_, is_create = await GroupConsole.get_or_create(group_id=133133133) _, is_create = await GroupConsole.get_or_create(group_id=133133133)
+10 -16
View File
@@ -5,9 +5,7 @@ from nonebot_plugin_alconna import (
AlconnaQuery, AlconnaQuery,
Args, Args,
Arparma, Arparma,
At,
Match, Match,
MultiVar,
Option, Option,
Query, Query,
Subcommand, Subcommand,
@@ -35,7 +33,6 @@ __plugin_meta__ = PluginMetadata(
usage=""" usage="""
商品操作 商品操作
指令: 指令:
商店
我的金币 我的金币
我的道具 我的道具
使用道具 [名称/Id] 使用道具 [名称/Id]
@@ -49,7 +46,6 @@ __plugin_meta__ = PluginMetadata(
plugin_type=PluginType.NORMAL, plugin_type=PluginType.NORMAL,
menu_type="商店", menu_type="商店",
commands=[ commands=[
Command(command="商店"),
Command(command="我的金币"), Command(command="我的金币"),
Command(command="我的道具"), Command(command="我的道具"),
Command(command="购买道具"), Command(command="购买道具"),
@@ -78,21 +74,13 @@ _matcher = on_alconna(
Subcommand("my-cost", help_text="我的金币"), Subcommand("my-cost", help_text="我的金币"),
Subcommand("my-props", help_text="我的道具"), Subcommand("my-props", help_text="我的道具"),
Subcommand("buy", Args["name?", str]["num?", int], help_text="购买道具"), Subcommand("buy", Args["name?", str]["num?", int], help_text="购买道具"),
Subcommand("use", Args["name?", str]["num?", int], help_text="使用道具"),
Subcommand("gold-list", Args["num?", int], help_text="金币排行"), Subcommand("gold-list", Args["num?", int], help_text="金币排行"),
), ),
priority=5, priority=5,
block=True, block=True,
) )
_use_matcher = on_alconna(
Alconna(
"使用道具",
Args["name?", str]["num?", int]["at_users?", MultiVar(At)],
),
priority=5,
block=True,
)
_matcher.shortcut( _matcher.shortcut(
"我的金币", "我的金币",
command="商店", command="商店",
@@ -114,6 +102,13 @@ _matcher.shortcut(
prefix=True, prefix=True,
) )
_matcher.shortcut(
"使用道具(?P<name>.*?)",
command="商店",
arguments=["use", "{name}"],
prefix=True,
)
_matcher.shortcut( _matcher.shortcut(
"金币排行", "金币排行",
command="商店", command="商店",
@@ -177,7 +172,7 @@ async def _(
await MessageUtils.build_message(result).send(reply_to=True) await MessageUtils.build_message(result).send(reply_to=True)
@_use_matcher.handle() @_matcher.assign("use")
async def _( async def _(
bot: Bot, bot: Bot,
event: Event, event: Event,
@@ -186,7 +181,6 @@ async def _(
arparma: Arparma, arparma: Arparma,
name: Match[str], name: Match[str],
num: Query[int] = AlconnaQuery("num", 1), num: Query[int] = AlconnaQuery("num", 1),
at_users: Query[list[At]] = AlconnaQuery("at_users", []),
): ):
if not name.available: if not name.available:
await MessageUtils.build_message( await MessageUtils.build_message(
@@ -194,7 +188,7 @@ async def _(
).finish(reply_to=True) ).finish(reply_to=True)
try: try:
result = await ShopManage.use( result = await ShopManage.use(
bot, event, session, message, name.result, num.result, "", at_users.result bot, event, session, message, name.result, num.result, ""
) )
logger.info( logger.info(
f"使用道具 {name.result}, 数量: {num.result}", f"使用道具 {name.result}, 数量: {num.result}",
+12 -35
View File
@@ -4,11 +4,11 @@ from datetime import datetime, timedelta
import inspect import inspect
import time import time
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Literal from typing import Any, ClassVar, Literal
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot, Event
from nonebot.compat import model_dump from nonebot.compat import model_dump
from nonebot_plugin_alconna import At, UniMessage, UniMsg from nonebot_plugin_alconna import UniMessage, UniMsg
from nonebot_plugin_uninfo import Uninfo from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel, Field, create_model from pydantic import BaseModel, Field, create_model
from tortoise.expressions import Q from tortoise.expressions import Q
@@ -48,10 +48,6 @@ class Goods(BaseModel):
"""model""" """model"""
session: Uninfo | None = None session: Uninfo | None = None
"""Uninfo""" """Uninfo"""
at_user: str | None = None
"""At对象"""
at_users: list[str] = []
"""At对象列表"""
class ShopParam(BaseModel): class ShopParam(BaseModel):
@@ -69,19 +65,15 @@ class ShopParam(BaseModel):
"""道具单次使用数量""" """道具单次使用数量"""
text: str text: str
"""text""" """text"""
send_success_msg: bool = True send_success_msg: ClassVar[bool] = True
"""是否发送使用成功信息""" """是否发送使用成功信息"""
max_num_limit: int = 1 max_num_limit: ClassVar[int] = 1
"""单次使用最大次数""" """单次使用最大次数"""
session: Uninfo | None = None session: Uninfo | None = None
"""Uninfo""" """Uninfo"""
message: UniMsg message: UniMsg
"""UniMessage""" """UniMessage"""
at_user: str | None = None extra_data: ClassVar[dict[str, Any]] = {}
"""At对象"""
at_users: list[str] = []
"""At对象列表"""
extra_data: dict[str, Any] = Field(default_factory=dict)
"""额外数据""" """额外数据"""
class Config: class Config:
@@ -164,7 +156,6 @@ class ShopManage:
goods: Goods, goods: Goods,
num: int, num: int,
text: str, text: str,
at_users: list[str] = [],
) -> tuple[ShopParam, dict[str, Any]]: ) -> tuple[ShopParam, dict[str, Any]]:
"""构造参数 """构造参数
@@ -174,7 +165,6 @@ class ShopManage:
goods_name: 商品名称 goods_name: 商品名称
num: 数量 num: 数量
text: 其他信息 text: 其他信息
at_users: at用户
""" """
group_id = None group_id = None
if session.group: if session.group:
@@ -182,7 +172,6 @@ class ShopManage:
session.group.parent.id if session.group.parent else session.group.id session.group.parent.id if session.group.parent else session.group.id
) )
_kwargs = goods.params _kwargs = goods.params
at_user = at_users[0] if at_users else None
model = goods.model( model = goods.model(
**{ **{
"goods_name": goods.name, "goods_name": goods.name,
@@ -194,8 +183,6 @@ class ShopManage:
"text": text, "text": text,
"session": session, "session": session,
"message": message, "message": message,
"at_user": at_user,
"at_users": at_users,
} }
) )
return model, { return model, {
@@ -207,8 +194,6 @@ class ShopManage:
"num": num, "num": num,
"text": text, "text": text,
"goods_name": goods.name, "goods_name": goods.name,
"at_user": at_user,
"at_users": at_users,
} }
@classmethod @classmethod
@@ -238,7 +223,6 @@ class ShopManage:
**param.extra_data, **param.extra_data,
"session": session, "session": session,
"message": message, "message": message,
"shop_param": ShopParam,
} }
for key in list(param_json.keys()): for key in list(param_json.keys()):
if key not in args: if key not in args:
@@ -324,7 +308,6 @@ class ShopManage:
goods_name: str, goods_name: str,
num: int, num: int,
text: str, text: str,
at_users: list[At] = [],
) -> str | UniMessage | None: ) -> str | UniMessage | None:
"""使用道具 """使用道具
@@ -336,7 +319,6 @@ class ShopManage:
goods_name: 商品名称 goods_name: 商品名称
num: 使用数量 num: 使用数量
text: 其他信息 text: 其他信息
at_users: at用户
返回: 返回:
str | MessageFactory | None: 使用完成后返回信息 str | MessageFactory | None: 使用完成后返回信息
@@ -357,18 +339,16 @@ class ShopManage:
goods = cls.uuid2goods.get(goods_info.uuid) goods = cls.uuid2goods.get(goods_info.uuid)
if not goods or not goods.func: if not goods or not goods.func:
return f"{goods_info.goods_name} 未注册使用函数, 无法使用..." return f"{goods_info.goods_name} 未注册使用函数, 无法使用..."
at_user_ids = [at.target for at in at_users]
param, kwargs = cls.__build_params( param, kwargs = cls.__build_params(
bot, event, session, message, goods, num, text, at_user_ids bot, event, session, message, goods, num, text
) )
if num > param.max_num_limit: if num > param.max_num_limit:
return f"{goods_info.goods_name} 单次使用最大数量为{param.max_num_limit}..." return f"{goods_info.goods_name} 单次使用最大数量为{param.max_num_limit}..."
await cls.run_before_after(goods, param, session, message, "before", **kwargs) await cls.run_before_after(goods, param, session, message, "before", **kwargs)
result = await cls.__run(goods, param, session, message, **kwargs)
await UserConsole.use_props( await UserConsole.use_props(
session.user.id, goods_info.uuid, num, PlatformUtils.get_platform(session) session.user.id, goods_info.uuid, num, PlatformUtils.get_platform(session)
) )
result = await cls.__run(goods, param, session, message, **kwargs)
await cls.run_before_after(goods, param, session, message, "after", **kwargs) await cls.run_before_after(goods, param, session, message, "after", **kwargs)
if not result and param.send_success_msg: if not result and param.send_success_msg:
result = f"使用道具 {goods.name} {num} 次成功!" result = f"使用道具 {goods.name} {num} 次成功!"
@@ -404,10 +384,10 @@ class ShopManage:
cls.uuid2goods[uuid] = Goods( cls.uuid2goods[uuid] = Goods(
model=create_model( model=create_model(
f"{uuid}_model", f"{uuid}_model",
send_success_msg=send_success_msg,
max_num_limit=max_num_limit,
__base__=ShopParam, __base__=ShopParam,
send_success_msg=(bool, Field(default=send_success_msg)), extra_data=kwargs,
max_num_limit=(int, Field(default=max_num_limit)),
extra_data=(dict[str, Any], Field(default=kwargs)),
), ),
params=kwargs, params=kwargs,
before_handle=before_handle, before_handle=before_handle,
@@ -499,13 +479,10 @@ class ShopManage:
if not user.props: if not user.props:
return None return None
user.props = {uuid: count for uuid, count in user.props.items() if count > 0}
goods_list = await GoodsInfo.filter(uuid__in=user.props.keys()).all() goods_list = await GoodsInfo.filter(uuid__in=user.props.keys()).all()
goods_by_uuid = {item.uuid: item for item in goods_list} goods_by_uuid = {item.uuid: item for item in goods_list}
user.props = {
uuid: count
for uuid, count in user.props.items()
if count > 0 and goods_by_uuid.get(uuid)
}
table_rows = [] table_rows = []
for i, prop_uuid in enumerate(user.props): for i, prop_uuid in enumerate(user.props):
+2 -1
View File
@@ -10,6 +10,7 @@ from nonebot_plugin_alconna import (
store_true, store_true,
) )
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import ( from zhenxun.configs.utils import (
Command, Command,
@@ -22,7 +23,7 @@ from zhenxun.utils.depends import UserName
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from ._data_source import SignManage from ._data_source import SignManage
from .goods_register import Uninfo from .goods_register import driver # noqa: F401
from .utils import clear_sign_data_pic from .utils import clear_sign_data_pic
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
@@ -1,6 +1,7 @@
from decimal import Decimal from decimal import Decimal
import nonebot import nonebot
from nonebot.drivers import Driver
from nonebot_plugin_uninfo import Uninfo from nonebot_plugin_uninfo import Uninfo
from zhenxun.models.sign_user import SignUser from zhenxun.models.sign_user import SignUser
@@ -8,7 +9,14 @@ from zhenxun.models.user_console import UserConsole
from zhenxun.utils.decorator.shop import shop_register from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
# @driver.on_startup
# async def _():
# """
# 导入内置的三个商品
# """
@shop_register( @shop_register(
+1 -2
View File
@@ -16,7 +16,6 @@ from zhenxun.models.sign_log import SignLog
from zhenxun.models.sign_user import SignUser from zhenxun.models.sign_user import SignUser
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.image_utils import BuildImage from zhenxun.utils.image_utils import BuildImage
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from .config import ( from .config import (
@@ -55,7 +54,7 @@ LG_MESSAGE = [
] ]
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def init_image(): async def init_image():
SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True)
SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True)
@@ -53,7 +53,10 @@ async def _(
) )
@scheduler.scheduled_job("interval", minutes=1, max_instances=5) @scheduler.scheduled_job(
"interval",
minutes=1,
)
async def _(): async def _():
try: try:
call_list = TEMP_LIST.copy() call_list = TEMP_LIST.copy()
@@ -110,7 +110,7 @@ async def enable_plugin(
) )
await BotConsole.enable_plugin(None, plugin.module) await BotConsole.enable_plugin(None, plugin.module)
await MessageUtils.build_message( await MessageUtils.build_message(
f"已开启全部 bot 的插件: {plugin_name.result}" f"已禁用全部 bot 的插件: {plugin_name.result}"
).finish() ).finish()
elif bot_id.available: elif bot_id.available:
logger.info( logger.info(
@@ -92,7 +92,7 @@ async def enable_task(
) )
await BotConsole.enable_task(None, task.module) await BotConsole.enable_task(None, task.module)
await MessageUtils.build_message( await MessageUtils.build_message(
f"已开启全部 bot 的被动: {task_name.available}" f"已禁用全部 bot 的被动: {task_name.available}"
).finish() ).finish()
elif bot_id.available: elif bot_id.available:
logger.info( logger.info(
@@ -1,77 +1,32 @@
from arclet.alconna import AllParam from typing import Annotated
from nepattern import UnionPattern
from nonebot.adapters import Bot, Event from nonebot import on_command
from nonebot.adapters import Bot
from nonebot.params import Command
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me from nonebot.rule import to_me
import nonebot_plugin_alconna as alc from nonebot_plugin_alconna import Text as alcText
from nonebot_plugin_alconna import ( from nonebot_plugin_alconna import UniMsg
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 nonebot_plugin_session import EventSession
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from .broadcast_manager import BroadcastManager from ._data_source import BroadcastManage
from .message_processor import (
_extract_broadcast_content,
get_broadcast_target_groups,
send_broadcast_and_notify,
)
BROADCAST_SEND_DELAY_RANGE = (1, 3)
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="广播", name="广播",
description="昭告天下!", description="昭告天下!",
usage=""" usage="""
广播 [消息内容] 广播 [消息] [图片]
- 直接发送消息到除当前群组外的所有群组 示例:广播 你们好!
- 支持文本、图片、@、表情、视频等多种消息类型
- 示例:广播 你们好!
- 示例:广播 [图片] 新活动开始啦!
广播 + 引用消息
- 将引用的消息作为广播内容发送
- 支持引用普通消息或合并转发消息
- 示例:(引用一条消息) 广播
广播撤回
- 撤回最近一次由您触发的广播消息
- 仅能撤回短时间内的消息
- 示例:广播撤回
特性:
- 在群组中使用广播时,不会将消息发送到当前群组
- 在私聊中使用广播时,会发送到所有群组
别名:
- bc (广播的简写)
- recall (广播撤回的别名)
""".strip(), """.strip(),
extra=PluginExtraData( extra=PluginExtraData(
author="HibiKier", author="HibiKier",
version="1.2", version="0.1",
plugin_type=PluginType.SUPERUSER, plugin_type=PluginType.SUPERUSER,
configs=[ configs=[
RegisterConfig( RegisterConfig(
@@ -87,106 +42,26 @@ __plugin_meta__ = PluginMetadata(
).to_dict(), ).to_dict(),
) )
AnySeg = ( _matcher = on_command(
UnionPattern( "广播", priority=1, permission=SUPERUSER, block=True, rule=to_me()
[
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() @_matcher.handle()
async def handle_broadcast( async def _(
bot: Bot, bot: Bot,
event: Event,
session: EventSession, session: EventSession,
arp: alc.Arparma, message: UniMsg,
command: Annotated[tuple[str, ...], Command()],
): ):
broadcast_content_msg = await _extract_broadcast_content(bot, event, arp, session) for msg in message:
if not broadcast_content_msg: if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]):
return msg.text = msg.text.replace(command[0], "", 1).strip()
break
target_groups, enabled_groups = await get_broadcast_target_groups(bot, session) await MessageUtils.build_message("正在发送..请等一下哦!").send()
if not target_groups or not enabled_groups: count, error_count = await BroadcastManage.send(bot, message, session)
return result = f"成功广播 {count} 个群组"
if error_count:
try: result += f"\n广播失败 {error_count} 个群组"
await send_broadcast_and_notify( await MessageUtils.build_message(f"发送广播完成!\n{result}").send(reply_to=True)
bot, event, broadcast_content_msg, enabled_groups, target_groups, session logger.info(f"发送广播信息: {message}", "广播", session=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}。")
@@ -0,0 +1,72 @@
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
@@ -1,490 +0,0 @@
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<Reference> 构造转发节点数: {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
@@ -1,584 +0,0 @@
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,
)
@@ -1,64 +0,0 @@
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,
}
@@ -1,175 +0,0 @@
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
@@ -2,7 +2,7 @@ from io import BytesIO
from arclet.alconna import Args, Option from arclet.alconna import Args, Option
from arclet.alconna.typing import CommandMeta from arclet.alconna.typing import CommandMeta
from nonebot.adapters import Bot, Event from nonebot.adapters import Bot
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot.plugin import PluginMetadata from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me from nonebot.rule import to_me
@@ -10,13 +10,10 @@ from nonebot_plugin_alconna import (
Alconna, Alconna,
AlconnaQuery, AlconnaQuery,
Arparma, Arparma,
Match,
Query, Query,
Reply,
on_alconna, on_alconna,
store_true, store_true,
) )
from nonebot_plugin_alconna.uniseg.tools import reply_fetch
from nonebot_plugin_session import EventSession from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
@@ -57,7 +54,7 @@ __plugin_meta__ = PluginMetadata(
_req_matcher = on_alconna( _req_matcher = on_alconna(
Alconna( Alconna(
"请求处理", "请求处理",
Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id?", int], Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id", int],
meta=CommandMeta( meta=CommandMeta(
description="好友/群组请求处理", description="好友/群组请求处理",
usage=usage, usage=usage,
@@ -108,12 +105,12 @@ _clear_matcher = on_alconna(
) )
reg_arg_list = [ reg_arg_list = [
(r"同意好友请求\s*(?P<id>\d*)", ["-fa", "{id}"]), (r"同意好友请求", ["-fa", "{%0}"]),
(r"拒绝好友请求\s*(?P<id>\d*)", ["-fr", "{id}"]), (r"拒绝好友请求", ["-fr", "{%0}"]),
(r"忽略好友请求\s*(?P<id>\d*)", ["-fi", "{id}"]), (r"忽略好友请求", ["-fi", "{%0}"]),
(r"同意群组请求\s*(?P<id>\d*)", ["-ga", "{id}"]), (r"同意群组请求", ["-ga", "{%0}"]),
(r"拒绝群组请求\s*(?P<id>\d*)", ["-gr", "{id}"]), (r"拒绝群组请求", ["-gr", "{%0}"]),
(r"忽略群组请求\s*(?P<id>\d*)", ["-gi", "{id}"]), (r"忽略群组请求", ["-gi", "{%0}"]),
] ]
for r in reg_arg_list: for r in reg_arg_list:
@@ -128,48 +125,32 @@ for r in reg_arg_list:
@_req_matcher.handle() @_req_matcher.handle()
async def _( async def _(
bot: Bot, bot: Bot,
event: Event,
session: EventSession, session: EventSession,
handle: str, handle: str,
id: Match[int], id: int,
arparma: Arparma, arparma: Arparma,
): ):
reply: Reply | None = None
type_dict = { type_dict = {
"a": RequestHandleType.APPROVE, "a": RequestHandleType.APPROVE,
"r": RequestHandleType.REFUSED, "r": RequestHandleType.REFUSED,
"i": RequestHandleType.IGNORE, "i": RequestHandleType.IGNORE,
} }
if not id.available:
reply = await reply_fetch(event, bot)
if not reply:
await MessageUtils.build_message("请引用消息处理或添加处理Id.").finish()
handle_id = id.result
if reply:
db_data = await FgRequest.get_or_none(message_ids__contains=reply.id)
if not db_data:
await MessageUtils.build_message(
"未发现此消息的Id,请使用Id进行处理..."
).finish(reply_to=True)
handle_id = db_data.id
req = None req = None
handle_type = type_dict[handle[-1]] handle_type = type_dict[handle[-1]]
try: try:
if handle_type == RequestHandleType.APPROVE: if handle_type == RequestHandleType.APPROVE:
req = await FgRequest.approve(bot, handle_id) req = await FgRequest.approve(bot, id)
if handle_type == RequestHandleType.REFUSED: if handle_type == RequestHandleType.REFUSED:
req = await FgRequest.refused(bot, handle_id) req = await FgRequest.refused(bot, id)
if handle_type == RequestHandleType.IGNORE: if handle_type == RequestHandleType.IGNORE:
req = await FgRequest.ignore(handle_id) req = await FgRequest.ignore(id)
except NotFoundError: except NotFoundError:
await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True) await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True)
except Exception: except Exception:
await MessageUtils.build_message("其他错误, 可能flag已失效...").finish( await MessageUtils.build_message("其他错误, 可能flag已失效...").finish(
reply_to=True reply_to=True
) )
logger.info( logger.info("处理请求", arparma.header_result, session=session)
f"处理请求 Id: {req.id if req else ''}", arparma.header_result, session=session
)
await MessageUtils.build_message("成功处理请求!").send(reply_to=True) await MessageUtils.build_message("成功处理请求!").send(reply_to=True)
if req and handle_type == RequestHandleType.APPROVE: if req and handle_type == RequestHandleType.APPROVE:
await bot.send_private_msg( await bot.send_private_msg(
+5 -9
View File
@@ -10,9 +10,7 @@ from zhenxun.configs.config import Config as gConfig
from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger, logger_ from zhenxun.services.log import logger, logger_
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .api.configure import router as configure_router
from .api.logs import router as ws_log_routes from .api.logs import router as ws_log_routes
from .api.logs.log_manager import LOG_STORAGE from .api.logs.log_manager import LOG_STORAGE
from .api.menu import router as menu_router from .api.menu import router as menu_router
@@ -31,7 +29,8 @@ from .public import init_public
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(
name="WebUi", name="WebUi",
description="WebUi API", description="WebUi API",
usage='"""\n """.strip(),', usage="""
""".strip(),
extra=PluginExtraData( extra=PluginExtraData(
author="HibiKier", author="HibiKier",
version="0.1", version="0.1",
@@ -83,7 +82,7 @@ BaseApiRouter.include_router(database_router)
BaseApiRouter.include_router(plugin_router) BaseApiRouter.include_router(plugin_router)
BaseApiRouter.include_router(system_router) BaseApiRouter.include_router(system_router)
BaseApiRouter.include_router(menu_router) BaseApiRouter.include_router(menu_router)
BaseApiRouter.include_router(configure_router)
WsApiRouter = APIRouter(prefix="/zhenxun/socket") WsApiRouter = APIRouter(prefix="/zhenxun/socket")
@@ -92,11 +91,9 @@ WsApiRouter.include_router(status_routes)
WsApiRouter.include_router(chat_routes) WsApiRouter.include_router(chat_routes)
@PriorityLifecycle.on_startup(priority=0) @driver.on_startup
async def _(): async def _():
try: try:
# 存储任务引用的列表,防止任务被垃圾回收
_tasks = []
async def log_sink(message: str): async def log_sink(message: str):
loop = None loop = None
@@ -107,8 +104,7 @@ async def _():
logger.warning("Web Ui log_sink", e=e) logger.warning("Web Ui log_sink", e=e)
if not loop: if not loop:
loop = asyncio.new_event_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( logger_.add(
log_sink, colorize=True, filter=default_filter, format=default_format log_sink, colorize=True, filter=default_filter, format=default_format
@@ -1,133 +0,0 @@
import asyncio
import os
from pathlib import Path
import re
import subprocess
import sys
import time
from fastapi import APIRouter
from fastapi.responses import JSONResponse
import nonebot
from zhenxun.configs.config import BotConfig, Config
from ...base_model import Result
from .data_source import test_db_connection
from .model import Setting
router = APIRouter(prefix="/configure")
driver = nonebot.get_driver()
port = driver.config.port
BAT_FILE = Path() / "win启动.bat"
FILE_NAME = ".configure_restart"
@router.post(
"/set_configure",
response_model=Result,
response_class=JSONResponse,
description="设置基础配置",
)
async def _(setting: Setting) -> Result:
global port
password = Config.get_config("web-ui", "password")
if password or BotConfig.db_url:
return Result.fail("配置已存在,请先删除DB_URL内容和前端密码再进行设置。")
env_file = Path() / ".env.dev"
if not env_file.exists():
return Result.fail("配置文件.env.dev不存在。")
env_text = env_file.read_text(encoding="utf-8")
if setting.db_url:
if setting.db_url.startswith("sqlite"):
base_dir = Path().resolve()
# 清理和验证数据库路径
db_path_str = setting.db_url.split(":")[-1].strip()
# 移除任何可能的路径遍历尝试
db_path_str = re.sub(r"[\\/]\.\.[\\/]", "", db_path_str)
# 规范化路径
db_path = Path(db_path_str).resolve()
parent_path = db_path.parent
# 验证路径是否在项目根目录内
try:
if not parent_path.absolute().is_relative_to(base_dir):
return Result.fail("数据库路径不在项目根目录内。")
except ValueError:
return Result.fail("无效的数据库路径。")
# 创建目录
try:
parent_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
return Result.fail(f"创建数据库目录失败: {e!s}")
env_text = env_text.replace('DB_URL = ""', f'DB_URL = "{setting.db_url}"')
if setting.superusers:
superusers = ", ".join([f'"{s}"' for s in setting.superusers])
env_text = re.sub(r"SUPERUSERS=\[.*?\]", f"SUPERUSERS=[{superusers}]", env_text)
if setting.host:
env_text = env_text.replace("HOST = 127.0.0.1", f"HOST = {setting.host}")
if setting.port:
env_text = env_text.replace("PORT = 8080", f"PORT = {setting.port}")
port = setting.port
if setting.username:
Config.set_config("web-ui", "username", setting.username)
Config.set_config("web-ui", "password", setting.password, True)
env_file.write_text(env_text, encoding="utf-8")
if BAT_FILE.exists():
for file in os.listdir(Path()):
if file.startswith(FILE_NAME):
Path(file).unlink()
flag_file = Path() / f"{FILE_NAME}_{int(time.time())}"
flag_file.touch()
return Result.ok(BAT_FILE.exists(), info="设置成功,请重启真寻以完成配置!")
@router.get(
"/test_db",
response_model=Result,
response_class=JSONResponse,
description="设置基础配置",
)
async def _(db_url: str) -> Result:
result = await test_db_connection(db_url)
if isinstance(result, str):
return Result.fail(result)
return Result.ok(info="数据库连接成功!")
async def run_restart_command(bat_path: Path, port: int):
"""在后台执行重启命令"""
await asyncio.sleep(1) # 确保 FastAPI 已返回响应
subprocess.Popen([bat_path, str(port)], shell=True) # noqa: ASYNC220
sys.exit(0) # 退出当前进程
@router.post(
"/restart",
response_model=Result,
response_class=JSONResponse,
description="重启",
)
async def _() -> Result:
if not BAT_FILE.exists():
return Result.fail("自动重启仅支持意见整合包,请尝试手动重启")
flag_file = next(
(Path() / file for file in os.listdir(Path()) if file.startswith(FILE_NAME)),
None,
)
if not flag_file or not flag_file.exists():
return Result.fail("重启标志文件不存在...")
set_time = flag_file.name.split("_")[-1]
if time.time() - float(set_time) > 10 * 60:
return Result.fail("重启标志文件已过期,请重新设置配置。")
flag_file.unlink()
try:
return Result.ok(info="执行重启命令成功")
finally:
asyncio.create_task(run_restart_command(BAT_FILE, port)) # noqa: RUF006
@@ -1,18 +0,0 @@
from tortoise import Tortoise
async def test_db_connection(db_url: str) -> bool | str:
try:
# 初始化 Tortoise ORM
await Tortoise.init(
db_url=db_url,
modules={"models": ["__main__"]}, # 这里不需要实际模型
)
# 测试连接
await Tortoise.get_connection("default").execute_query("SELECT 1")
return True
except Exception as e:
return str(e)
finally:
# 关闭连接
await Tortoise.close_connections()
@@ -1,16 +0,0 @@
from pydantic import BaseModel
class Setting(BaseModel):
superusers: list[str]
"""超级用户列表"""
db_url: str
"""数据库地址"""
host: str
"""主机地址"""
port: int
"""端口"""
username: str
"""前端用户名"""
password: str
"""前端密码"""
@@ -5,63 +5,51 @@ from zhenxun.services.log import logger
from .model import MenuData, MenuItem from .model import MenuData, MenuItem
default_menus = [
MenuItem(
name="仪表盘",
module="dashboard",
router="/dashboard",
icon="dashboard",
default=True,
),
MenuItem(
name="真寻控制台",
module="command",
router="/command",
icon="command",
),
MenuItem(name="插件列表", module="plugin", router="/plugin", icon="plugin"),
MenuItem(name="插件商店", module="store", router="/store", icon="store"),
MenuItem(name="好友/群组", module="manage", router="/manage", icon="user"),
MenuItem(
name="数据库管理",
module="database",
router="/database",
icon="database",
),
MenuItem(name="系统信息", module="system", router="/system", icon="system"),
MenuItem(name="关于我们", module="about", router="/about", icon="about"),
]
class MenuManage:
class MenuManager:
def __init__(self) -> None: def __init__(self) -> None:
self.file = DATA_PATH / "web_ui" / "menu.json" self.file = DATA_PATH / "web_ui" / "menu.json"
self.menu = [] self.menu = []
if self.file.exists(): if self.file.exists():
try: try:
temp_menu = []
self.menu = json.load(self.file.open(encoding="utf8")) self.menu = json.load(self.file.open(encoding="utf8"))
self_menu_name = [menu["name"] for menu in self.menu]
for module in [m.module for m in default_menus]:
if module in self_menu_name:
temp_menu.append(
MenuItem(
**next(m for m in self.menu if m["module"] == module)
)
)
else:
temp_menu.append(self.__get_menu_model(module))
self.menu = temp_menu
except Exception as e: except Exception as e:
logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e) logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e)
if not self.menu: if not self.menu:
self.menu = default_menus self.menu = [
self.save() MenuItem(
name="仪表盘",
def __get_menu_model(self, module: str): module="dashboard",
return default_menus[ router="/dashboard",
next(i for i, m in enumerate(default_menus) if m.module == module) icon="dashboard",
] default=True,
),
MenuItem(
name="真寻控制台",
module="command",
router="/command",
icon="command",
),
MenuItem(
name="插件列表", module="plugin", router="/plugin", icon="plugin"
),
MenuItem(
name="插件商店", module="store", router="/store", icon="store"
),
MenuItem(
name="好友/群组", module="manage", router="/manage", icon="user"
),
MenuItem(
name="数据库管理",
module="database",
router="/database",
icon="database",
),
MenuItem(
name="系统信息", module="system", router="/system", icon="system"
),
]
self.save()
def get_menus(self): def get_menus(self):
return MenuData(menus=self.menu) return MenuData(menus=self.menu)
@@ -73,4 +61,4 @@ class MenuManager:
json.dump(temp, f, ensure_ascii=False, indent=4) json.dump(temp, f, ensure_ascii=False, indent=4)
menu_manage = MenuManager() menu_manage = MenuManage()
@@ -13,7 +13,6 @@ from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.chat_history import ChatHistory from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.statistics import Statistics from zhenxun.models.statistics import Statistics
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from ....base_model import BaseResultModel, QueryModel from ....base_model import BaseResultModel, QueryModel
@@ -32,7 +31,7 @@ driver: Driver = nonebot.get_driver()
CONNECT_TIME = 0 CONNECT_TIME = 0
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
global CONNECT_TIME global CONNECT_TIME
CONNECT_TIME = int(time.time()) CONNECT_TIME = int(time.time())
@@ -8,7 +8,6 @@ from zhenxun.configs.config import BotConfig
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from ....base_model import BaseResultModel, QueryModel, Result from ....base_model import BaseResultModel, QueryModel, Result
from ....utils import authentication from ....utils import authentication
@@ -22,7 +21,7 @@ router = APIRouter(prefix="/database")
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
for plugin in nonebot.get_loaded_plugins(): for plugin in nonebot.get_loaded_plugins():
module = plugin.name module = plugin.name
@@ -16,7 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result from ....base_model import Result
from ....config import QueryDateType from ....config import QueryDateType
from ....utils import authentication, clear_help_image, get_system_status from ....utils import authentication, get_system_status
from .data_source import ApiDataSource from .data_source import ApiDataSource
from .model import ( from .model import (
ActiveGroup, ActiveGroup,
@@ -234,7 +234,6 @@ async def _(param: BotManageUpdateParam):
bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins) bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins)
bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks) bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks)
await bot_data.save(update_fields=["block_plugins", "block_tasks"]) await bot_data.save(update_fields=["block_plugins", "block_tasks"])
clear_help_image()
return Result.ok() return Result.ok()
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e)
@@ -92,7 +92,7 @@ class ApiDataSource:
""" """
version_file = Path() / "__version__" version_file = Path() / "__version__"
if version_file.exists(): if version_file.exists():
if text := version_file.open(encoding="utf-8").read(): if text := version_file.open().read():
return text.replace("__version__: ", "").strip() return text.replace("__version__: ", "").strip()
return "unknown" return "unknown"
@@ -1,5 +1,3 @@
from datetime import datetime
from fastapi import APIRouter from fastapi import APIRouter
import nonebot import nonebot
from nonebot import on_message from nonebot import on_message
@@ -51,14 +49,13 @@ async def message_handle(
message: UniMsg, message: UniMsg,
group_id: str | None, group_id: str | None,
): ):
time = str(datetime.now().replace(microsecond=0))
messages = [] messages = []
for m in message: for m in message:
if isinstance(m, Text | str): if isinstance(m, Text | str):
messages.append(MessageItem(type="text", msg=str(m), time=time)) messages.append(MessageItem(type="text", msg=str(m)))
elif isinstance(m, Image): elif isinstance(m, Image):
if m.url: if m.url:
messages.append(MessageItem(type="img", msg=m.url, time=time)) messages.append(MessageItem(type="img", msg=m.url))
elif isinstance(m, At): elif isinstance(m, At):
if group_id: if group_id:
if m.target == "0": if m.target == "0":
@@ -75,9 +72,9 @@ async def message_handle(
uname = group_user.user_name uname = group_user.user_name
if m.target not in ID2NAME[group_id]: if m.target not in ID2NAME[group_id]:
ID2NAME[group_id][m.target] = uname ID2NAME[group_id][m.target] = uname
messages.append(MessageItem(type="at", msg=f"@{uname}", time=time)) messages.append(MessageItem(type="at", msg=f"@{uname}"))
elif isinstance(m, Hyper): elif isinstance(m, Hyper):
messages.append(MessageItem(type="text", msg="[分享消息]", time=time)) messages.append(MessageItem(type="text", msg="[分享消息]"))
return messages return messages
@@ -237,8 +237,6 @@ class MessageItem(BaseModel):
"""消息类型""" """消息类型"""
msg: str msg: str
"""内容""" """内容"""
time: str
"""发送日期"""
class Message(BaseModel): class Message(BaseModel):
@@ -6,16 +6,13 @@ from zhenxun.services.log import logger
from zhenxun.utils.enum import BlockType, PluginType from zhenxun.utils.enum import BlockType, PluginType
from ....base_model import Result from ....base_model import Result
from ....utils import authentication, clear_help_image from ....utils import authentication
from .data_source import ApiDataSource from .data_source import ApiDataSource
from .model import ( from .model import (
BatchUpdatePlugins,
BatchUpdateResult,
PluginCount, PluginCount,
PluginDetail, PluginDetail,
PluginInfo, PluginInfo,
PluginSwitch, PluginSwitch,
RenameMenuTypePayload,
UpdatePlugin, UpdatePlugin,
) )
@@ -33,8 +30,9 @@ async def _(
plugin_type: list[PluginType] = Query(None), menu_type: str | None = None plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
) -> Result[list[PluginInfo]]: ) -> Result[list[PluginInfo]]:
try: try:
result = await ApiDataSource.get_plugin_list(plugin_type, menu_type) return Result.ok(
return Result.ok(result, "拿到信息啦!") await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!"
)
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e)
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}") return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
@@ -80,7 +78,6 @@ async def _() -> Result[PluginCount]:
async def _(param: UpdatePlugin) -> Result: async def _(param: UpdatePlugin) -> Result:
try: try:
await ApiDataSource.update_plugin(param) await ApiDataSource.update_plugin(param)
clear_help_image()
return Result.ok(info="已经帮你写好啦!") return Result.ok(info="已经帮你写好啦!")
except (ValueError, KeyError): except (ValueError, KeyError):
return Result.fail("插件数据不存在...") return Result.fail("插件数据不存在...")
@@ -108,7 +105,6 @@ async def _(param: PluginSwitch) -> Result:
db_plugin.block_type = None db_plugin.block_type = None
db_plugin.status = True db_plugin.status = True
await db_plugin.save() await db_plugin.save()
clear_help_image()
return Result.ok(info="成功改变了开关状态!") return Result.ok(info="成功改变了开关状态!")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e)
@@ -148,68 +144,11 @@ async def _() -> Result[list[str]]:
) )
async def _(module: str) -> Result[PluginDetail]: async def _(module: str) -> Result[PluginDetail]:
try: try:
detail = await ApiDataSource.get_plugin_detail(module) return Result.ok(
return Result.ok(detail, "已经帮你写好啦!") await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!"
)
except (ValueError, KeyError): except (ValueError, KeyError):
return Result.fail("插件数据不存在...") return Result.fail("插件数据不存在...")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e)
return Result.fail(f"{type(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"],
)
clear_help_image()
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"):
clear_help_image()
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__}")
@@ -2,20 +2,13 @@ import re
import cattrs import cattrs
from fastapi import Query from fastapi import Query
from tortoise.exceptions import DoesNotExist
from zhenxun.configs.config import Config from zhenxun.configs.config import Config
from zhenxun.configs.utils import ConfigGroup from zhenxun.configs.utils import ConfigGroup
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
from zhenxun.utils.enum import BlockType, PluginType from zhenxun.utils.enum import BlockType, PluginType
from .model import ( from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin
BatchUpdatePlugins,
PluginConfig,
PluginDetail,
PluginInfo,
UpdatePlugin,
)
class ApiDataSource: class ApiDataSource:
@@ -51,11 +44,6 @@ class ApiDataSource:
level=plugin.level, level=plugin.level,
status=plugin.status, status=plugin.status,
author=plugin.author, 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) plugin_list.append(plugin_info)
return plugin_list return plugin_list
@@ -81,6 +69,7 @@ class ApiDataSource:
db_plugin.block_type = param.block_type db_plugin.block_type = param.block_type
db_plugin.status = param.block_type != BlockType.ALL db_plugin.status = param.block_type != BlockType.ALL
await db_plugin.save() await db_plugin.save()
# 配置项
if param.configs and (configs := Config.get(param.module)): if param.configs and (configs := Config.get(param.module)):
for key in param.configs: for key in param.configs:
if c := configs.configs.get(key): if c := configs.configs.get(key):
@@ -91,87 +80,6 @@ class ApiDataSource:
Config.save(save_simple_data=True) Config.save(save_simple_data=True)
return db_plugin 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 @classmethod
def __build_plugin_config( def __build_plugin_config(
cls, module: str, cfg: str, config: ConfigGroup cls, module: str, cfg: str, config: ConfigGroup
@@ -207,41 +115,6 @@ class ApiDataSource:
type_inner=type_inner, # type: ignore 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 @classmethod
async def get_plugin_detail(cls, module: str) -> PluginDetail: async def get_plugin_detail(cls, module: str) -> PluginDetail:
"""获取插件详情 """获取插件详情
@@ -1,6 +1,6 @@
from typing import Any from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel
from zhenxun.utils.enum import BlockType from zhenxun.utils.enum import BlockType
@@ -37,19 +37,19 @@ class UpdatePlugin(BaseModel):
module: str module: str
"""模块""" """模块"""
default_status: bool default_status: bool
"""是否默认开启""" """默认开关"""
limit_superuser: bool limit_superuser: bool
"""是否限制超级用户""" """限制超级用户"""
level: int
"""等级"""
cost_gold: int cost_gold: int
"""花费金币""" """金币花费"""
menu_type: str menu_type: str
"""菜单类型""" """插件菜单类型"""
level: int
"""插件所需群权限"""
block_type: BlockType | None = None block_type: BlockType | None = None
"""禁用类型""" """禁用类型"""
configs: dict[str, Any] | None = None configs: dict[str, Any] | None = None
"""设置项""" """配置项"""
class PluginInfo(BaseModel): class PluginInfo(BaseModel):
@@ -58,33 +58,27 @@ class PluginInfo(BaseModel):
""" """
module: str module: str
"""模块"""
plugin_name: str
"""插件名称""" """插件名称"""
plugin_name: str
"""插件中文名称"""
default_status: bool default_status: bool
"""是否默认开启""" """默认开关"""
limit_superuser: bool limit_superuser: bool
"""是否限制超级用户""" """限制超级用户"""
level: int
"""等级"""
cost_gold: int cost_gold: int
"""花费金币""" """花费金币"""
menu_type: str menu_type: str
"""菜单类型""" """插件菜单类型"""
version: str version: str
"""版本""" """插件版本"""
level: int
"""群权限"""
status: bool status: bool
"""状态""" """当前状态"""
author: str | None = None author: str | None = None
"""作者""" """作者"""
block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)") block_type: BlockType | None = None
"""禁用状态""" """禁用类型"""
is_builtin: bool = False
"""是否为内置插件"""
allow_switch: bool = True
"""是否允许开关"""
allow_setting: bool = True
"""是否允许设置"""
class PluginConfig(BaseModel): class PluginConfig(BaseModel):
@@ -92,13 +86,20 @@ class PluginConfig(BaseModel):
插件配置项 插件配置项
""" """
module: str = Field(..., description="模块名") module: str
key: str = Field(..., description="键") """模块"""
value: Any = Field(None, description="值") key: str
help: str | None = Field(None, description="帮助信息") """键"""
default_value: Any = Field(None, description="默认值") value: Any
type: str | None = Field(None, description="类型") """值"""
type_inner: list[str] | None = Field(None, description="内部类型") help: str | None = None
"""帮助"""
default_value: Any
"""默认值"""
type: Any = None
"""值类型"""
type_inner: list[str] | None = None
"""List Tuple等内部类型检验"""
class PluginCount(BaseModel): class PluginCount(BaseModel):
@@ -116,21 +117,6 @@ 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): class PluginDetail(PluginInfo):
""" """
插件详情 插件详情
@@ -139,26 +125,6 @@ class PluginDetail(PluginInfo):
config_list: list[PluginConfig] config_list: list[PluginConfig]
class RenameMenuTypePayload(BaseModel):
old_name: str = Field(..., description="旧菜单类型名称")
new_name: str = Field(..., description="新菜单类型名称")
class PluginIr(BaseModel): class PluginIr(BaseModel):
id: int id: int
"""插件id""" """插件id"""
class BatchUpdateResult(BaseModel):
"""
批量更新插件结果
"""
success: bool = Field(..., description="是否全部成功")
"""是否全部成功"""
updated_count: int = Field(..., description="更新成功的数量")
"""更新成功的数量"""
errors: list[dict[str, str]] = Field(
default_factory=list, description="错误信息列表"
)
"""错误信息列表"""
@@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse
from zhenxun.utils._build_image import BuildImage from zhenxun.utils._build_image import BuildImage
from ....base_model import Result, SystemFolderSize from ....base_model import Result, SystemFolderSize
from ....utils import authentication, get_system_disk, validate_path from ....utils import authentication, get_system_disk
from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile
router = APIRouter(prefix="/system") router = APIRouter(prefix="/system")
@@ -25,29 +25,20 @@ IMAGE_TYPE = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"]
description="获取文件列表", description="获取文件列表",
) )
async def _(path: str | None = None) -> Result[list[DirFile]]: async def _(path: str | None = None) -> Result[list[DirFile]]:
try: base_path = Path(path) if path else Path()
base_path, error = validate_path(path) data_list = []
if error: for file in os.listdir(base_path):
return Result.fail(error) file_path = base_path / file
if not base_path: is_image = any(file.endswith(f".{t}") for t in IMAGE_TYPE)
return Result.fail("无效的路径") data_list.append(
data_list = [] DirFile(
for file in os.listdir(base_path): is_file=not file_path.is_dir(),
file_path = base_path / file is_image=is_image,
is_image = any(file.endswith(f".{t}") for t in IMAGE_TYPE) name=file,
data_list.append( parent=path,
DirFile(
is_file=not file_path.is_dir(),
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) )
except Exception as e: return Result.ok(data_list)
return Result.fail(f"获取文件列表失败: {e!s}")
@router.get( @router.get(
@@ -69,12 +60,8 @@ async def _(full_path: str | None = None) -> Result[list[SystemFolderSize]]:
description="删除文件", description="删除文件",
) )
async def _(param: DeleteFile) -> Result: async def _(param: DeleteFile) -> Result:
path, error = validate_path(param.full_path) path = Path(param.full_path)
if error: if not path or not path.exists():
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
path.unlink() path.unlink()
@@ -91,12 +78,8 @@ async def _(param: DeleteFile) -> Result:
description="删除文件夹", description="删除文件夹",
) )
async def _(param: DeleteFile) -> Result: async def _(param: DeleteFile) -> Result:
path, error = validate_path(param.full_path) path = Path(param.full_path)
if error: if not path or not path.exists() or path.is_file():
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists() or path.is_file():
return Result.warning_("文件夹不存在...") return Result.warning_("文件夹不存在...")
try: try:
shutil.rmtree(path.absolute()) shutil.rmtree(path.absolute())
@@ -113,14 +96,10 @@ async def _(param: DeleteFile) -> Result:
description="重命名文件", description="重命名文件",
) )
async def _(param: RenameFile) -> Result: async def _(param: RenameFile) -> Result:
parent_path, error = validate_path(param.parent) path = (
if error: (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name)
return Result.fail(error) )
if not parent_path: if not path or not path.exists():
return Result.fail("无效的路径")
path = (parent_path / param.old_name) if param.parent else Path(param.old_name)
if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
path.rename(path.parent / param.name) path.rename(path.parent / param.name)
@@ -137,14 +116,10 @@ async def _(param: RenameFile) -> Result:
description="重命名文件夹", description="重命名文件夹",
) )
async def _(param: RenameFile) -> Result: async def _(param: RenameFile) -> Result:
parent_path, error = validate_path(param.parent) path = (
if error: (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name)
return Result.fail(error) )
if not parent_path: if not path or not path.exists() or path.is_file():
return Result.fail("无效的路径")
path = (parent_path / param.old_name) if param.parent else Path(param.old_name)
if not path.exists() or path.is_file():
return Result.warning_("文件夹不存在...") return Result.warning_("文件夹不存在...")
try: try:
new_path = path.parent / param.name new_path = path.parent / param.name
@@ -162,13 +137,7 @@ async def _(param: RenameFile) -> Result:
description="新建文件", description="新建文件",
) )
async def _(param: AddFile) -> Result: async def _(param: AddFile) -> Result:
parent_path, error = validate_path(param.parent) path = (Path(param.parent) / param.name) if param.parent else Path(param.name)
if error:
return Result.fail(error)
if not parent_path:
return Result.fail("无效的路径")
path = (parent_path / param.name) if param.parent else Path(param.name)
if path.exists(): if path.exists():
return Result.warning_("文件已存在...") return Result.warning_("文件已存在...")
try: try:
@@ -186,13 +155,7 @@ async def _(param: AddFile) -> Result:
description="新建文件夹", description="新建文件夹",
) )
async def _(param: AddFile) -> Result: async def _(param: AddFile) -> Result:
parent_path, error = validate_path(param.parent) path = (Path(param.parent) / param.name) if param.parent else Path(param.name)
if error:
return Result.fail(error)
if not parent_path:
return Result.fail("无效的路径")
path = (parent_path / param.name) if param.parent else Path(param.name)
if path.exists(): if path.exists():
return Result.warning_("文件夹已存在...") return Result.warning_("文件夹已存在...")
try: try:
@@ -210,11 +173,7 @@ async def _(param: AddFile) -> Result:
description="读取文件", description="读取文件",
) )
async def _(full_path: str) -> Result: async def _(full_path: str) -> Result:
path, error = validate_path(full_path) path = Path(full_path)
if error:
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists(): if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
@@ -232,13 +191,9 @@ async def _(full_path: str) -> Result:
description="读取文件", description="读取文件",
) )
async def _(param: SaveFile) -> Result[str]: async def _(param: SaveFile) -> Result[str]:
path, error = validate_path(param.full_path) path = Path(param.full_path)
if error:
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
try: try:
async with aiofiles.open(str(path), "w", encoding="utf-8") as f: async with aiofiles.open(path, "w", encoding="utf-8") as f:
await f.write(param.content) await f.write(param.content)
return Result.ok("更新成功!") return Result.ok("更新成功!")
except Exception as e: except Exception as e:
@@ -253,24 +208,10 @@ async def _(param: SaveFile) -> Result[str]:
description="读取图片base64", description="读取图片base64",
) )
async def _(full_path: str) -> Result[str]: async def _(full_path: str) -> Result[str]:
path, error = validate_path(full_path) path = Path(full_path)
if error:
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists(): if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
return Result.ok(BuildImage.open(path).pic2bs4()) return Result.ok(BuildImage.open(path).pic2bs4())
except Exception as e: except Exception as e:
return Result.warning_(f"获取图片失败: {e!s}") 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")
@@ -14,10 +14,6 @@ class DirFile(BaseModel):
"""文件夹或文件名称""" """文件夹或文件名称"""
parent: str | None = None parent: str | None = None
"""父级""" """父级"""
size: int | None = None
"""文件大小"""
mtime: float | None = None
"""修改时间"""
class DeleteFile(BaseModel): class DeleteFile(BaseModel):
+1 -7
View File
@@ -1,12 +1,6 @@
import sys
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import nonebot import nonebot
from strenum import StrEnum
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
from zhenxun.configs.path_config import DATA_PATH, TEMP_PATH from zhenxun.configs.path_config import DATA_PATH, TEMP_PATH
@@ -18,7 +18,6 @@ async def update_webui_assets():
download_url = await GithubUtils.parse_github_url( download_url = await GithubUtils.parse_github_url(
WEBUI_DIST_GITHUB_URL WEBUI_DIST_GITHUB_URL
).get_archive_download_urls() ).get_archive_download_urls()
logger.info("开始下载 webui_assets 资源...", COMMAND_NAME)
if await AsyncHttpx.download_file( if await AsyncHttpx.download_file(
download_url, webui_assets_path, follow_redirects=True download_url, webui_assets_path, follow_redirects=True
): ):
+1 -57
View File
@@ -2,7 +2,6 @@ import contextlib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import os import os
from pathlib import Path from pathlib import Path
import re
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
@@ -12,7 +11,7 @@ import psutil
import ujson as json import ujson as json
from zhenxun.configs.config import Config from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH from zhenxun.configs.path_config import DATA_PATH
from .base_model import SystemFolderSize, SystemStatus, User from .base_model import SystemFolderSize, SystemStatus, User
@@ -29,61 +28,6 @@ if token_file.exists():
token_data = json.load(open(token_file, encoding="utf8")) token_data = json.load(open(token_file, encoding="utf8"))
def validate_path(path_str: str | None) -> tuple[Path | None, str | None]:
"""验证路径是否安全
参数:
path_str: 用户输入的路径
返回:
tuple[Path | None, str | None]: (验证后的路径, 错误信息)
"""
try:
if not path_str:
return Path().resolve(), None
# 1. 移除任何可能的路径遍历尝试
path_str = re.sub(r"[\\/]\.\.[\\/]", "", path_str)
# 2. 规范化路径并转换为绝对路径
path = Path(path_str).resolve()
# 3. 获取项目根目录
root_dir = Path().resolve()
# 4. 验证路径是否在项目根目录内
try:
if not path.is_relative_to(root_dir):
return None, "访问路径超出允许范围"
except ValueError:
return None, "无效的路径格式"
# 5. 验证路径是否包含任何危险字符
if any(c in str(path) for c in ["..", "~", "*", "?", ">", "<", "|", '"']):
return None, "路径包含非法字符"
# 6. 验证路径长度是否合理
return (None, "路径长度超出限制") if len(str(path)) > 4096 else (path, None)
except Exception as e:
return None, f"路径验证失败: {e!s}"
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: def get_user(uname: str) -> User | None:
"""获取账号密码 """获取账号密码
-29
View File
@@ -1,29 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.utils.enum import BotSentType
class BotMessageStore(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
bot_id = fields.CharField(255, null=True)
"""bot id"""
user_id = fields.CharField(255, null=True)
"""目标id"""
group_id = fields.CharField(255, null=True)
"""群组id"""
sent_type = fields.CharEnumField(BotSentType)
"""类型"""
text = fields.TextField(null=True)
"""文本内容"""
plain_text = fields.TextField(null=True)
"""纯文本"""
platform = fields.CharField(255, null=True)
"""平台"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "bot_message_store"
table_description = "Bot发送消息列表"
-21
View File
@@ -1,21 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.utils.enum import EventLogType
class EventLog(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255, description="用户id")
"""用户id"""
group_id = fields.CharField(255, description="群组id")
"""群组id"""
event_type = fields.CharEnumField(EventLogType, default=None, description="类型")
"""类型"""
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间")
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "event_log"
table_description = "各种请求通知记录表"
+5 -24
View File
@@ -3,10 +3,8 @@ from typing_extensions import Self
from nonebot.adapters import Bot from nonebot.adapters import Bot
from tortoise import fields from tortoise import fields
from zhenxun.configs.config import BotConfig
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.utils.common_utils import SqlUtils
from zhenxun.utils.enum import RequestHandleType, RequestType from zhenxun.utils.enum import RequestHandleType, RequestType
from zhenxun.utils.exception import NotFoundError from zhenxun.utils.exception import NotFoundError
@@ -36,8 +34,6 @@ class FgRequest(Model):
RequestHandleType, null=True, description="处理类型" RequestHandleType, null=True, description="处理类型"
) )
"""处理类型""" """处理类型"""
message_ids = fields.CharField(max_length=255, null=True, description="消息id列表")
"""消息id列表"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride] class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "fg_request" table = "fg_request"
@@ -127,24 +123,9 @@ class FgRequest(Model):
await GroupConsole.update_or_create( await GroupConsole.update_or_create(
group_id=req.group_id, defaults={"group_flag": 1} group_id=req.group_id, defaults={"group_flag": 1}
) )
if req.flag == "0": await bot.set_group_add_request(
# 用户手动申请入群,创建群认证后提醒用户拉群 flag=req.flag,
await bot.send_private_msg( sub_type="invite",
user_id=req.user_id, approve=handle_type == RequestHandleType.APPROVE,
message=f"已同意你对{BotConfig.self_nickname}的申请群组:" )
f"{req.group_id},可以直接手动拉入群组,{BotConfig.self_nickname}会自动同意。",
)
else:
# 正常同意群组请求
await bot.set_group_add_request(
flag=req.flag,
sub_type="invite",
approve=handle_type == RequestHandleType.APPROVE,
)
return req return req
@classmethod
async def _run_script(cls):
return [
SqlUtils.add_column("fg_request", "message_ids", "character varying(255)")
]
+2 -2
View File
@@ -41,9 +41,9 @@ def convert_module_format(data: str | list[str]) -> str | list[str]:
str | list[str]: 根据输入类型返回转换后的数据。 str | list[str]: 根据输入类型返回转换后的数据。
""" """
if isinstance(data, str): if isinstance(data, str):
return [item.strip(",") for item in data.split("<") if item.strip()] return [item.strip(",") for item in data.split("<") if item]
else: else:
return "".join(add_disable_marker(item) for item in data) return "".join(format(item) for item in data)
class GroupConsole(Model): class GroupConsole(Model):
-123
View File
@@ -1,123 +0,0 @@
from datetime import datetime
from typing_extensions import Self
from tortoise import fields
from zhenxun.services.db_context import Model
from .mahiro_bank_log import BankHandleType, MahiroBankLog
class MahiroBank(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255, description="用户id")
"""用户id"""
amount = fields.BigIntField(default=0, description="存款")
"""用户存款"""
rate = fields.FloatField(default=0.0005, description="小时利率")
"""小时利率"""
loan_amount = fields.BigIntField(default=0, description="贷款")
"""用户贷款"""
loan_rate = fields.FloatField(default=0.0005, description="贷款利率")
"""贷款利率"""
update_time = fields.DatetimeField(auto_now=True)
"""修改时间"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "mahiro_bank"
table_description = "小真寻银行"
@classmethod
async def deposit(cls, user_id: str, amount: int, rate: float) -> Self:
"""存款
参数:
user_id: 用户id
amount: 金币数量
rate: 小时利率
返回:
Self: MahiroBank
"""
effective_hour = int(24 - datetime.now().hour)
user, _ = await cls.get_or_create(user_id=user_id)
user.amount += amount
await user.save(update_fields=["amount", "rate"])
await MahiroBankLog.create(
user_id=user_id,
amount=amount,
rate=rate,
effective_hour=effective_hour,
handle_type=BankHandleType.DEPOSIT,
)
return user
@classmethod
async def withdraw(cls, user_id: str, amount: int) -> Self:
"""取款
参数:
user_id: 用户id
amount: 金币数量
返回:
Self: MahiroBank
"""
if amount <= 0:
raise ValueError("取款金额必须大于0")
user, _ = await cls.get_or_create(user_id=user_id)
if user.amount < amount:
raise ValueError("取款金额不能大于存款金额")
user.amount -= amount
await user.save(update_fields=["amount"])
await MahiroBankLog.create(
user_id=user_id, amount=amount, handle_type=BankHandleType.WITHDRAW
)
return user
@classmethod
async def loan(cls, user_id: str, amount: int, rate: float) -> Self:
"""贷款
参数:
user_id: 用户id
amount: 贷款金额
rate: 贷款利率
返回:
Self: MahiroBank
"""
user, _ = await cls.get_or_create(user_id=user_id)
user.loan_amount += amount
user.loan_rate = rate
await user.save(update_fields=["loan_amount", "loan_rate"])
await MahiroBankLog.create(
user_id=user_id, amount=amount, rate=rate, handle_type=BankHandleType.LOAN
)
return user
@classmethod
async def repayment(cls, user_id: str, amount: int) -> Self:
"""还款
参数:
user_id: 用户id
amount: 还款金额
返回:
Self: MahiroBank
"""
if amount <= 0:
raise ValueError("还款金额必须大于0")
user, _ = await cls.get_or_create(user_id=user_id)
if user.loan_amount < amount:
raise ValueError("还款金额不能大于贷款金额")
user.loan_amount -= amount
await user.save(update_fields=["loan_amount"])
await MahiroBankLog.create(
user_id=user_id, amount=amount, handle_type=BankHandleType.REPAYMENT
)
return user
-31
View File
@@ -1,31 +0,0 @@
from tortoise import fields
from zhenxun.services.db_context import Model
from zhenxun.utils.enum import BankHandleType
class MahiroBankLog(Model):
id = fields.IntField(pk=True, generated=True, auto_increment=True)
"""自增id"""
user_id = fields.CharField(255, description="用户id")
"""用户id"""
amount = fields.BigIntField(default=0, description="存款")
"""金币数量"""
rate = fields.FloatField(default=0, description="小时利率")
"""小时利率"""
handle_type = fields.CharEnumField(
BankHandleType, null=True, description="处理类型"
)
"""处理类型"""
is_completed = fields.BooleanField(default=False, description="是否完成")
"""是否完成"""
effective_hour = fields.IntField(default=0, description="有效小时")
"""有效小时"""
update_time = fields.DatetimeField(auto_now=True)
"""修改时间"""
create_time = fields.DatetimeField(auto_now_add=True)
"""创建时间"""
class Meta: # pyright: ignore [reportIncompatibleVariableOverride]
table = "mahiro_bank_log"
table_description = "小真寻银行日志"
+2 -16
View File
@@ -60,41 +60,27 @@ class PluginInfo(Model):
table_description = "插件基本信息" table_description = "插件基本信息"
@classmethod @classmethod
async def get_plugin( async def get_plugin(cls, load_status: bool = True, **kwargs) -> Self | None:
cls, load_status: bool = True, filter_parent: bool = True, **kwargs
) -> Self | None:
"""获取插件列表 """获取插件列表
参数: 参数:
load_status: 加载状态. load_status: 加载状态.
filter_parent: 过滤父组件
返回: 返回:
Self | None: 插件 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) return await cls.get_or_none(load_status=load_status, **kwargs)
@classmethod @classmethod
async def get_plugins( async def get_plugins(cls, load_status: bool = True, **kwargs) -> list[Self]:
cls, load_status: bool = True, filter_parent: bool = True, **kwargs
) -> list[Self]:
"""获取插件列表 """获取插件列表
参数: 参数:
load_status: 加载状态. load_status: 加载状态.
filter_parent: 过滤父组件
返回: 返回:
list[Self]: 插件列表 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() return await cls.filter(load_status=load_status, **kwargs).all()
@classmethod @classmethod
+2 -18
View File
@@ -1,12 +1,9 @@
import nonebot
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from tortoise import Tortoise from tortoise import Tortoise
from tortoise.connection import connections from tortoise.connection import connections
from tortoise.models import Model as Model_ from tortoise.models import Model as Model_
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
from zhenxun.utils.exception import HookPriorityException
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .log import logger from .log import logger
@@ -14,9 +11,6 @@ SCRIPT_METHOD = []
MODELS: list[str] = [] MODELS: list[str] = []
driver = nonebot.get_driver()
class Model(Model_): class Model(Model_):
""" """
自动添加模块 自动添加模块
@@ -32,7 +26,7 @@ class Model(Model_):
SCRIPT_METHOD.append((cls.__module__, func)) SCRIPT_METHOD.append((cls.__module__, func))
class DbUrlIsNode(HookPriorityException): class DbUrlIsNode(Exception):
""" """
数据库链接地址为空 数据库链接地址为空
""" """
@@ -48,19 +42,9 @@ class DbConnectError(Exception):
pass pass
@PriorityLifecycle.on_startup(priority=1)
async def init(): async def init():
if not BotConfig.db_url: if not BotConfig.db_url:
# raise DbUrlIsNode("数据库配置为空,请在.env.dev中配置DB_URL...") raise DbUrlIsNode("数据库配置为空,请在.env.dev中配置DB_URL...")
error = f"""
**********************************************************************
🌟 **************************** 配置为空 ************************* 🌟
🚀 请打开 WebUi 进行基础配置 🚀
🌐 配置地址:http://{driver.config.host}:{driver.config.port}/#/configure 🌐
***********************************************************************
***********************************************************************
"""
raise DbUrlIsNode("\n" + error.strip())
try: try:
await Tortoise.init( await Tortoise.init(
db_url=BotConfig.db_url, db_url=BotConfig.db_url,
+148 -184
View File
@@ -1,4 +1,4 @@
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any, overload from typing import Any, overload
import nonebot import nonebot
@@ -17,7 +17,7 @@ driver = nonebot.get_driver()
log_level = driver.config.log_level or "INFO" log_level = driver.config.log_level or "INFO"
logger_.add( logger_.add(
LOG_PATH / "{time:YYYY-MM-DD}.log", LOG_PATH / f"{datetime.now().date()}.log",
level=log_level, level=log_level,
rotation="00:00", rotation="00:00",
format=default_format, format=default_format,
@@ -26,7 +26,7 @@ logger_.add(
) )
logger_.add( logger_.add(
LOG_PATH / "error_{time:YYYY-MM-DD}.log", LOG_PATH / f"error_{datetime.now().date()}.log",
level="ERROR", level="ERROR",
rotation="00:00", rotation="00:00",
format=default_format, format=default_format,
@@ -36,92 +36,26 @@ logger_.add(
class logger: class logger:
""" TEMPLATE_A = "Adapter[{}] {}"
一个经过优化的、支持多种上下文和格式的日志记录器。 TEMPLATE_B = "Adapter[{}] [<u><c>{}</c></u>]: {}"
""" TEMPLATE_C = "Adapter[{}] 用户[<u><e>{}</e></u>] 触发 [<u><c>{}</c></u>]: {}"
TEMPLATE_D = "Adapter[{}] 群聊[<u><e>{}</e></u>] 用户[<u><e>{}</e></u>] 触发"
" [<u><c>{}</c></u>]: {}"
TEMPLATE_E = "Adapter[{}] 群聊[<u><e>{}</e></u>] 用户[<u><e>{}</e></u>] 触发"
" [<u><c>{}</c></u>] [Target](<u><e>{}</e></u>): {}"
TEMPLATE_ADAPTER = "Adapter[<m>{}</m>] "
TEMPLATE_USER = "用户[<u><e>{}</e></u>] "
TEMPLATE_GROUP = "群聊[<u><e>{}</e></u>] "
TEMPLATE_COMMAND = "CMD[<u><c>{}</c></u>] "
TEMPLATE_PLATFORM = "平台[<u><m>{}</m></u>] "
TEMPLATE_TARGET = "[Target]([<u><e>{}</e></u>]) "
TEMPLATE_ADAPTER = "Adapter[<m>{}</m>]"
TEMPLATE_USER = "用户[<u><e>{}</e></u>]"
TEMPLATE_GROUP = "群聊[<u><e>{}</e></u>]"
TEMPLATE_COMMAND = "CMD[<u><c>{}</c></u>]"
TEMPLATE_PLATFORM = "平台[<u><m>{}</m></u>]"
TEMPLATE_TARGET = "[Target]([<u><e>{}</e></u>])"
SUCCESS_TEMPLATE = "[<u><c>{}</c></u>]: {} | 参数[{}] 返回: [<y>{}</y>]" SUCCESS_TEMPLATE = "[<u><c>{}</c></u>]: {} | 参数[{}] 返回: [<y>{}</y>]"
@classmethod WARNING_TEMPLATE = "[<u><y>{}</y></u>]: {}"
def __parser_template(
cls,
info: str,
command: str | None = None,
user_id: int | str | None = None,
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
platform: str | None = None,
) -> str:
"""
优化后的模板解析器,构建并连接日志信息片段。
"""
parts = []
if adapter:
parts.append(cls.TEMPLATE_ADAPTER.format(adapter))
if platform:
parts.append(cls.TEMPLATE_PLATFORM.format(platform))
if group_id:
parts.append(cls.TEMPLATE_GROUP.format(group_id))
if user_id:
parts.append(cls.TEMPLATE_USER.format(user_id))
if command:
parts.append(cls.TEMPLATE_COMMAND.format(command))
if target:
parts.append(cls.TEMPLATE_TARGET.format(target))
parts.append(info) ERROR_TEMPLATE = "[<u><r>{}</r></u>]: {}"
return " ".join(parts)
@classmethod
def _log(
cls,
level: str,
info: str,
command: str | None = None,
session: int | str | Session | uninfoSession | None = None,
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
platform: str | None = None,
e: Exception | None = None,
):
"""
核心日志处理方法,处理所有日志级别的通用逻辑。
"""
user_id: str | None = str(session) if isinstance(session, int | str) else None
if isinstance(session, Session):
user_id = session.id1
adapter = session.bot_type
group_id = f"{session.id3}:{session.id2}" if session.id3 else session.id2
platform = platform or session.platform
elif isinstance(session, uninfoSession):
user_id = session.user.id
adapter = session.adapter
if session.group:
group_id = session.group.id
platform = session.basic.get("scope")
template = cls.__parser_template(
info, command, user_id, group_id, adapter, target, platform
)
if e:
template += f" || 错误 <r>{type(e).__name__}: {e}</r>"
try:
log_func = getattr(logger_.opt(colors=True), level)
log_func(template)
except Exception:
log_func_fallback = getattr(logger_, level)
log_func_fallback(template)
@overload @overload
@classmethod @classmethod
@@ -136,6 +70,7 @@ class logger:
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def info( def info(
@@ -147,6 +82,7 @@ class logger:
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def info( def info(
@@ -171,16 +107,28 @@ class logger:
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
): ):
cls._log( user_id: str | None = session # type: ignore
"info", if isinstance(session, Session):
info=info, user_id = session.id1
command=command, adapter = session.bot_type
session=session, if session.id3:
group_id=group_id, group_id = f"{session.id3}:{session.id2}"
adapter=adapter, elif session.id2:
target=target, group_id = f"{session.id2}"
platform=platform, platform = platform or session.platform
elif isinstance(session, uninfoSession):
user_id = session.user.id
adapter = session.adapter
if session.group:
group_id = session.group.id
platform = session.basic["scope"]
template = cls.__parser_template(
info, command, user_id, group_id, adapter, target, platform
) )
try:
logger_.opt(colors=True).info(template)
except Exception:
logger_.info(template)
@classmethod @classmethod
def success( def success(
@@ -190,11 +138,9 @@ class logger:
param: dict[str, Any] | None = None, param: dict[str, Any] | None = None,
result: str = "", result: str = "",
): ):
param_str = ( param_str = ""
",".join([f"<m>{k}</m>:<g>{v}</g>" for k, v in param.items()]) if param:
if param param_str = ",".join([f"<m>{k}</m>:<g>{v}</g>" for k, v in param.items()])
else ""
)
logger_.opt(colors=True).success( logger_.opt(colors=True).success(
cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) cls.SUCCESS_TEMPLATE.format(command, info, param_str, result)
) )
@@ -213,6 +159,7 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def warning( def warning(
@@ -221,10 +168,12 @@ class logger:
command: str | None = None, command: str | None = None,
*, *,
session: Session | None = None, session: Session | None = None,
adapter: str | None = None,
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def warning( def warning(
@@ -233,6 +182,7 @@ class logger:
command: str | None = None, command: str | None = None,
*, *,
session: uninfoSession | None = None, session: uninfoSession | None = None,
adapter: str | None = None,
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
@@ -251,17 +201,30 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ):
cls._log( user_id: str | None = session # type: ignore
"warning", if isinstance(session, Session):
info=info, user_id = session.id1
command=command, adapter = session.bot_type
session=session, if session.id3:
group_id=group_id, group_id = f"{session.id3}:{session.id2}"
adapter=adapter, elif session.id2:
target=target, group_id = f"{session.id2}"
platform=platform, platform = platform or session.platform
e=e, elif isinstance(session, uninfoSession):
user_id = session.user.id
adapter = session.adapter
if session.group:
group_id = session.group.id
platform = session.basic["scope"]
template = cls.__parser_template(
info, command, user_id, group_id, adapter, target, platform
) )
if e:
template += f" || 错误<r>{type(e)}: {e}</r>"
try:
logger_.opt(colors=True).warning(template)
except Exception as e:
logger_.warning(template)
@overload @overload
@classmethod @classmethod
@@ -277,6 +240,7 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def error( def error(
@@ -289,6 +253,7 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def error( def error(
@@ -315,17 +280,30 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ):
cls._log( user_id: str | None = session # type: ignore
"error", if isinstance(session, Session):
info=info, user_id = session.id1
command=command, adapter = session.bot_type
session=session, if session.id3:
group_id=group_id, group_id = f"{session.id3}:{session.id2}"
adapter=adapter, elif session.id2:
target=target, group_id = f"{session.id2}"
platform=platform, platform = platform or session.platform
e=e, elif isinstance(session, uninfoSession):
user_id = session.user.id
adapter = session.adapter
if session.group:
group_id = session.group.id
platform = session.basic["scope"]
template = cls.__parser_template(
info, command, user_id, group_id, adapter, target, platform
) )
if e:
template += f" || 错误 <r>{type(e)}: {e}</r>"
try:
logger_.opt(colors=True).error(template)
except Exception as e:
logger_.error(template)
@overload @overload
@classmethod @classmethod
@@ -341,6 +319,7 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def debug( def debug(
@@ -353,6 +332,7 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ... ): ...
@overload @overload
@classmethod @classmethod
def debug( def debug(
@@ -379,78 +359,62 @@ class logger:
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, e: Exception | None = None,
): ):
cls._log( user_id: str | None = session # type: ignore
"debug", if isinstance(session, Session):
info=info, user_id = session.id1
command=command, adapter = session.bot_type
session=session, if session.id3:
group_id=group_id, group_id = f"{session.id3}:{session.id2}"
adapter=adapter, elif session.id2:
target=target, group_id = f"{session.id2}"
platform=platform, platform = platform or session.platform
e=e, elif isinstance(session, uninfoSession):
user_id = session.user.id
adapter = session.adapter
if session.group:
group_id = session.group.id
platform = session.basic["scope"]
template = cls.__parser_template(
info, command, user_id, group_id, adapter, target, platform
) )
if e:
template += f" || 错误 <r>{type(e)}: {e}</r>"
try:
logger_.opt(colors=True).debug(template)
except Exception as e:
logger_.debug(template)
@overload
@classmethod @classmethod
def trace( def __parser_template(
cls, cls,
info: str, info: str,
command: str | None = None, command: str | None = None,
*, user_id: int | str | None = None,
session: int | str | None = None,
group_id: int | str | None = None, group_id: int | str | None = None,
adapter: str | None = None, adapter: str | None = None,
target: Any = None, target: Any = None,
platform: str | None = None, platform: str | None = None,
e: Exception | None = None, ) -> str:
): ... arg_list = []
@overload template = ""
@classmethod if adapter is not None:
def trace( template += cls.TEMPLATE_ADAPTER
cls, arg_list.append(adapter)
info: str, if platform is not None:
command: str | None = None, template += cls.TEMPLATE_PLATFORM
*, arg_list.append(platform)
session: Session | None = None, if group_id is not None:
target: Any = None, template += cls.TEMPLATE_GROUP
platform: str | None = None, arg_list.append(group_id)
e: Exception | None = None, if user_id is not None:
): ... template += cls.TEMPLATE_USER
@overload arg_list.append(user_id)
@classmethod if command is not None:
def trace( template += cls.TEMPLATE_COMMAND
cls, arg_list.append(command)
info: str, if target is not None:
command: str | None = None, template += cls.TEMPLATE_TARGET
*, arg_list.append(target)
session: uninfoSession | None = None, arg_list.append(info)
target: Any = None, template += "{}"
platform: str | None = None, return template.format(*arg_list)
e: Exception | None = None,
): ...
@classmethod
def trace(
cls,
info: str,
command: str | None = None,
*,
session: int | str | Session | uninfoSession | None = None,
group_id: int | str | None = None,
adapter: str | None = None,
target: Any = None,
platform: str | None = None,
e: Exception | None = None,
):
cls._log(
"trace",
info=info,
command=command,
session=session,
group_id=group_id,
adapter=adapter,
target=target,
platform=platform,
e=e,
)
+1 -2
View File
@@ -6,7 +6,6 @@ from nonebot.utils import is_coroutine_callable
from pydantic import BaseModel from pydantic import BaseModel
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
driver = nonebot.get_driver() driver = nonebot.get_driver()
@@ -101,6 +100,6 @@ class PluginInitManager:
logger.error(f"执行: {module_path}:remove 失败", e=e) logger.error(f"执行: {module_path}:remove 失败", e=e)
@PriorityLifecycle.on_startup(priority=5) @driver.on_startup
async def _(): async def _():
await PluginInitManager.install_all() await PluginInitManager.install_all()
+1 -6
View File
@@ -1,17 +1,12 @@
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
import random import random
import sys
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from strenum import StrEnum
from ._build_image import BuildImage from ._build_image import BuildImage
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
class MatType(StrEnum): class MatType(StrEnum):
LINE = "LINE" LINE = "LINE"
+1 -46
View File
@@ -1,47 +1,4 @@
import sys from strenum import StrEnum
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
class PriorityLifecycleType(StrEnum):
STARTUP = "STARTUP"
"""启动"""
SHUTDOWN = "SHUTDOWN"
"""关闭"""
class BotSentType(StrEnum):
GROUP = "GROUP"
PRIVATE = "PRIVATE"
class BankHandleType(StrEnum):
DEPOSIT = "DEPOSIT"
"""存款"""
WITHDRAW = "WITHDRAW"
"""取款"""
LOAN = "LOAN"
"""贷款"""
REPAYMENT = "REPAYMENT"
"""还款"""
INTEREST = "INTEREST"
"""利息"""
class EventLogType(StrEnum):
GROUP_MEMBER_INCREASE = "GROUP_MEMBER_INCREASE"
"""群成员增加"""
GROUP_MEMBER_DECREASE = "GROUP_MEMBER_DECREASE"
"""群成员减少"""
KICK_MEMBER = "KICK_MEMBER"
"""踢出群成员"""
KICK_BOT = "KICK_BOT"
"""踢出Bot"""
LEAVE_MEMBER = "LEAVE_MEMBER"
"""主动退群"""
class GoldHandle(StrEnum): class GoldHandle(StrEnum):
@@ -135,9 +92,7 @@ class RequestType(StrEnum):
""" """
FRIEND = "FRIEND" FRIEND = "FRIEND"
"""好友"""
GROUP = "GROUP" GROUP = "GROUP"
"""群组"""
class RequestHandleType(StrEnum): class RequestHandleType(StrEnum):
-12
View File
@@ -1,15 +1,3 @@
class HookPriorityException(BaseException):
"""
钩子优先级异常
"""
def __init__(self, info: str = "") -> None:
self.info = info
def __str__(self) -> str:
return self.info
class NotFoundError(Exception): class NotFoundError(Exception):
""" """
未发现 未发现

Some files were not shown because too many files have changed in this diff Show More