Architecture
Agent Feishu Channel is built around a WebSocket connection to Feishu and a per-chat session model that drives either Claude or Codex behind a shared Feishu orchestration layer.
System Overview
Feishu WebSocket
|
v
FeishuGateway (event decryption, dedup, access control)
|
+-- onMessage --> parseInput (router)
| |
| +-- /command --> CommandDispatcher
| |
| +-- plain text --> ClaudeSession.submit
| |
| v
| provider queryFn (Claude or Codex)
| |
| +-- tool_use --> PermissionBroker --> Feishu card
| +-- thinking --> Feishu card (streaming)
| +-- text --> Feishu answer card
|
+-- onCardAction --> PermissionBroker.resolveByCard
QuestionBroker.resolveByCard
CommandDispatcher.resolveCdConfirmComponents
FeishuGateway
The entry point for all Feishu events. It receives WebSocket messages, verifies signatures, deduplicates events (Feishu may deliver the same event more than once), and enforces access control based on the allowed_open_ids list.
Incoming messages are routed through parseInput:
- Slash commands (e.g.
/new,/cd) go to the CommandDispatcher. - Plain text and
!-prefixed interrupts go to the ClaudeSession. - Card action callbacks (permission approvals/denials) go to the appropriate broker.
ClaudeSession
A per-chat state machine that manages the conversation with the selected provider. Each Feishu chat has at most one active session record, with a sticky provider choice once selected.
State machine:
idle --> generating --> idle
^ |
| |
+---- (on complete) ---+- idle — waiting for user input. New messages are submitted immediately.
- generating — the provider is processing. Incoming messages are queued and will be submitted once the current turn completes.
The session drives the provider runtime: it submits user text to the active provider adapter, then handles the stream of normalized events (text chunks, tool use requests, thinking blocks) as they arrive.
The session also owns staged context mitigation before backend hard failures:
- warn when context usage is high
- compact the current provider thread if possible
- start a summarized fresh session that preserves unfinished work and key constraints
- keep the old backend-driven reset-and-retry path as the last fallback
ClaudeSessionManager
Maps chat_id to ClaudeSession instances. Handles:
- Creating new sessions on first contact from a chat.
- Persisting session state to disk (
state.json) for crash recovery. - Restoring sessions on process restart.
- Preserving provider selection and provider-native resume IDs.
- Pruning expired sessions based on
session_ttl_days.
Provider Runtime
The provider layer now sits between the session state machine and the SDK-specific transports:
- Claude uses
@anthropic-ai/claude-agent-sdk - Codex uses
@openai/codex-sdk
Both adapters normalize their run handles into the shared session/query contract so Feishu rendering, approvals, queueing, and persistence do not need provider-specific branches everywhere.
FeishuPermissionBroker
Manages the permission approval flow for tool calls:
- When the active provider wants to use a tool (file write, shell command, etc.), the broker posts an interactive permission card to the Feishu group.
- The card shows the tool name and parameters, with Approve / Deny buttons.
- Only the user who sent the triggering message can click.
- If no response is received within
permission_timeout_seconds, the request is auto-denied. - A warning reminder is posted
permission_warn_before_secondsbefore the deadline.
Codex currently degrades mid-turn acceptEdits escalation to a no-op, but the surrounding Feishu approval flow and session model remain shared.
CommandDispatcher
Handles all slash commands (/new, /stop, /cd, /config set, etc.). Each command is parsed, validated, and executed. Some commands (like /cd) post a confirmation card before taking effect.
Data Flow: Message Lifecycle
A typical message flows through the system like this:
- User sends a message in a Feishu group chat.
- FeishuGateway receives the WebSocket event, decrypts it if needed, deduplicates, and checks if the sender's
open_idis inallowed_open_ids. - parseInput determines if it is a slash command or plain text.
- For plain text: ClaudeSession.submit is called.
- If the session is idle, a new SDK query starts immediately.
- If the session is generating, the message is queued.
- The selected provider processes the input. As events stream back:
- Text chunks are assembled and rendered into a Feishu answer card (streaming updates).
- Thinking blocks are sent as separate card messages (unless
hide_thinkingis enabled). - Tool use requests trigger the PermissionBroker, which posts an approval card.
- The user approves or denies the tool call via the Feishu card.
- The tool result is fed back to the provider, which continues generating.
- When the turn completes, the session returns to idle and processes any queued messages.