Replace CF path with zip register solver; keep Gmail verification
Use grok-register.zip getTurnstileToken/fill_profile and pure MV2 turnstilePatch. Browser options match ReinerBRO headed flags. Local gmail worker + IMAP + defaultDomains catch-all stay intact.
This commit is contained in:
1 parent
60e24b89c1
commit
9e0f45ac3b
3 files changed
+480
-685
No files matched your search
+134
-661
@@ -1001,20 +1001,10 @@ def _first_existing_browser(candidates) -> str | None:
|
||||
|
||||
|
||||
def resolve_browser_path():
|
||||
"""Pick Chromium/Chrome for registration.
|
||||
"""Zip style: only return path if config/env override exists.
|
||||
|
||||
Zip style (grok-register / protocol_cpa): do NOT set browser_path unless
|
||||
config/env explicitly points at a real binary — let DrissionPage use system Chrome.
|
||||
|
||||
Priority:
|
||||
1. config browser_path (absolute, must exist)
|
||||
2. env BROWSER_PATH / CHROME_PATH / CHROMIUM_PATH
|
||||
3. browser_prefer=chromium|chrome → auto-detect candidates
|
||||
4. browser_prefer=auto/system → None (system default, zip style)
|
||||
|
||||
Never returns /usr/bin/snap (common snap symlink collapse bug).
|
||||
browser_prefer=auto/system → None (DrissionPage system Chrome).
|
||||
"""
|
||||
# Optional override from config.json "browser_path"
|
||||
override = str((config.get("browser_path") if isinstance(config, dict) else "") or "").strip()
|
||||
if override:
|
||||
norm = _normalize_browser_path(override)
|
||||
@@ -1022,7 +1012,7 @@ def resolve_browser_path():
|
||||
return norm
|
||||
print(f" [browser] ignore invalid browser_path={override!r}", flush=True)
|
||||
|
||||
for env_key in ("BROWSER_PATH", "CHROMIUM_PATH", "CHROME_PATH"):
|
||||
for env_key in ("BROWSER_PATH", "CHROME_PATH", "CHROMIUM_PATH"):
|
||||
env_path = (os.environ.get(env_key) or "").strip()
|
||||
if not env_path:
|
||||
continue
|
||||
@@ -1038,20 +1028,16 @@ def resolve_browser_path():
|
||||
if env_pref:
|
||||
prefer = env_pref
|
||||
|
||||
# Zip style: leave path empty so Chromium() uses OS default Chrome.
|
||||
if prefer in ("auto", "system"):
|
||||
return None
|
||||
|
||||
if prefer in ("chrome", "google-chrome", "google"):
|
||||
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
|
||||
elif prefer in ("chromium", "chromeium"):
|
||||
order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES)
|
||||
else:
|
||||
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
|
||||
|
||||
return None
|
||||
return _first_existing_browser(order)
|
||||
|
||||
|
||||
def apply_stealth_patches(page=None, log_callback=None) -> bool:
|
||||
"""Minimal anti-automation patch via CDP (Chrome cannot load unpacked extensions).
|
||||
|
||||
@@ -1100,69 +1086,63 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool:
|
||||
|
||||
|
||||
def create_browser_options(headless=None):
|
||||
"""Build ChromiumOptions — aligned with grok-register.zip / protocol_cpa zip.
|
||||
"""Zip-style Chromium options (grok-register / ReinerBRO).
|
||||
|
||||
Reference (both zips):
|
||||
options.auto_port()
|
||||
--no-first-run / --no-default-browser-check
|
||||
always options.add_extension(turnstilePatch)
|
||||
no browser_path override (system Chrome/Chromium)
|
||||
no --disable-images (breaks Turnstile)
|
||||
|
||||
headless:
|
||||
None → resolve from config/env/auto (no DISPLAY → headless)
|
||||
True → force headless
|
||||
False → prefer headed (still headless if no DISPLAY)
|
||||
Critical for CF:
|
||||
- always load turnstilePatch
|
||||
- no --disable-images
|
||||
- no browser_path unless config/env set
|
||||
- no AutomationControlled / heavy stealth flags on headed
|
||||
"""
|
||||
options = ChromiumOptions()
|
||||
options.auto_port()
|
||||
options.set_timeouts(base=1)
|
||||
|
||||
use_hl = should_use_headless(headless, log_hint=True)
|
||||
|
||||
# grok-register.zip headed flags (CF-friendly)
|
||||
for flag in CHROMIUM_BASE_FLAGS:
|
||||
# ReinerBRO headed flags
|
||||
for flag in (
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-background-networking",
|
||||
"--disable-component-update",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
"--disable-renderer-backgrounding",
|
||||
):
|
||||
options.set_argument(flag)
|
||||
|
||||
# Linux / headless-only extras (Windows headed stays minimal like the zip)
|
||||
if use_hl or sys.platform != "win32":
|
||||
for flag in CHROMIUM_HEADLESS_EXTRA_FLAGS:
|
||||
if flag in ("--disable-gpu", "--disable-software-rasterizer") and not use_hl:
|
||||
# headed Xvfb/desktop: keep software rasterizer so CF iframe paints
|
||||
continue
|
||||
use_headless = should_use_headless(headless, log_hint=True)
|
||||
if use_headless:
|
||||
apply_headless_to_options(options, True)
|
||||
for flag in (
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--mute-audio",
|
||||
):
|
||||
options.set_argument(flag)
|
||||
print(" [browser] headless=true", flush=True)
|
||||
else:
|
||||
print(" [browser] headless=false (zip headed)", flush=True)
|
||||
|
||||
try:
|
||||
options.set_pref("credentials_enable_service", False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Optional UA override (zip uses Chrome/138). Empty config → browser default.
|
||||
ua = (get_user_agent() or "").strip()
|
||||
if ua:
|
||||
try:
|
||||
options.set_user_agent(ua)
|
||||
except Exception:
|
||||
try:
|
||||
options.set_argument(f"--user-agent={ua}")
|
||||
except Exception:
|
||||
pass
|
||||
print(f" [browser] user-agent={ua[:72]}...", flush=True)
|
||||
|
||||
# Only set browser_path when config/env points to a real existing binary.
|
||||
# Zip never hardcodes path; empty browser_path → system default Chrome.
|
||||
# system default unless override
|
||||
browser_path = resolve_browser_path()
|
||||
if browser_path:
|
||||
try:
|
||||
options.set_browser_path(browser_path)
|
||||
print(f" [browser] path={browser_path}", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
print(f" [browser] set path failed: {exc}", flush=True)
|
||||
else:
|
||||
print(" [browser] path=system-default (zip style)", flush=True)
|
||||
|
||||
# BOTH zips always load turnstilePatch — do not skip for Google Chrome.
|
||||
# DrissionPage add_extension works with the temp profile even on Chrome.
|
||||
# Force UA from config (Chrome/138 etc.)
|
||||
try:
|
||||
ua = get_user_agent()
|
||||
if ua:
|
||||
options.set_user_agent(ua)
|
||||
print(f" [browser] ua={ua[:60]}...", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if os.path.exists(EXTENSION_PATH):
|
||||
try:
|
||||
options.add_extension(EXTENSION_PATH)
|
||||
@@ -1172,8 +1152,7 @@ def create_browser_options(headless=None):
|
||||
else:
|
||||
print(f" [ext] turnstilePatch missing: {EXTENSION_PATH}", flush=True)
|
||||
|
||||
# Apply config.json "proxy" to Chromium.
|
||||
proxy = (config.get("proxy") or "").strip()
|
||||
proxy = (config.get("proxy") or "").strip() if isinstance(config, dict) else ""
|
||||
if proxy:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
@@ -1184,21 +1163,11 @@ def create_browser_options(headless=None):
|
||||
port = u.port or (443 if (u.scheme or "http") == "https" else 80)
|
||||
scheme = u.scheme or "http"
|
||||
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
||||
print(f" [proxy] browser --proxy-server={scheme}://{host}:{port}", flush=True)
|
||||
except Exception as e:
|
||||
print(f" [proxy] set browser proxy failed: {e}")
|
||||
|
||||
apply_headless_to_options(options, use_hl)
|
||||
if use_hl:
|
||||
print(" [browser] headless=True", flush=True)
|
||||
else:
|
||||
print(
|
||||
f" [browser] headed (zip-style minimal flags) "
|
||||
f"DISPLAY={os.environ.get('DISPLAY', '')!r}",
|
||||
flush=True,
|
||||
)
|
||||
print(f" [proxy] set browser proxy failed: {e}", flush=True)
|
||||
return options
|
||||
|
||||
|
||||
def _build_request_kwargs(**kwargs):
|
||||
request_kwargs = dict(kwargs)
|
||||
proxies = request_kwargs.pop("proxies", None)
|
||||
@@ -4163,275 +4132,21 @@ return 'clicked';
|
||||
raise Exception("验证码已获取,但自动填写/提交失败")
|
||||
|
||||
|
||||
def _read_turnstile_token(page):
|
||||
"""Read cf-turnstile-response / turnstile.getResponse if present.
|
||||
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20):
|
||||
page = _get_page()
|
||||
if page is None:
|
||||
raise Exception("页面未就绪,无法执行 Turnstile")
|
||||
|
||||
Prefer DrissionPage native attr read (sees closed-shadow-ish values better),
|
||||
then JS walk of open shadow roots + turnstile.getResponse().
|
||||
"""
|
||||
# Path 1: native element value (historical success tokens were length ~773)
|
||||
for loc in (
|
||||
"@name=cf-turnstile-response",
|
||||
"css:input[name='cf-turnstile-response']",
|
||||
"css:textarea[name='cf-turnstile-response']",
|
||||
"tag:input@@name=cf-turnstile-response",
|
||||
):
|
||||
try:
|
||||
el = page.ele(loc, timeout=0.15)
|
||||
except Exception:
|
||||
el = None
|
||||
if el is None:
|
||||
continue
|
||||
try:
|
||||
val = el.attr("value")
|
||||
except Exception:
|
||||
val = None
|
||||
if not val:
|
||||
try:
|
||||
val = el.value
|
||||
except Exception:
|
||||
val = None
|
||||
if not val:
|
||||
try:
|
||||
val = page.run_js("return arguments[0] ? String(arguments[0].value||'') : '';", el)
|
||||
except Exception:
|
||||
val = None
|
||||
val = str(val or "").strip()
|
||||
if len(val) >= 80:
|
||||
return val
|
||||
|
||||
# Path 2: JS light DOM + open shadow + API
|
||||
try:
|
||||
token = page.run_js(
|
||||
"""
|
||||
try {
|
||||
function walk(root, acc, depth) {
|
||||
if (!root || depth > 6) return acc;
|
||||
try {
|
||||
const nodes = root.querySelectorAll
|
||||
? root.querySelectorAll('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]')
|
||||
: [];
|
||||
for (const n of nodes) acc.push(n);
|
||||
} catch (e) {}
|
||||
let children = [];
|
||||
try { children = root.querySelectorAll ? root.querySelectorAll('*') : []; } catch (e) { children = []; }
|
||||
for (const el of children) {
|
||||
try {
|
||||
if (el.shadowRoot) walk(el.shadowRoot, acc, depth + 1);
|
||||
} catch (e) {}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
const inputs = walk(document, [], 0);
|
||||
for (const input of inputs) {
|
||||
const v = String((input && input.value) || '').trim();
|
||||
if (v) return v;
|
||||
}
|
||||
if (window.turnstile && typeof turnstile.getResponse === 'function') {
|
||||
try {
|
||||
const r = String(turnstile.getResponse() || '').trim();
|
||||
if (r) return r;
|
||||
} catch (e) {}
|
||||
try {
|
||||
const r2 = String(turnstile.getResponse('') || '').trim();
|
||||
if (r2) return r2;
|
||||
} catch (e) {}
|
||||
}
|
||||
return '';
|
||||
} catch(e) { return ''; }
|
||||
"""
|
||||
)
|
||||
return str(token or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _turnstile_diag(page) -> dict:
|
||||
"""Lightweight CF widget diagnostics for logs (no secrets).
|
||||
|
||||
Searches light DOM + open shadow roots. Detects managed-mode "Verifying..."
|
||||
spinner text so callers can avoid reset/click storms.
|
||||
"""
|
||||
try:
|
||||
info = page.run_js(
|
||||
"""
|
||||
try {
|
||||
function walk(root, acc, depth) {
|
||||
if (!root || depth > 6) return acc;
|
||||
try {
|
||||
const q = root.querySelectorAll
|
||||
? root.querySelectorAll(
|
||||
'input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"], ' +
|
||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], ' +
|
||||
'div.cf-turnstile, [data-sitekey], iframe[title*="Cloudflare"], iframe[title*="cloudflare"], iframe[title*="Widget"]'
|
||||
)
|
||||
: [];
|
||||
for (const n of q) acc.push(n);
|
||||
} catch (e) {}
|
||||
let children = [];
|
||||
try { children = root.querySelectorAll ? root.querySelectorAll('*') : []; } catch (e) { children = []; }
|
||||
for (const el of children) {
|
||||
try { if (el.shadowRoot) walk(el.shadowRoot, acc, depth + 1); } catch (e) {}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
const all = walk(document, [], 0);
|
||||
const inputs = all.filter((n) => n && (n.tagName === 'INPUT' || n.tagName === 'TEXTAREA'));
|
||||
const iframes = all.filter((n) => n && n.tagName === 'IFRAME');
|
||||
const widgets = all.filter((n) => n && n.tagName !== 'IFRAME' && n.tagName !== 'INPUT' && n.tagName !== 'TEXTAREA');
|
||||
const input = inputs[0] || document.querySelector('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]');
|
||||
const srcs = iframes.slice(0, 3).map((f) => {
|
||||
try { return String(f.src || f.getAttribute('src') || '').slice(0, 120); } catch(e) { return ''; }
|
||||
});
|
||||
const bodyText = ((document.body && document.body.innerText) || '').slice(0, 4000);
|
||||
const verifying = /正在验证|verifying|checking|just a moment|请完成|人机验证/i.test(bodyText);
|
||||
// success checkmark / aria
|
||||
const successish = /成功|success|verified/i.test(bodyText) && !!input && String(input.value||'').trim().length >= 80;
|
||||
let sitekey = '';
|
||||
try {
|
||||
const w = widgets[0] || document.querySelector('[data-sitekey], div.cf-turnstile');
|
||||
if (w) sitekey = String(w.getAttribute('data-sitekey') || '').slice(0, 16);
|
||||
} catch (e) {}
|
||||
// iframe box size (0x0 means not painted)
|
||||
let iframeBox = '';
|
||||
try {
|
||||
if (iframes[0]) {
|
||||
const r = iframes[0].getBoundingClientRect();
|
||||
iframeBox = Math.round(r.width) + 'x' + Math.round(r.height);
|
||||
}
|
||||
} catch (e) {}
|
||||
return {
|
||||
hasInput: !!input,
|
||||
tokenLen: input ? String(input.value || '').trim().length : 0,
|
||||
iframeCount: iframes.length,
|
||||
widgetCount: widgets.length,
|
||||
sitekey: sitekey,
|
||||
iframeSrc0: srcs[0] || '',
|
||||
iframeBox: iframeBox,
|
||||
turnstileApi: !!(window.turnstile && typeof turnstile.getResponse === 'function'),
|
||||
webdriver: !!navigator.webdriver,
|
||||
verifying: verifying,
|
||||
successish: successish,
|
||||
};
|
||||
} catch(e) {
|
||||
return { err: String(e && e.message || e) };
|
||||
}
|
||||
"""
|
||||
)
|
||||
return info if isinstance(info, dict) else {"raw": str(info)[:200]}
|
||||
except Exception as exc:
|
||||
return {"err": str(exc)[:160]}
|
||||
|
||||
|
||||
def _click_turnstile_widget(page, log_callback=None) -> str:
|
||||
"""Best-effort click path for interactive Turnstile checkbox.
|
||||
|
||||
Ported from ReinerBRO/grok-register + Git-creat7/grokRegister-cpa:
|
||||
input[name=cf-turnstile-response] → parent shadow → iframe
|
||||
→ patch screenX/Y inside iframe → body.shadow_root → input.click()
|
||||
|
||||
Returns a short tag describing which path succeeded (or 'none').
|
||||
"""
|
||||
# Path A: classic ReinerBRO shadow-root checkbox
|
||||
try:
|
||||
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.6)
|
||||
except Exception:
|
||||
challenge_input = None
|
||||
if challenge_input:
|
||||
iframe = None
|
||||
try:
|
||||
wrapper = challenge_input.parent()
|
||||
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.6,
|
||||
)
|
||||
except Exception:
|
||||
iframe = None
|
||||
if iframe:
|
||||
# Match ReinerBRO exactly: value: not get: (CF reads own props).
|
||||
try:
|
||||
iframe.run_js(
|
||||
"""
|
||||
window.dtp = 1;
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
let screenX = getRandomInt(800, 1200);
|
||||
let screenY = getRandomInt(400, 600);
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|
||||
try {
|
||||
if (typeof PointerEvent !== 'undefined' && PointerEvent.prototype) {
|
||||
Object.defineProperty(PointerEvent.prototype, 'screenX', { value: screenX });
|
||||
Object.defineProperty(PointerEvent.prototype, 'screenY', { value: screenY });
|
||||
}
|
||||
} catch (e) {}
|
||||
"""
|
||||
page.run_js(
|
||||
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# A1: classic ReinerBRO path — body.shadow_root → tag:input → click
|
||||
try:
|
||||
body_sr = iframe.ele("tag:body").shadow_root
|
||||
btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark") or body_sr.ele("css:label")
|
||||
if btn:
|
||||
try:
|
||||
btn.click()
|
||||
except Exception:
|
||||
try:
|
||||
btn.click(by_js=True)
|
||||
except Exception:
|
||||
page.actions.click(btn)
|
||||
return "shadow-checkbox"
|
||||
except Exception:
|
||||
pass
|
||||
# A2: click iframe element itself (center)
|
||||
try:
|
||||
iframe.click()
|
||||
return "iframe-click"
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
page.actions.click(iframe)
|
||||
return "iframe-actions"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Path B: DOM query + synthetic pointer events (Git-creat7 fallback)
|
||||
try:
|
||||
clicked = page.run_js(
|
||||
"""
|
||||
const nodes = Array.from(document.querySelectorAll(
|
||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]'
|
||||
)).filter((n) => {
|
||||
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
|
||||
return String(txt).toLowerCase().includes('turnstile')
|
||||
|| String(n.getAttribute?.('src') || '').includes('challenges.cloudflare.com')
|
||||
|| n.hasAttribute?.('data-sitekey');
|
||||
});
|
||||
let ok = false;
|
||||
for (const n of nodes) {
|
||||
try {
|
||||
if (typeof n.click === 'function') { n.click(); ok = true; }
|
||||
} catch (e) {}
|
||||
}
|
||||
return ok;
|
||||
"""
|
||||
)
|
||||
if clicked:
|
||||
return "js-click"
|
||||
except Exception:
|
||||
pass
|
||||
return "none"
|
||||
|
||||
|
||||
def _read_turnstile_token_simple(page) -> str:
|
||||
"""Light token read used by solver (input + turnstile.getResponse)."""
|
||||
rounds = max(1, int(max_rounds or 20))
|
||||
for _ in range(0, rounds):
|
||||
raise_if_cancelled(cancel_callback)
|
||||
try:
|
||||
token = page.run_js(
|
||||
"""
|
||||
@@ -4445,130 +4160,17 @@ try {
|
||||
} catch(e) { return ''; }
|
||||
"""
|
||||
)
|
||||
return str(token or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _turnstile_ui_state(page) -> dict:
|
||||
"""Detect managed / interactive / failed Turnstile UI (best-effort)."""
|
||||
try:
|
||||
info = page.run_js(
|
||||
"""
|
||||
try {
|
||||
const body = ((document.body && document.body.innerText) || '').slice(0, 6000);
|
||||
const lower = body.toLowerCase();
|
||||
const input = document.querySelector('input[name="cf-turnstile-response"]');
|
||||
const tokenLen = input ? String(input.value || '').trim().length : 0;
|
||||
let iframeCount = 0;
|
||||
try {
|
||||
iframeCount = document.querySelectorAll(
|
||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
|
||||
).length;
|
||||
} catch (e) {}
|
||||
// open shadow iframes
|
||||
try {
|
||||
const all = document.querySelectorAll('*');
|
||||
for (const el of all) {
|
||||
if (el.shadowRoot) {
|
||||
iframeCount += el.shadowRoot.querySelectorAll(
|
||||
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
|
||||
).length;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
const interactive = /确认您是真人|verify you are human|确认您是人类|i am human|请完成安全检查/i.test(body);
|
||||
const verifying = /正在验证|verifying|checking your browser|just a moment/i.test(body);
|
||||
const failed = /验证失败|verification failed|try again|请重试|失败,请/i.test(body);
|
||||
return {
|
||||
tokenLen: tokenLen,
|
||||
iframeCount: iframeCount,
|
||||
interactive: interactive,
|
||||
verifying: verifying,
|
||||
failed: failed,
|
||||
};
|
||||
} catch (e) {
|
||||
return { err: String(e && e.message || e) };
|
||||
}
|
||||
"""
|
||||
)
|
||||
return info if isinstance(info, dict) else {}
|
||||
except Exception as exc:
|
||||
return {"err": str(exc)[:120]}
|
||||
|
||||
|
||||
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20):
|
||||
"""Turnstile solver (zip base + interactive-safe).
|
||||
|
||||
Critical fixes vs naive zip loop:
|
||||
- Do NOT reset when interactive checkbox already shown (reset kills progress → 验证失败)
|
||||
- Click at most once every few seconds; after click WAIT for token
|
||||
- Prefer real mouse actions over pure CDP ele.click spam
|
||||
"""
|
||||
page = _get_page()
|
||||
if page is None:
|
||||
raise Exception("页面未就绪,无法执行 Turnstile")
|
||||
|
||||
# Only reset when widget is missing / hard-failed — never while interactive is up.
|
||||
ui0 = _turnstile_ui_state(page)
|
||||
should_reset = False
|
||||
if ui0.get("failed"):
|
||||
should_reset = True
|
||||
elif not ui0.get("iframeCount") and not ui0.get("tokenLen"):
|
||||
should_reset = True
|
||||
if should_reset:
|
||||
try:
|
||||
page.run_js(
|
||||
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
|
||||
)
|
||||
if log_callback:
|
||||
log_callback("[Debug] Turnstile reset (widget missing/failed)")
|
||||
except Exception:
|
||||
pass
|
||||
elif log_callback and (ui0.get("interactive") or ui0.get("verifying")):
|
||||
log_callback(
|
||||
f"[Debug] Turnstile 已有 UI interactive={ui0.get('interactive')} "
|
||||
f"verifying={ui0.get('verifying')} — 跳过 reset,避免清掉真人勾选"
|
||||
)
|
||||
|
||||
rounds = max(1, int(max_rounds or 20))
|
||||
last_click_at = 0.0
|
||||
click_count = 0
|
||||
for round_i in range(0, rounds):
|
||||
raise_if_cancelled(cancel_callback)
|
||||
try:
|
||||
token = _read_turnstile_token_simple(page)
|
||||
token = str(token or "").strip()
|
||||
if len(token) >= 80:
|
||||
if log_callback:
|
||||
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
|
||||
log_callback(f"[Debug] Turnstile 已通过,token长度={len(token)}")
|
||||
return token
|
||||
|
||||
ui = _turnstile_ui_state(page)
|
||||
# After a click: prefer waiting over re-clicking (CF interactive fails on spam)
|
||||
now = time.time()
|
||||
min_click_gap = 4.5 if ui.get("interactive") else 2.8
|
||||
if ui.get("verifying") and not ui.get("failed"):
|
||||
if log_callback and round_i % 3 == 0:
|
||||
log_callback("[Debug] Turnstile verifying… 等待 token,不点击")
|
||||
cf_sleep(1.2, cancel_callback, floor=1.0)
|
||||
continue
|
||||
|
||||
# Only click when cool-down elapsed
|
||||
if now - last_click_at < min_click_gap and click_count > 0:
|
||||
cf_sleep(0.8, cancel_callback, floor=0.7)
|
||||
continue
|
||||
|
||||
# Zip path: input → parent.shadow_root → iframe → patch screenX → body.shadow_root → input
|
||||
challenge_input = None
|
||||
try:
|
||||
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5)
|
||||
except Exception:
|
||||
challenge_input = None
|
||||
clicked = False
|
||||
challenge_input = page.ele("@name=cf-turnstile-response")
|
||||
if challenge_input:
|
||||
wrapper = challenge_input.parent()
|
||||
iframe = None
|
||||
try:
|
||||
wrapper = challenge_input.parent()
|
||||
iframe = wrapper.shadow_root.ele("tag:iframe")
|
||||
except Exception:
|
||||
iframe = None
|
||||
@@ -4586,58 +4188,15 @@ Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Prefer real mouse path (less likely to trip interactive fail than ele.click spam)
|
||||
try:
|
||||
body_sr = iframe.ele("tag:body").shadow_root
|
||||
btn = (
|
||||
body_sr.ele("tag:input")
|
||||
or body_sr.ele("css:.mark")
|
||||
or body_sr.ele("css:label")
|
||||
or body_sr.ele("css:[role='checkbox']")
|
||||
)
|
||||
btn = body_sr.ele("tag:input")
|
||||
if btn:
|
||||
try:
|
||||
page.actions.move_to(btn).click()
|
||||
clicked = True
|
||||
path = "actions-checkbox"
|
||||
except Exception:
|
||||
try:
|
||||
btn.click()
|
||||
clicked = True
|
||||
path = "shadow-checkbox"
|
||||
except Exception:
|
||||
path = "none"
|
||||
if clicked:
|
||||
click_count += 1
|
||||
last_click_at = time.time()
|
||||
if log_callback:
|
||||
log_callback(
|
||||
f"[Debug] Turnstile click path={path} "
|
||||
f"round={round_i+1}/{rounds} clicks={click_count}"
|
||||
)
|
||||
# After interactive click, wait longer for token / fail UI
|
||||
cf_sleep(3.5 if ui.get("interactive") else 2.2, cancel_callback, floor=2.0)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
# iframe center click as last resort
|
||||
try:
|
||||
page.actions.move_to(iframe).click()
|
||||
clicked = True
|
||||
click_count += 1
|
||||
last_click_at = time.time()
|
||||
if log_callback:
|
||||
log_callback(
|
||||
f"[Debug] Turnstile click path=iframe-actions "
|
||||
f"round={round_i+1}/{rounds}"
|
||||
)
|
||||
cf_sleep(3.0, cancel_callback, floor=2.0)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
if not clicked:
|
||||
# Soft fallback: one JS click, not every round
|
||||
if click_count == 0 or (now - last_click_at) >= 5.0:
|
||||
else:
|
||||
# 兜底:尝试触发页面上可见的 Turnstile 容器
|
||||
page.run_js(
|
||||
"""
|
||||
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
|
||||
@@ -4647,18 +4206,12 @@ const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n
|
||||
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
|
||||
"""
|
||||
)
|
||||
click_count += 1
|
||||
last_click_at = time.time()
|
||||
if log_callback and (round_i == 0 or round_i % 5 == 0):
|
||||
log_callback(f"[Debug] Turnstile click path=js-fallback round={round_i+1}/{rounds}")
|
||||
except Exception:
|
||||
pass
|
||||
cf_sleep(1.0, cancel_callback, floor=0.9)
|
||||
sleep_with_cancel(1, cancel_callback)
|
||||
|
||||
raise Exception("Turnstile 获取 token 失败")
|
||||
|
||||
|
||||
|
||||
def build_profile():
|
||||
given_name_pool = [
|
||||
"Neo", "Ethan", "Liam", "Noah", "Lucas", "Mason", "Ryan", "Leo",
|
||||
@@ -4685,96 +4238,18 @@ def build_profile():
|
||||
|
||||
|
||||
def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):
|
||||
"""Fill profile form and submit after Turnstile is solved.
|
||||
|
||||
CF wait / retry loop follows Git-creat7/grokRegister-cpa:
|
||||
fill → if wait-cloudflare → after ~12s call getTurnstileToken (reset+click)
|
||||
→ re-fill token into hidden input → submit.
|
||||
"""
|
||||
page = _get_page()
|
||||
# zip 方案:靠 turnstilePatch 扩展,不额外 CDP stealth(stealth_mode=off)
|
||||
apply_stealth_patches(page, log_callback=log_callback)
|
||||
check_timeout(time.time())
|
||||
dismiss_cookie_banner(page, log_callback=log_callback)
|
||||
dump_state(page, "profile-form")
|
||||
take_screenshot(page, "profile-form")
|
||||
given_name, family_name, password = build_profile()
|
||||
# zip: 预热 Turnstile — 等 2 秒让 iframe 初始化,插件会自动点击 checkbox
|
||||
if log_callback:
|
||||
log_callback("[*] 预热 Turnstile...")
|
||||
try:
|
||||
human_sleep(2, cancel_callback)
|
||||
except Exception:
|
||||
cf_sleep(2.0, cancel_callback, floor=2.0)
|
||||
deadline = time.time() + timeout
|
||||
form_filled_once = False
|
||||
wait_cf_since = None
|
||||
last_cf_retry_at = 0.0
|
||||
last_cf_log_at = 0.0
|
||||
cf_token_fail_streak = 0
|
||||
try:
|
||||
# Git-creat7 风格:卡住就二次复用;允许多轮,别太早放弃
|
||||
max_cf_token_fails = max(3, int(config.get("turnstile_fast_fail_count", 3) or 3))
|
||||
except Exception:
|
||||
max_cf_token_fails = 3
|
||||
try:
|
||||
# 参考实现:等 12s 后开始 getTurnstileToken,间隔 8s
|
||||
cf_retry_after = float(config.get("turnstile_retry_after_sec", 45) or 45)
|
||||
except Exception:
|
||||
cf_retry_after = 12.0
|
||||
try:
|
||||
cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 8) or 8)
|
||||
except Exception:
|
||||
cf_retry_gap = 8.0
|
||||
try:
|
||||
# 参考 15-20 轮 reset+click;默认抬到 16
|
||||
turnstile_rounds = max(12, int(config.get("turnstile_max_rounds", 16) or 16))
|
||||
except Exception:
|
||||
turnstile_rounds = 16
|
||||
|
||||
def _sync_cf_token(token: str):
|
||||
return page.run_js(
|
||||
"""
|
||||
const token = String(arguments[0] || '').trim();
|
||||
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
|
||||
if (!cfInput || !token) return false;
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||
if (nativeSetter) nativeSetter.call(cfInput, token);
|
||||
else cfInput.value = token;
|
||||
cfInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
cfInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return String(cfInput.value || '').trim().length;
|
||||
""",
|
||||
token,
|
||||
)
|
||||
|
||||
def _retry_turnstile(reason: str) -> bool:
|
||||
nonlocal cf_token_fail_streak, last_cf_retry_at
|
||||
if log_callback:
|
||||
log_callback(f"[*] {reason}")
|
||||
try:
|
||||
token = getTurnstileToken(
|
||||
log_callback=log_callback,
|
||||
cancel_callback=cancel_callback,
|
||||
max_rounds=turnstile_rounds,
|
||||
)
|
||||
if token:
|
||||
cf_token_fail_streak = 0
|
||||
synced = _sync_cf_token(token)
|
||||
if log_callback:
|
||||
log_callback(f"[*] Turnstile 二次复用完成,回填长度={synced}")
|
||||
last_cf_retry_at = time.time()
|
||||
return True
|
||||
except Exception as cf_exc:
|
||||
cf_token_fail_streak += 1
|
||||
if log_callback:
|
||||
log_callback(
|
||||
f"[Debug] Turnstile 二次复用失败({cf_token_fail_streak}/{max_cf_token_fails}): {cf_exc}"
|
||||
)
|
||||
if cf_token_fail_streak >= max_cf_token_fails:
|
||||
raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}")
|
||||
last_cf_retry_at = time.time()
|
||||
return False
|
||||
|
||||
while time.time() < deadline:
|
||||
raise_if_cancelled(cancel_callback)
|
||||
@@ -4832,7 +4307,7 @@ const buttons = Array.from(document.querySelectorAll('button[type="submit"], but
|
||||
});
|
||||
const submitBtn = buttons.find((node) => {
|
||||
const t = (node.innerText || node.textContent || '').replace(/\\s+/g, '').toLowerCase();
|
||||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount') || t.includes('completesignup');
|
||||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount');
|
||||
});
|
||||
|
||||
// 必须等待 Cloudflare 校验通过后再提交
|
||||
@@ -4857,39 +4332,44 @@ return 'filled-no-submit';
|
||||
|
||||
if isinstance(filled, str) and filled.startswith("wait-cloudflare"):
|
||||
form_filled_once = True
|
||||
if log_callback:
|
||||
token_len = filled.split(":", 1)[1] if ":" in filled else "0"
|
||||
log_callback(f"[Debug] 资料已填写,等待 Cloudflare 人机验证通过... 当前token长度={token_len}")
|
||||
if token_len == "0":
|
||||
pause_seconds = random.uniform(1, 3)
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] Cloudflare token 为空,暂停 {pause_seconds:.1f}s 后继续检测")
|
||||
sleep_with_cancel(pause_seconds, cancel_callback)
|
||||
now = time.time()
|
||||
if wait_cf_since is None:
|
||||
wait_cf_since = now
|
||||
token_len = filled.split(":", 1)[1] if ":" in filled else "0"
|
||||
if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5):
|
||||
waited = now - wait_cf_since if wait_cf_since else 0
|
||||
log_callback(
|
||||
f"[*] 资料已填写,等待 Cloudflare 人机验证通过... "
|
||||
f"当前token长度={token_len} waited={waited:.0f}s"
|
||||
# 卡住后自动二次复用 Turnstile 组件
|
||||
if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8:
|
||||
if log_callback:
|
||||
log_callback("[Debug] Cloudflare 验证卡住,开始二次复用 Turnstile...")
|
||||
try:
|
||||
token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback)
|
||||
if token:
|
||||
synced = page.run_js(
|
||||
"""
|
||||
const token = String(arguments[0] || '').trim();
|
||||
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
|
||||
if (!cfInput || !token) return false;
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||
if (nativeSetter) nativeSetter.call(cfInput, token);
|
||||
else cfInput.value = token;
|
||||
cfInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
cfInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return String(cfInput.value || '').trim().length;
|
||||
""",
|
||||
token,
|
||||
)
|
||||
last_cf_log_at = now
|
||||
# Git-creat7: token 为空时多等 1-3s
|
||||
if token_len == "0":
|
||||
pause_seconds = random.uniform(1.0, 3.0)
|
||||
if log_callback and (now - last_cf_log_at >= 4):
|
||||
log_callback(f"[*] Cloudflare token 为空,暂停 {pause_seconds:.1f}s 后继续检测")
|
||||
last_cf_log_at = now
|
||||
sleep_with_cancel(pause_seconds, cancel_callback)
|
||||
# Cookie overlay can reappear mid-wait and keep iframe=0
|
||||
if wait_cf_since and (now - wait_cf_since) >= 2.0:
|
||||
try:
|
||||
dismiss_cookie_banner(page, log_callback=None)
|
||||
except Exception:
|
||||
pass
|
||||
# 轻点一次(参考会在 getTurnstileToken 内反复点;外层也补一次)
|
||||
if now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 3.0:
|
||||
try:
|
||||
_click_turnstile_widget(page)
|
||||
except Exception:
|
||||
pass
|
||||
# 卡住后二次复用 Turnstile 组件(reset + shadow click)
|
||||
if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap:
|
||||
_retry_turnstile("Cloudflare 验证卡住,开始二次复用 Turnstile...")
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] Turnstile 二次复用完成,回填长度={synced}")
|
||||
except Exception as cf_exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] Turnstile 二次复用失败: {cf_exc}")
|
||||
last_cf_retry_at = now
|
||||
sleep_with_cancel(0.8, cancel_callback)
|
||||
continue
|
||||
|
||||
@@ -4897,10 +4377,10 @@ return 'filled-no-submit';
|
||||
form_filled_once = True
|
||||
elif filled == "fill-failed" and log_callback:
|
||||
log_callback("[Debug] 资料输入失败,重试中...")
|
||||
human_sleep(0.5, cancel_callback)
|
||||
sleep_with_cancel(0.5, cancel_callback)
|
||||
continue
|
||||
elif filled == "not-ready":
|
||||
human_sleep(0.5, cancel_callback)
|
||||
sleep_with_cancel(0.5, cancel_callback)
|
||||
continue
|
||||
|
||||
submit_state = page.run_js(
|
||||
@@ -4936,7 +4416,7 @@ const buttons = Array.from(document.querySelectorAll('button[type="submit"], but
|
||||
});
|
||||
const submitBtn = buttons.find((node) => {
|
||||
const t = buttonText(node).replace(/\s+/g, '').toLowerCase();
|
||||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount') || t.includes('completesignup');
|
||||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount');
|
||||
});
|
||||
if (!submitBtn) {
|
||||
const visibleTexts = buttons.map(buttonText).filter(Boolean).slice(0, 8).join(' | ');
|
||||
@@ -4949,62 +4429,55 @@ return 'submitted';
|
||||
)
|
||||
|
||||
if isinstance(submit_state, str) and submit_state.startswith("wait-cloudflare"):
|
||||
if log_callback:
|
||||
token_len = submit_state.split(":", 1)[1] if ":" in submit_state else "0"
|
||||
log_callback(f"[Debug] 等待 Cloudflare 人机验证通过后再提交... 当前token长度={token_len}")
|
||||
now = time.time()
|
||||
if wait_cf_since is None:
|
||||
wait_cf_since = now
|
||||
ui = _turnstile_ui_state(page)
|
||||
if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5):
|
||||
token_len = submit_state.split(":", 1)[1] if ":" in submit_state else "0"
|
||||
waited = now - wait_cf_since if wait_cf_since else 0
|
||||
log_callback(
|
||||
f"[*] 等待 Cloudflare 人机验证通过后再提交... "
|
||||
f"当前token长度={token_len} waited={waited:.0f}s "
|
||||
f"interactive={ui.get('interactive')} verifying={ui.get('verifying')} failed={ui.get('failed')}"
|
||||
)
|
||||
last_cf_log_at = now
|
||||
# Interactive checkbox 已出现:不要每 3s 狂点,也不要立刻 reset 复用
|
||||
if ui.get("verifying") and not ui.get("failed"):
|
||||
sleep_with_cancel(1.2, cancel_callback)
|
||||
continue
|
||||
if ui.get("interactive") and not ui.get("failed"):
|
||||
# 轻点一次后耐心等 token;间隔拉长
|
||||
if wait_cf_since and now - last_cf_retry_at >= 6.0:
|
||||
try:
|
||||
_click_turnstile_widget(page)
|
||||
last_cf_retry_at = now
|
||||
if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8:
|
||||
if log_callback:
|
||||
log_callback("[Debug] interactive 模式:轻点一次 checkbox,等待通过…")
|
||||
except Exception:
|
||||
pass
|
||||
sleep_with_cancel(1.5, cancel_callback)
|
||||
continue
|
||||
# managed / 无 UI:沿用 zip 二次复用(内部已避免误 reset)
|
||||
if wait_cf_since and now - wait_cf_since >= 5.0 and now - last_cf_retry_at >= 5.0:
|
||||
log_callback("[Debug] 提交前仍卡住,自动再次复用 Turnstile...")
|
||||
try:
|
||||
_click_turnstile_widget(page)
|
||||
except Exception:
|
||||
pass
|
||||
if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap:
|
||||
_retry_turnstile("提交前仍卡住,自动再次复用 Turnstile...")
|
||||
token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback)
|
||||
if token:
|
||||
synced = page.run_js(
|
||||
"""
|
||||
const token = String(arguments[0] || '').trim();
|
||||
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
|
||||
if (!cfInput || !token) return false;
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||
if (nativeSetter) nativeSetter.call(cfInput, token);
|
||||
else cfInput.value = token;
|
||||
cfInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
cfInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return String(cfInput.value || '').trim().length;
|
||||
""",
|
||||
token,
|
||||
)
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] Turnstile 二次复用完成,回填长度={synced}")
|
||||
except Exception as cf_exc:
|
||||
if log_callback:
|
||||
log_callback(f"[Debug] Turnstile 二次复用失败: {cf_exc}")
|
||||
last_cf_retry_at = now
|
||||
sleep_with_cancel(0.8, cancel_callback)
|
||||
continue
|
||||
|
||||
if submit_state == "submitted":
|
||||
if log_callback:
|
||||
log_callback(f"[*] 已填写注册资料并提交: {given_name} {family_name}")
|
||||
log_callback(f"[Debug] 已填写注册资料并提交: {given_name} {family_name}")
|
||||
return {"given_name": given_name, "family_name": family_name, "password": password}
|
||||
wait_cf_since = None
|
||||
if isinstance(submit_state, str) and submit_state.startswith("no-submit-button") and log_callback:
|
||||
log_callback(f"[Debug] 未找到提交按钮,继续等待页面稳定... {submit_state[17:80]}")
|
||||
visible_buttons = submit_state.split(":", 1)[1] if ":" in submit_state else ""
|
||||
suffix = f" 可见按钮: {visible_buttons}" if visible_buttons else ""
|
||||
log_callback(f"[Debug] 未找到提交按钮,继续等待页面稳定...{suffix}")
|
||||
|
||||
human_sleep(0.5, cancel_callback)
|
||||
sleep_with_cancel(0.5, cancel_callback)
|
||||
|
||||
raise Exception("最终注册页资料填写失败")
|
||||
|
||||
|
||||
# ── NSFW 自动开启(从 AaronL725 移植) ──
|
||||
|
||||
|
||||
def generate_random_birthdate():
|
||||
"""生成随机生日(20-40 岁)。"""
|
||||
import datetime as dt
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replace local Turnstile/browser CF path with pure zip implementations.
|
||||
|
||||
Keeps local Gmail / hotmail / register_cli / cpa_xai intact.
|
||||
Source of truth: grok-register.zip (ReinerBRO) for CF + browser options.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"D:/Projects/AI/grok-keygen")
|
||||
# Git Bash /tmp maps to %TEMP% on Windows
|
||||
_TEMP = Path(os.environ.get("TEMP") or os.environ.get("TMP") or r"C:/Users/Chaos/AppData/Local/Temp")
|
||||
ZIP_REG = _TEMP / "zip_full/grok-register/grok_register_ttk.py"
|
||||
ZIP_CPA = _TEMP / "zip_full/grok_reg-protocol_cpa/grok_register_ttk.py"
|
||||
ZIP_PATCH = _TEMP / "zip_full/grok-register/turnstilePatch"
|
||||
LOCAL = ROOT / "grok_register_ttk.py"
|
||||
LOCAL_PATCH = ROOT / "turnstilePatch"
|
||||
|
||||
|
||||
def extract_func(text: str, name: str) -> str:
|
||||
lines = text.splitlines(keepends=True)
|
||||
starts = {}
|
||||
for i, line in enumerate(lines):
|
||||
m = re.match(r"^def (\w+)\(", line)
|
||||
if m:
|
||||
starts[m.group(1)] = i
|
||||
m = re.match(r"^([A-Z_][A-Z0-9_]*)\s*=", line)
|
||||
if m:
|
||||
starts[m.group(1)] = i
|
||||
if name not in starts:
|
||||
raise KeyError(f"missing {name}")
|
||||
s = starts[name]
|
||||
e = len(lines)
|
||||
for j in range(s + 1, len(lines)):
|
||||
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
||||
e = j
|
||||
break
|
||||
return "".join(lines[s:e]).rstrip() + "\n\n\n"
|
||||
|
||||
|
||||
def replace_func(src: str, name: str, new_body: str) -> str:
|
||||
"""Replace a top-level def/assignment block by name."""
|
||||
lines = src.splitlines(keepends=True)
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(rf"^def {re.escape(name)}\(", line) or re.match(
|
||||
rf"^{re.escape(name)}\s*=", line
|
||||
):
|
||||
start = i
|
||||
break
|
||||
if start is None:
|
||||
raise KeyError(f"local missing {name}")
|
||||
end = len(lines)
|
||||
for j in range(start + 1, len(lines)):
|
||||
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
||||
end = j
|
||||
break
|
||||
# preserve trailing blank style: ensure exactly two newlines before next
|
||||
body = new_body
|
||||
if not body.endswith("\n"):
|
||||
body += "\n"
|
||||
# normalize to end with two blank lines if next block exists
|
||||
while body.endswith("\n\n\n"):
|
||||
body = body[:-1]
|
||||
if not body.endswith("\n\n"):
|
||||
body = body.rstrip("\n") + "\n\n"
|
||||
return "".join(lines[:start]) + body + "".join(lines[end:])
|
||||
|
||||
|
||||
def remove_func(src: str, name: str) -> str:
|
||||
lines = src.splitlines(keepends=True)
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(rf"^def {re.escape(name)}\(", line) or re.match(
|
||||
rf"^{re.escape(name)}\s*=", line
|
||||
):
|
||||
start = i
|
||||
break
|
||||
if start is None:
|
||||
return src
|
||||
end = len(lines)
|
||||
for j in range(start + 1, len(lines)):
|
||||
if re.match(r"^(def |class |[A-Z_][A-Z0-9_]*\s*=)", lines[j]):
|
||||
end = j
|
||||
break
|
||||
return "".join(lines[:start]) + "".join(lines[end:])
|
||||
|
||||
|
||||
def adapt_reg_get_turnstile(zip_fn: str) -> str:
|
||||
"""grok-register uses session.page; local uses _get_page()."""
|
||||
text = zip_fn
|
||||
text = text.replace(
|
||||
"def getTurnstileToken(session, log_callback=None, cancel_callback=None):\n page = session.page\n",
|
||||
"def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20):\n page = _get_page()\n",
|
||||
)
|
||||
# support max_rounds
|
||||
text = text.replace(
|
||||
"for _ in range(0, 20):",
|
||||
"rounds = max(1, int(max_rounds or 20))\n for _ in range(0, rounds):",
|
||||
)
|
||||
# local uses human_sleep/cf_sleep; zip uses sleep_with_cancel — keep sleep_with_cancel (exists)
|
||||
return text
|
||||
|
||||
|
||||
def adapt_reg_fill_profile(zip_fn: str) -> str:
|
||||
"""Adapt fill_profile from grok-register to local globals."""
|
||||
text = zip_fn
|
||||
text = text.replace(
|
||||
"def fill_profile_and_submit(session, timeout=120, log_callback=None, cancel_callback=None):\n page = session.page\n",
|
||||
"def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):\n page = _get_page()\n check_timeout(time.time())\n dump_state(page, \"profile-form\")\n take_screenshot(page, \"profile-form\")\n",
|
||||
)
|
||||
# fix getTurnstileToken(session, ...) -> getTurnstileToken(...)
|
||||
text = text.replace(
|
||||
"token = getTurnstileToken(session, log_callback=log_callback, cancel_callback=cancel_callback)",
|
||||
"token = getTurnstileToken(log_callback=log_callback, cancel_callback=cancel_callback)",
|
||||
)
|
||||
# add zip warmup like protocol_cpa
|
||||
if "预热 Turnstile" not in text:
|
||||
text = text.replace(
|
||||
"given_name, family_name, password = build_profile()\n deadline = time.time() + timeout\n",
|
||||
"given_name, family_name, password = build_profile()\n"
|
||||
" if log_callback:\n"
|
||||
" log_callback(\"[*] 预热 Turnstile...\")\n"
|
||||
" human_sleep(2, cancel_callback)\n"
|
||||
" deadline = time.time() + timeout\n",
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def adapt_create_browser_options(zip_fn: str) -> str:
|
||||
"""Use zip minimal options; keep local headless/proxy helpers lightly."""
|
||||
# Prefer ReinerBRO pure options (no disable-images). Keep proxy + headless glue.
|
||||
return '''def create_browser_options(headless=None):
|
||||
"""Zip-style Chromium options (grok-register / ReinerBRO).
|
||||
|
||||
Critical for CF:
|
||||
- always load turnstilePatch
|
||||
- no --disable-images
|
||||
- no browser_path unless config/env set
|
||||
- no AutomationControlled / heavy stealth flags on headed
|
||||
"""
|
||||
options = ChromiumOptions()
|
||||
options.auto_port()
|
||||
options.set_timeouts(base=1)
|
||||
# ReinerBRO headed flags
|
||||
for flag in (
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-background-networking",
|
||||
"--disable-component-update",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
"--disable-renderer-backgrounding",
|
||||
):
|
||||
options.set_argument(flag)
|
||||
|
||||
use_headless = should_use_headless(headless, log_hint=True)
|
||||
if use_headless:
|
||||
apply_headless_to_options(options, True)
|
||||
for flag in (
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--mute-audio",
|
||||
):
|
||||
options.set_argument(flag)
|
||||
print(" [browser] headless=true", flush=True)
|
||||
else:
|
||||
print(" [browser] headless=false (zip headed)", flush=True)
|
||||
|
||||
# system default unless override
|
||||
browser_path = resolve_browser_path()
|
||||
if browser_path:
|
||||
try:
|
||||
options.set_browser_path(browser_path)
|
||||
print(f" [browser] path={browser_path}", flush=True)
|
||||
except Exception as exc:
|
||||
print(f" [browser] set path failed: {exc}", flush=True)
|
||||
else:
|
||||
print(" [browser] path=system-default (zip style)", flush=True)
|
||||
|
||||
# Force UA from config (Chrome/138 etc.)
|
||||
try:
|
||||
ua = get_user_agent()
|
||||
if ua:
|
||||
options.set_user_agent(ua)
|
||||
print(f" [browser] ua={ua[:60]}...", flush=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if os.path.exists(EXTENSION_PATH):
|
||||
try:
|
||||
options.add_extension(EXTENSION_PATH)
|
||||
print(f" [ext] turnstilePatch loaded: {EXTENSION_PATH}", flush=True)
|
||||
except Exception as ext_exc:
|
||||
print(f" [ext] turnstilePatch load failed: {ext_exc}", flush=True)
|
||||
else:
|
||||
print(f" [ext] turnstilePatch missing: {EXTENSION_PATH}", flush=True)
|
||||
|
||||
proxy = (config.get("proxy") or "").strip() if isinstance(config, dict) else ""
|
||||
if proxy:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
u = urlparse(proxy if "://" in proxy else f"http://{proxy}")
|
||||
host = u.hostname or ""
|
||||
if host:
|
||||
port = u.port or (443 if (u.scheme or "http") == "https" else 80)
|
||||
scheme = u.scheme or "http"
|
||||
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
||||
print(f" [proxy] browser --proxy-server={scheme}://{host}:{port}", flush=True)
|
||||
except Exception as e:
|
||||
print(f" [proxy] set browser proxy failed: {e}", flush=True)
|
||||
return options
|
||||
|
||||
|
||||
'''
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not ZIP_REG.is_file():
|
||||
raise SystemExit(f"missing {ZIP_REG}")
|
||||
reg_text = ZIP_REG.read_text(encoding="utf-8", errors="replace")
|
||||
local = LOCAL.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# backup
|
||||
bak = ROOT / "grok_register_ttk.py.bak_pre_zip_port"
|
||||
shutil.copy2(LOCAL, bak)
|
||||
print(f"backup -> {bak}")
|
||||
|
||||
# restore pure zip turnstilePatch
|
||||
if ZIP_PATCH.is_dir():
|
||||
LOCAL_PATCH.mkdir(exist_ok=True)
|
||||
for name in ("content.js", "manifest.json"):
|
||||
src = ZIP_PATCH / name
|
||||
if src.is_file():
|
||||
shutil.copy2(src, LOCAL_PATCH / name)
|
||||
print(f"copied {name}")
|
||||
# remove MV3 leftovers
|
||||
for stale in ("script.js",):
|
||||
p = LOCAL_PATCH / stale
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
print(f"removed {stale}")
|
||||
|
||||
get_ts = adapt_reg_get_turnstile(extract_func(reg_text, "getTurnstileToken"))
|
||||
fill = adapt_reg_fill_profile(extract_func(reg_text, "fill_profile_and_submit"))
|
||||
create_opts = adapt_create_browser_options("")
|
||||
|
||||
# Remove interactive-safe helpers that zip doesn't need (keep if referenced elsewhere carefully)
|
||||
for helper in (
|
||||
"_click_turnstile_widget",
|
||||
"_read_turnstile_token_simple",
|
||||
"_turnstile_ui_state",
|
||||
"_read_turnstile_token",
|
||||
"_turnstile_diag",
|
||||
):
|
||||
before = local
|
||||
local = remove_func(local, helper)
|
||||
if local != before:
|
||||
print(f"removed helper {helper}")
|
||||
|
||||
local = replace_func(local, "getTurnstileToken", get_ts)
|
||||
print("replaced getTurnstileToken")
|
||||
local = replace_func(local, "fill_profile_and_submit", fill)
|
||||
print("replaced fill_profile_and_submit")
|
||||
local = replace_func(local, "create_browser_options", create_opts)
|
||||
print("replaced create_browser_options")
|
||||
|
||||
# Simplify resolve_browser_path to zip style (auto -> None)
|
||||
resolve_browser = '''def resolve_browser_path():
|
||||
"""Zip style: only return path if config/env override exists.
|
||||
|
||||
browser_prefer=auto/system → None (DrissionPage system Chrome).
|
||||
"""
|
||||
override = str((config.get("browser_path") if isinstance(config, dict) else "") or "").strip()
|
||||
if override:
|
||||
norm = _normalize_browser_path(override)
|
||||
if norm:
|
||||
return norm
|
||||
print(f" [browser] ignore invalid browser_path={override!r}", flush=True)
|
||||
|
||||
for env_key in ("BROWSER_PATH", "CHROME_PATH", "CHROMIUM_PATH"):
|
||||
env_path = (os.environ.get(env_key) or "").strip()
|
||||
if not env_path:
|
||||
continue
|
||||
norm = _normalize_browser_path(env_path)
|
||||
if norm:
|
||||
return norm
|
||||
print(f" [browser] ignore invalid {env_key}={env_path!r}", flush=True)
|
||||
|
||||
prefer = str((config.get("browser_prefer") if isinstance(config, dict) else "") or "auto").strip().lower()
|
||||
if prefer in ("", "default", "detect", "system"):
|
||||
prefer = "auto"
|
||||
env_pref = (os.environ.get("BROWSER_PREFER") or "").strip().lower()
|
||||
if env_pref:
|
||||
prefer = env_pref
|
||||
|
||||
if prefer in ("auto", "system"):
|
||||
return None
|
||||
if prefer in ("chrome", "google-chrome", "google"):
|
||||
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
|
||||
elif prefer in ("chromium", "chromeium"):
|
||||
order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES)
|
||||
else:
|
||||
return None
|
||||
return _first_existing_browser(order)
|
||||
|
||||
|
||||
'''
|
||||
local = replace_func(local, "resolve_browser_path", resolve_browser)
|
||||
print("replaced resolve_browser_path")
|
||||
|
||||
# Force stealth off by default in apply_stealth_patches
|
||||
local = re.sub(
|
||||
r'mode = str\(\(config\.get\("stealth_mode"\).*?or "\w+"\)\.strip\(\)\.lower\(\)',
|
||||
'mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "off").strip().lower()',
|
||||
local,
|
||||
count=1,
|
||||
)
|
||||
|
||||
# Ensure fill_profile / other call sites don't reference removed helpers
|
||||
# getTurnstileToken(..., max_rounds=) still ok
|
||||
if "_turnstile_ui_state" in local or "_click_turnstile_widget" in local:
|
||||
print("WARN: leftover helper references:")
|
||||
for i, line in enumerate(local.splitlines(), 1):
|
||||
if "_turnstile_ui_state" in line or "_click_turnstile_widget" in line or "_read_turnstile_token" in line:
|
||||
print(f" L{i}: {line.strip()[:120]}")
|
||||
|
||||
LOCAL.write_text(local, encoding="utf-8")
|
||||
print(f"wrote {LOCAL} ({len(local.splitlines())} lines)")
|
||||
|
||||
# syntax check
|
||||
import py_compile
|
||||
|
||||
py_compile.compile(str(LOCAL), doraise=True)
|
||||
print("syntax OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,5 @@
|
||||
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证
|
||||
// 在 document_start 阶段执行,确保在页面脚本之前生效
|
||||
// Source: grok-register / grok_reg-protocol_cpa (zip reference)
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -55,29 +54,7 @@
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// 6. CDP MouseEvent.screenX/screenY 与 client 相同会被 CF 检测(Chromium 40280325)
|
||||
// 两个 zip 的 getTurnstileToken 也会在 iframe 内再补一次;扩展层提前全局打补丁更稳
|
||||
try {
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
var _sx = getRandomInt(800, 1200);
|
||||
var _sy = getRandomInt(400, 600);
|
||||
Object.defineProperty(MouseEvent.prototype, "screenX", {
|
||||
configurable: true,
|
||||
get: function () {
|
||||
return _sx + (this.clientX || 0);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(MouseEvent.prototype, "screenY", {
|
||||
configurable: true,
|
||||
get: function () {
|
||||
return _sy + (this.clientY || 0);
|
||||
},
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// 7. 页面加载完成后,自动监控并点击 Turnstile 复选框
|
||||
// 6. 页面加载完成后,自动监控并点击 Turnstile 复选框
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", autoClickTurnstile);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user