文档索引

博客评论区机器人接入说明

面向站外开发者。读完本文即可实现一个评论区机器人,无需阅读本站源码。

本文是接口契约的唯一对外口径。字段、限频数值、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。

0. 一句话说清

没有专用的「机器人接口」。 机器人就是一个普通的登录账号, 走和对人完全一样的 HTTP 接口。你需要的只有三件事:

  1. 一个 core+ 账号(§2)—— 注册出来是 user,需要提权一次
  2. 读评论(§6)—— 两条路都要登录,且同一档 core+
  3. 一个 POST 发评论(§7)

无需申请 token、无需白名单、无需站长开任何开关。

⚠️ 但有一件事本文档必须提前说清:评论区没有推送。 讨论那边有 SSE 长连接, 评论区没有 SSE、没有 webhook —— 想知道「有没有新评论」只能轮询(§6)。 这是评论区与讨论最大的差异,请先按轮询设计你的机器人。


1. 能力边界(先读这段)

范围 能读 能发 说明
某篇文章的评论 ✅ ✅ 读与发都需 core+ 且登录
全站最近评论 ✅ ❌ 只读接口,见 §6.2
其他用户的评论 ✅ ✅ 评论是公开内容,任何人都能回复任何人
别的用户的账号 ⚠️ ❌ 只有公开资料 GET /api/users/:id(免登录、按查看者收敛),见 docs/bot/account-bot.md §6

评论区没有成员表、没有可见性限制 —— 评论挂在文章下,所有读者看到的内容完全一致。 机器人的权限边界是三条:core+ 角色、登录状态、是否被禁言。 (角色要求与讨论一致,见 §2.2。)

⚠️ 读接口同样要求 core+。 本站没有只为读开放的豁免 —— 机器人模型统一是 「一个 core+ 账号 + 会话 cookie」,见 §2 与 §3。

ℹ️ 文章可以对外公开,评论域不受影响。 作者能逐篇把文章设为对外可见,那种情况下 站外读者只看得到正文,看不到评论区(也不显示评论数)。但评论接口本身的档位 一个字都没变:仍然是 core+ 且登录 —— 站内成员照常能读能发一篇对外文章的评论。 换句话说,「对外」是视图的属性,不是评论接口的属性。

⚠️ 机器人享受不到任何豁免:它同样会被禁言(被禁言期间发评论返回 403 您已被禁言,无法发表评论),也同样受每日限额约束(§9)。 这是刻意设计——出问题时站长能像处理普通用户一样处理它。

⚠️ 而且机器人发的评论会真的给别人发通知(§8)—— 你 @ 不到人,但你的 回复会出现在对方的通知列表里。这是一个会对真人产生后果的接口,见 §11。


2. 准备账号

2.1 注册

POST /api/auth/register
Content-Type: application/json

{
  "username": "mycommentbot",
  "email": "bot@example.com",
  "password": "********",
  "invite_code": "可选,填有效邀请码则直接升为 core",
  "turnstileToken": "可选,站点启用 Turnstile 时必填"
}

校验规则:

字段 规则
username 3–20 字符;仅允许字母 / 数字 / _ / -;不能以 - 或 _ 开头或结尾
email 常规邮箱格式,≤100 字符
password ≤100 字符

成功即自动登录(响应会 Set-Cookie,见 §3)。

📌 注册需由人工完成一次,这是预期流程。 机器人账号不开放脚本自助注册 (数量有限)。此外,站点若启用了人机验证(Turnstile),脚本同样无法自行注册。

做法:由人工在浏览器里建号一次,之后机器人用账号密码登录即可 —— 后续所有交互都不再涉及人机验证。

2.2 提权到 core+

注册出来是 user,而评论区的规则是「只有核心用户可以发表评论」 —— 与讨论一致。两条路:

  • 注册时带有效邀请码 → 直接是 core(推荐,全自动);
  • 让站长在服务器上执行:npm run cli -- promote-core mycommentbot

这两条路与讨论机器人完全相同(docs/bot/chat-bot.md §2.2)。


3. 鉴权

本站没有 Bearer token,会话是一个 HttpOnly Cookie。流程:

POST /api/auth/login
Content-Type: application/json

{ "username": "mycommentbot", "password": "********" }

响应头:

Set-Cookie: raricy_session=<JWT>; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000
属性 值
Cookie 名 raricy_session
有效期 30 天
HttpOnly 是
SameSite Lax
Path /
Secure 站点走 HTTPS 时为是

之后每个请求带上 Cookie: raricy_session=<JWT> 即可。

💡 读评论也要这个 Cookie:§6 那两条路都是 core+ 档。本站的机器人模型一直是 「一个 core+ 账号 + 会话 cookie」,没有例外的只读口 —— 发评论(§7)与看通知(§8) 同样要登录。所以哪怕只读不写,也得先备好一个 core+ 账号(§2.2)。

3.1 三个必须处理的点

  1. Cookie 会过期(30 天)。收到 401 就重新登录一次。
  2. 会话会被主动作废:改密码、被强制下线、被禁言 / 重置密码都会让旧 Cookie 立刻失效(服务端有 session_version 快照机制)。表现同样是 401。 ⚠️ 被降权(core → user)不属于这一类 —— 会话不会失效,只是评论接口 改回 403 需要核心用户权限。别把它当会话失效去重登,重登也还是 403。
  3. CSRF 校验对你无影响。 本站对写请求校验 Origin / Referer 同源, 但两者都缺失时放行。非浏览器客户端本来就不发这两个头 → 天然通过。 ⚠️ 所以不要自作主张伪造一个 Origin,写错反而会被判定为跨源而 403。

3.2 会话是否仍有效

GET /api/auth/me

⚠️ 这个接口永远返回 200 —— 会话缺失 / 失效时返回 { "code": 200, "message": "ok", "user": null },不会回 401。 判据是 user 是不是 null,不是状态码。适合发评论前探活,但请按 user 判。


4. 响应信封与错误码

站内接口(/api/...)共用同一信封:

{ "code": 200, "message": "ok", ...其他数据字段 }

出错时 code 与 HTTP 状态码一致,且只有 code 和 message:

{ "code": 403, "message": "您已被禁言,无法发表评论" }

⚠️ 例外:/api/spider/* 系列的成功响应不走信封,直接返回裸数组 / 裸对象(§6.2)。 但出错时仍走 { code, message }(401 / 403 / 404 都是)。所以别对 spider 一律 按裸数据解析 —— 先看 HTTP 状态码,非 2xx 就按信封读。

状态码 含义 常见 message
400 参数错误 评论内容不能为空 / 评论内容不能超过5000字 / 父评论不存在或已删除
401 未登录 / 会话失效 请先登录 / 用户名或密码错误
403 被禁言 / 角色不足 / 跨源 您已被禁言,无法发表评论 / 需要核心用户权限 / 跨源请求被拒绝 (CSRF)
404 文章不存在 文章不存在
429 触发限频 今日评论已达上限(8000条),请明日再试

⚠️ 站内接口一律 Cache-Control: no-store,别在客户端缓存这些响应。 spider 系列中较新的接口(如 GET /api/spider/favorites/:id)也带这个头; 评论 / 博客那几条早期接口没有,但它们在服务端也不做缓存 —— 轮询时请自行控制频率(§9)。


5. 时间戳格式(⚠️ 本站最容易踩的坑)

评论的 created_at 是 ISO 8601 字符串,带 Z 后缀:

"2026-09-11T20:31:05.000Z"

但那个 Z 是假的。 本站全库时间戳的语义是「UTC+8 墙上时间,贴 Z 标签」 (历史遗留,见 docs/architecture.md)。上面这个例子表示的是北京时间 2026-09-11 20:31:05,而不是 UTC 20:31:05。

正确处理

// ✅ 想原样展示给用户 —— 直接截断,得到的就是北京时间墙上时间
const wall = created_at.slice(0, 19).replace('T', ' '); // "2026-09-11 20:31:05"

// ✅ 想得到真实瞬间(做时间差、跨时区换算)
const realMs = new Date(created_at).getTime() - 8 * 3600 * 1000;
// ❌ 错误 —— 会整整差 8 小时
new Date(created_at).toLocaleString('zh-CN')
// "2026/9/12 04:31:05" ← 凭空多出 8 小时

简单记法:把字符串里的 Z 抹掉,当成 UTC+8 的墙上时间读,就对了。 时区无关的用法(比较两条评论谁更新)不受影响 —— 因为大家偏移量一致。


6. 读评论(两条路,都需要 core+ 账号)

6.1 按文章读 —— 完整的评论树

GET /api/blogs/:id/comments

:id 是文章的 UUID。需要 core+ 账号 + 会话 Cookie(建号提权见 §2, 登录换 cookie 见 §3)。未登录 401,非 core 403。

响应:

{ "code": 200, "message": "ok", "comments": [ /* CommentNode[],见 §10 */ ] }

特点:

  • 返回该文章下全部已批准评论,一次给完,没有分页;
  • 嵌套结构:comments 是顶层评论数组,回复在各自的 children 里;
  • 每条都带 content(Markdown 原文)—— 这是机器人要读的字段;
  • 已删除且没有回复的评论会被隐藏;有回复的保留,但正文被替换为 [该评论已删除];
  • 没有限频。

💡 轮询姿势:这个接口没有增量参数。请自行记住上次见到的评论 id 集合 (或最大的 created_at),每次拉回来做差集。

⚠️ 评论 id 是 UUID,不是自增数字 —— 不能当游标用。 这一点与讨论不同 (讨论的消息 id 是全局自增的)。你没法说「给我 id 大于 X 的评论」, 只能拉全量再自己比对。

6.2 读全站最近评论 —— 适合「发现新评论」

GET /api/spider/comments

需 core+ 登录,裸数组,不包信封(未登录 401,非 core 403):

[
  { "id": "…", "blog_id": "…", "author": {…}, "content_html": "…", "created_at": "…" }
]
特点 说明
条数 最近 100 条,固定
排序 按 created_at 倒序(最新在前)
结构 扁平数组,children 恒为 []
字段 只有公共字段(§10.1)。⚠️ 不含 content 原文,只有 content_html

⚠️ 两个坑:

  1. 只有 100 条上限。 如果你的轮询间隔里全站新增超过 100 条评论, 中间那些就永久错过了。轮询间隔请留足余量。
  2. content_html 不是 Markdown 渲染结果,是「HTML 转义 + 换行转 <br>」 的纯文本。想拿 Markdown 原文,请走 §6.1。

6.3 读单条评论 / 单篇文章

GET /api/spider/comments/:id     → 裸对象:**§10.1 公共字段 + 恒为空的 `children`**;
                                   ⚠️ **不含** §10.2 的 `content` / `image` / `blog` / `liked`;
                                   不存在或已删除 → 404 {code,message}
GET /api/spider/blogs/:id        → { "meta": {…}, "content": "Markdown 正文" };不存在 → 404
GET /api/spider/favorites/:id    → { "id","title","author","count","blogs" };不存在 → 404 {code,message}

这三条都需 core+ 登录(未登录 401,非 core 403)—— 与 §6.1 / §6.2 同一口径。

收藏夹那条的完整口径(含限频)见 docs/bot/favorite-bot.md。此处只点一句: 它只读公开收藏夹,且是 spider 系列里唯一有限频的一条。

meta 里有 title / author / category / comments_count 等,方便机器人在 回复时引用文章标题。


7. 发评论 / 回复

POST /api/blogs/:id/comments
Cookie: raricy_session=<JWT>
Content-Type: application/json

{ "content": "说点什么" }

7.1 请求字段

字段 类型 必填 说明
content string 否* 正文,Markdown 源文(站内客户端渲染)
parent_id string 否 回复某条评论,用被回复评论的 UUID。须同一篇文章且未被删除
image_id string 否 引用图床图片,必须是你自己上传的
quote_blog_id string 否 引用一篇文章的 UUID

* 至少要有一个有效载荷(正文或附件),否则报 评论内容不能为空。

长度上限 值
纯文本评论 content 5000 字
带附件(image_id / quote_blog_id)时的 content 5000 字

两档同值(2026-09 起)—— 此前带附件档是 500 字(「带附件时正文本来就是个图注」)。 服务端仍分两条错误码(tooLong / captionTooLong),所以两档都按 5000 记即可。 长度按 trim() 之后计算,恰好 5000 字放行。

不带 parent_id = 顶层评论(会通知文章作者); 带 parent_id = 回复(会通知被回复者)。详见 §8。

7.2 成功响应

{ "code": 200, "message": "评论成功", "comment": { /* CommentNode,见 §10 */ } }

✅ 这里 message 是字符串("评论成功"),新评论对象在 comment 字段里 —— 与讨论机器人的发送接口不同(那个接口的 message 被消息对象覆盖了)。 判断成功仍建议只看 code === 200。

评论创建即 approved,立即可见,没有审核队列。

内容引用 [@<内容ID>]

正文里可以写 [@<内容ID>],服务端原样入库,由渲染端按 ID 长度展开:

写法 长度 效果
[@a1b2c3d4] 8 位 内联云剪贴板的正文(该剪贴板须公开,或你是作者)
[@AbCdEf1234] 10 位 内联图床图片 —— 渲染成 /api/images/<ID>/raw 的图
[@音频/AbCdEf1234] —— 内联音频 —— 渲染成 /api/audio/<ID>/raw 的播放器(具名命名空间,不按长度分流)。一条正文最多展开 3 个,见 docs/bot/audio-bot.md §6.3
[@vOtE12345] 9 位 投票 —— 评论里不展开,原样显示
[@123456] 6 位数字 收藏夹 —— 评论里不展开,原样显示(只有博客正文会渲染成卡片)

要点:

  • 一条评论最多展开 1 条云剪贴板,多写的原样留在正文里
  • 图床图片最多 50 张;剪贴板正文超过 2000 字会截断并附回原剪贴板的链接
  • 写在代码块 / 行内代码里的 [@id] 不展开
  • 剪贴板正文是另一名用户写的,与正文走同一条净化管线
  • ⚠️ 评论区本身就要 core+(§1 / §6.1):未登录访客与非 core 读者根本看不到评论区 (页面的访客视图不渲染它)。所以 [剪贴板 <ID> 加载失败] 只会出现在 core+ 读者读到一条别人的私有剪贴板时 —— 它不是评论区的常态

正文里不能手写 <img>(白名单里没有,写了会以转义文本显示)。要贴图用上面的 [@<10位图片ID>],或者用 image_id 字段发图片附件。

📌 用户名片([@用户/<用户名>],渲染成头像 + 用户名的行内名片,点进主页) 与表情一样不在上面的表里 —— 它认的是名字而不是 ID。一条评论最多 5 张, 不算 @ 提及(不发通知),取数与权限见 docs/bot/account-bot.md §6。

7.3 会被拒绝的情形

情形 返回
未登录 / Cookie 失效 401 请先登录
账号不是 core+(见 §2.2) 403 需要核心用户权限
账号被禁言 403 您已被禁言,无法发表评论
文章不存在或已隐藏 404 文章不存在
正文为空且无附件 400 评论内容不能为空
正文超长 400 评论内容不能超过5000字 / …不能超过5000字
parent_id 指向的评论不存在 / 不同文章 / 已删除 400 父评论不存在或已删除
image_id 不存在、已删、或不属于你 400 图片不存在或不属于你,请重新上传
quote_blog_id 不存在或已删 400 引用的博客不存在或已删除
触发每日限额 429 今日评论已达上限(8000条),请明日再试

8. 怎么知道「有人回我了」

评论区没有 @ 提及机制(这点与讨论不同 —— 讨论有 @ 提及且会进通知列表)。 评论区唯一的通知来源是评论之间的回复关系:

你发的评论 谁收到通知
顶层评论(无 parent_id) 文章作者(action 文章评论)
回复某条评论(有 parent_id) 被回复评论的作者(action 评论回复)
—— 自己评自己不发通知

所以机器人想知道「有没有人回复我」,只有一条路:登录后轮询通知列表。

GET /api/notifications?page=1&unread_only=true
Cookie: raricy_session=<JWT>

响应:

{ "code": 200, "message": "ok", "unreadCount": 3,
  "notifications": [ /* 见下 */ ],
  "total": 12, "page": 1, "perPage": 20, "pages": 1,
  "hasPrev": false, "hasNext": false }

notifications 条目:

{
  "id": "…",
  "timestamp": "2026-09-11T20:31:05.000Z",   // ⚠️ 同 §5 的假 Z
  "action": "评论回复",                       // 或 "文章评论"
  "recipientId": "u_me",
  "actor": { "id": "u_xxx", "username": "alice" },  // 触发者,**这就是要回复的对象**
  "object": { "type": "blog", "id": "<文章 id>" },  // ⚠️ 嵌套对象,不是两个平铺字段
  "detail": "你的评论在《标题》下收到了回复",
  "read": false
}
字段 说明
action "评论回复"(有人回了你的评论)/ "文章评论"(你的文章被评论)
actor 触发者 {id, username}。⚠️ 没有头像字段;actor 已注销时为 {id: null, username: "system"}
object.type "blog"
object.id ⚠️ 是文章 id(blogId),不是评论 id
read 是否已读

⚠️ 通知里拿不到「是哪条评论回复了我」。 object.id 只给到文章层面。 所以标准做法是:

  1. 轮询通知,发现 action === "评论回复" 且 read === false;
  2. 用 object.id 去拉 GET /api/blogs/<object.id>/comments(§6.1);
  3. 在树里找 author.id === actor.id、且 parent_id 指向自己发的某条评论 的最新一条;
  4. 回复它(带上它的 id 作为 parent_id);
  5. POST /api/notifications/<通知 id>/read 标记已读,避免下轮重复处理。

💡 unread_only=true + 只处理 action === "评论回复" 是最省事的过滤方式 —— 顺便也把「文章评论」的通知排除掉了(那是给文章作者的,不是你被回复)。


9. 限频

规则 额度 适用
commentDaily 8000 次 / 24 小时 发评论(按用户计)
读评论(§6.1 / §6.2,以及 §6.3 的评论与博客那两条) 无限频 随便轮询
读公开收藏夹(§6.3 第三条) 120 次/分 / IP 该系列的例外,见 docs/bot/favorite-bot.md §4
登录 15 分钟内失败 300 次 / IP、100 次 / 用户名 仅统计失败
图片上传(§7.1 引用图时) 200 张 / 小时 POST /api/images(一次多张按张数计)
音频上传(引用音频时) 200 次 / 小时 POST /api/audio(一次一个文件,见 docs/bot/audio-bot.md §8)。与图片是两个独立的桶

超限一律 429。

⚠️ 发评论没有分钟级限额,只有每天 8000 条。 这意味着一台失控的机器人 可以在 1 分钟内刷 8000 条评论,而不是被平滑地限速。请务必在机器人侧 自己做节流(例如「同一篇文章 N 分钟内最多回 1 条」)。

📌 建议只回应「回复了你」的评论(§8 那条路径),而不是「看到新评论就回」 —— 后者在一篇热文下会瞬间刷屏,且很可能触怒站长与读者。

限频计数存在服务端(进程内计数桶,定期落盘快照),重启不会清零 —— 别指望靠重启洗掉自己的用量。但它也不是持久化账本,不要拿它做用量统计。


10. 数据结构

10.1 公共字段(站内与 spider 接口都有)

{
  "id": "9f1c…",                  // UUID,⚠️ 不是自增数字,不能当游标
  "blog_id": "3a7e…",
  "author": {
    "id": "u_xxx",                // 可能为 null(作者已被删除)
    "username": "alice",          // 可能为 null
    "is_admin": false,
    "avatar_url": "/api/avatar/u_xxx"  // 可能为 null
  },
  "parent_id": null,              // 顶层评论为 null
  "root_id": null,                // 所属楼中楼的根评论 id
  "content_html": "纯文本&lt;br&gt;已转义",  // 转义+<br>,非 Markdown 渲染
  "status": "approved",
  "is_deleted": false,
  "likes_count": 3,
  "created_at": "2026-09-11T20:31:05.000Z",  // ⚠️ 语义见 §5
  "updated_at": "2026-09-11T20:31:05.000Z"
}

10.2 CommentNode(站内接口额外字段)

在 §10.1 基础上多出:

{
  "content": "**Markdown** 原文",   // ← 机器人要读的就是这个
  "image": {                        // 引用的图床图片,无则 null
    "id": "img_xxx",
    "url": "https://…",
    "mime_type": "image/png"
  },
  "image_missing": false,           // 引用了图但图已不存在
  "blog": {                         // 引用的博客,无则 null
    "id": "uuid", "title": "标题", "description": "…",
    "author": "alice",              // 作者 username,可能为 null
    "updated_at": "2026-09-11T20:31:05.000Z"
  },
  "blog_missing": false,
  "liked": false,                   // 随「查看者」而变;未登录恒为 false
  "children": [ /* CommentNode[],回复 */ ]
}

⚠️ is_deleted: true 的评论,content 与 content_html 都已被替换为 [该评论已删除] —— 原文不会下发。请不要把占位符当正文去回复。


11. 礼仪与约定

技术之外,三点请配合 —— 评论区的后果比讨论更重,因为它会打扰到真人:

  1. 让机器人的身份可辨认。 建议用户名带 bot 后缀(如 mycommentbot), 或在其个人简介里写明「这是机器人」。否则读者会以为在跟人对话。
  2. ⚠️ 你的每一条评论都会给真人发通知。 顶层评论通知文章作者,回复通知被回复者。 一个「见评论就回」的机器人等于给全站作者群发通知 —— 这是骚扰,且会被禁言。 请只回应明确指向你的那类评论(§8)。
  3. 别把评论内容外送而不告知。 如果你打算把评论正文发给第三方服务 (AI 模型、日志平台等),请先和站长沟通,并在机器人简介里说明用途 —— 评论作者并没有同意自己的内容被送往站外。

12. 排错速查

现象 原因
读评论也 401 请先登录 读接口同样要登录(§6 标题)。没带 Cookie 或会话已过期 —— 重新登录(§3)
读评论 403 需要核心用户权限 账号不是 core+。注册出来是 user,需提权一次(§2.2)
发评论 401 请先登录 没带 Cookie,或会话已过期 —— 重新登录(§3)
所有写请求 403 跨源请求被拒绝 (CSRF) 你手动设置了错误的 Origin。非浏览器客户端直接不带该头即可(§3.1)
发评论 403 您已被禁言… 账号被禁言,等解禁或联系站长
429 触发限频,见 §9;注意 8000/天 的硬上限
时间显示差 8 小时 created_at 的 Z 是假标签,见 §5
拿不到评论的 Markdown 原文 你用的是 /api/spider/comments(只有 content_html),改用 §6.1
轮询漏掉评论 /api/spider/comments 只给最近 100 条,间隔太大就会错过(§6.2)
「给我 id 大于 X 的评论」拿不到 评论 id 是 UUID,没有游标语义(§6.1)
通知里找不到「是哪条评论」 通知只给到 blogId,需回查评论树(§8)
回复时报 父评论不存在或已删除 parent_id 那条评论被删了,或不属于这篇文章

13. 最小可用流程(轮询版)

准备:人工注册一个账号(§2.1)并提权到 core+(§2.2)

循环(每 N 分钟):
1. POST /api/auth/login                        → 存下 raricy_session
2. GET  /api/notifications?unread_only=true    → 筛 action === "评论回复"
3. 对每条:
   a. GET /api/blogs/<object.id>/comments      → 拉评论树(需 core+ 会话,见 §6.1)
   b. 在树里找:author.id === actor.id
                且 parent_id 指向我自己发的某条评论
                且不在「我回复过的」集合里
   c. POST /api/blogs/<object.id>/comments
      { "content": "...", "parent_id": "<那条评论的 id>" }
   d. POST /api/notifications/<通知 id>/read    → 标记已读,避免下轮重复
   e. 自行节流:同一篇文章 N 分钟内最多回 1 条

断线 / 401 → 重新登录(§3)。
429 → 退避;注意 8000 条/天 是硬上限。

💡 如果你的机器人只读不写(例如做评论分析、摘要、归档),动作确实更简单 —— 但它仍然需要一个 core+ 账号:§6.1 与 §6.2 的读接口与写接口同一档。 本站没有「匿名只读」的豁免,机器人模型统一是「一个 core+ 账号 + 会话 cookie」。

本页内容来自仓库 docs/bot/comment-bot.md·在 GitHub 上查看