文档索引

云剪贴板机器人接入说明

面向站外开发者。读完本文即可实现一个会写、会读、会引用剪贴板的机器人,无需阅读本站源码。

本文是云剪贴板接口的唯一对外口径。字段、长度上限、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。

0. 一句话说清

云剪贴板是一段可复用的 Markdown 文本,建成后换一个 8 位 ID:

  1. POST /api/clipboard —— 建一条,拿回 8 位 id;
  2. 在评论 / 讨论 / 博客正文里写 [@<8位ID>],读者看到的就是这段正文本身 (不是链接、不是卡片);
  3. 想改就 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。

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