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"""
-
-
-
参与者: {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 """
-
-
-
-
- 群聊日常分析报告
-
-
-
-
-
-
-
-
-
📈 基础统计
-
-
-
{{ participant_count }}
参与人数
-
{{ total_characters }}
总字符数
-
-
-
-
{{ most_active_period }}
-
最活跃时段
-
-
-
-
-
-
- {{ hourly_chart_html | safe }}
-
-
-
💬 热门话题
-
{{ topics_html | safe }}
-
-
-
🏆 群友称号
-
{{ titles_html | safe }}
-
-
-
💬 群圣经
- {{ quotes_html | safe }}
-
-
-
-
-
-"""
-
- @staticmethod
- def get_pdf_template() -> str:
- """获取PDF报告的HTML模板(使用{}占位符)"""
- return """
-
-
-
-
- 群聊日常分析报告
-
-
-
-
-
-
-
-
📈 基础统计
-
-
-
{message_count}
-
消息总数
-
-
-
{participant_count}
-
参与人数
-
-
-
{total_characters}
-
总字符数
-
-
-
-
-
{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 @@
+
+
+
+
+
+ 群聊日常分析报告
+
+
+
+
+
+
+
+
+
📈 基础统计
+
+
+
{{participant_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/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 @@
+
+
+
+
+
+ 群聊日常分析报告
+
+
+
+
+
+
+
+
📈 基础统计
+
+
+
{{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.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