Files
astrbot_plugin_qq_group_dai…/src/utils/pdf_utils.py
T

191 lines
7.0 KiB
Python

"""
PDF工具模块
负责PDF相关的安装和管理功能
"""
import asyncio
import sys
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
from .logger import logger
class PDFInstaller:
"""
工具组件:PDF 渲染引擎 (Playwright) 安装器
该组件负责管理 Playwright 及其对应浏览器内核 (Chromium) 的安装生命周期。
由于内核下载耗时较长且受网络波动影响,采用非阻塞的后台任务模式执行。
"""
# 类级别的线程池,专用于隔离耗时的 IO/Shell 操作
_executor = ThreadPoolExecutor(
max_workers=1, thread_name_prefix="playwright_install"
)
# 静态安装状态追踪
_install_status: dict[str, Any] = {
"in_progress": False,
"completed": False,
"failed": False,
"error_message": None,
}
@staticmethod
async def install_playwright(config_manager: Any) -> str:
"""
异步入口:安装 Playwright 环境。
流程:
1. 调用 pip 安装 `playwright` Python 包。
2. 验证自定义浏览器路径配置。
3. 若无自定义路径,则触发浏览器内核安装。
Args:
config_manager (Any): 配置管理实例,用于读取/设置安装状态。
Returns:
str: 安装阶段提示信息
"""
try:
logger.info("正在初始化 Playwright 安装流程...")
# 1. 下载并安装库文件
logger.info("第一步:正在运行 pip install playwright...")
process = await asyncio.create_subprocess_exec(
sys.executable,
"-m",
"pip",
"install",
"playwright>=1.40.0",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
logger.error(f"Playwright 库安装失败: {error_msg}")
return f"❌ pip install playwright 失败: {error_msg}"
logger.info("第一步完成。正在检查浏览器内核...")
custom_path = config_manager.get_browser_path()
if custom_path and Path(custom_path).exists():
logger.info(f"检测到自定义浏览器路径: {custom_path}。跳过内核下载。")
return f"✅ Playwright 库已就绪。已检测到自定义浏览器 `{custom_path}`,无需额外安装内核。您可以直接开始生成 PDF。"
# 3. 部署浏览器内核
return await PDFInstaller.install_system_deps()
except Exception as e:
logger.error(f"Playwright 设置过程中出错: {e}")
return f"❌ 安装过程中出错: {str(e)}"
@staticmethod
async def install_system_deps() -> str:
"""
触发浏览器内核的后台异步安装流程。
该方法检查防重入状态,并立即返回任务启动信息,不会阻塞主线程。
Returns:
str: 任务排队状态提示
"""
try:
if PDFInstaller._install_status["in_progress"]:
return "⏳ 浏览器内核正在后台部署中,请稍后检查日志或状态。"
PDFInstaller._install_status.update(
{
"in_progress": True,
"completed": False,
"failed": False,
"error_message": None,
}
)
logger.info("正在启动后台线程以部署 Chromium 内核...")
asyncio.create_task(PDFInstaller._background_playwright_install())
return (
"🚀 浏览器内核安装任务已成功在后台启动。\n\n"
"程序正在执行 `playwright install chromium`,由于体积较大,通常需花费 2-5 分钟。\n"
"此过程不会影响机器人正常响应。安装完成后,系统日志将进行通知。"
)
except Exception as e:
PDFInstaller._install_status["in_progress"] = False
logger.error(f"启动安装任务失败: {e}")
return f"❌ 启动安装任务失败: {e}"
@staticmethod
async def _background_playwright_install() -> None:
"""
底层宿主任务:驱动 system shell 执行浏览器二进制文件部署。
"""
try:
logger.info("正在执行二进制文件:playwright install chromium")
# 通过当前 Python 解释器环境调用子模块,确保环境隔离
process = await asyncio.create_subprocess_exec(
sys.executable,
"-m",
"playwright",
"install",
"chromium",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
PDFInstaller._install_status["completed"] = True
logger.info("✅ Chromium 内核安装成功。")
# Linux 特殊处理:提示用户补充系统依赖
if sys.platform.startswith("linux"):
logger.info(
"提示:在 Linux 上,如果 PDF 生成仍然失败,请尝试运行 'sudo playwright install-deps'。"
)
else:
PDFInstaller._install_status["failed"] = True
PDFInstaller._install_status["error_message"] = stderr.decode().strip()
logger.error(f"❌ Chromium 安装二进制文件执行失败: {stderr.decode()}")
except Exception as e:
PDFInstaller._install_status.update(
{"failed": True, "error_message": str(e)}
)
logger.error(f"Playwright 后台任务遇到异常: {e}")
finally:
PDFInstaller._install_status["in_progress"] = False
@staticmethod
def get_pdf_status(config_manager: Any) -> str:
"""
查询当前系统的 PDF 功能可用性状态描述。
Args:
config_manager (Any): 配置管理器,用于读取核心探测开关。
Returns:
str: 用户友好的状态文本
"""
if config_manager.playwright_available:
version = config_manager.playwright_version or "Unknown"
status = f"✅ PDF 功能可用 (核心版本: {version})"
if PDFInstaller._install_status["in_progress"]:
status += "\n⏳ 警告:浏览器内核仍在后台下载/部署中..."
elif PDFInstaller._install_status["failed"]:
status += f"\n⚠️ 上次内核安装异常: {PDFInstaller._install_status.get('error_message')}"
return status
else:
return "❌ PDF 渲染核心未安装 - 请发送管理员指令 `/安装PDF`。"