hermes - 💡(How to fix) Fix [feishu] Unified fix for markdown rendering: inbound escaping + GFM table → Card 2.0

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The Feishu adapter has three interrelated rendering problems that degrade message quality for all Feishu users. This issue consolidates the root cause analysis and proposes a unified fix.

Error Message

def _build_outbound_payload(self, content: str) -> tuple[str, str]: # Phase 2: table → interactive card if _MARKDOWN_TABLE_RE.search(content): try: card_json = _build_table_card_payload(content) return "interactive", card_json except Exception: logger.debug("[Feishu] Card table build failed, falling back to post")

# Existing logic (unchanged)
if _MARKDOWN_HINT_RE.search(content):
    return "post", _build_markdown_post_payload(content)
return "text", json.dumps({"text": content}, ensure_ascii=False)

Root Cause

The Feishu adapter has three interrelated rendering problems that degrade message quality for all Feishu users. This issue consolidates the root cause analysis and proposes a unified fix.

Fix Action

Fix / Workaround

Feishu's post-type md tag does not support GFM pipe table syntax. The current workaround in _build_outbound_payload detects tables via _MARKDOWN_TABLE_RE and forces them to msg_type=text, which renders as raw | col | col | garbage. This was a reasonable workaround for older SDK versions but is now strictly worse than the original problem.

  • #9816 — Inbound escaping (original report)
  • #9549 — Table not rendering (original table report)
  • #21778 — Table regression in v0.13.0
  • #26658 — Proposes removing the table workaround
  • #21873 — Native interactive card support
  • #6003 — Card 2.0 support for richer rendering

Feishu's CardKit supports streaming updates via patch API. This would enable progressive rendering during long agent responses, similar to what @Cheerwhy/hermes-lark-streaming achieves as an external plugin.

Code Example

def _build_outbound_payload(self, content: str) -> tuple[str, str]:
    # Phase 2: table → interactive card
    if _MARKDOWN_TABLE_RE.search(content):
        try:
            card_json = _build_table_card_payload(content)
            return "interactive", card_json
        except Exception:
            logger.debug("[Feishu] Card table build failed, falling back to post")
    
    # Existing logic (unchanged)
    if _MARKDOWN_HINT_RE.search(content):
        return "post", _build_markdown_post_payload(content)
    return "text", json.dumps({"text": content}, ensure_ascii=False)

---

{
  "config": {"wide_screen_mode": true},
  "elements": [
    {"tag": "markdown", "text": "Prose before table..."},
    {
      "tag": "table",
      "page_size": 20,
      "columns": [
        {"name": "col_1", "display_name": "Platform", "data_type": "text", "horizontal_align": "Left"},
        {"name": "col_2", "display_name": "Price", "data_type": "text", "horizontal_align": "Right"}
      ],
      "rows": [
        {"col_1": "京东", "col_2": "¥3,040"},
        {"col_1": "拼多多", "col_2": "¥2,999"}
      ]
    },
    {"tag": "markdown", "text": "Prose after table..."}
  ]
}
RAW_BUFFERClick to expand / collapse

Summary

The Feishu adapter has three interrelated rendering problems that degrade message quality for all Feishu users. This issue consolidates the root cause analysis and proposes a unified fix.

Current Problems

1. Inbound markdown escaping corrupts agent context

_render_text_element calls _escape_markdown_text on inbound post messages, which escapes every markdown special character (\, `, *, _, {, }, [, ], (, ), #, +, -, !, |, >, ~). The escaped text enters the agent's context window, and when the agent echoes or paraphrases user content, the backslash-escapes survive into the outbound reply. Feishu's md renderer then displays \\*\\*bold\\*\\* literally instead of as bold text.

Key insight: The outbound path itself does not escape — _build_markdown_post_rows passes content through untouched. The corruption happens entirely on the inbound side.

2. GFM tables silently dropped

Feishu's post-type md tag does not support GFM pipe table syntax. The current workaround in _build_outbound_payload detects tables via _MARKDOWN_TABLE_RE and forces them to msg_type=text, which renders as raw | col | col | garbage. This was a reasonable workaround for older SDK versions but is now strictly worse than the original problem.

3. No native interactive card support

Feishu supports rich interactive cards (Card 2.0) with native table elements that render beautifully with column alignment, pagination, and structured layouts. The Hermes adapter already uses interactive cards for approval workflows (send_exec_approval), but the general message path never uses them.

Related Issues

  • #9816 — Inbound escaping (original report)
  • #9549 — Table not rendering (original table report)
  • #21778 — Table regression in v0.13.0
  • #26658 — Proposes removing the table workaround
  • #21873 — Native interactive card support
  • #6003 — Card 2.0 support for richer rendering

Related PRs

Multiple PRs have attempted to fix the table rendering independently: #12114, #20028, #20152, #22272, #22316, #24249, #25453, #26429, #27046, #27328. The proliferation suggests strong community demand but also creates review burden.

Proposed Solution

Phase 1 — Fix inbound escaping (low risk, high impact)

In _render_text_element: stop applying _escape_markdown_text to text from inbound post messages. Feishu's structured post format already separates styled elements (bold, code, etc.) from plain text. The existing style-wrapping logic re-applies correct markdown syntax, so blanket escaping is redundant and harmful.

Phase 2 — GFM table → Interactive Card (medium effort, high impact)

Approach: When outbound content contains a GFM table, parse it into a Feishu Card 2.0 payload with native table elements.

Implementation sketch:

def _build_outbound_payload(self, content: str) -> tuple[str, str]:
    # Phase 2: table → interactive card
    if _MARKDOWN_TABLE_RE.search(content):
        try:
            card_json = _build_table_card_payload(content)
            return "interactive", card_json
        except Exception:
            logger.debug("[Feishu] Card table build failed, falling back to post")
    
    # Existing logic (unchanged)
    if _MARKDOWN_HINT_RE.search(content):
        return "post", _build_markdown_post_payload(content)
    return "text", json.dumps({"text": content}, ensure_ascii=False)

Card structure:

{
  "config": {"wide_screen_mode": true},
  "elements": [
    {"tag": "markdown", "text": "Prose before table..."},
    {
      "tag": "table",
      "page_size": 20,
      "columns": [
        {"name": "col_1", "display_name": "Platform", "data_type": "text", "horizontal_align": "Left"},
        {"name": "col_2", "display_name": "Price", "data_type": "text", "horizontal_align": "Right"}
      ],
      "rows": [
        {"col_1": "京东", "col_2": "¥3,040"},
        {"col_1": "拼多多", "col_2": "¥2,999"}
      ]
    },
    {"tag": "markdown", "text": "Prose after table..."}
  ]
}

Key design decisions:

  1. Mixed content handling — Split content at table boundaries, interleave markdown + table elements inside one card
  2. Multi-table pagination — When >5 tables, split into multiple cards
  3. Heading normalization# Title**Title** (Feishu md tag has inconsistent heading support)
  4. Fallback chain preserved — Card → post → text (existing degradation behavior)
  5. No new dependencies — Pure JSON construction, no external packages

Reference implementation: QwenPaw (agentscope-ai/QwenPaw) already implements this successfully via _parse_md_table / _build_elements / _split_elements. The approach can be adapted rather than invented from scratch.

Phase 3 — Streaming card updates (future, optional)

Feishu's CardKit supports streaming updates via patch API. This would enable progressive rendering during long agent responses, similar to what @Cheerwhy/hermes-lark-streaming achieves as an external plugin.

Testing

  • Messages with single table → should render as interactive card with formatted table
  • Messages with mixed prose + table → card with interleaved markdown and table elements
  • Messages with >5 tables → split into multiple cards
  • Messages with only inline markdown (bold, code) → existing post/text behavior unchanged
  • Messages with headings → headings converted to bold before sending
  • All fallback paths → card failure degrades to post, post failure degrades to text

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [feishu] Unified fix for markdown rendering: inbound escaping + GFM table → Card 2.0