Pourquoi ce plugin est la référence canonique
La plupart des plugins en production sont petits. Un driver d'envoi est une classe plus un blade de connexion. Une passerelle de paiement est un service plus un contrôleur de redirection. Un simple module sidebar est un seul appel Hook::add. Aucun de ceux-là ne sollicite tout le plugin SDK — et les petits plugins ne sont pas le bon exercice de lecture pour un auteur qui essaie de comprendre jusqu'où un plugin peut raisonnablement grandir.
acelle/ai existe à l'autre extrémité du spectre. C'est un sous-système d'IA auto-contenu : un chatbox agent, des composants universels de réécriture de texte, des personas coach ancrés sur une KB et un dashboard d'observabilité admin. Activer le plugin dans une installation AcelleMail ajoute toute la surface d'IA sans toucher au code du cœur ; le désactiver retire tout proprement. Le plugin utilise chaque concept couvert dans le reste de cette documentation, en production. Le lire est le moyen le plus rapide de voir comment les patterns se combinent quand la fonctionnalité n'est pas triviale.
L'arborescence en un coup d'œil
Tiré du README propre au plugin :
| Dossier | Ce qu'il possède |
src/AIHandler/ | Moteur d'exécution IA — engines, boucle d'agent, tools, résolveur de settings, writer/reader d'observabilité, lookup KB, désinfectant d'URL. |
src/Models/ | Huit modèles Eloquent pour le substrat d'audit (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord). |
src/Controllers/ | Contrôleurs admin (/rui/admin/ai-*) + contrôleurs d'API publique (/api/v1/ai/*) + le PluginDashboardController de landing du plugin. |
src/Services/ | PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, etc. |
src/ServiceProvider.php | Unique point d'entrée — enregistre PSR-4, routes, vues, fichiers de langue, hooks, alias de middleware, listeners de cycle de vie. |
database/migrations/ | Quatorze migrations de substrat d'audit. Exécutées à l'activation, rollback à la suppression. |
resources/views/ | Plus de soixante templates Blade admin + trois composants anonymes universels (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + partials JS chatbox / sparkle. |
resources/assets/ | CSS (~14 fichiers) + JS (~21 fichiers) publiés dans public/plugins/acelle/ai/ via vendor:publish --tag=plugin --force à l'installation du plugin. |
resources/lang/ | Dix-huit locales × neuf fichiers de langue = toute la surface du module IA traduite. |
tests/ | Plus de cent tests Pest (Feature + Unit) + la classe de base Acelle\Ai\Tests\PluginTestCase. |
routes.php | Routes du plugin (admin + API publique + le dashboard de landing du plugin à /plugins/acelle/ai/dashboard). |
composer.json | Métadonnées du plugin ; extra.setting-route pointe vers PluginDashboardController@index pour que le bouton "Settings" de la page admin Plugins deep-linke directement dans le dashboard propre au plugin. |
Aucun de ces dossiers n'est sur mesure. Chacun correspond directement à une section dans le reste de cette documentation. Lire le plugin est un processus de reconnaissance du même pattern livré à grande échelle.
Huit modèles Eloquent — le substrat d'audit
La couche données du module IA est façonnée autour de l'auditabilité : chaque conversation, chaque invocation de modèle, chaque appel d'outil, chaque retour utilisateur, et chaque blob de sortie brute du prestataire est capturé pour le replay et l'observabilité. Huit modèles couvrent ce substrat :
| Modèle | Table | Ce qu'il représente |
AIConversation | ai_conversations | Une ligne par session multi-tour agent / support. Porte les FK customer + user, la clé de tâche, la route d'écran, et les totaux roll-up de tokens / coût. |
AIMessage | ai_messages | Une ligne par tour user / agent. Rôle, contenu JSON, FK vers un tool-call, latence, modèle utilisé. |
AIRequest | ai_requests | Une ligne par appel d'API en amont. Engine, hash de prompt, latence, coût, état d'erreur. Pont entre AIMessage et le trafic HTTP réel. |
AIToolCall | ai_tool_calls | Invocations function-call générées par un tour d'agent. Nom de l'outil, JSON input/output, flag de source. |
AIFeedback | ai_feedback | Pouces haut/bas + retour texte libre par message et par conversation. |
AIRawBlob | ai_raw_blobs | Réponses brutes originales du prestataire, conservées pour replay / audit. Table séparée car la table de roll-up doit rester petite. |
AIDailyRollup | ai_daily_rollup | Agrégat par jour pour le dashboard d'observabilité admin — totaux de tokens, coût, taux d'erreur. Pré-agrégé pour que le dashboard lise à faible coût. |
AIToolUndoRecord | ai_tool_undo_records | Trace les actions d'outil réversibles pour la fonctionnalité "annuler la dernière". |
Trois patterns de cette liste se traduisent directement dans d'autres plugins. Séparer "réponses brutes du prestataire" dans une table distincte du "résumé roll-up" permet à la table de roll-up de rester suffisamment petite pour être scannée. Des FK nullables vers customers et users permettent à la même ligne de fonctionner pour le trafic authentifié et anonyme. Des rollups un-par-jour donnent au dashboard admin des lectures à faible coût sans un JOIN lourd contre les tables d'activité.
Quatorze migrations en une ligne chacune
Les noms de fichiers de migration dans storage/app/plugins/acelle/ai/database/migrations/ racontent leur propre histoire — additifs dans le temps, immédiatement réversibles, jamais un mouvement de schéma destructif :
| Nom de fichier | Ce qu'il fait |
2026_04_28_000001_create_ai_conversations_table.php | Sessions de chat multi-tour — uid, FK customer_id, enum de statut, rollups tokens / coût |
2026_04_28_000002_create_ai_messages_table.php | Tour user / agent unique — rôle, contenu JSON, FK tool-call, latence, modèle utilisé |
2026_04_28_000003_create_ai_requests_table.php | Une ligne par appel d'API en amont — engine, hash de prompt, latence, coût, erreur |
2026_04_28_000004_create_ai_tool_calls_table.php | Invocations function-call générées par un tour d'agent — JSON input / output |
2026_04_28_000005_create_ai_feedback_table.php | Pouces haut/bas + retour texte libre par message et par conversation |
2026_04_28_000006_create_ai_raw_blobs_table.php | Réponses brutes originales du prestataire, conservées pour replay / audit |
2026_04_28_000007_create_ai_daily_rollup_table.php | Agrégat par jour pour le dashboard admin — totaux de tokens, coût, taux d'erreur |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | Colonne de dédup cross-tab — additive, sans valeurs par défaut |
2026_04_30_000002_add_source_to_ai_tool_calls.php | Trace si un appel d'outil vient de la route agent vs support |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | Correctif de largeur ULID / UUID — migration qui modifie une colonne, entièrement réversible |
2026_05_02_200000_create_ai_tool_undo_records_table.php | Trace les actions d'outil réversibles pour la fonctionnalité "annuler la dernière" |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | Ajoute une colonne JSON pour la télémétrie d'URL désinfectée |
2026_05_04_000001_create_ai_settings_table.php | Paramètres admin au niveau du plugin — gardés séparément de plugins.data pour que chaque ligne puisse être indexée |
Lisez les migrations de haut en bas et vous avez l'évolution de schéma de tout le module IA. Chaque migration additive est un ship de fonctionnalité — la forme additive est ce qui fait que le rollback delete_plugin_* du plugin fonctionne toujours, même quand un admin désinstalle après un an d'accrétion de fonctionnalités.
Chaque hook utilisé par le plugin
Le ServiceProvider du plugin sollicite chaque pattern de hook sauf FILTER. Un grep de Hook::* contre storage/app/plugins/acelle/ai/src/ServiceProvider.php :
REGISTRY (Hook::add) — six contributions
- Neuf entrées
add_translation_file dans register() lignes 175-197 — une par surface de traduction (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Chaque surface est un fichier éditable séparément dans l'UI admin Languages.
layout.head.assets dans boot() ligne 688 — contribue le CSS + JS du chatbox à chaque page qui étend les layouts master app / admin.
layout.body.before_close dans boot() ligne 699 — contribue le HTML de la bulle chatbox et le popover sparkle avant le </body> de chaque page.
admin.sidebar.groups dans boot() ligne 718 — ajoute le groupe AI avec ses trois ou quatre liens enfants à la sidebar admin.
Les six se gardent eux-mêmes avec aiPluginAvailable() — un helper qui se résout finalement à Plugin::getByName('acelle/ai')->isActive(). Renvoyer null quand le plugin est désactivé est la manière conventionnelle de disparaître proprement sans décharger le service provider.
EVENT (Hook::on) — deux listeners de cycle de vie
activate_plugin_acelle/ai ligne 472 — exécute artisan migrate contre le dossier de migrations du plugin.
delete_plugin_acelle/ai ligne 546 — accepte le flag $keepData, fait un rollback des migrations quand il n'est pas défini, préserve les tables d'audit quand il l'est.
BEHAVIOR (Hook::set) — un override d'URL d'icône
icon_url_acelle/ai ligne 522 — override le hook BEHAVIOR par-plugin pour que la page admin Plugins rende l'icône propre au module IA au lieu de l'plugin.svg par défaut de l'hôte.
Ensemble, ceux-ci constituent l'essentiel de la surface de hooks du plugin. Lire ServiceProvider.php de haut en bas est le moyen le plus rapide de voir comment les patterns se combinent en production.
La surface UI du chatbox
Le plugin contribue trois composants Blade universels dans resources/views/components/ — aucun d'entre eux n'exige que l'application hôte sache qu'ils existent :
<x-mc-ai-chatbox> — la bulle chatbox flottante qui s'ouvre en une conversation agent multi-tour. Montée via le hook REGISTRY layout.body.before_close pour apparaître sur chaque page app + admin.
<x-mc-ai-rewrite> — affordance universelle "réécrire ce texte" qui peut être placée à côté de n'importe quel textarea de l'hôte. Namespacée par le plugin, aucun enregistrement central.
<x-mc-ai-subject-ab-generator> — génère des variantes de ligne de sujet A/B à partir d'un prompt. Utilisée dans l'éditeur de campagne.
Ces trois composants montrent le pattern pour "un plugin contribue à l'UI de plusieurs pages hôte sans forker chaque page" : livrez le composant comme un composant Blade anonyme sous le namespace de vue de votre plugin, enregistrez-le via les hooks REGISTRY de layout pour un montage global, ou laissez les pages hôte opter pour son inclusion directe. Les deux patterns fonctionnent ; le plugin IA utilise les deux.
Neuf fichiers × dix-huit locales
Le register() du plugin enregistre neuf fichiers de traduction séparés. La machinerie Language::dump() matérialise ensuite chacun en dix-sept dossiers runtime non-anglais sous storage/app/data/plugins/acelle/ai/lang/. Le résultat sur disque : 153 fichiers de traduction runtime (9 fichiers × 17 locales non-anglaises + 9 originaux anglais = 162 moins les 9 masters anglais = 153 dump-clones), chacun éditable séparément via l'UI admin Languages de l'hôte.
Les neuf fichiers de surface (enregistrés via la boucle $aiLangFiles dans ServiceProvider::register()) :
ai_rewrite — le composant universel de réécriture de texte
ai_chatbox — l'UI du chatbox
ai_chatbox_prompts — prompts prédéfinis affichés dans le chatbox
ai_chatbox_wait — l'UI smart-wait ("recherche en cours… exécution d'un outil… composition de la réponse")
ai_subject_ab — le générateur de sujet A/B
ai_settings — libellés de la page admin settings
admin_ai_usage — dashboard admin d'usage / coût
admin_ai_audit — UI admin d'audit / replay
admin_ai_permissions — toggles admin de permission par fonctionnalité
Ce découpage est la réponse pratique à "comment garder les fichiers de traduction suffisamment petits pour qu'un traducteur puisse en éditer un en une seule séance" : un master par surface logique, enregistré séparément, matérialisé par locale, édité indépendamment via l'UI admin.
Fichiers de config possédés par le plugin
Le plugin possède deux fichiers de config sous config/ — enregistrés dans ServiceProvider::boot() via $this->mergeConfigFrom() et accessibles via les helpers standards config('ai.*') et config('ai-navigation-hints.*'). Une config possédée par le plugin est le bon endroit pour les métadonnées statiques qui ne changent pas par installation (catalogue d'engines, templates de prompts, défauts de navigation) ; les paramètres éditables par admin vivent dans la table ai_settings seedée par migration.
Le découpage — config pour les défauts immuables livrés par le plugin, lignes DB pour les paramètres mutables éditables par admin — est un pattern qui se transpose proprement à d'autres plugins. Mettre les deux dans la colonne JSON plugins.data est tentant mais punit les performances de l'UI admin ; une table indexée dédiée a été le bon choix.
L'infrastructure de tests
Le répertoire de tests du plugin suit la forme du tests/ de l'hôte — dossiers Unit + Feature plus une classe de base PluginTestCase à la racine :
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
Le phpunit.xml de l'hôte enregistre la testsuite du plugin comme <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" exécute la suite complète en parallèle avec les suites Unit + Feature propres à l'hôte.
Le PluginTestCase à la racine montre le piège du reset de cache de gate par requête que chaque plugin livrant un middleware devrait adopter — voir la Tests § piège du cache de gate pour le pattern complet. Sans lui, le deuxième test de la suite et les suivants observent un état de cache obsolète provenant du boot du premier test.
Comment apprendre à partir de cela
Lire chaque ligne d'un plugin de cent mille tokens n'est pas le bon exercice. Une recette en quatre étapes est plus utile :
-
Clonez ou symlinkez le plugin dans une installation hôte. Activez-le. Ouvrez la sidebar admin — le groupe AI doit apparaître. Cliquez sur "Settings" sur l'entrée de la page admin Plugins — le dashboard de landing du plugin doit se charger. Cela prouve que la surface UI du plugin est câblée dans votre hôte local.
-
Lisez
src/ServiceProvider.php de haut en bas. Quarante minutes. Chaque concept couvert dans Architecture des plugins + Système de hooks + Injection d'UI + Traductions apparaît dans ce seul fichier à l'échelle de la production. Faites des références croisées avec les pages deep-dive au fur et à mesure.
-
Tracez une fonctionnalité de bout en bout. Choisissez la bulle chatbox. Trouvez le point d'entrée (hook
layout.body.before_close dans ServiceProvider), suivez le partial qu'il renvoie (ai::partials.body_assets), trouvez le composant anonyme qui rend la bulle, trouvez le JS qui le monte, trouvez la route API que le JS appelle, trouvez le contrôleur, suivez-le jusqu'au moteur d'exécution AIHandler, regardez les lignes AIConversation + AIMessage + AIRequest être insérées. Deux à trois heures, de bout en bout. Après cela, vous saurez quels patterns le plugin IA utilise et lesquels il n'utilise pas.
-
Adaptez un pattern à votre propre plugin. Choisissez le plus petit qui s'applique — habituellement la boucle d'enregistrement par surface de traduction, ou le groupe sidebar admin, ou l'approche table de rollup par jour. Retirez le code spécifique à l'IA ; gardez le pattern structurel. C'est le gain de productivité du premier jour pour l'auteur de plugin.
Où aller ensuite
C'est le dernier des onze deep-dives développeurs. À partir d'ici, le hub d'index est la récap naturelle : Index de la documentation montre chaque page du cluster organisée en Fondation / Construction / Qualité / Référence. La landing développeur est le point d'entrée à forme marketing pour les nouveaux visiteurs arrivant depuis la recherche.
Deux étapes pratiques suivantes quand vous êtes prêt à livrer un vrai plugin : le cycle activer → tester → supprimer du deep-dive Tests prouve que le listener de hook delete_plugin_* du plugin nettoie correctement. Architecture des plugins § Récupération depuis un état cassé est la page à mettre en favori pour les soucis d'exécution en production — trois modes d'échec plus le chemin de correctif exact pour chacun.
Au-delà des patterns spécifiques à AcelleMail, l'écosystème Laravel plus large s'applique. Le code du plugin est du Laravel vanille ; le loader d'exécution de l'hôte est la seule pièce non standard. Tout ce que vous savez sur Eloquent, Blade, Pest, les queues, le scheduling ou le middleware fonctionne dans le dossier du plugin sans changement.