1210 lines
42 KiB
Python
Executable File
1210 lines
42 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Grok 注册机 - TTK GUI 版本
|
|
整合 DrissionPage_example.py, openai_register.py, batch_open_nsfw.py
|
|
"""
|
|
|
|
try:
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, scrolledtext
|
|
TK_AVAILABLE = True
|
|
TK_IMPORT_ERROR = None
|
|
except ImportError as exc:
|
|
tk = None
|
|
ttk = None
|
|
messagebox = None
|
|
scrolledtext = None
|
|
TK_AVAILABLE = False
|
|
TK_IMPORT_ERROR = exc
|
|
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
|
|
import base64
|
|
import select
|
|
import socket
|
|
import socketserver
|
|
import ssl
|
|
import urllib.parse
|
|
import tempfile
|
|
import traceback
|
|
|
|
os.environ.setdefault("TK_SILENCE_DEPRECATION", "1")
|
|
|
|
from DrissionPage import Chromium, ChromiumOptions
|
|
from DrissionPage.errors import PageDisconnectedError
|
|
from curl_cffi import requests
|
|
|
|
import functools
|
|
import types
|
|
import app_config as _app_config
|
|
import account_outputs as _account_outputs
|
|
import browser_runtime as _browser_runtime
|
|
import mail_service as _mail_service
|
|
import registration_browser as _registration_browser
|
|
from app_config import (
|
|
CONFIG_FILE, DEFAULT_CONFIG, ConfigError, config, load_config, save_config,
|
|
validate_config, validate_config_structure, validate_run_requirements,
|
|
)
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
class RegistrationCancelled(Exception):
|
|
pass
|
|
|
|
|
|
class AccountRetryNeeded(Exception):
|
|
pass
|
|
|
|
|
|
|
|
|
|
class RemoteTokenCompatibilityError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class RemoteTokenRequestError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def log_exception(context, exc, log_callback=None):
|
|
message = f"{context}: {exc.__class__.__name__}: {exc}"
|
|
if log_callback:
|
|
log_callback(f"[!] {message}")
|
|
else:
|
|
print(f"[!] {message}", file=sys.stderr)
|
|
return message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = validate_config_structure(loaded)
|
|
except ConfigError:
|
|
raise
|
|
except Exception as exc:
|
|
raise ConfigError(f"配置文件解析失败: {CONFIG_FILE}: {exc}") from exc
|
|
else:
|
|
config = validate_config_structure(DEFAULT_CONFIG.copy())
|
|
return config
|
|
|
|
|
|
def save_config():
|
|
global config
|
|
config = validate_config_structure(config)
|
|
config_dir = os.path.dirname(os.path.abspath(CONFIG_FILE))
|
|
os.makedirs(config_dir, exist_ok=True)
|
|
fd = None
|
|
temp_path = None
|
|
try:
|
|
fd, temp_path = tempfile.mkstemp(prefix=".config-", suffix=".json.tmp", dir=config_dir)
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
fd = None
|
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
|
f.write("\n")
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
try:
|
|
os.chmod(temp_path, 0o600)
|
|
except Exception:
|
|
pass
|
|
os.replace(temp_path, CONFIG_FILE)
|
|
temp_path = None
|
|
try:
|
|
os.chmod(CONFIG_FILE, 0o600)
|
|
except Exception:
|
|
pass
|
|
except Exception as exc:
|
|
raise ConfigError(f"保存配置失败: {exc}") from exc
|
|
finally:
|
|
if fd is not None:
|
|
try:
|
|
os.close(fd)
|
|
except Exception:
|
|
pass
|
|
if temp_path and os.path.exists(temp_path):
|
|
try:
|
|
os.unlink(temp_path)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
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()
|
|
|
|
EXTENSION_PATH = os.path.abspath(
|
|
os.path.join(os.path.dirname(__file__), "turnstilePatch")
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_compat_proxy(module, name, binder=None):
|
|
target = getattr(module, name)
|
|
@functools.wraps(target)
|
|
def proxy(*args, **kwargs):
|
|
if binder is not None:
|
|
binder()
|
|
return getattr(module, name)(*args, **kwargs)
|
|
return proxy
|
|
|
|
|
|
def _bind_browser_runtime():
|
|
_browser_runtime.configure_runtime(config, EXTENSION_PATH)
|
|
|
|
|
|
def _bind_account_outputs():
|
|
_account_outputs.configure_token_runtime(
|
|
config, http_get, http_post, log_exception,
|
|
compatibility_error=RemoteTokenCompatibilityError,
|
|
request_error=RemoteTokenRequestError,
|
|
)
|
|
|
|
|
|
def _bind_mail_service():
|
|
_mail_service.bind_runtime(globals())
|
|
_current = globals().get("generate_username")
|
|
_standard = _MAIL_COMPAT_PROXIES.get("generate_username")
|
|
if _current is not None and _current is not _standard:
|
|
_mail_service.generate_username = _current
|
|
elif _standard is not None:
|
|
_mail_service.generate_username = _MAIL_ORIGINALS["generate_username"]
|
|
|
|
|
|
def _bind_registration_browser():
|
|
_registration_browser.bind_runtime(globals())
|
|
|
|
|
|
LocalAuthProxyBridge = _browser_runtime.LocalAuthProxyBridge
|
|
for _name in ['get_configured_proxy', 'get_proxies', '_parse_proxy_url', '_safe_proxy_port', '_proxy_has_auth', '_strip_proxy_auth', '_proxy_endpoint_terms', 'is_proxy_connection_error', 'page_has_proxy_error', '_ReusableThreadingTCPServer', '_proxy_recv_until_headers', '_proxy_relay', '_LocalAuthProxyBridgeHandler', 'LocalAuthProxyBridge', 'prepare_browser_proxy', 'apply_browser_proxy_option', 'create_browser_options', '_build_request_kwargs', 'http_get', 'http_post']:
|
|
if _name.startswith("_") and _name in {"_ReusableThreadingTCPServer", "_LocalAuthProxyBridgeHandler", "_proxy_recv_until_headers", "_proxy_relay"}:
|
|
continue
|
|
if _name != "LocalAuthProxyBridge":
|
|
globals()[_name] = _make_compat_proxy(_browser_runtime, _name, _bind_browser_runtime)
|
|
for _name in ['resolve_grok2api_local_token_file', '_normalize_sso_token', 'add_token_to_grok2api_local_pool', 'get_grok2api_remote_api_bases', 'add_token_to_grok2api_remote_pool', 'add_token_to_grok2api_pools']:
|
|
globals()[_name] = _make_compat_proxy(_account_outputs, _name, _bind_account_outputs)
|
|
_MAIL_ORIGINALS = dict((name, getattr(_mail_service, name)) for name in ['_pick_list_payload', 'cloudflare_apply_auth_params', 'cloudflare_build_headers', 'cloudflare_create_account', 'cloudflare_create_temp_address', 'cloudflare_get_domains', 'cloudflare_get_message_detail', 'cloudflare_get_messages', 'cloudflare_get_oai_code', 'cloudflare_get_token', 'cloudflare_is_admin_create_path', 'cloudflare_next_default_domain', 'cloudmail_get_email_and_token', 'cloudmail_get_messages', 'cloudmail_get_oai_code', 'cloudmail_next_domain', 'create_account', 'duckmail_get_oai_code', 'extract_verification_code', 'generate_username', 'get_cloudflare_api_base', 'get_cloudflare_api_key', 'get_cloudflare_auth_mode', 'get_cloudflare_path', 'get_cloudmail_api_base', 'get_cloudmail_path', 'get_cloudmail_public_token', 'get_domains', 'get_duckmail_api_key', 'get_email_and_token', 'get_email_provider', 'get_message_detail', 'get_messages', 'get_oai_code', 'get_token', 'get_user_agent', 'get_yyds_api_key', 'get_yyds_jwt', 'pick_domain', 'yyds_create_account', 'yyds_generate_username', 'yyds_get_domains', 'yyds_get_email_and_token', 'yyds_get_message_detail', 'yyds_get_messages', 'yyds_get_oai_code', 'yyds_get_token', 'yyds_pick_domain'])
|
|
_MAIL_COMPAT_PROXIES = dict()
|
|
for _name in ['_pick_list_payload', 'cloudflare_apply_auth_params', 'cloudflare_build_headers', 'cloudflare_create_account', 'cloudflare_create_temp_address', 'cloudflare_get_domains', 'cloudflare_get_message_detail', 'cloudflare_get_messages', 'cloudflare_get_oai_code', 'cloudflare_get_token', 'cloudflare_is_admin_create_path', 'cloudflare_next_default_domain', 'cloudmail_get_email_and_token', 'cloudmail_get_messages', 'cloudmail_get_oai_code', 'cloudmail_next_domain', 'create_account', 'duckmail_get_oai_code', 'extract_verification_code', 'generate_username', 'get_cloudflare_api_base', 'get_cloudflare_api_key', 'get_cloudflare_auth_mode', 'get_cloudflare_path', 'get_cloudmail_api_base', 'get_cloudmail_path', 'get_cloudmail_public_token', 'get_domains', 'get_duckmail_api_key', 'get_email_and_token', 'get_email_provider', 'get_message_detail', 'get_messages', 'get_oai_code', 'get_token', 'get_user_agent', 'get_yyds_api_key', 'get_yyds_jwt', 'pick_domain', 'yyds_create_account', 'yyds_generate_username', 'yyds_get_domains', 'yyds_get_email_and_token', 'yyds_get_message_detail', 'yyds_get_messages', 'yyds_get_oai_code', 'yyds_get_token', 'yyds_pick_domain']:
|
|
_proxy = _make_compat_proxy(_mail_service, _name, _bind_mail_service)
|
|
_MAIL_COMPAT_PROXIES[_name] = _proxy
|
|
globals()[_name] = _proxy
|
|
for _name in ['generate_random_birthdate', 'response_preview', 'is_cloudflare_block_response', 'set_birth_date', 'set_tos_accepted', 'encode_grpc_nsfw_settings', 'update_nsfw_settings', 'enable_nsfw_for_token', 'stop_browser_proxy_bridge', 'start_browser', 'stop_browser', 'restart_browser', 'cleanup_runtime_memory', 'refresh_active_page', 'click_email_signup_button', 'open_signup_page', 'has_profile_form', 'fill_email_and_submit', 'fill_code_and_submit', 'getTurnstileToken', 'build_profile', 'fill_profile_and_submit', 'wait_for_sso_cookie']:
|
|
globals()[_name] = _make_compat_proxy(_registration_browser, _name, _bind_registration_browser)
|
|
|
|
|
|
def __getattr__(name):
|
|
if name in {"browser", "page", "browser_proxy_bridge", "browser_started_with_proxy", "cf_clearance"}:
|
|
return getattr(_registration_browser, name)
|
|
if name in {"_cf_domain_index", "_cloudmail_domain_index"}:
|
|
return getattr(_mail_service, name)
|
|
raise AttributeError(name)
|
|
|
|
|
|
class _CompatibilityModule(types.ModuleType):
|
|
def __setattr__(self, name, value):
|
|
if name == "config":
|
|
if value is not _app_config.config:
|
|
if not isinstance(value, dict):
|
|
raise TypeError("config must be a dict")
|
|
_app_config.config.clear()
|
|
_app_config.config.update(value)
|
|
value = _app_config.config
|
|
elif name in {"_cf_domain_index", "_cloudmail_domain_index"}:
|
|
setattr(_mail_service, name, int(value))
|
|
self.__dict__.pop(name, None)
|
|
return
|
|
super().__setattr__(name, value)
|
|
|
|
|
|
sys.modules[__name__].__class__ = _CompatibilityModule
|
|
|
|
|
|
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 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="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 maybe_export_cpa_xai_after_success(email, password, sso="", log_callback=None, cancel_callback=None):
|
|
if not bool(config.get("cpa_export_enabled", False)):
|
|
return {"ok": False, "skipped": True, "reason": "disabled"}
|
|
logger = log_callback or (lambda message: None)
|
|
try:
|
|
from cpa_export import export_cpa_xai_for_account
|
|
except Exception as exc:
|
|
logger(f"[!] CPA 模块导入失败,已跳过 OIDC 导出: {exc}")
|
|
return {"ok": False, "error": str(exc)}
|
|
current_page = None
|
|
try:
|
|
current_page = _registration_browser.page
|
|
except Exception:
|
|
current_page = None
|
|
try:
|
|
result = export_cpa_xai_for_account(
|
|
email=email,
|
|
password=password,
|
|
page=current_page,
|
|
sso=sso,
|
|
config=config,
|
|
log_callback=logger,
|
|
cancel_callback=cancel_callback,
|
|
)
|
|
except Exception as exc:
|
|
logger(f"[!] CPA OIDC 导出失败,账号已保留: {exc}")
|
|
return {"ok": False, "error": str(exc)}
|
|
if result.get("ok"):
|
|
exported_path = result.get("hotload_path") or result.get("path") or ""
|
|
suffix = f": {exported_path}" if exported_path else ""
|
|
logger(f"[+] CPA OIDC 导出成功{suffix}")
|
|
elif not result.get("skipped"):
|
|
logger(f"[!] CPA OIDC 导出失败,账号已保留: {result.get('error') or result}")
|
|
return result
|
|
|
|
|
|
|
|
def _save_mail_credential(email, credential, log_callback=None):
|
|
from account_outputs import save_mail_credential
|
|
try:
|
|
return save_mail_credential(os.path.dirname(__file__), email, credential)
|
|
except Exception as exc:
|
|
log_exception("保存邮箱凭据失败", exc, log_callback)
|
|
return False
|
|
|
|
|
|
def _append_account_line(path, email, password, sso):
|
|
from account_outputs import append_account_line
|
|
return append_account_line(path, email, password, sso)
|
|
|
|
|
|
def _queue_unsaved_account(path, payload, error, log_callback=None):
|
|
from account_outputs import queue_unsaved_account
|
|
try:
|
|
return queue_unsaved_account(path, payload, error)
|
|
except Exception as exc:
|
|
log_exception("写入账号 pending 队列失败", exc, log_callback)
|
|
return False
|
|
|
|
|
|
def retry_pending_file(pending_path, output_path=None, log_callback=None):
|
|
from account_outputs import retry_pending_file as _retry_pending_file
|
|
return _retry_pending_file(pending_path, output_path=output_path, log_callback=log_callback)
|
|
|
|
|
|
def run_registration_common(count, log_callback, cancel_callback, accounts_output_file, observer):
|
|
from registration_flow import RegistrationCallbacks, RegistrationOperations, run_batch
|
|
callbacks = RegistrationCallbacks(log=log_callback, cancelled=cancel_callback)
|
|
operations = RegistrationOperations(
|
|
start_browser=lambda: start_browser(log_callback=log_callback),
|
|
restart_browser=lambda: restart_browser(log_callback=log_callback),
|
|
browser_missing=lambda: _registration_browser.browser is None,
|
|
open_signup_page=lambda: open_signup_page(log_callback=log_callback, cancel_callback=cancel_callback),
|
|
fill_email_and_submit=lambda: fill_email_and_submit(log_callback=log_callback, cancel_callback=cancel_callback),
|
|
save_mail_credential=lambda email, token: _save_mail_credential(email, token, log_callback),
|
|
fill_code_and_submit=lambda email, token: fill_code_and_submit(email, token, log_callback=log_callback, cancel_callback=cancel_callback),
|
|
fill_profile_and_submit=lambda: fill_profile_and_submit(log_callback=log_callback, cancel_callback=cancel_callback),
|
|
wait_for_sso_cookie=lambda: wait_for_sso_cookie(log_callback=log_callback, cancel_callback=cancel_callback),
|
|
enable_nsfw=lambda sso: enable_nsfw_for_token(sso, log_callback=log_callback),
|
|
persist_account_line=lambda email, password, sso: _append_account_line(accounts_output_file, email, password, sso),
|
|
queue_unsaved_result=lambda payload, error: _queue_unsaved_account(accounts_output_file, payload, error, log_callback),
|
|
add_tokens=lambda sso, email: add_token_to_grok2api_pools(sso, email=email, log_callback=log_callback),
|
|
export_cpa=lambda email, password, sso: maybe_export_cpa_xai_after_success(
|
|
email=email, password=password, sso=sso,
|
|
log_callback=log_callback, cancel_callback=cancel_callback,
|
|
),
|
|
cleanup=lambda reason: cleanup_runtime_memory(log_callback=log_callback, reason=reason),
|
|
sleep=lambda seconds: sleep_with_cancel(seconds, cancel_callback),
|
|
cancelled_exception=RegistrationCancelled,
|
|
retry_exception=AccountRetryNeeded,
|
|
)
|
|
return run_batch(
|
|
count=count,
|
|
callbacks=callbacks,
|
|
observer=observer,
|
|
ops=operations,
|
|
enable_nsfw=bool(config.get("enable_nsfw", True)),
|
|
cleanup_interval=MEMORY_CLEANUP_INTERVAL,
|
|
max_slot_retry=3,
|
|
max_mail_retry=3,
|
|
)
|
|
|
|
|
|
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.registered_unsaved_count = 0
|
|
self.postprocess_warning_count = 0
|
|
self.results = []
|
|
self.stop_requested = False
|
|
self.ui_queue = queue.Queue()
|
|
self.accounts_output_file = ""
|
|
self.setup_ui()
|
|
self.root.after(50, self.process_ui_queue)
|
|
|
|
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", "cloudmail"], width=12)
|
|
add_field(self.email_provider_combo, 0, 1, sticky=tk.W)
|
|
|
|
add_label(0, 2, "注册数量:")
|
|
self.count_var = tk.StringVar(value=str(config.get("register_count", 1)))
|
|
self.count_spinbox = tk.Spinbox(
|
|
config_frame,
|
|
from_=1,
|
|
to=2500,
|
|
width=8,
|
|
textvariable=self.count_var,
|
|
bg=UI_ENTRY_BG,
|
|
fg=UI_FG,
|
|
insertbackground=UI_FG,
|
|
buttonbackground=UI_BUTTON_BG,
|
|
disabledbackground="#2f2f2f",
|
|
disabledforeground=UI_MUTED_FG,
|
|
relief=tk.SOLID,
|
|
)
|
|
add_field(self.count_spinbox, 0, 3, sticky=tk.W)
|
|
|
|
add_label(1, 0, "注册选项:")
|
|
self.nsfw_var = tk.BooleanVar(value=config.get("enable_nsfw", True))
|
|
self.nsfw_check = tk_checkbutton(config_frame, text="注册后开启 NSFW", variable=self.nsfw_var)
|
|
add_field(self.nsfw_check, 1, 1, sticky=tk.W)
|
|
|
|
add_label(1, 2, "代理(可选):")
|
|
self.proxy_var = tk.StringVar(value=config.get("proxy", ""))
|
|
self.proxy_entry = tk_entry(config_frame, textvariable=self.proxy_var, width=34)
|
|
add_field(self.proxy_entry, 1, 3)
|
|
|
|
add_label(2, 0, "DuckMail API Key:")
|
|
self.api_key_var = tk.StringVar(value=config.get("duckmail_api_key", ""))
|
|
self.api_key_entry = tk_entry(config_frame, textvariable=self.api_key_var, width=34)
|
|
add_field(self.api_key_entry, 2, 1)
|
|
|
|
add_label(2, 2, "Cloudflare 鉴权模式:")
|
|
self.cloudflare_auth_mode_var = tk.StringVar(value=config.get("cloudflare_auth_mode", "none"))
|
|
self.cloudflare_auth_mode_combo = tk_option_menu(
|
|
config_frame, self.cloudflare_auth_mode_var, ["query-key", "bearer", "x-api-key", "x-admin-auth", "none"], width=12
|
|
)
|
|
add_field(self.cloudflare_auth_mode_combo, 2, 3, sticky=tk.W)
|
|
|
|
add_label(3, 0, "Cloudflare API Base:")
|
|
self.cloudflare_api_base_var = tk.StringVar(value=config.get("cloudflare_api_base", ""))
|
|
self.cloudflare_api_base_entry = tk_entry(config_frame, textvariable=self.cloudflare_api_base_var, width=72)
|
|
add_field(self.cloudflare_api_base_entry, 3, 1, columnspan=3)
|
|
|
|
add_label(4, 0, "Cloudflare API Key:")
|
|
self.cloudflare_api_key_var = tk.StringVar(value=config.get("cloudflare_api_key", ""))
|
|
self.cloudflare_api_key_entry = tk_entry(config_frame, textvariable=self.cloudflare_api_key_var, width=34)
|
|
add_field(self.cloudflare_api_key_entry, 4, 1)
|
|
|
|
add_label(4, 2, "CF 路径:")
|
|
self.cloudflare_paths_var = tk.StringVar(
|
|
value=",".join(
|
|
[
|
|
config.get("cloudflare_path_domains", "/api/domains"),
|
|
config.get("cloudflare_path_accounts", "/api/new_address"),
|
|
config.get("cloudflare_path_token", "/api/token"),
|
|
config.get("cloudflare_path_messages", "/api/mails"),
|
|
]
|
|
)
|
|
)
|
|
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, "Cloud Mail API Base:")
|
|
self.cloudmail_api_base_var = tk.StringVar(value=config.get("cloudmail_api_base", ""))
|
|
self.cloudmail_api_base_entry = tk_entry(config_frame, textvariable=self.cloudmail_api_base_var, width=34)
|
|
add_field(self.cloudmail_api_base_entry, 5, 1)
|
|
|
|
add_label(5, 2, "Cloud Mail 域名:")
|
|
self.cloudmail_domains_var = tk.StringVar(value=config.get("cloudmail_domains", ""))
|
|
self.cloudmail_domains_entry = tk_entry(config_frame, textvariable=self.cloudmail_domains_var, width=34)
|
|
add_field(self.cloudmail_domains_entry, 5, 3)
|
|
|
|
add_label(6, 0, "Cloud Mail Public Token:")
|
|
self.cloudmail_public_token_var = tk.StringVar(value=config.get("cloudmail_public_token", ""))
|
|
self.cloudmail_public_token_entry = tk_entry(config_frame, textvariable=self.cloudmail_public_token_var, width=72)
|
|
add_field(self.cloudmail_public_token_entry, 6, 1, columnspan=3)
|
|
|
|
add_label(7, 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, 7, 1, sticky=tk.W)
|
|
|
|
add_label(7, 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, 7, 3, sticky=tk.W)
|
|
|
|
add_label(8, 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, 8, 1, columnspan=3)
|
|
|
|
add_label(9, 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, 9, 1, sticky=tk.W)
|
|
|
|
add_label(10, 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, 10, 1, columnspan=3)
|
|
|
|
add_label(11, 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, 11, 1, columnspan=3)
|
|
|
|
add_label(12, 0, "OIDC / CPA:")
|
|
self.cpa_export_var = tk.BooleanVar(value=bool(config.get("cpa_export_enabled", False)))
|
|
self.cpa_export_check = tk_checkbutton(config_frame, text="注册成功后导出 CPA xAI OIDC", variable=self.cpa_export_var)
|
|
add_field(self.cpa_export_check, 12, 1, sticky=tk.W)
|
|
|
|
add_label(12, 2, "CPA 输出目录:")
|
|
self.cpa_auth_dir_var = tk.StringVar(value=str(config.get("cpa_auth_dir", "./cpa_auths")))
|
|
self.cpa_auth_dir_entry = tk_entry(config_frame, textvariable=self.cpa_auth_dir_var, width=34)
|
|
add_field(self.cpa_auth_dir_entry, 12, 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 | 待恢复: 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 process_ui_queue(self):
|
|
try:
|
|
while True:
|
|
event = self.ui_queue.get_nowait()
|
|
kind = event[0]
|
|
if kind == "log":
|
|
line = event[1]
|
|
self.log_text.insert(tk.END, f"{line}\n")
|
|
self.log_text.see(tk.END)
|
|
elif kind == "clear_log":
|
|
self.log_text.delete(1.0, tk.END)
|
|
elif kind == "stats":
|
|
self.stats_var.set(f"成功: {event[1]} | 失败: {event[2]} | 待恢复: {event[3]} | 后处理警告: {event[4]}")
|
|
elif kind == "running":
|
|
running = bool(event[1])
|
|
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")
|
|
elif kind == "error":
|
|
messagebox.showerror(event[1], event[2])
|
|
except queue.Empty:
|
|
pass
|
|
except Exception as exc:
|
|
print(f"[!] UI 队列处理失败: {exc}", file=sys.stderr)
|
|
finally:
|
|
try:
|
|
self.root.after(50, self.process_ui_queue)
|
|
except Exception:
|
|
pass
|
|
|
|
def log(self, message):
|
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
|
line = f"[{timestamp}] {message}"
|
|
print(line, flush=True)
|
|
self.ui_queue.put(("log", line))
|
|
|
|
def clear_log(self):
|
|
self.ui_queue.put(("clear_log",))
|
|
|
|
def update_stats(self):
|
|
self.ui_queue.put(("stats", self.success_count, self.fail_count, self.registered_unsaved_count, self.postprocess_warning_count))
|
|
|
|
def _set_running_ui(self, running):
|
|
self.is_running = bool(running)
|
|
self.ui_queue.put(("running", self.is_running))
|
|
|
|
|
|
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 "none"
|
|
config["cloudmail_api_base"] = self.cloudmail_api_base_var.get().strip()
|
|
config["cloudmail_public_token"] = self.cloudmail_public_token_var.get().strip()
|
|
config["cloudmail_domains"] = self.cloudmail_domains_var.get().strip()
|
|
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()
|
|
config["cpa_export_enabled"] = bool(self.cpa_export_var.get())
|
|
config["cpa_auth_dir"] = self.cpa_auth_dir_var.get().strip() or "./cpa_auths"
|
|
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])
|
|
try:
|
|
count = int(self.count_var.get())
|
|
config["register_count"] = count
|
|
validated = validate_run_requirements(config)
|
|
config.clear()
|
|
config.update(validated)
|
|
save_config()
|
|
except (ValueError, ConfigError) as exc:
|
|
self.log(f"[!] 配置无效或保存失败: {exc}")
|
|
return
|
|
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):
|
|
def observer(batch, account, output):
|
|
self.success_count = batch.success_count
|
|
self.fail_count = batch.fail_count
|
|
self.registered_unsaved_count = batch.registered_unsaved_count
|
|
self.postprocess_warning_count = batch.postprocess_warning_count
|
|
if account is not None:
|
|
self.results.append({"email": account.email, "sso": account.sso, "profile": account.profile, "output": output})
|
|
self.update_stats()
|
|
try:
|
|
batch = run_registration_common(
|
|
count=count,
|
|
log_callback=self.log,
|
|
cancel_callback=self.should_stop,
|
|
accounts_output_file=self.accounts_output_file,
|
|
observer=observer,
|
|
)
|
|
self.success_count = batch.success_count
|
|
self.fail_count = batch.fail_count
|
|
self.registered_unsaved_count = batch.registered_unsaved_count
|
|
self.postprocess_warning_count = batch.postprocess_warning_count
|
|
self.update_stats()
|
|
except Exception as exc:
|
|
log_exception("任务异常", exc, self.log)
|
|
finally:
|
|
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()
|
|
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}")
|
|
last_stats = {"success": 0, "fail": 0, "pending": 0, "warnings": 0}
|
|
def observer(batch, account, output):
|
|
last_stats["success"] = batch.success_count
|
|
last_stats["fail"] = batch.fail_count
|
|
last_stats["pending"] = batch.registered_unsaved_count
|
|
last_stats["warnings"] = batch.postprocess_warning_count
|
|
cli_log(f"[*] 当前统计: 成功 {batch.success_count} | 失败 {batch.fail_count} | 待恢复 {batch.registered_unsaved_count} | 后处理警告 {batch.postprocess_warning_count}")
|
|
try:
|
|
batch = run_registration_common(
|
|
count=count,
|
|
log_callback=cli_log,
|
|
cancel_callback=controller.should_stop,
|
|
accounts_output_file=accounts_output_file,
|
|
observer=observer,
|
|
)
|
|
last_stats["success"] = batch.success_count
|
|
last_stats["fail"] = batch.fail_count
|
|
last_stats["pending"] = batch.registered_unsaved_count
|
|
last_stats["warnings"] = batch.postprocess_warning_count
|
|
except KeyboardInterrupt:
|
|
controller.stop()
|
|
cli_log("[!] 收到 Ctrl+C,正在停止并清理")
|
|
except Exception as exc:
|
|
log_exception("任务异常", exc, cli_log)
|
|
finally:
|
|
cli_log(f"[*] 任务结束。成功 {last_stats['success']} | 失败 {last_stats['fail']} | 待恢复 {last_stats['pending']} | 后处理警告 {last_stats['warnings']}")
|
|
|
|
|
|
def main_cli():
|
|
try:
|
|
load_config()
|
|
except ConfigError as exc:
|
|
cli_log(f"[!] {exc}")
|
|
return
|
|
try:
|
|
validated = validate_run_requirements(config)
|
|
config.clear()
|
|
config.update(validated)
|
|
except ConfigError as exc:
|
|
cli_log(f"[!] {exc}")
|
|
return
|
|
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() == "retry-pending":
|
|
if len(sys.argv) < 3:
|
|
print("用法: python grok_register_ttk.py retry-pending <pending文件> [输出文件]", file=sys.stderr)
|
|
return
|
|
try:
|
|
summary = retry_pending_file(
|
|
sys.argv[2],
|
|
output_path=sys.argv[3] if len(sys.argv) > 3 else None,
|
|
log_callback=cli_log,
|
|
)
|
|
cli_log(
|
|
f"[*] pending 恢复完成: 已恢复 {summary['restored']} | 剩余 {summary['remaining']} | 输出 {summary['output_path']}"
|
|
)
|
|
except Exception as exc:
|
|
log_exception("pending 恢复失败", exc, cli_log)
|
|
return
|
|
if len(sys.argv) > 1 and sys.argv[1].strip().lower() in ("start", "cli", "--cli"):
|
|
main_cli()
|
|
return
|
|
if not TK_AVAILABLE:
|
|
print(f"[!] GUI 模式需要 Tkinter,但当前环境不可用: {TK_IMPORT_ERROR}", file=sys.stderr)
|
|
print("[*] 可改用 CLI 模式: python grok_register_ttk.py cli", file=sys.stderr)
|
|
return
|
|
root = tk.Tk()
|
|
setup_light_theme(root)
|
|
try:
|
|
app = GrokRegisterGUI(root)
|
|
except ConfigError as exc:
|
|
print(f"[!] {exc}", file=sys.stderr)
|
|
try:
|
|
messagebox.showerror("配置错误", str(exc))
|
|
except Exception:
|
|
pass
|
|
root.destroy()
|
|
return
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|