From c56d0d240af3b0d30aa6fe1ae705ed1f5570506c Mon Sep 17 00:00:00 2001 From: chaos Date: Sun, 12 Jul 2026 00:04:36 +0800 Subject: [PATCH] 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 --- grok_register_ttk.py | 351 ++++++++++++++++++++++++++++++----- register_cli.py | 17 +- start.bat | 189 +++++++++++++++++++ start.sh | 243 ++++++++++++++++++++++++ turnstilePatch/content.js | 142 +++++--------- turnstilePatch/manifest.json | 12 +- 6 files changed, 811 insertions(+), 143 deletions(-) create mode 100644 start.bat create mode 100755 start.sh diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 49deafe..4deeced 100644 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -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) diff --git a/register_cli.py b/register_cli.py index 1689e7a..859fe95 100644 --- a/register_cli.py +++ b/register_cli.py @@ -282,8 +282,21 @@ def register_one( break except Exception as exc: msg = str(exc) - if ("未收到验证码" in msg or "验证码" in msg) and mail_try < max_mail_retry: - log(worker_id, f"! 本邮箱未取到验证码,换邮箱重试: {msg}") + # Retry whole email stage on: no OTP mail, submit never reached code page, CF/rate limit + retryable = any( + key in msg + for key in ( + "未收到验证码", + "验证码", + "未进入验证码页", + "邮箱提交被拒绝", + "rate-limited", + "rate_limited", + "Turnstile", + ) + ) + if retryable and mail_try < max_mail_retry: + log(worker_id, f"! 邮箱阶段可重试,换邮箱/浏览器: {msg}") _mark_email_stage_error(email, msg) try: reg.restart_browser(log_callback=lambda m: log(worker_id, m)) diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..b8ed900 --- /dev/null +++ b/start.bat @@ -0,0 +1,189 @@ +@echo off +chcp 65001 >nul 2>&1 +setlocal enabledelayedexpansion +title Grok 注册机 - 一键启动 + +echo ============================================ +echo Grok 注册机 - 一键启动 (Windows) +echo ============================================ +echo. + +cd /d "%~dp0" + +:: ── 1. 检测 Python 3.13+ ── +echo [1/5] 检测 Python 环境... +set PYTHON_BIN= + +:: 尝试常见 Python 路径 +for %%P in ( + "%LOCALAPPDATA%\Programs\Python\Python313\python.exe" + "%LOCALAPPDATA%\Programs\Python\Python314\python.exe" + "python3.13" + "python3" + "python" + "py" +) do ( + if not defined PYTHON_BIN ( + where %%~P >nul 2>&1 + if !errorlevel! equ 0 ( + call :check_python_version %%~P + ) + ) +) + +:: 检查 .venv +if not defined PYTHON_BIN ( + if exist ".venv\Scripts\python.exe" ( + call :check_python_version ".venv\Scripts\python.exe" + ) +) + +if not defined PYTHON_BIN ( + echo [X] 未找到 Python 3.13+ + echo. + echo 请通过以下任一方式安装 Python 3.13: + echo. + echo 方式 1: 官方安装包 (推荐) + echo https://www.python.org/downloads/ + echo 下载 Python 3.13.x,安装时勾选 "Add Python to PATH" + echo. + echo 方式 2: 使用 uv 自动获取 + echo powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + echo uv python install 3.13 + echo. + echo 方式 3: winget + echo winget install Python.Python.3.13 + echo. + pause + exit /b 1 +) + +echo [OK] Python: %PYTHON_BIN% +echo. + +:: ── 2. 检测/安装 uv ── +echo [2/5] 检测 uv 包管理器... +where uv >nul 2>&1 +if %errorlevel% equ 0 ( + for /f "tokens=*" %%V in ('uv --version 2^>nul') do echo [OK] uv: %%V + goto :uv_ok +) + +echo [!] 未检测到 uv,正在自动安装... +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +if %errorlevel% neq 0 ( + echo [X] uv 安装失败,请手动安装: + echo powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + pause + exit /b 1 +) + +:: 刷新 PATH +call :refresh_path + +where uv >nul 2>&1 +if %errorlevel% equ 0 ( + for /f "tokens=*" %%V in ('uv --version 2^>nul') do echo [OK] uv 安装成功: %%V +) else ( + echo [X] uv 安装后仍未找到,请重启终端后重试 + pause + exit /b 1 +) +:uv_ok +echo. + +:: ── 3. 同步依赖 ── +echo [3/5] 同步 Python 依赖... +uv sync +if %errorlevel% neq 0 ( + echo [X] 依赖同步失败 + pause + exit /b 1 +) +echo [OK] 依赖同步完成 +echo. + +:: ── 4. 检测浏览器 ── +echo [4/5] 检测浏览器... +set BROWSER_FOUND=0 + +for %%B in ( + "C:\Program Files\Google\Chrome\Application\chrome.exe" + "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" + "%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe" + "chrome" + "chromium" +) do ( + if !BROWSER_FOUND! equ 0 ( + if exist %%B ( + echo [OK] 浏览器: %%B + set BROWSER_FOUND=1 + ) + ) +) + +if %BROWSER_FOUND% equ 0 ( + echo [!] 未检测到 Chrome/Chromium 浏览器 + echo. + echo DrissionPage 需要 Chrome 才能运行。 + echo 请安装 Google Chrome: https://www.google.com/chrome/ + echo. + echo 脚本将继续,但注册可能因缺少浏览器而失败。 +) +echo. + +:: ── 5. 配置文件模板 ── +echo [5/5] 检查配置文件... + +if not exist ".env" ( + if exist ".env.example" ( + copy ".env.example" ".env" >nul + echo [!] 已从 .env.example 创建 .env,请编辑填入真实配置 + ) +) else ( + echo [OK] .env 已存在 +) + +if not exist "config.json" ( + if exist "config.example.json" ( + copy "config.example.json" "config.json" >nul + echo [!] 已从 config.example.json 创建 config.json,请编辑填入真实配置 + ) +) else ( + echo [OK] config.json 已存在 +) +echo. + +echo ============================================ +echo 环境就绪,启动 register_cli.py +echo ============================================ +echo. + +:: 默认不限数量(--count 0),除非用户显式传参 +if "%~1"=="" ( + echo [*] 默认模式:不限数量,持续注册(Ctrl+C 停止) + echo. + uv run python -u register_cli.py --count 0 +) else ( + uv run python -u register_cli.py %* +) + +echo. +pause +exit /b 0 + +:: ── 辅助函数 ── + +:check_python_version +:: 检查指定 Python 版本是否 >= 3.13 +set "PY_PATH=%~1" +"%PY_PATH%" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" >nul 2>&1 +if %errorlevel% equ 0 ( + set "PYTHON_BIN=%PY_PATH%" +) +exit /b 0 + +:refresh_path +:: 刷新 PATH,加入 uv 安装路径 +set "PATH=%USERPROFILE%\.local\bin;%USERPROFILE%\.cargo\bin;%PATH%" +exit /b 0 \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..0fafe77 --- /dev/null +++ b/start.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +# ============================================================================ +# Grok 注册机 - 一键启动脚本 (Linux) +# 自动检测/安装环境依赖,然后启动 register_cli.py +# +# 用法: +# ./start.sh # 默认不限数量(持续注册) +# ./start.sh --count 10 # 跑 10 个 +# ./start.sh --count 10 --threads 3 # 3 线程跑 10 个 +# ./start.sh --extra 5 # 在已有基础上再注册 5 个 +# +# 首次运行会自动: +# 1. 检测 Python 3.13+ +# 2. 安装 uv 包管理器 +# 3. 同步 Python 依赖 +# 4. 检测 Chrome/Chromium 浏览器 +# 5. 从模板复制 .env / config.json(如缺失) +# ============================================================================ +set -euo pipefail + +# ── 颜色 ── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$PROJECT_DIR" + +echo -e "${CYAN}${BOLD}============================================${NC}" +echo -e "${CYAN}${BOLD} Grok 注册机 - 一键启动 (Linux)${NC}" +echo -e "${CYAN}${BOLD}============================================${NC}" +echo "" + +# ── 1. 检测 Python 3.13+ ── +step1_check_python() { + echo -e "${CYAN}[1/5] 检测 Python 环境...${NC}" + + # 优先使用 mise(如果已安装) + if command -v mise &>/dev/null && mise which python &>/dev/null 2>&1; then + local py + py=$(mise which python 2>/dev/null || true) + if [ -n "$py" ] && "$py" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} Python (mise): $($py --version 2>&1) at $py" + PYTHON_BIN="$py" + return 0 + fi + fi + + # 检查系统 Python + for candidate in python3.13 python3.14 python3 python; do + if command -v "$candidate" &>/dev/null; then + local ver + ver=$("$candidate" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "0") + local major + major=$(echo "$ver" | cut -d. -f1) + if [ "$major" -ge 3 ]; then + local minor + minor=$(echo "$ver" | cut -d. -f2) + if [ "$minor" -ge 13 ]; then + echo -e " ${GREEN}✓${NC} Python $ver: $("$candidate" --version 2>&1)" + PYTHON_BIN="$candidate" + return 0 + fi + fi + fi + done + + # 检查 .venv + if [ -f "$PROJECT_DIR/.venv/bin/python3" ]; then + local py="$PROJECT_DIR/.venv/bin/python3" + if "$py" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} Python (.venv): $("$py" --version 2>&1)" + PYTHON_BIN="$py" + return 0 + fi + fi + + echo -e " ${RED}✗${NC} 未找到 Python 3.13+" + echo "" + echo -e " ${YELLOW}请通过以下任一方式安装 Python 3.13:${NC}" + echo "" + echo " # 方式 1: 使用 mise(推荐,自动管理版本+venv)" + echo " curl https://mise.run | sh" + echo " mise install python@3.13" + echo " mise trust" + echo "" + echo " # 方式 2: Ubuntu/Debian deadsnakes PPA" + echo " sudo add-apt-repository -y ppa:deadsnakes/ppa" + echo " sudo apt update && sudo apt install -y python3.13 python3.13-venv" + echo "" + echo " # 方式 3: 使用 uv 自动获取 Python" + echo " curl -LsSf https://astral.sh/uv/install.sh | sh" + echo " uv python install 3.13" + echo "" + echo " # 方式 4: Arch Linux" + echo " sudo pacman -S python" + echo "" + return 1 +} + +# ── 2. 检测/安装 uv ── +step2_check_uv() { + echo -e "${CYAN}[2/5] 检测 uv 包管理器...${NC}" + + if command -v uv &>/dev/null; then + echo -e " ${GREEN}✓${NC} uv: $(uv --version 2>&1)" + return 0 + fi + + echo -e " ${YELLOW}⚠${NC} 未检测到 uv,正在自动安装..." + if command -v curl &>/dev/null; then + curl -LsSf https://astral.sh/uv/install.sh | sh + elif command -v wget &>/dev/null; then + wget -qO- https://astral.sh/uv/install.sh | sh + else + echo -e " ${RED}✗${NC} 需要 curl 或 wget 来安装 uv" + echo " 请手动安装: curl -LsSf https://astral.sh/uv/install.sh | sh" + return 1 + fi + + # 重新加载 PATH(uv 安装到 ~/.local/bin 或 ~/.cargo/bin) + export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" + + if command -v uv &>/dev/null; then + echo -e " ${GREEN}✓${NC} uv 安装成功: $(uv --version 2>&1)" + return 0 + fi + + echo -e " ${RED}✗${NC} uv 安装失败,请手动安装: https://docs.astral.sh/uv/" + return 1 +} + +# ── 3. 同步依赖 ── +step3_sync_deps() { + echo -e "${CYAN}[3/5] 同步 Python 依赖...${NC}" + + if command -v mise &>/dev/null && [ -f "$PROJECT_DIR/mise.toml" ]; then + # mise 管理 venv,直接用 uv sync + if [ -n "${PYTHON_BIN:-}" ]; then + "$PYTHON_BIN" -m uv sync 2>/dev/null || uv sync + else + uv sync + fi + else + # 纯 uv 模式 + uv sync + fi + + echo -e " ${GREEN}✓${NC} 依赖同步完成" +} + +# ── 4. 检测浏览器 ── +step4_check_browser() { + echo -e "${CYAN}[4/5] 检测浏览器...${NC}" + + local found=false + for candidate in \ + google-chrome-stable google-chrome chromium chromium-browser \ + /usr/bin/google-chrome-stable /usr/bin/google-chrome \ + /snap/bin/chromium /usr/bin/chromium /usr/bin/chromium-browser; do + if command -v "$candidate" &>/dev/null || [ -x "$candidate" ] 2>/dev/null; then + echo -e " ${GREEN}✓${NC} 浏览器: $(command -v "$candidate" 2>/dev/null || echo "$candidate")" + found=true + break + fi + done + + if ! $found; then + echo -e " ${YELLOW}⚠${NC} 未检测到 Chrome/Chromium 浏览器" + echo "" + echo " DrissionPage 需要 Chrome 或 Chromium 才能运行。" + echo "" + echo " 安装方式:" + echo " # Ubuntu/Debian" + echo " sudo apt install -y chromium-browser" + echo " # 或安装 Google Chrome:" + echo " wget -q -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" + echo " sudo dpkg -i /tmp/chrome.deb" + echo "" + echo " # Arch Linux" + echo " sudo pacman -S chromium" + echo "" + echo " ${YELLOW}脚本将继续,但注册可能因缺少浏览器而失败。${NC}" + fi +} + +# ── 5. 配置文件模板 ── +step5_config_templates() { + echo -e "${CYAN}[5/5] 检查配置文件...${NC}" + + if [ ! -f "$PROJECT_DIR/.env" ] && [ -f "$PROJECT_DIR/.env.example" ]; then + cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" + echo -e " ${YELLOW}⚠${NC} 已从 .env.example 创建 .env,请编辑填入真实配置" + elif [ -f "$PROJECT_DIR/.env" ]; then + echo -e " ${GREEN}✓${NC} .env 已存在" + fi + + if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$PROJECT_DIR/config.example.json" ]; then + cp "$PROJECT_DIR/config.example.json" "$PROJECT_DIR/config.json" + echo -e " ${YELLOW}⚠${NC} 已从 config.example.json 创建 config.json,请编辑填入真实配置" + elif [ -f "$PROJECT_DIR/config.json" ]; then + echo -e " ${GREEN}✓${NC} config.json 已存在" + fi +} + +# ── 主流程 ── +main() { + step1_check_python || exit 1 + echo "" + step2_check_uv || exit 1 + echo "" + step3_sync_deps + echo "" + step4_check_browser + echo "" + step5_config_templates + echo "" + + echo -e "${CYAN}${BOLD}============================================${NC}" + echo -e "${GREEN}${BOLD} 环境就绪,启动 register_cli.py${NC}" + echo -e "${CYAN}${BOLD}============================================${NC}" + echo "" + + # 默认不限数量(--count 0),除非用户显式传参 + if [ $# -eq 0 ]; then + echo -e "${YELLOW}[*] 默认模式:不限数量,持续注册(Ctrl+C 停止)${NC}" + echo "" + ARGS="--count 0" + else + ARGS="$*" + fi + + if command -v mise &>/dev/null && [ -f "$PROJECT_DIR/mise.toml" ]; then + exec mise run register -- -- $ARGS + else + exec uv run python -u register_cli.py $ARGS + fi +} + +main "$@" \ No newline at end of file diff --git a/turnstilePatch/content.js b/turnstilePatch/content.js index d7ddb20..a54e1e9 100644 --- a/turnstilePatch/content.js +++ b/turnstilePatch/content.js @@ -1,11 +1,15 @@ -// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证 -// MV3 MAIN world + document_start:在页面脚本之前修补真实页面上下文 +// 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. (function () { "use strict"; + if (window.__grokStealthApplied) return; + window.__grokStealthApplied = true; - // 1. 隐藏 navigator.webdriver 标识 - // Chrome 自动化模式下 navigator.webdriver = true,Turnstile 会检测此属性 + // 1. Hide navigator.webdriver (instance + prototype when possible) try { Object.defineProperty(navigator, "webdriver", { get: function () { @@ -14,103 +18,53 @@ configurable: true, }); } catch (e) {} - - // 2. 移除 Chrome 自动化相关的 Runtime 属性 try { - if (window.chrome && window.chrome.runtime) { - delete window.chrome.runtime.onConnect; - delete window.chrome.runtime.onMessage; + var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver"); + if (!desc || desc.configurable) { + Object.defineProperty(Navigator.prototype, "webdriver", { + get: function () { + return false; + }, + configurable: true, + }); } } catch (e) {} - // 3. 覆盖 permissions.query,隐藏 notifications 权限异常 - try { - if (navigator.permissions && navigator.permissions.query) { - var origQuery = navigator.permissions.query.bind(navigator.permissions); - navigator.permissions.query = function (params) { - if (params && params.name === "notifications") { - return Promise.resolve({ state: Notification.permission }); + // 2. Best-effort Turnstile checkbox click (same-origin only; no postMessage spam) + function tryClickTurnstile() { + try { + var iframes = document.querySelectorAll( + 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' + ); + for (var i = 0; i < iframes.length; i++) { + try { + var body = iframes[i].contentDocument || iframes[i].contentWindow.document; + var checkbox = body.querySelector('input[type="checkbox"], .mark'); + if (checkbox && !checkbox.checked) { + checkbox.click(); + } + } catch (e) { + // cross-origin: skip } - return origQuery(params); - }; - } - } catch (e) {} - - // 4. 修补 plugin 数量,模拟正常浏览器 - try { - Object.defineProperty(navigator, "plugins", { - get: function () { - return [1, 2, 3, 4, 5]; - }, - configurable: true, - }); - } catch (e) {} - - // 5. 修补 languages 属性 - try { - Object.defineProperty(navigator, "languages", { - get: function () { - return ["en-US", "en"]; - }, - configurable: true, - }); - } catch (e) {} - - // 6. 页面加载完成后,自动监控并点击 Turnstile 复选框 - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", autoClickTurnstile); - } else { - autoClickTurnstile(); + } + } catch (e) {} } - function autoClickTurnstile() { - // 定时检查 Turnstile iframe 是否出现 - var checkCount = 0; - var maxChecks = 100; // 最多检查 100 次(约 50 秒) - var timer = setInterval(function () { - checkCount++; - if (checkCount > maxChecks) { - clearInterval(timer); - return; - } - try { - // 查找 Turnstile iframe - var iframes = document.querySelectorAll( - 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' - ); - for (var i = 0; i < iframes.length; i++) { - var iframe = iframes[i]; - try { - // 尝试访问 iframe 内部的 checkbox - var body = iframe.contentDocument || iframe.contentWindow.document; - var checkbox = body.querySelector( - 'input[type="checkbox"], .mark, #cf-chl-widget-nomu1_resp' - ); - if (checkbox && !checkbox.checked) { - checkbox.click(); - } - } catch (e) { - // 跨域限制,尝试通过 postMessage 触发 - try { - iframe.contentWindow.postMessage( - { type: "turnstile-auto-click" }, - "*" - ); - } catch (e2) {} - } - } - - // 也尝试直接操作 Turnstile API - if ( - window.turnstile && - typeof window.turnstile.getResponse === "function" - ) { - var resp = window.turnstile.getResponse(); - if (resp && resp.length > 0) { - clearInterval(timer); // 已获得 token,停止检查 - } - } - } catch (e) {} + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", function () { + var n = 0; + var t = setInterval(function () { + n++; + tryClickTurnstile(); + if (n > 80) clearInterval(t); + }, 500); + }); + } else { + var n = 0; + var t = setInterval(function () { + n++; + tryClickTurnstile(); + if (n > 80) clearInterval(t); }, 500); } })(); diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json index 5ff94c2..6bab5c7 100644 --- a/turnstilePatch/manifest.json +++ b/turnstilePatch/manifest.json @@ -1,12 +1,16 @@ { "manifest_version": 3, "name": "Turnstile Patch", - "version": "1.1.0", - "description": "Patch browser automation detection for Turnstile", + "version": "1.2.0", + "description": "Minimal webdriver hide for Turnstile (no aggressive API patches)", "content_scripts": [ { - "matches": [""], - "js": ["content.js"], + "matches": [ + "" + ], + "js": [ + "content.js" + ], "run_at": "document_start", "all_frames": true, "world": "MAIN",