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: