Skip to content

fix(feishu): re-enable post-mode rendering for markdown tables - #29166

Open
John15Wil wants to merge 1 commit into
NousResearch:mainfrom
John15Wil:fix/feishu-table-render
Open

fix(feishu): re-enable post-mode rendering for markdown tables#29166
John15Wil wants to merge 1 commit into
NousResearch:mainfrom
John15Wil:fix/feishu-table-render

Conversation

@John15Wil

Copy link
Copy Markdown

Summary

Feishu's post-type md elements now correctly render GFM markdown tables as of mid-May 2026 — the server-side parser was updated to handle the table syntax. The earlier defensive fallback (commit 8e18d10, 2026-04-22) that forced text-mode for any content matching _MARKDOWN_TABLE_RE is no longer needed and is in fact harmful: tables now appear as raw markdown source instead of rendered grids.

Empirical verification

Sent a 3×3 table directly via the Feishu post API as {"tag":"md", "text":"| A | B | C |\n|---|---|---|\n| 1 | 2 | 3 |\n| 4 | 5 | 6 |"} — Feishu rendered it as a proper bordered table on the client.

After applying this patch and restarting the gateway, sending the same table through the agent → gateway pipeline now renders as a proper table grid in Feishu instead of raw | col | source.

Change

Collapses the explicit _MARKDOWN_TABLE_RE check into the existing _MARKDOWN_HINT_RE branch so tables go through the same post pipeline as other markdown:

-        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)

Test plan

  1. Restart Feishu gateway with this patch applied
  2. Send any message containing a GFM markdown table
  3. Verify it renders as a bordered table grid in Feishu (not raw markdown source)

Reverts the over-defensive workaround from #8e18d10.

@buwenzheng

Copy link
Copy Markdown

Hi @John15Wil — I independently arrived at the same fix and just dropped my duplicate (#50640). Thanks for getting here first!

One thing that might help this land: there's no test coverage yet, and reviewers have flagged the recurring nature of this bug (#26108 was closed by its author with "open too long, no longer relevant" despite three contributors verifying it worked). A regression test for the routing decision could give the next reviewer something concrete to point to.

Here are the 5 tests I wrote for #50640 — they target _build_outbound_payload directly so they exercise the routing decision, not the post-rendering pipeline. The key assertion is test_markdown_table_routes_to_post, which would have caught any future regression that re-introduces the msg_type=text downgrade.

Feel free to lift these into your PR if useful (MIT-licensed, no attribution needed):

tests/gateway/test_feishu_outbound_routing.py
"""Tests for `FeishuAdapter._build_outbound_payload` routing.

Covers the fix for hermes-agent #26658 / #27529: messages containing markdown
tables should be sent as ``post`` + ``tag: md`` (which Feishu renders
correctly across all current clients), not force-downgraded to plain
``text``.
"""

import json
import unittest


class TestOutboundPayloadRouting(unittest.TestCase):
    def _make_adapter(self):
        from gateway.config import PlatformConfig
        from plugins.platforms.feishu.adapter import FeishuAdapter

        return FeishuAdapter(PlatformConfig())

    def test_plain_text_routes_to_text(self):
        adapter = self._make_adapter()
        msg_type, payload = adapter._build_outbound_payload("Just a plain line.")
        self.assertEqual(msg_type, "text")
        self.assertEqual(json.loads(payload)["text"], "Just a plain line.")

    def test_markdown_without_table_routes_to_post(self):
        adapter = self._make_adapter()
        content = "Some **bold** text and a [link](https://example.com)."
        msg_type, _ = adapter._build_outbound_payload(content)
        self.assertEqual(msg_type, "post")

    def test_markdown_table_routes_to_post(self):
        """Regression test for #26658 / #27529.

        Tables must go to ``post`` (not ``text``) so Feishu renders them
        properly using the ``tag: md`` element. Forcing ``text`` was the
        old workaround for a Feishu rendering bug that has since been
        fixed upstream.
        """
        adapter = self._make_adapter()
        content = (
            "Look at this:\n\n"
            "| a | b |\n"
            "|---|---|\n"
            "| 1 | 2 |\n"
        )
        msg_type, payload = adapter._build_outbound_payload(content)
        self.assertEqual(msg_type, "post")
        # The post payload should carry the table text verbatim inside a
        # ``tag: md`` element so Feishu's renderer can format it.
        decoded = json.loads(payload)
        rows = decoded["zh_cn"]["content"]
        rendered = json.dumps(rows, ensure_ascii=False)
        self.assertIn("| a | b |", rendered)
        self.assertIn('"tag": "md"', rendered)

    def test_table_only_message_still_routes_to_post(self):
        """Table-only content (no surrounding prose) also goes to post."""
        adapter = self._make_adapter()
        content = (
            "| col1 | col2 |\n"
            "|------|------|\n"
            "| x    | y    |\n"
        )
        msg_type, _ = adapter._build_outbound_payload(content)
        self.assertEqual(msg_type, "post")

    def test_table_with_inline_markdown_routes_to_post(self):
        adapter = self._make_adapter()
        content = (
            "Summary below:\n\n"
            "| 维度 | 状态 |\n"
            "|------|------|\n"
            "| **粗体** | [链接](https://example.com) |\n\n"
            "End of summary."
        )
        msg_type, _ = adapter._build_outbound_payload(content)
        self.assertEqual(msg_type, "post")


if __name__ == "__main__":
    unittest.main()

Note: my branch was based on current main where the adapter has moved to plugins/platforms/feishu/adapter.py as part of the bundled-plugin migration. Your PR targets the pre-migration path gateway/platforms/feishu.py — depending on when you rebase, the test's from plugins.platforms.feishu.adapter import FeishuAdapter may need to be from gateway.platforms.feishu import FeishuAdapter. Other than that the tests should drop in cleanly.

Verified the underlying fix works end-to-end on macOS 26.4.1 / Feishu v7.x desktop+mobile (Feishu v1.0.39+ renders tables in post + tag:md correctly). 👍

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying the table-routing regression. The underlying routing branch is still present on current main, but this patch needs a small salvage before it can affect the active adapter.

Problems

  • gateway/platforms/feishu.py was deleted by bundled-plugin migration commit 476d8d9ccbee1b36d8fb6f4fabc0081c3e996cd2. Current Feishu sends call plugins/platforms/feishu/adapter.py:1904, whose _build_outbound_payload() still forces matching tables to text at plugins/platforms/feishu/adapter.py:4528.
  • No regression test accompanies the routing change. Existing post-routing coverage at tests/gateway/test_feishu.py:2693-2732 covers inline markdown, not tables.

Suggested changes

  • Apply the same conditional to plugins/platforms/feishu/adapter.py:4524-4534.
  • Add direct routing assertions for plain text, table-only content, and mixed markdown/table content, including the generated tag: md payload.

Automated hermes-sweeper review.

# Markdown tables render correctly in post-type 'md' elements as of
# mid-May 2026 (Feishu server-side now parses GFM tables). The earlier
# forced-text-mode fallback (commit 8e18d10) is no longer needed —
# tables now go through the post path with the rest of markdown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current main deleted this adapter in 476d8d9ccbee1b36d8fb6f4fabc0081c3e996cd2; the live method is now plugins/platforms/feishu/adapter.py:4524. Please salvage this conditional into that active plugin path so it changes shipped behavior.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Feishu's post-type 'md' elements now correctly render GFM tables as of
mid-May 2026 — the server-side parser was updated to handle the table
syntax. The earlier defensive fallback (commit 8e18d10, 2026-04-22) that
forced text-mode for any content matching _MARKDOWN_TABLE_RE is no
longer needed and is in fact harmful: tables now appear as raw markdown
source instead of rendered grids.

Empirically verified by sending a 3x3 table directly via the Feishu
post API — it renders as a proper bordered table on the client.

This change collapses the table check into the existing markdown-hint
branch so tables go through the same post pipeline as other markdown.

Test: send a message containing a markdown table — it should render as
a proper table grid in Feishu instead of raw '| col |' source.
@John15Wil
John15Wil force-pushed the fix/feishu-table-render branch from fc6c03e to ee2263c Compare July 14, 2026 07:24
@John15Wil

John15Wil commented Jul 14, 2026 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/feishu Feishu / Lark adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants