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
+796 -128

No files matched your search

+306 -41
View File
@@ -615,14 +615,16 @@ CHROMIUM_SLIM_FLAGS = [
"--disable-software-rasterizer", "--disable-software-rasterizer",
"--no-sandbox", "--no-sandbox",
"--disable-dev-shm-usage", "--disable-dev-shm-usage",
"--disable-images", # Keep images ON: Turnstile/CF widgets often need them; was a common fail cause.
"--mute-audio", "--mute-audio",
"--disable-background-networking", "--disable-background-networking",
"--no-first-run", "--no-first-run",
"--disable-blink-features=AutomationControlled",
] ]
# Prefer Google Chrome for registration reliability (email/CF path). # Prefer Google Chrome for registration reliability (email delivery path).
# Chromium is fallback; turnstilePatch only loads on Chromium (Chrome blocks --load-extension). # 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 = ( BROWSER_CANDIDATES = (
"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-stable",
"/usr/bin/google-chrome", "/usr/bin/google-chrome",
@@ -631,6 +633,31 @@ BROWSER_CANDIDATES = (
"/usr/bin/chromium-browser", "/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: def _is_google_chrome_path(path: str | None) -> bool:
if not path: if not path:
@@ -650,12 +677,63 @@ def resolve_browser_path():
return None 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(): def create_browser_options():
options = ChromiumOptions() options = ChromiumOptions()
options.auto_port() options.auto_port()
options.set_timeouts(base=1) options.set_timeouts(base=1)
for flag in CHROMIUM_SLIM_FLAGS: for flag in CHROMIUM_SLIM_FLAGS:
options.set_argument(flag) 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() browser_path = resolve_browser_path()
if browser_path: if browser_path:
try: try:
@@ -667,7 +745,7 @@ def create_browser_options():
# Google Chrome blocks CLI unpacked extension loading; Chromium still allows it. # Google Chrome blocks CLI unpacked extension loading; Chromium still allows it.
if _is_google_chrome_path(browser_path): if _is_google_chrome_path(browser_path):
print( print(
" [ext] skip turnstilePatch on Google Chrome (--load-extension blocked)", " [ext] Chrome: minimal CDP stealth only (extension blocked)",
flush=True, flush=True,
) )
else: else:
@@ -2700,6 +2778,7 @@ def start_browser(log_callback=None):
try: try:
TabPool.init(create_browser_options, log_callback=log_callback) TabPool.init(create_browser_options, log_callback=log_callback)
page = TabPool.get_tab() page = TabPool.get_tab()
apply_stealth_patches(page, log_callback=log_callback)
if log_callback and attempt > 1: if log_callback and attempt > 1:
log_callback(f"[*] 浏览器第 {attempt} 次启动成功") log_callback(f"[*] 浏览器第 {attempt} 次启动成功")
return TabPool.get_browser(), page 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 reuse and TabPool.get_browser() is not None and (every <= 0 or served < every):
if TabPool.clear_session(log_callback=log_callback): if TabPool.clear_session(log_callback=log_callback):
TabPool.mark_served() 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 # full recycle
if log_callback: if log_callback:
log_callback(f"[*] 浏览器完整回收(reuse={reuse}, served={served}, every={every})") log_callback(f"[*] 浏览器完整回收(reuse={reuse}, served={served}, every={every})")
@@ -2813,14 +2895,92 @@ return true;
raise Exception("未找到「使用邮箱注册」按钮") 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): def open_signup_page(log_callback=None, cancel_callback=None):
browser = _get_browser() browser = _get_browser()
page = _get_page() page = _get_page()
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
if browser is None: if browser is None:
browser, page = start_browser() browser, page = start_browser(log_callback=log_callback)
if log_callback: if log_callback:
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: try:
page = _get_page() page = _get_page()
page.get(SIGNUP_URL) page.get(SIGNUP_URL)
@@ -2829,23 +2989,29 @@ def open_signup_page(log_callback=None, cancel_callback=None):
log_callback(f"[Debug] 打开URL异常: {e}") log_callback(f"[Debug] 打开URL异常: {e}")
try: try:
TabPool.release_tab() TabPool.release_tab()
page = _get_page() browser, page = start_browser(log_callback=log_callback)
page.get(SIGNUP_URL) page.get(SIGNUP_URL)
except Exception as e2: except Exception as e2:
if log_callback: if log_callback:
log_callback(f"[Debug] 创建新标签页异常: {e2}") log_callback(f"[Debug] 创建新标签页异常: {e2}")
restart_browser() restart_browser(log_callback=log_callback)
page = _get_page() page = _get_page()
page.get(SIGNUP_URL) page.get(SIGNUP_URL)
page = _get_page()
apply_stealth_patches(page, log_callback=None)
page.wait.doc_loaded() 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") dump_state(page, "signup-loaded")
take_screenshot(page, "signup") 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: if log_callback:
log_callback(f"[*] 当前URL: {page.url}") log_callback(f"[*] 当前URL: {page.url}")
click_email_signup_button( click_email_signup_button(
log_callback=log_callback, cancel_callback=cancel_callback log_callback=log_callback, cancel_callback=cancel_callback
) )
dismiss_cookie_banner(_get_page(), log_callback=log_callback)
dump_state(page, "after-email-signup-click") 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() page = _get_page()
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
check_timeout(time.time()) 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() email, dev_token = get_email_and_token()
if not email or not dev_token: if not email or not dev_token:
raise Exception("获取邮箱失败") raise Exception("获取邮箱失败")
@@ -2878,6 +3046,7 @@ def fill_email_and_submit(timeout=15, log_callback=None, cancel_callback=None):
deadline = time.time() + timeout deadline = time.time() + timeout
while time.time() < deadline: while time.time() < deadline:
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
dismiss_cookie_banner(page, log_callback=None)
filled = page.run_js( filled = page.run_js(
""" """
const email = arguments[0]; const email = arguments[0];
@@ -2968,7 +3137,14 @@ return 'clicked';
if log_callback: if log_callback:
log_callback(f"[*] 已填写邮箱并点击注册: {email}") log_callback(f"[*] 已填写邮箱并点击注册: {email}")
# Wait for transition to OTP / next step (email form should leave or code inputs appear) # 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: while time.time() < transit_deadline:
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
try: try:
@@ -2981,32 +3157,95 @@ function isVisible(node) {
return rect.width > 0 && rect.height > 0; return rect.width > 0 && rect.height > 0;
} }
const codeInput = Array.from(document.querySelectorAll( 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)); )).find((n) => isVisible(n));
if (codeInput) return 'code-ready'; if (codeInput) return 'code-ready';
const emailInput = Array.from(document.querySelectorAll( const emailInput = Array.from(document.querySelectorAll(
'input[data-testid="email"], input[name="email"], input[type="email"]' 'input[data-testid="email"], input[name="email"], input[type="email"]'
)).find((n) => isVisible(n)); )).find((n) => isVisible(n));
if (!emailInput) return 'email-gone'; 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] || ''; .map((n) => (n.innerText || '').trim()).filter(Boolean)[0] || '';
if (err) return 'error:' + err.slice(0, 120); 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'; return 'waiting';
""") """)
except Exception: except Exception:
state = 'waiting' state = "waiting"
if isinstance(state, str) and state.startswith('error:'): last_state = str(state or "waiting")
if isinstance(state, str) and state.startswith("error:"):
if log_callback: if log_callback:
log_callback(f"[Debug] 邮箱提交后页面报错: {state}") log_callback(f"[Debug] 邮箱提交后页面报错: {state}")
break raise Exception(f"邮箱提交被拒绝: {state[6:]}")
if state in ('code-ready', 'email-gone'): if state == "rate-limited":
raise Exception("邮箱提交触发限流 rate-limited")
if state in ("code-ready", "email-gone"):
if log_callback: if log_callback:
log_callback(f"[*] 邮箱提交后页面状态: {state}") log_callback(f"[*] 邮箱提交后页面状态: {state}")
break
human_sleep(0.6, cancel_callback)
dump_state(page, "email-submitted") dump_state(page, "email-submitted")
take_screenshot(page, "email-submitted") take_screenshot(page, "email-submitted")
return email, dev_token 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, ""): if log_callback and clicked not in (False, None, ""):
log_callback(f"[Debug] 邮箱已写入,但注册按钮未点到: {clicked}") log_callback(f"[Debug] 邮箱已写入,但注册按钮未点到: {clicked}")
human_sleep(0.5, cancel_callback) human_sleep(0.5, cancel_callback)
@@ -3171,13 +3410,15 @@ return 'clicked';
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): 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() page = _get_page()
if page is None: if page is None:
raise Exception("页面未就绪,无法执行 Turnstile") raise Exception("页面未就绪,无法执行 Turnstile")
apply_stealth_patches(page, log_callback=None)
try: try:
page.run_js( page.run_js(
@@ -3187,7 +3428,9 @@ def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int =
pass pass
rounds = max(2, int(max_rounds or 4)) 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) raise_if_cancelled(cancel_callback)
try: try:
token = page.run_js( token = page.run_js(
@@ -3195,6 +3438,8 @@ def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int =
try { try {
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
if (byInput) return byInput; 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') { if (window.turnstile && typeof turnstile.getResponse === 'function') {
return String(turnstile.getResponse() || '').trim(); return String(turnstile.getResponse() || '').trim();
} }
@@ -3208,7 +3453,7 @@ try {
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}") log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return 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: if challenge_input:
wrapper = challenge_input.parent() wrapper = challenge_input.parent()
iframe = None iframe = None
@@ -3216,6 +3461,14 @@ try {
iframe = wrapper.shadow_root.ele("tag:iframe") iframe = wrapper.shadow_root.ele("tag:iframe")
except Exception: except Exception:
iframe = None 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: if iframe:
try: try:
iframe.run_js( iframe.run_js(
@@ -3230,27 +3483,37 @@ Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
) )
except Exception: except Exception:
pass pass
clicked = False
try: try:
body_sr = iframe.ele("tag:body").shadow_root 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: if btn:
btn.click() btn.click()
clicked = True
except Exception:
pass
if not clicked:
try:
iframe.click()
except Exception: except Exception:
pass pass
else: else:
# 兜底:尝试触发页面上可见的 Turnstile 容器 # 兜底:尝试触发页面上可见的 Turnstile 容器 / iframe
page.run_js( page.run_js(
""" """
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => { const nodes = Array.from(document.querySelectorAll(
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || ''); 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'
return String(txt).toLowerCase().includes('turnstile'); ));
}); for (const n of nodes) {
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click(); try { n.scrollIntoView({block:'center', inline:'center'}); } catch(e) {}
try { if (typeof n.click === 'function') n.click(); } catch(e) {}
}
""" """
) )
except Exception: except Exception:
pass 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 失败") raise Exception("Turnstile 获取 token 失败")
@@ -3282,15 +3545,16 @@ def build_profile():
def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None): def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):
page = _get_page() page = _get_page()
apply_stealth_patches(page, log_callback=log_callback)
check_timeout(time.time()) check_timeout(time.time())
dump_state(page, "profile-form") dump_state(page, "profile-form")
take_screenshot(page, "profile-form") take_screenshot(page, "profile-form")
given_name, family_name, password = build_profile() given_name, family_name, password = build_profile()
# 预热 Turnstile:等 2 秒让 iframe 初始化,插件会自动点击 checkbox # 预热 Turnstile:等 iframe 初始化;CDP stealth 会尝试自动点 checkbox
if log_callback: if log_callback:
log_callback("[*] 预热 Turnstile...") log_callback("[*] 预热 Turnstile...")
# fast mode scales human_sleep; keep a tiny floor so turnstile iframe can attach # fast mode scales human_sleep; keep enough time for turnstile iframe to attach
human_sleep(1.2 if not PERF_FLAGS.get("fast") else 0.4, cancel_callback) human_sleep(1.6 if not PERF_FLAGS.get("fast") else 0.8, cancel_callback)
deadline = time.time() + timeout deadline = time.time() + timeout
form_filled_once = False form_filled_once = False
wait_cf_since = None 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 last_cf_log_at = 0.0
cf_token_fail_streak = 0 cf_token_fail_streak = 0
try: 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: except Exception:
max_cf_token_fails = 2 max_cf_token_fails = 3
try: 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: except Exception:
cf_retry_after = 8.0 cf_retry_after = 10.0
try: 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: except Exception:
cf_retry_gap = 5.0 cf_retry_gap = 6.0
try: 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: except Exception:
turnstile_rounds = 4 turnstile_rounds = 6
while time.time() < deadline: while time.time() < deadline:
raise_if_cancelled(cancel_callback) raise_if_cancelled(cancel_callback)
+15 -2
View File
@@ -282,8 +282,21 @@ def register_one(
break break
except Exception as exc: except Exception as exc:
msg = str(exc) msg = str(exc)
if ("未收到验证码" in msg or "验证码" in msg) and mail_try < max_mail_retry: # Retry whole email stage on: no OTP mail, submit never reached code page, CF/rate limit
log(worker_id, f"! 本邮箱未取到验证码,换邮箱重试: {msg}") 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) _mark_email_stage_error(email, msg)
try: try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m)) reg.restart_browser(log_callback=lambda m: log(worker_id, m))
+189
View File
@@ -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
Executable
+243
View File
@@ -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
View File
@@ -1,11 +1,15 @@
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证 // Turnstile Patch - minimal only
// MV3 MAIN world + document_start:在页面脚本之前修补真实页面上下文 // 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 () { (function () {
"use strict"; "use strict";
if (window.__grokStealthApplied) return;
window.__grokStealthApplied = true;
// 1. 隐藏 navigator.webdriver 标识 // 1. Hide navigator.webdriver (instance + prototype when possible)
// Chrome 自动化模式下 navigator.webdriver = true,Turnstile 会检测此属性
try { try {
Object.defineProperty(navigator, "webdriver", { Object.defineProperty(navigator, "webdriver", {
get: function () { get: function () {
@@ -14,103 +18,53 @@
configurable: true, configurable: true,
}); });
} catch (e) {} } catch (e) {}
// 2. 移除 Chrome 自动化相关的 Runtime 属性
try { try {
if (window.chrome && window.chrome.runtime) { var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
delete window.chrome.runtime.onConnect; if (!desc || desc.configurable) {
delete window.chrome.runtime.onMessage; Object.defineProperty(Navigator.prototype, "webdriver", {
}
} 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", {
get: function () { get: function () {
return [1, 2, 3, 4, 5]; return false;
}, },
configurable: true, configurable: true,
}); });
}
} catch (e) {} } catch (e) {}
// 5. 修补 languages 属性 // 2. Best-effort Turnstile checkbox click (same-origin only; no postMessage spam)
function tryClickTurnstile() {
try { 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( var iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
); );
for (var i = 0; i < iframes.length; i++) { for (var i = 0; i < iframes.length; i++) {
var iframe = iframes[i];
try { try {
// 尝试访问 iframe 内部的 checkbox var body = iframes[i].contentDocument || iframes[i].contentWindow.document;
var body = iframe.contentDocument || iframe.contentWindow.document; var checkbox = body.querySelector('input[type="checkbox"], .mark');
var checkbox = body.querySelector(
'input[type="checkbox"], .mark, #cf-chl-widget-nomu1_resp'
);
if (checkbox && !checkbox.checked) { if (checkbox && !checkbox.checked) {
checkbox.click(); checkbox.click();
} }
} catch (e) { } catch (e) {
// 跨域限制,尝试通过 postMessage 触发 // cross-origin: skip
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) {} } 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); }, 500);
} }
})(); })();
+8 -4
View File
@@ -1,12 +1,16 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Turnstile Patch", "name": "Turnstile Patch",
"version": "1.1.0", "version": "1.2.0",
"description": "Patch browser automation detection for Turnstile", "description": "Minimal webdriver hide for Turnstile (no aggressive API patches)",
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": [
"js": ["content.js"], "<all_urls>"
],
"js": [
"content.js"
],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true, "all_frames": true,
"world": "MAIN", "world": "MAIN",