Files
Mortal-Rating/docs/headless-turnstile-troubleshooting.md
e2hang 9f4ff8fdc6 无头化改造 + 移除 Turnstile 模拟点击 + 跨平台支持
- 删除 uc_gui_click_captcha / _poke_captcha,Turnstile 改为无头自动放行
- _wait_for_turnstile_token 改为纯轮询,新增 _acquire_turnstile_token 做 JS 重置 + 重开页面恢复
- headless 默认 True,--headless 改用 BooleanOptionalAction(--no-headless 调试)
- 新增 proxy: "direct" 配置,通过 chromium_arg --proxy-server=direct:// 绕过系统代理
  (mjai.ekyu.moe Turnstile 风控机房代理 IP,需直连)
- install_uc_driver.py 平台感知:Linux/Mac/Win 选对应 chromedriver 包和二进制名
- .gitignore 加 .DS_Store/venv/pytest_cache/个人配置
- README 加 Fedora 支持、proxy direct、headless 默认说明
- docs/headless-turnstile-troubleshooting.md 排查记录
2026-07-16 15:46:25 +08:00

128 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 无头化与 Turnstile 排查记录
本文记录将 batchmortal 改造为无头模式、移除模拟点击、并解决 Cloudflare Turnstile 自动放行问题的完整排查过程。供后续维护或迁移到其他平台时参考。
## 背景
原实现通过 SeleniumBase 的 `sb.uc_gui_click_captcha()``pyautogui` 物理点击 Cloudflare Turnstile 复选框来获取 token。这条路径有三个硬伤
1. 必须授予 macOS 辅助功能Accessibility+ 屏幕录制Screen Recording权限。
2. 无头模式下没有可点击的 GUI根本无法工作。
3. `pyautogui` 会接管鼠标,运行期间不能并行使用电脑。
目标:无头、无点击、无系统权限依赖。
## 排查过程
### 第一步:确认服务器是否强制校验 Turnstile
直接 `curl -X POST https://mjai.ekyu.moe/review`(不带 token
```
HTTP/2 403
invalid captcha response
```
结论HTTP 层绕不开 Turnstile必须拿到合法 token。表单 POST 到 `/review`token 字段是 `cf-turnstile-response`,由 Cloudflare 的 `turnstile/v0/api.js` 在浏览器端生成。
### 第二步:无头 + 自动放行(初步方案)
删掉所有 `_poke_captcha` / `uc_gui_click_captcha` 调用,把 `_wait_for_turnstile_token` 改成纯轮询器,新增 `_acquire_turnstile_token` 做恢复(先 JS 重置 `window.turnstile.reset()`,再最多 2 次重开页面)。`_open_fresh_review_page``uc_open_with_reconnect``reconnect_time` 从 2 调到 4给 Turnstile JS 更多时间在断开驱动期间自动下发 token。
实跑 `--limit 2`(无头 + 系统代理开着):**100% 失败**。每次都 12s 轮询无 token → JS 重置 → 重开页面 → 仍无 token → stall。6 次重试全挂。
### 第三步:隔离变量——是“无头被识破”还是“代理被风控”?
`--no-headless --limit 1`(有头 + 同一代理):**同样 100% 失败**,模式完全一致。
结论:**不是无头的问题**。有头无头在代理下都挂。最大嫌疑变成代理出口 IP 被 Cloudflare 风控(机房 IP 触发交互挑战,而非 managed 模式自动放行)。
### 第四步:测直连
`curl --noproxy '*' https://mjai.ekyu.moe/` 返回 HTTP 2001.1sCloudflare 边缘 IP。站点直连可达代理并非访问必需。
让用户关掉 Clash 系统代理,跑 `--limit 1` 无头直连:
```
[Proxy] No system proxy detected, running directly.
[1/1][...] Submit slot granted, starting Turnstile
[1/1][...] Turnstile token missing, resetting widget
[1/1][...] Turnstile token still missing, reloading form (1/2)
[1/1][...] Turnstile token ready in 26.8s ← 成功
[1/1][...] OK rating=89.869 match=80.420%
```
**直连下无头自动放行成功**。根因确认:代理出口 IP 被 Turnstile 风控。
### 第五步:自动化直连——`proxy: "direct"`
不能要求用户每次手动关 Clash 系统代理。需要让浏览器强制直连、绕过系统代理。
#### 为什么不能直接用 SeleniumBase 的 `proxy=` 参数
SeleniumBase 的 `validate_proxy_string``seleniumbase/core/proxy_helper.py`)只接受 `ip:port``scheme://host:port` 格式,`"direct://"` 通不过校验,会被置为 `None`。而 `proxy=None` 意味着不传 `--proxy-server` 给 Chrome —— Chrome 就回退到系统代理,反而走回 Clash。所以“不传 proxy”不等于“直连”。
#### 解法:用 `chromium_arg` 注入 `--proxy-server=direct://`
Chrome 的 `--proxy-server=direct://` 会强制忽略所有代理(含系统代理),用 `direct`无代理scheme 直连。SeleniumBase 的 `SB()``chromium_arg` 参数可以塞额外 Chrome 启动参数。
`batchmortal/browser.py` 加模块级 helper
```python
def build_browser_kwargs(headless, proxy):
kwargs = {"uc": True, "headless": headless}
if proxy and str(proxy).strip().lower() == "direct":
kwargs["chromium_arg"] = "--proxy-server=direct://"
elif proxy:
kwargs["proxy"] = proxy
return kwargs
```
`main.py``detect_proxy` 识别 `"direct"` 关键字(大小写不敏感)返回 `"direct"`,所有 `SB()` 调用统一走 `build_browser_kwargs`
#### 验证
Clash 系统代理开着 + `proxy: "direct"` + 无头 + 全量 40 条:**38/38 成功0 失败**。
- 36/38 局 token 秒出0.0~2.1s)—— 浏览器会话热起来后指纹/cookie 被信任
- 2/38 局 stall冷启动 + 中途偶发),都经 JS 重置 + 1 次重开页面恢复,没有任何一局需要外层 `retry`
- 全程零点击、零系统权限弹窗、无头、Clash 开着
## 关键代码位置
- `batchmortal/browser.py:build_browser_kwargs` —— proxy→SB kwargs 翻译,`"direct"``chromium_arg`
- `batchmortal/browser.py:_acquire_turnstile_token` —— 纯轮询 + JS 重置 + 重开页面恢复,无点击
- `batchmortal/browser.py:_wait_for_turnstile_token` —— 纯轮询器(检测 token / 检测 `invalid captcha response` / 超时)
- `main.py:detect_proxy` —— 识别 `"direct"` 关键字
- `install_uc_driver.py:_platform_archive_info` —— 跨平台 driver 包选择
## 平台兼容性Fedora/Linux
核心运行路径跨平台:`detect_proxy``urllib.request.getproxies()`Linux 读环境变量/GSettings`browser.py` 全是 JS + SeleniumBase 调用,文件路径用 `os.path.join``uc_name``os.name` 分流。无头模式不需要 X server。
**唯一需要修的是 `install_uc_driver.py`**:原版硬编码 Windows 的 `chromedriver-win64.zip` / `chromedriver.exe`,在 Linux 上会下载 Windows 二进制。改成按平台选 archive 和二进制名:
| 平台 | archive | 二进制 | uc_driver 名 |
| :--- | :--- | :--- | :--- |
| Windows | `chromedriver-win64.zip` | `chromedriver.exe` | `uc_driver.exe` |
| Linux | `chromedriver-linux64.zip` | `chromedriver` | `uc_driver`(需 `chmod 755` |
| macOS | `chromedriver-mac-{arm64,x64}.zip` | `chromedriver` | `uc_driver` |
Chrome 版本检测也加了 `google-chrome-stable`Fedora/Debian 常见名)和 mac app 路径回退。旧版 Chrome<115在 npmmirror 没有 Linux/Mac 包,返回 None 交给 SeleniumBase 自带下载兜底。
## 常见问题速查
| 症状 | 原因 | 解决 |
| :--- | :--- | :--- |
| Turnstile 每局都 stall、100% 失败 | 浏览器走了机房代理 IP | 配 `proxy: "direct"` 走直连 |
| 直连下偶发 stall~5% | Turnstile 偶尔不出 token | 内置恢复会自动 JS 重置 + 重开页面,无需干预 |
| `proxy: "direct"` 不生效、仍走代理 | Clash 用 TUN 模式(网络层拦截) | 切回系统代理模式,或临时关 ClashTUN 模式下 Chrome 的 `--proxy-server` 绕不过 |
| 无头下 token 一直拿不到 | 先确认是不是代理问题(有头也测一次) | 有头无头都挂 = 代理问题;仅无头挂 = 指纹问题 |
| Fedora 上 uc_driver 不可执行 | 旧版 install_uc_driver 下了 Windows 包 | 已修;或直接删 `seleniumbase/drivers/uc_driver*` 让 SB 自带下载 |
## 注意事项
- **TUN 模式陷阱**:如果 Clash 用 TUN 模式(而非系统代理模式),`--proxy-server=direct://` 绕不过网络层拦截,`proxy: "direct"` 会失效。此时只能切回系统代理模式或关 Clash。判断方法`scutil --proxy` 显示 `HTTPEnable: 0` 但网络仍走代理,多半是 TUN。
- **断点续跑**`get_processed_uuids` 会跳过已成功写入的 UUID所以即使中途网络抖动或被 Turnstile 卡住导致中断,重跑会从未处理的牌谱继续,不会重复分析。
- **不要把 `proxy: "direct"` 和真正的代理混用**`"direct"` 是关键字,不是 URL。配了 `"direct"` 就不会再走任何代理。