Skip to Content
EntwicklerhandbuchEntwicklerhandbuch für Channel-Plugins

Entwicklerhandbuch für Channel-Plugins

Ein Channel-Plugin verbindet Qwen Code mit einer Messaging-Plattform. Es wird als Extension verpackt und beim Start geladen. Für die benutzerorientierte Dokumentation zur Installation und Konfiguration von Plugins siehe Plugins.

Zusammenspiel der Komponenten

Dein Plugin befindet sich in der Platform-Adapter-Schicht. Du kümmerst dich um plattformspezifische Aufgaben (Verbindungsaufbau, Empfangen von Nachrichten, Senden von Antworten). ChannelBase übernimmt alles Weitere (Zugriffskontrolle, Session-Routing, Prompt-Warteschlange, Slash-Commands, Crash-Recovery).

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

Das Plugin-Objekt

Der Einstiegspunkt deiner Extension exportiert ein plugin, das ChannelPlugin implementiert:

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

Der Channel-Adapter

Erweitere ChannelBase und implementiere drei Methoden:

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 } }

Das Envelope-Objekt

Das normalisierte Nachrichtenobjekt, das du aus den Plattformdaten erstellst. Die Boolean-Flags steuern die Gate-Logik und müssen daher korrekt sein.

FeldTypErforderlichHinweise
channelNamestringJaVerwende this.name
senderIdstringJaMuss über Nachrichten hinweg stabil sein (wird für Session-Routing + Zugriffskontrolle verwendet)
senderNamestringJaAnzeigename
chatIdstringJaMuss zwischen DMs und Gruppen unterscheiden
textstringJaEntferne Bot-@mentions
threadIdstringNeinFür sessionScope: "thread"
messageIdstringNeinPlattform-Nachrichten-ID – nützlich für die Antwortkorrelation
isGroupbooleanJaGroupGate verlässt sich darauf
isMentionedbooleanJaGroupGate verlässt sich darauf
isReplyToBotbooleanJaGroupGate verlässt sich darauf
referencedTextstringNeinZitierte Nachricht – wird als Kontext vorangestellt
imageBase64stringNeinBase64-kodiertes Bild (Legacy – bevorzuge attachments)
imageMimeTypestringNeinz. B. image/jpeg (Legacy – bevorzuge attachments)
attachmentsAttachment[]NeinStrukturierte Medienanhänge (siehe unten)

Attachments

Verwende das attachments-Array für Bilder, Dateien, Audio und Video. handleInbound() löst sie automatisch auf: Bilder mit Base64-data werden als Vision-Input an das Modell gesendet, Dateien mit einem filePath wird ihr Pfad an den Prompt angehängt, damit der Agent sie lesen kann.

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 }

Beispiel – Verarbeitung eines Datei-Uploads in deinem 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, }, ];

Die Legacy-Felder imageBase64/imageMimeType funktionieren weiterhin aus Gründen der Abwärtskompatibilität, für neuen Code wird jedoch attachments bevorzugt.

Extension-Manifest

Deine qwen-extension.json deklariert den Channel-Typ. Der Schlüssel muss mit channelType in deinem Plugin-Objekt übereinstimmen:

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

Optionale Erweiterungspunkte

Eigene Slash-Commands – registriere sie in deinem Konstruktor:

this.registerCommand('mycommand', async (envelope, args) => { await this.sendMessage(envelope.chatId, 'Response'); return true; // handled, don't forward to agent });

Verarbeitungsindikatoren – überschreibe onPromptStart() und onPromptEnd(), um plattformspezifische Tippindikatoren anzuzeigen. Diese Hooks werden nur ausgelöst, wenn ein Prompt tatsächlich verarbeitet wird – nicht für gepufferte Nachrichten (Collect-Mode) oder geblockte/gefilterte Nachrichten:

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

Tool-Call-Hooks – überschreibe onToolCall(), um Agent-Aktivitäten anzuzeigen (z. B. „Shell-Befehl wird ausgeführt…“).

Streaming-Hooks – überschreibe onResponseChunk(chatId, chunk, sessionId) für eine chunkweise progressive Anzeige (z. B. direktes Bearbeiten einer Nachricht). Überschreibe onResponseComplete(chatId, fullText, sessionId), um die finale Ausgabe anzupassen.

Block-Streaming – setze blockStreaming: "on" in der Channel-Konfiguration. Die Basisklasse teilt Antworten automatisch an Absatzgrenzen in mehrere Nachrichten auf. Es ist kein Plugin-Code erforderlich – es funktioniert parallel zu onResponseChunk.

Medien – befülle envelope.attachments mit Bildern/Dateien. Siehe Attachments oben.

Referenzimplementierungen

  • Plugin-Beispiel (packages/channels/plugin-example/) – minimaler WebSocket-basierter Adapter, guter Einstiegspunkt
  • Telegram (packages/channels/telegram/) – voll funktionsfähig: Bilder, Dateien, Formatierung, Tippindikatoren
  • DingTalk (packages/channels/dingtalk/) – stream-basiert mit Rich-Text-Verarbeitung
Last updated on