From ac838303cd7fecedef659d2f9cd1f97535e901f7 Mon Sep 17 00:00:00 2001 From: qtaxm <17772864223@163.com> Date: Fri, 10 Apr 2026 20:10:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20v4.5=20=E4=BF=AE=E5=A4=8D=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=BC=B9=E7=AA=97=E4=B8=8D=E5=BC=B9=E5=87=BA=20?= =?UTF-8?q?=E2=80=94=20=E6=94=B9=E7=94=A8=E5=85=88=E6=8A=A2=E5=86=8D?= =?UTF-8?q?=E5=96=82=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - startProactive 改为直接 retry 抢 bizId,成功后缓存响应再点按钮 - findBuyButton 按优先级排序,排除导航按钮 - clickButton 强制解除 disabled/is-disabled - 拦截器去掉 proactive 分支,只做 cache 返回 --- README.md | 104 ++++++++++++++++----------------- glm-rush-v4.user.js | 136 +++++++++++++++++++++++--------------------- 2 files changed, 118 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 566914f..9b8b343 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ -# GLM Coding 抢购助手 v4.0 +# GLM Coding 抢购助手 v4.5 智谱 GLM Coding Plan 限时抢购自动化脚本(Tampermonkey 油猴脚本) ## 功能特点 -- **并发重试** — 3 路并发请求,任一成功立即返回(比单线程快 3x) -- **自适应间隔** — 前 10 次零延迟爆发 → 快速重试 → 随机间隔,带 ±30% 抖动 +- **极速并发引擎** — 双模式并发:极速模式 10 路 + 普通模式 5 路,任一成功立即取消其余 +- **自适应间隔** — 前 20 次零延迟爆发 → 30ms 快速重试 → 100ms 随机间隔,带 ±30% 抖动 - **preview + check 双重校验** — 获取 bizId 后调用 check 确认有效,EXPIRE 立即重试 - **4 层支付恢复** — 暴力清弹窗 → 缓存重点击 → 直接获取支付链接 → 兜底提醒 -- **反检测** — JSON.parse 定向拦截(不污染全局)、fetch/XHR toString 伪装、Shadow DOM 面板隔离 +- **反检测** — 请求指纹随机化(X-Request-Id / X-Timestamp / Accept-Language)、JSON.parse 定向拦截、fetch/XHR toString 伪装、Shadow DOM 面板隔离 - **高精度定时** — requestAnimationFrame + performance.now,精度 ±2ms -- **配置持久化** — localStorage 保存并发数/上限等配置,sessionStorage 保存捕获的请求 -- **错误弹窗自动恢复** — MutationObserver 监控弹窗出现,自动关闭并重新触发购买 -- **TCP 预热** — 提前建立连接,减少首次请求延迟 +- **配置持久化** — localStorage 保存所有配置,sessionStorage 保存捕获的请求,刷新不丢失 +- **弹窗自动恢复** — MutationObserver 监控弹窗,自动关闭并重新触发,最多 3 次 - **快捷键** — `Alt+S` 开始 / `Alt+X` 停止 / `Alt+H` 隐藏面板 ## 安装 @@ -31,47 +30,26 @@ ## 使用方法 1. 打开 [GLM Coding 页面](https://bigmodel.cn/glm-coding) -2. 右上角出现 **GLM v4.0** 控制面板 +2. 右上角出现控制面板 3. **手动点一次购买按钮** — 脚本捕获请求参数(面板显示"已捕获") 4. 选择触发方式: - **主动抢购**:立即开始并发重试 - - **定时触发**:设定时间,到点自动开始 - - **预热**:提前建立 TCP 连接 + - **定时触发**:设定时间(默认 10:00:00),到点自动开始 5. 抢购成功后自动弹出支付页面 -## 控制面板 - -``` -┌─────────────────────────┐ -│ GLM v4.0 [-] │ -├─────────────────────────┤ -│ ● 重试中... 45/500 │ -│ 已捕获: POST .../preview│ -│ │ -│ [重试:45] [成功:0] [错误:3] │ -│ │ -│ 并发 [3] 上限 [500] │ -│ 定时 [--:--] [设定] │ -│ │ -│ [▶ 主动抢购] [停止] [预热]│ -│ │ -│ 10:00:01 捕获 preview │ -│ 10:00:01 #3 系统繁忙 │ -│ 10:00:02 #15 售罄 │ -│ 10:00:03 成功! bizId=xx │ -└─────────────────────────┘ -``` - ## 配置参数 | 参数 | 默认值 | 说明 | |------|--------|------| -| 并发数 | 3 | 同时发起的请求数 | -| 最大重试 | 500 | 达到上限后停止 | -| 爆发次数 | 10 | 前 N 次零延迟 | -| 快速间隔 | 50ms | 爆发后的重试间隔 | -| 慢速间隔 | 150ms | 后期重试间隔中值 | +| 并发路数 | 5 | 普通模式同时发起的请求数 | +| 极速并发 | 10 | 前 5 秒的高并发路数 | +| 极速时长 | 5s | 高并发持续多久 | +| 最大重试 | 2000 | 达到上限后停止 | +| 爆发次数 | 20 | 前 N 次零延迟 | +| 快速间隔 | 30ms | 爆发后的重试间隔 | +| 慢速间隔 | 100ms | 后期重试间隔中值 | | 抖动 | ±30% | 间隔随机化幅度 | +| 抢购时间 | 10:00:00 | 每天定时触发时间 | ## 快捷键 @@ -86,9 +64,15 @@ ``` 用户点击购买 → 脚本捕获 preview 请求 ↓ - ┌── 并发路1 ──┐ - ├── 并发路2 ──┤ → 任一获取 bizId - └── 并发路3 ──┘ + ┌── 极速模式 (前5秒) ──┐ + │ 10路并发 × 零延迟 │ + └──────────────────────┘ + ↓ + ┌── 普通模式 ──────────┐ + │ 5路并发 × 自适应间隔 │ + └──────────────────────┘ + ↓ + 任一获取 bizId ↓ check 校验 bizId ├── EXPIRE → 立即重试 @@ -101,34 +85,42 @@ └── 兜底提醒 ``` -## 注意事项 - -- 需要先登录智谱账号 -- 抢购前建议先点一次购买按钮让脚本捕获请求参数 -- 建议在抢购开始前 3 秒点击 **预热** 按钮 -- 如果支付弹窗未出现,脚本会自动尝试多种恢复策略 - ## 更新日志 +### v4.5 (2026-04-10) +- **修复** 支付弹窗不弹出的核心问题:改用"先抢再喂"策略,retry 独立抢到 bizId 后缓存响应,再点击按钮让前端正常处理 +- **修复** `findBuyButton` 找错按钮(匹配到"即刻订阅"导航按钮),现在按优先级排序,优先找特惠/购买按钮 +- **修复** disabled 按钮点击无效:`clickButton` 强制解除 disabled 和 is-disabled class +- **优化** 拦截器简化:去掉 proactive 分支,只做 cache 返回 + 普通捕获,逻辑更清晰 +- **优化** 按钮排除"即刻订阅"、"暂不"、"拼好模"等非购买按钮 + +### v4.4 (2026-04-09) +- **新增** 极速模式:前 5 秒 10 路并发,之后降为 5 路 +- **新增** 请求指纹随机化(X-Request-Id / X-Timestamp / Accept-Language 权重随机) +- **新增** 余额支付方式支持 +- **优化** 并发数从 3 路提升到 5 路(普通模式) +- **优化** 最大重试从 500 提升到 2000 +- **优化** 爆发次数从 10 提升到 20,快速间隔从 50ms 降到 30ms +- **优化** 连续售罄 / 限流智能退避 + ### v4.1 (2026-04-08) - **修复** 售罄状态下按钮不可点击的问题(恢复全局 JSON.parse patch) -- **修复** 支付弹窗不弹出的问题(4 层恢复策略:清弹窗→缓存重点击→获取支付链接→兜底提醒) -- **修复** `@match` 规则不匹配 `bigmodel.cn`(无 www)的问题 -- **修复** 原型链污染风险(Object.keys + WeakSet 循环引用保护) -- **修复** HTTP 401/403 会话过期检测(之前永远不会触发) +- **修复** 支付弹窗不弹出的问题(4 层恢复策略) +- **修复** `@match` 规则不匹配 `bigmodel.cn`(无 www) +- **修复** 原型链污染风险(Object.keys + WeakSet) +- **修复** HTTP 401/403 会话过期检测 - **修复** 限流退避使用错误的计数器 - **修复** stats.errors 永远显示 0 - **修复** Alt+H 快捷键在 Shadow DOM 中失效 -- **修复** `_glmShadow` 暴露在全局作用域 ### v4.0 (2026-04-08) -- 并发重试(3 路 Promise.race) +- 并发重试(Promise.race 变体) - 自适应间隔(爆发→快速→随机抖动) - 反检测(定向拦截、toString 伪装、Shadow DOM) - 高精度定时(rAF + performance.now) -- 配置/请求持久化(localStorage + sessionStorage) +- 配置/请求持久化 - MutationObserver 弹窗监控 -- TCP 预热、快捷键、离开保护 +- 快捷键、离开保护 ### v3.2 (原版) - 单线程串行重试 diff --git a/glm-rush-v4.user.js b/glm-rush-v4.user.js index 8188a90..1e9f9db 100644 --- a/glm-rush-v4.user.js +++ b/glm-rush-v4.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name 智谱 GLM Coding 抢购助手 v4.0 // @namespace http://tampermonkey.net/ -// @version 4.4 +// @version 4.5 // @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化 // @author Assistant // @match *://www.bigmodel.cn/* @@ -362,37 +362,20 @@ setState({ captured }); try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} - // 已经成功过 → 直接返回缓存 - if (state.status === 'success' && state.lastSuccess) { - log('已抢到, 返回成功响应'); - return new Response(state.lastSuccess.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); - } - - // 有缓存 → 返回(来自主动模式成功后的恢复) + // 有缓存 → 返回给前端(来自主动模式抢到后的 cache) + // 这是支付弹窗能弹出的关键: 前端发 preview → 拦截器返回缓存的成功响应 → 前端正常处理 if (state.cache) { - log('返回缓存响应'); + log('返回缓存的成功响应给前端'); const c = state.cache; setState({ cache: null }); recoveryAttempts = 0; return new Response(c.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); } - // 主动模式/正在抢购 → 进入重试引擎 - if (state.proactive || state.status === 'retrying') { - log('抢购中, 启动重试...'); - const result = await retry(url, { - method: init?.method || 'POST', - body: init?.body, - headers: extractHeaders(init?.headers), - }); - setState({ proactive: false }); - if (result.ok) { - log('拦截器内抢购成功! 返回响应给前端...'); - try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} - // 直接返回给前端的 fetch 调用 → 前端会正常弹出支付窗口 - return new Response(result.text, { status: result.status, headers: { 'Content-Type': 'application/json' } }); - } - return _fetch.apply(this, [input, init]); + // 已经成功过,再次点击也返回成功响应 + if (state.status === 'success' && state.lastSuccess) { + log('已抢到, 返回成功响应'); + return new Response(state.lastSuccess.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); } // 普通捕获 → 只记录参数,放行原始请求,自动设定定时 @@ -444,25 +427,19 @@ return; } + // 有缓存 → 返回成功响应给前端 if (state.cache) { - log('返回缓存响应 (XHR)'); + log('返回缓存的成功响应给前端 (XHR)'); const c = state.cache; setState({ cache: null }); recoveryAttempts = 0; fakeXHR(self, c.text); return; } - // 主动模式/正在抢购 → 重试 - if (state.proactive || state.status === 'retrying') { - log('抢购中, 启动重试 (XHR)...'); - retry(url, { method: this._m, body, headers: this._h || {} }).then(result => { - setState({ proactive: false }); - if (result.ok) { - log('XHR拦截器内抢购成功! 返回响应给前端...'); - try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} - } - fakeXHR(self, result.ok ? result.text : '{"code":-1,"msg":"重试失败"}'); - }); + // 已成功过 → 返回成功响应 + if (state.status === 'success' && state.lastSuccess) { + log('已抢到, 返回成功响应 (XHR)'); + fakeXHR(self, state.lastSuccess.text); return; } @@ -644,28 +621,47 @@ function findBuyButton() { // 优先返回用户上次点击的同一个按钮 if (_lastClickedBtn && _lastClickedBtn.offsetParent !== null) return _lastClickedBtn; - for (const el of document.querySelectorAll('button, a, [role="button"], div[class*="btn"], span[class*="btn"]')) { + + // 按优先级查找: 特惠订阅 > 订阅升级 > 其他购买按钮 + // 注意: 特惠按钮可能是 disabled 的,但我们会在 clickButton 里强制解除 + const priority = ['特惠', '购买', '抢购', '下单']; + const candidates = []; + for (const el of document.querySelectorAll('button.buy-btn, button[class*="buy"], button')) { const t = el.textContent.trim(); - if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20 && el.offsetParent !== null) return el; + if (/购买|抢购|立即|下单|特惠|订阅/.test(t) && t.length < 20 && el.offsetParent !== null) { + // 排除"即刻订阅"这类导航按钮和非购买按钮 + if (/即刻订阅|暂不|取消|拼好模/.test(t)) continue; + const pIdx = priority.findIndex(p => t.includes(p)); + candidates.push({ el, priority: pIdx >= 0 ? pIdx : 99 }); + } } - return null; + candidates.sort((a, b) => a.priority - b.priority); + return candidates.length > 0 ? candidates[0].el : null; } // 监听用户点击,记住是哪个按钮 document.addEventListener('click', e => { const t = (e.target.textContent || '').trim(); - if (/购买|抢购|立即|下单|订阅/.test(t) && t.length < 20) { + if (/购买|抢购|立即|下单|特惠|订阅/.test(t) && t.length < 20) { _lastClickedBtn = e.target.closest('button') || e.target; log('记住按钮: ' + t); } }, true); function clickButton(btn) { - // 多种方式触发点击,确保前端框架能响应 + // 强制解除 disabled 状态(售罄按钮需要解锁才能触发事件) + if (btn.disabled) { + btn.disabled = false; + btn.classList.remove('is-disabled', 'disabled'); + log('已解除按钮 disabled 状态'); + } + // 确保 pointer-events 可交互 + btn.style.pointerEvents = 'auto'; + // 多种方式触发点击,确保前端框架(Vue/Element UI)能响应 btn.focus(); - btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); - btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); + btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); + btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); + btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); btn.click(); } @@ -680,30 +676,38 @@ return; } - // 核心策略: 设置 proactive=true,然后点击按钮 - // 让前端自己发 fetch → 拦截器检测到 proactive → 启动重试 - // 响应直接返回给前端的 fetch 调用 → 前端正常弹出支付窗口 + // 核心策略(与旧版一致,更可靠): + // 1. 直接调 retry 抢 bizId(不依赖按钮点击) + // 2. 成功后把响应存入 cache + // 3. 点击购买按钮 → 前端发 preview → 拦截器返回 cache → 前端弹支付窗口 setState({ proactive: true }); - log(`极速抢购启动! 点击按钮触发前端请求...`); + log('极速抢购启动! 直接请求模式...'); - const btn = findBuyButton(); - if (btn) { - clickButton(btn); - log('已点击购买按钮, 等待拦截器重试...'); - // 拦截器会在 fetch/XHR 中自动处理重试 - // proactive 会在拦截器成功后由 retry 结束时保持 - } else { - // 找不到按钮 → 降级为直接调用方式 - log('未找到按钮, 降级为直接请求模式...'); - const { url, method, body, headers } = state.captured; - const result = await retry(url, { method, body, headers }); - setState({ proactive: false }); + const { url, method, body, headers } = state.captured; + const result = await retry(url, { method, body, headers }); + setState({ proactive: false }); - if (result.ok) { - setState({ cache: { text: result.text, data: result.data } }); - log('抢购成功! 请立即手动点击购买按钮!'); - try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} - alert('已抢到! 请立即手动点击「特惠订阅」按钮完成支付!'); + if (result.ok) { + // 存入 cache,等前端下一次 preview 请求时返回 + setState({ cache: { text: result.text, data: result.data } }); + log('抢购成功! 触发购买流程...'); + try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} + + // 清理可能存在的错误弹窗 + const errDlg = findErrorDialog(); + if (errDlg) { + dismissDialog(errDlg); + await sleep(300); + } + + // 点击购买按钮 → 前端发 preview → 拦截器返回 cache → 前端弹支付窗口 + const btn = findBuyButton(); + if (btn) { + clickButton(btn); + log('已自动点击购买按钮, 等待支付窗口...'); + } else { + log('未找到购买按钮, 请手动点击!'); + alert('已抢到! 请立即点击「特惠订阅」按钮完成支付!'); } } }