fix(feishu): unify outbound to post and degrade tables to text element - #16474
fix(feishu): unify outbound to post and degrade tables to text element#16474HanShaoshuai-k wants to merge 1 commit into
Conversation
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
left a comment
There was a problem hiding this comment.
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.pymodule. The active adapter isplugins/platforms/feishu/adapter.py, including_build_outbound_payload()at:4524; current tests import that plugin adapter attests/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
8e18d10318f9fb69f0b748db11e37de44b71da85to prevent blank post-md bubbles. Please validate the proposed posttext-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 |
There was a problem hiding this comment.
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.
|
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! |
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 atextrow inside the post payload, closing Bug 2 by avoiding Feishu's unsupportedmd-tag-with-table path.Bug 1 — cross-type edit during streaming
_build_outbound_payloadre-decidedmsg_typefor every chunk via_MARKDOWN_HINT_RE. The first plain-text frame went out asmsg_type=text; once a later frame contained##or**bold**it flipped tomsg_type=post. Feishu rejected the cross-type edit (or the fallback stripped markdown), so users saw literal markdown source (## Headingshown 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
mdelement with a GFM pipe table renders as a silently blank bubble in the Feishu client — Feishu'smdtag does not support GFM table syntax. The API returnssuccess=Trueso 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 formcol | col/--- | ---.Related Issue
Fixes #9549 (Bug 2). No separate issue tracks Bug 1.
Type of Change
Changes Made
gateway/platforms/feishu.py:_build_outbound_payloadalways returns("post", ...). Committing to a singlemsg_typefor 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 nativetextmessage in the Feishu client._contains_gfm_table(content)plus_looks_like_gfm_table_row/_looks_like_gfm_table_separatorhelpers. 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._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 structuredresponse.codefield — 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._log_post_fallbackhelper. 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:test_edit_message_updates_existing_feishu_messagefor the unifiedpost/md-row behavior on plain text.has_gfm_table(both True and False branches)._is_post_format_errorrecognises codes 230001 / 230099 fromresponse.code._is_post_format_errorfalls back to the English template when only a message string is available.Inbound parsing impact
normalize_feishu_messagealready handlestextandpostsymmetrically, so unifying outbound topostdoes 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
aiohttpinstalled — 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 -qManual repro (requires a Feishu app + test chat):
Working..., thenedit_messagetoWorking...\n\n## Result\n- a. Bubble should update in place with the markdown rendered. Before: literal##or stalled bubble.| col | col |\n| --- | --- |\n| 1 | 2 |. Bubble should show table content as plain text. Before: empty bubble.metric | value\n--- | ---\nQPS | 1200. Same expected behavior. Before: empty bubble.Checklist
Code
Documentation & Housekeeping
cli-config.yaml.example— N/ACONTRIBUTING.md/AGENTS.md— N/A