diff --git a/.gitignore b/.gitignore index c6416b9..9268170 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ scripts/output/mock_report_test_group_mock_*.pdf astrbot-lark-group-daily-analysis-main/** .ace-tool/ debug_atri.html +data/test/avatar/cache.db +test_mainland.html +test_overseas.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 564b47a..612e88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 (CHANGELOG) +## [v4.10.2] - ✨ 支持多地区渲染环境与自定义镜像 +* **🌍 多地区环境支持**: 新增 `t2i_font_source` 配置项,支持 `Mainland` (大陆) 和 `Overseas` (海外) 模式。自动切换 HTML 语言标识 (`zh-CN`/`zh-Hant`) 并动态调整 CSS 字体栈优先级,解决繁体中文支持与字形偏移问题。 +* **⚙️ 自定义镜像地址**: 移除了所有硬编码的字体资源站地址。现在用户可以为国内和国外环境分别自定义 Google Fonts、Gstatic 字体镜像站。 +* **📱 平台与主题优化**: + * **Telegram**: 修复了 Telegram 平台的头像处理逻辑 (#176 @lekoOwO)。 +* **🛠️ 基础设施与调试**: 重构模板实现完全的数据驱动渲染;更新 `debug_render.py` 并新增 `test_regional_rendering.py` 验证脚本。 + +--- + ## [v4.10.1] - ✨ 优化 T2I 渲染与自定义配置 * **✨ 自定义配置支持**: 支持配置两轮 T2I 渲染策略,允许用户自定义图片格式(PNG/JPEG)、质量、分辨率及超时时间(#164),支持复杂渲染场景的超时参数配置(#174 感谢 @uuutt2023)。 * **🛠️ 健壮性增强**: 实现 T2I 返回 HTML 错误页的自动识别与摘要提取(如 502 Bad Gateway),提升故障排查效率。(参考 #171 说明) diff --git a/_conf_schema.json b/_conf_schema.json index e3876f9..b734b48 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -215,6 +215,37 @@ "step": 5000 }, "hint": "回退尝试通常针对复杂页面,建议设置更长的超时时间(如 100000ms+)。" + }, + "t2i_font_source": { + "type": "string", + "description": "t2i 渲染环境切换", + "options": ["Mainland", "Overseas"], + "default": "Overseas", + "hint": "切换环境会自动应用下方对应的镜像站地址、语言标识(zh-CN/zh-Hant)及字体优先级。" + }, + "t2i_mainland_google_fonts": { + "type": "string", + "description": "[国内] Google Fonts 镜像", + "default": "https://fonts.loli.net", + "hint": "国内环境下推荐使用 loli.net 或其他可访问镜像。" + }, + "t2i_mainland_gstatic": { + "type": "string", + "description": "[国内] Gstatic 镜像", + "default": "https://gstatic.loli.net", + "hint": "国内环境下推荐使用 loli.net 或其他可访问镜像。" + }, + "t2i_overseas_google_fonts": { + "type": "string", + "description": "[国外] Google Fonts 官方", + "default": "https://fonts.googleapis.com", + "hint": "海外环境通常直接使用官方地址即可。" + }, + "t2i_overseas_gstatic": { + "type": "string", + "description": "[国外] Gstatic 官方", + "default": "https://fonts.gstatic.com", + "hint": "海外环境通常直接使用官方地址即可。" } } }, diff --git a/scripts/debug_render.py b/scripts/debug_render.py index c7ed58d..b7f449f 100644 --- a/scripts/debug_render.py +++ b/scripts/debug_render.py @@ -125,6 +125,21 @@ class MockConfigManager: def get_html_base_url(self) -> str: return "" + def get_t2i_font_source(self) -> str: + return "Overseas" + + def get_t2i_google_fonts_mirror(self) -> str: + return "https://fonts.googleapis.com" + + def get_t2i_gstatic_mirror(self) -> str: + return "https://fonts.gstatic.com" + + def get_t2i_atri_font_mirror(self) -> str: + return "https://tc.ciallo.ccwu.cc" + + def get_t2i_rendering_strategies(self) -> list: + return [] + async def mock_get_user_avatar(user_id: str) -> str: # Return a known avatar URL for testing diff --git a/scripts/test_regional_rendering.py b/scripts/test_regional_rendering.py new file mode 100644 index 0000000..607673a --- /dev/null +++ b/scripts/test_regional_rendering.py @@ -0,0 +1,219 @@ +import asyncio +import os +import sys +import types +from pathlib import Path + +# ========================================== +# 1. Environment Setup +# ========================================== +current_dir = os.path.dirname(os.path.abspath(__file__)) +plugin_root = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.insert(0, plugin_root) + +# Mock astrbot.api +astrbot_api = types.ModuleType("astrbot.api") + + +class MockLogger: + def info(self, msg, *args, **kwargs): + print(f"[INFO] {msg}") + + def error(self, msg, *args, **kwargs): + print(f"[ERROR] {msg}") + + def warning(self, msg, *args, **kwargs): + print(f"[WARN] {msg}") + + def debug(self, msg, *args, **kwargs): + pass + + def log(self, level, msg, *args, **kwargs): + pass + + def isEnabledFor(self, level): + return True + + +astrbot_api.logger = MockLogger() +astrbot_api.AstrBotConfig = dict +sys.modules["astrbot.api"] = astrbot_api + +# Mock astrbot.core.utils.astrbot_path +astrbot_core_utils = types.ModuleType("astrbot.core.utils") +astrbot_path = types.ModuleType("astrbot.core.utils.astrbot_path") +astrbot_path.get_astrbot_data_path = lambda: Path(".") +sys.modules["astrbot.core.utils"] = astrbot_core_utils +sys.modules["astrbot.core.utils.astrbot_path"] = astrbot_path + +from src.domain.models.data_models import ( # noqa: E402 + ActivityVisualization, + EmojiStatistics, + GroupStatistics, + QualityReview, + TokenUsage, +) +from src.infrastructure.reporting.generators import ReportGenerator # noqa: E402 + + +class MockConfigManager: + def __init__(self, source: str) -> None: + self.source = source + + def get_report_template(self) -> str: + return "simple" + + def get_max_topics(self) -> int: + return 5 + + def get_max_user_titles(self) -> int: + return 5 + + def get_max_golden_quotes(self) -> int: + return 5 + + def get_html_output_dir(self) -> str: + return "data/html" + + def get_html_filename_format(self) -> str: + return "report.html" + + def get_enable_user_card(self) -> bool: + return True + + def get_t2i_font_source(self) -> str: + return self.source + + def get_t2i_google_fonts_mirror(self) -> str: + return ( + "https://fonts.loli.net" + if self.source == "Mainland" + else "https://fonts.googleapis.com" + ) + + def get_t2i_gstatic_mirror(self) -> str: + return ( + "https://gstatic.loli.net" + if self.source == "Mainland" + else "https://fonts.gstatic.com" + ) + + def get_t2i_atri_font_mirror(self) -> str: + return "https://tc.ciallo.ccwu.cc" + + def get_profile_display_mode(self) -> str: + return "mbti" + + def get_profile_image_opacity(self) -> float: + return 0.2 + + def get_profile_image_size_mode(self) -> str: + return "contain" + + def get_profile_mapping_config(self) -> str: + return "" + + def get_t2i_max_concurrent(self) -> int: + return 4 + + def get_llm_max_concurrent(self) -> int: + return 2 + + def get_t2i_rendering_strategies(self) -> list: + return [] + + def get_html_base_url(self) -> str: + return "" + + +async def mock_get_user_avatar(user_id: str) -> str: + return f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=640" + + +async def verify_rendering(source: str): + print(f"\n--- Verifying {source} Rendering ---") + config = MockConfigManager(source) + data_dir = Path("data/test") + data_dir.mkdir(parents=True, exist_ok=True) + generator = ReportGenerator(config, data_dir) + + # Mock avatar cache + class MockCache(dict): + def __getitem__(self, key): + return self.get(key, "") + + def set(self, key, value, expire=None): + self[key] = value + + generator._avatar_cache = MockCache() + + stats = GroupStatistics( + message_count=100, + total_characters=1000, + participant_count=5, + most_active_period="12:00", + golden_quotes=[], + emoji_count=0, + emoji_statistics=EmojiStatistics(0, 0), + activity_visualization=ActivityVisualization({}), + token_usage=TokenUsage(0, 0, 0), + chat_quality_review=QualityReview("Test", "Test", [], "Summary"), + ) + analysis_result = { + "statistics": stats, + "topics": [], + "user_titles": [], + "user_analysis": {}, + "chat_quality_review": stats.chat_quality_review, + "analysis_date": "2026-04-25", + "group_id": "123", + "group_name": "Test Group", + } + + render_payload = await generator._prepare_render_data( + analysis_result, mock_get_user_avatar + ) + html = generator.html_templates.render_template( + "image_template.html", **render_payload + ) + + filename = f"test_{source.lower()}.html" + Path(filename).write_text(html, encoding="utf-8") + + # Verification + expected_lang = "zh-CN" if source == "Mainland" else "zh-Hant" + expected_font = ( + "https://fonts.loli.net" + if source == "Mainland" + else "https://fonts.googleapis.com" + ) + expected_gstatic = ( + "https://gstatic.loli.net" + if source == "Mainland" + else "https://fonts.gstatic.com" + ) + + success = True + if f'lang="{expected_lang}"' not in html: + print(f'[FAIL] Expected lang="{expected_lang}" not found.') + success = False + if expected_font not in html: + print(f'[FAIL] Expected font mirror "{expected_font}" not found.') + success = False + if expected_gstatic not in html: + print(f'[FAIL] Expected gstatic mirror "{expected_gstatic}" not found.') + success = False + + if success: + print(f"[PASS] {source} rendering verified. Output saved to {filename}") + + await generator.close() + + +async def main(): + await verify_rendering("Mainland") + await verify_rendering("Overseas") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index 801c0eb..fbaeac3 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -225,6 +225,32 @@ class ConfigManager: }, ] + def get_t2i_font_source(self) -> str: + """获取 T2I 字体源 (Mainland/Overseas)""" + return self._get_group("t2i_rendering").get("t2i_font_source", "Overseas") + + def get_t2i_google_fonts_mirror(self) -> str: + """根据环境选择获取 Google Fonts 镜像地址""" + source = self.get_t2i_font_source() + group = self._get_group("t2i_rendering") + if source == "Mainland": + return group.get("t2i_mainland_google_fonts", "https://fonts.loli.net") + return group.get("t2i_overseas_google_fonts", "https://fonts.googleapis.com") + + def get_t2i_gstatic_mirror(self) -> str: + """根据环境选择获取 Gstatic 镜像地址""" + source = self.get_t2i_font_source() + group = self._get_group("t2i_rendering") + if source == "Mainland": + return group.get("t2i_mainland_gstatic", "https://gstatic.loli.net") + return group.get("t2i_overseas_gstatic", "https://fonts.gstatic.com") + + def get_t2i_atri_font_mirror(self) -> str: + """获取 ATRI 主题字体镜像地址 (目前保持不变,如有需要可后续添加 Mainland/Overseas 配置)""" + return self._get_group("t2i_rendering").get( + "t2i_atri_font_mirror", "https://tc.ciallo.ccwu.cc" + ) + def get_llm_provider_id(self) -> str: """获取主 LLM Provider ID""" return self._get_group("llm").get("llm_provider_id", "") diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py index 9a64b81..798362f 100644 --- a/src/infrastructure/reporting/generators.py +++ b/src/infrastructure/reporting/generators.py @@ -816,6 +816,10 @@ class ReportGenerator(IReportGenerator): # 准备最终渲染数据 render_data = { + "t2i_font_source": self.config_manager.get_t2i_font_source(), + "t2i_google_fonts_mirror": self.config_manager.get_t2i_google_fonts_mirror(), + "t2i_gstatic_mirror": self.config_manager.get_t2i_gstatic_mirror(), + "t2i_atri_font_mirror": self.config_manager.get_t2i_atri_font_mirror(), "current_date": datetime.now().strftime("%Y年%m月%d日"), "current_datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "message_count": stats.message_count, diff --git a/src/infrastructure/reporting/templates/ATRI/activity_chart.html b/src/infrastructure/reporting/templates/ATRI/activity_chart.html index 188885f..93882bb 100644 --- a/src/infrastructure/reporting/templates/ATRI/activity_chart.html +++ b/src/infrastructure/reporting/templates/ATRI/activity_chart.html @@ -1,4 +1,4 @@ -
+
{% for hour_data in chart_data %} {% set ratio = loop.index0 / ((chart_data|length - 1) if (chart_data|length - 1) > 0 else 1) %} @@ -50,4 +50,4 @@
{% endfor %}
-
\ No newline at end of file + diff --git a/src/infrastructure/reporting/templates/ATRI/chat_quality_item.html b/src/infrastructure/reporting/templates/ATRI/chat_quality_item.html index 542403a..f66cd25 100644 --- a/src/infrastructure/reporting/templates/ATRI/chat_quality_item.html +++ b/src/infrastructure/reporting/templates/ATRI/chat_quality_item.html @@ -1,4 +1,4 @@ - + + + + diff --git a/src/infrastructure/reporting/templates/hack/topic_item.html b/src/infrastructure/reporting/templates/hack/topic_item.html index 1b12510..ee4cc0d 100644 --- a/src/infrastructure/reporting/templates/hack/topic_item.html +++ b/src/infrastructure/reporting/templates/hack/topic_item.html @@ -1,4 +1,4 @@ -{% if topics %} +{% if topics %}
{% for topic in topics %}
@@ -8,4 +8,4 @@
{% endfor %}
-{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/infrastructure/reporting/templates/hack/user_title_item.html b/src/infrastructure/reporting/templates/hack/user_title_item.html index c242c64..e372f5e 100644 --- a/src/infrastructure/reporting/templates/hack/user_title_item.html +++ b/src/infrastructure/reporting/templates/hack/user_title_item.html @@ -1,4 +1,4 @@ -{% if titles %} +{% if titles %}
{% for title in titles %}
@@ -27,3 +27,4 @@ {% endfor %}
{% endif %} + diff --git a/src/infrastructure/reporting/templates/retro_futurism/activity_chart.html b/src/infrastructure/reporting/templates/retro_futurism/activity_chart.html index 2b05a1d..410c600 100644 --- a/src/infrastructure/reporting/templates/retro_futurism/activity_chart.html +++ b/src/infrastructure/reporting/templates/retro_futurism/activity_chart.html @@ -1,4 +1,4 @@ -{% if chart_data %} +{% if chart_data %}
{% for item in chart_data %}
@@ -12,4 +12,4 @@
NO_DATA_AVAILABLE_FOR_ACTIVITY_SPECTRUM
-{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/infrastructure/reporting/templates/retro_futurism/chat_quality_item.html b/src/infrastructure/reporting/templates/retro_futurism/chat_quality_item.html index 6faf065..86fd0f0 100644 --- a/src/infrastructure/reporting/templates/retro_futurism/chat_quality_item.html +++ b/src/infrastructure/reporting/templates/retro_futurism/chat_quality_item.html @@ -1,4 +1,4 @@ -