@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
/v1completa (createRestApi, con documento OpenAPI 3.1) y dos superficies más pequeñas (createControlPlaneApiy la heredadacreateManagerApi).
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 concreateBot() 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 deconfigStore: new FileBotConfigStore({ dir }): un archivo JSON por bot en<dir>/<tenantId>/<botId>.json, escrituras atómicas, modo0600(directorios0700). Los archivos que no se pueden parsear se saltan, nunca se borran (onCorruptlos recibe). Los ids de bot y de tenant que solo se diferencian de uno existente en mayúsculas/minúsculas (shop/Shop) se rechazan conCONFLICT: en Windows y macOS compartirían archivo o carpeta.MemoryBotConfigStoreguarda las configs en RAM (tests). Sin store no se persiste nada yrestore()lanzaNOT_CONFIGURED.
Supervisión y reconexión
ConautoReconnect, el manager supervisa cada bot que pediste arrancar:
- cuando un bot emite
connection: closed(salvosessionTakenOver) y el provider no se recupera solo engraceMs, o - cuando falla un arranque,
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 deBot 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 comoAuthorization: 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 }.contentes un subconjunto permitido deOutboundContent:text,image,video,audio,voice,document,sticker(la media solo como{ url: "https://..." }, sin rutas ni buffers),location,contactypoll(2–12 opciones).trigger:{ flowId, chatId, userId?, isGroup?, state? }(statede hasta 16 KiB de JSON). Arranca el flujo comobot.trigger()conifActive: 'replace':404si el flujo no existe, el sujeto está bloqueado o el chat está en handoff;409 BOT_NOT_RUNNINGsi 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 aceptanIdempotency-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:
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
tenantIdy al menos una clave; no hay modo anónimo. Las claves van enX-API-Keyo enAuthorization: Bearer <key>. - Rutas:
GET /v1/healthyGET /v1/bots(bots:read),POST /v1/bots/:id/startyPOST /v1/bots/:id/stop(bots:operate),GET /v1/audit(audit:read). - Las respuestas son
{ data, requestId }; los errores,{ error: { code, requestId } }con400 BAD_REQUEST(un id de bot mal codificado en la ruta),401 UNAUTHORIZED,403 FORBIDDEN,404 NOT_FOUND,409 CONFLICT(unaIdempotency-Keyreutilizada para otra petición) y500 INTERNAL. ElX-Request-Idsolo 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 últimasmaxEntries, 10 000 por defecto; pasa el tuyo conauditLog). Las claves de metadatos que parecen tokens, secretos, contraseñas o API keys se redactan.
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
- README de
@botmesh/manager - Operación
- Transcripciones doradas: condiciona los despliegues a conversaciones grabadas