// ==UserScript== // @name 智谱 GLM Coding 抢购助手 v4.0 // @namespace http://tampermonkey.net/ // @version 4.6 // @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化 // @author Assistant // @match *://www.bigmodel.cn/* // @match *://bigmodel.cn/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; const VERSION = '4.6'; // ═══════════════════════════════════════════ // history 路由劫持 (防止 SPA 内跳转丢失状态) // ═══════════════════════════════════════════ const _pushState = history.pushState.bind(history); history.pushState = function(...args) { if (state.status === 'retrying' || state.timerId) { if (!confirm('抢购定时/进行中,确定要离开吗?')) return; stopAll(); } return _pushState(...args); }; history.replaceState = new Proxy(history.replaceState, { apply(target, thisArg, args) { if (state.status === 'retrying' || state.timerId) { if (!confirm('抢购定时/进行中,确定要离开吗?')) return; stopAll(); } return Reflect.apply(target, thisArg, args); } }); // ═══════════════════════════════════════════ // 版本号 // ═══════════════════════════════════════════ const CAPTURE_VER = 1; // ═══════════════════════════════════════════ // 配置 (localStorage 持久化) // ═══════════════════════════════════════════ const DEFAULT_CFG = { concurrency: 5, // 并发路数 (普通模式) turboConcurrency: 10, // 极速模式并发路数 turboSec: 5, // 极速模式持续秒数 maxRetry: 2000, // 最大重试次数 burstCount: 20, // 前N次零延迟爆发 fastDelay: 30, // 爆发后的快速间隔 slowDelay: 100, // 后期随机间隔中值 jitter: 0.3, // 间隔随机抖动 ±30% recoveryMax: 3, // 弹窗恢复最大次数 logMax: 100, // 日志条数上限 rushTime: '10:00:00', // 每天抢购时间 (北京时间) preAdvanceSec: 2, // 提前几秒触发,默认2秒 PREVIEW: '/api/biz/pay/preview', CHECK: '/api/biz/pay/check', }; function loadCfg() { try { const saved = JSON.parse(localStorage.getItem('glm_rush_cfg')); return { ...DEFAULT_CFG, ...saved }; } catch { return { ...DEFAULT_CFG }; } } function saveCfg(cfg) { const { PREVIEW, CHECK, ...save } = cfg; localStorage.setItem('glm_rush_cfg', JSON.stringify(save)); } const CFG = loadCfg(); // ═══════════════════════════════════════════ // 状态 (不可变更新) // ═══════════════════════════════════════════ let state = { status: 'idle', // idle | retrying | success | failed count: 0, bizId: null, captured: null, // 捕获的请求参数 cache: null, // 成功响应缓存 lastSuccess: null, proactive: false, timerId: null, logs: [], stats: { total: 0, success: 0, errors: 0, avgMs: 0, startTime: 0 }, }; function setState(patch) { Object.assign(state, patch); refreshUI(); } // 恢复上次捕获的请求 try { const raw = sessionStorage.getItem('glm_rush_captured'); if (raw) { const parsed = JSON.parse(raw); if (parsed.__v !== CAPTURE_VER) { sessionStorage.removeItem('glm_rush_captured'); } else { state.captured = parsed; } } } catch {} let stopRequested = false; let recovering = false; let recoveryAttempts = 0; let _shadowRef = null; let _activeControllers = []; // ═══════════════════════════════════════════ // 工具 // ═══════════════════════════════════════════ const sleep = ms => new Promise(r => setTimeout(r, ms)); const ts = () => new Date().toLocaleTimeString('zh-CN', { hour12: false }); const rand = (min, max) => min + Math.random() * (max - min); const jitteredDelay = base => Math.round(base * (1 + (Math.random() * 2 - 1) * CFG.jitter)); function getDelay(round) { if (round <= CFG.burstCount) return 0; // 前 N 轮零延迟 if (round <= 50) return jitteredDelay(CFG.fastDelay); return jitteredDelay(CFG.slowDelay); } function log(msg, level = 'info') { const entry = { ts: ts(), msg, level }; const logs = [...state.logs, entry]; if (logs.length > CFG.logMax) logs.splice(0, logs.length - CFG.logMax); state = { ...state, logs }; console.log(`[GLM] ${msg}`); appendLogDOM(entry); } function extractHeaders(h) { const o = {}; if (!h) return o; if (h instanceof Headers) h.forEach((v, k) => (o[k] = v)); else if (Array.isArray(h)) h.forEach(([k, v]) => (o[k] = v)); else Object.entries(h).forEach(([k, v]) => (o[k] = v)); return o; } // ═══════════════════════════════════════════ // Vue 2/3 兼容获取根实例 // ═══════════════════════════════════════════ function getVueRoot(selector = '#app') { const el = document.querySelector(selector); if (!el) return null; if (el.__vue__) return el.__vue__; // Vue 2 if (el.__vue_app__) return el.__vue_app__._instance?.proxy; // Vue 3 return null; } // ═══════════════════════════════════════════ // JSON.parse 定向拦截 (仅修改特定数据结构) // ═══════════════════════════════════════════ const _parse = JSON.parse; function patchSoldOut(obj, visited = new WeakSet()) { if (!obj || typeof obj !== 'object' || visited.has(obj)) return; if (obj.__ob__ !== undefined) return; // 跳过 Vue 响应式对象 if (obj.__v_isVNode || obj.__v_isRef) return; // 跳过 Vue 3 内部对象 visited.add(obj); if (obj.isSoldOut === true) obj.isSoldOut = false; if (obj.soldOut === true) obj.soldOut = false; if (obj.isServerBusy === true) obj.isServerBusy = false; if (obj.disabled === true && (obj.price !== undefined || obj.productId || obj.title)) obj.disabled = false; if (obj.stock === 0) obj.stock = 999; for (const k of Object.keys(obj)) { if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue; if (obj[k] && typeof obj[k] === 'object') patchSoldOut(obj[k], visited); } } // 全局 patch: 页面加载时也需要解除售罄状态,否则按钮不可点击 JSON.parse = function (text, reviver) { const result = _parse(text, reviver); try { patchSoldOut(result); } catch {} return result; }; Object.defineProperty(JSON.parse, 'toString', { value: () => 'function parse() { [native code] }' }); // ═══════════════════════════════════════════ // 核心: 并发重试引擎 // ═══════════════════════════════════════════ const _fetch = window.fetch; let _retryLock = null; async function singleAttempt(url, opts, attemptNum) { try { // 请求指纹随机化 — 每次请求看起来不一样,降低被识别为脚本的概率 const randHeaders = { ...opts.headers }; randHeaders['X-Request-Id'] = Math.random().toString(36).slice(2, 15); randHeaders['X-Timestamp'] = String(Date.now()); // 随机 Accept-Language 权重,让每次请求指纹不同 const q = (0.5 + Math.random() * 0.5).toFixed(1); randHeaders['Accept-Language'] = `zh-CN,zh;q=${q},en;q=${(q * 0.7).toFixed(1)}`; const resp = await _fetch(url, { ...opts, headers: randHeaders, credentials: 'include' }); // HTTP 状态码检测 if (resp.status === 401 || resp.status === 403) { return { ok: false, reason: `HTTP ${resp.status} 会话过期`, attempt: attemptNum }; } if (resp.status === 429) { return { ok: false, reason: '429 限流', attempt: attemptNum }; } const text = await resp.text(); let data; try { data = _parse(text); } catch { data = null; } if (data && data.code === 200 && data.data && data.data.bizId) { const bizId = data.data.bizId; // check 校验 try { const checkUrl = `${location.origin}${CFG.CHECK}?bizId=${encodeURIComponent(bizId)}`; const checkResp = await _fetch(checkUrl, { credentials: 'include' }); const checkText = await checkResp.text(); let checkData; try { checkData = _parse(checkText); } catch { checkData = null; } if (checkData && checkData.data === 'EXPIRE') { return { ok: false, reason: 'EXPIRE', attempt: attemptNum }; } // 新增:金额为0 = 空单,继续重试 const payData = checkData?.data; if (payData) { const amount = payData.amount ?? payData.totalAmount ?? payData.payAmount ?? 0; if (amount === 0) { return { ok: false, reason: '空单(金额为0)', attempt: attemptNum }; } } // 通过! return { ok: true, text, data, bizId, status: resp.status, attempt: attemptNum }; } catch (e) { return { ok: false, reason: `check异常: ${e.message}`, attempt: attemptNum }; } } const reason = !data ? '非JSON' : data.code === 555 ? '系统繁忙' : (data.data && data.data.bizId === null) ? '售罄' : `code=${data.code}`; return { ok: false, reason, attempt: attemptNum }; } catch (e) { if (e.name === 'AbortError') return { ok: false, reason: '已取消', attempt: attemptNum }; return { ok: false, reason: `网络: ${e.message}`, attempt: attemptNum }; } } async function retry(url, rawOpts) { if (_retryLock) { log('合并到当前重试...'); return _retryLock; } stopRequested = false; let roundNum = 0; const { signal, ...opts } = rawOpts || {}; _retryLock = (async () => { setState({ status: 'retrying', count: 0, stats: { ...state.stats, startTime: performance.now() } }); let totalAttempt = 0; let consecutiveErrors = 0; let throttleCount = 0; let consecutiveSoldOut = 0; while (totalAttempt < CFG.maxRetry && !stopRequested) { // 极速模式: 前N秒用更高并发 const elapsedMs = performance.now() - state.stats.startTime; const isTurbo = elapsedMs < CFG.turboSec * 1000; const curConcurrency = isTurbo ? CFG.turboConcurrency : CFG.concurrency; const batchSize = Math.min(curConcurrency, CFG.maxRetry - totalAttempt); const controllers = []; const promises = []; for (let j = 0; j < batchSize; j++) { totalAttempt++; const ac = new AbortController(); controllers.push(ac); promises.push( singleAttempt(url, { ...opts, signal: ac.signal }, totalAttempt) ); } _activeControllers = controllers; setState({ count: totalAttempt }); roundNum++; // 任一成功即取消其余 const winner = await new Promise(resolve => { let settled = false; let doneCount = 0; promises.forEach((p, idx) => { p.then(r => { if (r.ok && !settled) { settled = true; controllers.forEach((ac, i) => { if (i !== idx) try { ac.abort(); } catch {} }); resolve(r); } if (++doneCount === promises.length && !settled) resolve(null); }); }); }); // 收集失败原因 (用于日志) const settled = await Promise.allSettled(promises); const failedResults = settled .filter(r => r.status === 'fulfilled' && !r.value.ok && r.value.reason !== '已取消') .map(r => r.value); if (winner) { setState({ status: 'success', bizId: winner.bizId, lastSuccess: { text: winner.text, data: winner.data }, stats: { ...state.stats, total: totalAttempt, success: state.stats.success + 1 }, }); log(`成功! bizId=${winner.bizId} (第${winner.attempt}次)`); recoveryAttempts = 0; setTimeout(autoRecover, 500); return { ok: true, text: winner.text, data: winner.data, status: winner.status }; } // 统计错误 const reasons = failedResults.map(r => r.reason || '未知'); setState({ stats: { ...state.stats, errors: state.stats.errors + failedResults.length } }); const networkErrors = reasons.filter(r => r.startsWith('网络')).length; consecutiveErrors = (networkErrors > 0 && networkErrors === failedResults.length) ? consecutiveErrors + 1 : 0; // 连续网络错误 → 暂停 if (consecutiveErrors >= 3) { log('网络异常, 暂停3秒...'); await sleep(3000); consecutiveErrors = 0; } // 会话过期检测 if (reasons.some(r => r.includes('会话过期'))) { log('会话已过期, 请重新登录!', 'error'); setState({ status: 'failed' }); return { ok: false }; } // 只有 429(限流)才退避,EXPIRE 和系统繁忙无延迟立即重试 if (reasons.every(r => r === 'EXPIRE' || r === '系统繁忙')) continue; // 限流检测 (独立计数) if (reasons.some(r => r.includes('429') || r.includes('限流'))) { throttleCount++; const backoff = Math.min(1000 * (2 ** Math.min(throttleCount, 3)), 8000); log(`限流, 退避${backoff}ms...`, 'warn'); await sleep(backoff); } else { throttleCount = 0; } // 前20秒全速冲,之后才考虑降速 const elapsedSec = (performance.now() - state.stats.startTime) / 1000; if (elapsedSec > 20) { // 超过20秒 — 检测是否该降速 const soldOutRatio = reasons.filter(r => r === '售罄').length / batchSize; if (soldOutRatio >= 0.6) { consecutiveSoldOut++; } else { consecutiveSoldOut = 0; } // 连续10轮全售罄 → 可能已经抢完了 if (consecutiveSoldOut >= 10) { if (consecutiveSoldOut === 10) log('连续售罄, 可能已抢完, 降速 (2s)...'); await sleep(2000); continue; } } // 日志 (前5次 + 每20次) if (totalAttempt <= 5 * CFG.concurrency || totalAttempt % (20 * CFG.concurrency) === 0) { const sec = elapsedSec.toFixed(0); log(`#${totalAttempt} ${reasons[0]} (${sec}s)`); } // 自适应延迟 const d = getDelay(roundNum); if (d > 0) await sleep(d); // 超过 maxRetry 且在5分钟内 → 切捡漏模式 if (totalAttempt >= CFG.maxRetry) { const elapsedSec2 = (performance.now() - state.stats.startTime) / 1000; if (elapsedSec2 < 300) { log('进入捡漏模式,降速等待退票...'); CFG._savedConcurrency = CFG.concurrency; CFG._savedMaxRetry = CFG.maxRetry; CFG.concurrency = 2; CFG.slowDelay = 3000; CFG.maxRetry = totalAttempt + 200; continue; } else { setState({ status: 'failed' }); CFG.concurrency = CFG._savedConcurrency ?? CFG.concurrency; CFG.maxRetry = CFG._savedMaxRetry ?? CFG.maxRetry; log(`达到上限 ${CFG._savedMaxRetry ?? CFG.maxRetry} 次`); return { ok: false }; } } } return { ok: false }; try { return await _retryLock; } finally { _retryLock = null; } } // ═══════════════════════════════════════════ // Fetch 拦截 // ═══════════════════════════════════════════ window.fetch = async function (input, init) { const url = typeof input === 'string' ? input : input?.url; if (url && url.includes(CFG.PREVIEW)) { // 捕获请求参数 const captured = { url, method: init?.method || 'POST', body: init?.body, headers: extractHeaders(init?.headers), }; setState({ captured }); try { sessionStorage.setItem('glm_rush_captured', JSON.stringify({ ...captured, __v: CAPTURE_VER })); } catch {} // 已经成功过 → 直接返回缓存 if (state.status === 'success' && state.lastSuccess) { log('已抢到, 返回成功响应'); return new Response(state.lastSuccess.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 有缓存 → 返回(来自主动模式成功后的恢复) if (state.cache) { log('返回缓存响应'); const c = state.cache; setState({ cache: null }); recoveryAttempts = 0; return new Response(c.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 主动模式/正在抢购 → 进入重试引擎 if (state.proactive || state.status === 'retrying') { log('抢购中, 启动重试...'); const result = await retry(url, { method: init?.method || 'POST', body: init?.body, headers: extractHeaders(init?.headers), }); if (result.ok) { return new Response(result.text, { status: result.status, headers: { 'Content-Type': 'application/json' } }); } return _fetch.apply(this, [input, init]); } // 普通捕获 → 只记录参数,放行原始请求,自动设定定时 log('已捕获请求参数, 等待抢购时间...'); autoScheduleIfNeeded(); return _fetch.apply(this, [input, init]); } if (url && url.includes(CFG.CHECK) && url.includes('bizId=null')) { log('拦截 check(bizId=null)'); return new Response('{"code":-1,"msg":"等待有效bizId"}', { status: 200, headers: { 'Content-Type': 'application/json' }, }); } // 新增:对所有其他 JSON 接口的响应做文本替换 const resp = await _fetch.apply(this, [input, init]); const ct = resp.headers.get('content-type') || ''; if (ct.includes('application/json')) { const text = await resp.text(); const needsPatch = /"isSoldOut":true|"soldOut":true|"isServerBusy":true|"stock":0/.test(text); if (!needsPatch) { return new Response(text, { status: resp.status, statusText: resp.statusText, headers: resp.headers, }); } const patched = text .replace(/"isSoldOut":true/g, '"isSoldOut":false') .replace(/"soldOut":true/g, '"soldOut":false') .replace(/"isServerBusy":true/g, '"isServerBusy":false') .replace(/"stock":0/g, '"stock":999'); return new Response(patched, { status: resp.status, statusText: resp.statusText, headers: resp.headers, }); } return resp; }; // 伪装 window.fetch.toString = () => 'function fetch() { [native code] }'; // ═══════════════════════════════════════════ // XHR 拦截 // ═══════════════════════════════════════════ const _xhrOpen = XMLHttpRequest.prototype.open; const _xhrSend = XMLHttpRequest.prototype.send; const _xhrSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (k, v) { (this._h || (this._h = {}))[k] = v; return _xhrSetHeader.call(this, k, v); }; XMLHttpRequest.prototype.open = function (method, url) { this._m = method; this._u = url; return _xhrOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const url = this._u; if (typeof url === 'string' && url.includes(CFG.PREVIEW)) { const self = this; const captured = { url, method: this._m, body, headers: this._h || {} }; setState({ captured }); try { sessionStorage.setItem('glm_rush_captured', JSON.stringify({ ...captured, __v: CAPTURE_VER })); } catch {} // 已经成功过 → 直接返回缓存 if (state.status === 'success' && state.lastSuccess) { log('已抢到, 返回成功响应 (XHR)'); fakeXHR(self, state.lastSuccess.text); return; } if (state.cache) { log('返回缓存响应 (XHR)'); const c = state.cache; setState({ cache: null }); recoveryAttempts = 0; fakeXHR(self, c.text); return; } // 主动模式/正在抢购 → 重试 if (state.proactive || state.status === 'retrying') { log('抢购中, 启动重试 (XHR)...'); retry(url, { method: this._m, body, headers: this._h || {} }).then(result => { fakeXHR(self, result.ok ? result.text : '{"code":-1,"msg":"重试失败"}'); }); return; } // 普通捕获 → 放行原始请求,自动设定定时 log('已捕获请求参数, 等待抢购时间...'); autoScheduleIfNeeded(); return _xhrSend.call(this, body); } if (typeof url === 'string' && url.includes(CFG.CHECK) && url.includes('bizId=null')) { fakeXHR(this, '{"code":-1,"msg":"等待有效bizId"}'); return; } return _xhrSend.call(this, body); }; function setProp(obj, key, value) { try { obj[key] = value; } catch { try { Object.defineProperty(obj, key, { value, configurable: true, writable: true }); } catch {} } } function fakeXHR(xhr, text) { setTimeout(() => { setProp(xhr, 'readyState', 4); setProp(xhr, 'status', 200); setProp(xhr, 'statusText', 'OK'); setProp(xhr, 'responseText', text); setProp(xhr, 'response', text); const ev = new Event('readystatechange'); if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(ev); xhr.dispatchEvent(ev); const ld = new ProgressEvent('load'); if (typeof xhr.onload === 'function') xhr.onload(ld); xhr.dispatchEvent(ld); xhr.dispatchEvent(new ProgressEvent('loadend')); }, 0); } // ═══════════════════════════════════════════ // 弹窗恢复 // ═══════════════════════════════════════════ function findErrorDialog() { const sels = [ '.el-dialog', '.el-message-box', '.el-dialog__wrapper', '.ant-modal', '.ant-modal-wrap', '[class*="modal"]', '[class*="dialog"]', '[class*="popup"]', '[role="dialog"]', ]; for (const sel of sels) { for (const el of document.querySelectorAll(sel)) { const s = window.getComputedStyle(el); if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') continue; if (!el.offsetParent && s.position !== 'fixed') continue; if (/购买人数过多|系统繁忙|稍后再试|请重试|繁忙|失败|出错|异常/.test(el.textContent || '')) return el; } } return null; } function dismissDialog(dialog) { // 只在传入的 dialog 内部查找关闭按钮,不 fallback 到 document(避免关掉支付弹窗) for (const sel of ['.el-dialog__headerbtn', '.el-message-box__headerbtn', '.ant-modal-close', '[aria-label="Close"]', '[aria-label="close"]']) { const btn = dialog.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); return true; } } // 确定/取消按钮(仅 dialog 内部) for (const btn of dialog.querySelectorAll('button, [role="button"]')) { const t = (btn.textContent || '').trim(); if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) { btn.click(); return true; } } // 先发送 Escape 事件让 Vue 自己处理 const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }); dialog.dispatchEvent(esc); document.dispatchEvent(esc); setTimeout(() => { if (dialog.offsetParent !== null) dialog.style.display = 'none'; }, 300); return true; } async function autoRecover() { if (recovering || recoveryAttempts >= CFG.recoveryMax || !state.lastSuccess) return; // 如果页面上有支付相关弹窗,不要干扰 const payEl = document.querySelector('[class*="pay"], [class*="qrcode"], [class*="wechat"], [class*="alipay"], [class*="cashier"], iframe[src*="pay"]'); if (payEl && (payEl.offsetParent !== null || window.getComputedStyle(payEl).position === 'fixed')) { log('支付弹窗已出现, 跳过恢复'); return; } // 只处理明确的错误弹窗,不暴力清理所有弹窗 const dialog = findErrorDialog(); if (!dialog) return; recovering = true; recoveryAttempts++; try { log('检测到错误弹窗, 清理中...'); dismissDialog(dialog); await sleep(300); // 策略2: 缓存响应 + 重新点购买按钮 setState({ cache: state.lastSuccess }); const btn = await waitForBuyButton(); if (btn) { btn.click(); log('已重新点击购买按钮 (策略2)'); await sleep(2000); } // 策略3: 检查支付弹窗是否出现, 没有则直接用 bizId 构造支付 const payDialog = document.querySelector('[class*="pay"], [class*="qrcode"], [class*="wechat"], [class*="alipay"]'); if (!payDialog || payDialog.offsetParent === null) { const bizId = state.bizId; if (bizId) { log('支付弹窗未出现, 尝试直接调用 check 页面...'); // 尝试直接打开支付 — 有些网站 check 接口会返回支付链接 try { const checkUrl = `${location.origin}${CFG.CHECK}?bizId=${encodeURIComponent(bizId)}`; const resp = await _fetch(checkUrl, { credentials: 'include' }); const data = await resp.json(); log('check响应: ' + JSON.stringify(data).substring(0, 200)); // 如果有支付URL, 直接跳转 if (data.data && typeof data.data === 'string' && data.data.startsWith('http')) { log('获取到支付链接, 跳转中...'); window.open(data.data, '_blank'); } else if (data.data && data.data.payUrl) { log('获取到payUrl, 跳转中...'); window.open(data.data.payUrl, '_blank'); } else if (data.data && data.data.qrCode) { log('获取到二维码数据'); showQRCodeFallback(data.data.qrCode, bizId); } } catch (e) { log('check调用失败: ' + e.message); } } // 策略4: 最终兜底 — 弹窗提醒手动操作 if (!document.querySelector('[class*="pay"], [class*="qrcode"]')) { log('所有自动恢复策略已尝试, 请手动操作'); const bizId = state.bizId; alert(`已抢到 bizId=${bizId}\n\n请尝试:\n1. 刷新页面后立即点击购买\n2. 或手动访问支付页面`); } } else { log('支付弹窗已出现!'); recoveryAttempts = 0; return; } } finally { recovering = false; } } /** 兜底: 直接在页面上显示二维码 */ function showQRCodeFallback(qrData, bizId) { const div = document.createElement('div'); div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:999999;background:#fff;padding:30px;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.3);text-align:center'; div.innerHTML = `

扫码支付

bizId: ${bizId}

`; document.body.appendChild(div); log('已显示兜底支付二维码'); } // MutationObserver 监控弹窗 (替代 setInterval) function setupDialogWatcher() { const observer = new MutationObserver(() => { if (state.lastSuccess && !recovering && recoveryAttempts < CFG.recoveryMax) { const d = findErrorDialog(); if (d) autoRecover(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ═══════════════════════════════════════════ // 主动抢购 & 定时 // ═══════════════════════════════════════════ async function waitForBuyButton(timeout = 8000) { const start = Date.now(); while (Date.now() - start < timeout) { // 策略1:class 精确匹配 for (const el of document.querySelectorAll('button.buy-btn, button[class*="subscribe"], button[class*="buy"]')) { if (el.offsetParent !== null) return el; } // 策略2:文本精确匹配(最可靠) for (const el of document.querySelectorAll('button')) { const t = el.textContent?.trim(); if (/^特惠订购$|^立即订购$|^立即购买$/.test(t) && el.offsetParent !== null) return el; } await sleep(200); } return null; } async function startProactive() { if (!state.captured) { log('请先手动点一次购买按钮'); alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数'); return; } if (state.status === 'success') { log('已经抢到了, 不重复抢购'); return; } setState({ proactive: true }); log(`极速抢购启动! 前${CFG.turboSec}秒${CFG.turboConcurrency}路并发, 之后${CFG.concurrency}路`); const { url, method, body, headers } = state.captured; const result = await retry(url, { method, body, headers }); setState({ proactive: false }); if (result.ok) { setState({ cache: { text: result.text, data: result.data } }); log('抢购成功! 触发支付...'); // 自动通知 try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} const errDlg = findErrorDialog(); if (errDlg) { dismissDialog(errDlg); await sleep(300); } const btn = await waitForBuyButton(); if (btn) { btn.click(); log('已自动点击购买按钮'); } else { alert('已获取到商品! 请立即点击购买按钮!'); } // 兜底: 如果 fakeXHR 没能弹出支付窗口, 直接设置 Vue 数据 await sleep(1500); forcePayDialog(result.data); } } function stopAll() { stopRequested = true; if (window._glmRafCancelled) window._glmRafCancelled(); _activeControllers.forEach(ac => { try { ac.abort(); } catch {} }); _activeControllers = []; setState({ proactive: false, status: 'idle', count: 0 }); if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); } log('已停止'); } // ═══════════════════════════════════════════ // 北京时间同步 + 自动定时 // ═══════════════════════════════════════════ let serverTimeOffset = 0; // 本地时间与服务器时间的差值(ms) async function syncServerTime(samples = 3) { const offsets = []; for (let i = 0; i < samples; i++) { const t0 = Date.now(); const r = await _fetch(location.origin + '/favicon.ico', { method: 'HEAD' }).catch(() => null); if (!r) continue; const t1 = Date.now(); const d = r.headers.get('date'); if (d) offsets.push(new Date(d).getTime() - (t0 + t1) / 2); if (i < samples - 1) await sleep(500); } offsets.sort((a, b) => a - b); serverTimeOffset = offsets[Math.floor(offsets.length / 2)] ?? 0; log(`时间同步完成,服务器偏差 ${serverTimeOffset > 0 ? '+' : ''}${serverTimeOffset}ms`); } function getServerNow() { return Date.now() + serverTimeOffset; } /** 捕获请求后自动设定今天的抢购定时 */ function autoScheduleIfNeeded() { if (state.timerId) return; // 已经设定了 if (state.status === 'retrying') return; // 正在抢 if (state.status === 'success') return; // 已经抢到了 const parts = CFG.rushTime.split(':').map(Number); const now = new Date(getServerNow()); const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parts[0], parts[1], parts[2] || 0); if (target.getTime() <= getServerNow()) { // 已过今天的抢购时间 → 直接开始抢(可能正好在抢购窗口内) const passedSec = (getServerNow() - target.getTime()) / 1000; if (passedSec < 30) { // 过了不到30秒,还在窗口内,直接开抢 log(`已过${CFG.rushTime} ${passedSec.toFixed(0)}秒, 立即开抢!`); startProactive(); } else { // 超过30秒,顺延至明天 log(`今天${CFG.rushTime}已过, 明天自动抢购`); target.setDate(target.getDate() + 1); log(`顺延至明天 ${CFG.rushTime}`); } // 如果是顺延情况,继续往下设定定时 } // 未到时间 → 自动设定定时 scheduleAt(CFG.rushTime); log(`已自动设定 ${CFG.rushTime} 抢购`); } // 定时到指定时间 function scheduleAt(timeStr) { if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); } const parts = timeStr.split(':').map(Number); if (parts.length < 2 || parts[0] > 23 || parts[1] > 59) { log('时间格式错误'); return; } const now = new Date(getServerNow()); const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parts[0], parts[1], parts[2] || 0); if (target.getTime() <= getServerNow()) { log('目标时间已过'); return; } const ms = target.getTime() - getServerNow(); log(`定时: ${timeStr} (${Math.ceil(ms / 1000)}秒后, 北京时间)`); // 提前5分钟自动预热 if (ms > 310_000) { setTimeout(() => { log('定时前5分钟, 自动预热...'); preheat(); }, Math.max(0, ms - 300_000)); } // 精确等待: 用 setInterval 10ms 检查, 到时间立即启动 const preAdvanceMs = (CFG.preAdvanceSec || 0) * 1000; let rafCancelled = false; window._glmRafCancelled = () => { rafCancelled = true; }; const checkInterval = setInterval(() => { const remaining = target.getTime() - getServerNow(); if (remaining <= preAdvanceMs) { clearInterval(checkInterval); startProactive(); } else if (remaining > 0 && remaining <= 1000) { clearInterval(checkInterval); function rafWait() { if (rafCancelled || stopRequested) return; if (target.getTime() - getServerNow() <= preAdvanceMs) { startProactive(); } else { requestAnimationFrame(rafWait); } } requestAnimationFrame(rafWait); } }, 10); setState({ timerId: checkInterval }); } // 预热 async function preheat() { log('开始预热连接...'); let ok = 0; for (let i = 0; i < 5; i++) { const r = await _fetch(location.origin + '/favicon.ico', { method: 'HEAD' }) .catch(() => null); if (r) ok++; // 预热 preview TCP 连接(空 POST 不触发业务逻辑) await _fetch(location.origin + CFG.PREVIEW, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{}', }).catch(() => {}); await sleep(300); } log(`预热完成:${ok}/5 连接建立成功`); } // ═══════════════════════════════════════════ // 快捷键 // ═══════════════════════════════════════════ document.addEventListener('keydown', e => { if (!e.altKey) return; if (e.key === 's' || e.key === 'S') { e.preventDefault(); startProactive(); } if (e.key === 'x' || e.key === 'X') { e.preventDefault(); stopAll(); } if (e.key === 'h' || e.key === 'H') { e.preventDefault(); if (_shadowRef) { const bd = _shadowRef.getElementById('bd'); if (bd) bd.style.display = bd.style.display === 'none' ? '' : 'none'; } } }); // ═══════════════════════════════════════════ // Vue isServerBusy 兜底 patch // ═══════════════════════════════════════════ function patchVueServerBusy() { let attempts = 0; const tid = setInterval(() => { attempts++; if (attempts > 30) { clearInterval(tid); return; } // 15秒后放弃 const vue = getVueRoot(); if (!vue) return; let patched = 0; const walk = (vm, depth) => { if (depth > 8) return; if (vm.$data && vm.$data.isServerBusy === true) { vm.isServerBusy = false; patched++; } for (const child of (vm.$children || [])) walk(child, depth + 1); }; walk(vue, 0); if (patched > 0) { log(`已解除 isServerBusy (${patched}个组件)`); clearInterval(tid); } }, 500); } /** 兜底: 直接操作 Vue 组件弹出支付窗口 */ function forcePayDialog(responseData) { const vue = getVueRoot(); if (!vue) return; let payComp = null; const findComp = (vm, depth) => { if (depth > 8) return; if (vm.$data && 'payDialogVisible' in vm.$data) { payComp = vm; return; } for (const child of (vm.$children || [])) { findComp(child, depth + 1); if (payComp) return; } }; findComp(vue, 0); if (!payComp) { log('未找到支付组件'); return; } // 已经弹出了就不干预 if (payComp.payDialogVisible) { log('支付弹窗已显示'); return; } // 设置 priceData 和 payDialogVisible const data = responseData && responseData.data; if (data) { payComp.priceData = data; payComp.payDialogVisible = true; log('兜底: 已直接设置 payDialogVisible=true'); } else { log('兜底: 响应数据无 data 字段, 无法设置'); } } // ═══════════════════════════════════════════ // 浮动面板 (Shadow DOM) // ═══════════════════════════════════════════ function createPanel() { const host = document.createElement('div'); host.id = 'glm-rush-host'; const shadow = host.attachShadow({ mode: 'closed' }); shadow.innerHTML = `
GLM v${VERSION}
等待中
${state.captured ? '已恢复上次捕获的请求' : '请先点一次购买按钮'}
0
重试
0
成功
0
错误
并发 极速 上限
定时
⚠️ 每次确认放票时间
Alt+S 抢购 | Alt+X 停止 | Alt+H 隐藏
`; document.body.appendChild(host); const $ = id => shadow.getElementById(id); $('b-go').onclick = startProactive; $('b-stop').onclick = stopAll; $('b-heat').onclick = preheat; $('b-time').onclick = () => { const v = $('i-time').value; if (v) scheduleAt(v); }; $('i-conc').onchange = function() { CFG.concurrency = Math.max(1, +this.value || 5); saveCfg(CFG); }; $('i-turbo').onchange = function() { CFG.turboConcurrency = Math.max(1, +this.value || 10); saveCfg(CFG); }; $('i-max').onchange = function() { CFG.maxRetry = Math.max(10, +this.value || 2000); saveCfg(CFG); }; $('min').onclick = function() { const bd = $('bd'); const hidden = bd.style.display === 'none'; bd.style.display = hidden ? '' : 'none'; this.textContent = hidden ? '-' : '+'; }; // 拖拽 let sx, sy, sl, st; $('drag').onmousedown = function(e) { sx = e.clientX; sy = e.clientY; const rect = host.getBoundingClientRect(); sl = rect.left; st = rect.top; const onMove = e => { host.style.left = (sl + e.clientX - sx) + 'px'; host.style.top = (st + e.clientY - sy) + 'px'; host.style.right = 'auto'; host.style.position = 'fixed'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; // 闭包引用供 refreshUI 使用 _shadowRef = shadow; log(`v${VERSION} 已加载 (极速并发+时间同步+全自动抢购)`); if (state.captured) log('已恢复上次捕获的请求参数, 可直接设定时间'); setupDialogWatcher(); // 兜底: 定时 patch Vue 组件的 isServerBusy (batch-preview 可能在脚本前加载) patchVueServerBusy(); // 自动同步服务器时间 syncServerTime(); // 请求通知权限 if (Notification && Notification.permission === 'default') { Notification.requestPermission(); } } // ═══════════════════════════════════════════ // UI 更新 (rAF 节流) // ═══════════════════════════════════════════ let uiPending = false; function refreshUI() { if (uiPending) return; uiPending = true; requestAnimationFrame(() => { uiPending = false; const shadow = _shadowRef; if (!shadow) return; const $ = id => shadow.getElementById(id); const stEl = $('st'); if (stEl) { stEl.className = 'st st-' + state.status; const isTurbo = state.stats.startTime && (performance.now() - state.stats.startTime) < CFG.turboSec * 1000; stEl.textContent = state.status === 'idle' ? '等待中' : state.status === 'retrying' ? `${isTurbo ? '⚡极速' : ''}重试中... ${state.count}/${CFG.maxRetry}` : state.status === 'success' ? `成功! bizId=${state.bizId}` : `失败 (${state.count}次)`; } const capEl = $('cap'); if (capEl) { capEl.textContent = state.captured ? `已捕获: ${state.captured.method} ...${state.captured.url.split('?')[0].slice(-30)}` : '请先点一次购买按钮'; } const cntEl = $('s-cnt'); if (cntEl) cntEl.textContent = state.count; const okEl = $('s-ok'); if (okEl) okEl.textContent = state.stats.success; const errEl = $('s-err'); if (errEl) errEl.textContent = state.stats.errors; const goBtn = $('b-go'); const stopBtn = $('b-stop'); if (goBtn && stopBtn) { goBtn.style.display = state.status === 'retrying' ? 'none' : ''; stopBtn.style.display = state.status === 'retrying' ? '' : 'none'; } }); } function appendLogDOM(entry) { const shadow = _shadowRef; if (!shadow) return; const el = shadow.getElementById('logs'); if (!el) return; const div = document.createElement('div'); div.className = entry.level === 'error' ? 'err' : entry.level === 'warn' ? 'warn' : entry.msg.includes('成功') ? 'ok' : 'info'; div.textContent = `${entry.ts} ${entry.msg}`; el.appendChild(div); while (el.children.length > CFG.logMax) el.removeChild(el.firstChild); el.scrollTop = el.scrollHeight; } // ═══════════════════════════════════════════ // 离开保护 // ═══════════════════════════════════════════ window.addEventListener('beforeunload', e => { if (state.status === 'retrying') { e.preventDefault(); e.returnValue = '抢购正在进行中,确定要离开吗?'; } }); // ═══════════════════════════════════════════ // 启动 // ═══════════════════════════════════════════ console.log(`[GLM] v${VERSION} 已注入`); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createPanel); } else { createPanel(); } })();