Skip to Content
开发者指南Channel 插件开发者指南

Channel 插件开发者指南

Channel 插件用于将 Qwen Code 连接到消息平台。它被打包为一个 extension 并在启动时加载。有关安装和配置插件的用户文档,请参阅 Plugins

工作原理

你的插件位于 Platform Adapter 层。你需要处理特定于平台的事务(连接、接收消息、发送响应)。ChannelBase 负责处理其他所有事务(访问控制、会话路由、提示词排队、斜杠命令、崩溃恢复)。

Your Plugin → builds Envelope → handleInbound() ChannelBase → gates → commands → routing → ChannelAgentBridge.prompt() ChannelBase → calls your sendMessage() with the agent's response

ChannelAgentBridge 是面向 adapter 的桥接契约。当前的独立 qwen channel start 路径提供了一个 AcpBridge,但插件代码应将构造函数参数类型化为 ChannelAgentBridge,以便同一个 adapter 以后可以在其他桥接实现后运行。

现有 TypeScript 插件的迁移说明:如果你的 adapter 构造函数或工厂显式将 bridge 类型化为 AcpBridge,请将该注解更改为 ChannelAgentBridge,并仅继续使用该契约暴露的方法。JavaScript 插件在运行时不受影响,且独立的 qwen channel start 仍会传递当前的 AcpBridge 实现。

运行时模式

同一个插件 adapter 可以由以下任一 Channel 运行时托管:

  • qwen channel start [name] 是独立的 ACP 支持的服务。它仍然使用 AcpBridge,并且是在守护进程外运行 Channel 的稳定命令。
  • qwen serve --channel <name> 和可重复的 --channel 标志会启动一个实验性的由守护进程管理的 Channel worker。--channel all 会启动所有已配置的 Channel。该 worker 由 qwen serve 拥有,通过 SDK 连接到该守护进程,并向 adapter 传递一个由 DaemonChannelBridge 支持的 ChannelAgentBridge 门面。

由守护进程管理的 Channel 继承守护进程的生命周期和状态报告。它们被刻意设计为进程外运行,这样 adapter 或平台 SDK 的故障就不会导致守护进程崩溃。守护进程仍然绑定到一个工作区,因此每个选定的 Channel 配置都必须使用一个能解析到守护进程工作区的 cwd

插件对象

你的 extension 入口点导出一个符合 ChannelPluginplugin

import type { ChannelPlugin } from '@qwen-code/channel-base'; import { MyChannel } from './MyChannel.js'; export const plugin: ChannelPlugin = { channelType: 'my-platform', // Unique ID, used in settings.json "type" field displayName: 'My Platform', // Shown in CLI output requiredConfigFields: ['apiKey'], // Validated at startup (beyond standard ChannelConfig) createChannel: (name, config, bridge, options) => new MyChannel(name, config, bridge, options), };

Channel Adapter

继承 ChannelBase 并实现以下三个方法:

import { ChannelBase } from '@qwen-code/channel-base'; import type { ChannelBaseOptions, ChannelAgentBridge, ChannelConfig, Envelope, } from '@qwen-code/channel-base'; export class MyChannel extends ChannelBase { constructor( name: string, config: ChannelConfig, bridge: ChannelAgentBridge, options?: ChannelBaseOptions, ) { super(name, config, bridge, options); } async connect(): Promise<void> { // 连接到你的平台,注册消息处理器 // 当收到消息时: const envelope: Envelope = { channelName: this.name, senderId: '...', // 稳定、唯一的平台用户 ID senderName: '...', // 显示名称 chatId: '...', // 聊天/会话 ID(区分私聊和群聊) text: '...', // 消息文本(去除 @提及) isGroup: false, // 必须准确 — 由 GroupGate 使用 isMentioned: false, // 必须准确 — 由 GroupGate 使用 isReplyToBot: false, // 必须准确 — 由 GroupGate 使用 }; this.handleInbound(envelope); } async sendMessage(chatId: string, text: string): Promise<void> { // 将 markdown 格式化为平台格式,必要时分块,然后发送 } disconnect(): void { // 清理连接 } }

大多数 adapter 应原样传递 options。如果 adapter 创建了自己的 SessionRouter 并将该 router 传递给 super(),请在 ChannelBaseOptions 中设置 registerBridgeEvents: true,以便 ChannelBase 仍然能直接接收 toolCallsessionDied 事件。对于由 Channel gateway 提供的 router,请保持未设置状态。

如果你的 adapter 暴露了 shell 命令行为,请在启用前检查 bridge.shellCommand 是否存在。除非守护进程通告了 session_shell_command 能力,否则由守护进程管理的 worker 会省略该可选方法。

Envelope

这是你从平台数据构建的标准化消息对象。布尔标志驱动 gate 逻辑,因此它们必须准确。

FieldTypeRequiredNotes
channelNamestringYes使用 this.name
senderIdstringYes在多条消息中必须保持稳定(用于会话路由 + 访问控制)
senderNamestringYes显示名称
chatIdstringYes必须区分私聊和群聊
textstringYes去除 bot 的 @提及
threadIdstringNo用于 sessionScope: "thread"
messageIdstringNo平台消息 ID — 有助于响应关联
isGroupbooleanYesGroupGate 依赖此项
isMentionedbooleanYesGroupGate 依赖此项
isReplyToBotbooleanYesGroupGate 依赖此项
referencedTextstringNo引用的消息 — 作为上下文前置
imageBase64stringNoBase64 编码的图片(遗留 — 推荐使用 attachments
imageMimeTypestringNo例如 image/jpeg(遗留 — 推荐使用 attachments
attachmentsAttachment[]No结构化媒体附件(见下文)

附件

使用 attachments 数组处理图片、文件、音频和视频。handleInbound() 会自动解析它们:带有 base64 data 的图片会作为视觉输入发送给模型,带有 filePath 的文件会将其路径追加到提示词中,以便 agent 读取它们。

interface Attachment { type: 'image' | 'file' | 'audio' | 'video'; data?: string; // base64 编码的数据(图片、小文件) filePath?: string; // 本地文件的绝对路径(大文件保存到磁盘) mimeType: string; // 例如 'application/pdf', 'image/jpeg' fileName?: string; // 来自平台的原始文件名 }

示例 — 在你的 adapter 中处理文件上传:

import { writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; const buf = await downloadFromPlatform(fileId); const dir = join(tmpdir(), 'channel-files'); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const filePath = join(dir, fileName); writeFileSync(filePath, buf); envelope.attachments = [ { type: 'file', filePath, mimeType: 'application/pdf', fileName, }, ];

遗留的 imageBase64/imageMimeType 字段为了向后兼容仍然有效,但新代码推荐使用 attachments

扩展清单

你的 qwen-extension.json 声明了 Channel 类型。该 key 必须与你的 plugin 对象中的 channelType 匹配:

{ "name": "my-channel-extension", "version": "1.0.0", "channels": { "my-platform": { "entry": "dist/index.js", "displayName": "My Platform Channel" } } }

可选的扩展点

自定义斜杠命令 — 在你的构造函数中注册:

this.registerCommand('mycommand', async (envelope, args) => { await this.sendMessage(envelope.chatId, 'Response'); return true; // 已处理,不要转发给 agent });

工作状态指示器 — 重写 onPromptStart()onPromptEnd() 以显示特定于平台的输入指示器。这些钩子仅在提示词实际开始处理时触发 — 不会用于缓冲消息(collect 模式)或被 gate/阻止的消息:

protected override onPromptStart(chatId: string, sessionId: string, messageId?: string): void { this.platformClient.sendTyping(chatId); // 你的平台 API } protected override onPromptEnd(chatId: string, sessionId: string, messageId?: string): void { this.platformClient.stopTyping(chatId); }

工具调用钩子 — 重写 onToolCall() 以显示 agent 活动(例如,“正在运行 shell 命令…”)。

流式传输钩子 — 重写 onResponseChunk(chatId, chunk, sessionId) 以实现逐块渐进式显示(例如,就地编辑消息)。重写 onResponseComplete(chatId, fullText, sessionId) 以自定义最终交付。

块级流式传输 — 在 Channel 配置中设置 blockStreaming: "on"。基类会自动在段落边界处将响应拆分为多条消息。无需插件代码 — 它与 onResponseChunk 协同工作。

媒体 — 使用图片/文件填充 envelope.attachments。请参阅上文的 附件

参考实现

  • Plugin example (packages/channels/plugin-example/) — 最小的基于 WebSocket 的 adapter,良好的起点
  • Telegram (packages/channels/telegram/) — 功能齐全:图片、文件、格式化、输入指示器
  • DingTalk (packages/channels/dingtalk/) — 基于流并支持富文本处理
Last updated on