fix(pdf): 迁移为 playwright

This commit is contained in:
SXP-Simon
2026-01-20 20:26:20 +08:00
parent b5e094ff1a
commit 2438ab5b98
10 changed files with 361 additions and 620 deletions
+25 -89
View File
@@ -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. 如果遇到权限问题,请检查输出目录的写入权限
+6
View File
@@ -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文件名格式",
+8 -14
View File
@@ -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
View File
@@ -1 +1 @@
pyppeteer==1.0.2
playwright>=1.40.0
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
}
+1 -1
View File
@@ -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
View File
@@ -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 进行安装"