From c7c19d8578acaafce88e88411d1f3fbad75c385e Mon Sep 17 00:00:00 2001 From: hacxy Date: Thu, 16 Jul 2026 16:12:10 +0800 Subject: [PATCH] fix(feishu): remove outdated markdown table fallback, always use post format Feishu's post-type 'md' elements now natively support all markdown syntax including pipe tables. The old _MARKDOWN_TABLE_RE fallback forced table messages to plain text, causing them to render as raw source code instead of formatted tables. Changes: - Remove the table-to-text fallback in _build_outbound_payload() - Add _MARKDOWN_TABLE_RE check alongside _MARKDOWN_HINT_RE so tables route through the post format with md elements - Add 12 tests covering table routing, regex matching, and payload format Fixes #38755 --- plugins/platforms/feishu/adapter.py | 8 +- tests/gateway/test_feishu_outbound_payload.py | 111 ++++++++++++++++++ 2 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 tests/gateway/test_feishu_outbound_payload.py diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index 41e087069e85e..677919b641f1e 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -4527,13 +4527,7 @@ def _is_duplicate(self, message_id: str) -> bool: # ========================================================================= def _build_outbound_payload(self, content: str) -> tuple[str, str]: - # Feishu post-type 'md' elements do not render markdown tables; sending - # table content as post causes the message to appear blank on the client. - # Force plain text for anything that looks like a markdown table. - if _MARKDOWN_TABLE_RE.search(content): - text_payload = {"text": content} - return "text", json.dumps(text_payload, ensure_ascii=False) - if _MARKDOWN_HINT_RE.search(content): + if _MARKDOWN_HINT_RE.search(content) or _MARKDOWN_TABLE_RE.search(content): return "post", _build_markdown_post_payload(content) text_payload = {"text": content} return "text", json.dumps(text_payload, ensure_ascii=False) diff --git a/tests/gateway/test_feishu_outbound_payload.py b/tests/gateway/test_feishu_outbound_payload.py new file mode 100644 index 0000000000000..4f84e704e9fcd --- /dev/null +++ b/tests/gateway/test_feishu_outbound_payload.py @@ -0,0 +1,111 @@ +"""Tests for Feishu outbound payload routing — especially markdown table handling.""" + +import json +import sys +from pathlib import Path + +import pytest + +# Ensure the plugin adapter is importable +_repo = Path(__file__).resolve().parents[2] +if str(_repo) not in sys.path: + sys.path.insert(0, str(_repo)) + +from plugins.platforms.feishu.adapter import ( + FeishuAdapter, + _build_markdown_post_payload, + _MARKDOWN_TABLE_RE, +) + + +# --------------------------------------------------------------------------- +# Helper: extract the payload kind + parsed body from _build_outbound_payload +# --------------------------------------------------------------------------- + +def _route(content: str) -> tuple[str, dict]: + """Return (msg_type, parsed_json_body) for the given content.""" + adapter = object.__new__(FeishuAdapter) + msg_type, body_str = adapter._build_outbound_payload(content) + return msg_type, json.loads(body_str) + + +# --------------------------------------------------------------------------- +# _MARKDOWN_TABLE_RE regex +# --------------------------------------------------------------------------- + +class TestMarkdownTableRegex: + def test_simple_table(self): + t = "| a | b |\n|---|---|\n| 1 | 2 |" + assert _MARKDOWN_TABLE_RE.search(t) + + def test_table_with_heading_row(self): + t = "| Name | Score |\n| --- | --- |\n| Alice | 90 |" + assert _MARKDOWN_TABLE_RE.search(t) + + def test_no_match_on_plain_text(self): + assert not _MARKDOWN_TABLE_RE.search("hello world") + + def test_no_match_on_single_pipe_line(self): + assert not _MARKDOWN_TABLE_RE.search("use | as separator") + + +# --------------------------------------------------------------------------- +# _build_outbound_payload routing +# --------------------------------------------------------------------------- + +class TestOutboundPayloadRouting: + def test_table_routes_to_post(self): + """Tables must go through 'post' (md element), not plain text.""" + table = "| Name | Value |\n| --- | --- |\n| foo | bar |" + msg_type, body = _route(table) + assert msg_type == "post", f"expected post, got {msg_type}" + # The post body should contain an md element with the table text + md_text = body["zh_cn"]["content"][0][0]["text"] + assert "| Name | Value |" in md_text + + def test_heading_routes_to_post(self): + msg_type, _ = _route("# Hello") + assert msg_type == "post" + + def test_plain_text_stays_text(self): + msg_type, body = _route("just plain text") + assert msg_type == "text" + assert body["text"] == "just plain text" + + def test_mixed_table_and_heading(self): + content = "# Title\n\n| A | B |\n|---|---|\n| 1 | 2 |" + msg_type, body = _route(content) + assert msg_type == "post" + md_text = body["zh_cn"]["content"][0][0]["text"] + assert "| A | B |" in md_text + + def test_table_with_code_block(self): + content = "```\ncode\n```\n\n| X | Y |\n|---|---|\n| 1 | 2 |" + msg_type, body = _route(content) + assert msg_type == "post" + # Should have multiple rows (code block split) + assert len(body["zh_cn"]["content"]) >= 2 + + def test_empty_content(self): + msg_type, body = _route("") + assert msg_type == "text" + assert body["text"] == "" + + +# --------------------------------------------------------------------------- +# _build_markdown_post_payload +# --------------------------------------------------------------------------- + +class TestBuildMarkdownPostPayload: + def test_returns_valid_json(self): + result = _build_markdown_post_payload("hello") + parsed = json.loads(result) + assert "zh_cn" in parsed + assert "content" in parsed["zh_cn"] + + def test_table_in_md_element(self): + table = "| A | B |\n|---|---|\n| 1 | 2 |" + result = _build_markdown_post_payload(table) + parsed = json.loads(result) + md_text = parsed["zh_cn"]["content"][0][0]["text"] + assert "| A | B |" in md_text