From c87b01967f0f4b3ae078ee974ef11dd051540cda Mon Sep 17 00:00:00 2001 From: chaos Date: Sun, 12 Jul 2026 14:44:02 +0800 Subject: [PATCH] Refactor registration flow, update config, replace Turnstile patch - Major refactor of grok_register_ttk.py: improved flow, error handling, logging - Update config.example.json and config.json with new fields - Replace turnstilePatch/content.js with new script.js approach - Update start.sh for better Chromium/Turnstile handling - Remove deprecated turnstilePatch/content.js --- config.example.json | 8 +- config.json | 15 +- grok_register_ttk.py | 1000 +++++++++++++++++++++++++--------- start.sh | 26 +- turnstilePatch/content.js | 132 ----- turnstilePatch/manifest.json | 14 +- turnstilePatch/script.js | 24 + 7 files changed, 794 insertions(+), 425 deletions(-) delete mode 100644 turnstilePatch/content.js create mode 100644 turnstilePatch/script.js diff --git a/config.example.json b/config.example.json index 6d00d12..eda5792 100644 --- a/config.example.json +++ b/config.example.json @@ -99,14 +99,14 @@ "turnstile_retry_limit": 3, "// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)", "turnstile_stuck_timeout": 150, - "// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(服务器建议 4)", - "turnstile_fast_fail_count": 4, + "// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(参考 Git-creat7 建议 3)", + "turnstile_fast_fail_count": 3, "// 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, + "// turnstile_max_rounds": "单次 getTurnstileToken 内 reset+click 轮数(参考 15-20)", + "turnstile_max_rounds": 16, "// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)", "sso_timeout_base": 240, "sso_timeout_max": 480, diff --git a/config.json b/config.json index b39f23a..3450cb8 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,7 @@ "proxy": "", "enable_nsfw": true, "register_count": 1, - "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", "grok2api_auto_add_local": false, "grok2api_local_token_file": "", "grok2api_pool_name": "ssoBasic", @@ -46,9 +46,9 @@ "mail_poll_interval": 1, "mail_retry_count": 3, "code_form_timeout": 180, - "profile_timeout": 50, + "profile_timeout": 180, "turnstile_retry_limit": 3, - "turnstile_stuck_timeout": 150, + "turnstile_stuck_timeout": 180, "sso_timeout_base": 60, "sso_timeout_max": 480, "sso_progress_extension": 120, @@ -98,8 +98,11 @@ "gmail_imap_fallback": true, "gmail_imap_fallback_timeout_sec": 45, "gmail_imap_fallback_resend_sec": 10, - "turnstile_fast_fail_count": 4, - "turnstile_retry_after_sec": 12, + "turnstile_fast_fail_count": 3, + "turnstile_retry_after_sec": 10, "turnstile_retry_gap_sec": 8, - "turnstile_max_rounds": 8 + "turnstile_max_rounds": 20, + "browser_prefer": "chrome", + "browser_path": "/usr/bin/google-chrome-stable", + "browser_headless": false } diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 177b326..2f06f0d 100644 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -828,15 +828,52 @@ def _windows_browser_candidates() -> tuple[str, ...]: _WIN_BROWSERS = _windows_browser_candidates() -# Chromium first (turnstilePatch / --load-extension). On Windows, Playwright -# chromium-* paths contain "chromium" and pass _is_google_chrome_path=False. + + +def _playwright_chromium_candidates() -> tuple[str, ...]: + """Prefer Playwright's Chromium (no snap AppArmor; ReinerBRO/grok-register path).""" + roots = [ + os.path.expanduser("~/.cache/ms-playwright"), + os.path.join(os.environ.get("LOCALAPPDATA") or "", "ms-playwright"), + ] + out: list[str] = [] + for root in roots: + if not root or not os.path.isdir(root): + continue + try: + for name in sorted(os.listdir(root), reverse=True): + if not name.lower().startswith("chromium"): + continue + # linux: chromium-XXXX/chrome-linux64/chrome + # win: chromium-XXXX/chrome-win64/chrome.exe + for rel in ( + ("chrome-linux64", "chrome"), + ("chrome-linux", "chrome"), + ("chrome-win64", "chrome.exe"), + ("chrome-win", "chrome.exe"), + ): + cand = os.path.join(root, name, *rel) + if os.path.isfile(cand): + out.append(cand) + except Exception: + pass + return tuple(out) + + +# Chromium first (turnstilePatch / --load-extension). +# Playwright Chromium preferred over distro/snap (AppArmor + broken wrapper issues). +# On Windows, Playwright chromium-* paths contain "chromium" and pass _is_google_chrome_path=False. CHROMIUM_CANDIDATES = ( - "/usr/bin/chromium", - "/usr/bin/chromium-browser", - "/usr/lib/chromium/chromium", - "/usr/lib/chromium-browser/chromium-browser", - "/snap/bin/chromium", # keep as /snap/bin/chromium — do NOT resolve to /usr/bin/snap -) + tuple(p for p in _WIN_BROWSERS if "google" not in p.lower() or "chromium" in p.lower()) + _playwright_chromium_candidates() + + ( + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + "/usr/lib/chromium/chromium", + "/usr/lib/chromium-browser/chromium-browser", + "/snap/bin/chromium", # keep as /snap/bin/chromium — do NOT resolve to /usr/bin/snap + ) + + tuple(p for p in _WIN_BROWSERS if "google" not in p.lower() or "chromium" in p.lower()) +) CHROME_CANDIDATES = ( "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", @@ -1073,11 +1110,21 @@ def create_browser_options(headless=None): # Decide headless first so we can choose flag set (headed ≈ reference repo). use_hl = should_use_headless(headless, log_hint=True) - for flag in CHROMIUM_BASE_FLAGS: - options.set_argument(flag) - if use_hl: - for flag in CHROMIUM_HEADLESS_EXTRA_FLAGS: + min_browser = str(os.environ.get("GROK_MIN_BROWSER", "") or "").strip().lower() in ( + "1", "true", "yes", "on", + ) + if min_browser: + print(" [browser] minimal flags mode (Git-creat7 style)", flush=True) + options.set_argument("--disable-blink-features=AutomationControlled") + options.set_argument("--no-first-run") + if use_hl: + options.set_argument("--disable-gpu") + else: + for flag in CHROMIUM_BASE_FLAGS: options.set_argument(flag) + if use_hl: + for flag in CHROMIUM_HEADLESS_EXTRA_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) @@ -3293,81 +3340,380 @@ return true; def dismiss_cookie_banner(page=None, log_callback=None) -> bool: - """Dismiss xAI cookie consent overlay that blocks Sign up clicks. + """Dismiss xAI/OneTrust cookie consent that blocks Sign up / Turnstile. - 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. + Historical success path always did a real Accept All Cookies click. + force-hide alone often leaves preference-center DOM and CF stays token=0. + Prefer DrissionPage native element clicks; JS/force-hide only as fallback. + + IMPORTANT: once dismissed on a page, do NOT keep re-clicking OneTrust buttons + during Turnstile Verifying — that restarts CF and leaves token=0 forever. """ page = page if page is not None else _get_page() if page is None: return False - try: - result = page.run_js( - r""" + + # Sticky per-page: avoid click storms while Turnstile is Verifying + if getattr(page, "_grok_cookie_dismissed", False): + # Still force-hide if preference center somehow re-opened + try: + still = page.run_js( + r""" +try { + const ban = document.querySelector('#onetrust-banner-sdk, #onetrust-pc-sdk'); + if (!ban) return false; + const s = getComputedStyle(ban); + const r = ban.getBoundingClientRect(); + return s.display !== 'none' && s.visibility !== 'hidden' && r.width > 40 && r.height > 40; +} catch(e) { return false; } + """ + ) + if not still: + return False + except Exception: + return False + + # Ordered: accept/allow first (historical winner), then reject/confirm/close. + native_texts = [ + "Accept All Cookies", + "Accept All", + "Allow All", + "Allow all", + "接受所有 Cookie", + "接受所有", + "接受全部", + "全部接受", + "Reject All", + "Reject all", + "全部拒绝", + "拒绝全部", + "Confirm My Choices", + "Confirm my choices", + "确认我的选择", + ] + # Prefer real Accept/Allow All only. Do NOT click save-preference first — + # that only saves partial choices and CF Verifying often never completes. + css_ids = [ + "#onetrust-accept-btn-handler", + "#accept-recommended-btn-handler", + "#onetrust-reject-all-handler", + ".ot-pc-refuse-all-handler", + "#close-pc-btn-handler", + ".onetrust-close-btn-handler", + # save-preference last resort only + ".save-preference-btn-handler", + ] + + def _cookie_still_present() -> bool: + try: + return bool( + page.run_js( + r""" +function vis(n){ + if(!n) return false; + try{ + const s=getComputedStyle(n); + if(s.display==='none'||s.visibility==='hidden'||Number(s.opacity)===0) return false; + const r=n.getBoundingClientRect(); + return r.width>0 && r.height>0; + }catch(e){return false;} +} +if (['#onetrust-banner-sdk','#onetrust-consent-sdk','#onetrust-pc-sdk','.onetrust-pc-dark-filter'] + .some(sel => vis(document.querySelector(sel)))) return true; +return Array.from(document.querySelectorAll('button,a,[role="button"]')).some(n=>{ + if(!vis(n)) return false; + const t=((n.innerText||n.textContent||'')+'').toLowerCase(); + return /accept all cookies|reject all|cookie settings|allow all|confirm my choices|接受所有|全部拒绝/.test(t); +}); + """ + ) + ) + except Exception: + return False + + def _native_click_once() -> str: + # 1) OneTrust ids via CSS + for sel in css_ids: + try: + el = page.ele(f"css:{sel}", timeout=0.25) + except Exception: + el = None + if el is None: + continue + try: + el.click() + return f"clicked:{sel}" + except Exception: + try: + el.click(by_js=True) + return f"clicked-js:{sel}" + except Exception: + pass + # 2) Enumerate ALL buttons/links in Python (more reliable than text: locator) + prefer_keys = [ + "accept all cookies", "accept all", "allow all", "allow all cookies", + "接受所有 cookie", "接受所有", "接受全部", "全部接受", "允许全部", + "reject all", "reject all cookies", "全部拒绝", "拒绝全部", + # confirm/save last — partial consent often leaves CF Verifying forever + "confirm my choices", "确认我的选择", "save preferences", "save my preferences", + ] + try: + candidates = page.eles("tag:button", timeout=0.4) or [] + except Exception: + candidates = [] + try: + more = page.eles("css:a,[role='button']", timeout=0.2) or [] + candidates = list(candidates) + list(more) + except Exception: + pass + best = None + best_label = "" + best_rank = 10**9 + for el in candidates: + try: + label = ( + (getattr(el, "text", None) or el.attr("aria-label") or el.attr("title") or "") + .replace("\n", " ") + .strip() + .lower() + ) + except Exception: + continue + if not label or "cookie settings" in label or "cookies settings" in label: + continue + if label in ("filter icon", "clear", "apply", "back button", "cancel", "search…", "search..."): + continue + for rank, key in enumerate(prefer_keys): + if key == label or key in label: + if rank < best_rank: + best = el + best_label = label + best_rank = rank + break + if best is not None: + for mode in ("click", "js", "actions"): + try: + if mode == "click": + best.click() + elif mode == "js": + best.click(by_js=True) + else: + page.actions.click(best) + return f"clicked:{best_label[:40]}" + except Exception: + continue + # 3) Visible text match (DrissionPage text locator fallback) + for text in native_texts: + el = None + for loc in (f"text:{text}", f"tag:button@@text():{text}", f"@text():{text}"): + try: + el = page.ele(loc, timeout=0.15) + except Exception: + el = None + if el is not None: + break + if el is None: + continue + try: + el.click() + return f"clicked:{text}" + except Exception: + try: + el.click(by_js=True) + return f"clicked-js:{text}" + except Exception: + try: + page.actions.click(el) + return f"clicked-actions:{text}" + except Exception: + pass + return "none" + + def _js_click_once() -> str: + try: + return str( + 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; + try { + const style = window.getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false; + const rect = node.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } catch (e) { return false; } } -const texts = [ - 'accept all cookies', 'accept all', 'reject all', 'allow all', - 'accept cookies', 'got it', 'i agree', 'agree', - // zh: UI shows "接受所有 Cookie" / "全部拒绝" (not only 接受全部) - '接受所有 cookie', '接受所有', '接受全部', '全部接受', - '拒绝全部', '全部拒绝', '同意' +function labelOf(n) { + return ((n.innerText || n.textContent || n.getAttribute('aria-label') || n.getAttribute('title') || '') + '') + .replace(/\s+/g, ' ').trim().toLowerCase(); +} +const prefer = [ + 'accept all cookies', 'accept all', 'allow all', 'allow all cookies', + 'confirm my choices', 'confirm my choice', 'save preferences', + 'reject all', 'reject all cookies', + '接受所有 cookie', '接受所有', '接受全部', '全部接受', '允许全部', '确认我的选择', + '拒绝全部', '全部拒绝' ]; -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) => { +for (const sel of [ + '#onetrust-accept-btn-handler','#accept-recommended-btn-handler', + '#onetrust-reject-all-handler','.ot-pc-refuse-all-handler', + '.save-preference-btn-handler','button.save-preference-btn-handler', + '.onetrust-close-btn-handler','#close-pc-btn-handler' +]) { + const el = document.querySelector(sel); + if (el && isVisible(el) && !el.disabled) { + try { el.click(); return 'clicked:' + sel; } catch (e) {} + } +} +const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]')); +for (const t of prefer) { + const 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(); + const label = labelOf(n); + if (!label || label.includes('cookie settings') || label.includes('cookies settings')) return false; + if (['filter icon','clear','apply','back button','cancel'].includes(label)) return false; return label === t || label.includes(t); }); - if (target) break; + if (target) { + try { target.click(); return 'clicked:' + ((target.innerText||t)+'').trim().slice(0,40); } catch(e) {} + } } -// 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 === '关闭'); +return 'none'; + """ + ) + or "none" + ) + except Exception as exc: + if log_callback: + log_callback(f"[Debug] cookie js dismiss err: {exc}") + return "err" + + def _force_hide() -> str: + try: + page.run_js( + r""" +try { + document.querySelectorAll( + '#onetrust-banner-sdk, #onetrust-consent-sdk, .onetrust-pc-dark-filter, #onetrust-pc-sdk, #ot-sdk-btn-floating, [class*="cookie-banner"], [id*="cookie-banner"]' + ).forEach((el) => { + try { + el.style.setProperty('display', 'none', 'important'); + el.style.setProperty('visibility', 'hidden', 'important'); + el.style.setProperty('pointer-events', 'none', 'important'); + el.setAttribute('aria-hidden', 'true'); + } catch (e) {} }); -} -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)); + document.documentElement && document.documentElement.classList.remove('ot-fade-in', 'onetrust-no-scroll'); + if (document.body) { + document.body.classList.remove('ot-fade-in', 'onetrust-no-scroll'); + document.body.style.overflow = ''; + } +} catch (e) {} + """ + ) + return "force-hide" + except Exception: + return "force-hide-failed" + + def _seed_optanon_cookies() -> None: + """Best-effort: mark OneTrust already accepted so banner/PC does not block CF.""" + try: + page.run_js( + r""" +try { + const domains = [location.hostname, '.x.ai', '.accounts.x.ai']; + const now = new Date(); + const exp = new Date(now.getTime() + 365*24*3600*1000).toUTCString(); + const pairs = [ + 'OptanonAlertBoxClosed=' + now.toISOString(), + // groups: necessary + performance/functional/targeting commonly "1" + 'OptanonConsent=isGpcEnabled=0&datestamp=' + encodeURIComponent(now.toString()) + + '&version=202401.1.0&isIABGlobal=false&hosts=&consentId=auto&interactionCount=1&landingPath=NotLandingPage&groups=C0001%3A1%2CC0002%3A1%2CC0003%3A1%2CC0004%3A1&AwaitingReconsent=false' + ]; + for (const d of domains) { + for (const p of pairs) { + try { document.cookie = p + '; path=/; domain=' + d + '; expires=' + exp + '; SameSite=Lax'; } catch (e) {} + try { document.cookie = p + '; path=/; expires=' + exp + '; SameSite=Lax'; } catch (e) {} + } + } + try { + if (window.OneTrust && typeof OneTrust.AllowAll === 'function') OneTrust.AllowAll(); + } catch (e) {} + try { + if (window.OneTrust && typeof OneTrust.Close === 'function') OneTrust.Close(); + } catch (e) {} +} catch (e) {} + """ + ) + except Exception: + pass + + present = _cookie_still_present() + # Seed consent cookies first — often enough for OneTrust to stop re-opening PC. + _seed_optanon_cookies() + human_sleep(0.15) + + # Even if presence probe is flaky, still try a few click rounds when called + # from profile/signup — historical success always got "Accept All Cookies". + any_ok = False + clicked_label = "" + max_rounds = 5 if present else 3 + for round_i in range(max_rounds): + if round_i > 0 and not _cookie_still_present() and any_ok: + break + result = _native_click_once() + if result == "none": + result = _js_click_once() + # After first real click attempt, also force-hide residual PC/banner + if result.startswith("clicked") or (result in ("none", "err") and round_i >= 1): + if result.startswith("clicked"): + any_ok = True + clicked_label = result.split(":", 1)[-1] + if log_callback and round_i == 0: + log_callback(f"[*] 已关闭 Cookie 弹窗: {clicked_label}") + _seed_optanon_cookies() + _force_hide() + any_ok = True + human_sleep(0.3) + # Stop after one successful accept + hide — do not keep re-clicking + if clicked_label or not _cookie_still_present(): + break + else: + human_sleep(0.12) + # Final hard hide — remove entire OneTrust tree so it cannot re-open mid-CF + try: + page.run_js( + r""" +try { + ['#onetrust-consent-sdk','#onetrust-banner-sdk','#onetrust-pc-sdk','.onetrust-pc-dark-filter','#ot-sdk-btn-floating'] + .forEach((sel) => { + document.querySelectorAll(sel).forEach((el) => { + try { el.remove(); } catch (e) { + try { + el.style.setProperty('display','none','important'); + el.style.setProperty('visibility','hidden','important'); + el.style.setProperty('pointer-events','none','important'); + } catch (e2) {} + } + }); + }); + if (document.body) { + document.body.classList.remove('ot-fade-in','onetrust-no-scroll'); + document.body.style.overflow = ''; + } +} catch (e) {} """ ) - 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 + any_ok = True + except Exception: + if _cookie_still_present(): + _force_hide() + any_ok = True + try: + page._grok_cookie_dismissed = True + except Exception: + pass + return any_ok def open_signup_page(log_callback=None, cancel_callback=None): @@ -3809,17 +4155,78 @@ return 'clicked'; def _read_turnstile_token(page): - """Read cf-turnstile-response / turnstile.getResponse if present.""" + """Read cf-turnstile-response / turnstile.getResponse if present. + + Prefer DrissionPage native attr read (sees closed-shadow-ish values better), + then JS walk of open shadow roots + turnstile.getResponse(). + """ + # Path 1: native element value (historical success tokens were length ~773) + for loc in ( + "@name=cf-turnstile-response", + "css:input[name='cf-turnstile-response']", + "css:textarea[name='cf-turnstile-response']", + "tag:input@@name=cf-turnstile-response", + ): + try: + el = page.ele(loc, timeout=0.15) + except Exception: + el = None + if el is None: + continue + try: + val = el.attr("value") + except Exception: + val = None + if not val: + try: + val = el.value + except Exception: + val = None + if not val: + try: + val = page.run_js("return arguments[0] ? String(arguments[0].value||'') : '';", el) + except Exception: + val = None + val = str(val or "").strip() + if len(val) >= 80: + return val + + # Path 2: JS light DOM + open shadow + API try: token = page.run_js( """ 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(); + function walk(root, acc, depth) { + if (!root || depth > 6) return acc; + try { + const nodes = root.querySelectorAll + ? root.querySelectorAll('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]') + : []; + for (const n of nodes) acc.push(n); + } catch (e) {} + let children = []; + try { children = root.querySelectorAll ? root.querySelectorAll('*') : []; } catch (e) { children = []; } + for (const el of children) { + try { + if (el.shadowRoot) walk(el.shadowRoot, acc, depth + 1); + } catch (e) {} + } + return acc; + } + const inputs = walk(document, [], 0); + for (const input of inputs) { + const v = String((input && input.value) || '').trim(); + if (v) return v; + } if (window.turnstile && typeof turnstile.getResponse === 'function') { - return String(turnstile.getResponse() || '').trim(); + try { + const r = String(turnstile.getResponse() || '').trim(); + if (r) return r; + } catch (e) {} + try { + const r2 = String(turnstile.getResponse('') || '').trim(); + if (r2) return r2; + } catch (e) {} } return ''; } catch(e) { return ''; } @@ -3831,28 +4238,71 @@ try { def _turnstile_diag(page) -> dict: - """Lightweight CF widget diagnostics for logs (no secrets).""" + """Lightweight CF widget diagnostics for logs (no secrets). + + Searches light DOM + open shadow roots. Detects managed-mode "Verifying..." + spinner text so callers can avoid reset/click storms. + """ 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]')); + function walk(root, acc, depth) { + if (!root || depth > 6) return acc; + try { + const q = root.querySelectorAll + ? root.querySelectorAll( + 'input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"], ' + + 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], ' + + 'div.cf-turnstile, [data-sitekey], iframe[title*="Cloudflare"], iframe[title*="cloudflare"], iframe[title*="Widget"]' + ) + : []; + for (const n of q) acc.push(n); + } catch (e) {} + let children = []; + try { children = root.querySelectorAll ? root.querySelectorAll('*') : []; } catch (e) { children = []; } + for (const el of children) { + try { if (el.shadowRoot) walk(el.shadowRoot, acc, depth + 1); } catch (e) {} + } + return acc; + } + const all = walk(document, [], 0); + const inputs = all.filter((n) => n && (n.tagName === 'INPUT' || n.tagName === 'TEXTAREA')); + const iframes = all.filter((n) => n && n.tagName === 'IFRAME'); + const widgets = all.filter((n) => n && n.tagName !== 'IFRAME' && n.tagName !== 'INPUT' && n.tagName !== 'TEXTAREA'); + const input = inputs[0] || document.querySelector('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]'); const srcs = iframes.slice(0, 3).map((f) => { - try { return String(f.src || '').slice(0, 120); } catch(e) { return ''; } + try { return String(f.src || f.getAttribute('src') || '').slice(0, 120); } catch(e) { return ''; } }); + const bodyText = ((document.body && document.body.innerText) || '').slice(0, 4000); + const verifying = /正在验证|verifying|checking|just a moment|请完成|人机验证/i.test(bodyText); + // success checkmark / aria + const successish = /成功|success|verified/i.test(bodyText) && !!input && String(input.value||'').trim().length >= 80; + let sitekey = ''; + try { + const w = widgets[0] || document.querySelector('[data-sitekey], div.cf-turnstile'); + if (w) sitekey = String(w.getAttribute('data-sitekey') || '').slice(0, 16); + } catch (e) {} + // iframe box size (0x0 means not painted) + let iframeBox = ''; + try { + if (iframes[0]) { + const r = iframes[0].getBoundingClientRect(); + iframeBox = Math.round(r.width) + 'x' + Math.round(r.height); + } + } catch (e) {} 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) : '', + sitekey: sitekey, iframeSrc0: srcs[0] || '', + iframeBox: iframeBox, turnstileApi: !!(window.turnstile && typeof turnstile.getResponse === 'function'), webdriver: !!navigator.webdriver, + verifying: verifying, + successish: successish, }; } catch(e) { return { err: String(e && e.message || e) }; @@ -3867,9 +4317,13 @@ try { def _click_turnstile_widget(page, log_callback=None) -> str: """Best-effort click path for interactive Turnstile checkbox. + Ported from ReinerBRO/grok-register + Git-creat7/grokRegister-cpa: + input[name=cf-turnstile-response] → parent shadow → iframe + → patch screenX/Y inside iframe → body.shadow_root → input.click() + Returns a short tag describing which path succeeded (or 'none'). """ - # Path A: shadow-root checkbox under cf-turnstile-response parent + # Path A: classic ReinerBRO shadow-root checkbox try: challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.6) except Exception: @@ -3890,22 +4344,29 @@ def _click_turnstile_widget(page, log_callback=None) -> str: except Exception: iframe = None if iframe: + # Match ReinerBRO exactly: value: not get: (CF reads own props). 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); +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} +let screenX = getRandomInt(800, 1200); +let screenY = getRandomInt(400, 600); +Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX }); +Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY }); try { - Object.defineProperty(MouseEvent.prototype, 'screenX', { get: function(){ return sx; } }); - Object.defineProperty(MouseEvent.prototype, 'screenY', { get: function(){ return sy; } }); -} catch(e) {} + if (typeof PointerEvent !== 'undefined' && PointerEvent.prototype) { + Object.defineProperty(PointerEvent.prototype, 'screenX', { value: screenX }); + Object.defineProperty(PointerEvent.prototype, 'screenY', { value: screenY }); + } +} catch (e) {} """ ) except Exception: pass - # A1: body.shadow_root input / .mark + # A1: classic ReinerBRO path — body.shadow_root → tag:input → click 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") @@ -3932,83 +4393,56 @@ try { except Exception: pass - # Path B: DOM query + synthetic pointer events + # Path B: DOM query + synthetic pointer events (Git-creat7 fallback) 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]' -)); +)).filter((n) => { + const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || ''); + return String(txt).toLowerCase().includes('turnstile') + || String(n.getAttribute?.('src') || '').includes('challenges.cloudflare.com') + || n.hasAttribute?.('data-sitekey'); +}); let ok = false; for (const n of nodes) { - if (firePointer(n)) ok = true; + try { + if (typeof n.click === 'function') { n.click(); ok = true; } + } catch (e) {} } return ok; """ ) if clicked: - return "js-pointer" + return "js-click" except Exception: pass return "none" -def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): - """Try to obtain a Turnstile token. +def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20): + """batch100-era solver (55f56d2/c56d0d2) with longer waits. - 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. + Historical success: Google Chrome + CDP stealth, Accept All Cookies, + reset + shadow checkbox click, token ~773. """ 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}" + try: + page.run_js( + "try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}" ) + except Exception: + pass + + rounds = max(8, int(max_rounds or 20)) + # Initial settle for widget attach + cf_sleep(1.2 if not PERF_FLAGS.get("fast") else 0.9, cancel_callback, floor=0.7) - last_click = "none" for round_i in range(0, rounds): raise_if_cancelled(cancel_callback) try: @@ -4018,51 +4452,35 @@ def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 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 + # Historical pure JS read path as well + token = page.run_js( + """ +try { + const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); + if (byInput) return byInput; + if (window.turnstile && typeof turnstile.getResponse === 'function') { + return String(turnstile.getResponse() || '').trim(); + } + return ''; +} catch(e) { return ''; } + """ + ) + token = str(token or "").strip() + if len(token) >= 80: + if log_callback: + log_callback(f"[*] Turnstile 已通过,token长度={len(token)}") + return 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}") + path = _click_turnstile_widget(page, log_callback=log_callback) + if log_callback and (round_i == 0 or round_i % 3 == 0): + log_callback(f"[Debug] Turnstile click path={path} round={round_i+1}/{rounds}") + except Exception: + pass + # Progressive wait like c56d0d2, but wall-clock via cf_sleep + cf_sleep(0.9 + 0.15 * min(round_i, 6), cancel_callback, floor=0.8) - # 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}") + raise Exception("Turnstile 获取 token 失败") - # 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(): @@ -4091,22 +4509,41 @@ def build_profile(): def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None): + """Fill profile form and submit after Turnstile is solved. + + CF wait / retry loop follows Git-creat7/grokRegister-cpa: + fill → if wait-cloudflare → after ~12s call getTurnstileToken (reset+click) + → re-fill token into hidden input → submit. + """ page = _get_page() apply_stealth_patches(page, log_callback=log_callback) check_timeout(time.time()) + # Cookie overlay often reappears on profile step and blanks Turnstile (iframe=0). + dismiss_cookie_banner(page, log_callback=log_callback) dump_state(page, "profile-form") take_screenshot(page, "profile-form") given_name, family_name, password = build_profile() - # 预热 Turnstile:等 iframe 初始化;CDP stealth / extension 会尝试自动点 checkbox + # 预热 Turnstile:cookie 只关一次(sticky),然后给 Verifying 时间 if log_callback: log_callback("[*] 预热 Turnstile...") + try: + dismiss_cookie_banner(page, log_callback=log_callback) + except Exception: + pass # 必须用 cf_sleep:--fast 下 human_sleep 会把预热压到 <0.3s,iframe 来不及挂载 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)}") + else: + diag_pre = _turnstile_diag(page) + if log_callback: + log_callback( + f"[Debug] 预热后 Turnstile: tokenLen={diag_pre.get('tokenLen', 0)} " + f"hasInput={diag_pre.get('hasInput')} api={diag_pre.get('turnstileApi')} " + f"iframe={diag_pre.get('iframeCount', 0)}" + ) except Exception: pass deadline = time.time() + timeout @@ -4116,12 +4553,13 @@ 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: - # 服务器 Xvfb 上 CF 常需多轮;默认比桌面更耐心 - max_cf_token_fails = max(2, int(config.get("turnstile_fast_fail_count", 4) or 4)) + # Git-creat7 风格:卡住就二次复用;允许多轮,别太早放弃 + max_cf_token_fails = max(3, int(config.get("turnstile_fast_fail_count", 3) or 3)) except Exception: - max_cf_token_fails = 4 + max_cf_token_fails = 3 try: - cf_retry_after = float(config.get("turnstile_retry_after_sec", 12) or 12) + # 参考实现:等 12s 后开始 getTurnstileToken,间隔 8s + cf_retry_after = float(config.get("turnstile_retry_after_sec", 45) or 45) except Exception: cf_retry_after = 12.0 try: @@ -4129,9 +4567,54 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None except Exception: cf_retry_gap = 8.0 try: - turnstile_rounds = max(5, int(config.get("turnstile_max_rounds", 8) or 8)) + # 参考 15-20 轮 reset+click;默认抬到 16 + turnstile_rounds = max(12, int(config.get("turnstile_max_rounds", 16) or 16)) except Exception: - turnstile_rounds = 8 + turnstile_rounds = 16 + + def _sync_cf_token(token: str): + return page.run_js( + """ +const token = String(arguments[0] || '').trim(); +const cfInput = document.querySelector('input[name="cf-turnstile-response"]'); +if (!cfInput || !token) return false; +const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; +if (nativeSetter) nativeSetter.call(cfInput, token); +else cfInput.value = token; +cfInput.dispatchEvent(new Event('input', { bubbles: true })); +cfInput.dispatchEvent(new Event('change', { bubbles: true })); +return String(cfInput.value || '').trim().length; + """, + token, + ) + + def _retry_turnstile(reason: str) -> bool: + nonlocal cf_token_fail_streak, last_cf_retry_at + if log_callback: + log_callback(f"[*] {reason}") + try: + token = getTurnstileToken( + log_callback=log_callback, + cancel_callback=cancel_callback, + max_rounds=turnstile_rounds, + ) + if token: + cf_token_fail_streak = 0 + synced = _sync_cf_token(token) + if log_callback: + log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}") + last_cf_retry_at = time.time() + return True + except Exception as cf_exc: + cf_token_fail_streak += 1 + if log_callback: + log_callback( + f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}" + ) + if cf_token_fail_streak >= max_cf_token_fails: + raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") + last_cf_retry_at = time.time() + return False while time.time() < deadline: raise_if_cancelled(cancel_callback) @@ -4184,12 +4667,12 @@ const ok3 = setInputValue(passwordInput, password); if (!ok1 || !ok2 || !ok3) return 'fill-failed'; -const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => { +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]')).filter((node) => { return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true'; }); const submitBtn = buttons.find((node) => { const t = (node.innerText || node.textContent || '').replace(/\\s+/g, '').toLowerCase(); - return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount'); + return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount') || t.includes('completesignup'); }); // 必须等待 Cloudflare 校验通过后再提交 @@ -4217,51 +4700,37 @@ return 'filled-no-submit'; now = time.time() if wait_cf_since is None: wait_cf_since = now + token_len = filled.split(":", 1)[1] if ":" in filled else "0" if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5): - token_len = filled.split(":", 1)[1] if ":" in filled 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 人机验证通过... " + f"当前token长度={token_len} waited={waited:.0f}s" + ) 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: + # Git-creat7: token 为空时多等 1-3s + if token_len == "0": + pause_seconds = random.uniform(1.0, 3.0) + if log_callback and (now - last_cf_log_at >= 4): + log_callback(f"[*] Cloudflare token 为空,暂停 {pause_seconds:.1f}s 后继续检测") + last_cf_log_at = now + sleep_with_cancel(pause_seconds, cancel_callback) + # Cookie overlay can reappear mid-wait and keep iframe=0 + if wait_cf_since and (now - wait_cf_since) >= 2.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}") + dismiss_cookie_banner(page, log_callback=None) except Exception: pass - # 卡住后自动二次复用 Turnstile 组件 - if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: - if log_callback: - log_callback("[*] Cloudflare 验证卡住,开始二次复用 Turnstile...") + # 轻点一次(参考会在 getTurnstileToken 内反复点;外层也补一次) + if now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 3.0: try: - token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback, max_rounds=turnstile_rounds) - if token: - cf_token_fail_streak = 0 - synced = page.run_js( - """ -const token = String(arguments[0] || '').trim(); -const cfInput = document.querySelector('input[name="cf-turnstile-response"]'); -if (!cfInput || !token) return false; -const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; -if (nativeSetter) nativeSetter.call(cfInput, token); -else cfInput.value = token; -cfInput.dispatchEvent(new Event('input', { bubbles: true })); -cfInput.dispatchEvent(new Event('change', { bubbles: true })); -return String(cfInput.value || '').trim().length; - """, - token, - ) - if log_callback: - log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}") - except Exception as cf_exc: - cf_token_fail_streak += 1 - if log_callback: - log_callback(f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}") - if cf_token_fail_streak >= max_cf_token_fails: - raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") - last_cf_retry_at = now - cf_sleep(1.0, cancel_callback, floor=0.7) + _click_turnstile_widget(page) + except Exception: + pass + # 卡住后二次复用 Turnstile 组件(reset + shadow click) + if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: + _retry_turnstile("Cloudflare 验证卡住,开始二次复用 Turnstile...") + sleep_with_cancel(0.8, cancel_callback) continue if filled in ("ready-to-submit", "filled-no-submit"): @@ -4293,14 +4762,26 @@ if (cfPresent) { if (!solvedByToken) return 'wait-cloudflare:' + token.length; } -const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => { +function buttonText(node) { + return [ + node.innerText, + node.textContent, + node.getAttribute('value'), + node.getAttribute('aria-label'), + node.getAttribute('title'), + ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); +} +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]')).filter((node) => { return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true'; }); const submitBtn = buttons.find((node) => { - const t = (node.innerText || node.textContent || '').replace(/\s+/g, '').toLowerCase(); - return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount'); + const t = buttonText(node).replace(/\s+/g, '').toLowerCase(); + return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount') || t.includes('completesignup'); }); -if (!submitBtn) return 'no-submit-button'; +if (!submitBtn) { + const visibleTexts = buttons.map(buttonText).filter(Boolean).slice(0, 8).join(' | '); + return 'no-submit-button:' + visibleTexts; +} submitBtn.focus(); submitBtn.click(); return 'submitted'; @@ -4314,44 +4795,19 @@ return 'submitted'; if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5): token_len = submit_state.split(":", 1)[1] if ":" in submit_state 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 人机验证通过后再提交... " + f"当前token长度={token_len} waited={waited:.0f}s" + ) last_cf_log_at = now - if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 4.0: + if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 3.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 log_callback: - log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...") - try: - token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback, max_rounds=turnstile_rounds) - if token: - cf_token_fail_streak = 0 - synced = page.run_js( - """ -const token = String(arguments[0] || '').trim(); -const cfInput = document.querySelector('input[name="cf-turnstile-response"]'); -if (!cfInput || !token) return false; -const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; -if (nativeSetter) nativeSetter.call(cfInput, token); -else cfInput.value = token; -cfInput.dispatchEvent(new Event('input', { bubbles: true })); -cfInput.dispatchEvent(new Event('change', { bubbles: true })); -return String(cfInput.value || '').trim().length; - """, - token, - ) - if log_callback: - log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}") - except Exception as cf_exc: - cf_token_fail_streak += 1 - if log_callback: - log_callback(f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}") - if cf_token_fail_streak >= max_cf_token_fails: - raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") - last_cf_retry_at = now - cf_sleep(1.0, cancel_callback, floor=0.7) + _retry_turnstile("提交前仍卡住,自动再次复用 Turnstile...") + sleep_with_cancel(0.8, cancel_callback) continue if submit_state == "submitted": @@ -4359,8 +4815,8 @@ return String(cfInput.value || '').trim().length; log_callback(f"[*] 已填写注册资料并提交: {given_name} {family_name}") return {"given_name": given_name, "family_name": family_name, "password": password} wait_cf_since = None - if submit_state == "no-submit-button" and log_callback: - log_callback("[Debug] 未找到提交按钮,继续等待页面稳定...") + if isinstance(submit_state, str) and submit_state.startswith("no-submit-button") and log_callback: + log_callback(f"[Debug] 未找到提交按钮,继续等待页面稳定... {submit_state[17:80]}") human_sleep(0.5, cancel_callback) diff --git a/start.sh b/start.sh index 5784d34..cd80e1d 100755 --- a/start.sh +++ b/start.sh @@ -419,9 +419,31 @@ _resolve_bin() { return 1 } +find_playwright_chromium() { + # Playwright Chromium 无 snap AppArmor 限制,ReinerBRO/grok-register 同款优先路径 + local root cand + for root in \ + "${HOME}/.cache/ms-playwright" \ + "${LOCALAPPDATA:-}/ms-playwright"; do + [ -d "$root" ] || continue + # 取最新 chromium-* 目录 + cand=$(find "$root" -maxdepth 3 -type f \( -path '*/chrome-linux64/chrome' -o -path '*/chrome-linux/chrome' -o -path '*/chrome-win64/chrome.exe' \) 2>/dev/null | sort -r | head -1 || true) + if [ -n "$cand" ] && [ -x "$cand" ] || [ -f "$cand" ]; then + echo "$cand" + return 0 + fi + done + return 1 +} + find_chromium_bin() { local candidate p - # 优先 deb/系统包路径,snap 放后面(且不 resolve 成 /usr/bin/snap) + # 1) Playwright Chromium(推荐,服务器无 AppArmor/snap 坑) + if p="$(find_playwright_chromium 2>/dev/null)"; then + echo "$p" + return 0 + fi + # 2) deb/系统包路径,snap 放后面(且不 resolve 成 /usr/bin/snap) for candidate in \ /usr/bin/chromium \ /usr/bin/chromium-browser \ @@ -439,7 +461,7 @@ find_chromium_bin() { esac # 必须像 chromium case "$p" in - *chromium*|*Chromium*) + *chromium*|*Chromium*|*chrome-linux*) echo "$p" return 0 ;; diff --git a/turnstilePatch/content.js b/turnstilePatch/content.js deleted file mode 100644 index 92cffde..0000000 --- a/turnstilePatch/content.js +++ /dev/null @@ -1,132 +0,0 @@ -// Turnstile Patch - minimal only -// Full aggressive patches (chrome.runtime delete / permissions / plugins / languages -// / iframe postMessage spam) were observed to break xAI signup with: -// [permission_denied] HTTP 403 -// 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 () { - "use strict"; - if (window.__grokStealthApplied) return; - window.__grokStealthApplied = true; - - // 1. Hide navigator.webdriver (instance + prototype when possible) - try { - Object.defineProperty(navigator, "webdriver", { - get: function () { - return false; - }, - configurable: true, - }); - } catch (e) {} - try { - var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver"); - if (!desc || desc.configurable) { - Object.defineProperty(Navigator.prototype, "webdriver", { - get: function () { - return false; - }, - configurable: true, - }); - } - } catch (e) {} - - 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() { - try { - var iframes = document.querySelectorAll( - 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' - ); - for (var i = 0; i < iframes.length; i++) { - // Host-side synthetic click (works even when iframe is cross-origin) - firePointer(iframes[i]); - try { - var body = iframes[i].contentDocument || iframes[i].contentWindow.document; - if (!body) continue; - var checkbox = body.querySelector( - 'input[type="checkbox"], .mark, label, [role="checkbox"]' - ); - if (checkbox && !checkbox.checked) { - firePointer(checkbox); - } - } catch (e) { - // 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) {} - } - - function startLoop() { - var n = 0; - var t = setInterval(function () { - n++; - tryClickTurnstile(); - // ~90s of best-effort clicks (was 40s) - if (n > 180) clearInterval(t); - }, 500); - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", startLoop); - } else { - startLoop(); - } -})(); diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json index 0adbd32..68e00dc 100644 --- a/turnstilePatch/manifest.json +++ b/turnstilePatch/manifest.json @@ -1,16 +1,12 @@ { "manifest_version": 3, - "name": "Turnstile Patch", - "version": "1.3.0", - "description": "Minimal webdriver hide + best-effort host click for Turnstile", + "name": "Turnstile Patcher", + "version": "2.1", + "description": "Patch CDP MouseEvent.screenX/screenY for Turnstile (ReinerBRO / Git-creat7)", "content_scripts": [ { - "matches": [ - "" - ], - "js": [ - "content.js" - ], + "js": ["./script.js"], + "matches": [""], "run_at": "document_start", "all_frames": true, "world": "MAIN", diff --git a/turnstilePatch/script.js b/turnstilePatch/script.js new file mode 100644 index 0000000..f1c1d5d --- /dev/null +++ b/turnstilePatch/script.js @@ -0,0 +1,24 @@ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// CDP Input.dispatchMouseEvent creates MouseEvent where +// .screenX/.screenY equal .clientX/.clientY. Cloudflare Turnstile detects +// this (Chromium issue 40280325). Patch with realistic screen coords. +// Source: ObjectAscended/CDP-bug-MouseEvent-.screenX-.screenY-patcher +// Ported from ReinerBRO/grok-register + Git-creat7/grokRegister-cpa. + +// old method wouldn't work on 4k screens +let screenX = getRandomInt(800, 1200); +let screenY = getRandomInt(400, 600); + +Object.defineProperty(MouseEvent.prototype, "screenX", { value: screenX }); +Object.defineProperty(MouseEvent.prototype, "screenY", { value: screenY }); + +// Best-effort: also cover PointerEvent if present (CF may read either). +try { + if (typeof PointerEvent !== "undefined" && PointerEvent.prototype) { + Object.defineProperty(PointerEvent.prototype, "screenX", { value: screenX }); + Object.defineProperty(PointerEvent.prototype, "screenY", { value: screenY }); + } +} catch (e) {}