v4.6: 22项修改 (A1-A5/B1-B7/C1-C6/D1-D4)

This commit is contained in:
ephron
2026-04-18 22:03:18 +08:00
parent 9d5e454c40
commit 04dbfd3b7b

View File

@@ -13,6 +13,34 @@
(function () { (function () {
'use strict'; 'use strict';
const VERSION = '4.6';
// ═══════════════════════════════════════════
// history 路由劫持 (防止 SPA 内跳转丢失状态)
// ═══════════════════════════════════════════
const _pushState = history.pushState.bind(history);
history.pushState = function(...args) {
if (state.status === 'retrying' || state.timerId) {
if (!confirm('抢购定时/进行中,确定要离开吗?')) return;
stopAll();
}
return _pushState(...args);
};
history.replaceState = new Proxy(history.replaceState, {
apply(target, thisArg, args) {
if (state.status === 'retrying' || state.timerId) {
if (!confirm('抢购定时/进行中,确定要离开吗?')) return;
stopAll();
}
return Reflect.apply(target, thisArg, args);
}
});
// ═══════════════════════════════════════════
// 版本号
// ═══════════════════════════════════════════
const CAPTURE_VER = 1;
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 配置 (localStorage 持久化) // 配置 (localStorage 持久化)
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@@ -28,6 +56,7 @@
recoveryMax: 3, // 弹窗恢复最大次数 recoveryMax: 3, // 弹窗恢复最大次数
logMax: 100, // 日志条数上限 logMax: 100, // 日志条数上限
rushTime: '10:00:00', // 每天抢购时间 (北京时间) rushTime: '10:00:00', // 每天抢购时间 (北京时间)
preAdvanceSec: 2, // 提前几秒触发默认2秒
PREVIEW: '/api/biz/pay/preview', PREVIEW: '/api/biz/pay/preview',
CHECK: '/api/biz/pay/check', CHECK: '/api/biz/pay/check',
}; };
@@ -68,14 +97,22 @@
// 恢复上次捕获的请求 // 恢复上次捕获的请求
try { try {
const saved = sessionStorage.getItem('glm_rush_captured'); const raw = sessionStorage.getItem('glm_rush_captured');
if (saved) state.captured = JSON.parse(saved); if (raw) {
const parsed = JSON.parse(raw);
if (parsed.__v !== CAPTURE_VER) {
sessionStorage.removeItem('glm_rush_captured');
} else {
state.captured = parsed;
}
}
} catch {} } catch {}
let stopRequested = false; let stopRequested = false;
let recovering = false; let recovering = false;
let recoveryAttempts = 0; let recoveryAttempts = 0;
let _shadowRef = null; let _shadowRef = null;
let _activeControllers = [];
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 工具 // 工具
@@ -85,9 +122,9 @@
const rand = (min, max) => min + Math.random() * (max - min); const rand = (min, max) => min + Math.random() * (max - min);
const jitteredDelay = base => Math.round(base * (1 + (Math.random() * 2 - 1) * CFG.jitter)); const jitteredDelay = base => Math.round(base * (1 + (Math.random() * 2 - 1) * CFG.jitter));
function getDelay(attempt) { function getDelay(round) {
if (attempt <= CFG.burstCount) return 0; if (round <= CFG.burstCount) return 0; // 前 N 轮零延迟
if (attempt <= 50) return jitteredDelay(CFG.fastDelay); if (round <= 50) return jitteredDelay(CFG.fastDelay);
return jitteredDelay(CFG.slowDelay); return jitteredDelay(CFG.slowDelay);
} }
@@ -109,6 +146,17 @@
return o; return o;
} }
// ═══════════════════════════════════════════
// Vue 2/3 兼容获取根实例
// ═══════════════════════════════════════════
function getVueRoot(selector = '#app') {
const el = document.querySelector(selector);
if (!el) return null;
if (el.__vue__) return el.__vue__; // Vue 2
if (el.__vue_app__) return el.__vue_app__._instance?.proxy; // Vue 3
return null;
}
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// JSON.parse 定向拦截 (仅修改特定数据结构) // JSON.parse 定向拦截 (仅修改特定数据结构)
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@@ -116,6 +164,8 @@
function patchSoldOut(obj, visited = new WeakSet()) { function patchSoldOut(obj, visited = new WeakSet()) {
if (!obj || typeof obj !== 'object' || visited.has(obj)) return; if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
if (obj.__ob__ !== undefined) return; // 跳过 Vue 响应式对象
if (obj.__v_isVNode || obj.__v_isRef) return; // 跳过 Vue 3 内部对象
visited.add(obj); visited.add(obj);
if (obj.isSoldOut === true) obj.isSoldOut = false; if (obj.isSoldOut === true) obj.isSoldOut = false;
if (obj.soldOut === true) obj.soldOut = false; if (obj.soldOut === true) obj.soldOut = false;
@@ -180,6 +230,14 @@
if (checkData && checkData.data === 'EXPIRE') { if (checkData && checkData.data === 'EXPIRE') {
return { ok: false, reason: 'EXPIRE', attempt: attemptNum }; 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, data, bizId, status: resp.status, attempt: attemptNum };
@@ -206,6 +264,7 @@
} }
stopRequested = false; stopRequested = false;
let roundNum = 0;
const { signal, ...opts } = rawOpts || {}; const { signal, ...opts } = rawOpts || {};
_retryLock = (async () => { _retryLock = (async () => {
@@ -234,7 +293,10 @@
); );
} }
_activeControllers = controllers;
setState({ count: totalAttempt }); setState({ count: totalAttempt });
roundNum++;
// 任一成功即取消其余 // 任一成功即取消其余
const winner = await new Promise(resolve => { const winner = await new Promise(resolve => {
@@ -253,7 +315,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) { if (winner) {
setState({ setState({
@@ -269,7 +334,6 @@
} }
// 统计错误 // 统计错误
const failedResults = results.filter(r => !r.ok);
const reasons = failedResults.map(r => r.reason || '未知'); const reasons = failedResults.map(r => r.reason || '未知');
setState({ stats: { ...state.stats, errors: state.stats.errors + failedResults.length } }); setState({ stats: { ...state.stats, errors: state.stats.errors + failedResults.length } });
@@ -290,10 +354,13 @@
return { ok: false }; return { ok: false };
} }
// 只有 429限流才退避555 和 EXPIRE 无延迟立即重试
if (reasons.every(r => r === 'EXPIRE' || r === '系统繁忙' || r === '555')) continue;
// 限流检测 (独立计数) // 限流检测 (独立计数)
if (reasons.some(r => r.includes('429') || r.includes('限流'))) { if (reasons.some(r => r.includes('429') || r.includes('限流'))) {
throttleCount++; 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'); log(`限流, 退避${backoff}ms...`, 'warn');
await sleep(backoff); await sleep(backoff);
} else { } else {
@@ -308,8 +375,8 @@
if (elapsedSec > 20) { if (elapsedSec > 20) {
// 超过20秒 — 检测是否该降速 // 超过20秒 — 检测是否该降速
const soldOutCount = reasons.filter(r => r === '售罄').length; const soldOutRatio = reasons.filter(r => r === '售罄').length / batchSize;
if (soldOutCount === batchSize) { if (soldOutRatio >= 0.6) {
consecutiveSoldOut++; consecutiveSoldOut++;
} else { } else {
consecutiveSoldOut = 0; consecutiveSoldOut = 0;
@@ -329,15 +396,24 @@
} }
// 自适应延迟 // 自适应延迟
const d = getDelay(totalAttempt / CFG.concurrency); const d = getDelay(roundNum);
if (d > 0) await sleep(d); if (d > 0) await sleep(d);
} }
if (!stopRequested) { if (!stopRequested) {
setState({ status: 'failed' }); const elapsed = (performance.now() - state.stats.startTime) / 1000;
log(`达到上限 ${CFG.maxRetry}`); if (elapsed < 300) {
log('进入捡漏模式(10:00-10:05),降速等待退票...');
CFG._savedConcurrency = CFG.concurrency;
CFG.concurrency = 2;
CFG.slowDelay = 3000;
// 继续循环不走 else
} else {
setState({ status: 'failed' });
CFG.concurrency = CFG._savedConcurrency ?? CFG.concurrency;
}
} else { } else {
setState({ status: 'idle' }); CFG.concurrency = CFG._savedConcurrency ?? CFG.concurrency;
} }
return { ok: false }; return { ok: false };
})(); })();
@@ -361,7 +437,7 @@
headers: extractHeaders(init?.headers), headers: extractHeaders(init?.headers),
}; };
setState({ captured }); setState({ captured });
try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} try { sessionStorage.setItem('glm_rush_captured', JSON.stringify({ ...captured, __v: CAPTURE_VER })); } catch {}
// 已经成功过 → 直接返回缓存 // 已经成功过 → 直接返回缓存
if (state.status === 'success' && state.lastSuccess) { if (state.status === 'success' && state.lastSuccess) {
@@ -405,7 +481,30 @@
}); });
} }
return _fetch.apply(this, [input, init]); // 新增:对所有其他 JSON 接口的响应做文本替换
const resp = await _fetch.apply(this, [input, init]);
const ct = resp.headers.get('content-type') || '';
if (ct.includes('application/json')) {
const text = await resp.text();
if (/"isSoldOut":true|"soldOut":true|"isServerBusy":true/.test(text)) {
const patched = 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');
return new Response(patched, {
status: resp.status,
statusText: resp.statusText,
headers: resp.headers,
});
}
return new Response(text, {
status: resp.status,
statusText: resp.statusText,
headers: resp.headers,
});
}
return resp;
}; };
// 伪装 // 伪装
window.fetch.toString = () => 'function fetch() { [native code] }'; window.fetch.toString = () => 'function fetch() { [native code] }';
@@ -432,7 +531,7 @@
const self = this; const self = this;
const captured = { url, method: this._m, body, headers: this._h || {} }; const captured = { url, method: this._m, body, headers: this._h || {} };
setState({ captured }); setState({ captured });
try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} try { sessionStorage.setItem('glm_rush_captured', JSON.stringify({ ...captured, __v: CAPTURE_VER })); } catch {}
// 已经成功过 → 直接返回缓存 // 已经成功过 → 直接返回缓存
if (state.status === 'success' && state.lastSuccess) { if (state.status === 'success' && state.lastSuccess) {
@@ -472,11 +571,16 @@
return _xhrSend.call(this, body); return _xhrSend.call(this, body);
}; };
function setProp(obj, key, value) {
try { obj[key] = value; } catch {
try { Object.defineProperty(obj, key, { value, configurable: true, writable: true }); } catch {}
}
}
function fakeXHR(xhr, text) { function fakeXHR(xhr, text) {
setTimeout(() => { setTimeout(() => {
const dp = (k, v) => Object.defineProperty(xhr, k, { value: v, configurable: true }); setProp(xhr, 'readyState', 4); setProp(xhr, 'status', 200); setProp(xhr, 'statusText', 'OK');
dp('readyState', 4); dp('status', 200); dp('statusText', 'OK'); setProp(xhr, 'responseText', text); setProp(xhr, 'response', text);
dp('responseText', text); dp('response', text);
const ev = new Event('readystatechange'); const ev = new Event('readystatechange');
if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(ev); if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(ev);
xhr.dispatchEvent(ev); xhr.dispatchEvent(ev);
@@ -518,8 +622,13 @@
const t = (btn.textContent || '').trim(); const t = (btn.textContent || '').trim();
if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) { btn.click(); return true; } if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) { btn.click(); return true; }
} }
// 直接隐藏这个 dialog // 先发送 Escape 事件让 Vue 自己处理
dialog.style.display = 'none'; const esc = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
dialog.dispatchEvent(esc);
document.dispatchEvent(esc);
setTimeout(() => {
if (dialog.offsetParent !== null) dialog.style.display = 'none';
}, 300);
return true; return true;
} }
@@ -546,7 +655,7 @@
// 策略2: 缓存响应 + 重新点购买按钮 // 策略2: 缓存响应 + 重新点购买按钮
setState({ cache: state.lastSuccess }); setState({ cache: state.lastSuccess });
const btn = findBuyButton(); const btn = await waitForBuyButton();
if (btn) { if (btn) {
btn.click(); btn.click();
log('已重新点击购买按钮 (策略2)'); log('已重新点击购买按钮 (策略2)');
@@ -590,6 +699,8 @@
} }
} else { } else {
log('支付弹窗已出现!'); log('支付弹窗已出现!');
recoveryAttempts = 0;
return;
} }
} finally { recovering = false; } } finally { recovering = false; }
} }
@@ -622,16 +733,19 @@
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 主动抢购 & 定时 // 主动抢购 & 定时
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
function findBuyButton() { async function waitForBuyButton(timeout = 8000) {
// 优先找 buy-btn 类的按钮(特惠订阅/订阅升级) const start = Date.now();
for (const el of document.querySelectorAll('button.buy-btn')) { while (Date.now() - start < timeout) {
const t = el.textContent.trim(); // 策略1class 精确匹配
if (el.offsetParent !== null) return el; 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"]')) { // 策略2文本精确匹配最可靠
const t = el.textContent.trim(); for (const el of document.querySelectorAll('button')) {
if (/购买|抢购|下单|特惠/.test(t) && t.length < 15 && el.offsetParent !== null) return el; const t = el.textContent?.trim();
if (/^特惠订购$|^立即订购$|^立即购买$/.test(t) && el.offsetParent !== null) return el;
}
await sleep(200);
} }
return null; return null;
} }
@@ -660,7 +774,7 @@
try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {}
const errDlg = findErrorDialog(); const errDlg = findErrorDialog();
if (errDlg) { dismissDialog(errDlg); await sleep(300); } if (errDlg) { dismissDialog(errDlg); await sleep(300); }
const btn = findBuyButton(); const btn = await waitForBuyButton();
if (btn) { btn.click(); log('已自动点击购买按钮'); } if (btn) { btn.click(); log('已自动点击购买按钮'); }
else { alert('已获取到商品! 请立即点击购买按钮!'); } else { alert('已获取到商品! 请立即点击购买按钮!'); }
@@ -672,6 +786,8 @@
function stopAll() { function stopAll() {
stopRequested = true; stopRequested = true;
_activeControllers.forEach(ac => { try { ac.abort(); } catch {} });
_activeControllers = [];
setState({ proactive: false, status: 'idle', count: 0 }); setState({ proactive: false, status: 'idle', count: 0 });
if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); } if (state.timerId) { clearInterval(state.timerId); setState({ timerId: null }); }
log('已停止'); log('已停止');
@@ -682,36 +798,20 @@
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
let serverTimeOffset = 0; // 本地时间与服务器时间的差值(ms) let serverTimeOffset = 0; // 本地时间与服务器时间的差值(ms)
async function syncServerTime() { async function syncServerTime(samples = 3) {
// 用服务器响应头的 Date 字段同步时间 const offsets = [];
try { for (let i = 0; i < samples; i++) {
const t0 = Date.now(); const t0 = Date.now();
const resp = await _fetch(location.origin + '/api/biz/pay/check?bizId=sync', { credentials: 'include' }).catch(() => null); const r = await _fetch(location.origin + '/favicon.ico', { method: 'HEAD' }).catch(() => null);
if (!r) continue;
const t1 = Date.now(); const t1 = Date.now();
const rtt = t1 - t0; const d = r.headers.get('date');
if (d) offsets.push(new Date(d).getTime() - (t0 + t1) / 2);
if (resp && resp.headers.get('date')) { if (i < samples - 1) await sleep(500);
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;
} }
offsets.sort((a, b) => a - b);
serverTimeOffset = offsets[Math.floor(offsets.length / 2)] ?? 0;
log(`时间同步完成,服务器偏差 ${serverTimeOffset > 0 ? '+' : ''}${serverTimeOffset}ms`);
} }
function getServerNow() { function getServerNow() {
@@ -759,52 +859,57 @@
const ms = target.getTime() - getServerNow(); const ms = target.getTime() - getServerNow();
log(`定时: ${timeStr} (${Math.ceil(ms / 1000)}秒后, 北京时间)`); log(`定时: ${timeStr} (${Math.ceil(ms / 1000)}秒后, 北京时间)`);
// 提前3秒自动预热 // 提前5分钟自动预热
if (ms > 4000) { if (ms > 310_000) {
setTimeout(() => { setTimeout(() => {
log('定时前3秒, 自动预热...'); log('定时前5分钟, 自动预热...');
preheat(); preheat();
}, Math.max(0, ms - 3000)); }, Math.max(0, ms - 300_000));
} }
// 精确等待: 用 setInterval 10ms 检查, 到时间立即启动 // 精确等待: 用 setInterval 10ms 检查, 到时间立即启动
const tid = setInterval(() => { const preAdvanceMs = (CFG.preAdvanceSec || 0) * 1000;
const checkInterval = setInterval(() => {
const remaining = target.getTime() - getServerNow(); const remaining = target.getTime() - getServerNow();
// 更新面板倒计时 if (remaining <= preAdvanceMs) {
if (remaining > 0 && remaining < 60000) { clearInterval(checkInterval);
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(); startProactive();
} }
// 额外在剩余1秒内用 requestAnimationFrame 做最后精确对齐
if (remaining > 0 && remaining <= 1000) {
clearInterval(checkInterval);
function rafWait() {
if (target.getTime() - getServerNow() <= preAdvanceMs) {
startProactive();
} else {
requestAnimationFrame(rafWait);
}
}
requestAnimationFrame(rafWait);
}
}, 10); }, 10);
setState({ timerId: tid }); setState({ timerId: checkInterval });
} }
// 预热 // 预热
async function preheat() { async function preheat() {
try { log('开始预热连接...');
log('TCP预热中...'); let ok = 0;
// 连发3次预热请求确保连接池暖好 for (let i = 0; i < 5; i++) {
for (let i = 0; i < 3; i++) { const r = await _fetch(location.origin + '/favicon.ico', { method: 'HEAD' })
await _fetch(location.origin + '/api/biz/pay/check?bizId=preheat_' + i, { credentials: 'include' }).catch(() => {}); .catch(() => null);
await sleep(200); if (r) ok++;
} // 预热 preview TCP 连接(空 POST 不触发业务逻辑)
// 也预热 preview 的 DNS + TCP (用 HEAD 请求不产生副作用)
await _fetch(location.origin + CFG.PREVIEW, { await _fetch(location.origin + CFG.PREVIEW, {
method: 'HEAD', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: '{}',
}).catch(() => {}); }).catch(() => {});
log('预热完成 (4次连接已建立)'); await sleep(300);
} catch { log('预热部分失败,不影响使用'); } }
log(`预热完成:${ok}/5 连接建立成功`);
} }
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
@@ -831,8 +936,7 @@
const tid = setInterval(() => { const tid = setInterval(() => {
attempts++; attempts++;
if (attempts > 30) { clearInterval(tid); return; } // 15秒后放弃 if (attempts > 30) { clearInterval(tid); return; } // 15秒后放弃
const app = document.querySelector('#app'); const vue = getVueRoot();
const vue = app && app.__vue__;
if (!vue) return; if (!vue) return;
let patched = 0; let patched = 0;
const walk = (vm, depth) => { const walk = (vm, depth) => {
@@ -853,8 +957,7 @@
/** 兜底: 直接操作 Vue 组件弹出支付窗口 */ /** 兜底: 直接操作 Vue 组件弹出支付窗口 */
function forcePayDialog(responseData) { function forcePayDialog(responseData) {
const app = document.querySelector('#app'); const vue = getVueRoot();
const vue = app && app.__vue__;
if (!vue) return; if (!vue) return;
let payComp = null; let payComp = null;
@@ -925,7 +1028,7 @@
.keys{font-size:10px;color:#636e72;text-align:center;margin-top:6px} .keys{font-size:10px;color:#636e72;text-align:center;margin-top:6px}
</style> </style>
<div class="panel"> <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="bd" id="bd">
<div class="st st-idle" id="st">等待中</div> <div class="st st-idle" id="st">等待中</div>
<div class="cap" id="cap">${state.captured ? '已恢复上次捕获的请求' : '请先点一次购买按钮'}</div> <div class="cap" id="cap">${state.captured ? '已恢复上次捕获的请求' : '请先点一次购买按钮'}</div>
@@ -944,6 +1047,7 @@
<button class="b-time" id="b-time">设定</button> <button class="b-time" id="b-time">设定</button>
<span id="timer-info" style="color:#6c5ce7;font-size:11px"></span> <span id="timer-info" style="color:#6c5ce7;font-size:11px"></span>
</div> </div>
<div id="timer-tip" style="color:#fdcb6e;font-size:11px;margin-bottom:8px">⚠️ 每次确认放票时间</div>
<div class="btns"> <div class="btns">
<button class="b-go" id="b-go">▶ 主动抢购</button> <button class="b-go" id="b-go">▶ 主动抢购</button>
<button class="b-stop" id="b-stop" style="display:none">■ 停止</button> <button class="b-stop" id="b-stop" style="display:none">■ 停止</button>
@@ -986,7 +1090,7 @@
// 闭包引用供 refreshUI 使用 // 闭包引用供 refreshUI 使用
_shadowRef = shadow; _shadowRef = shadow;
log('v4.5 已加载 (极速并发+时间同步+全自动抢购)'); log(`v${VERSION} 已加载 (极速并发+时间同步+全自动抢购)`);
if (state.captured) log('已恢复上次捕获的请求参数, 可直接设定时间'); if (state.captured) log('已恢复上次捕获的请求参数, 可直接设定时间');
setupDialogWatcher(); setupDialogWatcher();
@@ -1072,7 +1176,7 @@
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
// 启动 // 启动
// ═══════════════════════════════════════════ // ═══════════════════════════════════════════
console.log('[GLM] v4.0 已注入'); console.log(`[GLM] v${VERSION} 已注入`);
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createPanel); document.addEventListener('DOMContentLoaded', createPanel);
} else { } else {