Taobao Auto Buyer (v2.1 - Refresh Window)

Automated Taobao flash buying script with scheduled purchasing, price range targeting, aggressive refresh window, automated checkout and order submission. Features dry run mode for safe testing.

// ==UserScript==
// @name         Taobao Auto Buyer (v2.1 - Refresh Window)
// @name:zh-CN   淘宝自动抢购脚本 (v2.1 - 刷新窗口版)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Automated Taobao flash buying script with scheduled purchasing, price range targeting, aggressive refresh window, automated checkout and order submission. Features dry run mode for safe testing.
// @description:zh-CN  淘宝自动抢购脚本,支持定时购买、价格区间筛选、激进刷新窗口、自动结算和订单提交。包含安全测试模式,专为秒杀活动优化。
// @author       dexhunter
// @license      MIT
// @match        *://cart.taobao.com/cart.htm*
// @match        *://buy.taobao.com/auction/order/confirm_order.htm*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  (() => {
    // ===================== CONFIG =====================
    const MIN = 2000;
    const MAX = 6000;
    const TARGET_TIME = "20:00:00"; // HH:MM:SS local time to start the buying window
    const BUYING_WINDOW_SECONDS = 5; // NEW: How long to aggressively refresh and check.
    const START_IN_MS = null; // Overrides TARGET_TIME for quick testing (e.g., 5000 for 5s)
    const START_EARLY_MS = 10_000; // How early to "wake up" before the target time
    const DRY_RUN = false; // Set to true for testing. Never clicks checkout/submit.
    const AUTO_CHECKOUT = true;
    const AUTO_SUBMIT = true;

    // ===================== SELECTORS =====================
    const ROW_SELECTOR = "[class*='cartItemInfoContainer']";
    const PRICE_INT = "span.trade-price-integer";
    const ANT_WRAP = "label.ant-checkbox-wrapper";
    const ANT_INPUT = "input.ant-checkbox-input";
    const CHECKOUT_CANDIDATES = ["[class*='btn--']", "button", "div", "a"];
    const ORDER_SUBMIT_MATCH_TEXT = /提交订单/;
    const ORDER_SUBMIT_CANDIDATES = ["[class*='btn--']", "div", "button", "a"];

    // ===================== UTILS =====================
    const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
    const num = (t) => {
      if (!t) return NaN;
      const x = (t + "").replace(/[,,\s¥¥]/g, "").replace(/[^0-9.]/g, "");
      return Number.isFinite(Number(x)) ? Number(x) : NaN;
    };
    const isClickable = (el) => {
      if (!el) return false;
      const s = getComputedStyle(el);
      if (s.display === "none" || s.visibility === "hidden" || s.opacity === "0" || s.pointerEvents === 'none') return false;
      const rect = el.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0;
    };
    const findByText = (candidates, re) => {
      for (const sel of candidates) {
        for (const el of document.querySelectorAll(sel)) {
          if (re.test((el.textContent || "").trim())) return el;
        }
      }
      return null;
    };
    const forceClick = (element) => {
      if (!element) return;
      const dispatchMouseEvent = (type) => {
        element.dispatchEvent(new MouseEvent(type, { view: window, bubbles: true, cancelable: true }));
      };
      console.log("[auto-buyer] Executing force click on:", element);
      dispatchMouseEvent('mousedown');
      dispatchMouseEvent('mouseup');
      dispatchMouseEvent('click');
    };

    // ===================== LOGIC FOR CART PAGE =====================

    async function attemptFromCart() {
      console.log(`[auto-buyer] Checking for items in price range...`);
      const rows = Array.from(document.querySelectorAll(ROW_SELECTOR));
      if (!rows.length) return { progressed: false };

      // First, deselect all items to ensure a clean slate.
      for (const row of rows) {
        const input = row.querySelector(ANT_INPUT);
        if (input && input.checked) {
          const wrapper = row.querySelector(ANT_WRAP);
          if (wrapper && !DRY_RUN) wrapper.click();
          await sleep(50); // Small pause between clicks
        }
      }

      // Now, find and select the target item.
      let itemFound = false;
      for (const row of rows) {
        const p = num(row.querySelector(PRICE_INT)?.textContent);
        if (Number.isFinite(p) && p > MIN && p < MAX) {
          console.log(`[auto-buyer] SUCCESS: Found item in price range (Price: ${p})`);
          itemFound = true;
          row.style.outline = "3px solid limegreen";
          const wrapper = row.querySelector(ANT_WRAP);
          if (wrapper && !DRY_RUN) {
            wrapper.click();
            await sleep(150); // Wait for price to recalculate
          }
          break; // Stop after finding the first one
        }
      }

      if (!itemFound) {
        console.log(`[auto-buyer] No item found in price range.`);
        return { progressed: false };
      }

      if (DRY_RUN || !AUTO_CHECKOUT) {
        console.log("[auto-buyer] DRY RUN: Would have clicked checkout.");
        return { progressed: false }; // Don't proceed, but don't refresh either.
      }

      const checkoutBtn = findByText(CHECKOUT_CANDIDATES, /结算/);
      if (!isClickable(checkoutBtn)) {
        console.warn("[auto-buyer] Checkout button not ready, will retry/refresh.");
        return { progressed: false };
      }

      console.log("[auto-buyer] Item selected. Clicking checkout button!");
      checkoutBtn.click();
      return { progressed: true };
    }

    async function aggressiveRefreshLoop(deadline) {
      console.log(`[auto-buyer] Entering refresh loop. Window ends at ${new Date(deadline).toLocaleString()}`);
      
      const result = await attemptFromCart();

      if (result.progressed) {
        console.log("[auto-buyer] Checkout initiated. Exiting loop.");
        sessionStorage.removeItem('buyWindowEndTime');
        return;
      }

      if (Date.now() < deadline) {
        console.log("[auto-buyer] Item not ready. Refreshing page to try again...");
        // Wait a very short, slightly random time before reloading
        await sleep(300 + Math.random() * 200);
        location.reload();
      } else {
        console.log(`[auto-buyer] ${BUYING_WINDOW_SECONDS}-second window has expired. Forfeiting attempt.`);
        sessionStorage.removeItem('buyWindowEndTime');
      }
    }

    async function scheduleCartChecker() {
      let target;
      if (START_IN_MS != null) {
        target = new Date(Date.now() + START_IN_MS);
        console.log(`[auto-buyer] Test run scheduled in ${START_IN_MS / 1000}s`);
      } else {
        const [hh, mm, ss] = TARGET_TIME.split(":").map(Number);
        target = new Date();
        target.setHours(hh, mm, ss || 0, 0);
        if (target <= new Date()) target.setDate(target.getDate() + 1);
        console.log(`[auto-buyer] Scheduled for ${target.toLocaleString()}`);
      }
      
      const msUntilTarget = target - new Date();
      if (msUntilTarget > START_EARLY_MS) {
        console.log("[auto-buyer] Sleeping until the ramp-up window.");
        await sleep(msUntilTarget - START_EARLY_MS);
      }

      console.log("[auto-buyer] Ramping up...");
      while (new Date() < target) await sleep(50);

      console.log("[auto-buyer] TARGET REACHED — starting aggressive refresh loop.");
      const deadline = Date.now() + (BUYING_WINDOW_SECONDS * 1000);
      sessionStorage.setItem('buyWindowEndTime', deadline);
      aggressiveRefreshLoop(deadline);
    }

    // ===================== LOGIC FOR CONFIRMATION PAGE =====================

    function handleConfirmPage() {
      console.log("[auto-buyer] Confirmation page logic activated. Waiting for submit button...");
      if (DRY_RUN || !AUTO_SUBMIT) {
        console.log("[auto-buyer] DRY RUN: Would have clicked submit order.");
        return;
      }
      
      let clickAttempted = false;
      const TIMEOUT_MS = 15000;
      const findAndClick = () => {
        if (clickAttempted) return;
        const submitBtn = findByText(ORDER_SUBMIT_CANDIDATES, ORDER_SUBMIT_MATCH_TEXT);
        if (isClickable(submitBtn)) {
          clickAttempted = true;
          console.log("[auto-buyer] Submit button is ready! Forcing click now.");
          forceClick(submitBtn);
          if (observer) observer.disconnect();
          if (timeoutId) clearTimeout(timeoutId);
        }
      };

      const observer = new MutationObserver(findAndClick);
      observer.observe(document.body, { childList: true, subtree: true });
      const timeoutId = setTimeout(() => {
        observer.disconnect();
        if (!clickAttempted) console.error(`[auto-buyer] FAILED: Submit button not found within ${TIMEOUT_MS / 1000}s.`);
      }, TIMEOUT_MS);
      
      findAndClick(); // Initial check
    }

    // ===================== BOOTSTRAPPER =====================
    
    function main() {
      console.log("[auto-buyer] Script loaded on:", location.href);
      if (location.pathname.includes("cart.htm")) {
        const windowEnd = sessionStorage.getItem('buyWindowEndTime');
        if (windowEnd && Date.now() < parseInt(windowEnd)) {
          // A refresh happened, we are inside the buying window.
          aggressiveRefreshLoop(parseInt(windowEnd));
        } else {
          // We are outside the window, schedule for the next time.
          sessionStorage.removeItem('buyWindowEndTime');
          scheduleCartChecker();
        }
      } else if (location.pathname.includes("confirm_order.htm")) {
        handleConfirmPage();
      }
    }

    main();
  })();
})();