Overview de canales
Ongrid habla con humanos a través de canales. Un canal es:
- un canal de notificación — Ongrid empuja alertas a una superficie de chat; el humano lee, pero el canal en sí nunca lleva una respuesta de vuelta al agente. Esto es
notify_channelsen la DB y los Senders deinternal/pkg/notify/webhook.go. - un canal IM — Ongrid se sienta en la superficie de chat de un workspace como un bot, lee los mensajes entrantes y corre el mismo razonamiento de agente que corre la UI web, streameando la respuesta de vuelta como ediciones a un mensaje placeholder. Esto es
im_appsen la DB einternal/manager/biz/imbridge/provider/*.
Los dos son independientes. Cablear alertas de Telegram (sendMessage out) no permite a los usuarios chatear con el agente en Telegram, y viceversa.
Distinta tabla, distinta credencial, misma UI
Ambas superficies se configuran en Settings → Channels en la UI web. Los canales de notificación van a la pestaña Notify; los canales IM van a la pestaña IM bridge.
¿Cuál quiero?
| Objetivo | Usa | Archivo |
|---|---|---|
| Empujar alertas disparadas a un canal de Slack | Slack incoming webhook (Notify) | internal/pkg/notify/webhook.go NewSlackSender |
| Empujar alertas disparadas a un grupo de Feishu/Lark | Bot custom de Feishu (Notify) | NewFeishuSender |
| Empujar alertas disparadas a un grupo de DingTalk | Bot custom de DingTalk (Notify) | NewDingTalkSender |
| Empujar alertas disparadas a un grupo de WeCom | Bot de grupo de WeCom (Notify) | NewWeComSender |
| POST JSON genérico a tu propio servicio | Webhook (Notify) | NewGenericWebhookSender |
| Alertas de Telegram (una sola dirección) | sendMessage del bot de Telegram (Notify) | NewTelegramSender |
| Hablar con el agente desde Slack como bot | App Socket Mode de Slack (IM bridge) | imbridge/provider/slack/ |
| Hablar con el agente desde Telegram | getUpdates del bot de Telegram (IM bridge) | imbridge/provider/telegram/ |
| Hablar con el agente desde Feishu/Larksuite | Long-connection de Feishu (IM bridge) | imbridge/provider/feishu/ |
Un workspace puede correr ambos: p. ej. un incoming webhook de Slack para alertas más una app Socket Mode de Slack separada para conversación. No comparten estado.
Canales de notificación
Un canal de notificación es un Sender stateless Send(ctx, Message). Lo llama el pipeline de alertas cada vez que dispara una alerta (o se recupera, o expira la ventana de dampening). Todos los tipos de canal renderizan la misma forma canónica notify.Message; solo difieren por provider el formato del payload y el protocolo de firma.
Campos comunes que recibe un Sender
type Message struct {
Severity Severity // critical | warning | info
Subject string // alert rule name + target
Body string // human-readable detail
Source string // "alert" | "test" | ...
Labels map[string]string // rule, incident_id, device_id, ...
DedupeKey string // pipeline:rule:label-set
OccurredAt time.Time
}El sender de Slack lo renderiza en el formato de attachments con un side rail coloreado por severidad, campos estructurados (Severity, Source, Rule, Incident, Device, Dedupe) y un footer ts. Los senders de Feishu / DingTalk / WeCom lo aplanan a un payload de texto [SEV] subject\nbody\nsource:…\ndedupe:… porque sus APIs de bot solo envían texto plano en v1.
Modelos de firma de un vistazo
| Provider | Cómo autentica |
|---|---|
| Slack | La propia URL del webhook es el secreto. Sin firma extra. |
| Feishu | HMAC-SHA256(ts\nsecret), puesto en el body JSON como sign. |
| DingTalk | HMAC-SHA256(ts\nsecret), puesto en el query de URL como sign. |
| WeCom | Bot key en el query de URL. Sin firma extra. |
| Telegram | Bot token en el path (/bot<TOKEN>/sendMessage). |
| Webhook | X-Ongrid-Signature: sha256=<HMAC> opcional sobre el body. |
Las implementaciones exactas viven en internal/pkg/notify/webhook.go: signFeishu, signDingTalkURL, signGenericWebhook.
Slack descarta el campo secret silenciosamente
La URL del incoming webhook de Slack es por sí misma la credencial. Si rellenas el campo Secret para un canal de Slack, el builder del canal lo descarta antes de construir el Sender. Esto es intencional — el protocolo de Slack no tiene una superficie de firma separada para incoming webhooks.
Canales IM
Un canal IM es una goroutine de larga vida que:
- Conecta saliente al provider (websocket para Feishu/Slack, long poll para Telegram). No abre puertos entrantes en el manager.
- Recibe mensajes entrantes del usuario, los filtra por
allow_fromy entrega el texto abizbridge.Bridge.HandleInbound. HandleInboundpostea una respuesta placeholder, corre el grafo completo del agente (el mismo que usa la UI web), y streamea ediciones de vuelta al id de mensaje placeholder.
El mismo kernel de agente, skills y registry de personas alimentan tanto la UI web como los canales IM. No hay "agente específico de IM" — el coordinator es el mismo que ves en /chat.
El inbound es específico del provider, el outbound es uniforme
Las diferencias de provider (ack envelope_id de Slack, offset de update de Telegram, encrypt_key de Feishu) viven en imbridge/provider/*/stream.go. Una vez que un mensaje está dentro de bizbridge.HandleInbound, todo downstream es agnóstico del provider.
default_locale
Tanto las filas IM como Notify llevan un default_locale opcional. El validator acepta string vacío (auto), en, o zh solo — en-US / zh-CN se doblan a su subtag primaria, y EN-us / un locale tipeado mal son rechazados de entrada en AppInput.validate.
| Valor | Comportamiento |
|---|---|
"" (auto) | Sin directiva. El LLM espeja el idioma del usuario. Default legacy. |
en | Añade una directiva Respond in English al system prompt. |
zh | Añade una directiva 「请用中文回复」 al system prompt. |
Esto es independiente del locale de UI (env ONGRID_DEFAULT_LOCALE o idioma del navegador). Un canal IM siempre gana para los mensajes recibidos a través de él. Ver el feedback de locale de salida de IA para la regla de fallback en triggers automáticos sobre salidas iniciadas por el manager.
allow_from
La allowlist de remitentes. Requerida para Telegram y Slack; opcional para Feishu/DingTalk. La parsea exactamente una vez ParseAllowFrom, compartida entre el validator y el loop de poll/stream de cada provider para que la regla de parse tenga una única definición.
Sintaxis. Coma, espacio, salto de línea, tab, punto y coma — cualquier combinación separa tokens. Se preserva el orden, se eliminan duplicados. Los prefijos telegram: y tg: se eliminan silenciosamente (compatibilidad con OpenClaw).
Formato por-provider.
- Telegram. IDs numéricos de usuario solamente. Los tokens no numéricos y los valores negativos (IDs de grupo de chat) se descartan en tiempo de validate. Se requiere al menos un ID — el bot es públicamente descubrible por username, así que una allowlist vacía dejaría que cualquier usuario de Telegram comandara un agente equipado con tools. Ver ADR-031.
- Slack. IDs de usuario empezando con
U(oWpara invitados Enterprise). Se requiere al menos uno. Encuentra el tuyo haciendo clic en tu perfil del workspace →⋯→ Copy member ID. - Feishu / DingTalk. Opcional. Estas plataformas están gated por membresía de tenant empresarial; solo los miembros de la organización pueden alcanzar al bot.
Una allowlist vacía para Telegram o Slack se rechaza
El validator devuelve telegram requires allow_from / slack requires allow_from cuando la lista parseada está vacía. No hay modo "deny por defecto pero permite setup después". El bot es públicamente alcanzable; el operador debe abrir la puerta conscientemente.
El modo de falla es drop silencioso. Un mensaje de un remitente no allowlisted se loguea en WARN con el user_id del remitente, luego se descarta. Sin respuesta, sin placeholder, sin ejecución del agente. El bot ni siquiera confirma que existe — la misma forma que dmPolicy: allowFrom de OpenClaw.
Cómo se ve una entrega de alerta disparada
alert evaluator → produces notify.Message
↓
notify.Sender (per channel) ↓ buildBody(msg) → JSON payload
↓ signTarget(endpoint, secret, body) → headers / URL params
↓ POST endpoint
↓ resp.StatusCode in [200, 299] → success
↓
chat surfacePara detalle más profundo ver las páginas por-canal: