9.9 KiB
Chat Platform Channel Adapters Design
Date: 2026-06-02
Goal
Add first-class Beaver channel adapters for four messaging platforms:
FeishuAdapterQQBotAdapterTelegramAdapterExternalConnectorChannelfor Weixin personal-account sidecars
Each runtime channel must plug into the existing ChannelRuntime, normalize inbound platform messages into InboundMessage with ChannelIdentity, and deliver OutboundMessage replies back to the original platform conversation. Feishu, QQBot, and Telegram use Beaver-owned protocol adapters. Weixin personal-account support uses a docker-compose predeclared sidecar connector, so Beaver exposes it as an ExternalConnectorChannel rather than a Beaver-owned WeixinAdapter.
Non-Goals
- Use internal adapters by default, but allow external connector processes where platform SDK or login state requires them.
- Do not implement WhatsApp in this phase.
- Do not replace
ChannelRuntime,MessageBus, orChannelManager. - Do not move platform access policy into
AgentService. - Do not implement streaming token deltas for these channels in this phase.
- Do not promise stable Weixin group support; Weixin group delivery is best-effort only.
Architecture
Keep Beaver's channel runtime as the owner of lifecycle, dedupe, event logging, and agent dispatch.
platform SDK/API or sidecar connector
-> {Channel}Adapter or ExternalConnectorChannel bridge endpoint
-> ChannelRuntime.accept_inbound()
-> MessageBus.inbound
-> ChannelRuntime bridge
-> AgentService.handle_inbound_message()
-> MessageBus.outbound
-> ChannelManager.dispatch_outbound()
-> {Channel}Adapter.send() or ExternalConnectorChannel.send()
-> platform SDK/API or sidecar connector API
Adapters own platform-specific transport and delivery details when Beaver directly integrates a platform API. For Weixin, the sidecar owns the platform protocol, QR login, receive loop, send behavior, and login-state persistence. The runtime owns Beaver session identity, dedupe, event logging, and run dispatch in both cases.
Shared Adapter Contract
Each runtime channel implements the existing ChannelAdapter protocol:
channel_id: str
kind: str
mode: str
async def start() -> None
async def stop() -> None
async def send(message: OutboundMessage) -> None
Each Beaver-owned adapter receives a ChannelInboundSink and calls accept_inbound() for every normalized user message. ExternalConnectorChannel receives inbound Weixin messages through Beaver's connector bridge endpoint, then submits normalized messages to ChannelRuntime.accept_inbound().
For all four adapters:
kindis one offeishu,qqbot,telegram,weixin.account_idcomes from channel config.- inbound messages must include
ChannelIdentity. - outbound replies route by
message.channel_identitywhen present, falling back tomessage.session_id. - unsupported media is represented as text metadata in phase one rather than dropped silently.
Channel Configuration
All channels use the existing BeaverConfig.channels map.
{
"channels": {
"telegram-main": {
"enabled": true,
"kind": "telegram",
"mode": "polling",
"accountId": "bot-main",
"displayName": "Telegram Main",
"secrets": {
"botToken": "..."
},
"config": {
"requireMentionInGroups": true,
"maxMessageChars": 4096
}
}
}
}
Config keys stay channel-specific inside config and secrets. The factory chooses the adapter by kind and mode.
For sidecar-backed channels, config also includes the connector base URL and bridge settings. Beaver must call the already-running connector HTTP API and must not dynamically create containers or require Docker socket access.
Identity Mapping
All adapters map platform identity into ChannelIdentity:
channel_id: configured Beaver channel id, such astelegram-mainkind: platform kindaccount_id: configured account idpeer_id: platform chat, group, openid, or user conversation idthread_id: platform topic/thread id when applicablepeer_type:dm,group,channel, or platform-specific valueuser_id: platform sender id when availablemessage_id: platform message id or event id
The runtime continues to derive sessions as:
<channel_id>:<account_id>:<peer_id>[:<thread_id>]
Group sessions can later become per-user or per-thread by adding adapter-level thread_id rules without changing ChannelRuntime.
Adapter Scope
FeishuAdapter
Supports:
- WebSocket long connection as the preferred mode.
- Optional webhook mode if configured.
- Direct messages.
- Group messages gated by mention or config.
- Text outbound replies.
- Basic inbound media metadata and cached local file paths when available.
Configuration:
secrets.appIdsecrets.appSecretconfig.domain:feishuorlarkconfig.connectionMode:websocketorwebhookconfig.requireMentionInGroupsconfig.allowFromconfig.groupAllowFrom
QQBotAdapter
Supports:
- Official QQ Bot WebSocket gateway for inbound events.
- Official REST API for outbound text replies.
- Private C2C messages.
- Group messages.
- Guild/channel messages when the platform event provides them.
- Basic rich media intake as cached local files or text metadata.
Configuration:
secrets.appIdsecrets.clientSecretconfig.markdownSupportconfig.dmPolicyconfig.allowFromconfig.groupPolicyconfig.groupAllowFrom
TelegramAdapter
Supports:
- Bot API long polling as the default mode.
- Optional webhook mode if configured.
- Direct messages.
- Group messages gated by mention or config.
- Text replies with platform-safe formatting and chunking.
- Photo/document/audio/video intake as cached local files or metadata.
Configuration:
secrets.botTokenconfig.mode:pollingorwebhookconfig.webhookUrlconfig.webhookSecretconfig.requireMentionInGroupsconfig.allowFromconfig.groupAllowFromconfig.maxMessageChars
ExternalConnectorChannel For Weixin
Supports:
- Docker-compose predeclared sidecar connector.
- QR-login sessions started and observed through the sidecar HTTP API.
- Direct messages.
- Text replies sent through the sidecar
/sendAPI. - Media send/receive when the sidecar provides normalized metadata.
- Group delivery as best-effort only.
Configuration:
secrets.connectionTokenconfig.accountIdconfig.baseUrlconfig.bridgeSecretconfig.dmPolicyconfig.allowFromconfig.groupPolicyconfig.groupAllowFromconfig.maxMessageChars
Inbound flow:
Weixin sidecar connector
-> Beaver connector bridge endpoint
-> ChannelRuntime.accept_inbound()
-> MessageBus
-> AgentService
Outbound flow:
AgentService
-> MessageBus outbound
-> ExternalConnectorChannel.send()
-> Weixin sidecar connector /send
The sidecar is the Weixin protocol adapter. Beaver's ExternalConnectorChannel only validates bridge calls, normalizes the sidecar event boundary, preserves runtime dedupe/session semantics, and forwards outbound sends to the sidecar HTTP API.
Access Control
Adapters may block inbound messages before calling accept_inbound() when the platform has channel-native allowlist settings. Runtime dedupe still applies after adapter admission.
Initial policy values:
open: allow matching platform scope.allowlist: requireallowFromorgroupAllowFrom.disabled: ignore inbound messages for that scope.
Pairing is owned by the connector layer. Platform adapters assume a materialized ChannelConnection and adapter-ready runtime config. For Weixin personal-account support, the runtime channel is an ExternalConnectorChannel, not a Beaver-owned WeixinAdapter.
Delivery Semantics
Inbound:
- validate required routing fields before submitting to runtime.
- preserve raw platform payload in metadata only when useful for debugging.
- keep metadata small enough for event logs.
- include media paths in metadata and text summaries in
contentwhen the agent needs to know an attachment exists.
Outbound:
- send only final assistant replies in phase one.
- chunk messages to platform limits.
- mark
delivery_status = "unclaimed"when a target cannot be resolved. - raise or return delivery failures so
ChannelManagerrecordsoutbound_delivery_failed.
Runtime Status
ChannelRuntime.statuses() should report platform channels with:
channel_idkindmodedisplay_nameenabledstateaccount_idlast_errorlast_event_atcapabilities
Capabilities are conservative:
- Feishu:
receive_text,send_text,receive_media,groups - QQBot:
receive_text,send_text,receive_media,groups - Telegram:
receive_text,send_text,receive_media,groups - Weixin:
receive_text,send_text,receive_media,direct_messages
Error Handling
- Adapter startup failure sets channel state to
errorand does not stop other channels. - Runtime shutdown calls every adapter
stop(). - Platform transient errors should retry inside the adapter only when retrying cannot duplicate user-visible sends.
- Fatal credential/config errors should surface in channel status.
- Inbound duplicates are handled by existing
ChannelDedupeStore.
Testing
Add tests in small layers:
- factory tests for
kindandmodeadapter selection. - identity normalization tests for each platform.
- inbound adapter tests using fake platform payloads.
- outbound adapter tests with fake platform clients.
- runtime status tests for configured enabled/disabled/error channels.
Network live tests are out of scope for unit tests. Adapter constructors should accept injectable clients or lightweight transport functions so tests do not call real platform APIs.
Rollout
Implement one adapter at a time:
- Telegram
- Feishu
- QQBot
- Weixin
Telegram is first because its bot-token flow and text path are the simplest proof of the shared adapter pattern. Weixin is last because QR/login state, context tokens, and media handling are more specialized.