mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix: 初步完成 PDF 生成,后续继续排查优化
This commit is contained in:
@@ -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 环境的情况下快速预览模板渲染效果。
|
||||
|
||||
**使用方法:**
|
||||
|
||||
@@ -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())
|
||||
+16
-10
@@ -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秒
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="chart-section-horizontal">
|
||||
{% 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 %}
|
||||
|
||||
<div class="chart-column">
|
||||
{% if item.count > 0 %}
|
||||
<div class="chart-value-top">{{ item.count }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% set style_str = 'height: ' ~ bar_height ~ '; background: ' ~ bar_bg ~ '; opacity: ' ~ bar_opacity ~ ';
|
||||
border-bottom: none;' %}
|
||||
<div class="chart-bar-vertical" style="{{ style_str }}"></div>
|
||||
|
||||
<div class="chart-label-xaxis">{{ "%02d" | format(item.hour) }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
群贤毕至 Bible Quotes
|
||||
</div>
|
||||
{% for quote in quotes %}
|
||||
<div class="quote-wrapper">
|
||||
<div class="quote-wrapper avoid-break">
|
||||
<div class="q-flex-container">
|
||||
<div class="q-user-col">
|
||||
{% if quote.avatar_url %}
|
||||
|
||||
@@ -12,16 +12,18 @@
|
||||
今日话题 Topics
|
||||
</div>
|
||||
{% for topic in topics %}
|
||||
<div class="topic-item">
|
||||
<div class="topic-item avoid-break">
|
||||
<div class="check-box">
|
||||
<div class="check-tick"></div>
|
||||
</div>
|
||||
<div class="topic-content">
|
||||
<div style="display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px;">
|
||||
<span class="topic-title">{{ topic.topic.topic }}</span>
|
||||
<span style="font-size: 0.85em; color: #999; font-family: var(--font-body);">#{{ "%02d"|format(topic.index) }}</span>
|
||||
<span style="font-size: 0.85em; color: #999; font-family: var(--font-body);">#{{
|
||||
"%02d"|format(topic.index) }}</span>
|
||||
</div>
|
||||
<div style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;">
|
||||
<div
|
||||
style="font-family: var(--font-hand); font-size: 1.05em; color: var(--ink-secondary); margin-bottom: 8px;">
|
||||
🙋♀️ 参与者: {{ topic.contributors }}
|
||||
</div>
|
||||
<div class="topic-detail">{{ topic.topic.detail }}</div>
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
<div class="user-section">
|
||||
<div class="section-title" style="justify-content: center;">
|
||||
<svg class="doodle" viewBox="0 0 24 24">
|
||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
</svg>
|
||||
群友画像 Portraits
|
||||
</div>
|
||||
<div class="masonry-grid">
|
||||
{% for title in titles %}
|
||||
<div class="user-card">
|
||||
<div class="user-card avoid-break">
|
||||
<div class="card-tape"></div>
|
||||
<div class="user-header">
|
||||
<div class="u-avatar">
|
||||
{% if title.avatar_data %}
|
||||
<img src="{{ title.avatar_data }}" alt="头像" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
{% else %}
|
||||
<svg class="doodle" style="font-size: 2rem;" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
<svg class="doodle" style="font-size: 2rem;" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="u-info">
|
||||
|
||||
Reference in New Issue
Block a user