DisPatch: um Aplicativo de Chat Self-Hosted para Agentes de IA Locais
- Categoria
- IA e LLM Local
- Publicado em
- 11 julho 2026
- Atualizado em
- 11 julho 2026
- Por
- Jacob Lloyd — escrito com ajuda de IA, depois do projeto
- Tempo de leitura
- 13 min de leitura
Em termos simples: O DisPatch é três coisas em uma, hospedadas inteiramente no seu próprio computador: um aplicativo de chat para conversar com assistentes de IA que também vivem ali, um servidor de arquivos e um terminal — disponíveis em todos os seus dispositivos, o tempo todo, sem nada indo para um serviço na nuvem. O servidor de arquivos move arquivos de qualquer dispositivo para o computador onde os assistentes rodam, para que eles possam trabalhar com o que você enviou. Em dispositivos compartilhados, um subconjunto seguro de assistentes fica disponível sem PIN; digitar o PIN libera os de poder total.
O DisPatch é o aplicativo que criei para parar de rodar uma instalação do Rocket.Chat com 30 containers só para trocar mensagens com meus próprios agentes de IA. Ele acabou virando três coisas em uma — um aplicativo de chat de LLM hospedado localmente, um servidor de arquivos e um terminal — disponível em todos os dispositivos que eu tenho, o tempo todo. É um zip de ~720KB, é gratuito, e é o único front-end de chat que eu uso hoje.
Resumo
- O que é: Três coisas em uma, self-hosted: um chat web para um time de agentes de IA locais (um gateway OpenClaw), um servidor de arquivos com arrastar e soltar, e um terminal de verdade — em todos os dispositivos da sua rede local ou da sua rede Tailscale, o tempo todo.
- O que custa: $0. Sem assinatura de SaaS, sem conta.
- O que você precisa: Linux, Python 3.12+ e
uv. Um gateway OpenClaw se você quiser respostas digitadas — todo o resto funciona sem ele. - O que você ganha: threads por bot, markdown em streaming, um Servidor de Arquivos com arrastar e soltar, um terminal de agente de codificação embutido no app, um "Modo Seguro" bloqueável por PIN, uma API de push para tarefas cron, e 187 testes que passam direto de dentro do zip.
O que você ganha
Antes de qualquer papo sobre configuração, é assim que ele se parece fazendo seu trabalho. As capturas de tela são de uma cópia em sandbox com dados fictícios — ninguém precisa ver os logs reais dos meus bots.



O que ele realmente faz
- Threads por bot, em todo lugar. Abra no celular no meio de uma conversa e é a mesma thread, o mesmo histórico, sem dança de relogin.
- Um Servidor de Arquivos — arraste e solte arquivos entre qualquer dispositivo e a máquina. O recurso que mais uso; ele ganha uma seção própria mais abaixo.
- Um terminal (Terminex) — um shell de verdade no navegador rodando um agente de codificação. Também tem sua própria seção mais abaixo.
- Respostas em markdown com streaming, com código com destaque de sintaxe (botão de copiar incluído), imagens/vídeos com lightbox de zoom, instalável como PWA.
- Um painel "trabalhando" ao vivo. Clique nos pontinhos de digitação para ver, em tempo real, as chamadas de ferramentas e o raciocínio do modelo — ele acompanha a transcrição do agente ao vivo, não é um spinner de mentirinha.
- Bloqueio em duas camadas. Um PIN separa os agentes de poder total de um subconjunto seguro, só de informação geral — detalhes na seção de bloqueio em duas camadas mais abaixo.
- Uma API de push —
POST /api/inject— para que uma tarefa cron ou outro agente possa colocar uma mensagem (ou uma imagem) em uma thread. É assim que um resumo diário chega no seu celular sem você pedir. - Geração de imagem local dentro da thread. Peça uma imagem a um agente e ele pode chamar uma instância local do ComfyUI; o resultado chega no chat como qualquer outra mídia, com lightbox e tudo. Sem API de imagem, sem upload para ninguém.
- Opcional, desligado por padrão: um painel de ComfyUI para iniciar ou parar esse serviço de geração de imagem direto do chat.
O Servidor de Arquivos
De tudo que tem no DisPatch, o Servidor de Arquivos é a peça que mais uso. É uma área de arrastar e soltar arquivos, servida pela mesma máquina: abra o DisPatch em qualquer dispositivo, solte um arquivo nele, e o arquivo já está no servidor — agrupado por dia, com miniaturas para imagens e vídeos.

O que torna isso mais do que uma conveniência é onde os arquivos pousam: na mesma máquina onde os agentes vivem. Uma foto enviada do meu celular pode ser lida no disco por qualquer agente segundos depois — descrita, arquivada, colocada em um site, alimentada nas ferramentas de imagem. É a ponte entre "um arquivo em qualquer dispositivo que eu esteja segurando" e "um arquivo com o qual meus agentes podem trabalhar", sem nenhum drive na nuvem no meio e sem cabos.
As partes menos glamorosas estão cobertas: os uploads têm o tamanho verificado antes de serem colocados em buffer (um celular não consegue estourar a memória da máquina), existe um limite geral de armazenamento, as gravações são atômicas para que um upload interrompido nunca deixe um arquivo pela metade, e o armazenamento de arquivos é espelhado no mesmo esquema de backup rotativo do histórico de chat. Uma flag de configuração também pode transformar tudo isso em uma caixa de entrada só de upload, de mão única — os dispositivos podem adicionar arquivos, mas não navegar por eles — para configurações em que mais gente tem acesso ao app do que deveria poder ler o arquivo completo.
Terminex: um terminal dentro do chat
O outro recurso que se paga todo santo dia: o DisPatch consegue colocar um terminal de verdade no navegador. Ele aparece na barra de bots como qualquer outro bot, mas abri-lo te conecta a um PTY do lado do servidor, transmitido por WebSocket para dentro do xterm.js — uma sessão de shell de verdade na máquina, não uma simulação.

O meu abre direto no Reasonix, o agente de codificação no estilo Claude Code que rodo no DeepSeek — a configuração que a página central de assistentes chama de Terminex. Isso transforma uma aba de chat em um console completo de agente de codificação: entregue uma tarefa a ele, observe-o trabalhar, a partir de qualquer dispositivo que consiga abrir o aplicativo. Junto com o Servidor de Arquivos, dá para operar a máquina inteira a partir de um celular — solte um arquivo de onde você estiver, abra o terminal, diga ao agente o que fazer com ele.
- Só no modo desbloqueado. O terminal não existe no Modo Seguro, e fica completamente desligado a menos que você o habilite (
LOCAL_CHAT_TERMINAL). - A sessão fica só na memória. Iniciar / Reiniciar / Parar pelo app; reiniciar o servidor encerra a sessão. Não há nenhum shell persistente escondido em segundo plano.
Por que ele existe
Ele substituiu uma implantação completa do Rocket.Chat — um banco de dados e uma pilha de containers só para uma pessoa conversar com alguns modelos locais. Funcionava, mas é muita infraestrutura para "mandar texto, receber texto de volta, sincronizar em todo lugar".
E a resposta mais profunda para "por que sequer hospedar um app de chat em casa": uma vez que seus modelos rodam localmente, a interface de chat é a última peça ainda te tentando na direção de um serviço na nuvem. Hospedá-la você mesmo significa que seus prompts, a saída dos seus agentes e as mensagens da sua família nunca saem de casa, e o ciclo inteiro — interface, gateway, modelos, geração de imagem — continua funcionando com a internet desligada. Se você já se comprometeu com LLMs locais, um front-end de chat hospedado é o único elemento que destoa.
Uma escolha de design que vale destacar: o DisPatch não tem etapa de build, de propósito. O frontend é JavaScript puro (módulos ES) e CSS/HTML simples. Sem bundler, sem npm run build. Meu requisito real desde o início era "fácil para um agente de IA modificar depois". Editar um arquivo JS, atualizar a página, pronto. Eu tomaria a mesma decisão de novo.
A stack
| Camada | O que é |
|---|---|
| Backend | Python 3.12+, FastAPI + uvicorn, aiosqlite (um único arquivo SQLite, modo WAL), Pillow, gerenciado com uv |
| Tamanho do backend | ~7.200 linhas em backend/app/ (só o main.py já tem ~3.600), mais ~2.700 linhas de testes |
| Frontend | JS puro sem build + HTML/CSS — ~4.900 linhas de JS, ~2.100 de CSS/HTML/service worker |
| Bibliotecas embutidas | marked, highlight.js, DOMPurify, xterm.js — nenhuma chamada a CDN |
| Dados | Fora do código, em ~/.local/share/local-chat/: BD, config.yaml, security.yaml, media/, files/, backups rotacionados do BD |
Configurando
Resumido a partir do README-SETUP.md dentro do zip (caixa de download abaixo).
unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh
Isso o inicia em http://0.0.0.0:8765 e imprime as URLs da rede local e do Tailscale, para você tocar em uma delas pelo celular. A partir daí:
- Edite
~/.local/share/local-chat/config.yaml(criado automaticamente, documentado emconfig.yaml.example) para que osids dos bots correspondam aos ids dos seus agentes OpenClaw. Ele recarrega a quente. - Opcional:
./install.sh --systemdgera uma unidade systemd de usuário. - Opcional: defina um PIN pelo ícone de engrenagem → Security. O
security.yaml.exampledocumenta o arquivo, incluindo oapi_tokenpara quem for acessar/api/injectde fora da máquina. - Ajustes de ambiente:
LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY, além deLOCAL_CHAT_COMFY=0/LOCAL_CHAT_TERMINAL=0para manter os painéis opcionais desligados.
A própria CLI openclaw não vem incluída — o DisPatch espera que um gateway OpenClaw já esteja rodando em algum lugar acessível. Ainda não tem gateway? O app inicia mesmo assim, avisa nos logs, e tudo funciona, exceto as respostas digitadas: threads, a API de inject, o servidor de arquivos, PIN e Modo Seguro. Foi assim que fiz o teste de fumaça deste zip — descompactação nova, uv sync, porta livre, sem gateway — e a suíte de testes empacotada rodou 187 passed in 8.4s.
O que realmente acontece quando você digita uma mensagem
Você digita algo e aperta enviar. Este é o caminho que isso percorre:
Esse é o caminho feliz. A engenharia de verdade foi nos caminhos infelizes: a CLI expira, trava, ou um subagente responde dez minutos depois. Quatro fontes independentes — o observador ao vivo, o payload JSON da CLI, um reconciliador pós-turno que reescaneia a transcrição, e um "seguidor" que continua acompanhando por mais ~30 minutos — alimentam um único funil de entrega com deduplicação por turno (texto canonicalizado mais impressão digital da mídia). Qualquer um desses caminhos pode falhar e a resposta ainda assim chega exatamente uma vez. Mesmo em um erro de CLI, o reconciliador roda primeiro, porque o modelo muitas vezes já gravou sua resposta na transcrição antes de morrer.
O bloqueio em duas camadas: modo normal vs. Modo Seguro
A distinção importa porque o lado desbloqueado não é um brinquedo. Esses agentes conseguem realmente fazer coisas: editar código-fonte, publicar mudanças no meu site em produção, rodar ferramentas, gerenciar a máquina onde vivem. O lado bloqueado é só informação geral — ainda genuinamente útil, dá para fazer perguntas, bater papo e receber ajuda de verdade — mas deliberadamente cercado, longe de todo esse poder. O motivo é simples: tenho um filho de três anos, e o celular da família nem sempre está nas mãos de um adulto. Imagine o que uma criança de três anos conseguiria fazer com acesso total pelo meu celular — o site inteiro viraria vídeos da Patrulha Canina. Então o Modo Seguro é uma trava para crianças, aplicada no servidor. Um pequeno toque de qualidade de vida: a tela inicial bloqueada agora mostra os mesmos avatares de bot do lado desbloqueado, em vez dos antigos placeholders em bloco de letras. Veja o que acontece quando chega uma requisição sem PIN ainda digitado:
Se você se trancar para fora, existe um arquivo de código de recuperação mais um YAML editável, não um chamado de suporte. E se o security.yaml corromper no meio de uma gravação, o modo de falha é "continua bloqueado", não "serve tudo para qualquer um na rede local". Essa foi uma decisão deliberada.
Pegadinhas
- O campo da API é
content, nãotext.POST /api/injectespera{"bot_id": "...", "content": "..."}. O erro de integração mais comum de todos, incluindo alguns que eu mesmo cometi. - Não passe mensagens longas como argumento de linha de comando. O kernel limita o argv a 128KB (
MAX_ARG_STRLEN); uma resposta longa derruba o spawn com E2BIG. O código usa--message-file <tmpfile>— não "simplifique" isso de volta para um argumento. - Os ids de bot diferenciam maiúsculas de minúsculas em um ponto específico. O gateway do OpenClaw transforma as chaves de sessão em minúsculas no seu índice, mas seus ids têm caixa mista (
Doxy,DS_Brain). Uma busca sensível a maiúsculas e minúsculas mata silenciosamente o painel "trabalhando" e o seguidor de respostas tardias, enquanto tudo o mais continua funcionando — difícil de perceber. Primeiro tenta correspondência exata, depois cai para minúsculas. - A CLI só retorna o bloco final. Qualquer coisa que um agente diga entre chamadas de ferramentas existe só na sua transcrição, não no valor de retorno da CLI — daí o observador e o reconciliador, e o tratamento especial para o "aqui está o que encontrei até agora" de um agente enquanto ele espera por um subagente.
- Sequestro de WebSocket entre sites é bloqueado por uma verificação de Origin. Ambas as rotas de WS rejeitam um handshake cujo host de
Originnão bate com oHost; clientes que não são navegadores não enviam Origin e passam sem problema. - SVGs deliberadamente não podem ser servidos como mídia. Um navegador trata SVG inline como script da mesma origem — servir um de volta como imagem é uma brecha de XSS armazenado. As respostas de mídia recebem, em vez disso, um CSP de sandbox e nosniff.
- Atualizações de service worker deixam abas antigas presas no JS antigo. Parece exatamente uma regressão até você perceber que a aba nunca pegou o novo worker. Resolvido com um recarregamento automático no
controllerchange.
Replicando a ideia
Mesmo que você nunca rode o meu zip, a arquitetura vale a pena copiar. O todo é feito de quatro peças independentes, cada uma substituível:
- Um backend de chat que é dono do histórico: um servidor web pequeno (aqui, FastAPI + um único arquivo SQLite em modo WAL) com um WebSocket para atualizações ao vivo e um endpoint HTTP simples para empurrar mensagens para dentro. É essa a substituição inteira do "banco de dados e pilha de containers".
- Um gateway de agentes que o backend chama por subprocesso ou por HTTP — aqui, o OpenClaw, mas qualquer coisa que aceite "id de sessão + mensagem entra, texto sai" funciona. Mantê-lo como um processo separado significa que o app de chat sobrevive se a stack de agentes travar, for atualizada ou for trocada por completo.
- Servidores de modelo locais por trás do gateway (LM Studio, vLLM, Ollama — problema do gateway, não do app de chat).
- Serviços de mídia opcionais, como um ComfyUI local para geração de imagem, que os agentes chamam como ferramentas e cuja saída o backend simplesmente armazena como mídia.
As lições que valem para qualquer stack: aplique seu modo seguro/bloqueado no servidor, nunca em CSS; trate "a resposta do modelo chegou ao usuário exatamente uma vez" como um problema real de sistemas distribuídos, não como uma suposição de caminho feliz; e mantenha o frontend sem etapa de build se você espera que agentes de IA sejam quem vai mantê-lo.
Downloads
Gratuito para uso pessoal. Se isso te economizar uma tarde, o botão do café está logo ali.