#!/usr/bin/env python # -*- coding: utf-8 -*- """ Grok 注册机 - TTK GUI 版本 整合 DrissionPage_example.py, openai_register.py, batch_open_nsfw.py """ import tkinter as tk from tkinter import ttk, messagebox, scrolledtext import threading import datetime import time import os import sys import gc import queue import secrets import struct import random import re import string import json os.environ.setdefault("TK_SILENCE_DEPRECATION", "1") from DrissionPage import Chromium, ChromiumOptions from DrissionPage.errors import PageDisconnectedError from curl_cffi import requests CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.json") MEMORY_CLEANUP_INTERVAL = 5 UI_BG = "#242424" UI_PANEL_BG = "#2b2b2b" UI_FG = "#f2f2f2" UI_MUTED_FG = "#b8b8b8" UI_ENTRY_BG = "#333333" UI_BUTTON_BG = "#3a3a3a" UI_ACTIVE_BG = "#4a6078" DEFAULT_CONFIG = { "duckmail_api_key": "", "cloudflare_api_base": "", "cloudflare_api_key": "", "cloudflare_auth_mode": "bearer", "cloudflare_path_domains": "/domains", "cloudflare_path_accounts": "/accounts", "cloudflare_path_token": "/token", "cloudflare_path_messages": "/messages", "proxy": "http://127.0.0.1:7890", "enable_nsfw": True, "register_count": 1, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", "grok2api_auto_add_local": True, "grok2api_local_token_file": "", "grok2api_pool_name": "ssoBasic", "grok2api_auto_add_remote": False, "grok2api_remote_base": "", "grok2api_remote_app_key": "", } config = DEFAULT_CONFIG.copy() _cf_domain_index = 0 class RegistrationCancelled(Exception): pass class AccountRetryNeeded(Exception): pass def load_config(): global config if os.path.exists(CONFIG_FILE): try: with open(CONFIG_FILE, "r", encoding="utf-8") as f: loaded = json.load(f) config = {**DEFAULT_CONFIG, **loaded} except Exception: config = DEFAULT_CONFIG.copy() return config def save_config(): try: with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=4, ensure_ascii=False) except Exception as e: print(f"保存配置失败: {e}") def ensure_stable_python_runtime(): 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(): 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() load_config() EXTENSION_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), "turnstilePatch") ) DUCKMAIL_API_BASE = "https://api.duckmail.sbs" def get_proxies(): proxy = config.get("proxy", "") if proxy: return {"http": proxy, "https": proxy} return {} def get_duckmail_api_key(): return config.get("duckmail_api_key", "") def get_cloudflare_api_base(): return str(config.get("cloudflare_api_base", "") or "").rstrip("/") def get_cloudflare_api_key(): return config.get("cloudflare_api_key", "") def get_cloudflare_auth_mode(): return str(config.get("cloudflare_auth_mode", "bearer") or "bearer").lower() def get_cloudflare_path(key, default_path): raw = str(config.get(key, default_path) or default_path).strip() if not raw.startswith("/"): raw = "/" + raw return raw def cloudflare_build_headers(content_type=False): headers = {"Content-Type": "application/json"} if content_type else {} key = get_cloudflare_api_key() mode = get_cloudflare_auth_mode() if key: if mode == "x-api-key": headers["X-API-Key"] = key elif mode == "x-admin-auth": headers["x-admin-auth"] = key elif mode != "none": headers["Authorization"] = f"Bearer {key}" return headers def cloudflare_apply_auth_params(params=None): merged = dict(params or {}) key = get_cloudflare_api_key() mode = get_cloudflare_auth_mode() if key and mode == "query-key": merged["key"] = key return merged def cloudflare_next_default_domain(): """按配置轮换选择 Cloudflare 临时邮箱域名。""" global _cf_domain_index domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()] if not domains: return "" domain = domains[_cf_domain_index % len(domains)] _cf_domain_index += 1 return domain def cloudflare_is_admin_create_path(path): """判断当前创建邮箱路径是否为 cloudflare_temp_email 管理员创建接口。""" return str(path or "").rstrip("/").lower() == "/admin/new_address" def _pick_list_payload(data): if isinstance(data, list): return data if isinstance(data, dict): if isinstance(data.get("results"), list): return data.get("results") if isinstance(data.get("hydra:member"), list): return data.get("hydra:member") if isinstance(data.get("data"), list): return data.get("data") if isinstance(data.get("messages"), list): return data.get("messages") if isinstance(data.get("data"), dict): nested = data.get("data") if isinstance(nested.get("messages"), list): return nested.get("messages") return [] def cloudflare_create_temp_address(api_base): """适配 cloudflare_temp_email 新建地址接口并兼容 admin 创建模式。""" path = get_cloudflare_path("cloudflare_path_accounts", "/api/new_address") url = f"{api_base}{path}" domain = cloudflare_next_default_domain() is_admin_create = cloudflare_is_admin_create_path(path) if is_admin_create: payload = {"name": generate_username(10), "enablePrefix": True} if domain: payload["domain"] = domain headers = cloudflare_build_headers(content_type=True) else: payload = {} if domain: payload["domain"] = domain headers = {"Content-Type": "application/json"} resp = http_post(url, json=payload, headers=headers) resp.raise_for_status() try: data = resp.json() except Exception: raise Exception(f"Cloudflare /api/new_address 返回非JSON: {resp.text[:300]}") address = data.get("address") jwt = data.get("jwt") if not address or not jwt: raise Exception(f"Cloudflare /api/new_address 缺少 address/jwt: {data}") return address, jwt def get_user_agent(): return config.get( "user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", ) def resolve_grok2api_local_token_file(): configured = str(config.get("grok2api_local_token_file", "") or "").strip() if configured: return configured return os.path.join(os.path.dirname(__file__), "token.json") def _normalize_sso_token(raw_token): token = str(raw_token or "").strip() if token.startswith("sso="): token = token[4:] return token def add_token_to_grok2api_local_pool(raw_token, email="", log_callback=None): token = _normalize_sso_token(raw_token) if not token: return False token_file = resolve_grok2api_local_token_file() pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip() if not pool_name: pool_name = "ssoBasic" os.makedirs(os.path.dirname(token_file), exist_ok=True) data = {} if os.path.exists(token_file): try: with open(token_file, "r", encoding="utf-8") as f: data = json.load(f) or {} except Exception: data = {} if not isinstance(data, dict): data = {} pool = data.get(pool_name) if not isinstance(pool, list): pool = [] existing = set() for item in pool: if isinstance(item, str): existing.add(_normalize_sso_token(item)) elif isinstance(item, dict): existing.add(_normalize_sso_token(item.get("token", ""))) if token in existing: if log_callback: log_callback(f"[*] grok2api 本地池已存在 token: {pool_name}") return True entry = {"token": token, "tags": ["auto-register"], "note": email} pool.append(entry) data[pool_name] = pool with open(token_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) if log_callback: log_callback(f"[+] 已写入 grok2api 本地池: {pool_name} ({token_file})") return True def get_grok2api_remote_api_bases(base): """生成 grok2api 管理 API 候选根路径。 参数: - base str: 用户配置的 grok2api 远端地址 返回: - list[str]: 依次尝试的管理 API 根路径 """ normalized = str(base or "").strip().rstrip("/") if not normalized: return [] lower = normalized.lower() candidates = [normalized] if lower.endswith("/admin/api"): return candidates if lower.endswith("/admin"): candidates.append(f"{normalized}/api") else: candidates.append(f"{normalized}/admin/api") seen = set() unique = [] for item in candidates: if item not in seen: unique.append(item) seen.add(item) return unique def add_token_to_grok2api_remote_pool(raw_token, email="", log_callback=None): token = _normalize_sso_token(raw_token) if not token: return False base = str(config.get("grok2api_remote_base", "") or "").strip().rstrip("/") app_key = str(config.get("grok2api_remote_app_key", "") or "").strip() pool_name = str(config.get("grok2api_pool_name", "ssoBasic") or "ssoBasic").strip() or "ssoBasic" if not base or not app_key: if log_callback: log_callback("[Debug] grok2api 远端未配置 base/app_key,跳过") return False headers = {"Content-Type": "application/json"} query = {"app_key": app_key} pool_map = {"ssoBasic": "basic", "ssoSuper": "super"} remote_pool = pool_map.get(pool_name, "basic") api_bases = get_grok2api_remote_api_bases(base) add_errors = [] # 优先使用 add 接口,避免全量覆盖远端池 add_payload = {"tokens": [token], "pool": remote_pool, "tags": ["auto-register"]} for api_base in api_bases: endpoint = f"{api_base}/tokens/add" try: resp_add = http_post( endpoint, headers=headers, params=query, json=add_payload, timeout=30, proxies={}, ) resp_add.raise_for_status() if log_callback: log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({endpoint})") return True except Exception as add_exc: add_errors.append(f"{endpoint}: {add_exc}") if log_callback: log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {'; '.join(add_errors)}") # 兜底:旧版全量保存接口 current = {} try: resp = http_get(f"{base}/tokens", headers=headers, params=query, timeout=20, proxies={}) if resp.status_code == 200: payload = resp.json() current = payload.get("tokens", {}) if isinstance(payload, dict) else {} except Exception: current = {} if not isinstance(current, dict): current = {} pool = current.get(pool_name) if not isinstance(pool, list): pool = [] existing = set() for item in pool: if isinstance(item, str): existing.add(_normalize_sso_token(item)) elif isinstance(item, dict): existing.add(_normalize_sso_token(item.get("token", ""))) if token not in existing: pool.append({"token": token, "tags": ["auto-register"], "note": email}) current[pool_name] = pool resp2 = http_post(f"{base}/tokens", headers=headers, params=query, json=current, timeout=30, proxies={}) resp2.raise_for_status() if log_callback: log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({base}/tokens)") return True def add_token_to_grok2api_pools(raw_token, email="", log_callback=None): if config.get("grok2api_auto_add_local", True): try: add_token_to_grok2api_local_pool(raw_token, email=email, log_callback=log_callback) except Exception as exc: if log_callback: log_callback(f"[Debug] 写入 grok2api 本地池失败: {exc}") if config.get("grok2api_auto_add_remote", False): try: add_token_to_grok2api_remote_pool(raw_token, email=email, log_callback=log_callback) except Exception as exc: if log_callback: log_callback(f"[Debug] 写入 grok2api 远端池失败: {exc}") def create_browser_options(): options = ChromiumOptions() options.auto_port() options.set_timeouts(base=1) if os.path.exists(EXTENSION_PATH): options.add_extension(EXTENSION_PATH) return options def _build_request_kwargs(**kwargs): request_kwargs = dict(kwargs) proxies = request_kwargs.pop("proxies", None) if proxies is None: proxies = get_proxies() if proxies: request_kwargs["proxies"] = proxies request_kwargs.setdefault("timeout", 15) return request_kwargs def http_get(url, **kwargs): try: return requests.get(url, **_build_request_kwargs(**kwargs)) except Exception as exc: err = str(exc) # 代理不可用时自动回退为直连,避免整个流程直接失败 if "127.0.0.1 port 7890" in err or "Could not connect to server" in err: retry_kwargs = dict(kwargs) retry_kwargs["proxies"] = {} return requests.get(url, **_build_request_kwargs(**retry_kwargs)) raise def http_post(url, **kwargs): try: return requests.post(url, **_build_request_kwargs(**kwargs)) except Exception as exc: err = str(exc) if "127.0.0.1 port 7890" in err or "Could not connect to server" in err: retry_kwargs = dict(kwargs) retry_kwargs["proxies"] = {} return requests.post(url, **_build_request_kwargs(**retry_kwargs)) raise def raise_if_cancelled(cancel_callback=None): if cancel_callback and cancel_callback(): raise RegistrationCancelled("鐢ㄦ埛鍋滄娉ㄥ唽") def sleep_with_cancel(seconds, cancel_callback=None): deadline = time.time() + max(seconds, 0) while True: raise_if_cancelled(cancel_callback) remaining = deadline - time.time() if remaining <= 0: return time.sleep(min(0.2, remaining)) def get_domains(api_key=None): headers = {} key = api_key or get_duckmail_api_key() if key: headers["Authorization"] = f"Bearer {key}" resp = http_get(f"{DUCKMAIL_API_BASE}/domains", headers=headers) resp.raise_for_status() return resp.json().get("hydra:member", []) def create_account(address, password, api_key=None, expires_in=0): headers = {"Content-Type": "application/json"} key = api_key or get_duckmail_api_key() if key: headers["Authorization"] = f"Bearer {key}" data = {"address": address, "password": password, "expiresIn": expires_in} resp = http_post(f"{DUCKMAIL_API_BASE}/accounts", json=data, headers=headers) resp.raise_for_status() return resp.json() def get_token(address, password): data = {"address": address, "password": password} resp = http_post(f"{DUCKMAIL_API_BASE}/token", json=data) resp.raise_for_status() return resp.json().get("token") def get_messages(token): headers = {"Authorization": f"Bearer {token}"} resp = http_get(f"{DUCKMAIL_API_BASE}/messages", headers=headers) resp.raise_for_status() return resp.json().get("hydra:member", []) def get_message_detail(token, message_id): headers = {"Authorization": f"Bearer {token}"} resp = http_get(f"{DUCKMAIL_API_BASE}/messages/{message_id}", headers=headers) resp.raise_for_status() return resp.json() def cloudflare_get_domains(api_base, api_key=None): headers = cloudflare_build_headers(content_type=False) if api_key and "Authorization" in headers: headers["Authorization"] = f"Bearer {api_key}" if api_key and "X-API-Key" in headers: headers["X-API-Key"] = api_key path = get_cloudflare_path("cloudflare_path_domains", "/domains") params = cloudflare_apply_auth_params() resp = http_get(f"{api_base}{path}", headers=headers, params=params) resp.raise_for_status() return _pick_list_payload(resp.json()) def cloudflare_create_account(api_base, address, password, api_key=None, expires_in=0): headers = cloudflare_build_headers(content_type=True) if api_key and "Authorization" in headers: headers["Authorization"] = f"Bearer {api_key}" if api_key and "X-API-Key" in headers: headers["X-API-Key"] = api_key payload = {"address": address, "password": password, "expiresIn": expires_in} path = get_cloudflare_path("cloudflare_path_accounts", "/accounts") params = cloudflare_apply_auth_params() resp = http_post(f"{api_base}{path}", json=payload, headers=headers, params=params) resp.raise_for_status() return resp.json() def cloudflare_get_token(api_base, address, password, api_key=None): headers = cloudflare_build_headers(content_type=True) if api_key and "Authorization" in headers: headers["Authorization"] = f"Bearer {api_key}" if api_key and "X-API-Key" in headers: headers["X-API-Key"] = api_key path = get_cloudflare_path("cloudflare_path_token", "/token") resp = http_post( f"{api_base}{path}", json={"address": address, "password": password}, headers=headers, params=cloudflare_apply_auth_params(), ) resp.raise_for_status() data = resp.json() if isinstance(data, dict): if data.get("token"): return data.get("token") if isinstance(data.get("data"), dict) and data["data"].get("token"): return data["data"].get("token") return None def cloudflare_get_messages(api_base, token): headers = {"Authorization": f"Bearer {token}"} path = get_cloudflare_path("cloudflare_path_messages", "/messages") params = {"limit": 20, "offset": 0} params = cloudflare_apply_auth_params(params) resp = http_get(f"{api_base}{path}", headers=headers, params=params) resp.raise_for_status() try: data = resp.json() except Exception: raise Exception(f"Cloudflare messages 返回非JSON: {resp.text[:300]}") return _pick_list_payload(data) def cloudflare_get_message_detail(api_base, token, message_id): headers = {"Authorization": f"Bearer {token}"} candidates = [ f"{api_base}/api/mail/{message_id}", f"{api_base}{get_cloudflare_path('cloudflare_path_messages', '/messages')}/{message_id}", ] last_err = None for url in candidates: try: resp = http_get( url, headers=headers, params=cloudflare_apply_auth_params(), ) resp.raise_for_status() data = resp.json() if isinstance(data, dict) and isinstance(data.get("data"), dict): return data["data"] return data except Exception as exc: last_err = exc continue raise Exception(f"Cloudflare 获取邮件详情失败: {last_err}") YYDS_API_BASE = "https://maliapi.215.im/v1" def get_yyds_api_key(): return config.get("yyds_api_key", "") def get_yyds_jwt(): return config.get("yyds_jwt", "") def yyds_get_domains(api_key=None, jwt=None): key = api_key or get_yyds_api_key() token = jwt or get_yyds_jwt() headers = {} if token: headers["Authorization"] = f"Bearer {token}" elif key: headers["X-API-Key"] = key resp = http_get(f"{YYDS_API_BASE}/domains", headers=headers) resp.raise_for_status() data = resp.json() return data.get("data", []) if data.get("success") else [] def yyds_create_account(address=None, domain=None, api_key=None, jwt=None): key = api_key or get_yyds_api_key() token = jwt or get_yyds_jwt() headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" elif key: headers["X-API-Key"] = key payload = {} if address: payload["address"] = address if domain: payload["domain"] = domain elif key or token: payload["autoDomainStrategy"] = "prefer_owned" resp = http_post(f"{YYDS_API_BASE}/accounts", json=payload, headers=headers) resp.raise_for_status() data = resp.json() if data.get("success"): return data.get("data", {}) raise Exception(f"YYDS 鍒涘缓閭澶辫触: {data}") def yyds_get_token(address, api_key=None, jwt=None): key = api_key or get_yyds_api_key() token = jwt or get_yyds_jwt() headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" elif key: headers["X-API-Key"] = key resp = http_post( f"{YYDS_API_BASE}/token", json={"address": address}, headers=headers ) resp.raise_for_status() data = resp.json() if data.get("success"): return data.get("data", {}).get("token") raise Exception(f"YYDS 鑾峰彇token澶辫触: {data}") def yyds_get_messages(address, token=None, api_key=None, jwt=None): key = api_key or get_yyds_api_key() temp_token = token or jwt or get_yyds_jwt() headers = {} if temp_token: headers["Authorization"] = f"Bearer {temp_token}" elif key: headers["X-API-Key"] = key resp = http_get( f"{YYDS_API_BASE}/messages", params={"address": address}, headers=headers, ) resp.raise_for_status() data = resp.json() if data.get("success"): return data.get("data", {}).get("messages", []) return [] def yyds_get_message_detail(message_id, token=None, api_key=None, jwt=None): key = api_key or get_yyds_api_key() temp_token = token or jwt or get_yyds_jwt() headers = {} if temp_token: headers["Authorization"] = f"Bearer {temp_token}" elif key: headers["X-API-Key"] = key resp = http_get(f"{YYDS_API_BASE}/messages/{message_id}", headers=headers) resp.raise_for_status() data = resp.json() if data.get("success"): return data.get("data", {}) raise Exception(f"YYDS 鑾峰彇閭欢璇︽儏澶辫触: {data}") def yyds_generate_username(length=10): chars = string.ascii_lowercase + string.digits return "".join(secrets.choice(chars) for _ in range(length)) def yyds_pick_domain(api_key=None, jwt=None): domains = yyds_get_domains(api_key=api_key, jwt=jwt) if not domains: raise Exception("YYDS 娌℃湁杩斿洖浠讳綍鍙敤鍩熷悕") private = [d for d in domains if d.get("isVerified") and not d.get("isPublic")] if private: return private[0]["domain"] public = [d for d in domains if d.get("isVerified") and d.get("isPublic")] if public: return public[0]["domain"] verified = [d for d in domains if d.get("isVerified")] if verified: return verified[0]["domain"] raise Exception("YYDS 鏃犲凡楠岃瘉鍩熷悕鍙敤") def yyds_get_email_and_token(api_key=None, jwt=None): key = api_key or get_yyds_api_key() token = jwt or get_yyds_jwt() if not token and not key: raise Exception("YYDS API Key 或 JWT 未配置") domain = yyds_pick_domain(api_key=key, jwt=token) username = yyds_generate_username(10) result = yyds_create_account( address=username, domain=domain, api_key=key, jwt=token ) address = result.get("address") or f"{username}@{domain}" temp_token = result.get("token") if not temp_token: temp_token = yyds_get_token(address, api_key=key, jwt=token) if not temp_token: raise Exception("鑾峰彇 YYDS token 澶辫触") print(f"[*] 宸插垱寤?YYDS 閭: {address}") return address, temp_token def yyds_get_oai_code( token, address, timeout=180, poll_interval=3, log_callback=None, jwt=None, cancel_callback=None, ): deadline = time.time() + timeout seen_ids = set() while time.time() < deadline: raise_if_cancelled(cancel_callback) try: messages = yyds_get_messages(address, token=token, jwt=jwt) except Exception as exc: if log_callback: log_callback(f"[Debug] YYDS 鎷夊彇閭欢鍒楄〃澶辫触: {exc}") sleep_with_cancel(poll_interval, cancel_callback) continue for msg in messages: msg_id = msg.get("id") if not msg_id or msg_id in seen_ids: continue seen_ids.add(msg_id) to_addrs = [t.get("address", "").lower() for t in (msg.get("to") or [])] if address.lower() not in to_addrs: continue try: detail = yyds_get_message_detail(msg_id, token=token, jwt=jwt) except Exception as exc: if log_callback: log_callback(f"[Debug] YYDS 鑾峰彇閭欢璇︽儏澶辫触: {exc}") continue parts = [] text_body = detail.get("text") or "" if text_body: parts.append(text_body) html_list = detail.get("html") or [] for h in html_list: parts.append(re.sub(r"<[^>]+>", " ", h)) combined = "\n".join(parts) subject = detail.get("subject", "") if log_callback: log_callback(f"[Debug] YYDS 鏀跺埌閭欢: {subject}") code = extract_verification_code(combined, subject) if code: if log_callback: log_callback(f"[*] YYDS 浠庨偖浠朵腑鎻愬彇鍒伴獙璇佺爜: {code}") return code sleep_with_cancel(poll_interval, cancel_callback) raise Exception(f"YYDS 在 {timeout}s 内未收到验证码邮件") def generate_username(length=10): chars = string.ascii_lowercase + string.digits return "".join(secrets.choice(chars) for _ in range(length)) def pick_domain(api_key=None): domains = get_domains(api_key=api_key) if not domains: raise Exception("DuckMail 娌℃湁杩斿洖浠讳綍鍙敤鍩熷悕") private = [d for d in domains if d.get("ownerId")] verified_private = [d for d in private if d.get("isVerified")] if verified_private: return verified_private[0]["domain"] public = [d for d in domains if d.get("isVerified")] if public: return public[0]["domain"] raise Exception("DuckMail 鏃犲凡楠岃瘉鍩熷悕鍙敤") def get_email_provider(): return config.get("email_provider", "duckmail") def get_email_and_token(api_key=None): provider = get_email_provider() if provider == "yyds": return yyds_get_email_and_token(api_key=api_key, jwt=get_yyds_jwt()) if provider == "cloudflare": api_base = get_cloudflare_api_base() if not api_base: raise Exception("Cloudflare API Base 未配置") try: # cloudflare_temp_email 专用模式 return cloudflare_create_temp_address(api_base) except Exception as primary_exc: # 兜底回退到 Mail.tm 风格 key = api_key or get_cloudflare_api_key() domains = cloudflare_get_domains(api_base, api_key=key) if not domains: raise Exception(f"Cloudflare 创建邮箱失败: {primary_exc}") verified = [d for d in domains if d.get("isVerified")] target = verified[0] if verified else domains[0] domain = target.get("domain") if not domain: raise Exception("Cloudflare 域名数据格式错误,缺少 domain 字段") username = generate_username(10) address = f"{username}@{domain}" password = secrets.token_urlsafe(12) cloudflare_create_account( api_base, address, password, api_key=key, expires_in=0 ) token = cloudflare_get_token(api_base, address, password, api_key=key) if not token: raise Exception("获取 Cloudflare 邮箱 token 失败") return address, token key = api_key or get_duckmail_api_key() domain = pick_domain(api_key=key) username = generate_username(10) address = f"{username}@{domain}" password = secrets.token_urlsafe(12) create_account(address, password, api_key=key, expires_in=0) token = get_token(address, password) if not token: raise Exception("鑾峰彇 DuckMail token 澶辫触") return address, token def get_oai_code( dev_token, email, timeout=180, poll_interval=3, log_callback=None, cancel_callback=None, resend_callback=None, ): provider = get_email_provider() if provider == "yyds": return yyds_get_oai_code( dev_token, email, timeout=timeout, poll_interval=poll_interval, log_callback=log_callback, jwt=get_yyds_jwt(), cancel_callback=cancel_callback, ) if provider == "cloudflare": return cloudflare_get_oai_code( dev_token, email, timeout=timeout, poll_interval=poll_interval, log_callback=log_callback, cancel_callback=cancel_callback, resend_callback=resend_callback, ) return duckmail_get_oai_code( dev_token, email, timeout=timeout, poll_interval=poll_interval, log_callback=log_callback, cancel_callback=cancel_callback, ) def extract_verification_code(text, subject=""): if subject: match = re.search(r"^([A-Z0-9]{3}-[A-Z0-9]{3})\s+xAI", subject, re.IGNORECASE) if match: return match.group(1) match = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", text, re.IGNORECASE) if match: return match.group(1) patterns = [ r"verification\s+code[:\s]+(\d{4,8})", r"your\s+code[:\s]+(\d{4,8})", r"confirm(?:ation)?\s+code[:\s]+(\d{4,8})", ] for pattern in patterns: match = re.search(pattern, text, re.IGNORECASE) if match: return match.group(1) return None def duckmail_get_oai_code( dev_token, email, timeout=180, poll_interval=3, log_callback=None, cancel_callback=None, ): deadline = time.time() + timeout seen_ids = set() while time.time() < deadline: raise_if_cancelled(cancel_callback) try: messages = get_messages(dev_token) except Exception as exc: if log_callback: log_callback(f"[Debug] 鎷夊彇閭欢鍒楄〃澶辫触: {exc}") sleep_with_cancel(poll_interval, cancel_callback) continue for msg in messages: msg_id = msg.get("id") or msg.get("msgid") if not msg_id or msg_id in seen_ids: continue seen_ids.add(msg_id) recipients = [t.get("address", "").lower() for t in (msg.get("to") or [])] if email.lower() not in recipients: continue try: detail = get_message_detail(dev_token, msg_id) except Exception as exc: if log_callback: log_callback(f"[Debug] 鑾峰彇閭欢璇︽儏澶辫触: {exc}") continue parts = [] text_body = detail.get("text") or "" if text_body: parts.append(text_body) html_list = detail.get("html") or [] for h in html_list: parts.append(re.sub(r"<[^>]+>", " ", h)) combined = "\n".join(parts) subject = detail.get("subject", "") if log_callback: log_callback(f"[Debug] 鏀跺埌閭欢: {subject}") code = extract_verification_code(combined, subject) if code: if log_callback: log_callback(f"[*] 浠庨偖浠朵腑鎻愬彇鍒伴獙璇佺爜: {code}") return code sleep_with_cancel(poll_interval, cancel_callback) raise Exception(f"在 {timeout}s 内未收到验证码邮件") def cloudflare_get_oai_code( dev_token, email, timeout=180, poll_interval=3, log_callback=None, cancel_callback=None, resend_callback=None, ): api_base = get_cloudflare_api_base() if not api_base: raise Exception("Cloudflare API Base 未配置") deadline = time.time() + timeout # 同一封邮件正文可能延迟可读,允许多次重试解析,避免偶发漏码 seen_attempts = {} next_resend_at = time.time() + 35 while time.time() < deadline: raise_if_cancelled(cancel_callback) if resend_callback and time.time() >= next_resend_at: try: resend_callback() if log_callback: log_callback("[*] 已触发重新发送验证码") except Exception as exc: if log_callback: log_callback(f"[Debug] 触发重发验证码失败: {exc}") next_resend_at = time.time() + 35 try: messages = cloudflare_get_messages(api_base, dev_token) except Exception as exc: if log_callback: log_callback(f"[Debug] Cloudflare 拉取邮件列表失败: {exc}") sleep_with_cancel(poll_interval, cancel_callback) continue if log_callback: log_callback(f"[Debug] Cloudflare 本轮邮件数量: {len(messages)}") for msg in messages: msg_id = msg.get("id") or msg.get("msgid") if not msg_id: continue attempt = int(seen_attempts.get(msg_id, 0)) if attempt >= 5: continue seen_attempts[msg_id] = attempt + 1 recipients = [t.get("address", "").lower() for t in (msg.get("to") or [])] msg_addr = str(msg.get("address", "")).lower() # 优先匹配目标邮箱;若结构不一致也允许继续解析,避免接口字段漂移导致漏码 address_matched = True if recipients: address_matched = email.lower() in recipients elif msg_addr: address_matched = msg_addr == email.lower() if not address_matched and log_callback: log_callback(f"[Debug] 跳过疑似非目标邮件 id={msg_id} address={msg_addr} to={recipients}") continue parts = [] # 先直接从列表项取内容,避免 detail 接口差异导致漏码 for field in ("text", "raw", "content", "intro", "body", "snippet"): value = msg.get(field) if isinstance(value, str) and value.strip(): parts.append(value) html_list = msg.get("html") or [] if isinstance(html_list, str): html_list = [html_list] for h in html_list: parts.append(re.sub(r"<[^>]+>", " ", h)) subject = str(msg.get("subject", "") or "") combined = "\n".join(parts) # 再尝试 detail 接口补全内容 try: detail = cloudflare_get_message_detail(api_base, dev_token, msg_id) for field in ("text", "raw", "content", "intro", "body", "snippet"): value = detail.get(field) if isinstance(value, str) and value.strip(): combined += "\n" + value html_list2 = detail.get("html") or [] if isinstance(html_list2, str): html_list2 = [html_list2] for h in html_list2: combined += "\n" + re.sub(r"<[^>]+>", " ", h) if not subject: subject = str(detail.get("subject", "") or "") except Exception as exc: if log_callback: log_callback(f"[Debug] Cloudflare detail接口失败,改用列表内容解析: {exc}") if log_callback: log_callback(f"[Debug] Cloudflare 收到邮件: {subject}") code = extract_verification_code(combined, subject) if code: if log_callback: log_callback(f"[*] Cloudflare 从邮件中提取到验证码: {code}") return code elif log_callback: log_callback(f"[Debug] 邮件已解析但未提取到验证码 id={msg_id} attempt={seen_attempts[msg_id]}") sleep_with_cancel(poll_interval, cancel_callback) raise Exception(f"Cloudflare 在 {timeout}s 内未收到验证码邮件") 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)}" SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com" browser = None page = None def setup_light_theme(root): try: root.option_add("*Background", UI_BG) root.option_add("*Foreground", UI_FG) root.option_add("*selectBackground", UI_ACTIVE_BG) root.option_add("*selectForeground", UI_FG) root.option_add("*insertBackground", UI_FG) root.option_add("*Entry.Background", UI_ENTRY_BG) root.option_add("*Text.Background", UI_ENTRY_BG) root.option_add("*Menu.Background", UI_ENTRY_BG) root.option_add("*Menu.Foreground", UI_FG) style = ttk.Style(root) available = set(style.theme_names()) if "clam" in available: style.theme_use("clam") elif "default" in available: style.theme_use("default") root.configure(bg=UI_BG) style.configure(".", background=UI_BG, foreground=UI_FG, fieldbackground=UI_ENTRY_BG) style.configure("TFrame", background=UI_BG) style.configure("TLabelframe", background=UI_BG, foreground=UI_FG) style.configure("TLabelframe.Label", background=UI_BG, foreground=UI_FG) style.configure("TLabel", background=UI_BG, foreground=UI_FG) style.configure("TCheckbutton", background=UI_BG, foreground=UI_FG) style.configure("TButton", background=UI_BUTTON_BG, foreground=UI_FG) style.configure("TEntry", fieldbackground=UI_ENTRY_BG, foreground=UI_FG) style.configure("TCombobox", fieldbackground=UI_ENTRY_BG, foreground=UI_FG) style.configure("TSpinbox", fieldbackground=UI_ENTRY_BG, foreground=UI_FG) except Exception: pass def tk_label(parent, text="", **kwargs): return tk.Label(parent, text=text, bg=kwargs.pop("bg", UI_BG), fg=kwargs.pop("fg", UI_FG), **kwargs) def tk_entry(parent, textvariable=None, width=30, **kwargs): return tk.Entry( parent, textvariable=textvariable, width=width, bg=UI_ENTRY_BG, fg=UI_FG, insertbackground=UI_FG, disabledbackground="#2f2f2f", disabledforeground=UI_MUTED_FG, highlightthickness=1, highlightbackground="#555555", relief=tk.SOLID, **kwargs, ) def tk_button(parent, text="", command=None, state=tk.NORMAL, **kwargs): return tk.Button( parent, text=text, command=command, state=state, bg=UI_BUTTON_BG, fg=UI_FG, activebackground=UI_ACTIVE_BG, activeforeground=UI_FG, disabledforeground="#777777", relief=tk.RAISED, padx=10, pady=3, **kwargs, ) def tk_checkbutton(parent, text="", variable=None, **kwargs): return tk.Checkbutton( parent, text=text, variable=variable, bg=UI_BG, fg=UI_FG, activebackground=UI_BG, activeforeground=UI_FG, selectcolor="#3d7be0", **kwargs, ) def tk_option_menu(parent, variable, values, width=12): menu = tk.OptionMenu(parent, variable, *values) menu.configure( width=width, bg=UI_ENTRY_BG, fg=UI_FG, activebackground=UI_ACTIVE_BG, activeforeground=UI_FG, highlightthickness=1, highlightbackground="#555555", relief=tk.SOLID, ) menu["menu"].configure(bg=UI_ENTRY_BG, fg=UI_FG, activebackground=UI_ACTIVE_BG, activeforeground=UI_FG) return menu def start_browser(log_callback=None): global browser, page last_exc = None for attempt in range(1, 5): try: browser = Chromium(create_browser_options()) 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 attempt > 1: log_callback(f"[*] 浏览器第 {attempt} 次启动成功") return browser, page except Exception as exc: last_exc = exc if log_callback: log_callback(f"[Debug] 浏览器启动失败(第{attempt}/4次): {exc}") try: if browser is not None: browser.quit(del_data=True) except Exception: pass browser = None page = None time.sleep(min(1.5 * attempt, 4)) raise Exception(f"浏览器启动失败,已重试4次: {last_exc}") def stop_browser(): global browser, page if browser is not None: try: browser.quit(del_data=True) except Exception: pass browser = None page = None def restart_browser(log_callback=None): stop_browser() return start_browser(log_callback=log_callback) def cleanup_runtime_memory(log_callback=None, reason="定期清理"): if log_callback: log_callback(f"[*] {reason}: 关闭浏览器并清理内存") stop_browser() 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() if log_callback: log_callback("[*] 浏览器已启动") try: page = browser.get_tab(0) page.get(SIGNUP_URL) except Exception as e: if log_callback: log_callback(f"[Debug] 打开URL异常: {e}") try: page = browser.new_tab(SIGNUP_URL) except Exception as e2: if log_callback: log_callback(f"[Debug] 创建新标签页异常: {e2}") restart_browser() page = browser.new_tab(SIGNUP_URL) page.wait.doc_loaded() 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 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: pass sleep_with_cancel(1, cancel_callback) raise Exception( f"等待超时:未获取到 sso cookie。已看到 cookies: {sorted(last_seen_names)}" ) class GrokRegisterGUI: def __init__(self, root): self.root = root self.root.title("Grok 注册机") self.root.geometry("1120x900") self.root.minsize(960, 700) self.is_running = False self.batch_count = 0 self.success_count = 0 self.fail_count = 0 self.results = [] self.stop_requested = False self.ui_queue = queue.Queue() self.accounts_output_file = "" self.setup_ui() def setup_ui(self): load_config() main_frame = tk.Frame(self.root, bg=UI_BG, padx=10, pady=10) main_frame.pack(fill=tk.BOTH, expand=True) main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_rowconfigure(3, weight=1) config_frame = tk.LabelFrame( main_frame, text="配置", bg=UI_PANEL_BG, fg=UI_FG, padx=10, pady=10, relief=tk.GROOVE, borderwidth=1, ) config_frame.grid(row=0, column=0, sticky=tk.EW, pady=(0, 8)) config_frame.grid_columnconfigure(1, weight=1, minsize=260) config_frame.grid_columnconfigure(3, weight=1, minsize=260) def add_label(row, column, text): tk_label(config_frame, text=text, bg=UI_PANEL_BG).grid( row=row, column=column, sticky=tk.W, padx=(0, 6), pady=3, ) def add_field(widget, row, column, columnspan=1, sticky=tk.EW): widget.grid( row=row, column=column, columnspan=columnspan, sticky=sticky, padx=(0, 14), pady=3, ) add_label(0, 0, "邮箱服务商:") self.email_provider_var = tk.StringVar(value=config.get("email_provider", "duckmail")) self.email_provider_combo = tk_option_menu(config_frame, self.email_provider_var, ["duckmail", "yyds", "cloudflare"], width=12) add_field(self.email_provider_combo, 0, 1, sticky=tk.W) add_label(0, 2, "注册数量:") self.count_var = tk.StringVar(value=str(config.get("register_count", 1))) self.count_spinbox = tk.Spinbox( config_frame, from_=1, to=2500, width=8, textvariable=self.count_var, bg=UI_ENTRY_BG, fg=UI_FG, insertbackground=UI_FG, buttonbackground=UI_BUTTON_BG, disabledbackground="#2f2f2f", disabledforeground=UI_MUTED_FG, relief=tk.SOLID, ) add_field(self.count_spinbox, 0, 3, sticky=tk.W) add_label(1, 0, "注册选项:") self.nsfw_var = tk.BooleanVar(value=config.get("enable_nsfw", True)) self.nsfw_check = tk_checkbutton(config_frame, text="注册后开启 NSFW", variable=self.nsfw_var) add_field(self.nsfw_check, 1, 1, sticky=tk.W) add_label(1, 2, "代理(可选):") self.proxy_var = tk.StringVar(value=config.get("proxy", "")) self.proxy_entry = tk_entry(config_frame, textvariable=self.proxy_var, width=34) add_field(self.proxy_entry, 1, 3) add_label(2, 0, "DuckMail API Key:") self.api_key_var = tk.StringVar(value=config.get("duckmail_api_key", "")) self.api_key_entry = tk_entry(config_frame, textvariable=self.api_key_var, width=34) add_field(self.api_key_entry, 2, 1) add_label(2, 2, "Cloudflare 鉴权模式:") self.cloudflare_auth_mode_var = tk.StringVar(value=config.get("cloudflare_auth_mode", "bearer")) self.cloudflare_auth_mode_combo = tk_option_menu( config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "x-admin-auth", "none"], width=12 ) add_field(self.cloudflare_auth_mode_combo, 2, 3, sticky=tk.W) add_label(3, 0, "Cloudflare API Base:") self.cloudflare_api_base_var = tk.StringVar(value=config.get("cloudflare_api_base", "")) self.cloudflare_api_base_entry = tk_entry(config_frame, textvariable=self.cloudflare_api_base_var, width=72) add_field(self.cloudflare_api_base_entry, 3, 1, columnspan=3) add_label(4, 0, "Cloudflare API Key:") self.cloudflare_api_key_var = tk.StringVar(value=config.get("cloudflare_api_key", "")) self.cloudflare_api_key_entry = tk_entry(config_frame, textvariable=self.cloudflare_api_key_var, width=34) add_field(self.cloudflare_api_key_entry, 4, 1) add_label(4, 2, "CF 路径:") self.cloudflare_paths_var = tk.StringVar( value=",".join( [ config.get("cloudflare_path_domains", "/domains"), config.get("cloudflare_path_accounts", "/accounts"), config.get("cloudflare_path_token", "/token"), config.get("cloudflare_path_messages", "/messages"), ] ) ) self.cloudflare_paths_entry = tk_entry(config_frame, textvariable=self.cloudflare_paths_var, width=34) add_field(self.cloudflare_paths_entry, 4, 3) add_label(5, 0, "grok2api 本地入池:") self.grok2api_local_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_local", True))) self.grok2api_local_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_local_auto_var) add_field(self.grok2api_local_auto_check, 5, 1, sticky=tk.W) add_label(5, 2, "grok2api 池名:") self.grok2api_pool_name_var = tk.StringVar(value=str(config.get("grok2api_pool_name", "ssoBasic"))) self.grok2api_pool_name_combo = tk_option_menu( config_frame, self.grok2api_pool_name_var, ["ssoBasic", "ssoSuper"], width=12 ) add_field(self.grok2api_pool_name_combo, 5, 3, sticky=tk.W) add_label(6, 0, "本地 token.json:") self.grok2api_local_file_var = tk.StringVar(value=str(config.get("grok2api_local_token_file", ""))) self.grok2api_local_file_entry = tk_entry(config_frame, textvariable=self.grok2api_local_file_var, width=72) add_field(self.grok2api_local_file_entry, 6, 1, columnspan=3) add_label(7, 0, "grok2api 远端入池:") self.grok2api_remote_auto_var = tk.BooleanVar(value=bool(config.get("grok2api_auto_add_remote", False))) self.grok2api_remote_auto_check = tk_checkbutton(config_frame, variable=self.grok2api_remote_auto_var) add_field(self.grok2api_remote_auto_check, 7, 1, sticky=tk.W) add_label(8, 0, "grok2api 远端 Base:") self.grok2api_remote_base_var = tk.StringVar(value=str(config.get("grok2api_remote_base", ""))) self.grok2api_remote_base_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_base_var, width=72) add_field(self.grok2api_remote_base_entry, 8, 1, columnspan=3) add_label(9, 0, "grok2api 远端 app_key:") self.grok2api_remote_key_var = tk.StringVar(value=str(config.get("grok2api_remote_app_key", ""))) self.grok2api_remote_key_entry = tk_entry(config_frame, textvariable=self.grok2api_remote_key_var, width=72) add_field(self.grok2api_remote_key_entry, 9, 1, columnspan=3) btn_frame = tk.Frame(main_frame, bg=UI_BG) btn_frame.grid(row=1, column=0, sticky=tk.EW, pady=(0, 6)) self.start_btn = tk_button(btn_frame, text="开始注册", command=self.start_registration) self.start_btn.pack(side=tk.LEFT, padx=5) self.stop_btn = tk_button(btn_frame, text="停止", command=self.stop_registration, state=tk.DISABLED) self.stop_btn.pack(side=tk.LEFT, padx=5) self.clear_btn = tk_button(btn_frame, text="清空日志", command=self.clear_log) self.clear_btn.pack(side=tk.LEFT, padx=5) status_frame = tk.Frame(main_frame, bg=UI_BG) status_frame.grid(row=2, column=0, sticky=tk.EW, pady=(0, 6)) self.status_var = tk.StringVar(value="就绪") tk_label(status_frame, text="状态: ").pack(side=tk.LEFT) self.status_label = tk.Label(status_frame, textvariable=self.status_var, bg=UI_BG, fg="green") self.status_label.pack(side=tk.LEFT) self.stats_var = tk.StringVar(value="成功: 0 | 失败: 0") tk.Label(status_frame, textvariable=self.stats_var, bg=UI_BG, fg=UI_FG).pack(side=tk.RIGHT) log_frame = tk.LabelFrame( main_frame, text="日志", bg=UI_PANEL_BG, fg=UI_FG, padx=5, pady=5, relief=tk.GROOVE, borderwidth=1, ) log_frame.grid(row=3, column=0, sticky=tk.NSEW) log_frame.grid_columnconfigure(0, weight=1) log_frame.grid_rowconfigure(0, weight=1) self.log_text = scrolledtext.ScrolledText( log_frame, height=18, width=60, bg="#111111", fg="#f5f5f5", insertbackground="#f5f5f5", selectbackground="#345a8a", selectforeground="#ffffff", relief=tk.SOLID, borderwidth=1, highlightthickness=1, highlightbackground="#555555", ) self.log_text.grid(row=0, column=0, sticky=tk.NSEW) self.log("[*] GUI 已就绪,配置已加载") self.log(f"[*] 当前邮箱服务商: {self.email_provider_var.get()} | 注册数量: {self.count_var.get()}") def log(self, message): timestamp = datetime.datetime.now().strftime("%H:%M:%S") line = f"[{timestamp}] {message}" print(line, flush=True) self.log_text.insert(tk.END, f"{line}\n") self.log_text.see(tk.END) def clear_log(self): self.log_text.delete(1.0, tk.END) def update_stats(self): self.stats_var.set(f"成功: {self.success_count} | 失败: {self.fail_count}") def _set_running_ui(self, running): self.is_running = running self.start_btn.config(state=tk.DISABLED if running else tk.NORMAL) self.stop_btn.config(state=tk.NORMAL if running else tk.DISABLED) self.status_var.set("运行中..." if running else "就绪") self.status_label.config(foreground="blue" if running else "green") def should_stop(self): return self.stop_requested or not self.is_running def start_registration(self): if self.is_running: self.log("[!] 当前已有任务在运行") return config["email_provider"] = self.email_provider_var.get().strip() or "duckmail" config["enable_nsfw"] = bool(self.nsfw_var.get()) config["proxy"] = self.proxy_var.get().strip() config["duckmail_api_key"] = self.api_key_var.get().strip() config["cloudflare_api_base"] = self.cloudflare_api_base_var.get().strip() config["cloudflare_api_key"] = self.cloudflare_api_key_var.get().strip() config["cloudflare_auth_mode"] = self.cloudflare_auth_mode_var.get().strip() or "bearer" config["grok2api_auto_add_local"] = bool(self.grok2api_local_auto_var.get()) config["grok2api_local_token_file"] = self.grok2api_local_file_var.get().strip() config["grok2api_pool_name"] = self.grok2api_pool_name_var.get().strip() or "ssoBasic" config["grok2api_auto_add_remote"] = bool(self.grok2api_remote_auto_var.get()) config["grok2api_remote_base"] = self.grok2api_remote_base_var.get().strip() config["grok2api_remote_app_key"] = self.grok2api_remote_key_var.get().strip() raw_paths = [x.strip() for x in self.cloudflare_paths_var.get().split(",") if x.strip()] if len(raw_paths) >= 4: config["cloudflare_path_domains"] = raw_paths[0] if raw_paths[0].startswith("/") else ("/" + raw_paths[0]) config["cloudflare_path_accounts"] = raw_paths[1] if raw_paths[1].startswith("/") else ("/" + raw_paths[1]) config["cloudflare_path_token"] = raw_paths[2] if raw_paths[2].startswith("/") else ("/" + raw_paths[2]) config["cloudflare_path_messages"] = raw_paths[3] if raw_paths[3].startswith("/") else ("/" + raw_paths[3]) save_config() if config["email_provider"] == "cloudflare" and not config["cloudflare_api_base"]: self.log("[!] Cloudflare 模式需要先填写 Cloudflare API Base") return try: count = int(self.count_var.get()) except Exception: self.log("[!] 注册数量无效") return config["register_count"] = count save_config() self.stop_requested = False self.success_count = 0 self.fail_count = 0 self.results = [] now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") self.accounts_output_file = os.path.join( os.path.dirname(__file__), f"accounts_{now}.txt" ) self.update_stats() self._set_running_ui(True) self.log(f"[*] 配置已保存,开始执行。目标数量: {count}") self.log(f"[*] 成功账号将实时保存到: {self.accounts_output_file}") threading.Thread( target=self.run_registration, args=(count,), daemon=True, ).start() def stop_registration(self): self.stop_requested = True self.log("[!] 用户停止注册") def run_registration(self, count): try: start_browser(log_callback=self.log) self.log("[*] 浏览器已启动") i = 0 retry_count_for_slot = 0 max_slot_retry = 3 while i < count: if self.should_stop(): break self.log(f"--- 开始第 {i + 1}/{count} 个账号 ---") try: email = "" dev_token = "" code = "" mail_ok = False max_mail_retry = 3 for mail_try in range(1, max_mail_retry + 1): self.log(f"[*] 1. 打开注册页 (尝试 {mail_try}/{max_mail_retry})") open_signup_page( log_callback=self.log, cancel_callback=self.should_stop ) self.log("[*] 2. 创建邮箱并提交") email, dev_token = fill_email_and_submit( log_callback=self.log, cancel_callback=self.should_stop ) self.log(f"[*] 邮箱: {email}") self.log(f"[Debug] 邮箱credential(jwt): {dev_token}") try: with open( os.path.join(os.path.dirname(__file__), "mail_credentials.txt"), "a", encoding="utf-8", ) as f: f.write(f"{email}\t{dev_token}\n") except Exception: pass self.log("[*] 3. 拉取验证码") try: code = fill_code_and_submit( email, dev_token, log_callback=self.log, cancel_callback=self.should_stop, ) mail_ok = True break except Exception as mail_exc: msg = str(mail_exc) if ("未收到验证码" in msg or "验证码" in msg) and mail_try < max_mail_retry: self.log(f"[!] 本邮箱未取到验证码,自动更换新邮箱重试: {msg}") restart_browser(log_callback=self.log) sleep_with_cancel(1, self.should_stop) continue raise if not mail_ok: raise Exception("验证码阶段失败,已达到最大重试次数") self.log(f"[*] 验证码: {code}") self.log("[*] 4. 填写资料") profile = fill_profile_and_submit( log_callback=self.log, cancel_callback=self.should_stop ) self.log(f"[*] 资料已填: {profile.get('given_name')} {profile.get('family_name')}") self.log("[*] 5. 等待 sso cookie") sso = wait_for_sso_cookie( log_callback=self.log, cancel_callback=self.should_stop ) if config.get("enable_nsfw", True): self.log("[*] 6. 开启 NSFW") nsfw_ok, nsfw_msg = enable_nsfw_for_token( sso, log_callback=self.log ) if nsfw_ok: self.log(f"[+] NSFW 开启成功: {nsfw_msg}") else: self.log(f"[!] NSFW 未开启,继续保存账号: {nsfw_msg}") self.results.append({"email": email, "sso": sso, "profile": profile}) try: line = f"{email}----{profile.get('password','')}----{sso}\n" with open(self.accounts_output_file, "a", encoding="utf-8") as f: f.write(line) except Exception as file_exc: self.log(f"[Debug] 保存账号文件失败: {file_exc}") add_token_to_grok2api_pools(sso, email=email, log_callback=self.log) self.success_count += 1 retry_count_for_slot = 0 i += 1 self.log(f"[+] 注册成功: {email}") if ( self.success_count > 0 and self.success_count % MEMORY_CLEANUP_INTERVAL == 0 and i < count ): cleanup_runtime_memory( log_callback=self.log, reason=f"已成功 {self.success_count} 个账号,执行定期清理", ) except RegistrationCancelled: self.log("[!] 注册被用户停止") break except AccountRetryNeeded as exc: retry_count_for_slot += 1 if retry_count_for_slot <= max_slot_retry: self.log( f"[!] 当前账号流程卡住,重试第 {retry_count_for_slot}/{max_slot_retry} 次: {exc}" ) else: self.fail_count += 1 self.log( f"[-] 当前账号已达到最大重试次数,跳过: {exc}" ) retry_count_for_slot = 0 i += 1 except Exception as exc: self.fail_count += 1 retry_count_for_slot = 0 i += 1 self.log(f"[-] 注册失败: {exc}") finally: self.update_stats() if self.should_stop(): break if browser is None: start_browser(log_callback=self.log) else: restart_browser(log_callback=self.log) sleep_with_cancel(1, self.should_stop) except Exception as exc: self.log(f"[!] 任务异常: {exc}") finally: stop_browser() self._set_running_ui(False) self.log("[*] 任务结束") class CliStopController: def __init__(self): self.stop_requested = False def should_stop(self): return self.stop_requested def stop(self): self.stop_requested = True def cli_log(message): timestamp = datetime.datetime.now().strftime("%H:%M:%S") print(f"[{timestamp}] {message}", flush=True) def run_registration_cli(count): controller = CliStopController() success_count = 0 fail_count = 0 retry_count_for_slot = 0 max_slot_retry = 3 accounts_output_file = os.path.join( os.path.dirname(__file__), f"accounts_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", ) cli_log(f"[*] 终端模式启动,目标数量: {count}") cli_log(f"[*] 成功账号将实时保存到: {accounts_output_file}") try: start_browser(log_callback=cli_log) cli_log("[*] 浏览器已启动") i = 0 while i < count: if controller.should_stop(): break cli_log(f"--- 开始第 {i + 1}/{count} 个账号 ---") try: email = "" dev_token = "" code = "" mail_ok = False max_mail_retry = 3 for mail_try in range(1, max_mail_retry + 1): cli_log(f"[*] 1. 打开注册页 (尝试 {mail_try}/{max_mail_retry})") open_signup_page( log_callback=cli_log, cancel_callback=controller.should_stop ) cli_log("[*] 2. 创建邮箱并提交") email, dev_token = fill_email_and_submit( log_callback=cli_log, cancel_callback=controller.should_stop ) cli_log(f"[*] 邮箱: {email}") cli_log(f"[Debug] 邮箱credential(jwt): {dev_token}") try: with open( os.path.join(os.path.dirname(__file__), "mail_credentials.txt"), "a", encoding="utf-8", ) as f: f.write(f"{email}\t{dev_token}\n") except Exception: pass cli_log("[*] 3. 拉取验证码") try: code = fill_code_and_submit( email, dev_token, log_callback=cli_log, cancel_callback=controller.should_stop, ) mail_ok = True break except Exception as mail_exc: msg = str(mail_exc) if ("未收到验证码" in msg or "验证码" in msg) and mail_try < max_mail_retry: cli_log(f"[!] 本邮箱未取到验证码,自动更换新邮箱重试: {msg}") restart_browser(log_callback=cli_log) sleep_with_cancel(1, controller.should_stop) continue raise if not mail_ok: raise Exception("验证码阶段失败,已达到最大重试次数") cli_log(f"[*] 验证码: {code}") cli_log("[*] 4. 填写资料") profile = fill_profile_and_submit( log_callback=cli_log, cancel_callback=controller.should_stop ) cli_log(f"[*] 资料已填: {profile.get('given_name')} {profile.get('family_name')}") cli_log("[*] 5. 等待 sso cookie") sso = wait_for_sso_cookie( log_callback=cli_log, cancel_callback=controller.should_stop ) if config.get("enable_nsfw", True): cli_log("[*] 6. 开启 NSFW") nsfw_ok, nsfw_msg = enable_nsfw_for_token( sso, log_callback=cli_log ) if nsfw_ok: cli_log(f"[+] NSFW 开启成功: {nsfw_msg}") else: cli_log(f"[!] NSFW 未开启,继续保存账号: {nsfw_msg}") try: line = f"{email}----{profile.get('password','')}----{sso}\n" with open(accounts_output_file, "a", encoding="utf-8") as f: f.write(line) except Exception as file_exc: cli_log(f"[Debug] 保存账号文件失败: {file_exc}") add_token_to_grok2api_pools(sso, email=email, log_callback=cli_log) success_count += 1 retry_count_for_slot = 0 i += 1 cli_log(f"[+] 注册成功: {email}") cli_log(f"[*] 当前统计: 成功 {success_count} | 失败 {fail_count}") if success_count > 0 and success_count % MEMORY_CLEANUP_INTERVAL == 0 and i < count: cleanup_runtime_memory( log_callback=cli_log, reason=f"已成功 {success_count} 个账号,执行定期清理", ) except RegistrationCancelled: cli_log("[!] 注册被停止") break except AccountRetryNeeded as exc: retry_count_for_slot += 1 if retry_count_for_slot <= max_slot_retry: cli_log( f"[!] 当前账号流程卡住,重试第 {retry_count_for_slot}/{max_slot_retry} 次: {exc}" ) else: fail_count += 1 retry_count_for_slot = 0 i += 1 cli_log(f"[-] 当前账号已达到最大重试次数,跳过: {exc}") except Exception as exc: fail_count += 1 retry_count_for_slot = 0 i += 1 cli_log(f"[-] 注册失败: {exc}") finally: if controller.should_stop(): break if browser is None: start_browser(log_callback=cli_log) else: restart_browser(log_callback=cli_log) sleep_with_cancel(1, controller.should_stop) except KeyboardInterrupt: controller.stop() cli_log("[!] 收到 Ctrl+C,正在停止并清理") except Exception as exc: cli_log(f"[!] 任务异常: {exc}") finally: cleanup_runtime_memory(log_callback=cli_log, reason="任务结束") cli_log(f"[*] 任务结束。成功 {success_count} | 失败 {fail_count}") def main_cli(): load_config() count = int(config.get("register_count", 1) or 1) cli_log("[*] CLI 已加载配置") cli_log(f"[*] 当前邮箱服务商: {config.get('email_provider', 'duckmail')} | 注册数量: {count}") cli_log("[*] 输入 start 后开始;按 Ctrl+C 可强制停止") try: command = input("> ").strip().lower() except KeyboardInterrupt: cli_log("[!] 已取消") return if command != "start": cli_log("[!] 未输入 start,已退出") return run_registration_cli(count) def main(): if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"): main_cli() return root = tk.Tk() setup_light_theme(root) app = GrokRegisterGUI(root) root.mainloop() if __name__ == "__main__": main()