feat(feishu): render markdown headings, tables, and horizontal rules … - #32455
feat(feishu): render markdown headings, tables, and horizontal rules …#32455lzcopyer wants to merge 1 commit into
Conversation
…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)
|
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 |
|
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
left a comment
There was a problem hiding this comment.
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-4479and:4526-4527). After a preceding heading,---inside that fence reaches the structural check at:4530-4534as non-code. gateway/platforms/feishu.py:4830-4834raises on every interactive exception, andsend()then falls back to text at:1804-1817; this bypasses the ordinary retry path for transient failures..gitignore:77concatenates two intended ignore patterns.- The adapter was relocated to
plugins/platforms/feishu/adapter.pyby5600105478ffde29d7566b45421b100eaa29c4ef, 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
.gitignorechange. - Coordinate schema/behavior with canonical PR #12114 and consolidation issue #27469.
Automated hermes-sweeper review.
| hermes_cli/tui_dist/* | ||
| hermes_cli/scripts/ | ||
| docs/superpowers/* No newline at end of file | ||
| docs/superpowers/*tinker-atropos/ |
There was a problem hiding this comment.
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("```"): |
There was a problem hiding this comment.
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": |
There was a problem hiding this comment.
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.
…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
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
postmessage 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:## Headingdiv+lark_mdbold| table |table{column_name: value}row objects per Feishu spec---hrmarkdownFenced 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
Changes Made
gateway/platforms/feishu.py(+183 lines)_MARKDOWN_HEADING_RE,_MARKDOWN_HR_RE) — detect headings and horizontal rules for routing to card format_build_outbound_payload()— routes headings/tables/HRs tointeractivecard instead oftextorpost_build_card_payload()— builds the card JSON with header + parsed elements_build_card_elements()(new static method) — parses markdown content intodiv/table/hr/markdownelements with code block tracking_build_card_table_element()(new static method) — converts pipe-table lines to Feishu's{name: value}row object formattests/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 elementtest_build_card_elements_preserves_hr_inside_code_blocks— verifies---in fenced blocks stays literaltest_send_uses_card_for_markdown_heading— verifies##routes to interactive card with div elementtest_send_uses_card_for_markdown_table— verifies table routing to cardtest_build_card_payload_custom_header— verifies custom card headertest_build_card_payload_exception_fallback— verifies fallback on card rejectiontest_edit_message_card_fallback_to_plain_text— verifies edit fallbackHow to Test
## Heading, pipe tables, and---:Test Heading
##)---or|are not splitRun tests:
python -m pytest tests/gateway/test_feishu.py -q(210 passed)Checklist
Code
pytest tests/gateway/test_feishu.pyand all 210 tests passDocumentation & Housekeeping
cli-config.yaml.example— N/ACONTRIBUTING.mdorAGENTS.md— N/AScreenshots / 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...