// ==UserScript==
// @name 秒杀助手
// @namespace https://github.com/june-t
// @version 1.3.0
// @description 主流电商平台抢购秒杀用户脚本(淘宝/天猫/京东/拼多多等):按钮模式 + 购物车模式(跨页自动结算)、服务器时间校准、到点自动高频点击
// @author june
// @match *://*.taobao.com/*
// @match *://*.tmall.com/*
// @match *://*.tmall.hk/*
// @match *://*.jd.com/*
// @match *://*.jd.hk/*
// @match *://*.pinduoduo.com/*
// @match *://*.yangkeduo.com/*
// @match *://*.vip.com/*
// @match *://*.suning.com/*
// @match *://*.1688.com/*
// @match *://cashier.tmall.com/*
// @match *://cashdesk.taobao.com/*
// @match *://cashier.alipay.com/*
// @match *://mclient.alipay.com/*
// @match *://pay.jd.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_registerMenuCommand
// @connect acs.m.taobao.com
// @connect www.taobao.com
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
let offset = Number(GM_getValue('offset', 0)); // 服务器时间 - 本地时间(ms)
let picking = false;
let running = false;
let stopFlag = false;
let hoverEl = null;
let altSelector = ''; // 备用选择器(纯结构路径),主选择器因按钮状态变化失配时回退用
let panel, statusEl, ball;
const $ = (sel) => document.querySelector(sel);
// 校准后的当前毫秒时间戳
const now = () => Date.now() + offset;
const sleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)));
// 剩余秒数格式化为「X 分 YY 秒」
const fmtSec = (sec) => Math.floor(sec / 60) + ' 分 ' + (sec % 60) + ' 秒';
// ---------- 时间校准(跨域请求需在脚本头部声明 @connect) ----------
function syncTime() {
return new Promise((resolve) => {
const t0 = Date.now();
GM_xmlhttpRequest({
method: 'GET',
url: 'https://acs.m.taobao.com/gw/mtop.common.getTimestamp/',
timeout: 5000,
onload: (res) => {
const t1 = Date.now();
try {
const data = JSON.parse(res.responseText);
offset = Math.round(Number(data.data.t) - (t0 + t1) / 2);
GM_setValue('offset', offset);
} catch (e) { /* 保留旧 offset */ }
resolve(offset);
},
onerror: () => {
// 回退:淘宝首页响应头 Date(秒级精度)
GM_xmlhttpRequest({
method: 'HEAD',
url: 'https://www.taobao.com',
timeout: 5000,
onload: (res2) => {
const t1 = Date.now();
const m = (res2.responseHeaders || '').match(/date:\s*(.+)/i);
if (m) {
offset = Math.round(new Date(m[1]).getTime() - (t0 + t1) / 2);
GM_setValue('offset', offset);
}
resolve(offset);
},
onerror: () => resolve(offset)
});
}
});
});
}
// ---------- 工具 ----------
// 元素的稳定属性(name / data-*),不随按钮状态样式(如禁用 class)变化
function stableAttr(el) {
const attrs = el.attributes;
for (let i = 0; i < attrs.length; i++) {
const a = attrs[i];
if (a.name === 'name' && a.value) return '[name="' + CSS.escape(a.value) + '"]';
if (a.name.indexOf('data-') === 0 && a.value) {
return '[' + a.name + '="' + CSS.escape(a.value) + '"]';
}
}
return '';
}
// 生成两套 CSS 选择器:
// primary:含 id/name/data-*/class,定位准,但按钮状态样式变化(如禁用样式切换)可能失效
// relaxed:仅 tag+nth-of-type 结构路径,只要页面结构不变就始终有效,作为回退
function cssPaths(el) {
const build = (withAttrs) => {
if (!(el instanceof Element)) return '';
if (el.id) return '#' + CSS.escape(el.id);
const parts = [];
let node = el;
while (node && node.nodeType === 1 && node !== document.documentElement) {
let sel = node.tagName.toLowerCase();
if (node.id) {
parts.unshift('#' + CSS.escape(node.id));
break;
}
if (withAttrs) {
const attr = stableAttr(node);
if (attr) {
sel += attr;
} else {
const cls = [...node.classList].slice(0, 2).map((c) => '.' + CSS.escape(c)).join('');
if (cls) sel += cls;
}
}
let sib = node, n = 1;
while ((sib = sib.previousElementSibling)) {
if (sib.tagName === node.tagName) n++;
}
if (node.parentElement && node.parentElement.children.length > 1) {
sel += ':nth-of-type(' + n + ')';
}
parts.unshift(sel);
node = node.parentElement;
}
return parts.join(' > ');
};
return { primary: build(true), relaxed: build(false) };
}
// 判断元素当前是否处于可点击状态(到点前按钮常为置灰/禁用,此时不空点、持续重试)
function isClickable(el) {
if (!el) return false;
if (el.disabled) return false;
if (el.getAttribute && el.getAttribute('aria-disabled') === 'true') return false;
const cls = (typeof el.className === 'string' ? el.className : '').toLowerCase();
if (/(^|\s)disabled(\s|$)/.test(cls)) return false;
const st = getComputedStyle(el);
if (st.display === 'none' || st.visibility === 'hidden' || st.pointerEvents === 'none') return false;
return true;
}
// 模拟真实点击(同时触发原生 click 与鼠标事件,兼容 React/Vue 监听)
function fireClick(el) {
el.click();
const rect = el.getBoundingClientRect();
const opts = {
bubbles: true, cancelable: true, view: window,
clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2,
button: 0
};
el.dispatchEvent(new MouseEvent('mousedown', opts));
el.dispatchEvent(new MouseEvent('mouseup', opts));
el.dispatchEvent(new MouseEvent('click', opts));
}
// ---------- 可视化选取 ----------
function startPick() {
if (picking) return;
picking = true;
panel.style.display = 'none';
ball.style.display = 'none';
setStatus('选取模式:移动鼠标高亮目标,左键确认,Esc 取消');
const tip = document.createElement('div');
tip.textContent = '【秒杀助手】点击选取目标按钮,Esc 取消';
Object.assign(tip.style, {
position: 'fixed', top: '10px', left: '50%', transform: 'translateX(-50%)',
zIndex: 2147483647, background: 'rgba(255,68,0,.9)', color: '#fff',
padding: '6px 14px', borderRadius: '4px', font: '13px sans-serif',
pointerEvents: 'none'
});
document.body.appendChild(tip);
const onMove = (e) => {
if (hoverEl) hoverEl.style.outline = hoverEl.dataset.__oldOutline || '';
hoverEl = e.target;
if (!hoverEl.dataset.__oldOutline) {
hoverEl.dataset.__oldOutline = hoverEl.style.outline;
}
hoverEl.style.outline = '3px solid #ff4400';
tip.textContent = '【秒杀助手】' + (cssPaths(hoverEl).primary || hoverEl.tagName);
};
const onClick = (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target;
cleanup();
const paths = cssPaths(target);
altSelector = paths.relaxed;
$('#sk-selector').value = paths.primary;
setStatus('已选中:' + paths.primary);
saveConfig();
showPanel();
};
const onKey = (e) => {
if (e.key === 'Escape') {
cleanup();
showPanel();
setStatus('已取消选取');
}
};
function cleanup() {
picking = false;
tip.remove();
if (hoverEl) hoverEl.style.outline = hoverEl.dataset.__oldOutline || '';
document.removeEventListener('mousemove', onMove, true);
document.removeEventListener('click', onClick, true);
document.removeEventListener('keydown', onKey, true);
}
document.addEventListener('mousemove', onMove, true);
document.addEventListener('click', onClick, true);
document.addEventListener('keydown', onKey, true);
}
// ---------- 点击引擎 ----------
// 等待到 targetTs 前 leadMs 醒来,然后忙等精确对齐,再开始点击循环
async function runTask(cfg) {
const lead = Number(cfg.leadMs) || 300;
const interval = Math.max(30, Number(cfg.intervalMs) || 200);
const maxClicks = Number(cfg.maxClicks) || 60;
const targetTs = Number(cfg.targetTs);
const selector = cfg.selector;
const alt = cfg.selectorAlt || altSelector || '';
// 主选择器优先,按钮因状态变化重新渲染导致 class 失配时,回退到纯结构路径
const resolveEl = () => $(selector) || (alt ? $(alt) : null);
// 开抢 2 分钟后自动收尾,防止按钮始终不可点导致死循环
const deadline = targetTs + 120 * 1000;
running = true;
stopFlag = false;
setStatus('任务运行中:' + new Date(targetTs).toLocaleString() + ',目标 ' + selector);
// 开抢前 2 分钟桌面提醒
const remindAt = targetTs - 2 * 60 * 1000;
const remindTimer = setTimeout(() => {
GM_notification({
title: '秒杀助手',
text: '距离开抢还有 2 分钟,请确认页面已就绪!',
timeout: 10000
});
}, remindAt - now());
// 到点前先睡眠,实时显示倒计时,每 30s 重校时差,并校验元素仍在
let lastSec = -1;
let lastSyncAt = now();
while (!stopFlag) {
const remain = targetTs - now();
if (remain <= lead) break;
if (now() - lastSyncAt > 30000) {
await syncTime(); // 长等待期间周期性重校,抵消本地时钟漂移
lastSyncAt = now();
}
if (!resolveEl()) {
setStatus('警告:目标元素暂时不存在,继续等待…');
} else {
const sec = Math.ceil(remain / 1000);
if (sec !== lastSec) {
lastSec = sec;
setStatus('倒计时 ' + fmtSec(sec) + '(目标:' + selector.slice(0, 24) + ')');
}
}
await sleep(Math.min(remain - lead, 500));
}
clearTimeout(remindTimer);
if (stopFlag) return finish('已手动停止');
// 忙等对齐(<2ms 粒度),保证触发误差在毫秒级
while (!stopFlag && now() < targetTs) { /* spin */ }
if (stopFlag) return finish('已手动停止');
setStatus('开抢!正在点击…');
let clicks = 0;
let lastClick = 0;
while (!stopFlag && clicks < maxClicks && now() < deadline) {
const el = resolveEl();
// 禁用/置灰状态不空点,等待其变为可点(状态切换导致的选择器失配由 resolveEl 兜底)
if (isClickable(el)) {
fireClick(el);
clicks++;
lastClick = performance.now();
setStatus('开抢!已点击 ' + clicks + '/' + maxClicks + ' 次');
}
// 按 interval 节流(首轮立即点)
const wait = interval - (performance.now() - lastClick);
await sleep(wait > 2 ? wait : 1);
}
if (clicks >= maxClicks) {
finish('已点击 ' + clicks + ' 次后结束(请检查是否已进入下单页)');
} else if (now() >= deadline) {
finish('开抢后 2 分钟内未能成功点击,任务结束(按钮可能一直不可点或选择器失配)');
} else {
finish('已停止');
}
}
function finish(msg) {
running = false;
setStatus(msg);
saveConfig();
}
// ---------- 浮窗面板 + 悬浮球 ----------
function buildUI() {
if ($('#sk-helper-panel')) {
// 页面上已存在面板(脚本被重复注入/bfcache 恢复),重新绑定引用,避免悬空引用导致 .style 崩溃
panel = $('#sk-helper-panel');
ball = $('#sk-ball');
statusEl = $('.sk-status');
if (!ball) createBall();
return;
}
createBall();
panel = document.createElement('div');
panel.id = 'sk-helper-panel';
panel.innerHTML = `
秒杀助手
提前勾选好商品即可,结算按钮自动识别
`;
const style = document.createElement('style');
style.textContent = `
#sk-helper-panel{position:fixed;top:80px;right:20px;z-index:2147483646;width:320px;
background:#fff;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.15);
font:13px/1.6 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif;color:#333;padding:12px}
#sk-helper-panel .sk-head{font-weight:700;font-size:14px;margin-bottom:8px;display:flex;justify-content:space-between}
#sk-helper-panel .sk-clock{color:#ff4400;font-weight:400;font-variant-numeric:tabular-nums}
#sk-helper-panel .sk-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap}
#sk-helper-panel label{min-width:56px;color:#666}
#sk-helper-panel input{flex:1;min-width:60px;padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:12px}
#sk-helper-panel input[type="checkbox"]{flex:none;width:auto}
#sk-helper-panel select{padding:4px;border:1px solid #ccc;border-radius:4px;font-size:12px;background:#fff}
#sk-helper-panel button{padding:4px 12px;border:1px solid #ccc;border-radius:4px;background:#f7f7f7;cursor:pointer}
#sk-helper-panel button:hover{background:#eee}
#sk-helper-panel .sk-primary{background:#ff4400;border-color:#ff4400;color:#fff}
#sk-helper-panel .sk-primary:hover{background:#e63e00}
#sk-helper-panel .sk-status{color:#888;font-size:12px;word-break:break-all}
`;
document.body.appendChild(style);
document.body.appendChild(panel);
statusEl = $('.sk-status');
$('#sk-pick').onclick = startPick;
$('#sk-mode').onchange = applyModeVisibility;
$('#sk-start').onclick = startFromPanel;
$('#sk-stop').onclick = () => {
stopFlag = true;
GM_setValue('flow', null); // 中断跨页流程
};
$('#sk-hide').onclick = () => {
panel.style.display = 'none';
ball.style.display = 'block';
};
$('#sk-sync').onclick = async () => {
setStatus('校时中…');
await syncTime();
setStatus('校时完成,时差 ' + offset + 'ms');
};
// 面板可拖动
dragPanel(panel, $('.sk-head'));
// 时钟:显示校准后的服务器时间
setInterval(() => {
const c = $('.sk-clock');
if (c) c.textContent = new Date(now()).toLocaleTimeString('zh-CN', { hour12: false });
}, 200);
}
// 创建悬浮球(面板收起后从这里唤出)
function createBall() {
ball = document.createElement('div');
ball.id = 'sk-ball';
ball.textContent = '秒';
Object.assign(ball.style, {
position: 'fixed', bottom: '30px', right: '30px', zIndex: 2147483646,
width: '40px', height: '40px', borderRadius: '50%', background: '#ff4400',
color: '#fff', font: 'bold 16px/40px sans-serif', textAlign: 'center',
cursor: 'pointer', boxShadow: '0 2px 10px rgba(0,0,0,.3)',
userSelect: 'none'
});
ball.title = '打开秒杀助手面板(Ctrl+Shift+S)';
ball.onclick = showPanel;
document.body.appendChild(ball);
}
function showPanel() {
if (!panel || !ball) return; // 防御:UI 未就绪时不操作
panel.style.display = 'block';
ball.style.display = 'none';
// 从悬浮球位置展开:面板右下角对齐悬浮球,超出视口则收回边界内
// 悬浮球隐藏时(如选取模式结束后)按其固定位置(右下角 30px 处)计算
const r = ball.style.display === 'none'
? { left: window.innerWidth - 70, top: window.innerHeight - 70, width: 40, height: 40 }
: ball.getBoundingClientRect();
const left = Math.max(8, Math.min(r.left + r.width - panel.offsetWidth, window.innerWidth - panel.offsetWidth - 8));
const top = Math.max(8, Math.min(r.bottom - panel.offsetHeight, window.innerHeight - panel.offsetHeight - 8));
panel.style.right = 'auto';
panel.style.left = left + 'px';
panel.style.top = top + 'px';
}
function dragPanel(box, handle) {
handle.onmousedown = (e) => {
e.preventDefault();
const sx = e.clientX - box.offsetLeft, sy = e.clientY - box.offsetTop;
const move = (ev) => {
box.style.left = ev.clientX - sx + 'px';
box.style.top = ev.clientY - sy + 'px';
box.style.right = 'auto';
};
const up = () => {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
};
}
function setStatus(text) {
if (statusEl) statusEl.textContent = text;
console.log('[秒杀助手]', text);
}
// 按模式显隐对应配置行
function applyModeVisibility() {
const mode = $('#sk-mode').value;
panel.querySelectorAll('.sk-btnonly').forEach((el) => { el.style.display = mode === 'btn' ? '' : 'none'; });
panel.querySelectorAll('.sk-cartonly').forEach((el) => { el.style.display = mode === 'cart' ? '' : 'none'; });
}
// ---------- 配置持久化 ----------
function readConfig() {
const timeVal = $('#sk-time').value; // yyyy-MM-ddTHH:mm:ss
return {
timeVal,
targetTs: timeVal ? new Date(timeVal).getTime() : 0,
mode: $('#sk-mode').value,
selector: $('#sk-selector').value,
autoSubmit: $('#sk-autosubmit').checked,
autoPay: $('#sk-autopay').checked,
intervalMs: $('#sk-interval').value,
maxClicks: $('#sk-max').value,
leadMs: $('#sk-lead').value
};
}
function saveConfig() {
const cfg = readConfig();
cfg.selectorAlt = altSelector;
GM_setValue('config', cfg);
}
function restoreConfig() {
const cfg = GM_getValue('config', null);
if (!cfg) return;
$('#sk-time').value = cfg.timeVal || '';
$('#sk-selector').value = cfg.selector || '';
$('#sk-autosubmit').checked = !!cfg.autoSubmit;
$('#sk-autopay').checked = !!cfg.autoPay;
$('#sk-interval').value = cfg.intervalMs || 200;
$('#sk-max').value = cfg.maxClicks || 60;
$('#sk-lead').value = cfg.leadMs || 300;
altSelector = cfg.selectorAlt || '';
if (cfg.mode) $('#sk-mode').value = cfg.mode;
applyModeVisibility();
}
async function startFromPanel() {
if (running) { setStatus('任务已在运行'); return; }
const cfg = readConfig();
if (!cfg.targetTs) return setStatus('请先设置开抢时间');
if (cfg.targetTs < now()) return setStatus('开抢时间已过,请重新设置');
// 启动前先校时,保证触发精度
await syncTime();
saveConfig();
if (cfg.mode === 'cart') {
// 跨页流程状态机:cart 阶段,结算跳转后由确认页接力
GM_setValue('flow', {
stage: 'cart',
targetTs: cfg.targetTs,
autoSubmit: !!cfg.autoSubmit,
autoPay: !!cfg.autoPay,
expires: cfg.targetTs + 120 * 1000,
reloads: 0
});
runCartTask(cfg);
} else {
if (!cfg.selector) return setStatus('请先选取目标按钮');
GM_setValue('flow', null);
runTask(cfg);
}
}
// ---------- 购物车模式 ----------
// 按可见文字查找按钮(结算/提交订单等),跨平台通用的兜底识别
// 先找原生交互元素,再找样式按钮类容器,避免命中外层包裹 div 导致点击无效
function findByText(regex) {
const groups = [
'button, a, input[type="button"], input[type="submit"]',
'div[role="button"], span[role="button"], [class*="btn"], [class*="button"]'
];
for (const sel of groups) {
for (const el of document.querySelectorAll(sel)) {
const t = (el.value || el.textContent || '').trim();
if (t && t.length <= 12 && regex.test(t)) return el;
}
}
return null;
}
// 平台识别(借鉴社区多平台抢购脚本积累的站点经验)
function platformKey() {
const h = location.hostname;
if (/taobao\.com$|tmall\.com$|tmall\.hk$|liangxinyao\.com$/.test(h)) return 'taobao';
if (/jd\.com$|jd\.hk$|yiyaojd\.com$/.test(h)) return 'jd';
if (/suning\.com$/.test(h)) return 'suning';
if (/vip\.com$|vipglobal\.hk$/.test(h)) return 'vip';
if (/vmall\.com$/.test(h)) return 'vmall';
if (/meizu\.com$/.test(h)) return 'meizu';
if (/lenovo\.com\.cn$/.test(h)) return 'lenovo';
if (/asus\.com\.cn$/.test(h)) return 'asus';
if (/xiaomiyoupin\.com$/.test(h)) return 'youpin';
if (/kaola\.com$/.test(h)) return 'kaola';
return 'generic';
}
// 各平台「结算」按钮选择器库(文字识别为主,这里是精准兜底)
const CHECKOUT_SELECTOR_DB = {
taobao: ['#J_Go', '#J_SmallSubmit', '[class*="submit-btn"]', '[class*="btn--"]'],
jd: ['a.submit-btn', 'a.common-submit-btn', '#order-submit', 'button.checkout-submit'],
suning: ['a.checkout-submit-btn'],
vip: ['#J_checkout', 'a[mars_sead="cart_checkout_btn"]'],
vmall: ['#checkoutSubmit', '.sc-total-btn'],
meizu: ['#submitForm'],
lenovo: ['.submitBtn'],
asus: ['button[class*="action-submit"]'],
youpin: ['a.m-btns'],
kaola: ['a.z-submitbtn']
};
const CHECKOUT_TEXT = /去?\s*结\s*算|领券结算/;
// 定位可点击的结算按钮:平台已知选择器 + 文字识别多路候选,容器自动下钻到内层按钮
function resolveCheckout() {
const cands = [];
for (const sel of (CHECKOUT_SELECTOR_DB[platformKey()] || [])) {
try {
const el = document.querySelector(sel);
if (el) cands.push(el);
} catch (e) { /* 忽略非法选择器 */ }
}
cands.push(findByText(CHECKOUT_TEXT));
for (const cand of cands) {
if (!cand) continue;
const el = descendToButton(cand);
// 候选必须与「结算」文案相关,防止数据库选择器误命中其他按钮
if (el && isClickable(el) && CHECKOUT_TEXT.test((el.textContent || '').trim())) {
return el;
}
}
return null;
}
// 选取时若点中包含结算文字的外层容器,向下找到最内层的按钮/文字节点
function descendToButton(el) {
let cur = el;
for (let depth = 0; depth < 5 && cur.children && cur.children.length; depth++) {
const kids = [...cur.children];
const next =
kids.find((c) => {
const t = (c.textContent || '').trim();
return t && t.length <= 12 && /结算/.test(t);
}) ||
kids.find((c) => /^(BUTTON|A)$/.test(c.tagName));
if (!next) break;
cur = next;
}
return cur;
}
// 各平台确认页「提交订单/立即支付」按钮选择器库
// 淘宝新版 PC 确认页提交与支付合并为「立即支付」,按钮类名为混淆哈希(btn--XXXX)
const SUBMIT_SELECTOR_DB = {
taobao: ['a.go-btn', '#submitOrder', '#order-submit', '[class*="submitOrder"]', '[class*="go-btn"]', '[class*="btn--"]'],
jd: ['#order-submit', 'button.checkout-submit'],
suning: ['a.checkout-submit-btn'],
vip: ['a.J_order_submit_btn'],
vmall: ['#checkoutSubmit'],
meizu: ['#submitForm'],
lenovo: ['.submitBtn'],
asus: ['button[class*="action-submit"]'],
youpin: ['a.m-btn-brown'],
kaola: ['a.z-submitbtn']
};
const SUBMIT_TEXT = /^(提交订单|立即支付)/;
// 定位可点击的「提交订单」按钮
function resolveSubmit() {
const cands = [];
for (const sel of (SUBMIT_SELECTOR_DB[platformKey()] || [])) {
try {
const el = document.querySelector(sel);
if (el) cands.push(el);
} catch (e) { /* 忽略非法选择器 */ }
}
cands.push(findByText(SUBMIT_TEXT));
for (const cand of cands) {
if (!cand) continue;
const el = descendToButton(cand);
if (el && isClickable(el) && SUBMIT_TEXT.test((el.textContent || '').trim())) {
return el;
}
}
return null;
}
// 自动勾选结算相关协议(定金/预售等,借鉴社区脚本经验),不涉及付款
function tickAgreements() {
for (const sel of ['label.pre-agree input[type="checkbox"]', '#presaleEarnest']) {
try {
const el = document.querySelector(sel);
if (el && !el.checked) el.click();
} catch (e) { /* 忽略 */ }
}
}
// 购物车模式入口:等待开抢(末秒监测结算按钮提前解锁),到点进入点击循环
async function runCartTask(cfg) {
const targetTs = cfg.targetTs;
const refreshWindow = 5 * 1000; // 开抢前 5 秒开始监测结算是否提前解锁
const deadline = targetTs + 120 * 1000;
running = true;
stopFlag = false;
setStatus('购物车模式:' + new Date(targetTs).toLocaleString() + ',请提前勾选商品并停留在此页');
// 开抢前 2 分钟桌面提醒
const remindAt = targetTs - 2 * 60 * 1000;
const remindTimer = setTimeout(() => {
GM_notification({
title: '秒杀助手',
text: '距离开抢还有 2 分钟,请停留在购物车页!',
timeout: 10000
});
}, remindAt - now());
// 阶段一:等待,实时显示倒计时,每 30s 重校时差
let lastSec = -1;
let lastSyncAt = now();
while (!stopFlag) {
const remain = targetTs - now();
if (remain <= refreshWindow) break;
if (now() - lastSyncAt > 30000) {
await syncTime(); // 长等待期间周期性重校,抵消本地时钟漂移
lastSyncAt = now();
}
if (!resolveCheckout()) {
setStatus('警告:当前页面未找到结算按钮,请确认在购物车页');
} else {
const sec = Math.ceil(remain / 1000);
if (sec !== lastSec) {
lastSec = sec;
setStatus('开抢倒计时 ' + fmtSec(sec));
}
}
await sleep(Math.min(remain - refreshWindow, 500));
}
// 阶段一点五:末秒监测(结算提前解锁就直接出手),显示亚秒倒计时
while (!stopFlag && now() < targetTs) {
if (isClickable(resolveCheckout())) {
setStatus('结算已解锁,提前出手!');
break;
}
setStatus('末秒倒计时 ' + Math.max(0, (targetTs - now()) / 1000).toFixed(1) + 's');
await sleep(250);
}
clearTimeout(remindTimer);
if (stopFlag) return finish('已手动停止');
cartActionLoop(cfg, {
stage: 'cart',
targetTs,
autoSubmit: cfg.autoSubmit,
expires: deadline,
reloads: 0
});
}
// 购物车模式阶段二:先勾选商品再点结算(结算按钮通常勾选后才解锁);
// 结算持续 5 秒不可点才刷新重试(刷新后由 resumeFlow 接力)
async function cartActionLoop(cfg, flow) {
running = true;
stopFlag = false;
let done = false;
let unclickableSince = 0;
while (!stopFlag && now() < flow.expires) {
// 定位并点击结算按钮(自动识别,无需选取;商品需提前勾选好)
const btn = resolveCheckout();
if (btn) {
fireClick(btn);
done = true;
GM_setValue('flow', { stage: 'confirm', autoSubmit: !!flow.autoSubmit, expires: now() + 5 * 60 * 1000 });
setStatus('已点击结算,等待跳转确认页…');
await sleep(3000); // 正常情况页面跳转,本页脚本随之销毁
// 未跳转(如校验失败弹窗),恢复 cart 阶段继续重试
GM_setValue('flow', {
stage: 'cart', targetTs: flow.targetTs, autoSubmit: flow.autoSubmit,
expires: flow.expires, reloads: flow.reloads
});
unclickableSince = 0;
} else {
if (!unclickableSince) unclickableSince = now();
setStatus('结算按钮不可点(已持续 ' + Math.round((now() - unclickableSince) / 1000) + ' 秒),重试中…');
// 持续 5 秒不可点才刷新购物车,避免无谓 reload
if (now() - unclickableSince > 5000 && flow.reloads < 15) {
flow.reloads++;
GM_setValue('flow', flow);
setStatus('刷新重试(' + flow.reloads + '/15)…');
await sleep(1000);
location.reload();
return;
}
await sleep(300);
}
}
GM_setValue('flow', null);
if (done) {
finish('结算点击后未能跳转,已停止(请检查商品是否可购)');
} else if (stopFlag) {
finish('已手动停止');
} else {
finish('购物车模式超时:结算始终不可点(可能已售罄)');
}
}
// 确认订单页处理:autoSubmit 开启时自动点「提交订单」,否则仅提醒
async function handleConfirmPage(flow) {
running = true;
stopFlag = false;
if (!flow.autoSubmit) {
GM_setValue('flow', null);
GM_notification({ title: '秒杀助手', text: '已进入确认订单页,请尽快手动提交订单!', timeout: 15000 });
setStatus('已进入确认页,请手动提交订单');
showPanel(); // 自动弹出面板,避免用户错过状态
return;
}
setStatus('确认页:等待「提交订单」可点…');
let tries = 0;
while (!stopFlag && now() < flow.expires) {
tickAgreements(); // 定金/预售协议自动勾选
const btn = resolveSubmit();
if (btn) {
fireClick(btn);
setStatus('已点击提交订单,检查后续确认弹窗…');
// 借鉴社区脚本:提交后可能出现「同意规则并付款」等中间弹窗,逐个确认
const t0 = now();
let dialogs = 0;
while (!stopFlag && now() - t0 < 8000 && dialogs < 3) {
const dlg = findByText(/^(同意.*付款|继续付款|确定|确认)$/);
if (dlg) {
fireClick(dlg);
dialogs++;
setStatus('已处理提交后弹窗(' + dialogs + '/3)…');
await sleep(500);
continue;
}
await sleep(400);
}
if (flow.autoPay) {
// 自动付款开启:接力到收银台阶段
GM_setValue('flow', { stage: 'pay', expires: now() + 3 * 60 * 1000 });
} else {
GM_setValue('flow', null);
}
GM_notification({
title: '秒杀助手',
text: flow.autoPay ? '订单已提交,正在前往付款…' : '订单已自动提交,请尽快付款!',
timeout: 15000
});
finish(flow.autoPay ? '订单已提交,正在前往付款…' : '已自动提交订单,请尽快付款!');
return;
}
tries++;
if (tries % 10 === 0) setStatus('确认页:尚未定位到可点的「提交订单」按钮(已重试 ' + tries + ' 次)…');
await sleep(300);
}
GM_setValue('flow', null);
finish(stopFlag ? '已手动停止' : '确认页处理超时,请手动提交订单');
}
// 各平台收银台付款按钮选择器库
const PAY_SELECTOR_DB = {
taobao: ['#J_authSubmit'],
jd: ['#payButton']
};
const PAY_TEXT = /立即付款|确认付款|立即支付|去支付|马上支付/;
// 定位可点击的付款按钮(收银台常为 Taro/React 自定义组件渲染,扫描范围放宽)
function resolvePay() {
const cands = [];
for (const sel of (PAY_SELECTOR_DB[platformKey()] || [])) {
try {
const el = document.querySelector(sel);
if (el) cands.push(el);
} catch (e) { /* 忽略非法选择器 */ }
}
// 宽域文字扫描:含自定义组件(taro-* 等)与支付相关类名
const els = document.querySelectorAll(
'button, a, div[role="button"], span[role="button"], input[type="button"], input[type="submit"], [class*="pay" i], [class*="btn" i], taro-button-core, taro-view-core'
);
for (const el of els) {
const t = (el.value || el.textContent || '').trim();
if (t && t.length <= 20 && PAY_TEXT.test(t)) cands.push(el);
}
for (const cand of cands) {
if (!cand) continue;
const el = descendToButton(cand);
if (el && isClickable(el) && PAY_TEXT.test((el.textContent || '').trim())) {
return el;
}
}
return null;
}
// 收银台处理:点「立即付款」→ 逐个确认中间弹窗(分期/优惠券/规则提示)→ 停在密码/验证界面
async function handlePayPage(flow) {
running = true;
stopFlag = false;
setStatus('收银台:等待付款按钮…');
const expires = flow.expires || now() + 3 * 60 * 1000;
let payClicked = false;
let dialogs = 0;
while (!stopFlag && now() < expires) {
if (!payClicked) {
const btn = resolvePay();
if (btn) {
fireClick(btn);
payClicked = true;
setStatus('已点击付款,检查后续确认弹窗…');
await sleep(500);
continue;
}
} else {
// 借鉴社区脚本:付款后常有中间确认弹窗(分期提示/优惠券/规则同意),逐个确认
const dlg = findByText(/^(继续支付|继续付款|确认付款|确定|确认)$/);
if (dlg) {
fireClick(dlg);
dialogs++;
setStatus('已处理确认弹窗(' + dialogs + '/3)…');
if (dialogs >= 3) break;
await sleep(500);
continue;
}
// 没有更多弹窗 → 已进入密码/验证界面,交还用户
GM_setValue('flow', null);
GM_notification({ title: '秒杀助手', text: '已点击付款,请在密码/验证界面完成支付!', timeout: 15000 });
finish('已点击付款,请完成密码/验证(此步始终手动)');
return;
}
await sleep(300);
}
GM_setValue('flow', null);
finish(stopFlag ? '已手动停止' : '收银台处理超时,请手动完成付款');
}
// DOM 兜底识别(URL 无法判定时的回退)
function pageKind() {
if (resolveSubmit()) return 'confirm';
// 购物车页:结算按钮存在即可(可能尚未解锁),不要求可点
for (const sel of (CHECKOUT_SELECTOR_DB[platformKey()] || [])) {
try {
if (document.querySelector(sel)) return 'cart';
} catch (e) { /* 忽略非法选择器 */ }
}
if (findByText(CHECKOUT_TEXT)) return 'cart';
if (findByText(/立即付款|确认付款|去支付|立即支付/)) return 'pay';
return 'other';
}
// Aice 式站点派发:按 URL 判定页面角色,各页面自激活(DOM 识别兜底)
function siteId() {
const href = location.href;
if (/cart\.(taobao|tmall)\.com/.test(href)) return 'cart';
if (/cashier\.(tmall|alipay)\.com|cashdesk\.taobao\.com|mclient\.alipay\.com|pay\.jd\.com/.test(href)) return 'pay';
if (/confirmOrderWap\.htm|confirm_order\.htm|order\/confirm|trade\.jd\.com\/shopping\/order/.test(href)) return 'confirm';
if (/buy\.(taobao|tmall)\.com|trade\.jd\.com/.test(href)) return 'confirm';
return pageKind(); // 其他平台走 DOM 兜底识别
}
// 跨页流程恢复:URL 派发 + GM 存储接力(Aice 模式:开关持久化,页面各自认领任务)
function resumeFlow() {
const flow = GM_getValue('flow', null);
if (!flow) return;
if (!flow.expires || flow.expires < now()) {
GM_setValue('flow', null);
return;
}
const site = siteId();
const stage = flow.stage;
// 购物车页:结算点击循环(刷新重试由此接力)
if (site === 'cart' && stage === 'cart') {
const cfg = GM_getValue('config', null);
if (cfg && cfg.mode === 'cart') {
setStatus('购物车模式:刷新后继续抢购流程…');
cartActionLoop(cfg, flow);
} else {
GM_setValue('flow', null);
}
return;
}
// 确认订单页:提交订单(含弹窗)
if (site === 'confirm' && (stage === 'confirm' || stage === 'pay')) {
handleConfirmPage(flow);
return;
}
// 收银台:付款
if (site === 'pay' && (stage === 'pay' || stage === 'confirm')) {
handlePayPage(flow);
return;
}
// 其他平台的 DOM 兜底
if (site === 'other') {
const kind = pageKind();
if (stage === 'confirm' && kind === 'confirm') return handleConfirmPage(flow);
if (stage === 'pay' && (kind === 'pay' || kind === 'confirm')) return handlePayPage(flow);
if (stage === 'cart' && kind === 'cart' && now() >= flow.targetTs - 5000) {
const cfg = GM_getValue('config', null);
if (cfg && cfg.mode === 'cart') {
cartActionLoop(cfg, flow);
return;
}
}
}
// 提交成功后回到购物车等场景:流程已结束,清除防误触
GM_setValue('flow', null);
}
// ---------- 启动 ----------
buildUI();
restoreConfig();
panel.style.display = 'none'; // 默认收起,从悬浮球/菜单/快捷键唤出
syncTime();
resumeFlow(); // 恢复跨页流程(购物车刷新重试 / 确认页自动提交)
// 油猴菜单入口(Tampermonkey / 脚本猫均支持)
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('打开秒杀助手面板', showPanel);
}
// 快捷键 Ctrl+Shift+S 唤出面板
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && (e.key === 'S' || e.key === 's')) {
e.preventDefault();
showPanel();
}
});
})();