feat: apply all A/B/C/D modifications to v4.6
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
// ==/UserScript==
|
||||
|
||||
(function () {
|
||||
const VERSION = '4.6';
|
||||
'use strict';
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -68,11 +69,19 @@
|
||||
|
||||
// 恢复上次捕获的请求
|
||||
try {
|
||||
const saved = sessionStorage.getItem('glm_rush_captured');
|
||||
if (saved) state.captured = JSON.parse(saved);
|
||||
const raw = sessionStorage.getItem('glm_rush_captured');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.__v !== 1) {
|
||||
sessionStorage.removeItem('glm_rush_captured');
|
||||
} else {
|
||||
state.captured = parsed;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
let stopRequested = false;
|
||||
let _activeControllers = [];
|
||||
let recovering = false;
|
||||
let recoveryAttempts = 0;
|
||||
let _shadowRef = null;
|
||||
@@ -85,9 +94,9 @@
|
||||
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);
|
||||
function getDelay(round) {
|
||||
if (round <= CFG.burstCount) return 0;
|
||||
if (round <= 50) return jitteredDelay(CFG.fastDelay);
|
||||
return jitteredDelay(CFG.slowDelay);
|
||||
}
|
||||
|
||||
@@ -116,6 +125,8 @@
|
||||
|
||||
function patchSoldOut(obj, visited = new WeakSet()) {
|
||||
if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
|
||||
if (obj.__ob__ !== undefined) return;
|
||||
if (obj.__v_isVNode || obj.__v_isRef) return;
|
||||
visited.add(obj);
|
||||
if (obj.isSoldOut === true) obj.isSoldOut = false;
|
||||
if (obj.soldOut === true) obj.soldOut = false;
|
||||
@@ -128,6 +139,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Vue 2/3 兼容获取根实例
|
||||
// ═══════════════════════════════════════════
|
||||
function getVueRoot(selector = '#app') {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return null;
|
||||
if (el.__vue__) return el.__vue__;
|
||||
if (el.__vue_app__) return el.__vue_app__._instance?.proxy;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 全局 patch: 页面加载时也需要解除售罄状态,否则按钮不可点击
|
||||
JSON.parse = function (text, reviver) {
|
||||
const result = _parse(text, reviver);
|
||||
@@ -163,8 +185,17 @@
|
||||
}
|
||||
|
||||
const text = await resp.text();
|
||||
// 全局 patch: 对所有 JSON 响应移除售罄/繁忙标记,只在命中时替换
|
||||
const needsPatch = /"isSoldOut":true|"soldOut":true|"isServerBusy":true|"stock":0/.test(text);
|
||||
const patchedText = needsPatch
|
||||
? text
|
||||
.replace(/"isSoldOut":true/g, '"isSoldOut":false')
|
||||
.replace(/"soldOut":true/g, '"soldOut":false')
|
||||
.replace(/"isServerBusy":true/g, '"isServerBusy":false')
|
||||
.replace(/"stock":0/g, '"stock":999')
|
||||
: text;
|
||||
let data;
|
||||
try { data = _parse(text); } catch { data = null; }
|
||||
try { data = _parse(patchedText); } catch { data = null; }
|
||||
|
||||
if (data && data.code === 200 && data.data && data.data.bizId) {
|
||||
const bizId = data.data.bizId;
|
||||
@@ -180,9 +211,17 @@
|
||||
if (checkData && checkData.data === 'EXPIRE') {
|
||||
return { ok: false, reason: 'EXPIRE', attempt: attemptNum };
|
||||
}
|
||||
// 新增:金额为0 = 空单,继续重试
|
||||
const payData = checkData?.data;
|
||||
if (payData) {
|
||||
const amount = payData.amount ?? payData.totalAmount ?? payData.payAmount ?? 0;
|
||||
if (amount === 0) {
|
||||
return { ok: false, reason: '空单(金额为0)', attempt: attemptNum };
|
||||
}
|
||||
}
|
||||
|
||||
// 通过!
|
||||
return { ok: true, text, data, bizId, status: resp.status, attempt: attemptNum };
|
||||
return { ok: true, text: patchedText, data, bizId, status: resp.status, attempt: attemptNum };
|
||||
} catch (e) {
|
||||
return { ok: false, reason: `check异常: ${e.message}`, attempt: attemptNum };
|
||||
}
|
||||
@@ -223,6 +262,7 @@
|
||||
const curConcurrency = isTurbo ? CFG.turboConcurrency : CFG.concurrency;
|
||||
const batchSize = Math.min(curConcurrency, CFG.maxRetry - totalAttempt);
|
||||
const controllers = [];
|
||||
_activeControllers = controllers;
|
||||
const promises = [];
|
||||
|
||||
for (let j = 0; j < batchSize; j++) {
|
||||
@@ -253,7 +293,10 @@
|
||||
});
|
||||
|
||||
// 收集失败原因 (用于日志)
|
||||
const results = await Promise.all(promises.map(p => p.catch(() => ({ ok: false, reason: '已取消' }))));
|
||||
const settled = await Promise.allSettled(promises);
|
||||
const failedResults = settled
|
||||
.filter(r => r.status === 'fulfilled' && !r.value.ok && r.value.reason !== '已取消')
|
||||
.map(r => r.value);
|
||||
|
||||
if (winner) {
|
||||
setState({
|
||||
@@ -269,12 +312,12 @@
|
||||
}
|
||||
|
||||
// 统计错误
|
||||
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;
|
||||
consecutiveErrors = (networkErrors > 0 && networkErrors === failedResults.length)
|
||||
? consecutiveErrors + 1 : 0;
|
||||
|
||||
// 连续网络错误 → 暂停
|
||||
if (consecutiveErrors >= 3) {
|
||||
@@ -293,23 +336,23 @@
|
||||
// 限流检测 (独立计数)
|
||||
if (reasons.some(r => r.includes('429') || r.includes('限流'))) {
|
||||
throttleCount++;
|
||||
const backoff = Math.min(2000 * (2 ** Math.min(throttleCount, 4)), 16000);
|
||||
const backoff = Math.min(1000 * (2 ** Math.min(throttleCount, 3)), 8000);
|
||||
log(`限流, 退避${backoff}ms...`, 'warn');
|
||||
await sleep(backoff);
|
||||
} else {
|
||||
throttleCount = 0;
|
||||
}
|
||||
|
||||
// EXPIRE → 立即重试不等待
|
||||
if (reasons.every(r => r === 'EXPIRE')) continue;
|
||||
// 只有 EXPIRE 和系统繁忙(555)无延迟立即重试
|
||||
if (reasons.every(r => r === 'EXPIRE' || r === '系统繁忙')) continue;
|
||||
|
||||
// 前20秒全速冲,之后才考虑降速
|
||||
const elapsedSec = (performance.now() - state.stats.startTime) / 1000;
|
||||
|
||||
if (elapsedSec > 20) {
|
||||
// 超过20秒 — 检测是否该降速
|
||||
const soldOutCount = reasons.filter(r => r === '售罄').length;
|
||||
if (soldOutCount === batchSize) {
|
||||
const soldOutRatio = reasons.filter(r => r === '售罄').length / batchSize;
|
||||
if (soldOutRatio >= 0.6) {
|
||||
consecutiveSoldOut++;
|
||||
} else {
|
||||
consecutiveSoldOut = 0;
|
||||
@@ -359,6 +402,7 @@
|
||||
method: init?.method || 'POST',
|
||||
body: init?.body,
|
||||
headers: extractHeaders(init?.headers),
|
||||
__v: 1,
|
||||
};
|
||||
setState({ captured });
|
||||
try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {}
|
||||
@@ -430,7 +474,7 @@
|
||||
|
||||
if (typeof url === 'string' && url.includes(CFG.PREVIEW)) {
|
||||
const self = this;
|
||||
const captured = { url, method: this._m, body, headers: this._h || {} };
|
||||
const captured = { url, method: this._m, body, headers: this._h || {}, __v: 1 };
|
||||
setState({ captured });
|
||||
try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {}
|
||||
|
||||
@@ -546,7 +590,7 @@
|
||||
|
||||
// 策略2: 缓存响应 + 重新点购买按钮
|
||||
setState({ cache: state.lastSuccess });
|
||||
const btn = findBuyButton();
|
||||
const btn = await waitForBuyButton();
|
||||
if (btn) {
|
||||
btn.click();
|
||||
log('已重新点击购买按钮 (策略2)');
|
||||
@@ -622,16 +666,17 @@
|
||||
// ═══════════════════════════════════════════
|
||||
// 主动抢购 & 定时
|
||||
// ═══════════════════════════════════════════
|
||||
function findBuyButton() {
|
||||
// 优先找 buy-btn 类的按钮(特惠订阅/订阅升级)
|
||||
for (const el of document.querySelectorAll('button.buy-btn')) {
|
||||
const t = el.textContent.trim();
|
||||
async function waitForBuyButton(timeout = 8000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
for (const el of document.querySelectorAll('button.buy-btn, button[class*="subscribe"], button[class*="buy"]')) {
|
||||
if (el.offsetParent !== null) return el;
|
||||
}
|
||||
// 降级:通用匹配,排除导航按钮
|
||||
for (const el of document.querySelectorAll('button, [role="button"]')) {
|
||||
const t = el.textContent.trim();
|
||||
if (/购买|抢购|下单|特惠/.test(t) && t.length < 15 && el.offsetParent !== null) return el;
|
||||
for (const el of document.querySelectorAll('button')) {
|
||||
const t = el.textContent?.trim();
|
||||
if (/^特惠订购$|^立即订购$|^立即购买$/.test(t) && el.offsetParent !== null) return el;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -659,8 +704,12 @@
|
||||
// 自动通知
|
||||
try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {}
|
||||
const errDlg = findErrorDialog();
|
||||
if (errDlg) { dismissDialog(errDlg); await sleep(300); }
|
||||
const btn = findBuyButton();
|
||||
if (errDlg) {
|
||||
dismissDialog(errDlg);
|
||||
if (errDlg.offsetParent !== null) recoveryAttempts = 0;
|
||||
await sleep(300);
|
||||
}
|
||||
const btn = await waitForBuyButton();
|
||||
if (btn) { btn.click(); log('已自动点击购买按钮'); }
|
||||
else { alert('已获取到商品! 请立即点击购买按钮!'); }
|
||||
|
||||
@@ -759,15 +808,15 @@
|
||||
const ms = target.getTime() - getServerNow();
|
||||
log(`定时: ${timeStr} (${Math.ceil(ms / 1000)}秒后, 北京时间)`);
|
||||
|
||||
// 提前3秒自动预热
|
||||
if (ms > 4000) {
|
||||
setTimeout(() => {
|
||||
log('定时前3秒, 自动预热...');
|
||||
preheat();
|
||||
}, Math.max(0, ms - 3000));
|
||||
// 提前5分钟自动预热
|
||||
if (ms > 310_000) {
|
||||
setTimeout(preheat, Math.max(0, ms - 300_000));
|
||||
}
|
||||
|
||||
// 精确等待: 用 setInterval 10ms 检查, 到时间立即启动
|
||||
// 精确等待: 用 setInterval 10ms 检查, 剩余1秒切换 rAF
|
||||
const preAdvanceMs = (CFG.preAdvanceSec || 0) * 1000;
|
||||
let rafCancelled = false;
|
||||
window._glmRafCancelled = () => { rafCancelled = true; };
|
||||
const tid = setInterval(() => {
|
||||
const remaining = target.getTime() - getServerNow();
|
||||
// 更新面板倒计时
|
||||
@@ -776,13 +825,26 @@
|
||||
const timerEl = _shadowRef?.getElementById('timer-info');
|
||||
if (timerEl) timerEl.textContent = `-${sec}s`;
|
||||
}
|
||||
if (remaining <= 0) {
|
||||
if (remaining <= preAdvanceMs) {
|
||||
clearInterval(tid);
|
||||
setState({ timerId: null });
|
||||
const timerEl = _shadowRef?.getElementById('timer-info');
|
||||
if (timerEl) timerEl.textContent = '';
|
||||
log('时间到! 自动启动抢购!');
|
||||
log(`提前 ${CFG.preAdvanceSec}s 触发!`);
|
||||
startProactive();
|
||||
} else if (remaining > 0 && remaining <= 1000) {
|
||||
clearInterval(tid);
|
||||
function rafWait() {
|
||||
if (rafCancelled || stopRequested) return;
|
||||
if (target.getTime() - getServerNow() <= preAdvanceMs) {
|
||||
const timerEl = _shadowRef?.getElementById('timer-info');
|
||||
if (timerEl) timerEl.textContent = '';
|
||||
startProactive();
|
||||
} else {
|
||||
requestAnimationFrame(rafWait);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(rafWait);
|
||||
}
|
||||
}, 10);
|
||||
|
||||
@@ -791,20 +853,20 @@
|
||||
|
||||
// 预热
|
||||
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 请求不产生副作用)
|
||||
log('开始预热连接...');
|
||||
let ok = 0;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const r = await _fetch(location.origin + '/favicon.ico', { method: 'HEAD' }).catch(() => null);
|
||||
if (r) ok++;
|
||||
await _fetch(location.origin + CFG.PREVIEW, {
|
||||
method: 'HEAD',
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
}).catch(() => {});
|
||||
log('预热完成 (4次连接已建立)');
|
||||
} catch { log('预热部分失败,不影响使用'); }
|
||||
await sleep(300);
|
||||
}
|
||||
log(`预热完成:${ok}/5 连接建立成功`);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
@@ -925,7 +987,7 @@
|
||||
.keys{font-size:10px;color:#636e72;text-align:center;margin-top:6px}
|
||||
</style>
|
||||
<div class="panel">
|
||||
<div class="hd" id="drag"><b>GLM v4.6</b><button class="mn" id="min">-</button></div>
|
||||
<div class="hd" id="drag"><b>GLM v${VERSION}</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>
|
||||
@@ -944,6 +1006,7 @@
|
||||
<button class="b-time" id="b-time">设定</button>
|
||||
<span id="timer-info" style="color:#6c5ce7;font-size:11px"></span>
|
||||
</div>
|
||||
<div id="timer-tip" style="color:#fdcb6e;font-size:11px;margin-bottom:8px">⚠️ 每次确认放票时间</div>
|
||||
<div class="btns">
|
||||
<button class="b-go" id="b-go">▶ 主动抢购</button>
|
||||
<button class="b-stop" id="b-stop" style="display:none">■ 停止</button>
|
||||
@@ -1072,7 +1135,7 @@
|
||||
// ═══════════════════════════════════════════
|
||||
// 启动
|
||||
// ═══════════════════════════════════════════
|
||||
console.log('[GLM] v4.0 已注入');
|
||||
console.log(`[GLM] v${VERSION} 已注入`);
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createPanel);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user