Skip to content

feat(feishu): render markdown tables as native CardKit v2 table components - #17006

Open
chapaofan wants to merge 1 commit into
NousResearch:mainfrom
chapaofan:feat/feishu-markdown-tables-cards
Open

feat(feishu): render markdown tables as native CardKit v2 table components#17006
chapaofan wants to merge 1 commit into
NousResearch:mainfrom
chapaofan:feat/feishu-markdown-tables-cards

Conversation

@chapaofan

Copy link
Copy Markdown

Summary

Feishu's built-in markdown parser does not support table syntax — tables render as raw pipe-delimited text, which is barely readable. This PR adds automatic detection and rendering of markdown tables as native CardKit v2 table components.

Changes

New functions

  1. _parse_markdown_table(text) — Parses markdown text into segments alternating between text blocks and table structures. Handles leading/trailing pipes, separator rows, and multi-line tables.

  2. _build_table_card(headers, rows) — Converts parsed table segments into Feishu CardKit v2 table component JSON. Strips markdown bold markers from cell content. Applies a clean header style (bold, left-aligned).

  3. _build_interactive_card_with_tables(text) — Wraps text+table segments into a full CardKit v2 interactive card payload. Returns None if no tables are found, so callers can fall back to the existing post/text payload.

  4. _convert_markdown_tables_to_code(text) — Deprecated backward-compatibility function (no-op). Previously served as a code-block fallback; now superseded by interactive cards.

Modified functions

  1. _build_outbound_payload(content) — Before falling through to post or text payload, checks for markdown tables. If found, sends as "interactive" card type instead.

  2. _build_markdown_post_payload(content) — Calls the deprecated backward-compat function as a safety net.

How it works

When the agent sends a message containing a markdown table like:

| Name  | Age | City    |
|-------|-----|---------|
| Alice | 30  | Beijing |
| Bob   | 25  | Shanghai|

Instead of showing raw pipe text, this renders a proper formatted table in the Feishu card. Non-table content before/after the table is also preserved in the same card.

Notes

  • A duplicate/dead-code return statement in the original _build_table_card was removed (the second bare return after the header_style return was unreachable).

… cards

Feishu's built-in markdown parser does not support table syntax -
tables render as raw pipe-delimited text. This change adds automatic
detection and rendering of markdown tables as native CardKit v2 table
components.

Changes:
- Add _parse_markdown_table() - parses markdown text into segments
  alternating between text blocks and table structures
- Add _build_table_card() - converts parsed table segments into
  Feishu CardKit v2 table component JSON
- Add _build_interactive_card_with_tables() - wraps text+table
  segments into a full CardKit v2 interactive card payload
- Add _convert_markdown_tables_to_code() - deprecated backward-compat
  no-op fallback for string-based callers
- Modify _build_outbound_payload() - before falling through to post
  or text payload, check for tables and render as interactive card
- Modify _build_markdown_post_payload() - call the deprecated
  backward-compat function for safety
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter labels Apr 28, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #12114 — same feature: render markdown tables as native Feishu CardKit v2 table components. Also related to competing approaches in #15956 and #13723.

@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 tackling a real Feishu rendering limitation. Current main still sends recognized Markdown tables as plain text at plugins/platforms/feishu/adapter.py:4524-4534, so the underlying feature remains needed.

Problems

  • The diff targets gateway/platforms/feishu.py, which was moved to plugins/platforms/feishu/adapter.py by 476d8d9cc; this needs a port to the live plugin adapter rather than a clean cherry-pick.
  • _parse_markdown_table() starts a table for any |...| line at gateway/platforms/feishu.py:219; it does not require a separator row. Non-table pipe-delimited prose can therefore become a header-only CardKit table.
  • The diff adds no tests. The live send/edit fallbacks currently cover only post failures (plugins/platforms/feishu/adapter.py:1914, :1963), so a native-card route needs an interactive rejection fallback as well.

Suggested changes

  • Port to the plugin adapter, require header-plus-separator detection, and add parser/routing/failure-fallback coverage in tests/gateway/test_feishu.py.

Automated hermes-sweeper review.

table_lines = []

for line in lines:
if _MARKDOWN_TABLE_LINE_RE.match(line):

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 accepts any single pipe-enclosed line as a table. Require a valid header-plus-separator pair before opening a table; otherwise non-table pipe-delimited prose is emitted as a CardKit table.

# for real table rendering instead of code blocks.
card = _build_interactive_card_with_tables(content)
if card is not None:
return "interactive", json.dumps(card, ensure_ascii=False)

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.

The live send/edit fallback paths only retry plain text for rejected post payloads. When porting this interactive route to the current plugin adapter, add equivalent interactive-card failure fallback so a CardKit API rejection does not fail the entire reply.

@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 12, 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/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants