fix(feishu): route tables and code blocks to CardKit 2.0 with post/text fallback - #23861
fix(feishu): route tables and code blocks to CardKit 2.0 with post/text fallback#23861lqhl wants to merge 2 commits into
Conversation
Cherry-pick of PR 19038 plus fallback mechanism: - _TABLE_MARKDOWN_RE: detect GFM tables - _CODE_BLOCK_RE: detect fenced code blocks with 2plus content lines - _build_card_payload: CardKit 2.0 interactive card with tag:markdown - _build_outbound_payload: route tables and long code blocks to interactive - send() and edit_message(): fallback to plain text on interactive failure - _feishu_send_with_retry(): fail fast on interactive errors Fixes 19035 and 9549 (table and code block rendering in Feishu)
Minor: Comment/regex mismatch in
|
…drop redundant DOTALL
_CODE_BLOCK_RE uses {2,} to match 2+ content lines, but the comment claimed '3+ content lines'. Fix the comment to match the actual regex. Also remove re.DOTALL — the pattern already uses explicit \n, so DOTALL only adds unnecessary backtracking complexity.
|
@liuhao1024 Thanks for catching this! Fixed in 8a60abe:
Both changes are comment/cosmetic — the regex behavior is unchanged, 209/209 feishu tests still pass. |
|
@teknium1 hi, can you take a look? |
…ards Closes NousResearch#9549, NousResearch#19035. Supersedes NousResearch#23861. ## Problem Feishu's post-type 'md' element does not render GFM tables and truncates multi-line fenced code blocks. The current behaviour (force-text fallback, PR NousResearch#20275) avoids the blank-message symptom but leaks raw markdown source to the user — pipes, separators and code fences are visible as plain text. ## Approach Route content containing GFM tables or multi-line fenced code blocks to CardKit 2.0 interactive messages (schema: 2.0, tag: markdown), which render both natively. Plain markdown without tables/code stays in post/md as before. Falls back to plain text when the interactive card is rejected (bot lacks card permission, malformed payload, etc.) so failures degrade gracefully. ## ErrCode 11310: per-card table cap CardKit 2.0 caps the total number of GFM tables across an entire card at 5. Exceeding this triggers ErrCode 11310 ('card table number over limit') and the API rejects the whole card. The cap is per-CARD, not per-element — verified empirically against the live Feishu API on 2026-05-27. A single element with 6 tables fails. So do two elements with 4+2 tables, three elements with 2+2+2, and a layout with one table per element. Five tables in any layout pass. When content has more than 5 tables, _build_outbound_messages() splits it into multiple cards and send() iterates the resulting (msg_type, payload) list. Splits cut at section boundaries — the first paragraph break after the previous table block ends — so each table's heading and lead-in prose travel into the same card as the table itself, rather than being orphaned in the previous chunk. ## Tests tests/gateway/test_feishu.py::TestCardTableLimitSplitting - single card for ≤5 tables - two cards for 6 tables (5 + 1) - three cards for 12 tables (5 + 5 + 2) - non-table content keeps post/text routing untouched - prose between tables stays with the following table at split points ## Migration notes - Bots without card-send permission will silently fall back to plain text (existing fallback path, extended for the interactive case). - Edit_message remains on the single-payload path; streaming edits do not currently produce > 5 tables in a single chunk in practice.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the Feishu rendering work. The table issue is still present on current main: plugins/platforms/feishu/adapter.py:4524-4534 detects GFM tables and intentionally sends them as plain text.
Problems
- The PR changes
gateway/platforms/feishu.py, but the live adapter was relocated toplugins/platforms/feishu/adapter.pyby560010547; GitHub now reports this branch as conflicting. - Current main already handles fenced blocks by splitting them into dedicated post rows (
plugins/platforms/feishu/adapter.py:577-623, covered fromtests/gateway/test_feishu.py:2666). The CardKit requirement for that path should be re-established against the current implementation.
Suggested changes
- Port the table/card routing and fallback behavior to the plugin adapter, and update tests to import
plugins.platforms.feishu.adapter. - Keep focused table and fallback coverage; revalidate the code-block cases against the current row-splitting behavior.
This is an automated hermes-sweeper review.
| _MARKDOWN_FENCE_OPEN_RE = re.compile(r"^```([^\n`]*)\s*$") | ||
| _MARKDOWN_FENCE_CLOSE_RE = re.compile(r"^```\s*$") | ||
| _MENTION_RE = re.compile(r"@_user_\d+") | ||
| # GFM table: a row of |cells| followed by a separator row (---|:---|...) |
There was a problem hiding this comment.
This adapter path was relocated to plugins/platforms/feishu/adapter.py by 560010547; current main has no gateway/platforms/feishu.py. Please port this change and its tests to the plugin adapter rather than resolving the conflict against the deleted path.

Summary
Routes GFM tables and multi-line fenced code blocks to CardKit 2.0 interactive messages in Feishu, with automatic fallback to plain text when card delivery fails.
Problem
Feishu's
post/mdtag has two rendering limitations:| col1 | col2 |) is silently droppedSolution
Three new regex detectors + CardKit 2.0 builder + failure fallback:
_TABLE_MARKDOWN_RE— detects GFM tables (pipe-separated rows + alignment separator)_CODE_BLOCK_RE— detects fenced code blocks with 2+ content lines (short blocks stay in post/md)_build_card_payload()— wraps content in a CardKit 2.0 card withtag: markdownelement_build_outbound_payload()— routes detected content tointeractivemsg_typeFallback mechanism (not present in original PR #19038)
If the interactive card send fails (bot lacks card permission, API rejection, etc.), the message automatically falls back to plain text. Three layers of protection:
send()— catches send failures, retries as plain textsend()andedit_message()— catches API rejection without exception_feishu_send_with_retry()— interactive failures raise immediately (no retry loop)Testing
Changes
gateway/platforms/feishu.py: +62/-3 linestests/gateway/test_feishu.py: +114/-0 linesRelated
_should_send_as_card()trigger condition #9536, ## Problem When sending markdown table syntax (| col1 | col2 |) in Feishu, the message displays as plain text instead of being rendered as a table. This works correctly in OpenClaw. ## Environment - Hermes Agent version: [latest] - Feishu integration via WebSocket - Using native markdown table syntax in messages ## Expected behavior Markdown tables should render as formatted Feishu table messages, similar to how OpenClaw handles them. ## Screenshots [Attach screenshot comparing OpenClaw vs Hermes Agent rendering] ## Additional context The Feishu platform adapter usespostmsg_type with markdown, but the markdown hint regex may not be detecting table syntax properly. #7310