Ponte de chat
Provedores
Seção intitulada “Provedores”| 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.
Comandos de chat
Seção intitulada “Comandos de chat”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 |
Campos de credencial (Slack)
Seção intitulada “Campos de credencial (Slack)”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.
Requisitos do app do Slack
Seção intitulada “Requisitos do app do Slack”Escopos do token de bot:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailAssinaturas de evento do bot:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imConfiguraçõ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.
Estados de conexão
Seção intitulada “Estados de conexão”| 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.
Capacidades do provedor (Slack)
Seção intitulada “Capacidades do provedor (Slack)”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.
Cartões de tarefa
Seção intitulada “Cartões de tarefa”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.
Preparação, antes de haver um turno
Seção intitulada “Preparação, antes de haver um turno”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.
A página de redirecionamento /open
Seção intitulada “A página de redirecionamento /open”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 |
Campos do perfil do bot
Seção intitulada “Campos do perfil do bot”| 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.
Códigos de vinculação
Seção intitulada “Códigos de vinculação”| 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étodos de vinculação
Seção intitulada “Métodos de vinculação”| Método | Valor serializado | Como |
|---|---|---|
| Código | code |
O membro resgatou /cc link <code> |
email |
O e-mail verificado do provedor correspondeu a um usuário que já é membro do espaço de trabalho |
Operações RPC
Seção intitulada “Operações RPC”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.
Armazenamento
Seção intitulada “Armazenamento”| 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.
Tabelas do banco de dados
Seção intitulada “Tabelas do banco de dados”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.
Tipo de espaço e metadados de mensagem
Seção intitulada “Tipo de espaço e metadados de mensagem”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 do relay
Seção intitulada “Comportamento do relay”| 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 |
Veja também
Seção intitulada “Veja também”- Configurar a integração com o Slack
- Vincular sua conta do Slack
- Personalizar o bot de chat
- Pontes de chat
- Barreiras para as classes de ação que as operações declaram