Skip to content

fix(feishu): render markdown tables and code blocks via CardKit 2.0 cards#33310

Open
huangyoje wants to merge 1 commit into
NousResearch:mainfrom
huangyoje:pr/feishu-table-cardkit-multi-card
Open

fix(feishu): render markdown tables and code blocks via CardKit 2.0 cards#33310
huangyoje wants to merge 1 commit into
NousResearch:mainfrom
huangyoje:pr/feishu-table-cardkit-multi-card

Conversation

@huangyoje

Copy link
Copy Markdown

Closes #9549. Closes #19035. Supersedes #23861.

Problem

Feishu's post-type md element 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 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 — 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:

layout result
1 element × 5 tables
1 element × 6 tables ✗ 11310
2 elements × (4+2) tables ✗ 11310
2 elements × (5+1) tables ✗ 11310
5 elements × 1 table each
8 elements × 1 table each ✗ 11310

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 (7 cases):

  • table content routes to interactive card (not text)
  • 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
  • multi-line fenced code block routes to interactive card

Full test suite (tests/gateway/test_feishu.py): 208 passed.

Migration notes

  • Bots without card-send permission silently fall back to plain text (existing fallback path, extended for the interactive case). No new permission required vs post/md.
  • edit_message remains on the single-payload path; streaming edits do not currently produce > 5 tables in a single chunk in practice.
  • The single-content-line code block case (e.g. ```json\n{"x":1}\n```) intentionally stays in post/md because it renders fine there — only blocks with two or more code lines route to the interactive card. This preserves the existing test_send_splits_fenced_code_blocks_into_separate_post_rows behaviour.

@alt-glitch alt-glitch added type/bug Something isn't working platform/feishu Feishu / Lark adapter comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels May 27, 2026
@huangyoje

Copy link
Copy Markdown
Author

Cross-linking #32488 (h/t @haozicnm), which targets the same issue (#9549) via the same Schema 2.0 markdown element approach. Wanted to surface the relationship and key trade-offs so reviewers can pick a direction.

Where this PR overlaps with #32488

Both route GFM tables / multi-line code blocks from force-text to a Schema 2.0 interactive card with a markdown element.

Where it differs

Table-count handling — load-bearing user-visible difference

#32488 this PR (#33310)
Per-card limit 3 5
Excess tables Wrapped into fenced code blocks (markdown source becomes user-visible) Split into multiple cards (every table renders normally)
Limit source "verified 2026-03 by openclaw + cc-haha" (no API trace) Empirical against live Feishu API on 2026-05-27

I just re-ran the test against the live API:

N=3:  code=0   success
N=4:  code=0   success
N=5:  code=0   success
N=6:  code=230099  ErrCode 11310 'card table number over limit'

So the real per-card cap is 5, and #32488's _FEISHU_CARD_TABLE_LIMIT = 3 is overly conservative — it forces tables 4-5 to render as raw markdown source even though the API would accept them.

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.

Scope

This PR is intentionally narrow — one regex + table-count split + interactive→text fallback. #32488 also bundles three orthogonal changes that I think deserve separate PRs:

  • HTML escape (& < >) for card content
  • A markdown optimisation pipeline (heading downgrade, <br> spacing, blank-line compression)
  • A rewrite of _build_markdown_post_rows to switch from tag: md to tag: text + inline parser

I tested HTML escape against a live card today — Feishu's CardKit 2.0 markdown element does not interpret <script>, <template>, or stray < / > as HTML; they all render as literal text without escaping. So the escape pass is defensive but not strictly required for safety on the outbound path.

Happy to defer

If 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 _FEISHU_CARD_TABLE_LIMIT = 5, not 3 — the 3 cap visibly degrades UX for content the API would otherwise accept.

haozicnm pushed a commit to haozicnm/hermes-agent that referenced this pull request May 27, 2026
…飞书平台网关新增了大量代码,属于功能增强而非简单修复或重构。

建议的 commit message:

`feat: 飞书平台网关功能增强`

(注:由于具体的 diff 代码内容未提供,此消息基于文件名和变更量推断。如需更精准的描述,请补充完整的 diff 内容。)
fix(feishu): bump card table limit from 3 to 5

Empirical test against live Feishu API (2026-05-28):
- N=3,4,5: code=0 success
- N=6: code=230099 'card table number over limit'

Ref: PR NousResearch#33310 comment by @huangyoje
@haozicnm

Copy link
Copy Markdown

Thanks for the thorough testing! I've confirmed your findings against the live API:

N=3: code=0   success
N=4: code=0   success
N=5: code=0   success
N=6: code=230099  card table number over limit

Updated #32488 to _FEISHU_CARD_TABLE_LIMIT = 5. Also fixed a bug where _build_table_card_payload was using the pre-sanitisation escaped variable instead of optimised, which meant the excess-table-to-codeblock conversion was silently discarded.

Agree on HTML escaping being defensive — Feishu's CardKit 2.0 markdown element treats </> as literal text. Keeping it for belt-and-suspenders since it's zero-cost.

Happy to coordinate on whichever approach maintainers prefer.

…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.
@huangyoje huangyoje force-pushed the pr/feishu-table-cardkit-multi-card branch from d72c7ad to c3ce969 Compare June 6, 2026 15:46
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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feishu] Code blocks cannot be expanded — only first ~2 lines visible [Feishu] Markdown tables not rendering in Feishu messages

3 participants