"""管理主注册浏览器生命周期并实现注册页面自动化操作。""" 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)}" )