✨ 首次启动时提供使用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>
114
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>
|
||||
@ -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/)
|
||||
|
||||
<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
|
||||
@ -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")
|
||||
|
||||
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 |
@ -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()
|
||||
"""签到与用户的数据迁移"""
|
||||
|
||||
@ -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 _():
|
||||
"""数据迁移
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 _():
|
||||
"""
|
||||
初始化插件数据配置
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
# 存储任务引用的列表,防止任务被垃圾回收
|
||||
|
||||
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,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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
):
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
未发现
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 []
|
||||
|
||||
57
zhenxun/utils/manager/priority_manager.py
Normal 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}")
|
||||