diff --git a/glm-rush-v4.user.js b/glm-rush-v4.user.js index b98ab80..88d4f3e 100644 --- a/glm-rush-v4.user.js +++ b/glm-rush-v4.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 智谱 GLM Coding 抢购助手 v4.0 // @namespace http://tampermonkey.net/ -// @version 4.3 +// @version 4.4 // @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化 // @author Assistant // @match *://www.bigmodel.cn/* @@ -17,11 +17,13 @@ // 配置 (localStorage 持久化) // ═══════════════════════════════════════════ const DEFAULT_CFG = { - concurrency: 3, // 并发路数 + concurrency: 5, // 并发路数 (普通模式) + turboConcurrency: 10, // 极速模式并发路数 + turboSec: 5, // 极速模式持续秒数 maxRetry: 2000, // 最大重试次数 - burstCount: 10, // 前N次零延迟爆发 - fastDelay: 50, // 爆发后的快速间隔 - slowDelay: 150, // 后期随机间隔中值 + burstCount: 20, // 前N次零延迟爆发 + fastDelay: 30, // 爆发后的快速间隔 + slowDelay: 100, // 后期随机间隔中值 jitter: 0.3, // 间隔随机抖动 ±30% recoveryMax: 3, // 弹窗恢复最大次数 logMax: 100, // 日志条数上限 @@ -140,7 +142,15 @@ async function singleAttempt(url, opts, attemptNum) { try { - const resp = await _fetch(url, { ...opts, credentials: 'include' }); + // 请求指纹随机化 — 每次请求看起来不一样,降低被识别为脚本的概率 + 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) { @@ -205,7 +215,11 @@ let consecutiveSoldOut = 0; while (totalAttempt < CFG.maxRetry && !stopRequested) { - const batchSize = Math.min(CFG.concurrency, CFG.maxRetry - totalAttempt); + // 极速模式: 前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 = []; @@ -347,6 +361,12 @@ try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} log('捕获 preview (Fetch)'); + // 已经成功过 → 直接返回缓存,不再重试 + 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; @@ -404,6 +424,13 @@ try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} log('捕获 preview (XHR)'); + // 已经成功过 → 直接返回缓存 + 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 }); @@ -596,11 +623,15 @@ async function startProactive() { if (!state.captured) { log('请先手动点一次购买按钮'); - alert('请先手动点一次购买按钮,让脚本捕获请求参数'); + alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数'); + return; + } + if (state.status === 'success') { + log('已经抢到了, 不重复抢购'); return; } setState({ proactive: true }); - log('主动抢购启动 (并发=' + CFG.concurrency + ')'); + log(`极速抢购启动! 前${CFG.turboSec}秒${CFG.turboConcurrency}路并发, 之后${CFG.concurrency}路`); const { url, method, body, headers } = state.captured; const result = await retry(url, { method, body, headers }); @@ -608,11 +639,13 @@ if (result.ok) { setState({ cache: { text: result.text, data: result.data } }); - log('主动模式成功!'); + 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('已点击购买按钮'); } + if (btn) { btn.click(); log('已自动点击购买按钮'); } else { alert('已获取到商品! 请立即点击购买按钮!'); } } } @@ -620,40 +653,90 @@ function stopAll() { stopRequested = true; setState({ proactive: false, status: 'idle', count: 0 }); - if (state.timerId) { clearTimeout(state.timerId); setState({ timerId: null }); } + if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); } log('已停止'); } - // 高精度定时 - function scheduleAt(timeStr) { - if (state.timerId) { clearTimeout(state.timerId); setState({ timerId: null }); } - const parts = timeStr.split(':').map(Number); - const now = new Date(); - const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parts[0], parts[1], parts[2] || 0); - if (target <= now) { log('目标时间已过'); return; } - const ms = target - now; - log(`定时: ${timeStr} (${Math.ceil(ms / 1000)}秒后)`); + // ═══════════════════════════════════════════ + // 北京时间同步 + 自动定时 + // ═══════════════════════════════════════════ + let serverTimeOffset = 0; // 本地时间与服务器时间的差值(ms) - // 提前500ms开始用rAF精确等待 - const earlyMs = Math.max(0, ms - 500); - const tid = setTimeout(() => { - const targetTs = performance.now() + 500; - function tick() { - if (performance.now() >= targetTs) { - log('时间到! 启动抢购!'); - setState({ timerId: null }); - if (state.captured) startProactive(); - else { - const btn = findBuyButton(); - if (btn) btn.click(); - else alert('定时到了! 请手动点击购买!'); - } - return; - } - requestAnimationFrame(tick); + 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; } - requestAnimationFrame(tick); - }, earlyMs); + } 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; + } + + // 自动定时: 同步时间后自动等待到 10:00:00 + 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 }); } @@ -661,9 +744,19 @@ // 预热 async function preheat() { try { - await _fetch(location.origin + '/api/biz/pay/check?bizId=preheat', { credentials: 'include' }); - log('TCP预热完成'); - } catch {} + 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('预热部分失败,不影响使用'); } } // ═══════════════════════════════════════════ @@ -727,7 +820,7 @@ .keys{font-size:10px;color:#636e72;text-align:center;margin-top:6px}