添加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:
Copaan
2026-04-18 23:42:10 +08:00
committed by GitHub
co-authored by pre-commit-ci[bot] ManyManyTomato AkashiCoin
parent 74bf912d04
commit 8b16126e40
83 changed files with 15394 additions and 7062 deletions
@@ -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: