Skip to content

feat(feishu): render markdown headings, tables, and horizontal rules … - #32455

Open
lzcopyer wants to merge 1 commit into
NousResearch:mainfrom
lzcopyer:feat/feishu-card-rendering
Open

feat(feishu): render markdown headings, tables, and horizontal rules …#32455
lzcopyer wants to merge 1 commit into
NousResearch:mainfrom
lzcopyer:feat/feishu-card-rendering

Conversation

@lzcopyer

@lzcopyer lzcopyer commented May 26, 2026

Copy link
Copy Markdown

…as interactive cards

Feishu post-type 'md' elements do not support headings (##), pipe tables (|...|), or horizontal rules (---). Previously, these were either stripped to plain text or rendered incorrectly.

This change routes messages containing headings, tables, or HRs to Feishu interactive cards, which support native components:

  • Heading → div element with lark_md bold text

  • | table | → native table element ({name: value} row objects)
  • --- → hr element
  • Other markdown → markdown element (preserves code blocks, lists, etc.)

Fenced code blocks are tracked so | and --- inside them are preserved as literal text rather than being split into separate elements.

Also adds detailed API error logging when interactive cards are rejected, showing the error code and message for easier debugging.

Tests: 210 feishu adapter tests pass (added 7 new card rendering tests)

What does this PR do?

Fixes Feishu message rendering for Markdown headings, pipe tables, and horizontal rules. Previously, these Markdown elements were either stripped to plain text or rendered incorrectly because Feishu's post message type does not support them.

This PR routes messages containing headings (##), pipe tables (|...|), or horizontal rules (---) to Feishu interactive cards, where they are rendered using native card components:

Markdown Card Component Notes
## Heading div + lark_md bold Headings become bold text in a div
| table | native table Uses {column_name: value} row objects per Feishu spec
--- hr Native horizontal rule
Other markdown markdown Code blocks, lists, bold, italic, links preserved

Fenced code blocks are tracked so --- and | inside them are preserved as literal text — they are not falsely split into separate card elements.

Related Issue

N/A (no existing issue)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

gateway/platforms/feishu.py (+183 lines)

  1. New regex patterns (_MARKDOWN_HEADING_RE, _MARKDOWN_HR_RE) — detect headings and horizontal rules for routing to card format
  2. _build_outbound_payload() — routes headings/tables/HRs to interactive card instead of text or post
  3. _build_card_payload() — builds the card JSON with header + parsed elements
  4. _build_card_elements() (new static method) — parses markdown content into div/table/hr/markdown elements with code block tracking
  5. _build_card_table_element() (new static method) — converts pipe-table lines to Feishu's {name: value} row object format
  6. Send/update fallback handling — when interactive cards are rejected, falls back to plain text instead of failing
  7. Enhanced error logging — logs API error code + message when interactive cards are rejected

tests/gateway/test_feishu.py (+66 lines)

Added 7 new tests and updated 2 existing tests:

  • test_build_card_elements_converts_horizontal_rules_to_hr — verifies HR → hr element
  • test_build_card_elements_preserves_hr_inside_code_blocks — verifies --- in fenced blocks stays literal
  • test_send_uses_card_for_markdown_heading — verifies ## routes to interactive card with div element
  • test_send_uses_card_for_markdown_table — verifies table routing to card
  • test_build_card_payload_custom_header — verifies custom card header
  • test_build_card_payload_exception_fallback — verifies fallback on card rejection
  • test_edit_message_card_fallback_to_plain_text — verifies edit fallback

How to Test

  1. Send a Feishu message containing ## Heading, pipe tables, and ---:
    Test Heading
A B
1 2

  1. Verify the heading renders as bold text (not raw ##)
  2. Verify the table renders as a native Feishu table
  3. Verify the horizontal rule renders as a separator line
  4. Verify code blocks containing --- or | are not split
    Run tests: python -m pytest tests/gateway/test_feishu.py -q (210 passed)

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature
  • I've run pytest tests/gateway/test_feishu.py and all 210 tests pass
  • I've added tests for my changes (7 new + 2 updated)
  • I've tested on my platform: Ubuntu 24.04 (WSL), Feishu 7.x

Documentation & Housekeeping

  • I've updated relevant documentation — docstrings added for all new methods
  • I've updated cli-config.yaml.example — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md — N/A
  • I've considered cross-platform impact — N/A (Feishu-specific)
  • I've updated tool descriptions/schemas — N/A

Screenshots / Logs

Gateway logs showing successful interactive card delivery (no rejection/fallback):
2026-05-26 14:28:09 INFO [Feishu] Connected in websocket mode
...no "Interactive card rejected" errors in logs...

The card is accepted by Feishu API and renders correctly with native table, bold headings, and horizontal rules.

…as interactive cards

Feishu post-type 'md' elements do not support headings (##),
pipe tables (|...|), or horizontal rules (---). Previously, these
were either stripped to plain text or rendered incorrectly.

This change routes messages containing headings, tables, or HRs to
Feishu interactive cards, which support native components:

- ## Heading → div element with lark_md bold text
- | table | → native table element ({name: value} row objects)
- --- → hr element
- Other markdown → markdown element (preserves code blocks, lists, etc.)

Fenced code blocks are tracked so | and --- inside them are preserved
as literal text rather than being split into separate elements.

Also adds detailed API error logging when interactive cards are
rejected, showing the error code and message for easier debugging.

Tests: 210 feishu adapter tests pass (added 7 new card rendering tests)
@lzcopyer

Copy link
Copy Markdown
Author

Fixes Feishu message rendering for Markdown headings, pipe tables, and horizontal rules. Previously, these Markdown elements were either stripped to plain text or rendered incorrectly because Feishu's post message type does not support them.

@lzcopyer lzcopyer closed this May 26, 2026
@lzcopyer lzcopyer reopened this May 26, 2026
@alt-glitch alt-glitch added P2 Medium — degraded but workaround exists type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter labels May 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #12114 (canonical Feishu markdown table → card rendering PR), tracked by consolidation issue #27469.

This is part of a 70+ PR cluster all implementing Feishu interactive card rendering for markdown tables/headings. See #27469 for the full list of competing PRs (#25453, #29630, #31056, #30928, #31038, #31804, #32488, etc.).

@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 tackling a real Feishu rendering gap. Current main still routes detected pipe tables to plain text at plugins/platforms/feishu/adapter.py:4524-4530, so the underlying need remains.

Problems

  • The parser toggles fenced-code state twice for a leading fence (gateway/platforms/feishu.py:4478-4479 and :4526-4527). After a preceding heading, --- inside that fence reaches the structural check at :4530-4534 as non-code.
  • gateway/platforms/feishu.py:4830-4834 raises on every interactive exception, and send() then falls back to text at :1804-1817; this bypasses the ordinary retry path for transient failures.
  • .gitignore:77 concatenates two intended ignore patterns.
  • The adapter was relocated to plugins/platforms/feishu/adapter.py by 5600105478ffde29d7566b45421b100eaa29c4ef, so this needs a deliberate port rather than a clean cherry-pick.

Suggested changes

  • Port the implementation/tests to the plugin path, fix the one-toggle-per-fence state machine, preserve retries except for confirmed card rejections, and remove the .gitignore change.
  • Coordinate schema/behavior with canonical PR #12114 and consolidation issue #27469.

Automated hermes-sweeper review.

Comment thread .gitignore
hermes_cli/tui_dist/*
hermes_cli/scripts/
docs/superpowers/* No newline at end of file
docs/superpowers/*tinker-atropos/

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 concatenates docs/superpowers/* and tinker-atropos/ into one pattern, so neither intended directory is ignored independently. Please remove this unrelated line or restore two newline-separated patterns.

s = lines[j].strip()

# Toggle code block state so | and --- inside fences are literal.
if s.startswith("```"):

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 toggles the same leading fence already toggled at line 4478. If a heading precedes a fenced block, the opening fence is toggled twice and --- within it reaches the structural-boundary branch as non-code. Track each fence exactly once and add that regression case.

last_error = exc
if msg_type == "post" and _POST_CONTENT_INVALID_RE.search(str(exc)):
raise
if msg_type == "interactive":

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.

Do not classify every interactive exception as a card rejection. This bypasses the retry logic below for transient transport failures, and send() then emits a text fallback. Restrict immediate fallback to verified card-validation failures or retain normal retries first.

@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
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/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants