Herramientas de extensiones
SDK, plantilla, validador, playground, galería y gestión en /extensions.
En esta página
Todo lo necesario para escribir, validar, probar y gestionar extensiones sin desplegar nada: SDK con tipos, plantilla, validador por línea de comandos, playground y galería de componentes en la propia aplicación, y la página /extensions para usuarios y autores. La referencia de componentes, expresiones y acciones está en Kit de componentes y UI.
TSDocs
¿Buscas los tipos exactos? TSDocs: referencia del SDK se genera de los .d.ts reales: Manifest, puntos de montaje, acciones, expresiones, ctx.services... con ejemplos, buscador y enlaces entre tipos.
Para hablar con Discord desde una extensión (bot por REST sin gateway, Interactions por HTTP): DiscordLib: bot de Discord.
SDK y plantilla#
En bloomx-extensions/_shared/sdk/, sin dependencias. ui.* (un helper por componente), act.* (acciones) y expr.* (expresiones) se generan del catálogo (ui-schema.ts), así que los nombres y los tipos (ui.d.ts) nunca se desfasan; un test falla si el catálogo cambia y no se regenera (npm run sdk:generate). ctx.d.ts tipa handler(ctx) y ctx.services para server.js con JSDoc.
// mi-ext/manifest.src.mjs
import { ui, act, expr, defineManifest } from '../_shared/sdk/index.js';
export default defineManifest({
id: 'acme-hello', name: 'Hello', version: '1.0.0', permissions: ['READ_USER'],
api: { functions: { greet: { handler: 'greet' } } },
mounts: [
{ point: 'COMPOSER_TOOLBAR', component: ui.button('Saludo', { icon: 'Smile', variant: 'ghost', onClick: act.openOverlay('acme-form') }) },
{ point: 'OVERLAY', id: 'acme-form', component: ui.modal('Saludo', { width: 'md' },
ui.form({
fields: [{ name: 'name', label: 'Nombre', type: 'text', rules: { required: true, minLength: 2 } }],
onSubmit: act.callBackend('greet', undefined, {
retry: { attempts: 2 },
onSuccess: act.all(act.insertContent(expr.of('result.text'), { closeOverlay: true }), act.toast('Listo', { tone: 'success' })),
}),
})) },
],
});_template/es una extensión completa lista para copiar (manifest con overlay y formulario validado,server.jscon la firmahandler(ctx), tests y README). Su JSON se llamamanifest.template.jsona propósito para que ningún script de sincronización la publique como extensión instalable.build-manifest.mjsconviertemanifest.src.mjsenmanifest.jsonvalidándolo (manifest, UI y sintaxis de expresiones);--checkfalla si el JSON no coincide.- Los helpers devuelven JSON plano: también puedes escribir el manifest a mano; el resultado es el mismo.
Validador por línea de comandos#
# Desde bloomx-extensions (sin instalar nada: Node 22+)
cp -r _template mi-extension
node --experimental-strip-types _shared/sdk/build-manifest.mjs mi-extension/manifest.src.mjs # genera manifest.json
node --experimental-strip-types _shared/validate.mjs mi-extension # valida (--strict: lo obsoleto es error)
node --experimental-strip-types --test mi-extension/tests/*.test.mjs # tests de la extension
npm test # contrato + schema + SDK + validadorvalidate.mjs comprueba el manifest, el UI (errores con ruta y avisos de obsolescencia), la firma de los handlers (existen en server.js y declaran un solo parámetro) y los permisos (los servicios usados frente a los declarados). Salida agrupada y legible, --json para CI, --all para todas las extensiones y la plantilla; código de salida 0 (correcto), 1 (errores) o 2 (uso incorrecto). Los tests de contrato lo ejecutan sobre las 21 extensiones.
Playground#
/extensions/playground permite pegar o editar un manifest completo (o un nodo de UI suelto) y verlo renderizado en vivo, con errores de validación en línea, en cualquier tema y tamaño:
- Validación en línea con ruta y mensaje: errores JSON (línea y columna), manifest, UI y expresiones; los avisos de obsolescencia (formato antiguo) van aparte y el interruptor Estricto los trata como errores. El botón Migrar reescribe el JSON al formato nuevo.
- Temas: claro, oscuro, los 8 genéricos, las paletas de empresa de prueba (claro/oscuro) y la empresa real del dominio. La vista previa aplica los tokens solo a su marco, sin cambiar el tema de la aplicación. Tamaños: móvil (375 px), tablet y escritorio.
- Contexto simulado: el correo abierto, las funciones del redactor (
insertBody,appendBody,setSubject,addAttachment,uploadAttachment; quedan en un panel de Eventos) y las respuestas del backend ({ "saveNote": { "result": {...}, "delayMs": 300 } }o{ "error": "...", "times": 1 }para probar el reintento). Nunca se llama al backend real. Un panel de Estado muestrastateen vivo.
Quién puede verlo
El playground y la galería son herramientas de administración: solo las ve quien administra la instancia (la misma comprobación de la consola de admin). Para probar en local sin sesión, arranca next dev con NEXT_PUBLIC_BLOOMX_THEME_OVERRIDE definida (solo desarrollo; se ignora en producción).
Galería de componentes#
/extensions/components es el catálogo vivo: se genera desde el mismo schema que valida y que pinta, así que siempre está al día. Para cada componente: descripción, tabla de props (tipos, valores permitidos, defectos), ejemplos renderizados en vivo, botón Copiar JSON, Abrir en el playground y una vista en todos los temas a la vez. Un test exige un ejemplo válido por cada componente del catálogo.
Gestión de extensiones (usuarios y autores)#
/extensions lista las extensiones instaladas en tu organización:
- Búsqueda (sin tildes ni mayúsculas), categorías (
manifest.categoryymanifest.tags, o derivadas de los puntos de montaje y permisos) y estado: activa, desactivada por ti, desactivada por la organización, con errores, de pago o gratuita. - Activar o desactivar por usuario sin desinstalar, y orden de botones y paneles (subir/bajar con anuncio accesible y "Restablecer orden"). Las preferencias se guardan en tu navegador y en tus ajustes (
expansionSettings), no afectan a otros usuarios. Desactivar una extensión también evita que sus reglas de servidor (por ejemplo al enviar un correo) se ejecuten para ti, salvo las obligatorias: si tu organización marca una como obligatoria (por ejemplo DLP), su interruptor aparece bloqueado con una explicación y no se puede apagar. Ajustes → Extensiones tiene un enlace directo a esta página. - Detalle: versión instalada y actualización disponible (si el catálogo la ofrece), permisos en lenguaje claro ordenados por sensibilidad, dónde aparece, vista previa del panel con datos simulados, capturas (
manifest.screenshots, solo https), historial (manifest.changelog) y errores. - Errores de ejecución para el autor: validación de UI, excepciones de render, acciones fallidas y expresiones inválidas se registran (no solo en consola) con extensión, ruta, mensaje, número de veces y hora; Copiar informe (sin correos ni tokens) y Limpiar.
Campos opcionales nuevos del manifest#
| Campo | Tipo | Para qué |
|---|---|---|
state | objeto pequeño | Estado inicial de cada mount/overlay (${state.x}); máx. 50 KB |
category | texto | Categoría en /extensions (correo, redactor, calendario, contactos, automatización, IA, integraciones, ajustes) |
tags | lista de textos | Etiquetas para la búsqueda |
screenshots | lista de URLs https | Capturas en el detalle |
changelog | [{ version, date, notes }] | Historial de versiones mostrado como texto |
Límites
Estos campos no se validan aún en el schema de manifest (se leen de forma defensiva). Las preferencias de usuario no se aplican a los hooks de servidor (DLP, webhooks): solo a lo que se pinta. El playground no ejecuta server.js: simula sus respuestas. La vista previa de /extensions es solo visual.
Campos de ajustes: secretos, usuarios y mapas por usuario#
El settingsSchema del manifest admite tres familias de campos sensibles a la organización: secretos write-only (secret: true, writeOnly: true o type: "secret"; cifrados, jamás se vuelven a leer, con pattern/format validados en servidor y rotación auditada), selectores de usuario (user, users) y `userMap` (un valor por usuario del dominio: boolean, string, number o select). En el handler llegan como ctx.env.X, ctx.settings.owner (id), ctx.settings.recipients (ids) y ctx.settings.digest (mapa), con ctx.settings.forUser(key, userId) para resolver con el default.
Referencia completa, validación y compatibilidad en Configuración por dominio; los tipos TypeScript están en la referencia tsdocs. Vista previa interactiva de los controles:
Vista previa de los campos de usuario
Datos de ejemplo en memoria (un usuario desactivado, uno eliminado). Los controles son los de la consola de administración.
Responsable user
Ningún usuario seleccionado.
Destinatarios users
- usr_002
1 de 5 usuarios
Recibe el resumen userMap · boolean
Quien no tenga entrada usa: no
| Usuario | Valor | Acciones |
|---|---|---|
| usr_001 | ||
| usr_003 | ||
| usr_099 |
Límite diario userMap · number
Quien no tenga entrada usa: 10
| Usuario | Valor | Acciones |
|---|---|---|
| usr_002 |
Lo que recibe el server.js
{
"recipients": [
"usr_002"
],
"digest": {
"usr_001": true,
"usr_003": true,
"usr_099": true
},
"quota": {
"usr_002": 25
}
}ctx.settings.forUser('digest', 'usr_005') -> false (sin entrada: usa default)
SDK de IA#
ctx.services.ai da acceso a la IA configurada por tu organización en /admin/ai, sin claves en la extensión. Requiere el permiso AI_GENERATE y el bloque ai del manifest. Los guardarrailes, las cuotas y la redacción de datos de /admin/ai se aplican igual que en el resto de la aplicación. No se guardan prompts ni respuestas.
| Método | Devuelve | Notas |
|---|---|---|
generate({ prompt, system?, feature?, maxTokens?, temperature?, responseFormat?, parts? }) | { text, json?, usage: { tokensIn, tokensOut }, model, warnings? } | responseFormat: text o json. La firma antigua generate(system, prompt, extra) sigue funcionando pero está obsoleta. |
chat({ messages, ... }) | igual que generate | Conversación con roles; mismas opciones. |
json(input, schema, { feature?, system?, maxTokens?, temperature?, retries?, parts? }) | { data, usage, model, warnings? } | Salida estructurada validada (ver abajo). retries: 0 a 2. |
status(feature?) | estado y cuota restante | No consume cuota de IA. |
isAvailable(feature?) | boolean | Nunca lanza; ideal para degradar. |
Funciones (feature): composer, smart-reply, summarize, translate, organizer y other. Cada una se puede desactivar por separado en /admin/ai. El uso (usage) informa de tokens de entrada y salida de esa llamada.
// permisos: AI_GENERATE · manifest.ai = { features: ['summarize'], required: false, purpose: {...} }
export async function summarize(ctx) {
const ai = ctx.services.ai;
if (!(await ai.isAvailable('summarize'))) return { text: null }; // degradar sin error
const r = await ai.generate({
prompt: 'Resume en 3 viñetas:\n' + ctx.args.body,
system: 'Responde en el idioma del texto.',
feature: 'summarize', maxTokens: 300, temperature: 0.2,
});
// r: { text, json?, usage: { tokensIn, tokensOut }, model, warnings? }
return { text: r.text, tokens: r.usage.tokensIn + r.usage.tokensOut, model: r.model };
}
// Conversación: chat({ messages, feature, ... }) devuelve el mismo resultado
const c = await ai.chat({ feature: 'composer', messages: [
{ role: 'user', content: 'Redacta un saludo breve' },
] });Salida estructurada#
ai.json(input, schema, opciones) devuelve datos ya validados contra un subconjunto de JSON Schema: object, array, string, number, integer, boolean, enum y nullable; required, items, minItems/maxItems, minimum/maximum, minLength/maxLength y additionalProperties: false. No se admiten $ref, $defs, allOf, anyOf, oneOf, not, pattern ni format.
- Límites del esquema: profundidad 6, 200 nodos, 16 KB y
enumde hasta 100 valores. Si se superan, la llamada falla coninvalid_args. - El servidor siempre valida, sea cual sea el proveedor. Usa el modo nativo cuando existe:
json_schemaen OpenAI, tool-use forzado en Anthropic yresponseSchemaen Gemini; en OpenRouter y compatibles se añade una instrucción y se valida igualmente. - Reintentos (
retries, 0 a 2): ante un incumplimiento se reintenta indicando a la IA la ruta del campo fallido. Cada reintento cuenta en la cuota y en el presupuesto por invocación. - Si sigue sin cumplirse: error
schema_validation_failed;err.detailes la ruta del primer incumplimiento (por ejemploprioridad) y nunca incluye contenido del modelo.
const schema = {
type: 'object', additionalProperties: false,
required: ['asunto', 'prioridad', 'fechas'],
properties: {
asunto: { type: 'string', maxLength: 120 },
prioridad: { enum: ['baja', 'media', 'alta'] },
fechas: { type: 'array', maxItems: 10, items: { type: 'string', minLength: 8, maxLength: 10 } },
},
};
const { data, usage } = await ctx.services.ai.json(
'Extrae asunto, prioridad y fechas (AAAA-MM-DD) de este correo:\n' + ctx.args.body,
schema,
{ feature: 'organizer', retries: 1, maxTokens: 400 },
);
// data: { asunto: string, prioridad: 'baja'|'media'|'alta', fechas: string[] }Errores de IA#
Todos los fallos son Error con .code. El SDK exporta AiErrorCodes con las constantes.
| Código | Cuándo ocurre | Cómo mostrarlo |
|---|---|---|
ai_disabled | La IA está apagada para la instancia u organización | Oculta la función o avisa; no reintentes |
feature_disabled | Esa feature está apagada en /admin/ai | Igual que el anterior, indicando la función |
quota_exceeded | Se agotó una cuota (día o mes, peticiones o tokens); .retryAfter en segundos | Muestra "inténtalo más tarde" y, si hay retryAfter, cuándo |
guardrail_blocked | Un guardarraíl bloqueó la entrada o la salida | Mensaje neutro; no repitas el contenido |
not_configured | No hay proveedor o clave configurados | Pide a un administrador configurar /admin/ai |
provider_error | Fallo del proveedor (red, 5xx, límite externo) | Permite reintentar |
schema_validation_failed | La salida no cumple el esquema tras los reintentos; .detail = ruta | Ofrece reintentar o rellenar a mano |
invalid_args | Argumentos o esquema no válidos (límites, palabras clave no admitidas) | Es un error de programación: corrígelo |
AI_PERMISSION_DENIED | Falta AI_GENERATE o la función no está declarada en ai.features | Corrige el manifest |
AI_CALL_BUDGET | Se superó el presupuesto de la invocación | Reduce llamadas o reintentos |
AI_SERVICE_ERROR | Fallo interno del puente de IA | Permite reintentar |
const MSG = {
ai_disabled: { es: 'La IA está desactivada en esta organización.', en: 'AI is turned off for this organization.' },
feature_disabled: { es: 'Esta función de IA está desactivada.', en: 'This AI feature is turned off.' },
quota_exceeded: { es: 'Has alcanzado la cuota de IA. Inténtalo más tarde.', en: 'AI quota reached. Try again later.' },
guardrail_blocked: { es: 'El contenido fue bloqueado por las reglas de seguridad.', en: 'The content was blocked by safety rules.' },
not_configured: { es: 'La IA aún no está configurada.', en: 'AI is not configured yet.' },
provider_error: { es: 'El proveedor de IA falló. Reintenta en un momento.', en: 'The AI provider failed. Please retry shortly.' },
schema_validation_failed: { es: 'La IA no devolvió el formato esperado.', en: 'The AI did not return the expected format.' },
invalid_args: { es: 'Petición de IA no válida.', en: 'Invalid AI request.' },
};
try {
const { text } = await ctx.services.ai.generate({ prompt, feature: 'summarize' });
return { text };
} catch (err) {
const code = err && err.code;
const m = MSG[code] || { es: 'No se pudo usar la IA.', en: 'AI is unavailable.' };
const wait = code === 'quota_exceeded' && err.retryAfter ? err.retryAfter : undefined; // segundos
return { error: m, retryAfter: wait }; // nunca muestres err.message crudo
}Cuotas y disponibilidad#
status(feature?) devuelve enabled, available, featureEnabled, provider, model (nombre, nunca la clave), remaining (requestsDay, requestsMonth, tokensDay, tokensMonth), limits, reason y state. isAvailable(feature?) es la versión corta y nunca lanza.
const s = await ctx.services.ai.status('summarize');
// { enabled, available, featureEnabled, provider, model, remaining: { requestsDay, requestsMonth, tokensDay, tokensMonth },
// limits, reason, state } (model es el nombre; nunca la clave)
if (!s.available) return { hint: s.reason };Declara el uso de IA en el manifest. Con required: false la extensión degrada: sigue activa y tú decides qué hacer según isAvailable. Con required: true, si se desactiva la IA o la función, la extensión queda pausada y el backend rechaza las llamadas con ai_disabled.
"permissions": ["AI_GENERATE"],
"ai": {
"features": ["organizer", "summarize"],
"required": false,
"maxTokens": 600,
"purpose": { "es": "Resume y clasifica tus correos", "en": "Summarizes and sorts your emails" }
}Presupuesto por invocación
Cada ejecución de server.js puede hacer hasta 5 generaciones (generate, chat y json, incluidos los reintentos) y 20 consultas de status/isAvailable; al excederlo se lanza AI_CALL_BUDGET.
Por qué no hay stream
El puente hacia el backend es una petición/respuesta firmada y el sandbox solo transfiere strings por RPC; un contexto vm no admite iteradores asíncronos del host. Por eso las respuestas llegan completas. Más sobre proveedores, cuotas y guardarrailes en IA.