This provides documentation for the base infrastructure used to build Qwen Code channel adapters, including architecture overview, quick start guide, and API reference. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
12 KiB
@qwen-code/channel-base
Base infrastructure for building Qwen Code channel adapters. Provides the abstract base class, access control, session routing, and the ACP bridge that communicates with the agent.
If you're building a channel plugin, this is your only dependency.
Install
npm install @qwen-code/channel-base
Quick start
Subclass ChannelBase and implement three methods:
import { ChannelBase } from '@qwen-code/channel-base';
import type {
ChannelConfig,
Envelope,
AcpBridge,
} from '@qwen-code/channel-base';
class MyChannel extends ChannelBase {
async connect(): Promise<void> {
// Connect to platform API, register message handlers.
// When a message arrives, build an Envelope and call:
// this.handleInbound(envelope)
}
async sendMessage(chatId: string, text: string): Promise<void> {
// Deliver the agent's response to the platform.
}
disconnect(): void {
// Clean up connections on shutdown.
}
}
Export a ChannelPlugin object so the extension loader can discover it:
import type { ChannelPlugin } from '@qwen-code/channel-base';
export const plugin: ChannelPlugin = {
channelType: 'my-platform',
displayName: 'My Platform',
requiredConfigFields: ['apiKey'],
createChannel: (name, config, bridge, options) =>
new MyChannel(name, config, bridge, options),
};
For a complete working example, see @qwen-code/channel-plugin-example.
Architecture
Inbound: Platform message
→ Envelope
→ GroupGate (group policy + mention gating)
→ SenderGate (allowlist / pairing / open)
→ Slash commands (/clear, /help, /status)
→ SessionRouter (resolve or create ACP session)
→ AcpBridge.prompt() → agent
Outbound: Agent response
→ ChannelBase
→ sendMessage() → platform
Everything between handleInbound() and sendMessage() is handled by the base class — your adapter only deals with platform I/O.
Exports
Classes
| Class | Purpose |
|---|---|
ChannelBase |
Abstract base class — extend this to build a channel adapter |
AcpBridge |
Spawns and communicates with the qwen-code --acp agent process |
SessionRouter |
Maps senders to ACP sessions with configurable scoping |
SenderGate |
DM access control (allowlist / pairing / open) |
GroupGate |
Group chat policy and @mention gating |
PairingStore |
Pairing code generation, approval, and allowlist persistence |
Types
| Type | Description |
|---|---|
ChannelConfig |
Channel configuration from settings.json |
ChannelPlugin |
Plugin factory interface (what you export) |
Envelope |
Normalized inbound message format |
SenderPolicy |
'allowlist' | 'pairing' | 'open' |
GroupPolicy |
'disabled' | 'allowlist' | 'open' |
SessionScope |
'user' | 'thread' | 'single' |
GroupConfig |
Per-group settings (e.g. requireMention) |
SessionTarget |
Maps a session back to its channel/sender/chat |
API reference
ChannelBase
constructor(name: string, config: ChannelConfig, bridge: AcpBridge, options?: ChannelBaseOptions)
Abstract methods (you must implement):
| Method | Signature |
|---|---|
connect() |
() => Promise<void> — Connect to the platform and start receiving messages |
sendMessage() |
(chatId: string, text: string) => Promise<void> — Deliver agent response |
disconnect() |
() => void — Clean up on shutdown |
Provided methods:
| Method | Description |
|---|---|
handleInbound(envelope) |
Route an inbound message through the full pipeline (gate checks, commands, session, prompt). Call this from your message handler. |
setBridge(bridge) |
Replace the ACP bridge after crash recovery |
registerCommand(name, handler) |
Register a custom slash command (e.g. /mycommand) |
onToolCall(chatId, event) |
Hook called on agent tool invocations — override to show indicators |
Built-in slash commands: /clear (/reset, /new), /help, /status
AcpBridge
Manages the qwen-code --acp child process and ACP sessions.
constructor(options: { cliEntryPath: string; cwd: string; model?: string })
| Method | Description |
|---|---|
start() |
Spawn the agent process |
stop() |
Kill the agent process |
newSession(cwd) |
Create a new ACP session, returns sessionId |
loadSession(sessionId, cwd) |
Restore an existing session |
prompt(sessionId, text, options?) |
Send a message to the agent, returns the full response text. Supports optional imageBase64 and imageMimeType. |
isConnected |
Whether the agent process is alive |
Events (EventEmitter):
| Event | Payload | Description |
|---|---|---|
textChunk |
(sessionId, chunk) |
Streaming response chunk |
toolCall |
(event: ToolCallEvent) |
Agent invoked a tool |
disconnected |
(code, signal) |
Agent process exited |
SessionRouter
Maps senders to ACP sessions based on the configured scope.
constructor(bridge: AcpBridge, defaultCwd: string, scope?: SessionScope, persistPath?: string)
Routing keys by scope:
| Scope | Key format | Effect |
|---|---|---|
user (default) |
channel:senderId:chatId |
Each user gets their own session per chat |
thread |
channel:threadId |
One session per thread |
single |
channel:__single__ |
One shared session for the entire channel |
| Method | Description |
|---|---|
resolve(channelName, senderId, chatId, threadId?, cwd?) |
Get or create a session for the given sender |
removeSession(channelName, senderId, chatId?) |
Remove session(s) — used by /clear |
restoreSessions() |
Reload sessions from disk after bridge restart |
clearAll() |
Clear all sessions and delete persist file (clean shutdown) |
SenderGate
constructor(policy: SenderPolicy, allowedUsers?: string[], pairingStore?: PairingStore)
| Method | Description |
|---|---|
check(senderId, senderName?) |
Returns { allowed: boolean, pairingCode?: string | null } |
Policy behavior:
| Policy | Behavior |
|---|---|
open |
Everyone allowed |
allowlist |
Only allowedUsers allowed |
pairing |
Check allowlist, then approved pairings, then generate a pairing code (8-char, 1hr expiry, max 3 pending) |
GroupGate
constructor(policy?: GroupPolicy, groups?: Record<string, GroupConfig>)
| Method | Description |
|---|---|
check(envelope) |
Returns { allowed: boolean, reason?: 'disabled' | 'not_allowlisted' | 'mention_required' } |
Policy behavior:
| Policy | Behavior |
|---|---|
disabled |
All group messages rejected |
allowlist |
Only groups listed in config are allowed |
open |
All groups allowed |
When requireMention is true (default), group messages are only processed if the bot is @mentioned or the message is a reply to the bot.
PairingStore
constructor(channelName: string)
Persists pairing state to ~/.qwen/channels/{channelName}-pairing.json and {channelName}-allowlist.json.
| Method | Description |
|---|---|
createRequest(senderId, senderName) |
Generate an 8-char pairing code (or return existing). Returns null if 3 pending requests already exist. |
approve(code) |
Approve a pairing request, adds sender to allowlist. Returns the request or null. |
isApproved(senderId) |
Check if sender is in the approved allowlist |
listPending() |
Get active (non-expired) pending requests |
Envelope
The normalized message format your adapter must construct:
interface Envelope {
channelName: string; // your channel instance name
senderId: string; // stable, unique sender ID
senderName: string; // display name
chatId: string; // distinguishes DMs from groups
text: string; // message text (@mentions stripped)
messageId?: string; // platform message ID
threadId?: string; // for thread-scoped sessions
isGroup: boolean; // true for group chats
isMentioned: boolean; // true if bot was @mentioned
isReplyToBot: boolean; // true if replying to bot's message
referencedText?: string; // quoted message text
imageBase64?: string; // base64-encoded image
imageMimeType?: string; // e.g. 'image/jpeg'
}
Further reading
- Channel Plugin Developer Guide
@qwen-code/channel-plugin-example— working reference implementation