npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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)。