Skip to content

fix(feishu): render Markdown tables as v2 cards instead of degrading to text - #37728

Open
315zuicai wants to merge 1 commit into
NousResearch:mainfrom
315zuicai:feat/feishu-markdown-table-as-card
Open

fix(feishu): render Markdown tables as v2 cards instead of degrading to text#37728
315zuicai wants to merge 1 commit into
NousResearch:mainfrom
315zuicai:feat/feishu-markdown-table-as-card

Conversation

@315zuicai

Copy link
Copy Markdown

Problem

Feishu IM post messages do not render Markdown tables — they appear blank or as raw | a | b | source characters on the client. Before this change, FeishuAdapter._build_outbound_payload reacted by force-degrading any table-bearing reply to plain text, which still surfaced the raw pipe/dash characters to the user.

This makes any agent that emits a markdown table (very common — comparison tables, status reports, structured data) look broken on Feishu.

Fix

Two parts:

A) gateway/platforms/feishu.py — when the outbound content contains a Markdown table, wrap it into a minimal v2 interactive card (schema: 2.0) whose body is one or more tag: markdown elements. Feishu's card markdown elements DO render tables correctly. If the card build raises for any reason, the existing plain-text fallback is preserved so the send pipeline cannot crash.

Two helpers are added:

  • _split_markdown_for_card_elements() — splits long content into chunks that fit Feishu's per-element soft limit (~3400 chars) and per-card element cap (40), respecting fenced code blocks.
  • _build_table_card_payload() — wraps the (chunked) markdown into the minimal card v2 envelope.

B) agent/prompt_builder.py — tightens the Feishu directive injected into the system prompt so the model knows which Markdown features Feishu IM actually renders. The previous wording (“Feishu renders Markdown … bold, italic, code blocks, and links are supported”) was misleading because it implied tables/headings work too. New wording explicitly lists what works (**bold**, *italic*, code, links, lists) and what doesn’t (tables, ATX headings, block quotes, hr), and recommends key-value bullet lists or bold lines as safe alternatives. This is best-effort steering — the code-side fix in (A) catches the cases where the model still emits a table.

Behaviour change

  • Before: a reply containing a Markdown table → user sees raw | a | b | source.
  • After: same reply → user sees a rendered interactive card with the table laid out properly.
  • No-table replies are unchanged (regression-tested).
  • Plain text is unchanged.

Tests

Adds 4 cases to TestFeishuAdapterMessaging in tests/gateway/test_feishu.py:

  1. test_build_outbound_payload_wraps_table_into_v2_card — table input produces ("interactive", <v2 card with tag:markdown elements>).
  2. test_build_outbound_payload_falls_back_to_text_when_card_build_fails_build_table_card_payload raising → returns ("text", ...) instead of crashing.
  3. test_build_outbound_payload_non_table_markdown_still_uses_post — non-table markdown still routes to the existing post path.
  4. test_build_outbound_payload_plain_text_unchanged — plain text stays as text.

All 361 existing tests in test_feishu.py + test_feishu_bot_admission.py + test_stream_consumer.py continue to pass.

Validation

Verified live in a real Feishu IM session: a reply containing a 3-row × 4-col Markdown table previously rendered as raw source; with this change it renders as an interactive card with the table laid out correctly.

Diff size

  • agent/prompt_builder.py: +13 / -2
  • gateway/platforms/feishu.py: +124 / -3 (two new module-level helpers + a small change in _build_outbound_payload)
  • tests/gateway/test_feishu.py: +78 / -0

…to text

Feishu IM 'post' messages do not render Markdown tables — a reply that
contains a `| a | b |` table previously surfaced the raw source characters
to the user, because `_build_outbound_payload` force-degraded any
table-bearing content to plain text.

This wraps replies containing a Markdown table into a minimal v2
interactive card with `tag: markdown` elements (which DO render tables
correctly on Feishu clients). If card construction raises, the existing
text fallback is preserved so the send pipeline cannot crash.

Also tightens the Feishu prompt directive in `agent/prompt_builder.py`
to explicitly enumerate which Markdown features Feishu IM does and does
not render, steering the model toward formats that survive the IM
renderer (e.g. bullet-list key-value pairs instead of pipe tables, bold
lines instead of ATX headings).

Tests: adds 4 cases to `TestFeishuAdapterMessaging` covering the table
→ card wrap, the build-failure fallback to text, the no-table
regression (still uses post), and the plain-text passthrough.
@alt-glitch alt-glitch added type/bug Something isn't working platform/feishu Feishu / Lark adapter comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jun 3, 2026

@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 addressing a real Feishu rendering regression. Current main still routes detected Markdown tables to raw text in plugins/platforms/feishu/adapter.py:4524-4534, so the underlying fix remains needed.

Problems

  • The new return "interactive", card_json at gateway/platforms/feishu.py:4433 has no matching API-rejection fallback. Current main only retries as text when msg_type == "post" (plugins/platforms/feishu/adapter.py:1913-1936), and edit_message() has the same post-only guard (:1963-1971). A rejected card can therefore fail delivery rather than preserve the stated fallback.
  • The adapter was moved to plugins/platforms/feishu/adapter.py by 560010547, so this needs a migration-aware salvage rather than a direct application.

Suggested changes

  • Port the routing/helpers to the bundled plugin and add interactive rejection fallback coverage for both send() and edit_message().
  • Update the tests to import the plugin adapter and exercise the send path in addition to payload construction.

Automated hermes-sweeper review.

return "text", json.dumps(text_payload, ensure_ascii=False)
try:
card_json = _build_table_card_payload(content)
return "interactive", card_json

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.

This changes the wire type to interactive, but the surrounding send and edit fallback logic is still guarded by msg_type == "post". Please add an interactive-card rejection fallback and a send-path test; otherwise a Feishu API rejection can fail delivery instead of degrading safely.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
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-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.

3 participants