{"repo":"NousResearch/hermes-agent","public_id":"6003","display_label":"Feishu markdown table rendering approaches and fixes","summary":"This group centers on a single core problem: Feishu/Lark does not natively render markdown tables, causing raw source to appear in messages. The members represent the bug reports and multiple competing implementation attempts (post type, CardKit 2.0, interactive cards, native table components) to address this, along with related Feishu formatting and platform support work.","subsystem":"gateway","counts":{"members":92,"open_issues":9,"open_issues_distinct":9,"open_prs":9,"closed_issues":32,"merged_prs":4,"attempt_prs":47},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":20275},"resolution":{"state":"partially_resolved","by":"pr-68122,pr-109140"},"freshness":{"state":"verified","last_verified_at":"2026-09-12T15:25:10+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.029049211657907296},"fix_edges":[{"issue_number":7310,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9549,"pr_number":20275,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9549,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9816,"pr_number":109140,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":19035,"pr_number":20275,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":21778,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":23938,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":25452,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":26841,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":26841,"pr_number":68122,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":27529,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":27695,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":29471,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":38755,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":46470,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52046,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52786,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":58269,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":61643,"pr_number":68121,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":6003,"title":"[Feature]: [Feishu] Support Card 2.0 for richer message rendering (e.g. tables)","url":null,"state":"open"},{"kind":"issue","number":7022,"title":"[Feishu Bug] Markdown source code displayed instead of rendered card format","url":null,"state":"closed"},{"kind":"issue","number":7310,"title":"## Problem\nWhen sending markdown table syntax (| col1 | col2 |) in Feishu, the message displays as plain text instead of being rendered as a table. This works correctly in OpenClaw.\n\n## Environment\n- Hermes Agent version: [latest]\n- Feishu integration via WebSocket\n- Using native markdown table syntax in messages\n\n## Expected behavior\nMarkdown tables should render as formatted Feishu table messages, similar to how OpenClaw handles them.\n\n## Screenshots\n[Attach screenshot comparing OpenClaw vs Hermes Agent rendering]\n\n## Additional context\nThe Feishu platform adapter uses `post` msg_type with markdown, but the markdown hint regex may not be detecting table syntax properly.","url":null,"state":"closed"},{"kind":"issue","number":9536,"title":"fix(feishu): markdown tables not rendering as interactive cards — simplify `_should_send_as_card()` trigger    condition","url":null,"state":"closed"},{"kind":"issue","number":9549,"title":"[Feishu] Markdown tables not rendering in Feishu messages","url":null,"state":"closed"},{"kind":"issue","number":9816,"title":"Feishu/Lark messages poorly formatted due to excessive Markdown escaping","url":null,"state":"closed"},{"kind":"issue","number":18704,"title":"feishu: Markdown tables render as garbled text — missing post-format table support","url":null,"state":"closed"},{"kind":"issue","number":18756,"title":"Feishu gateway: Markdown tables silently dropped in outgoing messages","url":null,"state":"closed"},{"kind":"issue","number":19226,"title":"[Feature]: use feishu card message to render the repsponse","url":null,"state":"open"},{"kind":"issue","number":21326,"title":"Feature Request: Support Card JSON 2.0 Tables in Feishu Adapter","url":null,"state":"open"},{"kind":"issue","number":21778,"title":"Bug Report: Feishu (Lark) Markdown Table Rendering Regression in v0.13.0","url":null,"state":"closed"},{"kind":"issue","number":21866,"title":"Feishu: markdown tables render as raw text instead of using interactive card","url":null,"state":"closed"},{"kind":"issue","number":21873,"title":"[Feature]: Native Feishu Interactive Card Support","url":null,"state":"open"},{"kind":"issue","number":23938,"title":"[Bug]: Feishu adapter strips all Markdown formatting when message contains a table","url":null,"state":"closed"},{"kind":"issue","number":24319,"title":"Feishu replies render raw Markdown while normal messages render correctly","url":null,"state":"open"},{"kind":"issue","number":25452,"title":"Feishu channel displays markdown tables as raw source code instead of rendering them","url":null,"state":"closed"},{"kind":"issue","number":26078,"title":"[Bug]: tables remain markdown source code in feishu chat window","url":null,"state":"closed"},{"kind":"issue","number":26174,"title":"[Bug]: hermes返回的内容无法在飞书渲染","url":null,"state":"closed"},{"kind":"issue","number":26236,"title":"[Feature]: Request Feishu support to return messages in the form of streaming cards, making the messages more aesthetically pleasing.","url":null,"state":"closed"},{"kind":"issue","number":26658,"title":"[feishu] Remove markdown table force-text workaround — Feishu now supports tables in post format","url":null,"state":"closed"},{"kind":"issue","number":26841,"title":"[Feishu] First chunk of long messages sent as msg_type=text instead of post, breaking Markdown rendering","url":null,"state":"closed"},{"kind":"issue","number":27469,"title":"[feishu] Unified fix for markdown rendering: inbound escaping + GFM table → Card 2.0","url":null,"state":"closed"},{"kind":"issue","number":27529,"title":"[feishu] Fix markdown table rendering: use post+tag:md instead of force-text workaround","url":null,"state":"closed"},{"kind":"issue","number":27692,"title":"feat(feishu): support markdown tables via CardKit table element","url":null,"state":"closed"},{"kind":"issue","number":27695,"title":"feat(feishu): support markdown tables via CardKit table element","url":null,"state":"closed"},{"kind":"issue","number":29245,"title":"Feishu gateway: Markdown tables render as broken plain text","url":null,"state":"closed"},{"kind":"issue","number":29471,"title":"bug(feishu): first chunk of long messages sent as msg_type=text, breaking Markdown rendering","url":null,"state":"closed"},{"kind":"issue","number":32607,"title":"Feishu messages: markdown tables not rendered, making them unreadable","url":null,"state":"closed"},{"kind":"issue","number":37777,"title":"[Feature] Support Feishu interactive card (msg_type=interactive) in send_message","url":null,"state":"open"},{"kind":"issue","number":38755,"title":"fix(feishu): remove outdated markdown table fallback, always use post format","url":null,"state":"closed"},{"kind":"issue","number":43437,"title":"Feishu/Lark: _escape_markdown_text over-escapes characters causing Markdown rendering corruption","url":null,"state":"closed"},{"kind":"issue","number":46187,"title":"feat(feishu): support interactive card messages in send_message for table rendering","url":null,"state":"open"},{"kind":"issue","number":46470,"title":"[feishu] Render all outbound markdown via Card 2.0 interactive cards (unified fix)","url":null,"state":"closed"},{"kind":"issue","number":50602,"title":"Feishu adapter sends raw Markdown source for table-containing messages","url":null,"state":"closed"},{"kind":"issue","number":52046,"title":"[Feishu] A markdown table downgrades the entire message to plain text, breaking all other formatting","url":null,"state":"closed"},{"kind":"issue","number":52786,"title":"Feishu adapter incorrectly downgrades markdown tables to plain text","url":null,"state":"closed"},{"kind":"issue","number":54950,"title":"Feishu adapter: add renderMode config option for automatic card message wrapping","url":null,"state":"open"},{"kind":"issue","number":54951,"title":"Feishu/Lark adapter: add renderMode config option for automatic card messages","url":null,"state":"open"},{"kind":"issue","number":56430,"title":"Feishu 消息中 Markdown 表格被降级为纯文本导致渲染错误","url":null,"state":"closed"},{"kind":"issue","number":58269,"title":"[feishu] Remove outdated table-to-text fallback — post(md) now supports GFM tables","url":null,"state":"closed"},{"kind":"issue","number":61643,"title":"🐛 Bug: Feishu messages show raw Markdown table syntax instead of rendering","url":null,"state":"closed"},{"kind":"pr","number":6015,"title":"feat(feishu): send markdown responses as Card 2.0 instead of post messages","url":null,"state":"open","close_reason":{"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7085,"title":"feat(feishu): send markdown responses as Card 2.0 instead of post messages","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":9991,"title":"fix: remove excessive markdown escaping in Feishu text element rendering","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d131391ff9","number":0},{"kind":"pr","value":"#109140","number":109140}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":10801,"title":"fix(feishu): WebSocket watchdog + approval card PATCH + markdown table rendering","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#10251","number":10251},{"kind":"pr","value":"#10251","number":10251},{"kind":"pr","value":"#77364","number":77364}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":12114,"title":"fix(feishu): render markdown tables via card v2 `table` component","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":16474,"title":"fix(feishu): unify outbound to post and degrade tables to text element","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":19015,"title":"fix(feishu): wrap pipe tables in code fences so columns survive","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8e18d10318f9fb69f0b748db11e37de44b71da85","number":0},{"kind":"pr","value":"#18756","number":18756}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":19038,"title":"fix(feishu): route tables and multi-line code blocks to CardKit 2.0","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20275","number":20275},{"kind":"pr","value":"#13723","number":13723},{"kind":"pr","value":"#19035","number":19035},{"kind":"pr","value":"#9549","number":9549}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":19231,"title":"feat(feishu): use card to show complex message","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#12459","number":12459}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":20275,"title":"fix(feishu): force text mode for markdown tables (salvage #13723)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/platforms/feishu.py"],"fix_hunks":[{"file":"gateway/platforms/feishu.py","hunks":["@@ -153,6 +153,9 @@\n     r\"(^#{1,6}\\s)|(^\\s*[-*]\\s)|(^\\s*\\d+\\.\\s)|(^\\s*---+\\s*$)|(```)|(`[^`\\n]+`)|(\\*\\*[^*\\n].+?\\*\\*)|(~~[^~\\n].+?~~)|(<u>.+?</u>)|(\\*[^*\\n]+\\*)|(\\[[^\\]]+\\]\\([^)]+\\))|(^>\\s)\",\n     re.MULTILINE,\n )\n+# Detect markdown tables: a line starting with | followed by a separator line.\n+# Feishu post-type 'md' elements do not render tables, so we force text mode.\n+_MARKDOWN_TABLE_RE = re.compile(r\"^\\|.*\\|\\n\\|[-|: ]+\\|\", re.MULTILINE)\n _MARKDOWN_LINK_RE = re.compile(r\"\\[([^\\]]+)\\]\\(([^)]+)\\)\")\n _MARKDOWN_FENCE_OPEN_RE = re.compile(r\"^```([^\\n`]*)\\s*$\")\n _MARKDOWN_FENCE_CLOSE_RE = re.compile(r\"^```\\s*$\")","@@ -3995,6 +3998,12 @@ def _is_duplicate(self, message_id: str) -> bool:\n     # =========================================================================\n \n     def _build_outbound_payload(self, content: str) -> tuple[str, str]:\n+        # Feishu post-type 'md' elements do not render markdown tables; sending\n+        # table content as post causes the message to appear blank on the client.\n+        # Force plain text for anything that looks like a markdown table.\n+        if _MARKDOWN_TABLE_RE.search(content):\n+            text_payload = {\"text\": content}\n+            return \"text\", json.dumps(text_payload, ensure_ascii=False)\n         if _MARKDOWN_HINT_RE.search(content):\n             return \"post\", _build_markdown_post_payload(content)\n         text_payload = {\"text\": content}\n"]}]},{"kind":"pr","number":22047,"title":"fix(feishu): keep markdown tables in post payload so prose still renders","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":22259,"title":"fix(feishu): wrap markdown tables in code_block tags for post rendering","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22272,"title":"fix(feishu): render markdown tables as native card tables","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"5600105478ffde29d7566b45421b100eaa29c4ef","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":23861,"title":"fix(feishu): route tables and code blocks to CardKit 2.0 with post/text fallback","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":25453,"title":"fix(feishu): render markdown tables as interactive card table components","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46472","number":46472}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":26108,"title":"fix(feishu): render markdown tables via post md tags instead of raw text","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":26119,"title":"fix(feishu): render markdown tables via rich post type instead of plain text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26108","number":26108}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":26848,"title":"fix(feishu): send every chunk of a long markdown reply as msg_type=post (#26841)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68122","number":68122},{"kind":"pr","value":"#68121","number":68121}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":27400,"title":"fix(feishu): remove markdown table force-text workaround, add pipe table to post hint (#26658)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#27922","number":27922}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":27739,"title":"fix(feishu): render markdown tables via post+md instead of force-text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26108","number":26108},{"kind":"pr","value":"#27469","number":27469}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":28837,"title":"fix(feishu): render markdown tables via cards","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"977884e6cd","number":0},{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":29472,"title":"fix(feishu): use consistent msg_type across all message chunks","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26848","number":26848},{"kind":"pr","value":"#16474","number":16474},{"kind":"pr","value":"#18371","number":18371}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":29510,"title":"fix(feishu): convert markdown tables to bullet lists instead of raw text","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":29552,"title":"fix(feishu): render markdown tables via post+tag:md instead of plain text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":31038,"title":"feat(feishu): support markdown tables via CardKit table element (JSON 2.0 schema)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"977884e6cd","number":0},{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":32488,"title":"feat(feishu): Schema 2.0 markdown card for GFM tables, code blocks, and inline formatting","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":32648,"title":"fix(feishu): render markdown tables as aligned code blocks instead of raw text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#27469","number":27469},{"kind":"pr","value":"#9549","number":9549},{"kind":"pr","value":"#12114","number":12114},{"kind":"pr","value":"#21872","number":21872},{"kind":"pr","value":"#20580","number":20580},{"kind":"pr","value":"#32488","number":32488},{"kind":"pr","value":"#20580","number":20580},{"kind":"pr","value":"#21872","number":21872}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":33310,"title":"fix(feishu): render markdown tables and code blocks via CardKit 2.0 cards","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53453","number":53453},{"kind":"pr","value":"#53355","number":53355},{"kind":"pr","value":"#36877","number":36877}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":33800,"title":"fix(feishu): render markdown tables via Schema 2.0 interactive cards","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26108","number":26108},{"kind":"pr","value":"#29552","number":29552},{"kind":"pr","value":"#31056","number":31056},{"kind":"pr","value":"#31410","number":31410},{"kind":"pr","value":"#9549","number":9549}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":37830,"title":"feat(gateway): support Feishu interactive card (msg_type=interactive) in send_message","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"552adbe08","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":38834,"title":"fix(feishu): remove outdated markdown table fallback, always use post format","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":38867,"title":"fix(feishu): remove outdated markdown table fallback, always use post format #38755","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#38834","number":38834},{"kind":"pr","value":"#38755","number":38755},{"kind":"pr","value":"#27469","number":27469},{"kind":"pr","value":"#31410","number":31410},{"kind":"pr","value":"#34181","number":34181}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":39955,"title":"fix(feishu): remove outdated markdown table text-mode fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":40445,"title":"feat(feishu): render markdown tables as native Card JSON 2.0 table components","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":43486,"title":"fix(feishu): stop over-escaping Markdown in _escape_markdown_text (#43437)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#9816","number":9816},{"kind":"pr","value":"#9991","number":9991},{"kind":"pr","value":"#43437","number":43437}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":45583,"title":"fix(feishu): route markdown tables through post instead of broken interactive card","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53453","number":53453}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":46472,"title":"fix(feishu): render all markdown via Card 2.0 interactive cards","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#25452","number":25452},{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#68121","number":68121}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":46502,"title":"fix(feishu): render markdown tables as interactive cards","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":48807,"title":"feat(feishu): render markdown tables as CardKit v2 native table components","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#12114","number":12114},{"kind":"pr","value":"#61496","number":61496}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":50640,"title":"fix(feishu): route markdown tables through post+md instead of forcing text","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#29166","number":29166},{"kind":"pr","value":"#29166","number":29166}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":51739,"title":"fix(plugins/feishu): render markdown tables as CardKit v2 interactive cards","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#12114","number":12114},{"kind":"pr","value":"#17006","number":17006},{"kind":"pr","value":"#48807","number":48807},{"kind":"pr","value":"#48334","number":48334},{"kind":"pr","value":"#12114","number":12114},{"kind":"pr","value":"#50602","number":50602}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":52066,"title":"fix(feishu): preserve markdown around tables","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#52046","number":52046}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":52790,"title":"fix: send Feishu markdown tables as post","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58019,"title":"fix(feishu): render markdown tables via post+md, not text downgrade","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58391,"title":"fix(feishu): use post(md) for messages containing markdown tables","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#38834","number":38834},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":61647,"title":"fix(feishu): render Markdown tables natively instead of falling back to plain text (#61643)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":65529,"title":"fix(feishu): remove outdated markdown table fallback, always use post format","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#68121","number":68121},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#58019","number":58019},{"kind":"pr","value":"#29552","number":29552}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":68121,"title":"fix(feishu): render markdown tables via post+md, remove force-text downgrade","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","contributors/emails/[redacted:email]","plugins/platforms/feishu/adapter.py","tests/gateway/test_feishu.py","tests/gateway/test_feishu_table_markdown.py"],"fix_hunks":[{"file":"contributors/emails/[redacted:email]","hunks":["@@ -0,0 +1 @@\n+M1racleShih"]},{"file":"contributors/emails/[redacted:email]","hunks":["@@ -0,0 +1 @@\n+JasonFang1993"]},{"file":"plugins/platforms/feishu/adapter.py","hunks":["@@ -152,11 +152,24 @@\n # ---------------------------------------------------------------------------\n \n _MARKDOWN_HINT_RE = re.compile(\n-    r\"(^#{1,6}\\s)|(^\\s*[-*]\\s)|(^\\s*\\d+\\.\\s)|(^\\s*---+\\s*$)|(```)|(`[^`\\n]+`)|(\\*\\*[^*\\n].+?\\*\\*)|(~~[^~\\n].+?~~)|(<u>.+?</u>)|(\\*[^*\\n]+\\*)|(\\[[^\\]]+\\]\\([^)]+\\))|(^>\\s)\",\n+    # Pipe table: any header line + separator line both starting with '|'.\n+    r\"(^\\|.*\\|\\s*\\n\\|[-:|\\s]+\\|)\"\n+    # Headings, lists, code, bold/italic/strike/underline, links, blockquotes.\n+    r\"|(^#{1,6}\\s)\"\n+    r\"|(^\\s*[-*]\\s)\"\n+    r\"|(^\\s*\\d+\\.\\s)\"\n+    r\"|(^\\s*---+\\s*$)\"\n+    r\"|(```)\"\n+    r\"|(`[^`\\n]+`)\"\n+    r\"|(\\*\\*[^*\\n].+?\\*\\*)\"\n+    r\"|(~~[^~\\n].+?~~)\"\n+    r\"|(<u>.+?</u>)\"\n+    r\"|(\\*[^*\\n]+\\*)\"\n+    r\"|(\\[[^\\]]+\\]\\([^)]+\\))\"\n+    r\"|(^>\\s)\",\n     re.MULTILINE,\n )\n-# Detect markdown tables: a line starting with | followed by a separator line.\n-# Feishu post-type 'md' elements do not render tables, so we force text mode.\n+# Backwards-compatible alias retained because external callers reference it.\n _MARKDOWN_TABLE_RE = re.compile(r\"^\\|.*\\|\\n\\|[-|: ]+\\|\", re.MULTILINE)\n _MARKDOWN_LINK_RE = re.compile(r\"\\[([^\\]]+)\\]\\(([^)]+)\\)\")\n _MARKDOWN_FENCE_OPEN_RE = re.compile(r\"^```([^\\n`]*)\\s*$\")","@@ -4533,12 +4546,12 @@ def _is_duplicate(self, message_id: str) -> bool:\n     # =========================================================================\n \n     def _build_outbound_payload(self, content: str) -> tuple[str, str]:\n-        # Feishu post-type 'md' elements do not render markdown tables; sending\n-        # table content as post causes the message to appear blank on the client.\n-        # Force plain text for anything that looks like a markdown table.\n-        if _MARKDOWN_TABLE_RE.search(content):\n-            text_payload = {\"text\": content}\n-            return \"text\", json.dumps(text_payload, ensure_ascii=False)\n+        # Empirically (issue #52786), current Feishu clients render markdown\n+        # tables inside ``post``-type ``md`` elements natively. The previous\n+        # table-downgrade branch forced any table-containing message to\n+        # ``text``, which left Feishu readers seeing the raw pipe-and-dash\n+        # source instead of a rendered table. Trust the common markdown path\n+        # for table content too.\n         if _MARKDOWN_HINT_RE.search(content):\n             return \"post\", _build_markdown_post_payload(content)\n         text_payload = {\"text\": content}"]},{"file":"tests/gateway/test_feishu.py","hunks":["@@ -2690,6 +2690,23 @@ def test_build_post_payload_keeps_full_markdown_text(self):\n             [[{\"tag\": \"md\", \"text\": \"---\\n1. 第一项\\n  2. 子项\\n- 外层\\n  - 内层\\n<u>下划线</u> 和 ~~删除线~~\"}]],\n         )\n \n+    @patch.dict(os.environ, {}, clear=True)\n+    def test_build_outbound_payload_uses_post_for_markdown_table(self):\n+        from gateway.config import PlatformConfig\n+        from plugins.platforms.feishu.adapter import FeishuAdapter\n+\n+        adapter = FeishuAdapter(PlatformConfig())\n+        content = \"| Name | Score |\\n| --- | ---: |\\n| Ada | 10 |\"\n+\n+        msg_type, raw_payload = adapter._build_outbound_payload(content)\n+\n+        self.assertEqual(msg_type, \"post\")\n+        payload = json.loads(raw_payload)\n+        self.assertEqual(\n+            payload[\"zh_cn\"][\"content\"],\n+            [[{\"tag\": \"md\", \"text\": content}]],\n+        )\n+\n     @patch.dict(os.environ, {}, clear=True)\n     def test_send_uses_post_for_inline_markdown(self):\n         from gateway.config import PlatformConfig"]},{"file":"tests/gateway/test_feishu_table_markdown.py","hunks":["@@ -0,0 +1,133 @@\n+\"\"\"Tests for Feishu adapter outbound markdown payload construction.\n+\n+Reproduces the bug tracked in hermes-agent issue #52786:\n+`_build_outbound_payload` was force-downgrading any message containing a\n+markdown pipe table to ``msg_type=text``, so Feishu clients rendered the raw\n+pipe-and-dash source instead of a table.  Empirically current Feishu clients\n+render ``post``+``md`` tables natively, so the downgrade branch must be removed.\n+\n+These tests guard the fix.  They invoke the real adapter via the project's\n+plugin-loader helper so that no ``sys.path`` / ``sys.modules`` games are\n+needed.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import json\n+\n+from tests.gateway._plugin_adapter_loader import load_plugin_adapter\n+\n+_adapter = load_plugin_adapter(\"feishu\")\n+\n+\n+def _call_build_outbound_payload(content: str) -> tuple[str, str]:\n+    \"\"\"Invoke ``_build_outbound_payload`` on a bare adapter instance.\n+\n+    ``_build_outbound_payload`` is a method that only uses module-level\n+    helpers (``_MARKDOWN_TABLE_RE``, ``_MARKDOWN_HINT_RE``,\n+    ``_build_markdown_post_payload``) and never touches ``self.*``, so a bare\n+    object is sufficient.\n+    \"\"\"\n+    inst = object.__new__(_adapter.FeishuAdapter)\n+    return inst._build_outbound_payload(content)\n+\n+\n+def _md_texts_from_post_payload(payload_str: str) -> list[str]:\n+    \"\"\"Pull every ``{tag:'md', text:'...'}`` element out of a Feishu post payload.\n+\n+    Real payload shape::\n+\n+        {\"zh_cn\": {\"content\": [[{\"tag\": \"md\", \"text\": \"...\"}], ...]}}\n+\n+    Helpers and tests need to introspect the ``md`` blocks regardless of\n+    locale, so we walk the structure generically.\n+    \"\"\"\n+    payload = json.loads(payload_str)\n+    if not isinstance(payload, dict):\n+        return []\n+    texts: list[str] = []\n+    for lang_val in payload.values():\n+        if not isinstance(lang_val, dict):\n+            continue\n+        content = lang_val.get(\"content\", [])\n+        if not isinstance(content, list):\n+            continue\n+        for block in content:\n+            if isinstance(block, list):\n+                candidates = block\n+            else:\n+                candidates = [block]\n+            for el in candidates:\n+                if isinstance(el, dict) and el.get(\"tag\") == \"md\":\n+                    texts.append(el.get(\"text\", \"\"))\n+    return texts\n+\n+\n+def test_markdown_table_uses_post_not_text():\n+    \"\"\"Regression test for issue #52786 (and its older sibling #23938).\n+\n+    A message whose only markdown is a table must take the ``post`` path,\n+    not be downgraded to plain text.\n+    \"\"\"\n+    content = (\n+        \"| col A | col B |\\n\"\n+        \"| ----- | ----- |\\n\"\n+        \"| 1     | 2     |\"\n+    )\n+    msg_type, payload_str = _call_build_outbound_payload(content)\n+    assert msg_type == \"post\", (\n+        f\"expected 'post' for a markdown table (issue #52786), got {msg_type!r}; \"\n+        \"the table-downgrade branch in _build_outbound_payload has been re-introduced\"\n+    )\n+    md_texts = _md_texts_from_post_payload(payload_str)\n+    assert md_texts, f\"post payload must include at least one md element; got {payload_str!r}\"\n+    joined = \"\".join(md_texts)\n+    assert \"col A\" in joined and \"|\" in joined, (\n+        \"table text was lost or reformatted when switching from text to post\"\n+    )\n+\n+\n+def test_plain_text_without_markdown_still_uses_text():\n+    \"\"\"Negative control: a message with no markdown hints and no table must\n+    still go to plain text.  Guards against accidentally promoting everything\n+    to ``post``.\"\"\"\n+    msg_type, _ = _call_build_outbound_payload(\"just a plain sentence with no markup\")\n+    assert msg_type == \"text\"\n+\n+\n+def test_existing_markdown_heading_still_uses_post():\n+    \"\"\"Sanity: the existing ``post`` path (heading / list / code / bold /\n+    link) must still work after the table downgrade is removed.\"\"\"\n+    msg_type, payload_str = _call_build_outbound_payload(\"# hello world\\n\")\n+    assert msg_type == \"post\"\n+    md_texts = _md_texts_from_post_payload(payload_str)\n+    assert md_texts, f\"expected at least one md element; got {payload_str!r}\"\n+    assert any(\"hello world\" in t for t in md_texts), (\n+        f\"expected 'hello world' in md elements; got {md_texts!r}\"\n+    )\n+\n+\n+def test_table_combined_with_other_markdown_does_not_downgrade():\n+    \"\"\"A message that mixes a table with surrounding markdown must also\n+    take the ``post`` path.\n+\n+    The old ``_MARKDOWN_TABLE_RE`` branch returned ``text`` unconditionally\n+    and stripped all the surrounding markdown formatting, so a Feishu\n+    reader saw literal pipes and lost the prose framing the table.\n+    \"\"\"\n+    content = (\n+        \"Here is the data:\\n\\n\"\n+        \"| col A | col B |\\n\"\n+        \"| ----- | ----- |\\n\"\n+        \"| 1     | 2     |\\n\\n\"\n+        \"Let me know.\"\n+    )\n+    msg_type, payload_str = _call_build_outbound_payload(content)\n+    assert msg_type == \"post\"\n+    md_texts = _md_texts_from_post_payload(payload_str)\n+    joined = \"\\n\".join(md_texts)\n+    assert \"Here is the data\" in joined, (\n+        \"leading prose was lost when downgrading a mixed-table message\"\n+    )\n+    assert \"col A\" in joined, \"table header was lost\"\n+    assert \"Let me know\" in joined, \"trailing prose was lost\"\n"]}]},{"kind":"pr","number":68122,"title":"fix(feishu): keep msg_type=post consistent across chunks of long markdown replies","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/feishu/adapter.py","tests/gateway/test_feishu.py"],"fix_hunks":[{"file":"plugins/platforms/feishu/adapter.py","hunks":["@@ -1910,11 +1910,21 @@ async def send(\n \n         formatted = self.format_message(content)\n         chunks = self.truncate_message(formatted, self.MAX_MESSAGE_LENGTH)\n+        # When chunking splits a long markdown response, an individual chunk\n+        # can end up as plain prose that doesn't match the per-chunk hint\n+        # regex — so it would be sent as ``msg_type=text`` and the user would\n+        # see literal ``**bold``/``## heading``/code fences in the Feishu\n+        # client while other chunks render correctly. Lock the markdown\n+        # decision at the whole-message level so every chunk consistently\n+        # uses ``post``. See #26841.\n+        prefer_post = bool(_MARKDOWN_HINT_RE.search(formatted))\n         last_response = None\n \n         try:\n             for chunk in chunks:\n-                msg_type, payload = self._build_outbound_payload(chunk)\n+                msg_type, payload = self._build_outbound_payload(\n+                    chunk, prefer_post=prefer_post,\n+                )\n                 try:\n                     response = await self._feishu_send_with_retry(\n                         chat_id=chat_id,","@@ -4545,14 +4555,21 @@ def _is_duplicate(self, message_id: str) -> bool:\n     # Outbound payload construction and send pipeline\n     # =========================================================================\n \n-    def _build_outbound_payload(self, content: str) -> tuple[str, str]:\n+    def _build_outbound_payload(\n+        self, content: str, *, prefer_post: bool = False,\n+    ) -> tuple[str, str]:\n         # Empirically (issue #52786), current Feishu clients render markdown\n         # tables inside ``post``-type ``md`` elements natively. The previous\n         # table-downgrade branch forced any table-containing message to\n         # ``text``, which left Feishu readers seeing the raw pipe-and-dash\n         # source instead of a rendered table. Trust the common markdown path\n         # for table content too.\n-        if _MARKDOWN_HINT_RE.search(content):\n+        #\n+        # ``prefer_post`` lets ``send`` treat the chunk as part of a larger\n+        # markdown document: when a long markdown reply is split at\n+        # MAX_MESSAGE_LENGTH, the per-chunk regex would otherwise\n+        # mis-classify a plain-prose chunk as ``text``. See #26841.\n+        if prefer_post or _MARKDOWN_HINT_RE.search(content):\n             return \"post\", _build_markdown_post_payload(content)\n         text_payload = {\"text\": content}\n         return \"text\", json.dumps(text_payload, ensure_ascii=False)"]},{"file":"tests/gateway/test_feishu.py","hunks":["@@ -2707,6 +2707,110 @@ def test_build_outbound_payload_uses_post_for_markdown_table(self):\n             [[{\"tag\": \"md\", \"text\": content}]],\n         )\n \n+    @patch.dict(os.environ, {}, clear=True)\n+    def test_send_uses_post_for_every_chunk_of_multi_chunk_markdown(self):\n+        \"\"\"Regression for #26841: when a long Markdown message is split\n+        across multiple chunks, every chunk must go out as\n+        ``msg_type=post`` — including chunk 1.  The bug was that the\n+        first chunk often had only plain prose (the per-chunk regex\n+        didn't match) and was sent as ``text``, so users saw literal\n+        ``**bold``/``## heading``/code fences while later chunks\n+        rendered correctly.\n+        \"\"\"\n+        from gateway.config import PlatformConfig\n+        from plugins.platforms.feishu.adapter import FeishuAdapter\n+\n+        adapter = FeishuAdapter(PlatformConfig())\n+        captured = []\n+\n+        class _MessageAPI:\n+            def create(self, request):\n+                captured.append(request)\n+                return SimpleNamespace(\n+                    success=lambda: True,\n+                    data=SimpleNamespace(\n+                        message_id=f\"om_chunk_{len(captured)}\",\n+                    ),\n+                )\n+\n+        adapter._client = SimpleNamespace(\n+            im=SimpleNamespace(\n+                v1=SimpleNamespace(\n+                    message=_MessageAPI(),\n+                )\n+            )\n+        )\n+\n+        async def _direct(func, *args, **kwargs):\n+            return func(*args, **kwargs)\n+\n+        # Force a deterministic split so the test doesn't depend on the\n+        # exact 8000-char limit.  Chunk 1 is plain prose; chunk 2 has\n+        # the markdown markers.  Without the fix, chunk 1 went out as\n+        # ``msg_type=text``.\n+        first_chunk = \"Here is a short intro that has no markdown markers at all.\"\n+        second_chunk = \"## Heading\\nAnd then some **bold** text.\"\n+\n+        with patch.object(\n+            adapter, \"truncate_message\", return_value=[first_chunk, second_chunk],\n+        ), patch(\"plugins.platforms.feishu.adapter.asyncio.to_thread\", side_effect=_direct):\n+            result = asyncio.run(\n+                adapter.send(\n+                    chat_id=\"oc_chat\",\n+                    content=first_chunk + \"\\n\" + second_chunk,\n+                )\n+            )\n+\n+        self.assertTrue(result.success)\n+        self.assertEqual(len(captured), 2)\n+        msg_types = [r.request_body.msg_type for r in captured]\n+        self.assertEqual(msg_types, [\"post\", \"post\"])\n+\n+    @patch.dict(os.environ, {}, clear=True)\n+    def test_send_plain_text_message_not_upgraded_by_prefer_post(self):\n+        \"\"\"A message with no markdown at all must still go out as plain\n+        ``msg_type=text`` — the whole-message ``prefer_post`` decision\n+        only flips on when the formatted message matches the hint regex.\n+        \"\"\"\n+        from gateway.config import PlatformConfig\n+        from plugins.platforms.feishu.adapter import FeishuAdapter\n+\n+        adapter = FeishuAdapter(PlatformConfig())\n+        captured = []\n+\n+        class _MessageAPI:\n+            def create(self, request):\n+                captured.append(request)\n+                return SimpleNamespace(\n+                    success=lambda: True,\n+                    data=SimpleNamespace(\n+                        message_id=f\"om_chunk_{len(captured)}\",\n+                    ),\n+                )\n+\n+        adapter._client = SimpleNamespace(\n+            im=SimpleNamespace(\n+                v1=SimpleNamespace(\n+                    message=_MessageAPI(),\n+                )\n+            )\n+        )\n+\n+        async def _direct(func, *args, **kwargs):\n+            return func(*args, **kwargs)\n+\n+        with patch(\"plugins.platforms.feishu.adapter.asyncio.to_thread\", side_effect=_direct):\n+            result = asyncio.run(\n+                adapter.send(\n+                    chat_id=\"oc_chat\",\n+                    content=\"just a plain sentence\",\n+                )\n+            )\n+\n+        self.assertTrue(result.success)\n+        self.assertEqual(len(captured), 1)\n+        self.assertEqual(captured[0].request_body.msg_type, \"text\")\n+\n     @patch.dict(os.environ, {}, clear=True)\n     def test_send_uses_post_for_inline_markdown(self):\n         from gateway.config import PlatformConfig\n"]}]},{"kind":"pr","number":79420,"title":"fix(feishu): render clarify prompts as interactive card buttons with text fallback","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54304","number":54304},{"kind":"pr","value":"#76274","number":76274}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":109140,"title":"Feishu inbound post text is no longer markdown-escaped (#9816, salvage #9991)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/platforms/feishu/adapter.py","tests/gateway/test_feishu.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}