Visão geral dos canais
O Ongrid fala com humanos através de canais. Um canal é qualquer um de:
- um canal de notificação — Ongrid empurra alertas para uma superfície de chat; o humano lê, mas o canal em si nunca carrega uma resposta de volta ao agent. Isto é
notify_channelsno DB e os Senders eminternal/pkg/notify/webhook.go. - um canal IM — Ongrid se senta na superfície de chat de um workspace como um bot, lê mensagens inbound, e roda o mesmo reasoning de agent que a web UI roda, fazendo stream da resposta de volta como edits a uma mensagem placeholder. Isto é
im_appsno DB einternal/manager/biz/imbridge/provider/*.
Os dois são independentes. Conectar alertas Telegram (sendMessage para fora) não deixa usuários chat com o agent no Telegram, e vice-versa.
Tabela diferente, credencial diferente, mesma UI
Ambas as superfícies são configuradas em Settings → Channels na web UI. Canais de notificação vão à aba Notify; canais IM vão à aba IM bridge.
Qual eu quero?
| Objetivo | Use | Arquivo |
|---|---|---|
| Push de alertas disparados para um canal Slack | Slack incoming webhook (Notify) | internal/pkg/notify/webhook.go NewSlackSender |
| Push de alertas disparados para grupo Feishu/Lark | Bot custom Feishu (Notify) | NewFeishuSender |
| Push de alertas disparados para grupo DingTalk | Bot custom DingTalk (Notify) | NewDingTalkSender |
| Push de alertas disparados para grupo WeCom | Bot de grupo WeCom (Notify) | NewWeComSender |
| POST JSON genérico para seu próprio serviço | Webhook (Notify) | NewGenericWebhookSender |
| Alertas Telegram (direção única) | Bot Telegram sendMessage (Notify) | NewTelegramSender |
| Falar com o agent do Slack como um bot | Slack Socket Mode app (IM bridge) | imbridge/provider/slack/ |
| Falar com o agent do Telegram | Bot Telegram getUpdates (IM bridge) | imbridge/provider/telegram/ |
| Falar com o agent do Feishu/Larksuite | Feishu long-connection (IM bridge) | imbridge/provider/feishu/ |
Um workspace pode rodar ambos: ex.: um Slack incoming webhook para alertas mais um Slack Socket Mode app separado para conversa. Eles não compartilham estado.
Canais de notificação
Um canal de notificação é um Sender stateless Send(ctx, Message). É chamado pelo pipeline de alerta sempre que um alerta dispara (ou recupera, ou a janela de dampening expira). Todos os tipos de canal renderizam o mesmo formato canônico notify.Message; só o formato de payload e o protocolo de assinatura diferem por provider.
Campos comuns que um Sender recebe
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
}O sender Slack renderiza isso no formato attachments com um trilho de cor matizado por severity, campos estruturados (Severity, Source, Rule, Incident, Device, Dedupe), e um footer ts. Os senders Feishu / DingTalk / WeCom achatam num payload de texto [SEV] subject\nbody\nsource:…\ndedupe:… porque suas APIs de bot só distribuem texto plano em v1.
Modelos de assinatura num vista d'olhos
| Provider | Como autentica |
|---|---|
| Slack | A própria URL do webhook é o secret. Sem assinatura extra. |
| Feishu | HMAC-SHA256(ts\nsecret), colocado no body JSON como sign. |
| DingTalk | HMAC-SHA256(ts\nsecret), colocado na query URL como sign. |
| WeCom | Chave do bot na query URL. Sem assinatura extra. |
| Telegram | Token do bot no path (/bot<TOKEN>/sendMessage). |
| Webhook | Opcional X-Ongrid-Signature: sha256=<HMAC> sobre o body. |
As implementações exatas vivem em internal/pkg/notify/webhook.go: signFeishu, signDingTalkURL, signGenericWebhook.
Slack descarta o campo secret silenciosamente
A URL do Slack incoming webhook é em si a credencial. Se você preencher o campo Secret para um canal Slack, o channel builder o descarta antes de construir o Sender. Isso é intencional — o protocolo do Slack não tem superfície separada de assinatura para incoming webhooks.
Canais IM
Um canal IM é uma goroutine de longa duração que:
- Conecta outbound ao provider (websocket para Feishu/Slack, long poll para Telegram). Nenhuma porta inbound é aberta no manager.
- Recebe mensagens inbound do usuário, filtra através de
allow_from, e entrega o texto abizbridge.Bridge.HandleInbound. HandleInboundposta uma resposta placeholder, roda o grafo completo do agent (o mesmo que a web UI usa), e streama edits de volta ao id da mensagem placeholder.
O mesmo kernel de agent, skills, e registry de personas alimenta tanto a web UI quanto os canais IM. Não existe "agent específico-de-IM" — o coordinator é o mesmo que você vê em /chat.
Inbound é específico-de-provider, outbound é uniforme
Diferenças de provider (ack envelope_id Slack, offset de update Telegram, encrypt_key Feishu) vivem em imbridge/provider/*/stream.go. Uma vez que uma mensagem está dentro de bizbridge.HandleInbound, tudo downstream é agnóstico-de-provider.
default_locale
Tanto linhas IM quanto Notify carregam um default_locale opcional. O validator aceita a string vazia (auto), en, ou zh apenas — en-US / zh-CN são dobrados ao seu subtag primário, e EN-us / um locale typo'd é rejeitado de cara em AppInput.validate.
| Valor | Comportamento |
|---|---|
"" (auto) | Nenhuma diretiva. O LLM espelha o idioma do usuário. Padrão legado. |
en | Adiciona uma diretiva Respond in English ao system prompt. |
zh | Adiciona uma diretiva 「请用中文回复」 ao system prompt. |
Isto é independente do locale da UI (env var ONGRID_DEFAULT_LOCALE ou idioma do browser). Um canal IM sempre vence para mensagens recebidas por ele. Veja o feedback de locale de saída de IA para a regra de fallback de auto-trigger em saídas iniciadas pelo manager.
allow_from
A allowlist de remetente. Obrigatória para Telegram e Slack; opcional para Feishu/DingTalk. É parseada exatamente uma vez por ParseAllowFrom, compartilhada entre o validator e o loop de poll/stream de cada provider para que a regra de parse tenha uma única definição.
Sintaxe. Vírgula, espaço, newline, tab, ponto-e-vírgula — qualquer combinação separa tokens. A ordem é preservada, duplicatas descartadas. Os prefixos telegram: e tg: são removidos silenciosamente (compatibilidade com OpenClaw).
Formato por provider.
- Telegram. Apenas IDs de usuário numéricos. Tokens não-numéricos e valores negativos (IDs de chat de grupo) são descartados no validate. Pelo menos um ID é obrigatório — o bot é publicamente descobrível por username, então uma allowlist vazia permitiria qualquer usuário Telegram comandar um agent equipado com tools. Veja ADR-031.
- Slack. IDs de usuário começando com
U(ouWpara Enterprise guests). Pelo menos um é obrigatório. Encontre o seu clicando no profile do workspace →⋯→ Copy member ID. - Feishu / DingTalk. Opcional. Essas plataformas são gateadas por membership de tenant-enterprise; só membros da org alcançam o bot.
Allowlist vazia para Telegram ou Slack é rejeitada
O validator retorna telegram requires allow_from / slack requires allow_from quando a lista parseada está vazia. Não existe modo "deny by default mas permitir setup depois". O bot é publicamente alcançável; o operador precisa conscientemente abrir a porta.
Modo de falha é silent drop. Uma mensagem de um sender não-allowlisted é logada em WARN com o user_id do sender, então descartada. Sem resposta, sem placeholder, sem run de agent. O bot nem confirma que existe — mesmo formato do OpenClaw dmPolicy: allowFrom.
Como uma entrega de alerta disparado se parece
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 detalhe mais profundo veja as páginas por canal: