Greasy Fork is available in English.
智能监控,支持配置多级备选抢购;自动穿透限流弹窗;默认使用拼团折扣码更优惠,介意误用
当前为
// ==UserScript==
// @name GLM Coding Plan抢购助手
// @namespace http://tampermonkey.net/
// @version 6.6
// @description 智能监控,支持配置多级备选抢购;自动穿透限流弹窗;默认使用拼团折扣码更优惠,介意误用
// @author mumumi
// @include https://*bigmodel.cn/glm-coding*
// @include https://*bigmodel.cn/html/rate-limit.html*
// @icon https://www.google.com/s2/favicons?sz=64&domain=bigmodel.cn
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
// ── 最早读配置(document-start 时还没有主流程,只读 INTERCEPT 标志)──────
const _ec = (() => { try { return JSON.parse(GM_getValue('glm_coding_config_v5', '{}')); } catch { return {}; } })();
// ── 调试模式:JSON.parse 钩子让页面按钮显示可购(重启后生效)────────────
// 仅影响前端渲染,不改变购买逻辑;真正有货仍需接口返回有效 bizId
if (_ec.INTERCEPT) {
const _oP = JSON.parse;
JSON.parse = function (t, r) {
const o = _oP(t, r);
try { (function f(x) {
if (!x || typeof x !== 'object') return;
if ('isSoldOut' in x && x.isSoldOut === true) x.isSoldOut = false;
if ('soldOut' in x && x.soldOut === true) x.soldOut = false;
for (const k in x) f(x[k]);
})(o); } catch {}
return o;
};
}
// ── 购买状态(供 fetch 拦截器与 UI 主循环共享)────────────────────────────
const PS = {
inProgress: false,
result: null, // null | 'success' | 'sold_out'
bizId: null,
payAmount: null,
};
// ── preview 接口拦截(始终开启)──────────────────────────────────────────
// 真实模式:连续重试直到拿到有效 bizId(最多12秒),超时后返回售罄响应
// 调试模式:立刻返回伪造的成功响应,触发支付弹窗验证完整流程
const _oF = window.fetch;
const _sl = ms => new Promise(r => setTimeout(r, ms));
window.fetch = async function (...a) {
const url = (typeof a[0] === 'string' ? a[0] : a[0]?.url) || '';
if (!url.includes('/api/biz/pay/preview')) return _oF.apply(this, a);
PS.inProgress = true; PS.result = null; PS.bizId = null; PS.payAmount = null;
if (_ec.INTERCEPT) {
// 调试模式:提取 productId 后伪造成功响应
let pid = 'debug';
try { pid = JSON.parse(a[1]?.body || '{}').productId || pid; } catch {}
PS.result = 'success'; PS.payAmount = 149; PS.bizId = 'debug-' + pid; PS.inProgress = false;
return new Response(
JSON.stringify({ code: 200, success: true, data: { productId: pid, bizId: 'debug-' + pid, soldOut: false, payAmount: 149, originalAmount: 149, renewAmount: 149 } }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
// 真实模式:剥离 AbortSignal,防止前端超时中断重试
const [, ini = {}] = a;
const { signal, ...ci } = ini;
for (let i = 0; i < 120; i++) {
try {
const r = await _oF(url, { ...ci, credentials: 'include' });
const txt = await r.text();
let d; try { d = JSON.parse(txt); } catch { await _sl(100); continue; }
if (d?.code === 200 && d?.data?.bizId) {
PS.result = 'success'; PS.bizId = d.data.bizId; PS.payAmount = d.data.payAmount; PS.inProgress = false;
return new Response(txt, { status: 200, headers: { 'Content-Type': 'application/json' } });
}
// soldOut:true 或其他错误码 → 继续重试,可能是高峰期瞬间售罄
} catch {}
await _sl(100);
}
// 120次(12秒)均未获得有效 bizId → 确认售罄
PS.result = 'sold_out'; PS.inProgress = false;
return new Response(
'{"code":200,"data":{"soldOut":true,"bizId":null},"success":true,"msg":""}',
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
};
// XHR 兜底(重定向到上方的 fetch 拦截器)
const _xO = XMLHttpRequest.prototype.open, _xS = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u, ...r) { this._u = u; return _xO.call(this, m, u, ...r); };
XMLHttpRequest.prototype.send = function (...a) {
if ((this._u || '').includes('/api/biz/pay/preview')) {
const self = this;
window.fetch(this._u, { method: this._m || 'POST', body: a[0], credentials: 'include' }).then(async r => {
const txt = await r.text();
const dp = (k, v) => Object.defineProperty(self, k, { value: v, configurable: true });
dp('readyState', 4); dp('status', 200); dp('statusText', 'OK');
dp('responseText', txt); dp('response', txt);
const ev = new Event('readystatechange');
if (typeof self.onreadystatechange === 'function') self.onreadystatechange(ev);
self.dispatchEvent(ev);
['load', 'loadend'].forEach(t => { const e = new ProgressEvent(t); if (typeof self[`on${t}`] === 'function') self[`on${t}`](e); self.dispatchEvent(e); });
});
return;
}
return _xS.apply(this, a);
};
// ── 限流页立即跳回 ────────────────────────────────────────────────────────
if (location.href.includes('rate-limit.html')) {
location.replace('https://www.bigmodel.cn/glm-coding?ic=UMYTQHW8I2&closedialog=true');
return;
}
// ── 配置 ──────────────────────────────────────────────────────────────────
const STORAGE_KEY = 'glm_coding_config_v5';
const TABS_MAP = { 1: '连续包月', 2: '连续包季', 3: '连续包年' };
const PKGS_MAP = { 1: 'Lite', 2: 'Pro', 3: 'Max' };
const DEF = { TABS_PRIORITY: '1', PACKAGES_PRIORITY: '2,3,1', CHECK_INTERVAL: 100, SMART_REFRESH: true, INTERCEPT: false, SCHEDULE_TIME: '' };
function loadCfg() { try { const s = GM_getValue(STORAGE_KEY, null); return s ? { ...DEF, ...JSON.parse(s) } : { ...DEF }; } catch { return { ...DEF }; } }
function saveCfg(c) { GM_setValue(STORAGE_KEY, JSON.stringify(c)); }
const CFG = loadCfg();
// ── 每日套餐状态(localStorage,按日隔离)────────────────────────────────
// -1未知 0进行中(重启复位) 1今日售罄 2今日已购
const _today = new Date().toISOString().slice(0, 10);
const _dsKey = `glm_ds_${_today}`;
let _ds = (() => { try { return JSON.parse(localStorage.getItem(_dsKey) || '{}'); } catch { return {}; } })();
Object.keys(_ds).forEach(k => { if (_ds[k] === 0) _ds[k] = -1; });
_flush();
function _flush() { localStorage.setItem(_dsKey, JSON.stringify(_ds)); }
function getS(t, p) { return _ds[`${t}-${p}`] ?? -1; }
function setS(t, p, v) { _ds[`${t}-${p}`] = v; _flush(); }
if (Object.values(_ds).includes(2)) {
setTimeout(() => setBar('🎉 今日已订阅成功,脚本停止。', '#237804'), 800);
return;
}
// ── 扫描队列(过滤今日已确认售罄)────────────────────────────────────────
const tabs = String(CFG.TABS_PRIORITY).split(',').map(Number).filter(Boolean);
const pkgs = String(CFG.PACKAGES_PRIORITY).split(',').map(Number).filter(Boolean);
const scanQueue = tabs.flatMap(t => pkgs.map(p => ({ tab: t, pkg: p }))).filter(({ tab: t, pkg: p }) => getS(t, p) !== 1);
if (!scanQueue.length) {
setTimeout(() => { setBar('📭 今日所有配置套餐均已售罄,脚本停止。', '#434343'); triggerPromo(); }, 800);
return;
}
// ── 状态机变量 ────────────────────────────────────────────────────────────
let state = 'SCANNING'; // SCANNING | TASK_UNIT | SLEEPING | DONE
let qIdx = 0, sweepRestocks = [], lastTabSwitch = 0;
let taskTarget = null, taskPhase = 'IDLE', taskClickTime = 0, taskRLCount = 0;
let sleepUntil = 0;
const MAX_RL = 3, MODAL_WAIT = 15000;
// ── 工具函数 ──────────────────────────────────────────────────────────────
function parseRestock(text) {
const m = (text || '').match(/0?(\d{1,2})月0?(\d{1,2})日\s*(\d{1,2}):0?(\d{1,2})/);
if (!m) return null;
const t = new Date(new Date().getFullYear(), +m[1] - 1, +m[2], +m[3], +m[4]);
return { dateStr: `${+m[1]}月${+m[2]}日`, msUntil: t - Date.now() };
}
function todayStr() { const d = new Date(); return `${d.getMonth() + 1}月${d.getDate()}日`; }
function fmt(ms) {
if (ms <= 0) return '0秒';
const s = Math.floor(ms / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60);
return h ? `${h}h${m % 60}m` : m ? `${m}分${s % 60}秒` : `${s}秒`;
}
function calcSleepMs(ms) {
if (ms > 3600000) return 240000;
if (ms > 1800000) return 180000;
if (ms > 900000) return 120000;
if (ms > 300000) return 60000;
if (ms > 120000) return 30000;
if (ms > 60000) return 10000;
if (ms > 10000) return 3000;
return 0;
}
function parseScheduleTime(str) {
if (!str) return null;
const p = str.split(':').map(Number);
const d = new Date();
const t = new Date(d.getFullYear(), d.getMonth(), d.getDate(), p[0], p[1], p[2] || 0);
return t.getTime();
}
// ── DOM 访问 ──────────────────────────────────────────────────────────────
const tabEl = n => document.querySelector(`#switchTabBox > div > .switch-tab-item:nth-child(${n + 1})`);
const btnEl = n => document.querySelector(`.glm-coding-package-list > div:nth-child(${n}) > div > .package-card-btn-box > button`);
const canBuy = b => b && !b.disabled && !b.classList.contains('is-disabled') && (b.innerText || '').includes('特惠订阅');
const isSoldOut = b => /售罄|补货|暂时/.test(b?.innerText || '');
// ── 弹窗检测 ──────────────────────────────────────────────────────────────
function findRLModal() {
for (const w of document.querySelectorAll('.el-dialog__wrapper'))
if (getComputedStyle(w).display !== 'none' && (w.innerText || '').includes('当前购买人数较多')) return w;
return null;
}
function isPayDialog() {
// .pay-dialog 出现且不含限流提示 = 支付二维码弹窗
const d = document.querySelector('.pay-dialog');
if (!d) return false;
const w = d.closest('.el-dialog__wrapper');
if (!w || getComputedStyle(w).display === 'none') return false;
return !(w.innerText || '').includes('当前购买人数较多');
}
function isSuccessDialog() {
const w = document.querySelector('.pay-success-dialog-box')?.closest('.el-dialog__wrapper');
return w ? getComputedStyle(w).display !== 'none' : false;
}
function closeModal(w) { w?.querySelector('.el-dialog__close')?.click(); }
// ── 底部状态栏 ────────────────────────────────────────────────────────────
let _bar = null;
function setBar(html, bg = '#1677ff') {
if (!_bar) {
_bar = document.createElement('div');
_bar.style.cssText = 'position:fixed;bottom:0;left:0;right:0;z-index:2147483647;padding:7px 16px;font:13px/1.5 system-ui,sans-serif;color:#fff;display:flex;align-items:center;justify-content:space-between;box-shadow:0 -2px 8px rgba(0,0,0,.25);transition:background .4s';
const x = document.createElement('button');
x.textContent = '×';
x.style.cssText = 'background:rgba(255,255,255,.2);border:none;color:#fff;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:16px;line-height:1;flex-shrink:0';
x.onclick = () => { _bar.remove(); _bar = null; };
_bar.append(document.createElement('span'), x);
document.body.appendChild(_bar);
}
_bar.style.background = bg;
_bar.firstElementChild.innerHTML = `🤖 <b>抢购助手</b> | ${html}`;
}
// ── 支付报警:视口边框红色闪烁,pointer-events:none 不遮挡弹窗 ──────────
let _alarm = null;
function showPayAlarm() {
if (_alarm) return;
if (!document.getElementById('glm-alarm-s')) {
const s = document.createElement('style'); s.id = 'glm-alarm-s';
s.textContent = '@keyframes glm-al{0%,100%{box-shadow:inset 0 0 0 12px rgba(220,38,38,.92)}50%{box-shadow:inset 0 0 0 12px rgba(220,38,38,.08)}}';
document.head.appendChild(s);
}
_alarm = document.createElement('div');
// 整个视口但 pointer-events:none,只有内嵌阴影 → 仅边缘可见,不挡中间弹窗
_alarm.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483646;animation:glm-al .5s steps(1) infinite';
// 顶部横幅(弹窗通常在 15vh 以下,顶部有空间)
const lbl = document.createElement('div');
lbl.style.cssText = 'position:absolute;top:12px;left:50%;transform:translateX(-50%);background:rgba(220,38,38,.95);color:#fff;padding:5px 22px;border-radius:20px;font:700 15px system-ui,sans-serif;white-space:nowrap;letter-spacing:.5px';
lbl.textContent = '⚠️ 请立即扫码支付!';
_alarm.appendChild(lbl);
document.body.appendChild(_alarm);
}
// ── 推广弹窗 ──────────────────────────────────────────────────────────────
function triggerPromo() {
if (Date.now() > new Date('2026-04-30T23:59:59').getTime()) { setBar('所有套餐今日售罄,脚本停止。', '#434343'); return; }
const PP = [
{ name: 'MiniMax', desc: '¥29起,赠视频/语音/音乐额度', color: '#4CAF50', url: 'https://platform.minimaxi.com/subscribe/token-plan?code=FoGlERWIF3&source=link' },
{ name: '字节·方舟', desc: '首月低至¥8.9,多模型切换', color: '#FF6B35', url: 'https://volcengine.com/L/YIeVPueJ2O4/' },
{ name: '讯飞星辰', desc: '¥19起,首月¥3.9最低门槛', color: '#00BFFF', url: 'https://maas.xfyun.cn/packageSubscription?inviteCode=MAAS-C6BE3A3B' },
{ name: '无问芯穹', desc: '多模型聚合,首月¥19.9', color: '#7B68EE', url: 'https://cloud.infini-ai.com/login?redirect=/genstudio/invitation&invite_code=IyveoKRS' },
];
const rows = PP.map(p => `
<a href="${p.url}" target="_blank" style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-radius:8px;text-decoration:none;border:1px solid #eee;background:#fafafa" onmouseover="this.style.background='#f0f7ff'" onmouseout="this.style.background='#fafafa'">
<div style="display:flex;align-items:center;gap:10px">
<span style="width:8px;height:8px;border-radius:50%;background:${p.color};display:inline-block"></span>
<span style="font-weight:600;font-size:14px;color:#111">${p.name}</span>
<span style="font-size:12px;color:#888">${p.desc}</span>
</div>
<span style="font-size:12px;color:${p.color};font-weight:500">立即开通 →</span>
</a>`).join('');
const ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:2147483645;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(5px);font-family:system-ui,sans-serif';
ov.innerHTML = `
<div style="background:#fff;width:520px;border-radius:16px;overflow:hidden;box-shadow:0 25px 50px -12px rgba(0,0,0,.5);max-height:90vh;display:flex;flex-direction:column">
<div style="background:linear-gradient(135deg,#1e3c72,#2a5298);padding:24px 24px 20px;color:#fff">
<h2 style="margin:0 0 6px;font-size:20px">GLM Coding Plan 全部售罄 🫠</h2>
<p style="margin:0;opacity:.85;font-size:14px">配置的所有套餐今日已售罄,补货后脚本将继续监控</p>
</div>
<div style="padding:16px 20px;overflow-y:auto;flex:1">
<div style="font-size:13px;color:#888;margin-bottom:12px">👇 以下平台也有编程套餐和折扣链接</div>
<div style="display:flex;flex-direction:column;gap:8px">${rows}</div>
</div>
<div style="padding:14px 20px;border-top:1px solid #f0f0f0;text-align:right">
<button id="promo-x" style="background:none;border:1px solid #ddd;color:#888;padding:7px 18px;border-radius:6px;cursor:pointer;font-size:13px">关闭并停止脚本</button>
</div>
</div>`;
document.body.appendChild(ov);
ov.querySelector('#promo-x').onclick = () => ov.remove();
ov.onclick = e => { if (e.target === ov) ov.remove(); };
}
// ── 配置面板 ──────────────────────────────────────────────────────────────
function buildTransferBox(ct, dataMap, selectedStr, title) {
const sel = selectedStr.split(',').filter(Boolean);
const avail = Object.keys(dataMap).filter(k => !sel.includes(k));
ct.innerHTML = `
<div style="font-size:13px;font-weight:bold;margin-bottom:8px;color:#444">${title}</div>
<div style="display:flex;align-items:stretch;gap:10px;margin-bottom:20px;height:140px">
<div style="flex:1;border:1px solid #ddd;border-radius:6px;display:flex;flex-direction:column;background:#fafafa">
<div style="padding:6px 10px;border-bottom:1px solid #ddd;font-size:12px;color:#666;background:#f0f0f0;border-radius:6px 6px 0 0">备选池</div>
<ul class="tf-left" style="list-style:none;padding:5px;margin:0;flex:1;overflow-y:auto">
${avail.map(k => `<li data-val="${k}" class="tf-item">${dataMap[k]}</li>`).join('')}
</ul>
</div>
<div style="display:flex;flex-direction:column;justify-content:center;gap:8px">
<button type="button" class="tf-btn tf-r">▶</button>
<button type="button" class="tf-btn tf-l">◀</button>
</div>
<div style="flex:1;border:1px solid #ddd;border-radius:6px;display:flex;flex-direction:column;background:#fff">
<div style="padding:6px 10px;border-bottom:1px solid #ddd;font-size:12px;color:#666;background:#e6f7ff;border-radius:6px 6px 0 0">选中且排序(自上而下)</div>
<ul class="tf-right" style="list-style:none;padding:5px;margin:0;flex:1;overflow-y:auto">
${sel.map(k => `<li data-val="${k}" class="tf-item">${dataMap[k]}</li>`).join('')}
</ul>
</div>
<div style="display:flex;flex-direction:column;justify-content:center;gap:8px">
<button type="button" class="tf-btn tf-up">▲</button>
<button type="button" class="tf-btn tf-dn">▼</button>
</div>
</div>`;
const L = ct.querySelector('.tf-left'), R = ct.querySelector('.tf-right');
ct.querySelectorAll('ul').forEach(ul => ul.addEventListener('click', e => {
if (e.target.tagName === 'LI') { ct.querySelectorAll('.tf-item').forEach(i => i.classList.remove('active')); e.target.classList.add('active'); }
}));
ct.querySelector('.tf-r').onclick = () => { const a = L.querySelector('.active'); if (a) { R.appendChild(a); a.classList.remove('active'); } };
ct.querySelector('.tf-l').onclick = () => { const a = R.querySelector('.active'); if (a) { L.appendChild(a); a.classList.remove('active'); } };
ct.querySelector('.tf-up').onclick = () => { const a = R.querySelector('.active'); if (a?.previousElementSibling) R.insertBefore(a, a.previousElementSibling); };
ct.querySelector('.tf-dn').onclick = () => { const a = R.querySelector('.active'); if (a?.nextElementSibling) R.insertBefore(a.nextElementSibling, a); };
return () => [...R.querySelectorAll('.tf-item')].map(i => i.dataset.val).join(',');
}
function openConfigPanel() {
document.getElementById('glm-cfg-ov')?.remove();
if (!document.getElementById('glm-tf-s')) {
const s = document.createElement('style'); s.id = 'glm-tf-s';
s.textContent = '.tf-item{padding:6px 10px;margin-bottom:4px;border-radius:4px;cursor:pointer;font-size:13px;color:#333;border:1px solid transparent;transition:all .15s}.tf-item:hover{background:#f5f5f5}.tf-item.active{background:#e6f7ff;border-color:#91d5ff;color:#1890ff;font-weight:700}.tf-btn{padding:4px 8px;font-size:10px;cursor:pointer;border:1px solid #d9d9d9;border-radius:4px;background:#fff;color:#555;height:28px;transition:.2s}.tf-btn:hover{border-color:#40a9ff;color:#40a9ff}';
document.head.appendChild(s);
}
const ov = document.createElement('div'); ov.id = 'glm-cfg-ov';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2147483646;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);font-family:system-ui,sans-serif';
const panel = document.createElement('div');
panel.style.cssText = 'background:#fff;color:#333;width:540px;padding:24px;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.3)';
panel.innerHTML = `
<h3 style="margin:0 0 20px;font-size:18px;color:#1a1a1a">⚙️ 抢购助手配置</h3>
<div id="glm-wp"></div>
<div id="glm-wt"></div>
<div style="margin-bottom:20px;padding-top:10px;border-top:1px dashed #eee;display:flex;flex-direction:column;gap:10px">
<label style="display:flex;align-items:center;cursor:pointer">
<input type="checkbox" id="glm-sm" ${CFG.SMART_REFRESH ? 'checked' : ''} style="margin-right:8px">
<span style="font-size:14px;color:#555">启用智能刷新(梯度嗅探补货时间)</span>
</label>
<label style="display:flex;align-items:center;cursor:pointer">
<input type="checkbox" id="glm-ic" ${CFG.INTERCEPT ? 'checked' : ''} style="margin-right:8px">
<span style="font-size:14px;color:#c41d7f">调试模式:强制显示可购按钮 + 伪造接口(重启生效)</span>
</label>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span style="font-size:14px;color:#555">定时自动点击:</span>
<input type="time" id="glm-sched" step="1" value="${CFG.SCHEDULE_TIME || ''}" style="padding:4px 8px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;color:#333">
<span style="font-size:12px;color:#999">不填则由扫描自动触发</span>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:10px">
<button id="glm-cc" style="padding:8px 16px;border:1px solid #ddd;background:#f5f5f5;border-radius:6px;cursor:pointer;color:#666">取消</button>
<button id="glm-cs" style="padding:8px 20px;border:none;background:#1890ff;color:#fff;border-radius:6px;cursor:pointer;font-weight:700">保存并刷新</button>
</div>`;
ov.appendChild(panel);
document.body.appendChild(ov);
const getPkgs = buildTransferBox(document.getElementById('glm-wp'), PKGS_MAP, CFG.PACKAGES_PRIORITY, '套餐优先级');
const getTabs = buildTransferBox(document.getElementById('glm-wt'), TABS_MAP, CFG.TABS_PRIORITY, '订阅周期优先级');
panel.querySelector('#glm-cc').onclick = () => ov.remove();
panel.querySelector('#glm-cs').onclick = () => {
const p = getPkgs(), t = getTabs();
if (!p || !t) { alert('请至少各选一个!'); return; }
saveCfg({ TABS_PRIORITY: t, PACKAGES_PRIORITY: p, SMART_REFRESH: panel.querySelector('#glm-sm').checked, INTERCEPT: panel.querySelector('#glm-ic').checked, CHECK_INTERVAL: CFG.CHECK_INTERVAL, SCHEDULE_TIME: panel.querySelector('#glm-sched').value });
ov.remove(); alert('已保存,即将刷新。'); location.reload();
};
ov.onclick = e => { if (e.target === ov) ov.remove(); };
}
GM_registerMenuCommand('⚙️ 打开配置面板', openConfigPanel);
GM_registerMenuCommand('🗑️ 清除今日套餐状态缓存', () => { localStorage.removeItem(_dsKey); alert('今日状态已清除,即将刷新。'); location.reload(); });
// ── 主循环 ────────────────────────────────────────────────────────────────
function tick() {
if (state === 'DONE') return;
if (state === 'SLEEPING') {
const rem = sleepUntil - Date.now();
if (rem <= 0) location.replace('https://www.bigmodel.cn/glm-coding?ic=UMYTQHW8I2&closedialog=true');
else setBar(`💤 休眠中,<b>${fmt(rem)}</b> 后刷新`, '#434343');
return;
}
if (state === 'TASK_UNIT') { doTaskUnit(); return; }
doScan();
}
function doScan() {
if (qIdx >= scanQueue.length) { onSweepDone(); return; }
const { tab, pkg } = scanQueue[qIdx];
const te = tabEl(tab);
if (!te) return;
if (!te.classList.contains('active')) {
te.click(); te.scrollIntoView({ behavior: 'auto', block: 'center' });
lastTabSwitch = Date.now(); setBar(`🔄 切换到 ${TABS_MAP[tab]}...`); return;
}
if (Date.now() - lastTabSwitch < 400) return;
const b = btnEl(pkg);
if (canBuy(b)) {
// 若配置了定时且尚未到时间,停在此处等候(不前进队列)
const schedTs = parseScheduleTime(CFG.SCHEDULE_TIME);
if (schedTs && Date.now() < schedTs - 800) {
setBar(`⏰ 预就位 ${TABS_MAP[tab]} · ${PKGS_MAP[pkg]},<b>${fmt(schedTs - Date.now())}</b> 后自动点击`, '#7c3aed');
return;
}
taskTarget = { tab, pkg }; taskPhase = 'IDLE'; taskRLCount = 0;
setS(tab, pkg, 0); state = 'TASK_UNIT';
setBar(`🎯 发现可购!${TABS_MAP[tab]} · ${PKGS_MAP[pkg]},即将点击...`, '#389e0d');
return;
}
const ri = parseRestock(b?.innerText);
if (ri?.dateStr === todayStr() && ri.msUntil > 0) sweepRestocks.push(ri);
setBar(`🔍 扫描 ${TABS_MAP[tab]} · ${PKGS_MAP[pkg]} (${qIdx + 1}/${scanQueue.length})`);
qIdx++;
}
function onSweepDone() {
if (!sweepRestocks.length) {
state = 'DONE'; setBar('📭 今日全部售罄,脚本停止。', '#434343'); triggerPromo(); return;
}
sweepRestocks.sort((a, b) => a.msUntil - b.msUntil);
const nearest = sweepRestocks[0];
const sleep = calcSleepMs(nearest.msUntil);
if (sleep === 0) {
setBar(`⚡ 补货倒计时 <b>${fmt(nearest.msUntil)}</b>,高频监控!`, '#d4380d');
qIdx = 0; sweepRestocks = []; return;
}
if (CFG.SMART_REFRESH) {
state = 'SLEEPING'; sleepUntil = Date.now() + sleep;
setBar(`💤 补货还需 <b>${fmt(nearest.msUntil)}</b>,<b>${fmt(sleep)}</b> 后刷新`, '#434343');
} else { qIdx = 0; sweepRestocks = []; }
}
function doTaskUnit() {
const { tab, pkg } = taskTarget;
const te = tabEl(tab);
if (!te) return;
if (!te.classList.contains('active')) { te.click(); return; }
const b = btnEl(pkg);
if (taskPhase === 'IDLE') {
if (isSoldOut(b)) { exitTask(); return; }
if (!canBuy(b)) {
// 防抖期(按钮暂时禁用但非售罄),等待恢复
setBar(`⏳ 等待按钮就绪... ${TABS_MAP[tab]} · ${PKGS_MAP[pkg]}`, '#d46b08');
return;
}
// 重置购买状态,点击按钮,fetch 拦截器接管后续重试
PS.result = null; PS.inProgress = true;
b.click(); taskClickTime = Date.now(); taskPhase = 'WAITING';
setBar(`🔄 已点击,接口重试中... ${TABS_MAP[tab]} · ${PKGS_MAP[pkg]}(限流 ${taskRLCount}/${MAX_RL})`, '#d46b08');
return;
}
if (taskPhase === 'WAITING') {
// 1. 限流弹窗(购买人数较多)→ 关闭后重试
const rlw = findRLModal();
if (rlw) {
closeModal(rlw); taskRLCount++;
if (taskRLCount >= MAX_RL) {
setBar(`🔁 连续 ${MAX_RL} 次限流,即将刷新...`, '#cf1322');
setTimeout(() => location.replace('https://www.bigmodel.cn/glm-coding?ic=UMYTQHW8I2&closedialog=true'), 50);
return;
}
setBar(`⚠️ 限流 ${taskRLCount}/${MAX_RL},重试中...`, '#d46b08');
taskPhase = 'IDLE'; return;
}
// 2. 支付二维码弹窗 = 抢购成功,立即停止并报警提示扫码
// 接口有金额(PS.payAmount>0)是最可靠判断;弹窗出现也作为兜底
if (isPayDialog()) {
const hasAmount = PS.payAmount && PS.payAmount > 0;
const waited3s = Date.now() - taskClickTime > 3000;
if (hasAmount || waited3s) {
setS(tab, pkg, 2); state = 'DONE';
showPayAlarm();
setBar('💳 <b>抢购成功!请立即扫码支付!</b> 脚本已停止。', '#16a34a');
}
// 弹窗刚出现但金额未渲染,等一会儿
return;
}
// 3. 付款完成弹窗(兜底)
if (isSuccessDialog()) {
setS(tab, pkg, 2); state = 'DONE';
setBar('🎉 订阅成功!恭喜!', '#237804'); return;
}
// 4. fetch 已返回 sold_out,给 Vue 留 2s 渲染弹窗;若没弹窗则直接退出
if (!PS.inProgress && PS.result === 'sold_out' && Date.now() - taskClickTime > 2000) {
exitTask(); return;
}
// 5. 超时保底(15s)
if (Date.now() - taskClickTime > MODAL_WAIT) {
if (isSoldOut(b)) exitTask(); else taskPhase = 'IDLE';
}
}
}
function exitTask() {
setS(taskTarget.tab, taskTarget.pkg, 1);
setBar(`📦 ${TABS_MAP[taskTarget.tab]} · ${PKGS_MAP[taskTarget.pkg]} 售罄,继续...`);
qIdx++; taskTarget = null; taskPhase = 'IDLE'; taskRLCount = 0;
state = 'SCANNING';
}
// ── 启动 ──────────────────────────────────────────────────────────────────
setInterval(tick, CFG.CHECK_INTERVAL);
})();