Worker + R2 作为 Twikoo 床图

ShinX 发布于 2025-10-06
笔记

前言

本站原本使用 SMMS 作为 Twikoo 评论的床图存储服务,但手贱把账号注销了😅。想重新注册却发现 SMMS 已关闭注册。

看了其他几种床图方案,要么收费,要么需要自己搭建服务器。

正好最近博客大改,图片都放在 R2,于是想:能不能写一个 Worker 来模拟这些服务提供的接口?

在各大文档转了一圈后发现,EasyImages 的接口最简单😎,所以决定直接用 EasyImages 的 API。

官方的API文档:EasyImages 2.0 API

Worker代码

EasyImages API 的上传流程非常简单:直接发送 token 和 image 即可。

例如前端上传表单:

<form action="http://127.0.0.1/api/index.php" method="post" enctype="multipart/form-data">
    <input type="file" name="image" accept="image/*" required>
    <input type="text" name="token" placeholder="在tokenList文件找到token并输入" required>
    <input type="submit" value="上传">
</form>

Worker 接收到请求后,验证 token,然后上传到 R2,返回格式与 EasyImages API 一致,例如:

{
    "result":"success",
    "code":200,
    "url":"https:\/\/i2.100024.xyz\/2023\/01\/24\/10gwv0y-0.webp",
    "srcName":"195124",
    "thumb":"https:\/\/png.cm\/application\/thumb.php?img=\/i\/2023\/01\/24\/10gwv0y-0.webp",
    "del":"https:\/\/png.cm\/application\/del.php?hash=bW8vWG4vcG8yM2pLQzRJUGI0dHlTZkN4L2grVmtwUTFhd1A4czJsbHlMST0="
}

为了防止滥用,可以在 Worker 中添加 访问速率控制,把访问 IP 保存到 KV。

下面是完整代码:

const customDomain = "https://cdn.zhengweixin.top/" // 访问域名
const prefix = "blog/comments/"                     // 上传路径
const TOKEN = "123456"                              // API Token
const RATE_LIMIT_WINDOW = 3600_000                  // 上传限制时间
const RATE_LIMIT_MAX = 5                            // 时间内可上传的次数
// 以上配置自行修改,并绑定KV和R2至该worker
export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 })
    }

    const ip = request.headers.get("CF-Connecting-IP") || "unknown"
    const limitKey = `upload:${ip}`
    const limitData = await env.KV.get(limitKey, { type: "json" }) || {
      count: 0,
      reset: Date.now() + RATE_LIMIT_WINDOW
    }

    if (Date.now() > limitData.reset) {
      limitData.count = 0
      limitData.reset = Date.now() + RATE_LIMIT_WINDOW
    }

    if (limitData.count >= RATE_LIMIT_MAX) {
      const retryAfter = Math.ceil((limitData.reset - Date.now()) / 1000)
      return new Response(JSON.stringify({
        result: "error",
        code: 429,
        message: `每小时只能上传 ${RATE_LIMIT_MAX} 次, 请稍后再试或自行找床图上传!`
      }), {
        headers: {
          "Content-Type": "application/json",
          "Retry-After": retryAfter.toString()
        },
        status: 429
      })
    }

    limitData.count++
    await env.KV.put(limitKey, JSON.stringify(limitData), { expirationTtl: RATE_LIMIT_WINDOW / 1000 })
    const reqClone = request.clone()
    const formData = await reqClone.formData()
    const file = formData.get("image")
    const token = formData.get("token")

    if (!token || token !== TOKEN) {
      return new Response(JSON.stringify({
        result: "error",
        code: 403,
        message: "无效的上传令牌"
      }), { headers: { "Content-Type": "application/json" }, status: 403 })
    }

    if (!file) {
      return new Response(JSON.stringify({ result: "error", code: 400, message: "没有文件上传" }), {
        headers: { "Content-Type": "application/json" },
        status: 400
      })
    }

    const arrayBuffer = await file.arrayBuffer()
    const ext = file.name.includes(".") ? file.name.substring(file.name.lastIndexOf(".")) : ""
    const timestamp = Date.now()
    const randomId = ('randomUUID' in crypto
      ? crypto.randomUUID()
      : Array.from(crypto.getRandomValues(new Uint8Array(16)).map(b => b.toString(16).padStart(2,'0')))
    ).replace(/-/g,'')
    const key = `${prefix}${timestamp}_${randomId}${ext}`

    try {
      await env.R2.put(key, arrayBuffer, { httpMetadata: { contentType: file.type } })
      return new Response(JSON.stringify({
        result: "success",
        code: 200,
        url: `${customDomain}${key}`,
        srcName: file.name,
        thumb: `${customDomain}${key}`,
        del: `${customDomain}del?key=${key}`
      }), {
        headers: { "Content-Type": "application/json" }
      })
    } catch (err) {
      return new Response(JSON.stringify({ result: "error", code: 500, message: err.message }), {
        headers: { "Content-Type": "application/json" },
        status: 500
      })
    }
  }
}

我也上传 Github 了:easyimg-api-r2

使用

代码开头的那些信息替换成自己的,然后部署到worker,并绑定R2和VK。

部署后前往 Twikoo 设置,IMAGE_CDN 填写 easyimageIMAGE_CDN_URL 填 Worker 的域名,IMAGE_CDN_TOKEN 填代码开头设置的 TOKEN 然后保存即可使用。

Twikoo设置

然后就可以在评论上传了!

Twikoo评论上传图片

到这里,你就有拥有了一个 10G 的免费高速的床图了,而且高达1000w次的读取额度和100w次的上传,完全是够用了。

而且还可以在我的博客后台进行管理,非常的方便。

当然,理论上在其他平台的云函数服务也可以部署,或者使用其他s3存储,但远远不及 Worker 直接绑定 R2 来的简单。

评论
© 2025 ShinX. All rights reserved.