[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-use-r2-as-twikoo-img":3},{"success":4,"message":5,"frontmatter":6,"content":13},true,"获取成功",{"slug":7,"title":8,"date":9,"description":10,"tags":11,"published":4},"use-r2-as-twikoo-img","Worker + R2 作为 Twikoo 床图","2025-10-06","用 Worker 模拟 EasyImages API 作为 Twikoo 评论床图，免费的 R2 存储代替 Twikoo 支持的几种付费床图。",[12],"笔记","## 前言\n\n本站原本使用 **SMMS** 作为 Twikoo 评论的床图存储服务，但手贱把账号注销了😅。想重新注册却发现 SMMS 已关闭注册。\n\n看了其他几种床图方案，要么收费，要么需要自己搭建服务器。\n\n正好最近博客大改，图片都放在 R2，于是想：能不能写一个 Worker 来模拟这些服务提供的接口？\n\n在各大文档转了一圈后发现，EasyImages 的接口最简单😎，所以决定直接用 EasyImages 的 API。\n\n官方的API文档：[EasyImages 2.0 API](https:\u002F\u002Fgithub.com\u002Ficret\u002FEasyImages2.0\u002Fblob\u002Fmaster\u002Fdocs\u002FAPI.md)\n\n## Worker代码\n\nEasyImages API 的上传流程非常简单：直接发送 token 和 image 即可。\n\n例如前端上传表单：\n\n```html\n\u003Cform action=\"http:\u002F\u002F127.0.0.1\u002Fapi\u002Findex.php\" method=\"post\" enctype=\"multipart\u002Fform-data\">\n    \u003Cinput type=\"file\" name=\"image\" accept=\"image\u002F*\" required>\n    \u003Cinput type=\"text\" name=\"token\" placeholder=\"在tokenList文件找到token并输入\" required>\n    \u003Cinput type=\"submit\" value=\"上传\">\n\u003C\u002Fform>\n```\n\nWorker 接收到请求后，验证 token，然后上传到 R2，返回格式与 EasyImages API 一致，例如：\n\n```json\n{\n    \"result\":\"success\",\n    \"code\":200,\n    \"url\":\"https:\\\u002F\\\u002Fi2.100024.xyz\\\u002F2023\\\u002F01\\\u002F24\\\u002F10gwv0y-0.webp\",\n    \"srcName\":\"195124\",\n    \"thumb\":\"https:\\\u002F\\\u002Fpng.cm\\\u002Fapplication\\\u002Fthumb.php?img=\\\u002Fi\\\u002F2023\\\u002F01\\\u002F24\\\u002F10gwv0y-0.webp\",\n    \"del\":\"https:\\\u002F\\\u002Fpng.cm\\\u002Fapplication\\\u002Fdel.php?hash=bW8vWG4vcG8yM2pLQzRJUGI0dHlTZkN4L2grVmtwUTFhd1A4czJsbHlMST0=\"\n}\n```\n\n为了防止滥用，可以在 Worker 中添加 访问速率控制，把访问 IP 保存到 KV。\n\n下面是完整代码:\n\n```javascript\nconst customDomain = \"https:\u002F\u002Fcdn.zhengweixin.top\u002F\" \u002F\u002F 访问域名\nconst prefix = \"blog\u002Fcomments\u002F\"                     \u002F\u002F 上传路径\nconst TOKEN = \"123456\"                              \u002F\u002F API Token\nconst RATE_LIMIT_WINDOW = 3600_000                  \u002F\u002F 上传限制时间\nconst RATE_LIMIT_MAX = 5                            \u002F\u002F 时间内可上传的次数\n\u002F\u002F 以上配置自行修改，并绑定KV和R2至该worker\nexport default {\n  async fetch(request, env) {\n    if (request.method !== \"POST\") {\n      return new Response(\"Method Not Allowed\", { status: 405 })\n    }\n\n    const ip = request.headers.get(\"CF-Connecting-IP\") || \"unknown\"\n    const limitKey = `upload:${ip}`\n    const limitData = await env.KV.get(limitKey, { type: \"json\" }) || {\n      count: 0,\n      reset: Date.now() + RATE_LIMIT_WINDOW\n    }\n\n    if (Date.now() > limitData.reset) {\n      limitData.count = 0\n      limitData.reset = Date.now() + RATE_LIMIT_WINDOW\n    }\n\n    if (limitData.count >= RATE_LIMIT_MAX) {\n      const retryAfter = Math.ceil((limitData.reset - Date.now()) \u002F 1000)\n      return new Response(JSON.stringify({\n        result: \"error\",\n        code: 429,\n        message: `每小时只能上传 ${RATE_LIMIT_MAX} 次, 请稍后再试或自行找床图上传！`\n      }), {\n        headers: {\n          \"Content-Type\": \"application\u002Fjson\",\n          \"Retry-After\": retryAfter.toString()\n        },\n        status: 429\n      })\n    }\n\n    limitData.count++\n    await env.KV.put(limitKey, JSON.stringify(limitData), { expirationTtl: RATE_LIMIT_WINDOW \u002F 1000 })\n    const reqClone = request.clone()\n    const formData = await reqClone.formData()\n    const file = formData.get(\"image\")\n    const token = formData.get(\"token\")\n\n    if (!token || token !== TOKEN) {\n      return new Response(JSON.stringify({\n        result: \"error\",\n        code: 403,\n        message: \"无效的上传令牌\"\n      }), { headers: { \"Content-Type\": \"application\u002Fjson\" }, status: 403 })\n    }\n\n    if (!file) {\n      return new Response(JSON.stringify({ result: \"error\", code: 400, message: \"没有文件上传\" }), {\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n        status: 400\n      })\n    }\n\n    const arrayBuffer = await file.arrayBuffer()\n    const ext = file.name.includes(\".\") ? file.name.substring(file.name.lastIndexOf(\".\")) : \"\"\n    const timestamp = Date.now()\n    const randomId = ('randomUUID' in crypto\n      ? crypto.randomUUID()\n      : Array.from(crypto.getRandomValues(new Uint8Array(16)).map(b => b.toString(16).padStart(2,'0')))\n    ).replace(\u002F-\u002Fg,'')\n    const key = `${prefix}${timestamp}_${randomId}${ext}`\n\n    try {\n      await env.R2.put(key, arrayBuffer, { httpMetadata: { contentType: file.type } })\n      return new Response(JSON.stringify({\n        result: \"success\",\n        code: 200,\n        url: `${customDomain}${key}`,\n        srcName: file.name,\n        thumb: `${customDomain}${key}`,\n        del: `${customDomain}del?key=${key}`\n      }), {\n        headers: { \"Content-Type\": \"application\u002Fjson\" }\n      })\n    } catch (err) {\n      return new Response(JSON.stringify({ result: \"error\", code: 500, message: err.message }), {\n        headers: { \"Content-Type\": \"application\u002Fjson\" },\n        status: 500\n      })\n    }\n  }\n}\n```\n\n我也上传 Github 了：[easyimg-api-r2](https:\u002F\u002Fgithub.com\u002Fzhengweixin0101\u002Feasyimg-api-r2)\n\n## 使用\n\n代码开头的那些信息替换成自己的，然后部署到worker，并绑定R2和VK。\n\n部署后前往 Twikoo 设置，`IMAGE_CDN` 填写 `easyimage` ，`IMAGE_CDN_URL` 填 Worker 的域名，`IMAGE_CDN_TOKEN` 填代码开头设置的 `TOKEN` 然后保存即可使用。\n\n![Twikoo设置](https:\u002F\u002Fcdn.zhengweixin.top\u002Fblog\u002Fposts\u002F1759793345940_af56508a77aa44d3baca48fe4f6e0260.png)\n\n然后就可以在评论上传了!\n\n![Twikoo评论上传图片](https:\u002F\u002Fcdn.zhengweixin.top\u002Fblog\u002Fposts\u002F1763776060475_8ea381f10e604e04a8a08ba5dca6e2c4.png)\n\n\n到这里，你就有拥有了一个 10G 的免费高速的床图了，而且高达1000w次的读取额度和100w次的上传，完全是够用了。\n\n而且还可以在我的博客后台进行管理，非常的方便。\n\n当然，理论上在其他平台的云函数服务也可以部署，或者使用其他s3存储，但远远不及 Worker 直接绑定 R2 来的简单。\n"]