文档索引

收藏夹接口(读公开收藏夹 · 站内写入)

面向站外开发者。读完本文即可读公开收藏夹、并管理自己的收藏夹,无需阅读本站源码。

本文是这几条接口的唯一对外口径。字段、限频数值、错误码均照实抄录, 若与源码不符以源码为准(也请提 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+ 登录、按 posterMinute 30 次/分/人限频)。它读的是同一份公开数据, 只是产物是图不是 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 条硬约束

  1. 这条接口只读。 /api/spider/favorites/:id 没有任何写入能力 —— 建收藏夹、改标题、增删条目是另一套会话接口(/api/favorites/*),见 §9。
  2. 只能读公开收藏夹。 本站的收藏夹分「公开」与「私密」两种,创建时选定、此后不可修改。 私密收藏夹根本没有 6 位 ID(不是「有 ID 但不给看」),所以它在这里结构性不可达 —— 你不可能猜中一个不存在的东西。私密 / 不存在 / 已删除,三者对外同为 404, 本站不确认某个 ID 是否存在过。
  3. 需 core+ 账号,且有限频(§2 鉴权、§4 限频)。这是本站 spider 系列接口里 唯一带限频的一条:其余几条只按 ID 查单篇内容,而这条一次会带出整个列表。
  4. 收藏与「点赞」无关。 本站不公开一篇文章的被收藏数,作者也不会收到任何收藏通知 —— 所以不要指望从这条接口或任何别的地方读到「某文章被收藏了多少次」。

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 条是给人用的尺度,不是给你做数据同步的。要存结构化数据请用别的机制。

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