ClearLove 插件社区

插件开发文档

不需要源码、不需要编译:一个 zip 包,让全网表白墙获得新能力。本文覆盖声明式与应用型两种插件的完整开发参考。

快速开始 清单与打包 配置项 声明式插件 应用型插件 Host API 扩展点全集 调试与安全

1. 快速开始:两种插件形态

上传 zip 时自动识别形态,无需手动标注。两种形态共用同一套安装、配置与审核机制:

对比项声明式(默认)应用型("kind": "app")
组成plugin.jsonplugin.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" }
]

4. 声明式插件(hooks 全参考)

typehook作用关键字段
htmlheader_html页面 body 起始处注入html
htmlfooter_html页脚(脚本之前)注入html
htmladmin_plugins_top后台插件管理页顶部注入html
filtercontent_filter正则改写帖子/评论(Go 正则,replace 支持 $1)match、replace
httppost_created发帖成功后回调url
httpcomment_created评论成功后回调url
httpreport_created举报后回调url
guardcompose_guard发帖守卫:命中昵称强制验证后台账号nicknames、require、label、message
badgepost_badge给指定昵称的帖子打标识(不验证)nicknames、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()(必须:注册全部扩展点)
  → 注册表快照生效 → 启动定时任务

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.contentstring发帖/评论内容(声明式正则之后)
filter.cardmap首页卡片数据(可改 nickname/content/topic/images/videos/likes/comment_count/badges)
filter.api.responsemap内核 JSON 接口成功响应
filter.compose.guardmap发帖守卫参数(require/nickname/labels/message)
filter.post.visiblebool帖子是否可见(返回 false 隐藏)
filter.auth.loginbool登录是否放行
filter.theme.cssstring主题 CSS(可追加全站样式)
filter.admin.stats / filter.settings.savemap后台仪表盘数据 / 保存的设置键值

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 发布到插件市场

  1. 注册开发者账号(邮箱验证)。
  2. 开发者后台「发布插件」上传 zip,填写分类、说明与标签。
  3. 管理员审核上架后,所有站点可在后台插件商店一键安装与更新。

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 Webhookclv.on("post_created", fn) + clv.http.request(可加签名与重试)
{config:key}clv.plugin.config("key")

迁移不必推倒重来:清单加 "kind": "app" 与 runtime/permissions,保留原 hooks 数组,再补 main.js 即可,内核会同时处理两者。