Last active 1775530113

Revision 5f0b0d02406c06ee320c76d3e985f7e835ce91cd

glm.js Raw
1// ==UserScript==
2// @name 智谱 GLM Coding 终极抢购助手 (全自动版)
3// @namespace http://tampermonkey.net/
4// @version 3.2
5// @description 全自动抢购:自动重试 preview + check 双重校验 + 错误弹窗自动恢复 + 主动模式 + 定时触发
6// @author Assistant
7// @match *://www.bigmodel.cn/*
8// @run-at document-start
9// @grant none
10// ==/UserScript==
11
12(function () {
13 'use strict';
14
15 // ======================== 配置 ========================
16 const CFG = {
17 delay: 100,
18 maxRetry: 300,
19 PREVIEW: '/api/biz/pay/preview',
20 CHECK: '/api/biz/pay/check',
21 };
22
23 // ======================== 全局状态 ========================
24 const S = {
25 status: 'idle',
26 count: 0,
27 bizId: null,
28 captured: null,
29 cache: null,
30 lastSuccess: null, // 最近一次成功的响应,用于弹窗恢复
31 proactive: false,
32 timerId: null,
33 logs: [],
34 };
35
36 let stopRequested = false;
37 let recovering = false;
38 let recoveryAttempts = 0;
39
40 // ======================== 工具函数 ========================
41 const sleep = ms => new Promise(r => setTimeout(r, ms));
42 const ts = () => new Date().toLocaleTimeString('zh-CN', { hour12: false });
43
44 function log(msg) {
45 S.logs.push(`${ts()} ${msg}`);
46 if (S.logs.length > 80) S.logs.shift();
47 console.log(`[GLM抢购] ${msg}`);
48 refreshLog();
49 }
50
51 function extractHeaders(h) {
52 const o = {};
53 if (!h) return o;
54 if (h instanceof Headers) h.forEach((v, k) => (o[k] = v));
55 else if (Array.isArray(h)) h.forEach(([k, v]) => (o[k] = v));
56 else Object.entries(h).forEach(([k, v]) => (o[k] = v));
57 return o;
58 }
59
60 // ======================== 一、JSON.parse 深层篡改 (UI 补丁) ========================
61 const _parse = JSON.parse;
62 JSON.parse = function (text, reviver) {
63 let result = _parse(text, reviver);
64 try {
65 (function fix(obj) {
66 if (!obj || typeof obj !== 'object') return;
67 if (obj.isSoldOut === true) obj.isSoldOut = false;
68 if (obj.soldOut === true) obj.soldOut = false;
69 if (obj.disabled === true && (obj.price !== undefined || obj.productId || obj.title)) obj.disabled = false;
70 if (obj.stock === 0) obj.stock = 999;
71 for (let k in obj) if (obj[k] && typeof obj[k] === 'object') fix(obj[k]);
72 })(result);
73 } catch (e) {}
74 return result;
75 };
76
77 // ======================== 二、核心重试引擎 ========================
78 const _fetch = window.fetch;
79 let _retryPromise = null;
80
81 async function retry(url, opts) {
82 if (_retryPromise) {
83 log('⏳ 合并到当前重试…');
84 return _retryPromise;
85 }
86
87 stopRequested = false;
88
89 _retryPromise = (async () => {
90 S.status = 'retrying';
91 S.count = 0;
92 refreshUI();
93
94 // 剥离 AbortSignal,防止前端超时中断我们的重试
95 const { signal, ...cleanOpts } = opts || {};
96
97 for (let i = 1; i <= CFG.maxRetry; i++) {
98 if (stopRequested) {
99 log('⏹ 重试被手动停止');
100 break;
101 }
102
103 S.count = i;
104 refreshUI();
105
106 try {
107 const resp = await _fetch(url, { ...cleanOpts, credentials: 'include' });
108 const text = await resp.text();
109 let data;
110 try { data = _parse(text); } catch { data = null; }
111
112 if (data && data.code === 200 && data.data && data.data.bizId) {
113 const bizId = data.data.bizId;
114 log(`🔑 获取到 bizId=${bizId},正在校验 check 接口…`);
115
116 // 关键校验:调用 check 接口确认 bizId 是否有效
117 try {
118 const checkUrl = `${location.origin}${CFG.CHECK}?bizId=${bizId}`;
119 const checkResp = await _fetch(checkUrl, { credentials: 'include' });
120 const checkText = await checkResp.text();
121 let checkData;
122 try { checkData = _parse(checkText); } catch { checkData = null; }
123
124 if (checkData && checkData.data === 'EXPIRE') {
125 // bizId 已过期,继续重试 preview
126 log(`#${i} bizId 已过期(EXPIRE),继续重试…`);
127 await sleep(CFG.delay);
128 continue;
129 }
130
131 // check 返回非 EXPIRE → 真正成功!
132 S.status = 'success';
133 S.bizId = bizId;
134 S.lastSuccess = { text, data };
135 log(`✅ 抢购成功! bizId=${bizId}, check 校验通过 (第${i}次)`);
136 refreshUI();
137 recoveryAttempts = 0;
138 setTimeout(autoRecover, 600);
139 return { ok: true, text, data, status: resp.status };
140 } catch (checkErr) {
141 // check 接口本身出错,也继续重试
142 log(`#${i} check 校验异常: ${checkErr.message},继续重试…`);
143 await sleep(CFG.delay);
144 continue;
145 }
146 }
147
148 const why = !data ? '非JSON响应'
149 : data.code === 555 ? '系统繁忙(555)'
150 : (data.data && data.data.bizId === null) ? '售罄(bizId=null)'
151 : `未知(code=${data.code})`;
152 if (i <= 5 || i % 20 === 0) log(`#${i} ${why}`);
153 } catch (e) {
154 if (i <= 3 || i % 20 === 0) log(`#${i} 网络错误: ${e.message}`);
155 }
156
157 await sleep(CFG.delay);
158 }
159
160 if (!stopRequested) {
161 S.status = 'failed';
162 log(`❌ 达到上限 ${CFG.maxRetry}`);
163 } else {
164 S.status = 'idle';
165 }
166 refreshUI();
167 return { ok: false };
168 })();
169
170 try { return await _retryPromise; }
171 finally { _retryPromise = null; }
172 }
173
174 // ======================== 三、错误弹窗自动恢复 ========================
175
176 /** 查找页面上可见的错误弹窗 */
177 function findErrorDialog() {
178 const selectors = [
179 '.el-dialog', '.el-message-box', '.el-dialog__wrapper',
180 '.ant-modal', '.ant-modal-wrap',
181 '[class*="modal"]', '[class*="dialog"]', '[class*="popup"]',
182 '[role="dialog"]',
183 ];
184 for (const sel of selectors) {
185 for (const el of document.querySelectorAll(sel)) {
186 // 必须可见
187 const style = window.getComputedStyle(el);
188 if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
189 if (!el.offsetParent && style.position !== 'fixed') continue;
190
191 const text = el.textContent || '';
192 if (/购买人数过多|系统繁忙|稍后再试|请重试|繁忙|失败|出错|异常/.test(text)) {
193 return el;
194 }
195 }
196 }
197 return null;
198 }
199
200 /** 关闭弹窗 */
201 function dismissDialog(dialog) {
202 // 策略 1:找关闭按钮
203 const closeSelectors = [
204 '.el-dialog__headerbtn', '.el-message-box__headerbtn',
205 '.el-dialog__close', '.ant-modal-close',
206 '[class*="close-btn"]', '[class*="closeBtn"]',
207 '[aria-label="Close"]', '[aria-label="close"]',
208 ];
209 for (const sel of closeSelectors) {
210 const btn = dialog.querySelector(sel) || document.querySelector(sel);
211 if (btn && btn.offsetParent !== null) {
212 btn.click();
213 log('🔄 点击关闭按钮');
214 return true;
215 }
216 }
217
218 // 策略 2:找弹窗内的 确定/取消/关闭 按钮
219 const btns = dialog.querySelectorAll('button, [role="button"]');
220 for (const btn of btns) {
221 const t = (btn.textContent || '').trim();
222 if (/关闭|确定|取消|知道了|OK|Cancel|Close|确认/.test(t) && t.length < 10) {
223 btn.click();
224 log(`🔄 点击 [${t}] 按钮`);
225 return true;
226 }
227 }
228
229 // 策略 3:按 Escape
230 document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', keyCode: 27, bubbles: true }));
231 document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', keyCode: 27, bubbles: true }));
232 log('🔄 发送 Escape 键');
233
234 // 策略 4:点击遮罩层
235 const masks = document.querySelectorAll('.el-overlay, .v-modal, .el-overlay-dialog, [class*="overlay"], [class*="mask"]');
236 for (const mask of masks) {
237 if (mask.offsetParent !== null || window.getComputedStyle(mask).position === 'fixed') {
238 mask.click();
239 log('🔄 点击遮罩层');
240 return true;
241 }
242 }
243
244 // 策略 5:强制隐藏
245 dialog.style.display = 'none';
246 const overlays = document.querySelectorAll('.el-overlay, .v-modal');
247 overlays.forEach(o => (o.style.display = 'none'));
248 log('🔄 强制隐藏弹窗');
249 return true;
250 }
251
252 /** 自动恢复:关闭错误弹窗 → 重新触发购买 */
253 async function autoRecover() {
254 if (recovering || recoveryAttempts >= 3) return;
255 if (!S.lastSuccess) return;
256
257 const dialog = findErrorDialog();
258 if (!dialog) return; // 没有错误弹窗,说明前端已正常处理,无需恢复
259
260 recovering = true;
261 recoveryAttempts++;
262
263 try {
264 log('🔄 检测到错误弹窗,启动自动恢复…');
265
266 // 把成功响应放入缓存,供拦截器下一次返回
267 S.cache = S.lastSuccess;
268
269 // 关闭错误弹窗
270 dismissDialog(dialog);
271 await sleep(500);
272
273 // 再次确认弹窗关了(有些弹窗有动画延迟)
274 const stillThere = findErrorDialog();
275 if (stillThere) {
276 dismissDialog(stillThere);
277 await sleep(300);
278 }
279
280 // 重新触发购买按钮 → 拦截器返回缓存响应 → 前端弹出支付二维码
281 const btn = findBuyButton();
282 if (btn) {
283 btn.click();
284 log('🖱 已自动重新点击购买按钮');
285 } else {
286 log('⚠️ 未找到购买按钮,请手动点击');
287 alert('已获取到商品!请立即手动点击购买按钮!');
288 }
289 } finally {
290 recovering = false;
291 }
292 }
293
294 /** 持续监控:每 500ms 检查一次是否有需要恢复的错误弹窗 */
295 function setupDialogWatcher() {
296 setInterval(() => {
297 if (S.lastSuccess && !recovering && recoveryAttempts < 3) {
298 const dialog = findErrorDialog();
299 if (dialog) autoRecover();
300 }
301 }, 500);
302 }
303
304 // ======================== 四、Fetch 拦截器 ========================
305 window.fetch = async function (input, init) {
306 const url = typeof input === 'string' ? input : input?.url;
307
308 if (url && url.includes(CFG.PREVIEW)) {
309 S.captured = {
310 url,
311 method: init?.method || 'POST',
312 body: init?.body,
313 headers: extractHeaders(init?.headers),
314 };
315 log('🎯 捕获 preview 请求 (Fetch)');
316 refreshUI();
317
318 // 有缓存 → 直接返回(来自主动模式或弹窗恢复)
319 if (S.cache) {
320 log('📦 返回缓存的成功响应');
321 const c = S.cache;
322 S.cache = null;
323 recoveryAttempts = 0; // 重置恢复计数
324 return new Response(c.text, {
325 status: 200,
326 headers: { 'Content-Type': 'application/json' },
327 });
328 }
329
330 const result = await retry(url, {
331 method: init?.method || 'POST',
332 body: init?.body,
333 headers: extractHeaders(init?.headers),
334 signal: init?.signal, // 传入以便 retry 内部剥离
335 });
336
337 if (result.ok) {
338 return new Response(result.text, {
339 status: result.status,
340 headers: { 'Content-Type': 'application/json' },
341 });
342 }
343 return _fetch.apply(this, [input, init]);
344 }
345
346 if (url && url.includes(CFG.CHECK) && url.includes('bizId=null')) {
347 log('🚫 拦截 check(bizId=null)');
348 return new Response(JSON.stringify({ code: -1, msg: '等待有效bizId' }), {
349 status: 200, headers: { 'Content-Type': 'application/json' },
350 });
351 }
352
353 return _fetch.apply(this, [input, init]);
354 };
355
356 // ======================== 五、XHR 拦截器 ========================
357 const _xhrOpen = XMLHttpRequest.prototype.open;
358 const _xhrSend = XMLHttpRequest.prototype.send;
359 const _xhrSetHeader = XMLHttpRequest.prototype.setRequestHeader;
360
361 XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
362 (this._h || (this._h = {}))[k] = v;
363 return _xhrSetHeader.call(this, k, v);
364 };
365 XMLHttpRequest.prototype.open = function (method, url) {
366 this._m = method;
367 this._u = url;
368 return _xhrOpen.apply(this, arguments);
369 };
370 XMLHttpRequest.prototype.send = function (body) {
371 const url = this._u;
372
373 if (typeof url === 'string' && url.includes(CFG.PREVIEW)) {
374 const self = this;
375 S.captured = { url, method: this._m, body, headers: this._h || {} };
376 log('🎯 捕获 preview 请求 (XHR)');
377 refreshUI();
378
379 if (S.cache) {
380 log('📦 返回缓存的成功响应 (XHR)');
381 const c = S.cache; S.cache = null;
382 recoveryAttempts = 0;
383 fakeXHR(self, c.text);
384 return;
385 }
386
387 retry(url, { method: this._m, body, headers: this._h || {} }).then(result => {
388 fakeXHR(self, result.ok ? result.text : '{"code":-1,"msg":"重试失败"}');
389 });
390 return;
391 }
392
393 if (typeof url === 'string' && url.includes(CFG.CHECK) && url.includes('bizId=null')) {
394 log('🚫 拦截 check(bizId=null) (XHR)');
395 fakeXHR(this, '{"code":-1,"msg":"等待有效bizId"}');
396 return;
397 }
398
399 return _xhrSend.call(this, body);
400 };
401
402 function fakeXHR(xhr, text) {
403 setTimeout(() => {
404 const dp = (k, v) => Object.defineProperty(xhr, k, { value: v, configurable: true });
405 dp('readyState', 4); dp('status', 200); dp('statusText', 'OK');
406 dp('responseText', text); dp('response', text);
407 const rsc = new Event('readystatechange');
408 if (typeof xhr.onreadystatechange === 'function') xhr.onreadystatechange(rsc);
409 xhr.dispatchEvent(rsc);
410 const load = new ProgressEvent('load');
411 if (typeof xhr.onload === 'function') xhr.onload(load);
412 xhr.dispatchEvent(load);
413 xhr.dispatchEvent(new ProgressEvent('loadend'));
414 }, 0);
415 }
416
417 // ======================== 六、主动抢购模式 ========================
418 async function startProactive() {
419 if (!S.captured) {
420 log('⚠️ 请先手动点一次购买/订阅按钮');
421 alert('请先手动点一次购买/订阅按钮,让脚本捕获请求参数');
422 return;
423 }
424
425 S.proactive = true;
426 log('🚀 主动抢购模式启动');
427
428 const { url, method, body, headers } = S.captured;
429 const result = await retry(url, { method, body, headers });
430
431 S.proactive = false;
432
433 if (result.ok) {
434 S.cache = { text: result.text, data: result.data };
435 log('🎉 主动模式成功! 触发购买流程…');
436
437 // 先关可能存在的弹窗
438 const errDlg = findErrorDialog();
439 if (errDlg) {
440 dismissDialog(errDlg);
441 await sleep(300);
442 }
443
444 const btn = findBuyButton();
445 if (btn) {
446 btn.click();
447 log('🖱 已自动点击购买按钮');
448 } else {
449 log('⚠️ 未找到按钮,请手动点击');
450 alert('已获取到商品!请立即点击购买按钮!');
451 }
452 }
453 }
454
455 function stopAll() {
456 stopRequested = true;
457 S.proactive = false;
458 S.status = 'idle';
459 S.count = 0;
460 if (S.timerId) { clearTimeout(S.timerId); S.timerId = null; }
461 log('⏹ 已停止');
462 refreshUI();
463 }
464
465 function findBuyButton() {
466 for (const el of document.querySelectorAll('button, a, [role="button"], div[class*="btn"], span[class*="btn"]')) {
467 const t = el.textContent.trim();
468 if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20 && el.offsetParent !== null) {
469 return el;
470 }
471 }
472 return null;
473 }
474
475 // ======================== 七、定时触发 ========================
476 function scheduleAt(timeStr) {
477 if (S.timerId) { clearTimeout(S.timerId); S.timerId = null; }
478 const parts = timeStr.split(':').map(Number);
479 const now = new Date();
480 const target = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parts[0], parts[1], parts[2] || 0);
481 if (target <= now) { log('⚠️ 目标时间已过'); return; }
482 const ms = target - now;
483 log(`⏰ 已设定: ${timeStr} (${Math.ceil(ms / 1000)}秒后)`);
484 S.timerId = setTimeout(() => {
485 S.timerId = null;
486 log('⏰ 时间到! 启动抢购!');
487 if (S.captured) {
488 startProactive();
489 } else {
490 const btn = findBuyButton();
491 if (btn) { btn.click(); log('🖱 定时: 已点击购买按钮'); }
492 else { log('⚠️ 未找到按钮'); alert('定时到了!请手动点击购买!'); }
493 }
494 }, ms - 50);
495 refreshUI();
496 }
497
498 // ======================== 八、浮动控制面板 ========================
499 function createPanel() {
500 const panel = document.createElement('div');
501 panel.id = 'glm-rush';
502 panel.innerHTML = `
503<style>
504#glm-rush{position:fixed;top:10px;right:10px;width:340px;background:#1a1a2e;color:#e0e0e0;
505 border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.6);z-index:999999;
506 font:13px/1.5 Consolas,'Courier New',monospace;user-select:none}
507#glm-rush *{box-sizing:border-box;margin:0;padding:0}
508.glm-hd{background:linear-gradient(135deg,#0f3460,#16213e);padding:9px 14px;
509 border-radius:12px 12px 0 0;display:flex;justify-content:space-between;align-items:center;cursor:move}
510.glm-hd b{font-size:14px;letter-spacing:.5px}
511.glm-mn{background:none;border:none;color:#aaa;cursor:pointer;font-size:20px;line-height:1;padding:0 4px}
512.glm-mn:hover{color:#fff}
513.glm-bd{padding:12px 14px 14px}
514.glm-st{padding:8px;border-radius:8px;text-align:center;font-weight:700;margin-bottom:10px;transition:background .3s}
515.glm-st-idle{background:#2d3436}
516.glm-st-retrying{background:#e17055;animation:glm-pulse 1s infinite}
517.glm-st-success{background:#00b894}
518.glm-st-failed{background:#d63031}
519@keyframes glm-pulse{50%{opacity:.7}}
520.glm-cap{font-size:11px;padding:5px 8px;background:#2d3436;border-radius:6px;margin-bottom:10px;
521 white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
522.glm-row{display:flex;align-items:center;gap:6px;margin-bottom:8px;font-size:12px;flex-wrap:wrap}
523.glm-row input[type=number],.glm-row input[type=time]{
524 width:72px;padding:4px 6px;border:1px solid #444;border-radius:4px;
525 background:#2d3436;color:#fff;text-align:center;font-size:12px}
526.glm-btns{display:flex;gap:8px;margin-bottom:10px}
527.glm-btns button{flex:1;padding:8px;border:none;border-radius:6px;cursor:pointer;
528 font-weight:700;font-size:12px;color:#fff;transition:opacity .2s}
529.glm-btns button:hover{opacity:.85}
530.glm-b-go{background:#0984e3}
531.glm-b-stop{background:#d63031}
532.glm-b-time{background:#6c5ce7;flex:0 0 auto !important;padding:4px 10px !important}
533.glm-logs{max-height:170px;overflow-y:auto;background:#0d1117;border-radius:6px;
534 padding:6px 8px;font-size:11px;line-height:1.7}
535.glm-logs div{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
536.glm-logs::-webkit-scrollbar{width:4px}
537.glm-logs::-webkit-scrollbar-thumb{background:#444;border-radius:2px}
538</style>
539<div class="glm-hd" id="glm-drag">
540 <b>🎯 GLM 抢购助手 v3.2</b>
541 <button class="glm-mn" id="glm-min">−</button>
542</div>
543<div class="glm-bd" id="glm-bd">
544 <div class="glm-st glm-st-idle" id="glm-st">⏳ 等待中</div>
545 <div class="glm-cap" id="glm-cap">📡 请求: 未捕获 — 请先点一次购买按钮</div>
546 <div class="glm-row">
547 <span>间隔</span><input type="number" id="glm-delay" value="${CFG.delay}" min="50" max="5000" step="50"><span>ms</span>
548 <span style="margin-left:6px">上限</span><input type="number" id="glm-max" value="${CFG.maxRetry}" min="10" max="9999" step="10"><span>次</span>
549 </div>
550 <div class="glm-row">
551 <span>定时</span><input type="time" id="glm-time" step="1">
552 <button class="glm-b-time" id="glm-time-set">设定</button>
553 <span id="glm-timer-info" style="color:#6c5ce7;font-size:11px"></span>
554 </div>
555 <div class="glm-btns">
556 <button class="glm-b-go" id="glm-go">▶ 主动抢购</button>
557 <button class="glm-b-stop" id="glm-stop" style="display:none">■ 停止</button>
558 </div>
559 <div class="glm-logs" id="glm-logs"></div>
560</div>`;
561 document.body.appendChild(panel);
562
563 const $ = id => document.getElementById(id);
564 $('glm-go').onclick = startProactive;
565 $('glm-stop').onclick = stopAll;
566 $('glm-delay').onchange = function () { CFG.delay = Math.max(50, +this.value || 100); };
567 $('glm-max').onchange = function () { CFG.maxRetry = Math.max(10, +this.value || 300); };
568 $('glm-time-set').onclick = function () { const v = $('glm-time').value; if (v) scheduleAt(v); };
569
570 $('glm-min').onclick = function () {
571 const bd = $('glm-bd');
572 const hidden = bd.style.display === 'none';
573 bd.style.display = hidden ? '' : 'none';
574 this.textContent = hidden ? '−' : '+';
575 };
576
577 // 拖拽
578 let sx, sy, sl, st;
579 $('glm-drag').onmousedown = function (e) {
580 sx = e.clientX; sy = e.clientY;
581 const rect = panel.getBoundingClientRect();
582 sl = rect.left; st = rect.top;
583 const onMove = function (e) {
584 panel.style.left = (sl + e.clientX - sx) + 'px';
585 panel.style.top = (st + e.clientY - sy) + 'px';
586 panel.style.right = 'auto';
587 };
588 const onUp = function () {
589 document.removeEventListener('mousemove', onMove);
590 document.removeEventListener('mouseup', onUp);
591 };
592 document.addEventListener('mousemove', onMove);
593 document.addEventListener('mouseup', onUp);
594 };
595
596 log('🚀 v3.2 已加载 (preview+check 双重校验)');
597
598 // 启动错误弹窗监控
599 setupDialogWatcher();
600 }
601
602 function refreshUI() {
603 const stEl = document.getElementById('glm-st');
604 if (!stEl) return;
605 stEl.className = 'glm-st glm-st-' + S.status;
606 stEl.textContent = S.status === 'idle' ? '⏳ 等待中'
607 : S.status === 'retrying' ? `🔄 重试中… ${S.count}/${CFG.maxRetry}`
608 : S.status === 'success' ? `✅ 成功! bizId=${S.bizId}`
609 : `❌ 失败 (${S.count}次)`;
610
611 const capEl = document.getElementById('glm-cap');
612 if (capEl) {
613 capEl.textContent = S.captured
614 ? `📡 已捕获: ${S.captured.method}${S.captured.url.split('?')[0].slice(-30)}`
615 : '📡 请求: 未捕获 — 请先点一次购买按钮';
616 }
617
618 const goBtn = document.getElementById('glm-go');
619 const stopBtn = document.getElementById('glm-stop');
620 if (goBtn && stopBtn) {
621 goBtn.style.display = S.status === 'retrying' ? 'none' : '';
622 stopBtn.style.display = S.status === 'retrying' ? '' : 'none';
623 }
624 }
625
626 function refreshLog() {
627 const el = document.getElementById('glm-logs');
628 if (!el) return;
629 const last = S.logs[S.logs.length - 1];
630 if (last) {
631 const div = document.createElement('div');
632 div.textContent = last;
633 el.appendChild(div);
634 while (el.children.length > 80) el.removeChild(el.firstChild);
635 el.scrollTop = el.scrollHeight;
636 }
637 }
638
639 // ======================== 启动 ========================
640 console.log('[GLM抢购] 🚀 v3.2 全自动版 (preview+check 双重校验) 已注入');
641
642 if (document.readyState === 'loading') {
643 document.addEventListener('DOMContentLoaded', createPanel);
644 } else {
645 createPanel();
646 }
647})();
648