Compare commits
4
Commits
3a0a76bf46
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e0f45ac3b | ||
|
|
60e24b89c1 | ||
|
|
c87b01967f | ||
|
|
7a02453601 |
No files matched your search
+4
-4
@@ -99,14 +99,14 @@
|
|||||||
"turnstile_retry_limit": 3,
|
"turnstile_retry_limit": 3,
|
||||||
"// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)",
|
"// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)",
|
||||||
"turnstile_stuck_timeout": 150,
|
"turnstile_stuck_timeout": 150,
|
||||||
"// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(服务器建议 4)",
|
"// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(参考 Git-creat7 建议 3)",
|
||||||
"turnstile_fast_fail_count": 4,
|
"turnstile_fast_fail_count": 3,
|
||||||
"// turnstile_retry_after_sec": "资料页等待 CF 多久后触发二次复用(秒)",
|
"// turnstile_retry_after_sec": "资料页等待 CF 多久后触发二次复用(秒)",
|
||||||
"turnstile_retry_after_sec": 12,
|
"turnstile_retry_after_sec": 12,
|
||||||
"// turnstile_retry_gap_sec": "两次二次复用之间的最小间隔(秒)",
|
"// turnstile_retry_gap_sec": "两次二次复用之间的最小间隔(秒)",
|
||||||
"turnstile_retry_gap_sec": 8,
|
"turnstile_retry_gap_sec": 8,
|
||||||
"// turnstile_max_rounds": "单次 getTurnstileToken 内 click/poll 轮数",
|
"// turnstile_max_rounds": "单次 getTurnstileToken 内 reset+click 轮数(参考 15-20)",
|
||||||
"turnstile_max_rounds": 8,
|
"turnstile_max_rounds": 16,
|
||||||
"// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)",
|
"// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)",
|
||||||
"sso_timeout_base": 240,
|
"sso_timeout_base": 240,
|
||||||
"sso_timeout_max": 480,
|
"sso_timeout_max": 480,
|
||||||
|
|||||||
+9
-5
@@ -46,9 +46,9 @@
|
|||||||
"mail_poll_interval": 1,
|
"mail_poll_interval": 1,
|
||||||
"mail_retry_count": 3,
|
"mail_retry_count": 3,
|
||||||
"code_form_timeout": 180,
|
"code_form_timeout": 180,
|
||||||
"profile_timeout": 50,
|
"profile_timeout": 180,
|
||||||
"turnstile_retry_limit": 3,
|
"turnstile_retry_limit": 3,
|
||||||
"turnstile_stuck_timeout": 150,
|
"turnstile_stuck_timeout": 180,
|
||||||
"sso_timeout_base": 60,
|
"sso_timeout_base": 60,
|
||||||
"sso_timeout_max": 480,
|
"sso_timeout_max": 480,
|
||||||
"sso_progress_extension": 120,
|
"sso_progress_extension": 120,
|
||||||
@@ -98,8 +98,12 @@
|
|||||||
"gmail_imap_fallback": true,
|
"gmail_imap_fallback": true,
|
||||||
"gmail_imap_fallback_timeout_sec": 45,
|
"gmail_imap_fallback_timeout_sec": 45,
|
||||||
"gmail_imap_fallback_resend_sec": 10,
|
"gmail_imap_fallback_resend_sec": 10,
|
||||||
"turnstile_fast_fail_count": 4,
|
"turnstile_fast_fail_count": 3,
|
||||||
"turnstile_retry_after_sec": 12,
|
"turnstile_retry_after_sec": 10,
|
||||||
"turnstile_retry_gap_sec": 8,
|
"turnstile_retry_gap_sec": 8,
|
||||||
"turnstile_max_rounds": 8
|
"turnstile_max_rounds": 20,
|
||||||
|
"browser_prefer": "auto",
|
||||||
|
"browser_path": "",
|
||||||
|
"browser_headless": false,
|
||||||
|
"stealth_mode": "off"
|
||||||
}
|
}
|
||||||
+561
-437
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||||
@@ -419,9 +419,31 @@ _resolve_bin() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find_playwright_chromium() {
|
||||||
|
# Playwright Chromium 无 snap AppArmor 限制,ReinerBRO/grok-register 同款优先路径
|
||||||
|
local root cand
|
||||||
|
for root in \
|
||||||
|
"${HOME}/.cache/ms-playwright" \
|
||||||
|
"${LOCALAPPDATA:-}/ms-playwright"; do
|
||||||
|
[ -d "$root" ] || continue
|
||||||
|
# 取最新 chromium-* 目录
|
||||||
|
cand=$(find "$root" -maxdepth 3 -type f \( -path '*/chrome-linux64/chrome' -o -path '*/chrome-linux/chrome' -o -path '*/chrome-win64/chrome.exe' \) 2>/dev/null | sort -r | head -1 || true)
|
||||||
|
if [ -n "$cand" ] && [ -x "$cand" ] || [ -f "$cand" ]; then
|
||||||
|
echo "$cand"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
find_chromium_bin() {
|
find_chromium_bin() {
|
||||||
local candidate p
|
local candidate p
|
||||||
# 优先 deb/系统包路径,snap 放后面(且不 resolve 成 /usr/bin/snap)
|
# 1) Playwright Chromium(推荐,服务器无 AppArmor/snap 坑)
|
||||||
|
if p="$(find_playwright_chromium 2>/dev/null)"; then
|
||||||
|
echo "$p"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 2) deb/系统包路径,snap 放后面(且不 resolve 成 /usr/bin/snap)
|
||||||
for candidate in \
|
for candidate in \
|
||||||
/usr/bin/chromium \
|
/usr/bin/chromium \
|
||||||
/usr/bin/chromium-browser \
|
/usr/bin/chromium-browser \
|
||||||
@@ -439,7 +461,7 @@ find_chromium_bin() {
|
|||||||
esac
|
esac
|
||||||
# 必须像 chromium
|
# 必须像 chromium
|
||||||
case "$p" in
|
case "$p" in
|
||||||
*chromium*|*Chromium*)
|
*chromium*|*Chromium*|*chrome-linux*)
|
||||||
echo "$p"
|
echo "$p"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
@@ -733,20 +755,34 @@ main() {
|
|||||||
export CHROMIUM_PATH
|
export CHROMIUM_PATH
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 组装 env:绝不把空 DISPLAY= 传给子进程。
|
||||||
|
# 否则会覆盖 xvfb-run 自动分配的 DISPLAY,Python 侧 has_display=False → 强制 headless,
|
||||||
|
# Turnstile 会卡在 token长度=0(参考 grokRegister-cpa 的成功路径是纯 headed + extension)。
|
||||||
|
local -a env_args=(
|
||||||
|
"BROWSER_PREFER=${BROWSER_PREFER:-}"
|
||||||
|
"BROWSER_PATH=${BROWSER_PATH:-}"
|
||||||
|
"CHROMIUM_PATH=${CHROMIUM_PATH:-}"
|
||||||
|
)
|
||||||
|
if [ -n "${DISPLAY:-}" ]; then
|
||||||
|
env_args+=("DISPLAY=${DISPLAY}")
|
||||||
|
fi
|
||||||
|
if [ -n "${XAUTHORITY:-}" ]; then
|
||||||
|
env_args+=("XAUTHORITY=${XAUTHORITY}")
|
||||||
|
fi
|
||||||
|
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
|
||||||
|
env_args+=("WAYLAND_DISPLAY=${WAYLAND_DISPLAY}")
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ${#RUN_PREFIX[@]} -gt 0 ]; then
|
if [ ${#RUN_PREFIX[@]} -gt 0 ]; then
|
||||||
echo -e "${CYAN}[*] 启动命令: ${RUN_PREFIX[*]} uv run --python 3.13 python -u register_cli.py ${args[*]}${NC}"
|
echo -e "${CYAN}[*] 启动命令: ${RUN_PREFIX[*]} uv run --python 3.13 python -u register_cli.py ${args[*]}${NC}"
|
||||||
|
echo -e "${CYAN}[*] 注意: 不向 xvfb-run 子进程注入空 DISPLAY(保留虚拟显示)${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
exec "${RUN_PREFIX[@]}" env \
|
exec "${RUN_PREFIX[@]}" env \
|
||||||
BROWSER_PREFER="${BROWSER_PREFER:-}" \
|
"${env_args[@]}" \
|
||||||
BROWSER_PATH="${BROWSER_PATH:-}" \
|
|
||||||
CHROMIUM_PATH="${CHROMIUM_PATH:-}" \
|
|
||||||
DISPLAY="${DISPLAY:-}" \
|
|
||||||
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||||||
else
|
else
|
||||||
exec env \
|
exec env \
|
||||||
BROWSER_PREFER="${BROWSER_PREFER:-}" \
|
"${env_args[@]}" \
|
||||||
BROWSER_PATH="${BROWSER_PATH:-}" \
|
|
||||||
CHROMIUM_PATH="${CHROMIUM_PATH:-}" \
|
|
||||||
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-92
@@ -1,17 +1,11 @@
|
|||||||
// Turnstile Patch - minimal only
|
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证
|
||||||
// Full aggressive patches (chrome.runtime delete / permissions / plugins / languages
|
// 在 document_start 阶段执行,确保在页面脚本之前生效
|
||||||
// / 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.
|
|
||||||
// Note: CF challenge iframes are cross-origin — contentDocument click usually fails;
|
|
||||||
// host-page click + Python CDP shadow path do the real work.
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
if (window.__grokStealthApplied) return;
|
|
||||||
window.__grokStealthApplied = true;
|
|
||||||
|
|
||||||
// 1. Hide navigator.webdriver (instance + prototype when possible)
|
// 1. 隐藏 navigator.webdriver 标识
|
||||||
|
// Chrome 自动化模式下 navigator.webdriver = true,Turnstile 会检测此属性
|
||||||
try {
|
try {
|
||||||
Object.defineProperty(navigator, "webdriver", {
|
Object.defineProperty(navigator, "webdriver", {
|
||||||
get: function () {
|
get: function () {
|
||||||
@@ -20,113 +14,101 @@
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 2. 移除 Chrome 自动化相关的 Runtime 属性
|
||||||
try {
|
try {
|
||||||
var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
|
if (window.chrome && window.chrome.runtime) {
|
||||||
if (!desc || desc.configurable) {
|
delete window.chrome.runtime.onConnect;
|
||||||
Object.defineProperty(Navigator.prototype, "webdriver", {
|
delete window.chrome.runtime.onMessage;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 3. 覆盖 permissions.query,隐藏 notifications 权限异常
|
||||||
|
try {
|
||||||
|
var origQuery = navigator.permissions.query.bind(navigator.permissions);
|
||||||
|
navigator.permissions.query = function (params) {
|
||||||
|
if (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 false;
|
return [1, 2, 3, 4, 5];
|
||||||
},
|
},
|
||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
function firePointer(el) {
|
// 5. 修补 languages 属性
|
||||||
if (!el) return;
|
|
||||||
try {
|
try {
|
||||||
el.scrollIntoView({ block: "center", inline: "center" });
|
Object.defineProperty(navigator, "languages", {
|
||||||
} catch (e) {}
|
get: function () {
|
||||||
var rect;
|
return ["en-US", "en"];
|
||||||
try {
|
},
|
||||||
rect = el.getBoundingClientRect();
|
configurable: true,
|
||||||
} catch (e) {
|
});
|
||||||
rect = null;
|
|
||||||
}
|
|
||||||
var x = 12;
|
|
||||||
var y = 12;
|
|
||||||
if (rect && rect.width > 0) {
|
|
||||||
x = rect.left + Math.max(6, Math.min(rect.width - 6, rect.width * 0.35));
|
|
||||||
y = rect.top + Math.max(6, Math.min(rect.height - 6, rect.height * 0.5));
|
|
||||||
}
|
|
||||||
var opts = {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
view: window,
|
|
||||||
clientX: x,
|
|
||||||
clientY: y,
|
|
||||||
button: 0,
|
|
||||||
};
|
|
||||||
var types = [
|
|
||||||
"pointerover",
|
|
||||||
"mouseover",
|
|
||||||
"mousemove",
|
|
||||||
"pointerdown",
|
|
||||||
"mousedown",
|
|
||||||
"pointerup",
|
|
||||||
"mouseup",
|
|
||||||
"click",
|
|
||||||
];
|
|
||||||
for (var i = 0; i < types.length; i++) {
|
|
||||||
var t = types[i];
|
|
||||||
try {
|
|
||||||
var Ev = t.indexOf("pointer") === 0 ? PointerEvent : MouseEvent;
|
|
||||||
el.dispatchEvent(new Ev(t, opts));
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
el.dispatchEvent(new MouseEvent(t, opts));
|
|
||||||
} catch (e2) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (typeof el.click === "function") el.click();
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 6. 页面加载完成后,自动监控并点击 Turnstile 复选框
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", autoClickTurnstile);
|
||||||
|
} else {
|
||||||
|
autoClickTurnstile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Best-effort Turnstile interaction (same-origin iframe + host widget)
|
function autoClickTurnstile() {
|
||||||
function tryClickTurnstile() {
|
// 定时检查 Turnstile iframe 是否出现
|
||||||
|
var checkCount = 0;
|
||||||
|
var maxChecks = 100; // 最多检查 100 次(约 50 秒)
|
||||||
|
var timer = setInterval(function () {
|
||||||
|
checkCount++;
|
||||||
|
if (checkCount > maxChecks) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
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++) {
|
||||||
// Host-side synthetic click (works even when iframe is cross-origin)
|
var iframe = iframes[i];
|
||||||
firePointer(iframes[i]);
|
|
||||||
try {
|
try {
|
||||||
var body = iframes[i].contentDocument || iframes[i].contentWindow.document;
|
// 尝试访问 iframe 内部的 checkbox
|
||||||
if (!body) continue;
|
var body = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
var checkbox = body.querySelector(
|
var checkbox = body.querySelector(
|
||||||
'input[type="checkbox"], .mark, label, [role="checkbox"]'
|
'input[type="checkbox"], .mark, #cf-chl-widget-nomu1_resp'
|
||||||
);
|
);
|
||||||
if (checkbox && !checkbox.checked) {
|
if (checkbox && !checkbox.checked) {
|
||||||
firePointer(checkbox);
|
checkbox.click();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// cross-origin: host click above is enough
|
// 跨域限制,尝试通过 postMessage 触发
|
||||||
}
|
try {
|
||||||
}
|
iframe.contentWindow.postMessage(
|
||||||
var widgets = document.querySelectorAll(
|
{ type: "turnstile-auto-click" },
|
||||||
"div.cf-turnstile, [data-sitekey], input[name='cf-turnstile-response']"
|
"*"
|
||||||
);
|
);
|
||||||
for (var j = 0; j < widgets.length; j++) {
|
} catch (e2) {}
|
||||||
firePointer(widgets[j]);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 也尝试直接操作 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) {}
|
||||||
}
|
|
||||||
|
|
||||||
function startLoop() {
|
|
||||||
var n = 0;
|
|
||||||
var t = setInterval(function () {
|
|
||||||
n++;
|
|
||||||
tryClickTurnstile();
|
|
||||||
// ~90s of best-effort clicks (was 40s)
|
|
||||||
if (n > 180) clearInterval(t);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", startLoop);
|
|
||||||
} else {
|
|
||||||
startLoop();
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 2,
|
||||||
"name": "Turnstile Patch",
|
"name": "Turnstile Patch",
|
||||||
"version": "1.3.0",
|
"version": "1.0.0",
|
||||||
"description": "Minimal webdriver hide + best-effort host click for Turnstile",
|
"description": "Patch browser automation detection for Turnstile",
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": [
|
"matches": ["<all_urls>"],
|
||||||
"<all_urls>"
|
"js": ["content.js"],
|
||||||
],
|
|
||||||
"js": [
|
|
||||||
"content.js"
|
|
||||||
],
|
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": true,
|
"all_frames": true
|
||||||
"world": "MAIN",
|
|
||||||
"match_about_blank": true
|
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"permissions": ["activeTab"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user