Skip to content

fix(feishu): route markdown through Card 2.0 interactive cards - #45036

Open
Mr8rock wants to merge 1 commit into
NousResearch:mainfrom
Mr8rock:fix/feishu-markdown-card-rendering
Open

fix(feishu): route markdown through Card 2.0 interactive cards#45036
Mr8rock wants to merge 1 commit into
NousResearch:mainfrom
Mr8rock:fix/feishu-markdown-card-rendering

Conversation

@Mr8rock

@Mr8rock Mr8rock commented Jun 12, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes a long-standing bug where Feishu (Lark) outbound messages lose
all markdown formatting except links. The wrong markdown renderer was
being used — the stripped-down tag: "md" post element instead of
the full-featured tag: "markdown" inside a Card 2.0.

Symptom

Sending a markdown table from Hermes to a Feishu chat rendered the
table as a code block (with line numbers, no borders, no column
alignment), lists and code blocks appeared as literal text, and
even bold/italic only worked inconsistently.

Root cause

Feishu has two markdown renderers and Hermes was using the wrong one:

Element Container Renderer What works
tag: "md" post type stripped links, partial bold
tag: "markdown" Card 2.0 body full CommonMark everything (tables, code, lists, etc.)

The code path was sending post elements for most markdown and
tag: "lark_md" (a Card 1.0 element, not v2) wrapped in a tag: "div"
for tables. Both are partial renderers — Card 1.0 is particularly
incomplete (no real tables, line numbers on code blocks).

Fix

Route all markdown through Card 2.0 ("schema": "2.0") using
tag: "markdown" in body.elements, sent with msg_type: "interactive".
This matches the approach used by the OpenClaw Feishu integration
(buildMarkdownCard in dist/send-DrvJaWvR.js) and aligns Hermes
with what the official Feishu plugins do.

{
  "msg_type": "interactive",
  "content": {
    "schema": "2.0",
    "config": {"width_mode": "fill"},
    "body": {
      "elements": [{"tag": "markdown", "content": "<original markdown>"}]
    }
  }
}

Fallback path updated

The post→text fallback on 230001 content format … is incorrect also
covers the new msg_type: "interactive" path and the regex was
broadened to match the card variant of the error. The retry loop
stops on invalid content (no point retrying the same broken payload).

Cleanup

The legacy _build_markdown_post_rows walker (heading/text/style
conversion, fenced-code → code_block post row, table → code_block
hack) is no longer used by _build_outbound_payload and is kept
only because _build_post_payload is still referenced by the
image-with-caption path. The dispatch path is now a clean two-way
choice: text for plain strings, interactive (Card 2.0) for
anything that contains a markdown hint.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✅ Tests (added + updated 7 tests covering the new card path)

Changes Made

  • gateway/platforms/feishu.py
    • New _build_interactive_card_payload(content) helper
    • _build_outbound_payload simplified: markdown → ("interactive", card);
      plain text → ("text", {"text": ...})
    • Fallback paths (3 sites) now trigger on interactive as well as post
    • _POST_CONTENT_INVALID_RE extended to match card error variants
    • _feishu_send_with_retry no longer retries on invalid content
  • tests/gateway/test_feishu.py
    • TestFeishuMarkdownTableRendering rewritten for Card 2.0 assertions
    • 6 pre-existing tests updated from msg_type="post" row checks to
      msg_type="interactive" card checks
    • 3 new test cases: schema 2.0 enforcement, no lark_md regression,
      width_mode=fill config

How to Test

  1. In a Feishu chat connected to Hermes, send a message containing
    a markdown table — it should now render as a real table with
    borders, column alignment, and no line numbers.
  2. Lists (- item), fenced code blocks (```python), and
    **bold** / *italic* / ~~strike~~ should all render correctly
    in the same message.
  3. Mixed content (heading + table + list) should all render in one
    card.
  4. Plain text without any markdown should still dispatch to
    msg_type: "text", not get wrapped in a card.
cd ~/.hermes/hermes-agent
venv/bin/python -m pytest tests/gateway/test_feishu.py -o addopts=""
# → 212 passed

Acknowledgements

Cribbed the Card 2.0 structure from the OpenClaw Feishu integration
(buildMarkdownCard / sendCardFeishu in dist/send-DrvJaWvR.js).
That implementation correctly uses the full-featured markdown
renderer; matching it here brings Hermes to parity with the official
Feishu plugin behaviour observed in production.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(feishu):)
  • I searched for existing PRs — none duplicate this fix
  • My PR contains only changes related to this fix
  • pytest tests/gateway/test_feishu.py — 212/212 pass
  • I added tests for my changes (7 tests added/updated)
  • Tested on Ubuntu 26.04, Python 3.11.15

Documentation & Housekeeping

  • N/A — no docs/AGENTS.md/CONTRIBUTING.md changes needed; the
    fix is internal to the Feishu adapter and the existing module
    docstrings still describe the behavior accurately
  • N/A — no new config keys
  • N/A — no architecture/workflow changes
  • N/A — Python-only, no cross-platform impact
  • N/A — no tool schema changes

Feishu has two markdown renderers and the wrong one was being used for
outbound messages. The 'tag: "md"' post element is a stripped-down
renderer that only handles links and partial bold — every other
markdown element (tables, code blocks, lists, headings, italic,
strikethrough, blockquote) is rendered as literal characters. The
'tag: "markdown"' element inside an interactive card v2.0 is the
full-featured renderer, but it was never used.

As a result, Feishu users were seeing:
  - tables rendered as a code block (the workaround) with line
    numbers, no borders, no column alignment
  - lists, code blocks, blockquotes appearing as literal text
  - even bold/italic only working inconsistently

The fix: wrap any message that contains markdown in a Card 2.0
('schema': '2.0') and send it with msg_type 'interactive' using
'tag: "markdown"' in the body. The card's renderer is the
CommonMark-compliant one and renders everything correctly.

This matches the approach used by the OpenClaw Feishu integration
(docs/dist/send-DrvJaWvR.js, buildMarkdownCard) and aligns Hermes
with what the official Feishu plugins do.

Fallback paths updated:
  - the invalid-content regex now also matches card rejection
    messages ("invalid card content", "invalid content format")
  - the post->text fallback also handles interactive->text so a
    malformed card doesn't fail the whole send
  - the _feishu_send_with_retry inner loop stops retrying on
    invalid card content instead of attempting N retries

Tests: 212/212 pass in tests/gateway/test_feishu.py (was 211;
updated 6 pre-existing tests that asserted the old post-row
structure to assert the new card structure, plus added Card 2.0
schema assertions).
@Mr8rock
Mr8rock marked this pull request as ready for review June 12, 2026 16:02
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Jun 12, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #12114 (the canonical Feishu markdown/table card-rendering PR). This enters a heavily saturated cluster of 70+ competing PRs (tracked in #27469) all routing Feishu markdown through interactive Card 2.0. Linking for reviewer consolidation.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Card 2.0 investigation. The underlying issue is still present on current main: plugins/platforms/feishu/adapter.py:4524-4534 downgrades detected tables to text and otherwise sends Markdown as post md.

Problems

  • The production change is against gateway/platforms/feishu.py, but commit 5600105478ffde29d7566b45421b100eaa29c4ef moved the active adapter to plugins/platforms/feishu/adapter.py. This needs a manual port, not a clean cherry-pick.
  • The diff also rewrites _build_markdown_post_rows. That helper remains live for media captions through _build_post_payload() and _build_media_post_payload() (plugins/platforms/feishu/adapter.py:5008-5015), although normal Markdown would no longer use it after the proposed routing change.

Suggested changes

  • Port only the Card 2.0 outbound routing and corresponding send/edit fallback handling to the active plugin adapter first.
  • Preserve the post-row builder unless caption formatting is intentionally in scope; add a media-caption regression test if retaining that rewrite.

This is an automated hermes-sweeper review.

@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 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 duplicate This issue or pull request already exists 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