diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 6e27d33e09..dc3d799c93 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -119,6 +119,8 @@ _MARKDOWN_HINT_RE = re.compile( re.MULTILINE, ) _MARKDOWN_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") +_MARKDOWN_FENCE_OPEN_RE = re.compile(r"^```([^\n`]*)\s*$") +_MARKDOWN_FENCE_CLOSE_RE = re.compile(r"^```\s*$") _MENTION_RE = re.compile(r"@_user_\d+") _MULTISPACE_RE = re.compile(r"[ \t]{2,}") _POST_CONTENT_INVALID_RE = re.compile(r"content format of the post type is incorrect", re.IGNORECASE) @@ -445,9 +447,9 @@ def _build_markdown_post_rows(content: str) -> List[List[Dict[str, str]]]: """Build Feishu post rows while isolating fenced code blocks. Feishu's `md` renderer can swallow trailing content when a fenced code block - appears inside one large markdown element. Splitting the reply at code - fences preserves the surrounding markdown while keeping the code block in a - dedicated row. + appears inside one large markdown element. Split the reply at real fence + lines so prose before/after the code block remains visible while code stays + in a dedicated row. """ if not content: return [[{"tag": "md", "text": ""}]] @@ -458,32 +460,35 @@ def _build_markdown_post_rows(content: str) -> List[List[Dict[str, str]]]: current: List[str] = [] in_code_block = False + def _flush_current() -> None: + nonlocal current + if not current: + return + segment = "\n".join(current) + if segment.strip(): + rows.append([{"tag": "md", "text": segment}]) + current = [] + for raw_line in content.splitlines(): - line = raw_line.rstrip() - is_fence = line.strip().startswith("```") + stripped_line = raw_line.strip() + is_fence = bool( + _MARKDOWN_FENCE_CLOSE_RE.match(stripped_line) + if in_code_block + else _MARKDOWN_FENCE_OPEN_RE.match(stripped_line) + ) if is_fence: - if not in_code_block and current: - segment = "\n".join(current).strip() - if segment: - rows.append([{"tag": "md", "text": segment}]) - current = [] - current.append(line) + if not in_code_block: + _flush_current() + current.append(raw_line) in_code_block = not in_code_block if not in_code_block: - segment = "\n".join(current).strip() - if segment: - rows.append([{"tag": "md", "text": segment}]) - current = [] + _flush_current() continue - current.append(line) - - if current: - segment = "\n".join(current).strip() - if segment: - rows.append([{"tag": "md", "text": segment}]) + current.append(raw_line) + _flush_current() return rows or [[{"tag": "md", "text": content}]] diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 47e5a94966..d5511c064e 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -2433,6 +2433,48 @@ class TestAdapterBehavior(unittest.TestCase): ], ) + @patch.dict(os.environ, {}, clear=True) + def test_build_post_payload_keeps_fence_like_code_lines_inside_code_block(self): + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + payload = json.loads( + adapter._build_post_payload( + "before\n```python\n```oops\n```\nafter" + ) + ) + + self.assertEqual( + payload["zh_cn"]["content"], + [ + [{"tag": "md", "text": "before"}], + [{"tag": "md", "text": "```python\n```oops\n```"}], + [{"tag": "md", "text": "after"}], + ], + ) + + @patch.dict(os.environ, {}, clear=True) + def test_build_post_payload_preserves_trailing_spaces_in_code_block(self): + from gateway.config import PlatformConfig + from gateway.platforms.feishu import FeishuAdapter + + adapter = FeishuAdapter(PlatformConfig()) + payload = json.loads( + adapter._build_post_payload( + "before\n```python\nline with two spaces \n```\nafter" + ) + ) + + self.assertEqual( + payload["zh_cn"]["content"], + [ + [{"tag": "md", "text": "before"}], + [{"tag": "md", "text": "```python\nline with two spaces \n```"}], + [{"tag": "md", "text": "after"}], + ], + ) + @patch.dict(os.environ, {}, clear=True) def test_send_falls_back_to_text_when_post_payload_is_rejected(self): from gateway.config import PlatformConfig