gateways/

Una carpeta, dos trabajos no relacionados: exponer el agente, y conectar plataformas de chat a él.

On this page

gateways/

Los archivos gateways/*.bx/.json bajo esta única carpeta cubren dos cosas distintas y no relacionadas - qué tipo de entrada es depende enteramente de si el struct configure() tiene una clave exposes.

Warning

No confundas estas cosas entre sí - un agente expuesto por HTTP (exposes: "agent") es una API REST para tu agente; un gateway de channel-adapter (type: "http") es un endpoint de webhook para una plataforma de chat o un flujo de aprobación human-in-the-loop. Generan rutas completamente diferentes.

flowchart TD
    F["un archivo bajo gateways/"] --> Q{"¿configure() devuelve<br/>una clave 'exposes'?"}
    Q -->|"sí"| E["EXPOSICIÓN<br/>una ruta hacia tu agente"]
    Q -->|"no - tiene una clave 'type' en su lugar"| C["CHANNEL ADAPTER<br/>una conexión a una plataforma de chat"]
    E --> E1["exposes: agent<br/>route().toAi()"]
    E --> E2["exposes: mcp<br/>route().toMCP()"]
    E --> E3["exposes: webui<br/>index.html generado + /api"]
    C --> C1["mock / cli / http<br/>impulsado por pull: algo nos llama a NOSOTROS"]
    C --> C2["telegram, slack, discord, email, whatsapp-cloud,<br/>teams, twilio, github, signal<br/>estilo push: mantiene su propia conexión"]
    C2 --> S["un GatewaySession<br/>vinculado al agente raíz"]

    style E fill:#d4edda,stroke:#155724
    style C fill:#cce5ff,stroke:#004085

1. Exposición HTTP/MCP/interfaz web (exposes: "agent" | "mcp" | "webui")

Expone el agente, o un servidor MCP local, sobre HTTP usando el DSL nativo de AI Routing de ColdBox 8.1 - o una interfaz de chat de navegador pre-construida, documentada por separado en La interfaz web de chat.

Expón el agente:

// gateways/expose.bx
class {

	function configure() {
		return {
			exposes : "agent",
			path    : "/api/chat"
		};
	}

}

Genera, en config/Router.bx:

route( "/api/chat" ).toAi( "GeneratedAgent" )

que auto-registra cuatro subrutas: POST /api/chat/invoke, POST /api/chat/stream (SSE), POST /api/chat/batch, GET /api/chat/info. La ruta desnuda /api/chat en sí no es enrutable.

Expón un servidor MCP local (ver mcp/):

class {
	function configure() {
		return {
			exposes : "mcp",
			path    : "/mcp/tools",
			target  : "local-server"   // debe coincidir con el nombre declarado de una entrada mcp/*.bx
		};
	}
}

Genera route( "/mcp/tools" ).toMCP( "local-server" ).

Expón la interfaz web de chat v1:

// gateways/chat.bx
class {
	function configure() {
		return {
			exposes     : "webui",
			path        : "/chat",
			apiKeyEnvVar: "CHAT_UI_API_KEY"   // opcional - ver abajo
		};
	}
}

Genera un archivo estático real <path>/index.html (servido directamente - no se necesita ruta para él) más su propia API dedicada bajo un prefijo fijo <path>/api, así que nunca colisiona con los propios archivos del shell. Esa API es un handlers/ChatUi.bx generado en lugar de toAi(), y la entrada también trae consigo un almacén SQLite generado.

La interfaz web es un subsistema en lugar de un simple interruptor de exposición - la lista de rutas, el almacén, las conversaciones y preferencias, el branding y la temática, y por qué no usa toAi() están todos en su propia página: La interfaz web de chat.

Validación: exposes debe ser agent, mcp, o webui; path es requerido y debe ser único a través de cada entrada de exposición; el target de una exposición mcp es requerido y debe coincidir con el nombre declarado de una entrada real mcp/*; el apiKeyEnvVar de webui es completamente opcional, sin ninguna comprobación de campo requerido (ver abajo).

2. Gateways de channel-adapter (type: "mock" | "cli" | "http")

Registra un IGateway de bx-ai (un channel adapter para entrega externa / aprobación human-in-the-loop) por nombre - distinto de exponer la propia API REST del agente.

// gateways/slack.bx
class {
	function configure() {
		return {
			type         : "http",
			secretEnvVar : "SLACK_WEBHOOK_SECRET"
		};
	}
}

secretEnvVar nombra una variable de entorno que contiene el secreto de firma - nunca el valor del secreto mismo. Genera, en el onApplicationStart() de Application.bx:

aiGatewayRegistry().register( aiGateway( "http", { secret : getSystemSetting( "SLACK_WEBHOOK_SECRET", "" ) } ) )

El secreto se resuelve en vivo en el arranque del servidor, coincidiendo con la regla de "los secretos permanecen externos" de este proyecto en cualquier otro lugar (ver Despliegue y secretos) - nunca se incrusta como un literal en el código fuente generado, así que tampoco está presente jamás en un .bxa empaquetado. Si la variable de entorno no está configurada, el propio HttpGateway de bx-ai trata un secreto vacío como "sin firma configurada" y rechaza requests en consecuencia, en lugar de fallar en el arranque.

Validación: type debe ser mock, cli, o http; una entrada type: "http" requiere un secretEnvVar; el propio nombre de archivo/nombre base de la entrada debe ser único a través de cada entrada de channel-adapter. mock es solo para pruebas; cli es el propio canal incorporado de aprobación human-in-the-loop de bx-ai (un prompt bloqueante de stdin/stdout A/R/Q) - es lo que HumanInTheLoopMiddleware conecta por defecto cuando no se especifica ningún gateway, y no está relacionado con el propio verbo chat de BX Agents (que nunca toca el registro de gateways en absoluto).

Las entradas de tipo http adicionalmente obtienen cableado HTTP real: una acción generada handlers/Gateway.bx que hace de proxy directamente hacia el propio GatewayRequestProcessor::processHttp() de bx-ai, y tres rutas en config/Router.bx:

post( "/gateways/:gatewayName/events" ).toHandler( "Gateway.process" )
get( "/interactions/:requestID" ).toHandler( "Gateway.process" )
post( "/interactions/:requestID/decisions" ).toHandler( "Gateway.process" )
Info

ColdBox no tiene un terminador de DSL toAiGateway() incorporado para esta superficie (solo toAi() y toMCP() existen nativamente) - este cableado es código propio generado por BX Agents, siguiendo la misma forma que produciría un futuro terminador del núcleo. Ver la propuesta toAiGateway() para ColdBox Core.

3. Push-style gateways (type: "telegram" / "slack" / "discord" / "email" / "whatsapp-cloud" / "teams" / "twilio" / "github" / "signal", and friends)

Un tipo diferente de channel adapter respecto a mock/cli/http de arriba: en lugar de ser impulsado por un request HTTP entrante, un gateway de estilo push mantiene su propia conexión a la plataforma y empuja mensajes entrantes a tu agente a medida que llegan - la experiencia más cercana a "bot de chat real". Hoy existen cuatro formas de transporte:

  • Long-poll (Telegram, Email): una tarea programada pregunta periódicamente a la plataforma "¿algo nuevo?" (el getUpdates de Telegram, el poll IMAP de Email).
  • Websocket persistente (Slack vía Socket Mode, Discord vía su Gateway API): el gateway mantiene una conexión viva y de larga duración por la que la plataforma empuja eventos en tiempo real.
  • Webhook, impulsado por pull (WhatsApp Business Cloud API, Microsoft Teams, Twilio SMS, GitHub): la plataforma nos llama a nosotros a través de un endpoint HTTP público en lugar de que este gateway mantenga su propia conexión saliente - no hay tarea de scheduler ni socket que gestionar. Ver sus propias subsecciones abajo.
  • Server-Sent Events (SSE) (Signal, contra un daemon signal-cli ejecutado localmente): una conexión HTTP de streaming unidireccional y de larga duración que el gateway mantiene abierta, leyendo eventos a medida que se empujan por el mismo cuerpo de respuesta. Ver su propia subsección abajo.
// gateways/telegramChannel.bx
class {
	function configure() {
		return {
			type          : "telegram",
			botTokenEnvVar: "TELEGRAM_BOT_TOKEN"
		};
	}
}
// gateways/slackChannel.bx
class {
	function configure() {
		return {
			type          : "slack",
			botTokenEnvVar: "SLACK_BOT_TOKEN",   // xoxb-... - chat.postMessage/chat.update
			appTokenEnvVar: "SLACK_APP_TOKEN"    // xapp-... - apps.connections.open (Socket Mode)
		};
	}
}
// gateways/discordChannel.bx
class {
	function configure() {
		return {
			type          : "discord",
			botTokenEnvVar: "DISCORD_BOT_TOKEN"   // Authorization: Bot <token> en cada llamada REST y dentro de Identify
			// intents: 37377   // override opcional - por defecto GUILDS+GUILD_MESSAGES+DIRECT_MESSAGES+MESSAGE_CONTENT
		};
	}
}
// gateways/emailChannel.bx
class {
	function configure() {
		return {
			type              : "email",
			imapHostEnvVar    : "IMAP_HOST",
			imapUsernameEnvVar: "IMAP_USERNAME",
			imapPasswordEnvVar: "IMAP_PASSWORD",
			fromAddressEnvVar : "EMAIL_FROM_ADDRESS"
			// imapPort: 993   // override opcional - por defecto 993 (IMAPS)
			// pollIntervalSeconds: 60   // override opcional - por defecto 60
		};
	}
}
// gateways/whatsappCloud.bx
class {
	function configure() {
		return {
			type               : "whatsapp-cloud",
			accessTokenEnvVar  : "WHATSAPP_ACCESS_TOKEN",     // token de acceso de la API de Graph
			phoneNumberIdEnvVar: "WHATSAPP_PHONE_NUMBER_ID",  // el ID del número de teléfono de WhatsApp Business por el que pasan los envíos
			appSecretEnvVar    : "WHATSAPP_APP_SECRET",       // clave HMAC que verifica X-Hub-Signature-256 en webhooks entrantes
			verifyTokenEnvVar  : "WHATSAPP_VERIFY_TOKEN"      // secreto compartido que el handshake GET de verificación de Meta debe devolver
			// apiVersion: "v21.0"   // override opcional - por defecto "v21.0"
		};
	}
}
// gateways/teamsChannel.bx
class {
	function configure() {
		return {
			type                : "teams",
			appIdEnvVar         : "TEAMS_APP_ID",         // el propio Microsoft App ID del bot (también el claim aud requerido del JWT entrante)
			appPasswordEnvVar   : "TEAMS_APP_PASSWORD"    // secreto de cliente OAuth2 client-credentials
			// tenantId: "..."   // override opcional para apps de un solo tenant - por defecto "botframework.com" (multi-tenant)
		};
	}
}
// gateways/twilioChannel.bx
class {
	function configure() {
		return {
			type            : "twilio",
			accountSidEnvVar: "TWILIO_ACCOUNT_SID",
			authTokenEnvVar : "TWILIO_AUTH_TOKEN",   // también la clave HMAC de X-Twilio-Signature
			fromEnvVar      : "TWILIO_FROM_NUMBER"   // el número de teléfono de Twilio por el que pasan los envíos salientes, E.164
			// messagingServiceSid: "MG..."   // opcional - si se configura, se usa en lugar de `from` en envíos salientes
			// publicUrl: "https://your-real-public-host/webhooks/twilio"   // override opcional para despliegues de proxy inverso/túnel - ver la subsección de Twilio abajo
		};
	}
}
// gateways/githubChannel.bx
class {
	function configure() {
		return {
			type               : "github",
			tokenEnvVar        : "GITHUB_TOKEN",           // un token de acceso personal (scope de lectura+escritura de repo/issues+PR)
			webhookSecretEnvVar: "GITHUB_WEBHOOK_SECRET",  // clave HMAC que verifica X-Hub-Signature-256 en webhooks entrantes
			botNameEnvVar      : "GITHUB_BOT_NAME"         // el propio login de GitHub del bot - coincidido como "@botName" en comentarios
			// apiBaseUrl: "https://api.github.com"   // override opcional - por defecto "https://api.github.com"
		};
	}
}
// gateways/signalChannel.bx
class {
	function configure() {
		return {
			type         : "signal",
			accountEnvVar: "MY_SIGNAL_ACCOUNT"   // el número de teléfono registrado en signal-cli con el que este gateway envía/recibe, E.164
			// httpUrl: "http://127.0.0.1:8080"   // override opcional - por defecto "http://127.0.0.1:8080", donde se espera que escuche el propio daemon HTTP API de signal-cli
		};
	}
}

La misma regla de "los secretos permanecen externos" que el secretEnvVar de http: cada clave *EnvVar nombra una variable de entorno, resuelta en vivo vía getSystemSetting() en el arranque, nunca incrustada como un literal - el imapHost/fromAddress de email no son secretos criptográficos, pero se usa de todos modos la misma convención impulsada por variable de entorno para cada uno de sus valores de configuración, ya que todos varían por despliegue. A diferencia de los tipos centrales, la clase de un gateway de estilo push vive dentro de BX Agents mismo (models/gateways/*.bx, no bx-ai), así que su registro se renderiza como una ruta de clase desnuda en lugar de un nombre corto:

aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TelegramGateway", { "botToken" : getSystemSetting( "TELEGRAM_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.SlackGateway", { "appToken" : getSystemSetting( "SLACK_APP_TOKEN", "" ), "botToken" : getSystemSetting( "SLACK_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.DiscordGateway", { "botToken" : getSystemSetting( "DISCORD_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.EmailGateway", { "imapHost" : getSystemSetting( "IMAP_HOST", "" ), "imapUsername" : getSystemSetting( "IMAP_USERNAME", "" ), "imapPassword" : getSystemSetting( "IMAP_PASSWORD", "" ), "fromAddress" : getSystemSetting( "EMAIL_FROM_ADDRESS", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.whatsapp.WhatsAppCloudGateway", { "accessToken" : getSystemSetting( "WHATSAPP_ACCESS_TOKEN", "" ), "phoneNumberId" : getSystemSetting( "WHATSAPP_PHONE_NUMBER_ID", "" ), "appSecret" : getSystemSetting( "WHATSAPP_APP_SECRET", "" ), "verifyToken" : getSystemSetting( "WHATSAPP_VERIFY_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TeamsGateway", { "appId" : getSystemSetting( "TEAMS_APP_ID", "" ), "appPassword" : getSystemSetting( "TEAMS_APP_PASSWORD", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TwilioGateway", { "accountSid" : getSystemSetting( "TWILIO_ACCOUNT_SID", "" ), "authToken" : getSystemSetting( "TWILIO_AUTH_TOKEN", "" ), "from" : getSystemSetting( "TWILIO_FROM_NUMBER", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.GitHubGateway", { "token" : getSystemSetting( "GITHUB_TOKEN", "" ), "webhookSecret" : getSystemSetting( "GITHUB_WEBHOOK_SECRET", "" ), "botName" : getSystemSetting( "GITHUB_BOT_NAME", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.SignalGateway", { "account" : getSystemSetting( "MY_SIGNAL_ACCOUNT", "" ) } ) )

Validación: type: "telegram" requiere botTokenEnvVar; type: "slack" requiere tanto botTokenEnvVar como appTokenEnvVar; type: "discord" requiere botTokenEnvVar; type: "email" requiere imapHostEnvVar, imapUsernameEnvVar, imapPasswordEnvVar, y fromAddressEnvVar; type: "whatsapp-cloud" requiere accessTokenEnvVar, phoneNumberIdEnvVar, appSecretEnvVar, y verifyTokenEnvVar; type: "teams" requiere appIdEnvVar y appPasswordEnvVar; type: "twilio" requiere accountSidEnvVar, authTokenEnvVar, y fromEnvVar; type: "github" requiere tokenEnvVar, webhookSecretEnvVar, y botNameEnvVar; type: "signal" requiere accountEnvVar - todo comprobado de la misma manera que se comprueba el secretEnvVar de http.

Info

Slack v1 es solo Socket Mode - no se necesita ni se genera ningún endpoint de webhook público para él (a diferencia de http, que obtiene rutas reales - ver §2 arriba). La alternativa de Events-API/webhook-HTTP que Slack también soporta no está construida aquí. Discord v1 es de igual manera la Gateway API real (un websocket persistente) en lugar del modo alternativo de webhook de HTTP Interactions Endpoint URL de Discord - no se necesita verificación de firma Ed25519 aquí como resultado, ya que las interacciones llegan por la misma conexión autenticada en lugar de un endpoint HTTP público (confirmado contra la propia documentación de Discord).

La conexión persistente de Slack

SlackGateway mantiene su websocket vía el cliente WebSocket asíncrono de java.net.http.HttpClient, mediado por una clase listener de BoxLang que implements="java:java.net.http.WebSocket$Listener" directamente (models/gateways/support/SlackSocketListener.bx) - BoxLang lo compila como un implementador de JVM real, confirmado empíricamente entregando una instancia directamente a HttpClient.newWebSocketBuilder().buildAsync( uri, listener ) sin ningún error de casting (solo el esperado java.net.ConnectException una vez que se alcanzó la frontera de red real). Solo los métodos que la clase realmente declara sobreescriben los métodos default de la interfaz del JDK; cualquier cosa no implementada cae automáticamente al comportamiento por defecto propio del JDK. Este es el patrón de referencia que sigue también cada otro gateway de conexión persistente (Discord, abajo).

Las reconexiones son impulsadas de forma reactiva por las propias señales del protocolo de Slack - un frame disconnect (warning/refresh_requested) o un cierre de socket inesperado - abriendo una conexión nueva antes de cerrar la vieja, según la recomendación documentada de Slack. Un watchdog ligero de scheduler (slack-watchdog-<name>, cada 30s) es solo una red de seguridad para el caso en que ninguna de esas señales se dispare.

La conexión persistente de Discord - latidos obligatorios impulsados por el cliente

DiscordGateway se conecta de la misma manera (models/gateways/support/DiscordSocketListener.bx, el mismo patrón implements="java:java.net.http.WebSocket$Listener" que Slack), pero el protocolo Gateway de Discord tiene un requisito que el Socket Mode de Slack no tiene: el propio frame Hello del servidor (opcode 10) le dice al cliente un heartbeat_interval, y el cliente debe seguir enviando frames Heartbeat (opcode 1) en esa cadencia por sí mismo o Discord trata la conexión como "zombificada" y la cierra. Ya que el intervalo solo se conoce una vez que llega Hello (no antes de conectar), el latido se registra como su propia tarea de scheduler (discord-heartbeat-<name>) dinámicamente desde dentro del manejador de frame, re-registrada en cada Hello fresco - distinto de la(s) tarea(s) fija(s)-en-el-momento-de-registerScheduledTasks() de cada otro gateway de estilo push, y distinto del propio watchdog de red de seguridad de Discord (discord-watchdog-<name>, cada 30s, el mismo rol que el de Slack).

Cada tick de latido comprueba si el latido anterior fue alguna vez confirmado (Heartbeat ACK, opcode 11) - si no, la conexión está zombificada y se reconecta proactivamente en lugar de dejarla expirar por tiempo. Las reconexiones de lo contrario siguen el propio modelo de sesión documentado de Discord: un frame Reconnect (opcode 7) o la mayoría de los códigos de cierre disparan un Resume (opcode 6, reproduciendo el último número de secuencia) en la nueva conexión cuando existe una sesión previa; un frame Invalid Session (opcode 9) con d: false, o un código de cierre que Discord documenta como invalidador de sesión (4007, 4009), en cambio fuerza un Identify fresco (opcode 2). Un conjunto pequeño y fijo de códigos de cierre (4004 token malo, 4010 shard inválido, 4011 sharding requerido, 4012 versión de API inválida, 4013/4014 intents inválidos/no permitidos) no son recuperables según la propia documentación de Discord - el gateway se detiene en lugar de reintentar una conexión que simplemente fallaría de nuevo.

Warning

MESSAGE_CONTENT (necesario para leer el texto del mensaje en absoluto, tanto en canales de guild como en DMs) es un Gateway Intent privilegiado de Discord - debe habilitarse explícitamente para tu bot en el Discord Developer Portal, y una vez que tu app está verificada (100+ guilds), aprobado por Discord. Sin él, cada mensaje entrante llega con un campo content vacío.

Email - dependencias a nivel de servidor, y enhebrado/HITL degradados

EmailGateway es el único gateway de estilo push que no habla la API de su plataforma directamente. El correo saliente pasa por el propio módulo cbmailservices de ColdBox (MailService@cbmailservices, su protocolo BXMail - que él mismo simplemente llama al propio componente bx:mail de BoxLang, del módulo bx-mail) en lugar de una llamada HTTP/SMTP hecha a mano. Ambas son instalaciones de módulo reales, a nivel de servidor - se declaran como dependencies en el propio box.json de este proyecto (así que instalar bx-agents también las trae al servidor), pero cbmailservices/bx-mail ambos todavía requieren una instalación explícita en cualquier servidor que realmente ejecute una app generada (confirmado contra la propia documentación/código fuente de ambos módulos - ninguno viene preinstalado con ColdBox o BoxLang) - haz un box install real (o equivalente) antes de bxAgents serve/desplegar un proyecto con un gateway email. EmailGateway resuelve MailService@cbmailservices manualmente fuera de application.cbController.getWireBox() (ver el propio docblock de ScheduledGatewayBase.resolveScheduler() para saber por qué - esta clase se construye directamente por aiGateway(), enteramente fuera de WireBox, así que inject="" nunca se honra en ella), de la misma manera que se resuelve el propio scheduler.

Ya que ni bx-mail ni cbmailservices reciben correo (solo lo envían), el entrante es IMAP hecho a mano vía la API estándar del JDK jakarta.mail - confirmado alcanzable transitivamente en el propio classpath de este proyecto (bx-mail depende de commons-email2-jakarta, que a su vez depende de jakarta.mail-api + una implementación de Angus Mail), verificado empíricamente esta sesión contra los jars reales, no asumido. Una tarea programada (email-poll-<name>) hace poll de IMAP en busca de correo no leído, la misma forma que el long-poll de Telegram.

El enhebrado y el human-in-the-loop están ambos degradados en relación a los gateways de plataforma de chat, y getDeclaredCapabilities() deliberadamente omite "interactiveActions" para decirlo honestamente:

  • El enhebrado usa cabeceras reales Message-ID/In-Reply-To/References para una respuesta ORDINARIA (el gateway siempre conoce el Message-ID entrante al que está respondiendo, así que configurar In-Reply-To en la respuesta saliente es confiable) - una simplificación v1 enhebra sobre la primera entrada de References (si no, In-Reply-To, si no, el propio Message-ID del mensaje), no un recorrido completo de la cadena.
  • El human-in-the-loop no tiene ninguna superficie de botón/componente nativa en absoluto - requestHumanInteraction() envía un correo de texto plano listando las palabras clave de decisión permitidas y le pide al humano que responda con una como la primera línea. Correlacionar esa respuesta de vuelta con la solicitud pendiente correcta no puede depender de In-Reply-To de la forma en que lo hacen las respuestas ordinarias (el send() de cbmailservices no expone qué Message-ID recibió el propio correo de aprobación saliente), así que se hace vía una etiqueta [bxagents:<requestID>] incrustada en la línea de Asunto en su lugar - la misma técnica que usan los sistemas reales de tickets de soporte basados en email por la razón idéntica. La primera línea de una respuesta se compara contra las propias decisiones permitidas de la solicitud (exacta o por prefijo, sin distinguir mayúsculas/minúsculas); una respuesta no reconocida se pasa textualmente en lugar de volver a pedirla, dejada para que el propio coordinador HITL de bx-ai la rechace.

WhatsApp Business Cloud API - impulsado por webhook, no por conexión

WhatsAppCloudGateway está moldeado de forma diferente a cada otro gateway de estilo push: Meta nos llama a nosotros, a través de un webhook público, en lugar de que este gateway mantenga su propia conexión saliente (una tarea de poll o un websocket). Extiende BaseGateway de bx-ai directamente, no ScheduledGatewayBase - no hay tarea de scheduler ni socket que gestionar, solo un handlers/WhatsAppCloud.bx generado (escrito siempre que existe una entrada de gateway whatsapp-cloud) conectado a dos rutas fijas:

get( "/webhooks/whatsapp-cloud" ).toHandler( "WhatsAppCloud.verify" )
post( "/webhooks/whatsapp-cloud" ).toHandler( "WhatsAppCloud.process" )

Ambas acciones son passthroughs delgados hacia los propios handleVerify()/handleWebhook() del gateway - verify responde al handshake de suscripción de Meta (GET ?hub.mode=subscribe&hub.verify_token=...&hub.challenge=..., devolviendo el challenge como texto plano solo cuando el modo y el token coinciden, comparados en tiempo constante); process verifica la propia cabecera X-Hub-Signature-256 de Meta (HMAC-SHA256 sobre el cuerpo POST crudo exacto - event.getHTTPContent(), nunca JSON re-analizado/re-serializado, lo que cambiaría los bytes y rompería la firma) antes de analizar o despachar nada. Este es un esquema genuinamente diferente al propio HttpGateway/GatewaySecurity de bx-ai (nombres de cabecera diferentes, construcción HMAC diferente), así que no se reutiliza aquí - ver el propio docblock de la clase.

Portado directamente desde el propio adaptador real y de producción de WhatsApp Cloud de Hermes Agent (gateway/platforms/whatsapp_cloud.py, licenciado MIT) - el handshake de verificación, el esquema de firma, el recorrido del payload de webhook (entry[].changes[].value.{messages,contacts}), las formas de mensaje saliente/botón interactivo (≤3 decisiones permitidas se renderizan como botones nativos, 4+ como una lista de tap-to-open, coincidiendo con los propios límites documentados de WhatsApp), y los límites de longitud (mensajes de 4096 caracteres, etiquetas de botón de 20 caracteres, texto de cuerpo interactivo de 1024 caracteres) se leyeron todos directamente de ese código fuente esta sesión, no reimplementados desde cero. Los mensajes entrantes se deduplican por su propio wamid (Meta reintenta la entrega de webhook en cualquier respuesta que no sea 200 hasta por 7 días) vía una caché FIFO acotada, reflejando el propio _dedup_wamid de Hermes.

Warning

Alcance de v1, coincidiendo con la propia limitación documentada de Hermes: los DMs de Cloud API no tienen una entidad "chat" separada - chat_id ES el wa_id del remitente - y los mensajes de grupo (que llevan su propio campo chat identificando el JID del grupo) están fuera de alcance; los medios (imagen/video/documento/audio) no se descargan, solo una leyenda si está presente. Cada otro gateway de estilo push comparte el mismo techo de registro de una-instancia-por-tipo documentado arriba - whatsapp-cloud no es la excepción.

Info

Las propias llamadas de contexto de request de ColdBox del handlers/WhatsAppCloud.bx generado (event.getHTTPContent()/event.getHTTPHeader()/event.renderData(), los parámetros de query fusionados en el scope URL de rc para el handshake GET) son los idiomas documentados y estándar de handler REST de ColdBox - pero a diferencia de la propia lógica de firma/despacho del gateway (probada unitariamente de forma exhaustiva y verificada empíricamente contra comportamiento HMAC/JSON real esta sesión), este cableado de ruta generada específico NO se ha ejercitado contra un arranque real de ColdBox. Ver limitaciones conocidas.

Microsoft Teams - protocolo Activity de Bot Framework

TeamsGateway es impulsado por webhook de la misma manera que WhatsAppCloudGateway - extiende BaseGateway directamente, y el propio servicio Bot Connector de Microsoft nos llama a nosotros, sobre una sola ruta generada:

post( "/webhooks/teams" ).toHandler( "Teams.process" )

A diferencia de WhatsApp Cloud no hay handshake GET de verificación (Bot Framework no tiene equivalente del hub.challenge de Meta) - cada actividad entrante llega como un POST firmado, verificado vía un JWT bearer en la cabecera Authorization en lugar de una firma HMAC sobre el cuerpo. El JWT se comprueba contra el propio JWKS de Bot Connector (https://login.botframework.com/v1/.well-known/openidconfiguration → su jwks_uri) - firma RS256, aud debe ser igual al propio appId configurado del bot, iss debe ser igual a la cadena de emisor fija de Bot Connector (https://api.botframework.com), ambos con una tolerancia de desfase de reloj de 5 minutos. Esta es verificación RSA/JWT genuina construida a partir de la propia interoperabilidad Java de BoxLang (java.security.Signature, java.security.KeyFactory, java.math.BigInteger) - sin biblioteca externa de JWT. Las llamadas salientes usan un token OAuth2 client-credentials separado (obtenido de login.microsoftonline.com/{tenantId}/oauth2/v2.0/token, cacheado y reobtenido 60s antes de su expiración declarada).

Portado desde el propio canal real de Teams de Vercel Eve (packages/eve/src/public/channels/teams/, licenciado MIT) - el flujo OAuth2, el esquema de verificación de JWT, la tríada REST v3/conversations/{id}/activities[/{activityId}], y la forma human-in-the-loop de Adaptive Card (esquema 1.5, un botón Action.Submit por decisión permitida) todos reflejan esa implementación. El propio msgraph_webhook.py de Hermes Agent no está relacionado a pesar del nombramiento similar de "webhook de Microsoft" - implementa webhooks de notificación de cambio de Microsoft Graph (eventos de cambio de recurso de buzón/unidad/lista, una superficie de producto de Microsoft diferente sin ninguna mensajería saliente de Teams funcional en absoluto) y nada de él se portó aquí.

Warning

El alcance de v1 es solo conversaciones personales (DM 1:1) - el chat grupal y los mensajes de todo el canal necesitan una compuerta de mención de bot y un modelo de enhebrado de respuesta diferente que Eve mismo implementa pero este port no - coincidiendo con el propio alcance v1 de solo-DM de cada otro gateway de estilo push. Se usa un límite de fragmento de mensaje de 4000 caracteres (la propia constante de truncamiento de texto de Adaptive Card de Eve) en lugar del verdadero techo de 80 KiB del protocolo de Bot Framework, por legibilidad de UI.

Info

El JWKS de Bot Connector se obtiene una vez y se cachea por la vida de la instancia del gateway - si Microsoft alguna vez rota sus claves de firma sin un kid coincidente ya cacheado, la verificación empezaría a fallar hasta que el gateway (y por lo tanto toda la app) se reinicie. No hay invalidación periódica de caché construida para v1. La propia lógica de verificación de JWT se verificó empíricamente esta sesión contra un par de claves RSA real y generado localmente y JWTs de prueba firmados a mano (firma válida aceptada, firma manipulada/audiencia incorrecta/token expirado todos rechazados con 401) - no solo leída contra el código fuente de Eve.

Twilio SMS - un esquema de firma genuinamente diferente, y un modelo de respuesta de dos caminos

TwilioGateway es impulsado por webhook de la misma manera que WhatsAppCloudGateway/TeamsGateway:

post( "/webhooks/twilio" ).toHandler( "Twilio.process" )

Dos cosas hacen que el propio contrato de webhook de Twilio sea significativamente diferente de cada otro gateway en este proyecto, ambas portadas fielmente desde el propio canal real de Twilio de Vercel Eve (packages/eve/src/public/channels/twilio/, licenciado MIT):

  • El cuerpo entrante está codificado como formulario (Body, From, To, MessageSid, AccountSid), no JSON - TwilioGateway lo analiza él mismo (java.net.URLDecoder), sin deserialización JSON involucrada.
  • La verificación de firma es X-Twilio-Signature: HMAC-SHA1, codificado en base64 (cada otro gateway de webhook en este proyecto usa HMAC-SHA256, codificado en hex) - la base de firma es la URL exacta del request seguida de cada key & value propio de los parámetros POST concatenados directamente (sin separadores), ordenados alfabéticamente por clave. Porque la URL misma es parte de lo que se firma, un proyecto ejecutándose detrás de un proxy inverso o túnel (donde la URL que ColdBox ve vía event.getUrl() no coincide con lo que Twilio realmente hizo POST) necesita el override opcional de configuración publicUrl - la misma clase de trampa que la propia documentación de Eve señala para su opción webhookUrl.
  • La respuesta síncrona del webhook siempre es un TwiML vacío <Response></Response> - el propio modelo clásico de dos caminos de Twilio. La respuesta real del agente se envía después, fuera de banda, vía una llamada REST separada a deliver() a la API de Messages una vez que el turno asíncrono de GatewaySession se completa - coincidiendo exactamente con el propio emptyTwilioResponse() de Eve (Eve nunca usa un <Message> TwiML síncrono para responder en línea).

Los envíos salientes son llamadas REST con Basic-Auth a POST /2010-04-01/Accounts/{AccountSid}/Messages.json, cuerpo codificado como formulario (To, Body, y ya sea From o MessagingServiceSid si está configurado). v1 es solo SMS-de-texto - el propio canal de Twilio de Eve es un canal combinado de SMS+voz (rutas /voice, TwiML <Gather>/<Say>, transcripción de llamada); ninguna de las piezas específicas de voz se portaron.

Warning

SMS no tiene ninguna capacidad nativa de botón/tarjeta en absoluto (confirmado vía la propia documentación de Eve), así que el human-in-the-loop está degradado de la misma manera que el de Email - getDeclaredCapabilities() omite "interactiveActions" (y "threads", ya que la clásica API de Messages de Twilio tampoco tiene un concepto nativo de respuesta/cita). requestHumanInteraction() envía un SMS de texto plano listando las decisiones permitidas; a diferencia de Email (que incrusta una etiqueta [bxagents:<requestID>] en la línea de Asunto para correlacionar la respuesta eventual), SMS no tiene línea de asunto que etiquetar - así que la solicitud pendiente se indexa por el propio número de teléfono del remitente (conversationID) en su lugar, una simplificación v1 que asume como máximo una solicitud HITL abierta por número de teléfono a la vez.

Info

A diferencia de Eve (que no tiene ninguna lógica de limitación de longitud en absoluto - confirmado ausente haciendo grep en su código fuente - y depende enteramente de la propia segmentación del lado del servidor de Twilio), TwilioGateway aún aplica MessageChunker en 1600 caracteres (el propio techo documentado de concatenación de un solo mensaje de Twilio) por consistencia con el comportamiento de fragmentación de cada otro gateway. El esquema de firma HMAC-SHA1 se verificó de forma cruzada esta sesión contra un valor de referencia computado independientemente en Python hmac/hashlib antes de confiar en la implementación de BoxLang, la misma disciplina usada para el propio esquema HMAC-SHA256 de WhatsApp Cloud.

GitHub - hilos de comentario de issue/PR con compuerta de @mention

GitHubGateway trata cada issue, PR, o hilo de comentario de review en línea como una conversación de chat - el agente responde cuando explícitamente se le hace @mention en un comentario, y responde publicando un nuevo comentario de vuelta al mismo hilo. Impulsado por webhook de la misma manera que cada otro gateway en esta sección:

post( "/webhooks/github" ).toHandler( "GitHub.process" )

Portado desde el propio canal real de GitHub de Vercel Eve (packages/eve/src/public/channels/github/, licenciado MIT) - la verificación de X-Hub-Signature-256 se confirma como la construcción idéntica al propio esquema de Meta de WhatsApp Cloud (HMAC-SHA256 sobre el cuerpo crudo, hex, prefijo sha256=) - el único gateway de webhook en este proyecto que reutiliza el algoritmo de firma exacto de otro, en lugar de necesitar el suyo propio. Solo se despachan los eventos issue_comment y pull_request_review_comment con action: "created" (coincidiendo con los propios tipos de evento manejados-por-defecto de Eve - issues/pull_request/check_suite/check_run/workflow_run tampoco tienen despacho por defecto en Eve, y no se conectan aquí); cada otro tipo de evento se reconoce (200) pero se ignora, para evitar el comportamiento de reintento/deshabilitar-hook-en-fallo de GitHub para eventos sobre los que este gateway no actúa.

La compuerta de despacho es un requisito genuino de @mention, portado desde el propio extractGitHubCommentTrigger() de Eve: un comentario solo alcanza al agente si contiene @<botName> seguido de fin-de-cadena o un carácter no identificador (así que un bot llamado mybot nunca se dispara en un comentario que menciona @mybot2) - confirmado vía una prueba de humo de regex-lookahead real esta sesión antes de confiar en ello. El token @mention coincidente se elimina del texto antes de que llegue al agente. La prevención de bucle de bot refleja la propia protección de tres partes de Eve: cualquier comentario cuyo autor tenga el propio type: "Bot" de GitHub, cuyo login coincida con {botName}[bot], o cuyo cuerpo contenga el propio marcador <!-- bxagents:posted --> de este gateway (añadido a cada comentario que publica) se ignora por completo, incluso si resulta contener una mención.

Una "conversación" se identifica por una de dos formas, coincidiendo con el propio modelo de Eve: repo:{owner}/{repo}:issue:{issueNumber} para un hilo de comentario de issue/PR ordinario, o repo:{owner}/{repo}:review-comment:{reviewThreadRootCommentId} para un hilo de comentario de review de PR en línea - las respuestas a un hilo de review siempre van al comentario raíz del hilo (comment.in_reply_to_id ?? comment.id), no al comentario específico al que se responde, así que un ida y vuelta multi-mensaje permanece un solo hilo. Las respuestas salientes hacen POST a repos/{owner}/{repo}/issues/{issueNumber}/comments (hilos ordinarios) o repos/{owner}/{repo}/pulls/{pullRequestNumber}/comments/{reviewCommentId}/replies (hilos de review).

Info

v1 auth es un token de acceso personal simple (tokenEnvVar), no el propio flujo de GitHub App JWT + token de instalación de Eve - más simple y más directamente portable para un primer corte (Eve mismo soporta un bypass de token pre-resuelto exactamente por esta razón, que es en lo que esto se mapea). Un futuro modo de GitHub App es una extensión natural, no construida aquí. A diferencia de Eve (que no tiene deduplicación de id de entrega en absoluto, confirmado ausente leyendo su código fuente), GitHubGateway deduplica por X-GitHub-Delivery vía una caché FIFO acotada, coincidiendo con la propia disciplina de deduplicación de wamid de WhatsApp Cloud.

Warning

No se portó ningún checkout de repo/edición de código (el propio checkout.ts de Eve, que clona el repo en un sandbox para que el agente pueda leer/editar código) - esta es solo una superficie de chat de comentario-entrada/comentario-salida. El human-in-the-loop está degradado de la misma manera que el de Twilio (sin capacidad nativa de botón/tarjeta) - requestHumanInteraction() publica un comentario pidiéndole al humano que haga @mention al bot de nuevo en una respuesta con una de las decisiones permitidas, correlacionado por conversationID (no por una etiqueta por solicitud), la misma simplificación v1 que usa el propio respaldo HITL de Twilio.

No hay ningún tipo "whatsapp-personal". El puente no oficial de cuenta personal (el protocolo Web multi-dispositivo de WhatsApp, el tipo que Hermes Agent alcanza vía un subproceso de Node.js/Baileys) se investigó pero deliberadamente no se construyó - la única opción nativa de Java licenciada MIT (Cobalt, com.github.auties00:cobalt) resultó traer una dependencia comercial/propietaria (com.aspose:aspose-words) en la versión realmente publicada en Maven Central, y un port de puente de subproceso se dejó de lado a favor de un enfoque nativo de JVM. Declarar type: "whatsapp-personal" en una entrada gateways/* falla la validación con un error de "tipo desconocido", igual que cualquier otro tipo no soportado. Ver docs/known-limitations.md para la investigación completa.

Signal - una cuarta forma de transporte, contra un daemon externo signal-cli

SignalGateway no está impulsado por webhook como WhatsApp Cloud/Teams/Twilio/GitHub arriba, y tampoco es un websocket como Slack/Discord - extiende ScheduledGatewayBase de la misma manera que lo hacen Telegram/Slack/Discord/Email, pero su propia conexión es Server-Sent Events: un único request de larga duración GET {httpUrl}/api/v1/events?account=... mantenido abierto vía la API asíncrona de java.net.http.HttpClient (sendAsync() + BodyHandlers.ofLines()), leyendo un evento JSON por línea a medida que el propio daemon de signal-cli los empuja por el mismo cuerpo de respuesta. Los envíos salientes son JSON-RPC 2.0 simple (POST {httpUrl}/api/v1/rpc, {"jsonrpc":"2.0","method":"send","params":{...},"id":...}) contra el mismo daemon.

No hay ninguna API oficial de bot de Signal - SignalGateway habla enteramente con signal-cli ejecutándose en su propio modo daemon --http, un prerequisito externo del que depende este gateway pero que no gestiona, la misma relación que EmailGateway tiene con un servidor externo IMAP/SMTP. Portado desde el propio canal real de Signal de Hermes Agent - las formas de cable SSE/JSON-RPC, las constantes de retroceso de reconexión (2s a 60s exponencial, +20% de jitter), y el watchdog de inactividad de 30s/120s se leen todos directamente de ese código fuente, no reimplementados desde cero.

Warning

Conseguir un daemon signal-cli funcional es un paso de configuración manual, real y de una sola vez, completamente fuera de este proyecto: instala signal-cli, regístralo/vincúlalo a una cuenta real de Signal (signal-cli link o register, ambos requieren un número de teléfono real y un paso de verificación/QR de vinculación de dispositivo), luego ejecuta signal-cli -a <account> daemon --http=127.0.0.1:8080 y mantén ese proceso vivo (un servicio systemd o sidecar de contenedor, no algo que bxAgents serve inicie por ti). El propio onConnect() de SignalGateway falla ruidosamente con MissingConfig si account no está configurado, pero no puede detectar ni iniciar el daemon él mismo - httpUrl inalcanzable en el momento de la conexión sale a la superficie como un ciclo ordinario de retroceso de reconexión, no un fallo rápido.

Info

v1 es solo DM - el propio canal de Signal de Hermes trata las conversaciones grupales como opt-in/desactivadas por defecto, y ese es el único modo portado aquí. El human-in-the-loop está degradado de la misma manera que el respaldo de Twilio/GitHub (getDeclaredCapabilities() omite "interactiveActions") - los recibos de lectura/reacciones de Signal son solo estado cosmético de solo-escritura en la propia API de signal-cli, no un canal de respuesta real, así que requestHumanInteraction() recae en un mensaje de texto plano listando las decisiones permitidas, correlacionado por conversationID como el propio respaldo indexado-por-número-de-teléfono de Twilio. La lógica de análisis JSON-RPC/SSE (handleSseEvent(), enhebrado de cita, filtrado de mensaje de grupo, coincidencia de decisión HITL) se condujo a través de métodos públicos reales con solo las llamadas de E/S rpcCaller/connector más externas siendo stubbeadas, la misma disciplina de prueba de seam que cada otro gateway - pero no había ningún daemon real de signal-cli disponible en este entorno, así que el ciclo de vida real de conexión asíncrona (abrir el stream SSE, el bucle de retroceso-con-reconexión contra una conexión genuinamente inestable, el round-trip JSON-RPC contra un daemon en vivo) nunca se ha ejercitado de extremo a extremo. La propia cadena de interoperabilidad de java.net.http.HttpClient se confirmó sólida - una prueba de humo independiente alcanzó un java.net.ConnectException genuino en la frontera de red real contra una dirección de prueba inalcanzable, probando que la plomería funciona aunque nunca haya tocado un daemon en vivo.

GatewaySession - wiring the agent to every push-style gateway

Cualquier proyecto con al menos una entrada de gateway de estilo push también obtiene un interceptors/GatewaySessionBootstrap.bx generado, que construye un único GatewaySession de bx-ai que agrupa cada gateway de estilo push en el proyecto, vinculado al agente raíz del proyecto, y lo inicia una vez que ColdBox mismo ha terminado de cargar:

// interceptors/GatewaySessionBootstrap.bx (GENERADO)
class {
	function afterConfigurationLoad( event, interceptData ) {
		var wirebox        = getController().getWireBox()
		var agent          = wirebox.getInstance( "GeneratedAgent" )
		var gatewaySession = aiGatewaySession(
			agent        : agent,
			gateways     : [ aiGatewayRegistry().get( "telegram" ) ],
			policy       : "queue",
			maxQueueDepth: 50
		)
		gatewaySession.start()
		application.bxaiGatewaySession = gatewaySession
	}
}
Info

La variable generada se nombra deliberadamente gatewaySession, no session - session es un nombre de scope reservado de BoxLang/ColdBox (como request/server/url/form/cgi/thread), y una variable local que reutiliza uno de esos nombres puede colisionar con el scope en vivo en lugar de comportarse como una local ordinaria.

Warning

La clave de aiGatewayRegistry().get(...) es siempre la cadena de TIPO del gateway ("telegram", "slack", "discord", "email", ...) - confirmado contra el propio código fuente real de GatewayRegistry.register() de bx-ai, que siempre indexa por el propio getName() fijo de la clase del gateway, nunca nada suministrado por el llamador. Una consecuencia real: dos entradas gateways/* del mismo tipo de estilo push colisionan en el mismo slot de registro a nivel de todo el proyecto - el segundo registro silenciosamente sobrescribe al primero. No hay alias por entrada hoy; usa un tipo distinto por cada cuenta de plataforma adicional, o espera al soporte multi-instancia.

Un interceptor (no una sentencia cruda de Application.bx/onApplicationStart(), a diferencia de las llamadas de registro simples de arriba) se usa específicamente porque su punto afterConfigurationLoad está garantizado por el propio ciclo de vida de ColdBox de dispararse estrictamente después de que el framework - incluyendo el scheduler del que estos gateways dependen (ver abajo) - haya terminado de cargar.

Controla la política de GatewaySession vía un bloque opcional gatewaySession en el propio Agent.bx del proyecto raíz:

// Agent.bx
class extends="bxModules.bxai.models.runnables.AiAgent" {

	function init() {
		super.init( name: "...", model: aiModel( provider: "..." ) )
		return this
	}

	function configure() {
		return {
			gatewaySession: { policy: "queue", maxQueueDepth: 50 }   // ambos opcionales - estos son los valores por defecto
		};
	}

}

policy debe ser uno de reject/queue/steer/interrupt (el propio vocabulario de política de GatewaySession de bx-ai - ver GatewaySession abajo) - comprobado en tiempo de build para que un typo falle ruidosamente en lugar de salir a la superficie como un error de runtime la primera vez que la app arranca.

Warning

Limitación de v1: exactamente un GatewaySession, siempre vinculado al agente raíz del proyecto - coincide con el precedente existente de que la exposición HTTP exposes: "agent" también es siempre solo-de-agente-raíz. Un proyecto con subagentes todavía no puede enrutar diferentes gateways a diferentes subagentes.

Qué hace en realidad cada política con un mensaje que llega mientras un turno todavía está en ejecución:

flowchart TD
    M["un mensaje llega en el hilo T"] --> B{"¿ya hay una ejecución<br/>en curso en T?"}
    B -->|"no"| D["despacha un turno nuevo.<br/>La respuesta fluye de vuelta a través del<br/>gateway del que vino el mensaje."]
    B -->|"sí"| P{"política"}
    P -->|"reject"| R["Respuesta inmediata de 'ocupado'.<br/>Nada se encola - el remitente debe reenviar."]
    P -->|"queue<br/>(la predeterminada)"| Q["Encolar, hasta maxQueueDepth.<br/>Se ejecuta como su propio turno una vez<br/>que termina el actual."]
    P -->|"steer"| ST["agent.steerRun( T, text )<br/>Empalmado en la MISMA ejecución en su siguiente<br/>checkpoint - nunca un segundo turno."]
    P -->|"interrupt"| I["agent.cancelRun( T ), Y encolar.<br/>El turno actual se va cerrando en su siguiente<br/>checkpoint, luego este mensaje se ejecuta."]
    Q --> OVER{"¿la cola ya está en<br/>maxQueueDepth?"}
    I --> OVER
    OVER -->|"sí"| R

    style D fill:#d4edda,stroke:#155724
    style R fill:#f8d7da,stroke:#721c24
Warning

"Steer" aquí significa el empalme no destructivo propio de Hermes Agent - el turno en ejecución sigue adelante y el texto nuevo se pliega dentro de él. NO significa lo que significa el turnPolicy: "steer" de Eve (cancelar el turno activo y comenzar un reemplazo); ese comportamiento es el interrupt de este vocabulario.

Info

Ni cancelRun() ni steerRun() son instantáneos. Ambos se señalan y toman efecto en el siguiente checkpoint de la ejecución (antes de la siguiente llamada al LLM o llamada de tool), así que interrupt es "pídele al turno actual que se vaya cerrando pronto", no "reemplázalo sincrónicamente".

Cómo un gateway de estilo push permanece conectado: el Scheduler compartido de ColdBox

En lugar de una nueva primitiva de bucle en segundo plano, los gateways de estilo push alcanzan el propio singleton en vivo del scheduler de ColdBox de la app (appScheduler@coldbox - el mismo bajo el que se ejecuta un schedules/Scheduler.bx escrito a mano, si el proyecto tiene uno) y registran su(s) propia(s) tarea(s) nombrada(s) en él dinámicamente - una tarea recurrente de long-poll para Telegram, por ejemplo. Un scheduler compartido, cada gateway de estilo push registrando sus propias tareas en él - nunca un scheduler por gateway, y nunca en conflicto con los propios cron jobs de un proyecto.

Logging

Cada gateway de estilo push escribe a su propio archivo de log gateway-<type> (por ejemplo, gateway-telegram) vía el writeLog() de BoxLang, en lugar de un log de app único/por defecto compartido - así que un operador puede seguir exactamente la plataforma que le importa sin ruido de todo lo demás que la app registra.

Edit this page Download Markdown Last updated Aug 21, 2026, 6:33:28 PM