// ==UserScript==
// @name GLM Coding 抢购助手 (简化版)
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 准点自动点击指定套餐按钮,仅点击不处理后续流程。
// @author Codex
// @match *://bigmodel.cn/glm-coding*
// @match *://bigmodel.cn/usercenter/glm-coding*
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
if (window.__autoGlmSimpleInitialized) return;
window.__autoGlmSimpleInitialized = true;
// ==========================================
// 网络拦截 - 绕过UI限制
// ==========================================
const originalJSONParse = JSON.parse;
JSON.parse = function (text, reviver) {
let result = originalJSONParse(text, reviver);
function deepModify(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 key in obj) {
if (obj[key] && typeof obj[key] === 'object') {
deepModify(obj[key]);
}
}
}
try {
deepModify(result);
} catch (e) {
console.log('[Auto-GLM-Simple] JSON拦截异常:', e.message);
}
return result;
};
const originalFetch = window.fetch;
window.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const clone = response.clone();
try {
let text = await clone.text();
if (text.includes('"isSoldOut":true') || text.includes('"disabled":true') || text.includes('"soldOut":true')) {
console.log('[Auto-GLM-Simple] 拦截Fetch售罄数据:', args[0]);
text = text.replace(/"isSoldOut":true/g, '"isSoldOut":false')
.replace(/"disabled":true/g, '"disabled":false')
.replace(/"soldOut":true/g, '"soldOut":false')
.replace(/"stock":0/g, '"stock":999');
return new Response(text, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
} catch (e) {
console.log('[Auto-GLM-Simple] Fetch拦截异常:', e.message);
}
}
return response;
};
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this._reqUrl = url;
return originalXHROpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function (...args) {
this.addEventListener('readystatechange', function () {
if (this.readyState === 4 && this.status === 200) {
const contentType = this.getResponseHeader('content-type') || '';
if (contentType.includes('application/json')) {
try {
let text = this.responseText;
if (text.includes('"isSoldOut":true') || text.includes('"disabled":true') || text.includes('"soldOut":true')) {
console.log('[Auto-GLM-Simple] 拦截XHR售罄数据:', this._reqUrl);
text = text.replace(/"isSoldOut":true/g, '"isSoldOut":false')
.replace(/"disabled":true/g, '"disabled":false')
.replace(/"soldOut":true/g, '"soldOut":false')
.replace(/"stock":0/g, '"stock":999');
Object.defineProperty(this, 'responseText', { get: function () { return text; } });
Object.defineProperty(this, 'response', { get: function () { return JSON.parse(text); } });
}
} catch (e) {
console.log('[Auto-GLM-Simple] XHR拦截异常:', e.message);
}
}
}
});
originalXHRSend.apply(this, args);
};
console.log('[Auto-GLM-Simple] 网络拦截器已注册');
// ==========================================
// 简化配置
// ==========================================
const STORAGE_KEY = 'glm-simple-config';
const WATCH_GRACE_MS = 5 * 60 * 1000;
const CYCLE_SETTLE_MS = 350;
const SECOND_CLICK_DELAY_MS = 90;
const DIALOG_RETRY_BASE_DELAY_MS = 500;
const DIALOG_RETRY_RANDOM_MS = 500;
const PRODUCT_MAP = {
Lite: { month: 'product-02434c', quarter: 'product-b8ea38', year: 'product-70a804' },
Pro: { month: 'product-1df3e1', quarter: 'product-fef82f', year: 'product-5643e6' },
Max: { month: 'product-2fc421', quarter: 'product-5d3a03', year: 'product-d46f8b' }
};
const CYCLE_LABELS = { month: '连续包月', quarter: '连续包季', year: '连续包年' };
const DEFAULT_CONFIG = {
targetPlan: 'Pro',
billingCycle: 'quarter',
targetHour: 10,
targetMinute: 0,
targetSecond: 0,
autoStart: false
};
let config = loadConfig();
let tickTimer = null;
let isWatching = false;
let hasClicked = false;
let isClicking = false;
let targetTimestamp = 0;
let lastCycleSwitchAt = 0;
let lastStatusText = '';
function clampNumber(value, min, max, fallback) {
const next = Number(value);
if (!Number.isFinite(next)) return fallback;
return Math.min(max, Math.max(min, Math.floor(next)));
}
function sanitizeConfig(raw = {}) {
return {
targetPlan: PRODUCT_MAP[raw.targetPlan] ? raw.targetPlan : DEFAULT_CONFIG.targetPlan,
billingCycle: CYCLE_LABELS[raw.billingCycle] ? raw.billingCycle : DEFAULT_CONFIG.billingCycle,
targetHour: clampNumber(raw.targetHour, 0, 23, DEFAULT_CONFIG.targetHour),
targetMinute: clampNumber(raw.targetMinute, 0, 59, DEFAULT_CONFIG.targetMinute),
targetSecond: clampNumber(raw.targetSecond, 0, 59, DEFAULT_CONFIG.targetSecond),
autoStart: Boolean(raw.autoStart)
};
}
function loadConfig() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { ...DEFAULT_CONFIG };
return { ...DEFAULT_CONFIG, ...sanitizeConfig(JSON.parse(raw)) };
} catch {
return { ...DEFAULT_CONFIG };
}
}
function saveConfig() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
} catch (e) {
console.warn('[Auto-GLM-Simple] 保存配置失败:', e);
}
}
function escapeHtml(text) {
return String(text)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function log(msg) {
console.log(`[Auto-GLM-Simple] ${msg}`);
const logBox = document.getElementById('glm-simple-log');
if (logBox) {
const time = new Date().toLocaleTimeString();
logBox.innerHTML = `
[${time}] ${escapeHtml(msg)}
` + logBox.innerHTML;
if (logBox.children.length > 50) logBox.lastElementChild.remove();
}
}
function updateStatus(text) {
if (text === lastStatusText) return;
lastStatusText = text;
const statusEl = document.getElementById('glm-simple-status');
if (statusEl) statusEl.textContent = text;
}
function normalizeText(text) {
return String(text || '').replace(/\s+/g, '').trim();
}
function getTargetDate(now = new Date()) {
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
config.targetHour,
config.targetMinute,
config.targetSecond || 0,
0
);
}
function refreshTargetTimestamp() {
targetTimestamp = getTargetDate().getTime();
}
function sleep(ms) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function isVisibleElement(node) {
if (!node || !node.isConnected) return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
// ==========================================
// 核心逻辑:准点点击
// ==========================================
function findCycleTab(cycle) {
const label = CYCLE_LABELS[cycle];
if (!label) return null;
return Array.from(document.querySelectorAll('.switch-tab-item'))
.find((node) => normalizeText(node.textContent).includes(normalizeText(label))) || null;
}
function ensureBillingCycleSelected() {
const tab = findCycleTab(config.billingCycle);
if (!tab) return false;
if (tab.classList.contains('active')) return true;
if (Date.now() - lastCycleSwitchAt < CYCLE_SETTLE_MS) return false;
lastCycleSwitchAt = Date.now();
dispatchRealClick(tab.querySelector('.switch-tab-item-content') || tab);
return false;
}
function findPlanCard(planName) {
return Array.from(document.querySelectorAll('.package-card-box .package-card'))
.filter(isVisibleElement)
.find((card) => {
const title = card.querySelector('.package-card-title .font-prompt');
return title && normalizeText(title.textContent) === normalizeText(planName);
}) || null;
}
function findBuyButton(card) {
if (!card) return null;
return Array.from(card.querySelectorAll('button.buy-btn, .package-card-btn-box button'))
.find(isVisibleElement) || null;
}
function getButtonState(button) {
if (!button) return { text: '', disabled: true };
return {
text: normalizeText(button.textContent),
disabled: button.disabled
|| button.getAttribute('aria-disabled') === 'true'
|| button.classList.contains('is-disabled')
|| button.classList.contains('disabled')
};
}
function temporarilyEnableButton(button) {
if (!button) return () => {};
const previous = {
disabled: button.disabled,
disabledAttr: button.getAttribute('disabled'),
ariaDisabled: button.getAttribute('aria-disabled'),
className: button.className
};
button.disabled = false;
button.removeAttribute('disabled');
button.setAttribute('aria-disabled', 'false');
button.classList.remove('is-disabled', 'disabled');
return () => {
button.disabled = previous.disabled;
if (previous.disabledAttr === null) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', previous.disabledAttr);
}
if (previous.ariaDisabled === null) {
button.removeAttribute('aria-disabled');
} else {
button.setAttribute('aria-disabled', previous.ariaDisabled);
}
button.className = previous.className;
};
}
function findPayDialogRoot() {
return document.querySelector('.white-mask-bg .pay-dialog, .white-mask-bg .scan-code-box, .confirm-pay-btn, .scan-qrcode-box');
}
function isRealPayDialog(root) {
if (!root) return false;
const priceEl = root.querySelector('.scan-qrcode-box .price-icon + span');
if (priceEl) {
const priceText = priceEl.textContent?.trim();
return Boolean(priceText && priceText.length > 0);
}
return Boolean(root.querySelector('.confirm-pay-btn'));
}
function findFailureDialog() {
const busyDialog = document.querySelector('.el-dialog__wrapper .empty-data-wrap');
if (busyDialog?.textContent?.includes('购买人数较多')) {
return { type: 'busy', closeBtn: busyDialog.closest('.el-dialog')?.querySelector('.el-dialog__headerbtn') };
}
const payRoot = document.querySelector('.white-mask-bg .pay-dialog, .white-mask-bg .scan-code-box');
if (payRoot && !isRealPayDialog(payRoot)) {
return { type: 'empty-price', closeBtn: payRoot.querySelector('.el-dialog__headerbtn, .confirm-pay-btn') };
}
return null;
}
function dispatchMouseLikeEvent(target, type, init) {
const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
target.dispatchEvent(new EventCtor(type, init));
}
function dispatchRealClick(target) {
if (!target || !target.isConnected) return false;
try {
target.scrollIntoView({ block: 'center', inline: 'center', behavior: 'auto' });
} catch {}
try {
target.focus({ preventScroll: true });
} catch {}
const rect = target.getBoundingClientRect();
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: rect.left + Math.max(1, rect.width / 2),
clientY: rect.top + Math.max(1, rect.height / 2)
};
['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
dispatchMouseLikeEvent(target, type, eventInit);
});
target.click();
return true;
}
function getNextTickDelay(now = Date.now()) {
const diff = targetTimestamp - now;
if (diff > 60_000) return 1000;
if (diff > 10_000) return 400;
if (diff > 3_000) return 120;
if (diff > 0) return 40;
if (diff > -WATCH_GRACE_MS) return 60;
return 250;
}
function scheduleNextTick(delay = getNextTickDelay()) {
if (!isWatching) return;
if (tickTimer) clearTimeout(tickTimer);
tickTimer = window.setTimeout(() => {
tickTimer = null;
void tick();
}, delay);
}
function isTargetWindowExpired(now = Date.now()) {
return now > targetTimestamp + WATCH_GRACE_MS;
}
function getCountdown() {
const diff = targetTimestamp - Date.now();
if (diff <= 0) return null;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
async function triggerBuyButton(button) {
if (!button || isClicking) return false;
isClicking = true;
let restoreButton = null;
const beforeText = getButtonState(button).text;
try {
const { disabled } = getButtonState(button);
if (disabled) {
restoreButton = temporarilyEnableButton(button);
log('到点后临时解除按钮禁用,准备触发购买按钮');
}
dispatchRealClick(button);
await sleep(SECOND_CLICK_DELAY_MS);
const dialogOpened = Boolean(findPayDialogRoot());
const buttonChanged = !button.isConnected || getButtonState(button).text !== beforeText;
if (!dialogOpened && !buttonChanged && button.isConnected) {
dispatchRealClick(button);
}
return true;
} finally {
if (restoreButton) {
window.setTimeout(() => {
restoreButton();
}, 1200);
}
isClicking = false;
}
}
async function tick() {
if (!isWatching || hasClicked) return;
if (isTargetWindowExpired()) {
stopWatching({ statusText: '已过时间', logMessage: '已超过目标时间窗口,监听结束' });
return;
}
const countdown = getCountdown();
if (countdown) {
updateStatus(`倒计时 ${countdown}`);
} else {
updateStatus('已到点,等待按钮就绪');
}
const cycleReady = ensureBillingCycleSelected();
if (!cycleReady) {
scheduleNextTick();
return;
}
if (Date.now() - lastCycleSwitchAt < CYCLE_SETTLE_MS) {
scheduleNextTick();
return;
}
const card = findPlanCard(config.targetPlan);
const button = findBuyButton(card);
if (!button) {
if (Date.now() >= targetTimestamp) updateStatus('已到点,等待购买按钮出现');
scheduleNextTick();
return;
}
if (Date.now() < targetTimestamp) {
scheduleNextTick();
return;
}
const clicked = await triggerBuyButton(button);
if (clicked) {
const failureDialog = findFailureDialog();
if (failureDialog) {
log(`出现失败弹窗(${failureDialog.type}),正在关闭...`);
dispatchRealClick(failureDialog.closeBtn);
await sleep(getDialogRetryDelay());
log(`重新点击购买按钮`);
scheduleNextTick();
return;
}
hasClicked = true;
const timeStr = `${config.targetHour}:${String(config.targetMinute).padStart(2, '0')}:${String(config.targetSecond || 0).padStart(2, '0')}`;
log(`[${timeStr}] 已点击 ${config.targetPlan} ${CYCLE_LABELS[config.billingCycle]} 购买按钮`);
stopWatching({ statusText: '已点击,等待后续操作', logMessage: '' });
return;
}
scheduleNextTick();
}
function stopWatching(options = {}) {
const { statusText = '已停止', logMessage = '已停止' } = options;
if (tickTimer) {
clearTimeout(tickTimer);
tickTimer = null;
}
isWatching = false;
if (logMessage) log(logMessage);
updateStatus(statusText);
}
function getDialogRetryDelay() {
return DIALOG_RETRY_BASE_DELAY_MS + Math.floor(Math.random() * DIALOG_RETRY_RANDOM_MS);
}
function startWatching() {
if (isWatching) return;
refreshTargetTimestamp();
if (isTargetWindowExpired()) {
log('已超过目标时间,无法启动监听');
updateStatus('已过时间');
return;
}
isWatching = true;
hasClicked = false;
isClicking = false;
lastCycleSwitchAt = 0;
const timeStr = `${config.targetHour}:${String(config.targetMinute).padStart(2, '0')}:${String(config.targetSecond || 0).padStart(2, '0')}`;
log(`开始监听,目标时间: ${timeStr}`);
updateStatus(getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪');
scheduleNextTick(0);
}
function resetClicked() {
hasClicked = false;
isClicking = false;
log(isWatching ? '已重置点击状态,继续监听' : '已重置点击状态');
updateStatus(isWatching ? (getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪') : '就绪');
if (isWatching) scheduleNextTick(0);
}
function handleConfigChange() {
saveConfig();
if (!isWatching) return;
refreshTargetTimestamp();
hasClicked = false;
isClicking = false;
lastCycleSwitchAt = 0;
log('配置已更新,监听目标时间已重新对齐');
updateStatus(getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪');
scheduleNextTick(0);
}
// ==========================================
// UI
// ==========================================
function injectStyles() {
if (document.getElementById('glm-simple-style')) return;
const style = document.createElement('style');
style.id = 'glm-simple-style';
style.textContent = `
#glm-simple-panel {
position: fixed;
left: 20px;
bottom: 20px;
width: 300px;
z-index: 999999;
border-radius: 16px;
overflow: hidden;
background: linear-gradient(135deg, #10233f 0%, #1d4ed8 64%, #38bdf8 100%);
box-shadow: 0 24px 64px -28px rgba(16, 35, 63, 0.45);
font-family: "SF Pro Display", "PingFang SC", "Segoe UI", sans-serif;
color: #eff6ff;
}
#glm-simple-panel * { box-sizing: border-box; }
.glm-simple-head {
padding: 14px 16px;
}
.glm-simple-title {
font-size: 14px;
font-weight: 700;
}
.glm-simple-body {
padding: 12px 14px;
background: rgba(255,255,255,0.95);
color: #1e293b;
}
.glm-simple-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
}
.glm-simple-field {
flex: 1;
}
.glm-simple-field label {
display: block;
font-size: 11px;
font-weight: 600;
color: #475569;
margin-bottom: 4px;
}
.glm-simple-field select,
.glm-simple-field input {
width: 100%;
padding: 6px 8px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 13px;
background: #f8fafc;
}
.glm-simple-time {
display: flex;
align-items: center;
gap: 4px;
}
.glm-simple-time input {
width: 50px;
text-align: center;
}
.glm-simple-time span {
font-size: 12px;
color: #64748b;
}
.glm-simple-status {
font-size: 13px;
margin-bottom: 10px;
padding: 8px;
background: #f1f5f9;
border-radius: 8px;
text-align: center;
}
.glm-simple-actions {
display: flex;
gap: 8px;
}
.glm-simple-btn {
flex: 1;
padding: 8px 12px;
border: none;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
color: white;
background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
}
.glm-simple-btn.secondary {
color: #475569;
background: #e2e8f0;
}
.glm-simple-log {
margin-top: 10px;
max-height: 100px;
overflow: auto;
font-size: 11px;
color: #334155;
background: #f8fafc;
border-radius: 8px;
padding: 6px 8px;
}
`;
document.head.appendChild(style);
}
function buildPanel() {
if (document.getElementById('glm-simple-panel')) return;
const panel = document.createElement('div');
panel.id = 'glm-simple-panel';
panel.innerHTML = `
就绪
`;
document.body.appendChild(panel);
// 绑定事件
const planEl = document.getElementById('glm-simple-plan');
const cycleEl = document.getElementById('glm-simple-cycle');
const hourEl = document.getElementById('glm-simple-hour');
const minEl = document.getElementById('glm-simple-minute');
const secEl = document.getElementById('glm-simple-second');
planEl.value = config.targetPlan;
cycleEl.value = config.billingCycle;
hourEl.value = config.targetHour;
minEl.value = config.targetMinute;
secEl.value = config.targetSecond || 0;
planEl.addEventListener('change', () => {
config.targetPlan = planEl.value;
handleConfigChange();
});
cycleEl.addEventListener('change', () => {
config.billingCycle = cycleEl.value;
handleConfigChange();
});
hourEl.addEventListener('change', () => {
config.targetHour = Math.max(0, Math.min(23, Number(hourEl.value) || 0));
hourEl.value = config.targetHour;
handleConfigChange();
});
minEl.addEventListener('change', () => {
config.targetMinute = Math.max(0, Math.min(59, Number(minEl.value) || 0));
minEl.value = config.targetMinute;
handleConfigChange();
});
secEl.addEventListener('change', () => {
config.targetSecond = Math.max(0, Math.min(59, Number(secEl.value) || 0));
secEl.value = config.targetSecond;
handleConfigChange();
});
document.getElementById('glm-simple-start').addEventListener('click', startWatching);
document.getElementById('glm-simple-stop').addEventListener('click', stopWatching);
document.getElementById('glm-simple-reset').addEventListener('click', resetClicked);
}
function bootstrap() {
injectStyles();
buildPanel();
log('脚本已加载');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {
bootstrap();
}
})();