Aller au contenu

Passerelle de clavardage

Fournisseur Valeur transmise 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 n’enregistre qu’un greffon. Le schéma, chaque recherche et chaque index unique ont provider pour clé, si bien que les identifiants ne traversent 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 celui avec lequel elle a été appelée en dernier.

Commande Effet
/cc help (ou /cc sans verbe) Liste ce que le robot peut faire
/cc link <code> Échange un code à usage unique et lie l’identité de clavardage de l’expéditeur
/cc ticket <title> Crée un ticket local signalé par le membre lié
/cc ticket <title> | <description> Idem, tout ce qui suit le premier | servant de description
Verbe inconnu Signale le verbe inconnu ainsi que 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 robot et hors d’un fil Slack relié)
Message privé au robot Un espace Control Center par conversation privée ; définit un titre de fil Slack et un état « is thinking… » pendant que l’agent travaille

Déclarés par le descripteur du fournisseur, qui génère la boîte de dialogue de connexion et valide la soumission.

Identifiant du champ Étiquette Obligatoire Préfixe Où il se trouve
botToken Jeton du bot Oui xoxb- OAuth & Permissions
appToken Jeton d’application Oui xapp- Basic Information → App-Level Tokens, portée connections:write
configRefreshToken Jeton de configuration d’app Non xoxe App configuration tokens sur api.slack.com/apps

configRefreshToken est l’identifiant de gestion de l’application : sans lui, la connexion fonctionne, mais le robot ne peut être ni créé ni modifié depuis Control Center. Il subit une rotation à chaque utilisation, et la valeur renouvelée est stockée immédiatement.

Les valeurs sont débarrassées des espaces superflus, les clés non déclarées sont écartées et un préfixe non conforme est refusé avec la même phrase côté client et côté serveur. Les champs obligatoires doivent être présents à chaque connexion ; un champ facultatif laissé vide conserve ce qui était déjà stocké.

Portées du jeton du robot :

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

Abonnements aux événements du robot :

app_home_opened
app_mention
message.channels
message.groups
message.im

Réglages structurels que Control Center impose quand il crée ou modifie l’application : Socket Mode activé, aucune URL de requête, l’onglet Messages activé et accessible en écriture, le robot always_online, les portées et les événements requis fusionnés dans 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 transmise Signification
Non connecté disconnected Aucun identifiant stocké pour cet espace de travail × fournisseur
Connexion… connecting Identifiants stockés, le transport se connecte ou se reconnecte
En ligne connected Transport établi et réception des événements
Erreur de connexion error Stockés mais en échec ; l’erreur propre au fournisseur est affichée à côté

La reconnexion est une temporisation exponentielle avec gigue, plafonnée à 30 secondes. Une rotation de socket amorcée par le fournisseur se reconnecte rapidement, sans faire grimper la temporisation. Un identifiant rejeté (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) ou une portée manquante est définitif : la passerelle cesse de réessayer jusqu’à l’arrivée de nouveaux identifiants.

La passerelle est écrite contre la surface la plus riche et se dégrade une capacité à la fois.

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 ancre de fil publie des réponses entières
ephemeralMessages oui Une réponse à une commande slash sans poignée de réponse utilisable est abandonnée (les refus sont toujours publiés dans le fil)
threadStatus oui Aucune ligne transitoire « is thinking… »
threadTitle oui Le fil de clavardage conserve son titre par défaut
slashCommands oui Aucune surface /cc
taskCards oui Texte seulement — 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 renvoi vers Control Center
maxStreamChunkLength 3 800 Les ajouts sont scindés à la frontière d’un paragraphe, d’une ligne ou d’un espace

Annoncer une diffusion n’est pas la promettre : celle de Slack exige la fonctionnalité Agents & AI Apps d’un forfait payant. Un refus au moment de l’appel (streaming_not_supported, feature_not_enabled, paid_only, …) est mémorisé pour toute la durée de la connexion et exposé sous streamingAvailable: false. Les cartes de tâche de Slack empruntent le même appel, de sorte qu’un Slack qui refuse le mode d’affichage de la carte conserve son flux et ne perd que la carte, une seule fois, pour toute la durée 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 avec missing_recipient_user_id / missing_recipient_team_id dans le cas contraire — un refus par appel, de sorte que la passerelle continue d’essayer et que, entre-temps, le tour arrive sous forme d’une seule réponse publiée. Le destinataire est la personne qui a demandé, d’après le message entrant (son équipe, et non celle de l’application, pour qu’un canal Slack Connect se résolve correctement). Pour un tour que personne n’a lancé depuis le clavardage — du travail amorcé dans Control Center sur une conversation déjà reliée —, il s’agit du membre dont la liaison a créé la passerelle ; s’il a depuis délié son compte de clavardage, ce tour est publié en entier plutôt que diffusé en continu.

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 ponctuel quand la réflexion est prête. Les details et l’output de même id se concatènent sans séparateur, donc les details sont envoyés une seule fois et l’output n’est jamais diffusé en continu. Le titre du plan est la demande ; la réponse est un ajout distinct et ultérieur portant un segment 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, dont le titre est remplacé
Ligne de réflexion Thinking…, ajoutée quand l’agent commence à raisonner ; la réflexion est le details de cette ligne, envoyé une seule fois à la fin du raisonnement
Lignes d’outil Une par outil (Bash which swift), avec un id unique pour qu’elles se placent côte à côte
État En cours pendant que cette ligne s’exécute ; terminée quand la ligne suivante démarre ou que le tour se termine ; en erreur en cas d’échec, d’interruption ou de limite de tours atteinte
Appel à l’action View in Control Center comme ligne finale quand le tour se termine — absente tant que le travail est en cours et absente 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’autorisation, 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), et non au corps du message. Une carte qui ne dirait 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 paroles sur un espace qui n’a jamais ouvert de flux (aucune carte, ou diffusion déjà refusée) publie quand 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 segments task_update ayant id pour clé Chaîne simple, plafonnée à 256 caractères par segment
Publiée Un bloc plan enveloppant des task_card ayant task_id pour clé Texte enrichi

Gestion des défaillances :

Situation Comportement
Le fournisseur refuse la carte lors d’un ajout L’ajout est réessayé immédiatement sans la carte ; la prose continue d’arriver en direct et les cartes sont abandonnées pour toute la durée 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 brise après qu’une partie de la réponse est arrivée Le flux est fermé et rien n’est republié

Voir Passerelles de clavardage pour comprendre pourquoi la charge utile est scindée ainsi.

La carte s’ouvre quand 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 vient ensuite 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 clavardage en cours
Aucune carte n’y rend déjà compte d’un tour

La demande reste en cours jusqu’à ce qu’un tour démarre ou que la préparation échoue. Un événement ready sans carte déjà ouverte correspond à un espace déjà chaud : il est laissé silencieux, et une préparation que personne n’attend dans le clavardage — une conversation ouverte dans l’application, une conversation abandonnée reprise au démarrage — ne produit aucune carte.

Les produits de clavardage n’acceptent que http(s) dans un lien et rien d’autre, de sorte qu’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) ; le loopback est conservé
Réponse Une petite page HTML no-store 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, ce n’est donc pas un oracle d’existence ; l’application qu’elle ouvre fait l’autorisation
Champ Limite Remarques
appName 1–35 caractères Par défaut : Control Center · <workspace name>
botDisplayName 1–80 caractères Par défaut : control-center
description ≤ 140 caractères
agentDescription ≤ 300 caractères Affiché au-dessus d’un message privé avec le robot
command ≤ 32 caractères Normalisé : les barres obliques initiales sont retirées, le tout mis en minuscules, [^a-z0-9_-] supprimé, et une valeur vide revient à cc
agentEnabled — Indique si l’expérience de message privé et d’agent est exposée

Une case vide signifie « laisser tel quel », et non « effacer ». Les limites sont celles de Slack, qui sont les plus serrées parmi les 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 circulation
Portée Un espace de travail et un fournisseur ; la création d’un nouveau code invalide le précédent du membre

L’utilisation d’un code est vérifiée sans égard à la casse. Les codes inconnus, expirés, propres à un autre espace de travail et destinés à un autre fournisseur échouent tous de façon identique, à dessein.

Méthode Valeur transmise Fonctionnement
Code code Le membre a utilisé /cc link <code>
Courriel email Le courriel vérifié 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 les arguments ; le répartiteur refuse une opération à portée d’espace de travail qui l’omet, avant même qu’une base de données ne soit ouverte. 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 lecture guest —
chat.status lecture guest —
chat.connect mutation admin secretAccess, networkEgress
chat.disconnect destructif admin secretAccess
chat.listUserLinks lecture guest —
chat.beginUserLink mutation member —
chat.unlinkUser mutation member pour vous-même, admin pour quiconque d’autre —
chat.createApp mutation admin secretAccess, networkEgress
chat.setupLink lecture admin —
chat.botProfile lecture admin networkEgress
chat.updateBotProfile mutation 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 du fournisseur à partir du profil du robot. Elle ne déclare aucune classe d’action parce qu’elle ne lit aucun secret et ne lance aucun appel — c’est le navigateur de l’utilisateur qui fait le seul appel.

Une requête réactive complète la surface :

Requête Type Émet
chat.watchUserLinks surveillance La liste, renvoyée chaque fois qu’une liaison est créée ou supprimée

Elle existe parce que la liaison se fait de l’autre côté : un membre tape le code à usage unique dans le robot, de sorte qu’aucune réponse de client ne peut rapporter le résultat. La liste des liaisons dans les paramètres et la boîte de dialogue « Lier mon compte » ouverte la suivent toutes les deux, et la boîte de dialogue bascule vers une confirmation dès que la liaison du membre arrive.

chat.beginUserLink crée toujours un code pour l’identité de l’appelant lui-même. chat.updateBotProfile ne renvoie un remaining_step que lorsqu’il reste une étape (typiquement une réinstallation) ; son absence signifie que la modification est active.

chat.providers renvoie une entrée par fournisseur offert — le descripteur plus, s’il existe, l’état de la connexion. Tout ce qu’affiche l’interface des paramètres provient de cette charge utile, et c’est pourquoi ajouter un fournisseur est un changement qui ne touche que le 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, et non dedans : supprimer un espace de travail emporte ses jetons de clavardage, et workspace.export ou une sauvegarde copie workspace.db — un espace de travail transmis à quelqu’un d’autre ne porte donc aucun jeton de robot actif. Les écritures sont atomiques (fichier temporaire puis renommage) et restreintes au 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 écrasé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 qui n’ont pas ce concept
external_channel_id Canal Slack C…, groupe G…, message privé D…
external_thread_id Ancre de fil Slack, ou null quand toute la conversation est l’ancre (messages privés du robot)
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 qui impose la même chose pour les identifiants de fil nuls (SQLite considère 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 aucune 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, alors qu’une même personne peut être liée sur plusieurs fournisseurs.

Une liaison est une résolution d’identité, pas un octroi d’accès : l’appartenance est revérifiée à chaque événement entrant, de sorte qu’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 (mise en miroir, badge, 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 sert aussi de garde contre l’écho : le miroir sortant saute tout message qui la porte, ce qui empêche un message de clavardage entrant d’être republié aussitôt.

Comportement Détail
Ce qui est relayé vers l’extérieur Le texte de l’agent seulement, 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 à une poussée 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 tapés dans Control Center Mis en miroir dans le fil lié, précédés du nom d’affichage de l’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édoublonnage Fondé sur l’id d’événement propre au fournisseur ; les 512 derniers sont mémorisés
Agent de premier contact 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 mauvais événement ne fait jamais tomber la passerelle ; la connexion continue de servir toutes les autres conversations