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:
1 parent
767516745e
commit
62b6ea8ea2
7 files changed
+391
-129
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
+293
-102
@@ -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,32 +3736,11 @@ 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:
|
try:
|
||||||
page.run_js(
|
token = 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:
|
|
||||||
token = page.run_js(
|
|
||||||
"""
|
|
||||||
try {
|
try {
|
||||||
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
|
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
|
||||||
if (byInput) return byInput;
|
if (byInput) return byInput;
|
||||||
@@ -3749,77 +3751,246 @@ try {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
} catch(e) { return ''; }
|
} catch(e) { return ''; }
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
token = str(token or "").strip()
|
return str(token or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
iframe = None
|
||||||
|
try:
|
||||||
|
wrapper = challenge_input.parent()
|
||||||
|
iframe = wrapper.shadow_root.ele("tag:iframe")
|
||||||
|
except Exception:
|
||||||
|
iframe = None
|
||||||
|
if iframe is None:
|
||||||
|
try:
|
||||||
|
iframe = page.ele(
|
||||||
|
'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]',
|
||||||
|
timeout=0.6,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
iframe = None
|
||||||
|
if iframe:
|
||||||
|
try:
|
||||||
|
iframe.run_js(
|
||||||
|
"""
|
||||||
|
window.dtp = 1;
|
||||||
|
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
|
||||||
|
let sx = getRandomInt(800, 1200);
|
||||||
|
let sy = getRandomInt(400, 700);
|
||||||
|
try {
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenX', { get: function(){ return sx; } });
|
||||||
|
Object.defineProperty(MouseEvent.prototype, 'screenY', { get: function(){ return sy; } });
|
||||||
|
} catch(e) {}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# A1: body.shadow_root input / .mark
|
||||||
|
try:
|
||||||
|
body_sr = iframe.ele("tag:body").shadow_root
|
||||||
|
btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark") or body_sr.ele("css:label")
|
||||||
|
if btn:
|
||||||
|
try:
|
||||||
|
btn.click()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
btn.click(by_js=True)
|
||||||
|
except Exception:
|
||||||
|
page.actions.click(btn)
|
||||||
|
return "shadow-checkbox"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# A2: click iframe element itself (center)
|
||||||
|
try:
|
||||||
|
iframe.click()
|
||||||
|
return "iframe-click"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
page.actions.click(iframe)
|
||||||
|
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(
|
||||||
|
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'
|
||||||
|
));
|
||||||
|
let ok = false;
|
||||||
|
for (const n of nodes) {
|
||||||
|
if (firePointer(n)) ok = true;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if clicked:
|
||||||
|
return "js-pointer"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
|
||||||
|
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 len(token) >= 80:
|
||||||
if log_callback:
|
if log_callback:
|
||||||
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
|
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5)
|
# Only reset when previous click rounds clearly failed (not on first)
|
||||||
if challenge_input:
|
if round_i >= 2 and round_i % 2 == 0:
|
||||||
wrapper = challenge_input.parent()
|
|
||||||
iframe = None
|
|
||||||
try:
|
try:
|
||||||
iframe = wrapper.shadow_root.ele("tag:iframe")
|
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:
|
except Exception:
|
||||||
iframe = None
|
pass
|
||||||
if iframe is None:
|
|
||||||
try:
|
|
||||||
iframe = page.ele(
|
|
||||||
'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]',
|
|
||||||
timeout=0.5,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
iframe = None
|
|
||||||
if iframe:
|
|
||||||
try:
|
|
||||||
iframe.run_js(
|
|
||||||
"""
|
|
||||||
window.dtp = 1;
|
|
||||||
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
|
|
||||||
let sx = getRandomInt(800, 1200);
|
|
||||||
let sy = getRandomInt(400, 700);
|
|
||||||
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx });
|
|
||||||
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
clicked = False
|
|
||||||
try:
|
|
||||||
body_sr = iframe.ele("tag:body").shadow_root
|
|
||||||
btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark")
|
|
||||||
if btn:
|
|
||||||
btn.click()
|
|
||||||
clicked = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if not clicked:
|
|
||||||
try:
|
|
||||||
iframe.click()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# 兜底:尝试触发页面上可见的 Turnstile 容器 / iframe
|
|
||||||
page.run_js(
|
|
||||||
"""
|
|
||||||
const nodes = Array.from(document.querySelectorAll(
|
|
||||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'
|
|
||||||
));
|
|
||||||
for (const n of nodes) {
|
|
||||||
try { n.scrollIntoView({block:'center', inline:'center'}); } catch(e) {}
|
|
||||||
try { if (typeof n.click === 'function') n.click(); } catch(e) {}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Progressive wait: later rounds wait a bit longer for CF challenge
|
|
||||||
human_sleep(0.9 + 0.25 * round_i, cancel_callback)
|
|
||||||
|
|
||||||
raise Exception("Turnstile 获取 token 失败")
|
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
@@ -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
@@ -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 t = setInterval(function () {
|
|
||||||
n++;
|
|
||||||
tryClickTurnstile();
|
|
||||||
if (n > 80) clearInterval(t);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
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)
|
||||||
|
if (n > 180) clearInterval(t);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", startLoop);
|
||||||
|
} else {
|
||||||
|
startLoop();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
@@ -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": [
|
||||||
|
|||||||
Reference in new issue
Block a user