diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index 2f9472ecc0..1c20b3f290 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -23,6 +23,7 @@ import re import secrets import struct import tempfile +import textwrap import time import uuid from datetime import datetime @@ -32,6 +33,8 @@ from urllib.parse import quote, urlparse logger = logging.getLogger(__name__) +WEIXIN_COPY_LINE_WIDTH = 120 + try: import aiohttp @@ -731,6 +734,46 @@ def _normalize_markdown_blocks(content: str) -> str: return "\n".join(result).strip() +def _wrap_copy_friendly_lines_for_weixin(content: str) -> str: + """Wrap long display lines that are hard to copy in WeChat clients.""" + if not content: + return content + + wrapped: List[str] = [] + in_code_block = False + + for raw_line in content.splitlines(): + line = raw_line.rstrip() + stripped = line.strip() + + if _FENCE_RE.match(stripped): + in_code_block = not in_code_block + wrapped.append(line) + continue + + if ( + in_code_block + or len(line) <= WEIXIN_COPY_LINE_WIDTH + or not stripped + or stripped.startswith("|") + or _TABLE_RULE_RE.match(stripped) + ): + wrapped.append(line) + continue + + wrapped_lines = textwrap.wrap( + line, + width=WEIXIN_COPY_LINE_WIDTH, + break_long_words=False, + break_on_hyphens=False, + replace_whitespace=False, + drop_whitespace=True, + ) + wrapped.extend(wrapped_lines or [line]) + + return "\n".join(wrapped).strip() + + def _split_markdown_blocks(content: str) -> List[str]: if not content: return [] @@ -2022,7 +2065,7 @@ class WeixinAdapter(BasePlatformAdapter): def format_message(self, content: Optional[str]) -> str: if content is None: return "" - return _normalize_markdown_blocks(content) + return _wrap_copy_friendly_lines_for_weixin(_normalize_markdown_blocks(content)) async def send_weixin_direct( diff --git a/tests/gateway/test_weixin.py b/tests/gateway/test_weixin.py index 68dfa76841..64258f7a29 100644 --- a/tests/gateway/test_weixin.py +++ b/tests/gateway/test_weixin.py @@ -54,6 +54,28 @@ class TestWeixinFormatting: assert adapter.format_message(content) == content + def test_format_message_wraps_long_plain_lines_for_copying(self): + adapter = _make_adapter() + + content = ( + "Here is a long issue template line with many copyable fields " + + " ".join(f"field_{idx}=value_{idx}" for idx in range(24)) + ) + + formatted = adapter.format_message(content) + + assert "\n" in formatted + assert all(len(line) <= weixin.WEIXIN_COPY_LINE_WIDTH for line in formatted.splitlines()) + assert " ".join(formatted.split()) == " ".join(content.split()) + + def test_format_message_does_not_wrap_long_code_block_lines(self): + adapter = _make_adapter() + + command = "hermes " + " ".join(f"--option-{idx}=value" for idx in range(30)) + content = f"```bash\n{command}\n```" + + assert adapter.format_message(content) == content + def test_format_message_returns_empty_string_for_none(self): adapter = _make_adapter()