Files
grok-register/registration_browser.py
Aaron LiangandGitHub 4b030214d9 docs: clarify Python module responsibilities
Rewrite the module-level descriptions for all 28 Python files and remove the obsolete integration note from grok_register_ttk.py. Verified on Python 3.9 and Python 3.12.
2026-07-15 12:26:57 +08:00

1301 lines
54 KiB
Python

"""管理主注册浏览器生命周期并实现注册页面自动化操作。"""
import gc
import random
import re
import secrets
import struct
import time
from DrissionPage import Chromium
from DrissionPage.errors import PageDisconnectedError
from curl_cffi import requests
browser = None
page = None
browser_proxy_bridge = None
browser_started_with_proxy = False
cf_clearance = ""
SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com"
_OWN_NAMES = {'is_cloudflare_block_response', 'response_preview', 'start_browser', 'enable_nsfw_for_token', 'stop_browser_proxy_bridge', 'set_tos_accepted', 'fill_email_and_submit', 'getTurnstileToken', 'set_birth_date', 'generate_random_birthdate', 'fill_profile_and_submit', 'click_email_signup_button', 'wait_for_sso_cookie', 'fill_code_and_submit', 'build_profile', 'cleanup_runtime_memory', 'open_signup_page', 'stop_browser', 'encode_grpc_nsfw_settings', 'restart_browser', 'has_profile_form', 'update_nsfw_settings', 'refresh_active_page'}
def bind_runtime(namespace):
for name, value in namespace.items():
if name.startswith("__") or name in _OWN_NAMES or name in {
"browser", "page", "browser_proxy_bridge", "browser_started_with_proxy", "cf_clearance",
}:
continue
globals()[name] = value
def generate_random_birthdate():
import datetime as dt
today = dt.date.today()
age = random.randint(20, 40)
birth_year = today.year - age
birth_month = random.randint(1, 12)
birth_day = random.randint(1, 28)
return f"{birth_year}-{birth_month:02d}-{birth_day:02d}T16:00:00.000Z"
def response_preview(res, limit=200):
try:
text = str(res.text or "")
except Exception:
text = ""
text = re.sub(r"\s+", " ", text).strip()
return text[:limit]
def is_cloudflare_block_response(res):
try:
headers = {str(k).lower(): str(v).lower() for k, v in dict(res.headers).items()}
text = str(res.text or "").lower()
server = headers.get("server", "")
content_type = headers.get("content-type", "")
return (
res.status_code in (403, 429, 503)
and (
"cloudflare" in server
or "cloudflare" in text
or "cf-error" in text
or "__cf_chl" in text
or "text/html" in content_type
)
)
except Exception:
return False
def set_birth_date(session, log_callback=None):
url = "https://grok.com/rest/auth/set-birth-date"
new_headers = {
"content-type": "application/json",
"origin": "https://grok.com",
"referer": "https://grok.com/",
}
payload = {"birthDate": generate_random_birthdate()}
try:
res = session.post(url, json=payload, headers=new_headers, timeout=15)
if log_callback:
log_callback(
f"[Debug] set_birth_date status: {res.status_code}, body: {response_preview(res)}"
)
if 200 <= res.status_code < 300:
return True, "ok"
if is_cloudflare_block_response(res):
return (
False,
"set_birth_date 被 grok.com 的 Cloudflare 防护拦截,HTTP "
f"{res.status_code}",
)
return False, f"set_birth_date HTTP {res.status_code}: {response_preview(res)}"
except Exception as e:
if log_callback:
log_callback(f"[set_birth_date] 异常: {e}")
return False, f"set_birth_date 异常: {e}"
def set_tos_accepted(session, log_callback=None):
url = "https://accounts.x.ai/auth_mgmt.AuthManagement/SetTosAcceptedVersion"
payload = struct.pack("B", (2 << 3) | 0) + struct.pack("B", 1)
data = b"\x00" + struct.pack(">I", len(payload)) + payload
new_headers = {
"content-type": "application/grpc-web+proto",
"x-grpc-web": "1",
"x-user-agent": "connect-es/2.1.1",
"origin": "https://accounts.x.ai",
"referer": "https://accounts.x.ai/accept-tos",
}
try:
res = session.post(url, data=data, headers=new_headers, timeout=15)
if log_callback:
log_callback(f"[Debug] set_tos_accepted status: {res.status_code}")
if 200 <= res.status_code < 300:
return True, "ok"
if is_cloudflare_block_response(res):
return (
False,
"set_tos_accepted 被 accounts.x.ai 的 Cloudflare 防护拦截,HTTP "
f"{res.status_code}",
)
return False, f"set_tos_accepted HTTP {res.status_code}: {response_preview(res)}"
except Exception as e:
if log_callback:
log_callback(f"[set_tos_accepted] 异常: {e}")
return False, f"set_tos_accepted 异常: {e}"
def encode_grpc_nsfw_settings():
field1_content = bytes([0x10, 0x01])
field1 = bytes([0x0A, len(field1_content)]) + field1_content
nsfw_string = b"always_show_nsfw_content"
field2_inner = bytes([0x0A, len(nsfw_string)]) + nsfw_string
field2 = bytes([0x12, len(field2_inner)]) + field2_inner
payload = field1 + field2
return b"\x00" + struct.pack(">I", len(payload)) + payload
def update_nsfw_settings(session, log_callback=None):
url = "https://grok.com/auth_mgmt.AuthManagement/UpdateUserFeatureControls"
data = encode_grpc_nsfw_settings()
new_headers = {
"content-type": "application/grpc-web+proto",
"x-grpc-web": "1",
"origin": "https://grok.com",
"referer": "https://grok.com/",
}
try:
res = session.post(url, data=data, headers=new_headers, timeout=15)
if log_callback:
log_callback(
f"[Debug] update_nsfw status: {res.status_code}, body: {response_preview(res)}"
)
if 200 <= res.status_code < 300:
return True, "ok"
if is_cloudflare_block_response(res):
return (
False,
"update_nsfw_settings 被 grok.com 的 Cloudflare 防护拦截,HTTP "
f"{res.status_code}",
)
return False, f"update_nsfw_settings HTTP {res.status_code}: {response_preview(res)}"
except Exception as e:
if log_callback:
log_callback(f"[update_nsfw] 异常: {e}")
return False, f"update_nsfw_settings 异常: {e}"
def enable_nsfw_for_token(token, cf_clearance="", log_callback=None):
proxies = get_proxies()
user_agent = get_user_agent()
try:
with requests.Session(impersonate="chrome120", proxies=proxies) as session:
cookie_parts = [f"sso={token}", f"sso-rw={token}"]
if cf_clearance:
cookie_parts.append(f"cf_clearance={cf_clearance}")
session.headers.update(
{
"user-agent": user_agent,
"cookie": "; ".join(cookie_parts),
}
)
ok, message = set_tos_accepted(session, log_callback)
if not ok:
return False, message
ok, message = set_birth_date(session, log_callback)
if not ok:
return False, message
ok, message = update_nsfw_settings(session, log_callback)
if not ok:
return False, message
return True, "成功开启 NSFW"
except Exception as e:
return False, f"异常: {str(e)}"
def stop_browser_proxy_bridge():
global browser_proxy_bridge
if browser_proxy_bridge is not None:
try:
browser_proxy_bridge.stop()
except Exception:
pass
browser_proxy_bridge = None
def start_browser(log_callback=None, use_proxy=True):
global browser, page, browser_proxy_bridge, browser_started_with_proxy
last_exc = None
proxy_enabled = bool(use_proxy and get_configured_proxy())
for attempt in range(1, 5):
bridge = None
try:
browser_proxy, bridge = prepare_browser_proxy(use_proxy=use_proxy, log_callback=log_callback)
browser = Chromium(create_browser_options(browser_proxy=browser_proxy))
browser_proxy_bridge = bridge
browser_started_with_proxy = bool(browser_proxy)
tabs = browser.get_tabs()
page = tabs[-1] if tabs else browser.new_tab()
if log_callback and getattr(browser, "user_data_path", None):
log_callback(f"[Debug] 当前浏览器资料目录: {browser.user_data_path}")
if log_callback and get_configured_proxy():
mode = "代理" if browser_started_with_proxy else "直连"
log_callback(f"[*] 浏览器网络模式: {mode}")
if log_callback and attempt > 1:
log_callback(f"[*] 浏览器第 {attempt} 次启动成功")
return browser, page
except Exception as exc:
last_exc = exc
if bridge is not None:
try:
bridge.stop()
except Exception:
pass
if log_callback:
mode = "代理" if proxy_enabled else "直连"
log_callback(f"[Debug] 浏览器{mode}启动失败(第{attempt}/4次): {exc}")
try:
if browser is not None:
browser.quit(del_data=True)
except Exception:
pass
browser = None
page = None
browser_proxy_bridge = None
browser_started_with_proxy = False
time.sleep(min(1.5 * attempt, 4))
raise Exception(f"浏览器启动失败,已重试4次: {last_exc}")
def stop_browser():
global browser, page, browser_started_with_proxy
if browser is not None:
try:
browser.quit(del_data=True)
except Exception:
pass
stop_browser_proxy_bridge()
browser = None
page = None
browser_started_with_proxy = False
def restart_browser(log_callback=None, use_proxy=True):
stop_browser()
return start_browser(log_callback=log_callback, use_proxy=use_proxy)
def cleanup_runtime_memory(log_callback=None, reason="定期清理"):
if log_callback:
log_callback(f"[*] {reason}: 关闭浏览器并清理内存")
stop_browser()
try:
from cpa_xai.browser_confirm import shutdown_mint_browsers
shutdown_mint_browsers()
except Exception as exc:
if log_callback:
log_callback(f"[Debug] CPA 浏览器清理失败: {exc}")
collected = gc.collect()
if log_callback:
log_callback(f"[*] Python GC 已回收对象数: {collected}")
def refresh_active_page():
global browser, page
if browser is None:
restart_browser()
try:
tabs = browser.get_tabs()
if tabs:
page = tabs[-1]
else:
page = browser.new_tab()
except Exception:
restart_browser()
return page
def click_email_signup_button(timeout=10, log_callback=None, cancel_callback=None):
global page
deadline = time.time() + timeout
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
if log_callback:
log_callback("[Debug] 尝试查找“使用邮箱注册”按钮...")
clicked = page.run_js(r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function nodeText(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('aria-label'),
node.getAttribute('title'),
node.getAttribute('href'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
function scoreEntry(node) {
const compact = nodeText(node).replace(/\s+/g, '');
const lower = compact.toLowerCase();
if (compact.includes('使用邮箱注册')) return 100;
if (lower.includes('signupwithemail')) return 95;
if (lower.includes('continuewithemail')) return 90;
if (lower.includes('email') && (lower.includes('sign') || lower.includes('continue') || lower.includes('use') || lower.includes('with'))) return 80;
if (lower === 'email' || lower.includes('邮箱')) return 70;
return 0;
}
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]'))
.filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true')
.map((node) => ({ node, score: scoreEntry(node), text: nodeText(node) }))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score);
const target = candidates[0]?.node || null;
if (!target) {
return false;
}
target.click();
return candidates[0].text || true;
""")
if clicked:
if log_callback:
detail = f": {clicked}" if isinstance(clicked, str) else ""
log_callback(f"[*] 已点击「使用邮箱注册」按钮{detail}")
sleep_with_cancel(2, cancel_callback)
return True
if log_callback:
current_url = page.url if page else "none"
log_callback(f"[Debug] 当前URL: {current_url}")
sleep_with_cancel(1, cancel_callback)
if log_callback:
page_html = page.html[:500] if page else "no page"
log_callback(f"[Debug] 页面内容片段: {page_html}")
raise Exception("未找到「使用邮箱注册」按钮")
def open_signup_page(log_callback=None, cancel_callback=None):
global browser, page
raise_if_cancelled(cancel_callback)
if browser is None:
start_browser(log_callback=log_callback)
if log_callback:
log_callback("[*] 浏览器已启动")
def _open_with_current_browser():
global page
try:
page = browser.get_tab(0)
page.get(SIGNUP_URL)
except Exception as e:
if log_callback:
log_callback(f"[Debug] 打开URL异常: {e}")
page = browser.new_tab(SIGNUP_URL)
page.wait.doc_loaded()
try:
_open_with_current_browser()
except Exception as e:
if browser_started_with_proxy and get_configured_proxy():
if log_callback:
log_callback(f"[!] 浏览器代理访问注册页失败,自动回退直连: {e}")
restart_browser(log_callback=log_callback, use_proxy=False)
_open_with_current_browser()
else:
raise
if browser_started_with_proxy and page_has_proxy_error(page):
if log_callback:
log_callback("[!] 浏览器页面显示代理错误,自动回退直连")
restart_browser(log_callback=log_callback, use_proxy=False)
_open_with_current_browser()
sleep_with_cancel(2, cancel_callback)
if log_callback:
log_callback(f"[*] 当前URL: {page.url}")
click_email_signup_button(
log_callback=log_callback, cancel_callback=cancel_callback
)
def has_profile_form(log_callback=None):
refresh_active_page()
try:
return bool(
page.run_js(
"""
const givenInput = document.querySelector('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"]');
const familyInput = document.querySelector('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"]');
const passwordInput = document.querySelector('input[data-testid="password"], input[name="password"], input[type="password"]');
return !!(givenInput && familyInput && passwordInput);
"""
)
)
except Exception:
return False
def fill_email_and_submit(timeout=45, log_callback=None, cancel_callback=None):
raise_if_cancelled(cancel_callback)
email, dev_token = get_email_and_token()
if not email or not dev_token:
raise Exception("获取邮箱失败")
if log_callback:
log_callback(f"[*] 已创建邮箱: {email}")
deadline = time.time() + timeout
last_diag_time = 0
last_reclick_time = 0
last_snapshot = None
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
filled = page.run_js(
"""
const email = arguments[0];
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function textOf(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('aria-label'),
node.getAttribute('title'),
node.getAttribute('placeholder'),
node.getAttribute('data-testid'),
node.getAttribute('name'),
node.getAttribute('id'),
node.getAttribute('autocomplete'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
function describeInput(node) {
return [
`type=${node.getAttribute('type') || ''}`,
`name=${node.getAttribute('name') || ''}`,
`id=${node.getAttribute('id') || ''}`,
`placeholder=${node.getAttribute('placeholder') || ''}`,
`aria=${node.getAttribute('aria-label') || ''}`,
`testid=${node.getAttribute('data-testid') || ''}`,
].join(' ').replace(/\s+/g, ' ').trim().slice(0, 160);
}
function describeAction(node) {
return textOf(node).slice(0, 120);
}
function emailCandidates() {
const direct = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"], input[placeholder*="mail" i], input[aria-label*="mail" i]'));
const all = Array.from(document.querySelectorAll('input, textarea'));
for (const node of all) {
const type = (node.getAttribute('type') || '').toLowerCase();
if (['hidden', 'submit', 'button', 'checkbox', 'radio', 'file', 'search'].includes(type)) continue;
const meta = textOf(node).toLowerCase();
if (meta.includes('email') || meta.includes('e-mail') || meta.includes('mail') || meta.includes('邮箱') || meta.includes('电子邮件')) {
direct.push(node);
}
}
return Array.from(new Set(direct));
}
const visibleInputs = Array.from(document.querySelectorAll('input, textarea'))
.filter((node) => isVisible(node) && !node.disabled && !node.readOnly)
.map(describeInput)
.slice(0, 8);
const visibleActions = Array.from(document.querySelectorAll('button, a, [role="button"]'))
.filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true')
.map(describeAction)
.filter(Boolean)
.slice(0, 10);
const input = emailCandidates().find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null;
if (!input) {
return {
state: 'not-ready',
url: location.href,
title: document.title,
inputs: visibleInputs,
buttons: visibleActions,
};
}
input.focus(); input.click();
const valueProto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const valueSetter = Object.getOwnPropertyDescriptor(valueProto, 'value')?.set;
const tracker = input._valueTracker;
if (tracker) tracker.setValue('');
if (valueSetter) valueSetter.call(input, email); else input.value = email;
input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: email, inputType: 'insertText' }));
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: email, inputType: 'insertText' }));
input.dispatchEvent(new Event('change', { bubbles: true }));
const inputType = (input.getAttribute('type') || '').toLowerCase();
const isValid = inputType !== 'email' || input.checkValidity();
if ((input.value || '').trim() !== email || !isValid) {
return {
state: 'fill-failed',
value: input.value || '',
valid: isValid,
input: describeInput(input),
url: location.href,
};
}
input.blur();
return {
state: 'filled',
input: describeInput(input),
url: location.href,
};
""",
email,
)
state = filled.get("state") if isinstance(filled, dict) else filled
if isinstance(filled, dict):
last_snapshot = filled
if state == "not-ready":
now = time.time()
if now - last_reclick_time >= 3:
reclicked = page.run_js(r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function nodeText(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('aria-label'),
node.getAttribute('title'),
node.getAttribute('href'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
function scoreEntry(node) {
const compact = nodeText(node).replace(/\s+/g, '');
const lower = compact.toLowerCase();
if (compact.includes('使用邮箱注册')) return 100;
if (lower.includes('signupwithemail')) return 95;
if (lower.includes('continuewithemail')) return 90;
if (lower.includes('email') && (lower.includes('sign') || lower.includes('continue') || lower.includes('use') || lower.includes('with'))) return 80;
if (lower === 'email' || lower.includes('邮箱')) return 70;
return 0;
}
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]'))
.filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true')
.map((node) => ({ node, score: scoreEntry(node), text: nodeText(node) }))
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score);
if (!candidates.length) return false;
candidates[0].node.click();
return candidates[0].text || true;
""")
last_reclick_time = now
if reclicked and log_callback:
detail = f": {reclicked}" if isinstance(reclicked, str) else ""
log_callback(f"[Debug] 邮箱输入框未出现,已再次触发邮箱注册入口{detail}")
if log_callback and now - last_diag_time >= 5:
last_diag_time = now
inputs = " | ".join((filled or {}).get("inputs", [])[:6]) if isinstance(filled, dict) else ""
buttons = " | ".join((filled or {}).get("buttons", [])[:8]) if isinstance(filled, dict) else ""
url = (filled or {}).get("url", page.url if page else "") if isinstance(filled, dict) else (page.url if page else "")
log_callback(f"[Debug] 等待邮箱输入框: url={url}; inputs={inputs or 'none'}; buttons={buttons or 'none'}")
sleep_with_cancel(0.5, cancel_callback)
continue
if state != "filled":
if log_callback:
log_callback(f"[Debug] 邮箱输入框已出现,但写入失败: {filled}")
sleep_with_cancel(0.5, cancel_callback)
continue
sleep_with_cancel(0.8, cancel_callback)
clicked = page.run_js(
r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function textOf(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('aria-label'),
node.getAttribute('title'),
node.getAttribute('placeholder'),
node.getAttribute('data-testid'),
node.getAttribute('name'),
node.getAttribute('id'),
node.getAttribute('autocomplete'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
function emailCandidates() {
const direct = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"], input[placeholder*="mail" i], input[aria-label*="mail" i]'));
const all = Array.from(document.querySelectorAll('input, textarea'));
for (const node of all) {
const type = (node.getAttribute('type') || '').toLowerCase();
if (['hidden', 'submit', 'button', 'checkbox', 'radio', 'file', 'search'].includes(type)) continue;
const meta = textOf(node).toLowerCase();
if (meta.includes('email') || meta.includes('e-mail') || meta.includes('mail') || meta.includes('邮箱') || meta.includes('电子邮件')) {
direct.push(node);
}
}
return Array.from(new Set(direct));
}
const input = emailCandidates().find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null;
if (!input || !(input.value || '').trim()) return false;
const inputType = (input.getAttribute('type') || '').toLowerCase();
if (inputType === 'email' && !input.checkValidity()) return false;
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]'))
.filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true');
const submitButton = buttons.find((node) => {
const text = textOf(node).replace(/\s+/g, '');
const lower = text.toLowerCase();
return (
text === '注册' ||
text.includes('注册') ||
text.includes('继续') ||
text.includes('下一步') ||
text.includes('确认') ||
lower.includes('signup') ||
lower.includes('sign up') ||
lower.includes('continue') ||
lower.includes('next') ||
lower.includes('createaccount') ||
lower.includes('submit')
);
});
if (submitButton) {
submitButton.click();
return textOf(submitButton) || true;
}
const form = input.closest('form');
if (form) {
if (form.requestSubmit) form.requestSubmit();
else form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
return 'form-submit';
}
input.focus();
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
return 'enter';
"""
)
if clicked:
if log_callback:
detail = f" ({clicked})" if isinstance(clicked, str) else ""
log_callback(f"[*] 已填写邮箱并提交: {email}{detail}")
return email, dev_token
sleep_with_cancel(0.5, cancel_callback)
if last_snapshot:
inputs = " | ".join(last_snapshot.get("inputs", [])[:6])
buttons = " | ".join(last_snapshot.get("buttons", [])[:8])
url = last_snapshot.get("url", page.url if page else "")
raise Exception(
f"未找到邮箱输入框或注册按钮,最后页面: url={url}; inputs={inputs or 'none'}; buttons={buttons or 'none'}"
)
raise Exception("未找到邮箱输入框或注册按钮")
def fill_code_and_submit(email, dev_token, timeout=180, log_callback=None, cancel_callback=None):
def _resend_code():
page.run_js(
r"""
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"]'));
const target = nodes.find((node) => {
const t = (node.innerText || node.textContent || '').replace(/\s+/g, '').toLowerCase();
return t.includes('重新发送') || t.includes('resend') || t.includes('再次发送');
});
if (target && !target.disabled) { target.click(); return true; }
return false;
"""
)
code = get_oai_code(
dev_token,
email,
log_callback=log_callback,
cancel_callback=cancel_callback,
resend_callback=_resend_code,
)
if not code:
raise Exception("获取验证码失败")
clean_code = str(code).replace("-", "").strip()
deadline = time.time() + timeout
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
filled = page.run_js(
"""
const code = String(arguments[0] || '').trim();
if (!code) return 'empty-code';
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function setInputValue(input, value) {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
const tracker = input._valueTracker;
if (tracker) tracker.setValue('');
if (nativeSetter) nativeSetter.call(input, value);
else input.value = value;
input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' }));
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
const aggregate = Array.from(document.querySelectorAll(
'input[data-input-otp=\"true\"], input[name=\"code\"], input[autocomplete=\"one-time-code\"], input[inputmode=\"numeric\"], input[inputmode=\"text\"]'
)).find((node) => isVisible(node) && !node.disabled && !node.readOnly && Number(node.maxLength || 6) > 1);
if (aggregate) {
aggregate.focus();
aggregate.click();
setInputValue(aggregate, code);
return String(aggregate.value || '').replace(/\\s+/g, '') ? 'filled-aggregate' : 'aggregate-failed';
}
const otpBoxes = Array.from(document.querySelectorAll('input')).filter((node) => {
if (!isVisible(node) || node.disabled || node.readOnly) return false;
const maxLength = Number(node.maxLength || 0);
const ac = String(node.autocomplete || '').toLowerCase();
return maxLength === 1 || ac === 'one-time-code';
});
if (otpBoxes.length >= code.length) {
for (let i = 0; i < code.length; i += 1) {
const ch = code[i] || '';
const box = otpBoxes[i];
box.focus();
box.click();
setInputValue(box, ch);
box.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: ch }));
box.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: ch }));
}
const merged = otpBoxes.slice(0, code.length).map((x) => String(x.value || '').trim()).join('');
return merged.length ? 'filled-boxes' : 'boxes-failed';
}
return 'not-ready';
""",
clean_code,
)
if filled == "not-ready":
sleep_with_cancel(0.5, cancel_callback)
continue
if "failed" in str(filled):
if log_callback:
log_callback(f"[Debug] 验证码填写失败: {filled}")
sleep_with_cancel(0.5, cancel_callback)
continue
clicked = page.run_js(
r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
const buttons = Array.from(document.querySelectorAll('button[type=\"submit\"], button')).filter((node) => {
return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true';
});
const btn = buttons.find((node) => {
const t = (node.innerText || node.textContent || '').replace(/\\s+/g, '').toLowerCase();
return (
t.includes('确认邮箱') ||
t.includes('继续') ||
t.includes('下一步') ||
t.includes('confirm') ||
t.includes('continue') ||
t.includes('next')
);
});
if (!btn) return 'no-button';
btn.focus();
btn.click();
return 'clicked';
"""
)
if clicked == "clicked" or clicked == "no-button":
if log_callback:
log_callback(f"[*] 已填写验证码并提交: {code}")
sleep_with_cancel(1.5, cancel_callback)
return code
sleep_with_cancel(0.5, cancel_callback)
raise Exception("验证码已获取,但自动填写/提交失败")
def getTurnstileToken(log_callback=None, cancel_callback=None):
global page
if page is None:
raise Exception("页面未就绪,无法执行 Turnstile")
try:
page.run_js(
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
)
except Exception:
pass
for _ in range(0, 20):
raise_if_cancelled(cancel_callback)
try:
token = page.run_js(
"""
try {
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
if (byInput) return byInput;
if (window.turnstile && typeof turnstile.getResponse === 'function') {
return String(turnstile.getResponse() || '').trim();
}
return '';
} catch(e) { return ''; }
"""
)
token = str(token or "").strip()
if len(token) >= 80:
if log_callback:
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token
challenge_input = page.ele("@name=cf-turnstile-response")
if challenge_input:
wrapper = challenge_input.parent()
iframe = None
try:
iframe = wrapper.shadow_root.ele("tag:iframe")
except Exception:
iframe = None
if iframe:
try:
iframe.run_js(
"""
window.dtp = 1;
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
let sx = getRandomInt(800, 1200);
let sy = getRandomInt(400, 700);
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx });
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
"""
)
except Exception:
pass
try:
body_sr = iframe.ele("tag:body").shadow_root
btn = body_sr.ele("tag:input")
if btn:
btn.click()
except Exception:
pass
else:
# 兜底:尝试触发页面上可见的 Turnstile 容器
page.run_js(
"""
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
return String(txt).toLowerCase().includes('turnstile');
});
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
"""
)
except Exception:
pass
sleep_with_cancel(1, cancel_callback)
raise Exception("Turnstile 获取 token 失败")
def build_profile():
given_name_pool = [
"Neo", "Ethan", "Liam", "Noah", "Lucas", "Mason", "Ryan", "Leo",
"Owen", "Aiden", "Elio", "Aron", "Ivan", "Nolan", "Evan", "Kai",
"Caleb", "Adam", "Ezra", "Miles", "Logan", "Carter", "Hunter", "Jason",
"Brian", "Dylan", "Alex", "Colin", "Blake", "Gavin", "Henry", "Julian",
"Kevin", "Louis", "Marcus", "Nathan", "Oscar", "Peter", "Quinn", "Robin",
"Simon", "Tristan", "Victor", "Wesley", "Xavier", "Yuri", "Zane", "Felix",
"Aaron", "Damian",
]
family_name_pool = [
"Lin", "Wang", "Zhao", "Liu", "Chen", "Zhang", "Xu", "Sun",
"Guo", "He", "Yang", "Wu", "Zhou", "Tang", "Qin", "Shi",
"Fang", "Peng", "Cao", "Deng", "Fan", "Fu", "Gao", "Han",
"Hu", "Jiang", "Kong", "Lu", "Ma", "Nie", "Pan", "Qiao",
"Ren", "Shao", "Tian", "Xie", "Yan", "Yao", "Yu", "Zeng",
"Bai", "Duan", "Hou", "Jin", "Kang", "Luo", "Mao", "Song",
"Wei", "Xiong",
]
given_name = random.choice(given_name_pool)
family_name = random.choice(family_name_pool)
password = "N" + secrets.token_hex(4) + "!a7#" + secrets.token_urlsafe(6)
return given_name, family_name, password
def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None):
given_name, family_name, password = build_profile()
deadline = time.time() + timeout
form_filled_once = False
wait_cf_since = None
last_cf_retry_at = 0.0
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
if not form_filled_once:
filled = page.run_js(
"""
const givenName = arguments[0];
const familyName = arguments[1];
const password = arguments[2];
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function pickInput(selector) {
return Array.from(document.querySelectorAll(selector)).find((node) => {
return isVisible(node) && !node.disabled && !node.readOnly;
}) || null;
}
function setInputValue(input, value) {
if (!input) return false;
input.focus();
input.click();
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
const tracker = input._valueTracker;
if (tracker) tracker.setValue('');
if (nativeSetter) nativeSetter.call(input, value);
else input.value = value;
input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' }));
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.blur();
return String(input.value || '').trim() === String(value || '').trim();
}
const givenInput = pickInput('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"], input[aria-label*="名"]');
const familyInput = pickInput('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"], input[aria-label*="姓"]');
const passwordInput = pickInput('input[data-testid="password"], input[name="password"], input[type="password"], input[autocomplete="new-password"]');
if (!givenInput || !familyInput || !passwordInput) return 'not-ready';
const ok1 = setInputValue(givenInput, givenName);
const ok2 = setInputValue(familyInput, familyName);
const ok3 = setInputValue(passwordInput, password);
if (!ok1 || !ok2 || !ok3) return 'fill-failed';
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]')).filter((node) => {
return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true';
});
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');
});
// 必须等待 Cloudflare 校验通过后再提交
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
const cfPresent = !!cfInput
|| !!document.querySelector('iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey], script[src*="turnstile"]');
if (cfPresent) {
const token = String((cfInput && cfInput.value) || '').trim();
const solvedByToken = token.length >= 80;
if (!solvedByToken) return 'wait-cloudflare:' + token.length;
}
if (submitBtn) {
return 'ready-to-submit';
}
return 'filled-no-submit';
""",
given_name,
family_name,
password,
)
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"[*] 资料已填写,等待 Cloudflare 人机验证通过... 当前token长度={token_len}")
if token_len == "0":
pause_seconds = random.uniform(1, 3)
if log_callback:
log_callback(f"[*] 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
# 卡住后自动二次复用 Turnstile 组件
if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8:
if log_callback:
log_callback("[*] 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,
)
if log_callback:
log_callback(f"[*] 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 filled in ("ready-to-submit", "filled-no-submit"):
form_filled_once = True
elif filled == "fill-failed" and log_callback:
log_callback("[Debug] 资料输入失败,重试中...")
sleep_with_cancel(0.5, cancel_callback)
continue
elif filled == "not-ready":
sleep_with_cancel(0.5, cancel_callback)
continue
submit_state = page.run_js(
r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
const cfPresent = !!cfInput
|| !!document.querySelector('iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey], script[src*="turnstile"]');
if (cfPresent) {
const token = String((cfInput && cfInput.value) || '').trim();
const solvedByToken = token.length >= 80;
if (!solvedByToken) return 'wait-cloudflare:' + token.length;
}
function buttonText(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('value'),
node.getAttribute('aria-label'),
node.getAttribute('title'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]')).filter((node) => {
return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true';
});
const submitBtn = buttons.find((node) => {
const t = buttonText(node).replace(/\s+/g, '').toLowerCase();
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount');
});
if (!submitBtn) {
const visibleTexts = buttons.map(buttonText).filter(Boolean).slice(0, 8).join(' | ');
return 'no-submit-button:' + visibleTexts;
}
submitBtn.focus();
submitBtn.click();
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"[*] 等待 Cloudflare 人机验证通过后再提交... 当前token长度={token_len}")
now = time.time()
if wait_cf_since is None:
wait_cf_since = now
if now - wait_cf_since >= 12 and now - last_cf_retry_at >= 8:
if log_callback:
log_callback("[*] 提交前仍卡住,自动再次复用 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,
)
if log_callback:
log_callback(f"[*] 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}")
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:
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}")
sleep_with_cancel(0.5, cancel_callback)
raise Exception("最终注册页资料填写失败")
def wait_for_sso_cookie(timeout=120, log_callback=None, cancel_callback=None):
deadline = time.time() + timeout
last_seen_names = set()
last_submit_retry = 0.0
last_cf_retry_at = 0.0
final_no_submit_state = ""
final_no_submit_since = None
final_no_submit_timeout = 25
last_wait_exception_message = ""
last_wait_exception_at = 0.0
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
try:
refresh_active_page()
if page is None:
sleep_with_cancel(1, cancel_callback)
continue
# 仍停留在“完成注册”页时,若 Cloudflare 已通过,周期性重试点击提交
now = time.time()
if now - last_submit_retry >= 2.5:
retried = page.run_js(
r"""
function isVisible(node) {
if (!node) return false;
const style = window.getComputedStyle(node);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
const titleHit = !!Array.from(document.querySelectorAll('h1,h2,div,span')).find((el) => {
const t = (el.textContent || '').replace(/\s+/g, '');
const lower = t.toLowerCase();
return t.includes('完成注册') || lower.includes('completeyoursignup') || lower.includes('completesignup');
});
if (!titleHit) return 'not-final-page';
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
const cfPresent = !!cfInput
|| !!document.querySelector('iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey], script[src*="turnstile"]');
if (cfPresent) {
const token = String((cfInput && cfInput.value) || '').trim();
const solved = token.length >= 80;
if (!solved) return 'final-page-wait-cf:' + token.length;
}
function buttonText(node) {
return [
node.innerText,
node.textContent,
node.getAttribute('value'),
node.getAttribute('aria-label'),
node.getAttribute('title'),
].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
}
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button, [role="button"], input[type="submit"]')).filter((node) => {
return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true';
});
const submitBtn = buttons.find((node) => {
const t = buttonText(node).replace(/\s+/g, '').toLowerCase();
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('createaccount');
});
if (!submitBtn) {
const visibleTexts = buttons.map(buttonText).filter(Boolean).slice(0, 8).join(' | ');
return 'final-page-no-submit:' + visibleTexts;
}
submitBtn.focus();
submitBtn.click();
return 'final-page-clicked-submit';
"""
)
last_submit_retry = now
if log_callback and (retried == "final-page-clicked-submit" or (isinstance(retried, str) and retried.startswith("final-page-no-submit"))):
log_callback(f"[Debug] 最终页状态: {retried}")
if isinstance(retried, str) and retried.startswith("final-page-no-submit"):
if retried != final_no_submit_state:
final_no_submit_state = retried
final_no_submit_since = now
elif final_no_submit_since and now - final_no_submit_since >= final_no_submit_timeout:
raise AccountRetryNeeded(
f"最终注册页状态 {final_no_submit_timeout}s 未变化且未找到提交按钮,重试当前账号: {retried}"
)
else:
final_no_submit_state = ""
final_no_submit_since = None
if log_callback and isinstance(retried, str) and retried.startswith("final-page-wait-cf"):
token_len = retried.split(":", 1)[1] if ":" in retried else "0"
log_callback(f"[Debug] 最终页状态: final-page-wait-cf, token长度={token_len}")
if now - last_cf_retry_at >= 10:
if log_callback:
log_callback("[*] 最终页 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,
)
if log_callback:
log_callback(f"[*] 最终页 Turnstile 二次复用完成,回填长度={synced}")
except Exception as cf_exc:
if log_callback:
log_callback(f"[Debug] 最终页 Turnstile 二次复用失败: {cf_exc}")
last_cf_retry_at = now
cookies = page.cookies(all_domains=True, all_info=True) or []
for item in cookies:
if isinstance(item, dict):
name = str(item.get("name", "")).strip()
value = str(item.get("value", "")).strip()
else:
name = str(getattr(item, "name", "")).strip()
value = str(getattr(item, "value", "")).strip()
if name:
last_seen_names.add(name)
if name == "sso" and value:
if log_callback:
log_callback("[*] 已获取到 sso cookie")
return value
except PageDisconnectedError:
refresh_active_page()
except AccountRetryNeeded:
raise
except Exception as exc:
if log_callback:
now = time.time()
message = f"{exc.__class__.__name__}: {exc}"
if message != last_wait_exception_message or now - last_wait_exception_at >= 10:
log_callback(f"[Debug] 等待 sso cookie 时出现异常,将继续等待: {message}")
last_wait_exception_message = message
last_wait_exception_at = now
sleep_with_cancel(1, cancel_callback)
raise Exception(
f"等待超时:未获取到 sso cookie。已看到 cookies: {sorted(last_seen_names)}"
)