Compare commits

..
4 Commits
Author SHA1 Message Date
chaos 9e0f45ac3b 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.
2026-07-12 07:09:45 +08:00
chaos 60e24b89c1 Align Turnstile with zip refs and harden interactive CF path
Restore MV2 turnstilePatch (content.js), zip-style headed browser flags,
system-default browser path, and interactive-safe solver that skips reset
while the human checkbox is up to avoid 验证失败 after the button appears.
2026-07-12 07:04:38 +08:00
chaos c87b01967f Refactor registration flow, update config, replace Turnstile patch
- Major refactor of grok_register_ttk.py: improved flow, error handling, logging
- Update config.example.json and config.json with new fields
- Replace turnstilePatch/content.js with new script.js approach
- Update start.sh for better Chromium/Turnstile handling
- Remove deprecated turnstilePatch/content.js
2026-07-12 14:44:02 +08:00
chaos 7a02453601 Fix Xvfb headed Turnstile path for headless servers
- start.sh: never inject empty DISPLAY= into xvfb-run children
  (was forcing Python headless and killing Turnstile)
- headed Chromium uses base flags only; headless-only flags
  (disable-software-rasterizer/gpu) no longer applied under Xvfb
- Align closer to Git-creat7/grokRegister-cpa headed+extension path
2026-07-12 01:14:18 +08:00
7 changed files with 1100 additions and 614 deletions

No files matched your search

+4 -4
View File
@@ -99,14 +99,14 @@
"turnstile_retry_limit": 3,
"// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)",
"turnstile_stuck_timeout": 150,
"// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(服务器建议 4)",
"turnstile_fast_fail_count": 4,
"// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(参考 Git-creat7 建议 3)",
"turnstile_fast_fail_count": 3,
"// turnstile_retry_after_sec": "资料页等待 CF 多久后触发二次复用(秒)",
"turnstile_retry_after_sec": 12,
"// turnstile_retry_gap_sec": "两次二次复用之间的最小间隔(秒)",
"turnstile_retry_gap_sec": 8,
"// turnstile_max_rounds": "单次 getTurnstileToken 内 click/poll 轮数",
"turnstile_max_rounds": 8,
"// turnstile_max_rounds": "单次 getTurnstileToken 内 reset+click 轮数(参考 15-20)",
"turnstile_max_rounds": 16,
"// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)",
"sso_timeout_base": 240,
"sso_timeout_max": 480,
+9 -5
View File
@@ -46,9 +46,9 @@
"mail_poll_interval": 1,
"mail_retry_count": 3,
"code_form_timeout": 180,
"profile_timeout": 50,
"profile_timeout": 180,
"turnstile_retry_limit": 3,
"turnstile_stuck_timeout": 150,
"turnstile_stuck_timeout": 180,
"sso_timeout_base": 60,
"sso_timeout_max": 480,
"sso_progress_extension": 120,
@@ -98,8 +98,12 @@
"gmail_imap_fallback": true,
"gmail_imap_fallback_timeout_sec": 45,
"gmail_imap_fallback_resend_sec": 10,
"turnstile_fast_fail_count": 4,
"turnstile_retry_after_sec": 12,
"turnstile_fast_fail_count": 3,
"turnstile_retry_after_sec": 10,
"turnstile_retry_gap_sec": 8,
"turnstile_max_rounds": 8
"turnstile_max_rounds": 20,
"browser_prefer": "auto",
"browser_path": "",
"browser_headless": false,
"stealth_mode": "off"
}
+599 -475
View File
File diff suppressed because it is too large. Load diff
+345
View File
@@ -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()
+45 -9
View File
@@ -419,9 +419,31 @@ _resolve_bin() {
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() {
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 \
/usr/bin/chromium \
/usr/bin/chromium-browser \
@@ -439,7 +461,7 @@ find_chromium_bin() {
esac
# 必须像 chromium
case "$p" in
*chromium*|*Chromium*)
*chromium*|*Chromium*|*chrome-linux*)
echo "$p"
return 0
;;
@@ -733,20 +755,34 @@ main() {
export CHROMIUM_PATH
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
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 ""
exec "${RUN_PREFIX[@]}" env \
BROWSER_PREFER="${BROWSER_PREFER:-}" \
BROWSER_PATH="${BROWSER_PATH:-}" \
CHROMIUM_PATH="${CHROMIUM_PATH:-}" \
DISPLAY="${DISPLAY:-}" \
"${env_args[@]}" \
uv run --python 3.13 python -u register_cli.py "${args[@]}"
else
exec env \
BROWSER_PREFER="${BROWSER_PREFER:-}" \
BROWSER_PATH="${BROWSER_PATH:-}" \
CHROMIUM_PATH="${CHROMIUM_PATH:-}" \
"${env_args[@]}" \
uv run --python 3.13 python -u register_cli.py "${args[@]}"
fi
}
+90 -108
View File
@@ -1,17 +1,11 @@
// Turnstile Patch - minimal only
// Full aggressive patches (chrome.runtime delete / permissions / plugins / languages
// / iframe postMessage spam) were observed to break xAI signup with:
// [permission_denied] HTTP 403
// Keep webdriver hide only. Auto-click remains best-effort and local-only.
// Note: CF challenge iframes are cross-origin — contentDocument click usually fails;
// host-page click + Python CDP shadow path do the real work.
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证
// 在 document_start 阶段执行,确保在页面脚本之前生效
(function () {
"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 {
Object.defineProperty(navigator, "webdriver", {
get: function () {
@@ -20,113 +14,101 @@
configurable: true,
});
} catch (e) {}
// 2. 移除 Chrome 自动化相关的 Runtime 属性
try {
var desc = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver");
if (!desc || desc.configurable) {
Object.defineProperty(Navigator.prototype, "webdriver", {
get: function () {
return false;
},
configurable: true,
});
if (window.chrome && window.chrome.runtime) {
delete window.chrome.runtime.onConnect;
delete window.chrome.runtime.onMessage;
}
} catch (e) {}
function firePointer(el) {
if (!el) return;
try {
el.scrollIntoView({ block: "center", inline: "center" });
} catch (e) {}
var rect;
try {
rect = el.getBoundingClientRect();
} 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,
// 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);
};
var types = [
"pointerover",
"mouseover",
"mousemove",
"pointerdown",
"mousedown",
"pointerup",
"mouseup",
"click",
];
for (var i = 0; i < types.length; i++) {
var t = types[i];
} catch (e) {}
// 4. 修补 plugin 数量,模拟正常浏览器
try {
Object.defineProperty(navigator, "plugins", {
get: function () {
return [1, 2, 3, 4, 5];
},
configurable: true,
});
} catch (e) {}
// 5. 修补 languages 属性
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 {
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) {}
}
// 2. Best-effort Turnstile interaction (same-origin iframe + host widget)
function tryClickTurnstile() {
try {
var iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
);
for (var i = 0; i < iframes.length; i++) {
// Host-side synthetic click (works even when iframe is cross-origin)
firePointer(iframes[i]);
try {
var body = iframes[i].contentDocument || iframes[i].contentWindow.document;
if (!body) continue;
var checkbox = body.querySelector(
'input[type="checkbox"], .mark, label, [role="checkbox"]'
);
if (checkbox && !checkbox.checked) {
firePointer(checkbox);
// 查找 Turnstile iframe
var iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
);
for (var i = 0; i < iframes.length; i++) {
var iframe = iframes[i];
try {
// 尝试访问 iframe 内部的 checkbox
var body = iframe.contentDocument || iframe.contentWindow.document;
var checkbox = body.querySelector(
'input[type="checkbox"], .mark, #cf-chl-widget-nomu1_resp'
);
if (checkbox && !checkbox.checked) {
checkbox.click();
}
} catch (e) {
// 跨域限制,尝试通过 postMessage 触发
try {
iframe.contentWindow.postMessage(
{ type: "turnstile-auto-click" },
"*"
);
} catch (e2) {}
}
} catch (e) {
// cross-origin: host click above is enough
}
}
var widgets = document.querySelectorAll(
"div.cf-turnstile, [data-sitekey], input[name='cf-turnstile-response']"
);
for (var j = 0; j < widgets.length; j++) {
firePointer(widgets[j]);
}
} 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);
// 也尝试直接操作 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) {}
}, 500);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", startLoop);
} else {
startLoop();
}
})();
+8 -13
View File
@@ -1,20 +1,15 @@
{
"manifest_version": 3,
"manifest_version": 2,
"name": "Turnstile Patch",
"version": "1.3.0",
"description": "Minimal webdriver hide + best-effort host click for Turnstile",
"version": "1.0.0",
"description": "Patch browser automation detection for Turnstile",
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN",
"match_about_blank": true
"all_frames": true
}
]
],
"permissions": ["activeTab"]
}