Warum dieses Plugin die kanonische Referenz ist
Die meisten Plugins in Produktion sind klein. Ein Sending Driver ist eine Klasse plus ein Connection-Blade. Eine Payment Gateway ist ein Service plus ein Redirect-Controller. Ein einfaches Sidebar-Add-on ist ein Hook::add-Aufruf. Keines davon übt das gesamte Plugin SDK aus — und kleine Plugins sind nicht die richtige Leseübung für einen Autor, der verstehen will, wie groß ein Plugin verantwortungsvoll werden kann.
acelle/ai liegt am anderen Ende des Spektrums. Es ist ein self-contained AI-Subsystem: eine Agent-Chatbox, universelle Text-Rewrite-Components, KB-grounded Coach-Personas und ein Admin-Observability-Dashboard. Das Aktivieren des Plugins in einer AcelleMail-Installation fügt die gesamte AI-Oberfläche hinzu, ohne Kern-Code anzufassen; das Deaktivieren entfernt sie sauber. Das Plugin nutzt jedes in dieser Dokumentation behandelte Konzept in Produktion. Es zu lesen ist der schnellste Weg, zu sehen, wie sich die Muster kombinieren, wenn das Feature nicht-trivial ist.
Der Dateibaum im Überblick
Aus dem README des Plugins:
| Ordner | Was er besitzt |
src/AIHandler/ | AI-Runtime-Engine — Engines, Agent-Loop, Tools, Settings-Resolver, Observability-Writer/Reader, KB-Lookup, URL-Sanitiser. |
src/Models/ | Acht Eloquent-Modelle für das Audit-Substrat (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord). |
src/Controllers/ | Admin-Controller (/rui/admin/ai-*) + Public-API-Controller (/api/v1/ai/*) + der Plugin-Landing-PluginDashboardController. |
src/Services/ | PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy usw. |
src/ServiceProvider.php | Alleiniger Einstiegspunkt — registriert PSR-4, Routen, Views, Sprachdateien, Hooks, Middleware-Aliases, Lifecycle-Listener. |
database/migrations/ | Vierzehn Audit-Substrat-Migrationen. Laufen auf activate, werden auf delete zurückgerollt. |
resources/views/ | Über sechzig Admin-Blade-Templates + drei universelle anonyme Components (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + Chatbox- / Sparkle-JS-Partials. |
resources/assets/ | CSS (~14 Dateien) + JS (~21 Dateien), publiziert nach public/plugins/acelle/ai/ über vendor:publish --tag=plugin --force bei Plugin-Installation. |
resources/lang/ | Achtzehn Locales × neun Sprachdateien = die gesamte Oberfläche des AI-Moduls übersetzt. |
tests/ | Über hundert Pest-Tests (Feature + Unit) + die Basisklasse Acelle\Ai\Tests\PluginTestCase. |
routes.php | Plugin-Routen (Admin + Public API + das Plugin-Landing-Dashboard unter /plugins/acelle/ai/dashboard). |
composer.json | Plugin-Metadaten; extra.setting-route zeigt auf PluginDashboardController@index, sodass der „Settings"-Button auf der Admin-Plugins-Seite tief in das eigene Dashboard des Plugins verlinkt. |
Keiner dieser Ordner ist bespoke. Jeder mappt direkt auf einen Abschnitt im Rest dieser Dokumentation. Das Plugin zu lesen ist ein Prozess des Wiedererkennens desselben Musters, ausgeliefert im Maßstab.
Acht Eloquent-Modelle — das Audit-Substrat
Die Datenschicht des AI-Moduls ist um Auditierbarkeit herum geformt: Jede Konversation, jeder Modell-Aufruf, jeder Tool-Call, jedes Nutzer-Feedback und jeder Blob roher Provider-Outputs wird für Replay und Observability erfasst. Acht Modelle decken dieses Substrat ab:
| Modell | Tabelle | Was es repräsentiert |
AIConversation | ai_conversations | Eine Zeile pro Multi-Turn-Agent- / Support-Session. Trägt Customer- + User-FKs, Task-Key, Screen-Route und aufgerollte Token- / Cost-Totals. |
AIMessage | ai_messages | Eine Zeile pro Nutzer- / Agent-Turn. Rolle, Content-JSON, FK auf einen Tool-Call, Latenz, verwendetes Modell. |
AIRequest | ai_requests | Eine Zeile pro Upstream-API-Aufruf. Engine, Prompt-Hash, Latenz, Kosten, Fehlerstatus. Verbindet AIMessage mit dem tatsächlichen HTTP-Traffic. |
AIToolCall | ai_tool_calls | Function-Call-Invocations, die von einem Agent-Turn ausgehen. Tool-Name, Input/Output-JSON, Source-Flag. |
AIFeedback | ai_feedback | Daumen hoch/runter + Freitext-Feedback pro Message + pro Konversation. |
AIRawBlob | ai_raw_blobs | Originale rohe Provider-Antworten, für Replay / Audit aufbewahrt. Separate Tabelle, weil die Rollup-Tabelle klein bleiben muss. |
AIDailyRollup | ai_daily_rollup | Tagesaggregat für das Admin-Observability-Dashboard — Token-Totals, Kosten, Fehlerrate. Vor-aggregiert, damit das Dashboard günstig liest. |
AIToolUndoRecord | ai_tool_undo_records | Verfolgt umkehrbare Tool-Aktionen für die Funktion „Letzten Schritt rückgängig machen". |
Drei Muster aus dieser Liste übertragen sich direkt auf andere Plugins. „Rohe Provider-Antworten" in eine separate Tabelle von „aufgerollter Zusammenfassung" zu splitten, lässt die Rollup-Tabelle klein genug bleiben, um sie zu scannen. Nullable FKs auf customers und users lassen dieselbe Zeile für authentifizierten und anonymen Traffic funktionieren. Eine-Zeile-pro-Tag-Rollups geben dem Admin-Dashboard günstige Reads ohne schweren JOIN gegen die Activity-Tabellen.
Vierzehn Migrationen, je eine Zeile
Die Migrations-Dateinamen in storage/app/plugins/acelle/ai/database/migrations/ erzählen ihre eigene Geschichte — additiv über die Zeit, sofort umkehrbar, niemals ein destruktiver Schema-Move:
| Dateiname | Was sie tut |
2026_04_28_000001_create_ai_conversations_table.php | Multi-Turn-Chat-Sessions — uid, customer_id FK, Status-Enum, Token-/Cost-Rollups |
2026_04_28_000002_create_ai_messages_table.php | Einzelner Nutzer- / Agent-Turn — Rolle, Content-JSON, Tool-Call-FK, Latenz, verwendetes Modell |
2026_04_28_000003_create_ai_requests_table.php | Eine Zeile pro Upstream-API-Aufruf — Engine, Prompt-Hash, Latenz, Kosten, Fehler |
2026_04_28_000004_create_ai_tool_calls_table.php | Function-Call-Invocations aus einem Agent-Turn — Input / Output JSON |
2026_04_28_000005_create_ai_feedback_table.php | Daumen hoch/runter + Freitext-Feedback pro Message + pro Konversation |
2026_04_28_000006_create_ai_raw_blobs_table.php | Originale rohe Provider-Antworten, für Replay / Audit aufbewahrt |
2026_04_28_000007_create_ai_daily_rollup_table.php | Tagesaggregat für das Admin-Dashboard — Token-Totals, Kosten, Fehlerrate |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | Cross-Tab-Dedup-Spalte — additiv, keine Defaults |
2026_04_30_000002_add_source_to_ai_tool_calls.php | Verfolgt, ob ein Tool-Call aus der Agent- vs. Support-Route kam |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | ULID- / UUID-Breiten-Fix — Spalten-ändernde Migration, vollständig umkehrbar |
2026_05_02_200000_create_ai_tool_undo_records_table.php | Verfolgt umkehrbare Tool-Aktionen für die „Undo last"-Funktion |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | Fügt eine JSON-Spalte für sanitierte URL-Telemetrie hinzu |
2026_05_04_000001_create_ai_settings_table.php | Plugin-Level-Admin-Settings — bewusst getrennt von plugins.data, sodass jede Zeile indizierbar ist |
Lesen Sie die Migrationen oben-nach-unten, und Sie haben die Schema-Evolution des gesamten AI-Moduls. Jede additive Migration ist ein Feature-Ship — die additive Form ist es, was den delete_plugin_*-Rollback des Plugins immer funktionieren lässt, selbst wenn ein Admin nach einem Jahr Feature-Akkretion deinstalliert.
Jeder Hook, den das Plugin verwendet
Der ServiceProvider des Plugins übt jedes Hook-Muster außer FILTER aus. Ein Grep nach Hook::* gegen storage/app/plugins/acelle/ai/src/ServiceProvider.php:
REGISTRY (Hook::add) — sechs Beiträge
- Neun
add_translation_file-Einträge bei register() Zeilen 175-197 — einer pro Übersetzungs-Oberfläche (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Jede Oberfläche ist eine separat editierbare Datei in der Admin-Languages-UI.
layout.head.assets bei boot() Zeile 688 — trägt Chatbox-CSS + JS zu jeder Seite bei, die die Master-app- / admin-Layouts erweitert.
layout.body.before_close bei boot() Zeile 699 — trägt das Chatbox-Bubble-HTML und den Sparkle-Popover vor jedem </body> bei.
admin.sidebar.groups bei boot() Zeile 718 — fügt die AI-Gruppe mit ihren drei oder vier Child-Links zur Admin-Sidebar hinzu.
Alle sechs sichern sich mit aiPluginAvailable() ab — einem Helper, der letztlich zu Plugin::getByName('acelle/ai')->isActive() auflöst. null zurückzugeben, wenn das Plugin abgeschaltet ist, ist die konventionelle Art, sauber zu verschwinden, ohne den Service Provider zu entladen.
EVENT (Hook::on) — zwei Lifecycle-Listener
activate_plugin_acelle/ai bei Zeile 472 — führt artisan migrate gegen den Migrations-Ordner des Plugins aus.
delete_plugin_acelle/ai bei Zeile 546 — akzeptiert das $keepData-Flag, rollt Migrationen zurück, wenn nicht gesetzt, behält die Audit-Tabellen, wenn gesetzt.
BEHAVIOR (Hook::set) — ein Icon-URL-Override
icon_url_acelle/ai bei Zeile 522 — überschreibt den per-Plugin BEHAVIOR-Hook, sodass die Admin-Plugins-Seite das Icon des AI-Moduls statt der Default-plugin.svg des Hosts rendert.
Zusammen sind diese die meiste Hook-Oberfläche des Plugins. Das ServiceProvider.php oben-nach-unten zu lesen, ist der schnellste Weg, zu sehen, wie sich die Muster in Produktion kombinieren.
Die Chatbox-UI-Oberfläche
Das Plugin trägt drei universelle Blade-Components in resources/views/components/ bei — keiner davon benötigt, dass die Host-Anwendung von ihrer Existenz weiß:
<x-mc-ai-chatbox> — das schwebende Chatbox-Bubble, das eine Multi-Turn-Agent-Konversation öffnet. Über den REGISTRY-Hook layout.body.before_close gemountet, sodass es auf jeder app- + admin-Seite erscheint.
<x-mc-ai-rewrite> — universelle „Text umschreiben"-Affordance, die neben jede Textarea im Host gesetzt werden kann. Plugin-namespaced, keine zentrale Registrierung.
<x-mc-ai-subject-ab-generator> — generiert A/B-Subject-Line-Varianten aus einem Prompt. Im Kampagnen-Editor verwendet.
Diese drei Components zeigen das Muster für „Plugin trägt UI zu mehreren Host-Seiten bei, ohne jede Seite zu forken": Liefern Sie die Component als anonyme Blade-Component unter dem View-Namespace Ihres Plugins aus, registrieren Sie sie über die Layout-REGISTRY-Hooks für globales Mounting, oder lassen Sie Host-Seiten per Opt-in einbinden. Beide Muster funktionieren; das AI-Plugin nutzt beide.
Neun Dateien × achtzehn Locales
Der register() des Plugins registriert neun separate Übersetzungsdateien. Die Language::dump()-Maschinerie materialisiert dann jede in siebzehn nicht-englische Locale-Laufzeitordner unter storage/app/data/plugins/acelle/ai/lang/. Das Ergebnis auf der Platte: 153 Laufzeit-Übersetzungsdateien (9 Dateien × 17 nicht-englische Locales + 9 englische Originale = 162 minus die 9 englischen Master = 153 Dump-Clones), jede separat editierbar über die Languages-Admin-UI des Hosts.
Die neun Oberflächen-Dateien (registriert über die $aiLangFiles-Schleife in ServiceProvider::register()):
ai_rewrite — die universelle Text-Rewrite-Component
ai_chatbox — die Chatbox-UI
ai_chatbox_prompts — vordefinierte Prompts, die in der Chatbox angezeigt werden
ai_chatbox_wait — die Smart-Wait-UI („nachschlagen… Tool ausführen… Antwort verfassen")
ai_subject_ab — der Subject-A/B-Generator
ai_settings — Admin-Settings-Seitenbeschriftungen
admin_ai_usage — Admin-Usage- / Kosten-Dashboard
admin_ai_audit — Admin-Audit- / Replay-UI
admin_ai_permissions — Admin-Per-Feature-Berechtigungs-Toggles
Dieser Split ist die praktische Antwort auf „Wie halte ich Übersetzungsdateien klein genug, dass ein Übersetzer eine in einer Sitzung bearbeiten kann": ein Master pro logischer Oberfläche, separat registriert, pro Locale materialisiert, unabhängig über die Admin-UI bearbeitet.
Plugin-eigene Config-Dateien
Das Plugin besitzt zwei Config-Dateien unter config/ — registriert in ServiceProvider::boot() über $this->mergeConfigFrom() und über die Standard-Helper config('ai.*') und config('ai-navigation-hints.*') erreichbar. Plugin-eigene Config ist der richtige Ort für statische Metadaten, die sich pro Installation nicht ändern (Engine-Katalog, Prompt-Templates, Navigation-Defaults); admin-editierbare Settings leben in der von der Migration geseedeten ai_settings-Tabelle.
Der Split — Config für plugin-ausgelieferte unveränderliche Defaults, DB-Zeilen für admin-editierbare veränderliche Settings — ist ein Muster, das sauber auf andere Plugins portiert. Beides in die plugins.data-JSON-Spalte zu legen, ist verlockend, bestraft aber die Performance der Admin-UI; eine dedizierte indizierte Tabelle war die richtige Wahl.
Die Test-Infrastruktur
Das Test-Verzeichnis des Plugins folgt der tests/-Form des Hosts — Unit- + Feature-Ordner plus eine PluginTestCase-Basisklasse im 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
Die phpunit.xml des Hosts registriert die Test-Suite des Plugins als <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" führt die volle Suite parallel zu den Unit- + Feature-Suites des Hosts aus.
Die PluginTestCase im Root demonstriert die per-Request-Gate-Cache-Reset-Falle, die jedes Plugin, das Middleware ausliefert, adoptieren sollte — siehe den Testing § Gate-Cache-Falle für das vollständige Muster. Ohne sie beobachtet der zweite Test in der Suite und folgende veralteten Cache-Zustand aus dem Boot des ersten Tests.
Wie man daraus lernt
Jede Zeile eines Plugins mit hunderttausend Token zu lesen, ist nicht die richtige Übung. Ein Vier-Schritt-Rezept ist nützlicher:
-
Klonen oder symlinken Sie das Plugin in eine Host-Installation. Aktivieren Sie es. Öffnen Sie die Admin-Sidebar — die AI-Gruppe sollte erscheinen. Klicken Sie „Settings" auf dem Eintrag der Admin-Plugins-Seite — das Plugin-Landing-Dashboard sollte laden. Das beweist, dass die UI-Oberfläche des Plugins in Ihren lokalen Host verdrahtet ist.
-
Lesen Sie
src/ServiceProvider.php oben-nach-unten. Vierzig Minuten. Jedes Konzept aus Plugin-Architektur + Hook-System + UI-Injection + Übersetzungen erscheint in dieser einen Datei im Produktions-Maßstab. Verweisen Sie nebenher auf die Deep-Dive-Seiten.
-
Verfolgen Sie ein Feature Ende-zu-Ende. Wählen Sie das Chatbox-Bubble. Finden Sie den Einstiegspunkt (Hook
layout.body.before_close im ServiceProvider), folgen Sie dem Partial, das er zurückgibt (ai::partials.body_assets), finden Sie die anonyme Component, die das Bubble rendert, finden Sie das JS, das es mountet, finden Sie die API-Route, die das JS aufruft, finden Sie den Controller, folgen Sie ihm bis zur AIHandler-Runtime-Engine, beobachten Sie, wie die Zeilen AIConversation + AIMessage + AIRequest eingefügt werden. Zwei bis drei Stunden, Ende-zu-Ende. Danach wissen Sie, welche Muster das AI-Plugin verwendet und welche nicht.
-
Adaptieren Sie ein Muster für Ihr eigenes Plugin. Wählen Sie das kleinste, das mappt — meist die Per-Übersetzungs-Oberfläche-Registrierungs-Schleife, die Admin-Sidebar-Gruppe oder der Per-Tag-Rollup-Tabellen-Ansatz. Schneiden Sie den AI-spezifischen Code heraus; behalten Sie das strukturelle Muster. Das ist der Day-One-Produktivitätsgewinn des Plugin-Autors.
Wie es weitergeht
Das ist die letzte der elf Entwickler-Deep-Dives. Von hier aus ist der Index-Hub die natürliche Zusammenfassung: Dokumentations-Index zeigt jede Seite im Cluster, organisiert in Foundation / Building / Quality / Reference. Die Entwickler-Landing ist der marketing-förmige Einstiegspunkt für neue Besucher aus der Suche.
Zwei praktische nächste Schritte, wenn Sie bereit sind, ein echtes Plugin auszuliefern: der activate → test → delete-Zyklus aus dem Testing-Deep-Dive beweist, dass der delete_plugin_*-Hook-Listener Ihres Plugins sauber aufräumt. Plugin-Architektur § Wiederherstellung aus fehlerhaftem Zustand ist die Seite zum Lesezeichen für produktive Runtime-Issues — drei Fehlermodi plus der exakte Fix-Pfad für jeden.
Über AcelleMail-spezifische Muster hinaus gilt das breitere Laravel-Ökosystem. Plugin-Code ist vanilla Laravel; der Host-Runtime-Loader ist das einzige nicht-standardisierte Stück. Alles, was Sie über Eloquent, Blade, Pest, Queues, Scheduling oder Middleware wissen, funktioniert unverändert innerhalb des Plugin-Ordners.