Skip to Content
Руководство для разработчиковРуководство разработчика плагинов каналов

Руководство разработчика плагинов каналов

Плагин канала подключает Qwen Code к платформе обмена сообщениями. Он упаковывается как расширение и загружается при запуске. Документацию для пользователей по установке и настройке плагинов см. в разделе Плагины.

Как это устроено

Ваш плагин находится на уровне Platform Adapter. Вы отвечаете за специфичные для платформы задачи (подключение, получение сообщений, отправка ответов). ChannelBase берет на себя всё остальное (управление доступом, маршрутизацию сессий, очередь промптов, слэш-команды, восстановление после сбоев).

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

Объект плагина

Точка входа вашего расширения экспортирует объект plugin, соответствующий интерфейсу ChannelPlugin:

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), };

Адаптер канала

Расширьте ChannelBase и реализуйте три метода:

import { ChannelBase } from '@qwen-code/channel-base'; import type { Envelope } from '@qwen-code/channel-base'; export class MyChannel extends ChannelBase { async connect(): Promise<void> { // Connect to your platform, register message handlers // When a message arrives: const envelope: Envelope = { channelName: this.name, senderId: '...', // Stable, unique platform user ID senderName: '...', // Display name chatId: '...', // Chat/conversation ID (distinct for DMs vs groups) text: '...', // Message text (strip @mentions) isGroup: false, // Accurate — used by GroupGate isMentioned: false, // Accurate — used by GroupGate isReplyToBot: false, // Accurate — used by GroupGate }; this.handleInbound(envelope); } async sendMessage(chatId: string, text: string): Promise<void> { // Format markdown → platform format, chunk if needed, deliver } disconnect(): void { // Clean up connections } }

Объект Envelope

Нормализованный объект сообщения, который вы формируете на основе данных платформы. Логические флаги управляют логикой шлюзов, поэтому они должны быть точными.

ПолеТипОбязательноПримечания
channelNamestringДаИспользуйте this.name
senderIdstringДаДолжен оставаться неизменным между сообщениями (используется для маршрутизации сессий и управления доступом)
senderNamestringДаОтображаемое имя
chatIdstringДаДолжен различать личные сообщения и группы
textstringДаУдалите @упоминания бота
threadIdstringНетДля sessionScope: "thread"
messageIdstringНетID сообщения платформы — полезно для сопоставления ответов
isGroupbooleanДаОт этого зависит работа GroupGate
isMentionedbooleanДаОт этого зависит работа GroupGate
isReplyToBotbooleanДаОт этого зависит работа GroupGate
referencedTextstringНетЦитируемое сообщение — добавляется в начало как контекст
imageBase64stringНетИзображение в формате Base64 (устаревшее — предпочтительно attachments)
imageMimeTypestringНетНапример, image/jpeg (устаревшее — предпочтительно attachments)
attachmentsAttachment[]НетСтруктурированные медиа-вложения (см. ниже)

Attachments

Используйте массив attachments для изображений, файлов, аудио и видео. handleInbound() обрабатывает их автоматически: изображения с data в формате base64 отправляются модели как визуальный ввод, а для файлов с filePath их путь добавляется к промпту, чтобы агент мог их прочитать.

interface Attachment { type: 'image' | 'file' | 'audio' | 'video'; data?: string; // base64-encoded data (images, small files) filePath?: string; // absolute path to local file (large files saved to disk) mimeType: string; // e.g. 'application/pdf', 'image/jpeg' fileName?: string; // original file name from the platform }

Пример — обработка загрузки файла в вашем адаптере:

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 объявляется тип канала. Ключ должен совпадать со значением 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; // handled, don't forward to agent });

Индикаторы работы — переопределите onPromptStart() и onPromptEnd(), чтобы отображать специфичные для платформы индикаторы набора текста. Эти хуки срабатывают только когда промпт действительно начинает обрабатываться — не для буферизованных сообщений (режим collect) или сообщений, отфильтрованных/заблокированных шлюзами:

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

Хуки вызова инструментов — переопределите onToolCall(), чтобы отображать активность агента (например, “Running shell command…”).

Хуки потоковой передачи — переопределите onResponseChunk(chatId, chunk, sessionId) для пошагового отображения по частям (например, редактирование сообщения на месте). Переопределите onResponseComplete(chatId, fullText, sessionId), чтобы настроить финальную доставку.

Потоковая передача блоками — установите blockStreaming: "on" в конфигурации канала. Базовый класс автоматически разбивает ответы на несколько сообщений по границам абзацев. Дополнительный код в плагине не требуется — это работает совместно с onResponseChunk.

Медиа — заполняйте envelope.attachments изображениями/файлами. См. раздел Attachments выше.

Референсные реализации

  • Пример плагина (packages/channels/plugin-example/) — минимальный адаптер на базе WebSocket, хорошая отправная точка
  • Telegram (packages/channels/telegram/) — полнофункциональный: поддержка изображений, файлов, форматирования, индикаторов набора текста
  • DingTalk (packages/channels/dingtalk/) — потоковая реализация с обработкой форматированного текста
Last updated on