仰望星辰工作室

clearlove2.1

clearlove2.1/ docs/PLUGIN-DECLARATIVE.md 16.9 KB · 398 行 原始文件
1# 声明式插件开发指南(A 型)
2
3> 本文档面向**声明式插件**:只有一份 `plugin.json`,用 `hooks` 描述"要往哪里注入什么",
4> 由内核解释执行。**无需写代码、无需编译**,适合轻量扩展。
5>
6> 需要独立页面、数据表、后台菜单、定时任务?请改用**应用型插件** → [PLUGIN-APP.md](PLUGIN-APP.md)。
7> 通用部分(打包、安装、配置项、权限、调试)见 [PLUGIN.md](PLUGIN.md)。
8
9---
10
11## 1. 插件包结构
12
13```
14your-plugin.zip
15├── plugin.json # 必需:插件清单(放在根目录或任意子目录均可)
16└── (可选)其他静态资源,如 @片段.html
17```
18
19打包要求:
20
21- 必须是 zip 格式,`plugin.json` 需为 **UTF-8 无 BOM**
22- 插件名(`name`)会被用作目录名,建议避免 `/`、`\` 等字符
23- 单包不超过 50MB
24- 各系统打包命令、开发调试循环(全程无需源码)见 [PLUGIN.md](PLUGIN.md) 2.0 节
25
26安装后在后台「插件管理」点击**启用**才生效。
27
28---
29
30## 2. plugin.json 完整字段
31
32```json
33{
34 "name": "插件名(必需,唯一)",
35 "version": "1.0.0",
36 "author": "作者",
37 "description": "一句话说明",
38 "requires": "2.0.0",
39 "config": [ ... ],
40 "hooks": [ ... ]
41}
42```
43
44| 字段 | 必填 | 说明 |
45|---|---|---|
46| `name` | ✅ | 插件名,同时作为安装目录名 |
47| `version` | | 版本号,展示用 |
48| `author` | | 作者名 |
49| `description` | | 简介,展示在插件列表与插件市场 |
50| `requires` | | 建议的最低表白墙版本(不满足时后台显示 ⚠,不阻止启用) |
51| `config` | | 配置项声明,见第 3 节 |
52| `hooks` | ✅ | 钩子列表,见第 4 节 |
53
54> 清单里**不要**写 `kind` 字段(那是应用型插件的标识);写了 `"kind": "app"` 会走另一套运行时。
55
56---
57
58## 3. 配置项(config)
59
60插件可以声明若干配置项,表白墙会**自动在后台「插件管理 → 插件设置」渲染表单**,
61管理员填写后立即生效,无需重新安装插件。
62
63```json
64"config": [
65 { "key": "nicknames", "label": "专享昵称", "type": "textarea",
66 "default": "官方,公告", "help": "多个昵称用逗号或换行分隔" },
67 { "key": "badge", "label": "帖子标识", "type": "text", "default": "官方" },
68 { "key": "enable", "label": "启用提示", "type": "switch", "default": "1" },
69 { "key": "position", "label": "显示位置", "type": "select",
70 "default": "bottom", "options": ["top", "bottom"] }
71]
72```
73
74| 字段 | 说明 |
75|---|---|
76| `key` | 字段名,供 `{config:key}` 引用 |
77| `label` | 后台表单里显示的名称 |
78| `type` | `text` / `textarea` / `switch`(值 `1`/`0`)/ `select` / `number` / `audio`(音频上传)/ `file` |
79| `default` | 未配置时的默认值 |
80| `help` | 表单下方的说明文字 |
81| `options` | 仅 `select` 使用,候选项数组 |
82
83### 上传型字段(audio / file)
84
85`type: "audio"` 会渲染成「试听 + 选择文件」的上传控件,保存后配置值即为文件的可访问地址
86(形如 `/uploads/plugins/<插件名>/audio-xxxxxxxx.mp3`),可直接用于 `{config:key}`:
87
88```json
89{ "key": "music", "label": "背景音乐文件", "type": "audio",
90 "default": "", "help": "支持 mp3 / m4a / aac / ogg / opus / wav / flac,单个文件不超过 20MB" }
91```
92
93- 文件名由服务端生成,不使用上传时的原始文件名
94- 重新上传或勾选「删除」会自动清理旧文件
95- 配置值会被清除引号与尖括号等危险字符 —— 因为它会被原样注入页面
96
97### 在钩子里引用配置
98
99任意字符串字段都支持 `{config:字段名}` 占位符,运行时替换为管理员填写的值:
100
101```json
102{ "hook": "compose_guard", "type": "guard",
103 "nicknames": "{config:nicknames}",
104 "label": "{config:badge}" }
105```
106
107```json
108{ "hook": "footer_html", "type": "html",
109 "html": "<audio src=\"{config:music}\" autoplay loop></audio>" }
110```
111
112配置值保存在 `settings` 表(键名 `plugin.<插件名>.<字段名>`),随站点一起备份。
113
114---
115
116## 4. 钩子一览
117
118`hooks` 是一个数组,每个元素至少包含 `hook` 与 `type`:
119
120| type | hook | 作用 | 关键字段 |
121|---|---|---|---|
122| `html` | `header_html` | 前台页面 `<head>` 后(body 起始处)注入 HTML | `html` |
123| `html` | `footer_html` | 前台页面页脚注入 HTML | `html` |
124| `html` | `admin_plugins_top` | 后台「插件管理」页顶部注入 HTML | `html` |
125| `filter` | `content_filter` | 用正则改写帖子 / 评论内容 | `match`、`replace` |
126| `http` | `post_created` | 发帖成功后回调 Webhook | `url` |
127| `http` | `comment_created` | 评论后回调 Webhook | `url` |
128| `http` | `report_created` | 举报后回调 Webhook | `url` |
129| `guard` | `compose_guard` | **发帖前置守卫**:命中昵称时强制验证后台账号 | `nicknames`、`require`、`label`、`message` |
130| `badge` | `post_badge` | 给指定昵称的帖子打上标识 | `nicknames`、`label` |
131
132> 内核遇到**不认识的钩子类型会跳过该钩子而不报错**,详见第 8 节"能力边界"。
133
134### 4.1 html:注入片段
135
136```json
137{ "hook": "footer_html", "type": "html",
138 "html": "<div class='daily-quote'>今天也要加油鸭</div>" }
139```
140
141注入内容会被原样输出到页面,因此**可以包含 `<style>` 与 `<script>`** —— 这也是插件实现
142自定义交互(弹窗、动效、统计)的主要方式。请勿注入恶意代码。
143
144三个注入点的位置:
145
146| hook | 渲染位置 | 适用 |
147|---|---|---|
148| `header_html` | `layout.html` 中 `<body>` 起始处 | 顶部公告条、全局横幅 |
149| `footer_html` | `layout.html` 页脚区域(脚本之前) | 悬浮按钮、播放器、统计脚本 |
150| `admin_plugins_top` | 后台插件管理页顶部 | 插件自检提示、批量操作入口 |
151
152> 需要更细的注入点(`head.end`、`body.end`、`post.detail.after`、`admin.sidebar.after` 等 14 处)?
153> 这些是**应用型插件**的 Slot(`clv.slot`),见 [PLUGIN-APP.md](PLUGIN-APP.md) 第 9.1 节。
154> 应用型插件注册同名 `footer_html` / `header_html` 时,输出会与 A 型片段拼接在一起。
155>
156> 注入到页面的脚本(不管是 A 型还是 B 型)都可以使用内核前端 API —— `CLV.api` 发请求、
157> `CLV.hooks.cardHtml` 给首页卡片追加内容等,见 [PLUGIN-APP.md](PLUGIN-APP.md) 第 9.7 节。
158
159### 4.2 片段较长时用 `@文件`
160
161在 JSON 里转义大段 HTML / JS 很痛苦。把片段放在插件目录下的独立文件里,
162`html` 字段写成 `@文件名` 即可,安装(zip)时会随插件一起落盘:
163
164```json
165{ "hook": "footer_html", "type": "html", "html": "@player.html" }
166```
167
168```
169插件 zip 根目录/
170├── plugin.json
171└── player.html ← 与 plugin.json 同级,整段 HTML/CSS/JS 原样放在这里
172```
173
174- 只读取插件目录下的**直接文件名**,`@../xxx` 这类路径会被无视
175- 片段文件缺失时该钩子被跳过(不会把 `@player.html` 原样输出到页面)
176- 官方「背景音乐」插件就是这么写的,可直接参考 `plugins/bgm/`
177
178### 4.3 filter:内容正则改写
179
180```json
181{ "hook": "content_filter", "type": "filter", "match": "广告|加群|代刷", "replace": "***" }
182```
183
184- `match` 是 Go 正则表达式;`replace` 支持 `$1` 捕获组引用
185- 对**表单发帖、API 发帖、评论**三类内容生效(均在 `StripHTML` 之后执行)
186- 规则有 30 秒缓存;插件启用后会自动热加载
187- 多个插件的规则按顺序依次应用
188- 应用型插件的 `filter.content` 会在此基础上继续链式处理
189
190### 4.4 http:事件 Webhook
191
192```json
193{ "hook": "post_created", "type": "http", "url": "https://example.com/hook" }
194```
195
196回调以 `POST` + JSON 发送,请求头带 `X-ClearLove-Event`,超时 10 秒,
197**失败只记录日志、不影响发帖**。回调体包含事件名、时间与业务字段:
198
199```json
200{ "event": "post_created", "post_id": 12, "nickname": "匿名", "content": "...", "time": "2026-09-12T15:00:00Z" }
201```
202
203| 事件 | 触发时机 | 载荷字段 |
204|---|---|---|
205| `post_created` | 发帖成功(表单 / API) | `post_id`、`nickname`、`content` |
206| `comment_created` | 评论成功 | `post_id`、`content` |
207| `report_created` | 提交举报 | `post_id`、`reason` |
208
209> 需要订阅更多事件(点赞、注册、登录、改帖、删帖…)或做进程内处理?用应用型插件的 `clv.on`。
210
211---
212
213## 5. guard:发帖守卫(需要验证后台账号)
214
215`compose_guard` 用于**防止他人冒充官方身份**:当发帖昵称命中名单时,
216表白墙会在提交前弹出「管理员身份验证」弹窗,要求输入后台管理员账号与密码,
217**服务端校验通过后才会发布**(不通过直接拒绝,无法绕过),并可给帖子打上标识。
218
219| 字段 | 说明 |
220|---|---|
221| `nicknames` | 触发的昵称名单,逗号 / 分号 / 换行分隔,支持 `{config:key}` |
222| `require` | `admin` = 必须验证后台管理员账号密码;留空表示仅打标识、不校验 |
223| `label` | 校验通过后写入帖子的标识文字(前台显示为昵称后的彩色徽章) |
224| `message` | 校验失败时返回给用户的提示语 |
225
226```json
227{
228 "hook": "compose_guard",
229 "type": "guard",
230 "nicknames": "官方,公告板",
231 "require": "admin",
232 "label": "官方",
233 "message": "该昵称为官方专享,请验证管理员账号后发布"
234}
235```
236
237行为说明:
238
239- 前台发帖页会在昵称命中时**自动弹出验证弹窗**(昵称名单由插件配置实时提供)
240- 表单发帖(`POST /compose`)与 API 发帖(`POST /api/v1/posts`)都会强制校验
241- API 发帖需在 JSON 中额外提供 `admin_user` 与 `admin_pass`
242- 同一 IP 连续失败 5 次会被锁定 10 分钟,防止暴力破解后台密码
243- 多个插件同时命中时,标识会合并、校验要求取并集
244- 通过验证的帖子会同时带上内置的「**管理员**」标识,插件自定义的 `label` 追加在后面,例如 `["管理员", "官方认证"]`
245
246### badge:帖子标识(无需验证)
247
248只想给某些昵称的帖子加个标记时使用,不涉及验证:
249
250```json
251{ "hook": "post_badge", "type": "badge", "nicknames": "小编,编辑", "label": "编辑" }
252```
253
254标识在发帖时写入帖子记录,**不随插件卸载而消失**(已发布的历史帖子仍保留标识)。
255
256---
257
258## 6. 完整示例:官方身份守卫插件
259
260`plugin.json`:
261
262```json
263{
264 "name": "官方身份守卫",
265 "version": "1.0.0",
266 "author": "yourname",
267 "description": "使用指定昵称发帖时必须验证后台管理员账号,帖子显示自定义标识,防止冒充官方身份",
268 "requires": "2.0.0",
269 "config": [
270 { "key": "nicknames", "label": "专享昵称", "type": "textarea", "default": "官方,公告",
271 "help": "使用这些昵称发帖时需验证后台账号,多个用逗号或换行分隔" },
272 { "key": "badge", "label": "帖子标识", "type": "text", "default": "官方",
273 "help": "验证通过的帖子在昵称后显示的徽章文字" },
274 { "key": "message", "label": "失败提示", "type": "text",
275 "default": "该昵称为官方专享,请验证管理员账号后发布" }
276 ],
277 "hooks": [
278 { "hook": "compose_guard", "type": "guard",
279 "nicknames": "{config:nicknames}",
280 "require": "admin",
281 "label": "{config:badge}",
282 "message": "{config:message}" },
283 { "hook": "footer_html", "type": "html",
284 "html": "<style>.post-badge{box-shadow:0 1px 6px rgba(255,123,169,.45)}</style>" }
285 ]
286}
287```
288
289打包与安装(三平台打包命令见 [PLUGIN.md](PLUGIN.md) 2.0 节):
290
291```bash
292cd your-plugin
293zip -r official-guard.zip . # macOS / Linux
294tar -a -c -f official-guard.zip * # Windows 10/11 自带命令
295```
296
297后台「插件管理 → 上传并安装」选择 zip → 点「启用」→ 在「插件设置」里填写专享昵称
298→ 用该昵称发帖,验证弹出管理员验证弹窗、通过后帖子带上标识。
299
300> 表白墙内置了「管理员专享昵称」(网站设置),覆盖最常见的官方身份保护场景;
301> 这个插件演示的是如何用插件实现同类能力,并支持自定义标识文字与多插件组合。
302
303### 6.1 另一个完整例子:每日一言(注入 + 配置 + 前端 JS)
304
305只需两个文件,演示 `footer_html` 注入、`{config:key}` 引用与注入脚本中的原生 JS:
306
307`plugin.json`:
308
309```json
310{
311 "name": "每日一言",
312 "version": "1.0.0",
313 "author": "yourname",
314 "description": "在每个前台页面底部随机展示一句语录,语录可在后台配置",
315 "config": [
316 { "key": "quotes", "label": "语录列表", "type": "textarea",
317 "default": "今天也要加油鸭\n慢慢来,比较快\n你笑起来真好看",
318 "help": "每行一句,访客每次刷新随机显示其中一条" }
319 ],
320 "hooks": [
321 { "hook": "footer_html", "type": "html", "html": "@quote.html" }
322 ]
323}
324```
325
326`quote.html`(与 plugin.json 同级,避免在 JSON 里转义大段内容,见 4.2 节):
327
328```html
329<script type="text/plain" id="clv-quotes">{config:quotes}</script>
330<div id="clv-quote" style="text-align:center;padding:10px;font-size:13px;opacity:.75"></div>
331<script>
332(function () {
333 var raw = document.getElementById("clv-quotes").textContent;
334 var list = raw.split(/\r?\n/).map(function (s) { return s.trim(); }).filter(Boolean);
335 var el = document.getElementById("clv-quote");
336 if (el && list.length) el.textContent = list[Math.floor(Math.random() * list.length)];
337})();
338</script>
339```
340
341说明:
342
343- `{config:quotes}` 在**服务端**就替换为管理员填写的文本(`@文件` 片段同样支持占位符展开)
344- 多行文本放进 `type="text/plain"` 的脚本块而不是 JS 字符串字面量——字符串里出现真实换行会导致语法错误,这是注入多行配置的推荐写法
345- 注入内容原样输出到页面,可以写 `<style>` / `<script>`;请只注入可信内容
346- 打包上传 → 启用 → 刷新前台首页,页脚出现随机语录;后台「插件设置」改语录即时生效
347
348---
349
350## 7. 能力边界:什么时候该换应用型插件
351
352声明式插件的钩子是**固定白名单**,它只能"往已有位置注入内容",不能新增功能载体:
353
354| 你想做 | A 型能做吗 | 应该用什么 |
355|---|---|---|
356| 往页面注入 HTML/JS/CSS | ✅ | `html` 钩子 |
357| 正则替换敏感词 | ✅ | `content_filter` |
358| 事件推送到外部系统 | ✅ | `http` 钩子 |
359| 给昵称加标识 / 要求验证身份 | ✅ | `guard` / `badge` |
360| 新增一个页面(如 /x/signin/) | ❌ | B 型 `clv.route` |
361| 存自己的数据(如表、记录) | ❌ | B 型 `clv.table` |
362| 后台管理菜单 / 管理页 | ❌ | B 型 `clv.adminMenu` / `clv.adminPage` |
363| 定时任务 | ❌ | B 型 `clv.job` |
364| 拦截 / 改写请求与响应 | ❌ | B 型 `clv.middleware` / `clv.filter` |
365| 读取或写入站点数据 | ❌ | B 型 `clv.post` / `clv.setting` 等 |
366
367### 从 A 型迁移到 B 型
368
369两种形态可以平滑过渡,常见对应关系:
370
371| A 型 | B 型等价做法 |
372|---|---|
373| `{ "hook": "footer_html", "html": "..." }` | `clv.slot("footer_html", fn)` 返回同一段 HTML(可动态生成) |
374| `{ "hook": "content_filter", "match": ..., "replace": ... }` | `clv.filter("filter.content", fn)` 用 JS 做更复杂的改写 |
375| `{ "hook": "post_created", "type": "http", "url": ... }` | `clv.on("post_created", fn)` 内用 `clv.http.request`(可加签名、重试、条件判断) |
376| `{ "hook": "compose_guard", ... }` | `clv.filter("filter.compose.guard", fn)` 动态决定是否需要验证 |
377| `{config:key}` 占位符 | `clv.plugin.config("key")` |
378| 配置项声明 `config` | 完全相同,照搬即可 |
379
380迁移不必推倒重来:在 `plugin.json` 里加 `"kind": "app"`、`runtime` 与 `permissions`,
381保留原有 `hooks` 数组,再补 `main.js` —— 内核会同时处理两者(参考 `plugins/ai-polish/`)。
382
383---
384
385## 8. 调试建议
386
387| 场景 | 建议 |
388|---|---|
389| **插件装了但完全没反应** | ① 是否已在「插件管理」点**启用**(安装 ≠ 启用);② 后台列表里插件名下是否出现 ⚠ —— 有提示说明正在运行的内核版本过旧,不认识插件的钩子类型,**升级二进制即可**;③ 浏览器打开页面查看源码,确认注入片段是否存在 |
390| 改了 plugin.json 不生效 | A 型清单有 5 秒短缓存;`content_filter` 规则有 30 秒缓存 |
391| 配置改了没生效 | 配置存在 `settings` 表并带内存缓存,保存后立即刷新,无需重启 |
392| 钩子没触发 | 确认插件已启用;`guard` / `badge` 依赖昵称精确匹配(忽略大小写) |
393| 前台注入的 JS 报错 | 打开浏览器控制台查看;注入内容原样输出,注意转义引号 |
394| 保存 plugin.json 后插件不出现 | 确认文件为 **UTF-8 无 BOM**,可用 `jq . plugin.json` 验证 |
395| 想看当前插件与配置 | 后台「插件管理」页可查看全部插件、状态、兼容性提示与配置表单 |
396
397> 兼容性原则:插件使用了当前内核不支持的钩子类型时,**该钩子会被跳过而不是报错**,
398> 因此请务必留意后台插件列表中的 ⚠ 兼容性提示。