// ==UserScript== // @name 秒杀助手 // @namespace https://github.com/june-t // @version 1.2.5 // @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')) return; // 悬浮球:面板收起后从这里唤出 ball = document.createElement('div'); 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); 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 showPanel() { 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 ? '已手动停止' : '收银台处理超时,请手动完成付款'); } // 跨页流程恢复:页面跳转/刷新后由 GM 存储接力 function resumeFlow() { const flow = GM_getValue('flow', null); if (!flow) return; if (!flow.expires || flow.expires < now()) { GM_setValue('flow', null); return; } if (flow.stage === 'confirm') { handleConfirmPage(flow); } else if (flow.stage === 'pay') { handlePayPage(flow); } else if (flow.stage === 'cart' && now() >= flow.targetTs - 5000) { // 购物车刷新重试:跳过等待阶段直接进入点击循环 const cfg = GM_getValue('config', null); if (cfg && cfg.mode === 'cart') { setStatus('购物车模式:刷新后继续抢购流程…'); cartActionLoop(cfg, flow); } else { 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(); } }); })();