Skip to content

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_channels no DB e os Senders em internal/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_apps no DB e internal/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?

ObjetivoUseArquivo
Push de alertas disparados para um canal SlackSlack incoming webhook (Notify)internal/pkg/notify/webhook.go NewSlackSender
Push de alertas disparados para grupo Feishu/LarkBot custom Feishu (Notify)NewFeishuSender
Push de alertas disparados para grupo DingTalkBot custom DingTalk (Notify)NewDingTalkSender
Push de alertas disparados para grupo WeComBot de grupo WeCom (Notify)NewWeComSender
POST JSON genérico para seu próprio serviçoWebhook (Notify)NewGenericWebhookSender
Alertas Telegram (direção única)Bot Telegram sendMessage (Notify)NewTelegramSender
Falar com o agent do Slack como um botSlack Socket Mode app (IM bridge)imbridge/provider/slack/
Falar com o agent do TelegramBot Telegram getUpdates (IM bridge)imbridge/provider/telegram/
Falar com o agent do Feishu/LarksuiteFeishu 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

go
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

ProviderComo autentica
SlackA própria URL do webhook é o secret. Sem assinatura extra.
FeishuHMAC-SHA256(ts\nsecret), colocado no body JSON como sign.
DingTalkHMAC-SHA256(ts\nsecret), colocado na query URL como sign.
WeComChave do bot na query URL. Sem assinatura extra.
TelegramToken do bot no path (/bot<TOKEN>/sendMessage).
WebhookOpcional 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:

  1. Conecta outbound ao provider (websocket para Feishu/Slack, long poll para Telegram). Nenhuma porta inbound é aberta no manager.
  2. Recebe mensagens inbound do usuário, filtra através de allow_from, e entrega o texto a bizbridge.Bridge.HandleInbound.
  3. HandleInbound posta 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.

ValorComportamento
"" (auto)Nenhuma diretiva. O LLM espelha o idioma do usuário. Padrão legado.
enAdiciona uma diretiva Respond in English ao system prompt.
zhAdiciona 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 (ou W para 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

text
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 surface

Para detalhe mais profundo veja as páginas por canal: