From c54d001f2a229d988647b7cec423e8994e62f8d9 Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Fri, 16 Jan 2026 23:41:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90=20P?= =?UTF-8?q?DF=20=E7=94=9F=E6=88=90=EF=BC=8C=E5=90=8E=E7=BB=AD=E7=BB=A7?= =?UTF-8?q?=E7=BB=AD=E6=8E=92=E6=9F=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 + scripts/mock_pdf_gen.py | 238 ++++++++++++++ src/reports/generators.py | 26 +- .../scrapbook/activity_chart_pdf.html | 34 ++ .../templates/scrapbook/pdf_template.html | 292 +++++++++++------- .../templates/scrapbook/quote_item.html | 2 +- .../templates/scrapbook/topic_item.html | 8 +- .../templates/scrapbook/user_title_item.html | 10 +- 8 files changed, 491 insertions(+), 127 deletions(-) create mode 100644 scripts/mock_pdf_gen.py create mode 100644 src/reports/templates/scrapbook/activity_chart_pdf.html diff --git a/README.md b/README.md index f6abd7f..a651484 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,14 @@ src/reports/templates/your_theme_name/ #### 5. 模板调试工具 +PDF 调试模板命令 Docker 下: + +``` +docker exec -it astrbot python data/plugins/astrbot_plugin_qq_group_daily_analysis/scripts/mock_pdf_gen.py +``` + +Image 模板调试: + 本项目提供了一个专门用于模板开发的调试工具 `scripts/debug_render.py`,可以在不启动完整 AstrBot 环境的情况下快速预览模板渲染效果。 **使用方法:** diff --git a/scripts/mock_pdf_gen.py b/scripts/mock_pdf_gen.py new file mode 100644 index 0000000..55c7eac --- /dev/null +++ b/scripts/mock_pdf_gen.py @@ -0,0 +1,238 @@ +import asyncio +import os +import sys +import json +from datetime import datetime + +# ========================================== +# 1. Environment Setup (Critical for Imports) +# ========================================== +# Add project root to sys.path so we can import 'astrbot' and plugin modules +current_dir = os.path.dirname(os.path.abspath(__file__)) +# Assuming structure: .../data/plugins/astrbot_plugin_qq_group_daily_analysis/scripts/mock_pdf_gen.py +# We need to go up 4 levels to reach 'AstrBot-master' root which contains the 'astrbot' package +# data/plugins/astrbot_plugin_qq_group_daily_analysis/scripts -> ... -> AstrBot-master +project_root = os.path.abspath(os.path.join(current_dir, "../../../../")) +sys.path.insert(0, project_root) + +print(f"Project Root: {project_root}") + +# Mock logger before importing anything that uses it +from astrbot.api import logger + +logger.info = lambda msg, *args, **kwargs: print(f"[INFO] {msg}") +logger.error = lambda msg, *args, **kwargs: print(f"[ERROR] {msg}") +logger.warning = lambda msg, *args, **kwargs: print(f"[WARN] {msg}") + +# Now import plugin modules +try: + from data.plugins.astrbot_plugin_qq_group_daily_analysis.src.reports.generators import ( + ReportGenerator, + ) + from data.plugins.astrbot_plugin_qq_group_daily_analysis.src.core.config import ( + ConfigManager, + ) +except ImportError as e: + print(f"Import Error: {e}") + sys.exit(1) + + +# ========================================== +# 2. Mocks +# ========================================== +class MockConfig: + def get(self, key, default=None): + return default + + def get_pdf_output_dir(self): + # Output to the scripts directory for easy access + return os.path.join(current_dir, "output") + + def get_pdf_filename_format(self): + return "mock_report_{group_id}_{date}.pdf" + + def get_max_topics(self): + return 5 + + def get_max_user_titles(self): + return 5 + + def get_max_golden_quotes(self): + return 5 + + @property + def pyppeteer_available(self): + return True + + +# ========================================== +# 3. Main Execution +# ========================================== +async def main(): + print("Initializing ReportGenerator...") + config_manager = ConfigManager(MockConfig()) + generator = ReportGenerator(config_manager) + + # Mock Data (Rich data to test layout) + analysis_result = { + "date": datetime.now().strftime("%Y年%m月%d日"), + "statistics": { + "total_messages": 1280, + "active_users": 42, + "emoji_count": 156, + "total_chars": 8500, + "message_count": 1280, + "participant_count": 42, + "total_characters": 8500, + "most_active_period": "20:00-22:00", + "token_usage": type( + "obj", + (object,), + {"total_tokens": 500, "prompt_tokens": 200, "completion_tokens": 300}, + ), + "activity_visualization": type( + "obj", + (object,), + { + "hourly_activity": { + i: (i * 5) % 60 for i in range(24) + }, # Fake activity data + "heatmap_data": [], + }, + ), + }, + "highlight_time": { + "period": "21:00-22:00", + "reason": "夜深人静,群里却热闹非凡,大家都在讨论新的游戏活动。", + }, + "topics": [ + { + "topic": "AstrBot新功能", + "detail": "大家对PDF生成功能的讨论非常热烈,提出了很多优化建议。", + "contributors": ["开发者", "测试员"], + }, + { + "topic": "周末计划", + "detail": "有人提议去爬山,也有人想在家打游戏。", + "contributors": ["旅行家", "宅男"], + }, + { + "topic": "代码调试", + "detail": "关于Python异步编程的深入探讨。", + "contributors": ["小白", "大神"], + }, + { + "topic": "美食分享", + "detail": "深夜放毒,发了很多火锅和烧烤的照片。", + "contributors": ["吃货A", "吃货B"], + }, + { + "topic": "模组推荐", + "detail": "推荐了一些好用的Minecraft模组。", + "contributors": ["MC玩家"], + }, + ], + "user_titles": [ + { + "name": "极客", + "title": "代码魔术师", + "mbti": "INTJ", + "reason": "总是能用一行代码解决复杂问题。", + "qq": "10001", + }, + { + "name": "社牛", + "title": "气氛组组长", + "mbti": "ENFP", + "reason": "群里冷场时总能第一时间活跃气氛。", + "qq": "10002", + }, + { + "name": "百科", + "title": "移动维基", + "mbti": "ISTJ", + "reason": "不管问什么问题,他都知道答案。", + "qq": "10003", + }, + { + "name": "潜水", + "title": "深海幽灵", + "mbti": "INTP", + "reason": "虽然很少说话,但每次发言都直击要害。", + "qq": "10004", + }, + { + "name": "欧皇", + "title": "天选之子", + "mbti": "ESFJ", + "reason": "抽卡次次出金,让人羡慕嫉妒恨。", + "qq": "10005", + }, + ], + } + + analysis_result["statistics"]["golden_quotes"] = [ + { + "sender": "大佬", + "content": "这代码能跑就行,别动它!", + "reason": "至理名言,动了就崩。", + "qq": "20001", + }, + { + "sender": "萌新", + "content": "为什么我的报错和你不一?", + "reason": "经典的灵魂发问。", + "qq": "20002", + }, + { + "sender": "群主", + "content": "再发黄色图全部禁言!", + "reason": "来自管理层的威慑。", + "qq": "888888", + }, + ] + + # Helper wrapper for dot notation access needed by template + class DictWrapper: + def __init__(self, data): + self._data = data + for k, v in data.items(): + if isinstance(v, list): + setattr( + self, + k, + [DictWrapper(i) if isinstance(i, dict) else i for i in v], + ) + elif isinstance(v, dict): + setattr(self, k, DictWrapper(v)) + else: + setattr(self, k, v) + + def __getitem__(self, key): + return self._data[key] + + def get(self, key, default=None): + return self._data.get(key, default) + + # Wrap sections that need dot access + analysis_result["statistics"] = DictWrapper(analysis_result["statistics"]) + analysis_result["topics"] = [DictWrapper(t) for t in analysis_result["topics"]] + analysis_result["user_titles"] = [ + DictWrapper(t) for t in analysis_result["user_titles"] + ] + + print("Generating PDF Report...") + group_id = "test_group_mock" + + # Direct generation + pdf_path = await generator.generate_pdf_report(analysis_result, group_id=group_id) + + if pdf_path: + print(f"\n[SUCCESS] PDF Generated Successfully: {pdf_path}") + print(f"File Size: {os.path.getsize(pdf_path) / 1024:.2f} KB") + else: + print("\n[FAILURE] PDF Generation Failed.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/reports/generators.py b/src/reports/generators.py index 2952f37..8fd7531 100644 --- a/src/reports/generators.py +++ b/src/reports/generators.py @@ -30,7 +30,9 @@ class ReportGenerator: """生成图片格式的分析报告""" try: # 准备渲染数据 - render_payload = await self._prepare_render_data(analysis_result) + render_payload = await self._prepare_render_data( + analysis_result, chart_template="activity_chart.html" + ) # 先渲染HTML模板(使用异步方法) image_template = await self.html_templates.get_image_template_async() @@ -99,7 +101,9 @@ class ReportGenerator: pdf_path = output_dir / filename # 准备渲染数据 - render_data = await self._prepare_render_data(analysis_result) + render_data = await self._prepare_render_data( + analysis_result, chart_template="activity_chart_pdf.html" + ) logger.info(f"PDF 渲染数据准备完成,包含 {len(render_data)} 个字段") # 生成 HTML 内容(使用异步方法) @@ -166,7 +170,9 @@ class ReportGenerator: return report - async def _prepare_render_data(self, analysis_result: dict) -> dict: + async def _prepare_render_data( + self, analysis_result: dict, chart_template: str = "activity_chart.html" + ) -> dict: """准备渲染数据""" stats = analysis_result["statistics"] topics = analysis_result["topics"] @@ -236,7 +242,7 @@ class ReportGenerator: activity_viz.hourly_activity ) hourly_chart_html = self.html_templates.render_template( - "activity_chart.html", chart_data=chart_data + chart_template, chart_data=chart_data ) logger.info(f"活跃度图表HTML生成完成,长度: {len(hourly_chart_html)}") @@ -294,7 +300,7 @@ class ReportGenerator: async def _get_user_avatar(self, user_id: str) -> str | None: """获取用户头像的base64编码""" try: - avatar_url = f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=640" + avatar_url = f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=100" async with aiohttp.ClientSession() as client: response = await client.get(avatar_url) response.raise_for_status() @@ -350,7 +356,7 @@ class ReportGenerator: "--enable-automation", "--password-store=basic", "--use-mock-keychain", - "--export-tagged-pdf", + # "--export-tagged-pdf", # Removed to reduce size "--disable-web-security", "--disable-features=VizDisplayCompositor", "--disable-blink-features=AutomationControlled", # 隐藏自动化特征 @@ -435,8 +441,8 @@ class ReportGenerator: # 设置页面视口,减少内存占用 await page.setViewport( { - "width": 1024, - "height": 768, + "width": 800, # Match A4 width approx (794px at 96PPI) + "height": 1000, "deviceScaleFactor": 1, "isMobile": False, "hasTouch": False, @@ -457,7 +463,7 @@ class ReportGenerator: # 确保CDN字体等资源加载完成 logger.info("等待资源加载(10s)...") - await asyncio.sleep(10) + await asyncio.sleep(5) # 导出 PDF,使用更保守的设置 logger.info("开始生成PDF...") @@ -471,7 +477,7 @@ class ReportGenerator: "bottom": "10mm", "left": "10mm", }, - "scale": 0.8, + "scale": 1.0, "displayHeaderFooter": False, "preferCSSPageSize": True, "timeout": 60000, # 增加PDF生成超时时间到60秒 diff --git a/src/reports/templates/scrapbook/activity_chart_pdf.html b/src/reports/templates/scrapbook/activity_chart_pdf.html new file mode 100644 index 0000000..d0c88ee --- /dev/null +++ b/src/reports/templates/scrapbook/activity_chart_pdf.html @@ -0,0 +1,34 @@ +
+ {% 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/reports/templates/scrapbook/pdf_template.html b/src/reports/templates/scrapbook/pdf_template.html index aecbf02..6701e5f 100644 --- a/src/reports/templates/scrapbook/pdf_template.html +++ b/src/reports/templates/scrapbook/pdf_template.html @@ -7,10 +7,9 @@ 五彩斑斓的一天,来看看群里发生了什么吧! · {{current_date}} - - + + + @@ -975,10 +1049,8 @@ {% for quote in quotes %} -
+
{% if quote.avatar_url %} diff --git a/src/reports/templates/scrapbook/topic_item.html b/src/reports/templates/scrapbook/topic_item.html index cc71f47..8dc606e 100644 --- a/src/reports/templates/scrapbook/topic_item.html +++ b/src/reports/templates/scrapbook/topic_item.html @@ -12,16 +12,18 @@ 今日话题 Topics
{% for topic in topics %} -
+
{{ topic.topic.topic }} - #{{ "%02d"|format(topic.index) }} + #{{ + "%02d"|format(topic.index) }}
-
+
🙋‍♀️ 参与者: {{ topic.contributors }}
{{ topic.topic.detail }}
diff --git a/src/reports/templates/scrapbook/user_title_item.html b/src/reports/templates/scrapbook/user_title_item.html index a63a090..d196797 100644 --- a/src/reports/templates/scrapbook/user_title_item.html +++ b/src/reports/templates/scrapbook/user_title_item.html @@ -2,20 +2,24 @@
- + 群友画像 Portraits
{% for title in titles %} -
+
{% if title.avatar_data %} 头像 {% else %} - + + + {% endif %}