Passerelle de chat
Fournisseurs
Section intitulée « Fournisseurs »| Fournisseur | Valeur de trame | Transport | Type d’espace |
|---|---|---|---|
| Slack | slack |
Socket Mode (WSS sortant) | slack |
Slack est le seul fournisseur. ChatProvider est une énumération à valeur unique et cc_server enregistre un seul plugin. Le schéma, chaque recherche et chaque index unique sont indexés par provider, si bien que les identifiants ne passent jamais d’un fournisseur à l’autre et qu’en ajouter un est un changement purement côté serveur.
Une application côté fournisseur par espace de travail × fournisseur.
Commandes de chat
Section intitulée « Commandes de chat »Le nom de la commande est propre à chaque application (cc par défaut, donc /cc). Un administrateur peut le renommer ; la passerelle utilise alors le nom sous lequel elle a été invoquée en dernier.
| Commande | Effet |
|---|---|
/cc help (ou /cc sans verbe) |
Liste ce que le bot sait faire |
/cc link <code> |
Utilise un code à usage unique et lie l’identité de chat de l’expéditeur |
/cc ticket <title> |
Crée un ticket local signalé par le membre lié |
/cc ticket <title> | <description> |
Idem, avec tout ce qui suit le premier | comme description |
| Verbe inconnu | Signale le verbe inconnu et renvoie le texte d’aide |
Façons de joindre un agent :
| Geste | Comportement |
|---|---|
@bot … dans un canal |
Ouvre un fil Slack et un espace Control Center correspondant ; l’agent répond dans le fil Slack |
| Réponse dans un fil Slack relié | Poursuit le même espace Control Center — aucune mention nécessaire |
| Message ordinaire dans un canal | Ignoré (non adressé au bot et hors d’un fil Slack relié) |
| Message privé au bot | Un espace Control Center par conversation en message privé ; définit un titre de fil Slack et un statut « is thinking… » pendant que l’agent travaille |
Champs d’identifiants (Slack)
Section intitulée « Champs d’identifiants (Slack) »Déclarés par le descripteur du fournisseur, qui est ce qui génère la boîte de dialogue de connexion et valide la soumission.
| Identifiant de champ | Libellé | Requis | Préfixe | Où il se trouve |
|---|---|---|---|---|
botToken |
Bot user OAuth token | Oui | xoxb- |
OAuth & Permissions |
appToken |
App-level token | Oui | xapp- |
Basic Information → App-Level Tokens, scope connections:write |
configRefreshToken |
App configuration refresh token | Non | xoxe |
App configuration tokens on api.slack.com/apps |
configRefreshToken est l’identifiant de gestion d’application : sans lui, la connexion fonctionne, mais le bot ne peut être ni créé ni modifié depuis Control Center. Il est renouvelé à chaque utilisation et la nouvelle valeur est stockée immédiatement.
Les valeurs sont nettoyées de leurs espaces superflus, les clés non déclarées sont ignorées et un préfixe incorrect est refusé avec la même phrase côté client et côté serveur. Les champs requis doivent être présents à chaque connexion ; un champ facultatif laissé vide conserve ce qui était déjà stocké.
Prérequis de l’application Slack
Section intitulée « Prérequis de l’application Slack »Scopes du jeton de bot :
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailAbonnements aux événements du bot :
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imRéglages structurels que Control Center impose lorsqu’il crée ou modifie l’application : Socket Mode activé, aucune URL de requête, onglet Messages activé et inscriptible, bot always_online, les scopes et événements requis fusionnés avec ce que l’application possède déjà (jamais retranchés) et une commande slash portant la description File a ticket or link your Control Center account — le marqueur qui identifie la commande propre à Control Center dans un manifeste exporté.
États de connexion
Section intitulée « États de connexion »| État | Valeur de trame | Signification |
|---|---|---|
| Non connecté | disconnected |
Aucun identifiant stocké pour cet espace de travail × fournisseur |
| Connexion… | connecting |
Identifiants stockés, transport en cours de connexion ou de reconnexion |
| En ligne | connected |
Transport actif et réception d’événements |
| Erreur de connexion | error |
Stocké mais en échec ; l’erreur propre au fournisseur est affichée à côté |
La reconnexion suit un repli exponentiel avec jitter, plafonné à 30 secondes. Une rotation de socket déclenchée par le fournisseur se reconnecte sans tarder, sans aggraver le repli. Un identifiant rejeté (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) ou un scope manquant est définitif : la passerelle cesse de réessayer jusqu’à l’arrivée de nouveaux identifiants.
Capacités du fournisseur (Slack)
Section intitulée « Capacités du fournisseur (Slack) »La passerelle est écrite pour la surface la plus riche et dégrade capacité par capacité.
| Capacité | Slack | Effet en son absence |
|---|---|---|
streaming |
oui | La réponse entière est publiée une seule fois, à la fin |
streamingRequiresThread |
oui | Une cible de réponse sans point d’ancrage de fil publie des réponses entières |
ephemeralMessages |
oui | Une réponse à une commande slash sans poignée de réponse exploitable est abandonnée (les refus sont toujours publiés dans le fil) |
threadStatus |
oui | Aucune ligne transitoire « is thinking… » |
threadTitle |
oui | Le fil de chat conserve son titre par défaut |
slashCommands |
oui | Aucune surface /cc |
taskCards |
oui | Texte uniquement — aucune carte au-dessus de la réponse et aucun lien « View in Control Center » |
maxMessageLength |
39,000 | Les réponses plus longues sont tronquées, avec un pointeur vers Control Center |
maxStreamChunkLength |
3,800 | Les ajouts sont découpés à une frontière de paragraphe, de ligne ou d’espace |
Annoncer la diffusion en continu n’est pas la promettre : celle de Slack exige la fonctionnalité Agents & AI Apps sur un forfait payant. Un refus au moment de l’appel (streaming_not_supported, feature_not_enabled, paid_only, …) est mémorisé pour toute la vie de la connexion et exposé sous la forme streamingAvailable: false. Les cartes de tâche de Slack passent par le même appel, donc un Slack qui refuse le mode d’affichage de la carte conserve son flux et ne perd que la carte, une fois, pour toute la vie de la connexion.
Un flux doit aussi nommer le lecteur auquel il est destiné. Slack exige à la fois un utilisateur destinataire et son équipe pour en ouvrir un dans un canal (un message privé ne le permet pas) et refuse sinon avec missing_recipient_user_id / missing_recipient_team_id — un refus par appel, donc la passerelle continue d’essayer et le tour arrive entre-temps sous la forme d’une réponse publiée en entier. Le destinataire est celui qui a posé la question, repris du message entrant (son équipe, pas celle de l’application, afin qu’un canal Slack Connect se résolve correctement). Pour un tour que personne n’a lancé depuis le chat — du travail déclenché dans Control Center sur une conversation déjà reliée — c’est le membre dont la liaison a créé la passerelle ; s’il a depuis délié son compte de chat, ce tour est publié en entier plutôt que diffusé.
Cartes de tâche
Section intitulée « Cartes de tâche »Une carte groupée par tour d’agent (task_display_mode: plan), avec une ligne par étape. Chaque ligne est un task_update composé de id + title + status, plus un details envoyé une seule fois lorsque la réflexion est prête. Les details et l’output de même identifiant se concatènent sans séparateur, donc les details sont envoyés une fois et l’output n’est jamais diffusé en continu. Le titre du plan est la demande ; la réponse est un ajout ultérieur distinct portant un fragment markdown_text.
| Partie | Contenu |
|---|---|
| Titre du plan | Ce qui a été demandé |
| Ligne de préparation | Working on it…, puis Cloning …, puis Starting the agent… — une seule ligne, titre remplacé |
| Ligne de réflexion | Thinking…, ajoutée quand l’agent commence à raisonner ; la réflexion est le details de cette ligne, envoyé une fois à la fin du raisonnement |
| Lignes d’outils | Une par outil (Bash which swift), identifiant unique pour qu’elles se côtoient |
| Statut | En cours tant que cette ligne s’exécute ; terminé quand la ligne suivante démarre ou que le tour se termine ; erreur en cas d’échec, d’interruption ou de limite de tours atteinte |
| Appel à l’action | View in Control Center comme dernière ligne à la fin du tour — absent tant que le travail est en cours, et absent quand le serveur n’a aucune URL publique joignable |
Ce qu’une carte ne porte jamais : la sortie des outils, un argument hors de cette liste d’autorisations, ou quoi que ce soit de lu dans la base de données. La prose de raisonnement se limite aux détails de la ligne Thinking… (plafonnés à 256 caractères), pas au corps du message. Une carte qui n’apprendrait rien de nouveau n’est pas renvoyée. Une mention ouvre le flux immédiatement pour que le fil Slack ne reste pas silencieux ; un tour qui n’a ensuite produit aucun texte termine cette même carte par une courte note plutôt que de publier un second message. Un tour sans mots sur un espace qui n’a jamais ouvert de flux (aucune carte, ou diffusion déjà refusée) publie tout de même un message portant la carte terminée et la note.
Les cartes diffusées et les cartes publiées sont des charges utiles différentes :
| Forme | Structure | Prose |
|---|---|---|
| Diffusée | plan_update plus des fragments task_update indexés par id |
Chaîne simple, plafonnée à 256 caractères par fragment |
| Publiée | Un bloc plan enveloppant des task_card indexés par task_id |
Texte enrichi |
Gestion des échecs :
| Situation | Comportement |
|---|---|
| Le fournisseur refuse la carte lors d’un ajout | L’ajout est réessayé immédiatement sans elle ; la prose arrive tout de même en direct et les cartes sont abandonnées pour toute la vie de la connexion |
| Le fournisseur refuse aussi la prose | Le flux est fermé et la réponse est publiée en entier |
| Le flux se coupe après qu’une partie de la réponse est arrivée | Le flux est fermé et rien n’est republié |
Voir Passerelles de chat pour savoir pourquoi la charge utile est découpée ainsi.
La préparation, avant qu’il y ait un tour
Section intitulée « La préparation, avant qu’il y ait un tour »La carte s’ouvre lorsqu’une mention est acceptée, avant le clonage et avant le premier jeton ; le tour qui suit adopte la même carte et le même flux. La narration qui suit provient de SpaceProvisioningChanged sur le bus d’événements de domaine — la même progression que la bannière de préparation de l’application de bureau lit sur la ligne de l’espace.
| État de préparation | Narration |
|---|---|
| Mention acceptée | Working on it… |
Étape repo |
Cloning <repo>… |
Étape prCheckout |
Checking out the pull request in <repo>… |
Étape script |
Running the setup script for <repo>… |
Étape agent |
Setting up <agent>… |
provisioning, aucune étape encore |
Preparing the workspace… |
ready |
Starting the agent… |
| L’agent raisonne | Thinking… |
failed |
Workspace setup failed — details are in Control Center. |
cancelled |
Workspace setup was stopped — details are in Control Center. |
La narration de préparation exige les quatre conditions suivantes :
| Condition |
|---|
| Le fournisseur affiche des cartes |
| La diffusion en continu est disponible sur cette connexion |
| L’espace a une demande de chat en cours |
| Aucune carte n’y rapporte déjà un tour |
La demande reste en cours jusqu’au démarrage d’un tour ou à l’échec de la préparation. Un événement ready sans carte déjà ouverte correspond à un espace déjà chaud, laissé silencieux, et une préparation que personne dans le chat n’attend — une conversation ouverte dans l’application, une conversation orpheline reprise au démarrage — ne produit aucune carte.
La page de rebond /open
Section intitulée « La page de rebond /open »Les produits de chat n’acceptent que http(s) dans un lien et rien d’autre, donc un appel à l’action ne peut pas nommer directement le schéma de l’application de bureau.
| Propriété | Valeur |
|---|---|
| Route | GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id> |
| Origine | Dérivée de l’URL publique du serveur (ws → http, wss → https) ; l’adresse de bouclage est conservée |
| Réponse | Une petite page HTML non mise en cache qui navigue vers control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> et affiche un bouton manuel |
| Validation | Les identifiants doivent correspondre à [A-Za-z0-9_.-]{1,128} ; tout le reste est un 404 |
| Accès | Public et inerte — elle ne lit aucune base de données, donc elle n’est pas un oracle d’existence ; l’application qu’elle ouvre fait l’autorisation |
Champs du profil du bot
Section intitulée « Champs du profil du bot »| Champ | Limite | Remarques |
|---|---|---|
appName |
1–35 caractères | Vaut par défaut Control Center · <workspace name> |
botDisplayName |
1–80 caractères | Vaut par défaut control-center |
description |
≤ 140 caractères | |
agentDescription |
≤ 300 caractères | Affiché au-dessus d’un message privé au bot |
command |
≤ 32 caractères | Normalisé : barres obliques initiales supprimées, passage en minuscules, [^a-z0-9_-] retiré, vide revient à cc |
agentEnabled |
— | Si l’expérience de messages privés et d’agent est exposée |
Un champ vide signifie « ne rien changer », et non « effacer ». Les limites sont celles de Slack, les plus strictes des produits modélisés.
Codes de liaison
Section intitulée « Codes de liaison »| Propriété | Valeur |
|---|---|
| Longueur | 6 caractères |
| Alphabet | 23456789ABCDEFGHJKLMNPQRSTVWXYZ (sans 0/O, 1/I, U) |
| Durée de vie | 15 minutes |
| Utilisations | Une |
| Stockage | En mémoire — un redémarrage du serveur invalide les codes en cours |
| Portée | Un espace de travail et un fournisseur ; générer un nouveau code invalide le code précédent du membre |
La comparaison d’un code se fait sans tenir compte de la casse. Les codes inconnus, expirés, appartenant à un autre espace de travail ou à un autre fournisseur échouent tous de la même façon, à dessein.
Méthodes de liaison
Section intitulée « Méthodes de liaison »| Méthode | Valeur de trame | Comment |
|---|---|---|
| Code | code |
Le membre a utilisé /cc link <code> |
email |
L’adresse e-mail vérifiée du fournisseur correspondait à un utilisateur déjà membre de l’espace de travail |
Opérations RPC
Section intitulée « Opérations RPC »Toutes sont limitées à un espace de travail. Le serveur est sans état, donc chaque appel porte son propre workspace_id dans ses arguments ; le répartiteur refuse une opération limitée à un espace de travail qui l’omet, avant l’ouverture de la moindre base de données. Les identifiants ne circulent que vers l’intérieur — les lectures d’état ne les renvoient jamais.
| Opération | Type | Rôle minimal | Classes d’action |
|---|---|---|---|
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 pour vous-même, admin pour tout autre |
— |
chat.createApp |
mutate | admin |
secretAccess, networkEgress |
chat.setupLink |
read | admin |
— |
chat.botProfile |
read | admin |
networkEgress |
chat.updateBotProfile |
mutate | admin |
networkEgress |
Les opérations de lecture héritent du plancher guest de leur type ; les mutations sans plancher explicite exigent member.
chat.setupLink compose une URL de création d’application côté fournisseur à partir du profil du bot. Elle ne déclare aucune classe d’action parce qu’elle ne lit aucun secret et n’établit aucune connexion — le navigateur de l’utilisateur fait le seul appel.
Une requête réactive complète la surface :
| Requête | Type | Émet |
|---|---|---|
chat.watchUserLinks |
watch | La liste des comptes liés, renvoyée dès qu’une liaison est créée ou supprimée |
Elle existe parce que la liaison se fait de l’autre côté : un membre saisit le code à usage unique dans le bot, donc aucune requête du client ne peut rapporter la réponse. La liste des comptes liés des paramètres et la boîte de dialogue « Lier mon compte » ouverte s’y abonnent toutes deux, et la boîte de dialogue bascule sur une confirmation dès que la liaison du membre arrive.
chat.beginUserLink génère toujours un code pour l’identité de l’appelant lui-même. chat.updateBotProfile ne renvoie un remaining_step que lorsqu’une étape reste à faire (typiquement une réinstallation) ; son absence signifie que la modification est active.
chat.providers renvoie une entrée par fournisseur proposé — le descripteur plus, quand il existe, l’état de la connexion. Tout ce que l’interface des paramètres affiche provient de cette charge utile, et c’est pourquoi ajouter un fournisseur est un changement purement côté serveur.
Stockage
Section intitulée « Stockage »| Quoi | Où |
|---|---|
| Identifiants de connexion | <dataDir>/<workspaceId>/chat_credentials/<provider>.json |
| Configuration guidée inachevée | <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json |
| Fichiers Slack hérités | <dataDir>/<workspaceId>/slack_credentials.json et slack_app_setup.json, lus une fois et migrés sur place |
Les identifiants vivent à côté de la base de données de l’espace de travail, pas à l’intérieur : supprimer un espace de travail emporte ses jetons de chat, et workspace.export ou une sauvegarde copie workspace.db — un espace de travail transmis à quelqu’un d’autre ne transporte donc aucun jeton de bot actif. Les écritures sont atomiques (fichier temporaire puis renommage) et resserrées au seul propriétaire.
Tables de base de données
Section intitulée « Tables de base de données »Les deux vivent dans la base de données de l’espace de travail, qui porte une base de référence v1 condensée — une lecture inter-espaces ne compile donc pas.
chat_space_links — une conversation externe ↔ un espace Control Center :
| Colonne | Remarques |
|---|---|
id |
UUID v4 |
workspace_id |
Portée de l’espace de travail |
provider |
ChatProvider.wire |
external_team_id |
Équipe Slack T…, vide pour les fournisseurs dépourvus de ce concept |
external_channel_id |
Canal Slack C…, groupe G…, message privé D… |
external_thread_id |
Point d’ancrage du fil Slack, ou null lorsque toute la conversation est le point d’ancrage (messages privés au bot) |
cc_space_id |
REFERENCES spaces(id) ON DELETE CASCADE |
created_by_user_id, created_at, last_activity_at |
Unique sur (workspace_id, provider, external_channel_id, external_thread_id), avec un index partiel appliquant la même contrainte aux identifiants de fil nuls (SQLite traite les NULL comme distincts).
chat_user_links — un membre externe ↔ un utilisateur Control Center :
| Colonne | Remarques |
|---|---|
id |
UUID v4 |
workspace_id |
Portée de l’espace de travail |
provider |
ChatProvider.wire |
external_team_id, external_user_id |
Membre Slack U… |
user_id |
Utilisateur Control Center (global, donc sans clé étrangère) |
method |
code ou email |
linked_at |
Unique sur (workspace_id, provider, external_team_id, external_user_id) et sur (workspace_id, provider, user_id) — une attribution à valeur unique dans les deux sens, tandis qu’une même personne peut être liée sur plusieurs fournisseurs.
Une liaison est une résolution d’identité, pas une autorisation d’accès : l’appartenance est revérifiée à chaque événement entrant, donc une liaison vers un membre retiré depuis se résout en refus.
Type d’espace et métadonnées des messages
Section intitulée « Type d’espace et métadonnées des messages »Un espace relié est créé avec le SpaceKind du fournisseur (slack). Il se comporte comme un espace ordinaire destiné aux humains — notifié et classé comme topic — et isExternalChat est le contrôle par lequel passe chaque comportement (miroir, badges, rétention).
Chaque message que la passerelle fait entrer porte une estampille metadata['chat'] :
{ "chat": { "provider": "slack", "teamId": "T024BE7LD", "channelId": "C0G9QF9GW", "threadId": "1700000000.000100", "messageId": "1700000000.000200", "userId": "U024BE7LH", "userLabel": "sam" }}L’estampille est aussi la garde anti-écho : le miroir sortant ignore tout message qui la porte, et c’est ce qui empêche un message de chat entrant d’être republié aussitôt en retour.
Comportement du relais
Section intitulée « Comportement du relais »| Comportement | Détail |
|---|---|
| Ce qui est relayé vers l’extérieur | Uniquement le texte de l’agent, plus la ligne d’état de la carte de tâche. La prose de raisonnement et la sortie des outils restent dans Control Center |
| Cadence de diffusion | Limitée à un envoi par seconde pendant qu’un tour s’exécute ; la carte et le texte qu’elle explique voyagent dans le même appel |
| Messages humains saisis dans Control Center | Reflétés vers le fil lié, préfixés par le nom d’affichage de leur auteur |
| Tours vides | Signalés par une courte note (exécution échouée, interrompue ou arrêtée à sa limite de tours) plutôt que par le silence |
| Déduplication | Indexée sur l’identifiant d’événement propre au fournisseur ; les 512 derniers sont mémorisés |
| Agent d’accueil | Une nouvelle conversation reliée réveille l’agent CEO de l’espace de travail, sinon son premier agent, sinon personne |
| Nommage des espaces | #channel · <request> pour un fil de canal Slack, Slack DM · <request> pour un message privé, titres plafonnés à 48 caractères |
| Isolation des défaillances | Un événement défectueux ne fait jamais tomber la passerelle ; la connexion continue de servir toutes les autres conversations |
Voir aussi
Section intitulée « Voir aussi »- Configurer l’intégration Slack
- Lier votre compte Slack
- Personnaliser le robot de chat
- Passerelles de chat
- Garde-fous pour les classes d’action que déclarent les opérations