diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py
index b3bff7f..583cdb2 100644
--- a/src/infrastructure/config/config_manager.py
+++ b/src/infrastructure/config/config_manager.py
@@ -226,11 +226,14 @@ class ConfigManager:
"""获取PDF输出目录"""
try:
default_path = StarTools.get_data_dir() / "reports"
- return self._get_group("pdf").get("pdf_output_dir", str(default_path))
+ val = self._get_group("pdf").get("pdf_output_dir")
+ return val if val else str(default_path)
except Exception:
- return self._get_group("pdf").get(
- "pdf_output_dir",
- "data/plugins/astrbot_plugin_qq_group_daily_analysis/reports",
+ val = self._get_group("pdf").get("pdf_output_dir")
+ return (
+ val
+ if val
+ else "data/plugins/astrbot_plugin_qq_group_daily_analysis/reports"
)
def get_bot_self_ids(self) -> list:
@@ -251,11 +254,14 @@ class ConfigManager:
"""获取HTML输出目录"""
try:
default_path = StarTools.get_data_dir() / "self_hosted_html_reports"
- return self._get_group("html").get("html_output_dir", str(default_path))
+ val = self._get_group("html").get("html_output_dir")
+ return val if val else str(default_path)
except Exception:
- return self._get_group("html").get(
- "html_output_dir",
- "data/plugins/astrbot_plugin_qq_group_daily_analysis/self_hosted_html_reports",
+ val = self._get_group("html").get("html_output_dir")
+ return (
+ val
+ if val
+ else "data/plugins/astrbot_plugin_qq_group_daily_analysis/self_hosted_html_reports"
)
def get_html_base_url(self) -> str:
diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py
index cb44fbb..ae85696 100644
--- a/src/infrastructure/reporting/generators.py
+++ b/src/infrastructure/reporting/generators.py
@@ -5,6 +5,7 @@
import asyncio
import base64
+import html
import os
import re
from dataclasses import asdict, is_dataclass
@@ -14,6 +15,7 @@ from pathlib import Path
import aiohttp
from diskcache import Cache
+from markupsafe import Markup
from ...domain.repositories.report_repository import IReportGenerator
from ...utils.logger import logger
@@ -215,14 +217,8 @@ class ReportGenerator(IReportGenerator):
) -> str | None:
"""生成PDF格式的分析报告"""
try:
- # 获取输出目录。如果未配置,则由 data_dir 推理得出。
- output_dir = self.config_manager.get_pdf_output_dir()
- if not output_dir:
- output_dir = self.data_dir / "reports"
- else:
- output_dir = Path(output_dir)
-
- # 确保输出目录存在 (使用 asyncio.to_thread 避免阻塞)
+ # 确保输出目录存在(使用 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)
# 生成文件名
@@ -287,14 +283,8 @@ class ReportGenerator(IReportGenerator):
try:
import json
- # 获取输出目录。如果未配置,则由 data_dir 推理得出。
- output_dir = self.config_manager.get_html_output_dir()
- if not output_dir:
- output_dir = self.data_dir / "self_hosted_html_reports"
- else:
- output_dir = Path(output_dir)
-
- # 确保输出目录存在 (使用 asyncio.to_thread 避免阻塞)
+ # 确保输出目录存在(使用 asyncio.to_thread 避免阻塞)
+ output_dir = Path(self.config_manager.get_html_output_dir())
await asyncio.to_thread(output_dir.mkdir, parents=True, exist_ok=True)
# 生成文件名
@@ -589,18 +579,19 @@ class ReportGenerator(IReportGenerator):
avatar_url_getter,
nickname_getter=None,
user_analysis: dict | None = None,
- ) -> str:
+ ) -> Markup:
"""
处理文本,将 [123456] 格式的用户引用替换为头像+名称的胶囊样式
"""
- import re
-
pattern = r"\[(\d+)\]"
- matches = re.findall(pattern, text)
- if not matches:
- return text
+ if not text:
+ return Markup("")
- async def replacer(match):
+ matches = list(re.finditer(pattern, text))
+ if not matches:
+ return self._escape_text_segment(text)
+
+ async def render_capsule(match: re.Match[str]) -> Markup:
uid = match.group(1)
url = await self._get_user_avatar(
uid, avatar_url_getter
@@ -633,34 +624,33 @@ class ReportGenerator(IReportGenerator):
name_style = "font-size:0.85em;color:inherit;font-weight:500;line-height:1;"
# 3. 最终后备: 确保有头像和名称
- if not url:
- url = self._get_default_avatar_base64()
- if self._is_placeholder_display_name(name, uid):
- name = str(uid)
-
- return (
- f''
- f'
'
- f'{name}'
- f""
+ final_url = url if url else self._get_default_avatar_base64()
+ final_name = (
+ name
+ if (name and not self._is_placeholder_display_name(name, uid))
+ else str(uid)
)
- # re.sub 不支持异步回调,需要先提取所有 ID 进行处理,或者使用自定义的替换逻辑
- # 这里为了保持异步特性,我们需要手动处理
+ return Markup(
+ f''
+ f'
'
+ f'{html.escape(final_name)}'
+ ""
+ )
- # 1. 找出所有匹配项
- matches = list(re.finditer(pattern, text))
- if not matches:
- return text
+ result: list[Markup | str] = []
+ last_end = 0
+ for match in matches:
+ result.append(self._escape_text_segment(text[last_end : match.start()]))
+ result.append(await render_capsule(match))
+ last_end = match.end()
- # 2. 从后往前替换,保持索引正确
- result = text
- for match in reversed(matches):
- replacement = await replacer(match)
- start, end = match.span()
- result = result[:start] + replacement + result[end:]
+ result.append(self._escape_text_segment(text[last_end:]))
+ return Markup("").join(result)
- return result
+ @staticmethod
+ def _escape_text_segment(text: str) -> Markup:
+ return Markup(html.escape(text, quote=False).replace("\n", "
"))
@staticmethod
def _is_placeholder_display_name(name: str | None, user_id: str) -> bool: