feat(channel): forward non-OAuth card actions to inbound message pipeline#152
feat(channel): forward non-OAuth card actions to inbound message pipeline#152ChenyqThu wants to merge 1 commit intolarksuite:mainfrom
Conversation
…line Feishu interactive cards can trigger multi-step interactions where a button click should cause the agent to continue processing (e.g. confirm a draft, choose an option). Currently handleCardActionEvent() only delegates to handleCardAction() for OAuth flows; any non-OAuth card action is silently dropped. This change adds a fallback path: when handleCardAction() returns undefined (not an OAuth action), we normalise the card action payload into a synthetic FeishuMessageEvent and forward it through the existing enqueueFeishuChatTask + withTicket + handleFeishuMessage pipeline. This reuses all existing session-isolation, serialisation, dedup, and agent-dispatch logic without modification. The synthetic message: - sets sender.sender_id.open_id to the button operator - merges action.value + action.form_value into a JSON text body - tags the content with _action_tag (button tag) so the agent can identify which button was clicked - sets _card_action: true on the message so downstream handlers can distinguish card callbacks from regular user messages Closes larksuite#128
HanShaoshuai-k
left a comment
There was a problem hiding this comment.
Review: Changes Requested
The overall direction — reusing the inbound message pipeline for non-OAuth card actions — makes sense. However, the current implementation wraps the card action as a synthetic FeishuMessageEvent text message without corresponding changes downstream, which means the agent cannot actually receive card callbacks correctly in several scenarios.
Critical issues
- Group mention gate will reject card actions
checkMessageGate → mentionedBot(ctx) returns false for the synthetic message (no mentions). In groups with requireMention enabled (the default), card actions are silently dropped. The _card_action: true flag on the message is never checked by gate/parse/dispatch — it's cast via as and lost during parseMessageEvent.
- _card_action / _action_tag never reach the agent
These fields are not part of FeishuMessageEvent['message'] (bypassed via as cast). parseMessageEvent maps only typed fields into MessageContext, so the card-action marker is stripped. The agent receives an indistinguishable text message.
- Double JSON-encoded content
JSON.stringify({ text: JSON.stringify(actionValue) }) produces a string-within-a-string. After convertMessageContent, the agent gets a raw JSON string rather than structured action data.
Missing guards
chatId can be empty (both open_chat_id sources missing) — the message still enters the queue with chat_type: 'p2p'.
message_id fallback card_action_${Date.now()} can collide under concurrency.
Suggested approach
Card actions should be a first-class event type in the pipeline, not a disguised text message:
Define a FeishuCardActionEvent type alongside FeishuMessageEvent / FeishuReactionCreatedEvent.
Add a dedicated parseCardActionEvent that outputs MessageContext with contentType: 'card_action' and structured action data — no double encoding.
In the gate, handle card_action as a distinct branch: mention checks don't apply (same as reactions — the event type itself doesn't require a mention, rather than "skipping" the check via a hack), while sender/group ACL still applies.
Reusing enqueueFeishuChatTask + withTicket for serialization is correct — keep that part.
This follows the same pattern the codebase already uses for reactions and keeps the type system honest.
Problem
When an agent sends an interactive card with buttons, clicking those buttons triggers a
card.action.triggerWebSocket event. CurrentlyhandleCardActionEvent()delegates entirely tohandleCardAction()which is designed for the OAuth authorisation flow. Any non-OAuth card action (e.g. a "Confirm draft", "Cancel", or form submission button) returnsundefinedfromhandleCardAction()and is silently dropped — the agent never sees it.This makes it impossible to implement multi-step card interactions without patching the plugin locally.
Closes #128
Solution
Add a fallback path in
handleCardActionEvent(): whenhandleCardAction()returnsundefined, normalise the card action payload into a syntheticFeishuMessageEventand forward it through the existingenqueueFeishuChatTask + withTicket + handleFeishuMessagepipeline.This reuses all existing session-isolation, serialisation, dedup, and agent-dispatch logic without modification.
Synthetic message structure
sender.sender_id.open_id— the user who clicked the button (fromevent.operator.open_id)message.content— JSON-encoded text containing mergedaction.value+action.form_valuemessage._action_tag— the button'stagfield, so the agent can identify which button was clickedmessage._card_action: true— signal to downstream handlers that this is a card callback, not a regular user messagemessage.chat_id/message.chat_type— resolved fromevent.open_chat_idorevent.context.open_chat_idWhy this approach
enqueueFeishuChatTaskso card actions are serialised against concurrent messages in the same chat — no race conditions.chatIdis preserved, the agent receives the card action in the same session context as the original message that sent the card.action.form_valueis merged alongsideaction.value, so interactive forms (text inputs, dropdowns) work out of the box.handleCardActionreturnsundefined.Testing
tsc --noEmitpasses with no errors_action_namein the contentRelated