commit 9beae21556c6846090dcd0ea2b32d3e8a94cad7e Author: qtaxm <17772864223@163.com> Date: Wed Apr 8 11:17:56 2026 +0800 feat: GLM Coding 抢购助手 v4.0 并发重试+自适应间隔+反检测+4层支付恢复 diff --git a/glm-rush-v4.user.js b/glm-rush-v4.user.js new file mode 100644 index 0000000..670cdfd --- /dev/null +++ b/glm-rush-v4.user.js @@ -0,0 +1,855 @@ +// ==UserScript== +// @name 智谱 GLM Coding 抢购助手 v4.0 +// @namespace http://tampermonkey.net/ +// @version 4.0 +// @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: 3, // 并发路数 + maxRetry: 500, // 最大重试次数 + burstCount: 10, // 前N次零延迟爆发 + fastDelay: 50, // 爆发后的快速间隔 + slowDelay: 150, // 后期随机间隔中值 + jitter: 0.3, // 间隔随机抖动 ±30% + recoveryMax: 3, // 弹窗恢复最大次数 + logMax: 100, // 日志条数上限 + 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); + } + } + + // 仅对目标URL响应做patch的标记 + let _shouldPatch = false; + + JSON.parse = function (text, reviver) { + const result = _parse(text, reviver); + if (_shouldPatch) { + try { patchSoldOut(result); } catch {} + _shouldPatch = false; + } + 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 resp = await _fetch(url, { ...opts, 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 }; + } + + _shouldPatch = true; // 让 JSON.parse 对此响应做 patch + const text = await resp.text(); + let data; + try { data = _parse(text); } catch { data = null; } + _shouldPatch = false; + + 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; + + while (totalAttempt < CFG.maxRetry && !stopRequested) { + const batchSize = Math.min(CFG.concurrency, 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; + + // 日志 (前5次 + 每20次) + if (totalAttempt <= 5 * CFG.concurrency || totalAttempt % (20 * CFG.concurrency) === 0) { + log(`#${totalAttempt} ${reasons[0]}`); + } + + // 自适应延迟 + 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 {} + log('捕获 preview (Fetch)'); + + 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' } }); + } + + 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]); + } + + 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 {} + log('捕获 preview (XHR)'); + + if (state.cache) { + log('返回缓存响应 (XHR)'); + const c = state.cache; setState({ cache: null }); + recoveryAttempts = 0; + fakeXHR(self, c.text); + return; + } + + retry(url, { method: this._m, body, headers: this._h || {} }).then(result => { + fakeXHR(self, result.ok ? result.text : '{"code":-1,"msg":"重试失败"}'); + }); + return; + } + + 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 }); + } + + // ═══════════════════════════════════════════ + // 主动抢购 & 定时 + // ═══════════════════════════════════════════ + function findBuyButton() { + 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; + } + + async function startProactive() { + if (!state.captured) { + log('请先手动点一次购买按钮'); + alert('请先手动点一次购买按钮,让脚本捕获请求参数'); + return; + } + setState({ proactive: true }); + log('主动抢购启动 (并发=' + 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('主动模式成功!'); + const errDlg = findErrorDialog(); + if (errDlg) { dismissDialog(errDlg); await sleep(300); } + const btn = findBuyButton(); + if (btn) { btn.click(); log('已点击购买按钮'); } + else { alert('已获取到商品! 请立即点击购买按钮!'); } + } + } + + function stopAll() { + stopRequested = true; + setState({ proactive: false, status: 'idle', count: 0 }); + if (state.timerId) { clearTimeout(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)}秒后)`); + + // 提前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); + } + requestAnimationFrame(tick); + }, earlyMs); + + setState({ timerId: tid }); + } + + // 预热 + async function preheat() { + try { + await _fetch(location.origin + '/api/biz/pay/check?bizId=preheat', { credentials: 'include' }); + log('TCP预热完成'); + } catch {} + } + + // ═══════════════════════════════════════════ + // 快捷键 + // ═══════════════════════════════════════════ + 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 = ` + +