Il plugin complesso canonico. Percorso end-to-end.

Ogni concetto coperto nel resto di questi docs — il flusso boot-and-load, i quattro pattern hook, gli hook REGISTRY di layout, le migration isolate per plugin, il flusso di traduzione indiretto, i quattro stati del lifecycle, il pattern testsuite-per-plugin — viene esercitato in storage/app/plugins/acelle/ai/. Il plugin shippa otto model Eloquent, quattordici migration, diciotto locale, sessanta-e-più template Blade, tre componenti anonimi universali, ogni hook di layout-injection, e cento-e-più test Pest nel proprio testsuite. Questa pagina è la ricetta di lettura — cosa guardare per primo, cosa per ultimo, e come imparare da essa senza perdersi nella plumbing production-grade.

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:

CartellaCosa 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.phpUnico 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.phpRoute del plugin (admin + API pubblica + la dashboard di plugin-landing in /plugins/acelle/ai/dashboard).
composer.jsonMetadati 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:

ModelTabellaCosa rappresenta
AIConversationai_conversationsUna riga per sessione multi-turn agent / support. Porta FK customer + user, task key, route screen, e totali roll-up di token / costo.
AIMessageai_messagesUna riga per turno user / agent. Ruolo, content JSON, FK a una tool-call, latenza, modello usato.
AIRequestai_requestsUna riga per chiamata API upstream. Engine, prompt hash, latenza, costo, status errore. Fa da ponte tra AIMessage e il traffico HTTP reale.
AIToolCallai_tool_callsInvocazioni di function-call generate da un turno agent. Nome del tool, input/output JSON, source flag.
AIFeedbackai_feedbackPollice su/giù + feedback free-text per messaggio + per conversazione.
AIRawBlobai_raw_blobsRisposte raw originali del provider, tenute per replay / audit. Tabella separata perché la tabella di rollup deve restare piccola.
AIDailyRollupai_daily_rollupAggregato per-giorno per la dashboard admin di osservabilità — totali token, costo, error rate. Pre-aggregato così la dashboard legge a basso costo.
AIToolUndoRecordai_tool_undo_recordsTraccia 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 fileCosa fa
2026_04_28_000001_create_ai_conversations_table.phpSessioni chat multi-turn — uid, FK customer_id, enum di status, roll-up token / costo
2026_04_28_000002_create_ai_messages_table.phpSingolo turno user / agent — ruolo, content JSON, FK tool-call, latenza, modello usato
2026_04_28_000003_create_ai_requests_table.phpUna riga per chiamata API upstream — engine, prompt hash, latenza, costo, errore
2026_04_28_000004_create_ai_tool_calls_table.phpInvocazioni di function-call generate da un turno agent — input / output JSON
2026_04_28_000005_create_ai_feedback_table.phpPollice su/giù + feedback free-text per messaggio + per conversazione
2026_04_28_000006_create_ai_raw_blobs_table.phpRisposte raw originali del provider, tenute per replay / audit
2026_04_28_000007_create_ai_daily_rollup_table.phpAggregato per-giorno per la dashboard admin — totali token, costo, error rate
2026_04_29_000001_add_client_message_id_to_ai_messages.phpColonna di dedup cross-tab — additiva, nessun default
2026_04_30_000002_add_source_to_ai_tool_calls.phpTraccia se una tool call è arrivata da agent vs route di support
2026_05_02_180000_widen_ai_conversations_client_session_uid.phpFix di larghezza ULID / UUID — migration column-altering, completamente reversibile
2026_05_02_200000_create_ai_tool_undo_records_table.phpTraccia le azioni tool reversibili per la feature "undo last"
2026_05_03_000001_add_url_sanitization_to_ai_requests.phpAggiunge una colonna JSON per la telemetria URL sanitizzata
2026_05_04_000001_create_ai_settings_table.phpSettings 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.