Merge branch 'main' into feature/db-cache
58
.github/workflows/publish-docker.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
name: Create and publish a Docker image
|
||||
|
||||
# Configures this workflow to run on demand via workflow_dispatch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
#
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v2
|
||||
with:
|
||||
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
13
.gitignore
vendored
@ -139,22 +139,9 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
demo.py
|
||||
test.py
|
||||
server_ip.py
|
||||
member_activity_handle.py
|
||||
Yu-Gi-Oh/
|
||||
csgo/
|
||||
fantasy_card/
|
||||
data/
|
||||
log/
|
||||
backup/
|
||||
extensive_plugin/
|
||||
test/
|
||||
bot.py
|
||||
.idea/
|
||||
resources/
|
||||
/configs/config.py
|
||||
configs/config.yaml
|
||||
.vscode/launch.json
|
||||
plugins_/
|
||||
2
.vscode/settings.json
vendored
@ -11,6 +11,8 @@
|
||||
"displayname",
|
||||
"flmt",
|
||||
"getbbox",
|
||||
"gitcode",
|
||||
"GITEE",
|
||||
"hibiapi",
|
||||
"httpx",
|
||||
"jsdelivr",
|
||||
|
||||
118
README.md
@ -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_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
|
||||
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
|
||||
| [WebUi](https://github.com/HibiKier/zhenxun_bot_webui) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
|
||||
| [WebUi](https://github.com/zhenxun-org/zhenxun_bot) | 管理 | [hibikier](https://github.com/HibiKier) | 基于真寻 WebApi 的 webui 实现 [预览](#-webui界面展示) |
|
||||
| [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
|
||||
|
||||
</div>
|
||||
@ -121,11 +121,33 @@ AccessToken: PUBLIC_ZHENXUN_TEST
|
||||
|
||||
- 实现了许多功能,且提供了大量功能管理命令,进行了多平台适配,兼容 nb2 商店插件
|
||||
- 拥有完善可用的 webui
|
||||
- 通过 Config 配置项将所有插件配置统计保存至 config.yaml,利于统一用户修改
|
||||
- 通过 Config 配置项将所有插件配置统一保存至 config.yaml,利于统一用户修改
|
||||
- 方便增删插件,原生 nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息`
|
||||
- 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等
|
||||
- **更多详细请通过 [传送门](https://zhenxun-org.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>
|
||||
|
||||
## 🛠️ 简单部署
|
||||
|
||||
```bash
|
||||
@ -150,7 +172,7 @@ poetry run python bot.py
|
||||
|
||||
1.在 .env.dev 文件中填写你的机器人配置项
|
||||
|
||||
2.在 configs/config.yaml 文件中修改你需要修改的插件配置项
|
||||
2.在 data/config.yaml 文件中修改你需要修改的插件配置项
|
||||
|
||||
<details>
|
||||
<summary>数据库地址(DB_URL)配置说明</summary>
|
||||
@ -272,12 +294,12 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
|
||||
## ❔ 需要帮助?
|
||||
|
||||
> [!TIP]
|
||||
> 发起 [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)
|
||||
> 发起 [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)
|
||||
>
|
||||
> - 善用[搜索引擎](https://www.google.com/)
|
||||
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue
|
||||
|
||||
欢迎前往 [issue](https://github.com/HibiKier/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
|
||||
欢迎前往 [issue](https://github.com/zhenxun-org/zhenxun_bot/issues/new/choose) 中提出你遇到的问题,或者加入我们的 [用户群](https://qm.qq.com/q/mRNtLSl6uc) 或 [技术群](https://qm.qq.com/q/YYYt5rkMYc)与我们联系
|
||||
|
||||
## 🛠️ 进度追踪
|
||||
|
||||
@ -287,6 +309,8 @@ Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
|
||||
|
||||
首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
|
||||
|
||||
LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
|
||||
|
||||
## 🙏 感谢
|
||||
|
||||
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
|
||||
@ -326,34 +350,68 @@ Project [zhenxun_bot](https://github.com/users/HibiKier/projects/2)
|
||||
<img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
|
||||
</a>
|
||||
|
||||
## 📸 WebUI 界面展示
|
||||
## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端)
|
||||
|
||||
<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>
|
||||
#### 登录界面
|
||||
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui04.png" alt="webui04" style="width: 100%; height: auto;">
|
||||
</div>
|
||||
<div style="width: 48%; margin-bottom: 10px;">
|
||||
<img src="./docs_image/webui05.png" alt="webui05" style="width: 100%; height: auto;">
|
||||
</div>
|
||||

|
||||
|
||||
#### API 设置
|
||||
|
||||

|
||||
|
||||
#### 仪表盘
|
||||
|
||||

|
||||
|
||||
#### 仪表盘(展开)
|
||||
|
||||

|
||||
|
||||
#### 控制台
|
||||
|
||||

|
||||
|
||||
#### 插件列表
|
||||
|
||||

|
||||
|
||||
#### 插件列表(配置项)
|
||||
|
||||

|
||||
|
||||
#### 插件商店
|
||||
|
||||

|
||||
|
||||
#### 好友/群组管理
|
||||
|
||||

|
||||
|
||||
#### 请求管理
|
||||
|
||||

|
||||
|
||||
#### 数据库管理
|
||||
|
||||

|
||||
|
||||
### 文件管理
|
||||
|
||||

|
||||
|
||||
### 文件管理(文本查看)
|
||||
|
||||

|
||||
|
||||
### 文件管理(图片查看)
|
||||
|
||||

|
||||
|
||||
### 关于
|
||||
|
||||

|
||||
|
||||
<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>
|
||||
|
||||
4
bot.py
@ -14,9 +14,9 @@ driver.register_adapter(OneBotV11Adapter)
|
||||
# driver.register_adapter(DoDoAdapter)
|
||||
# driver.register_adapter(DiscordAdapter)
|
||||
|
||||
from zhenxun.services.db_context import disconnect, init
|
||||
from zhenxun.services.db_context import disconnect
|
||||
|
||||
driver.on_startup(init)
|
||||
# driver.on_startup(init)
|
||||
driver.on_shutdown(disconnect)
|
||||
|
||||
# nonebot.load_builtin_plugins("echo")
|
||||
|
||||
1889
data/anime.json
BIN
docs_image/pc-about.jpg
Normal file
|
After Width: | Height: | Size: 388 KiB |
BIN
docs_image/pc-api.jpg
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
docs_image/pc-command.jpg
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
docs_image/pc-dashboard.jpg
Normal file
|
After Width: | Height: | Size: 708 KiB |
BIN
docs_image/pc-dashboard1.jpg
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
docs_image/pc-database.jpg
Normal file
|
After Width: | Height: | Size: 405 KiB |
BIN
docs_image/pc-login.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
docs_image/pc-manage.jpg
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
docs_image/pc-manage1.jpg
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
docs_image/pc-plugin.jpg
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
docs_image/pc-plugin1.jpg
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
docs_image/pc-store.jpg
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
docs_image/pc-system.jpg
Normal file
|
After Width: | Height: | Size: 336 KiB |
BIN
docs_image/pc-system1.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
docs_image/pc-system2.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 193 KiB |
@ -359,7 +359,7 @@ async def test_add_plugin_exist(
|
||||
|
||||
init_mocked_api(mocked_api=mocked_api)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
)
|
||||
plugin_id = 1
|
||||
|
||||
@ -57,7 +57,7 @@ async def test_search_plugin_name(
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
mock_table_page.assert_awaited_once_with(
|
||||
"插件列表",
|
||||
"商店插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
["-", "ID", "名称", "简介", "作者", "版本", "类型"],
|
||||
[
|
||||
@ -123,7 +123,7 @@ async def test_search_plugin_author(
|
||||
)
|
||||
ctx.receive_event(bot=bot, event=event)
|
||||
mock_table_page.assert_awaited_once_with(
|
||||
"插件列表",
|
||||
"商店插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
["-", "ID", "名称", "简介", "作者", "版本", "类型"],
|
||||
[
|
||||
|
||||
@ -32,7 +32,7 @@ async def test_update_all_plugin_basic_need_update(
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.0")],
|
||||
)
|
||||
|
||||
@ -87,7 +87,7 @@ async def test_update_all_plugin_basic_is_new(
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
)
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ async def test_update_plugin_basic_need_update(
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.0")],
|
||||
)
|
||||
|
||||
@ -87,7 +87,7 @@ async def test_update_plugin_basic_is_new(
|
||||
new=tmp_path / "zhenxun",
|
||||
)
|
||||
mocker.patch(
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.ShopManage.get_loaded_plugins",
|
||||
"zhenxun.builtin_plugins.plugin_store.data_source.StoreManager.get_loaded_plugins",
|
||||
return_value=[("search_image", "0.1")],
|
||||
)
|
||||
|
||||
|
||||
@ -116,6 +116,7 @@ async def app(app: App, tmp_path: Path, mocker: MockerFixture):
|
||||
await init()
|
||||
# await driver._lifespan.startup()
|
||||
os.environ["AIOCACHE_DISABLE"] = "1"
|
||||
os.environ["PYTEST_CURRENT_TEST"] = "1"
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"鸡汤": {
|
||||
[
|
||||
{
|
||||
"name": "鸡汤",
|
||||
"module": "jitang",
|
||||
"module_path": "plugins.alapi.jitang",
|
||||
"description": "喏,亲手为你煮的鸡汤",
|
||||
@ -9,7 +10,8 @@
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": false
|
||||
},
|
||||
"识图": {
|
||||
{
|
||||
"name": "识图",
|
||||
"module": "search_image",
|
||||
"module_path": "plugins.search_image",
|
||||
"description": "以图搜图,看破本源",
|
||||
@ -19,7 +21,8 @@
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true
|
||||
},
|
||||
"网易云热评": {
|
||||
{
|
||||
"name": "网易云热评",
|
||||
"module": "comments_163",
|
||||
"module_path": "plugins.alapi.comments_163",
|
||||
"description": "生了个人,我很抱歉",
|
||||
@ -29,7 +32,8 @@
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": false
|
||||
},
|
||||
"B站订阅": {
|
||||
{
|
||||
"name": "B站订阅",
|
||||
"module": "bilibili_sub",
|
||||
"module_path": "plugins.bilibili_sub",
|
||||
"description": "非常便利的B站订阅通知",
|
||||
@ -39,4 +43,4 @@
|
||||
"plugin_type": "NORMAL",
|
||||
"is_dir": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"github订阅": {
|
||||
[
|
||||
{
|
||||
"name": "github订阅",
|
||||
"module": "github_sub",
|
||||
"module_path": "github_sub",
|
||||
"description": "订阅github用户或仓库",
|
||||
@ -10,7 +11,8 @@
|
||||
"is_dir": true,
|
||||
"github_url": "https://github.com/xuanerwa/zhenxun_github_sub"
|
||||
},
|
||||
"Minecraft查服": {
|
||||
{
|
||||
"name": "Minecraft查服",
|
||||
"module": "mc_check",
|
||||
"module_path": "mc_check",
|
||||
"description": "Minecraft服务器状态查询,支持IPv6",
|
||||
@ -21,4 +23,4 @@
|
||||
"is_dir": true,
|
||||
"github_url": "https://github.com/molanp/zhenxun_check_Minecraft"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -16,6 +16,7 @@ from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.services.log import logger
|
||||
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.platform import PlatformUtils
|
||||
|
||||
@ -70,7 +71,7 @@ from public.bag_users t1
|
||||
"""
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
await ResourceManager.init_resources()
|
||||
"""签到与用户的数据迁移"""
|
||||
|
||||
@ -26,6 +26,21 @@ __plugin_meta__ = PluginMetadata(
|
||||
_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me())
|
||||
|
||||
|
||||
QQ_INFO = """
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
""".strip()
|
||||
|
||||
INFO = """
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
项目地址:https://github.com/zhenxun-org/zhenxun_bot
|
||||
文档地址:https://zhenxun-org.github.io/zhenxun_bot/
|
||||
""".strip()
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(session: Uninfo, arparma: Arparma):
|
||||
ver_file = Path() / "__version__"
|
||||
@ -35,25 +50,11 @@ async def _(session: Uninfo, arparma: Arparma):
|
||||
if text := await f.read():
|
||||
version = text.split(":")[-1].strip()
|
||||
if PlatformUtils.is_qbot(session):
|
||||
info: list[str | Path] = [
|
||||
f"""
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
""".strip()
|
||||
]
|
||||
result: list[str | Path] = [QQ_INFO.format(version=version)]
|
||||
path = DATA_PATH / "about.png"
|
||||
if path.exists():
|
||||
info.append(path)
|
||||
result.append(path)
|
||||
await MessageUtils.build_message(result).send() # type: ignore
|
||||
else:
|
||||
info = [
|
||||
f"""
|
||||
『绪山真寻Bot』
|
||||
版本:{version}
|
||||
简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处
|
||||
项目地址:https://github.com/HibiKier/zhenxun_bot
|
||||
文档地址:https://hibikier.github.io/zhenxun_bot/
|
||||
""".strip()
|
||||
]
|
||||
await MessageUtils.build_message(info).send() # type: ignore
|
||||
await MessageUtils.build_message(INFO.format(version=version)).send()
|
||||
logger.info("查看关于", arparma.header_result, session=session)
|
||||
|
||||
@ -14,13 +14,19 @@ from nonebot_plugin_alconna import (
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.configs.utils import (
|
||||
AICallableParam,
|
||||
AICallableProperties,
|
||||
AICallableTag,
|
||||
PluginExtraData,
|
||||
RegisterConfig,
|
||||
)
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
from ._data_source import BanManage
|
||||
from ._data_source import BanManage, call_ban
|
||||
|
||||
base_config = Config.get("ban")
|
||||
|
||||
@ -78,6 +84,22 @@ __plugin_meta__ = PluginMetadata(
|
||||
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(),
|
||||
)
|
||||
|
||||
|
||||
@ -5,9 +5,20 @@ from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.models.ban_console import BanConsole
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.services.log import logger
|
||||
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:
|
||||
@classmethod
|
||||
async def build_ban_image(
|
||||
|
||||
@ -14,6 +14,7 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
BASE_PATH = DATA_PATH / "welcome_message"
|
||||
@ -91,7 +92,7 @@ def migrate(path: Path):
|
||||
json.dump(new_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
def _():
|
||||
"""数据迁移
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_alconna import (
|
||||
@ -14,35 +15,38 @@ from nonebot_plugin_alconna import (
|
||||
from nonebot_plugin_session import EventSession
|
||||
import pytz
|
||||
|
||||
from zhenxun.configs.utils import Command, PluginExtraData
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.models.group_member_info import GroupInfoUser
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import ImageTemplate
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="消息统计",
|
||||
description="消息统计查询",
|
||||
usage="""
|
||||
格式:
|
||||
消息排行 ?[type [日,周,月,年]] ?[--des]
|
||||
消息排行 ?[type [日,周,月,季,年]] ?[--des]
|
||||
|
||||
快捷:
|
||||
[日,周,月,年]消息排行 ?[数量]
|
||||
[日,周,月,季,年]消息排行 ?[数量]
|
||||
|
||||
示例:
|
||||
消息排行 : 所有记录排行
|
||||
日消息排行 : 今日记录排行
|
||||
周消息排行 : 今日记录排行
|
||||
月消息排行 : 今日记录排行
|
||||
年消息排行 : 今日记录排行
|
||||
周消息排行 : 本周记录排行
|
||||
月消息排行 : 本月记录排行
|
||||
季消息排行 : 本季度记录排行
|
||||
年消息排行 : 本年记录排行
|
||||
消息排行 周 --des : 逆序周记录排行
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
version="0.2",
|
||||
plugin_type=PluginType.NORMAL,
|
||||
menu_type="数据统计",
|
||||
commands=[
|
||||
@ -50,8 +54,19 @@ __plugin_meta__ = PluginMetadata(
|
||||
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(),
|
||||
)
|
||||
|
||||
@ -60,7 +75,7 @@ _matcher = on_alconna(
|
||||
Alconna(
|
||||
"消息排行",
|
||||
Option("--des", action=store_true, help_text="逆序"),
|
||||
Args["type?", ["日", "周", "月", "年"]]["count?", int, 10],
|
||||
Args["type?", ["日", "周", "月", "季", "年"]]["count?", int, 10],
|
||||
),
|
||||
aliases={"消息统计"},
|
||||
priority=5,
|
||||
@ -68,7 +83,7 @@ _matcher = on_alconna(
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
r"(?P<type>['日', '周', '月', '年'])?消息(排行|统计)\s?(?P<cnt>\d+)?",
|
||||
r"(?P<type>['日', '周', '月', '季', '年'])?消息(排行|统计)\s?(?P<cnt>\d+)?",
|
||||
command="消息排行",
|
||||
arguments=["{type}", "{cnt}"],
|
||||
prefix=True,
|
||||
@ -96,20 +111,57 @@ async def _(
|
||||
date_scope = (time_now - timedelta(days=7), time_now)
|
||||
elif date in ["月"]:
|
||||
date_scope = (time_now - timedelta(days=30), time_now)
|
||||
column_name = ["名次", "昵称", "发言次数"]
|
||||
elif date in ["季"]:
|
||||
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(
|
||||
group_id, count.result, "DES" if arparma.find("des") else "DESC", date_scope
|
||||
group_id, fetch_count, "DES" if arparma.find("des") else "DESC", date_scope
|
||||
):
|
||||
idx = 1
|
||||
data_list = []
|
||||
|
||||
for uid, num in rank_data:
|
||||
if user := await GroupInfoUser.filter(
|
||||
if len(data_list) >= count.result:
|
||||
break
|
||||
|
||||
user_in_group = await GroupInfoUser.filter(
|
||||
user_id=uid, group_id=group_id
|
||||
).first():
|
||||
user_name = user.user_name
|
||||
).first()
|
||||
|
||||
if not user_in_group and not show_quit_member:
|
||||
continue
|
||||
|
||||
if user_in_group:
|
||||
user_name = user_in_group.user_name
|
||||
else:
|
||||
user_name = uid
|
||||
data_list.append([idx, user_name, num])
|
||||
user_name = f"{uid}(已退群)"
|
||||
|
||||
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
|
||||
if not date_scope:
|
||||
if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id):
|
||||
@ -132,13 +184,3 @@ async def _(
|
||||
)
|
||||
await MessageUtils.build_message(A).finish(reply_to=True)
|
||||
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))
|
||||
|
||||
@ -37,10 +37,16 @@ __plugin_meta__ = PluginMetadata(
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
key="type",
|
||||
value="normal",
|
||||
help="帮助图片样式 ['normal', 'HTML', 'zhenxun']",
|
||||
value="zhenxun",
|
||||
help="帮助图片样式 [normal, HTML, zhenxun]",
|
||||
default_value="zhenxun",
|
||||
)
|
||||
),
|
||||
RegisterConfig(
|
||||
key="detail_type",
|
||||
value="zhenxun",
|
||||
help="帮助详情图片样式 ['normal', 'zhenxun']",
|
||||
default_value="zhenxun",
|
||||
),
|
||||
],
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
from pathlib import Path
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.path_config import IMAGE_PATH
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import IMAGE_PATH, TEMPLATE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.level_user import LevelUser
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.statistics import Statistics
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
|
||||
from ._config import (
|
||||
GROUP_HELP_PATH,
|
||||
@ -40,7 +46,9 @@ async def create_help_img(
|
||||
|
||||
match help_type:
|
||||
case "html":
|
||||
result = BuildImage.open(await build_html_image(group_id, is_detail))
|
||||
result = BuildImage.open(
|
||||
await build_html_image(session, group_id, is_detail)
|
||||
)
|
||||
case "zhenxun":
|
||||
result = BuildImage.open(
|
||||
await build_zhenxun_image(session, group_id, is_detail)
|
||||
@ -78,9 +86,96 @@ async def get_user_allow_help(user_id: str) -> list[PluginType]:
|
||||
return type_list
|
||||
|
||||
|
||||
async def get_plugin_help(
|
||||
user_id: str, name: str, is_superuser: bool
|
||||
) -> str | BuildImage:
|
||||
async def get_normal_help(
|
||||
metadata: PluginMetadata, extra: PluginExtraData, is_superuser: bool
|
||||
) -> str | bytes:
|
||||
"""构建默认帮助详情
|
||||
|
||||
参数:
|
||||
metadata: PluginMetadata
|
||||
extra: PluginExtraData
|
||||
is_superuser: 是否超级用户帮助
|
||||
|
||||
返回:
|
||||
str | bytes: 返回信息
|
||||
"""
|
||||
items = None
|
||||
if is_superuser:
|
||||
if usage := extra.superuser_help:
|
||||
items = {
|
||||
"简介": metadata.description,
|
||||
"用法": usage,
|
||||
}
|
||||
else:
|
||||
items = {
|
||||
"简介": metadata.description,
|
||||
"用法": metadata.usage,
|
||||
}
|
||||
if items:
|
||||
return (await ImageTemplate.hl_page(metadata.name, items)).pic2bytes()
|
||||
return "该功能没有帮助信息"
|
||||
|
||||
|
||||
def min_leading_spaces(str_list: list[str]) -> int:
|
||||
min_spaces = 9999
|
||||
|
||||
for s in str_list:
|
||||
leading_spaces = len(s) - len(s.lstrip(" "))
|
||||
|
||||
if leading_spaces < min_spaces:
|
||||
min_spaces = leading_spaces
|
||||
|
||||
return min_spaces if min_spaces != 9999 else 0
|
||||
|
||||
|
||||
def split_text(text: str):
|
||||
split_text = text.split("\n")
|
||||
min_spaces = min_leading_spaces(split_text)
|
||||
if min_spaces > 0:
|
||||
split_text = [s[min_spaces:] for s in split_text]
|
||||
return [s.replace(" ", " ") for s in split_text]
|
||||
|
||||
|
||||
async def get_zhenxun_help(
|
||||
module: str, metadata: PluginMetadata, extra: PluginExtraData, is_superuser: bool
|
||||
) -> str | bytes:
|
||||
"""构建ZhenXun帮助详情
|
||||
|
||||
参数:
|
||||
module: 模块名
|
||||
metadata: PluginMetadata
|
||||
extra: PluginExtraData
|
||||
is_superuser: 是否超级用户帮助
|
||||
|
||||
返回:
|
||||
str | bytes: 返回信息
|
||||
"""
|
||||
call_count = await Statistics.filter(plugin_name=module).count()
|
||||
usage = metadata.usage
|
||||
if is_superuser:
|
||||
if not extra.superuser_help:
|
||||
return "该功能没有超级用户帮助信息"
|
||||
usage = extra.superuser_help
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "help_detail").absolute()),
|
||||
template_name="main.html",
|
||||
templates={
|
||||
"title": metadata.name,
|
||||
"author": extra.author,
|
||||
"version": extra.version,
|
||||
"call_count": call_count,
|
||||
"descriptions": split_text(metadata.description),
|
||||
"usages": split_text(usage),
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 824, "height": 590},
|
||||
"base_url": f"file://{TEMPLATE_PATH}",
|
||||
},
|
||||
wait=2,
|
||||
)
|
||||
|
||||
|
||||
async def get_plugin_help(user_id: str, name: str, is_superuser: bool) -> str | bytes:
|
||||
"""获取功能的帮助信息
|
||||
|
||||
参数:
|
||||
@ -98,20 +193,12 @@ async def get_plugin_help(
|
||||
if plugin:
|
||||
_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if _plugin and _plugin.metadata:
|
||||
items = None
|
||||
if is_superuser:
|
||||
extra = _plugin.metadata.extra
|
||||
if usage := extra.get("superuser_help"):
|
||||
items = {
|
||||
"简介": _plugin.metadata.description,
|
||||
"用法": usage,
|
||||
}
|
||||
extra_data = PluginExtraData(**_plugin.metadata.extra)
|
||||
if Config.get_config("help", "detail_type") == "zhenxun":
|
||||
return await get_zhenxun_help(
|
||||
plugin.module, _plugin.metadata, extra_data, is_superuser
|
||||
)
|
||||
else:
|
||||
items = {
|
||||
"简介": _plugin.metadata.description,
|
||||
"用法": _plugin.metadata.usage,
|
||||
}
|
||||
if items:
|
||||
return await ImageTemplate.hl_page(plugin.name, items)
|
||||
return await get_normal_help(_plugin.metadata, extra_data, is_superuser)
|
||||
return "糟糕! 该功能没有帮助喔..."
|
||||
return "没有查找到这个功能噢..."
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import PluginType
|
||||
@ -27,13 +30,15 @@ async def sort_type() -> dict[str, list[PluginInfo]]:
|
||||
|
||||
|
||||
async def classify_plugin(
|
||||
group_id: str | None, is_detail: bool, handle: Callable
|
||||
session: Uninfo, group_id: str | None, is_detail: bool, handle: Callable
|
||||
) -> dict[str, list]:
|
||||
"""对插件进行分类并判断状态
|
||||
|
||||
参数:
|
||||
session: Uninfo对象
|
||||
group_id: 群组id
|
||||
is_detail: 是否详细帮助
|
||||
handle: 回调方法
|
||||
|
||||
返回:
|
||||
dict[str, list[Item]]: 分类插件数据
|
||||
@ -41,9 +46,10 @@ async def classify_plugin(
|
||||
sort_data = await sort_type()
|
||||
classify: dict[str, list] = {}
|
||||
group = await GroupConsole.get_or_none(group_id=group_id) if group_id else None
|
||||
bot = await BotConsole.get_or_none(bot_id=session.self_id)
|
||||
for menu, value in sort_data.items():
|
||||
for plugin in value:
|
||||
if not classify.get(menu):
|
||||
classify[menu] = []
|
||||
classify[menu].append(handle(plugin, group, is_detail))
|
||||
classify[menu].append(handle(bot, plugin, group, is_detail))
|
||||
return classify
|
||||
|
||||
@ -2,9 +2,11 @@ import os
|
||||
import random
|
||||
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import BlockType
|
||||
@ -48,11 +50,12 @@ ICON2STR = {
|
||||
|
||||
|
||||
def __handle_item(
|
||||
plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
|
||||
bot: BotConsole, plugin: PluginInfo, group: GroupConsole | None, is_detail: bool
|
||||
) -> Item:
|
||||
"""构造Item
|
||||
|
||||
参数:
|
||||
bot: BotConsole
|
||||
plugin: PluginInfo
|
||||
group: 群组
|
||||
is_detail: 是否详细
|
||||
@ -73,10 +76,13 @@ def __handle_item(
|
||||
]:
|
||||
sta = 2
|
||||
if group:
|
||||
if f"{plugin.module}:super," in group.block_plugin:
|
||||
if f"{plugin.module}," in group.superuser_block_plugin:
|
||||
sta = 2
|
||||
if f"{plugin.module}," in group.block_plugin:
|
||||
sta = 1
|
||||
if bot:
|
||||
if f"{plugin.module}," in bot.block_plugins:
|
||||
sta = 2
|
||||
return Item(plugin_name=plugin.name, sta=sta)
|
||||
|
||||
|
||||
@ -119,14 +125,17 @@ def build_plugin_data(classify: dict[str, list[Item]]) -> list[dict[str, str]]:
|
||||
return plugin_list
|
||||
|
||||
|
||||
async def build_html_image(group_id: str | None, is_detail: bool) -> bytes:
|
||||
async def build_html_image(
|
||||
session: Uninfo, group_id: str | None, is_detail: bool
|
||||
) -> bytes:
|
||||
"""构造HTML帮助图片
|
||||
|
||||
参数:
|
||||
session: Uninfo
|
||||
group_id: 群号
|
||||
is_detail: 是否详细帮助
|
||||
"""
|
||||
classify = await classify_plugin(group_id, is_detail, __handle_item)
|
||||
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
|
||||
plugin_list = build_plugin_data(classify)
|
||||
return await template_to_pic(
|
||||
template_path=str((TEMPLATE_PATH / "menu").absolute()),
|
||||
|
||||
@ -6,6 +6,7 @@ from pydantic import BaseModel
|
||||
from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.configs.path_config import TEMPLATE_PATH
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.models.bot_console import BotConsole
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.utils.enum import BlockType
|
||||
@ -21,12 +22,19 @@ class Item(BaseModel):
|
||||
"""插件命令"""
|
||||
|
||||
|
||||
def __handle_item(plugin: PluginInfo, group: GroupConsole | None, is_detail: bool):
|
||||
def __handle_item(
|
||||
bot: BotConsole | None,
|
||||
plugin: PluginInfo,
|
||||
group: GroupConsole | None,
|
||||
is_detail: bool,
|
||||
):
|
||||
"""构造Item
|
||||
|
||||
参数:
|
||||
bot: BotConsole
|
||||
plugin: PluginInfo
|
||||
group: 群组
|
||||
is_detail: 是否为详细
|
||||
|
||||
返回:
|
||||
Item: Item
|
||||
@ -40,6 +48,8 @@ def __handle_item(plugin: PluginInfo, group: GroupConsole | None, is_detail: boo
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif group and f"{plugin.module}," in group.block_plugin:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
elif bot and f"{plugin.module}," in bot.block_plugins:
|
||||
plugin.name = f"{plugin.name}(不可用)"
|
||||
commands = []
|
||||
nb_plugin = nonebot.get_plugin_by_module_name(plugin.module_path)
|
||||
if is_detail and nb_plugin and nb_plugin.metadata and nb_plugin.metadata.extra:
|
||||
@ -142,7 +152,7 @@ async def build_zhenxun_image(
|
||||
group_id: 群号
|
||||
is_detail: 是否详细帮助
|
||||
"""
|
||||
classify = await classify_plugin(group_id, is_detail, __handle_item)
|
||||
classify = await classify_plugin(session, group_id, is_detail, __handle_item)
|
||||
plugin_list = build_plugin_data(classify)
|
||||
platform = PlatformUtils.get_platform(session)
|
||||
bot_id = BotConfig.get_qbot_uid(session.self_id) or session.self_id
|
||||
|
||||
@ -21,7 +21,7 @@ from zhenxun.utils.message import MessageUtils
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="笨蛋检测",
|
||||
description="功能名称当命令检测",
|
||||
usage="""被动""".strip(),
|
||||
usage="""当一些笨蛋直接输入功能名称时,提示笨蛋使用帮助指令查看功能帮助""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
|
||||
@ -49,4 +49,14 @@ Config.add_plugin_config(
|
||||
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()))
|
||||
|
||||
@ -1,23 +1,85 @@
|
||||
from typing import Any
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters import Bot, Message
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.bot_message_store import BotMessageStore
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BotSentType
|
||||
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
|
||||
async def handle_api_result(
|
||||
bot: Bot, exception: Exception | None, api: str, data: dict[str, Any], result: Any
|
||||
):
|
||||
if not exception and api == "send_msg":
|
||||
if exception or api != "send_msg":
|
||||
return
|
||||
user_id = data.get("user_id")
|
||||
group_id = data.get("group_id")
|
||||
message_id = result.get("message_id")
|
||||
message: Message = data.get("message", "")
|
||||
message_type = data.get("message_type")
|
||||
try:
|
||||
if (uid := data.get("user_id")) and (msg_id := result.get("message_id")):
|
||||
MessageManager.add(str(uid), str(msg_id))
|
||||
# 记录消息id
|
||||
if user_id and message_id:
|
||||
MessageManager.add(str(user_id), str(message_id))
|
||||
logger.debug(
|
||||
f"收集消息id,user_id: {uid}, msg_id: {msg_id}", "msg_hook"
|
||||
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,
|
||||
)
|
||||
|
||||
@ -11,6 +11,7 @@ from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.configs.utils import RegisterConfig
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
_yaml = YAML(pure=True)
|
||||
_yaml.allow_unicode = True
|
||||
@ -57,7 +58,7 @@ def _generate_simple_config(exists_module: list[str]):
|
||||
生成简易配置
|
||||
|
||||
异常:
|
||||
AttributeError: _description_
|
||||
AttributeError: AttributeError
|
||||
"""
|
||||
# 读取用户配置
|
||||
_data = {}
|
||||
@ -73,7 +74,9 @@ def _generate_simple_config(exists_module: list[str]):
|
||||
if _data.get(module) and k in _data[module].keys():
|
||||
Config.set_config(module, k, _data[module][k])
|
||||
if f"{module}:{k}".lower() in exists_module:
|
||||
_tmp_data[module][k] = Config.get_config(module, k)
|
||||
_tmp_data[module][k] = Config.get_config(
|
||||
module, k, build_model=False
|
||||
)
|
||||
except AttributeError as e:
|
||||
raise AttributeError(f"{e}\n可能为config.yaml配置文件填写不规范") from e
|
||||
if not _tmp_data[module]:
|
||||
@ -102,7 +105,7 @@ def _generate_simple_config(exists_module: list[str]):
|
||||
temp_file.unlink()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=0)
|
||||
def _():
|
||||
"""
|
||||
初始化插件数据配置
|
||||
@ -125,3 +128,4 @@ def _():
|
||||
with plugins2config_file.open("w", encoding="utf8") as wf:
|
||||
_yaml.dump(_data, wf)
|
||||
_generate_simple_config(exists_module)
|
||||
Config.reload()
|
||||
|
||||
@ -20,6 +20,7 @@ from zhenxun.utils.enum import (
|
||||
PluginLimitType,
|
||||
PluginType,
|
||||
)
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
from .manager import manager
|
||||
|
||||
@ -95,7 +96,7 @@ async def _handle_setting(
|
||||
)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""
|
||||
初始化插件数据配置
|
||||
|
||||
@ -10,6 +10,7 @@ from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
driver: Driver = nonebot.get_driver()
|
||||
|
||||
@ -132,7 +133,7 @@ async def create_schedule(task: Task):
|
||||
logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""
|
||||
初始化插件数据配置
|
||||
|
||||
252
zhenxun/builtin_plugins/mahiro_bank/__init__.py
Normal file
@ -0,0 +1,252 @@
|
||||
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("小真寻银行结算", "定时任务")
|
||||
450
zhenxun/builtin_plugins/mahiro_bank/data_source.py
Normal file
@ -0,0 +1,450 @@
|
||||
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,12 +1,17 @@
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from nonebot import on_regex
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Depends, RegexGroup
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Alconna, Option, on_alconna, store_true
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
Arparma,
|
||||
CommandMeta,
|
||||
Option,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
@ -54,15 +59,22 @@ __plugin_meta__ = PluginMetadata(
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
_nickname_matcher = on_regex(
|
||||
"(?:以后)?(?:叫我|请叫我|称呼我)(.*)",
|
||||
_nickname_matcher = on_alconna(
|
||||
Alconna(
|
||||
"re:(?:以后)?(?:叫我|请叫我|称呼我)",
|
||||
Args["name?", str],
|
||||
meta=CommandMeta(compact=True),
|
||||
),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_global_nickname_matcher = on_regex(
|
||||
"设置全局昵称(.*)", rule=to_me(), priority=5, block=True
|
||||
_global_nickname_matcher = on_alconna(
|
||||
Alconna("设置全局昵称", Args["name?", str], meta=CommandMeta(compact=True)),
|
||||
rule=to_me(),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
@ -117,18 +129,16 @@ CANCEL = [
|
||||
]
|
||||
|
||||
|
||||
def CheckNickname():
|
||||
async def CheckNickname(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
params: Arparma,
|
||||
):
|
||||
"""
|
||||
检查名称是否合法
|
||||
"""
|
||||
|
||||
async def dependency(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
black_word = Config.get_config("nickname", "BLACK_WORD")
|
||||
(name,) = reg_group
|
||||
name = params.query("name")
|
||||
logger.debug(f"昵称检查: {name}", "昵称设置", session=session)
|
||||
if not name:
|
||||
await MessageUtils.build_message("叫你空白?叫你虚空?叫你无名??").finish(
|
||||
@ -138,13 +148,13 @@ def CheckNickname():
|
||||
logger.debug(
|
||||
f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session
|
||||
)
|
||||
return
|
||||
else:
|
||||
if len(name) > 20:
|
||||
await MessageUtils.build_message("昵称可不能超过20个字!").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if name in bot.config.nickname:
|
||||
await MessageUtils.build_message("笨蛋!休想占用我的名字! #").finish(
|
||||
await MessageUtils.build_message("笨蛋!休想占用我的名字! ").finish(
|
||||
at_sender=True
|
||||
)
|
||||
if black_word:
|
||||
@ -162,17 +172,17 @@ def CheckNickname():
|
||||
await MessageUtils.build_message(
|
||||
f"字符 [{word}] 为禁止字符!"
|
||||
).finish(at_sender=True)
|
||||
|
||||
return Depends(dependency)
|
||||
return name
|
||||
|
||||
|
||||
@_nickname_matcher.handle(parameterless=[CheckNickname()])
|
||||
@_nickname_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
name_: Arparma,
|
||||
uname: str = UserName(),
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
(name,) = reg_group
|
||||
name = await CheckNickname(bot, session, name_)
|
||||
if len(name) < 5 and random.random() < 0.3:
|
||||
name = "~".join(name)
|
||||
group_id = None
|
||||
@ -200,13 +210,14 @@ async def _(
|
||||
)
|
||||
|
||||
|
||||
@_global_nickname_matcher.handle(parameterless=[CheckNickname()])
|
||||
@_global_nickname_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
session: Uninfo,
|
||||
name_: Arparma,
|
||||
nickname: str = UserName(),
|
||||
reg_group: tuple[Any, ...] = RegexGroup(),
|
||||
):
|
||||
(name,) = reg_group
|
||||
name = await CheckNickname(bot, session, name_)
|
||||
await FriendUser.set_user_nickname(
|
||||
session.user.id,
|
||||
name,
|
||||
@ -227,15 +238,14 @@ async def _(session: Uninfo, uname: str = UserName()):
|
||||
group_id = session.group.parent.id if session.group.parent else session.group.id
|
||||
if group_id:
|
||||
nickname = await GroupInfoUser.get_user_nickname(session.user.id, group_id)
|
||||
card = uname
|
||||
else:
|
||||
nickname = await FriendUser.get_user_nickname(session.user.id)
|
||||
card = uname
|
||||
if nickname:
|
||||
await MessageUtils.build_message(random.choice(REMIND).format(nickname)).finish(
|
||||
reply_to=True
|
||||
)
|
||||
else:
|
||||
card = uname
|
||||
await MessageUtils.build_message(
|
||||
random.choice(
|
||||
[
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from nonebot import on_notice, on_request
|
||||
from nonebot import on_notice
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import (
|
||||
GroupDecreaseNoticeEvent,
|
||||
@ -14,9 +14,10 @@ from nonebot_plugin_uninfo import Uninfo
|
||||
from zhenxun.builtin_plugins.platform.qq.exception import ForceAddGroupError
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.models.event_log import EventLog
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.enum import EventLogType, PluginType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.rules import notice_rule
|
||||
|
||||
@ -106,8 +107,6 @@ group_decrease_handle = on_notice(
|
||||
rule=notice_rule([GroupMemberDecreaseEvent, GroupDecreaseNoticeEvent]),
|
||||
)
|
||||
"""群员减少处理"""
|
||||
add_group = on_request(priority=1, block=False)
|
||||
"""加群同意请求"""
|
||||
|
||||
|
||||
@group_increase_handle.handle()
|
||||
@ -141,8 +140,21 @@ async def _(
|
||||
group_id = str(event.group_id)
|
||||
if event.sub_type == "kick_me":
|
||||
"""踢出Bot"""
|
||||
await GroupManager.kick_bot(bot, user_id, group_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"]:
|
||||
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(
|
||||
bot, user_id, group_id, str(event.operator_id), event.sub_type
|
||||
)
|
||||
|
||||
100
zhenxun/builtin_plugins/platform/qq/user_group_request.py
Normal file
@ -0,0 +1,100 @@
|
||||
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,
|
||||
)
|
||||
@ -9,7 +9,7 @@ from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.utils import is_number
|
||||
|
||||
from .data_source import ShopManage
|
||||
from .data_source import StoreManager
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="插件商店",
|
||||
@ -82,7 +82,7 @@ _matcher.shortcut(
|
||||
@_matcher.assign("$main")
|
||||
async def _(session: EventSession):
|
||||
try:
|
||||
result = await ShopManage.get_plugins_info()
|
||||
result = await StoreManager.get_plugins_info()
|
||||
logger.info("查看插件列表", "插件商店", session=session)
|
||||
await MessageUtils.build_message(result).send()
|
||||
except Exception as e:
|
||||
@ -97,7 +97,7 @@ async def _(session: EventSession, plugin_id: str):
|
||||
await MessageUtils.build_message(f"正在添加插件 Id: {plugin_id}").send()
|
||||
else:
|
||||
await MessageUtils.build_message(f"正在添加插件 Module: {plugin_id}").send()
|
||||
result = await ShopManage.add_plugin(plugin_id)
|
||||
result = await StoreManager.add_plugin(plugin_id)
|
||||
except Exception as e:
|
||||
logger.error(f"添加插件 Id: {plugin_id}失败", "插件商店", session=session, e=e)
|
||||
await MessageUtils.build_message(
|
||||
@ -110,7 +110,7 @@ async def _(session: EventSession, plugin_id: str):
|
||||
@_matcher.assign("remove")
|
||||
async def _(session: EventSession, plugin_id: str):
|
||||
try:
|
||||
result = await ShopManage.remove_plugin(plugin_id)
|
||||
result = await StoreManager.remove_plugin(plugin_id)
|
||||
except Exception as e:
|
||||
logger.error(f"移除插件 Id: {plugin_id}失败", "插件商店", session=session, e=e)
|
||||
await MessageUtils.build_message(
|
||||
@ -123,7 +123,7 @@ async def _(session: EventSession, plugin_id: str):
|
||||
@_matcher.assign("search")
|
||||
async def _(session: EventSession, plugin_name_or_author: str):
|
||||
try:
|
||||
result = await ShopManage.search_plugin(plugin_name_or_author)
|
||||
result = await StoreManager.search_plugin(plugin_name_or_author)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"搜索插件 name: {plugin_name_or_author}失败",
|
||||
@ -145,7 +145,7 @@ async def _(session: EventSession, plugin_id: str):
|
||||
await MessageUtils.build_message(f"正在更新插件 Id: {plugin_id}").send()
|
||||
else:
|
||||
await MessageUtils.build_message(f"正在更新插件 Module: {plugin_id}").send()
|
||||
result = await ShopManage.update_plugin(plugin_id)
|
||||
result = await StoreManager.update_plugin(plugin_id)
|
||||
except Exception as e:
|
||||
logger.error(f"更新插件 Id: {plugin_id}失败", "插件商店", session=session, e=e)
|
||||
await MessageUtils.build_message(
|
||||
@ -159,7 +159,7 @@ async def _(session: EventSession, plugin_id: str):
|
||||
async def _(session: EventSession):
|
||||
try:
|
||||
await MessageUtils.build_message("正在更新全部插件").send()
|
||||
result = await ShopManage.update_all_plugin()
|
||||
result = await StoreManager.update_all_plugin()
|
||||
except Exception as e:
|
||||
logger.error("更新全部插件失败", "插件商店", session=session, e=e)
|
||||
await MessageUtils.build_message(f"更新全部插件失败 e: {e}").finish()
|
||||
|
||||
@ -9,3 +9,5 @@ DEFAULT_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins/tree/ma
|
||||
|
||||
EXTRA_GITHUB_URL = "https://github.com/zhenxun-org/zhenxun_bot_plugins_index/tree/index"
|
||||
"""插件库索引github仓库地址"""
|
||||
|
||||
LOG_COMMAND = "插件商店"
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from aiocache import cached
|
||||
import ujson as json
|
||||
@ -14,9 +13,15 @@ from zhenxun.utils.github_utils import GithubUtils
|
||||
from zhenxun.utils.github_utils.models import RepoAPI
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle
|
||||
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
|
||||
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,
|
||||
LOG_COMMAND,
|
||||
)
|
||||
|
||||
|
||||
def row_style(column: str, text: str) -> RowStyle:
|
||||
@ -39,72 +44,69 @@ def install_requirement(plugin_path: Path):
|
||||
requirement_files = ["requirement.txt", "requirements.txt"]
|
||||
requirement_paths = [plugin_path / file for file in requirement_files]
|
||||
|
||||
existing_requirements = next(
|
||||
if existing_requirements := next(
|
||||
(path for path in requirement_paths if path.exists()), None
|
||||
)
|
||||
|
||||
if not existing_requirements:
|
||||
logger.debug(
|
||||
f"No requirement.txt found for plugin: {plugin_path.name}", "插件管理"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["poetry", "run", "pip", "install", "-r", str(existing_requirements)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.debug(
|
||||
"Successfully installed dependencies for"
|
||||
f" plugin: {plugin_path.name}. Output:\n{result.stdout}",
|
||||
"插件管理",
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
logger.error(
|
||||
f"Failed to install dependencies for plugin: {plugin_path.name}. "
|
||||
" Error:\n{e.stderr}"
|
||||
)
|
||||
):
|
||||
VirtualEnvPackageManager.install_requirement(existing_requirements)
|
||||
|
||||
|
||||
class ShopManage:
|
||||
class StoreManager:
|
||||
@classmethod
|
||||
@cached(60)
|
||||
async def get_data(cls) -> dict[str, StorePluginInfo]:
|
||||
"""获取插件信息数据
|
||||
|
||||
异常:
|
||||
ValueError: 访问请求失败
|
||||
async def get_github_plugins(cls) -> list[StorePluginInfo]:
|
||||
"""获取github插件列表信息
|
||||
|
||||
返回:
|
||||
dict: 插件信息数据
|
||||
list[StorePluginInfo]: 插件列表数据
|
||||
"""
|
||||
default_github_repo = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL)
|
||||
extra_github_repo = GithubUtils.parse_github_url(EXTRA_GITHUB_URL)
|
||||
for repo_info in [default_github_repo, extra_github_repo]:
|
||||
repo_info = GithubUtils.parse_github_url(DEFAULT_GITHUB_URL)
|
||||
if await repo_info.update_repo_commit():
|
||||
logger.info(f"获取最新提交: {repo_info.branch}", "插件管理")
|
||||
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
|
||||
else:
|
||||
logger.warning(f"获取最新提交失败: {repo_info}", "插件管理")
|
||||
default_github_url = await default_github_repo.get_raw_download_urls(
|
||||
"plugins.json"
|
||||
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
|
||||
default_github_url = await repo_info.get_raw_download_urls("plugins.json")
|
||||
response = await AsyncHttpx.get(default_github_url, check_status_code=200)
|
||||
if response.status_code == 200:
|
||||
logger.info("获取github插件列表成功", LOG_COMMAND)
|
||||
return [StorePluginInfo(**detail) for detail in json.loads(response.text)]
|
||||
else:
|
||||
logger.warning(
|
||||
f"获取github插件列表失败: {response.status_code}", LOG_COMMAND
|
||||
)
|
||||
extra_github_url = await extra_github_repo.get_raw_download_urls("plugins.json")
|
||||
res = await AsyncHttpx.get(default_github_url)
|
||||
res2 = await AsyncHttpx.get(extra_github_url)
|
||||
return []
|
||||
|
||||
# 检查请求结果
|
||||
if res.status_code != 200 or res2.status_code != 200:
|
||||
raise ValueError(f"下载错误, code: {res.status_code}, {res2.status_code}")
|
||||
@classmethod
|
||||
async def get_extra_plugins(cls) -> list[StorePluginInfo]:
|
||||
"""获取额外插件列表信息
|
||||
|
||||
# 解析并合并返回的 JSON 数据
|
||||
data1 = json.loads(res.text)
|
||||
data2 = json.loads(res2.text)
|
||||
return {
|
||||
name: StorePluginInfo(**detail)
|
||||
for name, detail in {**data1, **data2}.items()
|
||||
}
|
||||
返回:
|
||||
list[StorePluginInfo]: 插件列表数据
|
||||
"""
|
||||
repo_info = GithubUtils.parse_github_url(EXTRA_GITHUB_URL)
|
||||
if await repo_info.update_repo_commit():
|
||||
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
|
||||
else:
|
||||
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
|
||||
extra_github_url = await repo_info.get_raw_download_urls("plugins.json")
|
||||
response = await AsyncHttpx.get(extra_github_url, check_status_code=200)
|
||||
if response.status_code == 200:
|
||||
return [StorePluginInfo(**detail) for detail in json.loads(response.text)]
|
||||
else:
|
||||
logger.warning(
|
||||
f"获取github扩展插件列表失败: {response.status_code}", LOG_COMMAND
|
||||
)
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
@cached(60)
|
||||
async def get_data(cls) -> list[StorePluginInfo]:
|
||||
"""获取插件信息数据
|
||||
|
||||
返回:
|
||||
list[StorePluginInfo]: 插件信息数据
|
||||
"""
|
||||
plugins = await cls.get_github_plugins()
|
||||
extra_plugins = await cls.get_extra_plugins()
|
||||
return [*plugins, *extra_plugins]
|
||||
|
||||
@classmethod
|
||||
def version_check(cls, plugin_info: StorePluginInfo, suc_plugin: dict[str, str]):
|
||||
@ -112,7 +114,7 @@ class ShopManage:
|
||||
|
||||
参数:
|
||||
plugin_info: StorePluginInfo
|
||||
suc_plugin: dict[str, str]
|
||||
suc_plugin: 模块名: 版本号
|
||||
|
||||
返回:
|
||||
str: 版本号
|
||||
@ -132,7 +134,7 @@ class ShopManage:
|
||||
|
||||
参数:
|
||||
plugin_info: StorePluginInfo
|
||||
suc_plugin: dict[str, str]
|
||||
suc_plugin: 模块名: 版本号
|
||||
|
||||
返回:
|
||||
bool: 是否有更新
|
||||
@ -156,21 +158,21 @@ class ShopManage:
|
||||
返回:
|
||||
BuildImage | str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
|
||||
plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "0.1") for p in plugin_list}
|
||||
db_plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "0.1") for p in db_plugin_list}
|
||||
data_list = [
|
||||
[
|
||||
"已安装" if plugin_info[1].module in suc_plugin else "",
|
||||
"已安装" if plugin_info.module in suc_plugin else "",
|
||||
id,
|
||||
plugin_info[0],
|
||||
plugin_info[1].description,
|
||||
plugin_info[1].author,
|
||||
cls.version_check(plugin_info[1], suc_plugin),
|
||||
plugin_info[1].plugin_type_name,
|
||||
plugin_info.name,
|
||||
plugin_info.description,
|
||||
plugin_info.author,
|
||||
cls.version_check(plugin_info, suc_plugin),
|
||||
plugin_info.plugin_type_name,
|
||||
]
|
||||
for id, plugin_info in enumerate(data.items())
|
||||
for id, plugin_info in enumerate(plugin_list)
|
||||
]
|
||||
return await ImageTemplate.table_page(
|
||||
"插件列表",
|
||||
@ -190,15 +192,15 @@ class ShopManage:
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
try:
|
||||
plugin_key = await cls._resolve_plugin_key(plugin_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
plugin_list = await cls.get_loaded_plugins("module")
|
||||
plugin_info = data[plugin_key]
|
||||
if plugin_info.module in [p[0] for p in plugin_list]:
|
||||
return f"插件 {plugin_key} 已安装,无需重复安装"
|
||||
db_plugin_list = await cls.get_loaded_plugins("module")
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
if plugin_info.module in [p[0] for p in db_plugin_list]:
|
||||
return f"插件 {plugin_info.name} 已安装,无需重复安装"
|
||||
is_external = True
|
||||
if plugin_info.github_url is None:
|
||||
plugin_info.github_url = DEFAULT_GITHUB_URL
|
||||
@ -207,34 +209,39 @@ class ShopManage:
|
||||
if len(version_split) > 1:
|
||||
github_url_split = plugin_info.github_url.split("/tree/")
|
||||
plugin_info.github_url = f"{github_url_split[0]}/tree/{version_split[1]}"
|
||||
logger.info(f"正在安装插件 {plugin_key}...")
|
||||
logger.info(f"正在安装插件 {plugin_info.name}...", LOG_COMMAND)
|
||||
await cls.install_plugin_with_repo(
|
||||
plugin_info.github_url,
|
||||
plugin_info.module_path,
|
||||
plugin_info.is_dir,
|
||||
is_external,
|
||||
)
|
||||
return f"插件 {plugin_key} 安装成功! 重启后生效"
|
||||
return f"插件 {plugin_info.name} 安装成功! 重启后生效"
|
||||
|
||||
@classmethod
|
||||
async def install_plugin_with_repo(
|
||||
cls, github_url: str, module_path: str, is_dir: bool, is_external: bool = False
|
||||
cls,
|
||||
github_url: str,
|
||||
module_path: str,
|
||||
is_dir: bool,
|
||||
is_external: bool = False,
|
||||
):
|
||||
files: list[str]
|
||||
repo_api: RepoAPI
|
||||
repo_info = GithubUtils.parse_github_url(github_url)
|
||||
if await repo_info.update_repo_commit():
|
||||
logger.info(f"获取最新提交: {repo_info.branch}", "插件管理")
|
||||
logger.info(f"获取最新提交: {repo_info.branch}", LOG_COMMAND)
|
||||
else:
|
||||
logger.warning(f"获取最新提交失败: {repo_info}", "插件管理")
|
||||
logger.debug(f"成功获取仓库信息: {repo_info}", "插件管理")
|
||||
logger.warning(f"获取最新提交失败: {repo_info}", LOG_COMMAND)
|
||||
logger.debug(f"成功获取仓库信息: {repo_info}", LOG_COMMAND)
|
||||
for repo_api in GithubUtils.iter_api_strategies():
|
||||
try:
|
||||
await repo_api.parse_repo_info(repo_info)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"获取插件文件失败: {e} | API类型: {repo_api.strategy}", "插件管理"
|
||||
f"获取插件文件失败 | API类型: {repo_api.strategy}",
|
||||
LOG_COMMAND,
|
||||
e=e,
|
||||
)
|
||||
continue
|
||||
else:
|
||||
@ -250,7 +257,7 @@ class ShopManage:
|
||||
base_path = BASE_PATH / "plugins" if is_external else BASE_PATH
|
||||
base_path = base_path if module_path else base_path / repo_info.repo
|
||||
download_paths: list[Path | str] = [base_path / file for file in files]
|
||||
logger.debug(f"插件下载路径: {download_paths}", "插件管理")
|
||||
logger.debug(f"插件下载路径: {download_paths}", LOG_COMMAND)
|
||||
result = await AsyncHttpx.gather_download_file(download_urls, download_paths)
|
||||
for _id, success in enumerate(result):
|
||||
if not success:
|
||||
@ -265,12 +272,12 @@ class ShopManage:
|
||||
req_files.extend(
|
||||
repo_api.get_files(f"{replace_module_path}/requirement.txt", False)
|
||||
)
|
||||
logger.debug(f"获取插件依赖文件列表: {req_files}", "插件管理")
|
||||
logger.debug(f"获取插件依赖文件列表: {req_files}", LOG_COMMAND)
|
||||
req_download_urls = [
|
||||
await repo_info.get_raw_download_urls(file) for file in req_files
|
||||
]
|
||||
req_paths: list[Path | str] = [plugin_path / file for file in req_files]
|
||||
logger.debug(f"插件依赖文件下载路径: {req_paths}", "插件管理")
|
||||
logger.debug(f"插件依赖文件下载路径: {req_paths}", LOG_COMMAND)
|
||||
if req_files:
|
||||
result = await AsyncHttpx.gather_download_file(
|
||||
req_download_urls, req_paths
|
||||
@ -278,7 +285,7 @@ class ShopManage:
|
||||
for success in result:
|
||||
if not success:
|
||||
raise Exception("插件依赖文件下载失败")
|
||||
logger.debug(f"插件依赖文件列表: {req_paths}", "插件管理")
|
||||
logger.debug(f"插件依赖文件列表: {req_paths}", LOG_COMMAND)
|
||||
install_requirement(plugin_path)
|
||||
except ValueError as e:
|
||||
logger.warning("未获取到依赖文件路径...", e=e)
|
||||
@ -295,12 +302,12 @@ class ShopManage:
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
try:
|
||||
plugin_key = await cls._resolve_plugin_key(plugin_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
plugin_info = data[plugin_key]
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
path = BASE_PATH
|
||||
if plugin_info.github_url:
|
||||
path = BASE_PATH / "plugins"
|
||||
@ -309,14 +316,14 @@ class ShopManage:
|
||||
if not plugin_info.is_dir:
|
||||
path = Path(f"{path}.py")
|
||||
if not path.exists():
|
||||
return f"插件 {plugin_key} 不存在..."
|
||||
logger.debug(f"尝试移除插件 {plugin_key} 文件: {path}", "插件管理")
|
||||
return f"插件 {plugin_info.name} 不存在..."
|
||||
logger.debug(f"尝试移除插件 {plugin_info.name} 文件: {path}", LOG_COMMAND)
|
||||
if plugin_info.is_dir:
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
await PluginInitManager.remove(f"zhenxun.{plugin_info.module_path}")
|
||||
return f"插件 {plugin_key} 移除成功! 重启后生效"
|
||||
return f"插件 {plugin_info.name} 移除成功! 重启后生效"
|
||||
|
||||
@classmethod
|
||||
async def search_plugin(cls, plugin_name_or_author: str) -> BuildImage | str:
|
||||
@ -328,25 +335,25 @@ class ShopManage:
|
||||
返回:
|
||||
BuildImage | str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list}
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
db_plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
|
||||
filtered_data = [
|
||||
(id, plugin_info)
|
||||
for id, plugin_info in enumerate(data.items())
|
||||
if plugin_name_or_author.lower() in plugin_info[0].lower()
|
||||
or plugin_name_or_author.lower() in plugin_info[1].author.lower()
|
||||
for id, plugin_info in enumerate(plugin_list)
|
||||
if plugin_name_or_author.lower() in plugin_info.name.lower()
|
||||
or plugin_name_or_author.lower() in plugin_info.author.lower()
|
||||
]
|
||||
|
||||
data_list = [
|
||||
[
|
||||
"已安装" if plugin_info[1].module in suc_plugin else "",
|
||||
"已安装" if plugin_info.module in suc_plugin else "",
|
||||
id,
|
||||
plugin_info[0],
|
||||
plugin_info[1].description,
|
||||
plugin_info[1].author,
|
||||
cls.version_check(plugin_info[1], suc_plugin),
|
||||
plugin_info[1].plugin_type_name,
|
||||
plugin_info.name,
|
||||
plugin_info.description,
|
||||
plugin_info.author,
|
||||
cls.version_check(plugin_info, suc_plugin),
|
||||
plugin_info.plugin_type_name,
|
||||
]
|
||||
for id, plugin_info in filtered_data
|
||||
]
|
||||
@ -354,7 +361,7 @@ class ShopManage:
|
||||
return "未找到相关插件..."
|
||||
column_name = ["-", "ID", "名称", "简介", "作者", "版本", "类型"]
|
||||
return await ImageTemplate.table_page(
|
||||
"插件列表",
|
||||
"商店插件列表",
|
||||
"通过添加/移除插件 ID 来管理插件",
|
||||
column_name,
|
||||
data_list,
|
||||
@ -371,20 +378,20 @@ class ShopManage:
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
try:
|
||||
plugin_key = await cls._resolve_plugin_key(plugin_id)
|
||||
except ValueError as e:
|
||||
return str(e)
|
||||
logger.info(f"尝试更新插件 {plugin_key}", "插件管理")
|
||||
plugin_info = data[plugin_key]
|
||||
plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list}
|
||||
if plugin_info.module not in [p[0] for p in plugin_list]:
|
||||
return f"插件 {plugin_key} 未安装,无法更新"
|
||||
logger.debug(f"当前插件列表: {suc_plugin}", "插件管理")
|
||||
plugin_info = next(p for p in plugin_list if p.module == plugin_key)
|
||||
logger.info(f"尝试更新插件 {plugin_info.name}", LOG_COMMAND)
|
||||
db_plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
|
||||
if plugin_info.module not in [p[0] for p in db_plugin_list]:
|
||||
return f"插件 {plugin_info.name} 未安装,无法更新"
|
||||
logger.debug(f"当前插件列表: {suc_plugin}", LOG_COMMAND)
|
||||
if cls.check_version_is_new(plugin_info, suc_plugin):
|
||||
return f"插件 {plugin_key} 已是最新版本"
|
||||
return f"插件 {plugin_info.name} 已是最新版本"
|
||||
is_external = True
|
||||
if plugin_info.github_url is None:
|
||||
plugin_info.github_url = DEFAULT_GITHUB_URL
|
||||
@ -395,7 +402,7 @@ class ShopManage:
|
||||
plugin_info.is_dir,
|
||||
is_external,
|
||||
)
|
||||
return f"插件 {plugin_key} 更新成功! 重启后生效"
|
||||
return f"插件 {plugin_info.name} 更新成功! 重启后生效"
|
||||
|
||||
@classmethod
|
||||
async def update_all_plugin(cls) -> str:
|
||||
@ -407,24 +414,33 @@ class ShopManage:
|
||||
返回:
|
||||
str: 返回消息
|
||||
"""
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
plugin_list = list(data.keys())
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
plugin_name_list = [p.name for p in plugin_list]
|
||||
update_failed_list = []
|
||||
update_success_list = []
|
||||
result = "--已更新{}个插件 {}个失败 {}个成功--"
|
||||
logger.info(f"尝试更新全部插件 {plugin_list}", "插件管理")
|
||||
for plugin_key in plugin_list:
|
||||
logger.info(f"尝试更新全部插件 {plugin_name_list}", LOG_COMMAND)
|
||||
for plugin_info in plugin_list:
|
||||
try:
|
||||
plugin_info = data[plugin_key]
|
||||
plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in plugin_list}
|
||||
if plugin_info.module not in [p[0] for p in plugin_list]:
|
||||
logger.debug(f"插件 {plugin_key} 未安装,跳过", "插件管理")
|
||||
db_plugin_list = await cls.get_loaded_plugins("module", "version")
|
||||
suc_plugin = {p[0]: (p[1] or "Unknown") for p in db_plugin_list}
|
||||
if plugin_info.module not in [p[0] for p in db_plugin_list]:
|
||||
logger.debug(
|
||||
f"插件 {plugin_info.name}({plugin_info.module}) 未安装,跳过",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
continue
|
||||
if cls.check_version_is_new(plugin_info, suc_plugin):
|
||||
logger.debug(f"插件 {plugin_key} 已是最新版本,跳过", "插件管理")
|
||||
logger.debug(
|
||||
f"插件 {plugin_info.name}({plugin_info.module}) "
|
||||
"已是最新版本,跳过",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
continue
|
||||
logger.info(f"正在更新插件 {plugin_key}", "插件管理")
|
||||
logger.info(
|
||||
f"正在更新插件 {plugin_info.name}({plugin_info.module})",
|
||||
LOG_COMMAND,
|
||||
)
|
||||
is_external = True
|
||||
if plugin_info.github_url is None:
|
||||
plugin_info.github_url = DEFAULT_GITHUB_URL
|
||||
@ -435,10 +451,14 @@ class ShopManage:
|
||||
plugin_info.is_dir,
|
||||
is_external,
|
||||
)
|
||||
update_success_list.append(plugin_key)
|
||||
update_success_list.append(plugin_info.name)
|
||||
except Exception as e:
|
||||
logger.error(f"更新插件 {plugin_key} 失败: {e}", "插件管理")
|
||||
update_failed_list.append(plugin_key)
|
||||
logger.error(
|
||||
f"更新插件 {plugin_info.name}({plugin_info.module}) 失败",
|
||||
LOG_COMMAND,
|
||||
e=e,
|
||||
)
|
||||
update_failed_list.append(plugin_info.name)
|
||||
if not update_success_list and not update_failed_list:
|
||||
return "全部插件已是最新版本"
|
||||
if update_success_list:
|
||||
@ -460,13 +480,28 @@ class ShopManage:
|
||||
|
||||
@classmethod
|
||||
async def _resolve_plugin_key(cls, plugin_id: str) -> str:
|
||||
data: dict[str, StorePluginInfo] = await cls.get_data()
|
||||
"""获取插件module
|
||||
|
||||
参数:
|
||||
plugin_id: module,id或插件名称
|
||||
|
||||
异常:
|
||||
ValueError: 插件不存在
|
||||
ValueError: 插件不存在
|
||||
|
||||
返回:
|
||||
str: 插件模块名
|
||||
"""
|
||||
plugin_list: list[StorePluginInfo] = await cls.get_data()
|
||||
if is_number(plugin_id):
|
||||
idx = int(plugin_id)
|
||||
if idx < 0 or idx >= len(data):
|
||||
if idx < 0 or idx >= len(plugin_list):
|
||||
raise ValueError("插件ID不存在...")
|
||||
return list(data.keys())[idx]
|
||||
return plugin_list[idx].module
|
||||
elif isinstance(plugin_id, str):
|
||||
if plugin_id not in [v.module for k, v in data.items()]:
|
||||
raise ValueError("插件Module不存在...")
|
||||
return {v.module: k for k, v in data.items()}[plugin_id]
|
||||
result = (
|
||||
None if plugin_id not in [v.module for v in plugin_list] else plugin_id
|
||||
) or next(v for v in plugin_list if v.name == plugin_id).module
|
||||
if not result:
|
||||
raise ValueError("插件 Module / 名称 不存在...")
|
||||
return result
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from nonebot.compat import model_dump
|
||||
from pydantic import BaseModel
|
||||
|
||||
@ -13,9 +15,30 @@ type2name: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
class GiteeContents(BaseModel):
|
||||
"""Gitee Api内容"""
|
||||
|
||||
type: Literal["file", "dir"]
|
||||
"""类型"""
|
||||
size: Any
|
||||
"""文件大小"""
|
||||
name: str
|
||||
"""文件名"""
|
||||
path: str
|
||||
"""文件路径"""
|
||||
url: str
|
||||
"""文件链接"""
|
||||
html_url: str
|
||||
"""文件html链接"""
|
||||
download_url: str
|
||||
"""文件raw链接"""
|
||||
|
||||
|
||||
class StorePluginInfo(BaseModel):
|
||||
"""插件信息"""
|
||||
|
||||
name: str
|
||||
"""插件名"""
|
||||
module: str
|
||||
"""模块名"""
|
||||
module_path: str
|
||||
|
||||
@ -17,11 +17,12 @@ from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig, Config
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.models.event_log import EventLog
|
||||
from zhenxun.models.fg_request import FgRequest
|
||||
from zhenxun.models.friend_user import FriendUser
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType
|
||||
from zhenxun.utils.enum import EventLogType, PluginType, RequestHandleType, RequestType
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
base_config = Config.get("invite_manager")
|
||||
@ -112,21 +113,29 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi
|
||||
nickname=nickname,
|
||||
comment=comment,
|
||||
)
|
||||
await PlatformUtils.send_superuser(
|
||||
results = await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
f"*****一份好友申请*****\n"
|
||||
f"ID: {f.id}\n"
|
||||
f"昵称:{nickname}({event.user_id})\n"
|
||||
f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n"
|
||||
f"日期:{str(datetime.now()).split('.')[0]}\n"
|
||||
f"日期:{datetime.now().replace(microsecond=0)}\n"
|
||||
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:
|
||||
logger.debug("好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id)
|
||||
|
||||
|
||||
@group_req.handle()
|
||||
async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession):
|
||||
# sourcery skip: low-code-quality
|
||||
if event.sub_type != "invite":
|
||||
return
|
||||
if str(event.user_id) in bot.config.superusers or base_config.get("AUTO_ADD_GROUP"):
|
||||
@ -186,7 +195,7 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
group_id=str(event.group_id),
|
||||
handle_type=RequestHandleType.APPROVE,
|
||||
)
|
||||
await PlatformUtils.send_superuser(
|
||||
results = await PlatformUtils.send_superuser(
|
||||
bot,
|
||||
f"*****一份入群申请*****\n"
|
||||
f"ID:{f.id}\n"
|
||||
@ -230,13 +239,27 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio
|
||||
nickname=nickname,
|
||||
group_id=str(event.group_id),
|
||||
)
|
||||
await PlatformUtils.send_superuser(
|
||||
kick_count = await EventLog.filter(
|
||||
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,
|
||||
f"*****一份入群申请*****\n"
|
||||
f"ID:{f.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:
|
||||
logger.debug(
|
||||
"群聊请求五分钟内重复, 已忽略",
|
||||
|
||||
51
zhenxun/builtin_plugins/scheduler_admin/__init__.py
Normal file
@ -0,0 +1,51 @@
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData
|
||||
from zhenxun.utils.enum import PluginType
|
||||
|
||||
from . import command # noqa: F401
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="定时任务管理",
|
||||
description="查看和管理由 SchedulerManager 控制的定时任务。",
|
||||
usage="""
|
||||
📋 定时任务管理 - 支持群聊和私聊操作
|
||||
|
||||
🔍 查看任务:
|
||||
定时任务 查看 [-all] [-g <群号>] [-p <插件>] [--page <页码>]
|
||||
• 群聊中: 查看本群任务
|
||||
• 私聊中: 必须使用 -g <群号> 或 -all 选项 (SUPERUSER)
|
||||
|
||||
📊 任务状态:
|
||||
定时任务 状态 <任务ID> 或 任务状态 <任务ID>
|
||||
• 查看单个任务的详细信息和状态
|
||||
|
||||
⚙️ 任务管理 (SUPERUSER):
|
||||
定时任务 设置 <插件> [时间选项] [-g <群号> | -g all] [--kwargs <参数>]
|
||||
定时任务 删除 <任务ID> | -p <插件> [-g <群号>] | -all
|
||||
定时任务 暂停 <任务ID> | -p <插件> [-g <群号>] | -all
|
||||
定时任务 恢复 <任务ID> | -p <插件> [-g <群号>] | -all
|
||||
定时任务 执行 <任务ID>
|
||||
定时任务 更新 <任务ID> [时间选项] [--kwargs <参数>]
|
||||
|
||||
📝 时间选项 (三选一):
|
||||
--cron "<分> <时> <日> <月> <周>" # 例: --cron "0 8 * * *"
|
||||
--interval <时间间隔> # 例: --interval 30m, 2h, 10s
|
||||
--date "<YYYY-MM-DD HH:MM:SS>" # 例: --date "2024-01-01 08:00:00"
|
||||
--daily "<HH:MM>" # 例: --daily "08:30"
|
||||
|
||||
📚 其他功能:
|
||||
定时任务 插件列表 # 查看所有可设置定时任务的插件 (SUPERUSER)
|
||||
|
||||
🏷️ 别名支持:
|
||||
查看: ls, list | 设置: add, 开启 | 删除: del, rm, remove, 关闭, 取消
|
||||
暂停: pause | 恢复: resume | 执行: trigger, run | 状态: status, info
|
||||
更新: update, modify, 修改 | 插件列表: plugins
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1.2",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
is_show=False,
|
||||
).to_dict(),
|
||||
)
|
||||
836
zhenxun/builtin_plugins/scheduler_admin/command.py
Normal file
@ -0,0 +1,836 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
from nonebot.adapters import Event
|
||||
from nonebot.adapters.onebot.v11 import Bot
|
||||
from nonebot.params import Depends
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaMatch,
|
||||
Args,
|
||||
Arparma,
|
||||
Match,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
on_alconna,
|
||||
)
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.utils._image_template import ImageTemplate
|
||||
from zhenxun.utils.manager.schedule_manager import scheduler_manager
|
||||
|
||||
|
||||
def _get_type_name(annotation) -> str:
|
||||
"""获取类型注解的名称"""
|
||||
if hasattr(annotation, "__name__"):
|
||||
return annotation.__name__
|
||||
elif hasattr(annotation, "_name"):
|
||||
return annotation._name
|
||||
else:
|
||||
return str(annotation)
|
||||
|
||||
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.rules import admin_check
|
||||
|
||||
|
||||
def _format_trigger(schedule_status: dict) -> str:
|
||||
"""将触发器配置格式化为人类可读的字符串"""
|
||||
trigger_type = schedule_status["trigger_type"]
|
||||
config = schedule_status["trigger_config"]
|
||||
|
||||
if trigger_type == "cron":
|
||||
minute = config.get("minute", "*")
|
||||
hour = config.get("hour", "*")
|
||||
day = config.get("day", "*")
|
||||
month = config.get("month", "*")
|
||||
day_of_week = config.get("day_of_week", "*")
|
||||
|
||||
if day == "*" and month == "*" and day_of_week == "*":
|
||||
formatted_hour = hour if hour == "*" else f"{int(hour):02d}"
|
||||
formatted_minute = minute if minute == "*" else f"{int(minute):02d}"
|
||||
return f"每天 {formatted_hour}:{formatted_minute}"
|
||||
else:
|
||||
return f"Cron: {minute} {hour} {day} {month} {day_of_week}"
|
||||
elif trigger_type == "interval":
|
||||
seconds = config.get("seconds", 0)
|
||||
minutes = config.get("minutes", 0)
|
||||
hours = config.get("hours", 0)
|
||||
days = config.get("days", 0)
|
||||
if days:
|
||||
trigger_str = f"每 {days} 天"
|
||||
elif hours:
|
||||
trigger_str = f"每 {hours} 小时"
|
||||
elif minutes:
|
||||
trigger_str = f"每 {minutes} 分钟"
|
||||
else:
|
||||
trigger_str = f"每 {seconds} 秒"
|
||||
elif trigger_type == "date":
|
||||
run_date = config.get("run_date", "未知时间")
|
||||
trigger_str = f"在 {run_date}"
|
||||
else:
|
||||
trigger_str = f"{trigger_type}: {config}"
|
||||
|
||||
return trigger_str
|
||||
|
||||
|
||||
def _format_params(schedule_status: dict) -> str:
|
||||
"""将任务参数格式化为人类可读的字符串"""
|
||||
if kwargs := schedule_status.get("job_kwargs"):
|
||||
kwargs_str = " | ".join(f"{k}: {v}" for k, v in kwargs.items())
|
||||
return kwargs_str
|
||||
return "-"
|
||||
|
||||
|
||||
def _parse_interval(interval_str: str) -> dict:
|
||||
"""增强版解析器,支持 d(天)"""
|
||||
match = re.match(r"(\d+)([smhd])", interval_str.lower())
|
||||
if not match:
|
||||
raise ValueError("时间间隔格式错误, 请使用如 '30m', '2h', '1d', '10s' 的格式。")
|
||||
|
||||
value, unit = int(match.group(1)), match.group(2)
|
||||
if unit == "s":
|
||||
return {"seconds": value}
|
||||
if unit == "m":
|
||||
return {"minutes": value}
|
||||
if unit == "h":
|
||||
return {"hours": value}
|
||||
if unit == "d":
|
||||
return {"days": value}
|
||||
return {}
|
||||
|
||||
|
||||
def _parse_daily_time(time_str: str) -> dict:
|
||||
"""解析 HH:MM 或 HH:MM:SS 格式的时间为 cron 配置"""
|
||||
if match := re.match(r"^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$", time_str):
|
||||
hour, minute, second = match.groups()
|
||||
hour, minute = int(hour), int(minute)
|
||||
|
||||
if not (0 <= hour <= 23 and 0 <= minute <= 59):
|
||||
raise ValueError("小时或分钟数值超出范围。")
|
||||
|
||||
cron_config = {
|
||||
"minute": str(minute),
|
||||
"hour": str(hour),
|
||||
"day": "*",
|
||||
"month": "*",
|
||||
"day_of_week": "*",
|
||||
}
|
||||
if second is not None:
|
||||
if not (0 <= int(second) <= 59):
|
||||
raise ValueError("秒数值超出范围。")
|
||||
cron_config["second"] = str(second)
|
||||
|
||||
return cron_config
|
||||
else:
|
||||
raise ValueError("时间格式错误,请使用 'HH:MM' 或 'HH:MM:SS' 格式。")
|
||||
|
||||
|
||||
async def GetBotId(
|
||||
bot: Bot,
|
||||
bot_id_match: Match[str] = AlconnaMatch("bot_id"),
|
||||
) -> str:
|
||||
"""获取要操作的Bot ID"""
|
||||
if bot_id_match.available:
|
||||
return bot_id_match.result
|
||||
return bot.self_id
|
||||
|
||||
|
||||
class ScheduleTarget:
|
||||
"""定时任务操作目标的基类"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TargetByID(ScheduleTarget):
|
||||
"""按任务ID操作"""
|
||||
|
||||
def __init__(self, id: int):
|
||||
self.id = id
|
||||
|
||||
|
||||
class TargetByPlugin(ScheduleTarget):
|
||||
"""按插件名操作"""
|
||||
|
||||
def __init__(
|
||||
self, plugin: str, group_id: str | None = None, all_groups: bool = False
|
||||
):
|
||||
self.plugin = plugin
|
||||
self.group_id = group_id
|
||||
self.all_groups = all_groups
|
||||
|
||||
|
||||
class TargetAll(ScheduleTarget):
|
||||
"""操作所有任务"""
|
||||
|
||||
def __init__(self, for_group: str | None = None):
|
||||
self.for_group = for_group
|
||||
|
||||
|
||||
TargetScope = TargetByID | TargetByPlugin | TargetAll | None
|
||||
|
||||
|
||||
def create_target_parser(subcommand_name: str):
|
||||
"""
|
||||
创建一个依赖注入函数,用于解析删除、暂停、恢复等命令的操作目标。
|
||||
"""
|
||||
|
||||
async def dependency(
|
||||
event: Event,
|
||||
schedule_id: Match[int] = AlconnaMatch("schedule_id"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
group_id: Match[str] = AlconnaMatch("group_id"),
|
||||
all_enabled: Query[bool] = Query(f"{subcommand_name}.all"),
|
||||
) -> TargetScope:
|
||||
if schedule_id.available:
|
||||
return TargetByID(schedule_id.result)
|
||||
|
||||
if plugin_name.available:
|
||||
p_name = plugin_name.result
|
||||
if all_enabled.available:
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
elif group_id.available:
|
||||
gid = group_id.result
|
||||
if gid.lower() == "all":
|
||||
return TargetByPlugin(plugin=p_name, all_groups=True)
|
||||
return TargetByPlugin(plugin=p_name, group_id=gid)
|
||||
else:
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if current_group_id:
|
||||
return TargetByPlugin(plugin=p_name, group_id=str(current_group_id))
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中操作插件任务必须使用 -g <群号> 或 -all 选项。"
|
||||
)
|
||||
|
||||
if all_enabled.available:
|
||||
return TargetAll(for_group=group_id.result if group_id.available else None)
|
||||
|
||||
return None
|
||||
|
||||
return dependency
|
||||
|
||||
|
||||
schedule_cmd = on_alconna(
|
||||
Alconna(
|
||||
"定时任务",
|
||||
Subcommand(
|
||||
"查看",
|
||||
Option("-g", Args["target_group_id", str]),
|
||||
Option("-all", help_text="查看所有群聊 (SUPERUSER)"),
|
||||
Option("-p", Args["plugin_name", str], help_text="按插件名筛选"),
|
||||
Option("--page", Args["page", int, 1], help_text="指定页码"),
|
||||
alias=["ls", "list"],
|
||||
help_text="查看定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"设置",
|
||||
Args["plugin_name", str],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="设置每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID或'all'"),
|
||||
Option("-all", help_text="对所有群生效 (等同于 -g all)"),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="设置任务参数"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["add", "开启"],
|
||||
help_text="设置/开启一个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"删除",
|
||||
Args["schedule_id?", int],
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID"),
|
||||
Option("-all", help_text="对所有群生效"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["del", "rm", "remove", "关闭", "取消"],
|
||||
help_text="删除一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"暂停",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["pause"],
|
||||
help_text="暂停一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"恢复",
|
||||
Args["schedule_id?", int],
|
||||
Option("-all", help_text="对当前群所有任务生效"),
|
||||
Option("-p", Args["plugin_name", str], help_text="指定插件名"),
|
||||
Option("-g", Args["group_id", str], help_text="指定群组ID (SUPERUSER)"),
|
||||
Option(
|
||||
"--bot", Args["bot_id", str], help_text="指定操作的Bot ID (SUPERUSER)"
|
||||
),
|
||||
alias=["resume"],
|
||||
help_text="恢复一个或多个定时任务",
|
||||
),
|
||||
Subcommand(
|
||||
"执行",
|
||||
Args["schedule_id", int],
|
||||
alias=["trigger", "run"],
|
||||
help_text="立即执行一次任务",
|
||||
),
|
||||
Subcommand(
|
||||
"更新",
|
||||
Args["schedule_id", int],
|
||||
Option("--cron", Args["cron_expr", str], help_text="设置 cron 表达式"),
|
||||
Option("--interval", Args["interval_expr", str], help_text="设置时间间隔"),
|
||||
Option("--date", Args["date_expr", str], help_text="设置特定执行日期"),
|
||||
Option(
|
||||
"--daily",
|
||||
Args["daily_expr", str],
|
||||
help_text="更新每天执行的时间 (如 08:20)",
|
||||
),
|
||||
Option("--kwargs", Args["kwargs_str", str], help_text="更新参数"),
|
||||
alias=["update", "modify", "修改"],
|
||||
help_text="更新任务配置",
|
||||
),
|
||||
Subcommand(
|
||||
"状态",
|
||||
Args["schedule_id", int],
|
||||
alias=["status", "info"],
|
||||
help_text="查看单个任务的详细状态",
|
||||
),
|
||||
Subcommand(
|
||||
"插件列表",
|
||||
alias=["plugins"],
|
||||
help_text="列出所有可用的插件",
|
||||
),
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
rule=admin_check(1),
|
||||
)
|
||||
|
||||
schedule_cmd.shortcut(
|
||||
"任务状态",
|
||||
command="定时任务",
|
||||
arguments=["状态", "{%0}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.handle()
|
||||
async def _handle_time_options_mutex(arp: Arparma):
|
||||
time_options = ["cron", "interval", "date", "daily"]
|
||||
provided_options = [opt for opt in time_options if arp.query(opt) is not None]
|
||||
if len(provided_options) > 1:
|
||||
await schedule_cmd.finish(
|
||||
f"时间选项 --{', --'.join(provided_options)} 不能同时使用,请只选择一个。"
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.assign("查看")
|
||||
async def _(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
target_group_id: Match[str] = AlconnaMatch("target_group_id"),
|
||||
all_groups: Query[bool] = Query("查看.all"),
|
||||
plugin_name: Match[str] = AlconnaMatch("plugin_name"),
|
||||
page: Match[int] = AlconnaMatch("page"),
|
||||
):
|
||||
is_superuser = await SUPERUSER(bot, event)
|
||||
schedules = []
|
||||
title = ""
|
||||
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
if not (all_groups.available or target_group_id.available) and not current_group_id:
|
||||
await schedule_cmd.finish("私聊中查看任务必须使用 -g <群号> 或 -all 选项。")
|
||||
|
||||
if all_groups.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看所有群组的定时任务。")
|
||||
schedules = await scheduler_manager.get_all_schedules()
|
||||
title = "所有群组的定时任务"
|
||||
elif target_group_id.available:
|
||||
if not is_superuser:
|
||||
await schedule_cmd.finish("需要超级用户权限才能查看指定群组的定时任务。")
|
||||
gid = target_group_id.result
|
||||
schedules = [
|
||||
s for s in await scheduler_manager.get_all_schedules() if s.group_id == gid
|
||||
]
|
||||
title = f"群 {gid} 的定时任务"
|
||||
else:
|
||||
gid = str(current_group_id)
|
||||
schedules = [
|
||||
s for s in await scheduler_manager.get_all_schedules() if s.group_id == gid
|
||||
]
|
||||
title = "本群的定时任务"
|
||||
|
||||
if plugin_name.available:
|
||||
schedules = [s for s in schedules if s.plugin_name == plugin_name.result]
|
||||
title += f" [插件: {plugin_name.result}]"
|
||||
|
||||
if not schedules:
|
||||
await schedule_cmd.finish("没有找到任何相关的定时任务。")
|
||||
|
||||
page_size = 15
|
||||
current_page = page.result
|
||||
total_items = len(schedules)
|
||||
total_pages = (total_items + page_size - 1) // page_size
|
||||
start_index = (current_page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
paginated_schedules = schedules[start_index:end_index]
|
||||
|
||||
if not paginated_schedules:
|
||||
await schedule_cmd.finish("这一页没有内容了哦~")
|
||||
|
||||
status_tasks = [
|
||||
scheduler_manager.get_schedule_status(s.id) for s in paginated_schedules
|
||||
]
|
||||
all_statuses = await asyncio.gather(*status_tasks)
|
||||
data_list = [
|
||||
[
|
||||
s["id"],
|
||||
s["plugin_name"],
|
||||
s.get("bot_id") or "N/A",
|
||||
s["group_id"] or "全局",
|
||||
s["next_run_time"],
|
||||
_format_trigger(s),
|
||||
_format_params(s),
|
||||
"✔️ 已启用" if s["is_enabled"] else "⏸️ 已暂停",
|
||||
]
|
||||
for s in all_statuses
|
||||
if s
|
||||
]
|
||||
|
||||
if not data_list:
|
||||
await schedule_cmd.finish("没有找到任何相关的定时任务。")
|
||||
|
||||
img = await ImageTemplate.table_page(
|
||||
head_text=title,
|
||||
tip_text=f"第 {current_page}/{total_pages} 页,共 {total_items} 条任务",
|
||||
column_name=[
|
||||
"ID",
|
||||
"插件",
|
||||
"Bot ID",
|
||||
"群组/目标",
|
||||
"下次运行",
|
||||
"触发规则",
|
||||
"参数",
|
||||
"状态",
|
||||
],
|
||||
data_list=data_list,
|
||||
column_space=20,
|
||||
)
|
||||
await MessageUtils.build_message(img).send(reply_to=True)
|
||||
|
||||
|
||||
@schedule_cmd.assign("设置")
|
||||
async def _(
|
||||
event: Event,
|
||||
plugin_name: str,
|
||||
cron_expr: str | None = None,
|
||||
interval_expr: str | None = None,
|
||||
date_expr: str | None = None,
|
||||
daily_expr: str | None = None,
|
||||
group_id: str | None = None,
|
||||
kwargs_str: str | None = None,
|
||||
all_enabled: Query[bool] = Query("设置.all"),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if plugin_name not in scheduler_manager._registered_tasks:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 没有注册可用的定时任务。\n"
|
||||
f"可用插件: {list(scheduler_manager._registered_tasks.keys())}"
|
||||
)
|
||||
|
||||
trigger_type = ""
|
||||
trigger_config = {}
|
||||
|
||||
try:
|
||||
if cron_expr:
|
||||
trigger_type = "cron"
|
||||
parts = cron_expr.split()
|
||||
if len(parts) != 5:
|
||||
raise ValueError("Cron 表达式必须有5个部分 (分 时 日 月 周)")
|
||||
cron_keys = ["minute", "hour", "day", "month", "day_of_week"]
|
||||
trigger_config = dict(zip(cron_keys, parts))
|
||||
elif interval_expr:
|
||||
trigger_type = "interval"
|
||||
trigger_config = _parse_interval(interval_expr)
|
||||
elif date_expr:
|
||||
trigger_type = "date"
|
||||
trigger_config = {"run_date": datetime.fromisoformat(date_expr)}
|
||||
elif daily_expr:
|
||||
trigger_type = "cron"
|
||||
trigger_config = _parse_daily_time(daily_expr)
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"必须提供一种时间选项: --cron, --interval, --date, 或 --daily。"
|
||||
)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
job_kwargs = {}
|
||||
if kwargs_str:
|
||||
task_meta = scheduler_manager._registered_tasks[plugin_name]
|
||||
params_model = task_meta.get("model")
|
||||
if not params_model:
|
||||
await schedule_cmd.finish(f"插件 '{plugin_name}' 不支持设置额外参数。")
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
await schedule_cmd.finish(f"插件 '{plugin_name}' 的参数模型配置错误。")
|
||||
|
||||
raw_kwargs = {}
|
||||
try:
|
||||
for item in kwargs_str.split(","):
|
||||
key, value = item.strip().split("=", 1)
|
||||
raw_kwargs[key.strip()] = value
|
||||
except Exception as e:
|
||||
await schedule_cmd.finish(
|
||||
f"参数格式错误,请使用 'key=value,key2=value2' 格式。错误: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
model_validate = getattr(params_model, "model_validate", None)
|
||||
if not model_validate:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的参数模型不支持验证。"
|
||||
)
|
||||
return
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的参数模型不支持导出。"
|
||||
)
|
||||
return
|
||||
|
||||
job_kwargs = model_dump()
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
error_str = "\n".join(errors)
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{plugin_name}' 的任务参数验证失败:\n{error_str}"
|
||||
)
|
||||
return
|
||||
|
||||
target_group_id: str | None
|
||||
current_group_id = getattr(event, "group_id", None)
|
||||
|
||||
if group_id and group_id.lower() == "all":
|
||||
target_group_id = "__ALL_GROUPS__"
|
||||
elif all_enabled.available:
|
||||
target_group_id = "__ALL_GROUPS__"
|
||||
elif group_id:
|
||||
target_group_id = group_id
|
||||
elif current_group_id:
|
||||
target_group_id = str(current_group_id)
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"私聊中设置定时任务时,必须使用 -g <群号> 或 --all 选项指定目标。"
|
||||
)
|
||||
return
|
||||
|
||||
success, msg = await scheduler_manager.add_schedule(
|
||||
plugin_name,
|
||||
target_group_id,
|
||||
trigger_type,
|
||||
trigger_config,
|
||||
job_kwargs,
|
||||
bot_id=bot_id_to_operate,
|
||||
)
|
||||
|
||||
if target_group_id == "__ALL_GROUPS__":
|
||||
target_desc = f"所有群组 (Bot: {bot_id_to_operate})"
|
||||
elif target_group_id is None:
|
||||
target_desc = "全局"
|
||||
else:
|
||||
target_desc = f"群组 {target_group_id}"
|
||||
|
||||
if success:
|
||||
await schedule_cmd.finish(f"已成功为 [{target_desc}] {msg}")
|
||||
else:
|
||||
await schedule_cmd.finish(f"为 [{target_desc}] 设置任务失败: {msg}")
|
||||
|
||||
|
||||
@schedule_cmd.assign("删除")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("删除")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.remove_schedule_by_id(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
removed_count = await scheduler_manager.remove_schedule_for_all(
|
||||
p_name, bot_id=bot_id_to_operate
|
||||
)
|
||||
message = (
|
||||
f"已取消了 {removed_count} 个群组的插件 '{p_name}' 定时任务。"
|
||||
if removed_count > 0
|
||||
else f"没有找到插件 '{p_name}' 的定时任务。"
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.remove_schedule(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.remove_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.remove_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish(
|
||||
"删除任务失败:请提供任务ID,或通过 -p <插件> 或 -all 指定要删除的任务。"
|
||||
)
|
||||
|
||||
|
||||
@schedule_cmd.assign("暂停")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("暂停")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.pause_schedule(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
_, message = await scheduler_manager.pause_schedules_by_plugin(p_name)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.pause_schedule_by_plugin_group(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.pause_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.pause_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish("请提供任务ID、使用 -p <插件> 或 -all 选项。")
|
||||
|
||||
|
||||
@schedule_cmd.assign("恢复")
|
||||
async def _(
|
||||
target: TargetScope = Depends(create_target_parser("恢复")),
|
||||
bot_id_to_operate: str = Depends(GetBotId),
|
||||
):
|
||||
if isinstance(target, TargetByID):
|
||||
_, message = await scheduler_manager.resume_schedule(target.id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetByPlugin):
|
||||
p_name = target.plugin
|
||||
if p_name not in scheduler_manager.get_registered_plugins():
|
||||
await schedule_cmd.finish(f"未找到插件 '{p_name}'。")
|
||||
|
||||
if target.all_groups:
|
||||
_, message = await scheduler_manager.resume_schedules_by_plugin(p_name)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.resume_schedule_by_plugin_group(
|
||||
p_name, target.group_id, bot_id=bot_id_to_operate
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
elif isinstance(target, TargetAll):
|
||||
if target.for_group:
|
||||
_, message = await scheduler_manager.resume_schedules_by_group(
|
||||
target.for_group
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
else:
|
||||
_, message = await scheduler_manager.resume_all_schedules()
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
else:
|
||||
await schedule_cmd.finish("请提供任务ID、使用 -p <插件> 或 -all 选项。")
|
||||
|
||||
|
||||
@schedule_cmd.assign("执行")
|
||||
async def _(schedule_id: int):
|
||||
_, message = await scheduler_manager.trigger_now(schedule_id)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("更新")
|
||||
async def _(
|
||||
schedule_id: int,
|
||||
cron_expr: str | None = None,
|
||||
interval_expr: str | None = None,
|
||||
date_expr: str | None = None,
|
||||
daily_expr: str | None = None,
|
||||
kwargs_str: str | None = None,
|
||||
):
|
||||
if not any([cron_expr, interval_expr, date_expr, daily_expr, kwargs_str]):
|
||||
await schedule_cmd.finish(
|
||||
"请提供需要更新的时间 (--cron/--interval/--date/--daily) 或参数 (--kwargs)"
|
||||
)
|
||||
|
||||
trigger_config = None
|
||||
trigger_type = None
|
||||
try:
|
||||
if cron_expr:
|
||||
trigger_type = "cron"
|
||||
parts = cron_expr.split()
|
||||
if len(parts) != 5:
|
||||
raise ValueError("Cron 表达式必须有5个部分")
|
||||
cron_keys = ["minute", "hour", "day", "month", "day_of_week"]
|
||||
trigger_config = dict(zip(cron_keys, parts))
|
||||
elif interval_expr:
|
||||
trigger_type = "interval"
|
||||
trigger_config = _parse_interval(interval_expr)
|
||||
elif date_expr:
|
||||
trigger_type = "date"
|
||||
trigger_config = {"run_date": datetime.fromisoformat(date_expr)}
|
||||
elif daily_expr:
|
||||
trigger_type = "cron"
|
||||
trigger_config = _parse_daily_time(daily_expr)
|
||||
except ValueError as e:
|
||||
await schedule_cmd.finish(f"时间参数解析错误: {e}")
|
||||
|
||||
job_kwargs = None
|
||||
if kwargs_str:
|
||||
schedule = await scheduler_manager.get_schedule_by_id(schedule_id)
|
||||
if not schedule:
|
||||
await schedule_cmd.finish(f"未找到 ID 为 {schedule_id} 的任务。")
|
||||
|
||||
task_meta = scheduler_manager._registered_tasks.get(schedule.plugin_name)
|
||||
if not task_meta or not (params_model := task_meta.get("model")):
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 未定义参数模型,无法更新参数。"
|
||||
)
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型配置错误。"
|
||||
)
|
||||
|
||||
raw_kwargs = {}
|
||||
try:
|
||||
for item in kwargs_str.split(","):
|
||||
key, value = item.strip().split("=", 1)
|
||||
raw_kwargs[key.strip()] = value
|
||||
except Exception as e:
|
||||
await schedule_cmd.finish(
|
||||
f"参数格式错误,请使用 'key=value,key2=value2' 格式。错误: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
model_validate = getattr(params_model, "model_validate", None)
|
||||
if not model_validate:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型不支持验证。"
|
||||
)
|
||||
return
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(
|
||||
f"插件 '{schedule.plugin_name}' 的参数模型不支持导出。"
|
||||
)
|
||||
return
|
||||
|
||||
job_kwargs = model_dump(exclude_unset=True)
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
error_str = "\n".join(errors)
|
||||
await schedule_cmd.finish(f"更新的参数验证失败:\n{error_str}")
|
||||
return
|
||||
|
||||
_, message = await scheduler_manager.update_schedule(
|
||||
schedule_id, trigger_type, trigger_config, job_kwargs
|
||||
)
|
||||
await schedule_cmd.finish(message)
|
||||
|
||||
|
||||
@schedule_cmd.assign("插件列表")
|
||||
async def _():
|
||||
registered_plugins = scheduler_manager.get_registered_plugins()
|
||||
if not registered_plugins:
|
||||
await schedule_cmd.finish("当前没有已注册的定时任务插件。")
|
||||
|
||||
message_parts = ["📋 已注册的定时任务插件:"]
|
||||
for i, plugin_name in enumerate(registered_plugins, 1):
|
||||
task_meta = scheduler_manager._registered_tasks[plugin_name]
|
||||
params_model = task_meta.get("model")
|
||||
|
||||
if not params_model:
|
||||
message_parts.append(f"{i}. {plugin_name} - 无参数")
|
||||
continue
|
||||
|
||||
if not (isinstance(params_model, type) and issubclass(params_model, BaseModel)):
|
||||
message_parts.append(f"{i}. {plugin_name} - ⚠️ 参数模型配置错误")
|
||||
continue
|
||||
|
||||
model_fields = getattr(params_model, "model_fields", None)
|
||||
if model_fields:
|
||||
param_info = ", ".join(
|
||||
f"{field_name}({_get_type_name(field_info.annotation)})"
|
||||
for field_name, field_info in model_fields.items()
|
||||
)
|
||||
message_parts.append(f"{i}. {plugin_name} - 参数: {param_info}")
|
||||
else:
|
||||
message_parts.append(f"{i}. {plugin_name} - 无参数")
|
||||
|
||||
await schedule_cmd.finish("\n".join(message_parts))
|
||||
|
||||
|
||||
@schedule_cmd.assign("状态")
|
||||
async def _(schedule_id: int):
|
||||
status = await scheduler_manager.get_schedule_status(schedule_id)
|
||||
if not status:
|
||||
await schedule_cmd.finish(f"未找到ID为 {schedule_id} 的定时任务。")
|
||||
|
||||
info_lines = [
|
||||
f"📋 定时任务详细信息 (ID: {schedule_id})",
|
||||
"--------------------",
|
||||
f"▫️ 插件: {status['plugin_name']}",
|
||||
f"▫️ Bot ID: {status.get('bot_id') or '默认'}",
|
||||
f"▫️ 目标: {status['group_id'] or '全局'}",
|
||||
f"▫️ 状态: {'✔️ 已启用' if status['is_enabled'] else '⏸️ 已暂停'}",
|
||||
f"▫️ 下次运行: {status['next_run_time']}",
|
||||
f"▫️ 触发规则: {_format_trigger(status)}",
|
||||
f"▫️ 任务参数: {_format_params(status)}",
|
||||
]
|
||||
await schedule_cmd.finish("\n".join(info_lines))
|
||||
@ -1,67 +1,8 @@
|
||||
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.services.log import logger
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
|
||||
driver: Driver = nonebot.get_driver()
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
|
||||
@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
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
"""开启/禁用插件格式修改"""
|
||||
_, is_create = await GroupConsole.get_or_create(group_id=133133133)
|
||||
|
||||
@ -5,7 +5,9 @@ from nonebot_plugin_alconna import (
|
||||
AlconnaQuery,
|
||||
Args,
|
||||
Arparma,
|
||||
At,
|
||||
Match,
|
||||
MultiVar,
|
||||
Option,
|
||||
Query,
|
||||
Subcommand,
|
||||
@ -33,6 +35,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
usage="""
|
||||
商品操作
|
||||
指令:
|
||||
商店
|
||||
我的金币
|
||||
我的道具
|
||||
使用道具 [名称/Id]
|
||||
@ -46,6 +49,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
plugin_type=PluginType.NORMAL,
|
||||
menu_type="商店",
|
||||
commands=[
|
||||
Command(command="商店"),
|
||||
Command(command="我的金币"),
|
||||
Command(command="我的道具"),
|
||||
Command(command="购买道具"),
|
||||
@ -74,13 +78,21 @@ _matcher = on_alconna(
|
||||
Subcommand("my-cost", help_text="我的金币"),
|
||||
Subcommand("my-props", 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="金币排行"),
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_use_matcher = on_alconna(
|
||||
Alconna(
|
||||
"使用道具",
|
||||
Args["name?", str]["num?", int]["at_users?", MultiVar(At)],
|
||||
),
|
||||
priority=5,
|
||||
block=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"我的金币",
|
||||
command="商店",
|
||||
@ -102,13 +114,6 @@ _matcher.shortcut(
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"使用道具(?P<name>.*?)",
|
||||
command="商店",
|
||||
arguments=["use", "{name}"],
|
||||
prefix=True,
|
||||
)
|
||||
|
||||
_matcher.shortcut(
|
||||
"金币排行",
|
||||
command="商店",
|
||||
@ -172,7 +177,7 @@ async def _(
|
||||
await MessageUtils.build_message(result).send(reply_to=True)
|
||||
|
||||
|
||||
@_matcher.assign("use")
|
||||
@_use_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
@ -181,6 +186,7 @@ async def _(
|
||||
arparma: Arparma,
|
||||
name: Match[str],
|
||||
num: Query[int] = AlconnaQuery("num", 1),
|
||||
at_users: Query[list[At]] = AlconnaQuery("at_users", []),
|
||||
):
|
||||
if not name.available:
|
||||
await MessageUtils.build_message(
|
||||
@ -188,7 +194,7 @@ async def _(
|
||||
).finish(reply_to=True)
|
||||
try:
|
||||
result = await ShopManage.use(
|
||||
bot, event, session, message, name.result, num.result, ""
|
||||
bot, event, session, message, name.result, num.result, "", at_users.result
|
||||
)
|
||||
logger.info(
|
||||
f"使用道具 {name.result}, 数量: {num.result}",
|
||||
|
||||
@ -8,7 +8,7 @@ from typing import Any, Literal
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.compat import model_dump
|
||||
from nonebot_plugin_alconna import UniMessage, UniMsg
|
||||
from nonebot_plugin_alconna import At, UniMessage, UniMsg
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from tortoise.expressions import Q
|
||||
@ -48,6 +48,10 @@ class Goods(BaseModel):
|
||||
"""model"""
|
||||
session: Uninfo | None = None
|
||||
"""Uninfo"""
|
||||
at_user: str | None = None
|
||||
"""At对象"""
|
||||
at_users: list[str] = []
|
||||
"""At对象列表"""
|
||||
|
||||
|
||||
class ShopParam(BaseModel):
|
||||
@ -73,6 +77,10 @@ class ShopParam(BaseModel):
|
||||
"""Uninfo"""
|
||||
message: UniMsg
|
||||
"""UniMessage"""
|
||||
at_user: str | None = None
|
||||
"""At对象"""
|
||||
at_users: list[str] = []
|
||||
"""At对象列表"""
|
||||
extra_data: dict[str, Any] = Field(default_factory=dict)
|
||||
"""额外数据"""
|
||||
|
||||
@ -156,6 +164,7 @@ class ShopManage:
|
||||
goods: Goods,
|
||||
num: int,
|
||||
text: str,
|
||||
at_users: list[str] = [],
|
||||
) -> tuple[ShopParam, dict[str, Any]]:
|
||||
"""构造参数
|
||||
|
||||
@ -165,6 +174,7 @@ class ShopManage:
|
||||
goods_name: 商品名称
|
||||
num: 数量
|
||||
text: 其他信息
|
||||
at_users: at用户
|
||||
"""
|
||||
group_id = None
|
||||
if session.group:
|
||||
@ -172,6 +182,7 @@ class ShopManage:
|
||||
session.group.parent.id if session.group.parent else session.group.id
|
||||
)
|
||||
_kwargs = goods.params
|
||||
at_user = at_users[0] if at_users else None
|
||||
model = goods.model(
|
||||
**{
|
||||
"goods_name": goods.name,
|
||||
@ -183,6 +194,8 @@ class ShopManage:
|
||||
"text": text,
|
||||
"session": session,
|
||||
"message": message,
|
||||
"at_user": at_user,
|
||||
"at_users": at_users,
|
||||
}
|
||||
)
|
||||
return model, {
|
||||
@ -194,6 +207,8 @@ class ShopManage:
|
||||
"num": num,
|
||||
"text": text,
|
||||
"goods_name": goods.name,
|
||||
"at_user": at_user,
|
||||
"at_users": at_users,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -223,6 +238,7 @@ class ShopManage:
|
||||
**param.extra_data,
|
||||
"session": session,
|
||||
"message": message,
|
||||
"shop_param": ShopParam,
|
||||
}
|
||||
for key in list(param_json.keys()):
|
||||
if key not in args:
|
||||
@ -308,6 +324,7 @@ class ShopManage:
|
||||
goods_name: str,
|
||||
num: int,
|
||||
text: str,
|
||||
at_users: list[At] = [],
|
||||
) -> str | UniMessage | None:
|
||||
"""使用道具
|
||||
|
||||
@ -319,6 +336,7 @@ class ShopManage:
|
||||
goods_name: 商品名称
|
||||
num: 使用数量
|
||||
text: 其他信息
|
||||
at_users: at用户
|
||||
|
||||
返回:
|
||||
str | MessageFactory | None: 使用完成后返回信息
|
||||
@ -339,16 +357,18 @@ class ShopManage:
|
||||
goods = cls.uuid2goods.get(goods_info.uuid)
|
||||
if not goods or not goods.func:
|
||||
return f"{goods_info.goods_name} 未注册使用函数, 无法使用..."
|
||||
at_user_ids = [at.target for at in at_users]
|
||||
param, kwargs = cls.__build_params(
|
||||
bot, event, session, message, goods, num, text
|
||||
bot, event, session, message, goods, num, text, at_user_ids
|
||||
)
|
||||
if num > 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)
|
||||
result = await cls.__run(goods, param, session, message, **kwargs)
|
||||
await UserConsole.use_props(
|
||||
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)
|
||||
if not result and param.send_success_msg:
|
||||
result = f"使用道具 {goods.name} {num} 次成功!"
|
||||
@ -479,10 +499,13 @@ class ShopManage:
|
||||
if not user.props:
|
||||
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_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 = []
|
||||
for i, prop_uuid in enumerate(user.props):
|
||||
|
||||
@ -10,7 +10,6 @@ from nonebot_plugin_alconna import (
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.configs.utils import (
|
||||
Command,
|
||||
@ -23,7 +22,7 @@ from zhenxun.utils.depends import UserName
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import SignManage
|
||||
from .goods_register import driver # noqa: F401
|
||||
from .goods_register import Uninfo
|
||||
from .utils import clear_sign_data_pic
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import nonebot
|
||||
from nonebot.drivers import Driver
|
||||
from nonebot_plugin_uninfo import Uninfo
|
||||
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
@ -9,14 +8,7 @@ from zhenxun.models.user_console import UserConsole
|
||||
from zhenxun.utils.decorator.shop import shop_register
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
driver: Driver = nonebot.get_driver()
|
||||
|
||||
|
||||
# @driver.on_startup
|
||||
# async def _():
|
||||
# """
|
||||
# 导入内置的三个商品
|
||||
# """
|
||||
driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@shop_register(
|
||||
|
||||
@ -16,6 +16,7 @@ from zhenxun.models.sign_log import SignLog
|
||||
from zhenxun.models.sign_user import SignUser
|
||||
from zhenxun.utils.http_utils import AsyncHttpx
|
||||
from zhenxun.utils.image_utils import BuildImage
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .config import (
|
||||
@ -54,7 +55,7 @@ LG_MESSAGE = [
|
||||
]
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def init_image():
|
||||
SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True)
|
||||
SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
@ -53,10 +53,7 @@ async def _(
|
||||
)
|
||||
|
||||
|
||||
@scheduler.scheduled_job(
|
||||
"interval",
|
||||
minutes=1,
|
||||
)
|
||||
@scheduler.scheduled_job("interval", minutes=1, max_instances=5)
|
||||
async def _():
|
||||
try:
|
||||
call_list = TEMP_LIST.copy()
|
||||
|
||||
@ -110,7 +110,7 @@ async def enable_plugin(
|
||||
)
|
||||
await BotConsole.enable_plugin(None, plugin.module)
|
||||
await MessageUtils.build_message(
|
||||
f"已禁用全部 bot 的插件: {plugin_name.result}"
|
||||
f"已开启全部 bot 的插件: {plugin_name.result}"
|
||||
).finish()
|
||||
elif bot_id.available:
|
||||
logger.info(
|
||||
|
||||
@ -92,7 +92,7 @@ async def enable_task(
|
||||
)
|
||||
await BotConsole.enable_task(None, task.module)
|
||||
await MessageUtils.build_message(
|
||||
f"已禁用全部 bot 的被动: {task_name.available}"
|
||||
f"已开启全部 bot 的被动: {task_name.available}"
|
||||
).finish()
|
||||
elif bot_id.available:
|
||||
logger.info(
|
||||
|
||||
@ -1,32 +1,77 @@
|
||||
from typing import Annotated
|
||||
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.params import Command
|
||||
from arclet.alconna import AllParam
|
||||
from nepattern import UnionPattern
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
from nonebot_plugin_alconna import Text as alcText
|
||||
from nonebot_plugin_alconna import UniMsg
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
Args,
|
||||
on_alconna,
|
||||
)
|
||||
from nonebot_plugin_alconna.uniseg.segment import (
|
||||
At,
|
||||
AtAll,
|
||||
Audio,
|
||||
Button,
|
||||
Emoji,
|
||||
File,
|
||||
Hyper,
|
||||
Image,
|
||||
Keyboard,
|
||||
Reference,
|
||||
Reply,
|
||||
Text,
|
||||
Video,
|
||||
Voice,
|
||||
)
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import PluginType
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from ._data_source import BroadcastManage
|
||||
from .broadcast_manager import BroadcastManager
|
||||
from .message_processor import (
|
||||
_extract_broadcast_content,
|
||||
get_broadcast_target_groups,
|
||||
send_broadcast_and_notify,
|
||||
)
|
||||
|
||||
BROADCAST_SEND_DELAY_RANGE = (1, 3)
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="广播",
|
||||
description="昭告天下!",
|
||||
usage="""
|
||||
广播 [消息] [图片]
|
||||
示例:广播 你们好!
|
||||
广播 [消息内容]
|
||||
- 直接发送消息到除当前群组外的所有群组
|
||||
- 支持文本、图片、@、表情、视频等多种消息类型
|
||||
- 示例:广播 你们好!
|
||||
- 示例:广播 [图片] 新活动开始啦!
|
||||
|
||||
广播 + 引用消息
|
||||
- 将引用的消息作为广播内容发送
|
||||
- 支持引用普通消息或合并转发消息
|
||||
- 示例:(引用一条消息) 广播
|
||||
|
||||
广播撤回
|
||||
- 撤回最近一次由您触发的广播消息
|
||||
- 仅能撤回短时间内的消息
|
||||
- 示例:广播撤回
|
||||
|
||||
特性:
|
||||
- 在群组中使用广播时,不会将消息发送到当前群组
|
||||
- 在私聊中使用广播时,会发送到所有群组
|
||||
|
||||
别名:
|
||||
- bc (广播的简写)
|
||||
- recall (广播撤回的别名)
|
||||
""".strip(),
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
version="1.2",
|
||||
plugin_type=PluginType.SUPERUSER,
|
||||
configs=[
|
||||
RegisterConfig(
|
||||
@ -42,26 +87,106 @@ __plugin_meta__ = PluginMetadata(
|
||||
).to_dict(),
|
||||
)
|
||||
|
||||
_matcher = on_command(
|
||||
"广播", priority=1, permission=SUPERUSER, block=True, rule=to_me()
|
||||
AnySeg = (
|
||||
UnionPattern(
|
||||
[
|
||||
Text,
|
||||
Image,
|
||||
At,
|
||||
AtAll,
|
||||
Audio,
|
||||
Video,
|
||||
File,
|
||||
Emoji,
|
||||
Reply,
|
||||
Reference,
|
||||
Hyper,
|
||||
Button,
|
||||
Keyboard,
|
||||
Voice,
|
||||
]
|
||||
)
|
||||
@ "AnySeg"
|
||||
)
|
||||
|
||||
_matcher = on_alconna(
|
||||
Alconna(
|
||||
"广播",
|
||||
Args["content?", AllParam],
|
||||
),
|
||||
aliases={"bc"},
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
rule=to_me(),
|
||||
use_origin=False,
|
||||
)
|
||||
|
||||
_recall_matcher = on_alconna(
|
||||
Alconna("广播撤回"),
|
||||
aliases={"recall"},
|
||||
priority=1,
|
||||
permission=SUPERUSER,
|
||||
block=True,
|
||||
rule=to_me(),
|
||||
)
|
||||
|
||||
|
||||
@_matcher.handle()
|
||||
async def _(
|
||||
async def handle_broadcast(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: EventSession,
|
||||
message: UniMsg,
|
||||
command: Annotated[tuple[str, ...], Command()],
|
||||
arp: alc.Arparma,
|
||||
):
|
||||
for msg in message:
|
||||
if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]):
|
||||
msg.text = msg.text.replace(command[0], "", 1).strip()
|
||||
break
|
||||
await MessageUtils.build_message("正在发送..请等一下哦!").send()
|
||||
count, error_count = await BroadcastManage.send(bot, message, session)
|
||||
result = f"成功广播 {count} 个群组"
|
||||
broadcast_content_msg = await _extract_broadcast_content(bot, event, arp, session)
|
||||
if not broadcast_content_msg:
|
||||
return
|
||||
|
||||
target_groups, enabled_groups = await get_broadcast_target_groups(bot, session)
|
||||
if not target_groups or not enabled_groups:
|
||||
return
|
||||
|
||||
try:
|
||||
await send_broadcast_and_notify(
|
||||
bot, event, broadcast_content_msg, enabled_groups, target_groups, session
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = "发送广播失败"
|
||||
BroadcastManager.log_error(error_msg, e, session)
|
||||
await MessageUtils.build_message(f"{error_msg}。").send(reply_to=True)
|
||||
|
||||
|
||||
@_recall_matcher.handle()
|
||||
async def handle_broadcast_recall(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: EventSession,
|
||||
):
|
||||
"""处理广播撤回命令"""
|
||||
await MessageUtils.build_message("正在尝试撤回最近一次广播...").send()
|
||||
|
||||
try:
|
||||
success_count, error_count = await BroadcastManager.recall_last_broadcast(
|
||||
bot, session
|
||||
)
|
||||
|
||||
user_id = str(event.get_user_id())
|
||||
if success_count == 0 and error_count == 0:
|
||||
await bot.send_private_msg(
|
||||
user_id=user_id,
|
||||
message="没有找到最近的广播消息记录,可能已经撤回或超过可撤回时间。",
|
||||
)
|
||||
else:
|
||||
result = f"广播撤回完成!\n成功撤回 {success_count} 条消息"
|
||||
if error_count:
|
||||
result += f"\n广播失败 {error_count} 个群组"
|
||||
await MessageUtils.build_message(f"发送广播完成!\n{result}").send(reply_to=True)
|
||||
logger.info(f"发送广播信息: {message}", "广播", session=session)
|
||||
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}。")
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot_plugin_alconna import Image, UniMsg
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
|
||||
class BroadcastManage:
|
||||
@classmethod
|
||||
async def send(
|
||||
cls, bot: Bot, message: UniMsg, session: EventSession
|
||||
) -> tuple[int, int]:
|
||||
"""发送广播消息
|
||||
|
||||
参数:
|
||||
bot: Bot
|
||||
message: 消息内容
|
||||
session: Session
|
||||
|
||||
返回:
|
||||
tuple[int, int]: 发送成功的群组数量, 发送失败的群组数量
|
||||
"""
|
||||
message_list = []
|
||||
for msg in message:
|
||||
if isinstance(msg, alc.Image) and msg.url:
|
||||
message_list.append(Image(url=msg.url))
|
||||
elif isinstance(msg, alc.Text):
|
||||
message_list.append(msg.text)
|
||||
group_list, _ = await PlatformUtils.get_group_list(bot)
|
||||
if group_list:
|
||||
error_count = 0
|
||||
for group in group_list:
|
||||
try:
|
||||
if not await CommonUtils.task_is_block(
|
||||
bot,
|
||||
"broadcast", # group.channel_id
|
||||
group.group_id,
|
||||
):
|
||||
target = PlatformUtils.get_target(
|
||||
group_id=group.group_id, channel_id=group.channel_id
|
||||
)
|
||||
if target:
|
||||
await MessageUtils.build_message(message_list).send(
|
||||
target, bot
|
||||
)
|
||||
logger.debug(
|
||||
"发送成功",
|
||||
"广播",
|
||||
session=session,
|
||||
target=f"{group.group_id}:{group.channel_id}",
|
||||
)
|
||||
await asyncio.sleep(random.randint(1, 3))
|
||||
else:
|
||||
logger.warning("target为空", "广播", session=session)
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(
|
||||
"发送失败",
|
||||
"广播",
|
||||
session=session,
|
||||
target=f"{group.group_id}:{group.channel_id}",
|
||||
e=e,
|
||||
)
|
||||
return len(group_list) - error_count, error_count
|
||||
return 0, 0
|
||||
490
zhenxun/builtin_plugins/superuser/broadcast/broadcast_manager.py
Normal file
@ -0,0 +1,490 @@
|
||||
import asyncio
|
||||
import random
|
||||
import traceback
|
||||
from typing import ClassVar
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters.onebot.v11 import Bot as V11Bot
|
||||
from nonebot.exception import ActionFailed
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from nonebot_plugin_alconna.uniseg import Receipt, Reference
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from .models import BroadcastDetailResult, BroadcastResult
|
||||
from .utils import custom_nodes_to_v11_nodes, uni_message_to_v11_list_of_dicts
|
||||
|
||||
|
||||
class BroadcastManager:
|
||||
"""广播管理器"""
|
||||
|
||||
_last_broadcast_msg_ids: ClassVar[dict[str, int]] = {}
|
||||
|
||||
@staticmethod
|
||||
def _get_session_info(session: EventSession | None) -> str:
|
||||
"""获取会话信息字符串"""
|
||||
if not session:
|
||||
return ""
|
||||
|
||||
try:
|
||||
platform = getattr(session, "platform", "unknown")
|
||||
session_id = str(session)
|
||||
return f"[{platform}:{session_id}]"
|
||||
except Exception:
|
||||
return "[session-info-error]"
|
||||
|
||||
@staticmethod
|
||||
def log_error(
|
||||
message: str, error: Exception, session: EventSession | None = None, **kwargs
|
||||
):
|
||||
"""记录错误日志"""
|
||||
session_info = BroadcastManager._get_session_info(session)
|
||||
error_type = type(error).__name__
|
||||
stack_trace = traceback.format_exc()
|
||||
error_details = f"\n类型: {error_type}\n信息: {error!s}\n堆栈: {stack_trace}"
|
||||
|
||||
logger.error(
|
||||
f"{session_info} {message}{error_details}", "广播", e=error, **kwargs
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_warning(message: str, session: EventSession | None = None, **kwargs):
|
||||
"""记录警告级别日志"""
|
||||
session_info = BroadcastManager._get_session_info(session)
|
||||
logger.warning(f"{session_info} {message}", "广播", **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def log_info(message: str, session: EventSession | None = None, **kwargs):
|
||||
"""记录信息级别日志"""
|
||||
session_info = BroadcastManager._get_session_info(session)
|
||||
logger.info(f"{session_info} {message}", "广播", **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_last_broadcast_msg_ids(cls) -> dict[str, int]:
|
||||
"""获取最近广播消息ID"""
|
||||
return cls._last_broadcast_msg_ids.copy()
|
||||
|
||||
@classmethod
|
||||
def clear_last_broadcast_msg_ids(cls) -> None:
|
||||
"""清空消息ID记录"""
|
||||
cls._last_broadcast_msg_ids.clear()
|
||||
|
||||
@classmethod
|
||||
async def get_all_groups(cls, bot: Bot) -> tuple[list[GroupConsole], str]:
|
||||
"""获取群组列表"""
|
||||
return await PlatformUtils.get_group_list(bot)
|
||||
|
||||
@classmethod
|
||||
async def send(
|
||||
cls, bot: Bot, message: UniMessage, session: EventSession
|
||||
) -> BroadcastResult:
|
||||
"""发送广播到所有群组"""
|
||||
logger.debug(
|
||||
f"开始广播(send - 广播到所有群组),Bot ID: {bot.self_id}",
|
||||
"广播",
|
||||
session=session,
|
||||
)
|
||||
|
||||
logger.debug("清空上一次的广播消息ID记录", "广播", session=session)
|
||||
cls.clear_last_broadcast_msg_ids()
|
||||
|
||||
all_groups, _ = await cls.get_all_groups(bot)
|
||||
return await cls.send_to_specific_groups(bot, message, all_groups, session)
|
||||
|
||||
@classmethod
|
||||
async def send_to_specific_groups(
|
||||
cls,
|
||||
bot: Bot,
|
||||
message: UniMessage,
|
||||
target_groups: list[GroupConsole],
|
||||
session_info: EventSession | str | None = None,
|
||||
) -> BroadcastResult:
|
||||
"""发送广播到指定群组"""
|
||||
log_session = session_info or bot.self_id
|
||||
logger.debug(
|
||||
f"开始广播,目标 {len(target_groups)} 个群组,Bot ID: {bot.self_id}",
|
||||
"广播",
|
||||
session=log_session,
|
||||
)
|
||||
|
||||
if not target_groups:
|
||||
logger.debug("目标群组列表为空,广播结束", "广播", session=log_session)
|
||||
return 0, 0
|
||||
|
||||
platform = PlatformUtils.get_platform(bot)
|
||||
is_forward_broadcast = any(
|
||||
isinstance(seg, Reference) and getattr(seg, "nodes", None)
|
||||
for seg in message
|
||||
)
|
||||
|
||||
if platform == "qq" and isinstance(bot, V11Bot) and is_forward_broadcast:
|
||||
if (
|
||||
len(message) == 1
|
||||
and isinstance(message[0], Reference)
|
||||
and getattr(message[0], "nodes", None)
|
||||
):
|
||||
nodes_list = getattr(message[0], "nodes", [])
|
||||
v11_nodes = custom_nodes_to_v11_nodes(nodes_list)
|
||||
node_count = len(v11_nodes)
|
||||
logger.debug(
|
||||
f"从 UniMessage<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
|
||||
584
zhenxun/builtin_plugins/superuser/broadcast/message_processor.py
Normal file
@ -0,0 +1,584 @@
|
||||
import base64
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.adapters.onebot.v11 import Message as V11Message
|
||||
from nonebot.adapters.onebot.v11 import MessageSegment as V11MessageSegment
|
||||
from nonebot.exception import ActionFailed
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from nonebot_plugin_alconna.uniseg.segment import (
|
||||
At,
|
||||
AtAll,
|
||||
CustomNode,
|
||||
Image,
|
||||
Reference,
|
||||
Reply,
|
||||
Text,
|
||||
Video,
|
||||
)
|
||||
from nonebot_plugin_alconna.uniseg.tools import reply_fetch
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.common_utils import CommonUtils
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
|
||||
from .broadcast_manager import BroadcastManager
|
||||
|
||||
MAX_FORWARD_DEPTH = 3
|
||||
|
||||
|
||||
async def _process_forward_content(
|
||||
forward_content: Any, forward_id: str | None, bot: Bot, depth: int
|
||||
) -> list[CustomNode]:
|
||||
"""处理转发消息内容"""
|
||||
nodes_for_alc = []
|
||||
content_parsed = False
|
||||
|
||||
if forward_content:
|
||||
nodes_from_content = None
|
||||
if isinstance(forward_content, list):
|
||||
nodes_from_content = forward_content
|
||||
elif isinstance(forward_content, str):
|
||||
try:
|
||||
parsed_content = json.loads(forward_content)
|
||||
if isinstance(parsed_content, list):
|
||||
nodes_from_content = parsed_content
|
||||
except Exception as json_e:
|
||||
logger.debug(
|
||||
f"[Depth {depth}] JSON解析失败: {json_e}",
|
||||
"广播",
|
||||
)
|
||||
|
||||
if nodes_from_content is not None:
|
||||
logger.debug(
|
||||
f"[D{depth}] 节点数: {len(nodes_from_content)}",
|
||||
"广播",
|
||||
)
|
||||
content_parsed = True
|
||||
for node_data in nodes_from_content:
|
||||
node = await _create_custom_node_from_data(node_data, bot, depth + 1)
|
||||
if node:
|
||||
nodes_for_alc.append(node)
|
||||
|
||||
if not content_parsed and forward_id:
|
||||
logger.debug(
|
||||
f"[D{depth}] 尝试API调用ID: {forward_id}",
|
||||
"广播",
|
||||
)
|
||||
try:
|
||||
forward_data = await bot.call_api("get_forward_msg", id=forward_id)
|
||||
nodes_list = None
|
||||
|
||||
if isinstance(forward_data, dict) and "messages" in forward_data:
|
||||
nodes_list = forward_data["messages"]
|
||||
elif (
|
||||
isinstance(forward_data, dict)
|
||||
and "data" in forward_data
|
||||
and isinstance(forward_data["data"], dict)
|
||||
and "message" in forward_data["data"]
|
||||
):
|
||||
nodes_list = forward_data["data"]["message"]
|
||||
elif isinstance(forward_data, list):
|
||||
nodes_list = forward_data
|
||||
|
||||
if nodes_list:
|
||||
node_count = len(nodes_list)
|
||||
logger.debug(
|
||||
f"[D{depth + 1}] 节点:{node_count}",
|
||||
"广播",
|
||||
)
|
||||
for node_data in nodes_list:
|
||||
node = await _create_custom_node_from_data(
|
||||
node_data, bot, depth + 1
|
||||
)
|
||||
if node:
|
||||
nodes_for_alc.append(node)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[D{depth + 1}] ID:{forward_id}无节点",
|
||||
"广播",
|
||||
)
|
||||
nodes_for_alc.append(
|
||||
CustomNode(
|
||||
uid="0",
|
||||
name="错误",
|
||||
content="[嵌套转发消息获取失败]",
|
||||
)
|
||||
)
|
||||
except ActionFailed as af_e:
|
||||
logger.error(
|
||||
f"[D{depth + 1}] API失败: {af_e}",
|
||||
"广播",
|
||||
e=af_e,
|
||||
)
|
||||
nodes_for_alc.append(
|
||||
CustomNode(
|
||||
uid="0",
|
||||
name="错误",
|
||||
content="[嵌套转发消息获取失败]",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[D{depth + 1}] 处理出错: {e}",
|
||||
"广播",
|
||||
e=e,
|
||||
)
|
||||
nodes_for_alc.append(
|
||||
CustomNode(
|
||||
uid="0",
|
||||
name="错误",
|
||||
content="[处理嵌套转发时出错]",
|
||||
)
|
||||
)
|
||||
elif not content_parsed and not forward_id:
|
||||
logger.warning(
|
||||
f"[D{depth}] 转发段无内容也无ID",
|
||||
"广播",
|
||||
)
|
||||
nodes_for_alc.append(
|
||||
CustomNode(
|
||||
uid="0",
|
||||
name="错误",
|
||||
content="[嵌套转发消息无法解析]",
|
||||
)
|
||||
)
|
||||
elif content_parsed and not nodes_for_alc:
|
||||
logger.warning(
|
||||
f"[D{depth}] 解析成功但无有效节点",
|
||||
"广播",
|
||||
)
|
||||
nodes_for_alc.append(
|
||||
CustomNode(
|
||||
uid="0",
|
||||
name="信息",
|
||||
content="[嵌套转发内容为空]",
|
||||
)
|
||||
)
|
||||
|
||||
return nodes_for_alc
|
||||
|
||||
|
||||
async def _create_custom_node_from_data(
|
||||
node_data: dict, bot: Bot, depth: int
|
||||
) -> CustomNode | None:
|
||||
"""从节点数据创建CustomNode"""
|
||||
node_content_raw = node_data.get("message") or node_data.get("content")
|
||||
if not node_content_raw:
|
||||
logger.warning(f"[D{depth}] 节点缺少消息内容", "广播")
|
||||
return None
|
||||
|
||||
sender = node_data.get("sender", {})
|
||||
uid = str(sender.get("user_id", "10000"))
|
||||
name = sender.get("nickname", f"用户{uid[:4]}")
|
||||
|
||||
extracted_uni_msg = await _extract_content_from_message(
|
||||
node_content_raw, bot, depth
|
||||
)
|
||||
if not extracted_uni_msg:
|
||||
return None
|
||||
|
||||
return CustomNode(uid=uid, name=name, content=extracted_uni_msg)
|
||||
|
||||
|
||||
async def _extract_broadcast_content(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
arp: alc.Arparma,
|
||||
session: EventSession,
|
||||
) -> UniMessage | None:
|
||||
"""从命令参数或引用消息中提取广播内容"""
|
||||
broadcast_content_msg: UniMessage | None = None
|
||||
|
||||
command_content_list = arp.all_matched_args.get("content", [])
|
||||
|
||||
processed_command_list = []
|
||||
has_command_content = False
|
||||
|
||||
if command_content_list:
|
||||
for item in command_content_list:
|
||||
if isinstance(item, alc.Segment):
|
||||
processed_command_list.append(item)
|
||||
if not (isinstance(item, Text) and not item.text.strip()):
|
||||
has_command_content = True
|
||||
elif isinstance(item, str):
|
||||
if item.strip():
|
||||
processed_command_list.append(Text(item.strip()))
|
||||
has_command_content = True
|
||||
else:
|
||||
logger.warning(
|
||||
f"Unexpected type in command content: {type(item)}", "广播"
|
||||
)
|
||||
|
||||
if has_command_content:
|
||||
logger.debug("检测到命令参数内容,优先使用参数内容", "广播", session=session)
|
||||
broadcast_content_msg = UniMessage(processed_command_list)
|
||||
|
||||
if not broadcast_content_msg.filter(
|
||||
lambda x: not (isinstance(x, Text) and not x.text.strip())
|
||||
):
|
||||
logger.warning(
|
||||
"命令参数内容解析后为空或只包含空白", "广播", session=session
|
||||
)
|
||||
broadcast_content_msg = None
|
||||
|
||||
if not broadcast_content_msg:
|
||||
reply_segment_obj: Reply | None = await reply_fetch(event, bot)
|
||||
if (
|
||||
reply_segment_obj
|
||||
and hasattr(reply_segment_obj, "msg")
|
||||
and reply_segment_obj.msg
|
||||
):
|
||||
logger.debug(
|
||||
"未检测到有效命令参数,检测到引用消息", "广播", session=session
|
||||
)
|
||||
raw_quoted_content = reply_segment_obj.msg
|
||||
is_forward = False
|
||||
forward_id = None
|
||||
|
||||
if isinstance(raw_quoted_content, V11Message):
|
||||
for seg in raw_quoted_content:
|
||||
if isinstance(seg, V11MessageSegment):
|
||||
if seg.type == "forward":
|
||||
forward_id = seg.data.get("id")
|
||||
is_forward = bool(forward_id)
|
||||
break
|
||||
elif seg.type == "json":
|
||||
try:
|
||||
json_data_str = seg.data.get("data", "{}")
|
||||
if isinstance(json_data_str, str):
|
||||
import json
|
||||
|
||||
json_data = json.loads(json_data_str)
|
||||
if (
|
||||
json_data.get("app") == "com.tencent.multimsg"
|
||||
or json_data.get("view") == "Forward"
|
||||
) and json_data.get("meta", {}).get(
|
||||
"detail", {}
|
||||
).get("resid"):
|
||||
forward_id = json_data["meta"]["detail"][
|
||||
"resid"
|
||||
]
|
||||
is_forward = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if is_forward and forward_id:
|
||||
logger.info(
|
||||
f"尝试获取并构造合并转发内容 (ID: {forward_id})",
|
||||
"广播",
|
||||
session=session,
|
||||
)
|
||||
nodes_to_forward: list[CustomNode] = []
|
||||
try:
|
||||
forward_data = await bot.call_api("get_forward_msg", id=forward_id)
|
||||
nodes_list = None
|
||||
if isinstance(forward_data, dict) and "messages" in forward_data:
|
||||
nodes_list = forward_data["messages"]
|
||||
elif (
|
||||
isinstance(forward_data, dict)
|
||||
and "data" in forward_data
|
||||
and isinstance(forward_data["data"], dict)
|
||||
and "message" in forward_data["data"]
|
||||
):
|
||||
nodes_list = forward_data["data"]["message"]
|
||||
elif isinstance(forward_data, list):
|
||||
nodes_list = forward_data
|
||||
|
||||
if nodes_list is not None:
|
||||
for node_data in nodes_list:
|
||||
node_sender = node_data.get("sender", {})
|
||||
node_user_id = str(node_sender.get("user_id", "10000"))
|
||||
node_nickname = node_sender.get(
|
||||
"nickname", f"用户{node_user_id[:4]}"
|
||||
)
|
||||
node_content_raw = node_data.get(
|
||||
"message"
|
||||
) or node_data.get("content")
|
||||
if node_content_raw:
|
||||
extracted_node_uni_msg = (
|
||||
await _extract_content_from_message(
|
||||
node_content_raw, bot
|
||||
)
|
||||
)
|
||||
if extracted_node_uni_msg:
|
||||
nodes_to_forward.append(
|
||||
CustomNode(
|
||||
uid=node_user_id,
|
||||
name=node_nickname,
|
||||
content=extracted_node_uni_msg,
|
||||
)
|
||||
)
|
||||
if nodes_to_forward:
|
||||
broadcast_content_msg = UniMessage(
|
||||
Reference(nodes=nodes_to_forward)
|
||||
)
|
||||
except ActionFailed:
|
||||
await MessageUtils.build_message(
|
||||
"获取合并转发消息失败,可能不支持此 API。"
|
||||
).send(reply_to=True)
|
||||
return None
|
||||
except Exception as api_e:
|
||||
logger.error(f"处理合并转发时出错: {api_e}", "广播", e=api_e)
|
||||
await MessageUtils.build_message(
|
||||
"处理合并转发消息时发生内部错误。"
|
||||
).send(reply_to=True)
|
||||
return None
|
||||
else:
|
||||
broadcast_content_msg = await _extract_content_from_message(
|
||||
raw_quoted_content, bot
|
||||
)
|
||||
else:
|
||||
logger.debug("未检测到命令参数和引用消息", "广播", session=session)
|
||||
await MessageUtils.build_message("请提供广播内容或引用要广播的消息").send(
|
||||
reply_to=True
|
||||
)
|
||||
return None
|
||||
|
||||
if not broadcast_content_msg:
|
||||
logger.error(
|
||||
"未能从命令参数或引用消息中获取有效的广播内容", "广播", session=session
|
||||
)
|
||||
await MessageUtils.build_message("错误:未能获取有效的广播内容。").send(
|
||||
reply_to=True
|
||||
)
|
||||
return None
|
||||
|
||||
return broadcast_content_msg
|
||||
|
||||
|
||||
async def _process_v11_segment(
|
||||
seg_obj: V11MessageSegment | dict, depth: int, index: int, bot: Bot
|
||||
) -> list[alc.Segment]:
|
||||
"""处理V11消息段"""
|
||||
result = []
|
||||
seg_type = None
|
||||
data_dict = None
|
||||
|
||||
if isinstance(seg_obj, V11MessageSegment):
|
||||
seg_type = seg_obj.type
|
||||
data_dict = seg_obj.data
|
||||
elif isinstance(seg_obj, dict):
|
||||
seg_type = seg_obj.get("type")
|
||||
data_dict = seg_obj.get("data")
|
||||
else:
|
||||
return result
|
||||
|
||||
if not (seg_type and data_dict is not None):
|
||||
logger.warning(f"[D{depth}] 跳过无效数据: {type(seg_obj)}", "广播")
|
||||
return result
|
||||
|
||||
if seg_type == "text":
|
||||
text_content = data_dict.get("text", "")
|
||||
if isinstance(text_content, str) and text_content.strip():
|
||||
result.append(Text(text_content))
|
||||
elif seg_type == "image":
|
||||
img_seg = None
|
||||
if data_dict.get("url"):
|
||||
img_seg = Image(url=data_dict["url"])
|
||||
elif data_dict.get("file"):
|
||||
file_val = data_dict["file"]
|
||||
if isinstance(file_val, str) and file_val.startswith("base64://"):
|
||||
b64_data = file_val[9:]
|
||||
raw_bytes = base64.b64decode(b64_data)
|
||||
img_seg = Image(raw=raw_bytes)
|
||||
else:
|
||||
img_seg = Image(path=file_val)
|
||||
if img_seg:
|
||||
result.append(img_seg)
|
||||
else:
|
||||
logger.warning(f"[Depth {depth}] V11 图片 {index} 缺少URL/文件", "广播")
|
||||
elif seg_type == "at":
|
||||
target_qq = data_dict.get("qq", "")
|
||||
if target_qq.lower() == "all":
|
||||
result.append(AtAll())
|
||||
elif target_qq:
|
||||
result.append(At(flag="user", target=target_qq))
|
||||
elif seg_type == "video":
|
||||
video_seg = None
|
||||
if data_dict.get("url"):
|
||||
video_seg = Video(url=data_dict["url"])
|
||||
elif data_dict.get("file"):
|
||||
file_val = data_dict["file"]
|
||||
if isinstance(file_val, str) and file_val.startswith("base64://"):
|
||||
b64_data = file_val[9:]
|
||||
raw_bytes = base64.b64decode(b64_data)
|
||||
video_seg = Video(raw=raw_bytes)
|
||||
else:
|
||||
video_seg = Video(path=file_val)
|
||||
if video_seg:
|
||||
result.append(video_seg)
|
||||
logger.debug(f"[Depth {depth}] 处理视频消息成功", "广播")
|
||||
else:
|
||||
logger.warning(f"[Depth {depth}] V11 视频 {index} 缺少URL/文件", "广播")
|
||||
elif seg_type == "forward":
|
||||
nested_forward_id = data_dict.get("id") or data_dict.get("resid")
|
||||
nested_forward_content = data_dict.get("content")
|
||||
|
||||
logger.debug(f"[D{depth}] 嵌套转发ID: {nested_forward_id}", "广播")
|
||||
|
||||
nested_nodes = await _process_forward_content(
|
||||
nested_forward_content, nested_forward_id, bot, depth
|
||||
)
|
||||
|
||||
if nested_nodes:
|
||||
result.append(Reference(nodes=nested_nodes))
|
||||
else:
|
||||
logger.warning(f"[D{depth}] 跳过类型: {seg_type}", "广播")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _extract_content_from_message(
|
||||
message_content: Any, bot: Bot, depth: int = 0
|
||||
) -> UniMessage:
|
||||
"""提取消息内容到UniMessage"""
|
||||
temp_msg = UniMessage()
|
||||
input_type_str = str(type(message_content))
|
||||
|
||||
if depth >= MAX_FORWARD_DEPTH:
|
||||
logger.warning(
|
||||
f"[Depth {depth}] 达到最大递归深度 {MAX_FORWARD_DEPTH},停止解析嵌套转发。",
|
||||
"广播",
|
||||
)
|
||||
temp_msg.append(Text("[嵌套转发层数过多,内容已省略]"))
|
||||
return temp_msg
|
||||
|
||||
segments_to_process = []
|
||||
|
||||
if isinstance(message_content, UniMessage):
|
||||
segments_to_process = list(message_content)
|
||||
elif isinstance(message_content, V11Message):
|
||||
segments_to_process = list(message_content)
|
||||
elif isinstance(message_content, list):
|
||||
segments_to_process = message_content
|
||||
elif (
|
||||
isinstance(message_content, dict)
|
||||
and "type" in message_content
|
||||
and "data" in message_content
|
||||
):
|
||||
segments_to_process = [message_content]
|
||||
elif isinstance(message_content, str):
|
||||
if message_content.strip():
|
||||
temp_msg.append(Text(message_content))
|
||||
return temp_msg
|
||||
else:
|
||||
logger.warning(f"[Depth {depth}] 无法处理的输入类型: {input_type_str}", "广播")
|
||||
return temp_msg
|
||||
|
||||
if segments_to_process:
|
||||
for index, seg_obj in enumerate(segments_to_process):
|
||||
try:
|
||||
if isinstance(seg_obj, Text):
|
||||
text_content = getattr(seg_obj, "text", None)
|
||||
if isinstance(text_content, str) and text_content.strip():
|
||||
temp_msg.append(seg_obj)
|
||||
elif isinstance(seg_obj, Image):
|
||||
if (
|
||||
getattr(seg_obj, "url", None)
|
||||
or getattr(seg_obj, "path", None)
|
||||
or getattr(seg_obj, "raw", None)
|
||||
):
|
||||
temp_msg.append(seg_obj)
|
||||
elif isinstance(seg_obj, At):
|
||||
temp_msg.append(seg_obj)
|
||||
elif isinstance(seg_obj, AtAll):
|
||||
temp_msg.append(seg_obj)
|
||||
elif isinstance(seg_obj, Video):
|
||||
if (
|
||||
getattr(seg_obj, "url", None)
|
||||
or getattr(seg_obj, "path", None)
|
||||
or getattr(seg_obj, "raw", None)
|
||||
):
|
||||
temp_msg.append(seg_obj)
|
||||
logger.debug(f"[D{depth}] 处理Video对象成功", "广播")
|
||||
else:
|
||||
processed_segments = await _process_v11_segment(
|
||||
seg_obj, depth, index, bot
|
||||
)
|
||||
temp_msg.extend(processed_segments)
|
||||
except Exception as e_conv_seg:
|
||||
logger.warning(
|
||||
f"[D{depth}] 处理段 {index} 出错: {e_conv_seg}",
|
||||
"广播",
|
||||
e=e_conv_seg,
|
||||
)
|
||||
|
||||
if not temp_msg and message_content:
|
||||
logger.warning(f"未能从类型 {input_type_str} 中提取内容", "广播")
|
||||
|
||||
return temp_msg
|
||||
|
||||
|
||||
async def get_broadcast_target_groups(
|
||||
bot: Bot, session: EventSession
|
||||
) -> tuple[list, list]:
|
||||
"""获取广播目标群组和启用了广播功能的群组"""
|
||||
target_groups = []
|
||||
all_groups, _ = await BroadcastManager.get_all_groups(bot)
|
||||
|
||||
current_group_id = None
|
||||
if hasattr(session, "id2") and session.id2:
|
||||
current_group_id = session.id2
|
||||
|
||||
if current_group_id:
|
||||
target_groups = [
|
||||
group for group in all_groups if group.group_id != current_group_id
|
||||
]
|
||||
logger.info(
|
||||
f"向除当前群组({current_group_id})外的所有群组广播", "广播", session=session
|
||||
)
|
||||
else:
|
||||
target_groups = all_groups
|
||||
logger.info("向所有群组广播", "广播", session=session)
|
||||
|
||||
if not target_groups:
|
||||
await MessageUtils.build_message("没有找到符合条件的广播目标群组。").send(
|
||||
reply_to=True
|
||||
)
|
||||
return [], []
|
||||
|
||||
enabled_groups = []
|
||||
for group in target_groups:
|
||||
if not await CommonUtils.task_is_block(bot, "broadcast", group.group_id):
|
||||
enabled_groups.append(group)
|
||||
|
||||
if not enabled_groups:
|
||||
await MessageUtils.build_message(
|
||||
"没有启用了广播功能的目标群组可供立即发送。"
|
||||
).send(reply_to=True)
|
||||
return target_groups, []
|
||||
|
||||
return target_groups, enabled_groups
|
||||
|
||||
|
||||
async def send_broadcast_and_notify(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
message: UniMessage,
|
||||
enabled_groups: list,
|
||||
target_groups: list,
|
||||
session: EventSession,
|
||||
) -> None:
|
||||
"""发送广播并通知结果"""
|
||||
BroadcastManager.clear_last_broadcast_msg_ids()
|
||||
count, error_count = await BroadcastManager.send_to_specific_groups(
|
||||
bot, message, enabled_groups, session
|
||||
)
|
||||
|
||||
result = f"成功广播 {count} 个群组"
|
||||
if error_count:
|
||||
result += f"\n发送失败 {error_count} 个群组"
|
||||
result += f"\n有效: {len(enabled_groups)} / 总计: {len(target_groups)}"
|
||||
|
||||
user_id = str(event.get_user_id())
|
||||
await bot.send_private_msg(user_id=user_id, message=f"发送广播完成!\n{result}")
|
||||
|
||||
BroadcastManager.log_info(
|
||||
f"广播完成,有效/总计: {len(enabled_groups)}/{len(target_groups)}",
|
||||
session,
|
||||
)
|
||||
64
zhenxun/builtin_plugins/superuser/broadcast/models.py
Normal file
@ -0,0 +1,64 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from zhenxun.models.group_console import GroupConsole
|
||||
|
||||
GroupKey = str
|
||||
MessageID = int
|
||||
BroadcastResult = tuple[int, int]
|
||||
BroadcastDetailResult = tuple[int, int, int]
|
||||
|
||||
|
||||
class BroadcastTarget:
|
||||
"""广播目标"""
|
||||
|
||||
def __init__(self, group_id: str, channel_id: str | None = None):
|
||||
self.group_id = group_id
|
||||
self.channel_id = channel_id
|
||||
|
||||
def to_dict(self) -> dict[str, str | None]:
|
||||
"""转换为字典格式"""
|
||||
return {"group_id": self.group_id, "channel_id": self.channel_id}
|
||||
|
||||
@classmethod
|
||||
def from_group_console(cls, group: GroupConsole) -> "BroadcastTarget":
|
||||
"""从 GroupConsole 对象创建"""
|
||||
return cls(group_id=group.group_id, channel_id=group.channel_id)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""获取群组的唯一标识"""
|
||||
if self.channel_id:
|
||||
return f"{self.group_id}:{self.channel_id}"
|
||||
return str(self.group_id)
|
||||
|
||||
|
||||
class BroadcastTask:
|
||||
"""广播任务"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot_id: str,
|
||||
message: UniMessage,
|
||||
targets: list[BroadcastTarget],
|
||||
scheduled_time: datetime | None = None,
|
||||
task_id: str | None = None,
|
||||
):
|
||||
self.bot_id = bot_id
|
||||
self.message = message
|
||||
self.targets = targets
|
||||
self.scheduled_time = scheduled_time
|
||||
self.task_id = task_id
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典格式,用于序列化"""
|
||||
return {
|
||||
"bot_id": self.bot_id,
|
||||
"targets": [t.to_dict() for t in self.targets],
|
||||
"scheduled_time": self.scheduled_time.isoformat()
|
||||
if self.scheduled_time
|
||||
else None,
|
||||
"task_id": self.task_id,
|
||||
}
|
||||
175
zhenxun/builtin_plugins/superuser/broadcast/utils.py
Normal file
@ -0,0 +1,175 @@
|
||||
import base64
|
||||
|
||||
import nonebot_plugin_alconna as alc
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from nonebot_plugin_alconna.uniseg import Reference
|
||||
from nonebot_plugin_alconna.uniseg.segment import CustomNode, Video
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
|
||||
def uni_segment_to_v11_segment_dict(
|
||||
seg: alc.Segment, depth: int = 0
|
||||
) -> dict | list[dict] | None:
|
||||
"""UniSeg段转V11字典"""
|
||||
if isinstance(seg, alc.Text):
|
||||
return {"type": "text", "data": {"text": seg.text}}
|
||||
elif isinstance(seg, alc.Image):
|
||||
if getattr(seg, "url", None):
|
||||
return {
|
||||
"type": "image",
|
||||
"data": {"file": seg.url},
|
||||
}
|
||||
elif getattr(seg, "raw", None):
|
||||
raw_data = seg.raw
|
||||
if isinstance(raw_data, str):
|
||||
if len(raw_data) >= 9 and raw_data[:9] == "base64://":
|
||||
return {"type": "image", "data": {"file": raw_data}}
|
||||
elif isinstance(raw_data, bytes):
|
||||
b64_str = base64.b64encode(raw_data).decode()
|
||||
return {"type": "image", "data": {"file": f"base64://{b64_str}"}}
|
||||
else:
|
||||
logger.warning(f"无法处理 Image.raw 的类型: {type(raw_data)}", "广播")
|
||||
elif getattr(seg, "path", None):
|
||||
logger.warning(
|
||||
f"在合并转发中使用了本地图片路径,可能无法显示: {seg.path}", "广播"
|
||||
)
|
||||
return {"type": "image", "data": {"file": f"file:///{seg.path}"}}
|
||||
else:
|
||||
logger.warning(f"alc.Image 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
|
||||
elif isinstance(seg, alc.At):
|
||||
return {"type": "at", "data": {"qq": seg.target}}
|
||||
elif isinstance(seg, alc.AtAll):
|
||||
return {"type": "at", "data": {"qq": "all"}}
|
||||
elif isinstance(seg, Video):
|
||||
if getattr(seg, "url", None):
|
||||
return {
|
||||
"type": "video",
|
||||
"data": {"file": seg.url},
|
||||
}
|
||||
elif getattr(seg, "raw", None):
|
||||
raw_data = seg.raw
|
||||
if isinstance(raw_data, str):
|
||||
if len(raw_data) >= 9 and raw_data[:9] == "base64://":
|
||||
return {"type": "video", "data": {"file": raw_data}}
|
||||
elif isinstance(raw_data, bytes):
|
||||
b64_str = base64.b64encode(raw_data).decode()
|
||||
return {"type": "video", "data": {"file": f"base64://{b64_str}"}}
|
||||
else:
|
||||
logger.warning(f"无法处理 Video.raw 的类型: {type(raw_data)}", "广播")
|
||||
elif getattr(seg, "path", None):
|
||||
logger.warning(
|
||||
f"在合并转发中使用了本地视频路径,可能无法显示: {seg.path}", "广播"
|
||||
)
|
||||
return {"type": "video", "data": {"file": f"file:///{seg.path}"}}
|
||||
else:
|
||||
logger.warning(f"Video 缺少有效数据,无法转换为 V11 段: {seg}", "广播")
|
||||
elif isinstance(seg, Reference) and getattr(seg, "nodes", None):
|
||||
if depth >= 3:
|
||||
logger.warning(
|
||||
f"嵌套转发深度超过限制 (depth={depth}),不再继续解析", "广播"
|
||||
)
|
||||
return {"type": "text", "data": {"text": "[嵌套转发层数过多,内容已省略]"}}
|
||||
|
||||
nested_v11_content_list = []
|
||||
nodes_list = getattr(seg, "nodes", [])
|
||||
for node in nodes_list:
|
||||
if isinstance(node, CustomNode):
|
||||
node_v11_content = []
|
||||
if isinstance(node.content, UniMessage):
|
||||
for nested_seg in node.content:
|
||||
converted_dict = uni_segment_to_v11_segment_dict(
|
||||
nested_seg, depth + 1
|
||||
)
|
||||
if isinstance(converted_dict, list):
|
||||
node_v11_content.extend(converted_dict)
|
||||
elif converted_dict:
|
||||
node_v11_content.append(converted_dict)
|
||||
elif isinstance(node.content, str):
|
||||
node_v11_content.append(
|
||||
{"type": "text", "data": {"text": node.content}}
|
||||
)
|
||||
if node_v11_content:
|
||||
separator = {
|
||||
"type": "text",
|
||||
"data": {
|
||||
"text": f"\n--- 来自 {node.name} ({node.uid}) 的消息 ---\n"
|
||||
},
|
||||
}
|
||||
nested_v11_content_list.insert(0, separator)
|
||||
nested_v11_content_list.extend(node_v11_content)
|
||||
nested_v11_content_list.append(
|
||||
{"type": "text", "data": {"text": "\n---\n"}}
|
||||
)
|
||||
|
||||
return nested_v11_content_list
|
||||
|
||||
else:
|
||||
logger.warning(f"广播时跳过不支持的 UniSeg 段类型: {type(seg)}", "广播")
|
||||
return None
|
||||
|
||||
|
||||
def uni_message_to_v11_list_of_dicts(uni_msg: UniMessage | str | list) -> list[dict]:
|
||||
"""UniMessage转V11字典列表"""
|
||||
try:
|
||||
if isinstance(uni_msg, str):
|
||||
return [{"type": "text", "data": {"text": uni_msg}}]
|
||||
|
||||
if isinstance(uni_msg, list):
|
||||
if not uni_msg:
|
||||
return []
|
||||
|
||||
if all(isinstance(item, str) for item in uni_msg):
|
||||
return [{"type": "text", "data": {"text": item}} for item in uni_msg]
|
||||
|
||||
result = []
|
||||
for item in uni_msg:
|
||||
if hasattr(item, "__iter__") and not isinstance(item, str | bytes):
|
||||
result.extend(uni_message_to_v11_list_of_dicts(item))
|
||||
elif hasattr(item, "text") and not isinstance(item, str | bytes):
|
||||
text_value = getattr(item, "text", "")
|
||||
result.append({"type": "text", "data": {"text": str(text_value)}})
|
||||
elif hasattr(item, "url") and not isinstance(item, str | bytes):
|
||||
url_value = getattr(item, "url", "")
|
||||
if isinstance(item, Video):
|
||||
result.append(
|
||||
{"type": "video", "data": {"file": str(url_value)}}
|
||||
)
|
||||
else:
|
||||
result.append(
|
||||
{"type": "image", "data": {"file": str(url_value)}}
|
||||
)
|
||||
else:
|
||||
try:
|
||||
result.append({"type": "text", "data": {"text": str(item)}})
|
||||
except Exception as e:
|
||||
logger.warning(f"无法转换列表元素: {item}, 错误: {e}", "广播")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"消息转换过程中出错: {e}", "广播")
|
||||
|
||||
return [{"type": "text", "data": {"text": str(uni_msg)}}]
|
||||
|
||||
|
||||
def custom_nodes_to_v11_nodes(custom_nodes: list[CustomNode]) -> list[dict]:
|
||||
"""CustomNode列表转V11节点"""
|
||||
v11_nodes = []
|
||||
for node in custom_nodes:
|
||||
v11_content_list = uni_message_to_v11_list_of_dicts(node.content)
|
||||
|
||||
if v11_content_list:
|
||||
v11_nodes.append(
|
||||
{
|
||||
"type": "node",
|
||||
"data": {
|
||||
"user_id": str(node.uid),
|
||||
"nickname": node.name,
|
||||
"content": v11_content_list,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"CustomNode (uid={node.uid}) 内容转换后为空,跳过此节点", "广播"
|
||||
)
|
||||
return v11_nodes
|
||||
@ -2,7 +2,7 @@ from io import BytesIO
|
||||
|
||||
from arclet.alconna import Args, Option
|
||||
from arclet.alconna.typing import CommandMeta
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.adapters import Bot, Event
|
||||
from nonebot.permission import SUPERUSER
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
@ -10,10 +10,13 @@ from nonebot_plugin_alconna import (
|
||||
Alconna,
|
||||
AlconnaQuery,
|
||||
Arparma,
|
||||
Match,
|
||||
Query,
|
||||
Reply,
|
||||
on_alconna,
|
||||
store_true,
|
||||
)
|
||||
from nonebot_plugin_alconna.uniseg.tools import reply_fetch
|
||||
from nonebot_plugin_session import EventSession
|
||||
|
||||
from zhenxun.configs.config import BotConfig
|
||||
@ -54,7 +57,7 @@ __plugin_meta__ = PluginMetadata(
|
||||
_req_matcher = on_alconna(
|
||||
Alconna(
|
||||
"请求处理",
|
||||
Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id", int],
|
||||
Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id?", int],
|
||||
meta=CommandMeta(
|
||||
description="好友/群组请求处理",
|
||||
usage=usage,
|
||||
@ -105,12 +108,12 @@ _clear_matcher = on_alconna(
|
||||
)
|
||||
|
||||
reg_arg_list = [
|
||||
(r"同意好友请求", ["-fa", "{%0}"]),
|
||||
(r"拒绝好友请求", ["-fr", "{%0}"]),
|
||||
(r"忽略好友请求", ["-fi", "{%0}"]),
|
||||
(r"同意群组请求", ["-ga", "{%0}"]),
|
||||
(r"拒绝群组请求", ["-gr", "{%0}"]),
|
||||
(r"忽略群组请求", ["-gi", "{%0}"]),
|
||||
(r"同意好友请求\s*(?P<id>\d*)", ["-fa", "{id}"]),
|
||||
(r"拒绝好友请求\s*(?P<id>\d*)", ["-fr", "{id}"]),
|
||||
(r"忽略好友请求\s*(?P<id>\d*)", ["-fi", "{id}"]),
|
||||
(r"同意群组请求\s*(?P<id>\d*)", ["-ga", "{id}"]),
|
||||
(r"拒绝群组请求\s*(?P<id>\d*)", ["-gr", "{id}"]),
|
||||
(r"忽略群组请求\s*(?P<id>\d*)", ["-gi", "{id}"]),
|
||||
]
|
||||
|
||||
for r in reg_arg_list:
|
||||
@ -125,32 +128,48 @@ for r in reg_arg_list:
|
||||
@_req_matcher.handle()
|
||||
async def _(
|
||||
bot: Bot,
|
||||
event: Event,
|
||||
session: EventSession,
|
||||
handle: str,
|
||||
id: int,
|
||||
id: Match[int],
|
||||
arparma: Arparma,
|
||||
):
|
||||
reply: Reply | None = None
|
||||
type_dict = {
|
||||
"a": RequestHandleType.APPROVE,
|
||||
"r": RequestHandleType.REFUSED,
|
||||
"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
|
||||
handle_type = type_dict[handle[-1]]
|
||||
try:
|
||||
if handle_type == RequestHandleType.APPROVE:
|
||||
req = await FgRequest.approve(bot, id)
|
||||
req = await FgRequest.approve(bot, handle_id)
|
||||
if handle_type == RequestHandleType.REFUSED:
|
||||
req = await FgRequest.refused(bot, id)
|
||||
req = await FgRequest.refused(bot, handle_id)
|
||||
if handle_type == RequestHandleType.IGNORE:
|
||||
req = await FgRequest.ignore(id)
|
||||
req = await FgRequest.ignore(handle_id)
|
||||
except NotFoundError:
|
||||
await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True)
|
||||
except Exception:
|
||||
await MessageUtils.build_message("其他错误, 可能flag已失效...").finish(
|
||||
reply_to=True
|
||||
)
|
||||
logger.info("处理请求", arparma.header_result, session=session)
|
||||
logger.info(
|
||||
f"处理请求 Id: {req.id if req else ''}", arparma.header_result, session=session
|
||||
)
|
||||
await MessageUtils.build_message("成功处理请求!").send(reply_to=True)
|
||||
if req and handle_type == RequestHandleType.APPROVE:
|
||||
await bot.send_private_msg(
|
||||
|
||||
@ -10,7 +10,9 @@ from zhenxun.configs.config import Config as gConfig
|
||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
||||
from zhenxun.services.log import logger, logger_
|
||||
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.log_manager import LOG_STORAGE
|
||||
from .api.menu import router as menu_router
|
||||
@ -29,8 +31,7 @@ from .public import init_public
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="WebUi",
|
||||
description="WebUi API",
|
||||
usage="""
|
||||
""".strip(),
|
||||
usage='"""\n """.strip(),',
|
||||
extra=PluginExtraData(
|
||||
author="HibiKier",
|
||||
version="0.1",
|
||||
@ -82,7 +83,7 @@ BaseApiRouter.include_router(database_router)
|
||||
BaseApiRouter.include_router(plugin_router)
|
||||
BaseApiRouter.include_router(system_router)
|
||||
BaseApiRouter.include_router(menu_router)
|
||||
|
||||
BaseApiRouter.include_router(configure_router)
|
||||
|
||||
WsApiRouter = APIRouter(prefix="/zhenxun/socket")
|
||||
|
||||
@ -91,9 +92,11 @@ WsApiRouter.include_router(status_routes)
|
||||
WsApiRouter.include_router(chat_routes)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=0)
|
||||
async def _():
|
||||
try:
|
||||
# 存储任务引用的列表,防止任务被垃圾回收
|
||||
_tasks = []
|
||||
|
||||
async def log_sink(message: str):
|
||||
loop = None
|
||||
@ -104,7 +107,8 @@ async def _():
|
||||
logger.warning("Web Ui log_sink", e=e)
|
||||
if not loop:
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) # noqa: RUF006
|
||||
# 存储任务引用到外部列表中
|
||||
_tasks.append(loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))))
|
||||
|
||||
logger_.add(
|
||||
log_sink, colorize=True, filter=default_filter, format=default_format
|
||||
|
||||
133
zhenxun/builtin_plugins/web_ui/api/configure/__init__.py
Normal file
@ -0,0 +1,133 @@
|
||||
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
|
||||
18
zhenxun/builtin_plugins/web_ui/api/configure/data_source.py
Normal file
@ -0,0 +1,18 @@
|
||||
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()
|
||||
16
zhenxun/builtin_plugins/web_ui/api/configure/model.py
Normal file
@ -0,0 +1,16 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Setting(BaseModel):
|
||||
superusers: list[str]
|
||||
"""超级用户列表"""
|
||||
db_url: str
|
||||
"""数据库地址"""
|
||||
host: str
|
||||
"""主机地址"""
|
||||
port: int
|
||||
"""端口"""
|
||||
username: str
|
||||
"""前端用户名"""
|
||||
password: str
|
||||
"""前端密码"""
|
||||
@ -5,18 +5,7 @@ from zhenxun.services.log import logger
|
||||
|
||||
from .model import MenuData, MenuItem
|
||||
|
||||
|
||||
class MenuManage:
|
||||
def __init__(self) -> None:
|
||||
self.file = DATA_PATH / "web_ui" / "menu.json"
|
||||
self.menu = []
|
||||
if self.file.exists():
|
||||
try:
|
||||
self.menu = json.load(self.file.open(encoding="utf8"))
|
||||
except Exception as e:
|
||||
logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e)
|
||||
if not self.menu:
|
||||
self.menu = [
|
||||
default_menus = [
|
||||
MenuItem(
|
||||
name="仪表盘",
|
||||
module="dashboard",
|
||||
@ -30,27 +19,50 @@ class MenuManage:
|
||||
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="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 MenuManager:
|
||||
def __init__(self) -> None:
|
||||
self.file = DATA_PATH / "web_ui" / "menu.json"
|
||||
self.menu = []
|
||||
if self.file.exists():
|
||||
try:
|
||||
temp_menu = []
|
||||
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(
|
||||
name="系统信息", module="system", router="/system", icon="system"
|
||||
),
|
||||
]
|
||||
**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:
|
||||
logger.warning("菜单文件损坏,已重新生成...", "WebUi", e=e)
|
||||
if not self.menu:
|
||||
self.menu = default_menus
|
||||
self.save()
|
||||
|
||||
def __get_menu_model(self, module: str):
|
||||
return default_menus[
|
||||
next(i for i, m in enumerate(default_menus) if m.module == module)
|
||||
]
|
||||
|
||||
def get_menus(self):
|
||||
return MenuData(menus=self.menu)
|
||||
|
||||
@ -61,4 +73,4 @@ class MenuManage:
|
||||
json.dump(temp, f, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
menu_manage = MenuManage()
|
||||
menu_manage = MenuManager()
|
||||
|
||||
@ -13,6 +13,7 @@ from zhenxun.models.bot_connect_log import BotConnectLog
|
||||
from zhenxun.models.chat_history import ChatHistory
|
||||
from zhenxun.models.statistics import Statistics
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ....base_model import BaseResultModel, QueryModel
|
||||
@ -31,7 +32,7 @@ driver: Driver = nonebot.get_driver()
|
||||
CONNECT_TIME = 0
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
global CONNECT_TIME
|
||||
CONNECT_TIME = int(time.time())
|
||||
|
||||
@ -8,6 +8,7 @@ from zhenxun.configs.config import BotConfig
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.models.task_info import TaskInfo
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
||||
|
||||
from ....base_model import BaseResultModel, QueryModel, Result
|
||||
from ....utils import authentication
|
||||
@ -21,7 +22,7 @@ router = APIRouter(prefix="/database")
|
||||
driver: Driver = nonebot.get_driver()
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
@PriorityLifecycle.on_startup(priority=5)
|
||||
async def _():
|
||||
for plugin in nonebot.get_loaded_plugins():
|
||||
module = plugin.name
|
||||
|
||||
@ -16,7 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
|
||||
|
||||
from ....base_model import Result
|
||||
from ....config import QueryDateType
|
||||
from ....utils import authentication, get_system_status
|
||||
from ....utils import authentication, clear_help_image, get_system_status
|
||||
from .data_source import ApiDataSource
|
||||
from .model import (
|
||||
ActiveGroup,
|
||||
@ -234,6 +234,7 @@ async def _(param: BotManageUpdateParam):
|
||||
bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins)
|
||||
bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks)
|
||||
await bot_data.save(update_fields=["block_plugins", "block_tasks"])
|
||||
clear_help_image()
|
||||
return Result.ok()
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e)
|
||||
|
||||
@ -92,7 +92,7 @@ class ApiDataSource:
|
||||
"""
|
||||
version_file = Path() / "__version__"
|
||||
if version_file.exists():
|
||||
if text := version_file.open().read():
|
||||
if text := version_file.open(encoding="utf-8").read():
|
||||
return text.replace("__version__: ", "").strip()
|
||||
return "unknown"
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter
|
||||
import nonebot
|
||||
from nonebot import on_message
|
||||
@ -49,13 +51,14 @@ async def message_handle(
|
||||
message: UniMsg,
|
||||
group_id: str | None,
|
||||
):
|
||||
time = str(datetime.now().replace(microsecond=0))
|
||||
messages = []
|
||||
for m in message:
|
||||
if isinstance(m, Text | str):
|
||||
messages.append(MessageItem(type="text", msg=str(m)))
|
||||
messages.append(MessageItem(type="text", msg=str(m), time=time))
|
||||
elif isinstance(m, Image):
|
||||
if m.url:
|
||||
messages.append(MessageItem(type="img", msg=m.url))
|
||||
messages.append(MessageItem(type="img", msg=m.url, time=time))
|
||||
elif isinstance(m, At):
|
||||
if group_id:
|
||||
if m.target == "0":
|
||||
@ -72,9 +75,9 @@ async def message_handle(
|
||||
uname = group_user.user_name
|
||||
if m.target not in ID2NAME[group_id]:
|
||||
ID2NAME[group_id][m.target] = uname
|
||||
messages.append(MessageItem(type="at", msg=f"@{uname}"))
|
||||
messages.append(MessageItem(type="at", msg=f"@{uname}", time=time))
|
||||
elif isinstance(m, Hyper):
|
||||
messages.append(MessageItem(type="text", msg="[分享消息]"))
|
||||
messages.append(MessageItem(type="text", msg="[分享消息]", time=time))
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
@ -237,6 +237,8 @@ class MessageItem(BaseModel):
|
||||
"""消息类型"""
|
||||
msg: str
|
||||
"""内容"""
|
||||
time: str
|
||||
"""发送日期"""
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
|
||||
@ -6,13 +6,16 @@ from zhenxun.services.log import logger
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
|
||||
from ....base_model import Result
|
||||
from ....utils import authentication
|
||||
from ....utils import authentication, clear_help_image
|
||||
from .data_source import ApiDataSource
|
||||
from .model import (
|
||||
BatchUpdatePlugins,
|
||||
BatchUpdateResult,
|
||||
PluginCount,
|
||||
PluginDetail,
|
||||
PluginInfo,
|
||||
PluginSwitch,
|
||||
RenameMenuTypePayload,
|
||||
UpdatePlugin,
|
||||
)
|
||||
|
||||
@ -30,9 +33,8 @@ async def _(
|
||||
plugin_type: list[PluginType] = Query(None), menu_type: str | None = None
|
||||
) -> Result[list[PluginInfo]]:
|
||||
try:
|
||||
return Result.ok(
|
||||
await ApiDataSource.get_plugin_list(plugin_type, menu_type), "拿到信息啦!"
|
||||
)
|
||||
result = await ApiDataSource.get_plugin_list(plugin_type, menu_type)
|
||||
return Result.ok(result, "拿到信息啦!")
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/get_plugin_list 调用错误", "WebUi", e=e)
|
||||
return Result.fail(f"发生了一点错误捏 {type(e)}: {e}")
|
||||
@ -78,6 +80,7 @@ async def _() -> Result[PluginCount]:
|
||||
async def _(param: UpdatePlugin) -> Result:
|
||||
try:
|
||||
await ApiDataSource.update_plugin(param)
|
||||
clear_help_image()
|
||||
return Result.ok(info="已经帮你写好啦!")
|
||||
except (ValueError, KeyError):
|
||||
return Result.fail("插件数据不存在...")
|
||||
@ -105,6 +108,7 @@ async def _(param: PluginSwitch) -> Result:
|
||||
db_plugin.block_type = None
|
||||
db_plugin.status = True
|
||||
await db_plugin.save()
|
||||
clear_help_image()
|
||||
return Result.ok(info="成功改变了开关状态!")
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e)
|
||||
@ -144,11 +148,68 @@ async def _() -> Result[list[str]]:
|
||||
)
|
||||
async def _(module: str) -> Result[PluginDetail]:
|
||||
try:
|
||||
return Result.ok(
|
||||
await ApiDataSource.get_plugin_detail(module), "已经帮你写好啦!"
|
||||
)
|
||||
detail = await ApiDataSource.get_plugin_detail(module)
|
||||
return Result.ok(detail, "已经帮你写好啦!")
|
||||
except (ValueError, KeyError):
|
||||
return Result.fail("插件数据不存在...")
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/get_plugin 调用错误", "WebUi", e=e)
|
||||
return Result.fail(f"{type(e)}: {e}")
|
||||
|
||||
|
||||
@router.put(
|
||||
"/plugins/batch_update",
|
||||
dependencies=[authentication()],
|
||||
response_model=Result[BatchUpdateResult],
|
||||
response_class=JSONResponse,
|
||||
summary="批量更新插件配置",
|
||||
)
|
||||
async def batch_update_plugin_config_api(
|
||||
params: BatchUpdatePlugins,
|
||||
) -> Result[BatchUpdateResult]:
|
||||
"""批量更新插件配置,如开关、类型等"""
|
||||
try:
|
||||
result_dict = await ApiDataSource.batch_update_plugins(params=params)
|
||||
result_model = BatchUpdateResult(
|
||||
success=result_dict["success"],
|
||||
updated_count=result_dict["updated_count"],
|
||||
errors=result_dict["errors"],
|
||||
)
|
||||
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,13 +2,20 @@ import re
|
||||
|
||||
import cattrs
|
||||
from fastapi import Query
|
||||
from tortoise.exceptions import DoesNotExist
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.configs.utils import ConfigGroup
|
||||
from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo
|
||||
from zhenxun.utils.enum import BlockType, PluginType
|
||||
|
||||
from .model import PluginConfig, PluginDetail, PluginInfo, UpdatePlugin
|
||||
from .model import (
|
||||
BatchUpdatePlugins,
|
||||
PluginConfig,
|
||||
PluginDetail,
|
||||
PluginInfo,
|
||||
UpdatePlugin,
|
||||
)
|
||||
|
||||
|
||||
class ApiDataSource:
|
||||
@ -44,6 +51,11 @@ class ApiDataSource:
|
||||
level=plugin.level,
|
||||
status=plugin.status,
|
||||
author=plugin.author,
|
||||
block_type=plugin.block_type,
|
||||
is_builtin="builtin_plugins" in plugin.module_path
|
||||
or plugin.plugin_type == PluginType.HIDDEN,
|
||||
allow_setting=plugin.plugin_type != PluginType.HIDDEN,
|
||||
allow_switch=plugin.plugin_type != PluginType.HIDDEN,
|
||||
)
|
||||
plugin_list.append(plugin_info)
|
||||
return plugin_list
|
||||
@ -69,7 +81,6 @@ class ApiDataSource:
|
||||
db_plugin.block_type = param.block_type
|
||||
db_plugin.status = param.block_type != BlockType.ALL
|
||||
await db_plugin.save()
|
||||
# 配置项
|
||||
if param.configs and (configs := Config.get(param.module)):
|
||||
for key in param.configs:
|
||||
if c := configs.configs.get(key):
|
||||
@ -80,6 +91,87 @@ class ApiDataSource:
|
||||
Config.save(save_simple_data=True)
|
||||
return db_plugin
|
||||
|
||||
@classmethod
|
||||
async def batch_update_plugins(cls, params: BatchUpdatePlugins) -> dict:
|
||||
"""批量更新插件数据
|
||||
|
||||
参数:
|
||||
params: BatchUpdatePlugins
|
||||
|
||||
返回:
|
||||
dict: 更新结果, 例如 {'success': True, 'updated_count': 5, 'errors': []}
|
||||
"""
|
||||
plugins_to_update_other_fields = []
|
||||
other_update_fields = set()
|
||||
updated_count = 0
|
||||
errors = []
|
||||
|
||||
for item in params.updates:
|
||||
try:
|
||||
db_plugin = await DbPluginInfo.get(module=item.module)
|
||||
plugin_changed_other = False
|
||||
plugin_changed_block = False
|
||||
|
||||
if db_plugin.block_type != item.block_type:
|
||||
db_plugin.block_type = item.block_type
|
||||
db_plugin.status = item.block_type != BlockType.ALL
|
||||
plugin_changed_block = True
|
||||
|
||||
if item.menu_type is not None and db_plugin.menu_type != item.menu_type:
|
||||
db_plugin.menu_type = item.menu_type
|
||||
other_update_fields.add("menu_type")
|
||||
plugin_changed_other = True
|
||||
|
||||
if (
|
||||
item.default_status is not None
|
||||
and db_plugin.default_status != item.default_status
|
||||
):
|
||||
db_plugin.default_status = item.default_status
|
||||
other_update_fields.add("default_status")
|
||||
plugin_changed_other = True
|
||||
|
||||
if plugin_changed_block:
|
||||
try:
|
||||
await db_plugin.save(update_fields=["block_type", "status"])
|
||||
updated_count += 1
|
||||
except Exception as e_save:
|
||||
errors.append(
|
||||
{
|
||||
"module": item.module,
|
||||
"error": f"Save block_type failed: {e_save!s}",
|
||||
}
|
||||
)
|
||||
plugin_changed_other = False
|
||||
|
||||
if plugin_changed_other:
|
||||
plugins_to_update_other_fields.append(db_plugin)
|
||||
|
||||
except DoesNotExist:
|
||||
errors.append({"module": item.module, "error": "Plugin not found"})
|
||||
except Exception as e:
|
||||
errors.append({"module": item.module, "error": str(e)})
|
||||
|
||||
bulk_updated_count = 0
|
||||
if plugins_to_update_other_fields and other_update_fields:
|
||||
try:
|
||||
await DbPluginInfo.bulk_update(
|
||||
plugins_to_update_other_fields, list(other_update_fields)
|
||||
)
|
||||
bulk_updated_count = len(plugins_to_update_other_fields)
|
||||
except Exception as e_bulk:
|
||||
errors.append(
|
||||
{
|
||||
"module": "batch_update_other",
|
||||
"error": f"Bulk update failed: {e_bulk!s}",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": len(errors) == 0,
|
||||
"updated_count": updated_count + bulk_updated_count,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def __build_plugin_config(
|
||||
cls, module: str, cfg: str, config: ConfigGroup
|
||||
@ -115,6 +207,41 @@ class ApiDataSource:
|
||||
type_inner=type_inner, # type: ignore
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def rename_menu_type(cls, old_name: str, new_name: str) -> dict:
|
||||
"""重命名菜单类型,并更新所有相关插件
|
||||
|
||||
参数:
|
||||
old_name: 旧菜单类型名称
|
||||
new_name: 新菜单类型名称
|
||||
|
||||
返回:
|
||||
dict: 更新结果, 例如 {'success': True, 'updated_count': 3}
|
||||
"""
|
||||
if not old_name or not new_name:
|
||||
raise ValueError("旧名称和新名称都不能为空")
|
||||
if old_name == new_name:
|
||||
return {
|
||||
"success": True,
|
||||
"updated_count": 0,
|
||||
"info": "新旧名称相同,无需更新",
|
||||
}
|
||||
|
||||
# 检查新名称是否已存在(理论上前端会校验,后端再保险一次)
|
||||
exists = await DbPluginInfo.filter(menu_type=new_name).exists()
|
||||
if exists:
|
||||
raise ValueError(f"新的菜单类型名称 '{new_name}' 已被其他插件使用")
|
||||
|
||||
try:
|
||||
# 使用 filter().update() 进行批量更新
|
||||
updated_count = await DbPluginInfo.filter(menu_type=old_name).update(
|
||||
menu_type=new_name
|
||||
)
|
||||
return {"success": True, "updated_count": updated_count}
|
||||
except Exception as e:
|
||||
# 可以添加更详细的日志记录
|
||||
raise RuntimeError(f"数据库更新菜单类型失败: {e!s}")
|
||||
|
||||
@classmethod
|
||||
async def get_plugin_detail(cls, module: str) -> PluginDetail:
|
||||
"""获取插件详情
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.enum import BlockType
|
||||
|
||||
@ -37,19 +37,19 @@ class UpdatePlugin(BaseModel):
|
||||
module: str
|
||||
"""模块"""
|
||||
default_status: bool
|
||||
"""默认开关"""
|
||||
"""是否默认开启"""
|
||||
limit_superuser: bool
|
||||
"""限制超级用户"""
|
||||
cost_gold: int
|
||||
"""金币花费"""
|
||||
menu_type: str
|
||||
"""插件菜单类型"""
|
||||
"""是否限制超级用户"""
|
||||
level: int
|
||||
"""插件所需群权限"""
|
||||
"""等级"""
|
||||
cost_gold: int
|
||||
"""花费金币"""
|
||||
menu_type: str
|
||||
"""菜单类型"""
|
||||
block_type: BlockType | None = None
|
||||
"""禁用类型"""
|
||||
configs: dict[str, Any] | None = None
|
||||
"""配置项"""
|
||||
"""设置项"""
|
||||
|
||||
|
||||
class PluginInfo(BaseModel):
|
||||
@ -58,27 +58,33 @@ class PluginInfo(BaseModel):
|
||||
"""
|
||||
|
||||
module: str
|
||||
"""插件名称"""
|
||||
"""模块"""
|
||||
plugin_name: str
|
||||
"""插件中文名称"""
|
||||
"""插件名称"""
|
||||
default_status: bool
|
||||
"""默认开关"""
|
||||
"""是否默认开启"""
|
||||
limit_superuser: bool
|
||||
"""限制超级用户"""
|
||||
"""是否限制超级用户"""
|
||||
level: int
|
||||
"""等级"""
|
||||
cost_gold: int
|
||||
"""花费金币"""
|
||||
menu_type: str
|
||||
"""插件菜单类型"""
|
||||
"""菜单类型"""
|
||||
version: str
|
||||
"""插件版本"""
|
||||
level: int
|
||||
"""群权限"""
|
||||
"""版本"""
|
||||
status: bool
|
||||
"""当前状态"""
|
||||
"""状态"""
|
||||
author: str | None = None
|
||||
"""作者"""
|
||||
block_type: BlockType | None = None
|
||||
"""禁用类型"""
|
||||
block_type: BlockType | None = Field(None, description="插件禁用状态 (None: 启用)")
|
||||
"""禁用状态"""
|
||||
is_builtin: bool = False
|
||||
"""是否为内置插件"""
|
||||
allow_switch: bool = True
|
||||
"""是否允许开关"""
|
||||
allow_setting: bool = True
|
||||
"""是否允许设置"""
|
||||
|
||||
|
||||
class PluginConfig(BaseModel):
|
||||
@ -86,20 +92,13 @@ class PluginConfig(BaseModel):
|
||||
插件配置项
|
||||
"""
|
||||
|
||||
module: str
|
||||
"""模块"""
|
||||
key: str
|
||||
"""键"""
|
||||
value: Any
|
||||
"""值"""
|
||||
help: str | None = None
|
||||
"""帮助"""
|
||||
default_value: Any
|
||||
"""默认值"""
|
||||
type: Any = None
|
||||
"""值类型"""
|
||||
type_inner: list[str] | None = None
|
||||
"""List Tuple等内部类型检验"""
|
||||
module: str = Field(..., description="模块名")
|
||||
key: str = Field(..., description="键")
|
||||
value: Any = Field(None, description="值")
|
||||
help: str | None = Field(None, description="帮助信息")
|
||||
default_value: Any = Field(None, description="默认值")
|
||||
type: str | None = Field(None, description="类型")
|
||||
type_inner: list[str] | None = Field(None, description="内部类型")
|
||||
|
||||
|
||||
class PluginCount(BaseModel):
|
||||
@ -117,6 +116,21 @@ class PluginCount(BaseModel):
|
||||
"""其他插件"""
|
||||
|
||||
|
||||
class BatchUpdatePluginItem(BaseModel):
|
||||
module: str = Field(..., description="插件模块名")
|
||||
default_status: bool | None = Field(None, description="默认状态(开关)")
|
||||
menu_type: str | None = Field(None, description="菜单类型")
|
||||
block_type: BlockType | None = Field(
|
||||
None, description="插件禁用状态 (None: 启用, ALL: 禁用)"
|
||||
)
|
||||
|
||||
|
||||
class BatchUpdatePlugins(BaseModel):
|
||||
updates: list[BatchUpdatePluginItem] = Field(
|
||||
..., description="要批量更新的插件列表"
|
||||
)
|
||||
|
||||
|
||||
class PluginDetail(PluginInfo):
|
||||
"""
|
||||
插件详情
|
||||
@ -125,6 +139,26 @@ class PluginDetail(PluginInfo):
|
||||
config_list: list[PluginConfig]
|
||||
|
||||
|
||||
class RenameMenuTypePayload(BaseModel):
|
||||
old_name: str = Field(..., description="旧菜单类型名称")
|
||||
new_name: str = Field(..., description="新菜单类型名称")
|
||||
|
||||
|
||||
class PluginIr(BaseModel):
|
||||
id: int
|
||||
"""插件id"""
|
||||
|
||||
|
||||
class BatchUpdateResult(BaseModel):
|
||||
"""
|
||||
批量更新插件结果
|
||||
"""
|
||||
|
||||
success: bool = Field(..., description="是否全部成功")
|
||||
"""是否全部成功"""
|
||||
updated_count: int = Field(..., description="更新成功的数量")
|
||||
"""更新成功的数量"""
|
||||
errors: list[dict[str, str]] = Field(
|
||||
default_factory=list, description="错误信息列表"
|
||||
)
|
||||
"""错误信息列表"""
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from nonebot import require
|
||||
from nonebot.compat import model_dump
|
||||
|
||||
from zhenxun.models.plugin_info import PluginInfo
|
||||
from zhenxun.services.log import logger
|
||||
@ -22,12 +23,12 @@ router = APIRouter(prefix="/store")
|
||||
async def _() -> Result[dict]:
|
||||
try:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import ShopManage
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
data = await ShopManage.get_data()
|
||||
data = await StoreManager.get_data()
|
||||
plugin_list = [
|
||||
{**data[name].to_dict(), "name": name, "id": idx}
|
||||
for idx, name in enumerate(data)
|
||||
{**model_dump(plugin), "name": plugin.name, "id": idx}
|
||||
for idx, plugin in enumerate(data)
|
||||
]
|
||||
modules = await PluginInfo.filter(load_status=True).values_list(
|
||||
"module", flat=True
|
||||
@ -48,9 +49,9 @@ async def _() -> Result[dict]:
|
||||
async def _(param: PluginIr) -> Result:
|
||||
try:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import ShopManage
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
result = await ShopManage.add_plugin(param.id) # type: ignore
|
||||
result = await StoreManager.add_plugin(param.id) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"安装插件失败: {type(e)}: {e}")
|
||||
@ -66,9 +67,9 @@ async def _(param: PluginIr) -> Result:
|
||||
async def _(param: PluginIr) -> Result:
|
||||
try:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import ShopManage
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
result = await ShopManage.update_plugin(param.id) # type: ignore
|
||||
result = await StoreManager.update_plugin(param.id) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"更新插件失败: {type(e)}: {e}")
|
||||
@ -84,9 +85,9 @@ async def _(param: PluginIr) -> Result:
|
||||
async def _(param: PluginIr) -> Result:
|
||||
try:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import ShopManage
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
result = await ShopManage.remove_plugin(param.id) # type: ignore
|
||||
result = await StoreManager.remove_plugin(param.id) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"移除插件失败: {type(e)}: {e}")
|
||||
|
||||
@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse
|
||||
from zhenxun.utils._build_image import BuildImage
|
||||
|
||||
from ....base_model import Result, SystemFolderSize
|
||||
from ....utils import authentication, get_system_disk
|
||||
from ....utils import authentication, get_system_disk, validate_path
|
||||
from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile
|
||||
|
||||
router = APIRouter(prefix="/system")
|
||||
@ -25,7 +25,12 @@ IMAGE_TYPE = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"]
|
||||
description="获取文件列表",
|
||||
)
|
||||
async def _(path: str | None = None) -> Result[list[DirFile]]:
|
||||
base_path = Path(path) if path else Path()
|
||||
try:
|
||||
base_path, error = validate_path(path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not base_path:
|
||||
return Result.fail("无效的路径")
|
||||
data_list = []
|
||||
for file in os.listdir(base_path):
|
||||
file_path = base_path / file
|
||||
@ -36,10 +41,14 @@ async def _(path: str | None = None) -> Result[list[DirFile]]:
|
||||
is_image=is_image,
|
||||
name=file,
|
||||
parent=path,
|
||||
size=None if file_path.is_dir() else file_path.stat().st_size,
|
||||
mtime=file_path.stat().st_mtime,
|
||||
)
|
||||
)
|
||||
sorted(data_list, key=lambda f: f.name)
|
||||
return Result.ok(data_list)
|
||||
except Exception as e:
|
||||
return Result.fail(f"获取文件列表失败: {e!s}")
|
||||
|
||||
|
||||
@router.get(
|
||||
@ -61,8 +70,12 @@ async def _(full_path: str | None = None) -> Result[list[SystemFolderSize]]:
|
||||
description="删除文件",
|
||||
)
|
||||
async def _(param: DeleteFile) -> Result:
|
||||
path = Path(param.full_path)
|
||||
if not path or not path.exists():
|
||||
path, error = validate_path(param.full_path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not path:
|
||||
return Result.fail("无效的路径")
|
||||
if not path.exists():
|
||||
return Result.warning_("文件不存在...")
|
||||
try:
|
||||
path.unlink()
|
||||
@ -79,8 +92,12 @@ async def _(param: DeleteFile) -> Result:
|
||||
description="删除文件夹",
|
||||
)
|
||||
async def _(param: DeleteFile) -> Result:
|
||||
path = Path(param.full_path)
|
||||
if not path or not path.exists() or path.is_file():
|
||||
path, error = validate_path(param.full_path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not path:
|
||||
return Result.fail("无效的路径")
|
||||
if not path.exists() or path.is_file():
|
||||
return Result.warning_("文件夹不存在...")
|
||||
try:
|
||||
shutil.rmtree(path.absolute())
|
||||
@ -97,10 +114,14 @@ async def _(param: DeleteFile) -> Result:
|
||||
description="重命名文件",
|
||||
)
|
||||
async def _(param: RenameFile) -> Result:
|
||||
path = (
|
||||
(Path(param.parent) / param.old_name) if param.parent else Path(param.old_name)
|
||||
)
|
||||
if not path or not path.exists():
|
||||
parent_path, error = validate_path(param.parent)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not parent_path:
|
||||
return Result.fail("无效的路径")
|
||||
|
||||
path = (parent_path / param.old_name) if param.parent else Path(param.old_name)
|
||||
if not path.exists():
|
||||
return Result.warning_("文件不存在...")
|
||||
try:
|
||||
path.rename(path.parent / param.name)
|
||||
@ -117,10 +138,14 @@ async def _(param: RenameFile) -> Result:
|
||||
description="重命名文件夹",
|
||||
)
|
||||
async def _(param: RenameFile) -> Result:
|
||||
path = (
|
||||
(Path(param.parent) / param.old_name) if param.parent else Path(param.old_name)
|
||||
)
|
||||
if not path or not path.exists() or path.is_file():
|
||||
parent_path, error = validate_path(param.parent)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not parent_path:
|
||||
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_("文件夹不存在...")
|
||||
try:
|
||||
new_path = path.parent / param.name
|
||||
@ -138,7 +163,13 @@ async def _(param: RenameFile) -> Result:
|
||||
description="新建文件",
|
||||
)
|
||||
async def _(param: AddFile) -> Result:
|
||||
path = (Path(param.parent) / param.name) if param.parent else Path(param.name)
|
||||
parent_path, error = validate_path(param.parent)
|
||||
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():
|
||||
return Result.warning_("文件已存在...")
|
||||
try:
|
||||
@ -156,7 +187,13 @@ async def _(param: AddFile) -> Result:
|
||||
description="新建文件夹",
|
||||
)
|
||||
async def _(param: AddFile) -> Result:
|
||||
path = (Path(param.parent) / param.name) if param.parent else Path(param.name)
|
||||
parent_path, error = validate_path(param.parent)
|
||||
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():
|
||||
return Result.warning_("文件夹已存在...")
|
||||
try:
|
||||
@ -174,7 +211,11 @@ async def _(param: AddFile) -> Result:
|
||||
description="读取文件",
|
||||
)
|
||||
async def _(full_path: str) -> Result:
|
||||
path = Path(full_path)
|
||||
path, error = validate_path(full_path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not path:
|
||||
return Result.fail("无效的路径")
|
||||
if not path.exists():
|
||||
return Result.warning_("文件不存在...")
|
||||
try:
|
||||
@ -192,9 +233,13 @@ async def _(full_path: str) -> Result:
|
||||
description="读取文件",
|
||||
)
|
||||
async def _(param: SaveFile) -> Result[str]:
|
||||
path = Path(param.full_path)
|
||||
path, error = validate_path(param.full_path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not path:
|
||||
return Result.fail("无效的路径")
|
||||
try:
|
||||
async with aiofiles.open(path, "w", encoding="utf-8") as f:
|
||||
async with aiofiles.open(str(path), "w", encoding="utf-8") as f:
|
||||
await f.write(param.content)
|
||||
return Result.ok("更新成功!")
|
||||
except Exception as e:
|
||||
@ -209,10 +254,24 @@ async def _(param: SaveFile) -> Result[str]:
|
||||
description="读取图片base64",
|
||||
)
|
||||
async def _(full_path: str) -> Result[str]:
|
||||
path = Path(full_path)
|
||||
path, error = validate_path(full_path)
|
||||
if error:
|
||||
return Result.fail(error)
|
||||
if not path:
|
||||
return Result.fail("无效的路径")
|
||||
if not path.exists():
|
||||
return Result.warning_("文件不存在...")
|
||||
try:
|
||||
return Result.ok(BuildImage.open(path).pic2bs4())
|
||||
except Exception as e:
|
||||
return Result.warning_(f"获取图片失败: {e!s}")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/ping",
|
||||
response_model=Result[str],
|
||||
response_class=JSONResponse,
|
||||
description="检查服务器状态",
|
||||
)
|
||||
async def _() -> Result[str]:
|
||||
return Result.ok("pong")
|
||||
|
||||
@ -14,6 +14,10 @@ class DirFile(BaseModel):
|
||||
"""文件夹或文件名称"""
|
||||
parent: str | None = None
|
||||
"""父级"""
|
||||
size: int | None = None
|
||||
"""文件大小"""
|
||||
mtime: float | None = None
|
||||
"""修改时间"""
|
||||
|
||||
|
||||
class DeleteFile(BaseModel):
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import sys
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
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
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ async def update_webui_assets():
|
||||
download_url = await GithubUtils.parse_github_url(
|
||||
WEBUI_DIST_GITHUB_URL
|
||||
).get_archive_download_urls()
|
||||
logger.info("开始下载 webui_assets 资源...", COMMAND_NAME)
|
||||
if await AsyncHttpx.download_file(
|
||||
download_url, webui_assets_path, follow_redirects=True
|
||||
):
|
||||
|
||||