Changer le thème

Architecture OpenClaw en profondeur : principes de la conception en trois couches et pratiques d'extension

Easton editorial illustration: production agent control room

Conclusion rapide (saisir la ligne directrice de l’architecture)

La façon la plus rapide de comprendre OpenClaw est de saisir les responsabilités des trois couches : Gateway gère les sessions, Channel le routage des messages, LLM l’interface modèle.
Dès que vous classez un problème dans l’une de ces trois couches, le dépannage et l’extension deviennent beaucoup plus rapides.
Développer un nouveau Channel ou Provider revient essentiellement à implémenter l’interface et à l’enregistrer.

Deux heures du matin, je fixais le dépôt OpenClaw dans l’éditeur, prêt à ajouter un Channel DingTalk. Des dizaines de fichiers dans src/, gateway, channel et llm entremêlés — je ne savais pas par où commencer. Modifier gateway impacterait-il les autres Channels ? Copier le code WhatsApp serait-il fiable ? Et si rien ne démarre après mes modifications ?

Franchement, c’était décourageant. La documentation officielle explique comment utiliser le produit, pas comment le système fonctionne en interne. Pour du développement personnalisé, c’était comme tâtonner dans le noir — toucher le traitement webhook sans comprendre le routage ; voir l’appel LLM sans saisir l’enregistrement des Providers.

J’ai ensuite passé trois jours entiers à parcourir le code source, et découvert la finesse du design d’OpenClaw : Gateway gère les sessions, Channel le routage, LLM l’interface — trois couches aux responsabilités nettes. Une fois compris, le développement personnalisé n’est plus de l’exploration aveugle, mais un processus structuré.

Cet article rassemble systématiquement le fruit de ces trois jours. Vous verrez pourquoi OpenClaw se divise en trois couches, quel problème chacune résout, comment Gateway gère l’état des sessions, comment Channel adapte les différentes plateformes, comment le système de plugins Provider de la couche LLM est conçu, et enfin un guide pas à pas pour développer des Channels et Providers personnalisés.

Guide « élevage de homard » à faible coût : ArkClaw démocratise l’agent IA

OpenClaw (le « homard ») est populaire mais sa configuration rebute ? ArkClaw de ByteDance Volcano Engine abaisse la barrière au minimum. Sans serveur ni configuration de Token, un clic suffit pour un « agent IA » en ligne 24 h/24, capable de contrôler le navigateur, exécuter des scripts et gérer le calendrier.

Le prix est réellement bas : 9,9 yuans/mois ; avec mon code d’invitation ZLKUK54M (inscrivez-vous ici), seulement 8,9 yuans. Si vous êtes développeur, le Coding Plan Pro offre l’accès gratuit.

Panorama de l’architecture OpenClaw : pourquoi trois couches ?

En découvrant OpenClaw, je me demandais : pourquoi une telle complexité en couches ? Ne suffit-il pas de transmettre le message de l’utilisateur à l’IA ?

En étudiant le code source, j’ai compris : un design monolithique fonctionne à petite échelle, mais OpenClaw doit supporter plusieurs plateformes (WhatsApp, Telegram, Gmail), plusieurs modèles (Claude, GPT, modèles locaux) et gérer des centaines de sessions utilisateur. Sans séparation en couches, toute la logique s’entasse — modifier un point peut impacter l’ensemble, rendant la maintenance impossible.

Philosophie de la conception en trois couches

OpenClaw divise le système en trois couches, chacune avec sa responsabilité :

Couche Gateway (centre de gestion des sessions)

  • Gestion du cycle de vie complet des sessions utilisateur
  • File d’attente et ordonnancement des messages (priorités)
  • Authentification et contrôle des permissions (qui peut utiliser)
  • Maintien des connexions WebSocket longues

Couche Channel (adaptateur de plateforme)

  • Adaptation des formats de message selon la plateforme (WhatsApp et Telegram diffèrent)
  • Règles de routage (DM ou groupe, réponse uniquement sur @)
  • Gestion des événements (réception, envoi, erreurs)

Couche LLM (interface modèle)

  • Interface Provider unifiée (même mode d’appel pour Claude ou GPT)
  • Appel d’outils (Function Calling)
  • Traitement des réponses en streaming
  • Intégration des serveurs MCP
2026
Refonte plugin

Flux complet de traitement d’un message

Un scénario concret : vous envoyez un message au bot sur WhatsApp.

  1. Réception Channel : le Channel WhatsApp reçoit le webhook et normalise le message au format interne
  2. Décision de routage : vérification DM/groupe, mention du bot, permissions utilisateur
  3. Ordonnancement Gateway : trouver (ou créer) la Session de l’utilisateur, ajouter le message à la file
  4. Traitement LLM : selon la config, choisir un Provider (ex. Anthropic) et envoyer le contexte de conversation
  5. Retour de la réponse : résultat LLM → Gateway → Channel → l’utilisateur reçoit la réponse

La finesse de ce design : chaque couche est indépendante. Nouvelle plateforme ? Modifier uniquement Channel. Changer de modèle ? Modifier uniquement LLM. Gateway reste intact.

Couche Gateway : pivot central de la gestion des sessions

En lisant le code Gateway, ce qui m’a le plus déconcerté, c’est l’objet Session. Chaque utilisateur en a une — mais que contient-il exactement, et comment est-il géré ?

Cycle de vie d’une Session

Imaginez Gateway comme un centre de tri postal : chaque utilisateur est une adresse de livraison, la Session est le registre de cette adresse.

Contenu d’un objet Session :

  • conversationHistory : historique de conversation (N derniers messages)
  • context : variables de contexte (préférences utilisateur, données temporaires)
  • state : état courant (idle, processing, waiting)
  • channelInfo : informations de la plateforme source (Channel d’origine)

Gestion du cycle de vie :

// Exemple simplifié, logique centrale
class SessionManager {
  // À la réception d'un message
  async handleMessage(userId, channelId, message) {
    // 1. Trouver la Session (ou en créer une)
    let session = this.getOrCreate(userId, channelId);

    // 2. Mettre à jour l'historique
    session.conversationHistory.push(message);

    // 3. Ajouter à la file de traitement
    await this.messageQueue.enqueue(session, message);

    // 4. Persister (éviter la perte en cas de crash)
    await this.persist(session);
  }
}

Point clé : OpenClaw utilise le mode d’isolation per-channel-peer. Concrètement, le même utilisateur sur WhatsApp et Telegram a deux Sessions indépendantes. Cela évite la confusion de contexte — technique sur WhatsApp, météo sur Telegram, sans mélange.

Stratégie de priorité de l’ordonnancement des messages

Gateway ne traite pas immédiatement chaque message reçu ; il passe par une file d’ordonnancement. Deux problèmes résolus :

Problème 1 : contrôle de concurrence
Si 100 utilisateurs envoient un message simultanément et que tout part directement vers le LLM, l’API est saturée. La file Gateway limite le débit, par ex. « 10 requêtes simultanées maximum ».

Problème 2 : retry en cas d’erreur
Si l’appel LLM échoue ? Gateway retente automatiquement 3 fois avec un intervalle croissant (1 s, 2 s, 4 s), évitant la perte de messages sur une panne transitoire.

// Logique centrale de la file de messages
class MessageQueue {
  async enqueue(session, message) {
    // Vérifier la concurrence
    if (this.activeJobs >= this.maxConcurrency) {
      // Placer en file d'attente
      this.waitingQueue.push({ session, message });
      return;
    }

    // Exécuter le traitement
    this.activeJobs++;
    try {
      await this.process(session, message);
    } catch (error) {
      // Logique de retry
      await this.retryWithBackoff(session, message);
    } finally {
      this.activeJobs--;
      this.processNext(); // traiter le suivant
    }
  }
}

Les pièges des connexions WebSocket longues

Pour des applications exigeant du temps réel (ex. bot de support client), la gestion WebSocket est un vrai défi.

Approche d’OpenClaw :

  • Heartbeat : ping toutes les 30 secondes, timeout = connexion considérée comme coupée
  • Reconnexion automatique : backoff exponentiel après déconnexion (1 s, 2 s, 4 s… max 30 s)
  • Synchronisation d’état : restauration automatique de la Session après reconnexion

Ces détails semblent mineurs, mais améliorent nettement la stabilité. J’ai déjà écrit un système similaire sans heartbeat — connexion fantôme, messages perdus sans que le programme le sache.

Couche Channel : routage multi-plateforme

La couche Channel est pour moi la plus intéressante. Elle résout un problème central : les formats de message diffèrent totalement selon les plateformes — comment les traiter de façon unifiée ?

L’utilité du pattern Adapter

Message WhatsApp :

{
  "from": "1234567890",
  "body": "Bonjour",
  "type": "text"
}

Message Telegram :

{
  "message": {
    "chat": {"id": 123},
    "text": "Bonjour"
  }
}

Écrire une logique par plateforme ferait exploser le code. OpenClaw applique le pattern Adapter classique : une interface Message standardisée, chaque Channel convertissant le message de sa plateforme vers ce format.

// Format de message standardisé
interface StandardMessage {
  userId: string;      // ID utilisateur unifié
  content: string;     // Contenu du message
  timestamp: number;   // Horodatage
  metadata: any;       // Données spécifiques à la plateforme
}

// Adaptateur WhatsApp
class WhatsAppChannel implements Channel {
  adaptMessage(rawMessage): StandardMessage {
    return {
      userId: rawMessage.from,
      content: rawMessage.body,
      timestamp: Date.now(),
      metadata: { platform: 'whatsapp' }
    };
  }
}

Avantage : Gateway et LLM ignorent la plateforme d’origine — ils ne traitent que des StandardMessage.

Principe d’implémentation des règles de routage

Autre responsabilité importante de Channel : décider quels messages méritent une réponse.

OpenClaw supporte deux types de règles :

dmPolicy (politique de messages privés)

  • pairing : appairage préalable requis (le plus sûr)
  • allowlist : seuls les utilisateurs en liste blanche
  • open : tout le monde peut utiliser (bot public)
  • disabled : messages privés désactivés

mentionGating (déclenchement par @ en groupe)
En groupe, réponse uniquement si le bot est mentionné, évitant le spam. Logique simple :

class TelegramChannel {
  shouldRespond(message): boolean {
    // Message privé : répondre directement
    if (message.chat.type === 'private') {
      return this.checkDmPolicy(message.from.id);
    }

    // Groupe : vérifier @
    if (message.chat.type === 'group') {
      const mentioned = message.entities?.some(
        e => e.type === 'mention' && e.user.id === this.botId
      );
      return mentioned;
    }

    return false;
  }
}

En développant un Channel DingTalk, j’ai suivi cette logique. La détection @ DingTalk diffère légèrement (champ atUsers), mais le cadre reste le même.

Méthode pour développer un Channel personnalisé

Pour intégrer Discord, le flux ressemble à ceci :

  1. Créer la classe Channel : implémenter l’interface Channel
  2. Implémenter les méthodes requises :
    • start() : démarrer le Channel (écouter webhook ou WebSocket)
    • sendMessage() : envoyer un message vers la plateforme
    • adaptMessage() : conversion de format
  3. Enregistrer dans le système : ajouter la config Channel
  4. Tester : exposer le service local avec ngrok, tester le webhook

Les exemples complets sont dans la section pratique en fin d’article.

Couche LLM : conception plugin de l’interface modèle

La couche LLM a subi en 2026 une refonte majeure, passant du hardcodé au système de plugins. Ce changement détermine combien de modèles OpenClaw peut supporter.

Système de plugins Provider

Ancien design (pseudo-code) :

// Ancien design : hardcodé
if (config.provider === 'anthropic') {
  return new AnthropicClient();
} else if (config.provider === 'openai') {
  return new OpenAIClient();
}

Problème : chaque nouveau modèle ajoute un if-else, le code grossit indéfiniment.

Le nouveau design introduit l’interface Provider :

// Définition de l'interface Provider
interface LLMProvider {
  name: string;  // 'anthropic', 'openai', 'ollama'

  // Envoyer un message, retourner une réponse en streaming
  chat(messages: Message[], options: ChatOptions): AsyncIterator<string>;

  // Support des appels d'outils
  supportTools(): boolean;

  // Initialisation de la configuration
  initialize(config: ProviderConfig): void;
}

Tout Provider implémentant cette interface peut s’intégrer. Au démarrage, le système scanne et enregistre automatiquement :

// Mécanisme d'enregistrement des plugins
class ProviderRegistry {
  private providers = new Map<string, LLMProvider>();

  register(provider: LLMProvider) {
    this.providers.set(provider.name, provider);
  }

  get(name: string): LLMProvider {
    return this.providers.get(name);
  }
}

// Découverte et enregistrement automatiques
const registry = new ProviderRegistry();
registry.register(new AnthropicProvider());
registry.register(new OpenAIProvider());
registry.register(new OllamaProvider());

Avantage : nouveau modèle ? Écrire une classe Provider, l’enregistrer — sans toucher au code central.

Différences entre les Providers principaux

L’interface est unifiée, mais les détails d’implémentation varient. Quelques pièges que j’ai rencontrés :

Provider Anthropic (Claude)

  • Streaming natif (stream: true)
  • Format Tool Use particulier (tableau tools)
  • Grande fenêtre de contexte (Claude 3.5 jusqu’à 200k tokens)

Provider OpenAI (ChatGPT)

  • Function Calling et Tool Use sont deux API (ancienne version functions, nouvelle tools)
  • Le streaming retourne des fragments delta à concaténer manuellement
  • Limites de taux strictes (RPM/TPM à contrôler)

Provider Ollama (modèles locaux)

  • Pas de clé API, appel HTTP direct au service local
  • Performance très dépendante du matériel (CPU lent, GPU recommandé)
  • Support des outils variable selon le modèle (llama3 oui, qwen parfois non)

J’ai voulu faire tourner Llama3 en local via Ollama — le format d’appel d’outils diffère totalement de Claude, adaptation laborieuse.

Mécanisme Tool Use en détail

Le Tool Use (appel d’outils) est une fonction centrale de la couche LLM : permettre à l’IA d’« appeler des fonctions ».

Exemple : « Quelle heure est-il à Pékin ? » L’IA va :

  1. Décider d’appeler l’outil get_current_time
  2. Retourner la requête d’appel : {"name": "get_current_time", "args": {"city": "Pékin"}}
  3. OpenClaw exécute l’outil et retourne : {"time": "2026-02-05 20:30"}
  4. L’IA génère la réponse : « Il est 20 h 30 à Pékin. »

Mécanisme d’enregistrement des outils dans OpenClaw :

// Définition des outils
const tools = [
  {
    name: 'get_current_time',
    description: 'Obtenir l\'heure actuelle d\'une ville',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string', description: 'Nom de la ville' }
      },
      required: ['city']
    }
  }
];

// Exécution des outils
async function executeTool(toolName, args) {
  const handlers = {
    'get_current_time': (args) => {
      // L'implémentation réelle peut appeler une API
      return { time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) };
    }
  };

  return handlers[toolName](args);
}

Important : l’exécution des outils doit être isolée en sandbox — sinon l’IA pourrait demander rm -rf /. OpenClaw intègre un contrôle de permissions n’autorisant que les outils prédéfinis.

Pratique : étendre l’architecture OpenClaw

La théorie est posée, passons à la pratique. Deux exemples complets : développer un Channel Discord et un Provider Kimi.

Développer un Channel personnalisé : intégration Discord

Discord diffère de WhatsApp : WebSocket pour recevoir, REST API pour envoyer.

Étape 1 : implémenter l’interface Channel

import { Client, GatewayIntentBits } from 'discord.js';

class DiscordChannel implements Channel {
  private client: Client;
  private gateway: Gateway; // Instance Gateway OpenClaw

  async start() {
    // Initialiser le client Discord
    this.client = new Client({
      intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.DirectMessages
      ]
    });

    // Écouter les messages
    this.client.on('messageCreate', async (msg) => {
      if (msg.author.bot) return; // Ignorer les messages de bots

      // Convertir au format standard
      const standardMsg = this.adaptMessage(msg);

      // Confier au Gateway
      const response = await this.gateway.handleMessage(standardMsg);

      // Envoyer la réponse
      await msg.reply(response.content);
    });

    // Connexion
    await this.client.login(process.env.DISCORD_TOKEN);
  }

  adaptMessage(discordMsg): StandardMessage {
    return {
      userId: discordMsg.author.id,
      channelId: 'discord',
      content: discordMsg.content,
      timestamp: discordMsg.createdTimestamp,
      metadata: {
        guildId: discordMsg.guildId,
        channelType: discordMsg.channel.type
      }
    };
  }

  async sendMessage(userId: string, content: string) {
    const user = await this.client.users.fetch(userId);
    await user.send(content);
  }
}

Étape 2 : enregistrer dans OpenClaw

Dans config.json :

{
  "channels": {
    "discord": {
      "enabled": true,
      "token": "YOUR_DISCORD_BOT_TOKEN",
      "dmPolicy": "open"
    }
  }
}

Dans le script de démarrage :

import { DiscordChannel } from './channels/discord';

const gateway = new Gateway(config);
const discordChannel = new DiscordChannel(gateway, config.channels.discord);
gateway.registerChannel('discord', discordChannel);

await discordChannel.start();

Étape 3 : tester

  1. Créer un Bot sur la plateforme développeur Discord, obtenir le Token
  2. Inviter le Bot sur votre serveur
  3. Démarrer OpenClaw, envoyer un message privé au Bot
  4. Vérifier les logs pour confirmer le flux de messages

Piège rencontré : le système de permissions Discord est complexe — le Bot doit avoir Send Messages et Read Message History, sinon l’envoi échoue.

Développer un Provider personnalisé : intégration Kimi

L’API Kimi (Moonshot AI) ressemble à OpenAI, avec quelques différences.

Implémentation Provider :

class KimiProvider implements LLMProvider {
  name = 'kimi';
  private apiKey: string;
  private baseURL = 'https://api.moonshot.cn/v1';

  initialize(config: ProviderConfig) {
    this.apiKey = config.apiKey;
  }

  async *chat(messages: Message[], options: ChatOptions) {
    const response = await fetch(`${this.baseURL}/chat/completions`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: options.model || 'moonshot-v1-8k',
        messages: messages.map(m => ({
          role: m.role,
          content: m.content
        })),
        stream: true,
        temperature: options.temperature || 0.7
      })
    });

    // Traiter la réponse en streaming
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      const lines = chunk.split('\n').filter(line => line.trim());

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6);
          if (data === '[DONE]') continue;

          const parsed = JSON.parse(data);
          const content = parsed.choices[0]?.delta?.content;
          if (content) {
            yield content;
          }
        }
      }
    }
  }

  supportTools(): boolean {
    return false; // Kimi ne supporte pas encore Function Calling
  }
}

Enregistrer le Provider :

const registry = new ProviderRegistry();
registry.register(new KimiProvider());

// Configuration d'utilisation
const config = {
  llm: {
    provider: 'kimi',
    apiKey: process.env.KIMI_API_KEY,
    model: 'moonshot-v1-32k'
  }
};

Retours d’expérience :

  • Le format de streaming Kimi est identique à OpenAI, réutilisable directement
  • La gestion d’erreurs diffère — le timeout ne retourne pas de code d’erreur standard
  • Pas de Function Calling pour l’instant — inutilisable si votre application en dépend

Pratiques d’optimisation des performances

Faire fonctionner le code n’est qu’une première étape ; l’optimisation est le vrai travail. Quelques points testés :

Optimisation du cache Session
Par défaut, la Session est en mémoire — perdue au redémarrage. Intégration Redis possible :

class RedisSessionStore {
  private redis: Redis;

  async get(userId: string, channelId: string): Promise<Session> {
    const key = `session:${channelId}:${userId}`;
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  async set(session: Session) {
    const key = `session:${session.channelId}:${session.userId}`;
    await this.redis.setex(key, 3600, JSON.stringify(session)); // expiration 1 h
  }
}

Réglage de la file de messages
En haute concurrence, la file en mémoire ne suffit pas — Bull (file de tâches Redis) :

import Queue from 'bull';

const messageQueue = new Queue('openclaw-messages', {
  redis: { host: 'localhost', port: 6379 }
});

messageQueue.process(10, async (job) => { // 10 concurrents max
  const { session, message } = job.data;
  return await gateway.processMessage(session, message);
});

Contrôle du nombre de connexions concurrentes
Les API LLM ont des limites de taux (ex. OpenAI 60 RPM). Bibliothèque p-limit :

import pLimit from 'p-limit';

const limit = pLimit(10); // 10 requêtes concurrentes max

const tasks = messages.map(msg =>
  limit(() => provider.chat(msg))
);

await Promise.all(tasks);

Comparaison avant/après optimisation (mes mesures) :

  • Avant : 100 requêtes concurrentes, temps de réponse moyen 8 s, taux d’échec 15 %
  • Après : 100 requêtes concurrentes, temps de réponse moyen 3 s, taux d’échec < 1 %
2,6×
Gain de performance

Conclusion

De Gateway à Channel puis LLM, l’architecture en trois couches d’OpenClaw est remarquablement claire. Chaque couche a sa responsabilité, les frontières sont nettes, l’extension est simple.

Une fois ce cadre compris, développer de nouvelles fonctionnalités devient plus fluide. Nouvelle plateforme ? Écrire un Channel Adapter. Nouveau modèle ? Implémenter un Provider. Optimiser les performances ? Identifier la couche concernée et agir de façon ciblée.

Si vous comptez personnaliser OpenClaw en profondeur, clonez d’abord le code source et parcourez-le en suivant la logique de cet article. Session Gateway, routage Channel, enregistrement Provider — ces trois blocs sont le cœur du système.

Maîtriser cela, vous ne « copiez plus la config de la doc » — vous comprenez le système et pouvez l’étendre et l’optimiser librement.

Essayez ensuite un Channel personnalisé simple (WeCom, Lark/Feishu) : la pratique approfondit la compréhension. La communauté open source OpenClaw est active — les GitHub Issues accueillent vos questions.

Prochaines lectures

Processus complet de développement d'un Channel personnalisé OpenClaw

Développer et intégrer un Channel personnalisé dans OpenClaw, de zéro

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Comprendre la spécification de l'interface Channel

    L'interface Channel définit les méthodes qu'un adaptateur de plateforme doit implémenter :

    Méthodes principales :
    • start() : démarre le Channel, écoute les messages de la plateforme (webhook ou WebSocket)
    • sendMessage(userId, content) : envoie un message vers la plateforme
    • adaptMessage(rawMessage) : convertit le message de la plateforme au format StandardMessage

    Format StandardMessage :
    • userId: string (identifiant utilisateur unifié)
    • channelId: string (identifiant du Channel)
    • content: string (contenu du message)
    • timestamp: number (horodatage)
    • metadata: any (données spécifiques à la plateforme)

    Méthodes de contrôle du routage :
    • shouldRespond(message) : détermine s'il faut répondre au message
    • checkDmPolicy(userId) : vérifie la politique de messages privés
    • checkMention(message) : vérifie le déclenchement par @ en groupe

    Implémentations de référence : src/channels/whatsapp.ts ou src/channels/telegram.ts
  2. 2

    Step 2: Créer la classe Channel et implémenter l'interface

    Créez un nouveau fichier dans src/channels/ (par ex. discord.ts) :

    typescript
    class DiscordChannel implements Channel &#123;
    private client: Client;
    private gateway: Gateway;

    constructor(gateway: Gateway, config: ChannelConfig) &#123;
    this.gateway = gateway;
    this.config = config;
    &#125;

    async start() &#123;
    // Initialiser le client Discord
    this.client = new Client(&#123; intents: [...] &#125;);

    // Écouter les événements de message
    this.client.on('messageCreate', async (msg) => &#123;
    const standardMsg = this.adaptMessage(msg);
    const response = await this.gateway.handleMessage(standardMsg);
    await msg.reply(response.content);
    &#125;);

    await this.client.login(this.config.token);
    &#125;

    adaptMessage(msg): StandardMessage &#123;
    return &#123;
    userId: msg.author.id,
    channelId: 'discord',
    content: msg.content,
    timestamp: msg.createdTimestamp,
    metadata: &#123; guildId: msg.guildId &#125;
    &#125;;
    &#125;

    async sendMessage(userId: string, content: string) &#123;
    const user = await this.client.users.fetch(userId);
    await user.send(content);
    &#125;
    &#125;


    Points clés :
    • Initialiser le SDK de la plateforme dans start()
    • Convertir les messages reçus au format StandardMessage
    • Gérer les appels API spécifiques à la plateforme lors de l'envoi
    • Ne pas négliger la gestion des erreurs et la journalisation
  3. 3

    Step 3: Implémenter les règles de routage et le contrôle des permissions

    Implémentez la logique de filtrage des messages selon vos besoins métier :

    Implémentation dmPolicy :
    • mode pairing : maintient une liste d'utilisateurs appairés, ne répond qu'à eux
    • mode allowlist : vérifie si l'ID utilisateur est dans la liste blanche
    • mode open : répond à tous les utilisateurs
    • mode disabled : refuse tous les messages privés

    typescript
    shouldRespond(message): boolean &#123;
    // Vérifier la politique en message privé
    if (message.metadata.channelType === 'DM') &#123;
    return this.checkDmPolicy(message.userId);
    &#125;

    // Vérifier @ en groupe
    if (message.metadata.channelType === 'GROUP') &#123;
    return this.checkMention(message);
    &#125;

    return false;
    &#125;


    Implémentation mentionGating (déclenchement en groupe) :
    • Vérifier si le message contient une mention du bot
    • Le format de mention varie selon la plateforme (Discord utilise &lt;@botId&gt;, Telegram utilise @username)
    • Retourner true pour répondre, false pour ignorer
  4. 4

    Step 4: Configuration et enregistrement

    1. Ajouter la configuration Channel dans config.json :

    json
    &#123;
    "channels": &#123;
    "discord": &#123;
    "enabled": true,
    "token": "YOUR_BOT_TOKEN",
    "dmPolicy": "open",
    "mentionGating": true
    &#125;
    &#125;
    &#125;


    2. Enregistrer le Channel dans le script de démarrage :

    typescript
    import &#123; DiscordChannel &#125; from './channels/discord';

    const gateway = new Gateway(config);
    const discordChannel = new DiscordChannel(
    gateway,
    config.channels.discord
    );

    // Enregistrer auprès du Gateway
    gateway.registerChannel('discord', discordChannel);

    // Démarrer le Channel
    await discordChannel.start();


    3. Configuration des variables d'environnement :
    • Placer les informations sensibles (Token, clés) dans le fichier .env
    • Charger avec la bibliothèque dotenv : require('dotenv').config()
  5. 5

    Step 5: Tests et débogage

    Processus de test :

    1. Test en développement local :
    • Utiliser ngrok pour exposer le service local (nécessaire pour les plateformes webhook)
    • Configurer le webhook de la plateforme vers l'URL ngrok
    • Démarrer OpenClaw et vérifier les logs

    2. Validation du flux de messages :
    • Envoyer un message de test, vérifier que l'écoute dans start() se déclenche
    • Confirmer que adaptMessage() convertit correctement
    • Vérifier que Gateway.handleMessage() est appelé
    • Contrôler que sendMessage() envoie bien la réponse

    3. Test des règles de routage :
    • Tester les politiques de messages privés (pairing/allowlist/open)
    • Tester le déclenchement par @ en groupe (avec et sans @)
    • Tester les listes blanches/noires

    4. Test de la gestion des exceptions :
    • Simuler un timeout réseau
    • Simuler un token expiré
    • Simuler un format de message anormal

    Astuces de débogage :
    • Ajouter console.log() aux points clés ou utiliser la bibliothèque debug
    • Consulter les logs Gateway pour confirmer l'arrivée des messages
    • Utiliser les outils de test fournis par la plateforme (ex. Discord Bot Dashboard)
    • Activer le mode verbose : DEBUG=openclaw:* openclaw gateway (courant après installation globale npm ; en développement local depuis le dépôt officiel, se référer à la documentation du dépôt)
  6. 6

    Step 6: Optimisation des performances et préparation à la mise en production

    Checklist d'optimisation :

    1. Gestion des connexions :
    • Implémenter un heartbeat (éviter les connexions fantômes)
    • Ajouter une reconnexion automatique (backoff exponentiel)
    • Gérer l'arrêt gracieux (signal SIGTERM)

    2. Gestion des erreurs :
    • Capturer toutes les exceptions possibles
    • Implémenter un mécanisme de retry des messages (3 tentatives max)
    • Journaliser les erreurs dans un fichier ou un système de monitoring

    3. Optimisation des performances :
    • Traitement par lots des messages (réduire les appels API)
    • Utiliser un pool de connexions (base de données/Redis)
    • Contrôle du débit (éviter les limites de taux de la plateforme)

    4. Monitoring et logs :
    • Enregistrer le temps de traitement des messages
    • Statistiques de taux de succès et d'échec
    • Définir des seuils d'alerte (alerte si taux d'échec &gt; 5 %)

    Vérifications avant mise en production :
    • Test de charge (simuler 100+ utilisateurs concurrents)
    • Détection de fuites mémoire (test de longue durée)
    • Sauvegarde de configuration et plan de rollback
    • Rédiger la documentation d'exploitation (démarrage, arrêt, dépannage)

FAQ

Pourquoi utiliser l'isolation de session per-channel-peer plutôt qu'une Session unique pour toutes les plateformes ?
Le mode per-channel-peer offre deux avantages majeurs : éviter la confusion de contexte et renforcer la sécurité :

Isolation du contexte : si le même utilisateur discute de technique sur WhatsApp et demande la météo sur Telegram, une Session partagée mélangerait les deux conversations. L'IA transporterait le contexte technique dans la requête météo, produisant des réponses hors sujet.

Isolation de sécurité : les mécanismes de vérification d'identité diffèrent selon les plateformes. Une Session partagée pourrait permettre un contournement des permissions. Par exemple, l'utilisateur est authentifié sur WhatsApp, mais son compte Telegram pourrait être usurpé — l'isolation séparée est plus sûre.

Considération de performance : chaque Session de Channel est stockée indépendamment, permettant un traitement parallèle des messages de différentes plateformes sans blocage mutuel.

Si un contexte partagé entre plateformes est nécessaire, implémentez l'association de comptes utilisateur au niveau applicatif plutôt que de fusionner au niveau Session.
Lors du développement d'un Provider personnalisé, comment gérer les modèles qui ne supportent pas la réponse en streaming ?
L'interface Provider d'OpenClaw exige un retour AsyncIterator, mais certaines API de modèles ne supportent pas le streaming. Solutions :

Solution 1 : wrapper en pseudo-streaming (recommandé)
async *chat(messages) &#123;
const response = await fetch(apiUrl, &#123; ... &#125;); // requête non-streaming
const result = await response.json();
yield result.content; // retourne tout le contenu d'un coup
&#125;

Solution 2 : simuler le streaming par morceaux
const fullText = await getNonStreamResponse();
const chunkSize = 50;
for (let i = 0; i &lt; fullText.length; i += chunkSize) &#123;
yield fullText.slice(i, i + chunkSize);
await sleep(100); // simule un délai
&#125;

La solution 1 est simple et directe : l'utilisateur attend puis reçoit la réponse complète. La solution 2 simule un effet machine à écrire, mais ajoute de la complexité. Choisissez selon vos besoins.
Que se passe-t-il quand la file d'attente de messages du Gateway est pleine ? Comment éviter la perte de messages ?
Stratégie de gestion d'une file d'attente pleine :

Comportement par défaut : la file en mémoire d'OpenClaw a une capacité limitée (1000 messages par défaut). Au-delà, les nouveaux messages sont rejetés et l'utilisateur reçoit une erreur « système occupé ».

Solutions pour éviter la perte de messages :

1. File persistante (recommandé) :
Utiliser Bull ou RabbitMQ pour une file persistante — les messages survivent au redémarrage du service.

2. Augmenter la capacité :
Configurer maxQueueSize: 5000 dans config.json, en surveillant la consommation mémoire.

3. Limitation de débit + message :
Implémenter le throttling au niveau Channel ; au-delà du débit, inviter l'utilisateur à « réessayer plus tard », évitant l'accumulation massive.

4. File de priorité :
Les messages des utilisateurs importants (VIP) sont traités en priorité, les autres attendent.

En production, la combinaison Bull + Redis est recommandée : persistance et haute concurrence.
Comment déboguer le flux de messages entre Gateway et Channel quand un message semble envoyé mais sans réponse ?
Méthode systématique pour déboguer le flux de messages :

1. Activer les logs détaillés :
DEBUG=openclaw:* openclaw gateway
Après installation globale npm, cette commande sert au débogage en premier plan ; en développement local depuis le dépôt officiel, consultez le README/scripts du dépôt, sans supposer npm start.
Cela affiche les logs de chaque module : réception, conversion, traitement et envoi des messages.

2. Vérifier les nœuds clés :
• Channel.adaptMessage() : afficher le StandardMessage converti, confirmer le format
• Gateway.handleMessage() : afficher le message reçu et l'état de la Session
• Provider.chat() : afficher le contexte envoyé au LLM
• Channel.sendMessage() : afficher le contenu final envoyé

3. Utiliser le débogage par points d'arrêt :
Configurer launch.json dans VS Code et déboguer pas à pas.

4. Vérifier les problèmes courants :
• shouldRespond() retourne false : message filtré par les règles de routage
• Session introuvable : userId ou channelId ne correspond pas
• Échec d'appel LLM : vérifier clé API, réseau, limites de taux

5. Utiliser des outils de test :
Écrire des tests unitaires simulant l'entrée de messages et validant chaque étape.

En développement, pino-pretty améliore la lisibilité des logs ; en production, privilégier les logs structurés (JSON) pour l'analyse.
Comment le Tool Use du Provider empêche-t-il l'IA d'exécuter des opérations dangereuses (comme supprimer des fichiers) ?
Stratégies de protection pour le Tool Use :

1. Mécanisme de liste blanche (le plus important) :
N'enregistrer que des outils sûrs ; interdire les opérations sur le système de fichiers, les requêtes réseau, etc.

const safeTool = &#123;
name: 'get_weather',
description: 'Obtenir la météo',
handler: getWeatherData // opération sûre en lecture seule
&#125;;

2. Validation des paramètres :
Valider strictement les arguments des outils, rejeter les entrées anormales.

function validateArgs(args) &#123;
if (args.city.includes('&lt;script&gt;')) &#123; // anti-XSS
throw new Error('Invalid input');
&#125;
&#125;

3. Exécution en sandbox (avancé) :
Utiliser vm2 ou isolated-vm pour exécuter le code des outils en isolation.

4. Niveaux de permission :
Différents utilisateurs ont des permissions d'appel d'outils différentes ; les admins accèdent aux outils avancés, les utilisateurs ordinaires aux outils de base.

5. Journal d'audit :
Enregistrer tous les appels d'outils (qui, quand, quel outil, quels paramètres) pour traçabilité.

OpenClaw n'autorise par défaut que les outils prédéfinis, sans exécution dynamique de code — cela évite déjà la majorité des risques. Pour étendre les outils, évaluez soigneusement la sécurité.
Quand plusieurs Channels reçoivent simultanément un message du même utilisateur, comment le Gateway évite-t-il les conflits de concurrence ?
Mécanisme de contrôle de concurrence du Gateway :

Verrouillage de Session :
Chaque Session verrouille le traitement des messages — traitement sérialisé pour une même Session, parallèle pour des Sessions différentes.

Implémentation en pseudo-code :
async handleMessage(session, message) &#123;
const lock = await this.acquireLock(session.id);
try &#123;
// traiter le message
await this.processMessage(session, message);
&#125; finally &#123;
await lock.release();
&#125;
&#125;

Exemple concret :
L'utilisateur envoie un message simultanément sur WhatsApp et Telegram. Grâce à l'isolation per-channel-peer, ce sont deux Sessions indépendantes, traitées en parallèle sans conflit.

Pour des messages concurrents sur le même Channel (trois messages rapides consécutifs), ils entrent dans la file d'attente et sont traités en ordre FIFO.

Déploiement distribué :
Si OpenClaw est déployé sur plusieurs instances, utiliser Redis pour un verrou distribué (algorithme redlock) :

import Redlock from 'redlock';
const lock = await redlock.lock(session.id, 5000); // timeout 5 secondes

Ainsi, même avec plusieurs instances, une seule traite une Session donnée.

15 min de lecture · Publié le: 5 févr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog