DisPatch: una app de chat autoalojada para agentes de IA locales

Categoría
IA y LLM Local
Publicado
11 julio 2026
Actualizado
11 julio 2026
Por
Jacob Lloyd — escrito con ayuda de IA, después del proyecto
Tiempo de lectura
14 min de lectura

En palabras simples: DisPatch es tres cosas en una, alojadas enteramente en tu propio ordenador: una app de chat para hablar con asistentes de IA que también viven ahí, un servidor de archivos y una terminal — disponible en todos tus dispositivos en todo momento, sin que nada llegue a un servicio en la nube. Su servidor de archivos mueve archivos desde cualquier dispositivo hasta el ordenador donde corren los asistentes, para que puedan trabajar con lo que subas. En dispositivos compartidos, hay disponible un subconjunto seguro de asistentes sin necesidad de PIN; al introducir el PIN se desbloquean los de plena capacidad.

DisPatch es la app que construí para dejar de tener que mantener una instalación de Rocket.Chat con 30 contenedores solo para poder escribirle a mis propios agentes de IA. Acabó siendo tres cosas en una — una app de chat con LLM autoalojada, un servidor de archivos y una terminal — disponible en todos los dispositivos que tengo, en todo momento. Es un zip de ~720KB, es gratis, y es el único frontend de chat que uso ahora.

Resumen

  • Qué es: Tres cosas en una, autoalojadas: un chat web para un elenco de agentes de IA locales (un gateway de OpenClaw), un servidor de archivos con arrastrar y soltar, y una terminal real — en todos los dispositivos de tu red local o de tu red Tailscale, en todo momento.
  • Qué cuesta: $0. Sin plaza de SaaS, sin cuenta.
  • Qué necesitas: Linux, Python 3.12+ y uv. Un gateway de OpenClaw si quieres respuestas escritas por un modelo — todo lo demás funciona sin uno.
  • Con qué te quedas: hilos por bot, markdown en streaming, un servidor de archivos con arrastrar y soltar, una terminal de agente de programación integrada en la app, un "Modo Seguro" bloqueable con PIN, una API de envío para tareas cron, y 187 pruebas que pasan tal cual sale del zip.

Con qué te quedas

Antes de hablar de configuración, así es como se ve haciendo su trabajo. Las capturas son de una copia aislada con datos ficticios — nadie necesita ver los registros de mis bots reales.

Diseño de escritorio de DisPatch a 1440px: una barra de bots con avatares de bloques de letras de colores a la izquierda, una lista de hilos y un panel de chat mostrando un resumen matutino en markdown con un bloque de código con resaltado de sintaxis y una insignia de modelo.
Escritorio, 1440px: barra de bots, lista de hilos, panel de chat. El resumen y el bloque de código son markdown renderizado, no capturas pegadas.
Diseño móvil de DisPatch a 390px: la misma conversación en un solo panel con una barra de pestañas inferior para Bots, Chats y Messages, y acciones de Copiar, Regenerar y Eliminar por mensaje.
La misma conversación a 390px. La barra lateral se contrae en una barra de pestañas inferior; cada mensaje recibe Copiar / Regenerar / Eliminar.
DisPatch en Modo Seguro tras configurar un PIN: la barra de bots se reduce a tres bots aprobados, con un botón de candado ámbar abajo para desbloquear el resto.
Modo Seguro con un PIN configurado: solo los bots marcados como "seguros", mostrados con los mismos avatares que en el lado desbloqueado. No es un filtro CSS — el servidor simplemente nunca envía el resto.

Qué hace realmente

  • Hilos por bot, en todas partes. Ábrelo en tu móvil a mitad de una conversación y es el mismo hilo, el mismo historial, sin tener que volver a iniciar sesión.
  • Un servidor de archivos — arrastra y suelta archivos entre cualquier dispositivo y la máquina. La función que más uso; tiene su propia sección más abajo.
  • Una terminal (Terminex) — una shell real en el navegador ejecutando un agente de programación. También tiene su propia sección más abajo.
  • Respuestas en markdown con streaming, con código resaltado por sintaxis (botón de copiar incluido), imágenes y vídeo con una vista ampliada tipo lightbox, instalable como PWA.
  • Un panel de "trabajando" en vivo. Haz clic en los puntos de "escribiendo" para ver en directo las llamadas a herramientas y el razonamiento del modelo — está siguiendo la transcripción real del agente, no simulando un indicador de carga.
  • Bloqueo en dos niveles. Un PIN separa a los agentes de plena capacidad de un subconjunto seguro, de información general — los detalles están en la sección sobre el bloqueo en dos niveles más abajo.
  • Una API de envíoPOST /api/inject — para que una tarea cron u otro agente pueda dejar caer un mensaje (o una imagen) en un hilo. Así es como un resumen matutino llega a tu móvil sin que lo pidas.
  • Generación de imágenes local en el hilo. Pídele una imagen a un agente y puede llamar a una instancia local de ComfyUI; el resultado llega al chat como cualquier otro contenido multimedia, con lightbox incluido. Sin API de imágenes, sin subir nada a nadie.
  • Opcional, desactivado por defecto: un panel de ComfyUI para arrancar o detener ese servicio de generación de imágenes desde el chat.

El servidor de archivos

De todo lo que hay en DisPatch, el servidor de archivos es la pieza que más uso. Es un área de arrastrar y soltar archivos servida desde la misma máquina: abre DisPatch en cualquier dispositivo, suelta un archivo en él, y el archivo ya está en el servidor — agrupado por día, con miniaturas para imágenes y vídeo.

Vista del servidor de archivos de DisPatch: una zona de subida arriba, y luego archivos agrupados por día con miniaturas de imágenes y tarjetas por tipo de archivo.
El servidor de archivos: zona de subida arriba, subidas agrupadas por día debajo. Desde un móvil, este es todo el flujo de trabajo.

Lo que lo hace más que una comodidad es dónde aterrizan los archivos: en la misma máquina donde viven los agentes. Una foto subida desde mi móvil puede leerla del disco cualquier agente segundos después — describirla, archivarla, publicarla en una web, dársela a las herramientas de imagen. Es el puente entre "un archivo en el dispositivo que tenga en la mano" y "un archivo con el que mis agentes pueden trabajar", sin un disco en la nube en medio y sin cables.

Las partes menos vistosas están cubiertas: las subidas se comprueban en tamaño antes de almacenarse en memoria intermedia (un móvil no puede saturar la memoria de la máquina), hay un límite global de almacenamiento, las escrituras son atómicas para que una subida interrumpida nunca deje un archivo a medio escribir, y el almacén de archivos se refleja en el mismo esquema de copias de seguridad rotativas que el historial del chat. Un parámetro de configuración también puede convertir todo el sistema en un buzón de solo subida, en un solo sentido — los dispositivos pueden añadir archivos pero no navegarlos — para configuraciones donde más gente puede acceder a la app de la que debería poder leer su archivo.

Terminex: una terminal en el chat

La otra función que se gana su sitio a diario: DisPatch puede poner una terminal real en el navegador. Aparece en la barra de bots como cualquier otro bot, pero al abrirla te conectas a un PTY del lado del servidor transmitido por WebSocket hacia xterm.js — una sesión de shell real en la máquina, no una simulación.

Vista de la terminal de DisPatch: un panel de terminal xterm.js dentro de la app de chat con controles de Start, Restart y Stop, ejecutando un CLI de agente de programación.
Terminex: un PTY del lado del servidor dentro de la app de chat, con controles Start / Restart / Stop.

La mía arranca directamente en Reasonix, el agente de programación al estilo Claude Code que ejecuto sobre DeepSeek — la configuración que la página del centro de asistentes llama Terminex. Eso convierte una pestaña de chat en una consola completa de agente de programación: dale una tarea, míralo trabajar, desde cualquier dispositivo que pueda abrir la app. Combinado con el servidor de archivos, hace que la máquina sea totalmente manejable desde un móvil — suelta un archivo desde donde sea que estés, abre la terminal, dile al agente qué hacer con él.

  • Solo en modo desbloqueado. La terminal no existe en Modo Seguro, y permanece completamente apagada a menos que la actives (LOCAL_CHAT_TERMINAL).
  • La sesión está en memoria. Inicia / Reinicia / Detén desde la app; un reinicio del servidor la termina. No hay ninguna shell persistente escondida en segundo plano.

Por qué existe

Reemplazó un despliegue completo de Rocket.Chat — una base de datos y un montón de contenedores para que una sola persona pudiera hablar con algunos modelos locales. Funcionaba, pero es mucha infraestructura para "enviar texto, recibir texto, sincronizarlo en todas partes".

Y la respuesta más profunda a "por qué autoalojar siquiera una app de chat": una vez que tus modelos corren en local, la interfaz de chat es la última pieza que aún te tienta hacia un servicio en la nube. Autoalojarla significa que tus prompts, la salida de tus agentes y los mensajes de tu familia nunca salen de casa, y todo el ciclo — interfaz, gateway, modelos, generación de imágenes — sigue funcionando con internet desconectado. Si ya te has comprometido con los LLM locales, un frontend de chat alojado en la nube es la pieza que sobra.

Una decisión de diseño que vale la pena resaltar: DisPatch no tiene paso de compilación, a propósito. El frontend es JS puro (módulos ES) y CSS/HTML plano. Sin empaquetador, sin npm run build. Mi requisito real de partida era "fácil de modificar más adelante para un agente de IA". Editas un archivo JS, recargas, listo. Volvería a tomar la misma decisión.

La pila tecnológica

CapaQué es
BackendPython 3.12+, FastAPI + uvicorn, aiosqlite (un solo archivo SQLite, modo WAL), Pillow, gestionado con uv
Tamaño del backend~7.200 líneas en backend/app/ (solo main.py tiene ~3.600), más ~2.700 líneas de pruebas
FrontendJS puro sin compilación + HTML/CSS — ~4.900 líneas de JS, ~2.100 de CSS/HTML/service worker
Bibliotecas incluidasmarked, highlight.js, DOMPurify, xterm.js — sin llamadas a CDN
DatosFuera del código, en ~/.local/share/local-chat/: BD, config.yaml, security.yaml, media/, files/, copias de seguridad rotativas de la BD

Cómo configurarlo

Resumido de README-SETUP.md, dentro del zip (cuadro de descarga más abajo).

unzip dispatch-2026-07.zip
cd dispatch
./install.sh
./run.sh

Eso lo arranca en http://0.0.0.0:8765 e imprime las URLs de LAN y Tailscale, para que puedas tocar una desde tu móvil. A partir de ahí:

  • Edita ~/.local/share/local-chat/config.yaml (se crea automáticamente, documentado en config.yaml.example) para que los id de los bots coincidan con los ids de tus agentes de OpenClaw. Se recarga en caliente.
  • Opcional: ./install.sh --systemd genera una unidad systemd de usuario.
  • Opcional: configura un PIN desde el icono de engranaje → Security. security.yaml.example documenta el archivo, incluido el api_token para quien acceda a /api/inject desde fuera de la máquina.
  • Parámetros de entorno: LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY, además de LOCAL_CHAT_COMFY=0 / LOCAL_CHAT_TERMINAL=0 para mantener apagados los paneles opcionales.

El propio CLI de openclaw no viene incluido — DisPatch espera un gateway de OpenClaw ya en ejecución en algún lugar accesible. ¿Aún no tienes gateway? La app arranca igual, avisa en los registros, y todo funciona salvo las respuestas escritas por un modelo: hilos, la API de inyección, el servidor de archivos, PIN y Modo Seguro. Así probé este zip — descompresión limpia, uv sync, puerto libre, sin gateway — y la batería de pruebas incluida dio 187 passed in 8.4s.

Cómo ocurre realmente un mensaje escrito

Escribes algo y pulsas enviar. Este es el camino que recorre:

Ese es el camino feliz. La ingeniería de verdad se fue en los caminos infelices: el CLI se cuelga por tiempo, se cae, o un subagente responde diez minutos tarde. Cuatro fuentes independientes — el vigilante en vivo, la carga JSON del CLI, un reconciliador posterior al turno que vuelve a escanear la transcripción, y un "seguidor" que sigue la pista otros ~30 minutos — alimentan un único embudo de entrega con deduplicación por turno (texto canonicalizado más huella digital de contenido multimedia). Cualquiera de las rutas puede fallar y la respuesta igual llega exactamente una vez. Incluso ante un error del CLI, el reconciliador se ejecuta primero, porque el modelo muchas veces ya había volcado su respuesta a la transcripción antes de morir.

El bloqueo en dos niveles: normal vs. Modo Seguro

La distinción importa porque el lado desbloqueado no es un juguete. Esos agentes de verdad pueden hacer cosas: editar código fuente, publicar cambios en mi web en producción, ejecutar herramientas, administrar la máquina en la que viven. El lado bloqueado es solo información general — sigue siendo genuinamente útil, puedes hacer preguntas, charlar y obtener ayuda real — pero deliberadamente aislado de todo ese poder. La razón es simple: tengo un niño de tres años, y el móvil familiar no siempre está en manos adultas. Imagina qué podría hacer un niño de tres años con acceso total desde mi móvil — la web entera acabaría siendo vídeos de Paw Patrol. Así que el Modo Seguro es una protección infantil, aplicada en el servidor. Un pequeño detalle de comodidad: la pantalla de inicio bloqueada ahora muestra los mismos avatares de bots que el lado desbloqueado, en lugar de los antiguos marcadores de bloques de letras. Esto es lo que ocurre cuando llega una solicitud sin PIN introducido todavía:

Si te quedas fuera, hay un archivo de código de recuperación y un YAML editable, no un ticket de soporte. Y si security.yaml se corrompe a mitad de una escritura, el modo de fallo es "se queda bloqueado", no "sirve todo a cualquiera en la red local". Eso fue una decisión deliberada.

Trampas

  • El campo de la API es content, no text. POST /api/inject quiere {"bot_id": "...", "content": "..."}. El error de integración más común con diferencia, incluidos algunos que yo mismo he cometido.
  • No pases mensajes largos como argumento de línea de comandos. El kernel limita argv a 128KB (MAX_ARG_STRLEN); una respuesta larga hace que el arranque falle con E2BIG. El código usa --message-file <tmpfile> — no lo "simplifiques" de vuelta a un argumento.
  • Los ids de bot distinguen mayúsculas y minúsculas en un punto muy concreto. El gateway de OpenClaw pasa a minúsculas las claves de sesión en su índice, pero tus ids tienen mayúsculas mezcladas (Doxy, DS_Brain). Una búsqueda sensible a mayúsculas mata en silencio el panel de trabajo y el seguidor de respuestas tardías mientras todo lo demás sigue funcionando — difícil de notar. Primero coincidencia exacta, luego alternativa en minúsculas.
  • El CLI solo devuelve el bloque final. Cualquier cosa que un agente diga entre llamadas a herramientas existe solo en su transcripción, no en el valor de retorno del CLI — de ahí el vigilante y el reconciliador, y el tratamiento especial para el "esto es lo que he encontrado hasta ahora" de un agente mientras espera a un subagente.
  • El secuestro de WebSocket entre sitios se bloquea con una verificación de Origin. Ambas rutas de WS rechazan un handshake cuyo host de Origin no coincida con Host; los clientes que no son navegadores no envían Origin y pasan sin problema.
  • Los SVG deliberadamente no se pueden servir como contenido multimedia. Un navegador trata el SVG en línea como script del mismo origen — servir uno de vuelta como imagen es un agujero de XSS almacenado. Las respuestas multimedia reciben en su lugar una CSP de aislamiento y nosniff.
  • Las actualizaciones del service worker dejan pestañas antiguas atascadas con JS antiguo. Parece exactamente una regresión hasta que te das cuenta de que la pestaña nunca recogió el nuevo worker. Se solucionó con una recarga automática en controllerchange.

Cómo replicar la idea

Aunque nunca ejecutes mi zip, la arquitectura merece que la copies. El conjunto son cuatro piezas independientes, cada una reemplazable:

  • Un backend de chat que sea dueño del historial: un pequeño servidor web (aquí FastAPI + un solo archivo SQLite en modo WAL) con un WebSocket para actualizaciones en vivo y un endpoint HTTP plano para inyectar mensajes. Eso es todo el reemplazo de la "base de datos y montón de contenedores".
  • Un gateway de agentes al que el backend llama por subproceso o por HTTP — aquí OpenClaw, pero cualquier cosa que reciba "id de sesión + mensaje" y devuelva "texto" funciona. Mantenerlo como un proceso aparte significa que la app de chat sobrevive a que la pila de agentes se caiga, se actualice o se reemplace por completo.
  • Servidores de modelos locales detrás del gateway (LM Studio, vLLM, Ollama — problema del gateway, no de la app de chat).
  • Servicios de contenido multimedia opcionales, como un ComfyUI local para generación de imágenes, a los que los agentes llaman como herramientas y cuya salida el backend simplemente guarda como contenido multimedia.

Las lecciones que se trasladan sin importar la pila: aplica tu modo seguro/bloqueado en el servidor, nunca en CSS; trata "la respuesta del modelo llegó al usuario exactamente una vez" como un problema real de sistemas distribuidos, no como una suposición del camino feliz; y mantén el frontend libre de compilación si esperas que sean agentes de IA quienes lo mantengan.

Descargas

Gratis para uso personal. Si te ahorra una tarde, el botón del café está aquí cerca.


← Más de IA y LLM Local