admin revidoval tento gist . Přejít na revizi
1 file changed, 647 insertions
glm.js(vytvořil soubor)
| @@ -0,0 +1,647 @@ | |||
| 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 | + | })(); | |
Novější
Starší