Alternar tema

Análise aprofundada da arquitetura do OpenClaw: princípios técnicos do design em três camadas e práticas de extensão

Easton editorial illustration: production agent control room

Conclusão rápida (pegue primeiro o fio da arquitetura)

A forma mais rápida de entender o OpenClaw é começar pelas três responsabilidades: o Gateway cuida das sessões, o Channel cuida do roteamento de mensagens e o LLM cuida das interfaces de modelo.
Se você consegue classificar um problema em uma dessas três camadas, diagnosticar falhas e fazer extensões fica muito mais rápido.
Desenvolver um novo Channel ou um novo Provider, no fim, é preencher a implementação conforme a interface e registrá-la.

Às duas da manhã, eu estava olhando para o repositório do OpenClaw no editor, pronto para adicionar um Channel do DingTalk. Havia dezenas de arquivos no diretório src, com pastas de gateway, channel e llm entrelaçadas, e eu não fazia ideia por onde começar. Alterar o gateway afetaria outros Channels? Copiar direto o código do WhatsApp seria confiável? E se depois de mexer nada rodasse?

Para ser sincero, naquele momento foi bem frustrante. A documentação oficial ensina como usar, mas não explica como o sistema funciona por dentro. Querer fazer desenvolvimento secundário parecia tatear no escuro: eu encontrava o processamento de webhook, mas não sabia como a mensagem era roteada; via a chamada ao LLM, mas não entendia como o Provider era registrado.

Depois, passei três dias inteiros lendo o código-fonte de ponta a ponta, até perceber que o design do OpenClaw é realmente elegante: Gateway gerencia sessões, Channel gerencia roteamento, LLM gerencia interfaces. A arquitetura em três camadas é clara, e os limites de responsabilidade são bem definidos. Quando entendi isso, o desenvolvimento secundário deixou de ser tentativa e erro e passou a ter um caminho lógico.

Neste artigo, vou organizar de forma sistemática o que aprendi nesses três dias. Você verá por que o OpenClaw é dividido em três camadas, que problema cada camada resolve, como o Gateway gerencia o estado das sessões, como o Channel adapta plataformas diferentes, como o sistema de plugins Provider da camada LLM é projetado e, no final, como desenvolver um Channel e um Provider personalizados passo a passo.

Guia de baixo custo para usar ArkClaw: ArkClaw torna AI Agents realmente acessíveis

O OpenClaw, que ficou popular recentemente, é útil, mas a configuração pode afastar muita gente. O ArkClaw, da ByteDance Volcengine, reduz a barreira de entrada ao mínimo. Sem precisar lidar com servidor nem configuração de Token, você consegue em um clique um Agent de IA online 24 horas, capaz de controlar navegador, executar scripts e gerenciar calendário.

O ponto principal é que ele é barato de verdade: a mensalidade é de apenas 9,9 yuans; usando meu código de convite ZLKUK54M (registre-se aqui), fica por apenas 8,9 yuans. Se você é programador, também pode entrar direto no Coding Plan Pro e usar de graça.

Visão geral da arquitetura do OpenClaw: por que três camadas?

Quando tive o primeiro contato com o OpenClaw, fiquei com uma dúvida: por que criar uma divisão em camadas tão complexa? Não bastaria passar a mensagem do usuário direto para a IA?

Depois de estudar o código-fonte, entendi que um design monolítico até funciona em pequena escala. Mas o OpenClaw precisa dar suporte a múltiplas plataformas (WhatsApp, Telegram, Gmail), múltiplos modelos (Claude, GPT, modelos locais) e ainda gerenciar centenas ou milhares de sessões de usuários. Sem camadas, toda a lógica ficaria empilhada no mesmo lugar; mudar um ponto poderia afetar o sistema inteiro, tornando a manutenção praticamente inviável.

A filosofia de design das três camadas

O OpenClaw divide o sistema inteiro em três camadas, cada uma cuidando apenas do que lhe cabe:

Camada Gateway (centro de gerenciamento de sessões)

  • Gerencia todo o ciclo de vida das sessões de usuário
  • Fila e despacho de mensagens (quem vem antes, quem vem depois)
  • Autenticação e controle de permissões (quem pode usar)
  • Manutenção de conexões longas via WebSocket

Camada Channel (adaptador de plataforma)

  • Adapta formatos de mensagem de plataformas diferentes (WhatsApp e Telegram têm formatos diferentes)
  • Regras de roteamento de mensagens (DM ou grupo, precisa de @ para responder ou não)
  • Tratamento de eventos (receber mensagem, enviar mensagem, tratar erro)

Camada LLM (interface de modelos)

  • Interface Provider unificada (seja Claude ou GPT, a forma de chamada é consistente)
  • Chamada de ferramentas (Function Calling)
  • Processamento de respostas em streaming
  • Integração com servidores MCP
2026
Refatoração para plugins

O fluxo completo de uma mensagem

Um cenário concreto deixa isso bem claro. Quando você envia uma mensagem para o bot pelo WhatsApp, o processo inteiro é assim:

  1. Recebimento na camada Channel: o WhatsApp Channel recebe o webhook e padroniza a mensagem para o formato interno
  2. Decisão de roteamento: verifica se é DM ou grupo, se há @ para o bot e se o usuário tem permissão
  3. Despacho pelo Gateway: encontra (ou cria) a Session desse usuário e coloca a mensagem na fila
  4. Processamento pelo LLM: escolhe o Provider conforme a configuração (por exemplo, Anthropic) e envia o contexto da conversa
  5. Retorno da resposta: o LLM retorna o resultado → Gateway → Channel → o usuário recebe a resposta

O ponto mais inteligente desse design é que as camadas não interferem umas nas outras. Quer adicionar uma nova plataforma? Mexa só na camada Channel. Quer trocar o modelo? Mexa só na camada LLM. O Gateway não precisa mudar.

Camada Gateway: o núcleo do gerenciamento de sessões

Quando olhei o código do Gateway pela primeira vez, o que mais me confundiu foi o objeto Session. Cada usuário tem uma Session, mas o que exatamente ela guarda e como é gerenciada?

Ciclo de vida de uma Session

Pense no Gateway como um centro de triagem de entregas: cada usuário é um endereço de entrega, e a Session é o histórico de recebimento daquele endereço.

O que o objeto Session contém:

  • conversationHistory: histórico da conversa (as N mensagens mais recentes)
  • context: variáveis de contexto (configurações do usuário, dados temporários)
  • state: estado atual (idle, processing, waiting)
  • channelInfo: informações da plataforma de origem (de qual Channel veio)

Gerenciamento do ciclo de vida:

// Exemplo simplificado, mostrando a lógica central
class SessionManager {
  // Ao receber uma mensagem
  async handleMessage(userId, channelId, message) {
    // 1. Encontra a Session (cria se não existir)
    let session = this.getOrCreate(userId, channelId);

    // 2. Atualiza o histórico da conversa
    session.conversationHistory.push(message);

    // 3. Adiciona à fila de processamento
    await this.messageQueue.enqueue(session, message);

    // 4. Persiste (para evitar perda em caso de falha)
    await this.persist(session);
  }
}

Aqui está o ponto principal: o OpenClaw usa o modo de isolamento per-channel-peer. O que isso quer dizer? O mesmo usuário no WhatsApp e no Telegram tem duas Sessions independentes, sem interferência entre si. Esse design evita confusão de contexto: você pode discutir um problema técnico no WhatsApp e perguntar sobre o tempo no Telegram sem que uma conversa contamine a outra.

Estratégia de prioridade no despacho de mensagens

O Gateway não processa uma mensagem imediatamente após recebê-la; ele usa uma fila de despacho. Esse desenho resolve principalmente dois problemas:

Problema 1: controle de concorrência
Se 100 usuários enviam mensagens ao mesmo tempo e tudo vai direto para o LLM, a interface será sobrecarregada. A fila do Gateway pode limitar a vazão, por exemplo: “processar no máximo 10 requisições simultâneas”.

Problema 2: retry em caso de erro
E se a chamada ao LLM falhar? O Gateway tenta automaticamente 3 vezes, com intervalos crescentes (1 segundo, 2 segundos, 4 segundos), evitando que falhas momentâneas causem perda de mensagens.

// Lógica central da fila de mensagens
class MessageQueue {
  async enqueue(session, message) {
    // Verifica o número de tarefas concorrentes
    if (this.activeJobs >= this.maxConcurrency) {
      // Coloca na fila de espera
      this.waitingQueue.push({ session, message });
      return;
    }

    // Executa o processamento
    this.activeJobs++;
    try {
      await this.process(session, message);
    } catch (error) {
      // Lógica de retry
      await this.retryWithBackoff(session, message);
    } finally {
      this.activeJobs--;
      this.processNext(); // Processa o próximo
    }
  }
}

Armadilhas das conexões longas WebSocket

Se você pretende desenvolver aplicações com alta exigência de tempo real (como bots de atendimento ao cliente), o gerenciamento de conexões WebSocket é uma grande armadilha.

A abordagem do OpenClaw é:

  • Heartbeat: envia um ping a cada 30 segundos; se houver timeout, considera a conexão encerrada
  • Reconexão automática: depois da queda, reconecta com backoff exponencial (1 segundo, 2 segundos, 4 segundos… até 30 segundos)
  • Sincronização de estado: após reconectar, restaura automaticamente o estado da Session

Esses detalhes parecem pequenos, mas aumentam muito a estabilidade. Eu já escrevi um sistema parecido sem heartbeat; o resultado foi uma conexão zumbi que o programa não percebia, e as mensagens dos usuários simplesmente desapareciam.

Camada Channel: roteamento de mensagens entre múltiplas plataformas

A camada Channel é, para mim, a parte mais interessante. Ela resolve o problema central: se cada plataforma tem um formato de mensagem completamente diferente, como processar tudo de forma unificada?

O uso inteligente do padrão Adapter

A mensagem do WhatsApp é assim:

{
  "from": "1234567890",
  "body": "Olá",
  "type": "text"
}

A do Telegram é assim:

{
  "message": {
    "chat": {"id": 123},
    "text": "Olá"
  }
}

Se cada plataforma tivesse sua própria lógica completa, o código explodiria. O OpenClaw usa o clássico padrão Adapter: define uma interface Message padronizada, e cada Channel fica responsável por converter a mensagem da plataforma para esse formato.

// Formato padronizado de mensagem
interface StandardMessage {
  userId: string;      // ID de usuário unificado
  content: string;     // Conteúdo da mensagem
  timestamp: number;   // Timestamp
  metadata: any;       // Dados específicos da plataforma
}

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

A vantagem desse design é que o Gateway e a camada LLM não precisam se importar de qual plataforma a mensagem veio. Eles lidam apenas com StandardMessage.

Como as regras de roteamento funcionam

A camada Channel tem outra responsabilidade importante: decidir quais mensagens devem receber resposta e quais devem ser ignoradas.

O OpenClaw oferece dois tipos de regra de roteamento:

dmPolicy (política de conversa privada)

  • pairing: precisa parear antes de conversar (mais seguro)
  • allowlist: apenas usuários em lista de permissão podem usar
  • open: todos podem usar (bot público)
  • disabled: fecha conversas privadas

mentionGating (acionamento por @ em grupo)
Em grupos, o bot só responde quando recebe @, evitando inundar a conversa. A lógica de implementação é simples:

class TelegramChannel {
  shouldRespond(message): boolean {
    // Conversa privada responde diretamente
    if (message.chat.type === 'private') {
      return this.checkDmPolicy(message.from.id);
    }

    // Em grupo, verifica @
    if (message.chat.type === 'group') {
      const mentioned = message.entities?.some(
        e => e.type === 'mention' && e.user.id === this.botId
      );
      return mentioned;
    }

    return false;
  }
}

Quando desenvolvi um Channel para DingTalk, usei essa lógica como referência. A detecção de @ no DingTalk é um pouco diferente (usa o campo atUsers), mas a estrutura é a mesma.

O roteiro para desenvolver um Channel personalizado

Suponha que você queira integrar Discord. O fluxo geral é este:

  1. Criar a classe Channel: implementar a interface Channel
  2. Implementar os métodos obrigatórios:
    • start(): inicia o Channel (escuta webhook ou WebSocket)
    • sendMessage(): envia mensagem para a plataforma
    • adaptMessage(): converte o formato da mensagem
  3. Registrar no sistema: adicionar a configuração do Channel no arquivo de configuração
  4. Testar: expor o serviço local com ngrok e testar o webhook

Coloquei o exemplo completo de código na seção prática do fim do artigo, para você consultar diretamente.

Camada LLM: design plugável da interface de modelos

Em 2026, a camada LLM passou por uma grande refatoração: saiu de código hardcoded e virou um sistema de plugins. Essa mudança é realmente importante, porque determina diretamente quantos tipos de modelos o OpenClaw consegue suportar.

Sistema de plugins Provider

O design anterior era assim (pseudocódigo):

// Design antigo: hardcoded
if (config.provider === 'anthropic') {
  return new AnthropicClient();
} else if (config.provider === 'openai') {
  return new OpenAIClient();
}

O problema é que, a cada novo modelo, era preciso alterar esse if-else, e o código ficava cada vez mais inchado.

O novo design introduz a interface Provider:

// Definição da interface Provider
interface LLMProvider {
  name: string;  // 'anthropic', 'openai', 'ollama'

  // Envia mensagens e retorna resposta em streaming
  chat(messages: Message[], options: ChatOptions): AsyncIterator<string>;

  // Suporte a chamada de ferramentas
  supportTools(): boolean;

  // Configuração de inicialização
  initialize(config: ProviderConfig): void;
}

Qualquer Provider que implemente essa interface pode ser conectado ao sistema. Na inicialização, o sistema faz varredura e registro automático:

// Mecanismo de registro de 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);
  }
}

// Descoberta e registro automáticos
const registry = new ProviderRegistry();
registry.register(new AnthropicProvider());
registry.register(new OpenAIProvider());
registry.register(new OllamaProvider());

A vantagem é clara: quer usar um modelo novo? Escreva uma classe que implemente Provider, registre-a e pronto. Não é preciso alterar o código central.

Diferenças entre Providers populares

Embora a interface seja unificada, os detalhes de implementação variam bastante entre Providers. Eu já tropecei em alguns pontos, então vale compartilhar:

Anthropic Provider (Claude)

  • Dá suporte nativo a resposta em streaming (stream: true)
  • O formato de Tool Use é específico (precisa ser empacotado em um array tools)
  • A janela de contexto é grande (Claude 3.5 pode chegar a 200k tokens)

OpenAI Provider (ChatGPT)

  • Function Calling e Tool Use são duas APIs diferentes (versões antigas usam functions; versões novas usam tools)
  • A resposta em streaming retorna fragmentos delta, que precisam ser concatenados manualmente
  • Os limites de taxa são rígidos (é preciso controlar RPM e TPM)

Ollama Provider (modelos locais)

  • Não exige chave de API; chama diretamente o serviço local via HTTP
  • O desempenho depende muito do hardware (inferência em CPU é lenta; GPU é necessária)
  • O suporte a tool varia entre modelos (llama3 suporta, mas qwen pode não suportar)

Eu já tentei rodar Llama3 localmente com Ollama e descobri que o formato de chamada de ferramentas era completamente diferente do Claude. Levei meio dia até adaptar tudo com sucesso.

Entendendo o mecanismo de Tool Use

Tool Use (chamada de ferramentas) é uma das funções centrais da camada LLM. Em termos simples, é permitir que a IA “chame funções”.

Por exemplo, se você pergunta “Que horas são agora em Pequim?”, a IA vai:

  1. Julgar que precisa chamar a ferramenta get_current_time
  2. Retornar uma solicitação de chamada de ferramenta: {"name": "get_current_time", "args": {"city": "Pequim"}}
  3. O OpenClaw executa a ferramenta e retorna o resultado: {"time": "2026-02-05 20:30"}
  4. A IA gera a resposta com base no resultado: “Agora são 20h30 em Pequim”

O mecanismo de registro de ferramentas do OpenClaw funciona assim:

// Definição de ferramentas
const tools = [
  {
    name: 'get_current_time',
    description: 'Obter a hora atual de uma cidade especificada',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string', description: 'Nome da cidade' }
      },
      required: ['city']
    }
  }
];

// Execução de ferramenta
async function executeTool(toolName, args) {
  const handlers = {
    'get_current_time': (args) => {
      // A implementação real pode chamar uma API
      return { time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) };
    }
  };

  return handlers[toolName](args);
}

Aviso importante: a execução de ferramentas precisa de isolamento em sandbox. Caso contrário, se a IA pedir para executar rm -rf /, você terá um grande problema. O OpenClaw tem controle de permissões embutido e permite chamar apenas ferramentas predefinidas.

Prática: estendendo a arquitetura do OpenClaw

Depois da teoria, vamos para algo prático. Vou compartilhar dois exemplos completos: desenvolver um Discord Channel e um Kimi Provider.

Desenvolvendo um Channel personalizado: integração com Discord

O mecanismo de mensagens do Discord é diferente do WhatsApp. Ele recebe mensagens por WebSocket e envia mensagens por REST API.

Primeiro passo: implementar a interface Channel

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

class DiscordChannel implements Channel {
  private client: Client;
  private gateway: Gateway; // Instância Gateway do OpenClaw

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

    // Escuta eventos de mensagem
    this.client.on('messageCreate', async (msg) => {
      if (msg.author.bot) return; // Ignora mensagens de bots

      // Converte para o formato padrão
      const standardMsg = this.adaptMessage(msg);

      // Entrega ao Gateway para processamento
      const response = await this.gateway.handleMessage(standardMsg);

      // Envia a resposta
      await msg.reply(response.content);
    });

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

Segundo passo: registrar no OpenClaw

Adicione em config.json:

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

Registre no script de inicialização:

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

Terceiro passo: testar

  1. Vá até a plataforma de desenvolvedores do Discord, crie um Bot e obtenha o Token
  2. Convide o Bot para o seu servidor
  3. Inicie o OpenClaw e envie uma mensagem privada para o Bot
  4. Verifique os logs para confirmar que o fluxo de mensagens está normal

Uma armadilha que encontrei na prática: o sistema de permissões do Discord é complexo. Garanta que o Bot tenha as permissões Send Messages e Read Message History; caso contrário, ele não conseguirá enviar mensagens.

Desenvolvendo um Provider personalizado: integração com Kimi

A API do Kimi (modelo da Moonshot AI) é bem parecida com a da OpenAI, mas alguns detalhes são diferentes.

Implementação do 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
      })
    });

    // Processa a resposta em 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 ainda não oferece suporte a Function Calling
  }
}

Registrar o Provider:

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

// Configuração de uso
const config = {
  llm: {
    provider: 'kimi',
    apiKey: process.env.KIMI_API_KEY,
    model: 'moonshot-v1-32k'
  }
};

Anotações de armadilhas:

  • O formato de resposta em streaming do Kimi é idêntico ao da OpenAI, então dá para usar como referência direta
  • O tratamento de erro é diferente; timeouts não retornam código de erro padrão e exigem tratamento especial
  • Atualmente não há suporte a Function Calling; se sua aplicação depende de chamada de ferramentas, Kimi não será adequado

Práticas de otimização de desempenho

Fazer rodar é apenas o primeiro passo; otimização de desempenho é a parte pesada. Aqui estão alguns pontos que já usei:

Otimização do cache de Session
A Session padrão fica em memória e se perde ao reiniciar. Você pode integrar Redis:

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)); // expira em 1 hora
  }
}

Ajuste da fila de mensagens
Em cenários de alta concorrência, a fila em memória pode não ser suficiente. Você pode trocar por Bull (fila de tarefas baseada em Redis):

import Queue from 'bull';

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

messageQueue.process(10, async (job) => { // no máximo 10 concorrentes
  const { session, message } = job.data;
  return await gateway.processMessage(session, message);
});

Controle do número de conexões concorrentes
APIs de LLM normalmente têm rate limit (por exemplo, 60 RPM na OpenAI). Você pode usar a biblioteca p-limit para controlar concorrência:

import pLimit from 'p-limit';

const limit = pLimit(10); // no máximo 10 requisições concorrentes

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

await Promise.all(tasks);

Comparação do efeito da otimização (dados que medi):

  • Antes da otimização: 100 requisições concorrentes, tempo médio de resposta de 8 segundos, taxa de falha de 15%
  • Depois da otimização: 100 requisições concorrentes, tempo médio de resposta de 3 segundos, taxa de falha <1%
2,6x
Melhoria de desempenho

Resumo

Do Gateway ao Channel e depois ao LLM, o design de arquitetura em três camadas do OpenClaw é realmente claro. Cada camada cuida apenas da própria responsabilidade, os limites são bem definidos e a extensão fica especialmente conveniente.

Depois de entender essa arquitetura, desenvolver novas funcionalidades ficou muito mais fácil para mim. Quer adicionar uma nova plataforma? Escreva um Channel Adapter. Quer trocar de modelo? Implemente uma interface Provider. Quer otimizar desempenho? Identifique em qual camada está o gargalo e ajuste de forma direcionada.

Se você também pretende personalizar profundamente o OpenClaw, recomendo primeiro clonar o código-fonte e ler o código seguindo a lógica deste artigo. Em especial, o gerenciamento de Session no Gateway, a lógica de roteamento do Channel e o mecanismo de registro do Provider são o núcleo do núcleo.

Quando você entende essas partes, deixa de ser alguém que apenas “copia configurações da documentação” e passa a dominar de verdade o sistema, podendo expandi-lo e otimizá-lo com liberdade.

O próximo passo pode ser desenvolver um Channel personalizado simples (por exemplo, WeCom ou Feishu) e colocar a mão na massa. A compreensão fica muito mais profunda. A comunidade open source do OpenClaw também é bastante ativa; se encontrar problemas, você pode conversar pelo GitHub Issues.

Próximas leituras

Fluxo completo para desenvolver um Channel personalizado no OpenClaw

Como desenvolver do zero e integrar um Channel personalizado ao sistema OpenClaw

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Entenda a especificação da interface Channel

    A interface Channel define os métodos que um adaptador de plataforma precisa implementar:

    Métodos principais:
    • start(): inicia o Channel e escuta mensagens da plataforma (webhook ou WebSocket)
    • sendMessage(userId, content): envia uma mensagem para a plataforma
    • adaptMessage(rawMessage): converte a mensagem da plataforma para o formato StandardMessage

    Formato StandardMessage:
    • userId: string (ID de usuário unificado)
    • channelId: string (identificador do Channel)
    • content: string (conteúdo da mensagem)
    • timestamp: number (timestamp)
    • metadata: any (dados específicos da plataforma)

    Métodos de controle de roteamento:
    • shouldRespond(message): decide se a mensagem deve receber resposta
    • checkDmPolicy(userId): verifica a política de conversa privada
    • checkMention(message): verifica acionamento por @ em grupo

    Implementações de referência: src/channels/whatsapp.ts ou src/channels/telegram.ts
  2. 2

    Step 2: Crie a classe Channel e implemente a interface

    Crie um novo arquivo no diretório src/channels/ (por exemplo, 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;
    // Inicializa o cliente Discord
    this.client = new Client(&#123; intents: [...] &#125;);

    // Escuta eventos de mensagem
    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;


    Pontos-chave:
    • A inicialização do SDK da plataforma fica no método start()
    • O recebimento de mensagens deve ser convertido para o formato StandardMessage
    • O envio de mensagens precisa lidar com chamadas de API específicas da plataforma
    • Tratamento de erros e registro de logs são indispensáveis
  3. 3

    Step 3: Implemente regras de roteamento e controle de permissões

    Implemente a lógica de filtragem de mensagens conforme as necessidades do negócio:

    Implementação de dmPolicy:
    • modo pairing: mantém uma lista de usuários pareados e responde apenas a usuários da lista
    • modo allowlist: verifica se o ID do usuário está na lista de permissões
    • modo open: responde a todos os usuários
    • modo disabled: recusa todas as conversas privadas

    typescript
    shouldRespond(message): boolean &#123;
    // Política para conversas privadas
    if (message.metadata.channelType === 'DM') &#123;
    return this.checkDmPolicy(message.userId);
    &#125;

    // Verificação de @ em grupo
    if (message.metadata.channelType === 'GROUP') &#123;
    return this.checkMention(message);
    &#125;

    return false;
    &#125;


    Implementação de mentionGating (acionamento em grupo):
    • Verifique se a mensagem contém @ para o bot
    • O formato de mention varia entre plataformas (Discord usa <@botId>, Telegram usa @username)
    • Retorne true quando deve responder e false quando deve ignorar
  4. 4

    Step 4: Configure o arquivo e registre o Channel

    1. Adicione a configuração do Channel em config.json:

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


    2. Registre o Channel no script de inicialização:

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

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

    // Registra no Gateway
    gateway.registerChannel('discord', discordChannel);

    // Inicia o Channel
    await discordChannel.start();


    3. Configure variáveis de ambiente:
    • Coloque informações sensíveis (Token, chaves) no arquivo .env
    • Carregue com a biblioteca dotenv: require('dotenv').config()
  5. 5

    Step 5: Teste e depure

    Fluxo de teste:

    1. Teste local de desenvolvimento:
    • Use ngrok para expor o serviço local (necessário para plataformas baseadas em webhook)
    • Configure o webhook da plataforma para apontar para a URL do ngrok
    • Inicie o OpenClaw e verifique os logs

    2. Verificação do fluxo de mensagens:
    • Envie uma mensagem de teste e confira se ela aciona o listener de mensagens em start()
    • Confirme se adaptMessage() converte corretamente
    • Verifique se Gateway.handleMessage() foi chamado
    • Confira se sendMessage() envia a resposta com sucesso

    3. Teste das regras de roteamento:
    • Teste políticas de conversa privada (pairing/allowlist/open)
    • Teste acionamento por @ em grupos (com @ e sem @)
    • Teste listas de permissão e bloqueio

    4. Testes de exceção:
    • Simule timeout de rede
    • Simule expiração de Token
    • Simule formato de mensagem inválido

    Dicas de depuração:
    • Adicione console.log() em pontos-chave ou use a biblioteca debug
    • Confira os logs do Gateway para confirmar se a mensagem chegou
    • Use ferramentas de teste fornecidas pela plataforma (como Discord Bot Dashboard)
    • Ative o modo de logs detalhados: DEBUG=openclaw:* openclaw gateway (comum em instalação global; ao desenvolver localmente a partir do repositório oficial, siga a documentação do repositório)
  6. 6

    Step 6: Otimize desempenho e prepare a publicação

    Checklist de otimização:

    1. Gerenciamento de conexão:
    • Implemente heartbeat (para evitar conexões zumbi)
    • Adicione reconexão automática (backoff exponencial)
    • Trate encerramento gracioso (sinal SIGTERM)

    2. Tratamento de erros:
    • Capture todas as exceções possíveis
    • Implemente retry de mensagens (até 3 tentativas)
    • Registre logs de erro em arquivo ou sistema de monitoramento

    3. Otimização de desempenho:
    • Processamento em lote de mensagens (reduz chamadas de API)
    • Uso de pool de conexões (banco de dados/Redis)
    • Controle de rate limit (evita acionar limites da plataforma)

    4. Monitoramento e logs:
    • Registre o tempo de processamento de mensagens
    • Acompanhe taxa de sucesso e falha
    • Configure limiar de alerta (falha &gt;5%)

    Checklist antes de ir para produção:
    • Teste de carga (simule 100+ usuários concorrentes)
    • Detecção de vazamento de memória (teste de longa duração)
    • Plano de backup e rollback de configuração
    • Documentação operacional (iniciar, parar, diagnosticar falhas)

FAQ

Por que usar isolamento de sessão per-channel-peer em vez de compartilhar uma única Session entre todas as plataformas?
A principal vantagem do modo per-channel-peer é evitar confusão de contexto e melhorar a segurança:

Isolamento de contexto: se o mesmo usuário discute um problema técnico no WhatsApp e pergunta a previsão do tempo no Telegram, uma Session compartilhada misturaria as duas conversas. A IA levaria o contexto técnico para a consulta de clima, gerando respostas irrelevantes.

Isolamento de segurança: plataformas diferentes têm mecanismos de validação de permissão diferentes. Compartilhar Session pode abrir brechas de permissão. Por exemplo, o usuário passou por verificação de identidade no WhatsApp, mas a conta do Telegram pode ser falsa; separar as sessões é mais seguro.

Consideração de desempenho: a Session de cada Channel é armazenada de forma independente, permitindo processar mensagens de plataformas diferentes em paralelo sem bloqueios mútuos.

Se você realmente precisa compartilhar contexto entre plataformas, implemente a associação de contas na camada de aplicação, não mescle tudo na camada de Session.
Ao desenvolver um Provider personalizado, como lidar com modelos que não dão suporte a resposta em streaming?
A interface Provider do OpenClaw exige retornar um AsyncIterator, mas algumas APIs de modelo não dão suporte a streaming. Soluções:

Solução 1: empacotar como pseudo-streaming (recomendado)
async *chat(messages) &#123;
const response = await fetch(apiUrl, &#123; ... &#125;); // requisição sem streaming
const result = await response.json();
yield result.content; // retorna todo o conteúdo de uma vez
&#125;

Solução 2: simular streaming por blocos
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); // simula atraso
&#125;

A solução 1 é simples e direta; a experiência do usuário é "esperar e receber a resposta completa de uma vez". A solução 2 pode simular efeito de digitação, mas aumenta a complexidade. Escolha conforme a necessidade real.
O que acontece quando a fila de mensagens do Gateway fica cheia? Como evitar perda de mensagens?
Estratégias para quando a fila de mensagens fica cheia:

Comportamento padrão: a fila em memória do OpenClaw tem limite de capacidade (padrão de 1000 mensagens). Ao exceder o limite, novas mensagens são recusadas e o usuário recebe o erro "sistema ocupado".

Como evitar perda de mensagens:

1. Fila persistente (recomendado):
Use Bull, RabbitMQ ou outra fila de mensagens persistente; assim, mesmo se o serviço reiniciar, as mensagens não se perdem.

2. Aumentar a capacidade da fila:
Configure maxQueueSize: 5000 em config.json, mas observe o consumo de memória.

3. Rate limit + aviso:
Implemente rate limit na camada Channel. Quando o limite for excedido, avise o usuário com "tente novamente mais tarde", evitando acúmulo de mensagens.

4. Fila com prioridade:
Mensagens de usuários importantes (VIP) são processadas primeiro; usuários comuns aguardam na fila.

Em produção, a combinação Bull + Redis é recomendada porque oferece persistência e suporta alta concorrência.
Como depurar o fluxo de mensagens entre Gateway e Channel? Parece que a mensagem foi enviada, mas não há resposta.
Um método sistemático para depurar o fluxo de mensagens:

1. Ative logs detalhados:
DEBUG=openclaw:* openclaw gateway
Após instalação global via npm, esse é um modo comum de depurar em primeiro plano; se você desenvolve localmente a partir do repositório oficial, siga o README/scripts desse repositório e não presuma npm start.
Isso imprime logs de cada módulo, incluindo recebimento, conversão, processamento e envio de mensagens.

2. Verifique os pontos-chave:
• Channel.adaptMessage(): imprima o StandardMessage convertido e confirme o formato
• Gateway.handleMessage(): imprima a mensagem recebida e o estado da Session
• Provider.chat(): imprima o contexto enviado ao LLM
• Channel.sendMessage(): imprima o conteúdo final enviado

3. Use depuração com breakpoints:
Configure launch.json no VS Code, adicione breakpoints e depure passo a passo.

4. Verifique problemas comuns:
• shouldRespond() retorna false: a mensagem foi filtrada pelas regras de roteamento
• Session não encontrada: userId ou channelId não correspondem
• Falha na chamada ao LLM: verifique chave de API, rede e rate limit

5. Use ferramentas de teste:
Escreva testes unitários que simulem entrada de mensagens e validem a saída de cada etapa.

Recomendo usar pino-pretty no ambiente de desenvolvimento para embelezar logs; em produção, use logs estruturados (formato JSON) para facilitar análises futuras.
Como o recurso Tool Use do Provider impede que a IA execute operações perigosas, como apagar arquivos?
Estratégias de proteção para Tool Use:

1. Mecanismo de allowlist (o mais importante):
Registre apenas ferramentas seguras. Proíba ferramentas perigosas, como operações no sistema de arquivos e requisições de rede.

const safeTool = &#123;
name: 'get_weather',
description: 'Obter clima',
handler: getWeatherData // operação segura de leitura
&#125;;

2. Validação de parâmetros:
Valide rigorosamente os parâmetros das ferramentas e recuse entradas anormais.

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

3. Execução em sandbox (avançado):
Use vm2 ou isolated-vm para executar código de ferramenta em ambiente isolado.

4. Níveis de permissão:
Usuários diferentes têm permissões diferentes de chamada de ferramentas. Administradores podem executar ferramentas avançadas; usuários comuns usam apenas ferramentas básicas.

5. Logs de auditoria:
Registre todas as chamadas de ferramentas (quem, quando, qual ferramenta, quais parâmetros) para rastreabilidade e auditoria.

Por padrão, o OpenClaw só permite chamar ferramentas predefinidas e não oferece execução dinâmica de código, o que já evita a maior parte dos riscos. Se você quiser ampliar as ferramentas, avalie a segurança com cuidado.
Quando vários Channels recebem mensagens do mesmo usuário ao mesmo tempo, como o Gateway evita conflitos de concorrência?
O mecanismo de controle de concorrência do Gateway:

Mecanismo de lock de Session:
Cada Session é bloqueada durante o processamento da mensagem. Mensagens da mesma Session são processadas em série; mensagens de Sessions diferentes são processadas em paralelo.

Implementação em pseudocódigo:
async handleMessage(session, message) &#123;
const lock = await this.acquireLock(session.id);
try &#123;
// processa a mensagem
await this.processMessage(session, message);
&#125; finally &#123;
await lock.release();
&#125;
&#125;

Exemplo prático:
Se o usuário envia mensagens para o bot no WhatsApp e no Telegram ao mesmo tempo, o isolamento per-channel-peer faz com que sejam duas Sessions independentes. Elas podem ser processadas em paralelo sem conflito.

Se forem mensagens concorrentes no mesmo Channel (por exemplo, o usuário envia três mensagens rapidamente), elas entram na fila e são processadas na ordem FIFO.

Tratamento em implantação distribuída:
Se o OpenClaw for implantado em múltiplas instâncias, use Redis para implementar lock distribuído (algoritmo redlock):

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

Assim, mesmo com várias instâncias, apenas uma delas processará a mesma Session por vez.

19 min de leitura · Publicado em: 5 fev 2026 · Atualizado em: 14 jul 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog