Harden Turnstile solve path for --fast/Xvfb runs

Add cf_sleep, multi-round shadow clicks, richer diagnostics, and longer profile/CF budgets so tokenLen=0 fails less on headless servers.
This commit is contained in:
chaos committed 2026-07-12 00:58:28 +08:00
1 parent 767516745e
commit 62b6ea8ea2
7 files changed
+348 -86

No files matched your search

+1 -1
View File
@@ -544,7 +544,7 @@ curl -sS http://127.0.0.1:8317/v1/chat/completions \
| 协议 verify/approve 失败 | 会话态变化 / 风控;看日志后自动回退浏览器 | | 协议 verify/approve 失败 | 会话态变化 / 风控;看日志后自动回退浏览器 |
| 一直 `authorization_pending` | 浏览器路径未完成 consent;需到「设备已授权」且 token 200 | | 一直 `authorization_pending` | 浏览器路径未完成 consent;需到「设备已授权」且 token 200 |
| Cloudflare / Turnstile | 必须用 **Chromium + turnstilePatch**(Chrome 无法 `--load-extension`,token 常为 0);配合 `./start.sh` 自动 Xvfb | | Cloudflare / Turnstile | 必须用 **Chromium + turnstilePatch**(Chrome 无法 `--load-extension`,token 常为 0);配合 `./start.sh` 自动 Xvfb |
| Turnstile token 长度=0 | 日志若 `skip turnstilePatch on Google Chrome`:`sudo apt install -y chromium` 后重跑;或 `browser_path` 指向 chromium | | Turnstile token 长度=0 | ① 日志若 `skip turnstilePatch` / 选中 Chrome:装 Chromium 或设 `browser_path`;② 扩展已加载仍为 0:代码侧已用 `cf_sleep`(不受 `--fast` 0.15x 过度压缩)+ 多轮 shadow 点击;可再 `--no-fast` 或加大 `turnstile_max_rounds`/`profile_timeout`;③ 看日志 `Turnstile 初始态 iframe=` 是否为 0(widget 未挂载 / IP 被拦) |
| 浏览器 connection fails / no interface | 无 DISPLAY:`./start.sh` 自动 Xvfb / Python headless | | 浏览器 connection fails / no interface | 无 DISPLAY:`./start.sh` 自动 Xvfb / Python headless |
| Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 | | Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 |
| 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` | | 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` |
+9 -1
View File
@@ -95,10 +95,18 @@
"code_form_timeout": 180, "code_form_timeout": 180,
"// profile_timeout": "资料页(姓名密码生日)超时(秒)", "// profile_timeout": "资料页(姓名密码生日)超时(秒)",
"profile_timeout": 240, "profile_timeout": 240,
"// turnstile_retry_limit": "Turnstile 失败重试次数", "// turnstile_retry_limit": "Turnstile 失败重试次数(账号级)",
"turnstile_retry_limit": 3, "turnstile_retry_limit": 3,
"// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)", "// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)",
"turnstile_stuck_timeout": 150, "turnstile_stuck_timeout": 150,
"// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(服务器建议 4)",
"turnstile_fast_fail_count": 4,
"// turnstile_retry_after_sec": "资料页等待 CF 多久后触发二次复用(秒)",
"turnstile_retry_after_sec": 12,
"// turnstile_retry_gap_sec": "两次二次复用之间的最小间隔(秒)",
"turnstile_retry_gap_sec": 8,
"// turnstile_max_rounds": "单次 getTurnstileToken 内 click/poll 轮数",
"turnstile_max_rounds": 8,
"// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)", "// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)",
"sso_timeout_base": 240, "sso_timeout_base": 240,
"sso_timeout_max": 480, "sso_timeout_max": 480,
+4 -4
View File
@@ -98,8 +98,8 @@
"gmail_imap_fallback": true, "gmail_imap_fallback": true,
"gmail_imap_fallback_timeout_sec": 45, "gmail_imap_fallback_timeout_sec": 45,
"gmail_imap_fallback_resend_sec": 10, "gmail_imap_fallback_resend_sec": 10,
"turnstile_fast_fail_count": 2, "turnstile_fast_fail_count": 4,
"turnstile_retry_after_sec": 8, "turnstile_retry_after_sec": 12,
"turnstile_retry_gap_sec": 5, "turnstile_retry_gap_sec": 8,
"turnstile_max_rounds": 4 "turnstile_max_rounds": 8
} }
+250 -59
View File
@@ -1144,6 +1144,29 @@ def human_sleep(mean_seconds, cancel_callback=None):
sleep_with_cancel(delay, cancel_callback) sleep_with_cancel(delay, cancel_callback)
def cf_sleep(seconds, cancel_callback=None, *, floor: float = 0.6):
"""Turnstile/Cloudflare 专用等待:fast 模式仍保留足够墙钟时间。
human_sleep 在 --fast 下会被压到 ~0.15x,导致 CF iframe 根本来不及加载/解题。
这里仅轻度缩放,并强制 floor,避免 token 恒为 0。
"""
try:
sec = float(seconds)
except Exception:
sec = 1.0
scale = float(PERF_FLAGS.get("sleep_scale", 1.0) or 1.0)
if PERF_FLAGS.get("fast"):
# 最多压到 0.55x,且不低于 floor
scale = max(0.55, min(scale, 1.0))
delay = max(float(floor), sec * scale)
# 轻微抖动,避免机械节奏
try:
delay = max(float(floor), min(delay * 1.35, random.gauss(delay, delay * 0.12)))
except Exception:
pass
sleep_with_cancel(delay, cancel_callback)
def get_domains(api_key=None): def get_domains(api_key=None):
headers = {} headers = {}
@@ -3713,29 +3736,8 @@ return 'clicked';
raise Exception("验证码已获取,但自动填写/提交失败") raise Exception("验证码已获取,但自动填写/提交失败")
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): def _read_turnstile_token(page):
"""Try to obtain a Turnstile token. """Read cf-turnstile-response / turnstile.getResponse if present."""
max_rounds controls click/reset attempts; each round waits longer for CF.
CDP stealth should already be applied (webdriver hidden).
"""
page = _get_page()
if page is None:
raise Exception("页面未就绪,无法执行 Turnstile")
apply_stealth_patches(page, log_callback=None)
try:
page.run_js(
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
)
except Exception:
pass
rounds = max(2, int(max_rounds or 4))
# Give CF more time: ~1.2s * rounds, plus initial attach wait
human_sleep(0.8 if not PERF_FLAGS.get("fast") else 0.35, cancel_callback)
for round_i in range(0, rounds):
raise_if_cancelled(cancel_callback)
try: try:
token = page.run_js( token = page.run_js(
""" """
@@ -3751,17 +3753,59 @@ try {
} catch(e) { return ''; } } catch(e) { return ''; }
""" """
) )
token = str(token or "").strip() return str(token or "").strip()
if len(token) >= 80: except Exception:
if log_callback: return ""
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5)
def _turnstile_diag(page) -> dict:
"""Lightweight CF widget diagnostics for logs (no secrets)."""
try:
info = page.run_js(
"""
try {
const input = document.querySelector('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]');
const iframes = Array.from(document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
));
const widgets = Array.from(document.querySelectorAll('div.cf-turnstile, [data-sitekey]'));
const srcs = iframes.slice(0, 3).map((f) => {
try { return String(f.src || '').slice(0, 120); } catch(e) { return ''; }
});
return {
hasInput: !!input,
tokenLen: input ? String(input.value || '').trim().length : 0,
iframeCount: iframes.length,
widgetCount: widgets.length,
sitekey: widgets[0] ? String(widgets[0].getAttribute('data-sitekey') || '').slice(0, 16) : '',
iframeSrc0: srcs[0] || '',
turnstileApi: !!(window.turnstile && typeof turnstile.getResponse === 'function'),
webdriver: !!navigator.webdriver,
};
} catch(e) {
return { err: String(e && e.message || e) };
}
"""
)
return info if isinstance(info, dict) else {"raw": str(info)[:200]}
except Exception as exc:
return {"err": str(exc)[:160]}
def _click_turnstile_widget(page, log_callback=None) -> str:
"""Best-effort click path for interactive Turnstile checkbox.
Returns a short tag describing which path succeeded (or 'none').
"""
# Path A: shadow-root checkbox under cf-turnstile-response parent
try:
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.6)
except Exception:
challenge_input = None
if challenge_input: if challenge_input:
wrapper = challenge_input.parent()
iframe = None iframe = None
try: try:
wrapper = challenge_input.parent()
iframe = wrapper.shadow_root.ele("tag:iframe") iframe = wrapper.shadow_root.ele("tag:iframe")
except Exception: except Exception:
iframe = None iframe = None
@@ -3769,7 +3813,7 @@ try {
try: try:
iframe = page.ele( iframe = page.ele(
'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]', 'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]',
timeout=0.5, timeout=0.6,
) )
except Exception: except Exception:
iframe = None iframe = None
@@ -3781,45 +3825,172 @@ window.dtp = 1;
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
let sx = getRandomInt(800, 1200); let sx = getRandomInt(800, 1200);
let sy = getRandomInt(400, 700); let sy = getRandomInt(400, 700);
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx }); try {
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy }); Object.defineProperty(MouseEvent.prototype, 'screenX', { get: function(){ return sx; } });
Object.defineProperty(MouseEvent.prototype, 'screenY', { get: function(){ return sy; } });
} catch(e) {}
""" """
) )
except Exception: except Exception:
pass pass
clicked = False # A1: body.shadow_root input / .mark
try: try:
body_sr = iframe.ele("tag:body").shadow_root body_sr = iframe.ele("tag:body").shadow_root
btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark") btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark") or body_sr.ele("css:label")
if btn: if btn:
try:
btn.click() btn.click()
clicked = True except Exception:
try:
btn.click(by_js=True)
except Exception:
page.actions.click(btn)
return "shadow-checkbox"
except Exception: except Exception:
pass pass
if not clicked: # A2: click iframe element itself (center)
try: try:
iframe.click() iframe.click()
return "iframe-click"
except Exception: except Exception:
pass pass
else: try:
# 兜底:尝试触发页面上可见的 Turnstile 容器 / iframe page.actions.click(iframe)
page.run_js( return "iframe-actions"
except Exception:
pass
# Path B: DOM query + synthetic pointer events
try:
clicked = page.run_js(
""" """
function firePointer(el) {
if (!el) return false;
try { el.scrollIntoView({block:'center', inline:'center'}); } catch(e) {}
const rect = el.getBoundingClientRect();
const x = rect.left + Math.max(8, Math.min(rect.width - 8, rect.width * 0.35));
const y = rect.top + Math.max(8, Math.min(rect.height - 8, rect.height * 0.5));
const opts = {bubbles:true, cancelable:true, view:window, clientX:x, clientY:y, button:0};
for (const t of ['pointerover','pointerenter','mouseover','mouseenter','mousemove','pointerdown','mousedown','pointerup','mouseup','click']) {
try {
const Ev = t.startsWith('pointer') ? PointerEvent : MouseEvent;
el.dispatchEvent(new Ev(t, opts));
} catch(e) {
try { el.dispatchEvent(new MouseEvent(t, opts)); } catch(e2) {}
}
}
try { if (typeof el.click === 'function') el.click(); } catch(e) {}
return true;
}
const nodes = Array.from(document.querySelectorAll( const nodes = Array.from(document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]' 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'
)); ));
let ok = false;
for (const n of nodes) { for (const n of nodes) {
try { n.scrollIntoView({block:'center', inline:'center'}); } catch(e) {} if (firePointer(n)) ok = true;
try { if (typeof n.click === 'function') n.click(); } catch(e) {}
} }
return ok;
""" """
) )
if clicked:
return "js-pointer"
except Exception: except Exception:
pass pass
# Progressive wait: later rounds wait a bit longer for CF challenge return "none"
human_sleep(0.9 + 0.25 * round_i, cancel_callback)
raise Exception("Turnstile 获取 token 失败")
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4):
"""Try to obtain a Turnstile token.
max_rounds controls click/reset attempts; each round waits longer for CF.
CDP stealth should already be applied (webdriver hidden).
Note: --fast compresses human_sleep heavily; this function uses cf_sleep
so CF still has real wall-clock time to attach iframe + solve.
"""
page = _get_page()
if page is None:
raise Exception("页面未就绪,无法执行 Turnstile")
apply_stealth_patches(page, log_callback=None)
rounds = max(3, int(max_rounds or 4))
# Initial attach wait — managed / auto modes often fill without click
cf_sleep(1.8 if not PERF_FLAGS.get("fast") else 1.2, cancel_callback, floor=1.0)
# Passive poll first (don't reset — reset often kills in-flight solve)
for _ in range(4):
raise_if_cancelled(cancel_callback)
token = _read_turnstile_token(page)
if len(token) >= 80:
if log_callback:
log_callback(f"[*] Turnstile 已通过(被动),token长度={len(token)}")
return token
cf_sleep(0.9, cancel_callback, floor=0.7)
diag0 = _turnstile_diag(page)
if log_callback:
log_callback(
f"[Debug] Turnstile 初始态: tokenLen={diag0.get('tokenLen', 0)} "
f"iframe={diag0.get('iframeCount', 0)} widget={diag0.get('widgetCount', 0)} "
f"api={diag0.get('turnstileApi')} webdriver={diag0.get('webdriver')} "
f"sitekey={diag0.get('sitekey', '')!r}"
)
last_click = "none"
for round_i in range(0, rounds):
raise_if_cancelled(cancel_callback)
try:
token = _read_turnstile_token(page)
if len(token) >= 80:
if log_callback:
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token
# Only reset when previous click rounds clearly failed (not on first)
if round_i >= 2 and round_i % 2 == 0:
try:
page.run_js(
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
)
if log_callback:
log_callback(f"[Debug] Turnstile reset (round={round_i + 1})")
cf_sleep(1.0, cancel_callback, floor=0.8)
except Exception:
pass
last_click = _click_turnstile_widget(page, log_callback=log_callback)
if log_callback and (round_i == 0 or last_click != "none"):
log_callback(f"[Debug] Turnstile click path={last_click} round={round_i + 1}/{rounds}")
# After click: poll more densely for token
poll_budget = 3.2 + 0.7 * round_i
poll_deadline = time.time() + poll_budget
while time.time() < poll_deadline:
raise_if_cancelled(cancel_callback)
token = _read_turnstile_token(page)
if len(token) >= 80:
if log_callback:
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token
cf_sleep(0.55, cancel_callback, floor=0.45)
except Exception as exc:
if log_callback and round_i == 0:
log_callback(f"[Debug] Turnstile round error: {exc}")
# Progressive gap between rounds
cf_sleep(1.1 + 0.35 * round_i, cancel_callback, floor=0.9)
diag1 = _turnstile_diag(page)
if log_callback:
log_callback(
f"[Debug] Turnstile 失败态: tokenLen={diag1.get('tokenLen', 0)} "
f"iframe={diag1.get('iframeCount', 0)} last_click={last_click} "
f"src={str(diag1.get('iframeSrc0', ''))[:80]!r}"
)
raise Exception(
f"Turnstile 获取 token 失败 (tokenLen={diag1.get('tokenLen', 0)}, "
f"iframe={diag1.get('iframeCount', 0)}, click={last_click})"
)
def build_profile(): def build_profile():
@@ -3854,11 +4025,18 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None
dump_state(page, "profile-form") dump_state(page, "profile-form")
take_screenshot(page, "profile-form") take_screenshot(page, "profile-form")
given_name, family_name, password = build_profile() given_name, family_name, password = build_profile()
# 预热 Turnstile:等 iframe 初始化;CDP stealth 会尝试自动点 checkbox # 预热 Turnstile:等 iframe 初始化;CDP stealth / extension 会尝试自动点 checkbox
if log_callback: if log_callback:
log_callback("[*] 预热 Turnstile...") log_callback("[*] 预热 Turnstile...")
# fast mode scales human_sleep; keep enough time for turnstile iframe to attach # 必须用 cf_sleep:--fast 下 human_sleep 会把预热压到 <0.3s,iframe 来不及挂载
human_sleep(1.6 if not PERF_FLAGS.get("fast") else 0.8, cancel_callback) cf_sleep(2.4 if not PERF_FLAGS.get("fast") else 1.6, cancel_callback, floor=1.2)
# 被动预读一次,managed 模式常自动出 token
try:
pre = _read_turnstile_token(page)
if pre and len(pre) >= 80 and log_callback:
log_callback(f"[*] 预热阶段已有 token,长度={len(pre)}")
except Exception:
pass
deadline = time.time() + timeout deadline = time.time() + timeout
form_filled_once = False form_filled_once = False
wait_cf_since = None wait_cf_since = None
@@ -3866,22 +4044,22 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None
last_cf_log_at = 0.0 last_cf_log_at = 0.0
cf_token_fail_streak = 0 cf_token_fail_streak = 0
try: try:
# Slightly more patient by default — Chrome CDP stealth still needs CF time # 服务器 Xvfb 上 CF 常需多轮;默认比桌面更耐心
max_cf_token_fails = max(1, int(config.get("turnstile_fast_fail_count", 3) or 3)) max_cf_token_fails = max(2, int(config.get("turnstile_fast_fail_count", 4) or 4))
except Exception: except Exception:
max_cf_token_fails = 3 max_cf_token_fails = 4
try: try:
cf_retry_after = float(config.get("turnstile_retry_after_sec", 10) or 10) cf_retry_after = float(config.get("turnstile_retry_after_sec", 12) or 12)
except Exception: except Exception:
cf_retry_after = 10.0 cf_retry_after = 12.0
try: try:
cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 6) or 6) cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 8) or 8)
except Exception: except Exception:
cf_retry_gap = 6.0 cf_retry_gap = 8.0
try: try:
turnstile_rounds = max(4, int(config.get("turnstile_max_rounds", 6) or 6)) turnstile_rounds = max(5, int(config.get("turnstile_max_rounds", 8) or 8))
except Exception: except Exception:
turnstile_rounds = 6 turnstile_rounds = 8
while time.time() < deadline: while time.time() < deadline:
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
@@ -3972,6 +4150,14 @@ return 'filled-no-submit';
waited = now - wait_cf_since if wait_cf_since else 0 waited = now - wait_cf_since if wait_cf_since else 0
log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s") log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s")
last_cf_log_at = now last_cf_log_at = now
# 卡住前先轻点一次 widget(不 reset),给 managed/interactive 更多时间
if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 4.0:
try:
path = _click_turnstile_widget(page)
if path != "none" and log_callback and (now - last_cf_log_at >= 4):
log_callback(f"[Debug] 等待期轻点 Turnstile path={path}")
except Exception:
pass
# 卡住后自动二次复用 Turnstile 组件 # 卡住后自动二次复用 Turnstile 组件
if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap:
if log_callback: if log_callback:
@@ -4003,7 +4189,7 @@ return String(cfInput.value || '').trim().length;
if cf_token_fail_streak >= max_cf_token_fails: if cf_token_fail_streak >= max_cf_token_fails:
raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}")
last_cf_retry_at = now last_cf_retry_at = now
human_sleep(0.8, cancel_callback) cf_sleep(1.0, cancel_callback, floor=0.7)
continue continue
if filled in ("ready-to-submit", "filled-no-submit"): if filled in ("ready-to-submit", "filled-no-submit"):
@@ -4058,6 +4244,11 @@ return 'submitted';
waited = now - wait_cf_since if wait_cf_since else 0 waited = now - wait_cf_since if wait_cf_since else 0
log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s") log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s")
last_cf_log_at = now last_cf_log_at = now
if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 4.0:
try:
_click_turnstile_widget(page)
except Exception:
pass
if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap:
if log_callback: if log_callback:
log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...") log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...")
@@ -4088,7 +4279,7 @@ return String(cfInput.value || '').trim().length;
if cf_token_fail_streak >= max_cf_token_fails: if cf_token_fail_streak >= max_cf_token_fails:
raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}")
last_cf_retry_at = now last_cf_retry_at = now
human_sleep(0.8, cancel_callback) cf_sleep(1.0, cancel_callback, floor=0.7)
continue continue
if submit_state == "submitted": if submit_state == "submitted":
+5 -4
View File
@@ -377,10 +377,11 @@ def register_one(
sso_timeout = int(cfg.get("sso_timeout_base", 120) or 120) sso_timeout = int(cfg.get("sso_timeout_base", 120) or 120)
except Exception: except Exception:
sso_timeout = 120 sso_timeout = 120
# Fast-fail band: keep CF stalls from dominating wall clock. # Profile page is dominated by Turnstile on Xvfb/servers.
# batch10 lesson: success profiles finish in ~6–18s; 40–50s CF fail is enough. # Old 70s hard-cap + --fast human_sleep scale caused tokenLen=0 within ~18s.
profile_timeout = max(35, min(profile_timeout, 70)) # Allow enough wall-clock for multi-round CF (cf_sleep is only lightly scaled).
sso_timeout = max(30, min(sso_timeout, 90)) profile_timeout = max(60, min(profile_timeout, 150))
sso_timeout = max(30, min(sso_timeout, 120))
log(worker_id, "4. 填写资料") log(worker_id, "4. 填写资料")
profile = reg.fill_profile_and_submit( profile = reg.fill_profile_and_submit(
+77 -15
View File
@@ -3,6 +3,8 @@
// / iframe postMessage spam) were observed to break xAI signup with: // / iframe postMessage spam) were observed to break xAI signup with:
// [permission_denied] HTTP 403 // [permission_denied] HTTP 403
// Keep webdriver hide only. Auto-click remains best-effort and local-only. // Keep webdriver hide only. Auto-click remains best-effort and local-only.
// Note: CF challenge iframes are cross-origin — contentDocument click usually fails;
// host-page click + Python CDP shadow path do the real work.
(function () { (function () {
"use strict"; "use strict";
@@ -30,41 +32,101 @@
} }
} catch (e) {} } catch (e) {}
// 2. Best-effort Turnstile checkbox click (same-origin only; no postMessage spam) function firePointer(el) {
if (!el) return;
try {
el.scrollIntoView({ block: "center", inline: "center" });
} catch (e) {}
var rect;
try {
rect = el.getBoundingClientRect();
} catch (e) {
rect = null;
}
var x = 12;
var y = 12;
if (rect && rect.width > 0) {
x = rect.left + Math.max(6, Math.min(rect.width - 6, rect.width * 0.35));
y = rect.top + Math.max(6, Math.min(rect.height - 6, rect.height * 0.5));
}
var opts = {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
button: 0,
};
var types = [
"pointerover",
"mouseover",
"mousemove",
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
"click",
];
for (var i = 0; i < types.length; i++) {
var t = types[i];
try {
var Ev = t.indexOf("pointer") === 0 ? PointerEvent : MouseEvent;
el.dispatchEvent(new Ev(t, opts));
} catch (e) {
try {
el.dispatchEvent(new MouseEvent(t, opts));
} catch (e2) {}
}
}
try {
if (typeof el.click === "function") el.click();
} catch (e) {}
}
// 2. Best-effort Turnstile interaction (same-origin iframe + host widget)
function tryClickTurnstile() { function tryClickTurnstile() {
try { try {
var iframes = document.querySelectorAll( var iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
); );
for (var i = 0; i < iframes.length; i++) { for (var i = 0; i < iframes.length; i++) {
// Host-side synthetic click (works even when iframe is cross-origin)
firePointer(iframes[i]);
try { try {
var body = iframes[i].contentDocument || iframes[i].contentWindow.document; var body = iframes[i].contentDocument || iframes[i].contentWindow.document;
var checkbox = body.querySelector('input[type="checkbox"], .mark'); if (!body) continue;
var checkbox = body.querySelector(
'input[type="checkbox"], .mark, label, [role="checkbox"]'
);
if (checkbox && !checkbox.checked) { if (checkbox && !checkbox.checked) {
checkbox.click(); firePointer(checkbox);
} }
} catch (e) { } catch (e) {
// cross-origin: skip // cross-origin: host click above is enough
} }
} }
var widgets = document.querySelectorAll(
"div.cf-turnstile, [data-sitekey], input[name='cf-turnstile-response']"
);
for (var j = 0; j < widgets.length; j++) {
firePointer(widgets[j]);
}
} catch (e) {} } catch (e) {}
} }
if (document.readyState === "loading") { function startLoop() {
document.addEventListener("DOMContentLoaded", function () {
var n = 0; var n = 0;
var t = setInterval(function () { var t = setInterval(function () {
n++; n++;
tryClickTurnstile(); tryClickTurnstile();
if (n > 80) clearInterval(t); // ~90s of best-effort clicks (was 40s)
}, 500); if (n > 180) clearInterval(t);
});
} else {
var n = 0;
var t = setInterval(function () {
n++;
tryClickTurnstile();
if (n > 80) clearInterval(t);
}, 500); }, 500);
} }
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startLoop);
} else {
startLoop();
}
})(); })();
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Turnstile Patch", "name": "Turnstile Patch",
"version": "1.2.0", "version": "1.3.0",
"description": "Minimal webdriver hide for Turnstile (no aggressive API patches)", "description": "Minimal webdriver hide + best-effort host click for Turnstile",
"content_scripts": [ "content_scripts": [
{ {
"matches": [ "matches": [