文档索引

图床机器人接入说明

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

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

0. 一句话说清

图床就是传图换一个 10 位 ID,之后到处用这个 ID:

  1. POST /api/images —— multipart 上传,拿回 id(10 位字母数字);
  2. 用 [@<10位ID>] 写进评论 / 讨论正文(内联成图),或者用 image_id 字段把图作为附件发出去;
  3. 不想用了就 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,owner 100 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 张)。

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