O plugin complexo canônico. Percorrido de ponta a ponta.

Todo conceito coberto no resto destes docs — o fluxo boot-and-load, os quatro padrões de hook, hooks REGISTRY de layout, migrations isoladas por plugin, o fluxo de tradução indireto, os quatro estados de ciclo de vida, o padrão de testsuite-per-plugin — fica exercitado em storage/app/plugins/acelle/ai/. O plugin entrega oito models Eloquent, catorze migrations, dezoito locales, mais de sessenta templates Blade, três anonymous components universais, todo hook de injeção de layout e mais de cem testes Pest na sua própria testsuite. Esta página é a receita de leitura — o que olhar primeiro, o que olhar por último e como aprender com isso sem se perder no encanamento production-grade.

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:

PastaO 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.phpPonto 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.phpRoutes do plugin (admin + API pública + o dashboard de landing do plugin em /plugins/acelle/ai/dashboard).
composer.jsonMetadata 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:

ModelTabelaO que representa
AIConversationai_conversationsUma 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.
AIMessageai_messagesUma linha por turno de user / agent. Role, content JSON, FK para tool-call, latência, model usado.
AIRequestai_requestsUma 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.
AIToolCallai_tool_callsInvocações de function-call disparadas por um turno de agent. Nome da tool, JSON de input/output, flag de source.
AIFeedbackai_feedbackThumbs-up/down + feedback de texto livre por mensagem + por conversa.
AIRawBlobai_raw_blobsRespostas raw originais do provider, mantidas para replay / auditoria. Tabela separada porque a tabela de rollup precisa ficar pequena.
AIDailyRollupai_daily_rollupAgregado por dia para o dashboard admin de observabilidade — totais de tokens, custo, taxa de erro. Pré-agregado para que o dashboard leia barato.
AIToolUndoRecordai_tool_undo_recordsRastreia 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:

FilenameO que faz
2026_04_28_000001_create_ai_conversations_table.phpSessões de chat multi-turn — uid, FK customer_id, enum de status, rollups de token / custo
2026_04_28_000002_create_ai_messages_table.phpUm único turno de user / agent — role, content JSON, FK de tool-call, latência, model usado
2026_04_28_000003_create_ai_requests_table.phpUma linha por chamada upstream de API — engine, hash do prompt, latência, custo, erro
2026_04_28_000004_create_ai_tool_calls_table.phpInvocações de function-call disparadas por um turno de agent — JSON de input / output
2026_04_28_000005_create_ai_feedback_table.phpThumbs-up/down + feedback de texto livre por mensagem + por conversa
2026_04_28_000006_create_ai_raw_blobs_table.phpRespostas raw originais do provider, mantidas para replay / auditoria
2026_04_28_000007_create_ai_daily_rollup_table.phpAgregado por dia para o dashboard admin — totais de tokens, custo, taxa de erro
2026_04_29_000001_add_client_message_id_to_ai_messages.phpColuna de dedup cross-tab — aditiva, sem defaults
2026_04_30_000002_add_source_to_ai_tool_calls.phpRastreia se uma tool call veio da route de agent vs support
2026_05_02_180000_widen_ai_conversations_client_session_uid.phpFix de width ULID / UUID — migration que altera coluna, totalmente reversível
2026_05_02_200000_create_ai_tool_undo_records_table.phpRastreia ações de tool reversíveis para a feature "undo last"
2026_05_03_000001_add_url_sanitization_to_ai_requests.phpAdiciona uma coluna JSON para telemetria de URL sanitizada
2026_05_04_000001_create_ai_settings_table.phpConfiguraçõ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:

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