收藏夹接口(读公开收藏夹 · 站内写入)
面向站外开发者。读完本文即可读公开收藏夹、并管理自己的收藏夹,无需阅读本站源码。
本文是这几条接口的唯一对外口径。字段、限频数值、错误码均照实抄录, 若与源码不符以源码为准(也请提 issue —— 那就是本文过期了)。
0. 一句话说清
读:给一个 6 位收藏夹 ID,拿回它的标题、条目数与文章列表(§3)。
写:建收藏夹、改名、加/减条目、复制、导入导出 —— 走另一套会话接口
(/api/favorites/*,所有者限定,§9)。两套是不同的命名空间,别混:
| 读别人的公开收藏夹 | 管理自己的收藏夹 | |
|---|---|---|
| 路径 | /api/spider/favorites/:id |
/api/favorites/* |
:id 是什么 |
6 位公开句柄 | 内部 UUID(自己的,含私密) |
| 谁能用 | 任何 core+ 账号 | 只有所有者本人(站长也没有例外) |
| 限频 | 120 次/分 / IP | 见 §9.7 |
📌 公开收藏夹还有一条出图的读口:
GET /api/poster/favorite/<6 位句柄>返回分享用的 二维码 PNG(需 core+ 登录、按posterMinute30 次/分/人限频)。它读的是同一份公开数据, 只是产物是图不是 JSON。
都需要一个 core+ 账号 —— 本站的机器人模型统一是「一个 core+ 账号 + 会话 cookie」
(建号与提权见 chat-bot.md §2,登录换 cookie 见 §3)。带上 cookie 后:
curl -s -b jar.txt https://raricy.com/api/spider/favorites/123456
1. 能力边界与 4 条硬约束
- 这条接口只读。
/api/spider/favorites/:id没有任何写入能力 —— 建收藏夹、改标题、增删条目是另一套会话接口(/api/favorites/*),见 §9。 - 只能读公开收藏夹。 本站的收藏夹分「公开」与「私密」两种,创建时选定、此后不可修改。 私密收藏夹根本没有 6 位 ID(不是「有 ID 但不给看」),所以它在这里结构性不可达 —— 你不可能猜中一个不存在的东西。私密 / 不存在 / 已删除,三者对外同为 404, 本站不确认某个 ID 是否存在过。
- 需 core+ 账号,且有限频(§2 鉴权、§4 限频)。这是本站
spider系列接口里 唯一带限频的一条:其余几条只按 ID 查单篇内容,而这条一次会带出整个列表。 - 收藏与「点赞」无关。 本站不公开一篇文章的被收藏数,作者也不会收到任何收藏通知 —— 所以不要指望从这条接口或任何别的地方读到「某文章被收藏了多少次」。
2. 鉴权
需要一个 core+ 账号 + 会话 cookie。本站的会话不用 Bearer(它是 HttpOnly
Cookie),也没有专用的
「机器人接口」—— 机器人就是「一个 core+ 账号 + cookie」,本接口与 spider 系列
其余几条都遵循这一模型。建号、提权、取 cookie 的完整流程见
chat-bot.md §2 与 §3,这里只给最小可跑的版本:
# 1) 由人工在浏览器里注册一次,并提权到 core+(chat-bot.md §2)
# 2) 用账号密码换 cookie(存进 jar.txt)
curl -s -c jar.txt -X POST https://raricy.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"mybot","password":"********"}'
# 3) 带上 cookie 调本接口
curl -s -b jar.txt https://raricy.com/api/spider/favorites/123456
| 情况 | 响应 |
|---|---|
| 没带 cookie,或 cookie 已过期 / 被作废 | 401 |
| 已登录,但角色不是 core 及以上 | 403 |
Cookie 名
raricy_session,有效期 30 天,HttpOnly、SameSite=Lax。 改密码等操作会让旧 cookie 立刻失效(服务端有session_version快照)。 收到401就重新登录一次即可。详见chat-bot.md§3。
3. 接口
3.1 按 ID 读收藏夹
GET /api/spider/favorites/:id
id 是 6 位数字的收藏夹 ID(公开收藏夹在站内页面上会显示成 [@123456] 这个样子,
方括号与 @ 是站内正文引用语法的一部分,调接口时只传 6 位数字)。
curl -s -b jar.txt https://raricy.com/api/spider/favorites/123456
⚠️ 示例里的
-b jar.txt不能省 —— 这条接口要 core+ 登录,不带 cookie 是401。
成功(HTTP 200):
{
"id": "123456",
"title": "值得重读的几篇",
"author": "raricy",
"count": 2,
"blogs": [
{ "id": "9f8c1d2e-4a3b-4c5d-8e7f-0123456789ab", "title": "第一篇的标题" },
{ "id": "1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d", "title": "第二篇的标题" }
]
}
字段口径:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
string | 6 位收藏夹 ID,与请求里的一致 |
title |
string | 收藏夹标题(用户自定,长度 1–60) |
author |
string | 收藏夹创建者的用户名(不是昵称、不是 ID) |
count |
number | blogs 数组的长度。目前恒等于 blogs.length,将来若接口改为分页,这里会是总数 |
blogs |
array | 条目列表,最新的在前 |
blogs[].id |
string | 文章 ID(UUID)。站内地址是 https://raricy.com/blog/<id> |
blogs[].title |
string | 文章标题 |
文章列表最多 1000 条(一个收藏夹的条目上限)。已删除的文章不会出现在列表里。
3.2 关于响应信封
本条接口返回裸 JSON(没有 {"code":200,"message":"ok",...} 那层信封),
与 spider 系列的其它接口一致。判断成功请一律看 HTTP 状态码。
4. 限频
| 规则 | 额度 | 适用 |
|---|---|---|
| 收藏夹读取 | 120 次 / 分钟 / IP | 本条接口 |
超限一律返回 HTTP 429(响应体是 {"code":429,"message":"请求过于频繁,请稍后再试"})。
鉴权与限频是两件事:
401/403管「你有没有资格」,429管「你来得有多快」。 带上有效 cookie 不代表可以绕开额度 —— 这条额度按来源 IP 计,与账号无关。
⚠️ 判断一律只看 HTTP 状态码 429(别去匹配 message 文案,它可能变)。
计数存在服务端(进程内计数桶,定期落盘快照),重启不会清零 —— 别指望靠重启洗掉自己的用量。 但它也不是持久化账本,不要拿它做用量统计。
取不到客户端 IP 时该维度不计数(不传占位串)—— 所以这条额度是「按来源 IP」的门槛, 不是一份精确的配额账。
5. 错误码
| HTTP | 何时 | 机器人该怎么做 |
|---|---|---|
200 |
成功 | 正常解析 |
401 |
没带 cookie,或 cookie 已过期 / 被作废(§2) | 重新登录一次再试;别把它当「接口挂了」 |
403 |
已登录,但角色不是 core 及以上(§2) | 别再重试 —— 需要人工提权,见 chat-bot.md §2.2 |
404 |
ID 不存在 / 是私密收藏夹 / 已被删除 / 形态不是 6 位数字 | 别再重试,四种情况对外没有区别,重试也不会有结果 |
429 |
触发限频(§4) | 退避后重试,间隔 ≥ 1 分钟;不要把重试写成立即循环 |
404 的响应体形如 {"code":404,"message":"收藏夹不存在"} —— 与 spider 系列里
出错时的形状一致(成功时才是裸对象)。
⚠️
401/403与404是不同档位,别混。 身份没通过就根本走不到「这个 ID 存不存在」那一步 —— 所以看到404说明 cookie 是好的,那是 ID 本身的问题。
6. 一个完整的例子
#!/usr/bin/env bash
# 读出一个公开收藏夹,并把每条文章的站内地址打出来
# 前置:jar.txt 是已登录的 core+ 账号 cookie(见 §2)
ID="${1:?用法: $0 <6位收藏夹ID>}"
resp=$(curl -s -b jar.txt -w '\n%{http_code}' "https://raricy.com/api/spider/favorites/${ID}")
body=$(printf '%s' "$resp" | sed '$d')
code=$(printf '%s' "$resp" | tail -n1)
if [ "$code" != "200" ]; then
echo "读取失败(HTTP $code):$body" >&2
exit 1
fi
printf '%s' "$body" | python3 -c '
import json, sys
d = json.load(sys.stdin)
print(f"收藏夹:{d[\"title\"]}({d[\"count\"]} 篇,收藏者 @{d[\"author\"]})")
for b in d["blogs"]:
print(f" - {b[\"title\"]} https://raricy.com/blog/{b[\"id\"]}")
'
7. 礼仪与约定
- 缓存。 响应带
Cache-Control: no-store(本站对私有数据一律如此)。收藏夹内容变化不频繁, 你可以在自己那边缓存几分钟 —— 但别把限频额度当成缓存失效的理由。 - 别把
author当稳定标识。 它是用户名,本站用户名唯一但用户可能改名。 需要稳定标识请用文章 ID(blogs[].id)。 - 请求频率与站点负载。 120 次/分是上限不是目标。批量读取请拉开间隔。
8. 排错速查
| 现象 | 原因 |
|---|---|
| 一直 401 | 没带 cookie / cookie 过期或被作废(改密码会作废旧会话)。重新登录,见 §2 |
| 一直 403 | 账号不是 core+。注册出来是 user,需要提权一次,见 chat-bot.md §2.2 |
| 一直 404 | ID 不是 6 位数字(比如把 [@123456] 整串传进来了);或它不是公开收藏夹 |
| 一直 429 | 同一出口 IP 下短时间内请求过多;也可能是同 IP 的其它客户端共用了这一份额度 |
blogs 是空数组 |
收藏夹是空的(创建了但没收录文章) |
count 与预期不符 |
已删除的文章不计入;收藏夹条目上限 1000 |
| 读不到某个 ID | 它是私密收藏夹 —— 私密收藏夹没有 6 位 ID,读不到是设计如此,不是故障 |
9. 站内写入(/api/favorites/*,所有者限定)
📌 这一节与上面八节是两套接口:上面讲的是「读别人的公开收藏夹」(spider 命名空间, 6 位句柄);这里讲的是「管理自己的收藏夹」(会话命名空间,内部 UUID)。 全部需要 core+ 登录,写请求同样过 CSRF 来源校验(
Origin/Referer都缺失时放行, 非浏览器客户端天然通过 —— 别伪造)。
| 做什么 | 接口 |
|---|---|
| 列自己的收藏夹(含私密) | GET /api/favorites |
| 建一个 | POST /api/favorites |
| 读某一个(含条目) | GET /api/favorites/:id |
| 改名 | PATCH /api/favorites/:id |
| 删(软删) | DELETE /api/favorites/:id |
| 加 / 减条目 | POST /api/favorites/:id/items · DELETE /api/favorites/:id/items/:blogId |
| 复制成一个新的 | POST /api/favorites/:id/copy |
| 导出 / 导入 JSON | GET /api/favorites/:id/export · POST /api/favorites/import |
⚠️
:id是内部 UUID,不是 6 位句柄。 6 位句柄只在 公开收藏夹上存在,而且还是public_id字段的事(§9.1)。
9.1 列自己的收藏夹
GET /api/favorites?blogId=<文章 UUID>
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"favorites": [
{
"id": "f_xxx", "public_id": "123456", "title": "值得重读的几篇",
"is_public": true, "item_count": 2, "created_at": "2026-09-19T20:31:05.000Z",
"contains": true
}
]
}
| 字段 | 说明 |
|---|---|
id |
内部 UUID —— §9 所有路径参数都用它 |
public_id |
6 位公开句柄,只对公开收藏夹非空。这是所有者视角才给的字段 |
is_public |
公开 / 私密 |
contains |
只在传了 blogId 时出现:这篇在不在这个收藏夹里 |
⚠️
public_id为null不等于「藏起来了」,而是「根本没有」 —— 私密收藏夹没有句柄,所以任何按句柄的读法对它结构性不可达(§0 的表)。⚠️ 判「这个收藏夹对外可见吗」请永远用
is_public,别写public_id != null。 两者今天等价,但那是不变量不是巧合(服务层维持着 「is_public为真 ⇔public_id非空」)。📌 传了形态不对的
blogId(比如空串)不会报错,只当没传 —— 所以别拿contains缺失来判断「文章不存在」。
9.2 建一个
POST /api/favorites
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "title": "值得重读的几篇", "isPublic": true }
| 字段 | 说明 |
|---|---|
title |
1–60 字符 |
isPublic |
必须显式传布尔(没有默认值) |
成功:
{
"code": 200,
"message": "ok",
"favorite": { "id": "f_xxx", "public_id": "123456", "title": "值得重读的几篇", "is_public": true, "created_at": "…" }
}
| 情形 | 返回 |
|---|---|
| 未登录 / 非 core+ | 401 请先登录 / 403 需要核心用户权限 |
| 请求体不是 JSON / 不是对象 | 400 请求体格式错误 |
isPublic 不是布尔 / 没传 |
400 必须显式指定 isPublic(创建后不可修改) |
| 标题不在 1–60 | 400 请求参数有误 |
| 未软删的收藏夹已达 200 个 | 400 一个用户最多创建 200 个收藏夹 |
| 触发限频 | 429 操作过于频繁,请稍后再试 |
⚠️
isPublic没有默认值(与云剪贴板的publicity同一个理由): 性质创建后不可修改,静默给个默认值等于让少传一个字段的调用方 建错一个改不了的收藏夹。
9.3 读 / 改名 / 删
GET /api/favorites/f_xxx # 含条目列表
PATCH /api/favorites/f_xxx { "title": "新标题" }
DELETE /api/favorites/f_xxx
GET 的响应比 §9.1 多一个 items:
{ "code": 200, "message": "ok",
"favorite": {
"id": "f_xxx", "public_id": "123456", "title": "…", "is_public": true, "created_at": "…",
"items": [ { "blog_id": "<文章 UUID>", "title": "第一篇的标题" } ]
} }
| 情形 | 返回 |
|---|---|
| 不存在 / 不是你的 / 是别人的私密收藏夹 | 404 收藏夹不存在(三者同形) |
PATCH 的 body 里有 isPublic |
400 收藏夹的公开/私密性质创建后不可修改 |
PATCH 标题不合法 |
400 请求参数有误 |
DELETE 成功 |
{ "code": 200, "message": "已删除" } |
⚠️ 这三条都是所有者限定 —— 站长也没有例外。 服务层按
userId收口, 没有「管理员代管」这条路。想读别人的公开收藏夹请走 §3(spider 那条)。⚠️
PATCH只接受title。 传了isPublic会明确报错(不是静默忽略)—— 刻意的:允许改性质会造出「is_public为真但没有句柄」的死状态。 想换性质只有一个办法:复制成一个新的(§9.5)。⚠️
DELETE是软删(站点永不物理删除),删掉之后这个 UUID 一律 404。
9.4 加条目 / 减条目
POST /api/favorites/f_xxx/items { "blogId": "<文章 UUID>" }
DELETE /api/favorites/f_xxx/items/<文章 UUID>
两条都回 { "code": 200, "message": "ok", "item_count": 3 }(条目数按这次操作之后算)。
| 情形 | 返回 |
|---|---|
| 收藏夹不是你的 / 不存在 | 404 收藏夹不存在 |
| 文章不存在(或已软删) | 400 请求参数有误 |
| 触发限频(300/时、1500/天,增与减共用一个预算) | 429 操作过于频繁,请稍后再试 |
已经在里面了(POST) |
成功(幂等,不重复计数) |
条目已达 1000(POST) |
400 一个收藏夹最多收录 1000 篇博客 |
不在里面(DELETE) |
成功(幂等) |
✅ 两条都是幂等的 —— 反复勾选/取消不该报错,服务端按最终状态收口。
📌 同一篇文章可以同时待在你的多个收藏夹里,一个收藏夹的增删不影响另一个。 减条目是软删条目(同一篇以后还能再加回来)。
9.5 复制成一个新的
POST /api/favorites/f_xxx/copy
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "isPublic": false }
:id既可以是自己的 UUID,也可以是别人的 6 位公开句柄 —— 后者就是「从分享页复制别人的公开合辑」这条路;isPublic同样必须显式传:复制出来的那一份是新收藏夹,性质由你定;- 成功:
{ "code": 200, "message": "ok", "favorite": {…}, "item_count": 12 }
⚠️ 复制是快照,不是活链接:复制那一刻的标题与条目,之后两边各走各的。 原收藏夹后来加了什么,你这份不会跟着变。
⚠️ 复制与「新建」共用同一份限频与配额(都是「造一个新收藏夹」)—— 拿复制当绕过 200 个上限的手段是行不通的。
9.6 导出 / 导入
导出(所有者限定,私密也能导出):
GET /api/favorites/f_xxx/export
Cookie: raricy_session=<JWT>
响应是一个 JSON 文件下载(不是信封),内容形如:
{
"version": 1,
"title": "值得重读的几篇",
"blogs": [
{ "id": "<文章 UUID>", "title": "第一篇的标题", "url": "https://raricy.com/blog/<文章 UUID>" }
]
}
✅ 文件里不含收藏夹 id、不含是否公开 —— 这正是「私密收藏夹可以导出」 与「私密收藏夹不泄露身份」能同时成立的原因。
📌
url只是给人看/手改的;导入端优先认id,id不在时才从url末段取。
导入(总是新建一个收藏夹):
POST /api/favorites/import
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "isPublic": true, "data": { /* 上面那个文件的全部内容 */ } }
data直接放文件内容的 JSON 对象(不是 multipart 上传,见下);- 文件里的
isPublic/publicId/ 收藏夹 id 一律忽略 —— 性质只看请求体; - 成功:
{ "code": 200, "message": "ok", "favorite": {…}, "created": 10, "skipped": 2 }
⚠️ 导入走 JSON body,不是文件上传。 请先在自己那边把文件读成文本再 POST —— 这是刻意的:本站请求体有三道 12MB 的闸,文件上传会绕过它们并撞上静默截断; 走 JSON body 之后,唯一的体积约束落在应用层(条目封顶 1000)。
⚠️ 导入总是新建,不能并入已有收藏夹 —— 因为性质创建后不可改, 「并入」会撞上「目标夹是公开的、文件作者以为是私密」这类冲突。
📌
created/skipped分别是通过与跳过的条数(文章不存在、已在文件里重复的会跳过)。
9.7 限频与错误码
| 规则 | 额度 | 适用 |
|---|---|---|
| 新建收藏夹 | 20 次 / 小时 | POST /api/favorites 与复制共用这一份 |
| 导入 | 10 次 / 小时 | POST /api/favorites/import |
| 读 / 改名 / 删收藏夹 | 无限频 | §9.1 / §9.3 |
| 增 / 减条目 | 300 次 / 小时、1500 次 / 天 | §9.4 —— 与文章点赞同额,但另起一对桶 |
超限一律 429 操作过于频繁,请稍后再试。
⚠️ 「加条目」是吃配额的(
fav:item:h:/d:两个桶)—— 一个「把几百篇一次性收进 收藏夹」的机器人会在这里撞 429,而不是无限额。改标题、删收藏夹才不吃配额。
| 状态码 | 常见 message |
|---|---|
400 |
请求体格式错误 / 必须显式指定 isPublic(创建后不可修改) / 收藏夹的公开/私密性质创建后不可修改 / 请求参数有误 / 一个用户最多创建 200 个收藏夹 / 一个收藏夹最多收录 1000 篇博客 |
401 / 403 |
请先登录 / 需要核心用户权限 |
404 |
收藏夹不存在(不存在 / 不是你的 / 是私密的,三者同形) |
429 |
操作过于频繁,请稍后再试 |
500 |
无法生成唯一ID,请重试(极罕见;重试一次) |
⚠️ 对机器人的一条纪律:收藏夹是用户整理自己书签的地方。 200 个 × 1000 条是给人用的尺度,不是给你做数据同步的。要存结构化数据请用别的机制。