feat: add channel adapter abstraction with Telegram and WebChat adapters
Implement Phase 3 channel adapters that decouple message sources from the agent via a uniform ChannelAdapter interface and ChannelRegistry. - Add ChannelAdapter/InboundMessage/OutboundMessage types - Add ChannelRegistry for adapter lifecycle and message routing - Add TelegramAdapter (grammy bot, auth middleware, confirmations, chunking) - Add WebChatAdapter (thin shim over GatewayServer) - Refactor daemon to use ChannelRegistry with per-channel-per-user agents - Add config.get/config.patch gateway handlers (Phase 2 loose end) - Add system.restart gateway handler (Phase 2 loose end) - Add implementation plans and design docs Tests: 225 passing (33 new channel adapter + gateway handler tests)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# Phase 3: Channel Adapters — Implementation Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Introduce a `ChannelAdapter` abstraction that decouples message sources (Telegram, WebChat, future Discord/WhatsApp/Slack) from the agent. Each adapter handles platform-specific I/O and maps messages to a common interface. A `ChannelRegistry` manages adapter lifecycle and routes messages to/from the agent.
|
||||
|
||||
## Scope (This Iteration)
|
||||
|
||||
1. **Channel types** — `ChannelAdapter` interface, `InboundMessage`, `OutboundMessage`, `ChannelStatus`
|
||||
2. **Channel registry** — Register, start/stop, route messages, adapter lifecycle
|
||||
3. **Telegram adapter** — Refactor existing `src/frontends/telegram/` into a `ChannelAdapter`
|
||||
4. **WebChat adapter** — Wrap the existing gateway WS into a `ChannelAdapter`
|
||||
5. **Daemon integration** — Replace direct bot/gateway creation with registry-managed adapters
|
||||
|
||||
Discord, WhatsApp, and Slack adapters are deferred (require new dependencies + credentials).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/channels/
|
||||
├── types.ts # ChannelAdapter interface, message types
|
||||
├── registry.ts # ChannelRegistry: lifecycle + message routing
|
||||
├── registry.test.ts # Registry tests
|
||||
├── index.ts # Barrel exports
|
||||
├── telegram/
|
||||
│ ├── adapter.ts # TelegramAdapter implements ChannelAdapter
|
||||
│ ├── adapter.test.ts # Adapter tests
|
||||
│ └── index.ts # Barrel
|
||||
└── webchat/
|
||||
├── adapter.ts # WebChatAdapter implements ChannelAdapter
|
||||
├── adapter.test.ts # Adapter tests
|
||||
└── index.ts # Barrel
|
||||
```
|
||||
|
||||
The existing `src/frontends/telegram/` code (bot.ts, handlers.ts, confirmations.ts) stays in place and is wrapped by the adapter. The adapter delegates to the existing bot creation logic. No breaking changes to existing code.
|
||||
|
||||
## Types Design
|
||||
|
||||
### InboundMessage
|
||||
```typescript
|
||||
interface InboundMessage {
|
||||
id: string; // Platform message ID
|
||||
channel: string; // Adapter name: "telegram", "webchat", etc.
|
||||
senderId: string; // Platform user ID
|
||||
senderName?: string; // Display name (optional)
|
||||
text: string; // Message text
|
||||
replyTo?: string; // ID of message being replied to
|
||||
timestamp: number; // Unix ms
|
||||
metadata?: Record<string, unknown>; // Platform-specific extras
|
||||
}
|
||||
```
|
||||
|
||||
### OutboundMessage
|
||||
```typescript
|
||||
interface OutboundMessage {
|
||||
text: string; // Response text (markdown)
|
||||
replyTo?: string; // Original message ID
|
||||
metadata?: Record<string, unknown>; // Platform-specific extras (e.g. parse_mode)
|
||||
}
|
||||
```
|
||||
|
||||
### ChannelAdapter
|
||||
```typescript
|
||||
interface ChannelAdapter {
|
||||
readonly name: string;
|
||||
readonly status: ChannelStatus;
|
||||
|
||||
/** Start the adapter (connect to platform, begin listening). */
|
||||
connect(): Promise<void>;
|
||||
|
||||
/** Stop the adapter (disconnect, clean up). */
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
/** Send a message to a specific peer. */
|
||||
send(peerId: string, message: OutboundMessage): Promise<void>;
|
||||
|
||||
/** Register the inbound message handler. Called by registry. */
|
||||
onMessage(handler: (msg: InboundMessage) => void): void;
|
||||
|
||||
/** Register a tool event handler for displaying tool execution status. */
|
||||
onToolEvent?(handler: (peerId: string, event: ToolStatusEvent) => void): void;
|
||||
}
|
||||
|
||||
type ChannelStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||
|
||||
interface ToolStatusEvent {
|
||||
type: 'start' | 'end';
|
||||
tool: string;
|
||||
args?: unknown;
|
||||
result?: { success: boolean; output: string; error?: string };
|
||||
}
|
||||
```
|
||||
|
||||
### ChannelRegistry
|
||||
```typescript
|
||||
class ChannelRegistry {
|
||||
register(adapter: ChannelAdapter): void;
|
||||
unregister(name: string): void;
|
||||
get(name: string): ChannelAdapter | undefined;
|
||||
list(): ChannelAdapter[];
|
||||
|
||||
/** Start all registered adapters. */
|
||||
startAll(): Promise<void>;
|
||||
|
||||
/** Stop all registered adapters. */
|
||||
stopAll(): Promise<void>;
|
||||
|
||||
/** Set the message handler that all adapters route to. */
|
||||
setMessageHandler(handler: (msg: InboundMessage, reply: (msg: OutboundMessage) => Promise<void>) => Promise<void>): void;
|
||||
}
|
||||
```
|
||||
|
||||
## Telegram Adapter Design
|
||||
|
||||
The `TelegramAdapter` wraps the existing `createTelegramBot()` logic:
|
||||
|
||||
- `connect()`: Creates grammy Bot, starts long polling
|
||||
- `disconnect()`: Stops the bot
|
||||
- `send()`: Calls `bot.api.sendMessage(peerId, text, { parse_mode: 'Markdown' })`
|
||||
- `onMessage()`: Sets up `bot.on('message:text', ...)` to convert grammy context to `InboundMessage`
|
||||
- Preserves existing confirmations, commands (/start, /reset, /status, /local, /cloud, /model)
|
||||
- Preserves chat ID allowlist check as middleware
|
||||
- Tool status display: adapter handles the `onToolUse` events by posting/editing Telegram messages
|
||||
|
||||
Constructor takes:
|
||||
```typescript
|
||||
interface TelegramAdapterConfig {
|
||||
botToken: string;
|
||||
allowedChatIds: number[];
|
||||
hookEngine?: HookEngine;
|
||||
}
|
||||
```
|
||||
|
||||
The adapter does NOT take an agent directly — the registry routes messages to the agent.
|
||||
|
||||
## WebChat Adapter Design
|
||||
|
||||
The `WebChatAdapter` is a thin shim since the gateway already handles WS connections.
|
||||
|
||||
- `connect()`: No-op (gateway server is already running)
|
||||
- `disconnect()`: No-op (gateway lifecycle managed by daemon)
|
||||
- `send()`: Sends via the gateway's WS connection to the peer
|
||||
- `onMessage()`: Hooks into the gateway's agent.send handler to intercept messages
|
||||
|
||||
Constructor takes:
|
||||
```typescript
|
||||
interface WebChatAdapterConfig {
|
||||
gateway: GatewayServer;
|
||||
}
|
||||
```
|
||||
|
||||
This adapter is simpler because the gateway already has its own session bridge and agent management. The adapter primarily exists to:
|
||||
1. Report WebChat as a registered channel in the registry
|
||||
2. Allow the daemon to manage all channels uniformly
|
||||
3. Provide status/metrics via a common interface
|
||||
|
||||
## Daemon Integration
|
||||
|
||||
The daemon currently:
|
||||
1. Creates a grammy Bot directly
|
||||
2. Creates a GatewayServer directly
|
||||
3. Starts both independently
|
||||
|
||||
After refactor:
|
||||
1. Creates a ChannelRegistry
|
||||
2. Creates TelegramAdapter + WebChatAdapter
|
||||
3. Registers both with the registry
|
||||
4. Registry starts all adapters
|
||||
|
||||
The message handler in the registry creates per-channel agents via the session manager, same as the existing session bridge pattern.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. `src/channels/types.ts` — Pure types (no runtime)
|
||||
2. `src/channels/registry.ts` — Registry class
|
||||
3. `src/channels/registry.test.ts` — Registry unit tests
|
||||
4. `src/channels/telegram/adapter.ts` — Telegram adapter
|
||||
5. `src/channels/telegram/adapter.test.ts` — Telegram adapter tests
|
||||
6. `src/channels/webchat/adapter.ts` — WebChat adapter
|
||||
7. `src/channels/webchat/adapter.test.ts` — WebChat adapter tests
|
||||
8. `src/channels/index.ts` + sub-barrel exports
|
||||
9. `src/daemon/index.ts` — Wire registry
|
||||
10. Run full test suite
|
||||
|
||||
## Existing Code Impact
|
||||
|
||||
- `src/frontends/telegram/` — **NOT deleted**. The adapter wraps these existing modules.
|
||||
- `src/gateway/` — **NOT modified**. WebChat adapter wraps the existing gateway.
|
||||
- `src/daemon/index.ts` — **Modified** to use ChannelRegistry.
|
||||
- `src/backends/native/agent.ts` — **NOT modified**. Agent creation happens in the registry message handler.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
- Unit tests for ChannelRegistry (mock adapters)
|
||||
- Unit tests for TelegramAdapter (mock grammy Bot)
|
||||
- Unit tests for WebChatAdapter (mock GatewayServer)
|
||||
- Existing tests remain unchanged (frontends/telegram, gateway)
|
||||
|
||||
---
|
||||
|
||||
*Plan Version: 1.0*
|
||||
*Created: 2026-02-05*
|
||||
*Parent: docs/plans/2026-02-05-openclaw-parity-design.md Phase 3*
|
||||
Reference in New Issue
Block a user