CloudFlare Worker 免费部署 VPS 补货监控:实时追踪心仪小鸡 + 微信通知全攻略

前言

此工具致力于帮你监控心仪小鸡补货情况,并在补货时给你发送微信通知,避免没逛论坛错过买 🐔

手把手教程 + 原理讲解,面向新手/小白/中手。

操作更加详细具体,即使以前没用过 CloudFlare Works 也能跟着操作使用。

不需要配置 Telegram,直接发送通知到微信。

实现原理

根据网页中是否包含特定的缺货关键词进行是否缺货的判定, 当网页内容不包含缺货标志词时判定为有货。

例如:

  • 对于 BWG/DMIT/HostDZire 网页, 如果页面中包含 "out of stock" 则判定为缺货
  • 对于 Colocrossing, 如果页面中包含 "this service is not available" 则判定为缺货

因此你在添加监控对象时, 可以手动在浏览器中打开购买链接, 查看网页中缺货状态显示的关键词是什么, 添加到代码中的 TARGETS 数组中, 参考原有格式添加即可。

例如:对于 DMIT 和搬瓦工的网页, 如果页面中包含 "out of stock" 则判定为缺货。

TARGETS 数组中包含了: 监控名称, 链接, 缺货关键词, 补货提醒文字,后续会讲到。

操作步骤

登录或注册 CloudFlare,切换到 Workers & Pages

如果你找不到路径,也可以替换 account_id 快速访问:

登录之后浏览器链接中有一串很长的字符,例如:

https://dash.cloudflare.com/{account_id}/home/domains

复制 account_id 替换链接访问即可:

https://dash.cloudflare.com/{account_id}/workers-and-pages

创建 Worker

create worker

Get Started

选择第三个选项 "Start with Hello World!"
Get Started

取名

取一个 Worker name, 例如: "VPS 补货微信通知",然后点击右下角 Deploy
workername

编辑代码

Edit Code

替换代码

复制以下代码,进行替换
replace code

export default {
  async fetch(request, env, ctx) {
    // 默认页面响应
    const html = `
        <!DOCTYPE html>
        <html>
          <head>
            <title>Stock 监控</title>
            <meta charset="UTF-8">
            <style>
              body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
              h1 { color: #333; }
              p { color: #666; }
            </style>
          </head>
          <body>
            <h1>Stock 监控运行中</h1>
            <p>每分钟检查补货情况,如果补货则通过 Server 酱发送微信通知</p>
          </body>
        </html>
      `;
    await handleRequest(env);
    return new Response(html, {
      headers: { "Content-Type": "text/html; charset=utf-8" },
      status: 200,
    });
  },

  async scheduled(event, env, ctx) {
    ctx.waitUntil(handleRequest(env));
  },
};

async function handleRequest(env) {
  // Server 酱配置 - 获取环境变量中的 SCKEY
  const SCKEY = env.SCKEY;
  if (!SCKEY) {
    console.error("SCKEY environment variable not set");
    return new Response("SCKEY not configured", { status: 500 });
  }
  const SERVER_API = `https://sctapi.ftqq.com/${SCKEY}.send`;

  // 目标地址、对应的缺货标志词、名称和描述
  const TARGETS = [
    {
      name: "hostdzire 32刀",
      url: "https://hostdzire.com/billing/index.php?rp=/store/indian-cloudvps/in-cloudvps-5-nodeseek-special",
      outOfStockText: "out of stock",
      description: `hostdzire 32刀闪购补货了。 `,
    },
    {
      name: "colocrossing E3-2124G",
      url: "https://portal.colocrossing.com/register/order/service/592",
      outOfStockText: "this service is not available",
      description: `colocrossing E3-2124G有货了。`,
    },
  ];

  // 存储有货的目标信息
  const inStockTargets = [];

  async function checkStock() {
    // 记录循环开始
    console.log("Starting stock check for all targets...");

    for (const target of TARGETS) {
      console.log(`Checking target: ${target.name} (${target.url})`);
      try {
        // 设置超时(例如 10 秒)
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), 10000);

        const response = await fetch(target.url, {
          headers: {
            "User-Agent":
              "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
          },
          signal: controller.signal,
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }

        const text = await response.text();

        // 检查页面是否包含对应的缺货标志词
        if (!text.toLowerCase().includes(target.outOfStockText.toLowerCase())) {
          console.log(`Stock found for ${target.name}`);
          // 添加到有货列表
          inStockTargets.push({
            name: target.name,
            message: `🎉 ` + target.description + `\n\n🔗 ${target.url}`,
          });
        } else {
          console.log(`No stock for ${target.name}`);
        }
      } catch (error) {
        console.error(
          `Error checking ${target.name} (${target.url}):`,
          error.message
        );
        // 发送错误通知到 Server 酱
        await fetch(SERVER_API, {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          body: new URLSearchParams({
            title: "Stock监控错误",
            desp: `监控错误 (${target.name}): ${error.message}`,
          }),
        });
        // 继续循环,不中断
        continue;
      }
    }

    console.log("Completed stock check for all targets.");

    // 如果有货,发送合并通知
    if (inStockTargets.length > 0) {
      console.log(
        `Found ${inStockTargets.length} targets in stock, sending combined notification...`
      );
      // 合并消息
      const combinedMessage = inStockTargets
        .map((target) => target.message)
        .join("\n\n ----- \n\n");

      console.log(combinedMessage);

      const serverResponse = await fetch(SERVER_API, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          title: "🎉 Stock补货通知",
          desp: combinedMessage,
        }),
      });

      if (!serverResponse.ok) {
        console.error(
          `Failed to send Server 酱 notification: ${serverResponse.status}`
        );
      } else {
        console.log("Combined Server 酱 notification sent successfully.");
      }
    } else {
      console.log("No targets in stock, no Server 酱 notification sent.");
    }

    console.log(
      "Completed stock check for all targets with Server 酱 notification."
    );
  }

  // 立即执行一次检查
  await checkStock();

  // 返回响应(Cloudflare Workers需要返回Response对象)
  return new Response("Monitor running", { status: 200 });
}

Save
Save Changes
goback

点击左上角返回 Project 之后点击 Settings, 在 Variables and Secrets 中添加 Server 酱 Token
Settings
Variables

点击 Add 之后,环境变量类型选择 SecretVariable name 填写 SCKEYValue 填你的 Server 酱 Token
SCKEY

设置检测间隔时间

Trigger Events ~ Cron Triggers
trigger events

提示: 时间间隔不要太短,避免被限制访问

像搬瓦工的购买页面就没有限制,几分钟监控一次也没关系。DMIT 不要太频繁监控,如果你查看日志发现 403,建议创建一个新的 Worker 把时间间隔改长,然后删除旧的 Worker。

DMIT

即使像 DMIT 这种比较严格的,设置成 30 分钟检测一次也是可以的(下图左侧可见持续正常运行)。

DMIT stock

每家服务商情况不同,大部分都可以隔几分钟监控,具体可自行测试。日志查看方法见下文。

提示:或者你可以将限制严格和宽松的服务商分成(创建)两个不同的 Workers 运行,实现不同的检测时间间隔

Cron Triggers

Deploy 部署

deploy
点击 Deploy Version 之后继续点击 Deploy 即可部署修改后的版本。

添加或修改监控对象

如需添加监控对象, 参考原有格式编辑代码, 在 TARGETS 数组中添加监控对象即可

点击右上角(上图中标注位置)进入代码编辑界面,修改后示例:

const TARGETS = [
    {
      name: "hostdzire 32刀",
      url: "https://hostdzire.com/billing/index.php?rp=/store/indian-cloudvps/in-cloudvps-5-nodeseek-special",
      outOfStockText: "out of stock",
      description: `hostdzire 32刀闪购补货了。 `,
    },
    {
      name: "colocrossing E3-2124G",
      url: "https://portal.colocrossing.com/register/order/service/592",
      outOfStockText: "this service is not available",
      description: `colocrossing E3-2124G有货了。`,
    },
    {
      name: "DMIT",
      url: "https://www.dmit.io/cart.php?a=add&pid=205",
      outOfStockText: "out of stock",
      description: "DMIT xxx 补货了"
    }
  ];

提示:Cloudflare work 代码编辑界面也可以选中代码后按 Command + Shift + ↓ 或者 Ctrl + Shift + ↓ 进行复制, 复制一个监控对象之后再进行修改不容易出错

查看日志

Obse
blue
点击查看具体日志内容
Observability

实现优势和局限性

优点: 免费+门槛低, 无需服务器, 运维成本低, 支持定时执行

缺点: 持久化缺失, CloudFlare Workers 无状态, 不能区分 "刚补货" 和 "持续有货", 补货之后就会发送通知, 甚至会持续发送通知。

使用 CloudFlare KV 可以解决但会增加操作(后续可能考虑,暂不),对新手没那么友好。

// 建议添加状态管理
const KV_NAMESPACE = env.STOCK_STATUS; // 使用 CloudFlare KV 存储

async function checkStock() {
  for (const target of TARGETS) {
    // 获取上次状态
    const lastStatus = await KV_NAMESPACE.get(target.name);
    
    // 检查当前状态
    const currentStatus = !text.toLowerCase().includes(target.outOfStockText.toLowerCase());
    
    // 只在状态变化时发送通知
    if (lastStatus === 'out_of_stock' && currentStatus) {
      // 从缺货变为有货,发送通知
      await sendNotification(target);
    }
    
    // 保存当前状态
    await KV_NAMESPACE.put(target.name, currentStatus ? 'in_stock' : 'out_of_stock');
  }
}

不过补货之后实现了它的用途,修改或停用代码即可。

THE END