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

* ✨ 添加全局优先级hook

* ✨ 添加基础配置api

* ✨ 添加数据库连接测试

* 💬 提示重启

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

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

* ✨ 添加配置后重启接口

* ✨ 添加重启标志文件

* ✨ 添加重启脚本命令

* ✨ 添加重启系统限制

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

* 🔥 移除bat

* ✨ 添加关于菜单

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

* 🩹 检测数据库路径

* 🩹 修改数据库路径检测

* 🩹 修改数据库路径检测

* 🩹 修复路径注入

* 🎨 显示添加优先级

* 🐛 修改PriorityLifecycle字典类名称

* ⚡ 修复路径问题

* ⚡ 修复路径检测

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

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

* 🚨 auto fix by pre-commit hooks

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

* 📝 修改README中webui示例图

* ✨ 更新PriorityLifecycle.on_startup装饰器

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

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
HibiKier
2025-06-16 09:11:41 +08:00
committed by GitHub
co-authored by pre-commit-ci[bot]
parent bcfb47d9fd
commit 99f1388e23
54 changed files with 581 additions and 140 deletions
@@ -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
@@ -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()
@@ -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
"""前端密码"""