mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix(pdf): 迁移为 playwright
This commit is contained in:
+25
-89
@@ -2,18 +2,18 @@
|
||||
|
||||
## 概述
|
||||
|
||||
本插件现已支持生成 PDF 格式的群聊分析报告,提供更专业的报告输出格式。
|
||||
本插件现已支持生成 PDF 格式的群聊分析报告,提供更专业的报告输出格式。PDF 引擎已迁移至 Playwright,提供更好的兼容性。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 📄 **PDF 报告生成**: 将群聊分析结果导出为精美的 PDF 文档
|
||||
- 🎨 **专业排版**: 针对 PDF 打印优化的样式设计
|
||||
- 📊 **完整内容**: 包含所有分析数据(统计信息、话题分析、用户称号、群聊金句)
|
||||
- 🔧 **灵活配置**: 可自定义输出目录和文件名格式
|
||||
- 🔧 **灵活配置**: 可自定义输出目录、文件名格式以及**自定义浏览器路径**
|
||||
|
||||
## 安装依赖
|
||||
|
||||
使用 PDF 功能前,需要安装 pyppeteer 库。有以下几种安装方式:
|
||||
使用 PDF 功能前,需要安装 playwright 库。有以下几种安装方式:
|
||||
|
||||
### 方式一:使用命令安装(推荐)
|
||||
|
||||
@@ -23,22 +23,19 @@
|
||||
/安装PDF
|
||||
```
|
||||
|
||||
提示: 首次使用命令安装,最后出现提示告诉你需要重启生效,是对的,需要重启 astrbot,而不是热重载插件。
|
||||
|
||||
此命令会自动:
|
||||
- 检查 pyppeteer 是否已安装
|
||||
- 如未安装,自动安装 pyppeteer 库
|
||||
- 下载 Chromium 浏览器
|
||||
- 检查 playwright 是否已安装
|
||||
- 如未安装,自动安装 playwright 库
|
||||
- 检查是否需要下载 Chromium 浏览器(已配置自定义路径则跳过)
|
||||
- 测试 PDF 功能是否正常
|
||||
|
||||
### 方式二:手动安装
|
||||
|
||||
```bash
|
||||
pip install pyppeteer==1.0.2
|
||||
pip install playwright
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
首次使用时,pyppeteer 还需要下载 Chromium 浏览器,请确保网络连接正常。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 设置输出格式
|
||||
@@ -49,106 +46,45 @@ pip install pyppeteer==1.0.2
|
||||
/设置格式 pdf
|
||||
```
|
||||
|
||||
支持的格式:
|
||||
- `image` - 图片格式(默认)
|
||||
- `text` - 文本格式
|
||||
- `pdf` - PDF 格式
|
||||
|
||||
### 2. 生成 PDF 报告
|
||||
|
||||
设置为 PDF 格式后,使用常规的分析命令:
|
||||
|
||||
```
|
||||
/群分析
|
||||
/群分析 3 # 分析近3天的数据
|
||||
```
|
||||
|
||||
### 3. 查看当前设置
|
||||
### 3. 高级配置:自定义浏览器路径
|
||||
|
||||
如果您希望使用系统中已有的 Chrome 或 Edge,或者在特定的路径下运行浏览器,可以在配置中设置 `browser_path`:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||
}
|
||||
```
|
||||
/设置格式 # 不带参数,查看当前格式设置
|
||||
/分析设置 status # 查看完整的插件状态
|
||||
```
|
||||
|
||||
## PDF 报告内容
|
||||
|
||||
生成的 PDF 报告包含以下部分:
|
||||
|
||||
1. **报告标题和日期**
|
||||
2. **基础统计数据**
|
||||
- 消息总数
|
||||
- 参与人数
|
||||
- 总字符数
|
||||
- 表情数量
|
||||
- 最活跃时段
|
||||
|
||||
3. **热门话题分析**
|
||||
- 话题名称
|
||||
- 主要参与者
|
||||
- 详细描述
|
||||
|
||||
4. **群友称号**
|
||||
- 用户昵称
|
||||
- 获得称号
|
||||
- MBTI 类型
|
||||
- 获得理由
|
||||
|
||||
5. **群聊金句**
|
||||
- 精选的有趣发言
|
||||
- 发言人
|
||||
- 选择理由
|
||||
|
||||
## 故障排除
|
||||
|
||||
pyppeteer 版本要为稳定版的 1.0.2。
|
||||
### 1. Playwright 安装失败
|
||||
|
||||
### 1. pyppeteer 安装失败
|
||||
请确保您的系统网络能够访问官网下载浏览器内核。如果是在受限环境下,建议使用系统的 Chrome/Edge 并配置 `browser_path`。
|
||||
|
||||
```bash
|
||||
# 使用国内镜像安装
|
||||
pip install pyppeteer -i https://pypi.tuna.tsinghua.edu.cn/simple/
|
||||
```
|
||||
### 2. PDF 生成失败
|
||||
|
||||
### 2. Chromium 下载失败
|
||||
|
||||
```bash
|
||||
# 手动下载 Chromium
|
||||
python -m pyppeteer.install
|
||||
```
|
||||
|
||||
### 3. PDF 生成失败
|
||||
|
||||
- 检查系统内存是否充足
|
||||
- 确保有足够的磁盘空间
|
||||
- 检查输出目录权限
|
||||
|
||||
### 4. 中文字体显示问题
|
||||
|
||||
PDF 使用系统默认字体,如果中文显示异常,请确保系统安装了中文字体。
|
||||
|
||||
## 性能说明
|
||||
|
||||
- PDF 生成比图片格式稍慢,通常需要 3-10 秒
|
||||
- 首次使用时需要启动浏览器,可能需要更长时间
|
||||
- 建议在服务器资源充足时使用
|
||||
- 检查 `browser_path` 是否正确且可执行。
|
||||
- 确保系统安装了必要的字体。
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.1.0
|
||||
- ✨ 新增 PDF 报告生成功能
|
||||
- ✨ 新增 `/设置格式` 命令
|
||||
- 🎨 优化 PDF 样式和排版
|
||||
- 📝 完善配置选项和文档
|
||||
### v1.2.0
|
||||
- 🚀 **Playwright 迁移**: 替换了不稳定的 pyppeteer。
|
||||
- 🛠️ **自定义浏览器**: 支持手动指定 Chrome/Edge 路径。
|
||||
- ⚡ **安装优化**: 自动检测系统浏览器并减少不必要的下载。
|
||||
|
||||
## 技术实现
|
||||
|
||||
PDF 生成基于以下技术:
|
||||
- **pyppeteer**: 无头浏览器控制
|
||||
- **Playwright**: 现代无头浏览器控制
|
||||
- **HTML/CSS**: 报告模板和样式
|
||||
- **异步处理**: 非阻塞的 PDF 生成过程
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. PDF 功能需要额外的系统资源,建议在配置较好的服务器上使用
|
||||
2. 生成的 PDF 文件会保存在指定目录中,注意定期清理
|
||||
3. 如果遇到权限问题,请检查输出目录的写入权限
|
||||
|
||||
@@ -185,6 +185,12 @@
|
||||
"default": "data/plugins/astrbot_plugin_qq_group_daily_analysis/reports",
|
||||
"hint": "PDF报告文件的保存目录"
|
||||
},
|
||||
"browser_path": {
|
||||
"type": "string",
|
||||
"description": "自定义浏览器路径",
|
||||
"default": "",
|
||||
"hint": "要填写的话请你清楚自己在干什么。自定义浏览器的可执行文件路径(如 Chrome 或 Edge 的 .exe 文件)。提示:如果是在网页后台设置,则直接输入普通路径即可(如 C:\\Program Files\\...);如果是手动编辑 config.json 文件,请务必使用双反斜杠 '\\\\' 分隔路径。"
|
||||
},
|
||||
"pdf_filename_format": {
|
||||
"type": "string",
|
||||
"description": "PDF文件名格式",
|
||||
|
||||
@@ -275,9 +275,9 @@ class QQGroupDailyAnalysis(Star):
|
||||
f"[AstrBot QQ群日常分析总结插件] ⚠️ 图片报告生成失败,以下是文本版本:\n\n{text_report}"
|
||||
)
|
||||
elif output_format == "pdf":
|
||||
if not self.config_manager.pyppeteer_available:
|
||||
if not self.config_manager.playwright_available:
|
||||
yield event.plain_result(
|
||||
"❌ PDF 功能不可用,请使用 /安装PDF 命令安装 pyppeteer==1.0.2"
|
||||
"❌ PDF 功能不可用,请使用 /安装PDF 命令安装依赖"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -342,8 +342,8 @@ class QQGroupDailyAnalysis(Star):
|
||||
current_format = self.config_manager.get_output_format()
|
||||
pdf_status = (
|
||||
"✅"
|
||||
if self.config_manager.pyppeteer_available
|
||||
else "❌ (需安装 pyppeteer)"
|
||||
if self.config_manager.playwright_available
|
||||
else "❌ (需安装 Playwright)"
|
||||
)
|
||||
yield event.plain_result(f"""📊 当前输出格式: {current_format}
|
||||
|
||||
@@ -360,10 +360,8 @@ class QQGroupDailyAnalysis(Star):
|
||||
yield event.plain_result("❌ 无效的格式类型,支持: image, text, pdf")
|
||||
return
|
||||
|
||||
if format_type == "pdf" and not self.config_manager.pyppeteer_available:
|
||||
yield event.plain_result(
|
||||
"❌ PDF 格式不可用,请使用 /安装PDF 命令安装 pyppeteer==1.0.2"
|
||||
)
|
||||
if format_type == "pdf" and not self.config_manager.playwright_available:
|
||||
yield event.plain_result("❌ PDF 格式不可用,请使用 /安装PDF 命令安装依赖")
|
||||
return
|
||||
|
||||
self.config_manager.set_output_format(format_type)
|
||||
@@ -540,14 +538,10 @@ class QQGroupDailyAnalysis(Star):
|
||||
yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...")
|
||||
|
||||
try:
|
||||
# 安装 pyppeteer
|
||||
result = await PDFInstaller.install_pyppeteer(self.config_manager)
|
||||
# 安装 playwright (内部已包含浏览器内核安装逻辑)
|
||||
result = await PDFInstaller.install_playwright(self.config_manager)
|
||||
yield event.plain_result(result)
|
||||
|
||||
# 提供系统依赖安装指导
|
||||
system_deps_result = await PDFInstaller.install_system_deps()
|
||||
yield event.plain_result(system_deps_result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"安装 PDF 依赖失败: {e}", exc_info=True)
|
||||
yield event.plain_result(f"❌ 安装过程中出现错误: {str(e)}")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pyppeteer==1.0.2
|
||||
playwright>=1.40.0
|
||||
@@ -60,9 +60,12 @@ class MockConfig:
|
||||
return 5
|
||||
|
||||
@property
|
||||
def pyppeteer_available(self):
|
||||
def playwright_available(self):
|
||||
return True
|
||||
|
||||
def get_browser_path(self):
|
||||
return ""
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. Main Execution
|
||||
|
||||
+56
-45
@@ -14,9 +14,9 @@ class ConfigManager:
|
||||
|
||||
def __init__(self, config: AstrBotConfig):
|
||||
self.config = config
|
||||
self._pyppeteer_available = False
|
||||
self._pyppeteer_version = None
|
||||
self._check_pyppeteer_availability()
|
||||
self._playwright_available = False
|
||||
self._playwright_version = None
|
||||
self._check_playwright_availability()
|
||||
|
||||
def get_group_list_mode(self) -> str:
|
||||
"""获取群组列表模式 (whitelist/blacklist/none)"""
|
||||
@@ -335,45 +335,61 @@ class ConfigManager:
|
||||
return self.config.get("enable_user_card", False)
|
||||
|
||||
@property
|
||||
def pyppeteer_available(self) -> bool:
|
||||
"""检查pyppeteer是否可用"""
|
||||
return self._pyppeteer_available
|
||||
def playwright_available(self) -> bool:
|
||||
"""检查playwright是否可用"""
|
||||
return self._playwright_available
|
||||
|
||||
@property
|
||||
def pyppeteer_version(self) -> str | None:
|
||||
"""获取pyppeteer版本"""
|
||||
return self._pyppeteer_version
|
||||
def playwright_version(self) -> str | None:
|
||||
"""获取playwright版本"""
|
||||
return self._playwright_version
|
||||
|
||||
def _check_pyppeteer_availability(self):
|
||||
"""检查 pyppeteer 可用性"""
|
||||
def _check_playwright_availability(self):
|
||||
"""检查 playwright 可用性"""
|
||||
try:
|
||||
import pyppeteer
|
||||
import importlib.util
|
||||
|
||||
self._pyppeteer_available = True
|
||||
if importlib.util.find_spec("playwright") is None:
|
||||
raise ImportError
|
||||
|
||||
# 尝试导入以确保完整性
|
||||
import playwright
|
||||
from playwright.async_api import async_playwright # noqa: F401
|
||||
|
||||
self._playwright_available = True
|
||||
|
||||
# 检查版本
|
||||
try:
|
||||
self._pyppeteer_version = pyppeteer.__version__
|
||||
logger.info(f"使用 pyppeteer {self._pyppeteer_version} 作为 PDF 引擎")
|
||||
self._playwright_version = playwright.__version__
|
||||
logger.info(f"使用 playwright {self._playwright_version} 作为 PDF 引擎")
|
||||
except AttributeError:
|
||||
self._pyppeteer_version = "unknown"
|
||||
logger.info("使用 pyppeteer (版本未知) 作为 PDF 引擎")
|
||||
self._playwright_version = "unknown"
|
||||
logger.info("使用 playwright (版本未知) 作为 PDF 引擎")
|
||||
|
||||
except ImportError:
|
||||
self._pyppeteer_available = False
|
||||
self._pyppeteer_version = None
|
||||
self._playwright_available = False
|
||||
self._playwright_version = None
|
||||
logger.warning(
|
||||
"pyppeteer 未安装,PDF 功能将不可用。请使用 /安装PDF 命令安装 pyppeteer==1.0.2"
|
||||
"playwright 未安装,PDF 功能将不可用。请使用 pip install playwright 安装,并运行 playwright install chromium"
|
||||
)
|
||||
|
||||
def reload_pyppeteer(self) -> bool:
|
||||
"""重新加载 pyppeteer 模块"""
|
||||
try:
|
||||
logger.info("开始重新加载 pyppeteer 模块...")
|
||||
def get_browser_path(self) -> str:
|
||||
"""获取自定义浏览器路径"""
|
||||
return self.config.get("browser_path", "")
|
||||
|
||||
# 移除所有 pyppeteer 相关模块
|
||||
def set_browser_path(self, path: str):
|
||||
"""设置自定义浏览器路径"""
|
||||
self.config["browser_path"] = path
|
||||
self.config.save_config()
|
||||
|
||||
def reload_playwright(self) -> bool:
|
||||
"""重新加载 playwright 模块"""
|
||||
try:
|
||||
logger.info("开始重新加载 playwright 模块...")
|
||||
|
||||
# 移除所有 playwright 相关模块
|
||||
modules_to_remove = [
|
||||
mod for mod in sys.modules.keys() if mod.startswith("pyppeteer")
|
||||
mod for mod in sys.modules.keys() if mod.startswith("playwright")
|
||||
]
|
||||
logger.info(f"移除模块: {modules_to_remove}")
|
||||
for mod in modules_to_remove:
|
||||
@@ -381,40 +397,35 @@ class ConfigManager:
|
||||
|
||||
# 强制重新导入
|
||||
try:
|
||||
import pyppeteer
|
||||
import playwright
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
# 更新全局变量
|
||||
self._pyppeteer_available = True
|
||||
self._playwright_available = True
|
||||
try:
|
||||
self._pyppeteer_version = pyppeteer.__version__
|
||||
self._playwright_version = playwright.__version__
|
||||
logger.info(
|
||||
f"重新加载成功,pyppeteer 版本: {self._pyppeteer_version}"
|
||||
f"重新加载成功,playwright 版本: {self._playwright_version}"
|
||||
)
|
||||
except AttributeError:
|
||||
self._pyppeteer_version = "unknown"
|
||||
logger.info("重新加载成功,pyppeteer 版本未知")
|
||||
self._playwright_version = "unknown"
|
||||
logger.info("重新加载成功,playwright 版本未知")
|
||||
|
||||
return True
|
||||
|
||||
except ImportError:
|
||||
logger.info("pyppeteer 重新导入需要重启 AstrBot 才能生效")
|
||||
logger.info(
|
||||
"💡 提示:pyppeteer 安装成功,但需要重启 AstrBot 后才能使用 PDF 功能"
|
||||
)
|
||||
self._pyppeteer_available = False
|
||||
self._pyppeteer_version = None
|
||||
logger.info("playwright 重新导入可能需要重启 AstrBot")
|
||||
self._playwright_available = False
|
||||
self._playwright_version = None
|
||||
return False
|
||||
except Exception:
|
||||
logger.info("pyppeteer 重新导入需要重启 AstrBot 才能生效")
|
||||
logger.info(
|
||||
"💡 提示:pyppeteer 安装成功,但需要重启 AstrBot 后才能使用 PDF 功能"
|
||||
)
|
||||
self._pyppeteer_available = False
|
||||
self._pyppeteer_version = None
|
||||
logger.info("playwright 重新导入失败")
|
||||
self._playwright_available = False
|
||||
self._playwright_version = None
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"重新加载 pyppeteer 时出错: {e}")
|
||||
logger.error(f"重新加载 playwright 时出错: {e}")
|
||||
return False
|
||||
|
||||
def save_config(self):
|
||||
|
||||
+108
-176
@@ -360,235 +360,167 @@ class ReportGenerator:
|
||||
async def _html_to_pdf(self, html_content: str, output_path: str) -> bool:
|
||||
"""将 HTML 内容转换为 PDF 文件"""
|
||||
try:
|
||||
# 确保 pyppeteer 可用
|
||||
if not self.config_manager.pyppeteer_available:
|
||||
logger.error("pyppeteer 不可用,无法生成 PDF")
|
||||
# 动态导入 playwright
|
||||
try:
|
||||
from playwright.async_api import async_playwright
|
||||
except ImportError:
|
||||
logger.error("playwright 未安装,无法生成 PDF")
|
||||
logger.info("💡 请尝试运行: pip install playwright")
|
||||
return False
|
||||
|
||||
# 动态导入 pyppeteer
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pyppeteer
|
||||
from pyppeteer import launch
|
||||
logger.info("启动浏览器进行 PDF 转换 (使用 Playwright)")
|
||||
|
||||
# 尝试启动浏览器,如果 Chromium 不存在会自动下载
|
||||
logger.info("启动浏览器进行 PDF 转换")
|
||||
async with async_playwright() as p:
|
||||
browser = None
|
||||
|
||||
# 配置浏览器启动参数,解决Docker环境中的沙盒问题
|
||||
launch_options = {
|
||||
"headless": True,
|
||||
"args": [
|
||||
"--no-sandbox", # Docker环境必需 - 禁用沙盒
|
||||
"--disable-setuid-sandbox", # Docker环境必需 - 禁用setuid沙盒
|
||||
"--disable-dev-shm-usage", # 避免共享内存问题
|
||||
"--disable-gpu", # 禁用GPU加速
|
||||
"--no-first-run",
|
||||
"--disable-extensions",
|
||||
"--disable-default-apps",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
"--disable-renderer-backgrounding",
|
||||
"--disable-features=TranslateUI",
|
||||
"--disable-ipc-flooding-protection",
|
||||
"--disable-background-networking",
|
||||
"--enable-features=NetworkService,NetworkServiceInProcess",
|
||||
"--force-color-profile=srgb",
|
||||
"--metrics-recording-only",
|
||||
"--disable-breakpad",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--disable-features=Translate,BackForwardCache,AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync",
|
||||
"--enable-automation",
|
||||
"--password-store=basic",
|
||||
"--use-mock-keychain",
|
||||
# "--export-tagged-pdf", # Removed to reduce size
|
||||
"--disable-web-security",
|
||||
"--disable-features=VizDisplayCompositor",
|
||||
"--disable-blink-features=AutomationControlled", # 隐藏自动化特征
|
||||
],
|
||||
}
|
||||
executable_path = None
|
||||
|
||||
# 检测系统 Chrome/Chromium 路径
|
||||
chrome_paths = []
|
||||
# 0. 优先检查配置的自定义路径
|
||||
custom_browser_path = self.config_manager.get_browser_path()
|
||||
if custom_browser_path:
|
||||
if Path(custom_browser_path).exists():
|
||||
logger.info(
|
||||
f"使用配置的自定义浏览器路径: {custom_browser_path}"
|
||||
)
|
||||
executable_path = custom_browser_path
|
||||
else:
|
||||
logger.warning(
|
||||
f"配置的浏览器路径不存在: {custom_browser_path},尝试自动检测..."
|
||||
)
|
||||
|
||||
# 1. 如果没有自定义路径,尝试自动检测系统浏览器
|
||||
if not executable_path:
|
||||
system_browser_paths = []
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows 系统 Chrome 安装路径
|
||||
username = os.environ.get("USERNAME", "")
|
||||
chrome_paths = [
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||||
rf"C:\Users\{username}\AppData\Local\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files\Chromium\Application\chrome.exe",
|
||||
local_app_data = os.environ.get(
|
||||
"LOCALAPPDATA", rf"C:\Users\{username}\AppData\Local"
|
||||
)
|
||||
program_files = os.environ.get(
|
||||
"ProgramFiles", r"C:\Program Files"
|
||||
)
|
||||
program_files_x86 = os.environ.get(
|
||||
"ProgramFiles(x86)", r"C:\Program Files (x86)"
|
||||
)
|
||||
|
||||
system_browser_paths = [
|
||||
os.path.join(
|
||||
program_files, r"Google\Chrome\Application\chrome.exe"
|
||||
),
|
||||
os.path.join(
|
||||
program_files_x86,
|
||||
r"Google\Chrome\Application\chrome.exe",
|
||||
),
|
||||
os.path.join(
|
||||
local_app_data, r"Google\Chrome\Application\chrome.exe"
|
||||
),
|
||||
os.path.join(
|
||||
program_files_x86,
|
||||
r"Microsoft\Edge\Application\msedge.exe",
|
||||
),
|
||||
os.path.join(
|
||||
program_files, r"Microsoft\Edge\Application\msedge.exe"
|
||||
),
|
||||
]
|
||||
elif sys.platform.startswith("linux"):
|
||||
# Linux 系统 Chrome/Chromium 路径
|
||||
chrome_paths = [
|
||||
system_browser_paths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/snap/bin/chromium",
|
||||
"/usr/bin/chromium-freeworld",
|
||||
]
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# macOS 系统 Chrome 路径
|
||||
chrome_paths = [
|
||||
system_browser_paths = [
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
]
|
||||
|
||||
# 查找可用的浏览器
|
||||
found_browser = False
|
||||
for chrome_path in chrome_paths:
|
||||
if Path(chrome_path).exists():
|
||||
launch_options["executablePath"] = chrome_path
|
||||
logger.info(f"使用系统浏览器: {chrome_path}")
|
||||
found_browser = True
|
||||
# 尝试找到可用的系统浏览器
|
||||
for path in system_browser_paths:
|
||||
if Path(path).exists():
|
||||
executable_path = path
|
||||
logger.info(f"使用系统浏览器: {path}")
|
||||
break
|
||||
|
||||
if not found_browser:
|
||||
logger.info("未找到系统浏览器,将使用 pyppeteer 默认下载的 Chromium")
|
||||
# 先尝试确保 Chromium 已下载
|
||||
try:
|
||||
from pyppeteer import browser, launcher
|
||||
|
||||
launcher_instance = launcher.Launcher(
|
||||
headless=True,
|
||||
args=[
|
||||
# 定义默认启动参数
|
||||
launch_kwargs = {
|
||||
"headless": True,
|
||||
"args": [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--font-render-hinting=none",
|
||||
],
|
||||
)
|
||||
await launcher_instance._get_chromium_revision()
|
||||
await launcher_instance._download_chromium()
|
||||
chromium_path = pyppeteer.executablePath()
|
||||
launch_options["executablePath"] = chromium_path
|
||||
logger.info(f"使用 pyppeteer 下载的 Chromium: {chromium_path}")
|
||||
except Exception as pre_download_err:
|
||||
logger.warning(
|
||||
f"预下载 Chromium 失败,继续尝试直接启动: {pre_download_err}"
|
||||
}
|
||||
|
||||
if executable_path:
|
||||
launch_kwargs["executable_path"] = executable_path
|
||||
launch_kwargs["channel"] = (
|
||||
"chrome" if "chrome" in executable_path.lower() else "msedge"
|
||||
)
|
||||
|
||||
# 尝试启动浏览器
|
||||
try:
|
||||
logger.info("正在启动浏览器...")
|
||||
browser = await launch(**launch_options)
|
||||
logger.info("浏览器启动成功")
|
||||
if executable_path:
|
||||
# 如果指定了路径,通常使用 chromium 启动
|
||||
browser = await p.chromium.launch(**launch_kwargs)
|
||||
else:
|
||||
# 尝试直接启动,依赖 playwright install
|
||||
logger.info("尝试启动 Playwright 托管的浏览器...")
|
||||
browser = await p.chromium.launch(
|
||||
headless=True, args=launch_kwargs["args"]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"浏览器启动失败: {e}", exc_info=True)
|
||||
logger.warning(f"浏览器启动失败: {e}")
|
||||
if "Executable doesn't exist" in str(e) or "executable at" in str(
|
||||
e
|
||||
):
|
||||
logger.error("未找到可用的浏览器。")
|
||||
logger.info(
|
||||
"💡 请确保已安装 Playwright 浏览器: playwright install chromium"
|
||||
)
|
||||
logger.info("💡 或者安装 Google Chrome / Microsoft Edge")
|
||||
return False
|
||||
|
||||
if not browser:
|
||||
return False
|
||||
|
||||
try:
|
||||
# 创建新页面,设置更合理的超时时间
|
||||
page = await browser.newPage()
|
||||
context = await browser.new_context(device_scale_factor=1)
|
||||
page = await context.new_page()
|
||||
|
||||
# 设置页面视口,减少内存占用
|
||||
await page.setViewport(
|
||||
{
|
||||
"width": 800, # Match A4 width approx (794px at 96PPI)
|
||||
"height": 1000,
|
||||
"deviceScaleFactor": 1,
|
||||
"isMobile": False,
|
||||
"hasTouch": False,
|
||||
"isLandscape": False,
|
||||
}
|
||||
# 设置页面内容
|
||||
await page.set_content(
|
||||
html_content, wait_until="networkidle", timeout=60000
|
||||
)
|
||||
|
||||
# 设置页面内容,使用更安全的加载方式
|
||||
logger.info("开始设置页面内容...")
|
||||
await page.setContent(html_content)
|
||||
|
||||
# 等待页面基本加载完成,但不要太长时间
|
||||
try:
|
||||
await page.waitForSelector("body", {"timeout": 5000})
|
||||
logger.info("页面基本加载完成")
|
||||
except Exception:
|
||||
logger.warning("等待页面加载超时,继续执行")
|
||||
|
||||
# 确保CDN字体等资源加载完成
|
||||
logger.info("等待资源加载(10s)...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 导出 PDF,使用更保守的设置
|
||||
# 生成 PDF
|
||||
logger.info("开始生成 PDF...")
|
||||
pdf_options = {
|
||||
"path": output_path,
|
||||
"format": "A4",
|
||||
"printBackground": True,
|
||||
"margin": {
|
||||
await page.pdf(
|
||||
path=output_path,
|
||||
format="A4",
|
||||
print_background=True,
|
||||
margin={
|
||||
"top": "10mm",
|
||||
"right": "10mm",
|
||||
"bottom": "10mm",
|
||||
"left": "10mm",
|
||||
},
|
||||
"scale": 1.0,
|
||||
"displayHeaderFooter": False,
|
||||
"preferCSSPageSize": True,
|
||||
"timeout": 60000, # 增加PDF生成超时时间到60秒
|
||||
}
|
||||
|
||||
await page.pdf(pdf_options)
|
||||
)
|
||||
logger.info(f"PDF 生成成功: {output_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PDF生成过程中出错: {e}")
|
||||
logger.error(f"PDF 生成过程出错: {e}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
# 确保浏览器被正确关闭
|
||||
if browser:
|
||||
try:
|
||||
logger.info("正在关闭浏览器...")
|
||||
# 先关闭所有页面
|
||||
pages = await browser.pages()
|
||||
for page in pages:
|
||||
try:
|
||||
await page.close()
|
||||
except Exception as close_err:
|
||||
logger.debug(f"关闭页面时忽略的异常: {close_err}")
|
||||
|
||||
# 等待一小段时间让资源释放
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 关闭浏览器
|
||||
await browser.close()
|
||||
logger.info("浏览器已关闭")
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭浏览器时出错: {e}")
|
||||
# 强制清理
|
||||
try:
|
||||
await browser.disconnect()
|
||||
except Exception as disc_err:
|
||||
logger.debug(f"断开浏览器连接时忽略的异常: {disc_err}")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "Chromium downloadable not found" in error_msg:
|
||||
logger.error("Chromium 下载失败,建议安装系统 Chrome/Chromium")
|
||||
logger.info(
|
||||
"💡 Linux 系统建议: sudo apt-get install chromium-browser 或 sudo yum install chromium"
|
||||
)
|
||||
elif "No usable sandbox" in error_msg:
|
||||
logger.error("沙盒权限问题,已尝试禁用沙盒")
|
||||
elif "Connection refused" in error_msg or "connect" in error_msg.lower():
|
||||
logger.error("浏览器连接失败,请检查系统资源或尝试重启")
|
||||
elif "executablePath" in error_msg and "not found" in error_msg:
|
||||
logger.error("未找到系统浏览器,请安装 Chrome 或 Chromium")
|
||||
logger.info(
|
||||
"💡 安装建议: sudo apt-get install chromium-browser (Ubuntu/Debian) 或 sudo yum install chromium (CentOS/RHEL)"
|
||||
)
|
||||
elif "Browser closed unexpectedly" in error_msg:
|
||||
logger.error("浏览器意外关闭,可能是由于内存不足或系统资源限制")
|
||||
logger.info("💡 建议: 检查系统内存,或重启 AstrBot 后重试")
|
||||
logger.info("💡 如果问题持续,可以尝试以下解决方案:")
|
||||
logger.info(" 1. 增加系统交换空间")
|
||||
logger.info(" 2. 使用更简单的浏览器启动参数")
|
||||
logger.info(" 3. 考虑使用其他 PDF 生成方案")
|
||||
else:
|
||||
logger.error(f"HTML 转 PDF 失败: {e}")
|
||||
logger.info(
|
||||
"💡 可以尝试使用 /安装PDF 命令重新安装依赖,或检查系统日志获取更多信息"
|
||||
)
|
||||
logger.error(f"Playwright 运行出错: {e}")
|
||||
return False
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
<title>五彩斑斓的一天,来看看群里发生了什么吧! · {{current_date}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<!-- 引入手写字体 (保留艺术字体,移除 Noto Sans SC 以减小体积) -->
|
||||
<!-- 引入手写字体 (保留艺术字体,移除 Noto Sans SC 以减小体积) -->
|
||||
<!-- <link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Long+Cang&display=swap" rel="stylesheet"> -->
|
||||
<!-- 引入手写字体和正文阅读字体 -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Long+Cang&family=Noto+Sans+SC:wght@400;500;700&family=Patrick+Hand&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
@@ -37,10 +38,30 @@
|
||||
--title-border: #ffb74d;
|
||||
--title-text: #bf360c;
|
||||
|
||||
/* 使用系统字体替代在线字体 */
|
||||
--font-title: "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Microsoft YaHei", "SimHei", sans-serif;
|
||||
--font-hand: "WenQuanYi Micro Hei", "KaiTi", "STKaiti", "FangSong", serif;
|
||||
--font-body: "WenQuanYi Micro Hei", sans-serif;
|
||||
/* 优先使用 Google Fonts,如果失败则回退到本地 LXGW WenKai,最后回退到系统自带字体 */
|
||||
--font-title: 'ZCOOL KuaiLe',
|
||||
|
||||
"Microsoft YaHei",
|
||||
"SimHei",
|
||||
"WenQuanYi Micro Hei",
|
||||
sans-serif;
|
||||
|
||||
--font-hand: 'Patrick Hand',
|
||||
'Long Cang',
|
||||
|
||||
|
||||
"KaiTi",
|
||||
"STKaiti",
|
||||
"STKaiti",
|
||||
"WenQuanYi Micro Hei",
|
||||
serif;
|
||||
|
||||
--font-body: 'Noto Sans SC',
|
||||
|
||||
|
||||
"Microsoft YaHei",
|
||||
"WenQuanYi Micro Hei",
|
||||
sans-serif;
|
||||
/* Rely on browser default which should include emoji fallback */
|
||||
}
|
||||
|
||||
|
||||
@@ -613,7 +613,7 @@ class AutoScheduler:
|
||||
)
|
||||
|
||||
elif output_format == "pdf":
|
||||
if not self.config_manager.pyppeteer_available:
|
||||
if not self.config_manager.playwright_available:
|
||||
logger.warning(f"群 {group_id} PDF功能不可用,回退到文本报告")
|
||||
text_report = self.report_generator.generate_text_report(
|
||||
analysis_result
|
||||
|
||||
+88
-250
@@ -15,9 +15,9 @@ class PDFInstaller:
|
||||
|
||||
# 类级别的线程池,用于异步下载任务
|
||||
_executor = ThreadPoolExecutor(
|
||||
max_workers=1, thread_name_prefix="chromium_download"
|
||||
max_workers=1, thread_name_prefix="playwright_install"
|
||||
)
|
||||
_download_status = {
|
||||
_install_status = {
|
||||
"in_progress": False,
|
||||
"completed": False,
|
||||
"failed": False,
|
||||
@@ -25,300 +25,138 @@ class PDFInstaller:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def install_pyppeteer(config_manager):
|
||||
"""安装pyppeteer依赖"""
|
||||
async def install_playwright(config_manager):
|
||||
"""安装 Playwright 依赖"""
|
||||
try:
|
||||
logger.info("开始安装 pyppeteer...")
|
||||
logger.info("开始安装 Playwright...")
|
||||
|
||||
# 使用asyncio安装pyppeteer和兼容的websockets版本
|
||||
logger.info("安装 pyppeteer==1.0.2 和兼容的依赖...")
|
||||
# 1. 安装 pip 包
|
||||
logger.info("正在运行 pip install playwright...")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"pyppeteer==1.0.2",
|
||||
"websockets==10.4",
|
||||
"playwright>=1.40.0",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode == 0:
|
||||
logger.info("pyppeteer 安装成功")
|
||||
logger.info(f"安装输出: {stdout.decode()}")
|
||||
|
||||
# 重新加载pyppeteer模块
|
||||
success = config_manager.reload_pyppeteer()
|
||||
if success:
|
||||
return "✅ pyppeteer 安装成功!PDF 功能现已可用。"
|
||||
else:
|
||||
return "⚠️ pyppeteer 安装完成,但重新加载失败。请重启 AstrBot 以使用 PDF 功能。"
|
||||
else:
|
||||
if process.returncode != 0:
|
||||
error_msg = stderr.decode()
|
||||
logger.error(f"pyppeteer 安装失败: {error_msg}")
|
||||
return f"❌ pyppeteer 安装失败: {error_msg}"
|
||||
logger.error(f"playwright pip 安装失败: {error_msg}")
|
||||
return f"❌ pip install playwright 失败: {error_msg}"
|
||||
|
||||
logger.info("pip 包安装成功,检查是否需要安装浏览器内核...")
|
||||
|
||||
# 2. 检查自定义路径
|
||||
from pathlib import Path
|
||||
|
||||
custom_path = config_manager.get_browser_path()
|
||||
if custom_path and Path(custom_path).exists():
|
||||
logger.info(
|
||||
f"检测到自定义浏览器路径: {custom_path},将跳过 Chromium 内核安装。"
|
||||
)
|
||||
return f"✅ Playwright 包安装成功。检测到自定义浏览器路径 `{custom_path}`,已跳过浏览器内核安装。您可以现在尝试生成 PDF。"
|
||||
|
||||
# 3. 安装浏览器内核
|
||||
return await PDFInstaller.install_system_deps()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"安装 pyppeteer 时出错: {e}")
|
||||
logger.error(f"安装 playwright 时出错: {e}")
|
||||
return f"❌ 安装过程中出错: {str(e)}"
|
||||
|
||||
@staticmethod
|
||||
async def install_system_deps():
|
||||
"""安装系统依赖(Linux下安装库,所有平台下载Chromium)"""
|
||||
"""安装系统依赖 (运行 playwright install chromium)"""
|
||||
try:
|
||||
logger.info("开始安装 PDF 功能系统依赖...")
|
||||
# 检查是否已经在安装中
|
||||
if PDFInstaller._install_status["in_progress"]:
|
||||
return "⏳ 浏览器内核正在后台安装中,请稍候..."
|
||||
|
||||
# 1. 如果是Linux,尝试安装系统库
|
||||
if sys.platform.startswith("linux"):
|
||||
linux_deps_result = await PDFInstaller._install_linux_deps()
|
||||
if linux_deps_result:
|
||||
logger.info(f"Linux 依赖安装结果: {linux_deps_result}")
|
||||
PDFInstaller._install_status["in_progress"] = True
|
||||
PDFInstaller._install_status["completed"] = False
|
||||
PDFInstaller._install_status["failed"] = False
|
||||
PDFInstaller._install_status["error_message"] = None
|
||||
|
||||
# 2. 也是原有的逻辑:自动下载 Chromium
|
||||
logger.info("正在通过 pyppeteer 自动安装 Chromium...")
|
||||
logger.info("启动后台任务安装 Chromium...")
|
||||
asyncio.create_task(PDFInstaller._background_playwright_install())
|
||||
|
||||
# 检查是否已经在下载中
|
||||
if PDFInstaller._download_status["in_progress"]:
|
||||
return "⏳ Chromium 正在后台下载中,请稍候..."
|
||||
return """🚀 浏览器内核安装任务已启动
|
||||
|
||||
# 启动异步下载任务
|
||||
PDFInstaller._download_status["in_progress"] = True
|
||||
PDFInstaller._download_status["completed"] = False
|
||||
PDFInstaller._download_status["failed"] = False
|
||||
PDFInstaller._download_status["error_message"] = None
|
||||
|
||||
# 在后台线程中启动下载
|
||||
asyncio.create_task(PDFInstaller._background_chromium_download())
|
||||
|
||||
return """🚀 依赖安装任务已启动
|
||||
|
||||
1. Linux 系统依赖正在尝试自动安装...
|
||||
2. Chromium 下载已在后台启动...
|
||||
|
||||
这可能需要几分钟时间,请稍候...
|
||||
下载过程不会阻塞 Bot 的正常运行。
|
||||
|
||||
如果下载超时(10分钟),将自动取消。"""
|
||||
正在运行 `playwright install chromium`...
|
||||
这可能需要几分钟时间,取决于网络速度。
|
||||
安装过程不会阻塞 Bot 的正常运行。
|
||||
下载完成后平台日志会显示安装完成的日志。
|
||||
"""
|
||||
|
||||
except Exception as e:
|
||||
PDFInstaller._download_status["in_progress"] = False
|
||||
PDFInstaller._download_status["failed"] = True
|
||||
PDFInstaller._download_status["error_message"] = str(e)
|
||||
logger.error(f"启动依赖安装时出错: {e}")
|
||||
return f"❌ 启动依赖安装时出错: {str(e)}"
|
||||
PDFInstaller._install_status["in_progress"] = False
|
||||
logger.error(f"启动安装任务失败: {e}")
|
||||
return f"❌ 启动安装任务失败: {e}"
|
||||
|
||||
@staticmethod
|
||||
async def _install_linux_deps():
|
||||
"""尝试在 Linux 下安装 Chromium 所需的依赖库"""
|
||||
async def _background_playwright_install():
|
||||
"""后台运行 playwright install"""
|
||||
try:
|
||||
# 检查是否是 Debian/Ubuntu 系列
|
||||
try:
|
||||
# 简单检查 apt-get 是否存在
|
||||
logger.info("开始运行 playwright install chromium...")
|
||||
|
||||
# 使用 shell 命令确保能找到 path 中的 playwright
|
||||
# 或者使用 python -m playwright install chromium
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"which",
|
||||
"apt-get",
|
||||
sys.executable,
|
||||
"-m",
|
||||
"playwright",
|
||||
"install",
|
||||
"chromium",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await process.communicate()
|
||||
if process.returncode != 0:
|
||||
return "非 Debian/Ubuntu 系统,跳过自动安装系统库"
|
||||
except Exception:
|
||||
return "无法检测包管理器,跳过自动安装系统库"
|
||||
|
||||
logger.info("检测到 Debian/Ubuntu 系统,开始安装依赖库...")
|
||||
|
||||
# 依赖列表
|
||||
deps = [
|
||||
"ca-certificates",
|
||||
"fonts-liberation",
|
||||
"libappindicator3-1",
|
||||
"libasound2",
|
||||
"libatk-bridge2.0-0",
|
||||
"libatk1.0-0",
|
||||
"libc6",
|
||||
"libcairo2",
|
||||
"libcups2",
|
||||
"libdbus-1-3",
|
||||
"libexpat1",
|
||||
"libfontconfig1",
|
||||
"libgbm1",
|
||||
"libgcc1",
|
||||
"libglib2.0-0",
|
||||
"libgtk-3-0",
|
||||
"libnspr4",
|
||||
"libnss3",
|
||||
"libpango-1.0-0",
|
||||
"libpangocairo-1.0-0",
|
||||
"libstdc++6",
|
||||
"libx11-6",
|
||||
"libx11-xcb1",
|
||||
"libxcb1",
|
||||
"libxcomposite1",
|
||||
"libxcursor1",
|
||||
"libxdamage1",
|
||||
"libxext6",
|
||||
"libxfixes3",
|
||||
"libxi6",
|
||||
"libxrandr2",
|
||||
"libxrender1",
|
||||
"libxss1",
|
||||
"libxtst6",
|
||||
"lsb-release",
|
||||
"wget",
|
||||
"xdg-utils",
|
||||
# Chinese Fonts
|
||||
"fonts-noto-cjk",
|
||||
"fonts-wqy-zenhei",
|
||||
# Emoji Fonts
|
||||
"fonts-noto-color-emoji",
|
||||
]
|
||||
|
||||
# 使用 shell=True 来执行连接命令,但在 asyncio 中通常使用 shell wrap
|
||||
# 这里我们分两步执行
|
||||
|
||||
logger.info("执行: apt-get update")
|
||||
proc_update = await asyncio.create_subprocess_shell(
|
||||
"apt-get update",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc_update.communicate()
|
||||
if proc_update.returncode != 0:
|
||||
logger.error(f"apt-get update 失败: {stderr.decode()}")
|
||||
return f"apt-get update 失败: {stderr.decode()[:100]}..."
|
||||
|
||||
logger.info("执行: apt-get install ...")
|
||||
install_cmd = "apt-get install -y --no-install-recommends " + " ".join(deps)
|
||||
proc_install = await asyncio.create_subprocess_shell(
|
||||
install_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc_install.communicate()
|
||||
|
||||
if proc_install.returncode == 0:
|
||||
logger.info("Linux 系统依赖库安装成功")
|
||||
return "✅ Linux 系统依赖库安装成功"
|
||||
else:
|
||||
start_err = stderr.decode()[:200]
|
||||
logger.error(f"Linux 系统依赖库安装失败: {stderr.decode()}")
|
||||
return f"❌ Linux 系统依赖库安装失败: {start_err}..."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Linux 依赖安装异常: {e}")
|
||||
return f"❌ Linux 依赖安装异常: {e}"
|
||||
|
||||
@staticmethod
|
||||
async def _background_chromium_download():
|
||||
"""后台下载 Chromium,带超时控制"""
|
||||
try:
|
||||
logger.info("后台 Chromium 下载任务开始")
|
||||
|
||||
# 设置10分钟超时
|
||||
timeout_seconds = 600
|
||||
|
||||
try:
|
||||
# 使用 asyncio.wait_for 实现超时控制
|
||||
success = await asyncio.wait_for(
|
||||
PDFInstaller._download_chromium_via_pyppeteer(),
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
|
||||
if success:
|
||||
PDFInstaller._download_status["completed"] = True
|
||||
PDFInstaller._download_status["failed"] = False
|
||||
logger.info("✅ Chromium 后台下载完成!")
|
||||
return "✅ Chromium 后台下载完成!"
|
||||
else:
|
||||
PDFInstaller._download_status["failed"] = True
|
||||
PDFInstaller._download_status["error_message"] = (
|
||||
"下载失败,请检查网络连接"
|
||||
)
|
||||
logger.error("❌ Chromium 下载失败")
|
||||
return "❌ Chromium 下载失败"
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
PDFInstaller._download_status["failed"] = True
|
||||
PDFInstaller._download_status["error_message"] = (
|
||||
f"下载超时({timeout_seconds}秒)"
|
||||
)
|
||||
logger.error(f"❌ Chromium 下载超时({timeout_seconds}秒)")
|
||||
return f"❌ Chromium 下载超时({timeout_seconds}秒)"
|
||||
|
||||
except Exception as e:
|
||||
PDFInstaller._download_status["failed"] = True
|
||||
PDFInstaller._download_status["error_message"] = str(e)
|
||||
logger.error(f"后台下载 Chromium 时出错: {e}", exc_info=True)
|
||||
return f"❌ 后台下载 Chromium 时出错: {e}"
|
||||
finally:
|
||||
PDFInstaller._download_status["in_progress"] = False
|
||||
|
||||
@staticmethod
|
||||
async def _download_chromium_via_pyppeteer():
|
||||
"""通过 pyppeteer 自动下载 Chromium(不启动浏览器)"""
|
||||
try:
|
||||
logger.info("开始通过 pyppeteer 下载 Chromium...")
|
||||
|
||||
# 尝试方法1:使用 pyppeteer-install 命令行工具
|
||||
# 这是官方推荐的安装方式,会自动处理版本和路径
|
||||
try:
|
||||
logger.info("方法1: 尝试调用 pyppeteer-install 命令...")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"pyppeteer-install",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
# 等待完成,设置较长的超时
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode == 0:
|
||||
logger.info(f"✅ pyppeteer-install 执行成功: {stdout.decode()}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"pyppeteer-install 执行失败: {stderr.decode()}")
|
||||
except Exception as e:
|
||||
logger.warning(f"无法调用 pyppeteer-install 命令: {e}")
|
||||
PDFInstaller._install_status["completed"] = True
|
||||
logger.info(f"✅ Playwright Chromium 安装成功: {stdout.decode()}")
|
||||
|
||||
# 尝试方法2:直接调用内部下载函数
|
||||
try:
|
||||
logger.info("方法2: 尝试直接调用 pyppeteer.chromium_downloader...")
|
||||
import pyppeteer.chromium_downloader
|
||||
|
||||
# 检查是否已存在
|
||||
if pyppeteer.chromium_downloader.check_chromium():
|
||||
logger.info("✅ Chromium 已存在,无需下载")
|
||||
return True
|
||||
|
||||
logger.info("正在下载 Chromium...")
|
||||
# download_chromium 是同步阻塞的,需要在线程池中运行
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None, pyppeteer.chromium_downloader.download_chromium
|
||||
# 尝试安装系统依赖 (Linux only,通常不需要 root 无法执行,但尝试一下无妨或者提示用户)
|
||||
if sys.platform.startswith("linux"):
|
||||
logger.info("正在尝试安装系统依赖 (install-deps)...")
|
||||
# 无需 await 阻塞太久,这步通常需要 sudo,可能会失败,仅做尝试或提示
|
||||
# 真正的系统依赖安装通常由 Dockerfile 或用户手动完成
|
||||
# 这里我们仅记录日志建议
|
||||
logger.info(
|
||||
"💡 如果 Linux 下仍无法生成 PDF,请尝试运行: sudo playwright install-deps"
|
||||
)
|
||||
|
||||
if pyppeteer.chromium_downloader.check_chromium():
|
||||
logger.info("✅ Chromium 下载验证成功")
|
||||
return True
|
||||
else:
|
||||
logger.error("❌ Chromium 下载函数执行完成但未发现可执行文件")
|
||||
return False
|
||||
PDFInstaller._install_status["failed"] = True
|
||||
PDFInstaller._install_status["error_message"] = stderr.decode()
|
||||
logger.error(f"❌ Playwright Chromium 安装失败: {stderr.decode()}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"直接调用下载函数失败: {e}")
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"下载过程发生未知错误: {e}")
|
||||
return False
|
||||
PDFInstaller._install_status["failed"] = True
|
||||
PDFInstaller._install_status["error_message"] = str(e)
|
||||
logger.error(f"Playwright 安装后台任务出错: {e}")
|
||||
finally:
|
||||
PDFInstaller._install_status["in_progress"] = False
|
||||
|
||||
@staticmethod
|
||||
def get_pdf_status(config_manager) -> str:
|
||||
"""获取PDF功能状态"""
|
||||
if config_manager.pyppeteer_available:
|
||||
version = config_manager.pyppeteer_version or "未知版本"
|
||||
return f"✅ PDF 功能可用 (pyppeteer {version})"
|
||||
if config_manager.playwright_available:
|
||||
version = config_manager.playwright_version or "未知版本"
|
||||
|
||||
status = f"✅ PDF 功能可用 (playwright {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 功能不可用 - 需要安装 pyppeteer"
|
||||
return "❌ PDF 功能不可用 - 请输入 /安装PDF 进行安装"
|
||||
|
||||
Reference in New Issue
Block a user