图床机器人接入说明
面向站外开发者。读完本文即可实现一个会传图、会用图的机器人,无需阅读本站源码。
本文是图床接口的唯一对外口径。字段、长度上限、限频数值、错误码均照实抄录, 若与源码不符以源码为准(但也请提 issue —— 那就是本文过期了)。
0. 一句话说清
图床就是传图换一个 10 位 ID,之后到处用这个 ID:
POST /api/images—— multipart 上传,拿回id(10 位字母数字);- 用
[@<10位ID>]写进评论 / 讨论正文(内联成图),或者用image_id字段把图作为附件发出去; - 不想用了就
DELETE /api/images/:id(软删)。
图片本身走 GET /api/images/<ID>/raw,这条直链不需要登录(§6)。
⚠️ 正文里不能手写
<img>—— 评论与讨论的净化白名单里没有它, 写了只会以转义文本显示。贴图只有上面第 2 步那两条路。
📌 本文自包含。账号准备(注册 / 提权)与鉴权(cookie)是全部机器人共用的前置步骤, 三份完整口径见
docs/bot/chat-bot.md§2 与 §3。时间戳的假Z见docs/bot/comment-bot.md§5。
1. 能力边界(先读这段)
| 范围 | 能读 | 能写 | 说明 |
|---|---|---|---|
| 上传 | —— | ✅ | 需 core+ 登录且未被禁言;单文件 ≤ 10 MB |
| 列自己的图 | ✅ | ❌ | GET /api/images —— 只列自己的 |
| 删自己的图 | —— | ✅ | 软删;管理员可删任何人的 |
| 取图字节(直链) | ✅ | ❌ | GET /api/images/:id/raw,免登录 |
| 改图的文件名 / 公开性 | ❌ | ❌ | 没有这类接口(§6.2) |
| 看别人的图库列表 | ❌ | ❌ | 没有这类接口 |
| 硬删(真删盘上的文件) | ❌ | ❌ | 站长专属,走管理端;机器人永远只能软删 |
机器人的权限边界是三条:core+ 角色、登录状态、是否被禁言。
⚠️ 上传判禁言(
403 你已被禁言,暂时无法上传)—— 这与点赞不同。 别指望禁言期间还能往站里塞图。
⚠️ 图床有角色配额:
core与admin都是 50 MB,owner100 MB, 其他角色没有图床权限(403 你的角色无权使用图床)。core与admin同额是有意的,不是漏改。
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, 天然通过 —— 不要伪造一个。💡 取图直链(§6)不需要 cookie,所以它不受这条约束,也不吃限频。
3. 上传
POST /api/images
Cookie: raricy_session=<JWT>
Content-Type: multipart/form-data; boundary=...
--boundary
Content-Disposition: form-data; name="file"; filename="cat.png"
Content-Type: image/png
<二进制字节>
--boundary--
3.1 表单字段
| 字段 | 必填 | 说明 |
|---|---|---|
file |
是 | 图片字节。同名字段可以重复(一次传多张,见 §3.3) |
compress |
否 | 缺省压缩。传 '0' = 不压缩、原样存;传别的(含缺省)= 走压缩 |
⚠️
compress是批级的(一次请求一个值),不是逐文件的。📌 压缩会等比缩到最长边 2000 px 以内(超过才缩),并尽量重编码; 压缩只会让文件更小,绝不会因为「压缩后更大」而丢图 —— 那种情况原样存。 想保留原始画质(例如要放原图)就传
compress=0,但要自己看住 10 MB 的上限。
3.2 一次一张时的响应(最常用)
{
"code": 200,
"message": "上传成功",
"items": [{ "filename": "cat.png", "id": "aB3dE7gHiJ", "url": "/api/images/aB3dE7gHiJ/raw" }],
"failed": [],
"id": "aB3dE7gHiJ",
"url": "/api/images/aB3dE7gHiJ/raw"
}
✅
id/url只在「恰好一个文件、且零失败」时才出现。 单文件调用方 直接取它们即可;多文件时请走items[]。
3.3 一次多张:200 不等于「全都传上去了」
{
"code": 200,
"message": "成功上传 2 张,1 张失败",
"items": [
{ "filename": "a.png", "id": "aaa…", "url": "/api/images/aaa…/raw" },
{ "filename": "b.png", "id": "bbb…", "url": "/api/images/bbb…/raw" }
],
"failed": [{ "filename": "c.svg", "message": "文件内容与声明的格式不匹配" }]
}
⚠️ 多文件时不要用
code === 200判断「都成功了」 —— 逐条看failed[]。 这是本站少见的「部分成功」接口,有意的:一张图不合格不该拖垮整批 (前面几张已经落盘了,整体报错只会让人重传一遍、得到重复图)。
⚠️ 全军覆没时走错误通道:
items为空 →400+failed[0].message。 所以单文件上传的契约与「多文件支持之前」逐字一致。
3.4 校验链(每一张图都要过)
| 顺序 | 检查 | 失败时 |
|---|---|---|
| 0 | 请求体 ≤ 12 MB;file 字段至少有一个 |
413 请求体过大,请减少一次上传的图片数量 / 400 请选择文件 |
| 1 | MIME 在白名单里 | 不支持的文件格式,仅允许 PNG、JPEG、GIF、WebP、SVG |
| 2 | 内容与声明的格式相符(按 magic bytes 复核) | 文件内容与声明的格式不匹配 |
| 3 | 单文件 ≤ 10 MB | 文件过大,单文件上限 10 MB |
| 4 | 你的存储配额还够 | 存储空间不足,你的配额为 50 MB |
| 5 | 上传限频(按张计) | 上传频率过高,请稍后再试 |
| 6 | sharp 压缩 + 写盘 + 落库 | 图片保存失败,请重试 |
⚠️ 第 2 步不是多余的:
Content-Type是客户端自己声明的,可以伪造。 服务端按字节复核,声明与内容不符直接拒 —— 所以「把 zip 改名成 .png」 拿不到一个能用的直链。⚠️ 单文件闸与配额闸都按「原始字节」判(第 3、4 步都在压缩之前)—— 一张 11 MB 的 PNG 会被直接拒掉(
文件过大,单文件上限 10 MB), 别指望「压缩后会变小」能救它。📌 只有已用配额的累加记的是压缩后的大小(落盘成功之后才
used += saved.fileSize), 所以配额消耗会比你传进去的字节少。但那是入库之后的账,拦不住超限的那一张。💡 被拒的文件不消耗限频额度(限频在第 5 步,所有校验之后), 但同一批里前面的成功会消耗。
3.5 会被拒绝的情形(整批级)
| 情形 | 返回 |
|---|---|
| 未登录 / Cookie 失效 | 401 请先登录 |
| 账号不是 core+ | 403 需要核心用户权限 |
| 账号被禁言 | 403 你已被禁言,暂时无法上传 |
| 角色没有图床权限(配额 0) | 403 你的角色无权使用图床 |
Content-Type 不是 multipart / 解析失败 |
400 无效的上传请求 |
没有 file 字段 |
400 请选择文件 |
| 请求体超过 12 MB | 413 请求体过大,请减少一次上传的图片数量 |
4. 列自己的图
GET /api/images
Cookie: raricy_session=<JWT>
需 core+ 登录。只列你自己上传的(软删的不出现):
{
"code": 200,
"message": "ok",
"images": [
{
"id": "aB3dE7gHiJ",
"filename": "cat.png",
"file_size": 81234,
"mime_type": "image/png",
"author_id": "u_xxx",
"author_name": "mybot",
"created_at": "2026-09-19T20:31:05.000Z",
"is_public": true,
"ext": ".png",
"url": "/api/images/aB3dE7gHiJ/raw"
}
]
}
⚠️ 没有分页参数 —— 一次给你全部(你自己的图,受配额约束,量级有限)。
📌
file_size是压缩后的字节数(与配额口径一致)。
5. 存储配额
GET /api/images/quota
Cookie: raricy_session=<JWT>
{
"code": 200,
"message": "ok",
"quota": { "used_mb": 12.34, "limit_mb": 50, "remaining_mb": 37.66, "usage_percent": 24.7 }
}
| 字段 | 说明 |
|---|---|
used_mb / remaining_mb |
两位小数 |
limit_mb |
core / admin = 50,owner = 100 |
usage_percent |
一位小数;配额为 0 时直接给 100(不除零) |
📌 软删的图不计入配额 —— 删掉就腾出了空间。 但盘上的文件还在(软删,§7),所以「删了再传」是可行且常用的做法。
6. 用图:直链与引用
6.1 直链
GET /api/images/<10位ID>/raw
免登录、免鉴权,返回图片字节。上传响应里的 url 就是它(相对路径,域名自己拼)。
| 响应头 | 值 |
|---|---|
Content-Type |
上传时的 MIME |
X-Content-Type-Options |
nosniff |
Cache-Control |
公开图 public, max-age=31536000, immutable |
X-Robots-Tag |
公开图 all |
⚠️ SVG 以附件形式下发(
Content-Disposition: attachment)—— 强制下载而不是 在浏览器里内联渲染,避免 SVG 里的脚本在同源下执行。所以别指望 SVG 能当内联图用, 机器人要贴图请用 PNG / JPEG / GIF / WebP。⚠️ 软删的图 → 404;不存在的 id → 404(两者同形)。
6.2 公开性:没有开关,上传即公开
图片有一个 isPublic 字段,但它没有任何接口能改:
- 上传的图默认公开 —— 直链人人都取得(这对「贴到站内正文里给别人看」是必要的);
- 数据里存在私有图(
is_public: false),那种图只有作者与管理员取得到, 对其他人伪装成 404(连「存在过」都不确认)。但你没法通过接口把图变成私有。
⚠️ 所以:别把图床当私有存储用。 传上去的图,只要知道 10 位 ID 就能取。 敏感内容不要传,或者传之前自己先处理。
6.3 在正文里引用
| 写法 | 用在哪 | 效果 |
|---|---|---|
[@<10位ID>] |
评论 / 讨论正文 | 内联成图(可点开放大),一条消息最多 50 张 |
image_id: "<10位ID>" |
评论 / 讨论的请求字段 | 作为附件发出去(带缩略图与元信息) |
手写 <img src="…"> |
任何地方 | ❌ 不生效,会以转义文本显示 |
⚠️ 两种用法的图都必须是你的。
image_id引用别人的图会被拒 (400 图片不存在或不属于你,请重新上传)。⚠️ 图被软删后,正文里的
[@ID]会变成失效引用(image_missing: true), 别人看到的是裂图 —— 所以别删还在被引用的图。
7. 删除
DELETE /api/images/:id
Cookie: raricy_session=<JWT>
- 作者本人或管理员可删(不是你的 →
403 无权删除此图片); - 已删过 →
400 图片已被删除;不存在 →404 图片不存在; - 成功:
{ "code": 200, "message": "已删除" }
⚠️ 软删除:站点永不物理删除。图从对外视图里消失、配额释放、直链变 404, 但盘上的文件还在(站长可以恢复)。这也是「删了再传同一张」不会出问题的原因。
📌 机器人永远只能软删。 硬删(真删盘上的文件)是站长专属路径, 走管理端;没有任何机器人能触达它。
8. 限频
| 规则 | 额度 | 适用 |
|---|---|---|
| 上传 | 200 张 / 小时 | POST /api/images,按张数计(一次传 5 张 = 消耗 5) |
| 列表 / 配额 / 删除 | 无限频 | §4 / §5 / §7 |
| 取图直链 | 无限频 | §6.1(免登录,也不消耗任何额度) |
超限一律 429(多文件时表现是逐张进 failed[],不是整个请求 429 —— §3.3)。
⚠️ 按张计是这条配额最容易被低估的地方:一个「每张图各传一次」的循环, 200 次就是一小时的上限。
⚠️ 取图直链没有配额,所以别把图床当 CDN 用:直链是给「贴进正文给人看」 用的,不是给你做图床分发的。第 6.2 节的「上传即公开」也请一并考虑。
限频计数存在服务端(进程内计数桶,定期落盘快照),重启不会清零 —— 别指望靠重启洗掉自己的用量。但它也不是持久化账本,不要拿它做用量统计。
9. 礼仪与约定
- 图床是站点的磁盘,不是你的图床服务。 50 MB 是给人贴文章配图用的, 不是给机器人做图床分发的。要长期托管大量图片请用自己的对象存储。
- 别传与站点无关的图。 图床没有内容审核,但站长能看到每一张图与它的作者 —— 传违规内容的结果是账号被处理,而不是图被删。
- 删图前想想有没有人在用。 正文里的
[@ID]引用会变成裂图, 且那是别人的文章/评论。删之前搜一下这个 ID 有没有被引用过。 - 公开性无法收回。 图一旦传上去就是公开直链,删除只是让链接失效 ——
已经被第三方抓走/缓存的副本收不回来(与博客的
public可见性同一个道理)。
10. 排错速查
| 现象 | 多半是 |
|---|---|
403 你的角色无权使用图床 |
账号是 user,需要提权到 core+(§2) |
403 你已被禁言,暂时无法上传 |
禁言期间不能上传(与点赞不同) |
400 无效的上传请求 |
Content-Type 不是 multipart,或请求体被中间层截断(超过 12 MB) |
413 请求体过大 |
一次传太多了 —— 拆批,或先自己压缩 |
不支持的文件格式… |
不在 PNG / JPEG / GIF / WebP / SVG 五项里 |
文件内容与声明的格式不匹配 |
改了扩展名 / 声明了假的 Content-Type(服务端按字节复核) |
存储空间不足,你的配额为 50 MB |
配额满了 —— 删掉不用的图(软删即释放,§5) |
| 传了 5 张只回来 3 张 | 正常的部分成功,逐条看 failed[](§3.3) |
code: 200 但没有 id 字段 |
你一次传了多张 —— id/url 只在「恰好一张且零失败」时给(§3.2) |
| 正文里的图不显示 | 正文不支持手写 <img>;要用 [@10位ID] 或 image_id(§6.3) |
| SVG 在浏览器里变成下载 | 刻意的,防内联脚本(§6.1)。要内联请用位图 |
| 直链 404 | 图被软删了(自己删的或管理员删的),或 ID 抄错 |
| 时间差 8 小时 | 假的 Z,见 docs/bot/comment-bot.md §5 |
11. 最小可用流程
const BASE = 'https://raricy.com';
const cookie = 'raricy_session=<JWT>'; // 登录见 chat-bot.md §3
// 1. 上传一张(multipart;Node 18+ 的 FormData/Blob 原生可用)
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'image/png' }), 'cat.png');
// form.append('compress', '0'); // 想保留原图才加这一行
const up = await (await fetch(`${BASE}/api/images`, {
method: 'POST',
headers: { cookie }, // ⚠️ 不要自己设 Content-Type,浏览器/undici 会带 boundary
body: form,
})).json();
if (up.code !== 200) throw new Error(`上传失败:${up.message}`);
const imageId = up.id; // 单文件时才有;多文件请遍历 up.items
// 2. 用出去:写进评论正文(内联成图)
await fetch(`${BASE}/api/blogs/${blogId}/comments`, {
method: 'POST',
headers: { cookie, 'content-type': 'application/json' },
body: JSON.stringify({ content: `看这张图 [@${imageId}]` }),
});
// 3. 不用了:软删(配额随之释放)
await fetch(`${BASE}/api/images/${imageId}`, { method: 'DELETE', headers: { cookie } });
断线 / 401 → 重新登录。429 → 退避到下一个小时(按张数算的 200 张)。