mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user