fix(feishu): render markdown tables and code blocks via CardKit 2.0 cards - #33310
fix(feishu): render markdown tables and code blocks via CardKit 2.0 cards#33310huangyoje wants to merge 1 commit into
Conversation
|
Cross-linking #32488 (h/t @haozicnm), which targets the same issue (#9549) via the same Schema 2.0 Where this PR overlaps with #32488Both route GFM tables / multi-line code blocks from Where it differsTable-count handling — load-bearing user-visible difference
I just re-ran the test against the live API: So the real per-card cap is 5, and #32488's For >5 tables, #32488 keeps the first 3 as tables and dumps the rest as code blocks; this PR splits the content into N cards so every table still renders. Users get the same visual output regardless of how many tables the LLM produces. ScopeThis PR is intentionally narrow — one regex + table-count split + interactive→text fallback. #32488 also bundles three orthogonal changes that I think deserve separate PRs:
I tested HTML escape against a live card today — Feishu's CardKit 2.0 markdown element does not interpret Happy to deferIf maintainers prefer #32488's bundled approach, this PR can close. If a narrower fix is preferred, this one's ready. The single thing I'd advocate for either way is |
|
Thanks for the thorough testing! I've confirmed your findings against the live API: Updated #32488 to Agree on HTML escaping being defensive — Feishu's CardKit 2.0 markdown element treats Happy to coordinate on whichever approach maintainers prefer. |
d72c7ad to
c3ce969
Compare
c3ce969 to
07a3793
Compare
…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.
07a3793 to
7092e30
Compare
|
Rebased onto the latest main (no conflicts). Just noting this is still awaiting review — happy to address any feedback. The |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused investigation and for documenting the live five-table limit.
Problems
- The premise remains valid on current main:
plugins/platforms/feishu/adapter.py:4524-4530still routes detected GFM tables to a plaintextpayload. However, the PR changesgateway/platforms/feishu.py, which was moved toplugins/platforms/feishu/adapter.pyby5600105478ffde29d7566b45421b100eaa29c4ef. - In the proposed multi-card loop, the interactive fallback uses the original
chunk(gateway/platforms/feishu.py:1808in this diff), not the failed card’s segment. A failure on a later card would resend the complete original response as plain text, duplicating earlier cards.
Suggested changes
- Port the change to the bundled plugin and pair each generated card with its source segment so fallback is per-card.
- Add a test where a later card in a >5-table response is rejected and verify its fallback contains only that card’s content.
Automated hermes-sweeper review.
| except Exception as exc: | ||
| if msg_type == "interactive": | ||
| logger.warning("[Feishu] Interactive card send failed (exc=%r); falling back to plain text", exc) | ||
| response = await self._feishu_send_with_retry( |
There was a problem hiding this comment.
This fallback uses the full original chunk, even though the enclosing loop sends individual card payloads. If a later card from a >5-table response is rejected, this resends all earlier tables as raw text. Keep the source segment alongside each (msg_type, payload) result and fall back from that segment only.
|
Closing in favor of the After running both solutions locally for a few weeks,
The |
Closes #9549. Closes #19035. Supersedes #23861.
Problem
Feishu's post-type
mdelement does not render GFM tables and truncates multi-line fenced code blocks. The current behaviour (#20275, force-text fallback) avoids the blank-message symptom but leaks raw markdown source to the user — pipes, separators and code fences are visible as plain text.#23861 partially addressed this by routing tables/code blocks to CardKit 2.0, but it does not handle the per-card table count limit (see below) and stalled in review. This PR is a more complete take.
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 inpost/mdas before.Falls back to plain text when the interactive card is rejected (bot lacks card permission, malformed payload, etc.) so failures degrade gracefully — no regression vs current behaviour for misconfigured bots.
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:
When content has more than 5 tables,
_build_outbound_messages()splits it into multiple cards andsend()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(7 cases):Full test suite (
tests/gateway/test_feishu.py): 208 passed.Migration notes
post/md.edit_messageremains on the single-payload path; streaming edits do not currently produce > 5 tables in a single chunk in practice.```json\n{"x":1}\n```) intentionally stays inpost/mdbecause it renders fine there — only blocks with two or more code lines route to the interactive card. This preserves the existingtest_send_splits_fenced_code_blocks_into_separate_post_rowsbehaviour.