Aller au contenu

Passerelle de chat

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.

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

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é.

Scopes du jeton de bot :

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

Abonnements aux événements du bot :

app_home_opened
app_mention
message.channels
message.groups
message.im

Ré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é.

É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.

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é.

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 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.

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
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.

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éthode Valeur de trame Comment
Code code Le membre a utilisé /cc link <code>
E-mail email L’adresse e-mail vérifiée du fournisseur correspondait à un utilisateur déjà membre de l’espace de travail

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.

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.

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.

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 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