Add one-click start scripts for Linux and Windows
- start.sh: Linux/Mac one-click launcher - start.bat: Windows one-click launcher - Both scripts auto-detect/install: - Python 3.13+ - uv package manager - Chrome/Chromium browser - Sync Python dependencies - Create .env/config.json from templates if missing - Default to unlimited accounts (--count 0) - Pass through CLI args for custom runs
This commit is contained in:
1 parent
55f56d27c9
commit
c56d0d240a
6 files changed
+796
-128
No files matched your search
+306
-41
@@ -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
|
||||
human_sleep(0.6, cancel_callback)
|
||||
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)
|
||||
# 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)
|
||||
|
||||
+15
-2
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
+35
-81
@@ -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;
|
||||
}
|
||||
} 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 });
|
||||
}
|
||||
return origQuery(params);
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 4. 修补 plugin 数量,模拟正常浏览器
|
||||
try {
|
||||
Object.defineProperty(navigator, "plugins", {
|
||||
var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
|
||||
if (!desc || desc.configurable) {
|
||||
Object.defineProperty(Navigator.prototype, "webdriver", {
|
||||
get: function () {
|
||||
return [1, 2, 3, 4, 5];
|
||||
return false;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 5. 修补 languages 属性
|
||||
// 2. Best-effort Turnstile checkbox click (same-origin only; no postMessage spam)
|
||||
function tryClickTurnstile() {
|
||||
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();
|
||||
}
|
||||
|
||||
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'
|
||||
);
|
||||
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) {
|
||||
// 跨域限制,尝试通过 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,停止检查
|
||||
// cross-origin: skip
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
})();
|
||||
@@ -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": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"world": "MAIN",
|
||||
|
||||
Reference in new issue
Block a user