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.



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ío —
POST /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.

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.

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
| Capa | Qué es |
|---|---|
| Backend | Python 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 |
| Frontend | JS puro sin compilación + HTML/CSS — ~4.900 líneas de JS, ~2.100 de CSS/HTML/service worker |
| Bibliotecas incluidas | marked, highlight.js, DOMPurify, xterm.js — sin llamadas a CDN |
| Datos | Fuera 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 enconfig.yaml.example) para que losidde los bots coincidan con los ids de tus agentes de OpenClaw. Se recarga en caliente. - Opcional:
./install.sh --systemdgenera una unidad systemd de usuario. - Opcional: configura un PIN desde el icono de engranaje → Security.
security.yaml.exampledocumenta el archivo, incluido elapi_tokenpara quien acceda a/api/injectdesde fuera de la máquina. - Parámetros de entorno:
LOCAL_CHAT_HOST/PORT/DATA_DIR/AGENT_TIMEOUT/MAX_CONCURRENCY, además deLOCAL_CHAT_COMFY=0/LOCAL_CHAT_TERMINAL=0para 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, notext.POST /api/injectquiere{"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
Originno coincida conHost; 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.