fix(ReportGenerator): mark t2i mention capsules as safe html (#144)

* fix: mark t2i mention capsules as safe html

* fix: 优化PDF和HTML输出目录的获取逻辑

* fix(ReportGenerator): 确保返回正确的 Markup 类型

---------

Co-authored-by: SXP-Simon <sxp20061207@163.com>
This commit is contained in:
clown145
2026-03-31 20:44:25 +08:00
committed by GitHub
co-authored by SXP-Simon
parent 3662ac3d75
commit 98df26b88e
2 changed files with 50 additions and 54 deletions
+14 -8
View File
@@ -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:
+36 -46
View File
@@ -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'<span class="user-capsule" style="{capsule_style}">'
f'<img src="{url}" style="{img_style}">'
f'<span style="{name_style}">{name}</span>'
f"</span>"
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'<span class="user-capsule" style="{capsule_style}">'
f'<img src="{html.escape(final_url, quote=True)}" style="{img_style}">'
f'<span style="{name_style}">{html.escape(final_name)}</span>'
"</span>"
)
# 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", "<br>"))
@staticmethod
def _is_placeholder_display_name(name: str | None, user_id: str) -> bool: