@ricenote/plugin-comments
v1.5.0
Published
RiceNote 评论插件:嵌套回复、四态审核与反垃圾
Readme
rice.comments —— 评论
访客评论的完整实现:嵌套回复、四态审核状态机、关键词黑名单与提交频率限制,
并通过 post.comments 插槽把评论线程注入文章页。
清单摘要
| 项 | 值 |
| --- | --- |
| id | rice.comments |
| version | 1.5.0 |
| apiVersion | ^1.0.0 |
| entry | ./dist/index.js |
| provides | content.comments@1 |
| requires | 无(content.posts@1 为软依赖) |
| capabilities | database:own、services:provide、services:consume、routes:public、routes:admin、events:publish、slots:register |
| migrations | 0001_comments.sql |
| 数据表 | rn_comments |
扩展点(Hook)
原始路由(公开)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | /comments/submit | 访客提交评论(受频率限制与关键词黑名单约束) |
| GET | /api/comments | 按 postId 读取已通过评论(线程化,含作者邮箱) |
用原始路由而非页面路由:评论是站内局部交互(表单提交与数据读取), 不产生独立页面,也不需要主题渲染。
管理 API(权限点 comment.moderate)
| 方法 | 路径 |
| --- | --- |
| GET | /api/admin/comments |
| GET | /api/admin/comments/counts |
| POST | /api/admin/comments/moderate |
插槽
| 插槽 | 组件名 | 说明 |
| --- | --- | --- |
| post.comments | comments.thread | 输出评论列表 + 提交表单 |
主题侧写法(两套官方主题均已接入):
{% if has_feature('comments') %}
<section class="rn-comments">
<h2>评论</h2>
{{ slot('post.comments', { postId: post.id }) }}
</section>
{% endif %}服务契约
提供 content.comments@1:listApprovedThreaded / submit / listForAdmin / moderate /
countByStatus / countsForPosts / recent。
消费:
| 契约 | 必需性 | 用途 |
| --- | --- | --- |
| core.html@1、core.theme@1 | 必需 | 评论正文消毒、缓存失效 |
| content.posts@1 | 可选 | 后台列表显示所属文章标题(titlesOf) |
| identity.users@1 | 可选 | 关联已登录用户 |
事件
本插件只发布、不订阅(能力 events:publish)。
| 事件 | 何时广播 |
| --- | --- |
| content.comment.created.v1 | 评论入库后(含待审与垃圾;订阅方按 status 自行取舍) |
| content.comment.status.changed.v1 | 审核动作真正改变状态时(from ≠ status;delete 不广播) |
payload(两条事件共用的「评论快照」):
| 字段 | 说明 |
| --- | --- |
| id / postId | 评论 ID 与所属文章 ID |
| postTitle / postUrl | 文章标题与前台地址(取不到时缺省) |
| parentId | 被回复的评论 ID;非回复为 null |
| parentAuthorName / parentAuthorEmail | 仅回复时存在:「通知被回复的人」用它 —— 订阅方既拿不到也反查不了 rn_comments |
| authorName / authorEmail | 评论作者;邮箱已归一化为小写(站长允许留空时为空字符串) |
| content | 正文原文,未消毒 —— 要展示请自行转义 |
| status | 广播时的状态:pending / approved / spam / trash |
| from | 仅 status.changed 携带:变更前的状态 |
事件总线不保证投递(内存实现,进程重启即丢),适合"有新评论就立刻通知"这类场景; 需要至少一次投递的订阅方,应另用公开契约(
recent()/listForAdmin())做一次对账回捞。 通知类订阅方还应自行过滤status === 'spam'(垃圾评论也广播,因为它是一个事实)。
后台界面
界面文件位于 admin/main.js:零构建纯 ESM(不安装依赖、不打包,模板即字符串),
组件(AdminPage/Card/Button/Badge/Dialog/Input/Icon…)已由宿主全局注册,
依赖只来自 /admin-sdk/v1/ricenote-admin.js。
菜单与入口由 manifest.json 的 admin.pages 声明式提供,Core 负责注册菜单、
托管界面文件并计算前端入口 URL(无需插件再调用 ctx.adminMenu.add):
{ id: 'comments.moderate', label: '评论审核', path: '/comments', icon: 'MessageSquare', order: 20, permission: 'comment.moderate' }
- 入口 URL:
/plugin-admin/rice.comments/main.js - 访问权限:页面及列表 / 计数 / 审核操作均需
comment.moderate
审核状态机
pending(待审) ──approve──▶ approved(通过)
▲ │
│ ├─spam──▶ spam(垃圾)
└──────── pending ◀────────┤
└─trash─▶ trash(回收)后台批量操作支持 approve / pending / spam / trash / delete。
设置项(settingsSchema,后台自动生成表单)
后台「扩展中心 → 评论 → 设置」。保存后需重启服务生效(ctx.config 是插件启动期快照)。
分组「通用」
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| enabled | boolean | true | 是否开放评论区(关闭后插槽不输出任何内容) |
| requireApproval | boolean | true | 新评论是否进入待审 |
| requireEmail | boolean | true | 是否要求访客填写邮箱(关闭后可以留空) |
| pageSize | number(1–100) | 20 | 线程分页大小 |
| maxLength | number(1–20000) | 2000 | 正文长度上限 |
| maxDepth | number(1–10) | 5 | 嵌套回复最大深度 |
分组「反垃圾」
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| rateLimitWindowSeconds | number(0–86400) | 60 | 频率限制窗口(0 关闭该限制) |
| rateLimitMaxSubmissions | number(0–100) | 3 | 窗口内最多提交次数(0 关闭该限制) |
| maxLinksPerComment | number(0–100) | 2 | 单条评论允许的链接数(超限判为垃圾) |
| blockedKeywords | stringList(≤200 项) | [] | 关键词黑名单(命中即判为垃圾),表单里每行一项 |
把邮箱交给主题(头像等)
插件不代劳头像的渲染与获取:只把作者邮箱开放出来,主题自己决定怎么用。
- 评论项属性:
<li class="rn-comment" data-rn-author-email="[email protected]">(访客没填时该属性不输出); - 公开接口
GET /api/comments的条目同样带authorEmail字段(CommentThreadItem)。
主题脚本据此取头像时注意:Gravatar 需要邮箱的 md5,而浏览器 WebCrypto 不支持 md5, 主题需自带一份 md5 实现(或改用基于其它哈希的头像服务):
document.querySelectorAll('[data-rn-author-email]').forEach(function (item) {
var email = item.getAttribute('data-rn-author-email');
// 主题自行计算头像地址并挂到这条评论上
});⚠️ 邮箱是明文出现在前台 HTML 与公开接口里的,任何访客与爬虫都能读取; 不需要头像的主题也无法阻止这一输出。
安全要点
- 评论正文经
core.html@1消毒后才存库与输出,前台只输出TrustedHtml; - 作者邮箱会输出到前台(评论项属性与公开接口),供主题取头像;后台列表按需打码、 未填写时显示「未填写」,详见上节;
- 提交频率按 IP 哈希统计,不存储明文 IP;
- 评论插件不直接查询
rn_posts:文章标题通过[email protected]()批量获取, 避免 N+1 与跨插件联表。
依赖与启停
- 无硬依赖,可独立启停;
- 禁用后
post.comments插槽为空片段,主题无需改动(has_feature('comments')会返回 false)。
