Puente de chat
Proveedores
Sección titulada «Proveedores»| Proveedor | Valor serializado | Transporte | Tipo de espacio |
|---|---|---|---|
| Slack | slack |
Socket Mode (WSS saliente) | slack |
Slack es el único proveedor. ChatProvider es una enumeración de un único valor y cc_server registra un solo plugin. El esquema, todas las búsquedas y todos los índices únicos llevan la clave provider, así que los identificadores nunca cruzan entre proveedores y añadir uno es un cambio solo del servidor.
Una app del lado del proveedor por espacio de trabajo × proveedor.
Comandos de chat
Sección titulada «Comandos de chat»El nombre del comando es por app (cc de forma predeterminada, así que /cc). Un administrador puede cambiarlo; el puente usa entonces aquel con el que se invocó por última vez.
| Comando | Efecto |
|---|---|
/cc help (o /cc sin verbo) |
Enumera lo que puede hacer el bot |
/cc link <code> |
Canjea un código de un solo uso y vincula la identidad de chat del remitente |
/cc ticket <title> |
Registra un ticket local informado por el miembro vinculado |
/cc ticket <title> | <description> |
Lo mismo, con todo lo que sigue al primer | como descripción |
| Verbo desconocido | Informa del verbo desconocido junto con el texto de ayuda |
Formas de llegar a un agente:
| Gesto | Comportamiento |
|---|---|
@bot … en un canal |
Abre un hilo de Slack y un espacio correspondiente en Control Center; el agente responde en el hilo de Slack |
| Respuesta en un hilo de Slack puenteado | Continúa el mismo espacio de Control Center — no hace falta mención |
| Mensaje normal en un canal | Se ignora (no va dirigido al bot y no está en un hilo de Slack puenteado) |
| Mensaje directo al bot | Un espacio de Control Center por conversación por mensaje directo; define un título de hilo de Slack y un estado «is thinking…» mientras el agente trabaja |
Campos de credenciales (Slack)
Sección titulada «Campos de credenciales (Slack)»Los declara el descriptor del proveedor, que es lo que genera el diálogo de conexión y valida el envío.
| Id. de campo | Etiqueta | Obligatorio | Prefijo | Dónde se encuentra |
|---|---|---|---|---|
botToken |
Token del bot | Sí | xoxb- |
OAuth & Permissions |
appToken |
Token de aplicación | Sí | xapp- |
Basic Information → App-Level Tokens, ámbito connections:write |
configRefreshToken |
Token de configuración de la app | No | xoxe |
Tokens de configuración de la app en api.slack.com/apps |
configRefreshToken es la credencial de gestión de la app: sin ella la conexión funciona, pero el bot no se puede crear ni editar desde Control Center. Rota en cada uso y el valor rotado se guarda de inmediato.
Los valores se recortan, las claves no declaradas se descartan y un prefijo que no coincide se rechaza con la misma frase en el cliente y en el servidor. Los campos obligatorios deben estar presentes en cada conexión; un campo opcional que se deja vacío conserva lo que ya estaba guardado.
Requisitos de la app de Slack
Sección titulada «Requisitos de la app de Slack»Ámbitos del token del bot:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailSuscripciones a eventos del bot:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imAjustes estructurales que Control Center impone cuando crea o edita la app: Socket Mode activado, ninguna URL de solicitud, la pestaña Messages activada y con escritura habilitada, el bot always_online, los ámbitos y eventos necesarios fusionados con lo que la app ya tenga (nunca restados) y un comando slash con la descripción File a ticket or link your Control Center account — el marcador que identifica el comando propio de Control Center en un manifiesto exportado.
Estados de conexión
Sección titulada «Estados de conexión»| Estado | Valor serializado | Significado |
|---|---|---|
| Sin conectar | disconnected |
No hay credenciales guardadas para este espacio de trabajo × proveedor |
| Conectando… | connecting |
Credenciales guardadas, el transporte está conectando o reconectando |
| En vivo | connected |
El transporte está activo y recibe eventos |
| Error de conexión | error |
Guardadas pero fallando; el error del propio proveedor se muestra al lado |
La reconexión es un retroceso exponencial con fluctuación aleatoria, acotado a 30 segundos. Una rotación de socket iniciada por el proveedor reconecta de inmediato sin aumentar el retroceso. Una credencial rechazada (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) o un ámbito ausente es terminal: el puente deja de reintentar hasta que lleguen credenciales nuevas.
Capacidades del proveedor (Slack)
Sección titulada «Capacidades del proveedor (Slack)»El puente está escrito contra la superficie más rica y se degrada capacidad a capacidad.
| Capacidad | Slack | Efecto cuando falta |
|---|---|---|
streaming |
sí | La respuesta entera se publica una sola vez, al final |
streamingRequiresThread |
sí | Un destino de respuesta sin anclaje de hilo publica respuestas enteras |
ephemeralMessages |
sí | La respuesta a un comando slash sin un identificador de respuesta utilizable se descarta (las negativas siempre se publican en el hilo) |
threadStatus |
sí | Ninguna línea transitoria «is thinking…» |
threadTitle |
sí | El hilo de chat conserva su título predeterminado |
slashCommands |
sí | Ninguna superficie /cc |
taskCards |
sí | Solo texto — ninguna tarjeta sobre la respuesta y ningún enlace «View in Control Center» |
maxMessageLength |
39.000 | Las respuestas más largas se truncan con un puntero a Control Center |
maxStreamChunkLength |
3.800 | Los añadidos se dividen en un límite de párrafo, línea o espacio |
Anunciar streaming no es prometerlo: el de Slack necesita la función Agents & AI Apps en un plan de pago. Una negativa en el momento de la llamada (streaming_not_supported, feature_not_enabled, paid_only, …) se recuerda durante toda la vida de la conexión y se expone como streamingAvailable: false. Las tarjetas de tarea de Slack viajan en la misma llamada, así que un Slack que rechace el modo de visualización de la tarjeta conserva su transmisión y pierde solo la tarjeta, una vez, durante toda la vida de la conexión.
Una transmisión también tiene que indicar para quién es. Slack exige tanto un usuario destinatario como su equipo para abrir una en un canal (un mensaje directo no lo exige) y, en caso contrario, rechaza con missing_recipient_user_id / missing_recipient_team_id — una negativa por llamada, así que el puente sigue intentándolo y, mientras tanto, el turno llega como una sola respuesta publicada. El destinatario es quien preguntó, tomado del mensaje entrante (su equipo, no el de la app, de modo que un canal de Slack Connect se resuelva correctamente). Para un turno que nadie inició desde el chat —trabajo lanzado en Control Center sobre una conversación ya puenteada— es el miembro cuyo vínculo creó el puente; si desde entonces ha desvinculado su cuenta de chat, ese turno se publica entero en lugar de transmitirse.
Tarjetas de tarea
Sección titulada «Tarjetas de tarea»Una tarjeta agrupada por turno del agente (task_display_mode: plan), con una fila por paso. Cada fila es un task_update de id + title + status, más unos details de un solo envío cuando el pensamiento está listo. Los details y el output de un mismo id se concatenan sin separador, así que los details se envían una vez y el output nunca se transmite. El título del plan es la petición; la respuesta es un añadido posterior aparte que lleva un fragmento markdown_text.
| Parte | Contenido |
|---|---|
| Título del plan | Lo que se pidió |
| Fila de preparación | Working on it…, después Cloning …, después Starting the agent… — una fila, con el título reemplazado |
| Fila de pensamiento | Thinking…, añadida cuando el agente empieza a razonar; el pensamiento son los details de esta fila, enviados una vez al terminar el razonamiento |
| Filas de herramientas | Una por herramienta (Bash which swift), con un id único para que se sitúen una al lado de otra |
| Estado | En curso mientras esa fila se ejecuta; completa cuando empieza la fila siguiente o termina el turno; error si falló, se interrumpió o alcanzó el límite de turnos |
| Llamada a la acción | View in Control Center como fila final cuando termina el turno — ausente mientras el trabajo sigue en marcha y ausente cuando el servidor no tiene una URL pública accesible |
Lo que una tarjeta nunca lleva: salida de herramientas, un argumento fuera de esa lista de permitidos o cualquier cosa leída de la base de datos. La prosa del razonamiento son solo los detalles de la fila Thinking… (limitados a 256 caracteres), no el cuerpo del mensaje. Una tarjeta que no diría nada nuevo no se reenvía. Una mención abre la transmisión de inmediato para que el hilo de Slack no quede en silencio; un turno que después no produjo ningún texto cierra esa misma tarjeta con una nota breve en lugar de publicar un segundo mensaje. Un turno sin palabras en un espacio que nunca abrió una transmisión (sin tarjetas, o con el streaming ya rechazado) publica de todos modos un mensaje que lleva la tarjeta terminada y la nota.
Las tarjetas transmitidas y las publicadas son cargas útiles distintas:
| Forma | Estructura | Prosa |
|---|---|---|
| Transmitida | plan_update más fragmentos task_update con clave id |
Cadena simple, limitada a 256 caracteres por fragmento |
| Publicada | Un bloque plan que envuelve task_cards con clave task_id |
Texto enriquecido |
Gestión de fallos:
| Situación | Comportamiento |
|---|---|
| El proveedor rechaza la tarjeta en un añadido | El añadido se reintenta de inmediato sin ella; la prosa sigue llegando en directo y las tarjetas se descartan durante toda la vida de la conexión |
| El proveedor rechaza también la prosa | La transmisión se cierra y la respuesta se publica entera |
| La transmisión se rompe después de que parte de la respuesta haya llegado | La transmisión se cierra y no se vuelve a publicar nada |
Consulta Puentes de chat para saber por qué la carga útil se divide así.
Preparación, antes de que haya un turno
Sección titulada «Preparación, antes de que haya un turno»La tarjeta se abre cuando se acepta una mención, antes del clonado y antes del primer token; el turno que sigue adopta la misma tarjeta y la misma transmisión. La narración a partir de ahí viene de SpaceProvisioningChanged en el bus de eventos de dominio — el mismo progreso que el aviso de aprovisionamiento del escritorio lee de la fila del espacio.
| Estado de aprovisionamiento | Narración |
|---|---|
| Mención aceptada | Working on it… |
Paso repo |
Cloning <repo>… |
Paso prCheckout |
Checking out the pull request in <repo>… |
Paso script |
Running the setup script for <repo>… |
Paso agent |
Setting up <agent>… |
provisioning, aún sin paso |
Preparing the workspace… |
ready |
Starting the agent… |
| El agente razona | Thinking… |
failed |
Workspace setup failed — details are in Control Center. |
cancelled |
Workspace setup was stopped — details are in Control Center. |
La narración de la preparación exige las cuatro condiciones:
| Condición |
|---|
| El proveedor muestra tarjetas |
| El streaming está disponible en esta conexión |
| El espacio tiene una petición de chat en curso |
| Ninguna tarjeta informa ya de un turno allí |
La petición permanece en curso hasta que empieza un turno o falla la preparación. Un evento ready sin ninguna tarjeta ya abierta es un espacio ya preparado y se deja en silencio, y un aprovisionamiento que nadie en el chat está esperando —una conversación abierta en la app, una abandonada que se reanuda al arrancar— no produce ninguna tarjeta.
La página intermedia /open
Sección titulada «La página intermedia /open»Los productos de chat aceptan http(s) en un enlace y nada más, así que una llamada a la acción no puede nombrar directamente el esquema del escritorio.
| Propiedad | Valor |
|---|---|
| Ruta | GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id> |
| Origen | Derivado de la URL pública del servidor (ws → http, wss → https); el loopback se mantiene |
| Respuesta | Una pequeña página HTML con no-store que navega a control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> y muestra un botón manual |
| Validación | Los ids deben coincidir con [A-Za-z0-9_.-]{1,128}; cualquier otra cosa es un 404 |
| Acceso | Pública e inerte — no lee ninguna base de datos, así que no es un oráculo de existencia; la app a la que abre es la que autoriza |
Campos del perfil del bot
Sección titulada «Campos del perfil del bot»| Campo | Límite | Notas |
|---|---|---|
appName |
1–35 caracteres | Su valor predeterminado es Control Center · <workspace name> |
botDisplayName |
1–80 caracteres | Su valor predeterminado es control-center |
description |
≤ 140 caracteres | |
agentDescription |
≤ 300 caracteres | Se muestra encima de un mensaje directo con el bot |
command |
≤ 32 caracteres | Normalizado: se quitan las barras iniciales, se pasa a minúsculas, se elimina [^a-z0-9_-] y, si queda vacío, se recurre a cc |
agentEnabled |
— | Si se expone la experiencia de mensajes directos y agente |
Un campo vacío significa «dejarlo como está», no «vaciarlo». Los límites son los de Slack, que son los más estrictos de los productos modelados.
Códigos de vinculación
Sección titulada «Códigos de vinculación»| Propiedad | Valor |
|---|---|
| Longitud | 6 caracteres |
| Alfabeto | 23456789ABCDEFGHJKLMNPQRSTVWXYZ (sin 0/O, 1/I, U) |
| Vigencia | 15 minutos |
| Usos | Uno |
| Almacenamiento | En memoria — reiniciar el servidor invalida los códigos pendientes |
| Alcance | Un espacio de trabajo y un proveedor; generar un código nuevo invalida el anterior del miembro |
El canje se compara sin distinguir entre mayúsculas y minúsculas. Los códigos desconocidos, caducados, de otro espacio de trabajo y del proveedor equivocado fallan todos de forma idéntica a propósito.
Métodos de vinculación
Sección titulada «Métodos de vinculación»| Método | Valor serializado | Cómo |
|---|---|---|
| Código | code |
El miembro canjeó /cc link <code> |
| Correo | email |
El correo verificado del proveedor coincidió con un usuario que ya es miembro del espacio de trabajo |
Operaciones RPC
Sección titulada «Operaciones RPC»Todas tienen ámbito de espacio de trabajo. El servidor no tiene estado, así que cada llamada lleva su propio workspace_id en los argumentos; el despachador rechaza una operación con ámbito de espacio de trabajo que lo omita, antes de que se abra ninguna base de datos. Las credenciales solo viajan hacia dentro — las lecturas de estado nunca las devuelven.
| Operación | Tipo | Rol mínimo | Clases de acción |
|---|---|---|---|
chat.providers |
lectura | guest |
— |
chat.status |
lectura | guest |
— |
chat.connect |
mutación | admin |
secretAccess, networkEgress |
chat.disconnect |
destructiva | admin |
secretAccess |
chat.listUserLinks |
lectura | guest |
— |
chat.beginUserLink |
mutación | member |
— |
chat.unlinkUser |
mutación | member para ti mismo, admin para cualquier otra persona |
— |
chat.createApp |
mutación | admin |
secretAccess, networkEgress |
chat.setupLink |
lectura | admin |
— |
chat.botProfile |
lectura | admin |
networkEgress |
chat.updateBotProfile |
mutación | admin |
networkEgress |
Las operaciones de lectura heredan el suelo guest de su tipo; las mutaciones sin un suelo explícito exigen member.
chat.setupLink compone una URL de creación de app del proveedor a partir del perfil del bot. No declara ninguna clase de acción porque no lee ningún secreto ni establece ninguna conexión de red — el navegador del propio usuario hace la única llamada.
Una consulta reactiva completa la superficie:
| Consulta | Tipo | Emite |
|---|---|---|
chat.watchUserLinks |
observación | La lista, reenviada cada vez que se crea o se elimina un vínculo |
Existe porque la vinculación ocurre en el otro lado: un miembro escribe el código de un solo uso en el bot, así que ninguna respuesta del cliente puede devolver el resultado. Tanto la lista de ajustes como el diálogo abierto «Vincular mi cuenta» la siguen, y el diálogo cambia a una confirmación en el momento en que llega el vínculo del propio miembro.
chat.beginUserLink siempre genera un código para la identidad del propio llamante. chat.updateBotProfile devuelve un remaining_step solo cuando queda algún paso (normalmente una reinstalación); su ausencia significa que la edición ya está activa.
chat.providers devuelve una entrada por proveedor ofrecido — el descriptor y, cuando existe, el estado de la conexión. Todo lo que muestra la interfaz de ajustes procede de esa carga útil, y por eso añadir un proveedor es un cambio solo del servidor.
Almacenamiento
Sección titulada «Almacenamiento»| Qué | Dónde |
|---|---|
| Credenciales de conexión | <dataDir>/<workspaceId>/chat_credentials/<provider>.json |
| Configuración guiada a medias | <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json |
| Archivos heredados de Slack | <dataDir>/<workspaceId>/slack_credentials.json y slack_app_setup.json, leídos una vez y migrados en su sitio |
Las credenciales viven junto a la base de datos del espacio de trabajo, no dentro de ella: eliminar un espacio de trabajo se lleva consigo sus tokens de chat, y workspace.export o una copia de seguridad copian workspace.db — así que un espacio de trabajo entregado a otra persona no lleva tokens de bot activos. Las escrituras son atómicas (archivo temporal más renombrado) y se restringen al propietario.
Tablas de la base de datos
Sección titulada «Tablas de la base de datos»Ambas viven en la base de datos del espacio de trabajo, que lleva una línea base v1 consolidada — así que una lectura entre espacios de trabajo no compila.
chat_space_links — una conversación externa ↔ un espacio de Control Center:
| Columna | Notas |
|---|---|
id |
UUID v4 |
workspace_id |
Ámbito de espacio de trabajo |
provider |
ChatProvider.wire |
external_team_id |
Equipo de Slack T…, vacío en proveedores sin el concepto |
external_channel_id |
Canal de Slack C…, grupo G…, mensaje directo D… |
external_thread_id |
Anclaje de hilo de Slack, o null cuando toda la conversación es el anclaje (mensajes directos del bot) |
cc_space_id |
REFERENCES spaces(id) ON DELETE CASCADE |
created_by_user_id, created_at, last_activity_at |
Único en (workspace_id, provider, external_channel_id, external_thread_id), con un índice parcial que impone lo mismo para los identificadores de hilo nulos (SQLite trata los NULL como distintos).
chat_user_links — un miembro externo ↔ un usuario de Control Center:
| Columna | Notas |
|---|---|
id |
UUID v4 |
workspace_id |
Ámbito de espacio de trabajo |
provider |
ChatProvider.wire |
external_team_id, external_user_id |
Miembro de Slack U… |
user_id |
Usuario de Control Center (global, así que sin clave externa) |
method |
code o email |
linked_at |
Único en (workspace_id, provider, external_team_id, external_user_id) y en (workspace_id, provider, user_id) — atribución de valor único en ambas direcciones, mientras que una persona puede estar vinculada en varios proveedores.
Un vínculo es resolución de identidad, no una concesión de acceso: la pertenencia se vuelve a comprobar en cada evento entrante, así que un vínculo con un miembro que desde entonces ha sido eliminado se resuelve en una negativa.
Tipo de espacio y metadatos de mensajes
Sección titulada «Tipo de espacio y metadatos de mensajes»Un espacio puenteado se crea con el SpaceKind del proveedor (slack). Se comporta como un espacio corriente orientado a personas —se notifica y se secciona como topic— y isExternalChat es la comprobación por la que pasa cada comportamiento (réplica, insignias, retención).
Cada mensaje que el puente trae lleva un sello metadata['chat']:
{ "chat": { "provider": "slack", "teamId": "T024BE7LD", "channelId": "C0G9QF9GW", "threadId": "1700000000.000100", "messageId": "1700000000.000200", "userId": "U024BE7LH", "userLabel": "sam" }}El sello es también el guardián contra ecos: la réplica saliente omite cualquier mensaje que lo lleve, y eso es lo que impide que un mensaje de chat entrante se publique directamente de vuelta.
Comportamiento de retransmisión
Sección titulada «Comportamiento de retransmisión»| Comportamiento | Detalle |
|---|---|
| Qué se retransmite hacia fuera | Solo el texto del agente, más la línea de estado de la tarjeta de tarea. La prosa del razonamiento y la salida de las herramientas se quedan en Control Center |
| Cadencia del streaming | Limitada a un volcado por segundo mientras dura un turno; la tarjeta y el texto que explica viajan en la misma llamada |
| Mensajes humanos escritos en Control Center | Se replican en el hilo vinculado, con el nombre visible del autor como prefijo |
| Turnos vacíos | Se informan con una nota breve (la ejecución falló, se interrumpió o se detuvo en su límite de turnos) en lugar de silencio |
| Deduplicación | Con clave en el id de evento propio del proveedor; se recuerdan los últimos 512 |
| Agente de entrada | Una conversación puenteada nueva despierta al agente CEO del espacio de trabajo; si no, a su primer agente; si no, a nadie |
| Nombres de espacios | #channel · <request> para un hilo de un canal de Slack y Slack DM · <request> para un mensaje directo; los títulos se limitan a 48 caracteres |
| Aislamiento de fallos | Un evento defectuoso nunca tumba el puente; la conexión sigue atendiendo todas las demás conversaciones |
Consulta también
Sección titulada «Consulta también»- Configurar la integración con Slack
- Vincular tu cuenta de Slack
- Personalizar el bot de chat
- Puentes de chat
- Barandillas para las clases de acción que declaran las operaciones