From 2438ab5b98dd8c404b100f637aa5656ac677076a Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Tue, 20 Jan 2026 20:26:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(pdf):=20=E8=BF=81=E7=A7=BB=E4=B8=BA=20playw?= =?UTF-8?q?right?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PDF_功能说明.md | 114 ++---- _conf_schema.json | 6 + main.py | 22 +- requirements.txt | 2 +- scripts/mock_pdf_gen.py | 5 +- src/core/config.py | 101 ++--- src/reports/generators.py | 352 +++++++----------- .../templates/scrapbook/pdf_template.html | 35 +- src/scheduler/auto_scheduler.py | 2 +- src/utils/pdf_utils.py | 342 +++++------------ 10 files changed, 361 insertions(+), 620 deletions(-) diff --git a/PDF_功能说明.md b/PDF_功能说明.md index 189b2bb..b6653be 100644 --- a/PDF_功能说明.md +++ b/PDF_功能说明.md @@ -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. 如果遇到权限问题,请检查输出目录的写入权限 diff --git a/_conf_schema.json b/_conf_schema.json index f8862a7..8afdf03 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -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文件名格式", diff --git a/main.py b/main.py index 03e2def..772030f 100644 --- a/main.py +++ b/main.py @@ -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)}") diff --git a/requirements.txt b/requirements.txt index 70593cb..3f74a6a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -pyppeteer==1.0.2 \ No newline at end of file +playwright>=1.40.0 \ No newline at end of file diff --git a/scripts/mock_pdf_gen.py b/scripts/mock_pdf_gen.py index 76f7ce7..1b017d2 100644 --- a/scripts/mock_pdf_gen.py +++ b/scripts/mock_pdf_gen.py @@ -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 diff --git a/src/core/config.py b/src/core/config.py index e1398f2..6d98c84 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -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): diff --git a/src/reports/generators.py b/src/reports/generators.py index 029d810..84209bd 100644 --- a/src/reports/generators.py +++ b/src/reports/generators.py @@ -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},尝试自动检测..." + ) - 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", - ] - elif sys.platform.startswith("linux"): - # Linux 系统 Chrome/Chromium 路径 - chrome_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 = [ - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/Applications/Chromium.app/Contents/MacOS/Chromium", - ] + # 1. 如果没有自定义路径,尝试自动检测系统浏览器 + if not executable_path: + system_browser_paths = [] + if sys.platform.startswith("win"): + username = os.environ.get("USERNAME", "") + 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)" + ) - # 查找可用的浏览器 - 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 - break + 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"): + system_browser_paths = [ + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/snap/bin/chromium", + ] + elif sys.platform.startswith("darwin"): + 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", + ] - if not found_browser: - logger.info("未找到系统浏览器,将使用 pyppeteer 默认下载的 Chromium") - # 先尝试确保 Chromium 已下载 - try: - from pyppeteer import browser, launcher + # 尝试找到可用的系统浏览器 + for path in system_browser_paths: + if Path(path).exists(): + executable_path = path + logger.info(f"使用系统浏览器: {path}") + break - launcher_instance = launcher.Launcher( - headless=True, - args=[ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - ], - ) - 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}" - ) - - # 尝试启动浏览器 - try: - logger.info("正在启动浏览器...") - browser = await launch(**launch_options) - logger.info("浏览器启动成功") - except Exception as e: - logger.error(f"浏览器启动失败: {e}", exc_info=True) - return False - - try: - # 创建新页面,设置更合理的超时时间 - page = await browser.newPage() - - # 设置页面视口,减少内存占用 - await page.setViewport( - { - "width": 800, # Match A4 width approx (794px at 96PPI) - "height": 1000, - "deviceScaleFactor": 1, - "isMobile": False, - "hasTouch": False, - "isLandscape": False, - } - ) - - # 设置页面内容,使用更安全的加载方式 - 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,使用更保守的设置 - logger.info("开始生成PDF...") - pdf_options = { - "path": output_path, - "format": "A4", - "printBackground": True, - "margin": { - "top": "10mm", - "right": "10mm", - "bottom": "10mm", - "left": "10mm", - }, - "scale": 1.0, - "displayHeaderFooter": False, - "preferCSSPageSize": True, - "timeout": 60000, # 增加PDF生成超时时间到60秒 + # 定义默认启动参数 + launch_kwargs = { + "headless": True, + "args": [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--font-render-hinting=none", + ], } - await page.pdf(pdf_options) - logger.info(f"PDF 生成成功: {output_path}") - return True + if executable_path: + launch_kwargs["executable_path"] = executable_path + launch_kwargs["channel"] = ( + "chrome" if "chrome" in executable_path.lower() else "msedge" + ) - except Exception as e: - logger.error(f"PDF生成过程中出错: {e}") - return False + try: + 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"] + ) - 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}") + except Exception as e: + 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 - # 等待一小段时间让资源释放 - await asyncio.sleep(0.5) + if not browser: + return False - # 关闭浏览器 + try: + context = await browser.new_context(device_scale_factor=1) + page = await context.new_page() + + # 设置页面内容 + await page.set_content( + html_content, wait_until="networkidle", timeout=60000 + ) + + # 生成 PDF + logger.info("开始生成 PDF...") + await page.pdf( + path=output_path, + format="A4", + print_background=True, + margin={ + "top": "10mm", + "right": "10mm", + "bottom": "10mm", + "left": "10mm", + }, + ) + logger.info(f"PDF 生成成功: {output_path}") + return True + + except Exception as e: + logger.error(f"PDF 生成过程出错: {e}") + return False + finally: + if browser: 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 diff --git a/src/reports/templates/scrapbook/pdf_template.html b/src/reports/templates/scrapbook/pdf_template.html index 6701e5f..2a7b0a8 100644 --- a/src/reports/templates/scrapbook/pdf_template.html +++ b/src/reports/templates/scrapbook/pdf_template.html @@ -7,9 +7,10 @@ 五彩斑斓的一天,来看看群里发生了什么吧! · {{current_date}} - - - + +