mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
* 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>
217 lines
7.5 KiB
Python
217 lines
7.5 KiB
Python
import asyncio
|
|
from pathlib import Path
|
|
import subprocess
|
|
from subprocess import CalledProcessError
|
|
from typing import ClassVar
|
|
|
|
from zhenxun.configs.config import Config
|
|
from zhenxun.services.log import logger
|
|
|
|
LOG_COMMAND = "VirtualEnvPackageManager"
|
|
|
|
Config.add_plugin_config(
|
|
"virtualenv",
|
|
"python_path",
|
|
None,
|
|
help="虚拟环境python路径,为空时使用系统环境的uv",
|
|
)
|
|
|
|
|
|
class VirtualEnvPackageManager:
|
|
DEFAULT_COMMAND: ClassVar[list[str]] = ["uv", "pip"]
|
|
|
|
@classmethod
|
|
def __get_command(cls) -> list[str]:
|
|
if path := Config.get_config("virtualenv", "python_path"):
|
|
return [path, "-m", "pip"]
|
|
return cls.DEFAULT_COMMAND.copy()
|
|
|
|
@classmethod
|
|
async def install(cls, package: list[str] | str):
|
|
"""安装依赖包
|
|
|
|
参数:
|
|
package: 安装依赖包名称或列表
|
|
"""
|
|
if isinstance(package, str):
|
|
package = [package]
|
|
try:
|
|
command = cls.__get_command()
|
|
command.append("install")
|
|
command.extend(package)
|
|
logger.info(f"执行虚拟环境安装包指令: {command}", LOG_COMMAND)
|
|
result = await asyncio.to_thread(
|
|
subprocess.run,
|
|
command,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.debug(
|
|
f"安装虚拟环境包指令执行完成: {result.stdout}",
|
|
LOG_COMMAND,
|
|
)
|
|
return result.stdout
|
|
except (CalledProcessError, FileNotFoundError) as e:
|
|
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
|
logger.error(f"安装虚拟环境包指令执行失败: {stderr}.", LOG_COMMAND)
|
|
return stderr
|
|
|
|
@classmethod
|
|
async def uninstall(cls, package: list[str] | str):
|
|
"""卸载依赖包
|
|
|
|
参数:
|
|
package: 卸载依赖包名称或列表
|
|
"""
|
|
if isinstance(package, str):
|
|
package = [package]
|
|
try:
|
|
command = cls.__get_command()
|
|
command.append("uninstall")
|
|
command.extend(package)
|
|
logger.info(f"执行虚拟环境卸载包指令: {command}", LOG_COMMAND)
|
|
result = await asyncio.to_thread(
|
|
subprocess.run,
|
|
command,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.debug(
|
|
f"卸载虚拟环境包指令执行完成: {result.stdout}",
|
|
LOG_COMMAND,
|
|
)
|
|
return result.stdout
|
|
except (CalledProcessError, FileNotFoundError) as e:
|
|
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
|
logger.error(f"卸载虚拟环境包指令执行失败: {stderr}.", LOG_COMMAND)
|
|
return stderr
|
|
|
|
@classmethod
|
|
async def update(cls, package: list[str] | str):
|
|
"""更新依赖包
|
|
|
|
参数:
|
|
package: 更新依赖包名称或列表
|
|
"""
|
|
if isinstance(package, str):
|
|
package = [package]
|
|
try:
|
|
command = cls.__get_command()
|
|
command.append("install")
|
|
command.append("--upgrade")
|
|
command.extend(package)
|
|
logger.info(f"执行虚拟环境更新包指令: {command}", LOG_COMMAND)
|
|
result = await asyncio.to_thread(
|
|
subprocess.run,
|
|
command,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.debug(f"更新虚拟环境包指令执行完成: {result.stdout}", LOG_COMMAND)
|
|
return result.stdout
|
|
except (CalledProcessError, FileNotFoundError) as e:
|
|
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
|
logger.error(f"更新虚拟环境包指令执行失败: {stderr}.", LOG_COMMAND)
|
|
return stderr
|
|
|
|
@staticmethod
|
|
def _clean_requirements_file(file_path: Path) -> None:
|
|
"""清理 requirements 文件中的非ASCII注释
|
|
|
|
防止 Windows 上 pip 使用 GBK 编码读取 UTF-8 文件时出错
|
|
|
|
参数:
|
|
file_path: requirements 文件路径
|
|
"""
|
|
try:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
lines = content.splitlines()
|
|
cleaned_lines = []
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
# 跳过空行
|
|
if not stripped:
|
|
continue
|
|
# 如果是注释行且包含非ASCII字符,跳过
|
|
if stripped.startswith("#"):
|
|
try:
|
|
stripped.encode("ascii")
|
|
except UnicodeEncodeError:
|
|
continue
|
|
cleaned_lines.append(line)
|
|
# 写回文件
|
|
file_path.write_text(
|
|
"\n".join(cleaned_lines) + "\n" if cleaned_lines else "",
|
|
encoding="utf-8",
|
|
)
|
|
except Exception:
|
|
# 如果清理失败,忽略错误继续安装
|
|
pass
|
|
|
|
@classmethod
|
|
async def install_requirement(cls, requirement_file: Path):
|
|
"""安装依赖文件
|
|
|
|
参数:
|
|
requirement_file: requirement文件路径
|
|
|
|
异常:
|
|
FileNotFoundError: 文件不存在
|
|
"""
|
|
if not requirement_file.exists():
|
|
raise FileNotFoundError(f"依赖文件 {requirement_file} 不存在", LOG_COMMAND)
|
|
# 清理 requirements 文件中的非ASCII注释,防止 Windows GBK 编码问题
|
|
cls._clean_requirements_file(requirement_file)
|
|
try:
|
|
command = cls.__get_command()
|
|
command.append("install")
|
|
command.append("-r")
|
|
command.append(str(requirement_file.absolute()))
|
|
logger.info(f"执行虚拟环境安装依赖文件指令: {command}", LOG_COMMAND)
|
|
result = await asyncio.to_thread(
|
|
subprocess.run,
|
|
command,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.debug(
|
|
f"安装虚拟环境依赖文件指令执行完成: {result.stdout}",
|
|
LOG_COMMAND,
|
|
)
|
|
return result.stdout
|
|
except (CalledProcessError, FileNotFoundError) as e:
|
|
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
|
logger.error(
|
|
f"安装虚拟环境依赖文件指令执行失败: {stderr}.",
|
|
LOG_COMMAND,
|
|
)
|
|
return stderr
|
|
|
|
@classmethod
|
|
async def list(cls) -> str:
|
|
"""列出已安装的依赖包"""
|
|
try:
|
|
command = cls.__get_command()
|
|
command.append("list")
|
|
logger.info(f"执行虚拟环境列出包指令: {command}", LOG_COMMAND)
|
|
result = await asyncio.to_thread(
|
|
subprocess.run,
|
|
command,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
logger.debug(
|
|
f"列出虚拟环境包指令执行完成: {result.stdout}",
|
|
LOG_COMMAND,
|
|
)
|
|
return result.stdout
|
|
except (CalledProcessError, FileNotFoundError) as e:
|
|
stderr = e.stderr if isinstance(e, CalledProcessError) else str(e)
|
|
logger.error(f"列出虚拟环境包指令执行失败: {stderr}.", LOG_COMMAND)
|
|
return ""
|