Por que este plugin é a referência canônica
A maioria dos plugins em produção é pequena. Um sending driver é uma classe mais um blade de conexão. Um payment gateway é um service mais um redirect controller. Um sidebar add-on simples é uma chamada Hook::add. Nenhum desses exercita o SDK de plugin inteiro — e plugins pequenos não são o exercício de leitura certo para um autor tentando entender quão grande um plugin pode crescer com responsabilidade.
O acelle/ai existe no outro extremo do espectro. É um subsistema de AI autocontido: um chatbox agent, componentes universais de rewrite de texto, personas de coach KB-grounded e um dashboard admin de observabilidade. Ativar o plugin numa instalação AcelleMail adiciona toda a superfície de AI sem tocar no código do core; desativar remove de forma limpa. O plugin usa todo conceito que o resto destes docs cobre, em produção. Lê-lo é o jeito mais rápido de ver como os padrões se combinam quando a feature é não-trivial.
A árvore de arquivos num relance
Do próprio README do plugin:
| Pasta | O que ela é dona |
src/AIHandler/ | Engine runtime de AI — engines, agent loop, tools, settings resolver, observability writer/reader, lookup de KB, sanitiser de URL. |
src/Models/ | Oito models Eloquent para o substrato de auditoria (AIConversation, AIMessage, AIRequest, AIToolCall, AIFeedback, AIRawBlob, AIDailyRollup, AIToolUndoRecord). |
src/Controllers/ | Controllers admin (/rui/admin/ai-*) + controllers de API pública (/api/v1/ai/*) + o PluginDashboardController da landing do plugin. |
src/Services/ | PluginStatusReport, AISettingsService, AutomationService, AIObservabilityPolicy, etc. |
src/ServiceProvider.php | Ponto de entrada único — registra PSR-4, routes, views, arquivos lang, hooks, aliases de middleware, listeners de ciclo de vida. |
database/migrations/ | Catorze migrations do substrato de auditoria. Rodam no activate, rolled back no delete. |
resources/views/ | Mais de sessenta templates Blade admin + três anonymous components universais (<x-mc-ai-chatbox>, <x-mc-ai-rewrite>, <x-mc-ai-subject-ab-generator>) + partials JS de chatbox / sparkle. |
resources/assets/ | CSS (~14 arquivos) + JS (~21 arquivos) publicados em public/plugins/acelle/ai/ via vendor:publish --tag=plugin --force no install do plugin. |
resources/lang/ | Dezoito locales × nove arquivos de língua = a superfície completa do módulo AI traduzida. |
tests/ | Mais de cem testes Pest (Feature + Unit) + a classe base Acelle\Ai\Tests\PluginTestCase. |
routes.php | Routes do plugin (admin + API pública + o dashboard de landing do plugin em /plugins/acelle/ai/dashboard). |
composer.json | Metadata do plugin; extra.setting-route aponta para PluginDashboardController@index para que o botão "Settings" na página admin Plugins deep-linke direto para o dashboard próprio do plugin. |
Nenhuma dessas pastas é sob medida. Cada uma mapeia diretamente para uma seção no resto destes docs. Ler o plugin é um processo de reconhecer o mesmo padrão entregue em escala.
Oito models Eloquent — o substrato de auditoria
A camada de dados do módulo AI é moldada em torno de auditabilidade: toda conversa, toda invocação de model, toda chamada de tool, todo feedback do usuário e todo blob de saída raw do provider é capturado para replay e observabilidade. Oito models cobrem esse substrato:
| Model | Tabela | O que representa |
AIConversation | ai_conversations | Uma linha por sessão multi-turn de agent / support. Carrega FKs de customer + user, task key, route de tela e totais rolled-up de tokens / custo. |
AIMessage | ai_messages | Uma linha por turno de user / agent. Role, content JSON, FK para tool-call, latência, model usado. |
AIRequest | ai_requests | Uma linha por chamada upstream de API. Engine, hash do prompt, latência, custo, status de erro. Faz a ponte de AIMessage para o tráfego HTTP real. |
AIToolCall | ai_tool_calls | Invocações de function-call disparadas por um turno de agent. Nome da tool, JSON de input/output, flag de source. |
AIFeedback | ai_feedback | Thumbs-up/down + feedback de texto livre por mensagem + por conversa. |
AIRawBlob | ai_raw_blobs | Respostas raw originais do provider, mantidas para replay / auditoria. Tabela separada porque a tabela de rollup precisa ficar pequena. |
AIDailyRollup | ai_daily_rollup | Agregado por dia para o dashboard admin de observabilidade — totais de tokens, custo, taxa de erro. Pré-agregado para que o dashboard leia barato. |
AIToolUndoRecord | ai_tool_undo_records | Rastreia ações de tool reversíveis para a feature "undo last". |
Três padrões dessa lista traduzem diretamente para outros plugins. Separar "respostas raw do provider" numa tabela à parte de "rolled-up summary" deixa a tabela de rollup pequena o suficiente para scan. FKs nullable para customers e users deixam a mesma linha funcionar para tráfego autenticado e anônimo. Rollups de uma-linha-por-dia dão ao dashboard admin leituras baratas sem um JOIN pesado contra as tabelas de atividade.
Catorze migrations, uma linha cada
Os filenames de migration em storage/app/plugins/acelle/ai/database/migrations/ contam sua própria história — aditivos ao longo do tempo, imediatamente reversíveis, nunca um movimento de schema destrutivo:
| Filename | O que faz |
2026_04_28_000001_create_ai_conversations_table.php | Sessões de chat multi-turn — uid, FK customer_id, enum de status, rollups de token / custo |
2026_04_28_000002_create_ai_messages_table.php | Um único turno de user / agent — role, content JSON, FK de tool-call, latência, model usado |
2026_04_28_000003_create_ai_requests_table.php | Uma linha por chamada upstream de API — engine, hash do prompt, latência, custo, erro |
2026_04_28_000004_create_ai_tool_calls_table.php | Invocações de function-call disparadas por um turno de agent — JSON de input / output |
2026_04_28_000005_create_ai_feedback_table.php | Thumbs-up/down + feedback de texto livre por mensagem + por conversa |
2026_04_28_000006_create_ai_raw_blobs_table.php | Respostas raw originais do provider, mantidas para replay / auditoria |
2026_04_28_000007_create_ai_daily_rollup_table.php | Agregado por dia para o dashboard admin — totais de tokens, custo, taxa de erro |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | Coluna de dedup cross-tab — aditiva, sem defaults |
2026_04_30_000002_add_source_to_ai_tool_calls.php | Rastreia se uma tool call veio da route de agent vs support |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | Fix de width ULID / UUID — migration que altera coluna, totalmente reversível |
2026_05_02_200000_create_ai_tool_undo_records_table.php | Rastreia ações de tool reversíveis para a feature "undo last" |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | Adiciona uma coluna JSON para telemetria de URL sanitizada |
2026_05_04_000001_create_ai_settings_table.php | Configurações admin nível-plugin — mantidas separadas de plugins.data para que cada linha possa ser indexada |
Leia as migrations de cima para baixo e você tem a evolução de schema do módulo AI inteiro. Toda migration aditiva é um ship de feature — a shape aditiva é o que faz o rollback do delete_plugin_* do plugin sempre funcionar, mesmo quando um admin desinstala depois de um ano de accreção de features.
Todo hook que o plugin usa
O ServiceProvider do plugin exercita todo padrão de hook exceto FILTER. Um grep por Hook::* contra storage/app/plugins/acelle/ai/src/ServiceProvider.php:
REGISTRY (Hook::add) — seis contribuições
- Nove entradas
add_translation_file em register() linhas 175-197 — uma por superfície de tradução (rewrite, chatbox, prompts, wait, subject AB, settings, admin usage, audit, permissions). Cada superfície é um arquivo editável separadamente na UI admin Languages.
layout.head.assets em boot() linha 688 — contribui CSS + JS do chatbox para toda página que estende os layouts master app / admin.
layout.body.before_close em boot() linha 699 — contribui o HTML do bubble do chatbox e o popover sparkle antes do </body> de cada página.
admin.sidebar.groups em boot() linha 718 — adiciona o grupo AI com seus três ou quatro links filhos ao sidebar admin.
Todos os seis se gateiam com aiPluginAvailable() — um helper que em última instância resolve para Plugin::getByName('acelle/ai')->isActive(). Retornar null quando o plugin está gateado off é o jeito convencional de desaparecer de forma limpa sem descarregar o service provider.
EVENT (Hook::on) — dois listeners de ciclo de vida
activate_plugin_acelle/ai na linha 472 — roda artisan migrate contra a pasta de migrations do plugin.
delete_plugin_acelle/ai na linha 546 — aceita a flag $keepData, rolls back as migrations quando não setada, preserva as tabelas de auditoria quando setada.
BEHAVIOR (Hook::set) — um override de URL de ícone
icon_url_acelle/ai na linha 522 — sobrescreve o hook BEHAVIOR por-plugin para que a página admin Plugins renderize o ícone próprio do módulo AI em vez do plugin.svg default do host.
Juntos, esses são a maior parte da superfície de hooks do plugin. Ler ServiceProvider.php de cima para baixo é o jeito mais rápido de ver como os padrões se combinam em produção.
A superfície de UI do chatbox
O plugin contribui três componentes Blade universais em resources/views/components/ — nenhum deles requer que a aplicação host saiba que existem:
<x-mc-ai-chatbox> — o bubble flutuante de chatbox que abre numa conversa multi-turn de agent. Montado via o hook REGISTRY layout.body.before_close para aparecer em toda página app + admin.
<x-mc-ai-rewrite> — affordance universal "rewrite this text" que pode ser colocada ao lado de qualquer textarea no host. Plugin-namespaced, sem registro central.
<x-mc-ai-subject-ab-generator> — gera variantes A/B de subject line a partir de um prompt. Usado no editor de campanha.
Esses três componentes mostram o padrão para "plugin contribui UI para múltiplas páginas do host sem dar fork em cada página": entregue o componente como um anonymous Blade component sob o view namespace do seu plugin, registre via os hooks REGISTRY de layout para montagem global, ou faça as páginas do host opt-in incluindo direto. Ambos os padrões funcionam; o plugin AI usa os dois.
Nove arquivos × dezoito locales
O register() do plugin registra nove arquivos de tradução separados. A maquinaria Language::dump() então materializa cada um em dezessete pastas runtime de locale não-inglês sob storage/app/data/plugins/acelle/ai/lang/. O resultado no disco: 153 arquivos runtime de tradução (9 arquivos × 17 locales não-inglês + 9 originais inglês = 162 menos os 9 masters em inglês = 153 dump-clones), cada um editável separadamente pela UI admin Languages do host.
Os nove arquivos de superfície (registrados via o loop $aiLangFiles em ServiceProvider::register()):
ai_rewrite — o componente universal de rewrite de texto
ai_chatbox — a UI do chatbox
ai_chatbox_prompts — prompts pré-prontos mostrados no chatbox
ai_chatbox_wait — a UI de smart-wait ("looking up… running tool… composing reply")
ai_subject_ab — o gerador A/B de subject
ai_settings — labels da página admin de settings
admin_ai_usage — dashboard admin de uso / custo
admin_ai_audit — UI admin de audit / replay
admin_ai_permissions — toggles admin de permissão por feature
Essa divisão é a resposta prática para "como eu mantenho arquivos de tradução pequenos o bastante para um tradutor editar um numa sentada": um master por superfície lógica, registrado separadamente, materializado por locale, editado independentemente pela UI admin.
Arquivos de config próprios do plugin
O plugin possui dois arquivos de config sob config/ — registrados em ServiceProvider::boot() via $this->mergeConfigFrom() e acessíveis pelos helpers padrão config('ai.*') e config('ai-navigation-hints.*'). Config própria do plugin é o lugar certo para metadata estática que não muda por install (catálogo de engines, templates de prompt, defaults de navegação); settings admin-editáveis vivem na tabela ai_settings seedada por migration.
A divisão — config para defaults imutáveis entregues pelo plugin, linhas de DB para settings mutáveis admin-editáveis — é um padrão que porta de forma limpa para outros plugins. Colocar ambos na coluna JSON plugins.data é tentador mas castiga a performance da UI admin; uma tabela dedicada indexada foi a chamada certa.
A infraestrutura de teste
O diretório de testes do plugin segue a shape tests/ do host — pastas Unit + Feature mais uma classe base PluginTestCase na raiz:
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
O phpunit.xml do host registra a testsuite do plugin como <testsuite name="Plugin: acelle/ai">. ./vendor/bin/pest --testsuite="Plugin: acelle/ai" roda a suite completa em paralelo com as próprias suites Unit + Feature do host.
A PluginTestCase na raiz demonstra a armadilha de reset do gate-cache por-request que todo plugin entregando middleware deveria adotar — veja Testes § gate-cache trap para o padrão completo. Sem ela, o segundo teste em diante na suite observa estado de cache stale do boot do primeiro teste.
Como aprender com isso
Ler toda linha de um plugin de cem mil tokens não é o exercício certo. Uma receita de quatro passos é mais útil:
-
Clone ou symlink o plugin numa instalação host. Ative. Abra o sidebar admin — o grupo AI deve aparecer. Clique "Settings" na entrada da página admin Plugins — o dashboard de landing do plugin deve carregar. Isso prova que a superfície de UI do plugin está conectada no seu host local.
-
Leia
src/ServiceProvider.php de cima para baixo. Quarenta minutos. Todo conceito coberto em Arquitetura de plugin + Sistema de Hook + Injeção de UI + Traduções aparece nesse único arquivo em escala de produção. Cross-referencie com as páginas deep-dive enquanto vai.
-
Tracee uma feature de ponta a ponta. Pegue o bubble do chatbox. Ache o ponto de entrada (hook
layout.body.before_close no ServiceProvider), siga o partial que ele retorna (ai::partials.body_assets), ache o anonymous component que renderiza o bubble, ache o JS que monta, ache a route de API que o JS chama, ache o controller, siga até o engine runtime AIHandler, veja as linhas de AIConversation + AIMessage + AIRequest serem inseridas. Duas a três horas, ponta a ponta. Depois disso você vai saber quais padrões o plugin AI usa e quais ele deixa de fora.
-
Adapte um padrão para seu próprio plugin. Pegue o menor que mapeie — geralmente o loop de registro por-superfície de tradução, ou o grupo de sidebar admin, ou a abordagem de tabela de rollup por-dia. Tire o código AI-específico; mantenha o padrão estrutural. Esse é o ganho de produtividade do dia-um do autor de plugin.
Para onde ir em seguida
Este é o último dos onze deep-dives de desenvolvedor. Daqui, o hub do índice é a recapitulação natural: Índice de documentação mostra cada página no cluster organizada em Foundation / Building / Quality / Reference. A landing de desenvolvedor é o ponto de entrada marketing-shaped para novos visitantes chegando pela busca.
Dois próximos passos práticos quando você está pronto para entregar um plugin real: o ciclo ativar → testar → deletar do deep-dive de Testes prova que o listener do hook delete_plugin_* do plugin limpa corretamente. Arquitetura de plugin § Recuperação de estado quebrado é a página para bookmark para issues runtime de produção — três modos de falha mais o caminho de fix exato para cada.
Além de padrões AcelleMail-específicos, o ecossistema Laravel mais amplo se aplica. Código de plugin é Laravel vanilla; o loader runtime do host é a única peça não-padrão. Qualquer coisa que você sabe sobre Eloquent, Blade, Pest, queues, scheduling ou middleware funciona dentro da pasta de plugin sem alteração.