Skip to content

fix(feishu): unify outbound to post and degrade tables to text element - #16474

Closed
HanShaoshuai-k wants to merge 1 commit into
NousResearch:mainfrom
HanShaoshuai-k:fix/feishu-streaming-markdown-rendering
Closed

fix(feishu): unify outbound to post and degrade tables to text element#16474
HanShaoshuai-k wants to merge 1 commit into
NousResearch:mainfrom
HanShaoshuai-k:fix/feishu-streaming-markdown-rendering

Conversation

@HanShaoshuai-k

Copy link
Copy Markdown

What does this PR do?

Fixes two markdown-rendering bugs in the Feishu adapter for streaming AI replies. The fix unifies all outbound chunks onto msg_type=post — closing Bug 1 by removing the cross-type-edit possibility entirely — and adds a GFM table detector that routes table content to a text row inside the post payload, closing Bug 2 by avoiding Feishu's unsupported md-tag-with-table path.

Bug 1 — cross-type edit during streaming

_build_outbound_payload re-decided msg_type for every chunk via _MARKDOWN_HINT_RE. The first plain-text frame went out as msg_type=text; once a later frame contained ## or **bold** it flipped to msg_type=post. Feishu rejected the cross-type edit (or the fallback stripped markdown), so users saw literal markdown source (## Heading shown as plain text) or a bubble stuck on the first plain-text frame.

Reproduces with: any streaming reply that starts non-markdown and later contains markdown — common in agent traces (📖 read_file: ... first, then a structured summary).

Bug 2 — silent blank bubble for content containing a GFM table

A post md element with a GFM pipe table renders as a silently blank bubble in the Feishu client — Feishu's md tag does not support GFM table syntax. The API returns success=True so no existing fallback path could recover. AI generates a report, hermes streams it, every API call succeeds, the user sees an empty message.

Reproduces with: any reply that contains a | col | col | / | --- | --- | table, or the no-outer-pipe form col | col / --- | ---.

Related Issue

Fixes #9549 (Bug 2). No separate issue tracks Bug 1.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

gateway/platforms/feishu.py:

  • _build_outbound_payload always returns ("post", ...). Committing to a single msg_type for every chunk eliminates Bug 1 structurally — streaming edits cannot cross types because there is only one type. Plain text wraps in one {"tag":"md","text":"..."} row, which renders identically to a native text message in the Feishu client.
  • Add _contains_gfm_table(content) plus _looks_like_gfm_table_row / _looks_like_gfm_table_separator helpers. Detects GFM tables both with outer pipes (| a | b |) and without (a | b), rejects setext headings (Title\n---), and skips fenced code blocks to avoid false positives on code samples.
  • _build_markdown_post_rows: when a GFM table is present, emit a single {"tag":"text"} row with the plain-text projection of the content.
  • Add _FEISHU_POST_FORMAT_ERROR_CODES = {230001, 230099} (Feishu API codes for "invalid message content" and "invalid card content"). Format-error detection in _is_post_format_error(*, response=None, text="") prefers the structured response.code field — locale-independent and avoids string-matching brittleness. Falls back to a string match against the canonical English error template for paths that only have an exception message.
  • New _log_post_fallback helper. Logs the fallback event and records whether the rejected payload contained a GFM table, for diagnostic correlation when grepping logs.

tests/gateway/test_feishu.py:

  • Updated test_edit_message_updates_existing_feishu_message for the unified post/md-row behavior on plain text.
  • Added 8 cases:
    • GFM table → text-row routing (with outer pipes).
    • GFM table → text-row routing (no outer pipes).
    • Setext heading is not misclassified as a table.
    • Fenced-code pipe is not misdetected.
    • Plain-text unification onto post.
    • Post-fallback log includes has_gfm_table (both True and False branches).
    • _is_post_format_error recognises codes 230001 / 230099 from response.code.
    • _is_post_format_error falls back to the English template when only a message string is available.

Inbound parsing impact

normalize_feishu_message already handles text and post symmetrically, so unifying outbound to post does not affect inbound parsing, message echo handling, dedupe, or self-message filtering.

Relation to existing PRs targeting #9549

Aware of open alternatives (#15956 / #16194 / #12114). They take different approaches (table-to-bullet conversion / interactive card with native table component). Defer to maintainer judgement on which to merge.

How to Test

pytest tests/gateway/test_feishu.py -q -o addopts="-m 'not integration'" \
  --deselect tests/gateway/test_feishu.py::TestAdapterBehavior::test_webhook_request_uses_same_message_dispatch_path

(The deselected case fails on environments without aiohttp installed — unrelated to this PR.)

Cross-suite regression:

pytest tests/gateway/test_stream_consumer.py tests/gateway/test_stream_consumer_fresh_final.py \
       tests/gateway/test_feishu_onboard.py tests/gateway/test_feishu_approval_buttons.py \
       tests/gateway/test_feishu_comment.py tests/gateway/test_feishu_comment_rules.py -q

Manual repro (requires a Feishu app + test chat):

  1. Bug 1 — send Working..., then edit_message to Working...\n\n## Result\n- a. Bubble should update in place with the markdown rendered. Before: literal ## or stalled bubble.
  2. Bug 2a — send a message containing | col | col |\n| --- | --- |\n| 1 | 2 |. Bubble should show table content as plain text. Before: empty bubble.
  3. Bug 2b — send the no-outer-pipe form metric | value\n--- | ---\nQPS | 1200. Same expected behavior. Before: empty bubble.

Checklist

Code

  • I've read the Contributing Guide
  • Conventional Commits format
  • PR contains only changes related to this fix
  • Tests pass
  • Tests added
  • Tested on macOS 15.6 / Python 3.11

Documentation & Housekeeping

  • Documentation update — N/A (internal adapter behavior, no public API change)
  • cli-config.yaml.example — N/A
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact considered — pure-Python regex + JSON
  • Tool descriptions/schemas — N/A

Two streaming-rendering regressions in the Feishu adapter:

1. Per-chunk msg_type re-decision in _build_outbound_payload could flip
   text→post mid-edit; Feishu rejected the cross-type edit so users
   saw literal markdown source or a stalled bubble.
2. A post `md` element containing a GFM table renders as a silently
   blank bubble — Feishu's md tag does not support GFM table syntax
   and the API returns success=True, so no existing fallback could
   recover.

Fix:
- _build_outbound_payload always returns ("post", ...); plain text
  wraps in a single `md` row, eliminating cross-type edits.
- _contains_gfm_table (fence-aware, both outer-pipe and no-outer-pipe
  forms) routes table content to a single `text` row inside the post
  payload so the content stays visible.
- _is_post_format_error uses response.code (230001 / 230099) when
  available, falls back to a string match for exception paths.
- _log_post_fallback logs the fallback event with a has_gfm_table flag
  for diagnostic correlation.

Change-Id: I553a8f71a3aa16c18dc9ad7dadada51a8c7a690d

@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 the focused reproduction and the fence-aware table handling. The type-selection premise remains live on current main: plugins/platforms/feishu/adapter.py:1904 classifies each truncated chunk independently, and :1958 independently classifies edits.

Problems

  • The patch targets the retired gateway/platforms/feishu.py module. The active adapter is plugins/platforms/feishu/adapter.py, including _build_outbound_payload() at :4524; current tests import that plugin adapter at tests/gateway/test_feishu.py:420. The PR is currently conflicting and needs a port rather than application as-is.
  • The current table guard was intentionally added by 8e18d10318f9fb69f0b748db11e37de44b71da85 to prevent blank post-md bubbles. Please validate the proposed post text-row behavior through the active adapter against a live Feishu/Lark client before replacing it.

Suggested changes

  • Port the implementation and tests to plugins/platforms/feishu/adapter.py.
  • Add current-surface regressions for plain-to-markdown streaming edits, no-outer-pipe tables, and a complete fenced-code table.

Automated hermes-sweeper review.

text_payload = {"text": content}
return "text", json.dumps(text_payload, ensure_ascii=False)
# Unified post: every outbound chunk goes out as `post` so streaming
# edits never cross msg_type. `_build_markdown_post_rows` decides

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 outbound-path change must be ported to the active bundled plugin at plugins/platforms/feishu/adapter.py:4524; gateway/platforms/feishu.py is no longer the runtime adapter on current main.

@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 12, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Closing — the problem this solved (a table breaking ALL formatting in the message) was fixed on main by #68121, but in the opposite direction: Feishu fixed table rendering in post/md server-side, so tables now render natively rather than being degraded to a text element inside post. Your unify-to-post direction for the rest of the message matched where the code ended up. Thanks!

@teknium1 teknium1 closed this Jul 20, 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/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feishu] Markdown tables not rendering in Feishu messages

3 participants