diff --git a/src/reports/generators.py b/src/reports/generators.py index b1db42a..d14ee06 100644 --- a/src/reports/generators.py +++ b/src/reports/generators.py @@ -19,6 +19,7 @@ class ReportGenerator: def __init__(self, config_manager): self.config_manager = config_manager self.activity_visualizer = ActivityVisualizer() + self.html_templates = HTMLTemplates() # 实例化HTML模板管理器 async def generate_image_report( self, analysis_result: dict, group_id: str, html_render_func @@ -27,7 +28,20 @@ class ReportGenerator: try: # 准备渲染数据 render_payload = await self._prepare_render_data(analysis_result) - # 使用AstrBot内置的HTML渲染服务(直接传递模板和数据) + + # 先渲染HTML模板 + html_content = self._render_html_template( + self.html_templates.get_image_template(), render_payload + ) + + # 检查HTML内容是否有效 + if not html_content: + logger.error("图片报告HTML渲染失败:返回空内容") + return None + + logger.info(f"图片报告HTML渲染完成,长度: {len(html_content)} 字符") + + # 使用AstrBot内置的HTML渲染服务(传递渲染后的HTML) # 使用兼容的图片生成选项(基于NetworkRenderStrategy的默认设置) image_options = { "full_page": True, @@ -35,8 +49,8 @@ class ReportGenerator: "quality": 95, # 设置合理的质量 } image_url = await html_render_func( - HTMLTemplates.get_image_template(), - render_payload, + html_content, # 渲染后的HTML内容 + {}, # 空数据字典,因为数据已包含在HTML中 True, # return_url=True,返回URL而不是下载文件 image_options, ) @@ -55,8 +69,8 @@ class ReportGenerator: "quality": 70, # 降低质量以提高兼容性 } image_url = await html_render_func( - HTMLTemplates.get_image_template(), - render_payload, + html_content, # 使用已渲染的HTML + {}, # 空数据字典 True, simple_options, ) @@ -86,10 +100,16 @@ class ReportGenerator: render_data = await self._prepare_render_data(analysis_result) logger.info(f"PDF 渲染数据准备完成,包含 {len(render_data)} 个字段") - # 生成 HTML 内容(PDF模板使用{}占位符) + # 生成 HTML 内容(PDF模板使用{{}}占位符) html_content = self._render_html_template( - HTMLTemplates.get_pdf_template(), render_data, use_jinja_style=False + self.html_templates.get_pdf_template(), render_data ) + + # 检查HTML内容是否有效 + if not html_content: + logger.error("PDF报告HTML渲染失败:返回空内容") + return None + logger.info(f"HTML 内容生成完成,长度: {len(html_content)} 字符") # 转换为 PDF @@ -152,69 +172,68 @@ class ReportGenerator: user_titles = analysis_result["user_titles"] activity_viz = stats.activity_visualization - # 构建话题HTML - topics_html = "" + # 使用Jinja2模板构建话题HTML(批量渲染) max_topics = self.config_manager.get_max_topics() + topics_list = [] for i, topic in enumerate(topics[:max_topics], 1): - contributors_str = "、".join(topic.contributors) - topics_html += f""" -
-
- {i} - {topic.topic} -
-
参与者: {contributors_str}
-
{topic.detail}
-
- """ + topics_list.append( + { + "index": i, + "topic": topic, + "contributors": "、".join(topic.contributors), + } + ) - # 构建用户称号HTML(包含头像) - titles_html = "" + topics_html = self.html_templates.render_template( + "topic_item.html", topics=topics_list + ) + logger.info(f"话题HTML生成完成,长度: {len(topics_html)}") + + # 使用Jinja2模板构建用户称号HTML(批量渲染,包含头像) max_user_titles = self.config_manager.get_max_user_titles() + titles_list = [] for title in user_titles[:max_user_titles]: # 获取用户头像 avatar_data = await self._get_user_avatar(str(title.qq)) - avatar_html = ( - f'头像' - if avatar_data - else '
👤
' + title_data = { + "name": title.name, + "title": title.title, + "mbti": title.mbti, + "reason": title.reason, + "avatar_data": avatar_data, + } + titles_list.append(title_data) + + titles_html = self.html_templates.render_template( + "user_title_item.html", titles=titles_list + ) + logger.info(f"用户称号HTML生成完成,长度: {len(titles_html)}") + + # 使用Jinja2模板构建金句HTML(批量渲染) + max_golden_quotes = self.config_manager.get_max_golden_quotes() + quotes_list = [] + for quote in stats.golden_quotes[:max_golden_quotes]: + quotes_list.append( + { + "content": quote.content, + "sender": quote.sender, + "reason": quote.reason, + } ) - titles_html += f""" -
-
- {avatar_html} -
-
{title.name}
-
-
{title.title}
-
{title.mbti}
-
-
-
-
{title.reason}
-
- """ - - # 构建金句HTML - quotes_html = "" - max_golden_quotes = self.config_manager.get_max_golden_quotes() - for quote in stats.golden_quotes[:max_golden_quotes]: - quotes_html += f""" -
-
"{quote.content}"
-
—— {quote.sender}
-
{quote.reason}
-
- """ + quotes_html = self.html_templates.render_template( + "quote_item.html", quotes=quotes_list + ) + logger.info(f"金句HTML生成完成,长度: {len(quotes_html)}") # 生成活跃度可视化HTML hourly_chart_html = self.activity_visualizer.generate_hourly_chart_html( activity_viz.hourly_activity ) + logger.info(f"活跃度图表HTML生成完成,长度: {len(hourly_chart_html)}") - # 返回扁平化的渲染数据 - return { + # 准备最终渲染数据 + render_data = { "current_date": datetime.now().strftime("%Y年%m月%d日"), "current_datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "message_count": stats.message_count, @@ -237,46 +256,30 @@ class ReportGenerator: else 0, } - def _render_html_template( - self, template: str, data: dict, use_jinja_style: bool = False - ) -> str: - """HTML模板渲染,支持两种占位符格式 + logger.info(f"渲染数据准备完成,包含 {len(render_data)} 个字段") + return render_data + + def _render_html_template(self, template: str, data: dict) -> str: + """HTML模板渲染,使用 {{key}} 占位符格式 Args: template: HTML模板字符串 - data: 渲染数据 - use_jinja_style: 是否使用Jinja2风格的{{ }}占位符,否则使用{}占位符 + data: 渲染数据字典 """ result = template - # 调试:记录渲染数据 - logger.info( - f"渲染数据键: {list(data.keys())}, 使用Jinja风格: {use_jinja_style}" - ) - for key, value in data.items(): - if use_jinja_style: - # 图片模板使用{{ }}占位符 - placeholder = f"{{{{ {key} }}}}" - else: - # PDF模板使用{}占位符 - placeholder = f"{{{key}}}" - - # 调试:记录替换过程 - if placeholder in result: - logger.debug(f"替换 {placeholder} -> {str(value)[:100]}...") + # 统一使用双大括号格式 {{key}} + placeholder = "{{" + key + "}}" result = result.replace(placeholder, str(value)) # 检查是否还有未替换的占位符 import re - if use_jinja_style: - remaining_placeholders = re.findall(r"\{\{[^}]+\}\}", result) - else: - remaining_placeholders = re.findall(r"\{[^}]+\}", result) - - if remaining_placeholders: - logger.warning(f"未替换的占位符: {remaining_placeholders[:10]}") + if remaining_placeholders := re.findall(r"\{\{[^}]+\}\}", result): + logger.warning( + f"未替换的占位符 ({len(remaining_placeholders)}个): {remaining_placeholders[:10]}" + ) return result diff --git a/src/reports/templates.py b/src/reports/templates.py index 4007ab3..f679226 100644 --- a/src/reports/templates.py +++ b/src/reports/templates.py @@ -1,1044 +1,68 @@ """ HTML模板模块 -严格按照main-backup中的实现,包含图片报告和PDF报告的不同HTML模板 +使用Jinja2加载外部HTML模板文件 """ +import os +from astrbot.api import logger +from jinja2 import Environment, FileSystemLoader, select_autoescape + class HTMLTemplates: """HTML模板管理类""" - @staticmethod - def get_image_template() -> str: - """获取图片报告的HTML模板(使用{{ }}占位符)""" - return """ - - - - - 群聊日常分析报告 - - - - -
-
-

📊 群聊日常分析报告

-
{{ 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 }} -
-
- -
- -""" - - @staticmethod - def get_pdf_template() -> str: - """获取PDF报告的HTML模板(使用{}占位符)""" - return """ - - - - - 群聊日常分析报告 - - - - -
-
-

📊 群聊日常分析报告

-
{current_date}
-
-
-

📈 基础统计

-
-
-
{message_count}
-
消息总数
-
-
-
{participant_count}
-
参与人数
-
-
-
{total_characters}
-
总字符数
-
-
-
{emoji_count}
-
表情数量
-
-
-
-
{most_active_period}
-
最活跃时段
-
- -
-
-
⏱️ 活跃度分布
-
- {hourly_chart_html} -
-
- -
-

💬 热门话题

- {topics_html} -
-
-

🏆 群友称号

- {titles_html} -
-
-

💬 群圣经

- {quotes_html} -
- -
- - -""" + def __init__(self): + """初始化Jinja2环境""" + # 设置模板目录 + template_dir = os.path.join(os.path.dirname(__file__), "templates") + + # 创建Jinja2环境 + self.jinja_env = Environment( + loader=FileSystemLoader(template_dir), + autoescape=select_autoescape(["html", "xml"]), + trim_blocks=True, + lstrip_blocks=True, + ) + + def get_image_template(self) -> str: + """获取图片报告的HTML模板(返回原始模板字符串)""" + try: + # 获取模板对象 + template = self.jinja_env.get_template("image_template.html") + # 读取原始模板文件内容,而不是渲染它 + with open(template.filename, encoding="utf-8") as f: + return f.read() + except Exception: + # 如果加载失败,返回空字符串让调用者处理 + logger.error("加载图片模板失败") + return "" + + def get_pdf_template(self) -> str: + """获取PDF报告的HTML模板(返回原始模板字符串)""" + try: + # 获取模板对象 + template = self.jinja_env.get_template("pdf_template.html") + # 读取原始模板文件内容,而不是渲染它 + with open(template.filename, encoding="utf-8") as f: + return f.read() + except Exception: + # 如果加载失败,返回空字符串让调用者处理 + logger.error("加载PDF模板失败") + return "" + + def render_template(self, template_name: str, **kwargs) -> str: + """渲染指定的模板文件 + + Args: + template_name: 模板文件名 + **kwargs: 传递给模板的变量 + + Returns: + 渲染后的HTML字符串 + """ + try: + template = self.jinja_env.get_template(template_name) + return template.render(**kwargs) + except Exception: + logger.error(f"渲染模板 {template_name} 失败") + return "" diff --git a/src/reports/templates/image_template.html b/src/reports/templates/image_template.html new file mode 100644 index 0000000..cffe817 --- /dev/null +++ b/src/reports/templates/image_template.html @@ -0,0 +1,584 @@ + + + + + + 群聊日常分析报告 + + + + +
+
+

📊 群聊日常分析报告

+
{{current_date}}
+
+
+
+

📈 基础统计

+
+
{{message_count}}
消息总数
+
{{participant_count}}
参与人数
+
{{total_characters}}
总字符数
+
{{emoji_count}}
表情数量
+
+
+
{{most_active_period}}
+
最活跃时段
+
+
+ + +
+
+
+
⏱️ 24小时活跃度分布
+
+
+ {{hourly_chart_html}} +
+
+

💬 热门话题

+
{{topics_html}}
+
+
+

🏆 群友称号

+
{{titles_html}}
+
+
+

💬 群圣经

+ {{quotes_html}} +
+
+ +
+ + \ No newline at end of file diff --git a/src/reports/templates/pdf_template.html b/src/reports/templates/pdf_template.html new file mode 100644 index 0000000..23ebbbf --- /dev/null +++ b/src/reports/templates/pdf_template.html @@ -0,0 +1,444 @@ + + + + + + 群聊日常分析报告 + + + + +
+
+

📊 群聊日常分析报告

+
{{current_date}}
+
+
+

📈 基础统计

+
+
+
{{message_count}}
+
消息总数
+
+
+
{{participant_count}}
+
参与人数
+
+
+
{{total_characters}}
+
总字符数
+
+
+
{{emoji_count}}
+
表情数量
+
+
+
+
{{most_active_period}}
+
最活跃时段
+
+ +
+
+
⏱️ 活跃度分布
+
+ {{hourly_chart_html}} +
+
+ +
+

💬 热门话题

+ {{topics_html}} +
+
+

🏆 群友称号

+ {{titles_html}} +
+
+

💬 群圣经

+ {{quotes_html}} +
+ +
+ + + \ No newline at end of file diff --git a/src/reports/templates/quote_item.html b/src/reports/templates/quote_item.html new file mode 100644 index 0000000..72bb43a --- /dev/null +++ b/src/reports/templates/quote_item.html @@ -0,0 +1,7 @@ +{% for quote in quotes %} +
+
"{{ quote.content }}"
+
—— {{ quote.sender }}
+
{{ quote.reason }}
+
+{% endfor %} \ No newline at end of file diff --git a/src/reports/templates/topic_item.html b/src/reports/templates/topic_item.html new file mode 100644 index 0000000..d8ff7ad --- /dev/null +++ b/src/reports/templates/topic_item.html @@ -0,0 +1,10 @@ +{% for topic in topics %} +
+
+ {{ topic.index }} + {{ topic.topic.topic }} +
+
参与者: {{ topic.contributors }}
+
{{ topic.topic.detail }}
+
+{% endfor %} \ No newline at end of file diff --git a/src/reports/templates/user_title_item.html b/src/reports/templates/user_title_item.html new file mode 100644 index 0000000..0a3a4fe --- /dev/null +++ b/src/reports/templates/user_title_item.html @@ -0,0 +1,19 @@ +{% for title in titles %} +
+
+ {% if title.avatar_data %} + 头像 + {% else %} +
👤
+ {% endif %} +
+
{{ title.name }}
+
+
{{ title.title }}
+
{{ title.mbti }}
+
+
+
+
{{ title.reason }}
+
+{% endfor %} \ No newline at end of file