Skip to content

feat(feishu): support markdown tables via CardKit table element (JSON 2.0 schema) - #31038

Open
wangzxjh wants to merge 1 commit into
NousResearch:mainfrom
wangzxjh:feat/feishu-cardkit-table-support
Open

feat(feishu): support markdown tables via CardKit table element (JSON 2.0 schema)#31038
wangzxjh wants to merge 1 commit into
NousResearch:mainfrom
wangzxjh:feat/feishu-cardkit-table-support

Conversation

@wangzxjh

Copy link
Copy Markdown

Problem

Feishu post type messages do not render markdown tables — they display as raw | characters, making table-formatted data unreadable.

Solution

Detect code blocks (`) and markdown tables via regex and route them to a JSON 2.0 interactive card with native CardKit table elements instead of post or plain text.

Key changes

  • _build_card_payload_from_blocks() — splits content into markdown and table elements, builds a JSON 2.0 card
  • _build_card_table_element() — parses markdown tables into CardKit table components
  • _split_into_card_elements() — interleaves text and table blocks preserving original order
  • _send_interactive_via_rest() — sends interactive cards via direct REST API (lark-oapi SDK does not handle JSON 2.0 schema correctly for msg_type=interactive)
  • _feishu_send_with_retry() — routes msg_type=interactive to REST path
  • _build_outbound_payload() — detects code blocks and markdown tables, returns interactive cards

Backwards Compatibility

  • Non-code/non-table messages pass through unchanged (text or post as before)
  • Card failures cascade: interactive → post → text
  • _build_card_code_payload() kept as backward-compat alias

Closes #27695

… 2.0 schema)

Feishu post-type messages do not render markdown tables — they display as
raw | characters. This adds detection of code blocks and markdown tables in
_build_outbound_payload(), routing them to interactive cards (JSON 2.0
schema) instead of text or post.

Changes:
- _build_card_payload_from_blocks(): builds a JSON 2.0 card with mixed
  markdown + table elements
- _build_card_table_element(): parses markdown tables into native CardKit
  table components with proper columns and rows
- _split_into_card_elements(): interleaves text and table blocks preserving
  original order
- _get_tenant_access_token() + _send_interactive_via_rest(): sends
  interactive cards via direct REST API (lark-oapi SDK does not handle
  JSON 2.0 card schema correctly for msg_type=interactive)
- _feishu_send_with_retry(): routes msg_type=interactive to REST path
- _build_outbound_payload(): detects code blocks and markdown tables
  and returns interactive cards instead of text/post

Closes NousResearch#27695
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter P2 Medium — degraded but workaround exists labels May 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Closes #27695. Part of the Feishu markdown rendering consolidation tracked in #27469. Competing/related PRs: #23494, #19038 (closed). This is the most current implementation attempt for CardKit 2.0 table support.

@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 pursuing native Feishu table rendering; the current active adapter still downgrades tables to plain text at plugins/platforms/feishu/adapter.py:4524-4530, so the underlying issue remains.

Problems

  • This patch edits gateway/platforms/feishu.py, but 5600105478ffde29d7566b45421b100eaa29c4ef moved the active adapter to plugins/platforms/feishu/adapter.py; it needs a port.
  • The new REST path accepts reply_to and metadata but sends only a chat_id create request (gateway/platforms/feishu.py:1935-1963). The active path preserves replies and topic routing in plugins/platforms/feishu/adapter.py:4562-4601.
  • urlopen() is invoked directly inside async methods (gateway/platforms/feishu.py:1929, :1974), which can block the adapter loop.
  • edit_message() selects interactive through the changed payload builder (gateway/platforms/feishu.py:2081-2084), but the REST workaround and interactive fallback cover only sends.

Suggested changes

  • Port this to the bundled plugin and preserve its reply/thread, retry, and async execution behavior.
  • Add send/edit tests for tables, fenced code containing table syntax, reply/topic routing, and interactive rejection fallbacks.

Automated hermes-sweeper review.

receive_id_type = "open_id" if chat_id.startswith("ou_") else "chat_id"
url = f"https://{domain}/open-apis/im/v1/messages?receive_id_type={receive_id_type}"

body = {

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.

reply_to and metadata are accepted by this method but never influence this create-message request. The active sender uses them to route normal replies and topic messages (plugins/platforms/feishu/adapter.py:4562-4601); preserve those paths for interactive cards or table replies will be posted at the chat root.

},
)
try:
resp = json.loads(_ur.urlopen(req, timeout=15).read())

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 synchronous urlopen() runs directly in an async method, so a slow Feishu request blocks the adapter event loop for up to 15 seconds. Run the blocking REST operation off-loop and retain the existing retry semantics.

# 'md' elements do not render tables (message appears blank), and code
# blocks render much better in card markdown elements.
if "```" in content or _MARKDOWN_TABLE_RE.search(content):
return "interactive", _build_card_code_payload(content)

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.

edit_message() also consumes _build_outbound_payload() but continues to call the SDK update path; this PR's REST workaround and interactive fallback only cover sends. Add a supported interactive update path plus a post/text fallback before routing edited table content here.

# Regex to match a full markdown table block
# Matches the complete block: header line + separator line + data lines.
# Each line is captured as a whole row, then parsed by _build_card_table_element.
_TABLE_BLOCK_RE = re.compile(

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 multiline regex has no fence awareness. A fenced code example containing a valid pipe table will be extracted and rendered as a native table, changing the code block's content. Parse fenced blocks before recognizing tables and add coverage for this case.

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

feat(feishu): support markdown tables via CardKit table element

3 participants