博客发文机器人接入说明
面向站外开发者。读完本文即可实现一个发文机器人,无需阅读本站源码。
本文是发文接口的唯一对外口径。字段、长度上限、限频数值、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。
0. 一句话说清
没有专用的「机器人接口」。 机器人就是一个普通的登录账号, 走和对人完全一样的 HTTP 接口。你需要的只有四件事:
- 一个 core+ 账号(§2)—— 注册出来是
user,需要提权一次 - 拉一次栏目清单(§6)—— 拿到
category_id - 一个 POST 发文章(§7)
- 想改就 PUT(§8);只想改「对不对外」用 PATCH(§8.1)
无需申请 token、无需白名单、无需站长开任何开关。
⚠️ 发文接口的请求体是「键集封闭」的:多传一个它不认识的键就返回
400, 不会静默忽略。这条规则是本文 §6.2 那个坑逼出来的,请先读那一段再动手。
⚠️ 文章默认「仅站内可见」(
internal)。 想让站外的人(未登录访客)也读得到, 得显式传visibility(§7.1)—— 这个接口不是「发出去就等于公开」。 别把「发出去了」当成「公开了」:默认档下访客打开链接看到的是登录页。
1. 能力边界(先读这段)
| 范围 | 能读 | 能写 | 说明 |
|---|---|---|---|
| 发新文章 | —— | ✅ | POST /api/blogs,需 core+ 且未被禁言 |
| 改 / 删自己的文章 | —— | ✅ | 仅作者本人,别人改不了(§8) |
| 决定文章对不对外 | —— | ✅ | 发文 / 改文时传 visibility(三档,见 §7.1);默认 internal(仅站内) |
| 读任意文章(含正文) | ✅ | ❌ | 需 core+ 登录(§9) |
| 读栏目清单 | ✅ | ❌ | GET /api/categories,需 core+ 登录(§6) |
| 改 / 删别人的文章 | ✅ | ❌ | 那是管理员的路,机器人没有这条路 |
| 管理栏目(增删改) | ❌ | ❌ | 只有站长能建栏目,机器人只能选 |
机器人的权限边界是三条:core+ 角色、登录状态、是否被禁言。
⚠️ 机器人享受不到任何豁免:它同样会被禁言(被禁言期间发文返回
403),也同样受每日 20 篇的限额约束(§10)。这是刻意设计—— 出问题时站长能像处理普通用户一样处理它。
⚠️ 而且机器人发的文章会产生真实后果:创建即入库、立即可见(没有审核队列, 也没有草稿态),还可能给全站管理员发通知(§11)。这是一个会对真人产生后果的接口。
⚠️ 「可见」是两个层次,别混为一谈: · 站内可见 —— core+ 用户都能读到。这是发文的下限,任何文章都是这样。 · 对外可读 —— 未登录访客也能打开(
link/public),顺带决定进不进 sitemap、允不允许搜索引擎索引(§7.1 的表)。 新文章默认internal:站内可见、对外打不开。
2. 准备账号
发文要求 core+ 角色(core / admin / owner 都行)。注册出来的账号是
user,需要站长提权一次 —— 注册、登录、提权的完整步骤与错误码见
comment-bot.md §2,三份机器人文档说的是同一件事,不重复抄。
📌 文档之间只在这一处互相引用:账号准备是三个机器人共用的前置步骤, 抄三份迟早会漂。其余部分本文都是自包含的。
3. 鉴权
除注册 / 登录外,每个请求都要带上会话 cookie:
POST /api/blogs
Cookie: raricy_session=<JWT>
Content-Type: application/json
raricy_session 是登录接口 Set-Cookie 回给你的那个值,照着存、照着带即可。
⚠️ 写请求(POST / PUT / DELETE)会校验来源,跨站会被拦成
403 跨源请求被拒绝 (CSRF)。用任何 HTTP 客户端都没问题, 但别在浏览器页面里用fetch跨站调它。
会话可能被失效(改密、被强制下线、被禁言 / 重置密码)—— 此时所有接口一律回
401 请先登录。机器人应当把401当成「重新登录」的信号,而不是重试。⚠️ 被降权(core → user)不在其列:会话仍然有效,只是发文接口改回
403 只有核心用户才能发布文章。别当会话失效去重登 —— 重登也一样。
4. 响应信封与错误码
站内接口(/api/...)共用同一信封:
{ "code": 200, "message": "ok", ...其他数据字段 }
出错时 code 与 HTTP 状态码一致,且只有 code 和 message:
{ "code": 400, "message": "未知字段 \"category\",……" }
| 状态码 | 含义 | 常见 message |
|---|---|---|
400 |
参数错误 | 标题不能为空 / 未知字段 "tags",本接口只接受:… / 选择的栏目不存在 |
401 |
未登录 / 会话失效 | 请先登录 |
403 |
角色不足 / 被禁言 / 该栏目仅管理员 / 跨源 | 只有核心用户才能发布文章 / 该栏目仅允许管理员发布文章 |
404 |
文章不存在(或已被软删) | 文章不存在 |
429 |
触发限频 | 今日发布数量已达上限(20篇) |
⚠️ 发文接口的档位文案是
只有核心用户才能发布文章—— 与读接口的需要核心用户权限不是同一句。两处都照实收着,别只匹配其中一条。
⚠️ 站内接口大多带
Cache-Control: no-store(apiOk/apiErr统一加), 但 §9 那两条读口(GET /api/blogs、GET /api/blogs/:id)是裸Response.json, 不带这个头 —— 别把它们当可缓存的公共数据。
5. 时间戳格式
文章的时间戳是 ISO 8601 字符串带 Z 后缀:
"2026-09-11T20:31:05.000Z"
但那个 Z 是假的。 本站全库时间戳的语义是「UTC+8 墙上时间,贴 Z 标签」——
上面这个例子表示的是北京时间 20:31:05,而不是 UTC 20:31:05。
正确与错误的解析写法见 comment-bot.md §5,全站的坑是同一个。
6. 取栏目(⚠️ 发文前必须先做这一步)
6.1 栏目清单
GET /api/categories
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"categories": [
{
"id": 3,
"name": "技术",
"slug": "tech",
"icon": "💻",
"parent_id": null,
"path": "技术",
"admin_only_posting": false
},
{
"id": 7,
"name": "前端",
"slug": "frontend",
"icon": "",
"parent_id": 3,
"path": "技术 > 前端",
"admin_only_posting": false
}
]
}
| 字段 | 说明 |
|---|---|
id |
发文时要填进 category_id 的那个数 |
name |
栏目名(展示用) |
slug |
URL 用的短名,发文时不要用它(见 §6.2) |
icon |
emoji 图标,可能是空串 |
parent_id |
父栏目 id;顶层栏目为 null |
path |
完整路径,父 > 子;顶层就是栏目名本身 |
admin_only_posting |
true = 这个栏目只有管理员能发,机器人发过去必被 403 |
清单是摊平的(两层树摊成一张表),顺序即站点展示顺序。
✅ 停用(
isActive=false)的栏目不会出现在清单里 —— 所以你从这份清单里 挑出来的id,一定能过服务端那道「栏目存在且启用」的校验。✅
admin_only_posting给的是生效值:父栏目勾了「仅管理员可发」, 子栏目也会是true。照着它挑,就不会发出去才收一个403。
6.2 category_id 与 category 是两个东西(最容易踩)
这是本站最容易踩的一处字段名不对称:
| 场景 | 字段名 | 值 |
|---|---|---|
发文(POST /api/blogs) |
category_id |
栏目数字 id |
筛列表(GET /api/blogs) |
category |
栏目slug 字符串 |
把 category(或 categoryId、category_name 之类)传进发文接口,不会生效 ——
服务端不认识这个键。在 2026-09 之前它会被静默忽略:请求照样回
200 上传成功,文章却落进「未分类」,响应里看不出任何异常。
现在不会了:发文接口对未知字段直接 400,并且对栏目近义键会指路:
{
"code": 400,
"message": "未知字段 \"category\":栏目要传 category_id(栏目数字 ID,清单见 GET /api/categories)"
}
所以:
- 看到
未知字段 "…"→ 你键名写错了,改键名,别重试 - 栏目字段只认
category_id,值是 §6.1 里的id(数字) category_id也可以传数字字符串("3"),服务端按整数解析- 不传 / 传空串 / 传
null/ 传0= 未分类,这是合法的,不是错误
7. 发文章
POST /api/blogs
Cookie: raricy_session=<JWT>
Content-Type: application/json
{
"title": "标题",
"description": "一句话摘要",
"content": "# 正文\n\nMarkdown 源文",
"category_id": 7
}
7.1 请求字段
只有这 5 个键。多一个都会被 400 顶回来。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
title |
string | 是 | 标题,去空白后不能为空 |
description |
string | 是 | 摘要 / 简介,去空白后不能为空 |
content |
string | 是 | 正文,Markdown 源文,不 trim(原样入库) |
category_id |
number | string | 否 | 栏目数字 ID(§6);不传 = 未分类 |
visibility |
string | 否 | 对外可见性三档(见下);不传 = internal(仅站内) |
| 长度上限 | 值 | 计数口径 |
|---|---|---|
title |
30 字 | trim() 之后 |
description |
100 字 | trim() 之后 |
content |
250000 字 | 原样,不 trim |
⚠️ 恰好等于上限放行,超一个字符就
400。⚠️
title/description传成非字符串(数字、对象)会被按空串处理 → 报标题不能为空,而不是「类型错误」。这是有意为之:字段名对不上就当场报, 类型对不上按缺失算。
visibility 的三档(取值就是这三个字符串,别传别的):
| 档 | 谁能读 | 进 sitemap | 允许搜索引擎索引 |
|---|---|---|---|
internal(默认) |
只有站内 core+ | ❌ | ❌ |
link |
拿到链接的任何人(含未登录访客) | ❌ | ❌ |
public |
拿到链接的任何人 | ✅ | ✅ |
⚠️ 不传 =
internal(创建时)。「文章发出去了」只代表站内能看, 对外打不开 —— 访客拿到的是登录页。想对外就显式传。⚠️ 写错了不会静默:不在上面三档里的值(
"pulbic"这种拼错、以及true、1这类非字符串)一律400 可见性取值不合法,可选:internal / link / public。 这是刻意的 —— 静默按默认档处理会让你以为发出去了,其实没人看得到。⚠️ 但有一个例外:
""(空串)与null会被当作「没传」→ 落成默认档internal。JSON 里写出空串多半就是写错了,所以别用空串表示「不指定」 —— 要么省掉这个键,要么显式给三档之一。(新增的PATCH不吃这一套,见 §8.1。)⚠️
link与public只差「可不可被索引」,可读性是一样的。 不想被搜索引擎收录、只想给拿到链接的人看,就选link。⚠️ 改回
internal收不回已经被抓走的副本。 搜索引擎与第三方存档(以及社交 平台的卡片缓存)可能已经存下了一份 —— 这条是不可逆的,§11 再说一次。
设为 public 之后,这篇文章会出现在这些地方(link 不会):
| 出口 | 说明 |
|---|---|
/explore |
站内的对外公开列表页,免登录可看;只列 public 档(与 sitemap 同一集合) |
| sitemap | 允许搜索引擎收录(link 不进) |
分享卡片 GET /api/og/blog/:id |
一张 PNG,免登录;link / public 都回 200,internal / 已软删 / 不存在三者同形 404 |
📌 这三条是给站外的人看的入口,机器人自己不需要调它们 —— 但要知道传了
public就等于把这篇推到了这些地方。反过来,想给「拿到链接的人」看又不被 搜索引擎收走,选link。
7.2 成功响应
{
"code": 200,
"message": "上传成功",
"blog_id": "9f1c8e2a-…",
"redirect": "/blog/9f1c8e2a-…"
}
✅
blog_id是 UUID,不是自增数字。发完请把它存下来 —— 改文章(§8)要靠它。✅ 文章创建即可见(站内 core+ 立刻读得到),没有审核队列,也没有草稿态。 对外是否可读由
visibility决定(不传 = 仅站内,§7.1)。⚠️ 响应里不回声
visibility。想确认它落成了哪一档,改完GET /api/blogs/:id看blog.visibility(§9)。
7.3 会被拒绝的情形
| 情形 | 返回 |
|---|---|
| 未登录 / Cookie 失效 | 401 请先登录 |
| 账号不是 core+ | 403 只有核心用户才能发布文章 |
| 账号被禁言 | 403 您已被禁言,无法执行此操作。剩余约…。原因:… |
| 跨站调用 | 403 跨源请求被拒绝 (CSRF) |
| 标题 / 摘要 / 正文为空 | 400 标题不能为空 / 描述不能为空 / 内容不能为空 |
| 超过长度上限 | 400 标题不能超过30个字符 / 描述不能超过100个字符 / 内容不能超过250000个字符 |
| 传了不认识的键 | 400 未知字段 "…",本接口只接受:title / description / content / category_id / visibility |
category_id 不是整数 |
400 栏目ID格式错误 |
category_id 不存在或已停用 |
400 选择的栏目不存在 |
visibility 不在三档里 |
400 可见性取值不合法,可选:internal / link / public |
| 该栏目仅管理员可发(含父栏目勾选) | 403 该栏目仅允许管理员发布文章 |
| 触发每日限额 | 429 今日发布数量已达上限(20篇) |
校验顺序是固定的,一次只报第一条:未知字段 → 可见性 → 必填 → 长度 → 栏目格式 / 存在性。
所以「空标题 + visibility 拼错」报的是可见性那条;改完才会看到「标题不能为空」。
8. 改文章 / 删文章
PUT /api/blogs/:id
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "title": "新标题", "description": "新摘要", "content": "新正文", "category_id": null, "visibility": "link" }
- 请求字段与 §7.1 完全一致(同样只认那 5 个键,同样那几档长度上限)
- 是整体覆盖,不是局部更新 —— 想把文章改成未分类,就显式传
category_id: null - 成功:
{ "code": 200, "message": "更新成功", "blog_id": "…", "redirect": "/blog/…" }
⚠️
visibility是这条「整体覆盖」规则唯一的例外:不传 = 不改动这一档。 也就是说,一个只会传旧那 4 个键的调用方改一次标题,不会把文章打回internal(否则就是一次静默下架:对外消失、退出 sitemap)。 想改档位就显式传(传"internal"就是真的要改回仅站内)。📌 这条豁免只对 PUT 有效。
POST(创建)没有「原值」可保持,不传就是 默认档internal(§7.1)—— 两条路径的缺省语义相反,别一起记。
| 情形 | 返回 |
|---|---|
| 不是作者本人 | 403 无权编辑该文章 |
| 文章不存在 / 已被软删 | 404 文章不存在 |
| 其余 | 与 §7.3 同一批文案 |
8.1 只改可见性:PATCH /api/blogs/:id
PATCH /api/blogs/:id
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "visibility": "link" }
只认 visibility 这一个键 —— 多传任何一个键都 400。想改档位就用它,别用 PUT:
PUT 是整体覆盖,为了改一列得把标题、摘要、正文全部回传,既有丢更新的竞态
(另一个标签页刚改过标题就会被这次覆盖掉),又把正文白往返一遍。
- 权限与 PUT 逐条相同:core+ + 作者本人(管理员同样改不了别人的文章)+ 禁言检查
changed会告诉你到底动没动:
{ "code": 200, "message": "可见性已从「仅站内可见」改为「对外公开」",
"visibility": "public", "changed": true }
目标档与当前相同时 message 是 可见性没有变化、changed: false(不是固定的成功文案)。
- ⚠️ 取值必须恰好是三档之一,空串也
400—— 它刻意不套用 POST / PUT 那套 「空串 = 没传 = internal」的归一化(§7.1):那条规则是给「旧客户端压根没提这件事」用的, 而在这条窄接口上显式传空串几乎一定是写错了,静默当成「改成站内」会让人在文章 从公网消失之后才发现。
| 情形 | 返回 |
|---|---|
| 不是作者本人 | 403 无权编辑该文章 |
| 文章不存在 / 已被软删 | 404 文章不存在 |
| 多传了别的键 | 400 本接口只接受 visibility,多传了:… |
| 取值不在三档(含空串) | 400 可见性取值不合法,可选:internal / link / public |
DELETE /api/blogs/:id
- 同样仅作者本人(非作者
403 无权删除该文章) - 成功:
{ "code": 200, "message": "文章已删除", "blog": { "id": "…", "ignore": true } } - ⚠️ 这是软删除:站点永不物理删除任何内容。删掉的文章对读者等同不存在
(一律
404),但不要把它当成「可以反复清理重建」的机制 —— 复用同一个标题反复删建,在读者与审计侧都看得到痕迹。
9. 读文章
GET /api/blogs/:id # 单篇(含 Markdown 正文)
GET /api/blogs?page=1 # 列表
两条都需 core+ 登录,与站内页面同档 —— 本站没有「只读豁免」, 机器人模型统一是「一个 core+ 账号 + 会话 cookie」。
单篇返回的关键字段:
{
"code": 200,
"blog": {
"id": "9f1c8e2a-…",
"title": "标题",
"description": "摘要",
"author": "alice", // 作者用户名,可能为 null
"author_id": "u_xxx",
"created_at": "2026-09-11T20:31:05.000Z", // ⚠️ 假的 Z,见 §5
"likes_count": 0,
"comments_count": 0,
"fish_count": 0,
"category": "前端", // 栏目名,未分类为 null
"category_path": "技术 > 前端", // 完整路径
"content": "# 正文…", // Markdown 源文
"visibility": "internal" // 对外可见性三档(§7.1)
}
}
列表(GET /api/blogs)的查询参数:
| 参数 | 说明 |
|---|---|
page |
页码,从 1 起 |
per_page |
每页条数,clamp 到 1..50;不传走服务默认 |
category |
按栏目筛,⚠️ 传 slug(§6.2 的不对称就在这) |
featured |
1 = 只看精选,0 = 只看非精选,不传 = 不筛 |
search |
关键词 |
search_fields |
搜哪些字段,逗号分隔:title / description / author / content;未知字段名直接 400;不传 = 标题 / 摘要 / 作者名 |
sort |
updated = 按更新时间;其余一律按发布时间 |
📌 带
search_fields=content(搜正文)会计入另一条限频(§10): 正文搜索是全表扫描,是重活。
10. 限频
| 规则 | 额度 | 适用 |
|---|---|---|
| 发文 | 20 篇 / 本站当日 | POST /api/blogs(按作者计,写入与校验之间判) |
| 改文章 | 无限频 | PUT /api/blogs/:id |
| 读文章 / 读栏目 | 无限频 | §6、§9(搜正文那档除外) |
搜正文(search_fields=content) |
30 次 / 分钟 | GET /api/blogs,与 /blog 页面共用同一个预算 |
| 登录 | 15 分钟内失败 300 次 / IP、100 次 / 用户名 | 仅统计失败 |
超限一律 429。
⚠️ 「当日」按本站时区(UTC+8)的零点切分,不是 UTC 零点,也不是你的本地时区。
⚠️ 发文没有分钟级限额,只有每天 20 篇 —— 意味着一台失控的机器人 可以在 1 分钟内把 20 篇全发完,而不是被平滑地限速。请务必在机器人侧 自己做节流,并在启动前先确认这次真的要发(§11)。
限频计数存在服务端(进程内计数桶,定期落盘快照),重启不会清零 —— 别指望靠重启洗掉自己的用量。但它也不是持久化账本,不要拿它做用量统计。
11. 礼仪与约定
- 文章是公开内容。 发出去就被全站 core+ 用户看到,且会进博客列表与搜索。 机器人发文前请自问一句:这篇东西值得占一个位置吗?
- 对外公开是不可逆的。
visibility: "public"之后,搜索引擎与第三方存档 可能已经抓走副本 —— 改回internal收不回那些副本,社交平台的卡片缓存也要 数周才过期。所以「要不要对外」发布前就定下来,别先公开看看效果再收回。 - 别把接口当存储用。 一天 20 篇的额度不是用来做数据同步的 —— 要存结构化数据请用别的机制,博客区不是你的数据库。
- 选栏目要选对。 发到不相关的栏目比不选栏目更糟:读者按栏目订阅, 错发会被当成垃圾信息。
- 发到「仅管理员可发」的栏目只会拿到 403 —— 用 §6.1 的
admin_only_posting提前筛掉。 - 通知是真的。 某些栏目开了「发文提醒管理员」:你在那些栏目发文, 全站管理员与站长都会收到一条通知(说的是「用户 X 在栏目 Y 发布了新文章:标题」)。 别用高频发文去刷管理员的通知列表。(发文者本人若也是管理员,他收不到自己那条。)
12. 排错速查
| 现象 | 多半是 |
|---|---|
400 未知字段 "category" |
§6.2:栏目字段叫 category_id,值是数字 ID,不是 slug |
400 未知字段 "tags" |
键集封闭:只认 title / description / content / category_id / visibility |
400 可见性取值不合法… |
visibility 不在 internal / link / public 三档里 —— 多半是拼错了(§7.1) |
| 文章发出去,但访客打开是登录页 | 默认档就是仅站内(internal)。要对外得显式传 visibility(§7.1) |
| 改了标题之后,文章突然对外打不开 | 调用方没传 visibility。现在的语义是「不传 = 不改档」,所以这不该再发生 —— 若仍出现,请提 issue(§8) |
| 想让文章不被搜索引擎收录 | 选 link 而不是 public:两者可读性相同,只有 public 进 sitemap、允许索引(§7.1) |
400 选择的栏目不存在 |
栏目 ID 抄错,或该栏目已被停用 —— 重新拉一次 §6.1 的清单 |
400 标题不能为空 |
标题是空串、或传成了非字符串。拼错的字段名(titel)不会走到这条 —— 键集封闭会先报 未知字段 "titel" |
403 只有核心用户才能发布文章 |
账号还是 user,需要站长提权一次(§2) |
403 该栏目仅允许管理员发布文章 |
换个栏目,或看 §6.1 的 admin_only_posting |
403 跨源请求被拒绝 (CSRF) |
你在浏览器里跨站调了它(§3) |
429 今日发布数量已达上限(20篇) |
等本站时区(UTC+8)的下一个零点 |
| 响应 200,但文章是「未分类」 | 更新前的老接口会这样;现在同情形会报 400 未知字段。若仍出现,请提 issue |
| 时间整体差 8 小时 | §5 的假 Z |
13. 最小可用流程
// 1. 登录(步骤见 comment-bot.md §2),拿到 raricy_session
// 2. 拉栏目清单,挑一个机器人发得出去的
const cats = await (await fetch('https://raricy.com/api/categories', { headers: { cookie } })).json();
const target = cats.categories.find((c) => c.slug === 'tech' && !c.admin_only_posting);
if (!target) throw new Error('没有可发的栏目');
// 3. 发文 —— 注意是 category_id(数字),以及 visibility(决定对不对外)
const res = await fetch('https://raricy.com/api/blogs', {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({
title: '标题',
description: '摘要',
content: '# 正文\n\n用 Markdown 写。',
category_id: target.id,
// 不传 = internal(仅站内可见);'link' = 拿到链接的人可读但不许索引;
// 'public' = 对全互联网公开且可被搜索到(不可逆,见 §7.1)
visibility: 'internal',
}),
});
const body = await res.json();
console.log(body.code, body.message, body.blog_id);
📌 文档集里的机器人文档(
chat-bot.md、comment-bot.md、vote-bot.md、checkin-bot.md、like-feed-bot.md、image-bot.md、clipboard-bot.md、account-bot.md、favorite-bot.md、fish-bot.md、fish-bank-example.md) 加上本文,覆盖讨论 / 评论 / 投票 / 签到 / 点赞投喂 / 图床 / 剪贴板 / 账号通知 / 收藏夹 / 鱼干 / 发文十一个方向;账号准备、鉴权、时间戳三件事各份说的是同一套。