Skip to content

feat(feishu): auto-convert Markdown tables to interactive card tables - #47697

Open
brokenarrow2099 wants to merge 21 commits into
NousResearch:mainfrom
brokenarrow2099:feat/feishu-table-card
Open

feat(feishu): auto-convert Markdown tables to interactive card tables#47697
brokenarrow2099 wants to merge 21 commits into
NousResearch:mainfrom
brokenarrow2099:feat/feishu-table-card

Conversation

@brokenarrow2099

Copy link
Copy Markdown

Feishu Interactive Card Table Support / 飞书交互卡片表格支持

Problem / 问题

Feishu bot messages do not support Markdown tables — they render as plain text with poor formatting. Users need structured tabular data (model status, progress tables, etc.) displayed natively in Feishu.

飞书机器人消息不支持 Markdown 表格渲染,表格会以纯文本形式展示,格式混乱。用户需要以原生卡片表格的形式展示结构化数据(模型状态、进度表等)。

Solution / 解决方案

This PR adds automatic Markdown table to Feishu interactive card conversion at the Gateway level. When the agent sends a message containing a Markdown table to Feishu, the Gateway now:

  1. Detects Markdown tables via regex in the send() method
  2. Converts the table to a Feishu interactive card with the table component
  3. Sends the card via send_interactive_card() API
  4. Sends any remaining text (before/after the table) through the normal text pipeline
  5. Falls back gracefully to plain text if card parsing fails

本 PR 在 Gateway 层面实现了 Markdown 表格到飞书交互卡片表格的自动转换。当 agent 发送包含 Markdown 表格的消息到飞书时,Gateway 会:

  1. send() 方法中通过正则检测 Markdown 表格
  2. 将表格转换为飞书交互卡片(使用 table 组件)
  3. 通过 send_interactive_card() API 发送卡片
  4. 表格前后的文本通过正常文本通道发送
  5. 卡片解析失败时优雅降级为纯文本

Changes / 改动

Commit 1: 1a6edfc89 — feat(feishu): add interactive card support to send_message tool

  • _send_feishu_card() in send_message_tool.py: standalone function to send Feishu interactive cards via the tool
  • send_interactive_card() in feishu.py: new adapter method for card delivery
  • Fixed async bug: removed await from sync _send_feishu_card call
  • Fixed response.success to response.success() (method call, not property)
  • Fixed receive_id_type: oc_ prefix chat_ids use chat_id type

Commit 2: c030138b7 — feat(feishu): auto-convert Markdown tables in gateway send()

  • _markdown_table_to_card(): standalone converter function (Markdown table string to Feishu card JSON)
  • send() method: intercepts Markdown tables, routes to card path
  • Proper error handling and fallback to normal text path

Commit 3: 3b9b35e9b — fix(feishu): fix regex to capture full table including data rows

  • Original regex only matched header plus separator lines, missing data rows
  • Updated _MARKDOWN_TABLE_RE to require header plus separator plus data rows
  • Root cause: converter received 2-row table with no data, returned None

Files Changed / 修改的文件

File Purpose
tools/send_message_tool.py _send_feishu_card() — tool-level card sending
gateway/platforms/feishu.py send_interactive_card() + auto-convert in send() + _markdown_table_to_card()

Usage / 使用方式

Automatic (recommended) / 自动方式(推荐):
Just write Markdown tables in your response — the Gateway converts them automatically.
直接在回复中写 Markdown 表格,Gateway 会自动转换。

Manual via send_message tool / 手动方式:
send_message(target="feishu", card={"header": {...}, "elements": [{"tag": "table", ...}]})

Conversion script / 转换脚本:
echo "| A | B |\n|---|---|\n| 1 | 2 |" | python3 ~/.hermes/scripts/md_to_feishu_card.py --title "Title" --compact

Testing / 测试

  • Card sending via send_message tool works
  • Auto-conversion in Gateway send() path works
  • Table with Chinese characters renders correctly
  • Fallback to plain text when card parsing fails
  • Multiple table rows (3+) captured correctly

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/feishu Feishu / Lark adapter P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Jun 17, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #12114 — same card-based approach (detect markdown table at gateway send(), convert to a Feishu interactive card table component via send_interactive_card(), with a plain-text fallback). #12114 is the canonical, earliest-open PR in a heavily saturated Feishu-table cluster (#45907, #27768, #18155, #17006 and others all duplicate it). Note: the alternative post-routing mechanism is tracked separately in #45583.

Jiayi Zhang added 11 commits June 17, 2026 18:46
- Add _send_feishu_card() in send_message_tool.py for standalone card sending
- Add send_interactive_card() method to FeishuAdapter in feishu.py
- Support card parameter in send_message tool schema (type: object)
- Handle lark_oapi response.success() as method, with .code==0 fallback
- Correct receive_id_type to 'chat_id' for oc_ prefixed chat IDs
- Strip await from _send_feishu_card call (non-async context)
… in gateway send()

When the agent sends a message containing a Markdown table to Feishu,
the gateway now automatically:
1. Extracts the Markdown table from the message
2. Converts it to a Feishu interactive card with table component
3. Sends the card separately via send_interactive_card()
4. Sends the remaining text through the normal pipeline

This eliminates the need for the agent to manually construct card JSON
and fixes the poor rendering of Markdown tables on Feishu client.

Key changes:
- _markdown_table_to_card(): standalone converter function
- send() method: intercepts Markdown tables, routes to card path
- Proper error handling and fallback to normal text path
The original regex only matched header+separator lines, causing
_markdown_table_to_card to receive a 2-row table with no data and
return None. Updated regex to require header+separator+data rows,
so the full table content is captured.

Root cause: _MARKDOWN_TABLE_RE matched only '| header |\n|---|'
but not the data rows '| value |', so the converter had nothing
to work with.
Calculate column width based on max content length (header + data rows),
converting char count to pixel width (12px/char + 20px padding), clamped
to [80, 300]px. This significantly reduces the Feishu-native truncation
behavior where cells with insufficient space show ellipsis.
When a streaming response is finalized (edit_message finalize=True),
check if the accumulated text contains Markdown tables. If so, send
the table as a separate interactive card and replace the streaming
message with a brief notice. This ensures tables in streaming
responses are properly converted to cards, not just the first
non-streaming message.
Instead of only converting the first Markdown table, recursively call
send() for the remaining content after each table. This ensures all
tables in a long response are converted to cards, not just the first one.

Also sends the 'before' text separately from the 'after' text so that
text preceding a table is delivered before the card.
Replace recursive send() with asyncio.gather to send all table
cards in parallel, reducing latency from N×500ms to ~500ms regardless
of table count. After all cards are sent, deliver remaining text.
Use _display_width() to estimate display width (CJK=2, ASCII=1, emoji=2)
instead of raw char count. Increase pixel multiplier from 12 to 16 and
max width from 300 to 400px. This fixes truncation of Chinese content
like '零样本语音合成' which was too narrow at 116px.
User feedback: columns were too wide. Adjusted from display_width*16+30
to display_width*10+20, max 350px instead of 400px.
Integrates PR NousResearch#37476 (gateway-status-card) into our feishu-table-card branch.
Adds format_hermes_status_card() for compact status display with:
- Version, git commit, gateway/system uptime
- Model, fallbacks, tokens, cache hit rate
- Context usage, compactions, active tasks, queue depth
@brokenarrow2099
brokenarrow2099 force-pushed the feat/feishu-table-card branch from 3d8a69f to 60b3083 Compare June 17, 2026 11:13
Jiayi Zhang added 10 commits June 17, 2026 19:20
- _status_context_limit: also check model.context_length, skip
  non-numeric top-level context (which is a dict like {engine: ...})
- _format_context: show 'N used' when limit is unknown but current
  is available, instead of always returning 'unknown'
…nknown

When last_prompt_tokens is 0 (agent hasn't run yet), fallback to 0
instead of None so _format_context renders '0/131.1k (0%)' rather
than 'unknown'.
- Read context_tokens/context_limit from running agent's
  context_compressor for accurate real-time values (128K window).
- Fallback to persisted session_entry.last_prompt_tokens when
  no agent is active.
- _status_model_label now also reads 'default' key (for model.default
  in config.yaml) and never exposes api_key/base_url.
…uardrails

- Add _output_indicates_task_failure() to detect task-level failures
  (HTTP errors, tracebacks, etc.) in terminal/execute_code output even
  when exit code is 0
- Extend no_progress detection to ALL tools (not just idempotent),
  preventing infinite loops on mutating tools like terminal and execute_code
- Add repeated tool call prevention guidance to system prompt
- Update tests to reflect new behavior

@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 a native Feishu table presentation. The underlying limitation remains on current main: plugins/platforms/feishu/adapter.py:4524-4530 explicitly sends Markdown tables as plain text because Feishu post Markdown does not render them.

Problems

  • The table implementation changes gateway/platforms/feishu.py, but commit 5600105478ffde29d7566b45421b100eaa29c4ef moved the live adapter to plugins/platforms/feishu/adapter.py; current outbound delivery is FeishuAdapter.send() at plugins/platforms/feishu/adapter.py:1887-1942. This cannot be cleanly cherry-picked.
  • The diff also includes unrelated guardrail, /status, DingTalk, and media-delivery changes. In particular, tools/send_message_tool.py adds a cache-copy step before path filtering specifically so outside-root files pass validation; that should not ride with a table-rendering feature.

Suggested changes

  • Rework only the card-table feature against plugins/platforms/feishu/adapter.py, with focused adapter tests for conversion, fallback, ordering, multiple tables, and finalization.
  • Split the unrelated changes out, and retain the existing media path-validation boundary.

This is an automated hermes-sweeper review.

@@ -1778,11 +1883,108 @@ async def send(
reply_to: Optional[str] = None,

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.

Current main moved the live Feishu adapter to plugins/platforms/feishu/adapter.py in 5600105478ffde29d7566b45421b100eaa29c4ef; please rework this feature against that plugin surface rather than the removed inline adapter.

@@ -380,6 +388,12 @@ def _handle_send(args):
force_document_attachments = "[[as_document]]" in message

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 copies source paths into an allowed cache before filter_media_delivery_paths(), intentionally making paths outside the allowed roots pass validation. Please remove this unrelated validation bypass from the table feature.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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 duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have platform/feishu Feishu / Lark adapter sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants