Salta ai contenuti

Ponte di chat

Provider Valore serializzato Trasporto Tipo di spazio
Slack slack Socket Mode (WSS in uscita) slack

Slack è l’unico provider. ChatProvider è un enum a valore singolo e cc_server registra un solo plugin. Lo schema, ogni ricerca e ogni indice univoco hanno come chiave provider, quindi gli id non passano mai da un provider all’altro e aggiungerne uno è una modifica solo lato server.

Un’app lato provider per ogni spazio di lavoro × provider.

Il nome del comando è per app (cc per impostazione predefinita, quindi /cc). Un amministratore può rinominarlo; il ponte usa poi il nome con cui è stato invocato l’ultima volta.

Comando Effetto
/cc help (o /cc senza verbo) Elenca ciò che il bot può fare
/cc link <code> Riscatta un codice monouso e collega l’identità di chat del mittente
/cc ticket <title> Apre un ticket locale segnalato dal membro collegato
/cc ticket <title> | <description> Come sopra, con tutto ciò che segue il primo | come descrizione
Verbo sconosciuto Segnala il verbo sconosciuto insieme al testo di aiuto

Modi per raggiungere un agente:

Gesto Comportamento
@bot … in un canale Apre un thread di Slack e uno spazio di Control Center corrispondente; l’agente risponde nel thread di Slack
Risposta in un thread di Slack collegato Prosegue lo stesso spazio di Control Center — non serve alcuna menzione
Messaggio semplice in un canale Ignorato (non è rivolto al bot e non è in un thread di Slack collegato)
Messaggio diretto al bot Uno spazio di Control Center per ogni conversazione in messaggi diretti; imposta un titolo del thread di Slack e uno stato “sta pensando…” mentre l’agente lavora

Dichiarati dal descrittore del provider, che è ciò che genera la finestra di connessione e convalida l’invio.

Id del campo Etichetta Obbligatorio Prefisso Dove si trova
botToken Token OAuth dell’utente bot Sì xoxb- OAuth & Permissions
appToken Token a livello di app Sì xapp- Basic Information → App-Level Tokens, scope connections:write
configRefreshToken Token di aggiornamento della configurazione dell’app No xoxe App configuration tokens su api.slack.com/apps

configRefreshToken è la credenziale di gestione dell’app: senza di essa la connessione funziona, ma il bot non può essere creato o modificato da Control Center. Ruota a ogni utilizzo e il valore ruotato viene memorizzato immediatamente.

I valori vengono ripuliti dagli spazi ai bordi, le chiavi non dichiarate vengono scartate e un prefisso non corrispondente viene rifiutato con la stessa frase sul client e sul server. I campi obbligatori devono essere presenti a ogni connessione; un campo facoltativo lasciato vuoto conserva quanto era già memorizzato.

Scope del token del bot:

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

Sottoscrizioni agli eventi del bot:

app_home_opened
app_mention
message.channels
message.groups
message.im

Impostazioni strutturali che Control Center impone quando crea o modifica l’app: Socket Mode attivo, nessun request URL, la scheda Messages attiva e scrivibile, always_online per il bot, gli scope e gli eventi richiesti uniti a qualunque cosa l’app già possieda (mai sottratti) e un comando slash con la descrizione File a ticket or link your Control Center account — il marcatore che identifica il comando di Control Center in un manifest esportato.

Stato Valore serializzato Significato
Non connesso disconnected Nessuna credenziale memorizzata per questo spazio di lavoro × provider
Connessione in corso… connecting Credenziali memorizzate, trasporto in connessione o riconnessione
Attivo connected Trasporto attivo e in ricezione di eventi
Errore di connessione error Memorizzate ma in errore; l’errore del provider viene mostrato accanto

La riconnessione è un backoff esponenziale con jitter, limitato a 30 secondi. Una rotazione del socket avviata dal provider si riconnette prontamente senza aumentare il backoff. Una credenziale rifiutata (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) o uno scope mancante è definitivo: il ponte smette di riprovare finché non arrivano nuove credenziali.

Il ponte è scritto pensando alla superficie più ricca e degrada una capacità alla volta.

Capacità Slack Effetto quando manca
streaming sì L’intera risposta viene pubblicata una sola volta, alla fine
streamingRequiresThread sì Una destinazione di risposta senza ancora di thread pubblica risposte intere
ephemeralMessages sì Una risposta a un comando slash senza un riferimento di risposta utilizzabile viene scartata (i rifiuti vengono sempre pubblicati nel thread)
threadStatus sì Nessuna riga temporanea “sta pensando…”
threadTitle sì Il thread di chat conserva il titolo predefinito
slashCommands sì Nessuna superficie /cc
taskCards sì Solo testo — nessuna scheda sopra la risposta e nessun link “View in Control Center”
maxMessageLength 39.000 Le risposte più lunghe vengono troncate con un rimando a Control Center
maxStreamChunkLength 3.800 Le aggiunte vengono spezzate su un confine di paragrafo, riga o spazio

Annunciare lo streaming non significa prometterlo: quello di Slack richiede la funzione Agents & AI Apps su un piano a pagamento. Un rifiuto al momento della chiamata (streaming_not_supported, feature_not_enabled, paid_only, …) viene ricordato per tutta la vita della connessione e viene esposto come streamingAvailable: false. Le schede delle attività di Slack viaggiano sulla stessa chiamata, quindi uno Slack che rifiuta la modalità di visualizzazione della scheda conserva il proprio stream e perde solo la scheda, una volta, per tutta la vita della connessione.

Uno stream deve anche indicare il lettore a cui è destinato. Slack richiede sia un utente destinatario sia il suo team per aprirne uno in un canale (un messaggio diretto no) e in caso contrario rifiuta con missing_recipient_user_id / missing_recipient_team_id — un rifiuto per singola chiamata, quindi il ponte continua a riprovare e nel frattempo il turno arriva come un’unica risposta pubblicata. Il destinatario è chi ha posto la domanda, tratto dal messaggio in entrata (il suo team, non quello dell’app, così un canale Slack Connect viene risolto correttamente). Per un turno che nessuno ha avviato dalla chat — lavoro iniziato in Control Center su una conversazione già collegata — è il membro il cui collegamento ha creato il ponte; se nel frattempo ha scollegato il proprio account di chat, quel turno viene pubblicato intero invece che in streaming.

Una scheda raggruppata per ogni turno dell’agente (task_display_mode: plan), con una riga per passaggio. Ogni riga è un task_update di id + title + status, più details inviato una sola volta quando il pensiero è pronto. details e output dello stesso id si concatenano senza separatore, quindi details viene inviato una volta sola e output non passa mai in streaming. Il titolo del piano è la richiesta; la risposta è un’aggiunta separata successiva che porta un blocco markdown_text.

Parte Contenuto
Titolo del piano Ciò che è stato chiesto
Riga di preparazione Working on it…, poi Cloning …, poi Starting the agent… — una sola riga, con il titolo sostituito
Riga del pensiero Thinking…, aggiunta quando l’agente inizia a ragionare; il pensiero è il details di questa riga, inviato una volta quando il ragionamento termina
Righe degli strumenti Una per strumento (Bash which swift), con id univoco così stanno affiancate
Stato In corso mentre quella riga è in esecuzione; completata quando inizia la riga successiva o il turno termina; errore per fallito, interrotto e limite di turni
Invito all’azione View in Control Center come riga finale quando il turno termina — assente mentre il lavoro è ancora in corso e assente quando il server non ha un URL pubblico raggiungibile

Ciò che una scheda non porta mai: l’output degli strumenti, un argomento fuori da quella lista consentita o qualsiasi cosa letta dal database. Il testo del ragionamento è solo il details della riga Thinking… (limitato a 256 caratteri), non il corpo del messaggio. Una scheda che non direbbe nulla di nuovo non viene reinviata. Una menzione apre lo stream immediatamente, così il thread di Slack non resta in silenzio; un turno che poi non ha prodotto alcun testo chiude quella stessa scheda con una breve nota invece di pubblicare un secondo messaggio. Un turno senza parole su uno spazio che non ha mai aperto uno stream (nessuna scheda, o streaming già rifiutato) pubblica comunque un messaggio che porta la scheda completata e la nota.

Le schede in streaming e quelle pubblicate sono payload diversi:

Forma Struttura Testo
In streaming plan_update più blocchi task_update con chiave id Stringa semplice, limitata a 256 caratteri per blocco
Pubblicata Un blocco plan che avvolge task_card con chiave task_id Testo formattato

Gestione dei guasti:

Situazione Comportamento
Il provider rifiuta la scheda in un’aggiunta L’aggiunta viene ritentata immediatamente senza la scheda; il testo continua ad arrivare dal vivo e le schede vengono abbandonate per tutta la vita della connessione
Il provider rifiuta anche il testo Lo stream viene chiuso e la risposta viene pubblicata intera
Lo stream si interrompe dopo che parte della risposta è arrivata Lo stream viene chiuso e nulla viene ripubblicato

Vedi Ponti di chat per capire perché il payload è suddiviso in questo modo.

La scheda si apre quando una menzione viene accettata, prima del clone e prima del primo token; il turno che segue adotta la stessa scheda e lo stesso stream. La narrazione successiva arriva da SpaceProvisioningChanged sul bus di eventi di dominio — lo stesso avanzamento che il banner di preparazione del desktop legge dalla riga dello spazio.

Stato di preparazione Narrazione
Menzione accettata Working on it…
Passaggio repo Cloning <repo>…
Passaggio prCheckout Checking out the pull request in <repo>…
Passaggio script Running the setup script for <repo>…
Passaggio agent Setting up <agent>…
provisioning, nessun passaggio ancora Preparing the workspace…
ready Starting the agent…
L’agente ragiona Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

La narrazione della preparazione richiede tutte e quattro le condizioni:

Condizione
Il provider disegna le schede
Lo streaming è disponibile su questa connessione
Lo spazio ha una richiesta di chat in corso
Nessuna scheda sta già riportando un turno lì

La richiesta resta in corso finché non inizia un turno o la preparazione non fallisce. Un evento ready senza una scheda già aperta è uno spazio già caldo e viene lasciato in silenzio, e una preparazione che nessuno in chat sta aspettando — una conversazione aperta nell’app, una rimasta in sospeso e ripresa all’avvio — non produce alcuna scheda.

I prodotti di chat accettano http(s) in un link e nient’altro, quindi un invito all’azione non può nominare direttamente lo schema dell’app desktop.

Proprietà Valore
Route GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
Origine Derivata dall’URL pubblico del server (ws → http, wss → https); il loopback viene mantenuto
Risposta Una piccola pagina HTML no-store che naviga verso control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> e mostra un pulsante manuale
Convalida Gli id devono corrispondere a [A-Za-z0-9_.-]{1,128}; qualsiasi altra cosa è un 404
Accesso Pubblica e inerte — non legge alcun database, quindi non è un oracolo di esistenza; l’autorizzazione la fa l’app che apre
Campo Limite Note
appName 1–35 caratteri Predefinito: Control Center · <workspace name>
botDisplayName 1–80 caratteri Predefinito: control-center
description ≤ 140 caratteri
agentDescription ≤ 300 caratteri Mostrato sopra un messaggio diretto con il bot
command ≤ 32 caratteri Normalizzato: barre iniziali rimosse, convertito in minuscolo, [^a-z0-9_-] eliminato, se vuoto ripiega su cc
agentEnabled — Se l’esperienza di messaggi diretti e agente è esposta

Un campo vuoto significa “lascialo com’è”, non “svuotalo”. I limiti sono quelli di Slack, i più stretti tra i prodotti modellati.

Proprietà Valore
Lunghezza 6 caratteri
Alfabeto 23456789ABCDEFGHJKLMNPQRSTVWXYZ (senza 0/O, 1/I, U)
Durata 15 minuti
Utilizzi Uno
Archiviazione In memoria — un riavvio del server invalida i codici in sospeso
Ambito Uno spazio di lavoro e un provider; generare un nuovo codice invalida quello precedente del membro

Il riscatto viene confrontato senza distinzione tra maiuscole e minuscole. I codici sconosciuti, scaduti, di un altro spazio di lavoro e di un provider sbagliato falliscono tutti allo stesso modo, di proposito.

Metodo Valore serializzato Come
Codice code Il membro ha riscattato /cc link <code>
E-mail email L’e-mail verificata del provider corrispondeva a un utente già membro dello spazio di lavoro

Sono tutte con ambito di spazio di lavoro. Il server è stateless, quindi ogni chiamata porta il proprio workspace_id negli argomenti; il dispatcher rifiuta un’operazione con ambito di spazio di lavoro che lo omette, prima che si apra qualsiasi database. Le credenziali viaggiano solo in entrata — le letture di stato non le restituiscono mai.

Operazione Tipo Ruolo minimo Classi di azione
chat.providers read guest —
chat.status read guest —
chat.connect mutate admin secretAccess, networkEgress
chat.disconnect destructive admin secretAccess
chat.listUserLinks read guest —
chat.beginUserLink mutate member —
chat.unlinkUser mutate member per te stesso, admin per chiunque altro —
chat.createApp mutate admin secretAccess, networkEgress
chat.setupLink read admin —
chat.botProfile read admin networkEgress
chat.updateBotProfile mutate admin networkEgress

Le operazioni di lettura ereditano il livello minimo guest dal loro tipo; le mutazioni senza un livello esplicito richiedono member.

chat.setupLink compone un URL di creazione dell’app del provider a partire dal profilo del bot. Non dichiara alcuna classe di azione perché non legge alcun segreto e non apre alcuna connessione — l’unica chiamata la fa il browser dell’utente.

Una query reattiva completa la superficie:

Query Tipo Emette
chat.watchUserLinks watch L’elenco, ripubblicato ogni volta che un collegamento viene creato o rimosso

Esiste perché il collegamento avviene dall’altra parte: un membro digita il codice monouso nel bot, quindi nessuna risposta del client può riportare indietro l’esito. Sia l’elenco nelle impostazioni sia la finestra “collega il mio account” aperta lo seguono, e la finestra passa a una conferma nel momento in cui arriva il collegamento del membro stesso.

chat.beginUserLink genera sempre un codice per l’identità del chiamante. chat.updateBotProfile restituisce un remaining_step solo quando resta un passaggio (tipicamente una reinstallazione); la sua assenza significa che la modifica è già attiva.

chat.providers restituisce una voce per ogni provider offerto — il descrittore più, quando esiste, lo stato della connessione. Tutto ciò che l’interfaccia delle impostazioni mostra proviene da quel payload, ed è per questo che aggiungere un provider è una modifica solo lato server.

Cosa Dove
Credenziali di connessione <dataDir>/<workspaceId>/chat_credentials/<provider>.json
Preparazione guidata incompleta <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
File Slack legacy <dataDir>/<workspaceId>/slack_credentials.json e slack_app_setup.json, letti una volta e migrati sul posto

Le credenziali vivono accanto al database dello spazio di lavoro, non al suo interno: eliminare uno spazio di lavoro porta con sé i suoi token di chat e workspace.export o un backup copia workspace.db — quindi uno spazio di lavoro ceduto a qualcun altro non porta con sé token di bot attivi. Le scritture sono atomiche (file temporaneo più rename) e ristrette al solo proprietario.

Entrambe vivono nel database dello spazio di lavoro, che porta una baseline v1 consolidata — quindi una lettura tra spazi di lavoro non compila.

chat_space_links — una conversazione esterna ↔ uno spazio di Control Center:

Colonna Note
id UUID v4
workspace_id Ambito dello spazio di lavoro
provider ChatProvider.wire
external_team_id Team Slack T…, vuoto per i provider privi del concetto
external_channel_id Canale Slack C…, gruppo G…, messaggio diretto D…
external_thread_id Ancora del thread di Slack, oppure null quando l’intera conversazione è l’ancora (messaggi diretti al bot)
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id, created_at, last_activity_at

Univoco su (workspace_id, provider, external_channel_id, external_thread_id), con un indice parziale che impone lo stesso per gli id di thread null (SQLite tratta i NULL come distinti).

chat_user_links — un membro esterno ↔ un utente di Control Center:

Colonna Note
id UUID v4
workspace_id Ambito dello spazio di lavoro
provider ChatProvider.wire
external_team_id, external_user_id Membro Slack U…
user_id Utente di Control Center (globale, quindi nessuna chiave esterna)
method code o email
linked_at

Univoco su (workspace_id, provider, external_team_id, external_user_id) e su (workspace_id, provider, user_id) — attribuzione a valore singolo in entrambe le direzioni, mentre una stessa persona può essere collegata su più provider.

Un collegamento è risoluzione di identità, non una concessione di accesso: l’appartenenza viene ricontrollata a ogni evento in entrata, quindi un collegamento a un membro nel frattempo rimosso produce un rifiuto.

Uno spazio collegato viene creato con lo SpaceKind del provider (slack). Si comporta come uno spazio normale rivolto a persone — notificato e collocato in sezione come topic — e isExternalChat è il controllo da cui passa ogni comportamento (specchiamento, badge, conservazione).

Ogni messaggio che il ponte porta dentro reca un contrassegno metadata['chat']:

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

Il contrassegno è anche la protezione anti-eco: lo specchio in uscita salta qualsiasi messaggio che lo rechi, ed è ciò che impedisce a un messaggio di chat in entrata di essere ripubblicato subito indietro.

Comportamento Dettaglio
Cosa viene inoltrato verso l’esterno Solo il testo dell’agente, più la riga di stato della scheda delle attività. Il testo del ragionamento e l’output degli strumenti restano in Control Center
Cadenza dello streaming Limitata a un invio al secondo mentre un turno è in esecuzione; la scheda e il testo che illustra viaggiano nella stessa chiamata
Messaggi umani digitati in Control Center Rispecchiati nel thread collegato, con il nome visualizzato dell’autore come prefisso
Turni vuoti Segnalati con una breve nota (esecuzione fallita, interrotta o fermata al suo limite di turni) invece che con il silenzio
Deduplicazione Con chiave sull’id dell’evento del provider stesso; gli ultimi 512 vengono ricordati
Agente di primo contatto Una nuova conversazione collegata sveglia l’agente CEO dello spazio di lavoro, altrimenti il suo primo agente, altrimenti nessuno
Denominazione dello spazio #channel · <request> per un thread di un canale Slack, Slack DM · <request> per un messaggio diretto, titoli limitati a 48 caratteri
Isolamento dei guasti Un singolo evento difettoso non manda mai giù il ponte; la connessione continua a servire tutte le altre conversazioni