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

7.5 KiB
Raw Permalink Blame History

无头化与 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 到 /reviewtoken 字段是 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_pageuc_open_with_reconnectreconnect_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_stringseleniumbase/core/proxy_helper.py)只接受 ip:portscheme://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

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.pydetect_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_proxyurllib.request.getproxies()Linux 读环境变量/GSettingsbrowser.py 全是 JS + SeleniumBase 调用,文件路径用 os.path.joinuc_nameos.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-stableFedora/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" 就不会再走任何代理。