Skip to content

feat(channel): forward non-OAuth card actions to inbound message pipeline#152

Open
ChenyqThu wants to merge 1 commit intolarksuite:mainfrom
ChenyqThu:feat/card-action-forward-to-agent
Open

feat(channel): forward non-OAuth card actions to inbound message pipeline#152
ChenyqThu wants to merge 1 commit intolarksuite:mainfrom
ChenyqThu:feat/card-action-forward-to-agent

Conversation

@ChenyqThu
Copy link
Copy Markdown

Problem

When an agent sends an interactive card with buttons, clicking those buttons triggers a card.action.trigger WebSocket event. Currently handleCardActionEvent() delegates entirely to handleCardAction() which is designed for the OAuth authorisation flow. Any non-OAuth card action (e.g. a "Confirm draft", "Cancel", or form submission button) returns undefined from handleCardAction() 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(): when handleCardAction() returns undefined, 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.

Synthetic message structure

  • sender.sender_id.open_id — the user who clicked the button (from event.operator.open_id)
  • message.content — JSON-encoded text containing merged action.value + action.form_value
  • message._action_tag — the button's tag field, so the agent can identify which button was clicked
  • message._card_action: true — signal to downstream handlers that this is a card callback, not a regular user message
  • message.chat_id / message.chat_type — resolved from event.open_chat_id or event.context.open_chat_id

Why this approach

  • Zero changes to the message pipeline: the synthetic event flows through enqueueFeishuChatTask so card actions are serialised against concurrent messages in the same chat — no race conditions.
  • Session continuity: because the chatId is preserved, the agent receives the card action in the same session context as the original message that sent the card.
  • Form support: action.form_value is merged alongside action.value, so interactive forms (text inputs, dropdowns) work out of the box.
  • Backwards compatible: the OAuth path is completely unchanged; the new code only executes when handleCardAction returns undefined.

Testing

  • TypeScript: tsc --noEmit passes with no errors
  • Manually verified with a two-step email draft card: agent sends card with "AI Polish" / "Create Draft" / "Mark Done" buttons; clicking each button is received by the agent as a synthetic message with the correct _action_name in the content

Related

…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 HanShaoshuai-k added feature request New feature or request channel src/channel/ — WebSocket, events, monitor, reconnection labels Mar 19, 2026
Copy link
Copy Markdown
Collaborator

@HanShaoshuai-k HanShaoshuai-k left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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.

  1. _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.

  1. 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.

@HanShaoshuai-k HanShaoshuai-k added the changes requested Need do changes label Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changes requested Need do changes channel src/channel/ — WebSocket, events, monitor, reconnection feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support Feishu interactive card action callbacks in the standard inbound message pipeline

2 participants