Skip to content

Commit 948bfb1

Browse files
committed
refactor(render): 解决国内可能存在的资源访问问题,新增大陆资源和海外资源可选项,允许用户根据服务器位置选择不同的资源策略,并且允许自定义资源镜像站
- Updated language attributes in HTML templates to dynamically set based on `t2i_font_source`. - Replaced hardcoded Google Fonts links with dynamic links using `t2i_google_fonts_mirror` and `t2i_gstatic_mirror`. - Adjusted font-family declarations to conditionally include 'Noto Sans SC' and 'Noto Sans TC' based on the `t2i_font_source`. - Ensured consistent formatting and style across various templates including image, quote, topic, and user title items. - Added missing newlines at the end of several files for consistency.
1 parent 54f517d commit 948bfb1

64 files changed

Lines changed: 614 additions & 219 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ scripts/output/mock_report_test_group_mock_*.pdf
3434
astrbot-lark-group-daily-analysis-main/**
3535
.ace-tool/
3636
debug_atri.html
37+
data/test/avatar/cache.db
38+
test_mainland.html
39+
test_overseas.html

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# 更新日志 (CHANGELOG)
22

3+
## [v4.10.2] - ✨ 支持多地区渲染环境与自定义镜像
4+
* **🌍 多地区环境支持**: 新增 `t2i_font_source` 配置项,支持 `Mainland` (大陆) 和 `Overseas` (海外) 模式。自动切换 HTML 语言标识 (`zh-CN`/`zh-Hant`) 并动态调整 CSS 字体栈优先级,解决繁体中文支持与字形偏移问题。
5+
* **⚙️ 自定义镜像地址**: 移除了所有硬编码的字体资源站地址。现在用户可以为国内和国外环境分别自定义 Google Fonts、Gstatic 字体镜像站。
6+
* **📱 平台与主题优化**:
7+
* **Telegram**: 修复了 Telegram 平台的头像处理逻辑 (#176 @lekoOwO)。
8+
* **🛠️ 基础设施与调试**: 重构模板实现完全的数据驱动渲染;更新 `debug_render.py` 并新增 `test_regional_rendering.py` 验证脚本。
9+
10+
---
11+
312
## [v4.10.1] - ✨ 优化 T2I 渲染与自定义配置
413
* **✨ 自定义配置支持**: 支持配置两轮 T2I 渲染策略,允许用户自定义图片格式(PNG/JPEG)、质量、分辨率及超时时间(#164),支持复杂渲染场景的超时参数配置(#174 感谢 @uuutt2023)。
514
* **🛠️ 健壮性增强**: 实现 T2I 返回 HTML 错误页的自动识别与摘要提取(如 502 Bad Gateway),提升故障排查效率。(参考 #171 说明)

_conf_schema.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,37 @@
215215
"step": 5000
216216
},
217217
"hint": "回退尝试通常针对复杂页面,建议设置更长的超时时间(如 100000ms+)。"
218+
},
219+
"t2i_font_source": {
220+
"type": "string",
221+
"description": "t2i 渲染环境切换",
222+
"options": ["Mainland", "Overseas"],
223+
"default": "Overseas",
224+
"hint": "切换环境会自动应用下方对应的镜像站地址、语言标识(zh-CN/zh-Hant)及字体优先级。"
225+
},
226+
"t2i_mainland_google_fonts": {
227+
"type": "string",
228+
"description": "[国内] Google Fonts 镜像",
229+
"default": "https://fonts.loli.net",
230+
"hint": "国内环境下推荐使用 loli.net 或其他可访问镜像。"
231+
},
232+
"t2i_mainland_gstatic": {
233+
"type": "string",
234+
"description": "[国内] Gstatic 镜像",
235+
"default": "https://gstatic.loli.net",
236+
"hint": "国内环境下推荐使用 loli.net 或其他可访问镜像。"
237+
},
238+
"t2i_overseas_google_fonts": {
239+
"type": "string",
240+
"description": "[国外] Google Fonts 官方",
241+
"default": "https://fonts.googleapis.com",
242+
"hint": "海外环境通常直接使用官方地址即可。"
243+
},
244+
"t2i_overseas_gstatic": {
245+
"type": "string",
246+
"description": "[国外] Gstatic 官方",
247+
"default": "https://fonts.gstatic.com",
248+
"hint": "海外环境通常直接使用官方地址即可。"
218249
}
219250
}
220251
},

scripts/debug_render.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ def get_profile_mapping_config(self) -> str:
125125
def get_html_base_url(self) -> str:
126126
return ""
127127

128+
def get_t2i_font_source(self) -> str:
129+
return "Overseas"
130+
131+
def get_t2i_google_fonts_mirror(self) -> str:
132+
return "https://fonts.googleapis.com"
133+
134+
def get_t2i_gstatic_mirror(self) -> str:
135+
return "https://fonts.gstatic.com"
136+
137+
def get_t2i_atri_font_mirror(self) -> str:
138+
return "https://tc.ciallo.ccwu.cc"
139+
140+
def get_t2i_rendering_strategies(self) -> list:
141+
return []
142+
128143

129144
async def mock_get_user_avatar(user_id: str) -> str:
130145
# Return a known avatar URL for testing

scripts/test_regional_rendering.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import asyncio
2+
import os
3+
import sys
4+
import types
5+
from pathlib import Path
6+
7+
# ==========================================
8+
# 1. Environment Setup
9+
# ==========================================
10+
current_dir = os.path.dirname(os.path.abspath(__file__))
11+
plugin_root = os.path.abspath(os.path.join(current_dir, ".."))
12+
sys.path.insert(0, plugin_root)
13+
14+
# Mock astrbot.api
15+
astrbot_api = types.ModuleType("astrbot.api")
16+
17+
18+
class MockLogger:
19+
def info(self, msg, *args, **kwargs):
20+
print(f"[INFO] {msg}")
21+
22+
def error(self, msg, *args, **kwargs):
23+
print(f"[ERROR] {msg}")
24+
25+
def warning(self, msg, *args, **kwargs):
26+
print(f"[WARN] {msg}")
27+
28+
def debug(self, msg, *args, **kwargs):
29+
pass
30+
31+
def log(self, level, msg, *args, **kwargs):
32+
pass
33+
34+
def isEnabledFor(self, level):
35+
return True
36+
37+
38+
astrbot_api.logger = MockLogger()
39+
astrbot_api.AstrBotConfig = dict
40+
sys.modules["astrbot.api"] = astrbot_api
41+
42+
# Mock astrbot.core.utils.astrbot_path
43+
astrbot_core_utils = types.ModuleType("astrbot.core.utils")
44+
astrbot_path = types.ModuleType("astrbot.core.utils.astrbot_path")
45+
astrbot_path.get_astrbot_data_path = lambda: Path(".")
46+
sys.modules["astrbot.core.utils"] = astrbot_core_utils
47+
sys.modules["astrbot.core.utils.astrbot_path"] = astrbot_path
48+
49+
from src.domain.models.data_models import ( # noqa: E402
50+
ActivityVisualization,
51+
EmojiStatistics,
52+
GroupStatistics,
53+
QualityReview,
54+
TokenUsage,
55+
)
56+
from src.infrastructure.reporting.generators import ReportGenerator # noqa: E402
57+
58+
59+
class MockConfigManager:
60+
def __init__(self, source: str) -> None:
61+
self.source = source
62+
63+
def get_report_template(self) -> str:
64+
return "simple"
65+
66+
def get_max_topics(self) -> int:
67+
return 5
68+
69+
def get_max_user_titles(self) -> int:
70+
return 5
71+
72+
def get_max_golden_quotes(self) -> int:
73+
return 5
74+
75+
def get_html_output_dir(self) -> str:
76+
return "data/html"
77+
78+
def get_html_filename_format(self) -> str:
79+
return "report.html"
80+
81+
def get_enable_user_card(self) -> bool:
82+
return True
83+
84+
def get_t2i_font_source(self) -> str:
85+
return self.source
86+
87+
def get_t2i_google_fonts_mirror(self) -> str:
88+
return (
89+
"https://fonts.loli.net"
90+
if self.source == "Mainland"
91+
else "https://fonts.googleapis.com"
92+
)
93+
94+
def get_t2i_gstatic_mirror(self) -> str:
95+
return (
96+
"https://gstatic.loli.net"
97+
if self.source == "Mainland"
98+
else "https://fonts.gstatic.com"
99+
)
100+
101+
def get_t2i_atri_font_mirror(self) -> str:
102+
return "https://tc.ciallo.ccwu.cc"
103+
104+
def get_profile_display_mode(self) -> str:
105+
return "mbti"
106+
107+
def get_profile_image_opacity(self) -> float:
108+
return 0.2
109+
110+
def get_profile_image_size_mode(self) -> str:
111+
return "contain"
112+
113+
def get_profile_mapping_config(self) -> str:
114+
return ""
115+
116+
def get_t2i_max_concurrent(self) -> int:
117+
return 4
118+
119+
def get_llm_max_concurrent(self) -> int:
120+
return 2
121+
122+
def get_t2i_rendering_strategies(self) -> list:
123+
return []
124+
125+
def get_html_base_url(self) -> str:
126+
return ""
127+
128+
129+
async def mock_get_user_avatar(user_id: str) -> str:
130+
return f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=640"
131+
132+
133+
async def verify_rendering(source: str):
134+
print(f"\n--- Verifying {source} Rendering ---")
135+
config = MockConfigManager(source)
136+
data_dir = Path("data/test")
137+
data_dir.mkdir(parents=True, exist_ok=True)
138+
generator = ReportGenerator(config, data_dir)
139+
140+
# Mock avatar cache
141+
class MockCache(dict):
142+
def __getitem__(self, key):
143+
return self.get(key, "")
144+
145+
def set(self, key, value, expire=None):
146+
self[key] = value
147+
148+
generator._avatar_cache = MockCache()
149+
150+
stats = GroupStatistics(
151+
message_count=100,
152+
total_characters=1000,
153+
participant_count=5,
154+
most_active_period="12:00",
155+
golden_quotes=[],
156+
emoji_count=0,
157+
emoji_statistics=EmojiStatistics(0, 0),
158+
activity_visualization=ActivityVisualization({}),
159+
token_usage=TokenUsage(0, 0, 0),
160+
chat_quality_review=QualityReview("Test", "Test", [], "Summary"),
161+
)
162+
analysis_result = {
163+
"statistics": stats,
164+
"topics": [],
165+
"user_titles": [],
166+
"user_analysis": {},
167+
"chat_quality_review": stats.chat_quality_review,
168+
"analysis_date": "2026-04-25",
169+
"group_id": "123",
170+
"group_name": "Test Group",
171+
}
172+
173+
render_payload = await generator._prepare_render_data(
174+
analysis_result, mock_get_user_avatar
175+
)
176+
html = generator.html_templates.render_template(
177+
"image_template.html", **render_payload
178+
)
179+
180+
filename = f"test_{source.lower()}.html"
181+
Path(filename).write_text(html, encoding="utf-8")
182+
183+
# Verification
184+
expected_lang = "zh-CN" if source == "Mainland" else "zh-Hant"
185+
expected_font = (
186+
"https://fonts.loli.net"
187+
if source == "Mainland"
188+
else "https://fonts.googleapis.com"
189+
)
190+
expected_gstatic = (
191+
"https://gstatic.loli.net"
192+
if source == "Mainland"
193+
else "https://fonts.gstatic.com"
194+
)
195+
196+
success = True
197+
if f'lang="{expected_lang}"' not in html:
198+
print(f'[FAIL] Expected lang="{expected_lang}" not found.')
199+
success = False
200+
if expected_font not in html:
201+
print(f'[FAIL] Expected font mirror "{expected_font}" not found.')
202+
success = False
203+
if expected_gstatic not in html:
204+
print(f'[FAIL] Expected gstatic mirror "{expected_gstatic}" not found.')
205+
success = False
206+
207+
if success:
208+
print(f"[PASS] {source} rendering verified. Output saved to {filename}")
209+
210+
await generator.close()
211+
212+
213+
async def main():
214+
await verify_rendering("Mainland")
215+
await verify_rendering("Overseas")
216+
217+
218+
if __name__ == "__main__":
219+
asyncio.run(main())

src/infrastructure/config/config_manager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,32 @@ def get_t2i_rendering_strategies(self) -> list[dict]:
225225
},
226226
]
227227

228+
def get_t2i_font_source(self) -> str:
229+
"""获取 T2I 字体源 (Mainland/Overseas)"""
230+
return self._get_group("t2i_rendering").get("t2i_font_source", "Overseas")
231+
232+
def get_t2i_google_fonts_mirror(self) -> str:
233+
"""根据环境选择获取 Google Fonts 镜像地址"""
234+
source = self.get_t2i_font_source()
235+
group = self._get_group("t2i_rendering")
236+
if source == "Mainland":
237+
return group.get("t2i_mainland_google_fonts", "https://fonts.loli.net")
238+
return group.get("t2i_overseas_google_fonts", "https://fonts.googleapis.com")
239+
240+
def get_t2i_gstatic_mirror(self) -> str:
241+
"""根据环境选择获取 Gstatic 镜像地址"""
242+
source = self.get_t2i_font_source()
243+
group = self._get_group("t2i_rendering")
244+
if source == "Mainland":
245+
return group.get("t2i_mainland_gstatic", "https://gstatic.loli.net")
246+
return group.get("t2i_overseas_gstatic", "https://fonts.gstatic.com")
247+
248+
def get_t2i_atri_font_mirror(self) -> str:
249+
"""获取 ATRI 主题字体镜像地址 (目前保持不变,如有需要可后续添加 Mainland/Overseas 配置)"""
250+
return self._get_group("t2i_rendering").get(
251+
"t2i_atri_font_mirror", "https://tc.ciallo.ccwu.cc"
252+
)
253+
228254
def get_llm_provider_id(self) -> str:
229255
"""获取主 LLM Provider ID"""
230256
return self._get_group("llm").get("llm_provider_id", "")

src/infrastructure/reporting/generators.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,10 @@ async def _prepare_render_data(
816816

817817
# 准备最终渲染数据
818818
render_data = {
819+
"t2i_font_source": self.config_manager.get_t2i_font_source(),
820+
"t2i_google_fonts_mirror": self.config_manager.get_t2i_google_fonts_mirror(),
821+
"t2i_gstatic_mirror": self.config_manager.get_t2i_gstatic_mirror(),
822+
"t2i_atri_font_mirror": self.config_manager.get_t2i_atri_font_mirror(),
819823
"current_date": datetime.now().strftime("%Y年%m月%d日"),
820824
"current_datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
821825
"message_count": stats.message_count,

src/infrastructure/reporting/templates/ATRI/activity_chart.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="chart-container">
1+
<div class="chart-container">
22
<div style="display: flex; align-items: stretch; height: 220px; gap: 5px; padding: 20px 0 10px;">
33
{% for hour_data in chart_data %}
44
{% set ratio = loop.index0 / ((chart_data|length - 1) if (chart_data|length - 1) > 0 else 1) %}
@@ -50,4 +50,4 @@
5050
</div>
5151
{% endfor %}
5252
</div>
53-
</div>
53+
</div>

src/infrastructure/reporting/templates/ATRI/chat_quality_item.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<style>
1+
<style>
22
.quality-card {
33
position: relative;
44
padding: 24px 24px 20px;
@@ -152,4 +152,4 @@
152152
</div>
153153

154154
<div class="quality-summary">{{ summary }}</div>
155-
</div>
155+
</div>

0 commit comments

Comments
 (0)