mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-02 18:20:01 +08:00
添加uv支持 (#2119)
* bugfix:修复内存泄露和信号量饥饿问题 * 修复图片渲染按高度截断问题 * 优化图片渲染速度 * 权限检查去掉无效引用代码 * 添加uv支持 * 🚨 auto fix by pre-commit hooks * bugfix:修改gitignore换行 * bugfix:修复测试没有新生成uv.lock * 修复导入错误 * bugfix:移除重复调用 * 🚨 auto fix by pre-commit hooks * 清理残余poetry引用 * 更新uv安装方式 * 修复阿里云获取问题 * 增加资源下载提示 * 🚨 auto fix by pre-commit hooks * 修改资源下载为流式 * 🚨 auto fix by pre-commit hooks * 提高启动速度 * 移除bot.py支持 * 🚨 auto fix by pre-commit hooks * 优化win脚本逻辑 * 🚨 auto fix by pre-commit hooks * 清理残余无效逻辑 * 代码改进 * 🚨 auto fix by pre-commit hooks * 增加数据库迁移存在性检查 * 🚨 auto fix by pre-commit hooks * chore(test): 添加pytest超时控制和优雅关闭机制 - 在GitHub Actions工作流中添加作业级和步骤级超时限制,防止测试无限期挂起 - 添加pytest-timeout依赖并配置全局超时为120秒 - 在send_queue服务添加关闭钩子,确保worker任务正确取消 - 在priority_manager添加on_shutdown钩子,支持优先级生命周期的关闭阶段 * chore(lint): 禁用超长行的lint警告 * Modify restart logic for Windows platform * 🚨 auto fix by pre-commit hooks * bugfix:修复sys导入问题 * 清理无效结构 * bugfix:修复路径问题 * bugfix:修复shell语法传递给git导致资源获取失败问题 * 优化关闭显示 * bugfix:修复路径问题 * bugfix:增加路径安全 * bugfix:修复orm绕过问题 * 放宽numpy版本限制 * 修改重启方案 * bugfix:修复循环导入 * 优化逻辑 * Enhance disconnect function with error handling Added error handling for disconnect function and imported ConfigurationError. * Implement emergency restart mechanism Added emergency restart mechanism using atexit to ensure process restart even on severe exceptions during shutdown. * 🚨 auto fix by pre-commit hooks * 重启行为归一化 * 修复测试检测问题 * bugfix:修复测试侧类型报错问题 * 引入launcher机制 * 移除重启测试 * 收紧缓存调用路径 * 类型注解收敛 * 优化浏览器回收行为 * 优化浏览器渲染 * bugfix:解决重复关闭浏览器问题 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: ManyManyTomato <93612024+ATTomatoo@users.noreply.github.com> Co-authored-by: AkashiCoin <l1040186796@gmail.com>
This commit is contained in:
co-authored by
pre-commit-ci[bot]
ManyManyTomato
AkashiCoin
parent
74bf912d04
commit
8b16126e40
@@ -1,16 +1,12 @@
|
||||
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 zhenxun.utils._restart_utils import issue_restart_ticket, request_restart
|
||||
|
||||
from ...base_model import Result
|
||||
from .data_source import test_db_connection
|
||||
@@ -22,10 +18,6 @@ driver = nonebot.get_driver()
|
||||
|
||||
port = driver.config.port
|
||||
|
||||
BAT_FILE = Path() / "win启动.bat"
|
||||
|
||||
FILE_NAME = ".configure_restart"
|
||||
|
||||
|
||||
@router.post(
|
||||
"/set_configure",
|
||||
@@ -80,13 +72,8 @@ async def _(setting: Setting) -> Result:
|
||||
Config.set_config("web-ui", "username", setting.username)
|
||||
Config.set_config("web-ui", "password", setting.password, True)
|
||||
to_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="设置成功,请重启真寻以完成配置!")
|
||||
issue_restart_ticket("webui.configure", ttl_seconds=10 * 60)
|
||||
return Result.ok(True, info="设置成功,请重启真寻以完成配置!")
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -102,13 +89,6 @@ async def _(db_url: str) -> 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,
|
||||
@@ -116,19 +96,10 @@ async def run_restart_command(bat_path: Path, port: int):
|
||||
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,
|
||||
ok, message = await request_restart(
|
||||
"webui.configure",
|
||||
require_ticket="webui.configure",
|
||||
)
|
||||
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
|
||||
if not ok:
|
||||
return Result.fail(message)
|
||||
return Result.ok(info=message)
|
||||
|
||||
@@ -350,7 +350,11 @@ class ApiDataSource:
|
||||
)
|
||||
hot_plugin_list = []
|
||||
module_list = [x[0] for x in data_list]
|
||||
plugins = await PluginInfo.filter(module__in=module_list).all()
|
||||
plugins = await PluginInfo.get_plugins(
|
||||
load_status=None,
|
||||
filter_parent=False,
|
||||
module__in=module_list,
|
||||
)
|
||||
module2name = {p.module: p.name for p in plugins}
|
||||
for data in data_list:
|
||||
module = data[0]
|
||||
@@ -376,10 +380,16 @@ class ApiDataSource:
|
||||
return None
|
||||
block_tasks = []
|
||||
block_plugins = []
|
||||
all_plugins = await PluginInfo.filter(
|
||||
load_status=True, plugin_type=PluginType.NORMAL
|
||||
).values("module", "name")
|
||||
all_task = await TaskInfo.annotate().values("module", "name")
|
||||
plugin_records = await PluginInfo.get_plugins(
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
plugin_type=PluginType.NORMAL,
|
||||
)
|
||||
all_plugins = [
|
||||
{"module": plugin.module, "name": plugin.name} for plugin in plugin_records
|
||||
]
|
||||
task_records = await TaskInfo.get_tasks(load_status=None)
|
||||
all_task = [{"module": task.module, "name": task.name} for task in task_records]
|
||||
if bot_data.block_tasks:
|
||||
tasks = CommonUtils.convert_module_format(bot_data.block_tasks)
|
||||
block_tasks = [t["module"] for t in all_task if t["module"] in tasks]
|
||||
|
||||
@@ -36,7 +36,7 @@ class ApiDataSource:
|
||||
db_group = await GroupConsole.get_group_db(group.group_id) or GroupConsole(
|
||||
group_id=group.group_id
|
||||
)
|
||||
task_list = await TaskInfo.all().values_list("module", flat=True)
|
||||
task_list = await TaskInfo.get_modules(load_status=None)
|
||||
db_group.level = group.level
|
||||
db_group.status = group.status
|
||||
if group.close_plugins:
|
||||
@@ -120,7 +120,11 @@ class ApiDataSource:
|
||||
)
|
||||
like_plugin = {}
|
||||
module_list = [x[0] for x in like_plugin_list]
|
||||
plugins = await PluginInfo.filter(module__in=module_list).all()
|
||||
plugins = await PluginInfo.get_plugins(
|
||||
load_status=None,
|
||||
filter_parent=False,
|
||||
module__in=module_list,
|
||||
)
|
||||
module2name = {p.module: p.name for p in plugins}
|
||||
for data in like_plugin_list:
|
||||
name = module2name.get(data[0]) or data[0]
|
||||
@@ -213,26 +217,26 @@ class ApiDataSource:
|
||||
返回:
|
||||
list[Task]: 群组被动列表
|
||||
"""
|
||||
all_task = await TaskInfo.annotate().values_list("module", "name")
|
||||
task_module2name = {x[0]: x[1] for x in all_task}
|
||||
all_task = await TaskInfo.get_tasks(load_status=None)
|
||||
task_module2name = {task.module: task.name for task in all_task}
|
||||
task_list = []
|
||||
if group.block_task or group.superuser_block_plugin:
|
||||
sbp = CommonUtils.convert_module_format(group.superuser_block_task)
|
||||
tasks = CommonUtils.convert_module_format(group.block_task)
|
||||
task_list.extend(
|
||||
Task(
|
||||
name=task[0],
|
||||
zh_name=task_module2name.get(task[0]) or task[0],
|
||||
status=task[0] not in tasks and task[0] not in sbp,
|
||||
is_super_block=task[0] in sbp,
|
||||
name=task.module,
|
||||
zh_name=task_module2name.get(task.module) or task.module,
|
||||
status=task.module not in tasks and task.module not in sbp,
|
||||
is_super_block=task.module in sbp,
|
||||
)
|
||||
for task in all_task
|
||||
)
|
||||
else:
|
||||
task_list.extend(
|
||||
Task(
|
||||
name=task[0],
|
||||
zh_name=task_module2name.get(task[0]) or task[0],
|
||||
name=task.module,
|
||||
zh_name=task_module2name.get(task.module) or task.module,
|
||||
status=True,
|
||||
is_super_block=False,
|
||||
)
|
||||
|
||||
@@ -52,20 +52,34 @@ async def _(
|
||||
async def _() -> Result[PluginCount]:
|
||||
try:
|
||||
plugin_count = PluginCount()
|
||||
plugin_count.normal = await DbPluginInfo.filter(
|
||||
plugin_type=PluginType.NORMAL, load_status=True
|
||||
).count()
|
||||
plugin_count.admin = await DbPluginInfo.filter(
|
||||
plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
|
||||
load_status=True,
|
||||
).count()
|
||||
plugin_count.superuser = await DbPluginInfo.filter(
|
||||
plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN],
|
||||
load_status=True,
|
||||
).count()
|
||||
plugin_count.other = await DbPluginInfo.filter(
|
||||
plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT], load_status=True
|
||||
).count()
|
||||
plugin_count.normal = len(
|
||||
await DbPluginInfo.get_plugins(
|
||||
plugin_type=PluginType.NORMAL,
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
)
|
||||
)
|
||||
plugin_count.admin = len(
|
||||
await DbPluginInfo.get_plugins(
|
||||
plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN],
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
)
|
||||
)
|
||||
plugin_count.superuser = len(
|
||||
await DbPluginInfo.get_plugins(
|
||||
plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN],
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
)
|
||||
)
|
||||
plugin_count.other = len(
|
||||
await DbPluginInfo.get_plugins(
|
||||
plugin_type__in=[PluginType.HIDDEN, PluginType.DEPENDANT],
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
)
|
||||
)
|
||||
return Result.ok(plugin_count, "拿到信息啦!")
|
||||
except Exception as e:
|
||||
logger.error(f"{router.prefix}/get_plugin_count 调用错误", "WebUi", e=e)
|
||||
@@ -125,10 +139,10 @@ async def _(param: PluginSwitch) -> Result:
|
||||
async def _() -> Result[list[str]]:
|
||||
try:
|
||||
menu_type_list = []
|
||||
result = (
|
||||
await DbPluginInfo.filter(load_status=True)
|
||||
.annotate()
|
||||
.values_list("menu_type", flat=True)
|
||||
result = await DbPluginInfo.get_plugins_values_list(
|
||||
"menu_type",
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
)
|
||||
for r in result:
|
||||
if r not in menu_type_list and r:
|
||||
|
||||
@@ -34,12 +34,16 @@ class ApiDataSource:
|
||||
list[PluginInfo]: 插件数据列表
|
||||
"""
|
||||
plugin_list: list[PluginInfo] = []
|
||||
query = DbPluginInfo
|
||||
filters = {}
|
||||
if plugin_type:
|
||||
query = query.filter(plugin_type__in=plugin_type, load_status=True)
|
||||
filters["plugin_type__in"] = plugin_type
|
||||
if menu_type:
|
||||
query = query.filter(menu_type=menu_type, load_status=True)
|
||||
plugins = await query.all()
|
||||
filters["menu_type"] = menu_type
|
||||
plugins = await DbPluginInfo.get_plugins(
|
||||
load_status=True,
|
||||
filter_parent=False,
|
||||
**filters,
|
||||
)
|
||||
for plugin in plugins:
|
||||
plugin_info = PluginInfo(
|
||||
id=plugin.id,
|
||||
|
||||
@@ -30,9 +30,7 @@ async def _() -> Result[dict]:
|
||||
{**model_dump(plugin), "name": plugin.name, "id": idx}
|
||||
for idx, plugin in enumerate(plugin_list + extra_plugin_list)
|
||||
]
|
||||
modules = await PluginInfo.filter(load_status=True).values_list(
|
||||
"module", flat=True
|
||||
)
|
||||
modules = await PluginInfo.get_plugins_values_list("module", load_status=True)
|
||||
return Result.ok({"install_module": modules, "plugin_list": plugin_list})
|
||||
except Exception as e:
|
||||
logger.error("获取插件商店插件信息失败", "WebUi", e=e)
|
||||
@@ -51,7 +49,7 @@ async def _(param: PluginIr) -> Result:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
result = await StoreManager.add_plugin(param.id) # type: ignore
|
||||
result = await StoreManager.add_plugin(str(param.id)) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"安装插件失败: {type(e)}: {e}")
|
||||
@@ -69,7 +67,7 @@ async def _(param: PluginIr) -> Result:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
result = await StoreManager.update_plugin(param.id) # type: ignore
|
||||
result = await StoreManager.update_plugin(str(param.id)) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"更新插件失败: {type(e)}: {e}")
|
||||
@@ -87,11 +85,7 @@ async def _(param: PluginIr) -> Result:
|
||||
require("plugin_store")
|
||||
from zhenxun.builtin_plugins.plugin_store import StoreManager
|
||||
|
||||
plugin_info = await PluginInfo.get_plugin(id=param.id)
|
||||
if not plugin_info:
|
||||
return Result.fail("插件不存在")
|
||||
|
||||
result = await StoreManager.remove_plugin(plugin_info.module) # type: ignore
|
||||
result = await StoreManager.remove_plugin(str(param.id)) # type: ignore
|
||||
return Result.ok(info=result)
|
||||
except Exception as e:
|
||||
return Result.fail(f"移除插件失败: {type(e)}: {e}")
|
||||
|
||||
@@ -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, validate_path
|
||||
from ....utils import authentication, get_system_disk, validate_filename, validate_path
|
||||
from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile
|
||||
|
||||
router = APIRouter(prefix="/system")
|
||||
@@ -120,11 +120,22 @@ async def _(param: RenameFile) -> Result:
|
||||
if not parent_path:
|
||||
return Result.fail("无效的路径")
|
||||
|
||||
path = (parent_path / param.old_name) if param.parent else Path(param.old_name)
|
||||
if err := validate_filename(param.old_name):
|
||||
return Result.fail(err)
|
||||
if err := validate_filename(param.name):
|
||||
return Result.fail(err)
|
||||
|
||||
root = os.path.realpath(Path())
|
||||
path = Path(os.path.realpath(parent_path / param.old_name))
|
||||
if not str(path).startswith(root + os.sep):
|
||||
return Result.fail("访问路径超出允许范围")
|
||||
if not path.exists():
|
||||
return Result.warning_("文件不存在...")
|
||||
try:
|
||||
path.rename(path.parent / param.name)
|
||||
dest = Path(os.path.realpath(path.parent / param.name))
|
||||
if not str(dest).startswith(root + os.sep):
|
||||
return Result.fail("目标路径超出允许范围")
|
||||
path.rename(dest)
|
||||
return Result.ok("重命名成功!")
|
||||
except Exception as e:
|
||||
return Result.warning_(f"重命名失败: {e!s}")
|
||||
@@ -144,12 +155,22 @@ async def _(param: RenameFile) -> Result:
|
||||
if not parent_path:
|
||||
return Result.fail("无效的路径")
|
||||
|
||||
path = (parent_path / param.old_name) if param.parent else Path(param.old_name)
|
||||
if err := validate_filename(param.old_name):
|
||||
return Result.fail(err)
|
||||
if err := validate_filename(param.name):
|
||||
return Result.fail(err)
|
||||
|
||||
root = os.path.realpath(Path())
|
||||
path = Path(os.path.realpath(parent_path / param.old_name))
|
||||
if not str(path).startswith(root + os.sep):
|
||||
return Result.fail("访问路径超出允许范围")
|
||||
if not path.exists() or path.is_file():
|
||||
return Result.warning_("文件夹不存在...")
|
||||
try:
|
||||
new_path = path.parent / param.name
|
||||
shutil.move(path.absolute(), new_path.absolute())
|
||||
dest = Path(os.path.realpath(path.parent / param.name))
|
||||
if not str(dest).startswith(root + os.sep):
|
||||
return Result.fail("目标路径超出允许范围")
|
||||
shutil.move(path.absolute(), dest)
|
||||
return Result.ok("重命名成功!")
|
||||
except Exception as e:
|
||||
return Result.warning_(f"重命名失败: {e!s}")
|
||||
@@ -169,11 +190,19 @@ async def _(param: AddFile) -> Result:
|
||||
if not parent_path:
|
||||
return Result.fail("无效的路径")
|
||||
|
||||
if err := validate_filename(param.name):
|
||||
return Result.fail(err)
|
||||
|
||||
path = (parent_path / param.name) if param.parent else Path(param.name)
|
||||
# 二次确认拼接后路径仍在允许范围内
|
||||
resolved, err = validate_path(str(path))
|
||||
if err or not resolved:
|
||||
return Result.fail(err or "无效的路径")
|
||||
path = resolved
|
||||
if path.exists():
|
||||
return Result.warning_("文件已存在...")
|
||||
try:
|
||||
path.open("w")
|
||||
path.touch()
|
||||
return Result.ok("新建文件成功!")
|
||||
except Exception as e:
|
||||
return Result.warning_(f"新建文件失败: {e!s}")
|
||||
@@ -193,7 +222,15 @@ async def _(param: AddFile) -> Result:
|
||||
if not parent_path:
|
||||
return Result.fail("无效的路径")
|
||||
|
||||
if err := validate_filename(param.name):
|
||||
return Result.fail(err)
|
||||
|
||||
path = (parent_path / param.name) if param.parent else Path(param.name)
|
||||
# 二次确认拼接后路径仍在允许范围内
|
||||
resolved, err = validate_path(str(path))
|
||||
if err or not resolved:
|
||||
return Result.fail(err or "无效的路径")
|
||||
path = resolved
|
||||
if path.exists():
|
||||
return Result.warning_("文件夹已存在...")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user