Perché questo plugin è il riferimento canonico
La maggior parte dei plugin in produzione è piccola. Un sending driver è una classe più un blade di connessione. Un gateway di pagamento è un servizio più un controller di redirect. Un semplice add-on sidebar è una chiamata Hook::add. Nessuno di questi esercita l'intero plugin SDK — e i plugin piccoli non sono il giusto esercizio di lettura per un autore che cerca di capire quanto grande un plugin possa responsabilmente crescere.
acelle/ai esiste all'estremo opposto dello spettro. È un sottosistema AI autocontenuto: una chatbox agent, componenti universali text-rewrite, persona di coach grounded su KB, e una dashboard admin di osservabilità. Attivare il plugin in un'installazione AcelleMail aggiunge l'intera superficie AI senza toccare codice core; disattivare la rimuove pulitamente. Il plugin usa ogni concetto coperto nel resto di questi docs, in produzione. Leggerlo è il modo più veloce per vedere come i pattern si combinano quando la feature non è banale.
L'albero file a colpo d'occhio
Dal README del plugin stesso:
| Cartella | Cosa contiene |
src/AIHandler/ | Engine runtime AI — engine, agent loop, tool, settings resolver, writer/reader di osservabilità, lookup KB, URL sanitiser. |
src/Models/ | Otto model Eloquent per il substrato di audit (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord). |
src/Controllers/ | Controller admin (/rui/admin/ai-*) + controller API pubblici (/api/v1/ai/*) + il PluginDashboardController di plugin-landing. |
src/Services/ | PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, ecc. |
src/ServiceProvider.php | Unico entry point — registra PSR-4, route, view, file lang, hook, middleware alias, listener di lifecycle. |
database/migrations/ | Quattordici migration del substrato di audit. Girano su activate, fanno rollback su delete. |
resources/views/ | Sessanta-e-più template Blade admin + tre componenti anonimi universali (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + partial JS chatbox / sparkle. |
resources/assets/ | CSS (~14 file) + JS (~21 file) pubblicati in public/plugins/acelle/ai/ via vendor:publish --tag=plugin --force all'install del plugin. |
resources/lang/ | Diciotto locale × nove file lingua = l'intera superficie del modulo AI tradotta. |
tests/ | Cento-e-più test Pest (Feature + Unit) + la base class Acelle\Ai\Tests\PluginTestCase. |
routes.php | Route del plugin (admin + API pubblica + la dashboard di plugin-landing in /plugins/acelle/ai/dashboard). |
composer.json | Metadati del plugin; extra.setting-route punta a PluginDashboardController@index così che il bottone "Settings" della pagina admin Plugins fa deep-link nella dashboard stessa del plugin. |
Nessuna di quelle cartelle è bespoke. Ognuna mappa direttamente a una sezione nel resto di questi docs. Leggere il plugin è un processo di riconoscere lo stesso pattern shippato a scala.
Otto model Eloquent — il substrato di audit
Il data layer del modulo AI è plasmato attorno alla auditability: ogni conversazione, ogni invocazione del modello, ogni tool call, ogni feedback utente, e ogni blob di output raw del provider viene catturato per replay e osservabilità. Otto model coprono quel substrato:
| Model | Tabella | Cosa rappresenta |
AIConversation | ai_conversations | Una riga per sessione multi-turn agent / support. Porta FK customer + user, task key, route screen, e totali roll-up di token / costo. |
AIMessage | ai_messages | Una riga per turno user / agent. Ruolo, content JSON, FK a una tool-call, latenza, modello usato. |
AIRequest | ai_requests | Una riga per chiamata API upstream. Engine, prompt hash, latenza, costo, status errore. Fa da ponte tra AIMessage e il traffico HTTP reale. |
AIToolCall | ai_tool_calls | Invocazioni di function-call generate da un turno agent. Nome del tool, input/output JSON, source flag. |
AIFeedback | ai_feedback | Pollice su/giù + feedback free-text per messaggio + per conversazione. |
AIRawBlob | ai_raw_blobs | Risposte raw originali del provider, tenute per replay / audit. Tabella separata perché la tabella di rollup deve restare piccola. |
AIDailyRollup | ai_daily_rollup | Aggregato per-giorno per la dashboard admin di osservabilità — totali token, costo, error rate. Pre-aggregato così la dashboard legge a basso costo. |
AIToolUndoRecord | ai_tool_undo_records | Traccia le azioni tool reversibili per la feature "undo last". |
Tre pattern da questa lista si traducono direttamente in altri plugin. Splittare "risposte raw del provider" in una tabella separata da "summary rolled-up" lascia la tabella di rollup abbastanza piccola da scansionare. FK nullable a customers e users lasciano che la stessa riga funzioni per traffico autenticato e anonimo. Rollup one-row-per-day danno alla dashboard admin letture economiche senza un JOIN pesante contro le tabelle di attività.
Quattordici migration una riga ciascuna
I nomi file delle migration in storage/app/plugins/acelle/ai/database/migrations/ raccontano la propria storia — additivi nel tempo, immediatamente reversibili, mai una mossa di schema distruttiva:
| Nome file | Cosa fa |
2026_04_28_000001_create_ai_conversations_table.php | Sessioni chat multi-turn — uid, FK customer_id, enum di status, roll-up token / costo |
2026_04_28_000002_create_ai_messages_table.php | Singolo turno user / agent — ruolo, content JSON, FK tool-call, latenza, modello usato |
2026_04_28_000003_create_ai_requests_table.php | Una riga per chiamata API upstream — engine, prompt hash, latenza, costo, errore |
2026_04_28_000004_create_ai_tool_calls_table.php | Invocazioni di function-call generate da un turno agent — input / output JSON |
2026_04_28_000005_create_ai_feedback_table.php | Pollice su/giù + feedback free-text per messaggio + per conversazione |
2026_04_28_000006_create_ai_raw_blobs_table.php | Risposte raw originali del provider, tenute per replay / audit |
2026_04_28_000007_create_ai_daily_rollup_table.php | Aggregato per-giorno per la dashboard admin — totali token, costo, error rate |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | Colonna di dedup cross-tab — additiva, nessun default |
2026_04_30_000002_add_source_to_ai_tool_calls.php | Traccia se una tool call è arrivata da agent vs route di support |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | Fix di larghezza ULID / UUID — migration column-altering, completamente reversibile |
2026_05_02_200000_create_ai_tool_undo_records_table.php | Traccia le azioni tool reversibili per la feature "undo last" |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | Aggiunge una colonna JSON per la telemetria URL sanitizzata |
2026_05_04_000001_create_ai_settings_table.php | Settings admin a livello di plugin — tenute separate da plugins.data così ogni riga può essere indicizzata |
Leggi le migration dall'alto al basso e hai l'evoluzione dello schema dell'intero modulo AI. Ogni migration additiva è uno ship di feature — la shape additiva è ciò che fa funzionare sempre il rollback di delete_plugin_* del plugin, anche quando un admin disinstalla dopo un anno di accrezione di feature.
Ogni hook che il plugin usa
Il ServiceProvider del plugin esercita ogni pattern hook tranne FILTER. Un grep per Hook::* contro storage/app/plugins/acelle/ai/src/ServiceProvider.php:
REGISTRY (Hook::add) — sei contribuzioni
- Nove entry
add_translation_file in register() righe 175-197 — una per superficie di traduzione (rewrite, chatbox, prompt, wait, subject AB, settings, admin usage, audit, permission). Ogni superficie è un file editabile separatamente nella UI admin Languages.
layout.head.assets in boot() riga 688 — contribuisce CSS + JS chatbox a ogni pagina che estende i layout master app / admin.
layout.body.before_close in boot() riga 699 — contribuisce l'HTML della bolla chatbox e il popover sparkle prima del </body> di ogni pagina.
admin.sidebar.groups in boot() riga 718 — aggiunge il gruppo AI con i suoi tre o quattro link figli alla admin sidebar.
Tutti e sei si gate-ano con aiPluginAvailable() — un helper che alla fine si risolve in Plugin::getByName('acelle/ai')->isActive(). Restituire null quando il plugin è gated off è il modo convenzionale per scomparire pulitamente senza fare unload del service provider.
EVENT (Hook::on) — due listener di lifecycle
activate_plugin_acelle/ai in riga 472 — esegue artisan migrate contro la migrations folder del plugin.
delete_plugin_acelle/ai in riga 546 — accetta il flag $keepData, fa rollback delle migration quando non settato, preserva le tabelle di audit quando settato.
BEHAVIOR (Hook::set) — un override URL icona
icon_url_acelle/ai in riga 522 — fa override dell'hook BEHAVIOR per-plugin così che la pagina admin Plugins renderizzi l'icona del modulo AI invece del default plugin.svg dell'host.
Insieme questi sono la maggior parte della superficie hook del plugin. Leggere ServiceProvider.php dall'alto al basso è il modo più veloce di vedere come i pattern si combinano in produzione.
La superficie UI chatbox
Il plugin contribuisce tre componenti Blade universali in resources/views/components/ — nessuno di essi richiede che l'applicazione host sappia che esistano:
<x-mc-ai-chatbox> — la bolla chatbox floating che si apre in una conversazione agent multi-turn. Montata via l'hook REGISTRY layout.body.before_close così appare su ogni pagina app + admin.
<x-mc-ai-rewrite> — affordance universale "rewrite this text" che può essere droppata accanto a qualsiasi textarea nell'host. Plugin-namespaced, nessuna registrazione centrale.
<x-mc-ai-subject-ab-generator> — genera varianti A/B di subject-line da un prompt. Usato nell'editor di campaign.
Questi tre componenti mostrano il pattern per "il plugin contribuisce UI a molte pagine host senza forkare ogni pagina": shippa il componente come componente Blade anonimo sotto il namespace view del tuo plugin, registralo attraverso gli hook REGISTRY di layout per mounting globale, oppure fai opt-in delle pagine host includendolo direttamente. Entrambi i pattern funzionano; il plugin AI usa entrambi.
Nove file × diciotto locale
Il register() del plugin registra nove file traduzioni separati. La macchina Language::dump() poi materializza ognuno in diciassette cartelle runtime di locale non-inglese sotto storage/app/data/plugins/acelle/ai/lang/. Il risultato su disco: 153 file traduzione runtime (9 file × 17 locale non-inglese + 9 originali inglesi = 162 meno i 9 master inglesi = 153 dump-clone), ognuno editabile separatamente attraverso la UI admin Languages dell'host.
I nove file di superficie (registrati via il loop $aiLangFiles in ServiceProvider::register()):
ai_rewrite — il componente universale text rewrite
ai_chatbox — la UI chatbox
ai_chatbox_prompts — prompt pre-canned mostrati nella chatbox
ai_chatbox_wait — la UI smart-wait ("looking up… running tool… composing reply")
ai_subject_ab — il subject A/B generator
ai_settings — label della pagina admin settings
admin_ai_usage — dashboard admin usage / costo
admin_ai_audit — UI admin audit / replay
admin_ai_permissions — toggle admin di permission per-feature
Questo split è la risposta pratica a "come tengo i file traduzione abbastanza piccoli da farsì che un traduttore possa editarne uno in una singola sessione": un master per superficie logica, registrato separatamente, materializzato per-locale, editato indipendentemente attraverso la UI admin.
File config detenuti dal plugin
Il plugin detiene due file config sotto config/ — registrati in ServiceProvider::boot() via $this->mergeConfigFrom() e raggiungibili attraverso gli helper standard config('ai.*') e config('ai-navigation-hints.*'). La config detenuta dal plugin è il posto giusto per metadati statici che non cambiano per-install (catalogo engine, template di prompt, default di navigazione); le settings admin-editabili vivono nella tabella ai_settings seedata dalla migration.
Lo split — config per default immutabili shippati dal plugin, righe DB per settings mutabili admin-editabili — è un pattern che si porta pulitamente ad altri plugin. Mettere entrambi nella colonna JSON plugins.data è tentatore ma punisce la performance della UI admin; una tabella dedicata indicizzata era la scelta giusta.
L'infrastruttura di test
La directory di test del plugin segue la shape tests/ dell'host — cartelle Unit + Feature più una base class PluginTestCase alla root:
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
Il phpunit.xml dell'host registra il testsuite del plugin come <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" esegue l'intero suite in parallelo con le suite Unit + Feature dell'host stesso.
Il PluginTestCase alla root dimostra il tranello del reset della gate-cache per request che ogni plugin che shippa middleware dovrebbe adottare — vedi il Testing § tranello della gate-cache per il pattern completo. Senza, il secondo test nel suite in poi osserva stato di cache stale dal boot del primo test.
Come imparare da esso
Leggere ogni riga di un plugin da centomila token non è l'esercizio giusto. Una ricetta a quattro passi è più utile:
-
Clona o fai symlink del plugin in un'installazione host. Attivalo. Apri la admin sidebar — il gruppo AI dovrebbe apparire. Clicca "Settings" sulla entry della pagina admin Plugins — la dashboard di plugin-landing dovrebbe caricarsi. Questo prova che la superficie UI del plugin è cablata nel tuo host locale.
-
Leggi
src/ServiceProvider.php dall'alto al basso. Quaranta minuti. Ogni concetto coperto in Architettura del plugin + sistema Hook + UI injection + Traduzioni appare in questo unico file a scala produttiva. Cross-referenzia con le pagine di approfondimento man mano che procedi.
-
Traccia una feature end-to-end. Scegli la bolla chatbox. Trova l'entry point (hook
layout.body.before_close in ServiceProvider), segui il partial che restituisce (ai::partials.body_assets), trova il componente anonimo che renderizza la bolla, trova il JS che la monta, trova la route API che il JS chiama, trova il controller, seguilo giù fino all'engine runtime AIHandler, guarda le righe AIConversation + AIMessage + AIRequest essere inserite. Due-tre ore, end-to-end. Dopo questo saprai quali pattern il plugin AI usa e quali fa a meno.
-
Adatta un pattern al tuo plugin. Scegli il più piccolo che mappa — di solito il loop di registrazione per-translation-surface, o il gruppo admin sidebar, o l'approccio rollup-table per-day. Strappa via il codice AI-specifico; tieni il pattern strutturale. Quello è il guadagno di produttività del primo giorno dell'autore del plugin.
Dove andare poi
Questo è l'ultimo degli undici deep-dive sviluppatore. Da qui, l'hub indice è il recap naturale: Indice documentazione mostra ogni pagina del cluster organizzata in Fondamenta / Costruire / Qualità / Riferimento. La landing developer è l'entry point marketing per nuovi visitatori che arrivano dalla ricerca.
Due prossimi passi pratici quando sei pronto a shippare un plugin reale: il ciclo activate → test → delete dal deep-dive di Testing prova che il listener hook delete_plugin_* del plugin fa cleanup correttamente. Architettura del plugin § Recovery da stato corrotto è la pagina da bookmark per issue di runtime di produzione — tre failure mode più l'esatto path di fix per ognuno.
Oltre ai pattern AcelleMail-specifici, si applica l'ecosistema Laravel più ampio. Il codice del plugin è Laravel vanilla; il loader runtime dell'host è l'unico pezzo non-standard. Qualsiasi cosa tu sappia su Eloquent, Blade, Pest, queue, scheduling, o middleware funziona dentro la cartella del plugin invariata.