diff --git a/glm-rush-v4.user.js b/glm-rush-v4.user.js deleted file mode 100644 index b168f2a..0000000 --- a/glm-rush-v4.user.js +++ /dev/null @@ -1,1081 +0,0 @@ -// ==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'; - - // ═══════════════════════════════════════════ - // 配置 (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', // 每天抢购时间 (北京时间) - 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) { - state = { ...state, ...patch }; - refreshUI(); - } - - // 恢复上次捕获的请求 - try { - const saved = sessionStorage.getItem('glm_rush_captured'); - if (saved) state.captured = JSON.parse(saved); - } catch {} - - let stopRequested = false; - let recovering = false; - let recoveryAttempts = 0; - let _shadowRef = null; - - // ═══════════════════════════════════════════ - // 工具 - // ═══════════════════════════════════════════ - 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(attempt) { - if (attempt <= CFG.burstCount) return 0; - if (attempt <= 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; - } - - // ═══════════════════════════════════════════ - // JSON.parse 定向拦截 (仅修改特定数据结构) - // ═══════════════════════════════════════════ - const _parse = JSON.parse; - - function patchSoldOut(obj, visited = new WeakSet()) { - if (!obj || typeof obj !== 'object' || visited.has(obj)) return; - 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 }; - } - - // 通过! - 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; - 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) - ); - } - - setState({ count: totalAttempt }); - - // 任一成功即取消其余 - 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 results = await Promise.all(promises.map(p => p.catch(() => ({ ok: false, reason: '已取消' })))); - - 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 failedResults = results.filter(r => !r.ok); - 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 === batchSize ? 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 }; - } - - // 限流检测 (独立计数) - if (reasons.some(r => r.includes('429') || r.includes('限流'))) { - throttleCount++; - const backoff = Math.min(2000 * (2 ** Math.min(throttleCount, 4)), 16000); - log(`限流, 退避${backoff}ms...`, 'warn'); - await sleep(backoff); - } else { - throttleCount = 0; - } - - // EXPIRE → 立即重试不等待 - if (reasons.every(r => r === 'EXPIRE')) continue; - - // 前20秒全速冲,之后才考虑降速 - const elapsedSec = (performance.now() - state.stats.startTime) / 1000; - - if (elapsedSec > 20) { - // 超过20秒 — 检测是否该降速 - const soldOutCount = reasons.filter(r => r === '售罄').length; - if (soldOutCount === batchSize) { - 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(totalAttempt / CFG.concurrency); - if (d > 0) await sleep(d); - } - - if (!stopRequested) { - setState({ status: 'failed' }); - log(`达到上限 ${CFG.maxRetry} 次`); - } else { - setState({ status: 'idle' }); - } - 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)); } 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' }, - }); - } - - return _fetch.apply(this, [input, init]); - }; - // 伪装 - 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)); } 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 fakeXHR(xhr, text) { - setTimeout(() => { - const dp = (k, v) => Object.defineProperty(xhr, k, { value: v, configurable: true }); - dp('readyState', 4); dp('status', 200); dp('statusText', 'OK'); - dp('responseText', text); dp('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; } - } - // 直接隐藏这个 dialog - dialog.style.display = 'none'; - 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 = findBuyButton(); - 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('支付弹窗已出现!'); - } - } 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 }); - } - - // ═══════════════════════════════════════════ - // 主动抢购 & 定时 - // ═══════════════════════════════════════════ - function findBuyButton() { - // 优先找 buy-btn 类的按钮(特惠订阅/订阅升级) - for (const el of document.querySelectorAll('button.buy-btn')) { - const t = el.textContent.trim(); - if (el.offsetParent !== null) return el; - } - // 降级:通用匹配,排除导航按钮 - for (const el of document.querySelectorAll('button, [role="button"]')) { - const t = el.textContent.trim(); - if (/购买|抢购|下单|特惠/.test(t) && t.length < 15 && el.offsetParent !== null) return el; - } - 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 = findBuyButton(); - if (btn) { btn.click(); log('已自动点击购买按钮'); } - else { alert('已获取到商品! 请立即点击购买按钮!'); } - - // 兜底: 如果 fakeXHR 没能弹出支付窗口, 直接设置 Vue 数据 - await sleep(1500); - forcePayDialog(result.data); - } - } - - function stopAll() { - stopRequested = true; - setState({ proactive: false, status: 'idle', count: 0 }); - if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); } - log('已停止'); - } - - // ═══════════════════════════════════════════ - // 北京时间同步 + 自动定时 - // ═══════════════════════════════════════════ - let serverTimeOffset = 0; // 本地时间与服务器时间的差值(ms) - - async function syncServerTime() { - // 用服务器响应头的 Date 字段同步时间 - try { - const t0 = Date.now(); - const resp = await _fetch(location.origin + '/api/biz/pay/check?bizId=sync', { credentials: 'include' }).catch(() => null); - const t1 = Date.now(); - const rtt = t1 - t0; - - if (resp && resp.headers.get('date')) { - const serverTime = new Date(resp.headers.get('date')).getTime(); - // 服务器时间 ≈ 发送时间 + RTT/2 - serverTimeOffset = serverTime - (t0 + rtt / 2); - const localNow = new Date(Date.now() + serverTimeOffset); - log(`时间同步: 服务器偏差 ${serverTimeOffset > 0 ? '+' : ''}${serverTimeOffset}ms (RTT=${rtt}ms)`); - log(`北京时间: ${localNow.toLocaleTimeString('zh-CN', { hour12: false })}`); - return; - } - } catch {} - - // 备用: 用 worldtimeapi - try { - const resp = await fetch('https://worldtimeapi.org/api/timezone/Asia/Shanghai'); - const data = await resp.json(); - const serverTime = new Date(data.datetime).getTime(); - serverTimeOffset = serverTime - Date.now(); - log(`时间同步(备用): 偏差 ${serverTimeOffset > 0 ? '+' : ''}${serverTimeOffset}ms`); - } catch { - log('时间同步失败, 使用本地时钟'); - serverTimeOffset = 0; - } - } - - 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 { - log(`今天${CFG.rushTime}已过, 明天自动抢购`); - } - return; - } - - // 未到时间 → 自动设定定时 - 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)}秒后, 北京时间)`); - - // 提前3秒自动预热 - if (ms > 4000) { - setTimeout(() => { - log('定时前3秒, 自动预热...'); - preheat(); - }, Math.max(0, ms - 3000)); - } - - // 精确等待: 用 setInterval 10ms 检查, 到时间立即启动 - const tid = setInterval(() => { - const remaining = target.getTime() - getServerNow(); - // 更新面板倒计时 - if (remaining > 0 && remaining < 60000) { - const sec = (remaining / 1000).toFixed(1); - const timerEl = _shadowRef?.getElementById('timer-info'); - if (timerEl) timerEl.textContent = `-${sec}s`; - } - if (remaining <= 0) { - clearInterval(tid); - setState({ timerId: null }); - const timerEl = _shadowRef?.getElementById('timer-info'); - if (timerEl) timerEl.textContent = ''; - log('时间到! 自动启动抢购!'); - startProactive(); - } - }, 10); - - setState({ timerId: tid }); - } - - // 预热 - async function preheat() { - try { - log('TCP预热中...'); - // 连发3次预热请求,确保连接池暖好 - for (let i = 0; i < 3; i++) { - await _fetch(location.origin + '/api/biz/pay/check?bizId=preheat_' + i, { credentials: 'include' }).catch(() => {}); - await sleep(200); - } - // 也预热 preview 的 DNS + TCP (用 HEAD 请求不产生副作用) - await _fetch(location.origin + CFG.PREVIEW, { - method: 'HEAD', - credentials: 'include', - }).catch(() => {}); - log('预热完成 (4次连接已建立)'); - } catch { log('预热部分失败,不影响使用'); } - } - - // ═══════════════════════════════════════════ - // 快捷键 - // ═══════════════════════════════════════════ - 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 app = document.querySelector('#app'); - const vue = app && app.__vue__; - 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 app = document.querySelector('#app'); - const vue = app && app.__vue__; - 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 v4.6
-
-
等待中
-
${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('v4.5 已加载 (极速并发+时间同步+全自动抢购)'); - 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] v4.0 已注入'); - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createPanel); - } else { - createPanel(); - } -})();