作为大善人的忠实用户,R2 存储的免费额度(虽然只有10G)确实香,但总担心用量超标产生额外费用(其实完全没必要担心),手动每天查用量又太繁琐,于是动手搭建了一套自动监控系统,支持 Bark 实时推送、企业微信、飞书多渠道告警,全程零成本、零服务器,部署完彻底省心,实测稳定运行,今天把操作步骤做好记录,仅供需要的人参考。
一、监控核心功能
- ✅ 每日自动检测:配置凌晨1点定时执行,无需手动操作,后台静默运行;
- ✅ 多渠道推送:支持 Bark 、企业微信、飞书,按需开启,填了变量就发、不填不发;
- ✅ 防重复告警:解决同渠道重复推送问题,一次检测只发1条提醒;
- ✅ 异常容错:API 调用失败、无 R2 桶均不会崩溃,日志可查问题;
- ✅ 安全轻量:仅读取 R2 用量,无任何写入操作,API 令牌仅给只读权限,最小权限更安全。
二、前置准备(3样东西,提前准备好)
无需复杂工具,只要你有 Cloudflare 大善人账号,跟着准备以下3个核心内容即可:
- Cloudflare 账号 + 账号 ID(登录后可查,下文有具体路径)
- R2 只读权限 API Token(专门用于读取用量,不涉及修改/删除,安全可控)
推送渠道(三选一或全选或不选):
- Bark:iPhone 端,需获取完整 Bark 推送 URL(格式:https://api.day.app/你的设备密钥)
- 企业微信:需创建机器人,获取 Webhook Key
- 飞书:创建自定义机器人,获取 Hook Key
三、完整部署步骤
步骤1:创建 Cloudflare Workers
- 登录 Cloudflare 后台,找到「Workers \& Pages」,点击「创建应用程序」
- 选择「从 Hello World\! 开始」,给 Worker 起个好记的名字(比如 r2-usage-monitor),名字仅用于识别,不影响功能
- 点击「部署」,进入代码编辑界面(此时默认是 Hello World 代码,后续替换成我们的监控脚本)
步骤2:创建 R2 只读 API Token
API Token 是脚本读取 R2 用量的凭证,必须只给只读权限,避免泄露后造成风险,步骤如下:
- 访问 Cloudflare API Token 页面:https://dash.cloudflare.com/profile/api-tokens
- 点击右上角「创建令牌」,选择「创建自定义令牌」→「开始使用」
权限配置(重点):
- 令牌名称:自定义即可
- 权限类型:账户 → Workers R2 存储 → 读取
- 权限等级:读取(仅读取,其他全部保持默认不用动)
资源配置:
- 帐户资源:选择你自己的 Cloudflare 账号
- 有效期(TTL):不用设置有效期,避免后续过期需要重新创建
- 点击「继续以显示摘要」,确认权限无误后,点击「创建令牌」
- 生成 Token 后,立刻复制保存(仅显示一次,丢失需重新创建)
步骤3:配置环境变量
环境变量用于存储敏感信息(账号 ID、API Token、推送密钥),无需写在代码里,后续可随时修改,步骤如下:
- 回到创建好的 Worker 详情页,找到「变量和机密」,点击「+ 添加」
- 按以下列表添加变量,每个变量单独添加:
| 变量名 | 是否必填 | 填写说明 |
|---|---|---|
| CLOUDFLARE_ACCOUNT_ID | ✅ 必填 | Cloudflare 账号 ID,登录后在地址栏中域名后的那一串字符 |
| CLOUDFLARE_API_TOKEN | ✅ 必填 | 步骤2中创建的 R2 只读 API Token |
| WECOM_WEBHOOK_KEY | ❌ 可选 | 企业微信机器人 Webhook 链接中「key=」后面的部分(不填则不推送) |
| BARK_URL | ❌ 可选 | iPhone Bark 完整推送 URL(格式:https://api.day.app/设备密钥,不填则不推送) |
| LARK_WEBHOOK_KEY | ❌ 可选 | 飞书机器人 Hook 链接中「/hook/」后面的部分(不填则不推送) |
添加完成后,点击「保存」,环境变量配置完毕。
步骤4:替换监控脚本
回到 Worker 代码编辑界面,删除默认的 Hello World 代码,复制下面的代码,粘贴后点击「保存并部署」:
export default {
async scheduled(event, env, ctx) {
ctx.waitUntil(r2UsageCheck(env, true));
},
async fetch(request, env) {
// 拦截CF预检OPTIONS请求,杜绝一次访问两次执行
if (request.method === "OPTIONS") {
return new Response(null, { status: 204 });
}
const result = await r2UsageCheck(env, false);
return new Response(result, {
headers: { "Content-Type": "text/plain;charset=utf-8" }
});
}
};
const CONFIG = {
freeLimitGB: 10, // R2免费总额度(默认10GB,无需修改)
warnThresholdGB: 9 // 预警阈值(超过9GB触发推送,可自行调整)
};
// 全局简易防重(单实例短时间防重复)
let lastRunTime = 0;
const COOLDOWN_MS = 5000;
async function r2UsageCheck(env, isSchedule) {
const now = Date.now();
// 5秒内重复执行直接拦截,彻底杜绝双击/双请求重复推送
if (now - lastRunTime < COOLDOWN_MS) {
return "⏸️ 短时间内重复请求已拦截,防止重复推送";
}
lastRunTime = now;
const { CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN } = env;
if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_API_TOKEN) {
return "❌ 缺少必要环境变量";
}
try {
const res = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/r2/buckets`,
{ headers: { Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}` } }
);
const data = await res.json();
if (!data.success) return "❌ API权限错误:" + JSON.stringify(data.errors);
const buckets = Array.isArray(data.result) ? data.result : [];
let totalBytes = 0;
for (const bucket of buckets) {
try {
const usageRes = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/r2/buckets/${bucket.name}/usage`,
{
headers: { Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}` },
signal: AbortSignal.timeout(3000)
}
);
const usage = await usageRes.json();
if (usage.success && usage.result?.size) {
totalBytes += usage.result.size;
}
} catch (e) {}
}
const totalGB = totalBytes / 1073741824;
const msg = `【R2存储用量告警】
当前已用:${totalGB.toFixed(2)} GB
免费额度:${CONFIG.freeLimitGB} GB
预警阈值:${CONFIG.warnThresholdGB} GB`;
// 单次流程里 每个渠道只执行一次
if (totalGB >= CONFIG.warnThresholdGB) {
if (env.BARK_URL) await sendBark(env.BARK_URL, msg);
if (env.WECOM_WEBHOOK_KEY) await sendWeCom(env.WECOM_WEBHOOK_KEY, msg);
if (env.LARK_WEBHOOK_KEY) await sendLark(env.LARK_WEBHOOK_KEY, msg);
}
return "✅ 执行成功\n" + msg;
} catch (err) {
return "❌ 监控异常:" + err.message;
}
}
async function sendBark(barkUrl, text) {
try {
await fetch(`${barkUrl}/R2存储提醒/${encodeURIComponent(text)}`, {
method: "GET"
});
} catch {}
}
async function sendWeCom(key, text) {
try {
await fetch(`https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ msgtype: "text", text: { content: text } })
});
} catch {}
}
async function sendLark(key, text) {
try {
await fetch(`https://open.feishu.cn/open-apis/bot/v2/hook/${key}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ msg_type: "text", content: { text } })
});
} catch {}
}步骤5:配置定时触发器
脚本部署完成后,需要配置定时任务,让它每天自动检测,步骤如下:
- 在 Worker 详情页,找到「触发事件」,点击「+ 添加」
- 触发器类型:选择「Cron 触发器」
- Cron 表达式:填写
0 1 \* \* \*(每天凌晨1点自动执行,可自行调整时间) - 点击「保存」,定时触发器配置完成
四、常见问题排查(我踩过的坑)
问题1:访问 Worker 地址超时(ERR\_CONNECTION\_TIMED\_OUT)
原因:Cloudflare Workers 自带的 *.workers.dev 域名被墙了,属于正常现象,不影响定时任务执行。
解决:无需处理,定时任务会在 Cloudflare 云端正常运行,若想本地测试,可尝试使用手机流量访问,或给 Worker 绑定自定义域名。
问题2:日志空白,访问后无记录
原因:网络拦截导致请求未到达 Cloudflare 服务器,或 API Token 权限错误。
解决:切换手机流量访问 Worker 地址,同时检查 API Token 权限是否仅为 R2 读取,账号 ID 是否填写正确。
问题3:同渠道收到两条重复通知
原因:Cloudflare Workers 访问时会触发 OPTIONS 预检请求 + 正式请求,导致脚本执行两次。
解决:本文提供的最终版代码已拦截预检请求,并添加了5秒冷却防重(可自己延长或缩短时间),无需额外操作。
问题4:推送失败(Bark/企业微信收不到消息)
排查步骤:
- 检查环境变量是否填写正确(BARK\_URL 需完整,且不需要跟
/,企业微信仅填 Key); - 查看 Worker 日志,是否有推送失败提示;
- 用本文提供的测试代码(强制推送),验证推送渠道是否正常;
五、测试方法(确保监控正常运行)
部署完成后,可通过以下方式测试,确保功能正常:
- 把脚本中的
warnThresholdGB: 9改为0,访问 Worker 地址,查看 Bark/企业微信是否收到推送; - 测试完成后,将参数恢复即可;
- 查看 Worker 日志(Observability → 实时日志),确认脚本执行成功。
六、总结
这套 R2 监控系统,全程零成本、零服务器,部署一次,终身省心。每天自动检测用量,超标后实时推送提醒,再也不用手动登录 Cloudflare 查用量,避免不小心超标产生费用(虽然对于小破站来讲并不会超标)。
核心优势:配置简单、安全轻量、多渠道推送、防重复告警,新手也能轻松上手(最主要是不用花钱)
如果你在部署过程中遇到任何问题,欢迎在评论区交流!
评论