308 lines
9.9 KiB
Markdown
308 lines
9.9 KiB
Markdown
# 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.
|
|
|
|
```text
|
|
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:
|
|
|
|
```python
|
|
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.
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```text
|
|
<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:
|
|
|
|
```text
|
|
Weixin sidecar connector
|
|
-> Beaver connector bridge endpoint
|
|
-> ChannelRuntime.accept_inbound()
|
|
-> MessageBus
|
|
-> AgentService
|
|
```
|
|
|
|
Outbound flow:
|
|
|
|
```text
|
|
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.
|