111.js
· 25 KiB · JavaScript
Eredeti
// ==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 = `<div>[${time}] ${escapeHtml(msg)}</div>` + 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 = `
<div class="glm-simple-head">
<div class="glm-simple-title">GLM 准点点击助手</div>
</div>
<div class="glm-simple-body">
<div class="glm-simple-row">
<div class="glm-simple-field">
<label>套餐</label>
<select id="glm-simple-plan">
<option value="Lite">Lite</option>
<option value="Pro">Pro</option>
<option value="Max">Max</option>
</select>
</div>
<div class="glm-simple-field">
<label>周期</label>
<select id="glm-simple-cycle">
<option value="month">连续包月</option>
<option value="quarter">连续包季</option>
<option value="year">连续包年</option>
</select>
</div>
</div>
<div class="glm-simple-row glm-simple-time">
<div class="glm-simple-field">
<label>时</label>
<input id="glm-simple-hour" type="number" min="0" max="23" />
</div>
<span>:</span>
<div class="glm-simple-field">
<label>分</label>
<input id="glm-simple-minute" type="number" min="0" max="59" />
</div>
<span>:</span>
<div class="glm-simple-field">
<label>秒</label>
<input id="glm-simple-second" type="number" min="0" max="59" />
</div>
</div>
<div class="glm-simple-status" id="glm-simple-status">就绪</div>
<div class="glm-simple-actions">
<button class="glm-simple-btn" id="glm-simple-start" type="button">开始监听</button>
<button class="glm-simple-btn secondary" id="glm-simple-stop" type="button">停止</button>
<button class="glm-simple-btn secondary" id="glm-simple-reset" type="button">重置</button>
</div>
<div class="glm-simple-log" id="glm-simple-log"></div>
</div>
`;
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();
}
})();
| 1 | // ==UserScript== |
| 2 | // @name GLM Coding 抢购助手 (简化版) |
| 3 | // @namespace http://tampermonkey.net/ |
| 4 | // @version 1.2 |
| 5 | // @description 准点自动点击指定套餐按钮,仅点击不处理后续流程。 |
| 6 | // @author Codex |
| 7 | // @match *://bigmodel.cn/glm-coding* |
| 8 | // @match *://bigmodel.cn/usercenter/glm-coding* |
| 9 | // @grant none |
| 10 | // @run-at document-start |
| 11 | // ==/UserScript== |
| 12 | |
| 13 | (function () { |
| 14 | 'use strict'; |
| 15 | |
| 16 | if (window.__autoGlmSimpleInitialized) return; |
| 17 | window.__autoGlmSimpleInitialized = true; |
| 18 | |
| 19 | // ========================================== |
| 20 | // 网络拦截 - 绕过UI限制 |
| 21 | // ========================================== |
| 22 | const originalJSONParse = JSON.parse; |
| 23 | JSON.parse = function (text, reviver) { |
| 24 | let result = originalJSONParse(text, reviver); |
| 25 | |
| 26 | function deepModify(obj) { |
| 27 | if (!obj || typeof obj !== 'object') return; |
| 28 | |
| 29 | if (obj.isSoldOut === true) obj.isSoldOut = false; |
| 30 | if (obj.soldOut === true) obj.soldOut = false; |
| 31 | if (obj.disabled === true && (obj.price !== undefined || obj.productId || obj.title)) { |
| 32 | obj.disabled = false; |
| 33 | } |
| 34 | if (obj.stock === 0) obj.stock = 999; |
| 35 | |
| 36 | for (let key in obj) { |
| 37 | if (obj[key] && typeof obj[key] === 'object') { |
| 38 | deepModify(obj[key]); |
| 39 | } |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | try { |
| 44 | deepModify(result); |
| 45 | } catch (e) { |
| 46 | console.log('[Auto-GLM-Simple] JSON拦截异常:', e.message); |
| 47 | } |
| 48 | return result; |
| 49 | }; |
| 50 | |
| 51 | const originalFetch = window.fetch; |
| 52 | window.fetch = async function (...args) { |
| 53 | const response = await originalFetch.apply(this, args); |
| 54 | const contentType = response.headers.get('content-type') || ''; |
| 55 | if (contentType.includes('application/json')) { |
| 56 | const clone = response.clone(); |
| 57 | try { |
| 58 | let text = await clone.text(); |
| 59 | if (text.includes('"isSoldOut":true') || text.includes('"disabled":true') || text.includes('"soldOut":true')) { |
| 60 | console.log('[Auto-GLM-Simple] 拦截Fetch售罄数据:', args[0]); |
| 61 | text = text.replace(/"isSoldOut":true/g, '"isSoldOut":false') |
| 62 | .replace(/"disabled":true/g, '"disabled":false') |
| 63 | .replace(/"soldOut":true/g, '"soldOut":false') |
| 64 | .replace(/"stock":0/g, '"stock":999'); |
| 65 | return new Response(text, { |
| 66 | status: response.status, |
| 67 | statusText: response.statusText, |
| 68 | headers: response.headers |
| 69 | }); |
| 70 | } |
| 71 | } catch (e) { |
| 72 | console.log('[Auto-GLM-Simple] Fetch拦截异常:', e.message); |
| 73 | } |
| 74 | } |
| 75 | return response; |
| 76 | }; |
| 77 | |
| 78 | const originalXHROpen = XMLHttpRequest.prototype.open; |
| 79 | const originalXHRSend = XMLHttpRequest.prototype.send; |
| 80 | |
| 81 | XMLHttpRequest.prototype.open = function (method, url, ...rest) { |
| 82 | this._reqUrl = url; |
| 83 | return originalXHROpen.call(this, method, url, ...rest); |
| 84 | }; |
| 85 | |
| 86 | XMLHttpRequest.prototype.send = function (...args) { |
| 87 | this.addEventListener('readystatechange', function () { |
| 88 | if (this.readyState === 4 && this.status === 200) { |
| 89 | const contentType = this.getResponseHeader('content-type') || ''; |
| 90 | if (contentType.includes('application/json')) { |
| 91 | try { |
| 92 | let text = this.responseText; |
| 93 | if (text.includes('"isSoldOut":true') || text.includes('"disabled":true') || text.includes('"soldOut":true')) { |
| 94 | console.log('[Auto-GLM-Simple] 拦截XHR售罄数据:', this._reqUrl); |
| 95 | text = text.replace(/"isSoldOut":true/g, '"isSoldOut":false') |
| 96 | .replace(/"disabled":true/g, '"disabled":false') |
| 97 | .replace(/"soldOut":true/g, '"soldOut":false') |
| 98 | .replace(/"stock":0/g, '"stock":999'); |
| 99 | Object.defineProperty(this, 'responseText', { get: function () { return text; } }); |
| 100 | Object.defineProperty(this, 'response', { get: function () { return JSON.parse(text); } }); |
| 101 | } |
| 102 | } catch (e) { |
| 103 | console.log('[Auto-GLM-Simple] XHR拦截异常:', e.message); |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | }); |
| 108 | originalXHRSend.apply(this, args); |
| 109 | }; |
| 110 | |
| 111 | console.log('[Auto-GLM-Simple] 网络拦截器已注册'); |
| 112 | |
| 113 | // ========================================== |
| 114 | // 简化配置 |
| 115 | // ========================================== |
| 116 | const STORAGE_KEY = 'glm-simple-config'; |
| 117 | const WATCH_GRACE_MS = 5 * 60 * 1000; |
| 118 | const CYCLE_SETTLE_MS = 350; |
| 119 | const SECOND_CLICK_DELAY_MS = 90; |
| 120 | const DIALOG_RETRY_BASE_DELAY_MS = 500; |
| 121 | const DIALOG_RETRY_RANDOM_MS = 500; |
| 122 | const PRODUCT_MAP = { |
| 123 | Lite: { month: 'product-02434c', quarter: 'product-b8ea38', year: 'product-70a804' }, |
| 124 | Pro: { month: 'product-1df3e1', quarter: 'product-fef82f', year: 'product-5643e6' }, |
| 125 | Max: { month: 'product-2fc421', quarter: 'product-5d3a03', year: 'product-d46f8b' } |
| 126 | }; |
| 127 | const CYCLE_LABELS = { month: '连续包月', quarter: '连续包季', year: '连续包年' }; |
| 128 | |
| 129 | const DEFAULT_CONFIG = { |
| 130 | targetPlan: 'Pro', |
| 131 | billingCycle: 'quarter', |
| 132 | targetHour: 10, |
| 133 | targetMinute: 0, |
| 134 | targetSecond: 0, |
| 135 | autoStart: false |
| 136 | }; |
| 137 | |
| 138 | let config = loadConfig(); |
| 139 | let tickTimer = null; |
| 140 | let isWatching = false; |
| 141 | let hasClicked = false; |
| 142 | let isClicking = false; |
| 143 | let targetTimestamp = 0; |
| 144 | let lastCycleSwitchAt = 0; |
| 145 | let lastStatusText = ''; |
| 146 | |
| 147 | function clampNumber(value, min, max, fallback) { |
| 148 | const next = Number(value); |
| 149 | if (!Number.isFinite(next)) return fallback; |
| 150 | return Math.min(max, Math.max(min, Math.floor(next))); |
| 151 | } |
| 152 | |
| 153 | function sanitizeConfig(raw = {}) { |
| 154 | return { |
| 155 | targetPlan: PRODUCT_MAP[raw.targetPlan] ? raw.targetPlan : DEFAULT_CONFIG.targetPlan, |
| 156 | billingCycle: CYCLE_LABELS[raw.billingCycle] ? raw.billingCycle : DEFAULT_CONFIG.billingCycle, |
| 157 | targetHour: clampNumber(raw.targetHour, 0, 23, DEFAULT_CONFIG.targetHour), |
| 158 | targetMinute: clampNumber(raw.targetMinute, 0, 59, DEFAULT_CONFIG.targetMinute), |
| 159 | targetSecond: clampNumber(raw.targetSecond, 0, 59, DEFAULT_CONFIG.targetSecond), |
| 160 | autoStart: Boolean(raw.autoStart) |
| 161 | }; |
| 162 | } |
| 163 | |
| 164 | function loadConfig() { |
| 165 | try { |
| 166 | const raw = localStorage.getItem(STORAGE_KEY); |
| 167 | if (!raw) return { ...DEFAULT_CONFIG }; |
| 168 | return { ...DEFAULT_CONFIG, ...sanitizeConfig(JSON.parse(raw)) }; |
| 169 | } catch { |
| 170 | return { ...DEFAULT_CONFIG }; |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | function saveConfig() { |
| 175 | try { |
| 176 | localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); |
| 177 | } catch (e) { |
| 178 | console.warn('[Auto-GLM-Simple] 保存配置失败:', e); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | function escapeHtml(text) { |
| 183 | return String(text) |
| 184 | .replaceAll('&', '&') |
| 185 | .replaceAll('<', '<') |
| 186 | .replaceAll('>', '>') |
| 187 | .replaceAll('"', '"') |
| 188 | .replaceAll("'", '''); |
| 189 | } |
| 190 | |
| 191 | function log(msg) { |
| 192 | console.log(`[Auto-GLM-Simple] ${msg}`); |
| 193 | const logBox = document.getElementById('glm-simple-log'); |
| 194 | if (logBox) { |
| 195 | const time = new Date().toLocaleTimeString(); |
| 196 | logBox.innerHTML = `<div>[${time}] ${escapeHtml(msg)}</div>` + logBox.innerHTML; |
| 197 | if (logBox.children.length > 50) logBox.lastElementChild.remove(); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | function updateStatus(text) { |
| 202 | if (text === lastStatusText) return; |
| 203 | lastStatusText = text; |
| 204 | const statusEl = document.getElementById('glm-simple-status'); |
| 205 | if (statusEl) statusEl.textContent = text; |
| 206 | } |
| 207 | |
| 208 | function normalizeText(text) { |
| 209 | return String(text || '').replace(/\s+/g, '').trim(); |
| 210 | } |
| 211 | |
| 212 | function getTargetDate(now = new Date()) { |
| 213 | return new Date( |
| 214 | now.getFullYear(), |
| 215 | now.getMonth(), |
| 216 | now.getDate(), |
| 217 | config.targetHour, |
| 218 | config.targetMinute, |
| 219 | config.targetSecond || 0, |
| 220 | 0 |
| 221 | ); |
| 222 | } |
| 223 | |
| 224 | function refreshTargetTimestamp() { |
| 225 | targetTimestamp = getTargetDate().getTime(); |
| 226 | } |
| 227 | |
| 228 | function sleep(ms) { |
| 229 | return new Promise((resolve) => window.setTimeout(resolve, ms)); |
| 230 | } |
| 231 | |
| 232 | function isVisibleElement(node) { |
| 233 | if (!node || !node.isConnected) return false; |
| 234 | const rect = node.getBoundingClientRect(); |
| 235 | return rect.width > 0 && rect.height > 0; |
| 236 | } |
| 237 | |
| 238 | // ========================================== |
| 239 | // 核心逻辑:准点点击 |
| 240 | // ========================================== |
| 241 | function findCycleTab(cycle) { |
| 242 | const label = CYCLE_LABELS[cycle]; |
| 243 | if (!label) return null; |
| 244 | return Array.from(document.querySelectorAll('.switch-tab-item')) |
| 245 | .find((node) => normalizeText(node.textContent).includes(normalizeText(label))) || null; |
| 246 | } |
| 247 | |
| 248 | function ensureBillingCycleSelected() { |
| 249 | const tab = findCycleTab(config.billingCycle); |
| 250 | if (!tab) return false; |
| 251 | if (tab.classList.contains('active')) return true; |
| 252 | if (Date.now() - lastCycleSwitchAt < CYCLE_SETTLE_MS) return false; |
| 253 | lastCycleSwitchAt = Date.now(); |
| 254 | dispatchRealClick(tab.querySelector('.switch-tab-item-content') || tab); |
| 255 | return false; |
| 256 | } |
| 257 | |
| 258 | function findPlanCard(planName) { |
| 259 | return Array.from(document.querySelectorAll('.package-card-box .package-card')) |
| 260 | .filter(isVisibleElement) |
| 261 | .find((card) => { |
| 262 | const title = card.querySelector('.package-card-title .font-prompt'); |
| 263 | return title && normalizeText(title.textContent) === normalizeText(planName); |
| 264 | }) || null; |
| 265 | } |
| 266 | |
| 267 | function findBuyButton(card) { |
| 268 | if (!card) return null; |
| 269 | return Array.from(card.querySelectorAll('button.buy-btn, .package-card-btn-box button')) |
| 270 | .find(isVisibleElement) || null; |
| 271 | } |
| 272 | |
| 273 | function getButtonState(button) { |
| 274 | if (!button) return { text: '', disabled: true }; |
| 275 | return { |
| 276 | text: normalizeText(button.textContent), |
| 277 | disabled: button.disabled |
| 278 | || button.getAttribute('aria-disabled') === 'true' |
| 279 | || button.classList.contains('is-disabled') |
| 280 | || button.classList.contains('disabled') |
| 281 | }; |
| 282 | } |
| 283 | |
| 284 | function temporarilyEnableButton(button) { |
| 285 | if (!button) return () => {}; |
| 286 | const previous = { |
| 287 | disabled: button.disabled, |
| 288 | disabledAttr: button.getAttribute('disabled'), |
| 289 | ariaDisabled: button.getAttribute('aria-disabled'), |
| 290 | className: button.className |
| 291 | }; |
| 292 | button.disabled = false; |
| 293 | button.removeAttribute('disabled'); |
| 294 | button.setAttribute('aria-disabled', 'false'); |
| 295 | button.classList.remove('is-disabled', 'disabled'); |
| 296 | return () => { |
| 297 | button.disabled = previous.disabled; |
| 298 | if (previous.disabledAttr === null) { |
| 299 | button.removeAttribute('disabled'); |
| 300 | } else { |
| 301 | button.setAttribute('disabled', previous.disabledAttr); |
| 302 | } |
| 303 | if (previous.ariaDisabled === null) { |
| 304 | button.removeAttribute('aria-disabled'); |
| 305 | } else { |
| 306 | button.setAttribute('aria-disabled', previous.ariaDisabled); |
| 307 | } |
| 308 | button.className = previous.className; |
| 309 | }; |
| 310 | } |
| 311 | |
| 312 | function findPayDialogRoot() { |
| 313 | return document.querySelector('.white-mask-bg .pay-dialog, .white-mask-bg .scan-code-box, .confirm-pay-btn, .scan-qrcode-box'); |
| 314 | } |
| 315 | |
| 316 | function isRealPayDialog(root) { |
| 317 | if (!root) return false; |
| 318 | const priceEl = root.querySelector('.scan-qrcode-box .price-icon + span'); |
| 319 | if (priceEl) { |
| 320 | const priceText = priceEl.textContent?.trim(); |
| 321 | return Boolean(priceText && priceText.length > 0); |
| 322 | } |
| 323 | return Boolean(root.querySelector('.confirm-pay-btn')); |
| 324 | } |
| 325 | |
| 326 | function findFailureDialog() { |
| 327 | const busyDialog = document.querySelector('.el-dialog__wrapper .empty-data-wrap'); |
| 328 | if (busyDialog?.textContent?.includes('购买人数较多')) { |
| 329 | return { type: 'busy', closeBtn: busyDialog.closest('.el-dialog')?.querySelector('.el-dialog__headerbtn') }; |
| 330 | } |
| 331 | const payRoot = document.querySelector('.white-mask-bg .pay-dialog, .white-mask-bg .scan-code-box'); |
| 332 | if (payRoot && !isRealPayDialog(payRoot)) { |
| 333 | return { type: 'empty-price', closeBtn: payRoot.querySelector('.el-dialog__headerbtn, .confirm-pay-btn') }; |
| 334 | } |
| 335 | return null; |
| 336 | } |
| 337 | |
| 338 | function dispatchMouseLikeEvent(target, type, init) { |
| 339 | const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent; |
| 340 | target.dispatchEvent(new EventCtor(type, init)); |
| 341 | } |
| 342 | |
| 343 | function dispatchRealClick(target) { |
| 344 | if (!target || !target.isConnected) return false; |
| 345 | try { |
| 346 | target.scrollIntoView({ block: 'center', inline: 'center', behavior: 'auto' }); |
| 347 | } catch {} |
| 348 | try { |
| 349 | target.focus({ preventScroll: true }); |
| 350 | } catch {} |
| 351 | |
| 352 | const rect = target.getBoundingClientRect(); |
| 353 | const eventInit = { |
| 354 | bubbles: true, |
| 355 | cancelable: true, |
| 356 | composed: true, |
| 357 | view: window, |
| 358 | clientX: rect.left + Math.max(1, rect.width / 2), |
| 359 | clientY: rect.top + Math.max(1, rect.height / 2) |
| 360 | }; |
| 361 | ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => { |
| 362 | dispatchMouseLikeEvent(target, type, eventInit); |
| 363 | }); |
| 364 | target.click(); |
| 365 | return true; |
| 366 | } |
| 367 | |
| 368 | function getNextTickDelay(now = Date.now()) { |
| 369 | const diff = targetTimestamp - now; |
| 370 | if (diff > 60_000) return 1000; |
| 371 | if (diff > 10_000) return 400; |
| 372 | if (diff > 3_000) return 120; |
| 373 | if (diff > 0) return 40; |
| 374 | if (diff > -WATCH_GRACE_MS) return 60; |
| 375 | return 250; |
| 376 | } |
| 377 | |
| 378 | function scheduleNextTick(delay = getNextTickDelay()) { |
| 379 | if (!isWatching) return; |
| 380 | if (tickTimer) clearTimeout(tickTimer); |
| 381 | tickTimer = window.setTimeout(() => { |
| 382 | tickTimer = null; |
| 383 | void tick(); |
| 384 | }, delay); |
| 385 | } |
| 386 | |
| 387 | function isTargetWindowExpired(now = Date.now()) { |
| 388 | return now > targetTimestamp + WATCH_GRACE_MS; |
| 389 | } |
| 390 | |
| 391 | function getCountdown() { |
| 392 | const diff = targetTimestamp - Date.now(); |
| 393 | if (diff <= 0) return null; |
| 394 | const hours = Math.floor(diff / (1000 * 60 * 60)); |
| 395 | const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); |
| 396 | const seconds = Math.floor((diff % (1000 * 60)) / 1000); |
| 397 | return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; |
| 398 | } |
| 399 | |
| 400 | async function triggerBuyButton(button) { |
| 401 | if (!button || isClicking) return false; |
| 402 | isClicking = true; |
| 403 | let restoreButton = null; |
| 404 | const beforeText = getButtonState(button).text; |
| 405 | |
| 406 | try { |
| 407 | const { disabled } = getButtonState(button); |
| 408 | if (disabled) { |
| 409 | restoreButton = temporarilyEnableButton(button); |
| 410 | log('到点后临时解除按钮禁用,准备触发购买按钮'); |
| 411 | } |
| 412 | |
| 413 | dispatchRealClick(button); |
| 414 | await sleep(SECOND_CLICK_DELAY_MS); |
| 415 | |
| 416 | const dialogOpened = Boolean(findPayDialogRoot()); |
| 417 | const buttonChanged = !button.isConnected || getButtonState(button).text !== beforeText; |
| 418 | if (!dialogOpened && !buttonChanged && button.isConnected) { |
| 419 | dispatchRealClick(button); |
| 420 | } |
| 421 | |
| 422 | return true; |
| 423 | } finally { |
| 424 | if (restoreButton) { |
| 425 | window.setTimeout(() => { |
| 426 | restoreButton(); |
| 427 | }, 1200); |
| 428 | } |
| 429 | isClicking = false; |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | async function tick() { |
| 434 | if (!isWatching || hasClicked) return; |
| 435 | |
| 436 | if (isTargetWindowExpired()) { |
| 437 | stopWatching({ statusText: '已过时间', logMessage: '已超过目标时间窗口,监听结束' }); |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | const countdown = getCountdown(); |
| 442 | if (countdown) { |
| 443 | updateStatus(`倒计时 ${countdown}`); |
| 444 | } else { |
| 445 | updateStatus('已到点,等待按钮就绪'); |
| 446 | } |
| 447 | |
| 448 | const cycleReady = ensureBillingCycleSelected(); |
| 449 | if (!cycleReady) { |
| 450 | scheduleNextTick(); |
| 451 | return; |
| 452 | } |
| 453 | if (Date.now() - lastCycleSwitchAt < CYCLE_SETTLE_MS) { |
| 454 | scheduleNextTick(); |
| 455 | return; |
| 456 | } |
| 457 | |
| 458 | const card = findPlanCard(config.targetPlan); |
| 459 | const button = findBuyButton(card); |
| 460 | |
| 461 | if (!button) { |
| 462 | if (Date.now() >= targetTimestamp) updateStatus('已到点,等待购买按钮出现'); |
| 463 | scheduleNextTick(); |
| 464 | return; |
| 465 | } |
| 466 | |
| 467 | if (Date.now() < targetTimestamp) { |
| 468 | scheduleNextTick(); |
| 469 | return; |
| 470 | } |
| 471 | |
| 472 | const clicked = await triggerBuyButton(button); |
| 473 | if (clicked) { |
| 474 | const failureDialog = findFailureDialog(); |
| 475 | if (failureDialog) { |
| 476 | log(`出现失败弹窗(${failureDialog.type}),正在关闭...`); |
| 477 | dispatchRealClick(failureDialog.closeBtn); |
| 478 | await sleep(getDialogRetryDelay()); |
| 479 | log(`重新点击购买按钮`); |
| 480 | scheduleNextTick(); |
| 481 | return; |
| 482 | } |
| 483 | hasClicked = true; |
| 484 | const timeStr = `${config.targetHour}:${String(config.targetMinute).padStart(2, '0')}:${String(config.targetSecond || 0).padStart(2, '0')}`; |
| 485 | log(`[${timeStr}] 已点击 ${config.targetPlan} ${CYCLE_LABELS[config.billingCycle]} 购买按钮`); |
| 486 | stopWatching({ statusText: '已点击,等待后续操作', logMessage: '' }); |
| 487 | return; |
| 488 | } |
| 489 | |
| 490 | scheduleNextTick(); |
| 491 | } |
| 492 | |
| 493 | function stopWatching(options = {}) { |
| 494 | const { statusText = '已停止', logMessage = '已停止' } = options; |
| 495 | if (tickTimer) { |
| 496 | clearTimeout(tickTimer); |
| 497 | tickTimer = null; |
| 498 | } |
| 499 | isWatching = false; |
| 500 | if (logMessage) log(logMessage); |
| 501 | updateStatus(statusText); |
| 502 | } |
| 503 | |
| 504 | function getDialogRetryDelay() { |
| 505 | return DIALOG_RETRY_BASE_DELAY_MS + Math.floor(Math.random() * DIALOG_RETRY_RANDOM_MS); |
| 506 | } |
| 507 | |
| 508 | function startWatching() { |
| 509 | if (isWatching) return; |
| 510 | |
| 511 | refreshTargetTimestamp(); |
| 512 | if (isTargetWindowExpired()) { |
| 513 | log('已超过目标时间,无法启动监听'); |
| 514 | updateStatus('已过时间'); |
| 515 | return; |
| 516 | } |
| 517 | |
| 518 | isWatching = true; |
| 519 | hasClicked = false; |
| 520 | isClicking = false; |
| 521 | lastCycleSwitchAt = 0; |
| 522 | const timeStr = `${config.targetHour}:${String(config.targetMinute).padStart(2, '0')}:${String(config.targetSecond || 0).padStart(2, '0')}`; |
| 523 | log(`开始监听,目标时间: ${timeStr}`); |
| 524 | updateStatus(getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪'); |
| 525 | scheduleNextTick(0); |
| 526 | } |
| 527 | |
| 528 | function resetClicked() { |
| 529 | hasClicked = false; |
| 530 | isClicking = false; |
| 531 | log(isWatching ? '已重置点击状态,继续监听' : '已重置点击状态'); |
| 532 | updateStatus(isWatching ? (getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪') : '就绪'); |
| 533 | if (isWatching) scheduleNextTick(0); |
| 534 | } |
| 535 | |
| 536 | function handleConfigChange() { |
| 537 | saveConfig(); |
| 538 | if (!isWatching) return; |
| 539 | refreshTargetTimestamp(); |
| 540 | hasClicked = false; |
| 541 | isClicking = false; |
| 542 | lastCycleSwitchAt = 0; |
| 543 | log('配置已更新,监听目标时间已重新对齐'); |
| 544 | updateStatus(getCountdown() ? `倒计时 ${getCountdown()}` : '已到点,等待按钮就绪'); |
| 545 | scheduleNextTick(0); |
| 546 | } |
| 547 | |
| 548 | // ========================================== |
| 549 | // UI |
| 550 | // ========================================== |
| 551 | function injectStyles() { |
| 552 | if (document.getElementById('glm-simple-style')) return; |
| 553 | const style = document.createElement('style'); |
| 554 | style.id = 'glm-simple-style'; |
| 555 | style.textContent = ` |
| 556 | #glm-simple-panel { |
| 557 | position: fixed; |
| 558 | left: 20px; |
| 559 | bottom: 20px; |
| 560 | width: 300px; |
| 561 | z-index: 999999; |
| 562 | border-radius: 16px; |
| 563 | overflow: hidden; |
| 564 | background: linear-gradient(135deg, #10233f 0%, #1d4ed8 64%, #38bdf8 100%); |
| 565 | box-shadow: 0 24px 64px -28px rgba(16, 35, 63, 0.45); |
| 566 | font-family: "SF Pro Display", "PingFang SC", "Segoe UI", sans-serif; |
| 567 | color: #eff6ff; |
| 568 | } |
| 569 | #glm-simple-panel * { box-sizing: border-box; } |
| 570 | .glm-simple-head { |
| 571 | padding: 14px 16px; |
| 572 | } |
| 573 | .glm-simple-title { |
| 574 | font-size: 14px; |
| 575 | font-weight: 700; |
| 576 | } |
| 577 | .glm-simple-body { |
| 578 | padding: 12px 14px; |
| 579 | background: rgba(255,255,255,0.95); |
| 580 | color: #1e293b; |
| 581 | } |
| 582 | .glm-simple-row { |
| 583 | display: flex; |
| 584 | gap: 8px; |
| 585 | margin-bottom: 10px; |
| 586 | } |
| 587 | .glm-simple-field { |
| 588 | flex: 1; |
| 589 | } |
| 590 | .glm-simple-field label { |
| 591 | display: block; |
| 592 | font-size: 11px; |
| 593 | font-weight: 600; |
| 594 | color: #475569; |
| 595 | margin-bottom: 4px; |
| 596 | } |
| 597 | .glm-simple-field select, |
| 598 | .glm-simple-field input { |
| 599 | width: 100%; |
| 600 | padding: 6px 8px; |
| 601 | border: 1px solid #cbd5e1; |
| 602 | border-radius: 8px; |
| 603 | font-size: 13px; |
| 604 | background: #f8fafc; |
| 605 | } |
| 606 | .glm-simple-time { |
| 607 | display: flex; |
| 608 | align-items: center; |
| 609 | gap: 4px; |
| 610 | } |
| 611 | .glm-simple-time input { |
| 612 | width: 50px; |
| 613 | text-align: center; |
| 614 | } |
| 615 | .glm-simple-time span { |
| 616 | font-size: 12px; |
| 617 | color: #64748b; |
| 618 | } |
| 619 | .glm-simple-status { |
| 620 | font-size: 13px; |
| 621 | margin-bottom: 10px; |
| 622 | padding: 8px; |
| 623 | background: #f1f5f9; |
| 624 | border-radius: 8px; |
| 625 | text-align: center; |
| 626 | } |
| 627 | .glm-simple-actions { |
| 628 | display: flex; |
| 629 | gap: 8px; |
| 630 | } |
| 631 | .glm-simple-btn { |
| 632 | flex: 1; |
| 633 | padding: 8px 12px; |
| 634 | border: none; |
| 635 | border-radius: 10px; |
| 636 | font-size: 13px; |
| 637 | font-weight: 600; |
| 638 | cursor: pointer; |
| 639 | color: white; |
| 640 | background: linear-gradient(135deg, #1d4ed8, #0ea5e9); |
| 641 | } |
| 642 | .glm-simple-btn.secondary { |
| 643 | color: #475569; |
| 644 | background: #e2e8f0; |
| 645 | } |
| 646 | .glm-simple-log { |
| 647 | margin-top: 10px; |
| 648 | max-height: 100px; |
| 649 | overflow: auto; |
| 650 | font-size: 11px; |
| 651 | color: #334155; |
| 652 | background: #f8fafc; |
| 653 | border-radius: 8px; |
| 654 | padding: 6px 8px; |
| 655 | } |
| 656 | `; |
| 657 | document.head.appendChild(style); |
| 658 | } |
| 659 | |
| 660 | function buildPanel() { |
| 661 | if (document.getElementById('glm-simple-panel')) return; |
| 662 | |
| 663 | const panel = document.createElement('div'); |
| 664 | panel.id = 'glm-simple-panel'; |
| 665 | panel.innerHTML = ` |
| 666 | <div class="glm-simple-head"> |
| 667 | <div class="glm-simple-title">GLM 准点点击助手</div> |
| 668 | </div> |
| 669 | <div class="glm-simple-body"> |
| 670 | <div class="glm-simple-row"> |
| 671 | <div class="glm-simple-field"> |
| 672 | <label>套餐</label> |
| 673 | <select id="glm-simple-plan"> |
| 674 | <option value="Lite">Lite</option> |
| 675 | <option value="Pro">Pro</option> |
| 676 | <option value="Max">Max</option> |
| 677 | </select> |
| 678 | </div> |
| 679 | <div class="glm-simple-field"> |
| 680 | <label>周期</label> |
| 681 | <select id="glm-simple-cycle"> |
| 682 | <option value="month">连续包月</option> |
| 683 | <option value="quarter">连续包季</option> |
| 684 | <option value="year">连续包年</option> |
| 685 | </select> |
| 686 | </div> |
| 687 | </div> |
| 688 | <div class="glm-simple-row glm-simple-time"> |
| 689 | <div class="glm-simple-field"> |
| 690 | <label>时</label> |
| 691 | <input id="glm-simple-hour" type="number" min="0" max="23" /> |
| 692 | </div> |
| 693 | <span>:</span> |
| 694 | <div class="glm-simple-field"> |
| 695 | <label>分</label> |
| 696 | <input id="glm-simple-minute" type="number" min="0" max="59" /> |
| 697 | </div> |
| 698 | <span>:</span> |
| 699 | <div class="glm-simple-field"> |
| 700 | <label>秒</label> |
| 701 | <input id="glm-simple-second" type="number" min="0" max="59" /> |
| 702 | </div> |
| 703 | </div> |
| 704 | <div class="glm-simple-status" id="glm-simple-status">就绪</div> |
| 705 | <div class="glm-simple-actions"> |
| 706 | <button class="glm-simple-btn" id="glm-simple-start" type="button">开始监听</button> |
| 707 | <button class="glm-simple-btn secondary" id="glm-simple-stop" type="button">停止</button> |
| 708 | <button class="glm-simple-btn secondary" id="glm-simple-reset" type="button">重置</button> |
| 709 | </div> |
| 710 | <div class="glm-simple-log" id="glm-simple-log"></div> |
| 711 | </div> |
| 712 | `; |
| 713 | document.body.appendChild(panel); |
| 714 | |
| 715 | // 绑定事件 |
| 716 | const planEl = document.getElementById('glm-simple-plan'); |
| 717 | const cycleEl = document.getElementById('glm-simple-cycle'); |
| 718 | const hourEl = document.getElementById('glm-simple-hour'); |
| 719 | const minEl = document.getElementById('glm-simple-minute'); |
| 720 | const secEl = document.getElementById('glm-simple-second'); |
| 721 | |
| 722 | planEl.value = config.targetPlan; |
| 723 | cycleEl.value = config.billingCycle; |
| 724 | hourEl.value = config.targetHour; |
| 725 | minEl.value = config.targetMinute; |
| 726 | secEl.value = config.targetSecond || 0; |
| 727 | |
| 728 | planEl.addEventListener('change', () => { |
| 729 | config.targetPlan = planEl.value; |
| 730 | handleConfigChange(); |
| 731 | }); |
| 732 | cycleEl.addEventListener('change', () => { |
| 733 | config.billingCycle = cycleEl.value; |
| 734 | handleConfigChange(); |
| 735 | }); |
| 736 | hourEl.addEventListener('change', () => { |
| 737 | config.targetHour = Math.max(0, Math.min(23, Number(hourEl.value) || 0)); |
| 738 | hourEl.value = config.targetHour; |
| 739 | handleConfigChange(); |
| 740 | }); |
| 741 | minEl.addEventListener('change', () => { |
| 742 | config.targetMinute = Math.max(0, Math.min(59, Number(minEl.value) || 0)); |
| 743 | minEl.value = config.targetMinute; |
| 744 | handleConfigChange(); |
| 745 | }); |
| 746 | secEl.addEventListener('change', () => { |
| 747 | config.targetSecond = Math.max(0, Math.min(59, Number(secEl.value) || 0)); |
| 748 | secEl.value = config.targetSecond; |
| 749 | handleConfigChange(); |
| 750 | }); |
| 751 | |
| 752 | document.getElementById('glm-simple-start').addEventListener('click', startWatching); |
| 753 | document.getElementById('glm-simple-stop').addEventListener('click', stopWatching); |
| 754 | document.getElementById('glm-simple-reset').addEventListener('click', resetClicked); |
| 755 | } |
| 756 | |
| 757 | function bootstrap() { |
| 758 | injectStyles(); |
| 759 | buildPanel(); |
| 760 | log('脚本已加载'); |
| 761 | } |
| 762 | |
| 763 | if (document.readyState === 'loading') { |
| 764 | document.addEventListener('DOMContentLoaded', bootstrap); |
| 765 | } else { |
| 766 | bootstrap(); |
| 767 | } |
| 768 | })(); |