插件开发文档
不需要源码、不需要编译:一个 zip 包,让全网表白墙获得新能力。本文覆盖声明式与应用型两种插件的完整开发参考。
1. 快速开始:两种插件形态
上传 zip 时自动识别形态,无需手动标注。两种形态共用同一套安装、配置与审核机制:
| 对比项 | 声明式(默认) | 应用型("kind": "app") |
|---|---|---|
| 组成 | plugin.json | plugin.json + main.js |
| 能力来源 | hooks 白名单(5 类钩子) | clv.* 运行时 API,能力不受白名单限制 |
| 能实现 | 注入 HTML、正则改写、Webhook、发帖守卫、帖子标识 | 新页面、数据表、后台管理页、定时任务、请求拦截、改写卡片与接口响应 |
| 做不到 | 新增页面 / 数据表 / 后台菜单 | 替换内核页面流程、访问操作系统 |
| 门槛 | 会写 JSON | 会写 JavaScript |
| 权限 | 无需声明 | 必须声明 permissions,内核逐项校验 |
| 适合 | 换肤、提示条、敏感词替换、通知转发 | 签到积分、抽奖活动、数据报表、第三方接入 |
拿不准就选应用型:它是声明式的超集,且可以同时写 hooks。两种形态迁移对照见第 8 节。
1.1 最小可用示例
【声明式】plugin.json
{
"name": "每日一言",
"version": "1.0.0",
"author": "yourname",
"description": "在页面底部展示一句随机语录",
"hooks": [
{ "hook": "footer_html", "type": "html", "html": "<div class=\"quote\">今天也要加油鸭</div>" },
{ "hook": "content_filter", "type": "filter", "match": "广告|加群", "replace": "***" },
{ "hook": "post_created", "type": "http", "url": "https://example.com/webhook" }
]
}
【应用型】plugin.json
{
"kind": "app",
"name": "你好世界",
"slug": "hello",
"version": "1.0.0",
"runtime": { "engine": "js", "entry": "main.js" },
"permissions": []
}
main.js
:function setup() {
clv.route("GET", "/", "pageHello");
}
function pageHello(ctx) {
return { body: "<h1>你好,访客 #" + ctx.userId + "</h1>" };
}
应用型安装启用后即可访问 /x/hello/(路由自动挂在 /x/<slug>/ 前缀下)。
2. 插件清单与打包
2.1 包结构
your-plugin.zip ├── plugin.json # 必需:清单(zip 内任意子目录均可,自动识别) ├── main.js # 应用型必需:脚本入口(由 runtime.entry 指定) ├── page.html # 可选:插件自己的页面模板(Go html/template 语法) ├── assets/ # 可选:静态资源,经 /x/<slug>/assets/<文件> 访问 ├── templates/ # 可选:模板覆盖(应用型,见 6.5) └── 片段.html # 可选:声明式 hooks 的 @文件 引用
打包要求:zip 格式;plugin.json 为 UTF-8 无 BOM;单包 ≤ 50MB。Windows 10/11 自带命令 tar -a -c -f 插件.zip *,macOS/Linux 用 zip -r。插件名(name)同时作为站点上的安装目录名。
2.2 清单字段(两种形态通用)
| 字段 | 必填 | 说明 |
|---|---|---|
name | ✅ | 插件名,唯一,同时作为安装目录名 |
version | 版本号,展示用;商店按版本管理更新 | |
author / description | 作者与简介,展示在市场与后台列表 | |
requires | 建议的最低表白墙版本(不满足时显示 ⚠ 不阻止启用) | |
config | 配置项声明,后台自动渲染表单(见第 3 节) | |
hooks | 声明式 ✅ | 钩子数组(见第 4 节);应用型可选用(与脚本混用) |
kind | 应用型 ✅ | 固定 "app";缺省视为声明式 |
slug | 应用型建议 | URL 与表名标识,仅 [a-z0-9_-];缺省由 name 归一化(中文名会退化为 plugin-<hash>,建议显式声明) |
runtime | 应用型 ✅ | { "engine": "js", "entry": "main.js", "timeout_ms": 1500 }(超时上限 30000) |
permissions | 应用型 | 能力白名单(见 6.6),启用时确定,运行时无法追加 |
3. 配置项(config,两种形态通用)
清单里声明 config,站长后台「插件管理 → 插件设置」自动渲染表单,保存即生效无需重启:
"config": [
{ "key": "enable", "label": "启用提示", "type": "switch", "default": "1" },
{ "key": "text", "label": "提示文案", "type": "text", "default": "欢迎" },
{ "key": "body", "label": "长文本", "type": "textarea" },
{ "key": "pos", "label": "显示位置", "type": "select", "default": "bottom", "options": ["top", "bottom"] },
{ "key": "num", "label": "次数上限", "type": "number", "default": "3" },
{ "key": "music", "label": "背景音乐", "type": "audio", "help": "mp3/m4a/aac/ogg/opus/wav/flac,≤20MB" },
{ "key": "icon", "label": "图标文件", "type": "file" }
]
key:声明式钩子里用{config:key}占位符引用;应用型用clv.plugin.config("key")读取audio/file上传型:配置值即为可访问地址(/uploads/plugins/<插件名>/…),重新上传或删除会自动清理旧文件- 所有值会被清除 HTML 与危险字符(可能被原样注入页面);存储在站点 settings 表,随站点一起备份
4. 声明式插件(hooks 全参考)
| type | hook | 作用 | 关键字段 |
|---|---|---|---|
| html | header_html | 页面 body 起始处注入 | html |
| html | footer_html | 页脚(脚本之前)注入 | html |
| html | admin_plugins_top | 后台插件管理页顶部注入 | html |
| filter | content_filter | 正则改写帖子/评论(Go 正则,replace 支持 $1) | match、replace |
| http | post_created | 发帖成功后回调 | url |
| http | comment_created | 评论成功后回调 | url |
| http | report_created | 举报后回调 | url |
| guard | compose_guard | 发帖守卫:命中昵称强制验证后台账号 | nicknames、require、label、message |
| badge | post_badge | 给指定昵称的帖子打标识(不验证) | nicknames、label |
- html 注入可包含
<style>与<script>(原样输出),是实现自定义交互的主要方式;片段较长时用@文件名引用插件目录下的独立文件,避免 JSON 转义(只读直接文件名,@../ 会被无视) - Webhook 以 POST JSON 发送(带
X-ClearLove-Event头),载荷含事件名、时间与业务字段(如 post_id、nickname、content),超时 10 秒,失败只记日志不影响发帖 - guard 行为:发帖昵称命中名单时弹出「管理员身份验证」,表单发帖与 API 发帖都强制校验(API 需额外提供 admin_user / admin_pass);同一 IP 连续失败 5 次锁定 10 分钟;通过后帖子带内置「管理员」标识 + 自定义 label
- 内核遇到不认识的钩子类型会跳过而不报错(向前兼容)
4.1 完整示例:官方身份守卫
{
"name": "官方身份守卫",
"version": "1.0.0",
"author": "yourname",
"description": "使用指定昵称发帖时必须验证后台管理员账号,帖子显示自定义标识",
"config": [
{ "key": "nicknames", "label": "专享昵称", "type": "textarea", "default": "官方,公告" },
{ "key": "badge", "label": "帖子标识", "type": "text", "default": "官方" }
],
"hooks": [
{ "hook": "compose_guard", "type": "guard",
"nicknames": "{config:nicknames}", "require": "admin",
"label": "{config:badge}", "message": "该昵称为官方专享,请验证管理员账号后发布" }
]
}
5. 应用型插件(clv.* 运行时)
5.1 生命周期与执行模型
读取 plugin.json → 执行入口脚本(顶层代码:只做定义) → onInstall()(可选,仅首次)→ setup()(必须:注册全部扩展点) → 注册表快照生效 → 启动定时任务
- 串行队列:每个插件的所有回调在同一条队列排队执行,作者无需考虑并发
- 超时中断:单次调用超过 runtime.timeout_ms 强制中断(死循环也会被中断),之后插件仍可继续服务
- 失败熔断:连续失败 20 次自动禁用并卸载;onDisable() 在停用/卸载前调用,适合清理
- 改了 main.js 需重新启用(或后台点「重载」)才会生效
5.2 注册 API(setup() 中调用)
| API | 说明 |
|---|---|
clv.table(name, columns, opts) | 幂等建表,实际表名 pl_<slug>_<name>;列类型 int / bigint / bool / number / string / text / json / time;opts.unique / opts.indexes 声明索引;主键 id 自动创建 |
clv.route(method, path, handler, opts) | 前台路由,实际路径 /x/<slug><path>;opts.auth = none/user/admin,opts.json = true 时错误以 JSON 返回;支持 /* 后缀通配 |
clv.adminPage(path, handler, opts) | 后台页面,实际路径 /admin/plugins/<slug><path>;opts.perm 为内核权限组(缺省 plugins) |
clv.adminMenu({label, path, perm, order}) | 后台侧边栏菜单项,按 order 升序 |
clv.slot(name, handler) | UI 注入点,handler(data) 返回 HTML 字符串(可用 Slot 见 7.1) |
clv.on(event, handler) / clv.emit(event, payload) | 订阅站点事件 / 广播自定义事件(异步执行,失败仅记日志) |
clv.filter(name, handler) | 过滤器链式处理内核数据,返回 undefined/null 表示不改;可带 priority(小的先执行) |
clv.job(name, every, handler) | 定时任务,every 如 "30s" / "24h"(最小 10s) |
clv.middleware(phase, handler) | "http.before" 可短路请求(返回 {abort:true,...}),"http.after" 仅观察 |
5.3 请求处理器:ctx 与返回值
function myHandler(ctx) {
ctx.req.method / .path / .query / .form / .json / .ip / .fingerprint
ctx.params // 后台页面含 { admin: "1" }
ctx.userId // 登录用户 ID(未登录为 0) ctx.isAdmin / ctx.adminId
ctx.csrf // 当前请求的 CSRF 令牌(渲染表单用)
}
// 返回值按优先级只命中第一个:
return { redirect: "/x/demo/" } // 303 跳转
return { json: {...}, status: 200 } // JSON 响应
return { template: "page.html", data: {...} } // 渲染插件目录下的模板
return { body: "<h1>hi</h1>", status: 200 } // 原样输出
return { headers: {...}, ... } // 可与上述任意组合
POST 同样受 CSRF 保护:模板表单带 {{.csrf}} 隐藏域,JS 请求带 X-CSRF-Token 请求头。
5.4 JavaScript 环境
goja 引擎:支持 ES6 大部分语法(let/const、箭头函数、模板字符串、class、JSON、正则、Date);不支持 async/await、Promise 链、DOM、fetch 与 Node API。所有 clv.* API 均为同步调用,直接拿返回值。推荐 var + function 风格保证兼容。
6. Host API 参考(同步调用,出错抛异常)
6.1 基础信息
clv.version / clv.plugin.name / .slug / .version | 内核与插件自身信息 |
clv.plugin.config(key) / clv.plugin.configs() | 读取配置项(未设置时回落清单 default) |
clv.log.info / .warn / .error(msg) | 写运行日志(站长后台「插件日志」可查) |
clv.view.template(name, data) / clv.view.escape(s) / clv.view.nl2br(s) | 渲染插件目录下模板 / HTML 转义 / 转义后换行转 <br> |
clv.json.encode(v) / clv.json.decode(s) | JSON 互转(原生 JSON 也可用) |
6.2 数据层(权限 db)
clv.db.table(name) | 逻辑名 → 实际表名,拼接 SQL 时务必用它(不要硬编码) |
clv.db.dialect() | "sqlite" 或 "mysql"(写兼容 SQL 用) |
clv.db.query(sql, ...args) | 查询,返回对象数组(上限 1000 行,大表请加 LIMIT 与索引) |
clv.db.get(sql, ...args) | 单行查询,无结果返回 null |
clv.db.exec(sql, ...args) | 写操作,返回 { lastId, rowsAffected } |
clv.db.tx(fn) | 事务:fn(tx) 内 tx.exec/get/query,抛异常自动回滚 |
安全边界:写操作只能落在 pl_<slug>_* 表;内核表可读不可写(声明 db.admin 除外);始终用 ? 占位符传参,绝不拼接用户输入。
6.3 站点数据(site.read / site.write)
clv.post.list({ limit, before, topic, include_hidden }) / clv.post.get(id)
clv.post.create({ content, nickname, topic }) / .update(id, {...}) / .remove(id) // 级联删除
clv.comment.list(postId) / clv.comment.create({ post_id, content, nickname }) / clv.comment.remove(id)
clv.user.get(id) / clv.user.byName("name") / clv.user.current()
clv.stats.overview() / clv.stats.today()
clv.setting.get(key) / clv.setting.all() // 权限 settings.read
clv.setting.set(key, value) // 权限 settings.write,影响内核行为请谨慎
6.4 其它能力
clv.http.request({url, method, headers, body, timeout_ms, max_bytes}) | 出网请求(权限 net);默认拒绝内网/环回地址(SSRF 防护),访问本机服务需 net.local;同步阻塞,计入超时 |
clv.cache.set(key, value, ttl) / .get / .del | 内存缓存(权限 cache),按插件隔离,重启丢失,ttl 上限 24 小时 |
clv.crypto.bcrypt / .verify / .hmacSha256 / .randomHex / .uuid / .base64Encode / .base64Decode | 加密与随机(无需权限;bcrypt 为刻意慢操作,计入超时预算) |
clv.mail.send(to, subject, body) | 发邮件(权限 mail,需站点已配置 SMTP) |
clv.media.saveImage({name, data}) / clv.media.saveVideo({...}) | 保存 base64 媒体(权限 media.write),图片自动转 WebP,单图 ≤10MB |
6.5 模板覆盖(应用型,最强的前端改造能力)
插件目录 templates/ 下放与内核同名的模板文件,重写对应片段(可覆盖 pg_detail / pg_admin_plugins / layout.html 等全部内核片段),启用即生效、后解析者胜。覆盖前请以内核模板为蓝本复制修改,避免遗漏必要结构。
6.6 权限清单
db(数据能力)· db.admin(放开全库写,谨慎)· settings.read / settings.write · site.read / site.write · net / net.local · cache · mail · media.write。未声明的能力调用即抛异常,可在脚本里 try/catch。
7. 扩展点全集
7.1 UI Slot(clv.slot,14 处注入点)
header_html / footer_html / head.end / body.end / admin.head.end / admin.body.end / admin.sidebar.after / admin.settings.after / admin.dashboard.after / post.detail.actions.after / post.detail.after / comments.after / compose.after / profile.after
handler(data) 的 data 含 page / path / site / topic / notice / user / admin / post(视页面而定)。与声明式同名 html 钩子的输出会拼接(声明式在前)。
7.2 事件(clv.on)
post_created(post_id, nickname, content)· post_updated · post_deleted · comment_created(post_id, content)· like_toggled(post_id, liked, count)· report_created(post_id, reason)· user_login(user_id, username)· user_registered。载荷自动含 event 与 time 字段。
7.3 过滤器(clv.filter)
| 过滤器 | 值类型 | 触发点 |
|---|---|---|
filter.content | string | 发帖/评论内容(声明式正则之后) |
filter.card | map | 首页卡片数据(可改 nickname/content/topic/images/videos/likes/comment_count/badges) |
filter.api.response | map | 内核 JSON 接口成功响应 |
filter.compose.guard | map | 发帖守卫参数(require/nickname/labels/message) |
filter.post.visible | bool | 帖子是否可见(返回 false 隐藏) |
filter.auth.login | bool | 登录是否放行 |
filter.theme.css | string | 主题 CSS(可追加全站样式) |
filter.admin.stats / filter.settings.save | map | 后台仪表盘数据 / 保存的设置键值 |
7.4 前端 API(注入到页面的脚本可用)
CLV.api(url, opts)(fetch 封装,自动带 CSRF)· CLV.csrf · CLV.fingerprint() · CLV.toast(msg) · CLV.el(tag, className, text) · CLV.hooks.cardHtml / cardEl / afterRender(介入首页无限滚动卡片的唯一途径)。所有回调均被 try/catch 包裹,插件脚本报错不影响页面其它功能。
8. 发布流程与调试
8.1 发布到插件市场
- 注册开发者账号(邮箱验证)。
- 开发者后台「发布插件」上传 zip,填写分类、说明与标签。
- 管理员审核上架后,所有站点可在后台插件商店一键安装与更新。
8.2 安装到表白墙
表白墙后台 → 插件管理 → 插件商店(云端地址默认本站),一键安装后点「启用」。安装 ≠ 启用:应用型插件启用时执行 setup(),状态显示「运行中」。卸载默认保留数据表(pl_<slug>_*)。
8.3 调试速查
| 现象 | 排查方向 |
|---|---|
| 装了没反应 | 是否已「启用」;列表是否有 ⚠ 兼容提示;查看页面源码确认注入内容 |
| 应用型显示「未运行」 | 插件日志里的 load_error:setup() 抛错 / entry 文件缺失 / JS 语法错误 |
| 路由 404 | 前缀是 /x/<slug>/,方法要一致,插件需「运行中」 |
| 报"未声明权限" | 补 permissions 后需重新启用(权限在加载时确定) |
| 改了代码不生效 | 脚本驻留内存:停用 → 启用(或「重载」);声明式清单有 5 秒缓存 |
| 插件被自动停用 | 触发熔断(连续 20 次失败),查插件日志修好后再启用 |
| CSRF 校验失败 | 表单带 csrf 隐藏域,JS 请求带 X-CSRF-Token 头 |
| 脚本超时 | 调大 runtime.timeout_ms(上限 30000),慢操作放路由或任务里 |
8.4 能力边界与安全
应用型插件运行在进程内沙箱:不能读文件、不能执行系统命令、不能绕过权限、不能访问内网(除非声明 net.local)、不能修改内核业务逻辑。权限在启用时确定;关键动作写入审计日志。请只安装可信来源的插件;上架市场的插件均经管理员人工审核。
8.5 声明式 → 应用型迁移对照
| 声明式写法 | 应用型等价做法 |
|---|---|
| html 钩子注入片段 | clv.slot("footer_html", fn) 动态生成 |
| content_filter 正则 | clv.filter("filter.content", fn) 任意 JS 改写 |
| http Webhook | clv.on("post_created", fn) + clv.http.request(可加签名与重试) |
| {config:key} | clv.plugin.config("key") |
迁移不必推倒重来:清单加 "kind": "app" 与 runtime/permissions,保留原 hooks 数组,再补 main.js 即可,内核会同时处理两者。