Skip to content

Commit 60ca2d3

Browse files
committed
fix(ui): preserve complete Telegram message text
1 parent 864c6ba commit 60ca2d3

5 files changed

Lines changed: 81 additions & 63 deletions

File tree

src/handlers/group_handler.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,14 @@ class GroupHandler:
2020
"""群组消息处理器"""
2121

2222
@staticmethod
23-
def _escape_inline_code(value: object, max_length: int = 253) -> str:
24-
"""Keep a dynamic value on one safe legacy-Markdown code line."""
25-
text = " ".join(str(value).split())[:max_length]
23+
def _escape_inline_code(value: object) -> str:
24+
"""Keep a complete dynamic value on one safe legacy-Markdown code line."""
25+
text = " ".join(str(value).split())
2626
return text.replace("\\", "\\\\").replace("`", "\\`")
2727

28-
def _escape_detail(self, value: object, max_length: int = 160) -> str:
29-
"""Format a short, single-line dynamic explanation."""
28+
def _escape_detail(self, value: object) -> str:
29+
"""Format a complete dynamic explanation and let Telegram wrap it."""
3030
text = " ".join(str(value).split())
31-
if len(text) > max_length:
32-
text = f"{text[: max_length - 3]}..."
3331
text = text.replace("\\", "\\\\")
3432
return self.handler_manager.escape_markdown(text)
3533

@@ -244,7 +242,7 @@ async def _process_domain_request(
244242
if user is not None
245243
else self.handler_manager.escape_markdown(username)
246244
)
247-
identity = " ".join(str(identity).split())[:80]
245+
identity = " ".join(str(identity).split())
248246
safe_domain = self._escape_inline_code(domain)
249247
processing_msg = await message.reply_text(
250248
"🔍 *正在检查域名*\n\n"

src/handlers/handler_manager.py

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import secrets
77
import time
8-
import unicodedata
98
from datetime import datetime, timezone
109
from typing import Dict, Any, Optional
1110
from collections import defaultdict
@@ -73,7 +72,6 @@ def __init__(self, config: Config, data_manager: DataManager, application=None):
7372
self.MAX_DESCRIPTION_LENGTH = 20
7473
self.MAX_ADDS_PER_HOUR = 50
7574
self.MAX_DETAIL_LINES = 4
76-
self.MAX_DETAIL_LINE_LENGTH = 56
7775
self.STATE_TTL = 1800
7876
self.ACTION_TTL = 900
7977
self.MAX_USER_STATES = 4096
@@ -565,7 +563,7 @@ def _raw_telegram_identity(user) -> str:
565563

566564
def _format_telegram_identity(self, user) -> str:
567565
"""Format a Telegram identity for Markdown-visible messages."""
568-
identity = self._truncate_display(self._raw_telegram_identity(user), 40)
566+
identity = self._single_line_display(self._raw_telegram_identity(user))
569567
identity = self.escape_markdown(identity)
570568
if getattr(user, "username", None):
571569
return f"@{identity}"
@@ -610,45 +608,18 @@ def _repo_and_home_keyboard(self) -> InlineKeyboardMarkup:
610608
)
611609

612610
@staticmethod
613-
def _display_width(value: object) -> int:
614-
width = 0
615-
for char in str(value):
616-
if unicodedata.combining(char):
617-
continue
618-
width += 2 if unicodedata.east_asian_width(char) in {"W", "F"} else 1
619-
return width
620-
621-
@classmethod
622-
def _truncate_display(cls, value: object, max_width: int = 56) -> str:
623-
"""Collapse and bound dynamic text by its approximate rendered width."""
624-
text = " ".join(str(value).split())
625-
if cls._display_width(text) <= max_width:
626-
return text
627-
visible = []
628-
width = 0
629-
for char in text:
630-
char_width = (
631-
0
632-
if unicodedata.combining(char)
633-
else 2
634-
if unicodedata.east_asian_width(char) in {"W", "F"}
635-
else 1
636-
)
637-
if width + char_width > max_width - 1:
638-
break
639-
visible.append(char)
640-
width += char_width
641-
return "".join(visible).rstrip() + "…"
611+
def _single_line_display(value: object) -> str:
612+
"""Normalize dynamic text without discarding visible content."""
613+
return " ".join(str(value).split())
642614

643615
def _format_rule_matches(self, matches: list, limit: Optional[int] = None) -> str:
644-
"""Bound dynamic GitHub matches so Telegram messages remain below limits."""
616+
"""Format dynamic GitHub matches as complete, independently wrapped rows."""
645617
if not matches:
646618
return ""
647619
limit = limit or getattr(self, "MAX_DETAIL_LINES", 4)
648-
line_limit = getattr(self, "MAX_DETAIL_LINE_LENGTH", 56)
649620
lines = []
650621
for match in matches[:limit]:
651-
rule = self._truncate_display(match.get("rule", ""), line_limit - 12)
622+
rule = self._single_line_display(match.get("rule", ""))
652623
lines.append(f"• 第 {match.get('line', '?')} 行:{self.escape_markdown(rule)}")
653624
remaining = len(matches) - limit
654625
if remaining > 0:
@@ -659,7 +630,7 @@ def _format_rule_matches(self, matches: list, limit: Optional[int] = None) -> st
659630
def _format_value_list(values: list, limit: int = 3) -> str:
660631
"""Format network values one-per-line for narrow Telegram clients."""
661632
visible = [
662-
HandlerManager._truncate_display(value, 56)
633+
HandlerManager._single_line_display(value)
663634
for value in list(values or [])[:limit]
664635
]
665636
remaining = len(values or []) - len(visible)
@@ -704,9 +675,7 @@ def _build_add_review_text(self, domain: str, check_result: dict, user) -> tuple
704675
[
705676
"",
706677
"💡 *判断依据*",
707-
self.escape_markdown(
708-
self._truncate_display(recommendation, 56)
709-
),
678+
self.escape_markdown(self._single_line_display(recommendation)),
710679
]
711680
)
712681
if not should_reject:
@@ -880,7 +849,7 @@ async def _display_message_result(
880849

881850
def _build_main_menu_text(self, username: str) -> str:
882851
"""构建主菜单文案"""
883-
username = self.escape_markdown(self._truncate_display(username, 24))
852+
username = self.escape_markdown(self._single_line_display(username))
884853
repo = str(self.config.GITHUB_REPO).strip()
885854
repo_label = self.escape_markdown(repo)
886855
return "\n".join(
@@ -1024,9 +993,7 @@ def _format_detail_lines(
1024993
limit = limit or self.MAX_DETAIL_LINES
1025994
lines = []
1026995
for detail in details[:limit]:
1027-
detail = self._truncate_display(
1028-
detail, self.MAX_DETAIL_LINE_LENGTH - 2
1029-
)
996+
detail = self._single_line_display(detail)
1030997
lines.append(f"• {self.escape_markdown(detail)}")
1031998

1032999
remaining = len(details) - limit
@@ -1048,7 +1015,7 @@ def _build_query_summary_text(
10481015
github_exists = bool(github_result.get("exists"))
10491016
check_failed = "error" in check_result
10501017
matches = github_result.get("matches", [])
1051-
visible_domain = self._truncate_display(domain, 56)
1018+
visible_domain = self._single_line_display(domain)
10521019

10531020
if github_unavailable:
10541021
github_status = "暂时无法读取"
@@ -1109,8 +1076,13 @@ def _build_query_summary_text(
11091076
and not github_exists
11101077
and not in_geosite
11111078
):
1112-
visible = self._truncate_display(recommendation, 48)
1113-
lines.append(f"💡 综合判断:{self.escape_markdown(visible)}")
1079+
lines.extend(
1080+
[
1081+
"",
1082+
"💡 *综合判断*",
1083+
self.escape_markdown(self._single_line_display(recommendation)),
1084+
]
1085+
)
11141086
return "\n".join(lines)
11151087

11161088
def _build_query_detail_text(
@@ -1120,7 +1092,7 @@ def _build_query_detail_text(
11201092
check_result: dict,
11211093
) -> str:
11221094
"""Build a bounded technical page without repeating the summary."""
1123-
visible_domain = self._truncate_display(domain, 56)
1095+
visible_domain = self._single_line_display(domain)
11241096
lines = ["🔎 *技术详情*", "", f"`{visible_domain}`"]
11251097
github_unavailable = bool(github_result.get("error"))
11261098
matches = github_result.get("matches", [])

src/services/group_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def _escape_markdown(text: str) -> str:
4242
return text
4343

4444
@staticmethod
45-
def _escape_inline_code(value: object, max_length: int = 253) -> str:
46-
"""Keep a dynamic value on one safe legacy-Markdown code line."""
47-
text = " ".join(str(value).split())[:max_length]
45+
def _escape_inline_code(value: object) -> str:
46+
"""Keep a complete dynamic value on one safe legacy-Markdown code line."""
47+
text = " ".join(str(value).split())
4848
return text.replace("\\", "\\\\").replace("`", "\\`")
4949

5050
async def check_user_in_group(

tests/test_group_handler_ui.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ def _handler_for_result(self, result, *, is_admin=False):
3333
)
3434
return handler
3535

36-
async def test_error_detail_is_single_line_and_bounded(self):
36+
async def test_error_detail_is_single_line_and_complete(self):
37+
detail = f"bad\n{'x' * 300}*"
3738
handler = self._handler_for_result(
3839
{
3940
"action": "error",
40-
"message": f"bad\n{'x' * 300}*",
41+
"message": detail,
4142
}
4243
)
4344
processing = SimpleNamespace(edit_text=AsyncMock())
@@ -52,7 +53,8 @@ async def test_error_detail_is_single_line_and_bounded(self):
5253
self.assertNotIn("bad\n", text)
5354
self.assertIn("请稍后再次 @机器人", text)
5455
self.assertIn("🛡️ 本次操作未修改任何规则", text)
55-
self.assertLess(len(text), 320)
56+
self.assertIn(f"bad {'x' * 300}\\*", text)
57+
self.assertNotIn("...", text)
5658

5759
async def test_uncertain_write_result_warns_before_any_retry(self):
5860
handler = self._handler_for_result(

tests/test_ui_ux.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ def _manager(self, rule_bot_client_enabled=True):
2121
ANNOUNCEMENT_GROUP_ID=-100123 if rule_bot_client_enabled else None,
2222
)
2323
manager.MAX_DETAIL_LINES = 4
24-
manager.MAX_DETAIL_LINE_LENGTH = 56
2524
manager.MAX_DESCRIPTION_LENGTH = 20
2625
return manager
2726

@@ -74,6 +73,54 @@ def test_add_review_discloses_exact_rule_and_public_scope(self):
7473
self.assertNotIn("结论:", text)
7574
self.assertNotIn("检查详情", text)
7675

76+
def test_add_review_preserves_complete_recommendation(self):
77+
manager = self._manager()
78+
manager.domain_checker = SimpleNamespace(
79+
get_target_domain_to_add=MagicMock(return_value="codexradar.com"),
80+
should_reject=MagicMock(return_value=True),
81+
)
82+
recommendation = (
83+
"❌ 不建议添加可注册域名 codexradar.com:"
84+
"域名 IP 和 NS 服务器都不在中国大陆"
85+
)
86+
87+
text, _ = manager._build_add_review_text(
88+
"codexradar.com",
89+
{"recommendation": recommendation},
90+
SimpleNamespace(id=2, username="alice", first_name="Alice"),
91+
)
92+
93+
self.assertIn(f"💡 *判断依据*\n{recommendation}", text)
94+
self.assertNotIn("…", text)
95+
96+
def test_query_pages_preserve_complete_dynamic_text(self):
97+
manager = self._manager()
98+
recommendation = "综合判断:" + "完整内容" * 20
99+
detail = "归属检查:" + "详细结果" * 20
100+
rule = "DOMAIN-SUFFIX," + "long-label." * 12 + "example.com"
101+
check_result = {
102+
"domain_ips": [],
103+
"second_level_ips": [],
104+
"details": [detail],
105+
"recommendation": recommendation,
106+
}
107+
github_result = {
108+
"exists": False,
109+
"matches": [{"line": 42, "rule": rule}],
110+
}
111+
112+
summary = manager._build_query_summary_text(
113+
"example.com", github_result, False, check_result, "ℹ️ *查询完成*"
114+
)
115+
technical = manager._build_query_detail_text(
116+
"example.com", github_result, check_result
117+
)
118+
119+
self.assertIn(f"💡 *综合判断*\n{recommendation}", summary)
120+
self.assertIn(detail, technical)
121+
self.assertIn(manager.escape_markdown(rule), technical)
122+
self.assertNotIn("…", summary + technical)
123+
77124

78125
class TestVisibleFlows(unittest.IsolatedAsyncioTestCase):
79126
@staticmethod
@@ -100,7 +147,6 @@ def _stateful_manager():
100147
manager.ACTION_TTL = 900
101148
manager.MAX_USER_STATES = 4096
102149
manager.MAX_DETAIL_LINES = 4
103-
manager.MAX_DETAIL_LINE_LENGTH = 56
104150
manager.MAX_DESCRIPTION_LENGTH = 20
105151
manager.MAX_ADDS_PER_HOUR = 50
106152
return manager

0 commit comments

Comments
 (0)