Files
beaver_project/docs/superpowers/specs/2026-06-02-chat-platform-channel-adapters-design.md

9.9 KiB

Chat Platform Channel Adapters Design

Date: 2026-06-02

Goal

Add first-class Beaver channel adapters for four messaging platforms:

  • FeishuAdapter
  • QQBotAdapter
  • TelegramAdapter
  • ExternalConnectorChannel for 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, or ChannelManager.
  • 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:

  • kind is one of feishu, qqbot, telegram, weixin.
  • account_id comes from channel config.
  • inbound messages must include ChannelIdentity.
  • outbound replies route by message.channel_identity when present, falling back to message.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 as telegram-main
  • kind: platform kind
  • account_id: configured account id
  • peer_id: platform chat, group, openid, or user conversation id
  • thread_id: platform topic/thread id when applicable
  • peer_type: dm, group, channel, or platform-specific value
  • user_id: platform sender id when available
  • message_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.appId
  • secrets.appSecret
  • config.domain: feishu or lark
  • config.connectionMode: websocket or webhook
  • config.requireMentionInGroups
  • config.allowFrom
  • config.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.appId
  • secrets.clientSecret
  • config.markdownSupport
  • config.dmPolicy
  • config.allowFrom
  • config.groupPolicy
  • config.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.botToken
  • config.mode: polling or webhook
  • config.webhookUrl
  • config.webhookSecret
  • config.requireMentionInGroups
  • config.allowFrom
  • config.groupAllowFrom
  • config.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 /send API.
  • Media send/receive when the sidecar provides normalized metadata.
  • Group delivery as best-effort only.

Configuration:

  • secrets.connectionToken
  • config.accountId
  • config.baseUrl
  • config.bridgeSecret
  • config.dmPolicy
  • config.allowFrom
  • config.groupPolicy
  • config.groupAllowFrom
  • config.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: require allowFrom or groupAllowFrom.
  • 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 content when 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 ChannelManager records outbound_delivery_failed.

Runtime Status

ChannelRuntime.statuses() should report platform channels with:

  • channel_id
  • kind
  • mode
  • display_name
  • enabled
  • state
  • account_id
  • last_error
  • last_event_at
  • capabilities

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 error and 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 kind and mode adapter 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:

  1. Telegram
  2. Feishu
  3. QQBot
  4. 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.