为何此插件是规范参考
生产中大多数插件都很小。一个发送驱动是一个类加一个连接 blade。一个支付网关是一个服务加一个重定向控制器。一个简单的侧边栏附加是一行 Hook::add。它们都没有完整运用插件 SDK — 而小插件也不是作者想了解一个插件可负责任成长到多大时合适的阅读练习。
acelle/ai 处于另一端。它是一个自包含的 AI 子系统:代理聊天框、通用文本改写组件、KB 锚定的教练人设以及管理员可观测性仪表盘。在一个 AcelleMail 安装中激活该插件就能加入整个 AI 界面而不触动核心代码;停用则干净地移除。该插件在生产中使用了本文档其余部分覆盖的每一个概念。阅读它是观察这些模式在非平凡功能中如何组合的最快方式。
文件树一览
摘自插件自身的 README:
| 文件夹 | 它拥有的内容 |
src/AIHandler/ | AI 运行时引擎 — 引擎、代理循环、工具、设置解析器、可观测性写入/读取器、KB 查询、URL 脱敏器。 |
src/Models/ | 审计底座的八个 Eloquent 模型(AIConversation、AIMessage、AIRequest、AIToolCall、AIFeedback、AIRawBlob、AIDailyRollup、AIToolUndoRecord)。 |
src/Controllers/ | 管理员控制器(/rui/admin/ai-*)+ 公共 API 控制器(/api/v1/ai/*)+ 插件首页 PluginDashboardController。 |
src/Services/ | PluginStatusReport、AISettingsService、AutomationService、AIObservabilityPolicy 等。 |
src/ServiceProvider.php | 唯一入口点 — 注册 PSR-4、路由、视图、语言文件、hook、中间件别名、生命周期监听器。 |
database/migrations/ | 十四个审计底座迁移。激活时执行,删除时回滚。 |
resources/views/ | 60+ 个管理员 Blade 模板 + 三个通用匿名组件(<x-mc-ai-chatbox>、<x-mc-ai-rewrite>、<x-mc-ai-subject-ab-generator>)+ 聊天框 / sparkle JS partial。 |
resources/assets/ | CSS(约 14 个文件)+ JS(约 21 个文件),插件安装时通过 vendor:publish --tag=plugin --force 发布到 public/plugins/acelle/ai/。 |
resources/lang/ | 18 种语言 × 9 个语言文件 = AI 模块的完整界面翻译。 |
tests/ | 100+ 个 Pest 测试(Feature + Unit)+ Acelle\Ai\Tests\PluginTestCase 基类。 |
routes.php | 插件路由(管理员 + 公共 API + 插件首页仪表盘 /plugins/acelle/ai/dashboard)。 |
composer.json | 插件元数据;extra.setting-route 指向 PluginDashboardController@index,让管理员插件页的「Settings」按钮深链到插件自己的仪表盘。 |
这些文件夹没有一个是定制的。每一个都直接对应本文档其余部分的某一节。阅读该插件是识别同一模式在规模化发布中的过程。
八个 Eloquent 模型 — 审计底座
AI 模块的数据层围绕可审计性而设计:每次对话、每次模型调用、每次工具调用、每次用户反馈以及每个原始厂商输出 blob 都被捕获,用于回放与可观测性。八个模型覆盖该底座:
| 模型 | 表 | 代表什么 |
AIConversation | ai_conversations | 每个多轮代理 / 客服会话一行。携带 customer + user 外键、任务键、屏幕路由以及汇总后的 token / cost 总计。 |
AIMessage | ai_messages | 每一次用户 / 代理轮次一行。role、content JSON、指向工具调用的外键、延迟、所用模型。 |
AIRequest | ai_requests | 每一次上游 API 调用一行。引擎、prompt 哈希、延迟、成本、错误状态。把 AIMessage 桥接到真正的 HTTP 流量。 |
AIToolCall | ai_tool_calls | 由代理轮次催生的函数调用。工具名、输入/输出 JSON、来源标志。 |
AIFeedback | ai_feedback | 按消息与按对话的点赞/点踩 + 自由文本反馈。 |
AIRawBlob | ai_raw_blobs | 原始厂商响应,保留用于回放 / 审计。单独成表是因为 rollup 表需要保持精简。 |
AIDailyRollup | ai_daily_rollup | 管理员可观测性仪表盘的按日聚合 — token 总数、成本、错误率。预聚合,让仪表盘读取低成本。 |
AIToolUndoRecord | ai_tool_undo_records | 为「撤销上一步」功能跟踪可逆工具动作。 |
这份清单里有三种模式可直接迁移到其他插件。把「原始厂商响应」与「汇总摘要」拆成两张表,可让 rollup 表保持小到能扫描。指向 customers 与 users 的可空外键让同一行同时服务于已认证与匿名流量。一行/天的 rollup 让管理员仪表盘获得无需对活动表做重 JOIN 的廉价读取。
十四个迁移逐一介绍
storage/app/plugins/acelle/ai/database/migrations/ 中的迁移文件名讲述了它们自身的故事 — 随时间累加、立即可逆、从不做破坏性 schema 变更:
| 文件名 | 做什么 |
2026_04_28_000001_create_ai_conversations_table.php | 多轮聊天会话 — uid、customer_id 外键、status 枚举、token / cost 汇总 |
2026_04_28_000002_create_ai_messages_table.php | 单次用户 / 代理轮次 — role、content JSON、工具调用外键、延迟、所用模型 |
2026_04_28_000003_create_ai_requests_table.php | 每次上游 API 调用一行 — 引擎、prompt 哈希、延迟、成本、错误 |
2026_04_28_000004_create_ai_tool_calls_table.php | 由代理轮次催生的函数调用 — 输入 / 输出 JSON |
2026_04_28_000005_create_ai_feedback_table.php | 按消息与按对话的点赞/点踩 + 自由文本反馈 |
2026_04_28_000006_create_ai_raw_blobs_table.php | 原始厂商响应,保留用于回放 / 审计 |
2026_04_28_000007_create_ai_daily_rollup_table.php | 管理员仪表盘的按日聚合 — token 总数、成本、错误率 |
2026_04_29_000001_add_client_message_id_to_ai_messages.php | 跨选项卡去重列 — 累加式,无默认值 |
2026_04_30_000002_add_source_to_ai_tool_calls.php | 跟踪工具调用来自代理还是客服路由 |
2026_05_02_180000_widen_ai_conversations_client_session_uid.php | ULID / UUID 宽度修复 — 改列迁移,完全可逆 |
2026_05_02_200000_create_ai_tool_undo_records_table.php | 为「撤销上一步」功能跟踪可逆工具动作 |
2026_05_03_000001_add_url_sanitization_to_ai_requests.php | 为脱敏后的 URL 遥测增加一个 JSON 列 |
2026_05_04_000001_create_ai_settings_table.php | 插件级管理员设置 — 与 plugins.data 分开,让每一行都可建索引 |
自上而下读这些迁移,您就掌握了整个 AI 模块的 schema 演进。每一个累加式迁移都是一次功能发布 — 这种累加形状正是让插件的 delete_plugin_* 回滚始终能工作的原因,即便管理员在一年的功能累积之后才卸载。
插件使用的每一个 hook
插件的 ServiceProvider 运用了除 FILTER 之外的每一个 hook 模式。对 storage/app/plugins/acelle/ai/src/ServiceProvider.php grep Hook::*:
REGISTRY(Hook::add)— 六个贡献
- 九个
add_translation_file 条目 位于 register() 第 175-197 行 — 每个翻译界面一个(rewrite、chatbox、prompts、wait、subject AB、settings、admin usage、audit、permissions)。每个界面在管理员 Languages UI 中是一个可独立编辑的文件。
layout.head.assets 位于 boot() 第 688 行 — 向每一个继承主 app / admin 布局的页面贡献聊天框 CSS + JS。
layout.body.before_close 位于 boot() 第 699 行 — 在每个页面的 </body> 之前贡献聊天气泡 HTML 与 sparkle popover。
admin.sidebar.groups 位于 boot() 第 718 行 — 向管理员侧边栏添加带 3 或 4 个子链接的 AI 分组。
所有六处都用 aiPluginAvailable() 给自己把关 — 该辅助方法最终归结为 Plugin::getByName('acelle/ai')->isActive()。在插件被屏蔽时返回 null 是干净「消失」而无需卸载 service provider 的惯用方式。
EVENT(Hook::on)— 两个生命周期监听器
activate_plugin_acelle/ai 位于第 472 行 — 对插件迁移目录执行 artisan migrate。
delete_plugin_acelle/ai 位于第 546 行 — 接受 $keepData 标志,未设置时回滚迁移,设置时保留审计表。
BEHAVIOR(Hook::set)— 一个图标 URL 覆盖
icon_url_acelle/ai 位于第 522 行 — 覆盖按插件的 BEHAVIOR 钩子,让管理员插件页渲染 AI 模块自己的图标,而非宿主默认的 plugin.svg。
这些合起来就是插件大部分的 hook 界面。自上而下阅读 ServiceProvider.php 是观察这些模式在生产中如何组合的最快方式。
聊天框 UI 界面
插件在 resources/views/components/ 中贡献三个通用 Blade 组件 — 它们都不要求宿主应用知道它们存在:
<x-mc-ai-chatbox> — 浮动聊天气泡,打开后进入多轮代理对话。通过 layout.body.before_close REGISTRY 钩子挂载,因而出现在每一个 app + admin 页面。
<x-mc-ai-rewrite> — 通用「改写此文本」能力,可放在宿主任何 textarea 旁边。插件命名空间,无需中心注册。
<x-mc-ai-subject-ab-generator> — 根据 prompt 生成 A/B 主题行变体。用于营销活动编辑器。
这三个组件展示了「插件向多个宿主页贡献 UI 而不 fork 每一页」的模式:把组件作为匿名 Blade 组件发布在您插件的视图命名空间下,通过布局 REGISTRY 钩子实现全局挂载,或者让宿主页面通过直接 include 主动接入。两种模式都可行;AI 插件两者都用。
9 个文件 × 18 种语言
插件的 register() 注册 9 个独立翻译文件。Language::dump() 机制随后把每一个物化到 17 个非英语语言运行时目录 storage/app/data/plugins/acelle/ai/lang/。磁盘上的结果:153 个运行时翻译文件(9 个文件 × 17 种非英语 + 9 个英语原文 = 162,减去 9 个英语 master = 153 个 dump-clone),每一个都可通过宿主 Languages 管理员 UI 单独编辑。
九个界面文件(通过 ServiceProvider::register() 中的 $aiLangFiles 循环注册):
ai_rewrite — 通用文本改写组件
ai_chatbox — 聊天框 UI
ai_chatbox_prompts — 在聊天框中显示的预设 prompt
ai_chatbox_wait — 智能等待 UI(「查询中… 调用工具中… 撰写回复中」)
ai_subject_ab — 主题 A/B 生成器
ai_settings — 管理员设置页标签
admin_ai_usage — 管理员用量 / 成本仪表盘
admin_ai_audit — 管理员审计 / 回放 UI
admin_ai_permissions — 管理员按功能的权限开关
这种拆分是对「如何让翻译文件保持足够小,让译者一次就能编辑完」的实用回答:每个逻辑界面一个 master,分别注册,按语言物化,通过管理员 UI 独立编辑。
插件自有 config 文件
插件在 config/ 下拥有两个 config 文件 — 在 ServiceProvider::boot() 中通过 $this->mergeConfigFrom() 注册,并可通过标准的 config('ai.*') 和 config('ai-navigation-hints.*') 辅助方法访问。插件自有 config 适合放置不会随安装而变的静态元数据(引擎目录、prompt 模板、导航默认值);管理员可编辑的设置则存放在通过迁移播种的 ai_settings 表中。
这种拆分 — config 放置插件随发布的不变默认值,数据库行放置管理员可编辑的可变设置 — 是一种可干净移植到其他插件的模式。把两者都塞进 plugins.data JSON 列虽诱人,却会拖累管理员 UI 性能;专用的带索引表是正确选择。
测试基础设施
插件的测试目录沿用宿主 tests/ 的形状 — Unit + Feature 目录加根目录的 PluginTestCase 基类:
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
宿主的 phpunit.xml 把插件的测试套件注册为 <testsuite name="Plugin: acelle/ai">。./vendor/bin/pest --testsuite="Plugin: acelle/ai" 会与宿主自己的 Unit + Feature 套件并行运行完整套件。
根目录的 PluginTestCase 演示了每一个发布中间件的插件都应采用的「每次请求 gate-cache 重置」陷阱 — 完整模式见 测试 § gate-cache 陷阱。没有它,套件中第二个测试起就会观测到第一个测试启动留下的陈旧缓存状态。
如何从中学习
逐行阅读一个十万 token 的插件并不是合适的练习。四步配方更实用:
-
把插件克隆或软链到宿主安装。激活它。打开管理员侧边栏 — 应该出现 AI 分组。点击管理员插件页条目上的「Settings」 — 插件首页仪表盘应能加载。这证明插件 UI 界面已接入您的本地宿主。
-
自上而下阅读
src/ServiceProvider.php。40 分钟。插件架构 + Hook 系统 + UI 注入 + 翻译 涵盖的每一个概念都在这一个文件中以生产规模出现。边读边与深度文章交叉参考。
-
端到端追踪一个功能。挑选聊天气泡。找到入口(
ServiceProvider 中的 layout.body.before_close 钩子),跟随它返回的 partial(ai::partials.body_assets),找到渲染气泡的匿名组件,找到挂载它的 JS,找到 JS 调用的 API 路由,找到控制器,沿路径深入到 AIHandler 运行时引擎,观察 AIConversation + AIMessage + AIRequest 行被插入。两到三小时,端到端。完成后您就能清楚 AI 插件使用了哪些模式以及没有使用哪些。
-
把一种模式适配到您自己的插件。挑选最容易映射的最小模式 — 通常是按翻译界面的注册循环、管理员侧边栏分组,或按日 rollup 表方式。剔除 AI 专属代码;保留结构模式。这就是插件作者第一天的生产力收益。
下一步阅读
这是 11 篇开发者深度文章的最后一篇。自此,索引中心是自然的回顾:文档索引展示集群中按基础 / 构建 / 质量 / 参考组织的每一页。开发者首页是从搜索而来的新访客的营销式入口。
当您准备发布真实插件时,两个实用的下一步:来自测试深度文章的activate → 测试 → delete 周期证明您的 delete_plugin_* 钩子监听器正确清理。插件架构 § 从异常状态恢复是值得收藏的生产运行问题页 — 三种失败模式以及每一种的精确修复路径。
在 AcelleMail 特有模式之外,更广泛的 Laravel 生态依然适用。插件代码是原生 Laravel;宿主运行时加载器是唯一非标准部分。您所熟悉的 Eloquent、Blade、Pest、queue、调度或 middleware,在插件文件夹内都能原样工作。