From 48fd6925f83f6f71454aa49a5ba7f7cbd17d6c53 Mon Sep 17 00:00:00 2001 From: AaronL725 Date: Mon, 11 May 2026 02:40:03 +0800 Subject: [PATCH] Initial project setup --- .gitattributes | 4 + .gitignore | 23 + README.md | 151 +++ cf_mail_debug.py | 175 +++ config.example.json | 24 + grok_register_ttk.py | 2992 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 18 + 7 files changed, 3387 insertions(+) create mode 100644 .gitattributes create mode 100755 .gitignore create mode 100644 README.md create mode 100755 cf_mail_debug.py create mode 100755 config.example.json create mode 100755 grok_register_ttk.py create mode 100644 requirements.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0c5bb01 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.py text eol=lf +*.md text eol=lf +*.json text eol=lf +*.txt text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..91efff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +.venv/ +venv/ +env/ + +accounts_*.txt +mail_credentials.txt +*.txt +!requirements.txt + +config.json +config.local.json + +*.log +.DS_Store +.idea/ +.vscode/ +DrissionPage/ +autoPortData/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfba147 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# Grok Register + +Grok Register 是一个 Python 自动化注册工具,支持 GUI 和 CLI 两种运行方式。项目可接入临时邮箱服务完成注册流程,并将成功账号保存到本地文件或写入 [grok2api](https://github.com/chenyme/grok2api) 令牌池。 + +> 本项目仅用于自动化流程研究、测试环境验证和个人学习。请遵守目标网站服务条款、当地法律法规和第三方服务限制。 + +## 功能 + +- 支持 GUI 图形界面运行。 +- 支持 CLI 终端运行,不启动 Tk GUI。 +- 注册流程使用 Chromium/Chrome 浏览器页面完成。 +- 支持 DuckMail、YYDS、Cloudflare 临时邮箱接口。 +- 支持验证码邮件轮询和解析。 +- 支持成功账号实时写入 `accounts_*.txt`。 +- 支持将 SSO token 写入 grok2api 本地或远端池。 +- 支持注册后尝试开启 NSFW。 +- 支持页面卡住检测、当前账号重试、浏览器重启和内存清理。 + +## 环境要求 + +- Python 3.9+ +- Google Chrome 或 Chromium +- 可访问注册页面和临时邮箱 API 的网络环境 + +## 安装 + +下载项目到电脑: + +```bash +git clone https://github.com/AaronL725/grok-register.git +cd grok-register +``` + +安装依赖: + +```bash +pip install -r requirements.txt +``` + +复制配置文件: + +```bash +cp config.example.json config.json +``` + +然后按需编辑 `config.json`。 + +## 配置 + +常用配置项: + +| 配置项 | 说明 | +| --- | --- | +| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare` | +| `register_count` | 本次目标注册数量 | +| `proxy` | 代理地址,可留空 | +| `enable_nsfw` | 注册后是否尝试开启 NSFW | +| `cloudflare_api_base` | Cloudflare 临时邮箱 API 地址 | +| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none`、`bearer`、`x-api-key`、`query-key` | +| `defaultDomains` | Cloudflare 临时邮箱默认域名 | +| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 | +| `grok2api_local_token_file` | 本地 grok2api token 文件路径 | +| `grok2api_auto_add_remote` | 是否写入远端 grok2api | +| `grok2api_remote_base` | 远端 grok2api 管理 API 地址 | +| `grok2api_remote_app_key` | 远端 grok2api app key | + +`config.json` 包含个人配置和密钥,不要提交到 Git。 + +## 运行 + +### CLI 模式 + +CLI 模式不会启动 Tk GUI,但注册流程仍会打开 Chromium/Chrome 浏览器页面。 + +```bash +python grok_register_ttk.py cli +``` + +看到提示后输入: + +```text +start +``` + +停止任务: + +```text +Ctrl+C +``` + +CLI 模式适合长时间批量运行。程序每成功注册 5 个账号会关闭浏览器、清理运行时对象并重新启动浏览器,降低长任务内存占用。 + +### GUI 模式 + +```bash +python grok_register_ttk.py +``` + +GUI 模式会打开 Tkinter 窗口,适合手动调整配置和观察日志。 + +## 输出文件 + +运行过程中会生成: + +- `accounts_*.txt`:成功账号、密码和 SSO token。 +- `mail_credentials.txt`:临时邮箱凭证。 +- `*.log`:可选日志文件。 + +这些文件包含敏感信息,已被 `.gitignore` 忽略。 + +## 稳定性机制 + +- 每个账号结束后重启浏览器。 +- 每成功 5 个账号执行一次内存清理。 +- CLI 模式支持 `Ctrl+C` 中断并清理浏览器。 +- 最终页长时间无变化时自动重试当前账号。 +- 验证码未收到时自动更换邮箱重试。 + +## 常见问题 + +### CLI 模式为什么还会打开浏览器? + +CLI 模式只是不启动 Tk GUI。注册页、Turnstile、验证码提交和 SSO cookie 获取仍依赖真实浏览器环境。 + +### NSFW 开启失败怎么办? + +如果日志显示 `Cloudflare 防护拦截,HTTP 403`,说明请求被目标站点防护拦截。程序会继续保存账号和写入 grok2api。 + +### GUI 显示的数量和配置不同? + +GUI 数量控件可能有上限。CLI 模式直接读取 `config.json` 中的 `register_count`。 + +## Git 忽略规则 + +以下内容不会提交: + +- `config.json` +- `*.txt` +- `.venv/` +- 日志、缓存和本地 IDE 文件 + +## 目录结构 + +```text +. +├── grok_register_ttk.py # 主程序 +├── cf_mail_debug.py # Cloudflare 邮箱调试工具 +├── config.example.json # 配置示例 +├── requirements.txt # Python 依赖 +└── README.md +``` diff --git a/cf_mail_debug.py b/cf_mail_debug.py new file mode 100755 index 0000000..3ce62e2 --- /dev/null +++ b/cf_mail_debug.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse +import re +import time +from typing import Any, Dict, List, Optional, Tuple + +from curl_cffi import requests + + +def extract_code(text: str, subject: str = "") -> Optional[str]: + if subject: + m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", subject, re.IGNORECASE) + if m: + return m.group(1) + m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", text, re.IGNORECASE) + if m: + return m.group(1) + for p in [ + 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})", + ]: + m = re.search(p, text, re.IGNORECASE) + if m: + return m.group(1) + return None + + +def json_or_text(resp: requests.Response) -> Tuple[Optional[Dict[str, Any]], str]: + try: + data = resp.json() + return data, "" + except Exception: + return None, (resp.text or "")[:400] + + +def create_address(api_base: str) -> Tuple[str, str]: + resp = requests.post( + f"{api_base.rstrip('/')}/api/new_address", + json={}, + headers={"Content-Type": "application/json"}, + timeout=20, + ) + resp.raise_for_status() + data, raw = json_or_text(resp) + if not data: + raise RuntimeError(f"/api/new_address 非JSON: {raw}") + address = str(data.get("address", "")).strip() + jwt = str(data.get("jwt", "")).strip() + if not address or not jwt: + raise RuntimeError(f"/api/new_address 缺少 address/jwt: {data}") + return address, jwt + + +def fetch_box(api_base: str, jwt: str, path: str, params: Dict[str, Any]) -> List[Dict[str, Any]]: + resp = requests.get( + f"{api_base.rstrip('/')}{path}", + params=params, + headers={"Authorization": f"Bearer {jwt}"}, + timeout=20, + ) + if resp.status_code >= 400: + return [] + data, _ = json_or_text(resp) + if not isinstance(data, dict): + return [] + if isinstance(data.get("results"), list): + return data["results"] + if isinstance(data.get("data"), list): + return data["data"] + if isinstance(data.get("messages"), list): + return data["messages"] + return [] + + +def probe_all_boxes(api_base: str, jwt: str) -> List[Tuple[str, List[Dict[str, Any]]]]: + probes = [ + ("/api/mails", {"limit": 20, "offset": 0}), + ("/api/sendbox", {"limit": 20, "offset": 0}), + ("/api/mails", {"limit": 20, "offset": 0, "box": "trash"}), + ("/api/mails", {"limit": 20, "offset": 0, "folder": "trash"}), + ("/api/mails", {"limit": 20, "offset": 0, "deleted": "1"}), + ("/api/mails", {"limit": 20, "offset": 0, "status": "deleted"}), + ] + out: List[Tuple[str, List[Dict[str, Any]]]] = [] + for path, params in probes: + mails = fetch_box(api_base, jwt, path, params) + out.append((f"{path}?{params}", mails)) + return out + + +def get_detail(api_base: str, jwt: str, mail_id: Any) -> Dict[str, Any]: + for url in [ + f"{api_base.rstrip('/')}/api/mail/{mail_id}", + f"{api_base.rstrip('/')}/api/mails/{mail_id}", + ]: + try: + resp = requests.get(url, headers={"Authorization": f"Bearer {jwt}"}, timeout=20) + if resp.status_code >= 400: + continue + data, _ = json_or_text(resp) + if isinstance(data, dict): + return data + except Exception: + continue + return {} + + +def flatten_mail_text(item: Dict[str, Any], detail: Dict[str, Any]) -> Tuple[str, str]: + subject = str(item.get("subject") or detail.get("subject") or "") + parts: List[str] = [] + for src in (item, detail): + for k in ("text", "raw", "content", "intro", "body", "snippet"): + v = src.get(k) + if isinstance(v, str) and v.strip(): + parts.append(v) + html_val = src.get("html") + if isinstance(html_val, str): + html_val = [html_val] + if isinstance(html_val, list): + for h in html_val: + if isinstance(h, str): + parts.append(re.sub(r"<[^>]+>", " ", h)) + return subject, "\n".join(parts) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--api-base", required=True) + ap.add_argument("--address", default="") + ap.add_argument("--credential", default="") + ap.add_argument("--timeout", type=int, default=180) + ap.add_argument("--interval", type=int, default=3) + args = ap.parse_args() + + address = args.address.strip() + credential = args.credential.strip() + if not credential: + address, credential = create_address(args.api_base) + print(f"[NEW] address={address}") + print(f"[NEW] credential(jwt)={credential}") + else: + print(f"[USE] address={address or '(unknown, from credential)'}") + + deadline = time.time() + max(args.timeout, 1) + seen_ids = set() + while time.time() < deadline: + boxes = probe_all_boxes(args.api_base, credential) + total = 0 + for name, mails in boxes: + if mails: + print(f"[BOX] {name} -> {len(mails)}") + total += len(mails) + for m in mails: + mail_id = m.get("id") or m.get("mail_id") + if not mail_id or mail_id in seen_ids: + continue + seen_ids.add(mail_id) + detail = get_detail(args.api_base, credential, mail_id) + subj, text = flatten_mail_text(m, detail) + code = extract_code(text, subj) + print(f"[MAIL] id={mail_id} subject={subj!r} code={code!r}") + if code: + print(f"[FOUND] {code}") + return + if total == 0: + print("[INFO] no mails yet") + time.sleep(max(args.interval, 1)) + print("[TIMEOUT] no code found") + + +if __name__ == "__main__": + main() diff --git a/config.example.json b/config.example.json new file mode 100755 index 0000000..a7ef20d --- /dev/null +++ b/config.example.json @@ -0,0 +1,24 @@ +{ + "duckmail_api_key": "", + "cloudflare_api_base": "https://cloudflare.pages.dev", + "cloudflare_api_key": "", + "cloudflare_auth_mode": "none", + "cloudflare_path_domains": "/api/domains", + "cloudflare_path_accounts": "/api/new_address", + "cloudflare_path_token": "/api/token", + "cloudflare_path_messages": "/api/mails", + "proxy": "", + "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": false, + "grok2api_local_token_file": "", + "grok2api_pool_name": "ssoBasic", + "grok2api_auto_add_remote": false, + "grok2api_remote_base": "", + "grok2api_remote_app_key": "", + "defaultDomains": "example.com", + "email_provider": "cloudflare", + "yyds_api_key": "", + "yyds_jwt": "" +} diff --git a/grok_register_ttk.py b/grok_register_ttk.py new file mode 100755 index 0000000..ba43a2e --- /dev/null +++ b/grok_register_ttk.py @@ -0,0 +1,2992 @@ +#!/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 != "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 _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 v1.8.x: POST /api/new_address -> {address,jwt}""" + global _cf_domain_index + url = f"{api_base}/api/new_address" + payload = {} + try: + # 在多个域名之间轮换,降低单域偶发不收件导致的失败率 + domains = [x.strip() for x in str(config.get("defaultDomains", "") or "").split(",") if x.strip()] + if domains: + payload["domain"] = domains[_cf_domain_index % len(domains)] + _cf_domain_index += 1 + except Exception: + pass + resp = http_post(url, json=payload, headers={"Content-Type": "application/json"}) + 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 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") + # 优先使用 add 接口,避免全量覆盖远端池 + try: + add_payload = {"tokens": [token], "pool": remote_pool, "tags": ["auto-register"]} + resp_add = http_post( + f"{base}/tokens/add", + headers=headers, + params=query, + json=add_payload, + timeout=30, + proxies={}, + ) + resp_add.raise_for_status() + if log_callback: + log_callback(f"[+] 已写入 grok2api 远端池: {pool_name} ({base}/tokens/add)") + return True + except Exception as add_exc: + if log_callback: + log_callback(f"[Debug] /tokens/add 写入失败,尝试 /tokens 全量模式: {add_exc}") + + # 兜底:旧版全量保存接口 + 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=100, + 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", "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() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0a06f5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +certifi==2026.4.22 +cffi==2.0.0 +charset-normalizer==3.4.7 +cssselect==1.3.0 +curl_cffi==0.13.0 +DataRecorder==3.6.2 +DownloadKit==2.0.7 +DrissionPage==4.1.1.2 +filelock==3.19.1 +idna==3.13 +lxml==6.1.0 +psutil==7.2.2 +pycparser==2.23 +requests==2.32.5 +requests-file==3.0.1 +tldextract==5.3.0 +urllib3==2.6.3 +websocket-client==1.9.0