diff --git a/README.md b/README.md index 57f8d844..72641550 100644 --- a/README.md +++ b/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) | 第三方 | @@ -126,6 +126,28 @@ AccessToken: PUBLIC_ZHENXUN_TEST - 提供了 cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等 - **更多详细请通过 [传送门](https://zhenxun-org.github.io/zhenxun_bot/) 查看文档!** +## 🐣 小白整合 + +如果你系统是 **Windows** 且不想下载 Python +可以使用整合包(Python3.10+zhenxun+webui) + +文档地址:[整合包文档](https://hibikier.github.io/zhenxun_bot/beginner/) + +
+下载地址 + +- **百度云:** + 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 + +
+ ## 🛠️ 简单部署 ```bash @@ -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) contributors -## 📸 WebUI 界面展示 +## 📸 WebUI 界面展示(仅展示默认主题下的 pc 端)
-
- webui00 -
-
- webui01 -
-
- webui02 -
-
- webui03 -
+#### 登录界面 -
- webui04 -
-
- webui05 -
+![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-login.jpg) + +#### API 设置 + +![x](https://github.com/zhenxun-org/zhenxun_bot/blob/main/docs_image/pc-api.jpg) + +#### 仪表盘 + +![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) -
- webui06 -
-
- webui07 -
diff --git a/bot.py b/bot.py index 52cd29fc..aa047a71 100644 --- a/bot.py +++ b/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") diff --git a/docs_image/pc-about.jpg b/docs_image/pc-about.jpg new file mode 100644 index 00000000..0bef7a9e Binary files /dev/null and b/docs_image/pc-about.jpg differ diff --git a/docs_image/pc-api.jpg b/docs_image/pc-api.jpg new file mode 100644 index 00000000..59cee887 Binary files /dev/null and b/docs_image/pc-api.jpg differ diff --git a/docs_image/pc-command.jpg b/docs_image/pc-command.jpg new file mode 100644 index 00000000..0e310e29 Binary files /dev/null and b/docs_image/pc-command.jpg differ diff --git a/docs_image/pc-dashboard.jpg b/docs_image/pc-dashboard.jpg new file mode 100644 index 00000000..0478a850 Binary files /dev/null and b/docs_image/pc-dashboard.jpg differ diff --git a/docs_image/pc-dashboard1.jpg b/docs_image/pc-dashboard1.jpg new file mode 100644 index 00000000..3a0bc958 Binary files /dev/null and b/docs_image/pc-dashboard1.jpg differ diff --git a/docs_image/pc-database.jpg b/docs_image/pc-database.jpg new file mode 100644 index 00000000..68c60aa3 Binary files /dev/null and b/docs_image/pc-database.jpg differ diff --git a/docs_image/pc-login.jpg b/docs_image/pc-login.jpg new file mode 100644 index 00000000..65fe8b46 Binary files /dev/null and b/docs_image/pc-login.jpg differ diff --git a/docs_image/pc-manage.jpg b/docs_image/pc-manage.jpg new file mode 100644 index 00000000..e5f8902a Binary files /dev/null and b/docs_image/pc-manage.jpg differ diff --git a/docs_image/pc-manage1.jpg b/docs_image/pc-manage1.jpg new file mode 100644 index 00000000..4756c629 Binary files /dev/null and b/docs_image/pc-manage1.jpg differ diff --git a/docs_image/pc-plugin.jpg b/docs_image/pc-plugin.jpg new file mode 100644 index 00000000..147e26eb Binary files /dev/null and b/docs_image/pc-plugin.jpg differ diff --git a/docs_image/pc-plugin1.jpg b/docs_image/pc-plugin1.jpg new file mode 100644 index 00000000..58694e6d Binary files /dev/null and b/docs_image/pc-plugin1.jpg differ diff --git a/docs_image/pc-store.jpg b/docs_image/pc-store.jpg new file mode 100644 index 00000000..4c9b68e4 Binary files /dev/null and b/docs_image/pc-store.jpg differ diff --git a/docs_image/pc-system.jpg b/docs_image/pc-system.jpg new file mode 100644 index 00000000..9908a2bd Binary files /dev/null and b/docs_image/pc-system.jpg differ diff --git a/docs_image/pc-system1.jpg b/docs_image/pc-system1.jpg new file mode 100644 index 00000000..3333a1b5 Binary files /dev/null and b/docs_image/pc-system1.jpg differ diff --git a/docs_image/pc-system2.jpg b/docs_image/pc-system2.jpg new file mode 100644 index 00000000..649a5bc9 Binary files /dev/null and b/docs_image/pc-system2.jpg differ diff --git a/docs_image/webui00.png b/docs_image/webui00.png deleted file mode 100644 index 71f7d368..00000000 Binary files a/docs_image/webui00.png and /dev/null differ diff --git a/docs_image/webui01.png b/docs_image/webui01.png deleted file mode 100644 index cd415685..00000000 Binary files a/docs_image/webui01.png and /dev/null differ diff --git a/docs_image/webui02.png b/docs_image/webui02.png deleted file mode 100644 index 0fcc4f05..00000000 Binary files a/docs_image/webui02.png and /dev/null differ diff --git a/docs_image/webui03.png b/docs_image/webui03.png deleted file mode 100644 index 2e7426e3..00000000 Binary files a/docs_image/webui03.png and /dev/null differ diff --git a/docs_image/webui04.png b/docs_image/webui04.png deleted file mode 100644 index 5810f71b..00000000 Binary files a/docs_image/webui04.png and /dev/null differ diff --git a/docs_image/webui05.png b/docs_image/webui05.png deleted file mode 100644 index d5f5e304..00000000 Binary files a/docs_image/webui05.png and /dev/null differ diff --git a/docs_image/webui06.png b/docs_image/webui06.png deleted file mode 100644 index 7541f679..00000000 Binary files a/docs_image/webui06.png and /dev/null differ diff --git a/docs_image/webui07.png b/docs_image/webui07.png deleted file mode 100644 index 1628ade7..00000000 Binary files a/docs_image/webui07.png and /dev/null differ diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index fbaeb280..f2688905 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -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() """签到与用户的数据迁移""" diff --git a/zhenxun/builtin_plugins/admin/welcome_message/data_source.py b/zhenxun/builtin_plugins/admin/welcome_message/data_source.py index 2ccb33ee..c8e486ed 100644 --- a/zhenxun/builtin_plugins/admin/welcome_message/data_source.py +++ b/zhenxun/builtin_plugins/admin/welcome_message/data_source.py @@ -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 _(): """数据迁移 diff --git a/zhenxun/builtin_plugins/init/init_config.py b/zhenxun/builtin_plugins/init/init_config.py index 112d29de..eef63635 100644 --- a/zhenxun/builtin_plugins/init/init_config.py +++ b/zhenxun/builtin_plugins/init/init_config.py @@ -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 @@ -102,7 +103,7 @@ def _generate_simple_config(exists_module: list[str]): temp_file.unlink() -@driver.on_startup +@PriorityLifecycle.on_startup(priority=0) def _(): """ 初始化插件数据配置 @@ -125,3 +126,4 @@ def _(): with plugins2config_file.open("w", encoding="utf8") as wf: _yaml.dump(_data, wf) _generate_simple_config(exists_module) + Config.reload() diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index dbeddb54..5bf50409 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -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 _(): """ 初始化插件数据配置 diff --git a/zhenxun/builtin_plugins/init/init_task.py b/zhenxun/builtin_plugins/init/init_task.py index cead7d72..b9bab56d 100644 --- a/zhenxun/builtin_plugins/init/init_task.py +++ b/zhenxun/builtin_plugins/init/init_task.py @@ -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 _(): """ 初始化插件数据配置 diff --git a/zhenxun/builtin_plugins/plugin_store/data_source.py b/zhenxun/builtin_plugins/plugin_store/data_source.py index d21ba9cd..1df053f9 100644 --- a/zhenxun/builtin_plugins/plugin_store/data_source.py +++ b/zhenxun/builtin_plugins/plugin_store/data_source.py @@ -18,6 +18,12 @@ from zhenxun.utils.utils import is_number 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: """被动技能文本风格 @@ -50,8 +56,10 @@ def install_requirement(plugin_path: Path): return try: + command = WIN_COMMAND if BAT_FILE.exists() else DEFAULT_COMMAND + command.append(str(existing_requirements)) result = subprocess.run( - ["poetry", "run", "pip", "install", "-r", str(existing_requirements)], + command, check=True, capture_output=True, text=True, diff --git a/zhenxun/builtin_plugins/scripts.py b/zhenxun/builtin_plugins/scripts.py index 0be7527c..b5fca300 100644 --- a/zhenxun/builtin_plugins/scripts.py +++ b/zhenxun/builtin_plugins/scripts.py @@ -1,12 +1,8 @@ -import nonebot -from nonebot.drivers import Driver - from zhenxun.models.group_console import GroupConsole - -driver: Driver = nonebot.get_driver() +from zhenxun.utils.manager.priority_manager import PriorityLifecycle -@driver.on_startup +@PriorityLifecycle.on_startup(priority=5) async def _(): """开启/禁用插件格式修改""" _, is_create = await GroupConsole.get_or_create(group_id=133133133) diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index 0b48a0e7..0986e476 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -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( diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py index f7a65359..6c8e39bb 100644 --- a/zhenxun/builtin_plugins/sign_in/goods_register.py +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -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( diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 9faf1120..910b90d8 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -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) diff --git a/zhenxun/builtin_plugins/web_ui/__init__.py b/zhenxun/builtin_plugins/web_ui/__init__.py index 90772bc5..619d56bf 100644 --- a/zhenxun/builtin_plugins/web_ui/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/__init__.py @@ -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 @@ -81,6 +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") @@ -89,7 +92,7 @@ WsApiRouter.include_router(status_routes) WsApiRouter.include_router(chat_routes) -@driver.on_startup +@PriorityLifecycle.on_startup(priority=0) async def _(): try: # 存储任务引用的列表,防止任务被垃圾回收 diff --git a/zhenxun/builtin_plugins/web_ui/api/configure/__init__.py b/zhenxun/builtin_plugins/web_ui/api/configure/__init__.py new file mode 100644 index 00000000..0ecde197 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/configure/__init__.py @@ -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 diff --git a/zhenxun/builtin_plugins/web_ui/api/configure/data_source.py b/zhenxun/builtin_plugins/web_ui/api/configure/data_source.py new file mode 100644 index 00000000..ad8c73c9 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/configure/data_source.py @@ -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() diff --git a/zhenxun/builtin_plugins/web_ui/api/configure/model.py b/zhenxun/builtin_plugins/web_ui/api/configure/model.py new file mode 100644 index 00000000..4a6b3486 --- /dev/null +++ b/zhenxun/builtin_plugins/web_ui/api/configure/model.py @@ -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 + """前端密码""" diff --git a/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py index 14f5c928..e54bf9e5 100644 --- a/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/api/menu/data_source.py @@ -5,54 +5,63 @@ from zhenxun.services.log import logger from .model import MenuData, MenuItem +default_menus = [ + MenuItem( + name="仪表盘", + module="dashboard", + router="/dashboard", + icon="dashboard", + default=True, + ), + MenuItem( + name="真寻控制台", + module="command", + router="/command", + icon="command", + ), + MenuItem(name="插件列表", module="plugin", router="/plugin", icon="plugin"), + MenuItem(name="插件商店", module="store", router="/store", icon="store"), + MenuItem(name="好友/群组", module="manage", router="/manage", icon="user"), + MenuItem( + name="数据库管理", + module="database", + router="/database", + icon="database", + ), + MenuItem(name="系统信息", module="system", router="/system", icon="system"), + MenuItem(name="关于我们", module="about", router="/about", icon="about"), +] -class MenuManage: + +class MenuManager: def __init__(self) -> None: 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( + **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 = [ - MenuItem( - name="仪表盘", - module="dashboard", - router="/dashboard", - icon="dashboard", - default=True, - ), - MenuItem( - name="真寻控制台", - module="command", - router="/command", - icon="command", - ), - MenuItem( - name="插件列表", module="plugin", router="/plugin", icon="plugin" - ), - MenuItem( - name="插件商店", module="store", router="/store", icon="store" - ), - MenuItem( - name="好友/群组", module="manage", router="/manage", icon="user" - ), - MenuItem( - name="数据库管理", - module="database", - router="/database", - icon="database", - ), - MenuItem( - name="文件管理", module="system", router="/system", icon="system" - ), - MenuItem( - name="关于我们", module="about", router="/about", icon="about" - ), - ] - self.save() + 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) @@ -64,4 +73,4 @@ class MenuManage: json.dump(temp, f, ensure_ascii=False, indent=4) -menu_manage = MenuManage() +menu_manage = MenuManager() diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py index 6c312db3..87011c93 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/dashboard/data_source.py @@ -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()) diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py index b963e291..91fbc5c0 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/database/__init__.py @@ -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 diff --git a/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py index ffcd05be..949a69de 100644 --- a/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py +++ b/zhenxun/builtin_plugins/web_ui/api/tabs/system/__init__.py @@ -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,22 +25,29 @@ 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() - data_list = [] - for file in os.listdir(base_path): - file_path = base_path / file - is_image = any(file.endswith(f".{t}") for t in IMAGE_TYPE) - data_list.append( - DirFile( - is_file=not file_path.is_dir(), - is_image=is_image, - name=file, - parent=path, - size=None if file_path.is_dir() else file_path.stat().st_size, - mtime=file_path.stat().st_mtime, + 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 + is_image = any(file.endswith(f".{t}") for t in IMAGE_TYPE) + data_list.append( + DirFile( + is_file=not file_path.is_dir(), + is_image=is_image, + name=file, + parent=path, + size=None if file_path.is_dir() else file_path.stat().st_size, + mtime=file_path.stat().st_mtime, + ) ) - ) - return Result.ok(data_list) + return Result.ok(data_list) + except Exception as e: + return Result.fail(f"获取文件列表失败: {e!s}") @router.get( @@ -62,8 +69,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() @@ -80,8 +91,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()) @@ -98,10 +113,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) @@ -118,10 +137,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 @@ -139,7 +162,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: @@ -157,7 +186,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: @@ -175,7 +210,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: @@ -193,9 +232,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: @@ -210,7 +253,11 @@ 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: diff --git a/zhenxun/builtin_plugins/web_ui/config.py b/zhenxun/builtin_plugins/web_ui/config.py index bddcb062..4a88aad9 100644 --- a/zhenxun/builtin_plugins/web_ui/config.py +++ b/zhenxun/builtin_plugins/web_ui/config.py @@ -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 diff --git a/zhenxun/builtin_plugins/web_ui/public/data_source.py b/zhenxun/builtin_plugins/web_ui/public/data_source.py index 9f5a657e..51b29533 100644 --- a/zhenxun/builtin_plugins/web_ui/public/data_source.py +++ b/zhenxun/builtin_plugins/web_ui/public/data_source.py @@ -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 ): diff --git a/zhenxun/builtin_plugins/web_ui/utils.py b/zhenxun/builtin_plugins/web_ui/utils.py index a7e22a07..84459114 100644 --- a/zhenxun/builtin_plugins/web_ui/utils.py +++ b/zhenxun/builtin_plugins/web_ui/utils.py @@ -2,6 +2,7 @@ import contextlib from datetime import datetime, timedelta, timezone import os from pathlib import Path +import re from fastapi import Depends, HTTPException from fastapi.security import OAuth2PasswordBearer @@ -28,6 +29,45 @@ if token_file.exists(): token_data = json.load(open(token_file, encoding="utf8")) +def validate_path(path_str: str | None) -> tuple[Path | None, str | None]: + """验证路径是否安全 + + 参数: + path_str: 用户输入的路径 + + 返回: + tuple[Path | None, str | None]: (验证后的路径, 错误信息) + """ + try: + if not path_str: + return Path().resolve(), None + + # 1. 移除任何可能的路径遍历尝试 + path_str = re.sub(r"[\\/]\.\.[\\/]", "", path_str) + + # 2. 规范化路径并转换为绝对路径 + path = Path(path_str).resolve() + + # 3. 获取项目根目录 + root_dir = Path().resolve() + + # 4. 验证路径是否在项目根目录内 + try: + if not path.is_relative_to(root_dir): + return None, "访问路径超出允许范围" + except ValueError: + return None, "无效的路径格式" + + # 5. 验证路径是否包含任何危险字符 + if any(c in str(path) for c in ["..", "~", "*", "?", ">", "<", "|", '"']): + return None, "路径包含非法字符" + + # 6. 验证路径长度是否合理 + return (None, "路径长度超出限制") if len(str(path)) > 4096 else (path, None) + except Exception as e: + return None, f"路径验证失败: {e!s}" + + GROUP_HELP_PATH = DATA_PATH / "group_help" SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png" SIMPLE_DETAIL_HELP_IMAGE = IMAGE_PATH / "SIMPLE_DETAIL_HELP.png" diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 9a44fa74..33678965 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -1,9 +1,12 @@ +import nonebot from nonebot.utils import is_coroutine_callable from tortoise import Tortoise from tortoise.connection import connections from tortoise.models import Model as Model_ from zhenxun.configs.config import BotConfig +from zhenxun.utils.exception import HookPriorityException +from zhenxun.utils.manager.priority_manager import PriorityLifecycle from .log import logger @@ -11,6 +14,9 @@ SCRIPT_METHOD = [] MODELS: list[str] = [] +driver = nonebot.get_driver() + + class Model(Model_): """ 自动添加模块 @@ -26,7 +32,7 @@ class Model(Model_): SCRIPT_METHOD.append((cls.__module__, func)) -class DbUrlIsNode(Exception): +class DbUrlIsNode(HookPriorityException): """ 数据库链接地址为空 """ @@ -42,9 +48,19 @@ class DbConnectError(Exception): pass +@PriorityLifecycle.on_startup(priority=1) async def init(): 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: await Tortoise.init( db_url=BotConfig.db_url, diff --git a/zhenxun/services/plugin_init.py b/zhenxun/services/plugin_init.py index 159e042c..a622a9e8 100644 --- a/zhenxun/services/plugin_init.py +++ b/zhenxun/services/plugin_init.py @@ -6,6 +6,7 @@ from nonebot.utils import is_coroutine_callable from pydantic import BaseModel from zhenxun.services.log import logger +from zhenxun.utils.manager.priority_manager import PriorityLifecycle driver = nonebot.get_driver() @@ -100,6 +101,6 @@ class PluginInitManager: logger.error(f"执行: {module_path}:remove 失败", e=e) -@driver.on_startup +@PriorityLifecycle.on_startup(priority=5) async def _(): await PluginInitManager.install_all() diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py index de73e69d..a3de3087 100644 --- a/zhenxun/utils/_build_mat.py +++ b/zhenxun/utils/_build_mat.py @@ -1,12 +1,17 @@ from io import BytesIO from pathlib import Path import random +import sys from pydantic import BaseModel, Field -from strenum import StrEnum from ._build_image import BuildImage +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from strenum import StrEnum + class MatType(StrEnum): LINE = "LINE" diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 2ddf5297..ba012617 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -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): diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py index db8c0656..8ec925ec 100644 --- a/zhenxun/utils/exception.py +++ b/zhenxun/utils/exception.py @@ -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): """ 未发现 diff --git a/zhenxun/utils/github_utils/models.py b/zhenxun/utils/github_utils/models.py index e3e5dfe3..fb690616 100644 --- a/zhenxun/utils/github_utils/models.py +++ b/zhenxun/utils/github_utils/models.py @@ -1,13 +1,18 @@ import contextlib +import sys from typing import Protocol from aiocache import cached from nonebot.compat import model_dump from pydantic import BaseModel, Field -from strenum import StrEnum 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 ( CACHED_API_TTL, GIT_API_COMMIT_FORMAT, diff --git a/zhenxun/utils/manager/message_manager.py b/zhenxun/utils/manager/message_manager.py index e714c8d8..ee34369d 100644 --- a/zhenxun/utils/manager/message_manager.py +++ b/zhenxun/utils/manager/message_manager.py @@ -22,6 +22,4 @@ class MessageManager: @classmethod def get(cls, uid: str) -> list[str]: - if uid in cls.data: - return cls.data[uid] - return [] + return cls.data[uid] if uid in cls.data else [] diff --git a/zhenxun/utils/manager/priority_manager.py b/zhenxun/utils/manager/priority_manager.py new file mode 100644 index 00000000..1c59635c --- /dev/null +++ b/zhenxun/utils/manager/priority_manager.py @@ -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}")