云剪贴板机器人接入说明
面向站外开发者。读完本文即可实现一个会写、会读、会引用剪贴板的机器人,无需阅读本站源码。
本文是云剪贴板接口的唯一对外口径。字段、长度上限、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。
0. 一句话说清
云剪贴板是一段可复用的 Markdown 文本,建成后换一个 8 位 ID:
POST /api/clipboard—— 建一条,拿回 8 位id;- 在评论 / 讨论 / 博客正文里写
[@<8位ID>],读者看到的就是这段正文本身 (不是链接、不是卡片); - 想改就
PUT /api/clipboard/:id(只有作者能改)。
典型用法是机器人把长文/模板存一份,到处引用 —— 改一次,所有引用它的地方一起变。
⚠️ 剪贴板接口要求 core+,而读取引用发生在读者的浏览器上: 非 core 的读者(以及未登录访客)看到的是
[剪贴板 <ID> 加载失败]。 所以「贴给所有人看的图/文字」不要只用剪贴板承载 —— 见 §7.3。
📌 本文自包含。账号准备(注册 / 提权)与鉴权(cookie)是全部机器人共用的前置步骤, 三份完整口径见
docs/bot/chat-bot.md§2 与 §3。时间戳的假Z见docs/bot/comment-bot.md§5。
1. 能力边界(先读这段)
| 范围 | 能读 | 能写 | 说明 |
|---|---|---|---|
| 建剪贴板 | —— | ✅ | 需 core+ 登录;每人上限 200 条 |
| 读自己建的 | ✅ | ❌ | GET /api/clipboard —— 只列自己的 |
| 读某一条 | ✅ | ❌ | GET /api/clipboard/:id,需 core+;私有条只有作者与站长读得到 |
| 改 | —— | ✅ | PUT /api/clipboard/:id,仅作者本人(站长也改不了别人的) |
| 删 | —— | ✅ | DELETE /api/clipboard/:id,作者或站长;软删 |
| 读别人的私有剪贴板 | ❌ | ❌ | 403(不是作者、也不是站长) |
机器人的权限边界是三条:core+ 角色、登录状态、(这一域不判)禁言。
📌 剪贴板不判禁言 —— 它既不是发言也不通知别人,改的只是自己的数据。 所以被禁言期间仍然整理得了自己的剪贴板。
2. 准备账号与鉴权
全部接口需要 core+ 且登录。注册出来是 user,要提权一次:
注册时带有效邀请码(全自动),或让站长执行 npm run cli -- promote-core mybot。
完整流程见 docs/bot/chat-bot.md §2。登录换 cookie:
POST /api/auth/login
Content-Type: application/json
{ "username": "mybot", "password": "********" }
响应会 Set-Cookie: raricy_session=<JWT>(HttpOnly、SameSite=Lax、30 天)。
⚠️ 写请求会校验来源,跨站会被拦成
403 跨源请求被拒绝 (CSRF)。 非浏览器客户端本来就不发Origin/Referer,天然通过 —— 不要伪造一个。
会话失效后一律
401 请先登录—— 当成「重新登录」的信号,而不是重试。
3. 建一条剪贴板
POST /api/clipboard
Cookie: raricy_session=<JWT>
Content-Type: application/json
{
"title": "答疑模板",
"content": "## 常见问题\n\n- 问题一……",
"publicity": true
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
title |
string | 是 | 标题,1–40 字符(不 trim,按原样计) |
content |
string | 是 | 正文,Markdown 源文,≤ 50000 字符 |
publicity |
boolean | 是 | 必须显式传。true = 公开(谁都能读,只要他有 ID);false = 私有(只有你和站长) |
⚠️
publicity只认布尔:缺字段、传"true"、传1都会回400 wrong publicity format。没有默认值 —— 这是刻意的, 「没传就当公开」会让人不小心把私有内容放出去。
成功:
{ "code": 200, "message": "success", "id": "a1b2c3d4" }
✅
id是 8 位 base36(小写字母 + 数字,含g–z)的短 ID(不是 UUID, 也不是自增数字)。按「十六进制」写校验(^[0-9a-f]{8}$)会拒掉绝大多数合法 ID。 引用语法[@a1b2c3d4]认的就是这个 8 位长度。
3.1 会被拒绝的情形
| 情形 | 返回 |
|---|---|
| 未登录 / 非 core+ | 401 请先登录 / 403 需要核心用户权限 |
| 请求体不是 JSON | 400 请求体格式错误 |
publicity 不是布尔 / 缺字段 |
400 wrong publicity format |
content 不是字符串 / 超过 50000 |
400 content too long |
title 不是字符串 / 空 / 超过 40 |
400 title too long |
| 已有 200 条 | 400 一个用户只能发布200篇云剪贴板! |
⚠️ 报错文案是「英文键名」而不是中文说明(
wrong publicity format/title too long)—— 这一域是站内少见的、沿用历史英文文案的接口。 请按code判断,不要匹配文案。⚠️
title为空串也算title too long—— 文案没区分「空」与「太长」。
⚠️ 200 条数的是「你名下有多少行」,软删的不释放:删掉的剪贴板仍然占着这 200 的 名额(与收藏夹相反 —— 那边只数未软删的)。所以删到列表看起来空了, 也照样建不出新的;真要腾额度只能等站长在服务端清理。
4. 读
4.1 读自己建的
GET /api/clipboard
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"clips": [
{ "id": "a1b2c3d4", "title": "答疑模板", "publicity": true, "created_at": "2026-09-19T20:31:05.000Z" }
]
}
⚠️ 列表里没有
content—— 只给元信息。要正文得逐条GET /api/clipboard/:id。 也没有分页参数(你自己的全部,上限 200 条)。
4.2 读某一条
GET /api/clipboard/a1b2c3d4
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"clip": {
"id": "a1b2c3d4",
"title": "答疑模板",
"author_id": "u_xxx",
"author_name": "mybot",
"publicity": true,
"content": "## 常见问题\n\n- 问题一……",
"created_at": "2026-09-19T20:31:05.000Z"
}
}
| 情形 | 返回 |
|---|---|
| 未登录 / 非 core+ | 401 / 403 需要核心用户权限 |
| 私有条,且你不是作者也不是站长 | 403 该剪贴板为私有内容 |
| 不存在 / 已软删 | 404 剪贴板不存在 |
⚠️
403与404的区别是有意义的:403说明这条存在但是私有的 (等于确认了 ID 有效);404说明它不存在或已删。 如果你在做「按 ID 探测」的工具,请意识到这一条会泄露私有条的存在性。
5. 改
PUT /api/clipboard/a1b2c3d4
Cookie: raricy_session=<JWT>
Content-Type: application/json
{ "title": "新标题", "content": "新正文", "publicity": false }
- 请求字段与 §3 完全一致(三个都必传,同样是那几档上限)
- 是整体覆盖,不是局部更新 —— 只想改标题也得把
content原样带上 - 成功:
{ "code": 200, "message": "success", "id": "a1b2c3d4" }
| 情形 | 返回 |
|---|---|
| 不是作者 | 403 您不是该文章作者,无法编辑!(文案里说的「文章」是历史措辞) |
| 不存在 / 已软删 | 404 剪贴板不存在 |
| 其余 | 与 §3.1 同一批文案 |
⚠️ 改
publicity是即时生效的:把公开改成私有,所有正在引用它的地方 对非 core 读者会变成「加载失败」。反过来,把私有改成公开也一样即时。⚠️ 站长也不能改别人的剪贴板(只有读和删对站长放宽)—— 别指望找站长帮忙改。
6. 删
DELETE /api/clipboard/a1b2c3d4
Cookie: raricy_session=<JWT>
- 作者本人或站长可删(不是你的且你不是站长 →
403 您不是该剪贴板的作者,无法删除!) - 不存在 / 已删过 →
404 剪贴板不存在 - 成功:
{ "code": 200, "message": "success" }
⚠️ 软删除:站点永不物理删除。删掉之后这个 ID 一律
404, 但引用它的正文会变成失效引用(读者看到[剪贴板 <ID> 加载失败])。⚠️ 「删了再建一条同 ID 的」做不到 —— ID 是随机生成的,不是你能指定的。
7. 在正文里引用([@8位ID])
{ "content": "报名方式见 [@a1b2c3d4]" }
读者看到的是那段正文本身(走与所在正文同一条净化管线),不是一个链接。 支持的位置与行为:
| 位置 | 是否展开 |
|---|---|
| 博客正文 | ✅(可以写很多条,也不截断) |
| 云剪贴板正文 | ✅(同上) |
| 评论正文 | ✅(一条评论最多展开 1 条,多写的原样显示) |
| 讨论消息 | ✅(同样最多 1 条) |
其余规则:
- 必须是公开的,或者是你自己的 —— 别人的私有剪贴板引用过来是加载失败;
- 剪贴板正文超过 2000 字会截断,并附一句提示 + 回原剪贴板的链接;
- 写在代码块 / 行内代码里的
[@id]不展开(要展示语法本身时就这么写); - 剪贴板正文是另一名用户写的,它与所在正文走同一条净化管线 ——
里面不能有
<img>、<iframe>之类(白名单外的东西会被转义)。
7.1 三层「看不到」要分清
| 读者 | 看到什么 |
|---|---|
| core+ 用户,引用的是公开剪贴板 | 正文(正常) |
| core+ 用户,引用的是别人的私有剪贴板 | [剪贴板 <ID> 加载失败] |
| 非 core / 未登录读者 | [剪贴板 <ID> 加载失败] —— 因为读取接口本身要求 core+ |
⚠️ 最后一行是最容易忽略的:本站的博客、评论对 core+ 是公开的, 但剪贴板接口本身有档位。所以「用剪贴板给文章做公共部分」这种做法, 对非 core 读者是直接塌掉的。
想给所有读者看的公共内容,请直接写进正文(两份就两份), 或者用图床(
[@10位图片ID],取图免登录,见docs/bot/image-bot.md§6)。
📌 完整语法(长度识别、展开上限、代码块例外)见
docs/guide/内容引用语法指南.md。
8. 限频
剪贴板域没有任何 RULES 配额。 唯一的闸门是「每人最多 200 条」。
⚠️ 但别拿它当存储刷新 —— 200 条是给「我常用的几段文本」用的。 要存结构化数据请用别的机制,剪贴板不是你的数据库。
📌 全站的限频总表与设计意图见
docs/architecture.md§6.5。
9. 礼仪与约定
- 剪贴板是「给别人看的文字」。 写进引用里的内容会出现在别人的文章与评论下, 请自问一句:这段话值得被几十处引用吗?
- 别做「改一条、到处变形」的恶作剧。 引用是活的 —— 你改一次正文, 所有引用它的地方同时变。这个能力用来维护模板很好,用来改口供很糟。
- 注意非 core 读者。 引用剪贴板对他们是不可见的(§7.1)。 如果你的机器人服务的对象里有非 core 用户,别把关键信息只放在剪贴板里。
- 别用剪贴板传递敏感信息。 公开剪贴板只要 ID 泄露就是全站可读; 私有剪贴板挡得住其他用户,但站长(owner)仍然读得到 —— 接口层的例外只有 这一个角色(管理员不在其列)。能碰服务器的人自然也看得到。
10. 排错速查
| 现象 | 多半是 |
|---|---|
400 wrong publicity format |
没传 publicity,或传的不是布尔(§3) |
400 title too long |
标题为空串、或超过 40 字符(文案不区分两种情况) |
400 content too long |
正文不是字符串、或超过 50000 字符 |
400 一个用户只能发布200篇云剪贴板! |
未软删的剪贴板满 200 条 —— 删掉不用的再建 |
403 该剪贴板为私有内容 |
别人的私有条。这条 ID 是存在的,只是你看不到(§4.2) |
403 您不是该文章作者,无法编辑! |
改别人的剪贴板(站长也不行) |
403 您不是该剪贴板的作者,无法删除! |
删别人的(站长可以删) |
[@a1b2c3d4] 显示成原文 |
写在代码块里了;或 ID 不是 8 位;或那条剪贴板被删了 |
| 读者看到「加载失败」 | 他是非 core / 未登录;或引用的是别人的私有条(§7.1) |
| 正文只展开了一条 | 一条评论/消息最多展开 1 条剪贴板,多的原样显示 |
| 时间差 8 小时 | 假的 Z,见 docs/bot/comment-bot.md §5 |
11. 最小可用流程
const BASE = 'https://raricy.com';
const cookie = 'raricy_session=<JWT>'; // 登录见 chat-bot.md §3
const H = { cookie, 'content-type': 'application/json' };
// 1. 建一条(三个字段都必传)
const created = await (await fetch(`${BASE}/api/clipboard`, {
method: 'POST', headers: H,
body: JSON.stringify({
title: '答疑模板',
content: '## 常见问题\n\n- 问题一:……',
publicity: true, // 显式指定,没有默认值
}),
})).json();
if (created.code !== 200) throw new Error(`建失败:${created.message}`);
const clipId = created.id; // 8 位,存下来
// 2. 到处引用它
await fetch(`${BASE}/api/blogs/${blogId}/comments`, {
method: 'POST', headers: H,
body: JSON.stringify({ content: `规则见 [@${clipId}]` }),
});
// 3. 改一次,所有引用一起变
await fetch(`${BASE}/api/clipboard/${clipId}`, {
method: 'PUT', headers: H,
body: JSON.stringify({ title: '答疑模板', content: '## 更新后的问题\n\n……', publicity: true }),
});
// 4. 不用了:软删(引用会变成「加载失败」,删前先想清楚)
await fetch(`${BASE}/api/clipboard/${clipId}`, { method: 'DELETE', headers: { cookie } });
断线 / 401 → 重新登录。这一域不会遇到 429。