// ==UserScript== // @name 智谱 GLM Coding 抢购助手 v4.0 // @namespace http://tampermonkey.net/ // @version 4.4 // @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化 // @author Assistant // @match *://www.bigmodel.cn/* // @match *://bigmodel.cn/* // @run-at document-start // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/573080/%E6%99%BA%E8%B0%B1%20GLM%20Coding%20%E6%8A%A2%E8%B4%AD%E5%8A%A9%E6%89%8B%20v40.user.js // @updateURL https://update.greasyfork.icu/scripts/573080/%E6%99%BA%E8%B0%B1%20GLM%20Coding%20%E6%8A%A2%E8%B4%AD%E5%8A%A9%E6%89%8B%20v40.meta.js // ==/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.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), }); setState({ proactive: false }); if (result.ok) { log('拦截器内抢购成功! 返回响应给前端...'); try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} // 直接返回给前端的 fetch 调用 → 前端会正常弹出支付窗口 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 => { setState({ proactive: false }); if (result.ok) { log('XHR拦截器内抢购成功! 返回响应给前端...'); try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} } 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) { // 关闭按钮 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) || document.querySelector(sel); if (btn && btn.offsetParent !== null) { btn.click(); return true; } } // 确定/取消按钮 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 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); // 遮罩 for (const mask of document.querySelectorAll('.el-overlay, .v-modal, [class*="overlay"], [class*="mask"]')) { if (mask.offsetParent !== null || window.getComputedStyle(mask).position === 'fixed') { mask.click(); return true; } } dialog.style.display = 'none'; return true; } async function autoRecover() { if (recovering || recoveryAttempts >= CFG.recoveryMax || !state.lastSuccess) return; recovering = true; recoveryAttempts++; try { // 策略1: 关闭所有弹窗/遮罩 (暴力清理) const dialog = findErrorDialog(); if (dialog) { log('检测到错误弹窗, 清理中...'); dismissDialog(dialog); await sleep(300); } // 清理所有可能残留的遮罩层 document.querySelectorAll('.el-overlay, .v-modal, .el-overlay-dialog, [class*="overlay"], [class*="mask"]').forEach(el => { el.style.display = 'none'; }); document.querySelectorAll('.el-dialog__wrapper, .el-message-box__wrapper').forEach(el => { el.style.display = 'none'; }); // 移除 body 上的 overflow:hidden (弹窗锁定滚动) document.body.style.overflow = ''; document.body.classList.remove('el-popup-parent--hidden'); await sleep(200); // 策略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 }); } // ═══════════════════════════════════════════ // 主动抢购 & 定时 // ═══════════════════════════════════════════ let _lastClickedBtn = null; // 记住用户点的那个按钮 function findBuyButton() { // 优先返回用户上次点击的同一个按钮 if (_lastClickedBtn && _lastClickedBtn.offsetParent !== null) return _lastClickedBtn; for (const el of document.querySelectorAll('button, a, [role="button"], div[class*="btn"], span[class*="btn"]')) { const t = el.textContent.trim(); if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20 && el.offsetParent !== null) return el; } return null; } // 监听用户点击,记住是哪个按钮 document.addEventListener('click', e => { const t = (e.target.textContent || '').trim(); if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20) { _lastClickedBtn = e.target.closest('button') || e.target; log('记住按钮: ' + t); } }, true); function clickButton(btn) { // 多种方式触发点击,确保前端框架能响应 btn.focus(); btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); btn.click(); } async function startProactive() { if (!state.captured) { log('请先手动点一次购买按钮'); alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数'); return; } if (state.status === 'success') { log('已经抢到了, 不重复抢购'); return; } // 核心策略: 设置 proactive=true,然后点击按钮 // 让前端自己发 fetch → 拦截器检测到 proactive → 启动重试 // 响应直接返回给前端的 fetch 调用 → 前端正常弹出支付窗口 setState({ proactive: true }); log(`极速抢购启动! 点击按钮触发前端请求...`); const btn = findBuyButton(); if (btn) { clickButton(btn); log('已点击购买按钮, 等待拦截器重试...'); // 拦截器会在 fetch/XHR 中自动处理重试 // proactive 会在拦截器成功后由 retry 结束时保持 } else { // 找不到按钮 → 降级为直接调用方式 log('未找到按钮, 降级为直接请求模式...'); 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 {} alert('已抢到! 请立即手动点击「特惠订阅」按钮完成支付!'); } } } 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'; } } }); // ═══════════════════════════════════════════ // 浮动面板 (Shadow DOM) // ═══════════════════════════════════════════ function createPanel() { const host = document.createElement('div'); host.id = 'glm-rush-host'; const shadow = host.attachShadow({ mode: 'closed' }); shadow.innerHTML = `