Skip to content

feat(feishu): render Markdown tables as native card table component - #38453

Open
TonyHe-Lab wants to merge 2 commits into
NousResearch:mainfrom
TonyHe-Lab:feat/feishu-table-rendering
Open

feat(feishu): render Markdown tables as native card table component#38453
TonyHe-Lab wants to merge 2 commits into
NousResearch:mainfrom
TonyHe-Lab:feat/feishu-table-rendering

Conversation

@TonyHe-Lab

Copy link
Copy Markdown

Summary

Markdown tables in Feishu messages currently display as raw pipe-separated text because Feishu's post message type does not support table rendering.

This PR switches outbound messages to use Feishu interactive cards (JSON schema 2.0) with the native table component, while keeping non-table content as markdown elements within the same card.

Changes

gateway/platforms/feishu.py

  • New _parse_markdown_table(): parses | col | col | lines into Feishu table component JSON (columns, rows, header_style)
  • New _split_content_and_tables(): line-by-line scanner that groups consecutive table lines and converts them, collecting non-table lines into markdown elements
  • Rewritten _build_outbound_payload(): always returns interactive msg_type with schema 2.0 card structure, mixing markdown + table elements
  • Expanded _POST_CONTENT_INVALID_RE regex to match additional Feishu card error messages
  • Updated fallback logic in send_message, edit_message, _feishu_send_with_retry to handle interactive alongside post

tests/gateway/test_feishu.py

  • Updated 7 tests to assert interactive card structure with schema: "2.0" and body.elements instead of post type

Test Results

All 205 Feishu adapter tests pass.

Before / After

Before: Table messages show as raw | | | plain text
After: Tables render natively as Feishu card table component with headers and cell borders

TonyHE added 2 commits June 4, 2026 04:27
…onent

- Parse Markdown pipe tables into Feishu JSON 2.0 'table' component
  via new _parse_markdown_table() and _split_content_and_tables() methods
- Rewrite _build_outbound_payload() to always return 'interactive' card
  with schema 2.0, mixing markdown elements and table components
- Expand _POST_CONTENT_INVALID_RE to match additional Feishu card error
  messages (failed to create card content, card contains images)
- Update fallback logic in send_message, edit_message, and
  _feishu_send_with_retry to handle 'interactive' msg_type alongside 'post'
- Update 7 tests to assert 'interactive' card structure instead of 'post'
- All 205 feishu adapter tests pass
Previously table cells used data_type='text' with manual markdown
stripping. Feishu table component supports data_type='markdown' (v7.14+)
which renders bold, italic, links, images etc natively.

@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 addressing a real Feishu rendering gap. Current main still falls back to plain text for detected tables at plugins/platforms/feishu/adapter.py:4528-4530, but this implementation needs rework before it can be salvaged.

Problems

  • gateway/platforms/feishu.py:4396 accepts any two pipe-prefixed lines as a table, then _parse_markdown_table() unconditionally drops line two as a separator at :4344. Validate the GFM separator row.
  • gateway/platforms/feishu.py:4410 says a non-table pipe line is normal text, but advances past it without emitting it; | literal | is lost.
  • gateway/platforms/feishu.py:4446 changes every outbound message to interactive, replacing main's established text/post routing (plugins/platforms/feishu/adapter.py:4528-4534) without coverage for that compatibility change.
  • The target adapter moved in 5600105478ffde29d7566b45421b100eaa29c4ef; current work belongs in plugins/platforms/feishu/adapter.py.

Suggested changes

  • Port the table-only behavior to the plugin adapter, validate separators, preserve non-table pipe lines, and add regression tests for both parser cases and existing non-table routing.

Automated hermes-sweeper review.

# Detect potential table start: line begins with |
if "|" in line and line.strip().startswith("|"):
# Confirm it is a real table by checking the next line (separator)
if i + 1 < n and "|" in lines[i + 1] and lines[i + 1].strip().startswith("|"):

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 only verifies that the next line starts with |; it does not verify a GFM separator row. _parse_markdown_table() then skips that line unconditionally at line 4344, so two ordinary pipe-prefixed lines lose content. Validate separator cells (dashes with optional alignment colons) before entering the table path.

elements.append({"tag": "markdown", "content": "\n".join(table_lines)})
else:
# Single | line without separator — treat as normal text
i += 1

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 branch drops the current line: it advances i and emits no markdown element. A standalone | literal | line is therefore silently lost despite the comment saying it should be normal text. Preserve it in the surrounding markdown buffer and add a regression test.

"config": {"wide_screen_mode": True},
"body": {"elements": elements},
}
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.

This routes all text and Markdown through interactive cards, not only confirmed tables. Current main still deliberately selects text/post for non-table content in plugins/platforms/feishu/adapter.py:4528-4534; retain that routing unless the wider behavior change is explicitly covered and validated.

@teknium1 teknium1 added 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 sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 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 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.

3 participants