El plugin complejo canónico. Recorrido de principio a fin.

Cada concepto cubierto en el resto de estas docs (el flujo de arranque y carga, los cuatro patrones de hook, los hooks REGISTRY a nivel de layout, las migraciones aisladas del plugin, el flujo indirecto de traducciones, los cuatro estados del ciclo de vida, el patrón de testsuite por plugin) se ejercita en storage/app/plugins/acelle/ai/. El plugin entrega ocho modelos Eloquent, catorce migraciones, dieciocho locales, más de sesenta plantillas Blade, tres componentes anónimos universales, todos los hooks de inyección de layout y más de cien tests Pest en su propia testsuite. Esta página es la receta de lectura: qué mirar primero, qué mirar al final, y cómo aprender de él sin perderse en la fontanería de grado producción.

Por qué este plugin es la referencia canónica

La mayoría de los plugins en producción son pequeños. Un driver de envío es una clase más una blade de conexión. Una pasarela de pago es un servicio más un controlador de redirección. Un complemento de barra lateral simple es una sola llamada a Hook::add. Ninguno de esos ejercita todo el SDK de plugins, y los plugins pequeños no son el ejercicio de lectura adecuado para un autor que intenta entender hasta dónde puede crecer un plugin con responsabilidad.

acelle/ai existe en el otro extremo del espectro. Es un subsistema de IA autónomo: un chatbox de agente, componentes universales de reescritura de texto, asistentes coach fundamentados en la KB y un panel de observabilidad de administración. Activar el plugin en una instalación de AcelleMail añade toda la superficie de IA sin tocar el código del core; desactivarlo lo retira limpiamente. El plugin usa cada concepto que cubren el resto de estas docs, en producción. Leerlo es la forma más rápida de ver cómo se combinan los patrones cuando la funcionalidad no es trivial.

El árbol de archivos de un vistazo

Del propio README del plugin:

CarpetaDe qué es dueña
src/AIHandler/Motor de IA en runtime: motores, bucle del agente, herramientas, resolver de settings, escritor/lector de observabilidad, lookup de la KB, sanitizador de URLs.
src/Models/Ocho modelos Eloquent para el sustrato de auditoría (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord).
src/Controllers/Controladores de administración (/rui/admin/ai-*) + controladores de API pública (/api/v1/ai/*) + el PluginDashboardController de la landing del plugin.
src/Services/PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, etc.
src/ServiceProvider.phpÚnico punto de entrada: registra PSR-4, rutas, vistas, archivos de idioma, hooks, alias de middleware y listeners del ciclo de vida.
database/migrations/Catorce migraciones del sustrato de auditoría. Se ejecutan al activar, se revierten al eliminar.
resources/views/Más de sesenta plantillas Blade de administración + tres componentes anónimos universales (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + partials JS de chatbox / Sparkle.
resources/assets/CSS (~14 archivos) + JS (~21 archivos) publicados en public/plugins/acelle/ai/ vía vendor:publish --tag=plugin --force al instalar el plugin.
resources/lang/Dieciocho locales × nueve archivos de idioma = toda la superficie del módulo de IA traducida.
tests/Más de cien tests Pest (Feature + Unit) + la clase base Acelle\Ai\Tests\PluginTestCase.
routes.phpRutas del plugin (administración + API pública + el dashboard de landing del plugin en /plugins/acelle/ai/dashboard).
composer.jsonMetadatos del plugin; extra.setting-route apunta a PluginDashboardController@index para que el botón «Settings» de la página de Plugins de administración enlace directamente al dashboard propio del plugin.

Ninguna de esas carpetas es a medida. Cada una se mapea directamente a una sección del resto de estas docs. Leer el plugin es un proceso de reconocer el mismo patrón entregado a escala.

Ocho modelos Eloquent — el sustrato de auditoría

La capa de datos del módulo de IA está moldeada en torno a la auditabilidad: cada conversación, cada invocación de modelo, cada llamada a herramienta, cada valoración de usuario y cada blob de salida bruta del proveedor se captura para replay y observabilidad. Ocho modelos cubren ese sustrato:

ModeloTablaQué representa
AIConversationai_conversationsUna fila por sesión de agente / soporte de varios turnos. Lleva las FKs de customer + user, la clave de la tarea, la ruta de la pantalla y los totales acumulados de tokens / coste.
AIMessageai_messagesUna fila por turno de usuario / agente. Rol, JSON de contenido, FK a una llamada de herramienta, latencia, modelo usado.
AIRequestai_requestsUna fila por llamada a la API upstream. Motor, hash del prompt, latencia, coste, estado de error. Puentea AIMessage con el tráfico HTTP real.
AIToolCallai_tool_callsInvocaciones de function-call generadas por un turno del agente. Nombre de la herramienta, JSON de entrada/salida, flag de origen.
AIFeedbackai_feedbackValoraciones de pulgar arriba/abajo + texto libre por mensaje y por conversación.
AIRawBlobai_raw_blobsRespuestas brutas originales del proveedor, conservadas para replay / auditoría. Tabla aparte porque la tabla de rollup tiene que mantenerse pequeña.
AIDailyRollupai_daily_rollupAgregado por día para el panel de observabilidad de administración: totales de tokens, coste, tasa de error. Pre-agregado para que el panel lea barato.
AIToolUndoRecordai_tool_undo_recordsRastrea acciones de herramienta reversibles para la funcionalidad de «deshacer último».

Tres patrones de esta lista se trasladan directamente a otros plugins. Separar «respuestas brutas del proveedor» en una tabla aparte de «resumen acumulado» permite que la tabla de rollup se mantenga lo bastante pequeña como para escanearla. Las FKs anulables a customers y users permiten que la misma fila funcione para tráfico autenticado y anónimo. Los rollups de una fila por día dan al panel de administración lecturas baratas sin un JOIN pesado contra las tablas de actividad.

Catorce migraciones de una línea cada una

Los nombres de archivo de las migraciones en storage/app/plugins/acelle/ai/database/migrations/ cuentan su propia historia: aditivos en el tiempo, inmediatamente reversibles, nunca un movimiento de esquema destructivo:

Nombre del archivoQué hace
2026_04_28_000001_create_ai_conversations_table.phpSesiones de chat de varios turnos — uid, FK de customer_id, enum de estado, rollups de tokens / coste
2026_04_28_000002_create_ai_messages_table.phpTurno único de usuario / agente — rol, JSON de contenido, FK a tool-call, latencia, modelo usado
2026_04_28_000003_create_ai_requests_table.phpUna fila por llamada a la API upstream — motor, hash del prompt, latencia, coste, error
2026_04_28_000004_create_ai_tool_calls_table.phpInvocaciones de function-call generadas por un turno del agente — JSON de entrada / salida
2026_04_28_000005_create_ai_feedback_table.phpValoraciones de pulgar arriba/abajo + texto libre por mensaje y por conversación
2026_04_28_000006_create_ai_raw_blobs_table.phpRespuestas brutas originales del proveedor, conservadas para replay / auditoría
2026_04_28_000007_create_ai_daily_rollup_table.phpAgregado por día para el panel de administración — totales de tokens, coste, tasa de error
2026_04_29_000001_add_client_message_id_to_ai_messages.phpColumna de dedupe entre pestañas — aditiva, sin valores por defecto
2026_04_30_000002_add_source_to_ai_tool_calls.phpRastrea si una llamada a herramienta vino de la ruta de agente o de la ruta de soporte
2026_05_02_180000_widen_ai_conversations_client_session_uid.phpArreglo del ancho de ULID / UUID: migración que altera columnas, totalmente reversible
2026_05_02_200000_create_ai_tool_undo_records_table.phpRastrea acciones de herramienta reversibles para la funcionalidad de «deshacer último»
2026_05_03_000001_add_url_sanitization_to_ai_requests.phpAñade una columna JSON para la telemetría de URLs saneadas
2026_05_04_000001_create_ai_settings_table.phpSettings de administración a nivel de plugin — separados de plugins.data para que cada fila pueda indexarse

Lea las migraciones de arriba abajo y tendrá la evolución del esquema de todo el módulo de IA. Cada migración aditiva es un envío de feature: la forma aditiva es lo que hace que el rollback de delete_plugin_* del plugin siempre funcione, incluso cuando un admin lo desinstala después de un año de acumulación de features.

Todos los hooks que usa el plugin

El ServiceProvider del plugin ejercita todos los patrones de hook excepto FILTER. Un grep de Hook::* contra storage/app/plugins/acelle/ai/src/ServiceProvider.php:

REGISTRY (Hook::add) — seis contribuciones

  • Nueve entradas de add_translation_file en register() líneas 175-197: una por cada superficie de traducción (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Cada superficie es un archivo editable por separado en la UI de Idiomas de administración.
  • layout.head.assets en boot() línea 688: aporta el CSS + JS del chatbox a cada página que extiende los layouts maestros app / admin.
  • layout.body.before_close en boot() línea 699: aporta el HTML de la burbuja del chatbox y el popover de Sparkle antes del </body> de cada página.
  • admin.sidebar.groups en boot() línea 718: añade el grupo de IA con sus tres o cuatro enlaces hijos a la barra lateral de administración.

Las seis se protegen con aiPluginAvailable(): un helper que en última instancia resuelve a Plugin::getByName('acelle/ai')->isActive(). Devolver null cuando el plugin está bloqueado es la forma convencional de desaparecer limpiamente sin descargar el service provider.

EVENT (Hook::on) — dos listeners del ciclo de vida

  • activate_plugin_acelle/ai en la línea 472: ejecuta artisan migrate contra la carpeta de migraciones del plugin.
  • delete_plugin_acelle/ai en la línea 546: acepta el flag $keepData, revierte las migraciones cuando no está puesto y preserva las tablas de auditoría cuando sí lo está.

BEHAVIOR (Hook::set) — una sobrescritura de URL de icono

  • icon_url_acelle/ai en la línea 522: sobrescribe el hook BEHAVIOR por plugin para que la página de Plugins de administración renderice el icono propio del módulo de IA en lugar del plugin.svg por defecto del host.

Juntos son la mayor parte de la superficie de hook del plugin. Leer ServiceProvider.php de arriba abajo es la forma más rápida de ver cómo se combinan los patrones en producción.

La superficie de UI del chatbox

El plugin aporta tres componentes Blade universales en resources/views/components/: ninguno de ellos requiere que la aplicación host sepa que existen:

  • <x-mc-ai-chatbox>: la burbuja flotante del chatbox que se abre en una conversación de agente de varios turnos. Montada vía el hook REGISTRY layout.body.before_close para que aparezca en cada página app + admin.
  • <x-mc-ai-rewrite>: capacidad universal de «reescribe este texto» que se puede colocar junto a cualquier textarea del host. Con namespace del plugin, sin registro central.
  • <x-mc-ai-subject-ab-generator>: genera variantes A/B de línea de asunto a partir de un prompt. Se usa en el editor de campañas.

Estos tres componentes muestran el patrón para «el plugin aporta UI a varias páginas del host sin bifurcar cada página»: entregue el componente como un componente Blade anónimo bajo el namespace de vistas del plugin, regístrelo a través de los hooks REGISTRY de layout para un montaje global, o haga que las páginas del host hagan opt-in incluyéndolo directamente. Ambos patrones funcionan; el plugin de IA usa los dos.

Nueve archivos × dieciocho locales

El register() del plugin registra nueve archivos de traducción separados. La maquinaria de Language::dump() luego materializa cada uno en diecisiete carpetas de runtime de locales no ingleses bajo storage/app/data/plugins/acelle/ai/lang/. El resultado en disco: 153 archivos de traducción en runtime (9 archivos × 17 locales no ingleses + 9 originales en inglés = 162, menos los 9 maestros en inglés = 153 clones volcados), cada uno editable por separado a través de la UI de Idiomas de administración del host.

Los nueve archivos de superficie (registrados vía el bucle $aiLangFiles en ServiceProvider::register()):

  • ai_rewrite — el componente universal de reescritura de texto
  • ai_chatbox — la UI del chatbox
  • ai_chatbox_prompts — prompts predefinidos mostrados en el chatbox
  • ai_chatbox_wait — la UI de smart-wait («buscando… ejecutando herramienta… redactando respuesta»)
  • ai_subject_ab — el generador A/B de asuntos
  • ai_settings — etiquetas de la página de settings de administración
  • admin_ai_usage — panel de uso / coste de administración
  • admin_ai_audit — UI de auditoría / replay de administración
  • admin_ai_permissions — toggles de permisos por funcionalidad en administración

Esta división es la respuesta práctica a «cómo mantengo los archivos de traducción lo bastante pequeños como para que un traductor pueda editar uno en una sola sesión»: un maestro por superficie lógica, registrado por separado, materializado por locale, editado de forma independiente a través de la UI de administración.

Archivos de configuración propios del plugin

El plugin es dueño de dos archivos de configuración bajo config/: registrados en ServiceProvider::boot() vía $this->mergeConfigFrom() y alcanzables a través de los helpers estándar config('ai.*') y config('ai-navigation-hints.*'). La configuración propia del plugin es el sitio correcto para metadatos estáticos que no cambian por instalación (catálogo de motores, plantillas de prompt, valores por defecto de navegación); los ajustes editables por el admin viven en la tabla ai_settings, sembrada por migración.

La división (config para los valores por defecto inmutables que entrega el plugin, filas de BD para los ajustes mutables editables por el admin) es un patrón que se traslada limpiamente a otros plugins. Meter ambos en la columna JSON plugins.data es tentador, pero castiga el rendimiento de la UI de administración: una tabla dedicada e indexada fue la decisión correcta.

La infraestructura de tests

El directorio de tests del plugin sigue la forma de tests/ del host: carpetas Unit + Feature más una clase base PluginTestCase en la raíz:

storage/app/plugins/acelle/ai/tests/
├── PluginTestCase.php          ← seeds the plugin row as active before every test
├── Feature/
│   ├── AIHandler/              ← engines, agent loop, tools, observability writer
│   └── PluginLifecycle/        ← lifecycle integration tests
├── Unit/                       ← isolated unit tests (no Laravel boot for some)
├── Fixtures/                   ← test fixtures + factories
├── Snapshots/                  ← Pest snapshot artefacts
└── Support/                    ← test-only helpers

El phpunit.xml del host registra el testsuite del plugin como <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" ejecuta la suite completa en paralelo con las suites Unit + Feature del propio host.

El PluginTestCase en la raíz demuestra la trampa del reseteo de caché de gate por petición que todo plugin que entrega middleware debería adoptar: vea Testing § trampa de caché de gate para el patrón completo. Sin él, el segundo test de la suite en adelante observa estado de caché obsoleto del arranque del primero.

Cómo aprender de él

Leer cada línea de un plugin de cien mil tokens no es el ejercicio correcto. Una receta de cuatro pasos es más útil:

  1. Clone o symlinkee el plugin en una instalación host. Actívelo. Abra la barra lateral de administración: debería aparecer el grupo de IA. Haga clic en «Settings» en la entrada de la página de Plugins de administración: debería cargarse el dashboard de landing del plugin. Esto prueba que la superficie de UI del plugin está conectada con su host local.
  2. Lea src/ServiceProvider.php de arriba abajo. Cuarenta minutos. Cada concepto cubierto en Arquitectura de plugins + El sistema de Hooks + Inyección de UI + Traducciones aparece en este único archivo a escala de producción. Vaya cruzando referencias con las páginas de análisis a fondo a medida que avanza.
  3. Rastree una funcionalidad de principio a fin. Elija la burbuja del chatbox. Encuentre el punto de entrada (el hook layout.body.before_close en ServiceProvider), siga el partial que devuelve (ai::partials.body_assets), encuentre el componente anónimo que renderiza la burbuja, encuentre el JS que la monta, encuentre la ruta API que llama el JS, encuentre el controlador, sígalo hasta el motor en runtime AIHandler y observe cómo se insertan las filas de AIConversation + AIMessage + AIRequest. Dos o tres horas, de principio a fin. Después de esto sabrá qué patrones usa el plugin de IA y de cuáles prescinde.
  4. Adapte un patrón a su propio plugin. Elija el más pequeño que encaje: normalmente el bucle de registro por superficie de traducción, o el grupo de barra lateral de administración, o el enfoque de tabla de rollup por día. Quite el código específico de IA; conserve el patrón estructural. Esa es la ganancia de productividad del autor del plugin en el primer día.

A dónde ir después

Este es el último de los once análisis a fondo para desarrolladores. Desde aquí, el hub del índice es el recap natural: Índice de documentación muestra todas las páginas del cluster organizadas en Fundamentos / Construcción / Calidad / Referencia. La landing para desarrolladores es el punto de entrada con forma de marketing para los visitantes nuevos que llegan desde buscadores.

Dos próximos pasos prácticos cuando esté listo para entregar un plugin real: el ciclo activar → testear → eliminar del análisis a fondo de Testing prueba que el listener del hook delete_plugin_* del plugin limpia correctamente. Arquitectura de plugins § Recuperación de un estado roto es la página que conviene marcar para incidencias de runtime en producción: tres modos de fallo más la vía exacta de arreglo para cada uno.

Más allá de los patrones específicos de AcelleMail, aplica el ecosistema Laravel más amplio. El código del plugin es Laravel puro; el loader en tiempo de ejecución del host es la única pieza no estándar. Cualquier cosa que sepa sobre Eloquent, Blade, Pest, colas, scheduling o middleware funciona dentro de la carpeta del plugin sin cambios.