glm.js
· 26 KiB · JavaScript
Eredeti
// ==UserScript==
// @name 智谱 GLM Coding 终极抢购助手 (全自动版)
// @namespace http://tampermonkey.net/
// @version 3.2
// @description 全自动抢购:自动重试 preview + check 双重校验 + 错误弹窗自动恢复 + 主动模式 + 定时触发
// @author Assistant
// @match *://www.bigmodel.cn/*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ======================== 配置 ========================
const CFG = {
delay: 100,
maxRetry: 300,
PREVIEW: '/api/biz/pay/preview',
CHECK: '/api/biz/pay/check',
};
// ======================== 全局状态 ========================
const S = {
status: 'idle',
count: 0,
bizId: null,
captured: null,
cache: null,
lastSuccess: null, // 最近一次成功的响应,用于弹窗恢复
proactive: false,
timerId: null,
logs: [],
};
let stopRequested = false;
let recovering = false;
let recoveryAttempts = 0;
// ======================== 工具函数 ========================
const sleep = ms => new Promise(r => setTimeout(r, ms));
const ts = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
function log(msg) {
S.logs.push(`${ts()} ${msg}`);
if (S.logs.length > 80) S.logs.shift();
console.log(`[GLM抢购] ${msg}`);
refreshLog();
}
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 深层篡改 (UI 补丁) ========================
const _parse = JSON.parse;
JSON.parse = function (text, reviver) {
let result = _parse(text, reviver);
try {
(function fix(obj) {
if (!obj || typeof obj !== 'object') return;
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 (let k in obj) if (obj[k] && typeof obj[k] === 'object') fix(obj[k]);
})(result);
} catch (e) {}
return result;
};
// ======================== 二、核心重试引擎 ========================
const _fetch = window.fetch;
let _retryPromise = null;
async function retry(url, opts) {
if (_retryPromise) {
log('⏳ 合并到当前重试…');
return _retryPromise;
}
stopRequested = false;
_retryPromise = (async () => {
S.status = 'retrying';
S.count = 0;
refreshUI();
// 剥离 AbortSignal,防止前端超时中断我们的重试
const { signal, ...cleanOpts } = opts || {};
for (let i = 1; i <= CFG.maxRetry; i++) {
if (stopRequested) {
log('⏹ 重试被手动停止');
break;
}
S.count = i;
refreshUI();
try {
const resp = await _fetch(url, { ...cleanOpts, credentials: 'include' });
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;
log(`🔑 获取到 bizId=${bizId},正在校验 check 接口…`);
// 关键校验:调用 check 接口确认 bizId 是否有效
try {
const checkUrl = `${location.origin}${CFG.CHECK}?bizId=${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') {
// bizId 已过期,继续重试 preview
log(`#${i} bizId 已过期(EXPIRE),继续重试…`);
await sleep(CFG.delay);
continue;
}
// check 返回非 EXPIRE → 真正成功!
S.status = 'success';
S.bizId = bizId;
S.lastSuccess = { text, data };
log(`✅ 抢购成功! bizId=${bizId}, check 校验通过 (第${i}次)`);
refreshUI();
recoveryAttempts = 0;
setTimeout(autoRecover, 600);
return { ok: true, text, data, status: resp.status };
} catch (checkErr) {
// check 接口本身出错,也继续重试
log(`#${i} check 校验异常: ${checkErr.message},继续重试…`);
await sleep(CFG.delay);
continue;
}
}
const why = !data ? '非JSON响应'
: data.code === 555 ? '系统繁忙(555)'
: (data.data && data.data.bizId === null) ? '售罄(bizId=null)'
: `未知(code=${data.code})`;
if (i <= 5 || i % 20 === 0) log(`#${i} ${why}`);
} catch (e) {
if (i <= 3 || i % 20 === 0) log(`#${i} 网络错误: ${e.message}`);
}
await sleep(CFG.delay);
}
if (!stopRequested) {
S.status = 'failed';
log(`❌ 达到上限 ${CFG.maxRetry} 次`);
} else {
S.status = 'idle';
}
refreshUI();
return { ok: false };
})();
try { return await _retryPromise; }
finally { _retryPromise = null; }
}
// ======================== 三、错误弹窗自动恢复 ========================
/** 查找页面上可见的错误弹窗 */
function findErrorDialog() {
const selectors = [
'.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 selectors) {
for (const el of document.querySelectorAll(sel)) {
// 必须可见
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
if (!el.offsetParent && style.position !== 'fixed') continue;
const text = el.textContent || '';
if (/购买人数过多|系统繁忙|稍后再试|请重试|繁忙|失败|出错|异常/.test(text)) {
return el;
}
}
}
return null;
}
/** 关闭弹窗 */
function dismissDialog(dialog) {
// 策略 1:找关闭按钮
const closeSelectors = [
'.el-dialog__headerbtn', '.el-message-box__headerbtn',
'.el-dialog__close', '.ant-modal-close',
'[class*="close-btn"]', '[class*="closeBtn"]',
'[aria-label="Close"]', '[aria-label="close"]',
];
for (const sel of closeSelectors) {
const btn = dialog.querySelector(sel) || document.querySelector(sel);
if (btn && btn.offsetParent !== null) {
btn.click();
log('🔄 点击关闭按钮');
return true;
}
}
// 策略 2:找弹窗内的 确定/取消/关闭 按钮
const btns = dialog.querySelectorAll('button, [role="button"]');
for (const btn of btns) {
const t = (btn.textContent || '').trim();
if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) {
btn.click();
log(`🔄 点击 [${t}] 按钮`);
return true;
}
}
// 策略 3:按 Escape
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }));
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', keyCode: 27, bubbles: true }));
log('🔄 发送 Escape 键');
// 策略 4:点击遮罩层
const masks = document.querySelectorAll('.el-overlay, .v-modal, .el-overlay-dialog, [class*="overlay"], [class*="mask"]');
for (const mask of masks) {
if (mask.offsetParent !== null || window.getComputedStyle(mask).position === 'fixed') {
mask.click();
log('🔄 点击遮罩层');
return true;
}
}
// 策略 5:强制隐藏
dialog.style.display = 'none';
const overlays = document.querySelectorAll('.el-overlay, .v-modal');
overlays.forEach(o => (o.style.display = 'none'));
log('🔄 强制隐藏弹窗');
return true;
}
/** 自动恢复:关闭错误弹窗 → 重新触发购买 */
async function autoRecover() {
if (recovering || recoveryAttempts >= 3) return;
if (!S.lastSuccess) return;
const dialog = findErrorDialog();
if (!dialog) return; // 没有错误弹窗,说明前端已正常处理,无需恢复
recovering = true;
recoveryAttempts++;
try {
log('🔄 检测到错误弹窗,启动自动恢复…');
// 把成功响应放入缓存,供拦截器下一次返回
S.cache = S.lastSuccess;
// 关闭错误弹窗
dismissDialog(dialog);
await sleep(500);
// 再次确认弹窗关了(有些弹窗有动画延迟)
const stillThere = findErrorDialog();
if (stillThere) {
dismissDialog(stillThere);
await sleep(300);
}
// 重新触发购买按钮 → 拦截器返回缓存响应 → 前端弹出支付二维码
const btn = findBuyButton();
if (btn) {
btn.click();
log('🖱 已自动重新点击购买按钮');
} else {
log('⚠️ 未找到购买按钮,请手动点击');
alert('已获取到商品!请立即手动点击购买按钮!');
}
} finally {
recovering = false;
}
}
/** 持续监控:每 500ms 检查一次是否有需要恢复的错误弹窗 */
function setupDialogWatcher() {
setInterval(() => {
if (S.lastSuccess && !recovering && recoveryAttempts < 3) {
const dialog = findErrorDialog();
if (dialog) autoRecover();
}
}, 500);
}
// ======================== 四、Fetch 拦截器 ========================
window.fetch = async function (input, init) {
const url = typeof input === 'string' ? input : input?.url;
if (url && url.includes(CFG.PREVIEW)) {
S.captured = {
url,
method: init?.method || 'POST',
body: init?.body,
headers: extractHeaders(init?.headers),
};
log('🎯 捕获 preview 请求 (Fetch)');
refreshUI();
// 有缓存 → 直接返回(来自主动模式或弹窗恢复)
if (S.cache) {
log('📦 返回缓存的成功响应');
const c = S.cache;
S.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),
signal: init?.signal, // 传入以便 retry 内部剥离
});
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(JSON.stringify({ code: -1, msg: '等待有效bizId' }), {
status: 200, headers: { 'Content-Type': 'application/json' },
});
}
return _fetch.apply(this, [input, init]);
};
// ======================== 五、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;
S.captured = { url, method: this._m, body, headers: this._h || {} };
log('🎯 捕获 preview 请求 (XHR)');
refreshUI();
if (S.cache) {
log('📦 返回缓存的成功响应 (XHR)');
const c = S.cache; S.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')) {
log('🚫 拦截 check(bizId=null) (XHR)');
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 rsc = new Event('readystatechange');
if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(rsc);
xhr.dispatchEvent(rsc);
const load = new ProgressEvent('load');
if (typeof xhr.onload === 'function') xhr.onload(load);
xhr.dispatchEvent(load);
xhr.dispatchEvent(new ProgressEvent('loadend'));
}, 0);
}
// ======================== 六、主动抢购模式 ========================
async function startProactive() {
if (!S.captured) {
log('⚠️ 请先手动点一次购买/订阅按钮');
alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数');
return;
}
S.proactive = true;
log('🚀 主动抢购模式启动');
const { url, method, body, headers } = S.captured;
const result = await retry(url, { method, body, headers });
S.proactive = false;
if (result.ok) {
S.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 {
log('⚠️ 未找到按钮,请手动点击');
alert('已获取到商品!请立即点击购买按钮!');
}
}
}
function stopAll() {
stopRequested = true;
S.proactive = false;
S.status = 'idle';
S.count = 0;
if (S.timerId) { clearTimeout(S.timerId); S.timerId = null; }
log('⏹ 已停止');
refreshUI();
}
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;
}
// ======================== 七、定时触发 ========================
function scheduleAt(timeStr) {
if (S.timerId) { clearTimeout(S.timerId); S.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)}秒后)`);
S.timerId = setTimeout(() => {
S.timerId = null;
log('⏰ 时间到! 启动抢购!');
if (S.captured) {
startProactive();
} else {
const btn = findBuyButton();
if (btn) { btn.click(); log('🖱 定时: 已点击购买按钮'); }
else { log('⚠️ 未找到按钮'); alert('定时到了!请手动点击购买!'); }
}
}, ms - 50);
refreshUI();
}
// ======================== 八、浮动控制面板 ========================
function createPanel() {
const panel = document.createElement('div');
panel.id = 'glm-rush';
panel.innerHTML = `
<style>
#glm-rush{position:fixed;top:10px;right:10px;width:340px;background:#1a1a2e;color:#e0e0e0;
border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.6);z-index:999999;
font:13px/1.5 Consolas,'Courier New',monospace;user-select:none}
#glm-rush *{box-sizing:border-box;margin:0;padding:0}
.glm-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}
.glm-hd b{font-size:14px;letter-spacing:.5px}
.glm-mn{background:none;border:none;color:#aaa;cursor:pointer;font-size:20px;line-height:1;padding:0 4px}
.glm-mn:hover{color:#fff}
.glm-bd{padding:12px 14px 14px}
.glm-st{padding:8px;border-radius:8px;text-align:center;font-weight:700;margin-bottom:10px;transition:background .3s}
.glm-st-idle{background:#2d3436}
.glm-st-retrying{background:#e17055;animation:glm-pulse 1s infinite}
.glm-st-success{background:#00b894}
.glm-st-failed{background:#d63031}
@keyframes glm-pulse{50%{opacity:.7}}
.glm-cap{font-size:11px;padding:5px 8px;background:#2d3436;border-radius:6px;margin-bottom:10px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.glm-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;flex-wrap:wrap}
.glm-row input[type=number],.glm-row input[type=time]{
width:72px;padding:4px 6px;border:1px solid #444;border-radius:4px;
background:#2d3436;color:#fff;text-align:center;font-size:12px}
.glm-btns{display:flex;gap:8px;margin-bottom:10px}
.glm-btns button{flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer;
font-weight:700;font-size:12px;color:#fff;transition:opacity .2s}
.glm-btns button:hover{opacity:.85}
.glm-b-go{background:#0984e3}
.glm-b-stop{background:#d63031}
.glm-b-time{background:#6c5ce7;flex:0 0 auto !important;padding:4px 10px !important}
.glm-logs{max-height:170px;overflow-y:auto;background:#0d1117;border-radius:6px;
padding:6px 8px;font-size:11px;line-height:1.7}
.glm-logs div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.glm-logs::-webkit-scrollbar{width:4px}
.glm-logs::-webkit-scrollbar-thumb{background:#444;border-radius:2px}
</style>
<div class="glm-hd" id="glm-drag">
<b>🎯 GLM 抢购助手 v3.2</b>
<button class="glm-mn" id="glm-min">−</button>
</div>
<div class="glm-bd" id="glm-bd">
<div class="glm-st glm-st-idle" id="glm-st">⏳ 等待中</div>
<div class="glm-cap" id="glm-cap">📡 请求: 未捕获 — 请先点一次购买按钮</div>
<div class="glm-row">
<span>间隔</span><input type="number" id="glm-delay" value="${CFG.delay}" min="50" max="5000" step="50"><span>ms</span>
<span style="margin-left:6px">上限</span><input type="number" id="glm-max" value="${CFG.maxRetry}" min="10" max="9999" step="10"><span>次</span>
</div>
<div class="glm-row">
<span>定时</span><input type="time" id="glm-time" step="1">
<button class="glm-b-time" id="glm-time-set">设定</button>
<span id="glm-timer-info" style="color:#6c5ce7;font-size:11px"></span>
</div>
<div class="glm-btns">
<button class="glm-b-go" id="glm-go">▶ 主动抢购</button>
<button class="glm-b-stop" id="glm-stop" style="display:none">■ 停止</button>
</div>
<div class="glm-logs" id="glm-logs"></div>
</div>`;
document.body.appendChild(panel);
const $ = id => document.getElementById(id);
$('glm-go').onclick = startProactive;
$('glm-stop').onclick = stopAll;
$('glm-delay').onchange = function () { CFG.delay = Math.max(50, +this.value || 100); };
$('glm-max').onchange = function () { CFG.maxRetry = Math.max(10, +this.value || 300); };
$('glm-time-set').onclick = function () { const v = $('glm-time').value; if (v) scheduleAt(v); };
$('glm-min').onclick = function () {
const bd = $('glm-bd');
const hidden = bd.style.display === 'none';
bd.style.display = hidden ? '' : 'none';
this.textContent = hidden ? '−' : '+';
};
// 拖拽
let sx, sy, sl, st;
$('glm-drag').onmousedown = function (e) {
sx = e.clientX; sy = e.clientY;
const rect = panel.getBoundingClientRect();
sl = rect.left; st = rect.top;
const onMove = function (e) {
panel.style.left = (sl + e.clientX - sx) + 'px';
panel.style.top = (st + e.clientY - sy) + 'px';
panel.style.right = 'auto';
};
const onUp = function () {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
};
log('🚀 v3.2 已加载 (preview+check 双重校验)');
// 启动错误弹窗监控
setupDialogWatcher();
}
function refreshUI() {
const stEl = document.getElementById('glm-st');
if (!stEl) return;
stEl.className = 'glm-st glm-st-' + S.status;
stEl.textContent = S.status === 'idle' ? '⏳ 等待中'
: S.status === 'retrying' ? `🔄 重试中… ${S.count}/${CFG.maxRetry}`
: S.status === 'success' ? `✅ 成功! bizId=${S.bizId}`
: `❌ 失败 (${S.count}次)`;
const capEl = document.getElementById('glm-cap');
if (capEl) {
capEl.textContent = S.captured
? `📡 已捕获: ${S.captured.method} …${S.captured.url.split('?')[0].slice(-30)}`
: '📡 请求: 未捕获 — 请先点一次购买按钮';
}
const goBtn = document.getElementById('glm-go');
const stopBtn = document.getElementById('glm-stop');
if (goBtn && stopBtn) {
goBtn.style.display = S.status === 'retrying' ? 'none' : '';
stopBtn.style.display = S.status === 'retrying' ? '' : 'none';
}
}
function refreshLog() {
const el = document.getElementById('glm-logs');
if (!el) return;
const last = S.logs[S.logs.length - 1];
if (last) {
const div = document.createElement('div');
div.textContent = last;
el.appendChild(div);
while (el.children.length > 80) el.removeChild(el.firstChild);
el.scrollTop = el.scrollHeight;
}
}
// ======================== 启动 ========================
console.log('[GLM抢购] 🚀 v3.2 全自动版 (preview+check 双重校验) 已注入');
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createPanel);
} else {
createPanel();
}
})();
| 1 | // ==UserScript== |
| 2 | // @name 智谱 GLM Coding 终极抢购助手 (全自动版) |
| 3 | // @namespace http://tampermonkey.net/ |
| 4 | // @version 3.2 |
| 5 | // @description 全自动抢购:自动重试 preview + check 双重校验 + 错误弹窗自动恢复 + 主动模式 + 定时触发 |
| 6 | // @author Assistant |
| 7 | // @match *://www.bigmodel.cn/* |
| 8 | // @run-at document-start |
| 9 | // @grant none |
| 10 | // ==/UserScript== |
| 11 | |
| 12 | (function () { |
| 13 | 'use strict'; |
| 14 | |
| 15 | // ======================== 配置 ======================== |
| 16 | const CFG = { |
| 17 | delay: 100, |
| 18 | maxRetry: 300, |
| 19 | PREVIEW: '/api/biz/pay/preview', |
| 20 | CHECK: '/api/biz/pay/check', |
| 21 | }; |
| 22 | |
| 23 | // ======================== 全局状态 ======================== |
| 24 | const S = { |
| 25 | status: 'idle', |
| 26 | count: 0, |
| 27 | bizId: null, |
| 28 | captured: null, |
| 29 | cache: null, |
| 30 | lastSuccess: null, // 最近一次成功的响应,用于弹窗恢复 |
| 31 | proactive: false, |
| 32 | timerId: null, |
| 33 | logs: [], |
| 34 | }; |
| 35 | |
| 36 | let stopRequested = false; |
| 37 | let recovering = false; |
| 38 | let recoveryAttempts = 0; |
| 39 | |
| 40 | // ======================== 工具函数 ======================== |
| 41 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 42 | const ts = () => new Date().toLocaleTimeString('zh-CN', { hour12: false }); |
| 43 | |
| 44 | function log(msg) { |
| 45 | S.logs.push(`${ts()} ${msg}`); |
| 46 | if (S.logs.length > 80) S.logs.shift(); |
| 47 | console.log(`[GLM抢购] ${msg}`); |
| 48 | refreshLog(); |
| 49 | } |
| 50 | |
| 51 | function extractHeaders(h) { |
| 52 | const o = {}; |
| 53 | if (!h) return o; |
| 54 | if (h instanceof Headers) h.forEach((v, k) => (o[k] = v)); |
| 55 | else if (Array.isArray(h)) h.forEach(([k, v]) => (o[k] = v)); |
| 56 | else Object.entries(h).forEach(([k, v]) => (o[k] = v)); |
| 57 | return o; |
| 58 | } |
| 59 | |
| 60 | // ======================== 一、JSON.parse 深层篡改 (UI 补丁) ======================== |
| 61 | const _parse = JSON.parse; |
| 62 | JSON.parse = function (text, reviver) { |
| 63 | let result = _parse(text, reviver); |
| 64 | try { |
| 65 | (function fix(obj) { |
| 66 | if (!obj || typeof obj !== 'object') return; |
| 67 | if (obj.isSoldOut === true) obj.isSoldOut = false; |
| 68 | if (obj.soldOut === true) obj.soldOut = false; |
| 69 | if (obj.disabled === true && (obj.price !== undefined || obj.productId || obj.title)) obj.disabled = false; |
| 70 | if (obj.stock === 0) obj.stock = 999; |
| 71 | for (let k in obj) if (obj[k] && typeof obj[k] === 'object') fix(obj[k]); |
| 72 | })(result); |
| 73 | } catch (e) {} |
| 74 | return result; |
| 75 | }; |
| 76 | |
| 77 | // ======================== 二、核心重试引擎 ======================== |
| 78 | const _fetch = window.fetch; |
| 79 | let _retryPromise = null; |
| 80 | |
| 81 | async function retry(url, opts) { |
| 82 | if (_retryPromise) { |
| 83 | log('⏳ 合并到当前重试…'); |
| 84 | return _retryPromise; |
| 85 | } |
| 86 | |
| 87 | stopRequested = false; |
| 88 | |
| 89 | _retryPromise = (async () => { |
| 90 | S.status = 'retrying'; |
| 91 | S.count = 0; |
| 92 | refreshUI(); |
| 93 | |
| 94 | // 剥离 AbortSignal,防止前端超时中断我们的重试 |
| 95 | const { signal, ...cleanOpts } = opts || {}; |
| 96 | |
| 97 | for (let i = 1; i <= CFG.maxRetry; i++) { |
| 98 | if (stopRequested) { |
| 99 | log('⏹ 重试被手动停止'); |
| 100 | break; |
| 101 | } |
| 102 | |
| 103 | S.count = i; |
| 104 | refreshUI(); |
| 105 | |
| 106 | try { |
| 107 | const resp = await _fetch(url, { ...cleanOpts, credentials: 'include' }); |
| 108 | const text = await resp.text(); |
| 109 | let data; |
| 110 | try { data = _parse(text); } catch { data = null; } |
| 111 | |
| 112 | if (data && data.code === 200 && data.data && data.data.bizId) { |
| 113 | const bizId = data.data.bizId; |
| 114 | log(`🔑 获取到 bizId=${bizId},正在校验 check 接口…`); |
| 115 | |
| 116 | // 关键校验:调用 check 接口确认 bizId 是否有效 |
| 117 | try { |
| 118 | const checkUrl = `${location.origin}${CFG.CHECK}?bizId=${bizId}`; |
| 119 | const checkResp = await _fetch(checkUrl, { credentials: 'include' }); |
| 120 | const checkText = await checkResp.text(); |
| 121 | let checkData; |
| 122 | try { checkData = _parse(checkText); } catch { checkData = null; } |
| 123 | |
| 124 | if (checkData && checkData.data === 'EXPIRE') { |
| 125 | // bizId 已过期,继续重试 preview |
| 126 | log(`#${i} bizId 已过期(EXPIRE),继续重试…`); |
| 127 | await sleep(CFG.delay); |
| 128 | continue; |
| 129 | } |
| 130 | |
| 131 | // check 返回非 EXPIRE → 真正成功! |
| 132 | S.status = 'success'; |
| 133 | S.bizId = bizId; |
| 134 | S.lastSuccess = { text, data }; |
| 135 | log(`✅ 抢购成功! bizId=${bizId}, check 校验通过 (第${i}次)`); |
| 136 | refreshUI(); |
| 137 | recoveryAttempts = 0; |
| 138 | setTimeout(autoRecover, 600); |
| 139 | return { ok: true, text, data, status: resp.status }; |
| 140 | } catch (checkErr) { |
| 141 | // check 接口本身出错,也继续重试 |
| 142 | log(`#${i} check 校验异常: ${checkErr.message},继续重试…`); |
| 143 | await sleep(CFG.delay); |
| 144 | continue; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | const why = !data ? '非JSON响应' |
| 149 | : data.code === 555 ? '系统繁忙(555)' |
| 150 | : (data.data && data.data.bizId === null) ? '售罄(bizId=null)' |
| 151 | : `未知(code=${data.code})`; |
| 152 | if (i <= 5 || i % 20 === 0) log(`#${i} ${why}`); |
| 153 | } catch (e) { |
| 154 | if (i <= 3 || i % 20 === 0) log(`#${i} 网络错误: ${e.message}`); |
| 155 | } |
| 156 | |
| 157 | await sleep(CFG.delay); |
| 158 | } |
| 159 | |
| 160 | if (!stopRequested) { |
| 161 | S.status = 'failed'; |
| 162 | log(`❌ 达到上限 ${CFG.maxRetry} 次`); |
| 163 | } else { |
| 164 | S.status = 'idle'; |
| 165 | } |
| 166 | refreshUI(); |
| 167 | return { ok: false }; |
| 168 | })(); |
| 169 | |
| 170 | try { return await _retryPromise; } |
| 171 | finally { _retryPromise = null; } |
| 172 | } |
| 173 | |
| 174 | // ======================== 三、错误弹窗自动恢复 ======================== |
| 175 | |
| 176 | /** 查找页面上可见的错误弹窗 */ |
| 177 | function findErrorDialog() { |
| 178 | const selectors = [ |
| 179 | '.el-dialog', '.el-message-box', '.el-dialog__wrapper', |
| 180 | '.ant-modal', '.ant-modal-wrap', |
| 181 | '[class*="modal"]', '[class*="dialog"]', '[class*="popup"]', |
| 182 | '[role="dialog"]', |
| 183 | ]; |
| 184 | for (const sel of selectors) { |
| 185 | for (const el of document.querySelectorAll(sel)) { |
| 186 | // 必须可见 |
| 187 | const style = window.getComputedStyle(el); |
| 188 | if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue; |
| 189 | if (!el.offsetParent && style.position !== 'fixed') continue; |
| 190 | |
| 191 | const text = el.textContent || ''; |
| 192 | if (/购买人数过多|系统繁忙|稍后再试|请重试|繁忙|失败|出错|异常/.test(text)) { |
| 193 | return el; |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | return null; |
| 198 | } |
| 199 | |
| 200 | /** 关闭弹窗 */ |
| 201 | function dismissDialog(dialog) { |
| 202 | // 策略 1:找关闭按钮 |
| 203 | const closeSelectors = [ |
| 204 | '.el-dialog__headerbtn', '.el-message-box__headerbtn', |
| 205 | '.el-dialog__close', '.ant-modal-close', |
| 206 | '[class*="close-btn"]', '[class*="closeBtn"]', |
| 207 | '[aria-label="Close"]', '[aria-label="close"]', |
| 208 | ]; |
| 209 | for (const sel of closeSelectors) { |
| 210 | const btn = dialog.querySelector(sel) || document.querySelector(sel); |
| 211 | if (btn && btn.offsetParent !== null) { |
| 212 | btn.click(); |
| 213 | log('🔄 点击关闭按钮'); |
| 214 | return true; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // 策略 2:找弹窗内的 确定/取消/关闭 按钮 |
| 219 | const btns = dialog.querySelectorAll('button, [role="button"]'); |
| 220 | for (const btn of btns) { |
| 221 | const t = (btn.textContent || '').trim(); |
| 222 | if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) { |
| 223 | btn.click(); |
| 224 | log(`🔄 点击 [${t}] 按钮`); |
| 225 | return true; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | // 策略 3:按 Escape |
| 230 | document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true })); |
| 231 | document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', keyCode: 27, bubbles: true })); |
| 232 | log('🔄 发送 Escape 键'); |
| 233 | |
| 234 | // 策略 4:点击遮罩层 |
| 235 | const masks = document.querySelectorAll('.el-overlay, .v-modal, .el-overlay-dialog, [class*="overlay"], [class*="mask"]'); |
| 236 | for (const mask of masks) { |
| 237 | if (mask.offsetParent !== null || window.getComputedStyle(mask).position === 'fixed') { |
| 238 | mask.click(); |
| 239 | log('🔄 点击遮罩层'); |
| 240 | return true; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // 策略 5:强制隐藏 |
| 245 | dialog.style.display = 'none'; |
| 246 | const overlays = document.querySelectorAll('.el-overlay, .v-modal'); |
| 247 | overlays.forEach(o => (o.style.display = 'none')); |
| 248 | log('🔄 强制隐藏弹窗'); |
| 249 | return true; |
| 250 | } |
| 251 | |
| 252 | /** 自动恢复:关闭错误弹窗 → 重新触发购买 */ |
| 253 | async function autoRecover() { |
| 254 | if (recovering || recoveryAttempts >= 3) return; |
| 255 | if (!S.lastSuccess) return; |
| 256 | |
| 257 | const dialog = findErrorDialog(); |
| 258 | if (!dialog) return; // 没有错误弹窗,说明前端已正常处理,无需恢复 |
| 259 | |
| 260 | recovering = true; |
| 261 | recoveryAttempts++; |
| 262 | |
| 263 | try { |
| 264 | log('🔄 检测到错误弹窗,启动自动恢复…'); |
| 265 | |
| 266 | // 把成功响应放入缓存,供拦截器下一次返回 |
| 267 | S.cache = S.lastSuccess; |
| 268 | |
| 269 | // 关闭错误弹窗 |
| 270 | dismissDialog(dialog); |
| 271 | await sleep(500); |
| 272 | |
| 273 | // 再次确认弹窗关了(有些弹窗有动画延迟) |
| 274 | const stillThere = findErrorDialog(); |
| 275 | if (stillThere) { |
| 276 | dismissDialog(stillThere); |
| 277 | await sleep(300); |
| 278 | } |
| 279 | |
| 280 | // 重新触发购买按钮 → 拦截器返回缓存响应 → 前端弹出支付二维码 |
| 281 | const btn = findBuyButton(); |
| 282 | if (btn) { |
| 283 | btn.click(); |
| 284 | log('🖱 已自动重新点击购买按钮'); |
| 285 | } else { |
| 286 | log('⚠️ 未找到购买按钮,请手动点击'); |
| 287 | alert('已获取到商品!请立即手动点击购买按钮!'); |
| 288 | } |
| 289 | } finally { |
| 290 | recovering = false; |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | /** 持续监控:每 500ms 检查一次是否有需要恢复的错误弹窗 */ |
| 295 | function setupDialogWatcher() { |
| 296 | setInterval(() => { |
| 297 | if (S.lastSuccess && !recovering && recoveryAttempts < 3) { |
| 298 | const dialog = findErrorDialog(); |
| 299 | if (dialog) autoRecover(); |
| 300 | } |
| 301 | }, 500); |
| 302 | } |
| 303 | |
| 304 | // ======================== 四、Fetch 拦截器 ======================== |
| 305 | window.fetch = async function (input, init) { |
| 306 | const url = typeof input === 'string' ? input : input?.url; |
| 307 | |
| 308 | if (url && url.includes(CFG.PREVIEW)) { |
| 309 | S.captured = { |
| 310 | url, |
| 311 | method: init?.method || 'POST', |
| 312 | body: init?.body, |
| 313 | headers: extractHeaders(init?.headers), |
| 314 | }; |
| 315 | log('🎯 捕获 preview 请求 (Fetch)'); |
| 316 | refreshUI(); |
| 317 | |
| 318 | // 有缓存 → 直接返回(来自主动模式或弹窗恢复) |
| 319 | if (S.cache) { |
| 320 | log('📦 返回缓存的成功响应'); |
| 321 | const c = S.cache; |
| 322 | S.cache = null; |
| 323 | recoveryAttempts = 0; // 重置恢复计数 |
| 324 | return new Response(c.text, { |
| 325 | status: 200, |
| 326 | headers: { 'Content-Type': 'application/json' }, |
| 327 | }); |
| 328 | } |
| 329 | |
| 330 | const result = await retry(url, { |
| 331 | method: init?.method || 'POST', |
| 332 | body: init?.body, |
| 333 | headers: extractHeaders(init?.headers), |
| 334 | signal: init?.signal, // 传入以便 retry 内部剥离 |
| 335 | }); |
| 336 | |
| 337 | if (result.ok) { |
| 338 | return new Response(result.text, { |
| 339 | status: result.status, |
| 340 | headers: { 'Content-Type': 'application/json' }, |
| 341 | }); |
| 342 | } |
| 343 | return _fetch.apply(this, [input, init]); |
| 344 | } |
| 345 | |
| 346 | if (url && url.includes(CFG.CHECK) && url.includes('bizId=null')) { |
| 347 | log('🚫 拦截 check(bizId=null)'); |
| 348 | return new Response(JSON.stringify({ code: -1, msg: '等待有效bizId' }), { |
| 349 | status: 200, headers: { 'Content-Type': 'application/json' }, |
| 350 | }); |
| 351 | } |
| 352 | |
| 353 | return _fetch.apply(this, [input, init]); |
| 354 | }; |
| 355 | |
| 356 | // ======================== 五、XHR 拦截器 ======================== |
| 357 | const _xhrOpen = XMLHttpRequest.prototype.open; |
| 358 | const _xhrSend = XMLHttpRequest.prototype.send; |
| 359 | const _xhrSetHeader = XMLHttpRequest.prototype.setRequestHeader; |
| 360 | |
| 361 | XMLHttpRequest.prototype.setRequestHeader = function (k, v) { |
| 362 | (this._h || (this._h = {}))[k] = v; |
| 363 | return _xhrSetHeader.call(this, k, v); |
| 364 | }; |
| 365 | XMLHttpRequest.prototype.open = function (method, url) { |
| 366 | this._m = method; |
| 367 | this._u = url; |
| 368 | return _xhrOpen.apply(this, arguments); |
| 369 | }; |
| 370 | XMLHttpRequest.prototype.send = function (body) { |
| 371 | const url = this._u; |
| 372 | |
| 373 | if (typeof url === 'string' && url.includes(CFG.PREVIEW)) { |
| 374 | const self = this; |
| 375 | S.captured = { url, method: this._m, body, headers: this._h || {} }; |
| 376 | log('🎯 捕获 preview 请求 (XHR)'); |
| 377 | refreshUI(); |
| 378 | |
| 379 | if (S.cache) { |
| 380 | log('📦 返回缓存的成功响应 (XHR)'); |
| 381 | const c = S.cache; S.cache = null; |
| 382 | recoveryAttempts = 0; |
| 383 | fakeXHR(self, c.text); |
| 384 | return; |
| 385 | } |
| 386 | |
| 387 | retry(url, { method: this._m, body, headers: this._h || {} }).then(result => { |
| 388 | fakeXHR(self, result.ok ? result.text : '{"code":-1,"msg":"重试失败"}'); |
| 389 | }); |
| 390 | return; |
| 391 | } |
| 392 | |
| 393 | if (typeof url === 'string' && url.includes(CFG.CHECK) && url.includes('bizId=null')) { |
| 394 | log('🚫 拦截 check(bizId=null) (XHR)'); |
| 395 | fakeXHR(this, '{"code":-1,"msg":"等待有效bizId"}'); |
| 396 | return; |
| 397 | } |
| 398 | |
| 399 | return _xhrSend.call(this, body); |
| 400 | }; |
| 401 | |
| 402 | function fakeXHR(xhr, text) { |
| 403 | setTimeout(() => { |
| 404 | const dp = (k, v) => Object.defineProperty(xhr, k, { value: v, configurable: true }); |
| 405 | dp('readyState', 4); dp('status', 200); dp('statusText', 'OK'); |
| 406 | dp('responseText', text); dp('response', text); |
| 407 | const rsc = new Event('readystatechange'); |
| 408 | if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(rsc); |
| 409 | xhr.dispatchEvent(rsc); |
| 410 | const load = new ProgressEvent('load'); |
| 411 | if (typeof xhr.onload === 'function') xhr.onload(load); |
| 412 | xhr.dispatchEvent(load); |
| 413 | xhr.dispatchEvent(new ProgressEvent('loadend')); |
| 414 | }, 0); |
| 415 | } |
| 416 | |
| 417 | // ======================== 六、主动抢购模式 ======================== |
| 418 | async function startProactive() { |
| 419 | if (!S.captured) { |
| 420 | log('⚠️ 请先手动点一次购买/订阅按钮'); |
| 421 | alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数'); |
| 422 | return; |
| 423 | } |
| 424 | |
| 425 | S.proactive = true; |
| 426 | log('🚀 主动抢购模式启动'); |
| 427 | |
| 428 | const { url, method, body, headers } = S.captured; |
| 429 | const result = await retry(url, { method, body, headers }); |
| 430 | |
| 431 | S.proactive = false; |
| 432 | |
| 433 | if (result.ok) { |
| 434 | S.cache = { text: result.text, data: result.data }; |
| 435 | log('🎉 主动模式成功! 触发购买流程…'); |
| 436 | |
| 437 | // 先关可能存在的弹窗 |
| 438 | const errDlg = findErrorDialog(); |
| 439 | if (errDlg) { |
| 440 | dismissDialog(errDlg); |
| 441 | await sleep(300); |
| 442 | } |
| 443 | |
| 444 | const btn = findBuyButton(); |
| 445 | if (btn) { |
| 446 | btn.click(); |
| 447 | log('🖱 已自动点击购买按钮'); |
| 448 | } else { |
| 449 | log('⚠️ 未找到按钮,请手动点击'); |
| 450 | alert('已获取到商品!请立即点击购买按钮!'); |
| 451 | } |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | function stopAll() { |
| 456 | stopRequested = true; |
| 457 | S.proactive = false; |
| 458 | S.status = 'idle'; |
| 459 | S.count = 0; |
| 460 | if (S.timerId) { clearTimeout(S.timerId); S.timerId = null; } |
| 461 | log('⏹ 已停止'); |
| 462 | refreshUI(); |
| 463 | } |
| 464 | |
| 465 | function findBuyButton() { |
| 466 | for (const el of document.querySelectorAll('button, a, [role="button"], div[class*="btn"], span[class*="btn"]')) { |
| 467 | const t = el.textContent.trim(); |
| 468 | if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20 && el.offsetParent !== null) { |
| 469 | return el; |
| 470 | } |
| 471 | } |
| 472 | return null; |
| 473 | } |
| 474 | |
| 475 | // ======================== 七、定时触发 ======================== |
| 476 | function scheduleAt(timeStr) { |
| 477 | if (S.timerId) { clearTimeout(S.timerId); S.timerId = null; } |
| 478 | const parts = timeStr.split(':').map(Number); |
| 479 | const now = new Date(); |
| 480 | const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parts[0], parts[1], parts[2] || 0); |
| 481 | if (target <= now) { log('⚠️ 目标时间已过'); return; } |
| 482 | const ms = target - now; |
| 483 | log(`⏰ 已设定: ${timeStr} (${Math.ceil(ms / 1000)}秒后)`); |
| 484 | S.timerId = setTimeout(() => { |
| 485 | S.timerId = null; |
| 486 | log('⏰ 时间到! 启动抢购!'); |
| 487 | if (S.captured) { |
| 488 | startProactive(); |
| 489 | } else { |
| 490 | const btn = findBuyButton(); |
| 491 | if (btn) { btn.click(); log('🖱 定时: 已点击购买按钮'); } |
| 492 | else { log('⚠️ 未找到按钮'); alert('定时到了!请手动点击购买!'); } |
| 493 | } |
| 494 | }, ms - 50); |
| 495 | refreshUI(); |
| 496 | } |
| 497 | |
| 498 | // ======================== 八、浮动控制面板 ======================== |
| 499 | function createPanel() { |
| 500 | const panel = document.createElement('div'); |
| 501 | panel.id = 'glm-rush'; |
| 502 | panel.innerHTML = ` |
| 503 | <style> |
| 504 | #glm-rush{position:fixed;top:10px;right:10px;width:340px;background:#1a1a2e;color:#e0e0e0; |
| 505 | border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.6);z-index:999999; |
| 506 | font:13px/1.5 Consolas,'Courier New',monospace;user-select:none} |
| 507 | #glm-rush *{box-sizing:border-box;margin:0;padding:0} |
| 508 | .glm-hd{background:linear-gradient(135deg,#0f3460,#16213e);padding:9px 14px; |
| 509 | border-radius:12px 12px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move} |
| 510 | .glm-hd b{font-size:14px;letter-spacing:.5px} |
| 511 | .glm-mn{background:none;border:none;color:#aaa;cursor:pointer;font-size:20px;line-height:1;padding:0 4px} |
| 512 | .glm-mn:hover{color:#fff} |
| 513 | .glm-bd{padding:12px 14px 14px} |
| 514 | .glm-st{padding:8px;border-radius:8px;text-align:center;font-weight:700;margin-bottom:10px;transition:background .3s} |
| 515 | .glm-st-idle{background:#2d3436} |
| 516 | .glm-st-retrying{background:#e17055;animation:glm-pulse 1s infinite} |
| 517 | .glm-st-success{background:#00b894} |
| 518 | .glm-st-failed{background:#d63031} |
| 519 | @keyframes glm-pulse{50%{opacity:.7}} |
| 520 | .glm-cap{font-size:11px;padding:5px 8px;background:#2d3436;border-radius:6px;margin-bottom:10px; |
| 521 | white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| 522 | .glm-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;flex-wrap:wrap} |
| 523 | .glm-row input[type=number],.glm-row input[type=time]{ |
| 524 | width:72px;padding:4px 6px;border:1px solid #444;border-radius:4px; |
| 525 | background:#2d3436;color:#fff;text-align:center;font-size:12px} |
| 526 | .glm-btns{display:flex;gap:8px;margin-bottom:10px} |
| 527 | .glm-btns button{flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer; |
| 528 | font-weight:700;font-size:12px;color:#fff;transition:opacity .2s} |
| 529 | .glm-btns button:hover{opacity:.85} |
| 530 | .glm-b-go{background:#0984e3} |
| 531 | .glm-b-stop{background:#d63031} |
| 532 | .glm-b-time{background:#6c5ce7;flex:0 0 auto !important;padding:4px 10px !important} |
| 533 | .glm-logs{max-height:170px;overflow-y:auto;background:#0d1117;border-radius:6px; |
| 534 | padding:6px 8px;font-size:11px;line-height:1.7} |
| 535 | .glm-logs div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} |
| 536 | .glm-logs::-webkit-scrollbar{width:4px} |
| 537 | .glm-logs::-webkit-scrollbar-thumb{background:#444;border-radius:2px} |
| 538 | </style> |
| 539 | <div class="glm-hd" id="glm-drag"> |
| 540 | <b>🎯 GLM 抢购助手 v3.2</b> |
| 541 | <button class="glm-mn" id="glm-min">−</button> |
| 542 | </div> |
| 543 | <div class="glm-bd" id="glm-bd"> |
| 544 | <div class="glm-st glm-st-idle" id="glm-st">⏳ 等待中</div> |
| 545 | <div class="glm-cap" id="glm-cap">📡 请求: 未捕获 — 请先点一次购买按钮</div> |
| 546 | <div class="glm-row"> |
| 547 | <span>间隔</span><input type="number" id="glm-delay" value="${CFG.delay}" min="50" max="5000" step="50"><span>ms</span> |
| 548 | <span style="margin-left:6px">上限</span><input type="number" id="glm-max" value="${CFG.maxRetry}" min="10" max="9999" step="10"><span>次</span> |
| 549 | </div> |
| 550 | <div class="glm-row"> |
| 551 | <span>定时</span><input type="time" id="glm-time" step="1"> |
| 552 | <button class="glm-b-time" id="glm-time-set">设定</button> |
| 553 | <span id="glm-timer-info" style="color:#6c5ce7;font-size:11px"></span> |
| 554 | </div> |
| 555 | <div class="glm-btns"> |
| 556 | <button class="glm-b-go" id="glm-go">▶ 主动抢购</button> |
| 557 | <button class="glm-b-stop" id="glm-stop" style="display:none">■ 停止</button> |
| 558 | </div> |
| 559 | <div class="glm-logs" id="glm-logs"></div> |
| 560 | </div>`; |
| 561 | document.body.appendChild(panel); |
| 562 | |
| 563 | const $ = id => document.getElementById(id); |
| 564 | $('glm-go').onclick = startProactive; |
| 565 | $('glm-stop').onclick = stopAll; |
| 566 | $('glm-delay').onchange = function () { CFG.delay = Math.max(50, +this.value || 100); }; |
| 567 | $('glm-max').onchange = function () { CFG.maxRetry = Math.max(10, +this.value || 300); }; |
| 568 | $('glm-time-set').onclick = function () { const v = $('glm-time').value; if (v) scheduleAt(v); }; |
| 569 | |
| 570 | $('glm-min').onclick = function () { |
| 571 | const bd = $('glm-bd'); |
| 572 | const hidden = bd.style.display === 'none'; |
| 573 | bd.style.display = hidden ? '' : 'none'; |
| 574 | this.textContent = hidden ? '−' : '+'; |
| 575 | }; |
| 576 | |
| 577 | // 拖拽 |
| 578 | let sx, sy, sl, st; |
| 579 | $('glm-drag').onmousedown = function (e) { |
| 580 | sx = e.clientX; sy = e.clientY; |
| 581 | const rect = panel.getBoundingClientRect(); |
| 582 | sl = rect.left; st = rect.top; |
| 583 | const onMove = function (e) { |
| 584 | panel.style.left = (sl + e.clientX - sx) + 'px'; |
| 585 | panel.style.top = (st + e.clientY - sy) + 'px'; |
| 586 | panel.style.right = 'auto'; |
| 587 | }; |
| 588 | const onUp = function () { |
| 589 | document.removeEventListener('mousemove', onMove); |
| 590 | document.removeEventListener('mouseup', onUp); |
| 591 | }; |
| 592 | document.addEventListener('mousemove', onMove); |
| 593 | document.addEventListener('mouseup', onUp); |
| 594 | }; |
| 595 | |
| 596 | log('🚀 v3.2 已加载 (preview+check 双重校验)'); |
| 597 | |
| 598 | // 启动错误弹窗监控 |
| 599 | setupDialogWatcher(); |
| 600 | } |
| 601 | |
| 602 | function refreshUI() { |
| 603 | const stEl = document.getElementById('glm-st'); |
| 604 | if (!stEl) return; |
| 605 | stEl.className = 'glm-st glm-st-' + S.status; |
| 606 | stEl.textContent = S.status === 'idle' ? '⏳ 等待中' |
| 607 | : S.status === 'retrying' ? `🔄 重试中… ${S.count}/${CFG.maxRetry}` |
| 608 | : S.status === 'success' ? `✅ 成功! bizId=${S.bizId}` |
| 609 | : `❌ 失败 (${S.count}次)`; |
| 610 | |
| 611 | const capEl = document.getElementById('glm-cap'); |
| 612 | if (capEl) { |
| 613 | capEl.textContent = S.captured |
| 614 | ? `📡 已捕获: ${S.captured.method} …${S.captured.url.split('?')[0].slice(-30)}` |
| 615 | : '📡 请求: 未捕获 — 请先点一次购买按钮'; |
| 616 | } |
| 617 | |
| 618 | const goBtn = document.getElementById('glm-go'); |
| 619 | const stopBtn = document.getElementById('glm-stop'); |
| 620 | if (goBtn && stopBtn) { |
| 621 | goBtn.style.display = S.status === 'retrying' ? 'none' : ''; |
| 622 | stopBtn.style.display = S.status === 'retrying' ? '' : 'none'; |
| 623 | } |
| 624 | } |
| 625 | |
| 626 | function refreshLog() { |
| 627 | const el = document.getElementById('glm-logs'); |
| 628 | if (!el) return; |
| 629 | const last = S.logs[S.logs.length - 1]; |
| 630 | if (last) { |
| 631 | const div = document.createElement('div'); |
| 632 | div.textContent = last; |
| 633 | el.appendChild(div); |
| 634 | while (el.children.length > 80) el.removeChild(el.firstChild); |
| 635 | el.scrollTop = el.scrollHeight; |
| 636 | } |
| 637 | } |
| 638 | |
| 639 | // ======================== 启动 ======================== |
| 640 | console.log('[GLM抢购] 🚀 v3.2 全自动版 (preview+check 双重校验) 已注入'); |
| 641 | |
| 642 | if (document.readyState === 'loading') { |
| 643 | document.addEventListener('DOMContentLoaded', createPanel); |
| 644 | } else { |
| 645 | createPanel(); |
| 646 | } |
| 647 | })(); |
| 648 |