Skip to Content
Guide développeurGuide du développeur de plugins de canal

Guide du développeur de plugins de canal

Un plugin de canal connecte Qwen Code à une plateforme de messagerie. Il est empaqueté sous forme d’extension et chargé au démarrage. Pour la documentation destinée aux utilisateurs sur l’installation et la configuration des plugins, consultez Plugins.

Comment cela s’articule

Votre plugin se situe dans la couche Platform Adapter. Vous gérez les aspects spécifiques à la plateforme (connexion, réception des messages, envoi des réponses). ChannelBase gère tout le reste (contrôle d’accès, routage des sessions, mise en file d’attente des prompts, commandes slash, récupération après crash).

Votre Plugin → construit Envelope → handleInbound() ChannelBase → gates → commandes → routage → AcpBridge.prompt() ChannelBase → appelle votre sendMessage() avec la réponse de l'agent

L’objet Plugin

Le point d’entrée de votre extension exporte un plugin conforme à 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), };

L’adaptateur de canal

Étendez ChannelBase et implémentez trois méthodes :

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

L’objet Envelope

L’objet message normalisé que vous construisez à partir des données de la plateforme. Les indicateurs booléens pilotent la logique des gates, ils doivent donc être exacts.

ChampTypeRequisNotes
channelNamestringOuiUtilisez this.name
senderIdstringOuiDoit rester stable entre les messages (utilisé pour le routage des sessions + le contrôle d’accès)
senderNamestringOuiNom d’affichage
chatIdstringOuiDoit distinguer les messages privés des groupes
textstringOuiSupprimez les @mentions du bot
threadIdstringNonPour sessionScope: "thread"
messageIdstringNonID du message sur la plateforme — utile pour la corrélation des réponses
isGroupbooleanOuiGroupGate s’appuie sur cette valeur
isMentionedbooleanOuiGroupGate s’appuie sur cette valeur
isReplyToBotbooleanOuiGroupGate s’appuie sur cette valeur
referencedTextstringNonMessage cité — ajouté en préfixe comme contexte
imageBase64stringNonImage encodée en Base64 (hérité — préférez attachments)
imageMimeTypestringNonex. image/jpeg (hérité — préférez attachments)
attachmentsAttachment[]NonPièces jointes multimédias structurées (voir ci-dessous)

Attachments

Utilisez le tableau attachments pour les images, fichiers, audio et vidéo. handleInbound() les résout automatiquement : les images avec des data en base64 sont envoyées au modèle en tant qu’entrée visuelle, et les fichiers avec un filePath voient leur chemin ajouté au prompt pour que l’agent puisse les lire.

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 }

Exemple — gestion d’un téléchargement de fichier dans votre adaptateur :

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

Les champs hérités imageBase64/imageMimeType fonctionnent toujours pour la rétrocompatibilité, mais attachments est recommandé pour le nouveau code.

Manifeste de l’extension

Votre fichier qwen-extension.json déclare le type de canal. La clé doit correspondre à channelType dans votre objet plugin :

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

Points d’extension optionnels

Commandes slash personnalisées — enregistrez-les dans votre constructeur :

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

Indicateurs d’activité — redéfinissez onPromptStart() et onPromptEnd() pour afficher les indicateurs de frappe spécifiques à la plateforme. Ces hooks ne se déclenchent que lorsqu’un prompt commence réellement à être traité — pas pour les messages mis en mémoire tampon (mode collect) ou les messages filtrés/bloqués :

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

Hooks d’appel d’outils — redéfinissez onToolCall() pour afficher l’activité de l’agent (ex. « Exécution de la commande shell… »).

Hooks de streaming — redéfinissez onResponseChunk(chatId, chunk, sessionId) pour un affichage progressif par chunk (ex. modification d’un message sur place). Redéfinissez onResponseComplete(chatId, fullText, sessionId) pour personnaliser la livraison finale.

Streaming par blocs — définissez blockStreaming: "on" dans la configuration du canal. La classe de base divise automatiquement les réponses en plusieurs messages aux limites des paragraphes. Aucun code plugin n’est nécessaire — cela fonctionne conjointement avec onResponseChunk.

Médias — renseignez envelope.attachments avec des images/fichiers. Voir Attachments ci-dessus.

Implémentations de référence

  • Exemple de plugin (packages/channels/plugin-example/) — adaptateur minimal basé sur WebSocket, bon point de départ
  • Telegram (packages/channels/telegram/) — complet : images, fichiers, formatage, indicateurs de frappe
  • DingTalk (packages/channels/dingtalk/) — basé sur le streaming avec gestion du texte enrichi
Last updated on