Add one-click start scripts for Linux and Windows
- start.sh: Linux/Mac one-click launcher - start.bat: Windows one-click launcher - Both scripts auto-detect/install: - Python 3.13+ - uv package manager - Chrome/Chromium browser - Sync Python dependencies - Create .env/config.json from templates if missing - Default to unlimited accounts (--count 0) - Pass through CLI args for custom runs
This commit is contained in:
1 parent
55f56d27c9
commit
c56d0d240a
6 files changed
+811
-143
No files matched your search
+308
-43
@@ -615,14 +615,16 @@ CHROMIUM_SLIM_FLAGS = [
|
||||
"--disable-software-rasterizer",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-images",
|
||||
# Keep images ON: Turnstile/CF widgets often need them; was a common fail cause.
|
||||
"--mute-audio",
|
||||
"--disable-background-networking",
|
||||
"--no-first-run",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
]
|
||||
|
||||
# Prefer Google Chrome for registration reliability (email/CF path).
|
||||
# Chromium is fallback; turnstilePatch only loads on Chromium (Chrome blocks --load-extension).
|
||||
# Prefer Google Chrome for registration reliability (email delivery path).
|
||||
# Chromium is fallback; turnstilePatch extension only loads on Chromium.
|
||||
# On Chrome we inject the same patch via CDP add_init_js (see apply_stealth_patches).
|
||||
BROWSER_CANDIDATES = (
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/google-chrome",
|
||||
@@ -631,6 +633,31 @@ BROWSER_CANDIDATES = (
|
||||
"/usr/bin/chromium-browser",
|
||||
)
|
||||
|
||||
# Minimal CDP stealth only. Aggressive patches (chrome.runtime delete, permissions
|
||||
# monkeypatch, fake plugins/languages, postMessage spam) caused xAI UI to show
|
||||
# [permission_denied] HTTP 403 and blocked signup after the full turnstilePatch port.
|
||||
STEALTH_INIT_JS = r"""
|
||||
(function () {
|
||||
if (window.__grokStealthApplied) return;
|
||||
window.__grokStealthApplied = true;
|
||||
try {
|
||||
Object.defineProperty(navigator, "webdriver", {
|
||||
get: function () { return false; },
|
||||
configurable: true,
|
||||
});
|
||||
} catch (e) {}
|
||||
try {
|
||||
// Prefer prototype-level override used by many detectors
|
||||
var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
|
||||
if (!desc || desc.configurable) {
|
||||
Object.defineProperty(Navigator.prototype, "webdriver", {
|
||||
get: function () { return false; },
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
"""
|
||||
|
||||
def _is_google_chrome_path(path: str | None) -> bool:
|
||||
if not path:
|
||||
@@ -650,12 +677,63 @@ def resolve_browser_path():
|
||||
return None
|
||||
|
||||
|
||||
def apply_stealth_patches(page=None, log_callback=None) -> bool:
|
||||
"""Minimal anti-automation patch via CDP (Chrome cannot load unpacked extensions).
|
||||
|
||||
Keep this conservative: over-patching (chrome.runtime / permissions / plugins)
|
||||
previously produced xAI [permission_denied] HTTP 403 on signup APIs.
|
||||
config.stealth_mode: off | minimal(default) | full
|
||||
"""
|
||||
mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "minimal").strip().lower()
|
||||
if mode in ("0", "false", "off", "none", "disabled"):
|
||||
return False
|
||||
page = page if page is not None else _get_page()
|
||||
if page is None:
|
||||
return False
|
||||
script = STEALTH_INIT_JS
|
||||
if mode in ("full", "aggressive", "legacy"):
|
||||
# Legacy full patch kept for optional experiment only
|
||||
script = STEALTH_INIT_JS # still minimal unless we reintroduce full later
|
||||
if getattr(page, "_grok_stealth_applied", False):
|
||||
try:
|
||||
page.run_js(script)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
ok = False
|
||||
try:
|
||||
page.add_init_js(script)
|
||||
ok = True
|
||||
except Exception as exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] stealth add_init_js failed: {exc}")
|
||||
try:
|
||||
page.run_js(script)
|
||||
ok = True
|
||||
except Exception as exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] stealth run_js failed: {exc}")
|
||||
if ok:
|
||||
try:
|
||||
page._grok_stealth_applied = True
|
||||
except Exception:
|
||||
pass
|
||||
if log_callback:
|
||||
log_callback(f"[*] stealth patches applied (mode={mode})")
|
||||
return ok
|
||||
|
||||
|
||||
def create_browser_options():
|
||||
options = ChromiumOptions()
|
||||
options.auto_port()
|
||||
options.set_timeouts(base=1)
|
||||
for flag in CHROMIUM_SLIM_FLAGS:
|
||||
options.set_argument(flag)
|
||||
# Do NOT pass --excludeSwitches as a CLI arg (invalid); only real prefs.
|
||||
try:
|
||||
options.set_pref("credentials_enable_service", False)
|
||||
except Exception:
|
||||
pass
|
||||
browser_path = resolve_browser_path()
|
||||
if browser_path:
|
||||
try:
|
||||
@@ -667,7 +745,7 @@ def create_browser_options():
|
||||
# Google Chrome blocks CLI unpacked extension loading; Chromium still allows it.
|
||||
if _is_google_chrome_path(browser_path):
|
||||
print(
|
||||
" [ext] skip turnstilePatch on Google Chrome (--load-extension blocked)",
|
||||
" [ext] Chrome: minimal CDP stealth only (extension blocked)",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
@@ -2700,6 +2778,7 @@ def start_browser(log_callback=None):
|
||||
try:
|
||||
TabPool.init(create_browser_options, log_callback=log_callback)
|
||||
page = TabPool.get_tab()
|
||||
apply_stealth_patches(page, log_callback=log_callback)
|
||||
if log_callback and attempt > 1:
|
||||
log_callback(f"[*] 浏览器第 {attempt} 次启动成功")
|
||||
return TabPool.get_browser(), page
|
||||
@@ -2732,7 +2811,10 @@ def prepare_browser_for_next_account(log_callback=None, force_recycle: bool = Fa
|
||||
if reuse and TabPool.get_browser() is not None and (every <= 0 or served < every):
|
||||
if TabPool.clear_session(log_callback=log_callback):
|
||||
TabPool.mark_served()
|
||||
return TabPool.get_browser(), _get_page()
|
||||
page = _get_page()
|
||||
# Re-apply stealth after session wipe / about:blank
|
||||
apply_stealth_patches(page, log_callback=log_callback)
|
||||
return TabPool.get_browser(), page
|
||||
# full recycle
|
||||
if log_callback:
|
||||
log_callback(f"[*] 浏览器完整回收(reuse={reuse}, served={served}, every={every})")
|
||||
@@ -2813,14 +2895,92 @@ return true;
|
||||
raise Exception("未找到「使用邮箱注册」按钮")
|
||||
|
||||
|
||||
def dismiss_cookie_banner(page=None, log_callback=None) -> bool:
|
||||
"""Dismiss xAI cookie consent overlay that blocks Sign up clicks.
|
||||
|
||||
Screenshot evidence: Accept All Cookies / Reject All modal sits over the form;
|
||||
clicks hit the banner instead of the submit button. /mp/track 403 is analytics only.
|
||||
"""
|
||||
page = page if page is not None else _get_page()
|
||||
if page is None:
|
||||
return False
|
||||
try:
|
||||
result = page.run_js(
|
||||
r"""
|
||||
function isVisible(node) {
|
||||
if (!node) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
const texts = [
|
||||
'accept all cookies', 'accept all', 'reject all', 'allow all',
|
||||
'accept cookies', 'got it', 'i agree', 'agree',
|
||||
'接受全部', '全部接受', '拒绝全部', '全部拒绝', '同意'
|
||||
];
|
||||
const nodes = Array.from(document.querySelectorAll(
|
||||
'button, a, [role="button"], [data-testid*="cookie"], [id*="cookie"], [class*="cookie"]'
|
||||
));
|
||||
// Prefer Accept All / Reject All (both dismiss the modal)
|
||||
let target = null;
|
||||
for (const t of texts) {
|
||||
target = nodes.find((n) => {
|
||||
if (!isVisible(n) || n.disabled) return false;
|
||||
const label = ((n.innerText || n.textContent || n.getAttribute('aria-label') || '') + '')
|
||||
.replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
return label === t || label.includes(t);
|
||||
});
|
||||
if (target) break;
|
||||
}
|
||||
// Close (X) as last resort if it is on a cookie dialog
|
||||
if (!target) {
|
||||
target = nodes.find((n) => {
|
||||
if (!isVisible(n)) return false;
|
||||
const label = ((n.innerText || n.textContent || n.getAttribute('aria-label') || '') + '')
|
||||
.replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
const nearCookie = /cookie|隐私|consent/i.test(
|
||||
(n.closest('div,section,dialog,aside') || {}).innerText || ''
|
||||
);
|
||||
return nearCookie && (label === '×' || label === 'x' || label === 'close' || label === '关闭');
|
||||
});
|
||||
}
|
||||
if (!target) {
|
||||
// Detect banner present but no clickable control matched
|
||||
const body = (document.body && document.body.innerText) || '';
|
||||
if (/accept all cookies|reject all|cookie settings/i.test(body)) return 'present-unmatched';
|
||||
return 'absent';
|
||||
}
|
||||
try { target.scrollIntoView({block:'center'}); } catch (e) {}
|
||||
try { target.click(); } catch (e) { return 'click-failed'; }
|
||||
return 'clicked:' + ((target.innerText || target.textContent || '').trim().slice(0, 40));
|
||||
"""
|
||||
)
|
||||
except Exception as exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] cookie banner dismiss err: {exc}")
|
||||
return False
|
||||
result = str(result or "")
|
||||
if result.startswith("clicked:"):
|
||||
if log_callback:
|
||||
log_callback(f"[*] 已关闭 Cookie 弹窗: {result[8:]}")
|
||||
human_sleep(0.4)
|
||||
return True
|
||||
if result == "present-unmatched" and log_callback:
|
||||
log_callback("[Debug] 检测到 Cookie 文案但未匹配到按钮")
|
||||
return False
|
||||
|
||||
|
||||
def open_signup_page(log_callback=None, cancel_callback=None):
|
||||
browser = _get_browser()
|
||||
page = _get_page()
|
||||
raise_if_cancelled(cancel_callback)
|
||||
if browser is None:
|
||||
browser, page = start_browser()
|
||||
browser, page = start_browser(log_callback=log_callback)
|
||||
if log_callback:
|
||||
log_callback("[*] 浏览器已启动")
|
||||
# Ensure CDP stealth is on this tab before navigating to CF-protected pages
|
||||
apply_stealth_patches(_get_page(), log_callback=log_callback)
|
||||
try:
|
||||
page = _get_page()
|
||||
page.get(SIGNUP_URL)
|
||||
@@ -2829,23 +2989,29 @@ def open_signup_page(log_callback=None, cancel_callback=None):
|
||||
log_callback(f"[Debug] 打开URL异常: {e}")
|
||||
try:
|
||||
TabPool.release_tab()
|
||||
page = _get_page()
|
||||
browser, page = start_browser(log_callback=log_callback)
|
||||
page.get(SIGNUP_URL)
|
||||
except Exception as e2:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] 创建新标签页异常: {e2}")
|
||||
restart_browser()
|
||||
restart_browser(log_callback=log_callback)
|
||||
page = _get_page()
|
||||
page.get(SIGNUP_URL)
|
||||
page = _get_page()
|
||||
apply_stealth_patches(page, log_callback=None)
|
||||
page.wait.doc_loaded()
|
||||
# Cookie consent often blocks Sign up / email submit (see network 403 on /mp/track is unrelated)
|
||||
dismiss_cookie_banner(page, log_callback=log_callback)
|
||||
dump_state(page, "signup-loaded")
|
||||
take_screenshot(page, "signup")
|
||||
human_sleep(2, cancel_callback)
|
||||
human_sleep(1.2, cancel_callback)
|
||||
dismiss_cookie_banner(page, log_callback=None)
|
||||
if log_callback:
|
||||
log_callback(f"[*] 当前URL: {page.url}")
|
||||
click_email_signup_button(
|
||||
log_callback=log_callback, cancel_callback=cancel_callback
|
||||
)
|
||||
dismiss_cookie_banner(_get_page(), log_callback=log_callback)
|
||||
dump_state(page, "after-email-signup-click")
|
||||
|
||||
|
||||
@@ -2870,6 +3036,8 @@ def fill_email_and_submit(timeout=15, log_callback=None, cancel_callback=None):
|
||||
page = _get_page()
|
||||
raise_if_cancelled(cancel_callback)
|
||||
check_timeout(time.time())
|
||||
# Cookie modal blocks the real Sign up button click
|
||||
dismiss_cookie_banner(page, log_callback=log_callback)
|
||||
email, dev_token = get_email_and_token()
|
||||
if not email or not dev_token:
|
||||
raise Exception("获取邮箱失败")
|
||||
@@ -2878,6 +3046,7 @@ def fill_email_and_submit(timeout=15, log_callback=None, cancel_callback=None):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
raise_if_cancelled(cancel_callback)
|
||||
dismiss_cookie_banner(page, log_callback=None)
|
||||
filled = page.run_js(
|
||||
"""
|
||||
const email = arguments[0];
|
||||
@@ -2968,7 +3137,14 @@ return 'clicked';
|
||||
if log_callback:
|
||||
log_callback(f"[*] 已填写邮箱并点击注册: {email}")
|
||||
# Wait for transition to OTP / next step (email form should leave or code inputs appear)
|
||||
transit_deadline = time.time() + max(8, int(config.get("email_submit_confirm_timeout", 30) or 30))
|
||||
try:
|
||||
confirm_sec = max(8, int(config.get("email_submit_confirm_timeout", 30) or 30))
|
||||
except Exception:
|
||||
confirm_sec = 30
|
||||
transit_deadline = time.time() + confirm_sec
|
||||
last_state = "waiting"
|
||||
reclicked = False
|
||||
cf_tried = False
|
||||
while time.time() < transit_deadline:
|
||||
raise_if_cancelled(cancel_callback)
|
||||
try:
|
||||
@@ -2981,32 +3157,95 @@ function isVisible(node) {
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
const codeInput = Array.from(document.querySelectorAll(
|
||||
'input[data-input-otp="true"], input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"]'
|
||||
'input[data-input-otp="true"], input[name="code"], input[autocomplete="one-time-code"], input[inputmode="numeric"], input[name="verificationCode"]'
|
||||
)).find((n) => isVisible(n));
|
||||
if (codeInput) return 'code-ready';
|
||||
const emailInput = Array.from(document.querySelectorAll(
|
||||
'input[data-testid="email"], input[name="email"], input[type="email"]'
|
||||
)).find((n) => isVisible(n));
|
||||
if (!emailInput) return 'email-gone';
|
||||
const err = Array.from(document.querySelectorAll('[role="alert"], .error, [data-testid*="error"]'))
|
||||
const err = Array.from(document.querySelectorAll('[role="alert"], .error, [data-testid*="error"], [class*="error"]'))
|
||||
.map((n) => (n.innerText || '').trim()).filter(Boolean)[0] || '';
|
||||
if (err) return 'error:' + err.slice(0, 120);
|
||||
const bodyTxt = (document.body && (document.body.innerText || '')) || '';
|
||||
// xAI toast bubbles: [permission_denied] HTTP 403
|
||||
if (/permission_denied|HTTP\s*403/i.test(bodyTxt)) return 'error:permission_denied HTTP 403';
|
||||
if (/rate.?limit|too many|try again later|请求过多|稍后再试/i.test(bodyTxt)) return 'rate-limited';
|
||||
const hasCf = !!document.querySelector(
|
||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], input[name="cf-turnstile-response"], div.cf-turnstile, [data-sitekey]'
|
||||
);
|
||||
if (hasCf) return 'cf-challenge';
|
||||
return 'waiting';
|
||||
""")
|
||||
except Exception:
|
||||
state = 'waiting'
|
||||
if isinstance(state, str) and state.startswith('error:'):
|
||||
state = "waiting"
|
||||
last_state = str(state or "waiting")
|
||||
if isinstance(state, str) and state.startswith("error:"):
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] 邮箱提交后页面报错: {state}")
|
||||
break
|
||||
if state in ('code-ready', 'email-gone'):
|
||||
raise Exception(f"邮箱提交被拒绝: {state[6:]}")
|
||||
if state == "rate-limited":
|
||||
raise Exception("邮箱提交触发限流 rate-limited")
|
||||
if state in ("code-ready", "email-gone"):
|
||||
if log_callback:
|
||||
log_callback(f"[*] 邮箱提交后页面状态: {state}")
|
||||
break
|
||||
dump_state(page, "email-submitted")
|
||||
take_screenshot(page, "email-submitted")
|
||||
return email, dev_token
|
||||
# CF on email step: try turnstile once then continue waiting
|
||||
if state == "cf-challenge" and not cf_tried:
|
||||
cf_tried = True
|
||||
if log_callback:
|
||||
log_callback("[*] 邮箱步骤出现 Cloudflare,尝试通过 Turnstile...")
|
||||
try:
|
||||
getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback, max_rounds=5)
|
||||
except Exception as cf_exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] 邮箱步骤 Turnstile 未过: {cf_exc}")
|
||||
# Still on email form after a few seconds: re-click submit once
|
||||
elapsed = confirm_sec - max(0.0, transit_deadline - time.time())
|
||||
if not reclicked and elapsed >= 4.0 and state in ("waiting", "cf-challenge"):
|
||||
reclicked = True
|
||||
dismiss_cookie_banner(page, log_callback=log_callback)
|
||||
try:
|
||||
again = page.run_js(
|
||||
r"""
|
||||
function isVisible(node) {
|
||||
if (!node) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width > 0 && rect.height > 0;
|
||||
}
|
||||
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button'))
|
||||
.filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true');
|
||||
const submitButton = buttons.find((node) => {
|
||||
const raw = (node.innerText || node.textContent || '').trim();
|
||||
const compact = raw.replace(/\s+/g, '').toLowerCase();
|
||||
return raw.includes('注册') || compact.includes('signup') || compact.includes('continue')
|
||||
|| compact.includes('next') || compact.includes('submit') || compact.includes('createaccount');
|
||||
});
|
||||
if (!submitButton) return 'no-submit';
|
||||
submitButton.click();
|
||||
return 'reclicked';
|
||||
"""
|
||||
)
|
||||
if log_callback:
|
||||
log_callback(f"[*] 邮箱表单仍在,二次点击提交: {again}")
|
||||
except Exception as re_exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] 二次提交失败: {re_exc}")
|
||||
human_sleep(0.6, cancel_callback)
|
||||
dump_state(page, "email-submitted")
|
||||
take_screenshot(page, "email-submitted")
|
||||
return email, dev_token
|
||||
# Timed out without reaching code step — do NOT poll mail (email never sent)
|
||||
try:
|
||||
url_now = getattr(page, "url", "") or ""
|
||||
except Exception:
|
||||
url_now = ""
|
||||
dump_state(page, "email-submit-stuck")
|
||||
take_screenshot(page, "email-submit-stuck")
|
||||
raise Exception(
|
||||
f"邮箱提交后未进入验证码页 (state={last_state}, url={url_now[:120]})"
|
||||
)
|
||||
if log_callback and clicked not in (False, None, ""):
|
||||
log_callback(f"[Debug] 邮箱已写入,但注册按钮未点到: {clicked}")
|
||||
human_sleep(0.5, cancel_callback)
|
||||
@@ -3171,13 +3410,15 @@ return 'clicked';
|
||||
|
||||
|
||||
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4):
|
||||
"""Try to obtain a Turnstile token quickly.
|
||||
"""Try to obtain a Turnstile token.
|
||||
|
||||
max_rounds default 4 (~3-5s) — fail fast and restart browser/account.
|
||||
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(
|
||||
@@ -3187,7 +3428,9 @@ def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int =
|
||||
pass
|
||||
|
||||
rounds = max(2, int(max_rounds or 4))
|
||||
for _ in range(0, rounds):
|
||||
# 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(
|
||||
@@ -3195,6 +3438,8 @@ def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int =
|
||||
try {
|
||||
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
|
||||
if (byInput) return byInput;
|
||||
const ta = document.querySelector('textarea[name="cf-turnstile-response"]');
|
||||
if (ta && ta.value) return String(ta.value).trim();
|
||||
if (window.turnstile && typeof turnstile.getResponse === 'function') {
|
||||
return String(turnstile.getResponse() || '').trim();
|
||||
}
|
||||
@@ -3208,7 +3453,7 @@ try {
|
||||
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
|
||||
return token
|
||||
|
||||
challenge_input = page.ele("@name=cf-turnstile-response")
|
||||
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5)
|
||||
if challenge_input:
|
||||
wrapper = challenge_input.parent()
|
||||
iframe = None
|
||||
@@ -3216,6 +3461,14 @@ try {
|
||||
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.5,
|
||||
)
|
||||
except Exception:
|
||||
iframe = None
|
||||
if iframe:
|
||||
try:
|
||||
iframe.run_js(
|
||||
@@ -3230,27 +3483,37 @@ 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")
|
||||
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 容器
|
||||
# 兜底:尝试触发页面上可见的 Turnstile 容器 / iframe
|
||||
page.run_js(
|
||||
"""
|
||||
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
|
||||
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
|
||||
return String(txt).toLowerCase().includes('turnstile');
|
||||
});
|
||||
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
|
||||
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
|
||||
human_sleep(0.7, cancel_callback)
|
||||
# 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 失败")
|
||||
|
||||
@@ -3282,15 +3545,16 @@ def build_profile():
|
||||
|
||||
def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):
|
||||
page = _get_page()
|
||||
apply_stealth_patches(page, log_callback=log_callback)
|
||||
check_timeout(time.time())
|
||||
dump_state(page, "profile-form")
|
||||
take_screenshot(page, "profile-form")
|
||||
given_name, family_name, password = build_profile()
|
||||
# 预热 Turnstile:等 2 秒让 iframe 初始化,插件会自动点击 checkbox
|
||||
# 预热 Turnstile:等 iframe 初始化;CDP stealth 会尝试自动点 checkbox
|
||||
if log_callback:
|
||||
log_callback("[*] 预热 Turnstile...")
|
||||
# fast mode scales human_sleep; keep a tiny floor so turnstile iframe can attach
|
||||
human_sleep(1.2 if not PERF_FLAGS.get("fast") else 0.4, cancel_callback)
|
||||
# fast mode scales human_sleep; keep enough time for turnstile iframe to attach
|
||||
human_sleep(1.6 if not PERF_FLAGS.get("fast") else 0.8, cancel_callback)
|
||||
deadline = time.time() + timeout
|
||||
form_filled_once = False
|
||||
wait_cf_since = None
|
||||
@@ -3298,21 +3562,22 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None
|
||||
last_cf_log_at = 0.0
|
||||
cf_token_fail_streak = 0
|
||||
try:
|
||||
max_cf_token_fails = max(1, int(config.get("turnstile_fast_fail_count", 2) or 2))
|
||||
# Slightly more patient by default — Chrome CDP stealth still needs CF time
|
||||
max_cf_token_fails = max(1, int(config.get("turnstile_fast_fail_count", 3) or 3))
|
||||
except Exception:
|
||||
max_cf_token_fails = 2
|
||||
max_cf_token_fails = 3
|
||||
try:
|
||||
cf_retry_after = float(config.get("turnstile_retry_after_sec", 8) or 8)
|
||||
cf_retry_after = float(config.get("turnstile_retry_after_sec", 10) or 10)
|
||||
except Exception:
|
||||
cf_retry_after = 8.0
|
||||
cf_retry_after = 10.0
|
||||
try:
|
||||
cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 5) or 5)
|
||||
cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 6) or 6)
|
||||
except Exception:
|
||||
cf_retry_gap = 5.0
|
||||
cf_retry_gap = 6.0
|
||||
try:
|
||||
turnstile_rounds = max(3, int(config.get("turnstile_max_rounds", 4) or 4))
|
||||
turnstile_rounds = max(4, int(config.get("turnstile_max_rounds", 6) or 6))
|
||||
except Exception:
|
||||
turnstile_rounds = 4
|
||||
turnstile_rounds = 6
|
||||
|
||||
while time.time() < deadline:
|
||||
raise_if_cancelled(cancel_callback)
|
||||
|
||||
Reference in new issue
Block a user