Feishu CardKit reply sends can fail after the platform has already accepted the message. Reusing one outbound UUID across the initial reply send and the create fallback keeps the fallback idempotent instead of risking a second visible message.
Constraint: Feishu im.message.reply and im.message.create both accept uuid idempotency keys
Rejected: Removing the create fallback entirely | would regress CardKit create/send fallback behavior when the original reply truly did not land
Confidence: high
Scope-risk: narrow
Directive: Keep reply fallback sends on the same outbound UUID; do not regenerate it per API attempt
Tested: bun test feishu/__tests__/streaming-card.test.ts
Tested: bun test feishu/__tests__/cardkit.test.ts feishu/__tests__/streaming-card.test.ts
Tested: cd adapters && bun run test:feishu
Tested: bun run check:adapters
Tested: cd adapters && bunx tsc --noEmit
Feishu CardKit and DingTalk AI Card updates run inside the serialized adapter message path. A hung platform update can prevent the later message_complete event from being handled, leaving mobile IM users stuck on a partial streaming card.
This bounds platform card API calls and gives Feishu a final patch fallback when CardKit finalization fails, so a bad intermediate frame cannot permanently block the chat turn.
Constraint: Feishu and DingTalk card APIs are external network calls without reliable SDK-level deadline guarantees
Rejected: Disable streaming cards entirely | loses the live IM experience for normal fast responses
Confidence: high
Scope-risk: narrow
Directive: Keep IM card platform calls bounded because adapter server messages are serialized per chat
Tested: bun test adapters/feishu/__tests__/streaming-card.test.ts
Tested: bun test adapters/dingtalk/__tests__/ai-card.test.ts
Tested: bun run check:adapters
Tested: bunx tsc -p adapters/tsconfig.json --noEmit
Not-tested: Full bun run verify was interrupted during the coverage lane after dependency setup
Replace legacy im.message.patch streaming path with a Feishu CardKit
Schema 2.0 pipeline mirroring openclaw-lark's implementation. Desktop
now renders real formatted markdown (headings, tables, code blocks)
instead of literal text, and streaming output updates per chat without
cross-talk.
New modules:
- card-errors.ts: structured Lark SDK error parsing for code 230020
(rate limit -> skip frame) and 230099/11310 (table limit -> disable
CardKit streaming and fall back to settings+update at finalize)
- flush-controller.ts: mutex + needsReflush + long-gap batching
throttler for per-chat flush coordination
- cardkit.ts: thin wrapper over client.cardkit.v1.* (card.create +
im.message.{create,reply} + cardElement.content + card.settings +
card.update), with monotonic sequence and CardKitApiError
- streaming-card.ts: per-chat StreamingCard state machine covering
idle -> creating -> streaming -> finalizing -> completed/aborted,
including patch fallback when CardKit create/send fails
index.ts rewrite:
- replaces MessageBuffer/chatStates/buffers maps with a single
streamingCards Map<chatId, StreamingCard>
- content_start{text}/content_delta create or reuse the card; finalize
runs on message_complete; abort renders a red error card
- /clear and other silent commands stay out of the streaming-card path
so they don't leave empty cards
- all command handlers + normal chat are wrapped in the per-chat
enqueue() serial queue, preventing reply reordering when commands
are fired rapidly
- createSessionForChat() now first resets any stale WS session before
calling connectSession, fixing /projects pick_project leaving the
old session bound to the previous workDir
- normal messages pre-create the card before sendUserMessage so users
see a "☁️ 正在思考中..." loading indicator immediately
- content_start{tool_use} no longer finalizes the current card, letting
one user turn's full text stay in a single card
markdown-style.ts:
- default cardVersion changed from 1 to 2 (Schema 2.0)
- headings H2~H6 demoted to H5, H1 to H4, only when source has H1~H3
- Schema 2.0 <br> padding around headings, tables, code blocks
- stripInvalidImageKeys() drops non-img_* image references
- FEISHU_CARD_TABLE_LIMIT=3 + sanitizeTextForCard() wraps 4th+ table
in a fenced code block to avoid 230099/11310
Tests: 182 pass, 0 fail, 383 expect() calls across 6 files. tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>