06 · 插件系统
核心实现:[app/Core/Plugin.php](../app/Core/Plugin.php) + [app/Core/Hook.php](../app/Core/Hook.php)。插件目录:[content/plugins/](../content/plugins/)。
系统共预留 164 个钩子(61 个业务事件 fire + 103 个数据/视图插槽 filter;2026-10-01 源码普查校准),按四层体系组织:
| 层 | 类型 | 数量 | 用途 |
|---|
| 业务事件层 | Hook::fire(action) | 61 | 写操作完成后通知插件(发帖/回帖/交易/签到/后台管理……),只通知不改数据 |
| 数据管线层 | Hook::filter | 6 | 数据流经插件链式加工(正文渲染/页面标题/通知链接/头像 HTML/货币类型名/首页轮询指纹) |
| 前台视图插槽层 | Hook::filter | 82 | 在前台各页面位置注入 HTML |
| 后台视图插槽层 | Hook::filter | 15 | 在后台页面位置注入 HTML |
1. 插件规范
content/plugins/{目录名}/ # 目录名:字母数字开头,可含 - _,≤50 字符
├── plugin.php # 主文件(必需)
├── install.php # 启用时执行(建表/初始化,可选;需幂等——禁用后再启用会重复执行)
├── uninstall.php # 卸载前执行(清表,可选)
└── views/ # 插件视图(View::renderFile 渲染,可选)
plugin.php 结构:
<?php
/* 防直接 HTTP 访问(必须) */
if (!defined('BASE_PATH')) {
exit;
}
/*
Plugin Name: 插件名称
Plugin URI: https://example.com
Description: 插件描述
Version: 1.0.0
Author: 作者
*/
/* 在此注册钩子(文件被 include 时执行) */
Hook::on('thread_created', function ($data) {
// ...
});
/* 文件末尾 return 配置声明(后台「插件中心 → 配置」自动渲染表单) */
return array(
'config' => array(
'some_key' => array(
'label' => '配置项名',
'type' => 'text', // text / textarea / select 等
'default' => '默认值',
// 'options' => array(...) // select 用
),
),
// 'admin_url' => 'ai-reply', // 可选:声明后「配置」按钮直达该前台路由
);
要点:
- 元信息解析(
Plugin::parseMeta):扫描全部块注释,取含 Plugin Name 的那个;行首 * 可有可无(兼容 WordPress 格式);键名小写下划线化。
- 防重复 include(
Plugin::loadFile 的 $loadedFiles 请求级缓存):boot 与 configDef 共用一次 include,防止插件内全局函数重复声明致命错误——在插件里声明全局函数是安全的。
- 数据约定:自建表必须以
plugin_{目录名}_ 为前缀;配置读写用 Plugin::config($dir, $defaults) / Plugin::saveConfig($dir, $data)(存 settings 表 plugin_config_{目录} 键)。
2. 插件生命周期
| 操作 | 入口 | 行为 |
|---|
| 启用 | 后台插件中心 → Plugin::activate($dir) | 执行 install.php → 写 enabled_plugins → 写 installed_plugins(此后显示卸载按钮)→ 初始化默认配置(已有不覆盖) |
| 禁用 | Plugin::deactivate($dir) | 仅移出启用列表;数据/配置保留,重新启用即恢复 |
| 卸载 | Plugin::uninstall($dir) | 先禁用 → 移除 installed 标记 → 执行 uninstall.php(清插件表)→ 删除配置键;插件文件目录保留(手动删除) |
| 每请求 | bootstrap → Plugin::boot() | include 所有已启用插件的 plugin.php(注册钩子);目录被删则跳过 |
安全边界:Plugin::all() 列表扫描只读源码不执行——未启用插件代码永不运行;boot/loadFile 异常全部捕获写 storage/logs/plugin_error_Ymd.log。
3. 钩子点全景(164 个 · 2026-10-01 源码普查校准)
3.0 钩子机制
Hook::on($name, $callback, $priority = 10); // 注册(Hook::register 为别名)
Hook::fire($name, $data); // action:依次执行回调,忽略返回值,返回原始 $data
Hook::filter($name, $value, $data = null); // filter:链式加工,返回最终值
Hook::has($name); Hook::clear($name); // 查询 / 清空(测试用)
- 优先级:第三参
$priority 越小越先执行(默认 10),同优先级按注册顺序。
- 异常隔离:所有回调在 try/catch 中执行,插件抛错只记
storage/logs/plugin_error_Ymd.log,不影响主流程。
- filter 回调签名:
function ($value, $data);核心未传上下文时回调只收到 $value($data 参数务必给默认值,如 function ($html, $thread = null))。
- fire 回调签名:
function ($data),$data 为关联数组。
3.1 业务事件层(action,61 个)
系统与路由(2)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
routes_register | bootstrap.php,前台路由加载后 | $router | rank(注册 /rank 页)、api\_hub |
admin_routes_register | admin\_routes.php,后台路由加载后、通配之前 | $router | ai\_reply、api\_hub、ai\_writer(各注册自管面板路由) |
账户与资料(9)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
user_registered | 注册成功 | array(id, username, email) | — |
user_login | 登录成功(含记住我自动登录、2FA 二次验证) | array(user, remember) | — |
user_login_failed | 登录失败(密码错误/账号锁定等) | array(login, ip) | — |
user_logout | 退出登录 | array(user) | — |
profile_updated | 用户中心保存资料 | array(user_id, data) | — |
avatar_updated | 用户中心上传新头像 | array(user_id, url) | — |
password_changed | 修改密码成功 | array(user_id) | — |
user_followed | 关注用户 | array(user_id, target_id) | — |
user_unfollowed | 取消关注 | array(user_id, target_id) | — |
主题与互动(14)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
thread_created | 发帖成功(前台发帖 / 后台发布 / ai\_writer 代发,三处均触发) | 前台:array(id, user_id, forum_id, title);后台与 ai\_writer 额外携带 thread_id(同 id)、content | ai\_reply(主题级回复)、rank(活跃分) |
thread_updated | 主题编辑保存后 | array(thread 合并后数据, editor_id) | — |
thread_deleted | 主题删除:前台软删(仅状态实际变化时一次);后台删除带 permanent=true | array(thread, operator_id[, permanent]) | — |
thread_moderated | 版务操作(置顶/加精/关闭/隐藏)后 | array(thread, field, column, value, operator_id) | — |
thread_liked | 点赞/取消点赞主题(两个状态共用) | array(thread_id, user_id, liked) | — |
thread_favorited | 收藏/取消收藏主题 | array(thread_id, user_id, favorited) | — |
post_created | 回帖成功(含 AI 回帖) | array(id, thread_id, user_id, content) | ai\_reply(楼层级回应)、rank |
post_deleted | 回复删除后 | array(post, operator_id) | — |
post_liked | 楼层点赞/取消 | array(post_id, thread_id, user_id, liked) | — |
poll_voted | 投票提交成功 | array(poll_id, thread_id, user_id, option_ids) | — |
bounty_accepted | 悬赏采纳最佳答案 | array(thread, post, amount, currency_id, operator_id) | — |
bounty_refunded | 取消悬赏、托管款退回 | array(thread, amount, currency_id, operator_id) | — |
thread_purchased | 购买付费主题成功 | array(thread, buyer_id, price, currency_id) | — |
thread_viewed | 帖子被浏览(与 views 计数同语义:会话去重 1 小时后触发,插件无需自行防刷新;游客 user\_id=0;v2026.09.06) | array(thread_id, user_id, forum_id) | — |
附件(2)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
attachment_uploaded | 上传附件:type=file 文件模式与 type=link 网盘模式共用同一钩子名,插件按 type 分流 | array(attachment_id, type, user_id, url, price) | — |
attachment_bought | 付费附件购买成功 | array(attachment, buyer_id, price, currency_id) | — |
财富与交易(5)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
currency_changed | 全站货币变动统一出口(currency\_change() 内部,覆盖签到/悬赏/附件/商城/卡密/邀请等全部记账点;事务提交后触发——监听器异常不影响已落账数据,也不可再回滚余额) | array(user_id, currency_id, amount, type, note, balance) | — |
gift_sent | 送礼成功(帖内礼物与用户礼物共用) | array(from_user_id, to_user_id, thread_id, gift, price) | — |
member_purchased | 购买会员成功 | array(membership, user_id, price) | — |
frame_purchased | 购买头像框成功 | array(frame, user_id, price, expire_at) | — |
card_redeemed | 充值卡兑换成功 | array(card, user_id) | — |
签到(2)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
checkin_done | 每日签到成功 | array(user_id, streak, total) | rank |
checkin_repaired | 补签成功 | array(user_id, date, cost, streak) | — |
通知与私信(3)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
notification_sent | notify() 发送通知(合并进已有未读时 merged=true,新发为 false) | array(uid, type, title, content, link, merged) | — |
pm_sent | 私信投递成功(新会话与会话内回复共用 deliver 统一出口) | array(conv_id, from_id, to_id) | — |
pm_deleted | 私信会话删除(仅本人侧隐藏,对方不受影响;v2026.09.06) | array(conv, user_id) | — |
设计边界:通知中心的已读/删除(NotificationController::read/delete)不设事件——纯用户对本人通知的管理操作,无对外语义,插件监听无实际价值;如需通知联动,监听 notification_sent 即可。
后台管理(18)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
user_edited | 后台编辑用户保存 | array(user_id, old, new, operator_id) | — |
user_muted | 后台禁言用户 | array(user_id, until, reason, operator_id) | — |
user_unmuted | 后台解除禁言 | array(user_id, operator_id) | — |
user_banned | 后台封禁账号 | array(user_id, operator_id) | — |
user_unbanned | 后台解封账号 | array(user_id, operator_id) | — |
user_group_changed | 后台变更用户组 | array(user_id, old_group_id, group_id, operator_id) | — |
forum_saved | 后台保存版块(新建/编辑) | array(forum_id, data, operator_id) | — |
forum_deleted | 后台删除版块 | array(forum, operator_id) | — |
group_saved | 后台保存用户组 | array(group_id, data, operator_id) | — |
group_deleted | 后台删除用户组 | array(group, operator_id) | — |
ip_ban_added | 后台新增 IP 封禁 | array(ip, reason, expires_at, operator_id) | — |
ip_ban_updated | 后台更新 IP 封禁配置 | array(ip, reason, expires_at, operator_id) | — |
ip_ban_removed | 后台解封 IP | array(ip, operator_id) | — |
settings_saved | 后台系统设置保存(含分组名与该组键列表) | array(tab, keys, operator_id) | — |
mail_batch_sent | 后台邮件群发完成 | array(subject, to, ok, fail, operator_id) | — |
admin_threads_batch | 后台帖子批量操作(删除/隐藏/恢复/移动等) | array(act, ids, affected, operator_id) | — |
report_submitted | 用户提交举报(前台) | array(type, target_id, reporter_id, reason) | — |
report_handled | 后台举报处理完成 | array(report, operator_id) | — |
插件生命周期(3)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
plugin_activated | 插件启用成功 | array(dir, operator_id) | — |
plugin_deactivated | 插件禁用 | array(dir, operator_id) | — |
plugin_uninstalled | 插件卸载完成 | array(dir, operator_id) | — |
版块关注与举报(3,v2026.09-10 补录)
| 钩子 | 触发点 | 载荷($data) | 现有消费者 |
|---|
forum_followed | ForumController@followToggle 关注成功 | array('user_id', 'forum_id') | 关注通知类插件 |
forum_unfollowed | ForumController@followToggle 取消关注 | array('user_id', 'forum_id') | 同 |
report_cleared | AdminReportsController@clearPost 批量清空举报 | array('status', 'count', 'operator_id') | 举报清理联动插件 |
3.2 数据管线层(filter,6 个)
核心数据流的统一出口,回调 function ($value, $data),必须 return 加工后的值。
| 钩子 | 位置 | $value | 上下文 $data | 典型用途 |
|---|
content_format | helpers.php md_render() 返回处(所有帖文/签名/个人简介渲染的唯一出口) | 渲染后的 HTML | array('raw' => 原始文本) | 插件自定义标签(如 [mytag]…[/mytag])、内容水印、外链转跳转页 |
page_title | layout.php 页面标题 | 当前页面标题串 | 不传 | 按页面动态追加标题后缀 |
notification_link | helpers.php notify() 入口 | 通知跳转链接 | array(uid, type, title) | 改写通知落地页(如积分任务通知跳插件页) |
avatar_html | helpers.php avatar_html() | 头像 <img> HTML | array(user, size) | 头像挂件、VIP 边框 |
currency_type_name | helpers.php currency_type_name() | 流水类型中文名 | 不传(返回值匹配) | 注册插件货币流水类型的中文显示名 |
home_sync_sig | HomeController 首页轮询接口 /home/sync 指纹 | 轮询指纹串 | 不传 | 并入插件状态指纹,驱动已打开的首页自动刷新(仅首页有此钩子) |
/* 示例:自定义内容标签(在 md_render 之后、输出之前生效) */
Hook::on('content_format', function ($html, $data = null) {
return str_replace('[hr]', '<hr class="my-hr">', $html);
});
3.3 前台视图插槽层(filter,82 个)
回调接收并返回 HTML 串(视图用 <?= Hook::filter(...) ?> 直接输出)。输出前必须自行 e() 转义来源数据。
全站布局(layout.php,15 个)
| 钩子 | 位置 | 上下文 $data | 典型用途 |
|---|
layout_head | </head> 前 | — | 注入 CSS/JS/meta(每页生效) |
layout_body_start | <body> 顶部 | — | 全站浮层/顶部公告条 |
layout_body_end | </body> 前(晚于核心脚本) | — | 底部 DOM/脚本 |
nav_links | 顶部导航 | — | 加导航链接(用 class="nav-link" 保持样式) |
nav_logo_after | 品牌 Logo/站名链接后 | — | 站点徽标/内测标注(只能放非交互内联元素,禁嵌套链接) |
nav_tools_before | 顶栏工具区前(搜索/通知/头像之前) | — | 顶栏工具类入口 |
nav_search_after | 顶栏搜索框后 | — | 搜索增强/快捷入口 |
user_menu_links | 用户下拉菜单 | array(user) | 我的订单/我的商店等菜单项 |
main_area_before / main_area_after | 主内容区顶/底 | — | 全站横幅;三栏主题左栏挂载点 |
footer_links | 页脚 | — | 页脚链接 |
side_nav_links / side_nav_bottom | 左侧吸附导航(后台开侧导航时) | — | 侧导航链接与底部卡片(nav_links 原样并入) |
lq_links / lq_bottom | 左侧快捷导航卡(partials/left-quick-nav.php) | — | 快捷导航卡专用追加位(与顶部导航共用 nav_menu_* 开关) |
三栏主题接入模式:核心不提供固定左栏 DOM,由主题插件组合实现——main_area_before 输出 <div class="plugin-left-col"> + layout_head 注入栅格 CSS(把 .main-area 改为 grid 三栏/两栏)+ layout_body_end 注入交互 JS。改核心布局零侵入,多主题插件可共存按启用互斥。
登录 / 注册(6 个)
| 钩子 | 位置 | 上下文 $data |
|---|
login_form_before / login_form_after | 登录页顶/底 | — |
login_form_fields | 登录表单内附加字段 | — |
register_form_before / register_form_after | 注册页顶/底 | — |
register_form_fields | 注册表单内附加字段 | — |
首页(home.php,5 个)
| 钩子 | 位置 | 上下文 $data |
|---|
home_content_before | 首页主体顶部 | — |
home_hero_after | 统计横幅之下、版块列表之上 | — |
home_left_top / home_left_bottom | 三栏布局左栏顶/底 | — |
home_main_bottom | 三栏布局中栏底 | — |
侧栏(首页/版块/帖子三处共用 + 语义化卡片锚点,12 个)
| 钩子 | 上下文 $data |
|---|
sidebar_top / sidebar_mid / sidebar_bottom | 首页/版块/帖子三处侧栏顶/中/底(mid = 首页热门主题后 / 版块页版块信息卡后 / 主题页作者卡后);home.php 不带上下文;forum.php 传 $forum;thread.php 传 $thread |
sidebar_hot_after / sidebar_newuser_after / sidebar_friendlink_after | 首页语义化卡片锚点(2026-09-10 补全,对齐 XiunoX「每张原生卡片后留缝」):热门主题卡后 / 新成员卡后 / 友链卡后 |
sidebar_foruminfo_after / sidebar_mods_after | 版块页语义化锚点:版块信息卡后 / 版主卡后(无版主不渲染) |
sidebar_author_after / sidebar_related_after / sidebar_latest_after / sidebar_forummini_after | 帖子页语义化锚点:作者卡后 / 相关主题卡后(无相关帖不渲染)/ 本版最新回复卡后 / 版块迷你卡后 |
ctx 与所在页侧栏钩子一致;卡片被条件隐藏时对应缝不渲染。
版块页(forum.php,6 个)
| 钩子 | 位置 | 上下文 $data |
|---|
forum_content_before / forum_content_after | 版块页顶/底(两种布局均生效) | $forum |
forum_threads_before | 主题列表卡内顶部 | array(forum, filter, cat_id) |
forum_threads_after | 主题列表卡内底部 | array(forum, filter, threads) |
forum_left_top / forum_left_bottom | 三栏布局左栏顶/底 | — |
帖子详情页(thread.php,10 个)
| 钩子 | 位置 | 上下文 $data |
|---|
thread_head_after | 标题/标签区与正文之间 | $thread |
thread_content_before / thread_content_after | 正文前/后 | $thread(现有消费者:thread\_visitors) |
thread_actions_extra | 主楼操作条内(点赞/收藏/分享按钮追加,与楼层 post_actions_extra 对称) | $thread |
post_author_extra | 每层楼作者名附加徽章 | $p 楼层行 |
post_actions_extra | 每层楼操作按钮区 | $p |
post_floor_extra | 每层楼操作条下附加内容 | $p |
reply_form_before | 回复表单前 | $thread |
thread_left_top / thread_left_bottom | 三栏布局左栏顶/底 | — |
发帖 / 编辑页(thread-new\.php + thread-edit.php 共用,2 个)
| 钩子 | 位置 | 上下文 $data |
|---|
post_form_before / post_form_after | 表单上/下方 | 发帖 array(forum);编辑 array(forum, thread) |
编辑器(partials/editor.php,前后台共用,2 个)
| 钩子 | 位置 | 上下文 $data |
|---|
editor_toolbar_buttons | 工具栏按钮区 | array(canUpload, editorId) |
editor_after | 编辑器下方附加区 | array(canUpload, editorId) |
列表行(partials/thread-row\.php,1 个)
| 钩子 | 位置 | 上下文 $data |
|---|
thread_row_extra | 主题行内附加信息(首页/版块/用户主页列表共用) | $t 主题行 |
用户主页(user.php,3 个)
| 钩子 | 位置 | 上下文 $data |
|---|
user_profile_header_after | 主页头部之下 | $u |
user_profile_tabs | tab 栏尾(链接用 ?tab=xxx#profile-tabs 防跳页顶) | $u |
user_profile_tab_content | tab 面板区首(插件自判 $data['tab'] 渲染面板) | array(tab, user) |
用户中心(uc.php,3 个)
| 钩子 | 位置 | 上下文 $data |
|---|
uc_nav_links | 中心导航尾 | $me |
uc_content_before | 内容区顶部(每页生效) | array(tab, me) |
uc_tab_content | 主区首(插件自判 $data['tab'] 渲染整页) | array(tab, me) |
搜索 / 标签 / 签到 / 商城 / 礼物页(8 个)
| 钩子 | 位置 | 上下文 $data |
|---|
search_content_before / search_content_after | 搜索页顶/底 | array(q) / array(q, total) |
tag_content_before / tag_content_after | 标签页顶/底 | array(tag) |
checkin_content_before | 签到页顶部 | array(streak) |
shop_content_before / shop_hero_after | 商城页顶部 / 横幅下 | — |
gifts_content_before | 礼物墙页顶部 | — |
私信页(pm-inbox.php + pm-view.php 共用,2 个;v2026.09.06)
| 钩子 | 位置 | 上下文 $data |
|---|
pm_content_before / pm_content_after | 私信页顶/底(两页共用) | 收件箱不传;会话页传 array(conv, partner) |
插件自定义 tab 页模式(uc_tab_content / user_profile_tab_content):插件注册路由渲染页面骨架,或在钩子里输出面板——内置 tab 为 if/elseif 链且无 else 兜底,插件 tab(如 shop)不命中任何内置分支,仅插件内容渲染;tab 链接用 ?tab=xxx(user 主页需带 #profile-tabs 锚点)。
应用中心(apps.php,2 个;v2026.09 补录)
| 钩子 | 位置 | 上下文 $data |
|---|
app_cards | 应用中心功能卡片区(与 nav_links 同挂,结构化:icon/name/desc/url) | $cards(数组) |
apps_content_after | 应用中心页底部 | $html |
专题页(topic.php / topics.php,5 个;v2026.09 补录)
| 钩子 | 位置 | 上下文 $data |
|---|
topic_content_before / topic_content_after | 专题页顶/底(详情与总览共用,载荷含 null 兼容) | array('topic' => $topic 或 null) |
topic_main_bottom | 专题详情主区底部 | $html |
topic_sidebar_top / topic_sidebar_bottom | 专题详情侧栏顶/底 | $html |
3.4 后台视图插槽层(filter,15 个)
回调接收并返回 HTML 串,仅在后台页面生效;后台只加载 admin.css(样式需自带 <style> 内联或经 admin_head 注入,不能依赖 app.css)。
布局(admin/layout.php,6 个)
| 钩子 | 位置 | 上下文 $data |
|---|
admin_head | </head> 前 | — |
admin_body_start | <body> 起始 | — |
admin_body_end | </body> 前(晚于核心脚本) | — |
admin_nav_links | 侧栏菜单尾(自组 .nav-group 结构) | array(module) 当前模块 |
admin_content_before / admin_content_after | 内容区顶/底(每页生效) | array(module) |
注意:核心侧栏菜单按多级管理组模块授权自动过滤(Auth::adminModules()),但插件经 admin_nav_links 输出的菜单项不会被自动过滤——插件需自行调用 Auth::adminModules() 判断(返回 null=全权限,否则为模块名数组)再决定是否输出。
各管理页(9 个)
| 钩子 | 位置 | 上下文 $data |
|---|
admin_dashboard_stats_after | 仪表盘统计卡下方 | array(stats) |
admin_dashboard_content_after | 仪表盘页尾 | array(stats, env) |
admin_settings_tabs | 系统设置页 tab 栏尾 | array(tab) |
admin_settings_panels | 系统设置页面板区(配套 tabs 加的页签) | array(tab) |
admin_user_edit_sections | 用户编辑页底部区块 | $u |
admin_group_form_after | 用户组编辑页表单下方 | $group(新建时 null) |
admin_thread_row_actions | 帖子列表行操作区 | $t 行数据 |
admin_thread_form_after | 帖子发布/编辑页表单下方 | array(context: new/edit[, thread]) |
admin_tools_items_before | 系统工具列表前置项(自组 .tool-item 表单) | — |
集成系统设置页模式(admin_settings_tabs + admin_settings_panels 配对使用):
/* 1. tab 栏尾加页签链接 */
Hook::on('admin_settings_tabs', function ($html, $data = null) {
return $html . '<a class="tab-btn ' . (($data['tab'] ?? '') === 'myplug' ? 'is-active' : '') . '"'
. ' href="' . aurl('settings?tab=myplug') . '">我的插件</a>';
});
/* 2. 对应 tab 渲染面板 */
Hook::on('admin_settings_panels', function ($html, $data = null) {
if (($data['tab'] ?? '') !== 'myplug') {
return $html; /* 非本插件 tab 直接返回 */
}
return $html . '<form method="post" action="' . url('myplug-save.html') . '">'
. csrf_field() . ' /* 表单字段 */ </form>';
});
关键约束:面板表单必须提交到插件自有路由(经 routes_register 注册)——核心 settings/save 端点有 $keys[$tab] 白名单,只接受内置 tab 的固定键名,插件的 POST 字段会被丢弃。自有路由内自行做 Auth::isAdmin() + Csrf::check() 后 Plugin::saveConfig()。
4. 内置插件(17 个)
系统内置 17 个官方插件全部随包交付、装完即启用,覆盖社区运营的主要进阶场景——它们全部以标准插件形态实现、核心代码零改动,本身就是「插件能承载完整功能」的活证。功能入口见前台「应用中心」。
插件实现细节(内部机制、数据表结构、调度与并发策略等)不在公开文档范围内。插件开发所需的规范、钩子契约与公共设施 API 见上方章节——按这些公开契约开发,能力与官方插件完全对等。
| 插件 | 功能 |
|---|
| ai_reply | AI 智能回复:多提供商自动回帖 |
| site_rank | 社区排行榜:热门主题 / 活跃用户 / 财富榜 |
| thread_visitors | 帖子访客:最近浏览会员展示 |
| ai_writer | AI 智能写作:标题池 + 文章生成 + 定时发布 |
| api_hub | API 工具箱:多平台解析 + 按次计费 |
| member_wall | 首页会员墙:推荐 / 最新 / 活跃 / 财富四榜 |
| ad_self | 广告位自助投放:按周期购买广告位 |
| lottery | 幸运抽奖:回帖抽奖 + 概率抽奖 + 转盘 |
| ks_exam | 入站考试:通过考试解锁互动权限 |
| geo_qa_factory | GEO 问答工厂:AI 问答对生产与发布 |
| brand_landing | 品牌落地页:站点介绍全屏页 |
| quick_reply | 快速回帖:楼层一键短语回复 |
| copy_tail | 复制保护:复制内容追加来源小尾巴 |
| mourning | 黑白哀悼模式:全站灰度一键切换 |
| pet_garden | 论坛萌宠:养成互动与萌宠榜 |
| sync_platform | 外站同步:公众号 / CSDN 一键分发 |
| geo_check | GEO 检测:单篇收录体检 |
5. 插件开发指南
5.1 最小可用插件
<?php
// content/plugins/hello/plugin.php
if (!defined('BASE_PATH')) {
exit;
}
/*
Plugin Name: Hello
Description: 在帖子正文后追加一句问候
Version: 1.0.0
Author: Me
*/
Hook::on('thread_content_after', function ($html, $thread = null) {
return $html . '<div class="hello-box">感谢阅读「' . e($thread['title'] ?? '') . '」</div>';
});
启用:后台「插件中心 → 启用」即生效(无需重启,每请求 boot 加载)。
5.2 带配置与自建表
install.php 中用 Db::q() 建表(表名 Db::t('plugin_hello_items'),自动带前缀);uninstall.php 中 DROP TABLE。
plugin.php 末尾 return array('config' => array(...)) 声明配置 → 插件中心出现「配置」按钮(AdminPluginsController::settings 通用表单)。按钮仅启用后显示,配置端点校验启用状态(未启用插件的主文件不得被 include 执行)。
- 读配置:
Plugin::config('hello', array('k' => '默认值'))。
5.3 自定义后台面板
return array(
'admin_url' => 'hello-admin', // 「配置」按钮直达 url('admin.php/hello-admin')
);
Hook::on('admin_routes_register', function ($router) {
$router->any('/admin.php/hello-admin', function () {
Auth::boot();
if (!Auth::isAdmin()) {
abort_page(403, '无权限');
}
if (Request::isPost()) {
Csrf::check();
// 保存配置 Plugin::saveConfig('hello', $_POST)
}
$content = View::renderFile(BASE_PATH . '/content/plugins/hello/views/admin.php', array(
'cfg' => Plugin::config('hello'),
));
echo View::render('admin/layout', array('pageTitle' => 'Hello 设置', 'content' => $content));
});
});
注意:插件后台路由必须自行做 Auth::isAdmin() + Csrf::check()(核心守卫不覆盖插件路由);且必须注册在 admin_routes_register 钩子内(先于通配路由匹配)。
5.4 核心公共设施:Cache 缓存与 Cron 计划任务(v2026.09.06)
插件需要的两类通用能力已抽入核心,不要再自造轮子(参照 member_wall 迁移:删掉自有的 mw_cache_get/set/purge 约 30 行,改 Cache::remember 一行式,行为不变)。
缓存([app/Core/Cache.php](../app/Core/Cache.php))——榜单、热点数据、外部接口响应等:
/* 读缓存,未命中查库并回写 5 分钟(最常用) */
$rows = Cache::remember('mw_rank_new_0', 300, function () {
return Db::all('SELECT ...');
});
/* 配置变更时主动失效(不等 TTL 自然过期) */
Cache::forget('mw_rank_new_0');
- 键约定:加插件前缀防冲突(如
mw_、aw_);只含 [A-Za-z0-9_.-](其余字符自动转下划线)。
remember 的空值也算命中(防空穿透:查询失败返回空数组也被缓存,不会每次打穿到库);需区分「未命中」用 Cache::get($key) === null。
- 后台「系统工具 → 清理缓存」会一并清掉(
*.cache 后缀约定)。
计划任务([app/Core/Cron.php](../app/Core/Cron.php))——定时刷新、清理、汇总等:
/* plugin.php 顶层注册(boot 阶段即生效) */
Cron::register('member_wall/refresh', '会员墙榜单刷新', 300, function () {
foreach (array('new', 'active', 'rich') as $t) {
Cache::forget('mw_rank_' . $t . '_0');
}
return '已刷新 3 个榜单'; /* 返回摘要,后台「系统工具→计划任务」展示 */
});
- 任务名全局唯一,建议
插件目录/功能;间隔 ≥60 秒。
- 触发零配置:前台有访问即自动驱动(懒触发);低流量站可在服务器配置 cron 每分钟请求
/cron.html?key=密钥 精准触发(密钥在后台系统工具页)。
- 任务体要求幂等(锁超时或进程被杀后下次触发会重跑);异常自动记录为失败,不连坐其他任务。
- 依赖
cron_runs 表(数据库升级建);未升级时任务静默跳过,站点不受影响。
- 特例说明:ai_writer 因需要超出固定间隔模型的定制调度能力,保留自有调度不迁移;简单定时需求一律用核心 Cron。
常用核心函数速查
记账与通知(多货币与通知体系的唯一入口——勿直接写 SQL 改余额表/通知表,并发正确性与事件触发都在封装内):
/* 记账统一入口:$amount 可负(扣款);返回 bool(余额不足且非 $force 时失败)
事务内行锁保证并发正确,成功后触发 currency_changed 事件 */
currency_change($uid, $cid, $amount, $type, $note = '', $force = false);
/* 发送站内通知(同语义重复通知自动合并未读) */
notify($uid, $type, $title, $content = '', $link = '');
**数据库(Db 封装,全部预处理参数化防注入;列名自动反引号,手写 SQL 避开 MySQL 保留字)**:
Db::t('threads'); // 表名 → 带前缀完整表名
Db::q($sql, $params); // 预处理执行(INSERT/UPDATE/DELETE/DDL),返回 PDOStatement
Db::all($sql, $params); // 多行 → 二维数组
Db::one($sql, $params); // 单行 → 关联数组(无结果 null)
Db::val($sql, $params); // 单值(如 COUNT),无结果 null
Db::insert($table, $data); // 关联数组插入
Db::update($table, $data, $where, $params); // 更新($where 字符串条件)
Db::delete($table, $where, $params); // 删除
Db::count($table, $where, $params); // 计数
权限与输出:
Auth::check(); // 是否已登录
Auth::isAdmin(); // 是否管理员(含全权限组)
Auth::can($perm); // 当前用户是否拥有某权限点
url('thread-123.html'); // 站内 URL(自动兼容子目录部署)
e($text); // HTML 转义——输出一切来源不明数据前必用
5.5 界面文案 i18n(v2026.09.15 起)
核心 i18n 机制详见 [02-核心框架.md](02-核心框架.md) Lang 章节(源串即键:zh-cn 直通,未收录回退中文永不空文案)。插件接入零门槛——照常写中文,输出层包 t():
/* PHP 侧(视图/控制器输出) */
echo t('已复制 ') . $n . t(' 条');
echo t('奖励 %s 金币', array($amount)); /* 占位符 sprintf 语法 */
/* JS 侧(fetch 回包后的提示等):查 window.CLARA_I18N,zh-cn 空表回退原文 */
claraToast(T('购买成功'));
两大禁忌(违者 fatal,生产事故沉淀):
- 编译期常量位置禁调 t()——
static $x = array(...t()...) / const / 类属性默认值 / 参数默认值皆编译级 fatal。static 用惰性初始化(= null + if (=== null) 填充)
- plugin.php 顶级代码禁调 t()——加载期
setting() 未定义,t() → Lang::boot() 即 fatal。Cron::register 的 $label 传中文字面量(后台任务列表展示期自动 t() 翻译),其余顶级 t() 移入函数/闭包体内
补译(可选增强,不补也不破功能):新串在 content/lang/en.php + zh-tw.php 同键补条目(键 = 中文源串逐字符一致;占位符序列/换行数/首尾空格守恒);仅 JS 用的串另加入 content/lang/js-keys.php 清单(JS 翻译表按此清单裁剪注入,不整包内联)。
5.6 开发注意事项
- 插件 PHP 文件必须加
BASE_PATH 防护头,防直接 HTTP 访问。
- 页面级样式用内联
<style> 随输出渲染,类名统一加插件前缀(如 tv-),复用 app.css 的 CSS 变量(--bd/--soft-2/--tx-2/--radius-s/--ac)保证主题一致。后台插槽输出的内容不能依赖 app.css(后台只加载 admin.css)。
- 插件抛错会被 Hook 捕获记日志,但不要在回调里做致命假设(如未判空直接用
$thread['x'])。
- 钩子回调签名:filter 型为
function ($value, $data)($data 可能不传,需默认值);fire 型为 function ($data)。
- 多 tab 共用一个 save 端点的插件面板:保存时按表单 hidden 标识分支构造
$new,只更新本页字段,否则未提交字段会经 array_merge 默认值洗掉另一 tab 的配置。
- 监听
thread_created 时以 id 键为主(前台/后台/ai\_writer 三处触发均已对齐;thread_id/content 仅后台与 ai\_writer 额外携带,勿作必需字段依赖)。
- 动作型钩子(点赞/收藏等)携带状态布尔(
liked/favorited),监听方需区分正向与取消操作,避免重复记账。
- 建表与手写 SQL 避开 MySQL 保留字:列名撞保留字(如
explain、order、group、key)会直接 1064 语法错误导致 install.php 中断(真实案例:ks_exam 首版 explain 列)。Db::insert/update 等封装会自动反引号列名,但手写 SQL(建表 DDL、SELECT 列清单)必须自己加反引号(` explain ),或干脆换列名(如 tip`)。