Ir al contenido

Puente de chat

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.

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

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.

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

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.

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.

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.

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.

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

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

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.

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.

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.

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