Channels Design
External messaging integrations for Qwen Code — interact with an agent from Telegram, WeChat, and more.
User documentation: Channels Overview.
Overview
A channel connects an external messaging platform to a Qwen Code agent. Configured in settings.json, managed via qwen channel subcommands, multi-user (each user gets an isolated ACP session).
Architecture
┌──────────┐ ┌─────────────────────────────────────┐
│ Telegram │ Platform API │ Channel Service │
│ User A │◄──────────────────────►│ │
├──────────┤ (WebSocket/polling) │ ┌───────────┐ ┌──────────────┐ │
│ WeChat │◄──────────────────────►│ │ Platform │ │ ACP Bridge │ │
│ User B │ │ │ Adapter │ │ (shared) │ │
└──────────┘ │ │ │ │ │ │
│ │ - connect │ │ - spawns │ │
│ │ - receive │ │ qwen-code │ │
│ │ - send │ │ - manages │ │
│ │ │ │ sessions │ │
│ └─────┬──────┘ └──────┬───────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ SenderGate · GroupGate │ │
│ │ SessionRouter · ChannelBase │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
│
│ stdio (ACP ndjson)
▼
┌─────────────────────────────────────┐
│ qwen-code --acp │
│ Session A (user alice, id: "abc") │
│ Session B (user bob, id: "def") │
└─────────────────────────────────────┘Platform Adapter — connects to external API, translates messages to/from Envelopes. ACP Bridge — spawns qwen-code --acp, manages sessions, emits textChunk/toolCall/disconnected events. Session Router — maps senders to ACP sessions via namespaced keys (<channel>:<sender>). Sender Gate / Group Gate — access control (allowlist / pairing / open) and mention gating. Channel Base — abstract base with Template Method pattern: plugins override connect, sendMessage, disconnect. Channel Registry — Map<string, ChannelPlugin> with collision detection.
Envelope
Normalized message format all platforms convert to:
- Identity:
senderId,senderName,chatId,channelName - Content:
text, optionalimageBase64/imageMimeType, optionalreferencedText - Context:
isGroup,isMentioned,isReplyToBot, optionalthreadId
Plugin responsibilities: senderId must be stable/unique; chatId must distinguish DMs from groups; boolean flags must be accurate for gate logic; @mentions stripped from text.
Message Flow
Inbound: User message → Adapter → GroupGate → SenderGate → Slash commands → SessionRouter → AcpBridge → Agent
Outbound: Agent response → AcpBridge → SessionRouter → Adapter → UserSlash commands (/clear, /help, /status) are handled in ChannelBase before reaching the agent.
Sessions
One qwen-code --acp process with multiple ACP sessions. Scope per channel: user (default), thread, or single. Routing keys namespaced as <channelName>:<key>.
Error Handling
- Connection failures — logged; service continues if at least one channel connects
- Bridge crashes — exponential backoff (max 3 retries),
setBridge()on all channels, session restore - Session serialization — per-session promise chains prevent concurrent prompt collisions
Plugin System
The architecture is extensible — new adapters (including third-party) can be added without modifying core. Built-in channels use the same plugin interface (dogfooding).
Plugin Contract
A ChannelPlugin declares channelType, displayName, requiredConfigFields, and a createChannel() factory. Plugins implement three methods:
| Method | Responsibility |
|---|---|
connect() | Connect to platform and register message handlers |
sendMessage(chatId, text) | Format and deliver agent response |
disconnect() | Clean up on shutdown |
On inbound messages, plugins build an Envelope and call this.handleInbound(envelope) — the base class handles the rest: access control, group gating, pairing, session routing, prompt serialization, slash commands, instructions injection, reply context, and crash recovery.
Extension Points
- Custom slash commands via
registerCommand() - Working indicators by wrapping
handleInbound()with typing/reaction display - Tool call hooks via
onToolCall() - Media handling by attaching to Envelope before
handleInbound()
Discovery & Loading
External plugins are extensions managed by ExtensionManager, declared in qwen-extension.json:
{
"name": "my-channel-extension",
"version": "1.0.0",
"channels": {
"my-platform": {
"entry": "dist/index.js",
"displayName": "My Platform Channel"
}
}
}Loading sequence at qwen channel start: load settings → register built-ins → scan extensions → dynamic import + validate → register (reject collisions) → validate config → createChannel() → connect().
Plugins run in-process (no sandbox), same trust model as npm dependencies.
Configuration
{
"channels": {
"my-telegram": {
"type": "telegram",
"token": "$TELEGRAM_BOT_TOKEN", // env var reference
"senderPolicy": "allowlist", // allowlist | pairing | open
"allowedUsers": ["123456"],
"sessionScope": "user", // user | thread | single
"cwd": "/path/to/project",
"model": "qwen3.5-plus",
"instructions": "Keep responses short.",
"groupPolicy": "disabled", // disabled | allowlist | open
"groups": { "*": { "requireMention": true } },
},
},
}Auth is plugin-specific: static token (Telegram), app credentials (DingTalk), QR code login (WeChat), proxy token (TMCP).
CLI Commands
# Channels
qwen channel start [name] # start all or one channel
qwen channel stop # stop running service
qwen channel status # show channels, sessions, uptime
qwen channel pairing list <ch> # pending pairing requests
qwen channel pairing approve <ch> <code> # approve a request
# Extensions
qwen extensions install <path-or-package> # install
qwen extensions link <local-path> # symlink for dev
qwen extensions list # show installed
qwen extensions remove <name> # uninstallPackage Structure
packages/channels/
├── base/ # @qwen-code/channel-base
│ └── src/
│ ├── AcpBridge.ts # ACP process lifecycle, session management
│ ├── SessionRouter.ts # sender ↔ session mapping, persistence
│ ├── SenderGate.ts # allowlist / pairing / open
│ ├── GroupGate.ts # group chat policy + mention gating
│ ├── PairingStore.ts # pairing code generation + approval
│ ├── ChannelBase.ts # abstract base: routing, slash commands
│ └── types.ts # Envelope, ChannelConfig, etc.
├── telegram/ # @qwen-code/channel-telegram
├── weixin/ # @qwen-code/channel-weixin
└── dingtalk/ # @qwen-code/channel-dingtalkFuture Work
Safety & Group Chat
- Per-group tool restrictions —
tools/toolsBySenderdeny/allow lists per group - Group context history — ring buffer of recent skipped messages, prepended on @mention
- Regex mention patterns — fallback
mentionPatternsfor unreliable @mention metadata - Per-group instructions —
instructionsfield onGroupConfigfor per-group personas /activationcommand — runtime toggle forrequireMention, persisted to disk
Operational Tooling
qwen channel doctor— config validation, env vars, bot tokens, network checksqwen channel status --probe— real connectivity checks per channel
Platform Expansion
- Discord — Bot API + Gateway, servers/channels/DMs/threads
- Slack — Bolt SDK, Socket Mode, workspaces/channels/DMs/threads
Multi-Agent
- Multi-agent routing — multiple agents with bindings per channel/group/user
- Broadcast groups — multiple agents respond to the same message
Plugin Ecosystem
- Community plugin template —
create-qwen-channelscaffolding tool - Plugin registry/discovery —
qwen extensions search, version compatibility