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:
chaos committed 2026-07-12 00:04:36 +08:00
1 parent 55f56d27c9
commit c56d0d240a
6 files changed
+811 -143

No files matched your search

+308 -43
View File
@@ -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)