Saltar al contenido principal
Experimental — aún no publicado en npm@botmesh/manager es experimental: una implementación real y con tests, pero todavía privada (no está en npm ni forma parte de la beta) y su API puede cambiar. Úsalo desde el monorepo (workspace:*).
@botmesh/manager ejecuta muchos bots en un proceso y los expone por HTTP. Tiene cuatro piezas:
  • BotManager: un registro de bots (add, start, stop, restart, startAll, stopAll, shutdown) con eventos tipados.
  • Persistencia y supervisión: configs serializables de bots por tenant, restore() tras un reinicio y reconexión automática con backoff.
  • Flows JSON: documentos de flujo declarativos, sin código, que se compilan a flujos del core y se cambian en caliente con el registro dinámico de flujos del core.
  • APIs HTTP: la API REST /v1 completa (createRestApi, con documento OpenAPI 3.1) y dos superficies más pequeñas (createControlPlaneApi y la heredada createManagerApi).
Todos los handlers HTTP son handlers de node:http sin dependencias.

Inicio rápido

BotManager

Se puede gestionar cualquier cosa con start() / stop() (el tipo estructural ManageableBot): un Bot del core o un stub. Las funciones más allá de start/stop (enviar, flujos, conversaciones, historial de mensajes) solo se usan si el bot tiene el método correspondiente; si no, la API REST responde 501 NOT_SUPPORTED. Un Bot del core los tiene todos. Eventos (tipados con ManagerEventMap): Los logs pasan por pino en debug/warn: inyecta un logger (new BotManager({ logger })) o define BOTMESH_MANAGER_LOG_LEVEL=debug (por defecto silent; el nombre anterior, BOTWA_MANAGER_LOG_LEVEL, se sigue leyendo).

Persistencia multi-tenant

Un bot creado con createBot() tiene detrás un BotConfigRecord: id, tenantId, name?, provider?: { type, options? }, flows? (documentos de flujo JSON), meta?, desiredState (running | stopped), version, createdAt y updatedAt. El bloque provider es opaco para el manager: lo interpreta tu factory(config). Los ids de bot y de tenant deben cumplir [A-Za-z0-9][A-Za-z0-9_.-]{0,63} (se usan como nombres de archivo). Los bots creados sin tenant usan defaultTenantId ('default'). El almacenamiento es intercambiable mediante BotConfigStore (list, get, put, delete):
  • persistence: { dir } es un atajo de configStore: new FileBotConfigStore({ dir }): un archivo JSON por bot en <dir>/<tenantId>/<botId>.json, escrituras atómicas, modo 0600 (directorios 0700). Los archivos que no se pueden parsear se saltan, nunca se borran (onCorrupt los recibe). Los ids de bot y de tenant que solo se diferencian de uno existente en mayúsculas/minúsculas (shop / Shop) se rechazan con CONFLICT: en Windows y macOS compartirían archivo o carpeta.
  • MemoryBotConfigStore guarda las configs en RAM (tests). Sin store no se persiste nada y restore() lanza NOT_CONFIGURED.
Secretosprovider.options se escribe en disco tal cual. Guarda una referencia (el nombre de una variable de entorno, una ref de SecretVault) y resuélvela dentro de la factory.

Supervisión y reconexión

Con autoReconnect, el manager supervisa cada bot que pediste arrancar:
  • cuando un bot emite connection: closed (salvo sessionTakenOver) y el provider no se recupera solo en graceMs, o
  • cuando falla un arranque,
reinicia el bot con backoff y jitter hasta agotar retries. Los bots con config se recrean mediante la factory (provider nuevo). Un stop() manual cancela la supervisión. computeBackoff(attempt, opts) y resolveAutoReconnect(input) se exportan para supervisores propios.

Registro dinámico de flujos del core

El manager usa estos métodos de Bot de @botmesh/core; puedes usarlos directamente en cualquier bot: Con ifActive: 'replace' (lo que hacía siempre el obsoleto bot.triggerFlow()) pasa por los mismos gates de listas de acceso y handoff, el turno FIFO del chat, la resolución de idioma y las métricas, y reemplaza una conversación que ya esté en curso (o dormida) en la misma clave. data se mezcla (superficialmente) en c.state (la misma opción que bot.trigger(); state es un alias obsoleto y pasar los dos lanza un TypeError); body es el ctx.body del mensaje sintético (por defecto ''). Rechaza si el flujo no está registrado; started es false si el bot está parado, las listas de acceso bloquean al sujeto o el chat está en handoff (reason dice cuál).

Flows JSON

Un documento de flujo es solo datos, así que es seguro aceptarlo por la API y persistirlo. compileFlowDocument(doc) lo valida y devuelve un CompiledFlow para bot.registerFlow() / bot.replaceFlow(); lanza FlowDocumentError (con issues: { path, message }[]) si el documento no es válido. validateFlowDocument(doc) devuelve los problemas sin lanzar, e isFlowDocument(doc) es un type guard.
Pasos: Los documentos admiten además los pasos media, form, guard, subflow, http, handoff y ai: todos los tipos de paso se describen en Esquema JSON de flujos. Los textos aceptan marcadores {{ path }} sobre c.state y $body, $from, $sender, $isGroup. Las expresiones (condition.if, ask.validate.expr) usan un lenguaje pequeño y seguro que se parsea al compilar (sin eval ni regex): literales, rutas del estado, == != < <= > >= && || ! + -, paréntesis y las funciones len, lower, upper, trim, number, string, contains, startsWith, endsWith, empty, oneOf. Límites (FLOW_DOCUMENT_LIMITS): 500 pasos, 8 niveles de anidación, 4096 caracteres por texto, 20 opciones, 50 palabras clave y expresiones de hasta 1000 caracteres. El JSON Schema (draft 2020-12) se exporta como FLOW_DOCUMENT_SCHEMA y se sirve en GET /v1/flows/schema. En un bot gestionado usa manager.addFlow(botId, doc), manager.putFlow(botId, flowId, doc) (crea o cambia en caliente; el id del documento debe coincidir), manager.removeFlow(botId, flowId) y manager.listFlowDocuments(botId). En los bots con config, los documentos se persisten con la config y se vuelven a registrar en cada reconstrucción. Los pasos subflow se resuelven contra los demás flujos guardados del bot (también en createBot({ flows }), updateBot() y restore()), y removeFlow() responde CONFLICT (409 por REST) mientras otro flujo guardado siga usando como subflujo el que se quiere borrar. Un subflujo se copia dentro del flujo al compilarlo, así que putFlow() recompila también cada flujo guardado que use el que cambia (directamente o a través de otro subflujo); todos compilan antes de cambiar nada. La config se guarda antes de tocar el bot en vivo: si el guardado falla, la llamada rechaza y el bot sigue con los flujos anteriores.

API REST /v1 (createRestApi)

createRestApi(manager, options) devuelve un handler (req, res, next?) => void. Las peticiones fuera del prefijo llaman a next() si se pasa (para montarlo dentro de otro servidor) o reciben 404.

Autenticación y tenants

Envía el token como Authorization: Bearer <token> o X-API-Key: <token> (se compara mediante digests SHA-256 en tiempo constante). Un token sin tenantId (o con '*') ve todos los tenants; un token de tenant solo ve sus propios bots, y cualquier otro bot responde 404. Scopes ('*' los concede todos): bots:read, bots:write, bots:operate, connections:read, messages:read, messages:send, conversations:read, conversations:write, flows:read, flows:write, metrics:read.

Rutas

Todas las rutas son relativas al prefijo. Cuerpos de las peticiones:
  • send: { chatId, text } o { chatId, content }. content es un subconjunto permitido de OutboundContent: text, image, video, audio, voice, document, sticker (la media solo como { url: "https://..." }, sin rutas ni buffers), location, contact y poll (2–12 opciones).
  • trigger: { flowId, chatId, userId?, isGroup?, state? } (state de hasta 16 KiB de JSON). Arranca el flujo como bot.trigger() con ifActive: 'replace': 404 si el flujo no existe, el sujeto está bloqueado o el chat está en handoff; 409 BOT_NOT_RUNNING si el bot está detenido.
  • POST /bots: { id?, tenantId?, name?, provider?, flows?, meta?, autoStart? }. Un token de tenant solo puede crear bots en su propio tenant.

Respuestas y códigos de estado

Las respuestas correctas son { data, requestId } (el documento OpenAPI y el schema de flujos se devuelven tal cual). Los errores son { error: { code, message, requestId, details? } }. Toda respuesta lleva X-Request-Id (se reutiliza uno entrante válido), Cache-Control: no-store y X-Content-Type-Options: nosniff. Con el rate limit activo (por defecto), todas las rutas, públicas incluidas, están limitadas y añaden RateLimit-Limit y RateLimit-Remaining.

Idempotencia

Las rutas que mutan aceptan Idempotency-Key (1–200 caracteres ASCII visibles). Las claves van por token. Repetir una petición con la misma clave, método, ruta y cuerpo devuelve la respuesta guardada (cabecera Idempotent-Replayed: true); reutilizar la clave para otra petición da 422 IDEMPOTENCY_KEY_REUSED. Los resultados 5xx no se guardan, así que se pueden reintentar. MemoryIdempotencyStore y TokenBucketRateLimiter (los de por defecto) viven en la memoria del proceso: con varias instancias, conecta tu propio IdempotencyStore / RateLimiter.

Documento OpenAPI

El documento OpenAPI 3.1 se genera a partir de la misma tabla de rutas que usa el router (ROUTES), así que no puede desalinearse de la implementación. Obtenlo de un servidor en marcha:
o constrúyelo en código, sin servidor:
buildOpenApiDocument({ prefix?, title?, version?, servers? }) usa por defecto '/v1', 'botmesh Manager API', '1.0.0' y un servidor relativo. Declara los dos esquemas de seguridad (bearerAuth y apiKey mediante X-API-Key) y un x-required-scope por operación.

Otras superficies HTTP

createControlPlaneApi

Una superficie mínima y auditada, con API keys ligadas a un tenant. También usa el prefijo /v1 por defecto, así que no la montes junto a createRestApi en el mismo prefijo.
  • Exige tenantId y al menos una clave; no hay modo anónimo. Las claves van en X-API-Key o en Authorization: Bearer <key>.
  • Rutas: GET /v1/health y GET /v1/bots (bots:read), POST /v1/bots/:id/start y POST /v1/bots/:id/stop (bots:operate), GET /v1/audit (audit:read).
  • Las respuestas son { data, requestId }; los errores, { error: { code, requestId } } con 400 BAD_REQUEST (un id de bot mal codificado en la ruta), 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND, 409 CONFLICT (una Idempotency-Key reutilizada para otra petición) y 500 INTERNAL. El X-Request-Id solo se devuelve tal cual si tiene de 1 a 128 caracteres de [A-Za-z0-9._:-]; si no, se genera uno.
  • Las llamadas de health, listado y start/stop, y toda petición denegada o a un bot desconocido de una clave autenticada, se escriben en un AuditLog (en memoria y acotado: las últimas maxEntries, 10 000 por defecto; pasa el tuyo con auditLog). Las claves de metadatos que parecen tokens, secretos, contraseñas o API keys se redactan.
Piezas relacionadas: SecretVault (AES-256-GCM, clave maestra de 32 bytes; put solo devuelve { ref, version }), IdempotencyRegistry (recuerda las últimas maxEntries claves, 1000 por defecto), assertAuthorized y verifyApiKey (comparación en tiempo constante).

Reconciliador de estado deseado

MemoryDesiredStateStore y BotReconciler separan el estado deseado (running / stopped) del observado. reconcile(tenantId, botId) arranca o detiene un bot solo cuando hace falta; reconcileTenant(tenantId) lo hace con todos los bots de un tenant. Las escrituras usan versiones compare-and-set (DesiredStateConflictError), así que dos actualizaciones en competencia no se pisan en silencio. Sustituye el store en memoria por un DesiredStateStore durable antes de ejecutar más de un proceso.

createManagerApi (heredada)

createManagerApi(manager, { prefix?, auth?: { token, header? } }) conserva la API compacta original: GET {prefix}/health, GET {prefix}/metrics (texto Prometheus), GET {prefix}/bots, GET {prefix}/bots/:id, POST {prefix}/bots/:id/start y POST {prefix}/bots/:id/stop. Sin auth no tiene autenticación y registra un aviso una sola vez; pon siempre un token. Un start/stop fallido responde 500 { "error": "start failed" } (el error va a onError(error, { id, action }), nunca al cliente). Para despliegues nuevos, usa createRestApi.

Ver también