Puente de chat
Proveedores
Sección titulada «Proveedores»| Proveedor | Valor de protocolo | Transporte | Tipo de espacio |
|---|---|---|---|
| Slack | slack |
Socket Mode (WSS saliente) | slack |
Slack es el único proveedor. ChatProvider es un enum de un solo valor y cc_server registra un solo plugin. El esquema, todas las búsquedas y todos los índices únicos usan provider como clave, así que los ids nunca cruzan entre proveedores y agregar uno es un cambio exclusivo 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 entonces usa el nombre con el que se invocó por última vez.
| Comando | Efecto |
|---|---|
/cc help (o /cc sin verbo) |
Enumera lo que el bot puede hacer |
/cc link <code> |
Canjea un código de un solo uso y vincula la identidad de chat de quien lo envía |
/cc ticket <title> |
Registra un ticket local reportado por el miembro vinculado |
/cc ticket <title> | <description> |
Igual, con todo lo que sigue a la primera | como descripción |
| Verbo desconocido | Informa el 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 de Control Center correspondiente; el agente responde en el hilo de Slack |
| Responder en un hilo de Slack puenteado | Continúa el mismo espacio de Control Center — no se necesita mención |
| Mensaje simple en un canal | Se ignora (no está dirigido al bot y no está en un hilo de Slack puenteado) |
| Mensaje directo al bot | Un espacio de Control Center por conversación de mensaje directo; establece 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 del campo | Etiqueta | Obligatorio | Prefijo | Dónde se encuentra |
|---|---|---|---|---|
botToken |
Token de OAuth del usuario bot | Sí | xoxb- |
OAuth & Permissions |
appToken |
Token de nivel de app | Sí | xapp- |
Basic Information → App-Level Tokens, ámbito connections:write |
configRefreshToken |
Token de refresco 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 almacena 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 almacenado.
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 aplica cuando crea o edita la app: Socket Mode habilitado, sin URL de solicitud, la pestaña Messages habilitada y editable, el bot always_online, los ámbitos y eventos requeridos fusionados con lo que la app ya tenga (nunca restados) y un comando slash que lleva la descripción File a ticket or link your Control Center account — la marca que identifica el comando propio de Control Center en un manifiesto exportado.
Estados de la conexión
Sección titulada «Estados de la conexión»| Estado | Valor de protocolo | Significado |
|---|---|---|
| Sin conectar | disconnected |
No hay credenciales almacenadas para este espacio de trabajo × proveedor |
| Conectando… | connecting |
Credenciales almacenadas, el transporte está marcando o volviendo a marcar |
| En vivo | connected |
Transporte activo y recibiendo eventos |
| Error de conexión | error |
Almacenadas pero fallando; el propio error del proveedor se muestra junto a ellas |
La reconexión usa retroceso exponencial con fluctuación aleatoria, acotado a 30 segundos. Una rotación de socket iniciada por el proveedor se reconecta de inmediato sin escalar el retroceso. Una credencial rechazada (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) o un ámbito faltante 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 degrada una capacidad a la vez.
| Capacidad | Slack | Efecto cuando falta |
|---|---|---|
streaming |
sí | La respuesta completa se publica una sola vez, al final |
streamingRequiresThread |
sí | Un destino de respuesta sin anclaje de hilo publica respuestas completas |
ephemeralMessages |
sí | La respuesta a un comando slash sin un identificador de respuesta utilizable se descarta (los rechazos siempre se publican en el hilo) |
threadStatus |
sí | Sin línea transitoria «is thinking…» |
threadTitle |
sí | El hilo de chat conserva su título predeterminado |
slashCommands |
sí | Sin superficie /cc |
taskCards |
sí | Solo texto — sin tarjeta encima de la respuesta y sin 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. Un rechazo 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 rechaza 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 debe nombrar al lector al que va dirigida. Slack exige tanto un usuario destinatario como su equipo para abrir una en un canal (un mensaje directo no) y, de lo contrario, rechaza con missing_recipient_user_id / missing_recipient_team_id — un rechazo 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, así que un canal de Slack Connect se resuelve 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 desvinculó su cuenta de chat, ese turno se publica completo en lugar de transmitirse en streaming.
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 un details de una sola vez cuando el pensamiento está listo. Los details y output del mismo id se concatenan sin separador, así que los details se envían una sola vez y output nunca se transmite en streaming. El título del plan es la solicitud; 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…, luego Cloning …, luego Starting the agent… — una fila, con el título reemplazado |
| Fila de pensamiento | Thinking…, agregada cuando el agente empieza a razonar; el pensamiento son los details de esta fila, enviados una sola vez cuando termina el razonamiento |
| Filas de herramientas | Una por herramienta (Bash which swift), con id único para que queden una al lado de la otra |
| Estado | En curso mientras esa fila se ejecuta; completado cuando empieza la fila siguiente o termina el turno; error para fallido, interrumpido y 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 curso 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 details 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 termina 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) igual publica un solo mensaje que lleva la tarjeta terminada y la nota.
Las tarjetas transmitidas en streaming y las publicadas son cargas útiles distintas:
| Forma | Estructura | Prosa |
|---|---|---|
| Transmitida en streaming | 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 |
Manejo 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 igual llega en vivo y las tarjetas se descartan durante toda la vida de la conexión |
| El proveedor también rechaza la prosa | La transmisión se cierra y la respuesta se publica completa |
| La transmisión se rompe después de que parte de la respuesta llegó | 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 de esta manera.
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 posterior proviene 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 requiere las cuatro condiciones:
| Condición |
|---|
| El proveedor representa tarjetas |
| El streaming está disponible en esta conexión |
| El espacio tiene una solicitud de chat en curso |
| Ninguna tarjeta ya está informando un turno allí |
La solicitud permanece en curso hasta que empieza un turno o falla la preparación. Un evento ready sin una tarjeta ya abierta corresponde a un espacio en caliente y se deja en silencio, y la preparación que nadie en el chat está esperando — una conversación abierta en la app, una que quedó varada y se reanuda al arrancar — no produce ninguna tarjeta.
La página de rebote /open
Sección titulada «La página de rebote /open»Los productos de chat solo aceptan http(s) en un enlace y nada más, así que una llamada a la acción no puede nombrar directamente el esquema de la aplicación de escritorio.
| Propiedad | Valor |
|---|---|
| Ruta | GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id> |
| Origen | Derivado de la URL pública del servidor (ws → http, wss → https); se conserva loopback |
| Respuesta | Una pequeña página HTML 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 que abre hace la autorización |
Campos del perfil del bot
Sección titulada «Campos del perfil del bot»| Campo | Límite | Notas |
|---|---|---|
appName |
1–35 caracteres | De forma predeterminada Control Center · <workspace name> |
botDisplayName |
1–80 caracteres | De forma predeterminada 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 un valor vacío recurre a cc |
agentEnabled |
— | Si se expone la experiencia de mensajes directos y de agente |
Un campo vacío significa «déjalo como está», no «bórralo». Los límites son 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 mayúsculas de minúsculas. Los códigos desconocidos, caducados, de otro espacio de trabajo y de un proveedor incorrecto 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 de protocolo | 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 alcance 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 alcance de espacio de trabajo que lo omita, antes de que se abra cualquier 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 otro |
— |
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 piso guest de su tipo; las mutaciones sin un piso explícito requieren 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 marca nada — el único llamado lo hace el navegador del propio usuario.
Una consulta reactiva completa la superficie:
| Consulta | Tipo | Emite |
|---|---|---|
chat.watchUserLinks |
observación | La lista de miembros, reenviada cada vez que se crea o elimina un vínculo |
Existe porque la vinculación ocurre del otro lado: un miembro escribe el código de un solo uso en el bot, así que ninguna respuesta de un cliente puede devolver la respuesta. Tanto la lista de miembros de los 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 autor de la llamada. chat.updateBotProfile devuelve un remaining_step solo cuando queda un paso (normalmente una reinstalación); su ausencia significa que la edición ya está activa.
chat.providers devuelve una entrada por cada proveedor ofrecido — el descriptor más, cuando existe, el estado de la conexión. Todo lo que muestra la interfaz de ajustes proviene de esa carga útil, y por eso agregar un proveedor es un cambio exclusivo 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 lugar |
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 un respaldo 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 a solo el 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 comprimida — 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 |
Alcance del espacio de trabajo |
provider |
ChatProvider.wire |
external_team_id |
Equipo de Slack T…, vacío para proveedores sin el concepto |
external_channel_id |
Canal de Slack C…, grupo G…, mensaje directo D… |
external_thread_id |
Anclaje del 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 aplica lo mismo para los ids 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 |
Alcance del 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 foránea) |
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 un solo valor 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 verificar en cada evento entrante, así que un vínculo con un miembro que desde entonces fue eliminado se resuelve como un rechazo.
Tipo de espacio y metadatos de los mensajes
Sección titulada «Tipo de espacio y metadatos de los mensajes»Un espacio puenteado se crea con el SpaceKind del proveedor (slack). Se comporta como un espacio normal 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 la protección contra el eco: la réplica saliente omite cualquier mensaje que lo lleve, y eso es lo que evita que un mensaje de chat entrante se publique de vuelta directamente.
Comportamiento del relé
Sección titulada «Comportamiento del relé»| 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 vaciado 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 al hilo vinculado, con el nombre visible del autor como prefijo |
| Turnos vacíos | Se informan como 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 propio id de evento del proveedor; se recuerdan los últimos 512 |
| Agente de la puerta 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 canal de Slack, Slack DM · <request> para un mensaje directo, títulos limitados a 48 caracteres |
| Aislamiento de fallos | Un evento defectuoso nunca derriba el puente; la conexión sigue sirviendo a todas las demás conversaciones |
Ver también
Sección titulada «Ver también»- Configurar la integración de 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