commit 2302e7420966316eebdd68e235519cae1e897a68 Author: chaos Date: Tue Jun 16 07:12:51 2026 +0800 feat: xAI Grok auto-registration tool - DrissionPage + Chrome browser automation - Temporary email via mail.tm API - Verification code auto-extraction - Turnstile CAPTCHA bypass via extension - SSO cookie extraction and persistence diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..925cc0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.venv/ +sso.txt +*.pyc \ No newline at end of file diff --git a/01.py b/01.py new file mode 100644 index 0000000..54d800d --- /dev/null +++ b/01.py @@ -0,0 +1,568 @@ +from pprint import pprint +from DrissionPage import Chromium, ChromiumOptions +from DrissionPage.items import ChromiumTab, MixTab +import time +import json +import re +import threading + +PAYMENT_INFO = { + 'guid': None, + 'muid': None, + 'sid': None, + 'hcaptcha_response': None, + 'portal_session_id': None, + 'session_api_key': None, + 'payment_user_agent': None, + 'stripeJsId': None, + 'api_key': None, + 'setup_intent_id': None, + 'setup_intent_client_secret': None, + 'acct_id': None, + 'hcaptcha_duration': None, + 'payment_method_id': None, + 'setup_intent_status': None, + 'page_load_time': 0 +} + +HCAPTCHA_INFO = { + 'response': None, + 'duration': None, + 'updated_at': 0 +} + +CAPTURED_MESSAGES = [] +CAPTURED_MESSAGE_LOCK = threading.Lock() + +DATA_EVENTS = { + 'hcaptcha_updated': threading.Event(), + 'guid_updated': threading.Event(), + 'muid_updated': threading.Event(), + 'sid_updated': threading.Event(), + 'portal_session_updated': threading.Event(), + 'session_api_key_updated': threading.Event(), + 'acct_id_updated': threading.Event(), + 'api_key_updated': threading.Event(), + 'stripe_js_id_updated': threading.Event(), + 'payment_user_agent_updated': threading.Event(), + 'setup_intent_updated': threading.Event(), + 'payment_method_updated': threading.Event() +} + + +BACKGROUND_THREAD_RUNNING = True + +def init_browser(): + co = ChromiumOptions() + co.incognito() + c = Chromium(co) + c.set.timeouts(9000) + return c, c.latest_tab + +def background_data_collector(tab): + global PAYMENT_INFO, HCAPTCHA_INFO, CAPTURED_MESSAGES, DATA_EVENTS, BACKGROUND_THREAD_RUNNING + + + while BACKGROUND_THREAD_RUNNING: + try: + + for log in tab.console.steps(timeout=1): + if log.text.startswith('捕获postMessage:'): + message_content = log.text.replace('捕获postMessage:', '', 1).strip() + try: + message_data = json.loads(message_content) + + with CAPTURED_MESSAGE_LOCK: + CAPTURED_MESSAGES.append(message_data) + + if (isinstance(message_data, dict) and message_data.get('type') == 'execute' + and message_data.get('channel') == 'hcaptcha-invisible'): + body = message_data.get('body', {}) + if isinstance(body, dict) and 'response' in body: + HCAPTCHA_INFO['response'] = body['response'] + if 'duration' in body: + HCAPTCHA_INFO['duration'] = body['duration'] + HCAPTCHA_INFO['updated_at'] = time.time() + print(f"[后台线程] 获取到新的hCaptcha响应: {HCAPTCHA_INFO['response'][:30]}... 持续时间: {HCAPTCHA_INFO['duration']}") + + PAYMENT_INFO['hcaptcha_response'] = HCAPTCHA_INFO['response'] + PAYMENT_INFO['hcaptcha_duration'] = HCAPTCHA_INFO['duration'] + + DATA_EVENTS['hcaptcha_updated'].set() + + if isinstance(message_data, dict) and 'originatingScript' in message_data and 'payload' in message_data: + payload = message_data.get('payload', {}) + if isinstance(payload, dict): + if 'guid' in payload and not PAYMENT_INFO['guid']: + PAYMENT_INFO['guid'] = payload['guid'] + print(f"[后台线程] 提取到GUID: {payload['guid']}") + DATA_EVENTS['guid_updated'].set() + + if 'muid' in payload and not PAYMENT_INFO['muid']: + PAYMENT_INFO['muid'] = payload['muid'] + print(f"[后台线程] 提取到MUID: {payload['muid']}") + DATA_EVENTS['muid_updated'].set() + + if 'sid' in payload and not PAYMENT_INFO['sid']: + PAYMENT_INFO['sid'] = payload['sid'] + print(f"[后台线程] 提取到SID: {payload['sid']}") + DATA_EVENTS['sid_updated'].set() + except Exception as e: + pass + + time.sleep(0.1) + except Exception as e: + print(f"[后台线程] 数据收集出错: {e}") + time.sleep(1) + +def wait_for_hcaptcha(timeout=60): + print(f"等待hCaptcha响应,最多等待{timeout}秒...") + return DATA_EVENTS['hcaptcha_updated'].wait(timeout) + +def wait_for_payment_info(fields, timeout=60): + if isinstance(fields, str): + fields = [fields] + + print(f"等待支付信息字段: {', '.join(fields)},最多等待{timeout}秒...") + + events_to_wait = [] + for field in fields: + event_name = f"{field}_updated" + if event_name in DATA_EVENTS: + events_to_wait.append(DATA_EVENTS[event_name]) + + if not events_to_wait: + print(f"警告: 没有找到与字段 {', '.join(fields)} 对应的事件") + return False + + start_time = time.time() + all_set = True + for event in events_to_wait: + remaining_time = timeout - (time.time() - start_time) + if remaining_time <= 0: + all_set = False + break + if not event.wait(remaining_time): + all_set = False + break + + return all_set + +def wait_for_specific_data(check_func, timeout=60, check_interval=0.5): + print(f"等待特定条件满足,最多等待{timeout}秒...") + + start_time = time.time() + while time.time() - start_time < timeout: + if check_func(): + return True + time.sleep(check_interval) + + return False + +def login_cccc(tab, email, password): + tab.get('cccccccccccc') + print(f'登录页面加载完成,准备使用账号 {email} 登录') + + # 通过Google登录 + tab.ele('tag:button@@text():Google').click() + tab.wait.load_start() + print('Google登录页面加载完成') + + # 输入邮箱 + tab.ele('#identifierId').input(email) + tab.ele('tag:button@@text():下一步').click() + print('邮箱账号输入完成') + + # 输入密码 + password_input = tab.ele('#password') + password_input.wait.clickable() + password_input.input(password) + tab.ele('tag:button@@text():下一步').click() + tab.wait.load_start() + print('密码输入完成') + + # 点击继续按钮(如果存在) + try: + tab.ele('tag:button@@text():Continue').click() + tab.wait.load_start() + print('继续按钮点击完成') + except: + print('继续按钮不存在') + + tab.wait.load_start() + print('跳转完成') + +def handle_billing_page(tab: ChromiumTab, c: Chromium): + global PAYMENT_INFO, HCAPTCHA_INFO, CAPTURED_MESSAGES, DATA_EVENTS, BACKGROUND_THREAD_RUNNING + + try: + for key in PAYMENT_INFO: + PAYMENT_INFO[key] = None + PAYMENT_INFO['page_load_time'] = int(time.time()) + + HCAPTCHA_INFO['response'] = None + HCAPTCHA_INFO['duration'] = None + HCAPTCHA_INFO['updated_at'] = 0 + + with CAPTURED_MESSAGE_LOCK: + CAPTURED_MESSAGES.clear() + + for event in DATA_EVENTS.values(): + event.clear() + + BACKGROUND_THREAD_RUNNING = True + + tab.listen.start([ + "p/session/live_" + ]) + + manage_button = tab.ele('tag:button@@text():Manage') + manage_button.wait.clickable() + manage_button.click() + tab.wait.load_start() + + tab.wait(10) + + tab.console.start() + + res = tab.listen.wait(1) + redirect_url = res.url # type: ignore + response = res.response # type: ignore + data = response.raw_body + + bps_match = re.search(r'portal_session_id":"(bps_[^&]+)"', data) + if bps_match: + portal_session_id = bps_match.group(1) + PAYMENT_INFO['portal_session_id'] = portal_session_id + print(f"通过正则提取到的Portal Session ID: {portal_session_id}") + DATA_EVENTS['portal_session_updated'].set() + + ek_match = re.search(r'session_api_key":"(ek_live_[^&]+)"', data) + if ek_match: + session_api_key = ek_match.group(1) + PAYMENT_INFO['session_api_key'] = session_api_key + print(f"通过正则提取到的Session API Key: {session_api_key}") + DATA_EVENTS['session_api_key_updated'].set() + + acct_match = re.search(r'id":"(acct_[^&]+)"', data) + if acct_match: + acct_id = acct_match.group(1) + PAYMENT_INFO['acct_id'] = acct_id + DATA_EVENTS['acct_id_updated'].set() + + js_script = ''' + (function() { + let messages = []; + + function captureMessage(event) { + console.log('捕获postMessage:', event.data); + messages.push({ + source: event.source ? 'iframe' : 'window', + origin: event.origin, + data: event.data + }); + } + + window.addEventListener('message', captureMessage, false); + })(); + ''' + + tab.run_js(js_script, as_expr=True) + + js_script = ''' + (function() { + let messages = []; + + function captureMessage(event) { + console.log('捕获postMessage:', event.data); + messages.push({ + source: event.source ? 'iframe' : 'window', + origin: event.origin, + data: event.data + }); + } + + window.addEventListener('message', captureMessage, false); + })(); + ''' + + tab.run_js(js_script, as_expr=True) + + data_thread = threading.Thread(target=background_data_collector, args=(tab,), daemon=True) + data_thread.start() + + add_payment_button = tab.ele('tag:a@@text():添加支付方式') + add_payment_button.wait.clickable() + add_payment_button.click() + tab.wait.load_start() + + print("等待收集消息...") + time.sleep(5) + + def get_hcaptcha_info(): + return HCAPTCHA_INFO + + try: + if CAPTURED_MESSAGE_LOCK.acquire(timeout=2): # 最多等待2秒 + try: + collected_messages = list(CAPTURED_MESSAGES) + print(f"已收集到 {len(collected_messages)} 条消息") + finally: + CAPTURED_MESSAGE_LOCK.release() + else: + print("警告:获取消息超时,继续执行") + collected_messages = [] + except Exception as e: + print(f"获取消息时出错: {e}") + collected_messages = [] + + if not all([PAYMENT_INFO['guid'], PAYMENT_INFO['muid'], PAYMENT_INFO['sid']]): + for message_data in collected_messages: + if isinstance(message_data, dict) and 'originatingScript' in message_data and 'payload' in message_data: + payload = message_data.get('payload', {}) + if isinstance(payload, dict): + if 'guid' in payload and not PAYMENT_INFO['guid']: + PAYMENT_INFO['guid'] = payload['guid'] + print(f"提取到GUID: {payload['guid']}") + DATA_EVENTS['guid_updated'].set() + if 'muid' in payload and not PAYMENT_INFO['muid']: + PAYMENT_INFO['muid'] = payload['muid'] + print(f"提取到MUID: {payload['muid']}") + DATA_EVENTS['muid_updated'].set() + if 'sid' in payload and not PAYMENT_INFO['sid']: + PAYMENT_INFO['sid'] = payload['sid'] + print(f"提取到SID: {payload['sid']}") + DATA_EVENTS['sid_updated'].set() + + frames = tab.get_frames() + for frame in frames: # type: ignore + frame_url =frame.url + print(f"Frame URL: {frame_url}") + + if 'apiKey' in frame_url: + api_key_match = re.search(r'apiKey]=([^&]+)', frame_url) + if api_key_match: + api_key = api_key_match.group(1) + PAYMENT_INFO['api_key'] = api_key + DATA_EVENTS['api_key_updated'].set() + + if 'link-auth-modal-inner' in frame_url: + stripe_js_id_match = re.search(r'stripeJsId=([^&]+)', frame_url) + if stripe_js_id_match: + stripe_js_id = stripe_js_id_match.group(1) + print(f"提取到的Stripe JS ID: {stripe_js_id}") + PAYMENT_INFO['stripeJsId'] = stripe_js_id + DATA_EVENTS['stripe_js_id_updated'].set() + + if PAYMENT_INFO['api_key'] is None: + print('无法从URL中提取apiKey') + + if 'stripeJsId' not in PAYMENT_INFO or PAYMENT_INFO['stripeJsId'] is None: + print('无法从URL中提取stripeJsId') + + + tmp_tab: MixTab = c.new_tab() + res = tmp_tab.get("https://js.stripe.com/v3/.deploy_status_henson.json") + print(res, tmp_tab.json, tmp_tab.url) + + try: + deploy_status = tab.json + if deploy_status and 'deployedRevisions' in deploy_status and len(deploy_status['deployedRevisions']) > 0: + first_revision = deploy_status['deployedRevisions'][0] + revision_prefix = first_revision[:10] + payment_user_agent = f"stripe.js/{revision_prefix}; stripe-js-v3/{revision_prefix}; payment-element" + PAYMENT_INFO['payment_user_agent'] = payment_user_agent + print(f"Payment User Agent: {payment_user_agent}") + DATA_EVENTS['payment_user_agent_updated'].set() + else: + print("无法获取deployedRevisions信息") + except Exception as e: + print(f"提取deployedRevisions信息时出错: {e}") + + print("\n提取的支付信息:") + print(f"GUID: {PAYMENT_INFO['guid']}") + print(f"MUID: {PAYMENT_INFO['muid']}") + print(f"SID: {PAYMENT_INFO['sid']}") + print(f"hCaptcha响应: {PAYMENT_INFO['hcaptcha_response']}") + print(f"API Key: {PAYMENT_INFO['api_key']}") + print(f"Payment User Agent: {PAYMENT_INFO['payment_user_agent']}") + print(f"Stripe JS ID: {PAYMENT_INFO['stripeJsId']}") + print(f"Portal Session ID: {PAYMENT_INFO['portal_session_id']}") + print(f"Session API Key: {PAYMENT_INFO['session_api_key']}") + print(f"Account ID: {PAYMENT_INFO['acct_id']}") + + + + try: + portal_session_id = PAYMENT_INFO['portal_session_id'] + session_api_key = PAYMENT_INFO['session_api_key'] + + setup_intent_url = f"https://billing.stripe.com/v1/billing_portal/sessions/{portal_session_id}/setup_intents/" + query_params = {"include_only[]": ["id", "object", "client_secret", "payment_method_types"]} + headers = { + "Authorization": f"Bearer {session_api_key}", + "Content-Type": "application/x-www-form-urlencoded", + "stripe-account": PAYMENT_INFO['acct_id'], + "stripe-livemode": "true", + "stripe-version": "2025-03-01.dashboard" + } + + latest_info = get_hcaptcha_info() + if latest_info['response']: + PAYMENT_INFO['hcaptcha_response'] = latest_info['response'] + PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration'] + print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前") + + response = tab.post( # type: ignore + url=setup_intent_url, + params=query_params, + headers=headers + ) + + if response.status_code == 200: + setup_intent = response.json() + if setup_intent and 'id' in setup_intent: + setup_intent_id = setup_intent['id'] + setup_intent_client_secret = setup_intent.get('client_secret') + PAYMENT_INFO['setup_intent_id'] = setup_intent_id + PAYMENT_INFO['setup_intent_client_secret'] = setup_intent_client_secret + DATA_EVENTS['setup_intent_updated'].set() + + print(f"\n成功获取Setup Intent:") + print(f"Setup Intent ID: {setup_intent_id}") + print(f"Setup Intent Client Secret: {setup_intent_client_secret}") + else: + print(f"\n无法从响应中获取setup_intent信息") + print(f"响应内容: {tab.json}") + else: + print(f"\n获取setup_intent失败,状态码: {response.status_code}") + print(f"响应内容: {response.text}") + except Exception as e: + print(f"\n获取setup_intent时出错: {e}") + + try: + latest_info = get_hcaptcha_info() + if latest_info['response']: + PAYMENT_INFO['hcaptcha_response'] = latest_info['response'] + PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration'] + print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前") + + time_on_page = int(time.time()) - PAYMENT_INFO['page_load_time'] + + card_data = { + "type": "card", + "card[number]": "5154 cccccccccccccc", + "card[cvc]": "ccc", + "card[exp_year]": "cc", + "card[exp_month]": "cc", + "allow_redisplay": "unspecified", + "billing_details[address][country]": "cc", + "pasted_fields": "number", + "payment_user_agent": PAYMENT_INFO.get('payment_user_agent'), + "referrer": "https://billing.stripe.com", + "time_on_page": str(time_on_page), + "client_attribution_metadata[client_session_id]": PAYMENT_INFO.get('stripeJsId'), + "client_attribution_metadata[merchant_integration_source]": "elements", + "client_attribution_metadata[merchant_integration_subtype]": "payment-element", + "client_attribution_metadata[merchant_integration_version]": "2021", + "client_attribution_metadata[payment_intent_creation_flow]": "standard", + "client_attribution_metadata[payment_method_selection_flow]": "merchant_specified", + "guid": PAYMENT_INFO.get('guid'), + "muid": PAYMENT_INFO.get('muid'), + "sid": PAYMENT_INFO.get('sid'), + "key": PAYMENT_INFO.get('api_key'), + } + + if PAYMENT_INFO.get('hcaptcha_response'): + card_data["radar_options[hcaptcha_token]"] = PAYMENT_INFO.get('hcaptcha_response') + + payment_methods_url = "https://api.stripe.com/v1/payment_methods" + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + print("\n正在提交卡片信息...") + card_response = tab.post( # type: ignore + url=payment_methods_url, + data=card_data, + headers=headers + ) + + if card_response.status_code == 200: + payment_method = card_response.json() + if payment_method and 'id' in payment_method: + payment_method_id = payment_method['id'] + PAYMENT_INFO['payment_method_id'] = payment_method_id + DATA_EVENTS['payment_method_updated'].set() + + print(f"\n成功创建支付方式:") + print(f"Payment Method ID: {payment_method_id}") + print(f"Card Brand: {payment_method.get('card', {}).get('brand')}") + print(f"Last4: {payment_method.get('card', {}).get('last4')}") + else: + print(f"\n无法从响应中获取payment_method信息") + print(f"响应内容: {tab.json}") + else: + print(f"\n提交卡片信息失败,状态码: {card_response.status_code}") + print(f"响应内容: {card_response.text}") + + if 'payment_method_id' in PAYMENT_INFO and 'setup_intent_id' in PAYMENT_INFO: + latest_info = get_hcaptcha_info() + if latest_info['response']: + PAYMENT_INFO['hcaptcha_response'] = latest_info['response'] + PAYMENT_INFO['hcaptcha_duration'] = latest_info['duration'] + print(f"使用最新的hCaptcha响应,更新于 {int(time.time() - latest_info['updated_at'])} 秒前") + + confirm_url = f"https://billing.stripe.com/v1/billing_portal/sessions/{PAYMENT_INFO['portal_session_id']}/setup_intents/{PAYMENT_INFO['setup_intent_id']}/confirm" + confirm_params = {"include_only[]": ["id", "status", "client_secret", "payment_method"]} + confirm_data = { + "payment_method": PAYMENT_INFO['payment_method_id'], + "return_url": f"{redirect_url}/payment-methods/return?in_flow=false&make_customer_default=true" + } + + if PAYMENT_INFO.get('hcaptcha_response'): + confirm_data["passive_captcha_token"] = PAYMENT_INFO.get('hcaptcha_response') + + confirm_headers = { + "Authorization": f"Bearer {PAYMENT_INFO['session_api_key']}", + "Content-Type": "application/x-www-form-urlencoded", + "stripe-account": PAYMENT_INFO['acct_id'], + "stripe-livemode": "true", + "stripe-version": "2025-03-01.dashboard" + } + + print("\n正在确认setup_intent...") + confirm_response = tab.post( # type: ignore + url=confirm_url, + params=confirm_params, + data=confirm_data, + headers=confirm_headers + ) + + if confirm_response.status_code == 200: + confirm_result = confirm_response.json() + print(f"\n确认结果: {confirm_result.get('status')}") + PAYMENT_INFO['setup_intent_status'] = confirm_result.get('status') + DATA_EVENTS['setup_intent_updated'].set() + else: + print(f"\n确认setup_intent失败,状态码: {confirm_response.status_code}") + print(f"响应内容: {confirm_response.text}") + + except Exception as e: + print(f"\n提交卡片信息或确认setup_intent时出错: {e}") + + return PAYMENT_INFO + + except Exception as e: + print(f'添加支付方式按钮不存在: {e}') + return None + + +# 辅助函数,用于等待特定数据 +def wait_for_data(data_type, timeout=60): + if data_type in DATA_EVENTS: + print(f"等待{data_type}数据,最多等待{timeout}秒...") + return DATA_EVENTS[data_type].wait(timeout) + else: + print(f"警告: 未知的数据类型 {data_type}") + return False \ No newline at end of file diff --git a/DrissionPage_example.py b/DrissionPage_example.py new file mode 100644 index 0000000..e90b063 --- /dev/null +++ b/DrissionPage_example.py @@ -0,0 +1,1139 @@ +from DrissionPage import Chromium, ChromiumOptions +from DrissionPage.errors import PageDisconnectedError +import argparse +import time +import os +import secrets +import sys +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError +import threading + +from openai_register import get_email_and_token, get_oai_code + + +def ensure_stable_python_runtime(): + # 优先自动切到更稳定的 3.12 / 3.13,避免 3.14 下 Mail.tm 偶发 TLS/兼容问题。 + if sys.version_info < (3, 14) or os.environ.get("DPE_REEXEC_DONE") == "1": + return + + local_app_data = os.environ.get("LOCALAPPDATA", "") + candidates = [ + os.path.join(local_app_data, "Programs", "Python", "Python312", "python.exe"), + os.path.join(local_app_data, "Programs", "Python", "Python313", "python.exe"), + ] + + current_python = os.path.normcase(os.path.abspath(sys.executable)) + for candidate in candidates: + if not os.path.isfile(candidate): + continue + if os.path.normcase(os.path.abspath(candidate)) == current_python: + return + + print(f"[*] 检测到 Python {sys.version.split()[0]},自动切换到更稳定的解释器: {candidate}") + env = os.environ.copy() + env["DPE_REEXEC_DONE"] = "1" + os.execve(candidate, [candidate, os.path.abspath(__file__), *sys.argv[1:]], env) + + +def warn_runtime_compatibility(): + # 中文提示:避免把底层 TLS 兼容问题误判成脚本逻辑错误。 + if sys.version_info >= (3, 14): + print("[提示] 当前 Python 为 3.14+;若出现 Mail.tm TLS 异常,建议改用 Python 3.12 或 3.13。") + + +ensure_stable_python_runtime() +warn_runtime_compatibility() + +co = ChromiumOptions() +co.auto_port() +co.set_timeouts(base=1) + +# 加载修复 MouseEvent.screenX / screenY 的扩展。 +EXTENSION_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "turnstilePatch")) +co.add_extension(EXTENSION_PATH) + +# 保持可视化浏览器模式,方便直接观察注册过程。 + +browser = None +page = None + +SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com" +DEFAULT_SSO_FILE = os.path.join(os.path.dirname(__file__), "sso.txt") + + +def start_browser(): + # 每轮从全新浏览器开始,降低长时间复用带来的页面与 Cookie 污染。 + global browser, page + browser = None + page = None + + def _create(): + global browser + try: + browser = Chromium(co) + except Exception: + browser = None + + t = threading.Thread(target=_create) + t.daemon = True + t.start() + t.join(timeout=15) + + if browser is None: + raise Exception("启动浏览器超时(>15秒)") + + tabs = browser.get_tabs() + page = tabs[-1] if tabs else browser.new_tab() + return browser, page + + +def stop_browser(): + # 完整关闭整个浏览器实例,供下一轮重新拉起。 + global browser, page + if browser is not None: + def _safe_quit(): + try: + browser.quit() + except Exception: + pass + t = threading.Thread(target=_safe_quit) + t.daemon = True + t.start() + t.join(timeout=5) # 最多等 5 秒,避免卡死 + browser = None + page = None + + +def restart_browser(): + # 用户要求不要长期复用同一浏览器,因此每轮结束都重启整个实例。 + stop_browser() + time.sleep(1.5) + start_browser() + + +def refresh_active_page(): + # 验证码确认后页面会跳转,旧 page 句柄可能断开,这里统一重新获取当前活动标签页。 + global browser, page + if browser is None: + start_browser() + try: + tabs = browser.get_tabs() + if tabs: + page = tabs[-1] + else: + page = browser.new_tab() + except Exception: + restart_browser() + return page + + +def open_signup_page(): + # 每轮开始时打开注册页,并切到“使用邮箱注册”流程。 + global page + refresh_active_page() + try: + page.get(SIGNUP_URL) + except Exception: + refresh_active_page() + page = browser.new_tab(SIGNUP_URL) + click_email_signup_button() + + +def close_current_page(): + # 兼容旧调用名,实际行为改为整轮重启浏览器。 + restart_browser() + + +def has_profile_form(): + # 最终注册页只要出现姓名和密码输入框,就认为已经成功进入资料填写阶段。 + 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 click_email_signup_button(timeout=10): + # 页面打开后,自动点击“使用邮箱注册”按钮。 + deadline = time.time() + timeout + while time.time() < deadline: + clicked = page.run_js(r""" +const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]')); +const target = candidates.find((node) => { + const text = (node.innerText || node.textContent || '').replace(/\s+/g, ''); + return text.includes('使用邮箱注册'); +}); + +if (!target) { + return false; +} + +target.click(); +return true; + """) + + if clicked: + return True + + time.sleep(0.5) + + raise Exception('未找到“使用邮箱注册”按钮') + + +def fill_email_and_submit(timeout=15): + # 复用 `openai_register.py` 里的邮箱获取逻辑,保留邮箱与 token 供后续验证码步骤继续使用。 + email, dev_token = get_email_and_token() + if not email or not dev_token: + raise Exception("获取邮箱失败") + + deadline = time.time() + timeout + while time.time() < deadline: + 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; +} + +const input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => { + return isVisible(node) && !node.disabled && !node.readOnly; +}) || null; + +if (!input) { + return 'not-ready'; +} + +input.focus(); +input.click(); + +// 不能只写 `input.value = xxx`,否则 React / 受控表单可能没有同步内部状态。 +const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, '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 })); + +if ((input.value || '').trim() !== email || !input.checkValidity()) { + return false; +} + +input.blur(); +return 'filled'; + """, + email, + ) + + if filled == 'not-ready': + time.sleep(0.5) + continue + + if filled != 'filled': + print(f"[Debug] 邮箱输入框已出现,但写入失败: {filled}") + time.sleep(0.5) + continue + + if filled == 'filled': + time.sleep(0.8) + 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 input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => { + return isVisible(node) && !node.disabled && !node.readOnly; +}) || null; + +if (!input || !input.checkValidity() || !(input.value || '').trim()) { + return false; +} + +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => { + return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true'; +}); +const submitButton = buttons.find((node) => { + const text = (node.innerText || node.textContent || '').replace(/\s+/g, ''); + return text === '注册' || text.includes('注册'); +}); + +if (!submitButton || submitButton.disabled) { + return false; +} + +submitButton.click(); +return true; + """ + ) + + if clicked: + print(f"[*] 已填写邮箱并点击注册: {email}") + return email, dev_token + + time.sleep(0.5) + + raise Exception("未找到邮箱输入框或注册按钮") + + + +def fill_code_and_submit(email, dev_token, timeout=180): + # 复用 `openai_register.py` 里的验证码轮询逻辑,等待邮件到达后自动填写 OTP。 + code = get_oai_code(dev_token, email) + if not code: + raise Exception("获取验证码失败") + + deadline = time.time() + timeout + while time.time() < deadline: + try: + filled = page.run_js( + """ +const code = String(arguments[0] || '').trim(); + +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 setNativeValue(input, value) { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; + const tracker = input._valueTracker; + if (tracker) { + tracker.setValue(''); + } + if (nativeInputValueSetter) { + nativeInputValueSetter.call(input, ''); + nativeInputValueSetter.call(input, value); + } else { + input.value = ''; + input.value = value; + } +} + +function dispatchInputEvents(input, value) { + input.dispatchEvent(new InputEvent('beforeinput', { + bubbles: true, + cancelable: true, + data: value, + inputType: 'insertText', + })); + input.dispatchEvent(new InputEvent('input', { + bubbles: true, + cancelable: true, + data: value, + inputType: 'insertText', + })); + input.dispatchEvent(new Event('change', { bubbles: true })); +} + +const input = 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) => { + return isVisible(node) && !node.disabled && !node.readOnly && Number(node.maxLength || code.length || 6) > 1; +}) || null; + +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 autocomplete = String(node.autocomplete || '').toLowerCase(); + return maxLength === 1 || autocomplete === 'one-time-code'; +}); + +if (!input && otpBoxes.length < code.length) { + return 'not-ready'; +} + +if (input) { + input.focus(); + input.click(); + setNativeValue(input, code); + dispatchInputEvents(input, code); + + const normalizedValue = String(input.value || '').trim(); + const expectedLength = Number(input.maxLength || code.length || 6); + const slots = Array.from(document.querySelectorAll('[data-input-otp-slot="true"]')); + const filledSlots = slots.filter((slot) => (slot.textContent || '').trim()).length; + + if (normalizedValue !== code) { + // 聚合输入框写入失败,尝试回退到分格输入方式 + if (otpBoxes.length >= code.length) { + const orderedBoxes = otpBoxes.slice(0, code.length); + for (let i = 0; i < orderedBoxes.length; i += 1) { + const box = orderedBoxes[i]; + const char = code[i] || ''; + box.focus(); + box.click(); + setNativeValue(box, char); + dispatchInputEvents(box, char); + box.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: char })); + box.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: char })); + box.blur(); + } + const merged = orderedBoxes.map((node) => String(node.value || '').trim()).join(''); + return merged === code ? 'filled' : 'box-mismatch'; + } + return 'aggregate-mismatch'; + } + + if (expectedLength > 0 && normalizedValue.length !== expectedLength) { + return 'aggregate-length-mismatch'; + } + + if (slots.length && filledSlots && filledSlots !== normalizedValue.length) { + return 'aggregate-slot-mismatch'; + } + + input.blur(); + return 'filled'; +} + +const orderedBoxes = otpBoxes.slice(0, code.length); +for (let i = 0; i < orderedBoxes.length; i += 1) { + const box = orderedBoxes[i]; + const char = code[i] || ''; + box.focus(); + box.click(); + setNativeValue(box, char); + dispatchInputEvents(box, char); + box.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: char })); + box.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: char })); + box.blur(); +} + +const merged = orderedBoxes.map((node) => String(node.value || '').trim()).join(''); +return merged === code ? 'filled' : 'box-mismatch'; + """, + code, + ) + except PageDisconnectedError: + # 点击确认邮箱后如果刚好发生跳转,旧页面句柄会断开;此时切到新页继续判断即可。 + refresh_active_page() + if has_profile_form(): + print("[*] 验证码提交后已跳转到最终注册页。") + return code + time.sleep(1) + continue + + if filled == 'not-ready': + if has_profile_form(): + print("[*] 已直接进入最终注册页,跳过验证码按钮确认。") + return code + time.sleep(0.5) + continue + + if filled != 'filled': + print(f"[Debug] 验证码输入框已出现,但写入失败: {filled}") + time.sleep(0.5) + continue + + if filled == 'filled': + time.sleep(1.2) + try: + 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 aggregateInput = 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) => { + return isVisible(node) && !node.disabled && !node.readOnly && Number(node.maxLength || 0) > 1; +}) || null; + +let value = ''; +if (aggregateInput) { + value = String(aggregateInput.value || '').trim(); + const expectedLength = Number(aggregateInput.maxLength || value.length || 6); + if (!value || (expectedLength > 0 && value.length !== expectedLength)) { + return false; + } + + const slots = Array.from(document.querySelectorAll('[data-input-otp-slot="true"]')); + if (slots.length) { + const filledSlots = slots.filter((slot) => (slot.textContent || '').trim()).length; + if (filledSlots && filledSlots !== value.length) { + return false; + } + } +} else { + 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 autocomplete = String(node.autocomplete || '').toLowerCase(); + return maxLength === 1 || autocomplete === 'one-time-code'; + }); + value = otpBoxes.map((node) => String(node.value || '').trim()).join(''); + if (!value || value.length < 6) { + return false; + } +} + +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => { + return isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true'; +}); +const confirmButton = buttons.find((node) => { + const text = (node.innerText || node.textContent || '').replace(/\s+/g, ''); + return text === '确认邮箱' || text.includes('确认邮箱') || text === '继续' || text.includes('继续') || text === '下一步' || text.includes('下一步'); +}); + +if (!confirmButton) { + return 'no-button'; +} + +confirmButton.focus(); +confirmButton.click(); +return 'clicked'; + """ + ) + except PageDisconnectedError: + refresh_active_page() + if has_profile_form(): + print("[*] 确认邮箱后页面跳转成功,已进入最终注册页。") + return code + clicked = 'disconnected' + + if clicked == 'clicked': + print(f"[*] 已填写验证码并点击确认邮箱: {code}") + time.sleep(2) + refresh_active_page() + if has_profile_form(): + print("[*] 验证码确认完成,最终注册页已就绪。") + return code + + if clicked == 'no-button': + current_url = page.url + if 'sign-up' in current_url or 'signup' in current_url: + print(f"[*] 已填写验证码,页面已自动跳转到下一步: {current_url}") + return code + + if clicked == 'disconnected': + time.sleep(1) + continue + + time.sleep(0.5) + + debug_snapshot = 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 inputs = Array.from(document.querySelectorAll('input')).filter(isVisible).map((node) => ({ + type: node.type || '', + name: node.name || '', + testid: node.getAttribute('data-testid') || '', + autocomplete: node.autocomplete || '', + maxLength: Number(node.maxLength || 0), + value: String(node.value || ''), +})); + +const buttons = Array.from(document.querySelectorAll('button')).filter(isVisible).map((node) => ({ + text: String(node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim(), + disabled: !!node.disabled, + ariaDisabled: node.getAttribute('aria-disabled') || '', +})); + +return { url: location.href, inputs, buttons }; + """ + ) + print(f"[Debug] 验证码页 DOM 摘要: {debug_snapshot}") + raise Exception("未找到验证码输入框或确认邮箱按钮") + + +def getTurnstileToken(): + # 复用现有 turnstile 处理逻辑,在最终注册页需要时再触发。 + try: + page.run_js("try { turnstile.reset() } catch(e) { }") + except Exception: + pass + + turnstileResponse = None + + for i in range(0, 15): + try: + turnstileResponse = page.run_js("try { return turnstile.getResponse() } catch(e) { return null }") + if turnstileResponse: + return turnstileResponse + + # 先尝试直接点击页面主文档中可见的 Turnstile 容器/iframe + page.run_js(""" +const box = document.querySelector('.cf-turnstile, .turnstile, [data-sitekey]'); +if (box) { + box.scrollIntoView({ behavior: 'smooth', block: 'center' }); + const rect = box.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + box.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: x, clientY: y })); +} + """) + + challengeSolution = page.ele("@name=cf-turnstile-response", timeout=3) + challengeWrapper = challengeSolution.parent() + challengeIframe = challengeWrapper.shadow_root.ele("tag:iframe", timeout=3) + + challengeIframe.run_js(""" +window.dtp = 1 +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// 旧方案在 4K 屏下不稳定,这里给出更自然的屏幕坐标。 +let screenX = getRandomInt(800, 1200); +let screenY = getRandomInt(400, 600); + +Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX }); +Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY }); + """) + + challengeIframeBody = challengeIframe.ele("tag:body", timeout=3).shadow_root + challengeButton = challengeIframeBody.ele("tag:input", timeout=3) + challengeButton.click() + except: + pass + time.sleep(1) + raise Exception("failed to solve turnstile") + + +def build_profile(): + # 生成一组可重复使用的注册资料,密码至少包含大小写、数字和特殊字符。 + given_name = "Fan" + family_name = "YuXi" + password = "N" + secrets.token_hex(4) + "!a7#" + secrets.token_urlsafe(6) + return given_name, family_name, password + + +def fill_profile_and_submit(timeout=120): + # 在验证码通过后,直接锁定“可见且可写”的真实输入框,避免命中隐藏节点或 React 受控副本。 + given_name, family_name, password = build_profile() + deadline = time.time() + timeout + turnstile_token = "" + + while time.time() < deadline: + 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, ''); + nativeSetter.call(input, value); + } else { + input.value = ''; + input.value = value; + } + + input.dispatchEvent(new InputEvent('beforeinput', { + bubbles: true, + cancelable: true, + data: value, + inputType: 'insertText', + })); + input.dispatchEvent(new InputEvent('input', { + bubbles: true, + cancelable: true, + data: value, + inputType: 'insertText', + })); + input.dispatchEvent(new Event('change', { bubbles: true })); + input.dispatchEvent(new Event('blur', { bubbles: true })); + + return String(input.value || '') === String(value || ''); +} + +const givenInput = pickInput('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"]'); +const familyInput = pickInput('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"]'); +const passwordInput = pickInput('input[data-testid="password"], input[name="password"], input[type="password"]'); + +if (!givenInput || !familyInput || !passwordInput) { + return 'not-ready'; +} + +const givenOk = setInputValue(givenInput, givenName); +const familyOk = setInputValue(familyInput, familyName); +const passwordOk = setInputValue(passwordInput, password); + +if (!givenOk || !familyOk || !passwordOk) { + return 'filled-failed'; +} + +return [ + String(givenInput.value || '').trim() === String(givenName || '').trim(), + String(familyInput.value || '').trim() === String(familyName || '').trim(), + String(passwordInput.value || '') === String(password || ''), +].every(Boolean) ? 'filled' : 'verify-failed'; + """, + given_name, + family_name, + password, + ) + + if filled == 'not-ready': + time.sleep(0.5) + continue + + if filled != 'filled': + print(f"[Debug] 最终注册页输入框已出现,但姓名/密码写入失败: {filled}") + time.sleep(0.5) + continue + + values_ok = page.run_js( + """ +const expectedGiven = arguments[0]; +const expectedFamily = arguments[1]; +const expectedPassword = 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; +} + +const givenInput = pickInput('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"]'); +const familyInput = pickInput('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"]'); +const passwordInput = pickInput('input[data-testid="password"], input[name="password"], input[type="password"]'); + +if (!givenInput || !familyInput || !passwordInput) { + return false; +} + +return String(givenInput.value || '').trim() === String(expectedGiven || '').trim() + && String(familyInput.value || '').trim() === String(expectedFamily || '').trim() + && String(passwordInput.value || '') === String(expectedPassword || ''); + """, + given_name, + family_name, + password, + ) + if not values_ok: + print("[Debug] 最终注册页字段值校验失败,继续重试填写。") + time.sleep(0.5) + continue + + turnstile_state = page.run_js( + """ +const challengeInput = document.querySelector('input[name="cf-turnstile-response"]'); +if (!challengeInput) { + return 'not-found'; +} +const value = String(challengeInput.value || '').trim(); +return value ? 'ready' : 'pending'; + """ + ) + + if turnstile_state == "pending" and not turnstile_token: + print("[*] 检测到最终注册页存在 Turnstile,开始使用现有真人化点击逻辑。") + turnstile_token = getTurnstileToken() + if turnstile_token: + synced = page.run_js( + """ +const token = arguments[0]; +const challengeInput = document.querySelector('input[name="cf-turnstile-response"]'); +if (!challengeInput) { + return false; +} +const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; +if (nativeSetter) { + nativeSetter.call(challengeInput, token); +} else { + challengeInput.value = token; +} +challengeInput.dispatchEvent(new Event('input', { bubbles: true })); +challengeInput.dispatchEvent(new Event('change', { bubbles: true })); +return String(challengeInput.value || '').trim() === String(token || '').trim(); + """, + turnstile_token, + ) + if synced: + print("[*] Turnstile 响应已同步到最终注册表单。") + + time.sleep(1.2) + + try: + submit_button = page.ele('tag:button@@text()=完成注册', timeout=3) + except Exception: + submit_button = None + + if not submit_button: + clicked = page.run_js( + r""" +const challengeInput = document.querySelector('input[name="cf-turnstile-response"]'); +if (challengeInput && !String(challengeInput.value || '').trim()) { + return false; +} +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')); +const submitButton = buttons.find((node) => { + const text = (node.innerText || node.textContent || '').replace(/\s+/g, ''); + return text === '完成注册' || text.includes('完成注册'); +}); +if (!submitButton || submitButton.disabled || submitButton.getAttribute('aria-disabled') === 'true') { + return false; +} +submitButton.focus(); +submitButton.click(); +return true; + """ + ) + else: + challenge_value = page.run_js( + """ +const challengeInput = document.querySelector('input[name="cf-turnstile-response"]'); +return challengeInput ? String(challengeInput.value || '').trim() : 'not-found'; + """ + ) + if challenge_value not in ('not-found', ''): + submit_button.click() + clicked = True + else: + # Turnstile 值可能为空,但按钮可能已启用,回退到 JS 方式尝试点击 + clicked = page.run_js( + r""" +const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')); +const submitButton = buttons.find((node) => { + const text = (node.innerText || node.textContent || '').replace(/\s+/g, ''); + return text === '完成注册' || text.includes('完成注册'); +}); +if (!submitButton || submitButton.disabled || submitButton.getAttribute('aria-disabled') === 'true') { + return false; +} +submitButton.focus(); +submitButton.click(); +return true; + """ + ) + if clicked: + print("[*] Turnstile 值为空,但按钮已启用,已强制点击完成注册。") + + if clicked: + print(f"[*] 已填写注册资料并点击完成注册: {given_name} {family_name} / {password}") + # 等待页面跳转,最多 15 秒 + for _ in range(30): + time.sleep(0.5) + try: + current_url = page.url + if 'sign-up' not in current_url and 'signup' not in current_url: + print(f"[*] 页面已跳转,当前URL: {current_url}") + break + except Exception: + pass + else: + # 页面未跳转,检查是否有错误提示 + try: + error_text = page.run_js(r""" +const errorNode = document.querySelector('[role="alert"], .error, [data-testid="error-message"], .text-red-500, .text-error'); +return errorNode ? (errorNode.innerText || errorNode.textContent || '').trim().substring(0, 200) : ''; + """) + except Exception: + error_text = "" + if error_text: + raise Exception(f"注册提交失败,页面提示错误: {error_text}") + print("[*] 警告: 点击完成注册后页面未跳转,将继续尝试获取 sso cookie。") + + return { + "given_name": given_name, + "family_name": family_name, + "password": password, + } + + time.sleep(0.5) + + raise Exception("未找到最终注册表单或完成注册按钮") + + +def extract_visible_numbers(timeout=60): + # 登录/注册完成后,提取页面上可见的普通数字文本,不处理任何敏感 Cookie。 + deadline = time.time() + timeout + while time.time() < deadline: + result = page.run_js( + r""" +function isVisible(el) { + if (!el) { + return false; + } + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { + return false; + } + const rect = el.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; +} + +const selector = [ + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'div', 'span', 'p', 'strong', 'b', 'small', + '[data-testid]', '[class]', '[role="heading"]' +].join(','); + +const seen = new Set(); +const matches = []; +for (const node of document.querySelectorAll(selector)) { + if (!isVisible(node)) { + continue; + } + const text = String(node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim(); + if (!text) { + continue; + } + const found = text.match(/\d+(?:\.\d+)?/g); + if (!found) { + continue; + } + for (const value of found) { + const key = `${value}@@${text}`; + if (seen.has(key)) { + continue; + } + seen.add(key); + matches.push({ value, text }); + } +} + +return matches.slice(0, 30); + """ + ) + + if result: + print("[*] 页面可见数字文本提取结果:") + for item in result: + try: + print(f" - 数字: {item['value']} | 上下文: {item['text']}") + except Exception: + pass + return result + + time.sleep(1) + + raise Exception("登录后未提取到可见数字文本") + + +def wait_for_sso_cookie(timeout=120): + # 必须在注册完成后再取 sso,优先抓取精确的 sso cookie。 + deadline = time.time() + timeout + last_seen_names = set() + last_report = 0 + + while time.time() < deadline: + try: + refresh_active_page() + if page is None: + time.sleep(1) + continue + + 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: + print("[*] 注册完成后已获取到 sso cookie。") + return value + + except PageDisconnectedError: + refresh_active_page() + except Exception: + pass + + # 每 10 秒报告一次进度,避免看起来像是卡死 + if time.time() - last_report >= 10: + try: + current_url = page.url if page else "unknown" + except Exception: + current_url = "unknown" + print(f"[*] 等待 sso cookie 中... 当前URL: {current_url} 已见cookie: {sorted(last_seen_names)}") + last_report = time.time() + + time.sleep(1) + + raise Exception(f"注册完成后未获取到 sso cookie,当前已见 cookie: {sorted(last_seen_names)}") + + +def append_sso_to_txt(sso_value, output_path=DEFAULT_SSO_FILE): + # 按用户要求,一行写一个 sso 值,持续追加。 + normalized = str(sso_value or "").strip() + if not normalized: + raise Exception("待写入的 sso 为空") + + with open(output_path, "a", encoding="utf-8") as file: + file.write(normalized + "\n") + + print(f"[*] 已追加写入 sso 到文件: {output_path}") + + +def run_single_registration(output_path=DEFAULT_SSO_FILE, extract_numbers=False): + # 单轮流程:打开注册页 -> 完成注册 -> 获取 sso -> 写 txt。 + open_signup_page() + email, dev_token = fill_email_and_submit() + fill_code_and_submit(email, dev_token) + profile = fill_profile_and_submit() + sso_value = wait_for_sso_cookie() + append_sso_to_txt(sso_value, output_path) + + if extract_numbers: + extract_visible_numbers() + + result = { + "email": email, + "sso": sso_value, + **profile, + } + print(f"[*] 本轮注册完成,邮箱: {email}") + return result + + +def main(): + # 默认循环执行;每轮完成后关闭当前页,再自动进入下一轮。 + parser = argparse.ArgumentParser(description="xAI 自动注册并采集 sso") + parser.add_argument("--count", type=int, default=0, help="执行轮数,0 表示无限循环") + parser.add_argument("--output", default=DEFAULT_SSO_FILE, help="sso 输出 txt 路径") + parser.add_argument("--extract-numbers", action="store_true", help="注册完成后额外提取页面数字文本") + args = parser.parse_args() + + current_round = 0 + try: + start_browser() + while True: + if args.count > 0 and current_round >= args.count: + break + + current_round += 1 + print(f"\n[*] 开始第 {current_round} 轮注册") + round_succeeded = False + + try: + with ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(run_single_registration, args.output, extract_numbers=args.extract_numbers) + try: + future.result(timeout=120) + round_succeeded = True + except FutureTimeoutError: + print(f"[Error] 第 {current_round} 轮注册超时(>120秒),强制终止并进入下一轮。") + except KeyboardInterrupt: + print("\n[Info] 收到中断信号,停止后续轮次。") + break + except Exception as error: + print(f"[Error] 第 {current_round} 轮失败: {error}") + finally: + restart_browser() + + if args.count == 0 or current_round < args.count: + time.sleep(2) + + finally: + stop_browser() + + +if __name__ == "__main__": + main() diff --git a/openai_register.py b/openai_register.py new file mode 100644 index 0000000..6c84d35 --- /dev/null +++ b/openai_register.py @@ -0,0 +1,160 @@ +import requests +import re +import time +import secrets +import string + +BASE_URL = "https://api.mail.tm" + + +def _random_string(length=12): + return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)) + + +def get_email_and_token(): + try: + # 1. 获取可用域名 + r = requests.get(f"{BASE_URL}/domains", timeout=30) + r.raise_for_status() + domains = r.json().get("hydra:member", []) + if not domains: + print("[Error] Mail.tm 没有可用域名") + return None, None + domain = domains[0]["domain"] + + # 2. 生成随机邮箱和密码 + local_part = _random_string(10) + email = f"{local_part}@{domain}" + password = _random_string(16) + + # 3. 创建账户 + r = requests.post( + f"{BASE_URL}/accounts", + json={"address": email, "password": password}, + timeout=30, + ) + r.raise_for_status() + + # 4. 获取 token + r = requests.post( + f"{BASE_URL}/token", + json={"address": email, "password": password}, + timeout=30, + ) + r.raise_for_status() + token = r.json().get("token") + if not token: + print("[Error] 无法获取 Mail.tm token") + return None, None + + print(f"[*] 已获取临时邮箱: {email}") + return email, token + except Exception as e: + print(f"[Error] 获取邮箱失败: {e}") + return None, None + + +def _extract_code(subject, text, html_text=""): + """从邮件文本中提取验证码。优先匹配 xAI/常见服务的验证码格式。""" + text = text or "" + html_text = html_text or "" + subject = subject or "" + full_text = f"{subject}\n{text}\n{html_text}" + + # 1. xAI 主题自带验证码格式: EB5-XJA xAI confirmation code + m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b.*confirmation\s*code", subject, re.IGNORECASE) + if m: + return m.group(1).upper() + + # 2. 通用 xAI/OpenAI 验证码格式(字母数字混合) + patterns = [ + # xAI 正文: code below to validate ... EB5-XJA + r"code\s+below\s+to\s+validate.*?\n\s*([A-Z0-9]{3}-[A-Z0-9]{3})\s*\n", + # 通用字母数字验证码: ABC-123, 123-ABC, A1B-2C3 + r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", + # verification code is 123456 + r"(?:verification\s*code|code\s*is)[:\s]+(\d{6})", + # 中文验证码 + r"(?:验证码|代码|确认码)[:\s为]+(\d{6})", + # code: 123456 + r"(?:code|验证码)[:\s]+(\d{6})", + # 6位数字带上下文 + r"\b(\d{6})\b(?:\s*(?:is your|作为您的)\s*(?:verification\s*code|code|验证码))?", + ] + + for pat in patterns: + m = re.search(pat, full_text, re.IGNORECASE | re.DOTALL) + if m: + code = m.group(1) + if code: + return code.upper() if "-" in code else code + + # 3. 兜底:仅在纯文本中匹配 6 位数字,避开 HTML 颜色代码 + if text: + for m in re.finditer(r"(?" + ], + "run_at": "document_start", + "all_frames": true, + "world": "MAIN" + } + ] +} \ No newline at end of file diff --git a/turnstilePatch/script.js b/turnstilePatch/script.js new file mode 100644 index 0000000..4263a0a --- /dev/null +++ b/turnstilePatch/script.js @@ -0,0 +1,12 @@ +function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// old method wouldn't work on 4k screens + +let screenX = getRandomInt(800, 1200); +let screenY = getRandomInt(400, 600); + +Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX }); + +Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY }); \ No newline at end of file