Ir al contenido

Puente de chat

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.

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

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.

Ámbitos del token del bot:

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

Suscripciones a eventos del bot:

app_home_opened
app_mention
message.channels
message.groups
message.im

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

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.

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.

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

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.

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

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

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.

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.

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.

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