Skip to content

fix(feishu): route markdown tables through post+tag:md instead of force-text - #53355

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

fix(feishu): route markdown tables through post+tag:md instead of force-text#53355
YzFrocket wants to merge 1 commit into
NousResearch:mainfrom
YzFrocket:fix/feishu-table-render

Conversation

@YzFrocket

Copy link
Copy Markdown

Summary

Feishu's post-type md element renders the full GFM surface today, including tables. The historic _MARKDOWN_TABLE_RE workaround in _build_outbound_payload predates that and force-downgrades the entire message to msg_type: text whenever a GFM table appears. That side-effect strips formatting from every other markdown element in the same message (headings, bold, code blocks, lists, quotes), so mixed-content replies render as raw markdown source on the client.

This PR routes table-bearing markdown through post + tag: md like every other markdown shape, removing the force-text branch.

Repro before this fix

Send any Feishu message that mixes a markdown table with other markdown:

## Some heading
Some **bold** prose.

| col1 | col2 |
|------|------|
| a    | b    |

Trailing text with `inline code`.

Result on the Feishu client: the whole message arrives as raw text — ##, **, |, fences all visible as characters. Same content without the table renders correctly as a post.

Root cause

plugins/platforms/feishu/adapter.py::FeishuAdapter._build_outbound_payload (line 4377 on main):

def _build_outbound_payload(self, content):
    if _MARKDOWN_TABLE_RE.search(content):
        # Force plain text for anything that looks like a markdown table.
        return "text", json.dumps({"text": content}, ensure_ascii=False)
    if _MARKDOWN_HINT_RE.search(content):
        return "post", _build_markdown_post_payload(content)
    return "text", json.dumps({"text": content}, ensure_ascii=False)

The first branch fires whenever the regex matches ^|...|\n|---|... anywhere in the message and demotes the whole reply to plain text — losing all other markdown for the sake of "rescuing" the table.

The original justification (Feishu's md tag didn't render tables) is no longer accurate: the official create_json doc lists tables under the tag: md example, and empirical testing on current Feishu clients (verified in the linked issue #27529) confirms tables render correctly inside post + md.

Fix

Send tables through post + tag: md like every other markdown shape:

def _build_outbound_payload(self, content):
    if _MARKDOWN_TABLE_RE.search(content) or _MARKDOWN_HINT_RE.search(content):
        return "post", _build_markdown_post_payload(content)
    return "text", json.dumps({"text": content}, ensure_ascii=False)

A pre-emptive text fallback is no longer needed: the send loop already handles a rejected post payload via _POST_CONTENT_INVALID_RE and degrades to plain text on both API exception and unsuccessful API response (existing code, _send_text_message around line 1801). So if Feishu ever regresses on table rendering inside posts, behaviour reverts to the current safety net automatically — but on success the receiver gets a properly-rendered post for the table and all surrounding markdown.

The detector _MARKDOWN_TABLE_RE itself is preserved (some metrics / logging paths may want to know a message contained a table). Only the routing changes. The stale comment block above the regex is updated to record the new contract.

Test Plan

  • New: test_outbound_payload_routes_markdown_tables_to_post_md pins the regression — a message containing a table plus heading + bold + trailing prose is emitted as a single post+md payload, not downgraded to text.
  • New: test_outbound_payload_keeps_plain_text_for_non_markdown guards the second branch so the change doesn't accidentally widen the post path to plain-text content (which would cost a richer-rendering payload for nothing).
  • Full tests/gateway/test_feishu.py suite — 207/207 passed.
  • Existing fallback tests (test_send_falls_back_to_text_when_post_payload_is_rejected, test_send_falls_back_to_text_when_post_response_is_unsuccessful, test_edit_message_falls_back_to_text_when_post_update_is_rejected) still pass — confirms the post-rejection safety net is intact for tables now too.

References

Not closing #27529 / #9549 — leaving it to maintainers' judgement whether this PR addresses them in full.

…ce-text

Feishu's post-type 'md' element renders the full GFM surface today,
including tables. The historic _MARKDOWN_TABLE_RE workaround in
_build_outbound_payload predates that and downgrades the *entire*
message to msg_type: text whenever a table is detected — which
strips formatting from every other markdown element in the same
message (headings, bold, code blocks, lists, quotes), making
mixed-content replies arrive as raw markdown source on the client.

Send tables through post + tag: md like every other markdown
shape. The existing call-site already handles a rejected post payload
via _POST_CONTENT_INVALID_RE and falls back to plain text, so we
do not need a pre-emptive force-text branch.

Tests:
- New: test_outbound_payload_routes_markdown_tables_to_post_md
  pins the regression — a message containing a table plus other
  markdown elements is emitted as a single post+md payload, not
  downgraded to text.
- New: test_outbound_payload_keeps_plain_text_for_non_markdown
  guards the second branch so we don't widen the post path to
  plain-text content for no reason.
- All 207 tests in tests/gateway/test_feishu.py pass.

References: NousResearch#27529 (proposed this exact fix), NousResearch#9549 (original bug
report on table rendering), NousResearch#26658 (alternative proposal to remove
the detector entirely).
@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins platform/feishu Feishu / Lark adapter P3 Low — cosmetic, nice to have labels Jun 27, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related/competing: this is the latest entrant in the long-standing Feishu markdown-table rendering cluster — same post+tag:md mechanism as open #29552 and #33800 (canonical #26108 is closed). Note this PR targets the relocated adapter at plugins/platforms/feishu/adapter.py while the earlier PRs touched gateway/platforms/feishu.py. Not marking a duplicate — these compete on the same fix and a maintainer should pick one.

@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 the focused Feishu fix. The functional direction is sound: current main still returns plain text for matching tables at plugins/platforms/feishu/adapter.py:4528, while Feishu’s current message-content documentation recommends post + md for GFM tables.

Problems

  • The new detector comment says it is retained for logging/metrics, but _MARKDOWN_TABLE_RE has no logging or metrics consumer; its remaining use is outbound routing (plugins/platforms/feishu/adapter.py:158-160, 4528).

Suggested changes

  • Describe the detector as selecting the post+md path for table-bearing content, rather than as a logging/metrics signal.

This is an automated hermes-sweeper review.

# workaround now strips formatting from every other element in the same
# message. We keep the detector so we can still observe table content for
# logging/metrics, but the outbound payload path uses post + tag:md.
_MARKDOWN_TABLE_RE = re.compile(r"^\|.*\|\n\|[-|: ]+\|", re.MULTILINE)

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.

The detector remains required by the routing condition below, but repository-wide search finds no logging or metrics consumer for it. Please remove that rationale and describe it as selecting the post+md route for table-bearing content.

@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 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have 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.

3 participants