Skip to content

fix(feishu): route tables and code blocks to CardKit 2.0 with post/text fallback - #23861

Closed
lqhl wants to merge 2 commits into
NousResearch:mainfrom
lqhl:fix/feishu-card-tables-codeblocks-v2
Closed

fix(feishu): route tables and code blocks to CardKit 2.0 with post/text fallback#23861
lqhl wants to merge 2 commits into
NousResearch:mainfrom
lqhl:fix/feishu-card-tables-codeblocks-v2

Conversation

@lqhl

@lqhl lqhl commented May 11, 2026

Copy link
Copy Markdown

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/md tag has two rendering limitations:

  1. Tables — GFM table syntax (| col1 | col2 |) is silently dropped
  2. Code blocks — Fenced code blocks longer than ~6 lines are truncated

Solution

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 with tag: markdown element
  • _build_outbound_payload() — routes detected content to interactive msg_type

Fallback 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:

  1. Exception handler in send() — catches send failures, retries as plain text
  2. Response check in send() and edit_message() — catches API rejection without exception
  3. _feishu_send_with_retry() — interactive failures raise immediately (no retry loop)

Testing

  • 11 new unit tests covering table detection, code block detection, CardKit JSON format, regex boundary cases, and mixed content
  • 209/209 existing Feishu tests pass (no regressions)
  • Live-tested on a real Feishu bot (both tables and code blocks render correctly as CardKit 2.0 cards)

Changes

  • gateway/platforms/feishu.py: +62/-3 lines
  • tests/gateway/test_feishu.py: +114/-0 lines

Related

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)
@lqhl

lqhl commented May 11, 2026

Copy link
Copy Markdown
Author

I've tested this PR with my feishu bot:
image

@liuhao1024

Copy link
Copy Markdown
Contributor

Minor: Comment/regex mismatch in _CODE_BLOCK_RE

The inline comment says "route 3+ content lines to CardKit 2.0" but the regex uses {2,} which matches 2+ content lines:

# Multi-line fenced code block (opening fence + 2+ content lines + closing fence)
# Empty or 1-line blocks render fine in post/md; route 3+ content lines to CardKit 2.0.
_CODE_BLOCK_RE = re.compile(r"^```[^\n]*\n(.*\n){2,}```", re.MULTILINE | re.DOTALL)

The test at line 217 confirms a 2-line block ("```\na\nb\n```") does match, so the regex behavior is {2,}. Either:

  • Change {2,}{3,} if the intent is truly 3+ content lines, or
  • Update the comment to say "2+ content lines"

Additionally, re.DOTALL is unnecessary here since \n is already explicit in the pattern. It makes .* match across newlines which adds backtracking complexity without changing the outcome on well-formed input. Consider dropping it.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter P2 Medium — degraded but workaround exists labels May 11, 2026
…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.
@lqhl

lqhl commented May 12, 2026

Copy link
Copy Markdown
Author

@liuhao1024 Thanks for catching this! Fixed in 8a60abe:

  1. Comment updated: "3+ content lines" → "2+ content lines" to match the {2,} regex
  2. Removed re.DOTALL — as you noted, \n is already explicit so DOTALL only adds backtracking overhead

Both changes are comment/cosmetic — the regex behavior is unchanged, 209/209 feishu tests still pass.

@lqhl

lqhl commented May 18, 2026

Copy link
Copy Markdown
Author

@teknium1 hi, can you take a look?

huangyoje added a commit to huangyoje/hermes-agent that referenced this pull request Jun 19, 2026
…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 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 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 to plugins/platforms/feishu/adapter.py by 560010547; 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 from tests/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 (---|:---|...)

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 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.

@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
@lqhl lqhl closed this by deleting the head repository Jul 25, 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/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

4 participants