feat(channels): add extensible Channels platform with plugin system and Telegram/WeChat/DingTalk channels#2628
Conversation
📋 Review SummaryThis PR introduces a well-architected Channels feature that enables Qwen Code to interact with users through messaging platforms, starting with Telegram support. The implementation leverages the Agent Client Protocol (ACP) to bridge messaging channels with the Qwen Code CLI. Overall, the code demonstrates solid architectural decisions with clean separation of concerns, though there are several security, reliability, and maintainability issues that should be addressed before merging. 🔍 General Feedback
🎯 Specific Feedback🔴 Critical
🟡 High
🟢 Medium
🔵 Low
✅ Highlights
SummaryThis is a well-designed feature that adds significant value to Qwen Code. However, the critical security issue with auto-approving tool permissions must be addressed before merging. Additionally, I strongly recommend adding test coverage for the core components (AcpBridge, SessionRouter, SenderGate) and implementing proper session cleanup to prevent memory leaks in long-running instances. |
Implements the channels infrastructure for connecting external messaging platforms to Qwen Code via ACP. Phase 1 supports plain text round-trip: Telegram user sends message -> AcpBridge -> qwen-code --acp -> response back to Telegram. New packages: - @qwen-code/channel-base: AcpBridge, SessionRouter, SenderGate, ChannelBase - @qwen-code/channel-telegram: TelegramAdapter using telegraf CLI: `qwen channel start <name>` reads from settings.json channels config, spawns ACP agent, connects to Telegram via polling.
Use telegram-markdown-formatter to convert agent markdown responses to Telegram HTML (bold, italic, code blocks, links). Falls back to plain text if HTML parsing fails. Also uses the package's built-in HTML-aware message splitting for long responses.
- Local commands: /start (welcome), /help (dynamic list), /reset (clear session) - Non-local slash commands forwarded to ACP agent as prompts - AcpBridge captures available_commands_update to populate /help dynamically - SessionRouter gains hasSession/removeSession for /reset support
… support - Validate required fields (type, token) with clear error messages - Prepend channel instructions to first prompt of each session - SessionRouter respects sessionScope (user/thread/single) for routing keys
37a0c97 to
615ccd0
Compare
- Use bracket notation for index signature properties - Add tsconfig.json for channels/base and channels/telegram packages Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nt handling - Add ToolCallEvent interface and emit typed events from AcpBridge - Refactor sessionUpdate handling into dedicated method - Simplify TelegramAdapter to use simple 'Working...' message - Change to non-awaited handler to avoid Telegraf 90s timeout - Remove console.log statements for cleaner code Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add overview page explaining channels architecture and configuration - Add Telegram channel setup guide with bot creation steps - Add navigation entries for channels section This documents the new Channels feature that allows users to interact with Qwen Code agents from messaging platforms like Telegram. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add PairingStore for managing pending requests and approved users - Update SenderGate to support pairing policy with code generation - Add CLI commands: `qwen channel pairing list/approve` - Document pairing flow with rules and usage examples This allows unknown senders to request access via a pairing code that the bot operator approves through the CLI. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add GroupGate class for group access control with three policies: disabled (default), allowlist, and open - Implement mention gating: bot only responds when @mentioned or replied to in groups (configurable per-group) - Extend Envelope type with isGroup, isMentioned, isReplyToBot fields - Update TelegramAdapter to detect group context and mentions - Add comprehensive documentation for group chat setup and troubleshooting This enables using Qwen Code bots in Telegram groups with fine-grained access control and mention-based activation to prevent noise. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add WeixinAdapter with accounts, api, login, monitor, send modules - Add channel configure command for interactive setup - Update TelegramAdapter for consistency Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add model configuration option for channel-specific model selection - Support base64-encoded images in prompts via AcpBridge - Add media utilities for WeChat/Weixin channel - Update settings schema for model configuration Enables channels to process images and use custom models. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> - Add photo message handling in Telegram adapter with base64 encoding - Add document/file message handling with temp directory storage - Extend WeChat adapter to support file downloads from CDN - Refactor envelope building into reusable buildEnvelope method - Rename ImageCdnRef to CdnRef for generic media handling This enables users to send images and files through both Telegram and WeChat channels, with files saved to a temp directory for agent access.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> - Add Media Support section to overview with images and files - Document model option for multimodal channel support - Add Images and Files section to Telegram guide - Add complete WeChat (Weixin) setup guide with QR auth This documents the new media handling capabilities added to both Telegram and WeChat channels.
- Add session persistence to SessionRouter for crash recovery - Add loadSession method to AcpBridge for restoring sessions - Add ChannelBaseOptions to support external router injection - Refactor start.ts to support both standalone and gateway modes - Extract config utilities into separate module This enables channels to recover sessions after bridge crashes and supports running multiple channels under a gateway process. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add `qwen channel status` to check running service info - Add `qwen channel stop` to gracefully stop the service - Add PID file tracking to prevent duplicate service instances - Update documentation with new commands and usage This enables users to manage the channel service from another terminal without needing to use Ctrl+C on the foreground process. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Add /help, /clear (aliases: /reset, /new), /status commands to ChannelBase - Commands are handled locally without agent round-trip - TelegramAdapter skips "Working..." indicator for local commands - Update docs to reflect new command structure This provides a consistent command interface across all channel types (Telegram, WeChat, etc.) with platform-specific extensibility. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
| TOPIC_ROBOT, | ||
| (msg: DWClientDownStream) => { | ||
| // ACK immediately so DingTalk doesn't retry | ||
| this.client.send(msg.headers.messageId, { |
There was a problem hiding this comment.
ACKing immediately before local processing completes creates a real loss window: if the process crashes after the ACK but before onMessage()/handleInbound() succeeds, DingTalk will not retry and the message is gone. This callback needs either a minimal durable checkpoint before ACK or a recoverable inbox/queue model.
— qwen-code + gpt-5.4
There was a problem hiding this comment.
Accepted risk, deferred — ACK-first is an intentional tradeoff. Agent prompts can take minutes; deferring ACK would cause DingTalk to retry during processing, creating duplicate messages. The crash-after-ACK message loss scenario is real but rare, and fixing it properly requires a durable message queue or checkpoint, which is out of scope for MVP.
补充审计意见 (Supplemental Review)在 1. 并发瓶颈风险 (Concurrency Bottleneck)在
2. Bridge 启动可靠性 (Fragile Startup)
3. 流式渲染鲁棒性 (Markdown Fragment Risk)
4. 内存管理策略 (Memory Management)
5. 进程生命周期处理 (Incomplete SIGTERM Handling)在 6. 安全实践肯定 (Positive Implementation)肯定一下: Audit by: gemini-3.1-pro-preview |
wenshao
left a comment
There was a problem hiding this comment.
针对该 PR 的部分关键技术细节进行的补充评审。建议采纳相关优化以提升生产环境下的鲁棒性和并发能力。
Audit by: gemini-3.1-pro-preview
| const SAFE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; | ||
| const CODE_LENGTH = 8; | ||
| const EXPIRY_MS = 60 * 60 * 1000; // 1 hour | ||
| const MAX_PENDING = 3; |
There was a problem hiding this comment.
MAX_PENDING = 3 的硬编码数值在群聊或高流量场景下过小,容易导致合法用户被误拒。建议根据实际负载需求调优此阈值。 -- gemini-3.1-pro-preview
There was a problem hiding this comment.
Won't fix — 3 concurrent pairings is a reasonable limit for MVP. It's a product policy, not a bug.
| }); | ||
|
|
||
| // Give the process a moment to start | ||
| await new Promise((resolve) => setTimeout(resolve, 1000)); |
There was a problem hiding this comment.
使用固定的 setTimeout 等待子进程启动较为脆弱。建议改为监听子进程 stdout 的就绪信号或实现标准握手机制。 -- gemini-3.1-pro-preview
There was a problem hiding this comment.
Won't fix — 1s is fine for local process startup in MVP. Will revisit if users report race conditions.
| } | ||
|
|
||
| export class SessionRouter { | ||
| private toSession: Map<string, string> = new Map(); // routing key → session ID |
There was a problem hiding this comment.
目前的 Map 结构随时间无限增长。对于长期运行的服务,建议引入 LRU 淘汰策略以防止潜在的内存溢出。 -- gemini-3.1-pro-preview
There was a problem hiding this comment.
Deferred — each entry is small; not a concern at MVP scale. Will revisit if long-running bots show memory growth.
| * Find the best break point at or before maxPos. | ||
| * Prefers paragraph break > newline > space > maxPos. | ||
| */ | ||
| private findBreakPoint(text: string, maxPos: number): number { |
There was a problem hiding this comment.
此处的分块逻辑在处理 Markdown 复杂结构(如嵌套代码块或列表)时可能导致渲染断点错误。建议增强分块时对 Markdown 语法的感知。 -- gemini-3.1-pro-preview
There was a problem hiding this comment.
Deferred — cosmetic issue, worst case is a broken bold/code block across two messages.
| } | ||
| }); | ||
|
|
||
| const shutdown = () => { |
There was a problem hiding this comment.
SIGTERM 回调中的 shutdown 逻辑应确保异步清理过程(如连接断开、临时文件清理)被完整执行并被 await,否则进程可能在资源释放前即被强制终止。 -- gemini-3.1-pro-preview
There was a problem hiding this comment.
Won't fix — all cleanup operations (disconnect(), bridge.stop(), clearAll(), removeServiceInfo()) are synchronous in all current implementations. No async work to await.
|
|
||
| export function resolveEnvVars(value: string): string { | ||
| if (value.startsWith('$')) { | ||
| const envName = value.substring(1); |
There was a problem hiding this comment.
环境变量语法不支持 ${VAR}(带花括号)
resolveEnvVars 检测 $ 前缀后直接取 substring(1) 作为变量名。对于 ${TELEGRAM_TOKEN} 这种写法,实际查找的变量名是 {TELEGRAM_TOKEN}(含花括号),会报出令人困惑的 "variable not set" 错误。
${VAR} 是 Docker Compose、shell 等配置系统中的标准写法,用户很自然会使用这种格式。
建议:增加花括号剥离逻辑:
let envName = value.substring(1);
if (envName.startsWith('{') && envName.endsWith('}')) {
envName = envName.slice(1, -1);
}— qwen-code + glm-5.1
There was a problem hiding this comment.
Deferred — only $VAR is documented and used. A fuller implementation exists elsewhere in the codebase if needed later.
| private seenMessages: Map<string, number> = new Map(); | ||
| private dedupTimer?: ReturnType<typeof setInterval>; | ||
| /** Map conversationId → latest sessionWebhook URL for sending replies. */ | ||
| private webhooks: Map<string, string> = new Map(); |
There was a problem hiding this comment.
DingTalk sessionWebhook 无限期缓存,但实际有 TTL
DingTalk 的 sessionWebhook 通常有 10-30 分钟的有效期,但这里缓存在 Map 中永不清理。对于执行时间较长的 agent 任务,响应发送时 webhook 可能已过期,导致静默发送失败——用户收不到任何回复,也无错误通知。
建议:
- 存储 webhook 时附带时间戳,发送前检查新鲜度
- 发送失败时检查是否为过期错误,给用户发送提醒
- 或添加定期清理过期 webhook 的定时器
— qwen-code + glm-5.1
There was a problem hiding this comment.
Deferred — only affects conversations idle >10 min. Active conversations refresh the webhook on each message.
| const result = await router.restoreSessions(); | ||
| writeStdoutLine( | ||
| `[Channel] Bridge restarted. Sessions restored: ${result.restored}, failed: ${result.failed}`, | ||
| ); |
There was a problem hiding this comment.
crashCount 在成功重启后重置为 0,可能导致无限崩溃循环
第 213 行在 bridge 成功重启后将 crashCount 重置为 0。这意味着如果 bridge 持续不稳定(每次启动后短暂存活再崩溃),它会无限循环:崩溃 3 次 → 重启 → 重置 → 崩溃 3 次 → 重启……,掩盖了底层问题并可能刷满日志。
建议:使用时间窗口计数(例如 10 分钟内 3 次崩溃则放弃),或使用单调递增计数器不重置,或添加生命周期总崩溃数硬上限。
— qwen-code + glm-5.1
There was a problem hiding this comment.
Fixed — replaced resettable counter with time-window counting (3 crashes within 5 minutes = give up).
|
|
||
| /** Detect image MIME type from magic bytes. */ | ||
| function detectImageMime(data: Buffer): string { | ||
| if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e) { |
There was a problem hiding this comment.
detectImageMime 访问 Buffer 未做边界检查
函数直接访问 data[0]、data[1]、data[2]、data[3] 而未检查 buffer 是否至少有 4 字节。空或截断的图片下载会导致 undefined 比较,静默降级为 'image/jpeg'。虽然不会崩溃,但错误的 MIME 类型可能影响 agent 处理。
建议:添加边界检查:
function detectImageMime(data: Buffer): string {
if (data.length < 4) return 'image/jpeg';
if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e) {
return 'image/png';
}
// ...
}— qwen-code + glm-5.1
There was a problem hiding this comment.
Deferred — accessing undefined bytes just fails the comparisons and defaults to jpeg. No crash or incorrect behavior.
| cleanText = text | ||
| .replace(new RegExp(`@${this.botUsername}`, 'gi'), '') | ||
| .trim(); | ||
| } |
There was a problem hiding this comment.
botUsername 未转义直接构造正则表达式
当 botUsername 为空字符串时(例如 getMe() 返回空值),new RegExp(@${this.botUsername}, 'gi') 会生成 /@/gi,将删除消息中 所有 @ 字符,破坏消息内容。
虽然 Telegram 用户名约束为 [a-zA-Z0-9_] 不含正则元字符,但空值防护仍然必要。
建议:使用 String.replaceAll 或添加空值守卫:
if (isMentioned && this.botUsername) {
cleanText = text.replaceAll(`@${this.botUsername}`, '').trim();
}— qwen-code + glm-5.1
There was a problem hiding this comment.
Won't fix — already guarded by if (isMentioned && this.botUsername) on line 229. Empty botUsername never reaches the regex.
| import { TypingStatus } from './types.js'; | ||
|
|
||
| /** In-memory typing ticket cache: userId -> typingTicket */ | ||
| const typingTickets = new Map<string, string>(); |
There was a problem hiding this comment.
模块级单例 Map 在多实例场景下交叉污染
typingTickets 和 contextTokens(在 monitor.ts 中)是模块作用域的 Map。如果 startAll() 模式下创建了多个 WeChat channel(不同账号),它们会共享同一个 Map,导致不同实例之间的状态交叉污染。
此外这些 Map 在 disconnect() 时不会被清理,长期运行会导致内存持续增长。
建议:将 typingTickets 和 contextTokens 移为 WeixinChannel 的实例属性,在 disconnect() 中清理。
— qwen-code + glm-5.1
There was a problem hiding this comment.
Deferred — only matters for multi-account WeChat in startAll(). Single-instance MVP is fine.
- fix: sanitize remote filenames with basename() and isolate uploads in UUID subdirs to prevent path traversal and collision (#2-4, #27) - fix: use crypto.randomInt() for pairing codes instead of Math.random() (#5) - fix: pass config.sessionScope instead of hardcoded 'user' (#6); add per-channel scope overrides via setChannelScope() for startAll (#7) - fix: removeSession now returns removed session IDs and persists when chatId is provided (#8) - fix: /clear only removes the cleared session from instructedSessions, not all sessions (#9) - fix: DingTalk @mention stripping now removes only the first mention instead of all mentions (#10) - fix: remove dead TELEGRAF_COMMANDS Set and its guard (#13) - fix: WeChat cursor saved after message processing, not before (#14) - fix: crash recovery uses time-window counting instead of resettable counter to prevent infinite restart loops (#17) - fix: call channel.disconnect() before exit on crash exhaustion (#18)
…script - Add plugin-example to build order in scripts/build.js - Add prepublishOnly script to auto-build before npm publish This ensures the plugin-example package is built during the main build process and automatically compiled before publishing to npm. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Replace Telegraf with Grammy as the Telegram Bot framework. - Replace @telegraf/types with @grammyjs/types in package-lock.json - Swap telegraf dependency for grammy ^1.41.1 in package.json - Update TelegramAdapter.ts: Bot instead of Telegraf, .api.* instead of .telegram.* calls, .start() instead of .launch(), adjusted event subscription syntax (message:text, message:photo, message:document) Grammy is a more modern and actively maintained Telegram bot framework for Node.js, improving reliability and reduce legacy dependencies. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
TLDR
This PR introduces a new Channels platform that enables Qwen Code to interact with users through messaging platforms. It includes a plugin system for building custom channel adapters, plus built-in support for Telegram, WeChat, and DingTalk.
Key highlights:
@qwen-code/channel-baseScreenshots / Video Demo
Features
Core Platform
ChannelBaseSDK withconnect/sendMessage/disconnect, extension manifest, compiled JS +.d.tsAccess Control
allowlist,pairing(8-char codes, CLI approval),openpoliciesopen/disabled/allowlistgroup policy,requireMentionper group, reply-as-mentionSession Management
user,thread,singlescopes with per-channelcwd,model,instructionssteer(default: cancel + re-prompt),collect(buffer + coalesce),followup(sequential queue). Per-channel and per-group config.Messaging Features
onPromptStart/onPromptEndhooks. Telegram: typing bar. WeChat: typing API. DingTalk: 👀 emoji reaction.onResponseChunk/onResponseCompletefor plugins to implement progressive displayAttachmentinterface onEnvelopeCommands & Service
/help,/clear(/reset,/new),/status, custom viaregisterCommand()qwen channel start/stop/status, PID tracking, crash recovery (auto-restart, session persistence)$ENV_VARsyntax in configSupported Platforms
Telegram
Full-featured Telegram bot support:
/clear,/help,/status)WeChat
DM support via iLink Bot API:
DingTalk
Enterprise messaging via Stream Mode WebSocket:
Custom Channels (Plugin System)
Build your own channel adapter for any messaging platform. The
@qwen-code/channel-basepackage provides the core infrastructure, and@qwen-code/channel-plugin-exampleoffers a working reference implementation to get started quickly.See the Channel Plugin Developer Guide for details.
Architecture
Core Components:
qwen-code --acpsubprocess, manages ACP sessionsuser/thread/singlescope)allowlist/pairing/open)Configuration
Channels are configured in
~/.qwen/settings.json:{ "channels": { "my-telegram": { "type": "telegram", "token": "$TELEGRAM_BOT_TOKEN", "senderPolicy": "allowlist", "allowedUsers": ["123456789"], "sessionScope": "user", "cwd": "/path/to/project", "instructions": "You are a helpful coding assistant.", "groupPolicy": "disabled" } } }See the Channels documentation for full configuration options.
CLI Commands
Future Work
Safety & Group Chat
tools/toolsBySenderdeny/allow lists per groupmentionPatternsfor unreliable @mention metadatainstructionsfield onGroupConfigfor per-group personas/activationcommand — runtime toggle forrequireMention, persisted to diskOperational Tooling
qwen channel doctor— config validation, env vars, bot tokens, network checksqwen channel status --probe— real connectivity checks per channelPlatform Expansion
Multi-Agent
Plugin Ecosystem
create-qwen-channelscaffolding toolqwen extensions search, version compatibilityReviewer Test Plan
Telegram
~/.qwen/settings.jsonnpm run build && qwen channel start my-telegramWeChat
qwen channel configure-weixin, scan QR code~/.qwen/settings.jsonqwen channel start my-weixinDingTalk
~/.qwen/settings.jsonnpm run build && qwen channel start my-dingtalkDocumentation
Packages
@qwen-code/channel-base@qwen-code/channel-plugin-example@qwen-code/channel-telegram@qwen-code/channel-weixin@qwen-code/channel-dingtalk