docs(channels): add custom channel plugins documentation

- Document channels config in extension manifest
- Add guide for creating custom channel adapters
- Explain ChannelPlugin interface and ChannelBase usage

This enables users to extend the channel system with custom platform adapters.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-26 14:41:20 +00:00
parent 0f9e4409df
commit 01c2e5a373
4 changed files with 253 additions and 1 deletions

View file

@ -0,0 +1,242 @@
# Custom Channel Plugins
You can extend the channel system with custom platform adapters packaged as [extensions](../../extension/introduction). This lets you connect Qwen Code to any messaging platform, webhook, or custom transport.
## How It Works
Channel plugins are loaded at startup from active extensions. When `qwen channel start` runs, it:
1. Scans all enabled extensions for `channels` entries in their `qwen-extension.json`
2. Dynamically imports each channel's entry point
3. Registers the channel type so it can be referenced in `settings.json`
4. Creates channel instances using the plugin's factory function
The plugin provides a `ChannelPlugin` object that tells the channel system how to create your adapter. Your adapter extends `ChannelBase`, which gives you the full pipeline for free: sender gating, group policies, session routing, and the ACP bridge to the agent.
## Creating a Channel Plugin
### 1. Set up the project
Create a new directory for your extension:
```bash
mkdir my-channel-extension
cd my-channel-extension
npm init -y
```
Install the channel base package (adjust the path to your qwen-code checkout):
```bash
npm install @qwen-code/channel-base
```
### 2. Write the channel adapter
Create a class that extends `ChannelBase`. You need to implement three methods:
- **`connect()`** — Connect to your platform (WebSocket, polling, webhook, etc.)
- **`sendMessage(chatId, text)`** — Send a response back to a specific chat
- **`disconnect()`** — Clean up connections on shutdown
When your platform delivers an incoming message, build an `Envelope` and call `this.handleInbound(envelope)`. The base class handles everything else: access control, session routing, prompting the agent, and calling your `sendMessage()` with the response.
```typescript
import { ChannelBase } from '@qwen-code/channel-base';
import type {
ChannelConfig,
ChannelBaseOptions,
Envelope,
AcpBridge,
} from '@qwen-code/channel-base';
interface MyPlatformConfig extends ChannelConfig {
apiKey: string;
webhookUrl: string;
}
export class MyPlatformChannel extends ChannelBase {
private client: any;
constructor(
name: string,
config: MyPlatformConfig & Record<string, unknown>,
bridge: AcpBridge,
options?: ChannelBaseOptions,
) {
super(name, config, bridge, options);
}
async connect(): Promise<void> {
// Connect to your platform
this.client = await createPlatformClient(this.config);
// When a message arrives, push it through the pipeline
this.client.on('message', (msg) => {
const envelope: Envelope = {
channelName: this.name,
senderId: msg.userId,
senderName: msg.userName,
chatId: msg.chatId,
text: msg.text,
isGroup: msg.isGroup ?? false,
isMentioned: msg.isMentioned ?? false,
isReplyToBot: msg.isReplyToBot ?? false,
};
this.handleInbound(envelope);
});
}
async sendMessage(chatId: string, text: string): Promise<void> {
await this.client.send(chatId, text);
}
disconnect(): void {
this.client?.close();
}
}
```
### 3. Export the plugin
Create an `index.ts` (or `index.js`) that exports a `plugin` object conforming to the `ChannelPlugin` interface:
```typescript
import type { ChannelPlugin } from '@qwen-code/channel-base';
import { MyPlatformChannel } from './MyPlatformChannel.js';
export const plugin: ChannelPlugin = {
channelType: 'my-platform',
displayName: 'My Platform',
requiredConfigFields: ['apiKey'],
createChannel: (name, config, bridge, options) =>
new MyPlatformChannel(name, config as any, bridge, options),
};
```
The fields are:
| Field | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------- |
| `channelType` | Yes | Unique type identifier. Must match the key in `qwen-extension.json` |
| `displayName` | Yes | Human-readable name shown in CLI output |
| `requiredConfigFields` | No | Extra config fields your channel needs beyond the standard `ChannelConfig` |
| `createChannel` | Yes | Factory function that creates your channel adapter instance |
### 4. Create the extension manifest
Create `qwen-extension.json` in your project root:
```json
{
"name": "my-channel-extension",
"version": "1.0.0",
"channels": {
"my-platform": {
"entry": "dist/index.js",
"displayName": "My Platform Channel"
}
}
}
```
The `channels` field maps channel type names to their configuration:
| Field | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------- |
| `entry` | Yes | Relative path to the compiled JS entry point (must export `plugin`) |
| `displayName` | No | Human-readable name for CLI output |
| `requiredConfigFields` | No | Extra config fields the channel requires |
> **Note:** The channel type key (e.g., `my-platform`) must match the `channelType` value in your exported plugin object. The system validates this at load time.
### 5. Build the extension
Compile your TypeScript to JavaScript. The entry point in `qwen-extension.json` must point to compiled JS, not TypeScript source:
```bash
npx tsc
```
### 6. Install the extension
You can install from a local path during development:
```bash
qwen extensions install /path/to/my-channel-extension
```
Or link it for development (changes are reflected immediately):
```bash
qwen extensions link /path/to/my-channel-extension
```
### 7. Configure the channel
Add a channel entry to `~/.qwen/settings.json` using your custom type:
```json
{
"channels": {
"my-bot": {
"type": "my-platform",
"apiKey": "$MY_PLATFORM_API_KEY",
"senderPolicy": "open",
"cwd": "/path/to/project"
}
}
}
```
All standard channel options (`senderPolicy`, `allowedUsers`, `sessionScope`, `cwd`, `instructions`, `groupPolicy`, `groups`, `model`) work with custom channels.
### 8. Start the channel
```bash
qwen channel start my-bot
```
## The Envelope
The `Envelope` is the message object you build from your platform's incoming data and pass to `handleInbound()`:
| Field | Type | Required | Description |
| ---------------- | ------- | -------- | ------------------------------------------------- |
| `channelName` | string | Yes | The channel name (use `this.name`) |
| `senderId` | string | Yes | Platform-specific user identifier |
| `senderName` | string | Yes | Display name of the sender |
| `chatId` | string | Yes | Platform-specific chat/conversation identifier |
| `text` | string | Yes | The message text |
| `threadId` | string | No | Thread identifier (for `sessionScope: "thread"`) |
| `isGroup` | boolean | Yes | Whether the message is from a group chat |
| `isMentioned` | boolean | Yes | Whether the bot was @mentioned |
| `isReplyToBot` | boolean | Yes | Whether the message is a reply to the bot |
| `referencedText` | string | No | Text of a quoted/replied-to message (for context) |
| `imageBase64` | string | No | Base64-encoded image data (for multimodal models) |
| `imageMimeType` | string | No | MIME type of the image (e.g., `image/jpeg`) |
## What You Get for Free
By extending `ChannelBase`, your channel automatically supports:
- **Sender policies**`allowlist`, `pairing`, and `open` access control
- **Group policies** — Per-group settings with optional @mention gating
- **Session routing** — Per-user, per-thread, or single shared sessions
- **DM pairing** — Full pairing code flow for unknown users
- **Slash commands**`/help`, `/clear`, `/status` work out of the box
- **Custom instructions** — Prepended to the first message in each session
- **Crash recovery** — Automatic restart with session preservation
- **Per-session serialization** — Messages are queued to prevent race conditions
## Example: Mock Channel Plugin
The `@qwen-code/channel-mock` package (in `packages/channels/mock/`) is a complete reference implementation. It connects to a WebSocket server and routes messages through the full pipeline:
```
Mock Client → HTTP → Mock Server → WebSocket → MockPluginChannel
→ ChannelBase → AcpBridge → qwen-code agent
→ response flows back the same path
```
See `packages/channels/mock/src/MockPluginChannel.ts` for a working example of a WebSocket-based channel adapter.