首次启动时提供使用web ui方式完全配置 (#1870)

*  添加全局优先级hook

*  添加基础配置api

*  添加数据库连接测试

* 💬 提示重启

* 🩹 填充过配置时友好提示

* 🐛 首次生成简易配置后自动加载

*  添加配置后重启接口

*  添加重启标志文件

*  添加重启脚本命令

*  添加重启系统限制

*  首次配置判断是否为win系统

* 🔥 移除bat

*  添加关于菜单

*  支持整合包插件安装和添加整合包文档

* 🩹 检测数据库路径

* 🩹 修改数据库路径检测

* 🩹 修改数据库路径检测

* 🩹 修复路径注入

* 🎨 显示添加优先级

* 🐛 修改PriorityLifecycle字典类名称

*  修复路径问题

*  修复路径检测

*  新增路径验证功能,确保用户输入的路径安全并在项目根目录内

*  优化路径验证功能,增加对非法字符和路径长度的检查,确保用户输入的路径更加安全

* 🚨 auto fix by pre-commit hooks

*  优化获取文件列表的代码格式

* 📝 修改README中webui示例图

*  更新PriorityLifecycle.on_startup装饰器

*  简化安装依赖的命令构建逻辑

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
HibiKier 2025-06-16 09:11:41 +08:00 committed by GitHub
parent bcfb47d9fd
commit 99f1388e23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 581 additions and 140 deletions

114
README.md
View File

@ -112,7 +112,7 @@ AccessToken: PUBLIC_ZHENXUN_TEST
| [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 | | [插件库](https://github.com/zhenxun-org/zhenxun_bot_plugins) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 原 plugins 文件夹插件 |
| [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 | | [插件索引库](https://github.com/zhenxun-org/zhenxun_bot_plugins_index) | 插件 | [zhenxun-org](https://github.com/zhenxun-org) | 扩展插件索引库 |
| [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 | | [一键安装](https://github.com/soloxiaoye2022/zhenxun_bot-deploy) | 安装 | [soloxiaoye2022](https://github.com/soloxiaoye2022) | 第三方 |
| [WebUi](https://github.com/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) | 第三方 | | [安卓 app(WebUi)](https://github.com/YuS1aN/zhenxun_bot_android_ui) | 安装 | [YuS1aN](https://github.com/YuS1aN) | 第三方 |
</div> </div>
@ -126,6 +126,28 @@ AccessToken: PUBLIC_ZHENXUN_TEST
- 提供了 cd阻塞每日次数等限制仅仅通过简单的属性就可以生成一个限制例如`PluginCdBlock` 等 - 提供了 cd阻塞每日次数等限制仅仅通过简单的属性就可以生成一个限制例如`PluginCdBlock` 等
- **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!** - **更多详细请通过 [传送门](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 ```bash
@ -272,12 +294,12 @@ DB_URL 是基于 Tortoise ORM 的数据库连接字符串,用于指定项目
## ❔ 需要帮助? ## ❔ 需要帮助?
> [!TIP] > [!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/) > - 善用[搜索引擎](https://www.google.com/)
> - 查阅 issue 中是否有类似问题,如果没有请按照模板发起 issue > - 查阅 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) 首席设计师:[酥酥/coldly-ss](https://github.com/coldly-ss)
LOGO 设计:[FrostN0v0](https://github.com/FrostN0v0)
## 🙏 感谢 ## 🙏 感谢
[botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议 [botuniverse / onebot](https://github.com/botuniverse/onebot) :超棒的机器人协议
@ -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"/> <img src="https://contrib.rocks/image?repo=HibiKier/zhenxun_bot&max=1000" alt="contributors"/>
</a> </a>
## 📸 WebUI 界面展示 ## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端)
<div style="display: flex; flex-wrap: wrap; justify-content: space-between;"> <div style="display: flex; flex-wrap: wrap; justify-content: space-between;">
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui00.png" alt="webui00" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui01.png" alt="webui01" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;"> #### 登录界面
<img src="./docs_image/webui02.png" alt="webui02" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui03.png" alt="webui03" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;"> ![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-login.jpg)
<img src="./docs_image/webui04.png" alt="webui04" style="width: 100%; height: auto;">
</div> #### API 设置
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui05.png" alt="webui05" style="width: 100%; height: auto;"> ![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-api.jpg)
</div>
#### 仪表盘
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard.jpg)
#### 仪表盘(展开)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-dashboard1.jpg)
#### 控制台
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-command.jpg)
#### 插件列表
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin.jpg)
#### 插件列表(配置项)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-plugin1.jpg)
#### 插件商店
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-store.jpg)
#### 好友/群组管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage.jpg)
#### 请求管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-manage1.jpg)
#### 数据库管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-database.jpg)
### 文件管理
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system.jpg)
### 文件管理(文本查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system1.jpg)
### 文件管理(图片查看)
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-system2.jpg)
### 关于
![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-about.jpg)
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui06.png" alt="webui06" style="width: 100%; height: auto;">
</div>
<div style="width: 48%; margin-bottom: 10px;">
<img src="./docs_image/webui07.png" alt="webui07" style="width: 100%; height: auto;">
</div>
</div> </div>

4
bot.py
View File

@ -14,9 +14,9 @@ driver.register_adapter(OneBotV11Adapter)
# driver.register_adapter(DoDoAdapter) # driver.register_adapter(DoDoAdapter)
# driver.register_adapter(DiscordAdapter) # driver.register_adapter(DiscordAdapter)
from zhenxun.services.db_context import disconnect, init from zhenxun.services.db_context import disconnect
driver.on_startup(init) # driver.on_startup(init)
driver.on_shutdown(disconnect) driver.on_shutdown(disconnect)
# nonebot.load_builtin_plugins("echo") # nonebot.load_builtin_plugins("echo")

BIN
docs_image/pc-about.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

BIN
docs_image/pc-api.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

BIN
docs_image/pc-command.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

BIN
docs_image/pc-dashboard.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

BIN
docs_image/pc-database.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

BIN
docs_image/pc-login.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

BIN
docs_image/pc-manage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 KiB

BIN
docs_image/pc-manage1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

BIN
docs_image/pc-plugin.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

BIN
docs_image/pc-plugin1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

BIN
docs_image/pc-store.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

BIN
docs_image/pc-system.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
docs_image/pc-system1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
docs_image/pc-system2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

View File

@ -16,6 +16,7 @@ from zhenxun.models.sign_user import SignUser
from zhenxun.models.user_console import UserConsole from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.decorator.shop import shop_register from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.manager.resource_manager import ResourceManager from zhenxun.utils.manager.resource_manager import ResourceManager
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
@ -70,7 +71,7 @@ from public.bag_users t1
""" """
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
await ResourceManager.init_resources() await ResourceManager.init_resources()
"""签到与用户的数据迁移""" """签到与用户的数据迁移"""

View File

@ -14,6 +14,7 @@ from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage from zhenxun.utils._build_image import BuildImage
from zhenxun.utils._image_template import ImageTemplate from zhenxun.utils._image_template import ImageTemplate
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
BASE_PATH = DATA_PATH / "welcome_message" BASE_PATH = DATA_PATH / "welcome_message"
@ -91,7 +92,7 @@ def migrate(path: Path):
json.dump(new_data, f, ensure_ascii=False, indent=4) json.dump(new_data, f, ensure_ascii=False, indent=4)
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
def _(): def _():
"""数据迁移 """数据迁移

View File

@ -11,6 +11,7 @@ from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH from zhenxun.configs.path_config import DATA_PATH
from zhenxun.configs.utils import RegisterConfig from zhenxun.configs.utils import RegisterConfig
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
_yaml = YAML(pure=True) _yaml = YAML(pure=True)
_yaml.allow_unicode = True _yaml.allow_unicode = True
@ -102,7 +103,7 @@ def _generate_simple_config(exists_module: list[str]):
temp_file.unlink() temp_file.unlink()
@driver.on_startup @PriorityLifecycle.on_startup(priority=0)
def _(): def _():
""" """
初始化插件数据配置 初始化插件数据配置
@ -125,3 +126,4 @@ def _():
with plugins2config_file.open("w", encoding="utf8") as wf: with plugins2config_file.open("w", encoding="utf8") as wf:
_yaml.dump(_data, wf) _yaml.dump(_data, wf)
_generate_simple_config(exists_module) _generate_simple_config(exists_module)
Config.reload()

View File

@ -20,6 +20,7 @@ from zhenxun.utils.enum import (
PluginLimitType, PluginLimitType,
PluginType, PluginType,
) )
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .manager import manager from .manager import manager
@ -95,7 +96,7 @@ async def _handle_setting(
) )
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
""" """
初始化插件数据配置 初始化插件数据配置

View File

@ -10,6 +10,7 @@ from zhenxun.models.group_console import GroupConsole
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.common_utils import CommonUtils from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@ -132,7 +133,7 @@ async def create_schedule(task: Task):
logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e) logger.error(f"动态创建定时任务 {task.name}({task.module}) 失败", e=e)
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
""" """
初始化插件数据配置 初始化插件数据配置

View File

@ -18,6 +18,12 @@ from zhenxun.utils.utils import is_number
from .config import BASE_PATH, DEFAULT_GITHUB_URL, EXTRA_GITHUB_URL from .config import BASE_PATH, DEFAULT_GITHUB_URL, EXTRA_GITHUB_URL
BAT_FILE = Path() / "win启动.bat"
WIN_COMMAND = ["./Python310/python.exe", "-m", "pip", "install", "-r"]
DEFAULT_COMMAND = ["poetry", "run", "pip", "install", "-r"]
def row_style(column: str, text: str) -> RowStyle: def row_style(column: str, text: str) -> RowStyle:
"""被动技能文本风格 """被动技能文本风格
@ -50,8 +56,10 @@ def install_requirement(plugin_path: Path):
return return
try: try:
command = WIN_COMMAND if BAT_FILE.exists() else DEFAULT_COMMAND
command.append(str(existing_requirements))
result = subprocess.run( result = subprocess.run(
["poetry", "run", "pip", "install", "-r", str(existing_requirements)], command,
check=True, check=True,
capture_output=True, capture_output=True,
text=True, text=True,

View File

@ -1,12 +1,8 @@
import nonebot
from nonebot.drivers import Driver
from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_console import GroupConsole
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
driver: Driver = nonebot.get_driver()
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
"""开启/禁用插件格式修改""" """开启/禁用插件格式修改"""
_, is_create = await GroupConsole.get_or_create(group_id=133133133) _, is_create = await GroupConsole.get_or_create(group_id=133133133)

View File

@ -10,7 +10,6 @@ from nonebot_plugin_alconna import (
store_true, store_true,
) )
from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_apscheduler import scheduler
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.utils import ( from zhenxun.configs.utils import (
Command, Command,
@ -23,7 +22,7 @@ from zhenxun.utils.depends import UserName
from zhenxun.utils.message import MessageUtils from zhenxun.utils.message import MessageUtils
from ._data_source import SignManage from ._data_source import SignManage
from .goods_register import driver # noqa: F401 from .goods_register import Uninfo
from .utils import clear_sign_data_pic from .utils import clear_sign_data_pic
__plugin_meta__ = PluginMetadata( __plugin_meta__ = PluginMetadata(

View File

@ -1,7 +1,6 @@
from decimal import Decimal from decimal import Decimal
import nonebot import nonebot
from nonebot.drivers import Driver
from nonebot_plugin_uninfo import Uninfo from nonebot_plugin_uninfo import Uninfo
from zhenxun.models.sign_user import SignUser from zhenxun.models.sign_user import SignUser
@ -9,14 +8,7 @@ from zhenxun.models.user_console import UserConsole
from zhenxun.utils.decorator.shop import shop_register from zhenxun.utils.decorator.shop import shop_register
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
driver: Driver = nonebot.get_driver() driver = nonebot.get_driver()
# @driver.on_startup
# async def _():
# """
# 导入内置的三个商品
# """
@shop_register( @shop_register(

View File

@ -16,6 +16,7 @@ from zhenxun.models.sign_log import SignLog
from zhenxun.models.sign_user import SignUser from zhenxun.models.sign_user import SignUser
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.image_utils import BuildImage from zhenxun.utils.image_utils import BuildImage
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from .config import ( from .config import (
@ -54,7 +55,7 @@ LG_MESSAGE = [
] ]
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def init_image(): async def init_image():
SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True)
SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True)

View File

@ -10,7 +10,9 @@ from zhenxun.configs.config import Config as gConfig
from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger, logger_ from zhenxun.services.log import logger, logger_
from zhenxun.utils.enum import PluginType from zhenxun.utils.enum import PluginType
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .api.configure import router as configure_router
from .api.logs import router as ws_log_routes from .api.logs import router as ws_log_routes
from .api.logs.log_manager import LOG_STORAGE from .api.logs.log_manager import LOG_STORAGE
from .api.menu import router as menu_router from .api.menu import router as menu_router
@ -81,6 +83,7 @@ BaseApiRouter.include_router(database_router)
BaseApiRouter.include_router(plugin_router) BaseApiRouter.include_router(plugin_router)
BaseApiRouter.include_router(system_router) BaseApiRouter.include_router(system_router)
BaseApiRouter.include_router(menu_router) BaseApiRouter.include_router(menu_router)
BaseApiRouter.include_router(configure_router)
WsApiRouter = APIRouter(prefix="/zhenxun/socket") WsApiRouter = APIRouter(prefix="/zhenxun/socket")
@ -89,7 +92,7 @@ WsApiRouter.include_router(status_routes)
WsApiRouter.include_router(chat_routes) WsApiRouter.include_router(chat_routes)
@driver.on_startup @PriorityLifecycle.on_startup(priority=0)
async def _(): async def _():
try: try:
# 存储任务引用的列表,防止任务被垃圾回收 # 存储任务引用的列表,防止任务被垃圾回收

View 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

View 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()

View 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
"""前端密码"""

View File

@ -5,18 +5,7 @@ from zhenxun.services.log import logger
from .model import MenuData, MenuItem from .model import MenuData, MenuItem
default_menus = [
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 = [
MenuItem( MenuItem(
name="仪表盘", name="仪表盘",
module="dashboard", module="dashboard",
@ -30,30 +19,50 @@ class MenuManage:
router="/command", router="/command",
icon="command", icon="command",
), ),
MenuItem( MenuItem(name="插件列表", module="plugin", router="/plugin", icon="plugin"),
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="store", router="/store", icon="store"
),
MenuItem(
name="好友/群组", module="manage", router="/manage", icon="user"
),
MenuItem( MenuItem(
name="数据库管理", name="数据库管理",
module="database", module="database",
router="/database", router="/database",
icon="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( MenuItem(
name="文件管理", module="system", router="/system", icon="system" **next(m for m in self.menu if m["module"] == module)
), )
MenuItem( )
name="关于我们", module="about", router="/about", icon="about" 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() 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): def get_menus(self):
return MenuData(menus=self.menu) return MenuData(menus=self.menu)
@ -64,4 +73,4 @@ class MenuManage:
json.dump(temp, f, ensure_ascii=False, indent=4) json.dump(temp, f, ensure_ascii=False, indent=4)
menu_manage = MenuManage() menu_manage = MenuManager()

View File

@ -13,6 +13,7 @@ from zhenxun.models.bot_connect_log import BotConnectLog
from zhenxun.models.chat_history import ChatHistory from zhenxun.models.chat_history import ChatHistory
from zhenxun.models.statistics import Statistics from zhenxun.models.statistics import Statistics
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.platform import PlatformUtils
from ....base_model import BaseResultModel, QueryModel from ....base_model import BaseResultModel, QueryModel
@ -31,7 +32,7 @@ driver: Driver = nonebot.get_driver()
CONNECT_TIME = 0 CONNECT_TIME = 0
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
global CONNECT_TIME global CONNECT_TIME
CONNECT_TIME = int(time.time()) CONNECT_TIME = int(time.time())

View File

@ -8,6 +8,7 @@ from zhenxun.configs.config import BotConfig
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from ....base_model import BaseResultModel, QueryModel, Result from ....base_model import BaseResultModel, QueryModel, Result
from ....utils import authentication from ....utils import authentication
@ -21,7 +22,7 @@ router = APIRouter(prefix="/database")
driver: Driver = nonebot.get_driver() driver: Driver = nonebot.get_driver()
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
for plugin in nonebot.get_loaded_plugins(): for plugin in nonebot.get_loaded_plugins():
module = plugin.name module = plugin.name

View File

@ -9,7 +9,7 @@ from fastapi.responses import JSONResponse
from zhenxun.utils._build_image import BuildImage from zhenxun.utils._build_image import BuildImage
from ....base_model import Result, SystemFolderSize from ....base_model import Result, SystemFolderSize
from ....utils import authentication, get_system_disk from ....utils import authentication, get_system_disk, validate_path
from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile
router = APIRouter(prefix="/system") router = APIRouter(prefix="/system")
@ -25,7 +25,12 @@ IMAGE_TYPE = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"]
description="获取文件列表", description="获取文件列表",
) )
async def _(path: str | None = None) -> Result[list[DirFile]]: async def _(path: str | None = None) -> Result[list[DirFile]]:
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 = [] data_list = []
for file in os.listdir(base_path): for file in os.listdir(base_path):
file_path = base_path / file file_path = base_path / file
@ -41,6 +46,8 @@ async def _(path: str | None = None) -> Result[list[DirFile]]:
) )
) )
return Result.ok(data_list) return Result.ok(data_list)
except Exception as e:
return Result.fail(f"获取文件列表失败: {e!s}")
@router.get( @router.get(
@ -62,8 +69,12 @@ async def _(full_path: str | None = None) -> Result[list[SystemFolderSize]]:
description="删除文件", description="删除文件",
) )
async def _(param: DeleteFile) -> Result: async def _(param: DeleteFile) -> Result:
path = Path(param.full_path) path, error = validate_path(param.full_path)
if not path or not path.exists(): if error:
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
path.unlink() path.unlink()
@ -80,8 +91,12 @@ async def _(param: DeleteFile) -> Result:
description="删除文件夹", description="删除文件夹",
) )
async def _(param: DeleteFile) -> Result: async def _(param: DeleteFile) -> Result:
path = Path(param.full_path) path, error = validate_path(param.full_path)
if not path or not path.exists() or path.is_file(): if error:
return Result.fail(error)
if not path:
return Result.fail("无效的路径")
if not path.exists() or path.is_file():
return Result.warning_("文件夹不存在...") return Result.warning_("文件夹不存在...")
try: try:
shutil.rmtree(path.absolute()) shutil.rmtree(path.absolute())
@ -98,10 +113,14 @@ async def _(param: DeleteFile) -> Result:
description="重命名文件", description="重命名文件",
) )
async def _(param: RenameFile) -> Result: async def _(param: RenameFile) -> Result:
path = ( parent_path, error = validate_path(param.parent)
(Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) if error:
) return Result.fail(error)
if not path or not path.exists(): 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_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
path.rename(path.parent / param.name) path.rename(path.parent / param.name)
@ -118,10 +137,14 @@ async def _(param: RenameFile) -> Result:
description="重命名文件夹", description="重命名文件夹",
) )
async def _(param: RenameFile) -> Result: async def _(param: RenameFile) -> Result:
path = ( parent_path, error = validate_path(param.parent)
(Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) if error:
) return Result.fail(error)
if not path or not path.exists() or path.is_file(): 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_("文件夹不存在...") return Result.warning_("文件夹不存在...")
try: try:
new_path = path.parent / param.name new_path = path.parent / param.name
@ -139,7 +162,13 @@ async def _(param: RenameFile) -> Result:
description="新建文件", description="新建文件",
) )
async def _(param: AddFile) -> Result: 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(): if path.exists():
return Result.warning_("文件已存在...") return Result.warning_("文件已存在...")
try: try:
@ -157,7 +186,13 @@ async def _(param: AddFile) -> Result:
description="新建文件夹", description="新建文件夹",
) )
async def _(param: AddFile) -> Result: 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(): if path.exists():
return Result.warning_("文件夹已存在...") return Result.warning_("文件夹已存在...")
try: try:
@ -175,7 +210,11 @@ async def _(param: AddFile) -> Result:
description="读取文件", description="读取文件",
) )
async def _(full_path: str) -> Result: 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(): if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:
@ -193,9 +232,13 @@ async def _(full_path: str) -> Result:
description="读取文件", description="读取文件",
) )
async def _(param: SaveFile) -> Result[str]: 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: 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) await f.write(param.content)
return Result.ok("更新成功!") return Result.ok("更新成功!")
except Exception as e: except Exception as e:
@ -210,7 +253,11 @@ async def _(param: SaveFile) -> Result[str]:
description="读取图片base64", description="读取图片base64",
) )
async def _(full_path: str) -> Result[str]: async def _(full_path: str) -> Result[str]:
path = 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(): if not path.exists():
return Result.warning_("文件不存在...") return Result.warning_("文件不存在...")
try: try:

View File

@ -1,6 +1,12 @@
import sys
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import nonebot import nonebot
from strenum import StrEnum
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
from zhenxun.configs.path_config import DATA_PATH, TEMP_PATH from zhenxun.configs.path_config import DATA_PATH, TEMP_PATH

View File

@ -18,6 +18,7 @@ async def update_webui_assets():
download_url = await GithubUtils.parse_github_url( download_url = await GithubUtils.parse_github_url(
WEBUI_DIST_GITHUB_URL WEBUI_DIST_GITHUB_URL
).get_archive_download_urls() ).get_archive_download_urls()
logger.info("开始下载 webui_assets 资源...", COMMAND_NAME)
if await AsyncHttpx.download_file( if await AsyncHttpx.download_file(
download_url, webui_assets_path, follow_redirects=True download_url, webui_assets_path, follow_redirects=True
): ):

View File

@ -2,6 +2,7 @@ import contextlib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import os import os
from pathlib import Path from pathlib import Path
import re
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
@ -28,6 +29,45 @@ if token_file.exists():
token_data = json.load(open(token_file, encoding="utf8")) token_data = json.load(open(token_file, encoding="utf8"))
def validate_path(path_str: str | None) -> tuple[Path | None, str | None]:
"""验证路径是否安全
参数:
path_str: 用户输入的路径
返回:
tuple[Path | None, str | None]: (验证后的路径, 错误信息)
"""
try:
if not path_str:
return Path().resolve(), None
# 1. 移除任何可能的路径遍历尝试
path_str = re.sub(r"[\\/]\.\.[\\/]", "", path_str)
# 2. 规范化路径并转换为绝对路径
path = Path(path_str).resolve()
# 3. 获取项目根目录
root_dir = Path().resolve()
# 4. 验证路径是否在项目根目录内
try:
if not path.is_relative_to(root_dir):
return None, "访问路径超出允许范围"
except ValueError:
return None, "无效的路径格式"
# 5. 验证路径是否包含任何危险字符
if any(c in str(path) for c in ["..", "~", "*", "?", ">", "<", "|", '"']):
return None, "路径包含非法字符"
# 6. 验证路径长度是否合理
return (None, "路径长度超出限制") if len(str(path)) > 4096 else (path, None)
except Exception as e:
return None, f"路径验证失败: {e!s}"
GROUP_HELP_PATH = DATA_PATH / "group_help" GROUP_HELP_PATH = DATA_PATH / "group_help"
SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png" SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png"
SIMPLE_DETAIL_HELP_IMAGE = IMAGE_PATH / "SIMPLE_DETAIL_HELP.png" SIMPLE_DETAIL_HELP_IMAGE = IMAGE_PATH / "SIMPLE_DETAIL_HELP.png"

View File

@ -1,9 +1,12 @@
import nonebot
from nonebot.utils import is_coroutine_callable from nonebot.utils import is_coroutine_callable
from tortoise import Tortoise from tortoise import Tortoise
from tortoise.connection import connections from tortoise.connection import connections
from tortoise.models import Model as Model_ from tortoise.models import Model as Model_
from zhenxun.configs.config import BotConfig from zhenxun.configs.config import BotConfig
from zhenxun.utils.exception import HookPriorityException
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
from .log import logger from .log import logger
@ -11,6 +14,9 @@ SCRIPT_METHOD = []
MODELS: list[str] = [] MODELS: list[str] = []
driver = nonebot.get_driver()
class Model(Model_): class Model(Model_):
""" """
自动添加模块 自动添加模块
@ -26,7 +32,7 @@ class Model(Model_):
SCRIPT_METHOD.append((cls.__module__, func)) SCRIPT_METHOD.append((cls.__module__, func))
class DbUrlIsNode(Exception): class DbUrlIsNode(HookPriorityException):
""" """
数据库链接地址为空 数据库链接地址为空
""" """
@ -42,9 +48,19 @@ class DbConnectError(Exception):
pass pass
@PriorityLifecycle.on_startup(priority=1)
async def init(): async def init():
if not BotConfig.db_url: if not BotConfig.db_url:
raise DbUrlIsNode("数据库配置为空,请在.env.dev中配置DB_URL...") # raise DbUrlIsNode("数据库配置为空,请在.env.dev中配置DB_URL...")
error = f"""
**********************************************************************
🌟 **************************** 配置为空 ************************* 🌟
🚀 请打开 WebUi 进行基础配置 🚀
🌐 配置地址http://{driver.config.host}:{driver.config.port}/#/configure 🌐
***********************************************************************
***********************************************************************
"""
raise DbUrlIsNode("\n" + error.strip())
try: try:
await Tortoise.init( await Tortoise.init(
db_url=BotConfig.db_url, db_url=BotConfig.db_url,

View File

@ -6,6 +6,7 @@ from nonebot.utils import is_coroutine_callable
from pydantic import BaseModel from pydantic import BaseModel
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
driver = nonebot.get_driver() driver = nonebot.get_driver()
@ -100,6 +101,6 @@ class PluginInitManager:
logger.error(f"执行: {module_path}:remove 失败", e=e) logger.error(f"执行: {module_path}:remove 失败", e=e)
@driver.on_startup @PriorityLifecycle.on_startup(priority=5)
async def _(): async def _():
await PluginInitManager.install_all() await PluginInitManager.install_all()

View File

@ -1,12 +1,17 @@
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
import random import random
import sys
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from strenum import StrEnum
from ._build_image import BuildImage from ._build_image import BuildImage
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
class MatType(StrEnum): class MatType(StrEnum):
LINE = "LINE" LINE = "LINE"

View File

@ -1,4 +1,16 @@
from strenum import StrEnum import sys
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
class PriorityLifecycleType(StrEnum):
STARTUP = "STARTUP"
"""启动"""
SHUTDOWN = "SHUTDOWN"
"""关闭"""
class BankHandleType(StrEnum): class BankHandleType(StrEnum):

View File

@ -1,3 +1,15 @@
class HookPriorityException(BaseException):
"""
钩子优先级异常
"""
def __init__(self, info: str = "") -> None:
self.info = info
def __str__(self) -> str:
return self.info
class NotFoundError(Exception): class NotFoundError(Exception):
""" """
未发现 未发现

View File

@ -1,13 +1,18 @@
import contextlib import contextlib
import sys
from typing import Protocol from typing import Protocol
from aiocache import cached from aiocache import cached
from nonebot.compat import model_dump from nonebot.compat import model_dump
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from strenum import StrEnum
from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.http_utils import AsyncHttpx
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from strenum import StrEnum
from .const import ( from .const import (
CACHED_API_TTL, CACHED_API_TTL,
GIT_API_COMMIT_FORMAT, GIT_API_COMMIT_FORMAT,

View File

@ -22,6 +22,4 @@ class MessageManager:
@classmethod @classmethod
def get(cls, uid: str) -> list[str]: def get(cls, uid: str) -> list[str]:
if uid in cls.data: return cls.data[uid] if uid in cls.data else []
return cls.data[uid]
return []

View File

@ -0,0 +1,57 @@
from collections.abc import Callable
from typing import ClassVar
import nonebot
from nonebot.utils import is_coroutine_callable
from zhenxun.services.log import logger
from zhenxun.utils.enum import PriorityLifecycleType
from zhenxun.utils.exception import HookPriorityException
driver = nonebot.get_driver()
class PriorityLifecycle:
_data: ClassVar[dict[PriorityLifecycleType, dict[int, list[Callable]]]] = {}
@classmethod
def add(cls, hook_type: PriorityLifecycleType, func: Callable, priority: int):
if hook_type not in cls._data:
cls._data[hook_type] = {}
if priority not in cls._data[hook_type]:
cls._data[hook_type][priority] = []
cls._data[hook_type][priority].append(func)
@classmethod
def on_startup(cls, *, priority: int):
def wrapper(func):
cls.add(PriorityLifecycleType.STARTUP, func, priority)
return func
return wrapper
@classmethod
def on_shutdown(cls, *, priority: int):
def wrapper(func):
cls.add(PriorityLifecycleType.SHUTDOWN, func, priority)
return func
return wrapper
@driver.on_startup
async def _():
priority_data = PriorityLifecycle._data.get(PriorityLifecycleType.STARTUP)
if not priority_data:
return
priority_list = sorted(priority_data.keys())
priority = 0
try:
for priority in priority_list:
for func in priority_data[priority]:
if is_coroutine_callable(func):
await func()
else:
func()
except HookPriorityException as e:
logger.error(f"打断优先级 [{priority}] on_startup 方法. {type(e)}: {e}")