Skip to content

fix(plugins/feishu): render markdown tables as CardKit v2 interactive cards - #51739

Open
chapaofan wants to merge 1 commit into
NousResearch:mainfrom
chapaofan:fix/feishu-table-cardkit
Open

chapaofan wants to merge 1 commit into
NousResearch:mainfrom
chapaofan:fix/feishu-table-cardkit

Conversation

@chapaofan

Copy link
Copy Markdown

Summary

Fixes #50602

Feishu's post-type md elements do not support table syntax, so table-containing messages were force-sent as plain text, losing all table structure. This PR converts markdown tables to Feishu CardKit v2 table components.

Changes

  1. _parse_markdown_table — Parse markdown text into segments of non-table text and structured table data (header + rows).
  2. _build_table_card — Convert table data into a CardKit v2 table component with proper column definitions, row data, and header style.
  3. _build_interactive_card_with_tables — Assemble a full schema 2.0 interactive card containing markdown text and table elements.
  4. _build_outbound_payload — Send as interactive card when tables are detected, falling back to post/text as before.

Design decisions

  • Cell content is stripped of markdown bold markers (**, __) since Feishu table data_type: text does not support inline markdown
  • Table header uses header_style: {bold: true} for visual emphasis
  • Non-table text segments between tables are rendered as markdown elements in the same card
  • If no tables are detected, the original text/post fallback path is unchanged

Testing

Verified working with Hermes v0.16.0+ plugin architecture. Tested locally with real Feishu messages containing markdown tables — renders as native CardKit table component with proper column alignment and header styling.

… cards

Fixes NousResearch#50602

Feishu's post-type 'md' elements do not support table syntax, so
table-containing messages were force-sent as plain text, losing all
table structure.

This commit converts markdown tables to Feishu CardKit v2 table
components by:

1. _parse_markdown_table — Parse markdown text into segments of
   non-table text and structured table data (header + rows).
2. _build_table_card — Convert table data into a CardKit v2 table
   component with proper column definitions, row data, and header
   style (bold, left-aligned).
3. _build_interactive_card_with_tables — Assemble a full schema 2.0
   interactive card containing markdown text and table elements.
4. _build_outbound_payload — Send as interactive card when tables
   are detected, falling back to post/text as before.

Cell content is stripped of markdown bold markers (**, __) since
Feishu table data_type 'text' does not support inline markdown.
Table header uses header_style: {bold: true} for visual emphasis.
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins platform/feishu Feishu / Lark adapter P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Jun 24, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Duplicate of #12114 — same Feishu CardKit v2 native-table rendering mechanism (helper names differ but the approach is identical), entering the saturated Feishu-table PR cluster (#17006, #48807, #48334 all point to #12114). Fixes #50602.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for targeting a real Feishu formatting gap: current main still routes detected tables to plain text at plugins/platforms/feishu/adapter.py:4524-4530.

Problems

  • _parse_markdown_table() classifies every outer-pipe line as a table (plugins/platforms/feishu/adapter.py:218) and _flush_table() emits it even when no GFM divider was present (:214). Literal pipe-delimited text will therefore become an interactive card.
  • The parser has no fenced-code state (:217-245), so a code sample containing table-shaped lines is converted instead of preserved.
  • _build_outbound_payload() is shared by send() and edit_message() on current main (:1904, :1958), while the PR always selects interactive for a recognized table (:4548-4550). The change needs an explicit streaming-preview/final-delivery contract and coverage for both paths.
  • The PR modifies no tests despite adding parsing and delivery behavior.

Suggested changes

  • Require a validated header-plus-divider sequence, preserve fenced-code content, and add parser plus send/edit regression tests.
  • Define and test how streamed previews transition to the final CardKit payload before routing edits through the new path.

Automated hermes-sweeper review.

if len(row) > len(headers):
row[:] = row[:len(headers)]

segments.append({"type": "table", "headers": headers, "rows": rows})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emits a table even when table_lines contains only | literal | or otherwise lacks a GFM divider. Require a validated header-plus-divider sequence before appending a table segment; otherwise return these lines to the text segment.

table_lines = []

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table detection runs inside fenced code because this parser tracks no fence state. Preserve pipe-shaped examples inside ``` fences as text/code rather than turning them into CardKit tables.

return "text", json.dumps(text_payload, ensure_ascii=False)
# Check for markdown tables first — if found, send as CardKit v2 interactive card
# for real table rendering instead of code blocks.
card = _build_interactive_card_with_tables(content)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper is also used by edit_message() on current main. Please make the streaming-preview versus final-card behavior explicit and test both call paths before returning an interactive payload for every detected table.

@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 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have 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.

Feishu adapter sends raw Markdown source for table-containing messages

3 participants