Ponte di chat
Provider
Sezione intitolata “Provider”| 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.
Comandi di chat
Sezione intitolata “Comandi di chat”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 |
Campi delle credenziali (Slack)
Sezione intitolata “Campi delle credenziali (Slack)”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.
Requisiti dell’app Slack
Sezione intitolata “Requisiti dell’app Slack”Scope del token del bot:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailSottoscrizioni agli eventi del bot:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imImpostazioni 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.
Stati della connessione
Sezione intitolata “Stati della connessione”| 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.
Capacità del provider (Slack)
Sezione intitolata “Capacità del provider (Slack)”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.
Schede delle attività
Sezione intitolata “Schede delle attività”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.
Preparazione, prima che esista un turno
Sezione intitolata “Preparazione, prima che esista un turno”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.
La pagina di rimbalzo /open
Sezione intitolata “La pagina di rimbalzo /open”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 |
Campi del profilo del bot
Sezione intitolata “Campi del profilo del bot”| 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.
Codici di collegamento
Sezione intitolata “Codici di collegamento”| 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.
Metodi di collegamento
Sezione intitolata “Metodi di collegamento”| Metodo | Valore serializzato | Come |
|---|---|---|
| Codice | code |
Il membro ha riscattato /cc link <code> |
email |
L’e-mail verificata del provider corrispondeva a un utente già membro dello spazio di lavoro |
Operazioni RPC
Sezione intitolata “Operazioni RPC”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.
Archiviazione
Sezione intitolata “Archiviazione”| 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.
Tabelle del database
Sezione intitolata “Tabelle del database”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.
Tipo di spazio e metadati dei messaggi
Sezione intitolata “Tipo di spazio e metadati dei messaggi”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 del relay
Sezione intitolata “Comportamento del relay”| 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 |
Vedi anche
Sezione intitolata “Vedi anche”- Configura l’integrazione Slack
- Collega il tuo account Slack
- Personalizza il bot di chat
- Ponti di chat
- Guardrail per le classi di azione che le operazioni dichiarano