Utoljára aktív 1776218155

111.js Eredeti
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('<', '&lt;')
186 .replaceAll('>', '&gt;')
187 .replaceAll('"', '&quot;')
188 .replaceAll("'", '&#39;');
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})();