diff --git a/README.md b/README.md index a11e0d6..23f7329 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ _✨ 一个基于 AstrBot 的智能群聊分析插件,支持 **QQ (OneBot)** ### 📊 可视化报告 - **多种格式**: 支持图片和文本输出格式 - **精美图片**: 生成美观的可视化报告 - - **PDF报告**: 生成专业的PDF格式分析报告(需配置) + - **HTML报告**: 生成清晰的HTML格式分析报告,可以进一步配置为外链形式发送 - **QQ群**: 支持上传到群相册和群文件,查阅黑历史友好 - **详细数据**: 包含消息统计、时间分布、关键词、金句等 diff --git a/_conf_schema.json b/_conf_schema.json index 5a77417..e629cb1 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -61,7 +61,7 @@ "type": "string", "description": "输出格式", "default": "image", - "hint": "分析报告的输出格式:image(图片)、text(文本)、pdf(PDF文件)、html(HTML文件)。使用 PDF 需要额外配置,根据文件中的 PDF_功能说明.md 进行配置" + "hint": "分析报告的输出格式:image(图片)、text(文本)、html(HTML文件)。" }, "report_template": { "type": "string", @@ -350,31 +350,6 @@ } } }, - "pdf": { - "description": "PDF 设置", - "type": "object", - "hint": "PDF 报告输出相关配置,包括输出目录、浏览器路径和文件名格式", - "items": { - "pdf_output_dir": { - "type": "string", - "description": "PDF输出目录", - "default": "", - "hint": "PDF报告文件的保存目录。留空则自动使用插件数据目录下的 reports 目录。" - }, - "browser_path": { - "type": "string", - "description": "自定义浏览器路径", - "default": "", - "hint": "要填写的话请你清楚自己在干什么。自定义浏览器的可执行文件路径(如 Chrome 或 Edge 的 .exe 文件)。提示:如果是在网页后台设置,则直接输入普通路径即可(如 C:\\Program Files\\...);如果是手动编辑 config.json 文件,请务必使用双反斜杠 '\\\\' 分隔路径。" - }, - "pdf_filename_format": { - "type": "string", - "description": "PDF文件名格式", - "default": "群聊分析报告_${group_id}_${date}_${ulid}.pdf", - "hint": "PDF文件名格式,支持变量:${group_id}(群号)、${date}(日期)、${ulid}(时间排序ID),支持子目录/层级。" - } - } - }, "html": { "description": "HTML 设置", "type": "object", diff --git a/docs/pdf_feature_guide.md b/docs/pdf_feature_guide.md deleted file mode 100644 index b10c77e..0000000 --- a/docs/pdf_feature_guide.md +++ /dev/null @@ -1,90 +0,0 @@ -# 群日常分析插件 - PDF 功能说明 - -## 概述 - -本插件现已支持生成 PDF 格式的群聊分析报告,提供更专业的报告输出格式。PDF 引擎已迁移至 Playwright,提供更好的兼容性。 - -## 功能特性 - -- 📄 **PDF 报告生成**: 将群聊分析结果导出为精美的 PDF 文档 -- 🎨 **专业排版**: 针对 PDF 打印优化的样式设计 -- 📊 **完整内容**: 包含所有分析数据(统计信息、话题分析、用户称号、群聊金句) -- 🔧 **灵活配置**: 可自定义输出目录、文件名格式以及**自定义浏览器路径** - -## 安装依赖 - -使用 PDF 功能前,需要安装 playwright 库。有以下几种安装方式: - -### 方式一:使用命令安装(推荐) - -在群聊中直接使用安装命令: - -``` -/安装PDF -``` - -此命令会自动: -- 检查 playwright 是否已安装 -- 如未安装,自动安装 playwright 库 -- 检查是否需要下载 Chromium 浏览器(已配置自定义路径则跳过) -- 测试 PDF 功能是否正常 - -### 方式二:手动安装 - -```bash -pip install playwright -playwright install chromium -``` - -## 使用方法 - -### 1. 设置输出格式 - -使用新增的 `/设置格式` 命令来切换输出格式: - -``` -/设置格式 pdf -``` - -### 2. 生成 PDF 报告 - -设置为 PDF 格式后,使用常规的分析命令: - -``` -/群分析 -``` - -### 3. 高级配置:自定义浏览器路径 - -如果您希望使用系统中已有的 Chrome 或 Edge,或者在特定的路径下运行浏览器,可以在配置中设置 `browser_path`: - -```json -{ - "browser_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" -} -``` - -## 故障排除 - -### 1. Playwright 安装失败 - -请确保您的系统网络能够访问官网下载浏览器内核。如果是在受限环境下,建议使用系统的 Chrome/Edge 并配置 `browser_path`。 - -### 2. PDF 生成失败 - -- 检查 `browser_path` 是否正确且可执行。 -- 确保系统安装了必要的字体。 - -## 更新日志 - -### v1.2.0 -- 🚀 **Playwright 迁移**: 替换了不稳定的 pyppeteer。 -- 🛠️ **自定义浏览器**: 支持手动指定 Chrome/Edge 路径。 -- ⚡ **安装优化**: 自动检测系统浏览器并减少不必要的下载。 - -## 技术实现 - -PDF 生成基于以下技术: -- **Playwright**: 现代无头浏览器控制 -- **HTML/CSS**: 报告模板和样式 -- **异步处理**: 非阻塞的 PDF 生成过程 diff --git a/main.py b/main.py index 220785a..b404982 100644 --- a/main.py +++ b/main.py @@ -47,7 +47,6 @@ from .src.infrastructure.reporting.generators import ReportGenerator from .src.infrastructure.scheduler.auto_scheduler import AutoScheduler from .src.shared.trace_context import TraceContext, TraceLogFilter from .src.utils.logger import logger -from .src.utils.pdf_utils import PDFInstaller from .src.utils.resilience import GlobalRateLimiter @@ -621,21 +620,6 @@ class GroupDailyAnalysis(Star): await adapter.send_text_report(group_id, text_report) return - elif output_format == "pdf": - pdf_path = await self.report_generator.generate_pdf_report( - analysis_result, - group_id, - avatar_getter=avatar_url_getter, - nickname_getter=nickname_getter, - ) - if pdf_path: - if not await adapter.send_file(group_id, pdf_path): - yield event.chain_result( - [File(name=Path(pdf_path).name, file=pdf_path)] - ) - else: - yield event.plain_result("⚠️ PDF 生成失败。") - elif output_format == "html": html_path, json_path = await self.report_generator.generate_html_report( analysis_result, @@ -689,29 +673,19 @@ class GroupDailyAnalysis(Star): if not format_type: current_format = self.config_manager.get_output_format() - pdf_status = ( - "✅" - if self.config_manager.playwright_available - else "❌ (需安装 Playwright)" - ) yield event.plain_result(f"""📊 当前输出格式: {current_format} 可用格式: • image - 图片格式 (默认) • text - 文本格式 -• pdf - PDF 格式 {pdf_status} • html - HTML 格式 用法: /设置格式 [格式名称]""") return format_type = format_type.lower() - if format_type not in ["image", "text", "pdf", "html"]: - yield event.plain_result("❌ 无效的格式类型,支持: image, text, pdf, html") - return - - if format_type == "pdf" and not self.config_manager.playwright_available: - yield event.plain_result("❌ PDF 格式不可用,请使用 /安装PDF 命令安装依赖") + if format_type not in ["image", "text", "html"]: + yield event.plain_result("❌ 无效的格式类型,支持: image, text, html") return self.config_manager.set_output_format(format_type) @@ -807,37 +781,6 @@ class GroupDailyAnalysis(Star): ) yield event.chain_result([preview_nodes]) - @filter.command("安装PDF", alias={"install_pdf"}) - @filter.permission_type(PermissionType.ADMIN) - async def install_pdf_deps(self, event: AstrMessageEvent): - """ - 安装 PDF 功能依赖(跨平台支持) - 用法: /安装PDF - """ - if self._terminating: - return - - current_task = asyncio.current_task() - if current_task: - self._background_tasks.add(current_task) - - try: - yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...") - - result = await PDFInstaller.install_playwright( - self.config_manager, task_registry=self._background_tasks - ) - yield event.plain_result(result) - - except asyncio.CancelledError: - logger.info("PDF 安装任务被取消") - except Exception as e: - logger.error(f"安装 PDF 依赖失败: {e}", exc_info=True) - yield event.plain_result(f"❌ 安装过程中出现错误: {str(e)}") - finally: - if current_task: - self._background_tasks.discard(current_task) - @filter.command("分析设置", alias={"analysis_settings"}) @filter.permission_type(PermissionType.ADMIN) async def analysis_settings(self, event: AstrMessageEvent, action: str = "status"): @@ -915,7 +858,6 @@ class GroupDailyAnalysis(Star): ) auto_time = self.config_manager.get_auto_analysis_time() - pdf_status = PDFInstaller.get_pdf_status(self.config_manager) output_format = self.config_manager.get_output_format() min_threshold = self.config_manager.get_min_messages_threshold() @@ -941,12 +883,11 @@ class GroupDailyAnalysis(Star): • 增量分析: {incremental_status_text} • 调试模式: {debug_status} (增量立即报告) • 输出格式: {output_format} -• PDF 功能: {pdf_status} • 最小消息数: {min_threshold} 💡 可用命令: enable, disable, status, reload, test, incremental_debug -💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化) -💡 其他命令: /设置格式, /安装PDF, /增量状态""") +💡 支持的输出格式: image, text (图片包含活跃度可视化) +💡 其他命令: /设置格式, /增量状态""") @filter.command("增量状态", alias={"incremental_status"}) @filter.permission_type(PermissionType.ADMIN) diff --git a/requirements.txt b/requirements.txt index 2d64e91..cccf4e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -playwright>=1.40.0 diskcache ulid-py \ No newline at end of file diff --git a/src/domain/repositories/report_repository.py b/src/domain/repositories/report_repository.py index f457be6..0c02399 100644 --- a/src/domain/repositories/report_repository.py +++ b/src/domain/repositories/report_repository.py @@ -25,17 +25,22 @@ class IReportGenerator(ABC): pass @abstractmethod - async def generate_pdf_report( + async def generate_html_report( self, analysis_result: dict, group_id: str, - avatar_getter: Any = None, + avatar_url_getter: Any = None, nickname_getter: Any = None, - ) -> str | None: - """生成 PDF 报告""" + ) -> tuple[str | None, str | None]: + """生成 HTML 报告""" pass @abstractmethod def generate_text_report(self, analysis_result: dict) -> str: """生成文本报告""" pass + + @abstractmethod + async def close(self): + """释放资源""" + pass diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index beb1ac6..502e361 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -1,10 +1,8 @@ """ 配置管理模块 - 基础设施层 -负责处理插件配置和PDF依赖检查 +负责处理插件配置 """ -import sys - from astrbot.api import AstrBotConfig from astrbot.api.star import StarTools @@ -21,15 +19,11 @@ class ConfigManager: - llm: LLM 设置 - analysis_features: 分析功能开关 - incremental: 增量分析设置 - - pdf: PDF 设置 - prompts: 提示词模板 """ def __init__(self, config: AstrBotConfig): self.config = config - self._playwright_available = False - self._playwright_version = None - self._check_playwright_availability() def _get_group(self, group: str) -> dict: """获取指定分组的配置字典,不存在时返回空字典""" @@ -240,26 +234,6 @@ class ConfigManager: "plugin_specific_persona_id", "" ) - def get_pdf_output_dir(self) -> str: - """获取PDF输出目录""" - from pathlib import Path - - from astrbot.core.utils.astrbot_path import get_astrbot_data_path - - try: - default_path = StarTools.get_data_dir() / "reports" - val = self._get_group("pdf").get("pdf_output_dir") - return val if val else str(default_path) - except Exception: - val = self._get_group("pdf").get("pdf_output_dir") - fallback_path = ( - Path(get_astrbot_data_path()) - / "plugin_data" - / "astrbot_plugin_qq_group_daily_analysis" - / "reports" - ) - return val if val else str(fallback_path) - def get_bot_self_ids(self) -> list: """获取机器人自身的 ID 列表 (兼容 bot_qq_ids)""" basic = self._get_group("basic") @@ -268,12 +242,6 @@ class ConfigManager: ids = basic.get("bot_qq_ids", []) return ids - def get_pdf_filename_format(self) -> str: - """获取PDF文件名格式""" - return self._get_group("pdf").get( - "pdf_filename_format", "群聊分析报告_{group_id}_{date}.pdf" - ) - def get_html_output_dir(self) -> str: """获取HTML输出目录""" from pathlib import Path @@ -407,11 +375,6 @@ class ConfigManager: ) # 2. 文件名格式升级 - modified |= self._upgrade_config_item( - "pdf", - "pdf_filename_format", - self.set_pdf_filename_format, - ) modified |= self._upgrade_config_item( "html", "html_filename_format", @@ -616,16 +579,6 @@ class ConfigManager: self._ensure_group("analysis_features")["max_golden_quotes"] = count self.config.save_config() - def set_pdf_output_dir(self, directory: str): - """设置PDF输出目录""" - self._ensure_group("pdf")["pdf_output_dir"] = directory - self.config.save_config() - - def set_pdf_filename_format(self, format_str: str): - """设置PDF文件名格式""" - self._ensure_group("pdf")["pdf_filename_format"] = format_str - self.config.save_config() - def set_html_filename_format(self, format_str: str): """设置HTML文件名格式""" self._ensure_group("html")["html_filename_format"] = format_str @@ -754,94 +707,6 @@ class ConfigManager: """获取多群增量分析的交错间隔(秒),避免 API 压力""" return self._get_group("incremental").get("incremental_stagger_seconds", 30) - @property - def playwright_available(self) -> bool: - """检查playwright是否可用""" - return self._playwright_available - - @property - def playwright_version(self) -> str | None: - """获取playwright版本""" - return self._playwright_version - - def _check_playwright_availability(self): - """检查 playwright 可用性""" - try: - import importlib.util - - 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._playwright_version = playwright.__version__ - logger.info(f"使用 playwright {self._playwright_version} 作为 PDF 引擎") - except AttributeError: - self._playwright_version = "unknown" - logger.info("使用 playwright (版本未知) 作为 PDF 引擎") - - except ImportError: - self._playwright_available = False - self._playwright_version = None - logger.warning( - "playwright 未安装,PDF 功能将不可用。请使用 pip install playwright 安装,并运行 playwright install chromium" - ) - - def get_browser_path(self) -> str: - """获取自定义浏览器路径""" - return self._get_group("pdf").get("browser_path", "") - - def set_browser_path(self, path: str): - """设置自定义浏览器路径""" - self._ensure_group("pdf")["browser_path"] = path - self.config.save_config() - - def reload_playwright(self) -> bool: - """重新加载 playwright 模块""" - try: - logger.info("开始重新加载 playwright 模块...") - - modules_to_remove = [ - mod for mod in sys.modules.keys() if mod.startswith("playwright") - ] - logger.info(f"移除模块: {modules_to_remove}") - for mod in modules_to_remove: - del sys.modules[mod] - - try: - import playwright - - self._playwright_available = True - try: - self._playwright_version = playwright.__version__ - logger.info( - f"重新加载成功,playwright 版本: {self._playwright_version}" - ) - except AttributeError: - self._playwright_version = "unknown" - logger.info("重新加载成功,playwright 版本未知") - - return True - - except ImportError: - logger.info("playwright 重新导入可能需要重启 AstrBot") - self._playwright_available = False - self._playwright_version = None - return False - except Exception: - logger.info("playwright 重新导入失败") - self._playwright_available = False - self._playwright_version = None - return False - - except Exception as e: - logger.error(f"重新加载 playwright 时出错: {e}") - return False - def save_config(self): """保存配置到AstrBot配置系统""" try: diff --git a/src/infrastructure/reporting/__init__.py b/src/infrastructure/reporting/__init__.py index 6ff4113..52a016a 100644 --- a/src/infrastructure/reporting/__init__.py +++ b/src/infrastructure/reporting/__init__.py @@ -1,6 +1,6 @@ """ 报告生成模块 -包含HTML、PDF、文本报告生成功能 +包含HTML、图片、文本报告生成功能 """ from .generators import ReportGenerator diff --git a/src/infrastructure/reporting/dispatcher.py b/src/infrastructure/reporting/dispatcher.py index e168437..0326b78 100644 --- a/src/infrastructure/reporting/dispatcher.py +++ b/src/infrastructure/reporting/dispatcher.py @@ -48,8 +48,6 @@ class ReportDispatcher: success = False if output_format == "image": success = await self._dispatch_image(group_id, analysis_result, platform_id) - elif output_format == "pdf": - success = await self._dispatch_pdf(group_id, analysis_result, platform_id) elif output_format == "html": success = await self._dispatch_html(group_id, analysis_result, platform_id) else: @@ -113,43 +111,6 @@ class ReportDispatcher: ) return await self._dispatch_text(group_id, analysis_result, platform_id) - async def _dispatch_pdf( - self, group_id: str, analysis_result: dict[str, Any], platform_id: str | None - ) -> bool: - trace_id = TraceContext.get() - # 1. 检查 Playwright - if not self.config_manager.playwright_available: - logger.warning( - f"[{trace_id}] Playwright not available, falling back to text." - ) - return await self._dispatch_text(group_id, analysis_result, platform_id) - - # 2. 生成 PDF - pdf_path = None - try: - pdf_path = await self.report_generator.generate_pdf_report( - analysis_result, group_id - ) - except Exception as e: - logger.error(f"[{trace_id}] Failed to generate PDF report: {e}") - - # 3. 发送 PDF - if pdf_path: - sent = await self.message_sender.send_file( - group_id, - pdf_path, - caption="📊 每日群聊分析报告已生成:", - platform_id=platform_id, - ) - if sent: - return True - - # 4. 回退:文本报告 - logger.warning( - f"[{trace_id}] PDF dispatch failed, falling back to text report." - ) - return await self._dispatch_text(group_id, analysis_result, platform_id) - async def _dispatch_html( self, group_id: str, analysis_result: dict[str, Any], platform_id: str | None ) -> bool: diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py index 9ef599b..39ecbba 100644 --- a/src/infrastructure/reporting/generators.py +++ b/src/infrastructure/reporting/generators.py @@ -282,61 +282,6 @@ class ReportGenerator(IReportGenerator): await self._avatar_session.close() self._avatar_session = None - async def generate_pdf_report( - self, - analysis_result: dict, - group_id: str, - avatar_getter=None, - nickname_getter=None, - ) -> str | None: - """生成PDF格式的分析报告""" - try: - # 确保输出目录存在(使用 asyncio.to_thread 避免阻塞) - output_dir = Path(self.config_manager.get_pdf_output_dir()) - await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True) - - # 生成文件路径,支持 {group_id}/{date}/{ulid} 自定义子目录 - current_date = datetime.now().strftime("%Y%m%d") - pdf_path = self._build_safe_report_path( - output_dir, - self.config_manager.get_pdf_filename_format(), - group_id=group_id, - date=current_date, - ) - - # 准备渲染数据 - render_data = await self._prepare_render_data( - analysis_result, - chart_template="activity_chart_pdf.html", - avatar_url_getter=avatar_getter, - nickname_getter=nickname_getter, - ) - logger.info(f"PDF 渲染数据准备完成,包含 {len(render_data)} 个字段") - - # 生成 HTML 内容(使用 Jinja2 渲染器以支持逻辑标签) - html_content = self.html_templates.render_template( - "pdf_template.html", **render_data - ) - - # 检查HTML内容是否有效 - if not html_content: - logger.error("PDF报告HTML渲染失败:返回空内容") - return None - - logger.info(f"HTML 内容生成完成,长度: {len(html_content)} 字符") - - # 转换为 PDF - success = await self._html_to_pdf(html_content, str(pdf_path)) - - if success: - return str(pdf_path.absolute()) - else: - return None - - except Exception as e: - logger.error(f"生成 PDF 报告失败: {e}") - return None - async def generate_html_report( self, analysis_result: dict, @@ -920,171 +865,3 @@ class ReportGenerator(IReportGenerator): logger.debug("头像缓存已关闭") except Exception as e: logger.warning(f"关闭头像缓存失败: {e}") - - async def _html_to_pdf(self, html_content: str, output_path: str) -> bool: - """将 HTML 内容转换为 PDF 文件""" - try: - # 动态导入 playwright - try: - from playwright.async_api import async_playwright # type: ignore - except ImportError: - logger.error("playwright 未安装,无法生成 PDF") - logger.info("💡 请尝试运行: pip install playwright") - return False - - import os - import sys - - logger.info("启动浏览器进行 PDF 转换 (使用 Playwright)") - - async with async_playwright() as p: - browser = None - - executable_path = None - - # 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"): - 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)" - ) - - 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", - ] - - # 尝试找到可用的系统浏览器 - for path in system_browser_paths: - if Path(path).exists(): - executable_path = path - logger.info(f"使用系统浏览器: {path}") - break - - # 定义默认启动参数 - launch_kwargs = { - "headless": True, - "args": [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--font-render-hinting=none", - ], - } - - if executable_path: - launch_kwargs["executable_path"] = executable_path - launch_kwargs["channel"] = ( - "chrome" if "chrome" in executable_path.lower() else "msedge" - ) - - 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"] - ) - - 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 - - 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() - - except Exception as e: - logger.error(f"Playwright 运行出错: {e}") - return False diff --git a/src/infrastructure/reporting/templates.py b/src/infrastructure/reporting/templates.py index d277ee5..fae62ce 100644 --- a/src/infrastructure/reporting/templates.py +++ b/src/infrastructure/reporting/templates.py @@ -91,29 +91,6 @@ class HTMLTemplates: logger.error(f"加载图片模板失败: {e}") return "" - async def get_pdf_template_async(self) -> str: - """获取PDF报告的HTML模板(异步版本,返回原始模板字符串)""" - try: - env = await self._get_env_async() - template = env.get_template("pdf_template.html") - return await asyncio.to_thread( - self._read_template_file_sync, template.filename - ) - except Exception as e: - logger.error(f"加载PDF模板失败: {e}") - return "" - - def get_pdf_template(self) -> str: - """获取PDF报告的HTML模板(同步版本,向后兼容)""" - try: - env = self._get_env() - template = env.get_template("pdf_template.html") - with open(template.filename, encoding="utf-8") as f: - return f.read() - except Exception as e: - logger.error(f"加载PDF模板失败: {e}") - return "" - def render_template(self, template_name: str, **kwargs) -> str: """渲染指定的模板文件 diff --git a/src/infrastructure/reporting/templates/ATRI/pdf_template.html b/src/infrastructure/reporting/templates/ATRI/pdf_template.html deleted file mode 100644 index 8ada117..0000000 --- a/src/infrastructure/reporting/templates/ATRI/pdf_template.html +++ /dev/null @@ -1,577 +0,0 @@ - - - - - - ATRI 群聊日报 · PDF版 - - - -
-
-

ATRI 群聊日报 · {{ current_date }}

-

{{current_datetime}}

-
- -
-
-
今日群聊概览
-

今天的聊天像夏日的海风,温柔又热闹。

-
- 今天共有 {{ message_count }} 条消息,来自 {{ participant_count }} 位成员。我已经把这些温暖的对话整理好了,揉进这份属于你们的日报里。 -
-
-
-
MOST ACTIVE PERIOD
-
{{ current_datetime }}
-
{{ most_active_period }}
-
这段时间的讨论最集中,像潮水推到岸边一样热烈,是今天群聊最明亮的一段波峰。
-
-
- -
-
-
{{message_count}}
-
总消息数
-
-
-
{{participant_count}}
-
参与人数
-
-
-
{{total_characters}}
-
总字符数
-
-
-
{{emoji_count}}
-
表情数量
-
-
- -
-

活跃潮汐

-
- {{hourly_chart_html}} -
-
- -
-

热门话题

- -
- -
-

今日称号

- -
- -
-

今日金句

-
- {{quotes_html | safe}} -
-
- -
-

Token 统计

-
-
-
- 总消耗 - {{total_tokens}} tokens -
-
- 提示词 Token - {{prompt_tokens}} tokens -
-
- 生成内容 Token - {{completion_tokens}} tokens -
-
-
-
- - -
- - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/HatsuneMiku/pdf_template.html b/src/infrastructure/reporting/templates/HatsuneMiku/pdf_template.html deleted file mode 100644 index 20540e5..0000000 --- a/src/infrastructure/reporting/templates/HatsuneMiku/pdf_template.html +++ /dev/null @@ -1,320 +0,0 @@ - - - - - - - 群聊日常分析看板 · {{current_date}} - - - - - - -
-
01
-
39
-
[ SYSTEM_VOCALOID_INITIALIZED ]
HATSUNE MIKU v4X
-
- -
-

群聊日常分析看板

- -
- -
-
-
- -
今日消息数
-
-
{{message_count}}
-
-
-
- -
活跃成员数
-
-
{{participant_count}}
-
-
-
- -
表情互动
-
-
{{emoji_count}}
-
-
-
- -
今日字数累计
-
-
{{total_characters}}
-
-
- -
-
-
- - 峰值活跃时间段 -
-
{{most_active_period}}
-
- -
- Peak Activity Visualization -
-
- -
-
-
-
24小时活动图段
-
- {{hourly_chart_html | safe}} -
- - {{topics_html | safe}} - {{titles_html | safe}} - {{quotes_html | safe}} - {{chat_quality_html | safe}} - - -
- - - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/format/activity_chart_pdf.html b/src/infrastructure/reporting/templates/format/activity_chart_pdf.html deleted file mode 100644 index d4d2991..0000000 --- a/src/infrastructure/reporting/templates/format/activity_chart_pdf.html +++ /dev/null @@ -1,14 +0,0 @@ -{% for item in chart_data %} -
-
{{ "%02d" | format(item.hour) }}:00
-
-
- {% if item.percentage > 15 %} - {{ item.count }} - {% endif %} -
- {% if item.percentage <= 15 %} {{ item.count }} - {% endif %} -
-
-{% endfor %} diff --git a/src/infrastructure/reporting/templates/format/pdf_template.html b/src/infrastructure/reporting/templates/format/pdf_template.html deleted file mode 100644 index 8d1e7ce..0000000 --- a/src/infrastructure/reporting/templates/format/pdf_template.html +++ /dev/null @@ -1,563 +0,0 @@ - - - - - - - 群聊日常分析报告 - - - - - -
-
-

群聊日常分析报告

-
{{current_date}}
-
-
-
-

基础统计

-
-
-
{{message_count}}
-
消息总数
-
-
-
{{participant_count}}
-
参与人数
-
-
-
{{total_characters}}
-
总字符数
-
-
-
{{emoji_count}}
-
表情数量
-
-
-
-
{{most_active_period}}
-
最活跃时段
-
-
- -
-
-
24小时活跃度分布
-
- {{hourly_chart_html | safe}} -
- - {{topics_html | safe}} - {{titles_html | safe}} - {{quotes_html | safe}} - - {% if chat_quality_html %} -
-

智睿洞察 // Chat Quality

-
- {{ chat_quality_html | safe }} -
-
- {% endif %} -
- -
- - - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/hack/activity_chart_pdf.html b/src/infrastructure/reporting/templates/hack/activity_chart_pdf.html deleted file mode 100644 index 7371ee8..0000000 --- a/src/infrastructure/reporting/templates/hack/activity_chart_pdf.html +++ /dev/null @@ -1,13 +0,0 @@ -{% if chart_data %} -{% for item in chart_data %} -
- {{ item.count }} -
- {{ "%02d"|format(item.hour) }} -
-{% endfor %} -{% else %} -
-
// NO_DATA_STREAM
-
-{% endif %} diff --git a/src/infrastructure/reporting/templates/hack/pdf_template.html b/src/infrastructure/reporting/templates/hack/pdf_template.html deleted file mode 100644 index 6dd7ad5..0000000 --- a/src/infrastructure/reporting/templates/hack/pdf_template.html +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - Night Mode Analysis Portal (PDF) - - - - - - - -
-
- - -
- -
- -
-
-
// Report: {{current_date}}
-

> Daily Analysis

-
- stats = {
-   msg: {{message_count}},
-   users: {{participant_count}}
- } -
-
- -
-
$ ls ./topics
-
- {{topics_html | safe}} -
-
- - {% if chat_quality_html %} -
-
$ analyze quality
- {{ chat_quality_html | safe }} -
- {% endif %} -
- - -
-
-
-
-
-
-
-
activity.log
-
-
- {{hourly_chart_html | safe}} -
-
- -
$ cat users
-
- {{titles_html | safe}} -
- -
-
$ grep golden
-
- {{quotes_html | safe}} -
-
- -
-
-
- - - - - - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/retro_futurism/activity_chart_pdf.html b/src/infrastructure/reporting/templates/retro_futurism/activity_chart_pdf.html deleted file mode 100644 index 86c9971..0000000 --- a/src/infrastructure/reporting/templates/retro_futurism/activity_chart_pdf.html +++ /dev/null @@ -1,15 +0,0 @@ -{% if chart_data %} -
- {% for item in chart_data %} -
- {{ item.count }} -
- {{ "%02d"|format(item.hour) }} -
- {% endfor %} -
-{% else %} -
-
NO_DATA_AVAILABLE_FOR_ACTIVITY_SPECTRUM
-
-{% endif %} diff --git a/src/infrastructure/reporting/templates/retro_futurism/pdf_template.html b/src/infrastructure/reporting/templates/retro_futurism/pdf_template.html deleted file mode 100644 index 06700c0..0000000 --- a/src/infrastructure/reporting/templates/retro_futurism/pdf_template.html +++ /dev/null @@ -1,562 +0,0 @@ - - - - - - Retro Futurism Daily Export (PDF) - - - - - - - -
-
-
System Report // {{current_date}}
-

Daily
Analysis
Export

-

- Activity analysis report, generated based on message statistics and AI analysis -

- -
- -
-
- Messages -
{{message_count}}
-
-
- Users -
{{participant_count}}
-
-
- Chars -
{{total_characters}}
-
-
- Emoji -
{{emoji_count}}
-
-
- -
-
- - - -
STABILITY_METRICS.LOG
-
-
PEAK WINDOW SCAN
-
{{most_active_period}}
-
- INTENSITY: {{message_count}} MSGS/DAY - STATUS: OPERATIONAL -
-
- -
- Hourly Broadcast ACTIVITY_SPECTRUM -
-
- {{hourly_chart_html|safe}} -
- - {% if topics_html %} -
- Thread Matrix TOPICS_MODULE -
-
- {{topics_html|safe}} -
- {% endif %} - - {% if titles_html %} -
- Operator Registry TITLES_MODULE -
-
- {{titles_html|safe}} -
- {% endif %} - - {% if quotes_html %} -
- Golden Lines QUOTES_MODULE -
-
- {{quotes_html|safe}} -
- {% endif %} - - {% if chat_quality_html %} -
- Quality Analysis STABILITY_METRICS -
-
-
- {{ chat_quality_html|safe }} -
-
- {% endif %} - - -
- - - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/scrapbook/activity_chart_pdf.html b/src/infrastructure/reporting/templates/scrapbook/activity_chart_pdf.html deleted file mode 100644 index d0c88ee..0000000 --- a/src/infrastructure/reporting/templates/scrapbook/activity_chart_pdf.html +++ /dev/null @@ -1,34 +0,0 @@ -
- {% for item in chart_data %} - {% set bar_bg = 'var(--color-purple)' %} - {% set bar_height = '4px' %} - {% set bar_opacity = '1' %} - - {% if item.count == 0 %} - {% set bar_bg = 'var(--ink-secondary)' %} - {% set bar_height = '4px' %} - {% set bar_opacity = '0.2' %} - {% elif item.percentage >= 70 %} - {% set bar_bg = 'var(--accent-orange)' %} - {% set bar_height = item.percentage ~ '%' %} - {% elif item.percentage >= 30 %} - {% set bar_bg = 'var(--color-green)' %} - {% set bar_height = item.percentage ~ '%' %} - {% else %} - {% set bar_bg = 'var(--color-blue)' %} - {% set bar_height = item.percentage ~ '%' %} - {% endif %} - -
- {% if item.count > 0 %} -
{{ item.count }}
- {% endif %} - - {% set style_str = 'height: ' ~ bar_height ~ '; background: ' ~ bar_bg ~ '; opacity: ' ~ bar_opacity ~ '; - border-bottom: none;' %} -
- -
{{ "%02d" | format(item.hour) }}
-
- {% endfor %} -
\ No newline at end of file diff --git a/src/infrastructure/reporting/templates/scrapbook/pdf_template.html b/src/infrastructure/reporting/templates/scrapbook/pdf_template.html deleted file mode 100644 index 3fc47e0..0000000 --- a/src/infrastructure/reporting/templates/scrapbook/pdf_template.html +++ /dev/null @@ -1,1034 +0,0 @@ - - - - - - - 五彩斑斓的一天,来看看群里发生了什么吧! · {{current_date}} - - - - - - - - - -
- - -
-
-
-

五彩斑斓的一天,来看看群里发生了什么吧!

-
{{current_date}}
-
-
- - -
- -
- -
-
- - - -
{{message_count}}
-
消息总数
-
-
- -
-
- - - -
{{participant_count}}
-
参与人数
-
-
- -
-
- - - -
{{emoji_count}}
-
表情统计
-
-
- -
-
- - - -
{{total_characters}}
-
总字符数
-
-
-
- - -
-
-
- ✨ Highlight Time -
-
{{most_active_period}}
-
(此刻,世界色彩斑斓)
-
-
- -
- - -
-
-
- - - - 24H 活跃轨迹 -
-
- {{hourly_chart_html | safe}} -
-
- - {{topics_html | safe}} - - {{titles_html | safe}} - - {{quotes_html | safe}} - -
-
- - - - - 智睿洞察 // Chat Quality -
- {{ chat_quality_html | safe }} -
-
- -
- - - - -
- - - diff --git a/src/infrastructure/reporting/templates/simple/activity_chart_pdf.html b/src/infrastructure/reporting/templates/simple/activity_chart_pdf.html deleted file mode 100644 index e3ab980..0000000 --- a/src/infrastructure/reporting/templates/simple/activity_chart_pdf.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {% for item in chart_data %} -
- {{ "%02d" | format(item.hour) }}:00 -
-
-
- {{ item.count }} -
- {% endfor %} -
diff --git a/src/infrastructure/reporting/templates/simple/pdf_template.html b/src/infrastructure/reporting/templates/simple/pdf_template.html deleted file mode 100644 index 611e296..0000000 --- a/src/infrastructure/reporting/templates/simple/pdf_template.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - Simple Report - - - - -
-

群聊日报 - {{current_date}}

- -
-

基础统计

-
-

消息数: {{message_count}}

-

参与人数: {{participant_count}}

-

总字符数: {{total_characters}}

-

表情数: {{emoji_count}}

-

最活跃时段: {{most_active_period}}

-
-
- -
-

活跃度图表

-
- {{hourly_chart_html | safe}} -
-
- -
- {{topics_html | safe}} -
- -
- {{titles_html | safe}} -
- -
- {{quotes_html | safe}} -
- - {% if chat_quality_html %} -
-

Chat Quality

- {{ chat_quality_html | safe }} -
- {% endif %} - - -
- - - \ No newline at end of file diff --git a/src/infrastructure/reporting/templates/spring_festival/activity_chart_pdf.html b/src/infrastructure/reporting/templates/spring_festival/activity_chart_pdf.html deleted file mode 100644 index f95b24b..0000000 --- a/src/infrastructure/reporting/templates/spring_festival/activity_chart_pdf.html +++ /dev/null @@ -1,44 +0,0 @@ -{% if chart_data %} -
- {% for item in chart_data %} -
- -
- {{ item.count if item.count > 0 else '' }} -
- - -
- - -
- {{ "%02d"|format(item.hour) }} -
-
- {% endfor %} -
-{% else %} -
- 🏮 尚无活跃记录 🏮 -
-{% endif %} diff --git a/src/infrastructure/reporting/templates/spring_festival/pdf_template.html b/src/infrastructure/reporting/templates/spring_festival/pdf_template.html deleted file mode 100644 index 6fe538b..0000000 --- a/src/infrastructure/reporting/templates/spring_festival/pdf_template.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - - 春节特供日报 (PDF) - - - - - - - -
-
- -
-

🏮 春节特供日报 🏮

-
—— {{current_date}} ——
-
- -
-
-
消息数
-
{{message_count}}
-
-
-
群友数
-
{{participant_count}}
-
-
-
总字数
-
{{total_characters}}
-
-
-
活跃窗口
-
{{most_active_period}}
-
-
- -
-
-
核心话题回顾
- {{topics_html | safe}} -
-
-
活跃趋势
-
- {{hourly_chart_html | safe}} -
- -
群友风云榜
- {{titles_html | safe}} -
-
- - {% if chat_quality_html %} -
智睿点评 // Chat Quality
- {{ chat_quality_html | safe }} - {% endif %} - -
每日金句回响
- {{quotes_html | safe}} - - -
- - - \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 58c2f69..4e06a57 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,8 +1,6 @@ """ 工具函数模块 -包含PDF处理和通用工具函数 +包含通用工具函数 """ -from .pdf_utils import PDFInstaller - -__all__ = ["PDFInstaller"] +__all__ = [] diff --git a/src/utils/pdf_utils.py b/src/utils/pdf_utils.py deleted file mode 100644 index 7103bb9..0000000 --- a/src/utils/pdf_utils.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -PDF工具模块 -负责PDF相关的安装和管理功能 -""" - -import asyncio -import sys -from pathlib import Path -from typing import Any - -from .logger import logger - - -class PDFInstaller: - """ - 工具组件:PDF 渲染引擎 (Playwright) 安装器 - - 该组件负责管理 Playwright 及其对应浏览器内核 (Chromium) 的安装生命周期。 - 由于内核下载耗时较长且受网络波动影响,采用非阻塞的后台任务模式执行。 - """ - - # 静态安装状态追踪 - _install_status: dict[str, Any] = { - "in_progress": False, - "completed": False, - "failed": False, - "error_message": None, - } - - @staticmethod - async def install_playwright( - config_manager: Any, task_registry: set[asyncio.Task] | None = None - ) -> 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(task_registry) - - except Exception as e: - logger.error(f"Playwright 设置过程中出错: {e}") - return f"❌ 安装过程中出错: {str(e)}" - - @staticmethod - async def install_system_deps( - task_registry: set[asyncio.Task] | None = None, - ) -> 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 内核...") - task = asyncio.create_task(PDFInstaller._background_playwright_install()) - if task_registry is not None: - task_registry.add(task) - task.add_done_callback(task_registry.discard) - - 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 getattr(config_manager, "playwright_available", False): - version = getattr(config_manager, "playwright_version", "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`。"