fix: v4.5 修复支付弹窗不弹出 — 改用先抢再喂策略

- startProactive 改为直接 retry 抢 bizId,成功后缓存响应再点按钮
- findBuyButton 按优先级排序,排除导航按钮
- clickButton 强制解除 disabled/is-disabled
- 拦截器去掉 proactive 分支,只做 cache 返回
This commit is contained in:
qtaxm
2026-04-10 20:10:42 +08:00
parent 436e8415a2
commit ac838303cd
2 changed files with 118 additions and 122 deletions

104
README.md
View File

@@ -1,18 +1,17 @@
# GLM Coding 抢购助手 v4.0 # GLM Coding 抢购助手 v4.5
智谱 GLM Coding Plan 限时抢购自动化脚本Tampermonkey 油猴脚本) 智谱 GLM Coding Plan 限时抢购自动化脚本Tampermonkey 油猴脚本)
## 功能特点 ## 功能特点
- **并发重试** — 3 路并发请求,任一成功立即返回(比单线程快 3x - **极速并发引擎** — 双模式并发:极速模式 10 路 + 普通模式 5 路,任一成功立即取消其余
- **自适应间隔** — 前 10 次零延迟爆发 → 快速重试 → 随机间隔,带 ±30% 抖动 - **自适应间隔** — 前 20 次零延迟爆发 → 30ms 快速重试 → 100ms 随机间隔,带 ±30% 抖动
- **preview + check 双重校验** — 获取 bizId 后调用 check 确认有效EXPIRE 立即重试 - **preview + check 双重校验** — 获取 bizId 后调用 check 确认有效EXPIRE 立即重试
- **4 层支付恢复** — 暴力清弹窗 → 缓存重点击 → 直接获取支付链接 → 兜底提醒 - **4 层支付恢复** — 暴力清弹窗 → 缓存重点击 → 直接获取支付链接 → 兜底提醒
- **反检测** — JSON.parse 定向拦截(不污染全局)、fetch/XHR toString 伪装、Shadow DOM 面板隔离 - **反检测** — 请求指纹随机化X-Request-Id / X-Timestamp / Accept-LanguageJSON.parse 定向拦截、fetch/XHR toString 伪装、Shadow DOM 面板隔离
- **高精度定时** — requestAnimationFrame + performance.now精度 ±2ms - **高精度定时** — requestAnimationFrame + performance.now精度 ±2ms
- **配置持久化** — localStorage 保存并发数/上限等配置sessionStorage 保存捕获的请求 - **配置持久化** — localStorage 保存所有配置sessionStorage 保存捕获的请求,刷新不丢失
- **错误弹窗自动恢复** — MutationObserver 监控弹窗出现,自动关闭并重新触发购买 - **弹窗自动恢复** — MutationObserver 监控弹窗,自动关闭并重新触发,最多 3 次
- **TCP 预热** — 提前建立连接,减少首次请求延迟
- **快捷键** — `Alt+S` 开始 / `Alt+X` 停止 / `Alt+H` 隐藏面板 - **快捷键** — `Alt+S` 开始 / `Alt+X` 停止 / `Alt+H` 隐藏面板
## 安装 ## 安装
@@ -31,47 +30,26 @@
## 使用方法 ## 使用方法
1. 打开 [GLM Coding 页面](https://bigmodel.cn/glm-coding) 1. 打开 [GLM Coding 页面](https://bigmodel.cn/glm-coding)
2. 右上角出现 **GLM v4.0** 控制面板 2. 右上角出现控制面板
3. **手动点一次购买按钮** — 脚本捕获请求参数(面板显示"已捕获" 3. **手动点一次购买按钮** — 脚本捕获请求参数(面板显示"已捕获"
4. 选择触发方式: 4. 选择触发方式:
- **主动抢购**:立即开始并发重试 - **主动抢购**:立即开始并发重试
- **定时触发**:设定时间,到点自动开始 - **定时触发**:设定时间(默认 10:00:00,到点自动开始
- **预热**:提前建立 TCP 连接
5. 抢购成功后自动弹出支付页面 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 | 同时发起的请求数 | | 并发数 | 5 | 普通模式同时发起的请求数 |
| 最大重试 | 500 | 达到上限后停止 | | 极速并发 | 10 | 前 5 秒的高并发路数 |
| 爆发次数 | 10 | 前 N 次零延迟 | | 极速时长 | 5s | 高并发持续多久 |
| 快速间隔 | 50ms | 爆发后的重试间隔 | | 最大重试 | 2000 | 达到上限后停止 |
| 慢速间隔 | 150ms | 后期重试间隔中值 | | 爆发次数 | 20 | 前 N 次零延迟 |
| 快速间隔 | 30ms | 爆发后的重试间隔 |
| 慢速间隔 | 100ms | 后期重试间隔中值 |
| 抖动 | ±30% | 间隔随机化幅度 | | 抖动 | ±30% | 间隔随机化幅度 |
| 抢购时间 | 10:00:00 | 每天定时触发时间 |
## 快捷键 ## 快捷键
@@ -86,9 +64,15 @@
``` ```
用户点击购买 → 脚本捕获 preview 请求 用户点击购买 → 脚本捕获 preview 请求
┌── 并发路1 ──┐ ┌── 极速模式 (前5秒) ──┐
├── 并发路2 ──┤ → 任一获取 bizId 10路并发 × 零延迟 │
└── 并发路3 ──┘ └──────────────────────┘
┌── 普通模式 ──────────┐
│ 5路并发 × 自适应间隔 │
└──────────────────────┘
任一获取 bizId
check 校验 bizId check 校验 bizId
├── EXPIRE → 立即重试 ├── 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) ### v4.1 (2026-04-08)
- **修复** 售罄状态下按钮不可点击的问题(恢复全局 JSON.parse patch - **修复** 售罄状态下按钮不可点击的问题(恢复全局 JSON.parse patch
- **修复** 支付弹窗不弹出的问题4 层恢复策略:清弹窗→缓存重点击→获取支付链接→兜底提醒 - **修复** 支付弹窗不弹出的问题4 层恢复策略)
- **修复** `@match` 规则不匹配 `bigmodel.cn`(无 www的问题 - **修复** `@match` 规则不匹配 `bigmodel.cn`(无 www
- **修复** 原型链污染风险Object.keys + WeakSet 循环引用保护 - **修复** 原型链污染风险Object.keys + WeakSet
- **修复** HTTP 401/403 会话过期检测(之前永远不会触发) - **修复** HTTP 401/403 会话过期检测
- **修复** 限流退避使用错误的计数器 - **修复** 限流退避使用错误的计数器
- **修复** stats.errors 永远显示 0 - **修复** stats.errors 永远显示 0
- **修复** Alt+H 快捷键在 Shadow DOM 中失效 - **修复** Alt+H 快捷键在 Shadow DOM 中失效
- **修复** `_glmShadow` 暴露在全局作用域
### v4.0 (2026-04-08) ### v4.0 (2026-04-08)
- 并发重试(3 路 Promise.race - 并发重试Promise.race 变体
- 自适应间隔(爆发→快速→随机抖动) - 自适应间隔(爆发→快速→随机抖动)
- 反检测定向拦截、toString 伪装、Shadow DOM - 反检测定向拦截、toString 伪装、Shadow DOM
- 高精度定时rAF + performance.now - 高精度定时rAF + performance.now
- 配置/请求持久化localStorage + sessionStorage - 配置/请求持久化
- MutationObserver 弹窗监控 - MutationObserver 弹窗监控
- TCP 预热、快捷键、离开保护 - 快捷键、离开保护
### v3.2 (原版) ### v3.2 (原版)
- 单线程串行重试 - 单线程串行重试

View File

@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name 智谱 GLM Coding 抢购助手 v4.0 // @name 智谱 GLM Coding 抢购助手 v4.0
// @namespace http://tampermonkey.net/ // @namespace http://tampermonkey.net/
// @version 4.4 // @version 4.5
// @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化 // @description 并发重试 + 自适应间隔 + 反检测 + check校验 + 弹窗恢复 + 定时触发 + 配置持久化
// @author Assistant // @author Assistant
// @match *://www.bigmodel.cn/* // @match *://www.bigmodel.cn/*
@@ -362,37 +362,20 @@
setState({ captured }); setState({ captured });
try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {} try { sessionStorage.setItem('glm_rush_captured', JSON.stringify(captured)); } catch {}
// 已经成功过 → 直接返回缓存 // 有缓存 → 返回给前端(来自主动模式抢到后的 cache
if (state.status === 'success' && state.lastSuccess) { // 这是支付弹窗能弹出的关键: 前端发 preview → 拦截器返回缓存的成功响应 → 前端正常处理
log('已抢到, 返回成功响应');
return new Response(state.lastSuccess.text, { status: 200, headers: { 'Content-Type': 'application/json' } });
}
// 有缓存 → 返回(来自主动模式成功后的恢复)
if (state.cache) { if (state.cache) {
log('返回缓存响应'); log('返回缓存的成功响应给前端');
const c = state.cache; const c = state.cache;
setState({ cache: null }); setState({ cache: null });
recoveryAttempts = 0; recoveryAttempts = 0;
return new Response(c.text, { status: 200, headers: { 'Content-Type': 'application/json' } }); return new Response(c.text, { status: 200, headers: { 'Content-Type': 'application/json' } });
} }
// 主动模式/正在抢购 → 进入重试引擎 // 已经成功过,再次点击也返回成功响应
if (state.proactive || state.status === 'retrying') { if (state.status === 'success' && state.lastSuccess) {
log('抢购中, 启动重试...'); log('已抢到, 返回成功响应');
const result = await retry(url, { return new Response(state.lastSuccess.text, { status: 200, headers: { 'Content-Type': 'application/json' } });
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]);
} }
// 普通捕获 → 只记录参数,放行原始请求,自动设定定时 // 普通捕获 → 只记录参数,放行原始请求,自动设定定时
@@ -444,25 +427,19 @@
return; return;
} }
// 有缓存 → 返回成功响应给前端
if (state.cache) { if (state.cache) {
log('返回缓存响应 (XHR)'); log('返回缓存的成功响应给前端 (XHR)');
const c = state.cache; setState({ cache: null }); const c = state.cache; setState({ cache: null });
recoveryAttempts = 0; recoveryAttempts = 0;
fakeXHR(self, c.text); fakeXHR(self, c.text);
return; return;
} }
// 主动模式/正在抢购 → 重试 // 已成功过 → 返回成功响应
if (state.proactive || state.status === 'retrying') { if (state.status === 'success' && state.lastSuccess) {
log('抢购中, 启动重试 (XHR)...'); log('已抢到, 返回成功响应 (XHR)');
retry(url, { method: this._m, body, headers: this._h || {} }).then(result => { fakeXHR(self, state.lastSuccess.text);
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":"重试失败"}');
});
return; return;
} }
@@ -644,28 +621,47 @@
function findBuyButton() { function findBuyButton() {
// 优先返回用户上次点击的同一个按钮 // 优先返回用户上次点击的同一个按钮
if (_lastClickedBtn && _lastClickedBtn.offsetParent !== null) return _lastClickedBtn; 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(); 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 => { document.addEventListener('click', e => {
const t = (e.target.textContent || '').trim(); 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; _lastClickedBtn = e.target.closest('button') || e.target;
log('记住按钮: ' + t); log('记住按钮: ' + t);
} }
}, true); }, true);
function clickButton(btn) { 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.focus();
btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); btn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
btn.click(); btn.click();
} }
@@ -680,30 +676,38 @@
return; return;
} }
// 核心策略: 设置 proactive=true然后点击按钮 // 核心策略(与旧版一致,更可靠):
// 让前端自己发 fetch → 拦截器检测到 proactive → 启动重试 // 1. 直接调 retry 抢 bizId不依赖按钮点击
// 响应直接返回给前端的 fetch 调用 → 前端正常弹出支付窗口 // 2. 成功后把响应存入 cache
// 3. 点击购买按钮 → 前端发 preview → 拦截器返回 cache → 前端弹支付窗口
setState({ proactive: true }); 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 { url, method, body, headers } = state.captured;
const result = await retry(url, { method, body, headers }); const result = await retry(url, { method, body, headers });
setState({ proactive: false }); setState({ proactive: false });
if (result.ok) { if (result.ok) {
// 存入 cache等前端下一次 preview 请求时返回
setState({ cache: { text: result.text, data: result.data } }); setState({ cache: { text: result.text, data: result.data } });
log('抢购成功! 请立即手动点击购买按钮!'); log('抢购成功! 触发购买流程...');
try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {} try { new Notification('GLM 抢购成功!', { body: `bizId=${state.bizId}` }); } catch {}
alert('已抢到! 请立即手动点击「特惠订阅」按钮完成支付!');
// 清理可能存在的错误弹窗
const errDlg = findErrorDialog();
if (errDlg) {
dismissDialog(errDlg);
await sleep(300);
}
// 点击购买按钮 → 前端发 preview → 拦截器返回 cache → 前端弹支付窗口
const btn = findBuyButton();
if (btn) {
clickButton(btn);
log('已自动点击购买按钮, 等待支付窗口...');
} else {
log('未找到购买按钮, 请手动点击!');
alert('已抢到! 请立即点击「特惠订阅」按钮完成支付!');
} }
} }
} }