Saltar para o conteúdo

Ponte de chat

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

O Slack é o único fornecedor. ChatProvider é um enum de valor único e o cc_server regista um plugin. O esquema, todas as consultas e todos os índices únicos são indexados por provider, pelo que os ids nunca atravessam fornecedores e acrescentar um é uma alteração apenas no servidor.

Uma aplicação do lado do fornecedor por espaço de trabalho × fornecedor.

O nome do comando é por aplicação (cc por predefinição, logo /cc). Um administrador pode renomeá-lo; a ponte passa então a usar aquele com que foi invocado pela última vez.

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

Formas de chegar a um agente:

Gesto Comportamento
@bot … num canal Abre uma thread do Slack e um espaço correspondente no Control Center; o agente responde na thread do Slack
Resposta numa thread do Slack com ponte Continua o mesmo espaço do Control Center — não é precisa nenhuma menção
Mensagem simples num canal Ignorada (não é dirigida ao bot nem está numa thread do Slack com 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 estado «is thinking…» enquanto o agente trabalha

Declarados pelo descritor do fornecedor, que é o que gera a caixa de diálogo de ligação e valida a submissão.

Id do campo Etiqueta Obrigatório Prefixo Onde fica
botToken Token OAuth de utilizador de bot Sim xoxb- OAuth & Permissions
appToken Token ao nível da aplicação Sim xapp- Basic Information → App-Level Tokens, escopo connections:write
configRefreshToken Token de atualização da configuração da aplicação Não xoxe App configuration tokens em api.slack.com/apps

configRefreshToken é a credencial de gestão da aplicação: sem ela a ligação funciona, mas o bot não pode ser criado nem editado a partir do Control Center. Roda a cada utilização e o valor rodado é guardado de imediato.

Os valores são aparados, as chaves não declaradas são descartadas e uma incompatibilidade de prefixo é recusada com a mesma frase no cliente e no servidor. Os campos obrigatórios têm de estar presentes em cada ligação; um campo opcional deixado em branco mantém o que já estava guardado.

Escopos do token de bot:

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

Subscrições de eventos do bot:

app_home_opened
app_mention
message.channels
message.groups
message.im

Definições estruturais que o Control Center impõe quando cria ou edita a aplicação: Socket Mode ativado, nenhum URL de pedido, o separador Messages ativado e editável, o bot always_online, os escopos e eventos necessários fundidos no que a aplicação já tiver (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 próprio comando do Control Center num manifesto exportado.

Estado Valor serializado Significado
Não ligado disconnected Nenhuma credencial guardada para este espaço de trabalho × fornecedor
A ligar… connecting Credenciais guardadas, o transporte a estabelecer ou a restabelecer a ligação
Ao vivo connected Transporte ativo e a receber eventos
Erro de ligação error Guardadas mas a falhar; o erro do próprio fornecedor é mostrado ao lado

A reconexão é um backoff exponencial com jitter limitado a 30 segundos. Uma rotação de socket iniciada pelo fornecedor reconecta prontamente sem agravar o backoff. Uma credencial rejeitada (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) ou um escopo em falta é terminal: a ponte deixa de tentar novamente até chegarem novas credenciais.

A ponte está escrita para a superfície mais rica e degrada capacidade a capacidade.

Capacidade Slack Efeito quando ausente
streaming sim A resposta inteira é publicada uma só vez, no fim
streamingRequiresThread sim Um alvo de resposta sem âncora de thread publica respostas inteiras
ephemeralMessages sim Uma resposta a um comando slash sem um identificador de resposta utilizável é descartada (as recusas são sempre publicadas na thread)
threadStatus sim Nenhuma linha transitória «is thinking…»
threadTitle sim A thread de chat mantém o seu título predefinido
slashCommands sim Nenhuma superfície /cc
taskCards sim Apenas texto — nenhum cartão por cima da resposta e nenhuma ligação «View in Control Center»
maxMessageLength 39,000 As respostas mais longas são truncadas com uma referência ao Control Center
maxStreamChunkLength 3,800 Os acrescentos são divididos num limite de parágrafo, de linha ou de espaço

Anunciar streaming não é prometê-lo: o do Slack precisa da funcionalidade Agents & AI Apps num plano pago. Uma recusa no momento da chamada (streaming_not_supported, feature_not_enabled, paid_only, …) é memorizada durante toda a vida da ligação e apresentada como streamingAvailable: false. Os cartões de tarefa do Slack viajam na mesma chamada, por isso um Slack que recuse o modo de apresentação do cartão mantém o seu fluxo e perde apenas o cartão, uma vez, durante toda a vida da ligação.

Um fluxo também tem de nomear o leitor a que se destina. O Slack exige tanto um utilizador destinatário como a respetiva equipa para abrir um fluxo num canal (uma mensagem direta não) e recusa com missing_recipient_user_id / missing_recipient_team_id caso contrário — uma recusa por chamada, pelo que a ponte continua a tentar e, entretanto, o turno chega como uma única resposta publicada. O destinatário é quem perguntou, retirado da mensagem recebida (a equipa do utilizador, não a da aplicação, para que um canal do Slack Connect seja resolvido corretamente). Para um turno que ninguém iniciou a partir do chat — trabalho lançado no Control Center sobre uma conversa que já tem ponte — é o membro cuja associação criou a ponte; se entretanto desassociou a sua conta de chat, esse turno é publicado inteiro em vez de ser transmitido em fluxo.

Um cartão agrupado por turno do agente (task_display_mode: plan), com uma linha por passo. Cada linha é um task_update de id + title + status, mais um details de uso único quando o pensamento está pronto. details e output com o mesmo id concatenam-se sem separador, pelo que os details são enviados uma vez e o output nunca é transmitido em fluxo. O título do plano é o pedido; a resposta é um acrescento posterior separado que transporta um fragmento 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 raciocínio 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 ferramentas Uma por ferramenta (Bash which swift), com id único para que fiquem lado a lado
Estado Em curso enquanto essa linha corre; completo quando a próxima linha começa ou o turno termina; erro em caso de falha, interrupção e limite de turnos
Apelo à ação View in Control Center como linha final quando o turno termina — ausente enquanto o trabalho ainda corre e ausente quando o servidor não tem nenhum URL público acessível

O que um cartão nunca transporta: saída de ferramentas, um argumento fora dessa lista de autorizações, ou qualquer coisa lida da base de dados. A prosa de raciocínio são apenas os details 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 fluxo de imediato para que a thread do Slack não fique silenciosa; um turno que depois não produziu texto nenhum conclui esse mesmo cartão com uma nota curta em vez de publicar uma segunda mensagem. Um turno sem palavras num espaço que nunca abriu um fluxo (sem cartões, ou com o streaming já recusado) publica na mesma uma mensagem que transporta o cartão concluído e a nota.

Os cartões transmitidos em fluxo e os publicados são payloads diferentes:

Forma Estrutura Prosa
Transmitido em fluxo plan_update mais fragmentos task_update com a chave id Cadeia simples, limitada a 256 caracteres por fragmento
Publicado Um bloco plan que envolve task_cards com a chave task_id Texto formatado

Tratamento de falhas:

Situação Comportamento
O fornecedor recusa o cartão num acrescento O acrescento é repetido de imediato sem ele; a prosa continua a chegar ao vivo e os cartões são descartados durante toda a vida da ligação
O fornecedor também recusa a prosa O fluxo é fechado e a resposta é publicada inteira
O fluxo parte-se depois de parte da resposta ter chegado O fluxo é fechado e não é republicado nada

Ver Pontes de chat para saber por que razão o payload é dividido desta forma.

O cartão abre quando uma menção é aceite, antes do clone e antes do primeiro token; o turno que se segue adota o mesmo cartão e o mesmo fluxo. A narração a partir daí vem de SpaceProvisioningChanged no barramento de eventos de domínio — o mesmo progresso que a faixa de preparação da aplicação para computador lê da linha do espaço.

Estado de preparação Narração
Menção aceite Working on it…
Passo repo Cloning <repo>…
Passo prCheckout Checking out the pull request in <repo>…
Passo script Running the setup script for <repo>…
Passo agent Setting up <agent>…
provisioning, ainda sem passo Preparing the workspace…
ready Starting the agent…
Agente a raciocinar 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 as quatro condições:

Condição
O fornecedor apresenta cartões
O streaming está disponível nesta ligação
O espaço tem um pedido de chat em curso
Nenhum cartão está já a reportar um turno aí

O pedido permanece em curso até começar um turno ou falhar a preparação. Um evento ready sem nenhum cartão já aberto é um espaço já preparado e é deixado em silêncio, e uma preparação que ninguém no chat está à espera — uma conversa aberta na aplicação, uma que ficou pendente e é retomada no arranque — não produz cartão nenhum.

Os produtos de chat aceitam http(s) numa ligação e nada mais, por isso um apelo à ação não pode nomear diretamente o esquema da aplicação para computador.

Propriedade Valor
Rota GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
Origem Derivada do URL público 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 têm de corresponder a [A-Za-z0-9_.-]{1,128}; tudo o resto é um 404
Acesso Pública e inerte — não lê nenhuma base de dados, por isso não é um oráculo de existência; é a aplicação que abre que faz a autorização
Campo Limite Notas
appName 1–35 caracteres Por predefinição, Control Center · <workspace name>
botDisplayName 1–80 caracteres Por predefinição, control-center
description ≤ 140 caracteres
agentDescription ≤ 300 caracteres Mostrado por cima de uma mensagem direta ao bot
command ≤ 32 caracteres Normalizado: barras iniciais removidas, convertido em minúsculas, [^a-z0-9_-] removido, e se ficar vazio volta a cc
agentEnabled — Se a experiência de mensagem direta/agente é exposta

Uma caixa vazia significa «deixar como está», não «limpar». Os limites são do Slack, que são os mais restritos dos produtos modelados.

Propriedade Valor
Comprimento 6 caracteres
Alfabeto 23456789ABCDEFGHJKLMNPQRSTVWXYZ (sem 0/O, 1/I, U)
Validade 15 minutos
Utilizações Uma
Armazenamento Em memória — um reinício do servidor invalida os códigos pendentes
Âmbito Um espaço de trabalho e um fornecedor; emitir um novo código invalida o anterior do membro

O resgate é validado sem distinguir maiúsculas de minúsculas. Códigos desconhecidos, expirados, de outro espaço de trabalho e de outro fornecedor falham todos de forma idêntica, de propósito.

Método Valor serializado Como
Código code O membro resgatou /cc link <code>
Email email O email verificado do fornecedor correspondeu a um utilizador que já é membro do espaço de trabalho

Todas têm âmbito de espaço de trabalho. O servidor não tem estado, por isso cada chamada transporta o seu próprio workspace_id nos argumentos; o despachante recusa uma operação com âmbito de espaço de trabalho que o omita, antes de abrir qualquer base de dados. As credenciais só viajam para dentro — as leituras de estado nunca as devolvem.

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 si próprio, 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; as mutações sem um piso explícito exigem member.

chat.setupLink compõe um URL de criação de aplicação do fornecedor a partir do perfil do bot. Não declara nenhuma classe de ação porque não lê nenhum segredo nem estabelece nenhuma ligação — é o navegador do próprio utilizador que faz a única chamada.

Uma consulta reativa completa a superfície:

Consulta Tipo Emite
chat.watchUserLinks observação A lista, reenviada sempre que uma associação é criada ou removida

Existe porque a associação acontece no outro lado: um membro escreve o código de uso único no bot, por isso nenhuma resposta do cliente pode transportar a resposta de volta. A lista nas definições e a caixa de diálogo «associar a minha conta» aberta seguem ambas esta consulta, e a caixa de diálogo passa a uma confirmação no momento em que a associação do próprio membro chega.

chat.beginUserLink emite sempre um código para a própria identidade de quem chama. chat.updateBotProfile devolve um remaining_step apenas quando resta um passo (tipicamente uma reinstalação); a sua ausência significa que a edição já está em vigor.

chat.providers devolve uma entrada por fornecedor oferecido — o descritor e, quando existe, o estado da ligação. Tudo o que a interface de definições apresenta vem desse payload, e é por isso que acrescentar um fornecedor é uma alteração apenas no servidor.

O quê Onde
Credenciais da ligação <dataDir>/<workspaceId>/chat_credentials/<provider>.json
Configuração guiada a meio <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
Ficheiros do Slack antigos <dataDir>/<workspaceId>/slack_credentials.json e slack_app_setup.json, lidos uma vez e migrados no local

As credenciais vivem ao lado da base de dados do espaço de trabalho, não dentro dela: eliminar um espaço de trabalho leva consigo os seus tokens de chat, e workspace.export ou uma cópia de segurança copiam workspace.db — por isso um espaço de trabalho entregue a outra pessoa não transporta tokens de bot ativos. As escritas são atómicas (ficheiro temporário mais renomeação) e ficam restritas ao proprietário.

Ambas vivem na base de dados do espaço de trabalho, que transporta uma linha de base v1 consolidada — por isso uma leitura entre espaços de trabalho não compila.

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

Coluna Notas
id UUID v4
workspace_id Âmbito do espaço de trabalho
provider ChatProvider.wire
external_team_id Equipa do Slack T…, vazio para fornecedores sem o conceito
external_channel_id Canal do Slack C…, grupo G…, mensagem direta D…
external_thread_id Âncora de thread do Slack, ou nulo quando a conversa inteira é a âncora (mensagens diretas ao 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 que impõe o mesmo para ids de thread nulos (o SQLite trata os NULL como distintos).

chat_user_links — um membro externo ↔ um utilizador do Control Center:

Coluna Notas
id UUID v4
workspace_id Âmbito do espaço de trabalho
provider ChatProvider.wire
external_team_id, external_user_id Membro do Slack U…
user_id Utilizador do Control Center (global, logo sem chave externa)
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 nos dois sentidos, enquanto uma pessoa pode estar associada em vários fornecedores.

Uma associação é resolução de identidade, não uma concessão de acesso: a pertença é verificada de novo em cada evento recebido, por isso uma associação a um membro que entretanto foi removido resolve-se numa recusa.

Um espaço com ponte é criado com o SpaceKind do fornecedor (slack). Comporta-se como um espaço comum virado para pessoas — notificado e seccionado como topic — e isExternalChat é a verificação por que passa cada comportamento (espelhamento, distintivos, retenção).

Cada mensagem que a ponte traz para dentro transporta uma marca metadata['chat']:

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

A marca é também a guarda contra ecos: o espelho de saída ignora qualquer mensagem que a transporte, e é isso que impede que uma mensagem de chat recebida seja publicada logo de volta.

Comportamento Detalhe
O que é retransmitido para fora Apenas o texto do agente, mais a linha de estado do cartão de tarefa. A prosa de raciocínio e a saída de ferramentas ficam no Control Center
Cadência da transmissão em fluxo Limitada a um envio por segundo enquanto um turno corre; o cartão e o texto que ele explica viajam na mesma chamada
Mensagens humanas escritas no Control Center Espelhadas para a thread associada, com o nome de apresentação do autor como prefixo
Turnos vazios Reportados como uma nota curta (a execução falhou, foi interrompida ou parou no seu limite de turnos) em vez de silêncio
Deduplicação Com a chave no próprio id de evento do fornecedor; os últimos 512 são memorizados
Agente de entrada Uma nova conversa com ponte acorda o agente CEO do espaço de trabalho, senão o seu primeiro agente, senão ninguém
Nomeação do espaço #channel · <request> para uma thread de canal do Slack, Slack DM · <request> para uma mensagem direta, títulos limitados a 48 caracteres
Isolamento de falhas Um evento mau nunca deita a ponte abaixo; a ligação continua a servir todas as outras conversas