✨ 首次启动时提供使用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) | 插件 | [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;">
|

|
||||||
<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;">
|

|
||||||
</div>
|
|
||||||
|
#### 仪表盘
|
||||||
|
|
||||||
|

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

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

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

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

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

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

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

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

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

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

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

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

|
||||||
|
|
||||||
<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
@ -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
|
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.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()
|
||||||
"""签到与用户的数据迁移"""
|
"""签到与用户的数据迁移"""
|
||||||
|
|||||||
@ -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 _():
|
||||||
"""数据迁移
|
"""数据迁移
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 _():
|
||||||
"""
|
"""
|
||||||
初始化插件数据配置
|
初始化插件数据配置
|
||||||
|
|||||||
@ -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 _():
|
||||||
"""
|
"""
|
||||||
初始化插件数据配置
|
初始化插件数据配置
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
# 存储任务引用的列表,防止任务被垃圾回收
|
# 存储任务引用的列表,防止任务被垃圾回收
|
||||||
|
|||||||
133
zhenxun/builtin_plugins/web_ui/api/configure/__init__.py
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import nonebot
|
||||||
|
|
||||||
|
from zhenxun.configs.config import BotConfig, Config
|
||||||
|
|
||||||
|
from ...base_model import Result
|
||||||
|
from .data_source import test_db_connection
|
||||||
|
from .model import Setting
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/configure")
|
||||||
|
|
||||||
|
driver = nonebot.get_driver()
|
||||||
|
|
||||||
|
port = driver.config.port
|
||||||
|
|
||||||
|
BAT_FILE = Path() / "win启动.bat"
|
||||||
|
|
||||||
|
FILE_NAME = ".configure_restart"
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/set_configure",
|
||||||
|
response_model=Result,
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="设置基础配置",
|
||||||
|
)
|
||||||
|
async def _(setting: Setting) -> Result:
|
||||||
|
global port
|
||||||
|
password = Config.get_config("web-ui", "password")
|
||||||
|
if password or BotConfig.db_url:
|
||||||
|
return Result.fail("配置已存在,请先删除DB_URL内容和前端密码再进行设置。")
|
||||||
|
env_file = Path() / ".env.dev"
|
||||||
|
if not env_file.exists():
|
||||||
|
return Result.fail("配置文件.env.dev不存在。")
|
||||||
|
env_text = env_file.read_text(encoding="utf-8")
|
||||||
|
if setting.db_url:
|
||||||
|
if setting.db_url.startswith("sqlite"):
|
||||||
|
base_dir = Path().resolve()
|
||||||
|
# 清理和验证数据库路径
|
||||||
|
db_path_str = setting.db_url.split(":")[-1].strip()
|
||||||
|
# 移除任何可能的路径遍历尝试
|
||||||
|
db_path_str = re.sub(r"[\\/]\.\.[\\/]", "", db_path_str)
|
||||||
|
# 规范化路径
|
||||||
|
db_path = Path(db_path_str).resolve()
|
||||||
|
parent_path = db_path.parent
|
||||||
|
|
||||||
|
# 验证路径是否在项目根目录内
|
||||||
|
try:
|
||||||
|
if not parent_path.absolute().is_relative_to(base_dir):
|
||||||
|
return Result.fail("数据库路径不在项目根目录内。")
|
||||||
|
except ValueError:
|
||||||
|
return Result.fail("无效的数据库路径。")
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
try:
|
||||||
|
parent_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
return Result.fail(f"创建数据库目录失败: {e!s}")
|
||||||
|
|
||||||
|
env_text = env_text.replace('DB_URL = ""', f'DB_URL = "{setting.db_url}"')
|
||||||
|
if setting.superusers:
|
||||||
|
superusers = ", ".join([f'"{s}"' for s in setting.superusers])
|
||||||
|
env_text = re.sub(r"SUPERUSERS=\[.*?\]", f"SUPERUSERS=[{superusers}]", env_text)
|
||||||
|
if setting.host:
|
||||||
|
env_text = env_text.replace("HOST = 127.0.0.1", f"HOST = {setting.host}")
|
||||||
|
if setting.port:
|
||||||
|
env_text = env_text.replace("PORT = 8080", f"PORT = {setting.port}")
|
||||||
|
port = setting.port
|
||||||
|
if setting.username:
|
||||||
|
Config.set_config("web-ui", "username", setting.username)
|
||||||
|
Config.set_config("web-ui", "password", setting.password, True)
|
||||||
|
env_file.write_text(env_text, encoding="utf-8")
|
||||||
|
if BAT_FILE.exists():
|
||||||
|
for file in os.listdir(Path()):
|
||||||
|
if file.startswith(FILE_NAME):
|
||||||
|
Path(file).unlink()
|
||||||
|
flag_file = Path() / f"{FILE_NAME}_{int(time.time())}"
|
||||||
|
flag_file.touch()
|
||||||
|
return Result.ok(BAT_FILE.exists(), info="设置成功,请重启真寻以完成配置!")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/test_db",
|
||||||
|
response_model=Result,
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="设置基础配置",
|
||||||
|
)
|
||||||
|
async def _(db_url: str) -> Result:
|
||||||
|
result = await test_db_connection(db_url)
|
||||||
|
if isinstance(result, str):
|
||||||
|
return Result.fail(result)
|
||||||
|
return Result.ok(info="数据库连接成功!")
|
||||||
|
|
||||||
|
|
||||||
|
async def run_restart_command(bat_path: Path, port: int):
|
||||||
|
"""在后台执行重启命令"""
|
||||||
|
await asyncio.sleep(1) # 确保 FastAPI 已返回响应
|
||||||
|
subprocess.Popen([bat_path, str(port)], shell=True) # noqa: ASYNC220
|
||||||
|
sys.exit(0) # 退出当前进程
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/restart",
|
||||||
|
response_model=Result,
|
||||||
|
response_class=JSONResponse,
|
||||||
|
description="重启",
|
||||||
|
)
|
||||||
|
async def _() -> Result:
|
||||||
|
if not BAT_FILE.exists():
|
||||||
|
return Result.fail("自动重启仅支持意见整合包,请尝试手动重启")
|
||||||
|
flag_file = next(
|
||||||
|
(Path() / file for file in os.listdir(Path()) if file.startswith(FILE_NAME)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not flag_file or not flag_file.exists():
|
||||||
|
return Result.fail("重启标志文件不存在...")
|
||||||
|
set_time = flag_file.name.split("_")[-1]
|
||||||
|
if time.time() - float(set_time) > 10 * 60:
|
||||||
|
return Result.fail("重启标志文件已过期,请重新设置配置。")
|
||||||
|
flag_file.unlink()
|
||||||
|
try:
|
||||||
|
return Result.ok(info="执行重启命令成功")
|
||||||
|
finally:
|
||||||
|
asyncio.create_task(run_restart_command(BAT_FILE, port)) # noqa: RUF006
|
||||||
18
zhenxun/builtin_plugins/web_ui/api/configure/data_source.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from tortoise import Tortoise
|
||||||
|
|
||||||
|
|
||||||
|
async def test_db_connection(db_url: str) -> bool | str:
|
||||||
|
try:
|
||||||
|
# 初始化 Tortoise ORM
|
||||||
|
await Tortoise.init(
|
||||||
|
db_url=db_url,
|
||||||
|
modules={"models": ["__main__"]}, # 这里不需要实际模型
|
||||||
|
)
|
||||||
|
# 测试连接
|
||||||
|
await Tortoise.get_connection("default").execute_query("SELECT 1")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
finally:
|
||||||
|
# 关闭连接
|
||||||
|
await Tortoise.close_connections()
|
||||||
16
zhenxun/builtin_plugins/web_ui/api/configure/model.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Setting(BaseModel):
|
||||||
|
superusers: list[str]
|
||||||
|
"""超级用户列表"""
|
||||||
|
db_url: str
|
||||||
|
"""数据库地址"""
|
||||||
|
host: str
|
||||||
|
"""主机地址"""
|
||||||
|
port: int
|
||||||
|
"""端口"""
|
||||||
|
username: str
|
||||||
|
"""前端用户名"""
|
||||||
|
password: str
|
||||||
|
"""前端密码"""
|
||||||
@ -5,18 +5,7 @@ from zhenxun.services.log import logger
|
|||||||
|
|
||||||
from .model import MenuData, MenuItem
|
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()
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
):
|
):
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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):
|
||||||
"""
|
"""
|
||||||
未发现
|
未发现
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 []
|
|
||||||
|
|||||||
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}")
|
||||||