Pular para o conteúdo

Ponte de chat

Provedor Valor serializado Transporte Tipo de espaço
Slack slack Socket Mode (WSS de saída) slack

O Slack é o único provedor. ChatProvider é um enum de valor único e o cc_server registra um único plugin. O esquema, todas as consultas e todos os índices únicos têm como chave provider, então os ids nunca cruzam entre provedores e adicionar um é uma mudança somente no servidor.

Um app do lado do provedor por espaço de trabalho × provedor.

O nome do comando é por app (cc por padrão, então /cc). Um admin pode renomeá-lo; a ponte então usa aquele com que o comando foi invocado por último.

Comando Efeito
/cc help (ou /cc sem verbo) Lista o que o bot pode fazer
/cc link <code> Resgata um código de uso único e vincula a identidade de chat do remetente
/cc ticket <title> Abre um ticket local relatado pelo membro vinculado
/cc ticket <title> | <description> O mesmo, com tudo o que vem depois do primeiro | como descrição
Verbo desconhecido Informa o verbo desconhecido junto com o texto de ajuda

Formas de alcançar um agente:

Gesto Comportamento
@bot … em um canal Abre um thread do Slack e um espaço correspondente no Control Center; o agente responde no thread do Slack
Resposta em um thread do Slack conectado por ponte Continua o mesmo espaço do Control Center — não é preciso mencionar
Mensagem comum em um canal Ignorada (não é dirigida ao bot e não está em um thread do Slack conectado por ponte)
Mensagem direta ao bot Um espaço do Control Center por conversa por mensagem direta; define um título de thread do Slack e um status “está pensando…” enquanto o agente trabalha

Declarados pelo descritor do provedor, que é o que gera o diálogo de conexão e valida o envio.

ID do campo Rótulo Obrigatório Prefixo Onde fica
botToken Token OAuth do usuário bot Sim xoxb- OAuth & Permissions
appToken Token de aplicação Sim xapp- Basic Information → App-Level Tokens, escopo connections:write
configRefreshToken Token de atualização da configuração do app Não xoxe Tokens de configuração do app em api.slack.com/apps

configRefreshToken é a credencial de gerenciamento do app: sem ela a conexão funciona, mas o bot não pode ser criado nem editado a partir do Control Center. Ela rotaciona a cada uso e o valor rotacionado é armazenado imediatamente.

Os valores têm os espaços das pontas removidos, as chaves não declaradas são descartadas e um prefixo divergente é recusado com a mesma frase no cliente e no servidor. Os campos obrigatórios precisam estar presentes em toda conexão; um campo opcional deixado vazio mantém o que já estava armazenado.

Escopos do token de bot:

app_mentions:read
assistant:write
channels:history
chat:write
commands
groups:history
im:history
users:read
users:read.email

Assinaturas de evento do bot:

app_home_opened
app_mention
message.channels
message.groups
message.im

Configurações estruturais que o Control Center afirma quando cria ou edita o app: Socket Mode ativado, nenhuma URL de solicitação, a aba Messages ativada e gravável, o bot always_online, os escopos e eventos necessários mesclados ao que o app já tem (nunca subtraídos) e um comando slash com a descrição File a ticket or link your Control Center account — o marcador que identifica o comando do próprio Control Center em um manifesto exportado.

Estado Valor serializado Significado
Não conectado disconnected Nenhuma credencial armazenada para este espaço de trabalho × provedor
Conectando… connecting Credenciais armazenadas, transporte discando ou rediscando
Ao vivo connected Transporte ativo e recebendo eventos
Erro de conexão error Armazenadas, mas com falha; o erro do próprio provedor é mostrado ao lado

A reconexão é backoff exponencial com jitter limitado a 30 segundos. Uma rotação de socket iniciada pelo provedor reconecta prontamente sem escalar o backoff. Uma credencial rejeitada (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) ou um escopo ausente é terminal: a ponte para de tentar novamente até que novas credenciais cheguem.

A ponte é escrita contra a superfície mais rica e regride capacidade a capacidade.

Capacidade Slack Efeito quando ausente
streaming sim A resposta inteira é publicada uma única vez, no final
streamingRequiresThread sim Um destino de resposta sem âncora de thread publica respostas inteiras
ephemeralMessages sim Uma resposta de comando slash sem um identificador de resposta utilizável é descartada (as recusas sempre são publicadas no thread)
threadStatus sim Nenhuma linha transitória “está pensando…”
threadTitle sim O thread de chat mantém seu título padrão
slashCommands sim Nenhuma superfície /cc
taskCards sim Somente texto — nenhum cartão acima da resposta e nenhum link “View in Control Center”
maxMessageLength 39.000 Respostas mais longas são truncadas com um ponteiro para o Control Center
maxStreamChunkLength 3.800 Os acréscimos são divididos em um limite de parágrafo, linha ou espaço

Anunciar streaming não é prometer streaming: o do Slack precisa do recurso Agents & AI Apps em um plano pago. Uma recusa no momento da chamada (streaming_not_supported, feature_not_enabled, paid_only, …) é memorizada por toda a vida da conexão e exposta como streamingAvailable: false. Os cartões de tarefa do Slack viajam na mesma chamada, então um Slack que recusa o modo de exibição do cartão mantém seu stream e perde apenas o cartão, uma vez, por toda a vida da conexão.

Um stream também precisa nomear o leitor a quem se destina. O Slack exige tanto um usuário destinatário quanto a equipe dele para abrir um em um canal (uma mensagem direta não exige) e recusa com missing_recipient_user_id / missing_recipient_team_id caso contrário — uma recusa por chamada, então a ponte continua tentando e, enquanto isso, o turno chega como uma única resposta publicada. O destinatário é quem perguntou, extraído da mensagem de entrada (a equipe dele, não a do app, para que um canal do Slack Connect seja resolvido corretamente). Para um turno que ninguém iniciou a partir do chat — trabalho iniciado no Control Center em uma conversa já conectada por ponte —, é o membro cujo vínculo criou a ponte; se ele desde então desvinculou sua conta de chat, esse turno é publicado inteiro em vez de transmitido.

Um cartão agrupado por turno do agente (task_display_mode: plan), com uma linha por etapa. Cada linha é um task_update de id + title + status, mais um details de envio único quando o pensamento está pronto. details e output de mesmo id são concatenados sem separador, então os details são enviados uma vez e o output nunca é transmitido em stream. O título do plano é a solicitação; a resposta é um acréscimo posterior separado que carrega um trecho markdown_text.

Parte Conteúdo
Título do plano O que foi pedido
Linha de preparação Working on it…, depois Cloning …, depois Starting the agent… — uma linha, com o título substituído
Linha de pensamento Thinking…, acrescentada quando o agente começa a raciocinar; o pensamento são os details desta linha, enviados uma vez quando o raciocínio termina
Linhas de ferramenta Uma por ferramenta (Bash which swift), com id único para que fiquem lado a lado
Status Em andamento enquanto essa linha é executada; concluído quando a próxima linha começa ou o turno termina; erro para falha, interrupção e limite de turnos
Chamada para ação View in Control Center como linha final quando o turno termina — ausente enquanto o trabalho ainda está em execução e ausente quando o servidor não tem uma URL pública alcançável

O que um cartão nunca carrega: saída de ferramentas, um argumento fora dessa lista de permissões ou qualquer coisa lida do banco de dados. A prosa de raciocínio são apenas os detalhes da linha Thinking… (limitados a 256 caracteres), não o corpo da mensagem. Um cartão que não diria nada de novo não é reenviado. Uma menção abre o stream imediatamente para que o thread do Slack não fique em silêncio; um turno que depois não produziu texto nenhum encerra esse mesmo cartão com uma nota breve em vez de publicar uma segunda mensagem. Um turno sem palavras em um espaço que nunca abriu um stream (sem cartões, ou com o streaming já recusado) ainda publica uma mensagem que carrega o cartão concluído e a nota.

Cartões transmitidos e publicados são cargas úteis diferentes:

Forma Estrutura Prosa
Transmitido plan_update mais trechos task_update com chave id String simples, limitada a 256 caracteres por trecho
Publicado Um bloco plan que envolve task_cards com chave task_id Texto rico

Tratamento de falhas:

Situação Comportamento
O provedor recusa o cartão em um acréscimo O acréscimo é repetido imediatamente sem ele; a prosa continua chegando ao vivo e os cartões são descartados por toda a vida da conexão
O provedor também recusa a prosa O stream é fechado e a resposta é publicada inteira
O stream se rompe depois que parte da resposta chegou O stream é fechado e nada é publicado de novo

Consulte Pontes de chat para saber por que a carga útil é dividida assim.

O cartão abre quando uma menção é aceita, antes do clone e antes do primeiro token; o turno que se segue adota o mesmo cartão e o mesmo stream. A narração a partir daí vem de SpaceProvisioningChanged no barramento de eventos de domínio — o mesmo progresso que o banner de provisionamento do desktop lê da linha do espaço.

Estado de provisionamento Narração
Menção aceita Working on it…
Etapa repo Cloning <repo>…
Etapa prCheckout Checking out the pull request in <repo>…
Etapa script Running the setup script for <repo>…
Etapa agent Setting up <agent>…
provisioning, ainda sem etapa Preparing the workspace…
ready Starting the agent…
O agente raciocinando Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

A narração da preparação exige todas as quatro condições:

Condição
O provedor renderiza cartões
O streaming está disponível nesta conexão
O espaço tem uma solicitação de chat em andamento
Nenhum cartão já está relatando um turno ali

A solicitação permanece em andamento até que um turno comece ou a preparação falhe. Um evento ready sem nenhum cartão já aberto é um espaço já preparado e é deixado em silêncio, e um provisionamento que ninguém no chat está esperando — uma conversa aberta no app, uma abandonada retomada na inicialização — não produz cartão nenhum.

Produtos de chat aceitam http(s) em um link e nada mais, então uma chamada para ação não pode nomear o esquema do desktop diretamente.

Propriedade Valor
Rota GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
Origem Derivada da URL pública do servidor (ws → http, wss → https); o loopback é mantido
Resposta Uma pequena página HTML no-store que navega para control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> e mostra um botão manual
Validação Os ids devem corresponder a [A-Za-z0-9_.-]{1,128}; qualquer outra coisa é um 404
Acesso Pública e inerte — não lê nenhum banco de dados, então não é um oráculo de existência; o app que ela abre é quem faz a autorização
Campo Limite Observações
appName 1–35 caracteres Padrão: Control Center · <workspace name>
botDisplayName 1–80 caracteres Padrão: control-center
description ≤ 140 caracteres
agentDescription ≤ 300 caracteres Mostrado acima de uma mensagem direta com o bot
command ≤ 32 caracteres Normalizado: as barras iniciais são removidas, passa para minúsculas, [^a-z0-9_-] é removido e vazio recorre a cc
agentEnabled — Se a experiência de mensagem direta e de agente é exposta

Uma caixa vazia significa “deixe como está”, não “limpe”. Os limites são os do Slack, que são os mais restritos dos produtos modelados.

Propriedade Valor
Comprimento 6 caracteres
Alfabeto 23456789ABCDEFGHJKLMNPQRSTVWXYZ (sem 0/O, 1/I, U)
Tempo de vida 15 minutos
Usos Um
Armazenamento Em memória — reiniciar o servidor invalida os códigos pendentes
Abrangência Um espaço de trabalho e um provedor; gerar um novo código invalida o anterior do membro

O resgate é correspondido sem diferenciar maiúsculas de minúsculas. Códigos desconhecidos, expirados, de outro espaço de trabalho e do provedor errado falham todos de forma idêntica de propósito.

Método Valor serializado Como
Código code O membro resgatou /cc link <code>
E-mail email O e-mail verificado do provedor correspondeu a um usuário que já é membro do espaço de trabalho

Todas têm escopo de espaço de trabalho. O servidor não tem estado, então cada chamada carrega seu próprio workspace_id nos argumentos; o dispatcher recusa uma operação com escopo de espaço de trabalho que o omita, antes que qualquer banco de dados seja aberto. As credenciais viajam apenas para dentro — leituras de status nunca as retornam.

Operação Tipo Papel mínimo Classes de ação
chat.providers leitura guest —
chat.status leitura guest —
chat.connect mutação admin secretAccess, networkEgress
chat.disconnect destrutiva admin secretAccess
chat.listUserLinks leitura guest —
chat.beginUserLink mutação member —
chat.unlinkUser mutação member para você mesmo, admin para qualquer outra pessoa —
chat.createApp mutação admin secretAccess, networkEgress
chat.setupLink leitura admin —
chat.botProfile leitura admin networkEgress
chat.updateBotProfile mutação admin networkEgress

As operações de leitura herdam o piso guest do seu tipo; mutações sem um piso explícito exigem member.

chat.setupLink compõe uma URL de criação de app do provedor a partir do perfil do bot. Ele não declara nenhuma classe de ação porque não lê nenhum segredo e não disca nada — o navegador do próprio usuário faz a única chamada.

Uma consulta reativa completa a superfície:

Consulta Tipo Emite
chat.watchUserLinks observação A lista, reenviada sempre que um vínculo é criado ou removido

Ela existe porque a vinculação acontece no outro lado: um membro digita o código de uso único no bot, então nenhuma resposta do cliente pode levar a resposta de volta. A lista de configurações e o diálogo “vincular minha conta” aberto seguem ambos essa consulta, e o diálogo muda para uma confirmação no momento em que o vínculo do próprio membro chega.

chat.beginUserLink sempre gera um código para a própria identidade de quem chama. chat.updateBotProfile retorna um remaining_step somente quando resta uma etapa (normalmente uma reinstalação); sua ausência significa que a edição já está ativa.

chat.providers retorna uma entrada por provedor oferecido — o descritor e, quando existe, o status da conexão. Tudo o que a interface de configurações renderiza vem dessa carga útil, e é por isso que adicionar um provedor é uma mudança somente no servidor.

O quê Onde
Credenciais de conexão <dataDir>/<workspaceId>/chat_credentials/<provider>.json
Configuração guiada pela metade <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
Arquivos legados do Slack <dataDir>/<workspaceId>/slack_credentials.json e slack_app_setup.json, lidos uma vez e migrados no lugar

As credenciais ficam ao lado do banco de dados do espaço de trabalho, não dentro dele: excluir um espaço de trabalho leva junto seus tokens de chat, e workspace.export ou um backup copia workspace.db — então um espaço de trabalho entregue a outra pessoa não carrega tokens de bot ativos. As gravações são atômicas (arquivo temporário mais renomeação) e restritas ao proprietário.

Ambas ficam no banco de dados do espaço de trabalho, que carrega uma linha de base v1 consolidada — então uma leitura entre espaços de trabalho não compila.

chat_space_links — uma conversa externa ↔ um espaço do Control Center:

Coluna Observações
id UUID v4
workspace_id Escopo do espaço de trabalho
provider ChatProvider.wire
external_team_id Equipe do Slack T…, vazio para provedores sem o conceito
external_channel_id Canal do Slack C…, grupo G…, mensagem direta D…
external_thread_id Âncora de thread do Slack, ou null quando a conversa inteira é a âncora (mensagens diretas do bot)
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id, created_at, last_activity_at

Único em (workspace_id, provider, external_channel_id, external_thread_id), com um índice parcial impondo o mesmo para ids de thread nulos (o SQLite trata NULLs como distintos).

chat_user_links — um membro externo ↔ um usuário do Control Center:

Coluna Observações
id UUID v4
workspace_id Escopo do espaço de trabalho
provider ChatProvider.wire
external_team_id, external_user_id Membro do Slack U…
user_id Usuário do Control Center (global, então sem chave estrangeira)
method code ou email
linked_at

Único em (workspace_id, provider, external_team_id, external_user_id) e em (workspace_id, provider, user_id) — atribuição de valor único nas duas direções, enquanto uma pessoa pode estar vinculada em vários provedores.

Um vínculo é resolução de identidade, não uma concessão de acesso: a associação é verificada de novo a cada evento de entrada, então um vínculo com um membro que desde então foi removido resulta em recusa.

Um espaço conectado por ponte é criado com o SpaceKind do provedor (slack). Ele se comporta como um espaço comum voltado a pessoas — notificado e seccionado como topic — e isExternalChat é a verificação pela qual passa cada comportamento (espelhamento, emblemas, retenção).

Toda mensagem que a ponte traz carrega um carimbo metadata['chat']:

{
"chat": {
"provider": "slack",
"teamId": "T024BE7LD",
"channelId": "C0G9QF9GW",
"threadId": "1700000000.000100",
"messageId": "1700000000.000200",
"userId": "U024BE7LH",
"userLabel": "sam"
}
}

O carimbo também é a proteção contra eco: o espelhamento de saída ignora qualquer mensagem que o carregue, e é isso que impede que uma mensagem de chat de entrada seja publicada direto de volta.

Comportamento Detalhe
O que é retransmitido para fora Apenas o texto do agente, mais a linha de status do cartão de tarefa. A prosa de raciocínio e a saída de ferramentas ficam no Control Center
Cadência do streaming Limitada a uma descarga por segundo enquanto um turno é executado; o cartão e o texto que ele explica viajam na mesma chamada
Mensagens humanas digitadas no Control Center Espelhadas no thread vinculado, prefixadas com o nome de exibição do autor
Turnos vazios Relatados como uma nota breve (a execução falhou, foi interrompida ou parou no limite de turnos) em vez de silêncio
Deduplicação Com chave no id de evento do próprio provedor; os últimos 512 são lembrados
Agente de entrada Uma nova conversa conectada por ponte desperta o agente CEO do espaço de trabalho; se não houver, seu primeiro agente; se não houver, ninguém
Nomes dos espaços #channel · <request> para um thread de canal do Slack e Slack DM · <request> para uma mensagem direta; os títulos são limitados a 48 caracteres
Isolamento de falhas Um evento ruim nunca derruba a ponte; a conexão continua atendendo todas as outras conversas