Greasy Fork is available in English.
并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化
// ==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
// ==/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 = `
<h3 style="margin:0 0 15px;color:#333">扫码支付</h3>
<img src="${qrData}" style="width:200px;height:200px" onerror="this.parentElement.innerHTML+='<p>二维码加载失败</p>'">
<p style="margin:15px 0 0;color:#666;font-size:13px">bizId: ${bizId}</p>
<button onclick="this.parentElement.remove()" style="margin-top:10px;padding:6px 20px;border:1px solid #ddd;border-radius:4px;cursor:pointer">关闭</button>
`;
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 = `
<style>
:host{all:initial;position:fixed;top:10px;right:10px;z-index:999999;font-family:Consolas,'Courier New',monospace}
*{box-sizing:border-box;margin:0;padding:0}
.panel{width:360px;background:#1a1a2e;color:#e0e0e0;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.6);font-size:13px;line-height:1.5;user-select:none}
.hd{background:linear-gradient(135deg,#0f3460,#16213e);padding:9px 14px;border-radius:12px 12px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move}
.hd b{font-size:14px;letter-spacing:.5px}
.mn{background:none;border:none;color:#aaa;cursor:pointer;font-size:20px;line-height:1;padding:0 4px}
.mn:hover{color:#fff}
.bd{padding:12px 14px 14px}
.st{padding:8px;border-radius:8px;text-align:center;font-weight:700;margin-bottom:10px;transition:background .3s}
.st-idle{background:#2d3436}
.st-retrying{background:#e17055;animation:pulse 1s infinite}
.st-success{background:#00b894}
.st-failed{background:#d63031}
@keyframes pulse{50%{opacity:.7}}
.cap{font-size:11px;padding:5px 8px;background:#2d3436;border-radius:6px;margin-bottom:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.row{display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;flex-wrap:wrap}
.row input[type=number],.row input[type=time]{width:60px;padding:4px 6px;border:1px solid #444;border-radius:4px;background:#2d3436;color:#fff;text-align:center;font-size:12px}
.btns{display:flex;gap:8px;margin-bottom:10px}
.btns button{flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer;font-weight:700;font-size:12px;color:#fff;transition:opacity .2s}
.btns button:hover{opacity:.85}
.b-go{background:#0984e3}
.b-stop{background:#d63031}
.b-heat{background:#fdcb6e;color:#2d3436}
.b-time{background:#6c5ce7;flex:0 0 auto!important;padding:4px 10px!important}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px;font-size:11px;text-align:center}
.stats div{background:#2d3436;border-radius:4px;padding:4px}
.stats .v{font-size:16px;font-weight:700;color:#74b9ff}
.logs{max-height:180px;overflow-y:auto;background:#0d1117;border-radius:6px;padding:6px 8px;font-size:11px;line-height:1.7}
.logs div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.logs .ok{color:#00b894} .logs .warn{color:#fdcb6e} .logs .err{color:#d63031} .logs .info{color:#dfe6e9}
.logs::-webkit-scrollbar{width:4px}
.logs::-webkit-scrollbar-thumb{background:#444;border-radius:2px}
.keys{font-size:10px;color:#636e72;text-align:center;margin-top:6px}
</style>
<div class="panel">
<div class="hd" id="drag"><b>GLM v4.4</b><button class="mn" id="min">-</button></div>
<div class="bd" id="bd">
<div class="st st-idle" id="st">等待中</div>
<div class="cap" id="cap">${state.captured ? '已恢复上次捕获的请求' : '请先点一次购买按钮'}</div>
<div class="stats">
<div><div class="v" id="s-cnt">0</div>重试</div>
<div><div class="v" id="s-ok">0</div>成功</div>
<div><div class="v" id="s-err">0</div>错误</div>
</div>
<div class="row">
<span>并发</span><input type="number" id="i-conc" value="${CFG.concurrency}" min="1" max="20" step="1">
<span>极速</span><input type="number" id="i-turbo" value="${CFG.turboConcurrency}" min="1" max="20" step="1">
<span>上限</span><input type="number" id="i-max" value="${CFG.maxRetry}" min="10" max="9999" step="50">
</div>
<div class="row">
<span>定时</span><input type="time" id="i-time" step="1">
<button class="b-time" id="b-time">设定</button>
<span id="timer-info" style="color:#6c5ce7;font-size:11px"></span>
</div>
<div class="btns">
<button class="b-go" id="b-go">▶ 主动抢购</button>
<button class="b-stop" id="b-stop" style="display:none">■ 停止</button>
<button class="b-heat" id="b-heat">预热</button>
</div>
<div class="logs" id="logs"></div>
<div class="keys">Alt+S 抢购 | Alt+X 停止 | Alt+H 隐藏</div>
</div>
</div>`;
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.4 已加载 (极速并发+时间同步+全自动抢购)');
if (state.captured) log('已恢复上次捕获的请求参数, 可直接设定时间');
setupDialogWatcher();
// 自动同步服务器时间
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();
}
})();