Files
grok-keygen/register_cli.py
T
chaos 6f7c949681 Replace repository with grok-reg-protocol-cpa project
Overwrite previous grok-keygen contents with the local registration/CPA protocol codebase (secrets and runtime data excluded).
2026-07-11 17:25:37 +08:00

675 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CLI wrapper for grok_register_ttk — multi-thread register + async CPA mint pipeline.
Architecture:
Register workers (R) → accounts_cli + mint_queue
Mint workers (M) → cpa_auths/xai-*.json + optional hotload
Browser lifecycle:
- One Chromium per register worker, reused via TabPool.clear_session
- Full recycle every N accounts or on error
- Register browser released BEFORE mint (mint always standalone Chromium)
- Peak browsers ≈ R + M (not 2×R)
"""
from __future__ import annotations
import argparse
import os
import queue
import sys
import threading
import time
import traceback
from typing import Any
# 强制走本目录的 grok_register_ttk
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import grok_register_ttk as reg # noqa: E402
# Linux 适配: DrissionPage 默认找 'chrome', 我们装的是 chromium
# 保留原版 slim flags + proxy,再补 chromium 路径与 turnstilePatch。
_orig_create_browser_options = reg.create_browser_options
def _patched_create_browser_options():
# Prefer original factory (proxy + CHROMIUM_SLIM_FLAGS + extension)
try:
opts = _orig_create_browser_options()
except Exception:
from DrissionPage import ChromiumOptions
opts = ChromiumOptions()
opts.auto_port()
opts.set_timeouts(base=1)
for flag in getattr(reg, "CHROMIUM_SLIM_FLAGS", ()) or ():
try:
opts.set_argument(flag)
except Exception:
pass
try:
opts.auto_port()
except Exception:
pass
try:
opts.set_timeouts(base=1)
except Exception:
pass
for cand in (
"/snap/bin/chromium",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
):
if os.path.isfile(cand):
try:
opts.set_browser_path(cand)
except Exception:
pass
break
ext_path = os.path.join(os.path.dirname(os.path.abspath(reg.__file__)), "turnstilePatch")
if os.path.isdir(ext_path):
try:
opts.add_extension(ext_path)
except Exception:
pass
return opts
reg.create_browser_options = _patched_create_browser_options
# ── 线程安全日志 ──
_log_queue: queue.Queue = queue.Queue()
def _log_writer():
while True:
msg = _log_queue.get()
if msg is None:
break
print(msg, flush=True)
def log(worker_id: int | str, msg: str) -> None:
_log_queue.put(f"[{time.strftime('%H:%M:%S')}] [W{worker_id}] {msg}")
# ── 统计 ──
_stats_lock = threading.Lock()
_stats = {
"reg_success": 0,
"reg_fail": 0,
"mint_success": 0,
"mint_fail": 0,
"mint_skip": 0,
}
def _inc(key: str, n: int = 1) -> None:
with _stats_lock:
_stats[key] = _stats.get(key, 0) + n
# forever 任务索引
_next_idx_lock = threading.Lock()
_next_idx = [1]
# mint 队列结束哨兵
_MINT_STOP = object()
def resolve_mint_workers(
*,
cli_value: int,
threads: int,
config: dict,
inline_mint: bool,
) -> int:
"""Resolve mint worker count.
Priority: --inline-mint > CLI --mint-workers (>=0) > config cpa_mint_workers > auto.
auto (-1): min(threads, 4) when CPA export enabled, else 0.
0: inline mint on register threads.
"""
if inline_mint:
return 0
if cli_value >= 0:
return max(0, min(int(cli_value), 10))
cfg_v = config.get("cpa_mint_workers", -1)
try:
cfg_v = int(cfg_v)
except Exception:
cfg_v = -1
if cfg_v >= 0:
return max(0, min(cfg_v, 10))
# auto
if config.get("cpa_export_enabled", True):
return max(1, min(int(threads), 4))
return 0
def resolve_mint_queue_max(config: dict, mint_workers: int, cli_value: int | None = None) -> int:
if cli_value is not None and cli_value >= 0:
return int(cli_value)
try:
v = int(config.get("cpa_mint_queue_max", 0) or 0)
except Exception:
v = 0
if v > 0:
return v
# default backpressure: 2 × mint workers (0 if no mint pool)
return max(0, mint_workers * 2) if mint_workers > 0 else 0
class DummyStop:
def __call__(self) -> bool:
return False
def _is_hotmail_provider() -> bool:
try:
provider = reg.get_email_provider()
except Exception:
provider = (getattr(reg, "config", {}) or {}).get("email_provider", "")
return str(provider or "").strip().lower() in {"hotmail", "outlook", "outlookmail", "microsoft"}
def _mark_email_stage_error(email: str, reason: str) -> None:
"""Persist failed Hotmail/Outlook aliases so the next run does not reuse them."""
if not email or not _is_hotmail_provider():
return
try:
reg.mark_error(email, reason=str(reason)[:120])
except Exception:
pass
def _ensure_browser(worker_id: int, force_recycle: bool = False):
"""Start browser if missing; optional full recycle."""
if force_recycle:
try:
reg.stop_browser()
except Exception:
pass
if reg.TabPool.get_browser() is None:
reg.start_browser(log_callback=lambda m: log(worker_id, m))
def register_one(
worker_id: int,
idx: int,
total: int,
accounts_file: str,
*,
do_mint_inline: bool = False,
mint_queue: queue.Queue | None = None,
) -> dict | None:
"""Run one registration. Enqueue CPA mint (default) instead of blocking.
Returns dict(email, sso, profile) or None.
"""
email = ""
dev_token = ""
try:
max_mail_retry = max(1, int((getattr(reg, "config", {}) or {}).get("mail_retry_count", 3) or 3))
except Exception:
max_mail_retry = 3
cancel = DummyStop()
try:
_ensure_browser(worker_id, force_recycle=False)
except Exception as exc:
log(worker_id, f"! 浏览器启动失败: {exc}")
return None
for mail_try in range(1, max_mail_retry + 1):
email = ""
dev_token = ""
try:
log(worker_id, f"--- 第 {idx}/{total} 个账号, 邮箱尝试 {mail_try}/{max_mail_retry} ---")
log(worker_id, "1. 打开注册页")
reg.open_signup_page(log_callback=lambda m: log(worker_id, m), cancel_callback=cancel)
log(worker_id, "2. 创建邮箱并提交")
email, dev_token = reg.fill_email_and_submit(
log_callback=lambda m: log(worker_id, m), cancel_callback=cancel
)
log(worker_id, f"邮箱: {email}")
log(worker_id, "3. 拉取验证码")
code = reg.fill_code_and_submit(
email,
dev_token,
log_callback=lambda m: log(worker_id, m),
cancel_callback=cancel,
)
log(worker_id, f"验证码: {code}")
break
except Exception as exc:
msg = str(exc)
if ("未收到验证码" in msg or "验证码" in msg) and mail_try < max_mail_retry:
log(worker_id, f"! 本邮箱未取到验证码,换邮箱重试: {msg}")
_mark_email_stage_error(email, msg)
try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m))
except Exception:
pass
reg.sleep_with_cancel(1, cancel)
continue
log(worker_id, f"! 邮箱阶段失败: {msg}")
_mark_email_stage_error(email, msg)
traceback.print_exc()
_inc("reg_fail")
try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m))
except Exception:
pass
return None
try:
log(worker_id, "4. 填写资料")
profile = reg.fill_profile_and_submit(
log_callback=lambda m: log(worker_id, m), cancel_callback=cancel
)
log(worker_id, f"资料已填: {profile.get('given_name')} {profile.get('family_name')}")
log(worker_id, "5. 等待 sso cookie")
sso = reg.wait_for_sso_cookie(
log_callback=lambda m: log(worker_id, m), cancel_callback=cancel
)
password = profile.get("password", "") or ""
line = f"{email}----{password}----{sso}\n"
with open(accounts_file, "a", encoding="utf-8") as f:
f.write(line)
log(worker_id, f"+ 注册成功: {email}")
reg.mark_used(email, password)
# Capture cookies BEFORE releasing browser (for mint cookie inject)
page = reg._get_page()
cookies = []
try:
import cpa_export as _cpa_exp
cookies = _cpa_exp.export_cookies_from_page(page) if page is not None else []
except Exception:
cookies = []
if cookies:
log(worker_id, f"[*] 导出 cookie {len(cookies)} 条供 mint 注入")
if page and reg.PERF_FLAGS.get("cookie_snapshot", True):
try:
reg.save_cookies_snapshot(page, "success", email)
except Exception:
pass
try:
reg.add_token_to_grok2api_pools(
sso, email=email, log_callback=lambda m: log(worker_id, m)
)
except Exception as exc:
log(worker_id, f"[Debug] grok2api: {exc}")
# Release / recycle register browser BEFORE mint so peak browsers ≈ R+M
try:
reg.prepare_browser_for_next_account(log_callback=lambda m: log(worker_id, m))
except Exception:
try:
reg.stop_browser()
except Exception:
pass
job = {
"email": email,
"password": password,
"sso": sso,
"profile": profile,
"idx": idx,
"cookies": cookies,
}
if do_mint_inline:
_run_mint_job(f"R{worker_id}", job, getattr(reg, "config", {}) or {})
elif mint_queue is not None:
# backpressure: wait while queue is saturated
qmax = int(getattr(mint_queue, "_reg_qmax", 0) or 0)
while qmax > 0 and mint_queue.qsize() >= qmax:
log(worker_id, f"[cpa] mint 队列背压 qsize={mint_queue.qsize()}≥{qmax},等待...")
time.sleep(1.0)
mint_queue.put(job)
log(worker_id, f"[cpa] enqueued mint for {email} (queue≈{mint_queue.qsize()})")
else:
log(worker_id, "[cpa] mint skipped (no queue / inline)")
_inc("reg_success")
return job
except Exception as exc:
log(worker_id, f"! 注册失败: {exc}")
reg.mark_error(email or "", reason=str(exc)[:120])
traceback.print_exc()
_inc("reg_fail")
try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m))
except Exception:
pass
return None
def _run_mint_job(worker_id: int | str, job: dict[str, Any], config: dict) -> dict:
"""Standalone CPA mint (own Chromium). Never reuses register browser."""
email = job.get("email") or ""
password = job.get("password") or ""
if not email or not password:
_inc("mint_fail")
return {"ok": False, "error": "missing email/password", "email": email}
if not config.get("cpa_export_enabled", True):
_inc("mint_skip")
log(worker_id, f"[cpa] export disabled, skip {email}")
return {"ok": False, "skipped": True, "email": email}
try:
import cpa_export
# page=None always — force standalone path inside export
result = cpa_export.export_cpa_xai_for_account(
email,
password,
page=None,
cookies=job.get("cookies"),
sso=job.get("sso") or "",
config=config,
log_callback=lambda m: log(worker_id, m),
)
if result.get("ok"):
log(worker_id, f"+ CPA auth: {result.get('path')}")
_inc("mint_success")
elif result.get("skipped"):
_inc("mint_skip")
log(worker_id, f"[cpa] skipped: {result.get('reason')}")
else:
_inc("mint_fail")
log(worker_id, f"! CPA auth 未成功: {result.get('error') or result}")
return result
except Exception as exc:
_inc("mint_fail")
log(worker_id, f"! CPA export 异常: {exc}")
traceback.print_exc()
return {"ok": False, "error": str(exc), "email": email}
def _register_worker(
worker_id: int,
task_queue: queue.Queue,
total: int,
accounts_file: str,
mint_queue: queue.Queue | None,
forever: bool,
do_mint_inline: bool,
):
while True:
try:
idx = task_queue.get_nowait()
except queue.Empty:
if not forever:
break
with _next_idx_lock:
nxt = _next_idx[0]
_next_idx[0] = nxt + 5
for i in range(nxt, nxt + 5):
task_queue.put(i)
continue
retry = 0
while retry < 2:
try:
result = register_one(
worker_id,
idx,
total,
accounts_file,
do_mint_inline=do_mint_inline,
mint_queue=mint_queue,
)
if result:
break
retry += 1
if retry < 2:
log(worker_id, f"[retry] 账号 {idx} 失败,重试 {retry}/1")
try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m))
except Exception:
pass
except Exception:
retry += 1
if retry < 2:
log(worker_id, f"[retry] 账号 {idx} 异常,重试 {retry}/1")
traceback.print_exc()
try:
reg.restart_browser(log_callback=lambda m: log(worker_id, m))
except Exception:
pass
if retry >= 2:
# register_one already counted fail on exception path; if both returned None, count once more only if needed
pass
# worker exit: free browser
try:
reg.stop_browser()
except Exception:
pass
log(worker_id, "register worker exit")
def _mint_worker(worker_id: str, mint_queue: queue.Queue, config: dict):
while True:
job = mint_queue.get()
try:
if job is _MINT_STOP:
break
if not isinstance(job, dict):
continue
_run_mint_job(worker_id, job, config)
finally:
mint_queue.task_done()
try:
from cpa_xai.browser_confirm import shutdown_mint_browsers
shutdown_mint_browsers()
except Exception:
pass
log(worker_id, "mint worker exit")
def main() -> int:
parser = argparse.ArgumentParser(description="CLI runner for grok_register_ttk (pipelined).")
parser.add_argument("--count", type=int, default=1, help="账号总数目标(0=不限;含已有)")
parser.add_argument(
"--extra",
type=int,
default=0,
help="在已有 accounts 基础上再新注册 N 个",
)
parser.add_argument("--threads", type=int, default=1, help="注册并发线程数(1-10)")
parser.add_argument(
"--mint-workers",
type=int,
default=-1,
help="CPA mint 并发:-1=用 config/auto;0=内联;1-10=固定。覆盖 config.cpa_mint_workers",
)
parser.add_argument(
"--mint-queue-max",
type=int,
default=-1,
help="mint 队列背压上限:-1=用 config/auto(2×workers);0=不限制",
)
parser.add_argument("--accounts-file", default=os.path.join(os.path.dirname(__file__), "accounts_cli.txt"))
parser.add_argument("--fast", action="store_true", default=True, help="快速模式(默认开):压缩 sleep、关截图")
parser.add_argument("--no-fast", action="store_true", help="关闭快速模式")
parser.add_argument("--no-browser-reuse", action="store_true", help="每号强制 quit 浏览器")
parser.add_argument("--browser-recycle-every", type=int, default=25, help="复用 N 次后完整回收")
parser.add_argument("--cookie-snapshot", action="store_true", help="注册成功写 cookie 快照(默认关,fast)")
parser.add_argument("--inline-mint", action="store_true", help="强制注册线程内联 mint(调试用)")
args = parser.parse_args()
reg.load_config()
cfg0 = getattr(reg, "config", {}) or {}
threads = max(1, min(args.threads, 10))
fast = bool(args.fast) and not bool(args.no_fast)
mint_workers = resolve_mint_workers(
cli_value=args.mint_workers,
threads=threads,
config=cfg0,
inline_mint=bool(args.inline_mint),
)
do_mint_inline = mint_workers == 0
mint_qmax = resolve_mint_queue_max(
cfg0,
mint_workers,
cli_value=(None if args.mint_queue_max < 0 else args.mint_queue_max),
)
# perf knobs
reg.configure_perf(
fast=fast,
sleep_scale=0.15 if fast else 1.0,
skip_debug_io=fast,
cookie_snapshot=bool(args.cookie_snapshot) or not fast,
async_side_effects=True,
browser_reuse=not args.no_browser_reuse,
browser_recycle_every=max(1, int(args.browser_recycle_every)),
)
# 断点续跑
done_count = 0
if os.path.exists(args.accounts_file):
with open(args.accounts_file) as f:
done_count = sum(1 for line in f if line.strip())
if args.extra and args.extra > 0:
target_total = done_count + args.extra
remaining = args.extra
print(
f"[*] 配置加载完成,额外新注册 {args.extra} 个(当前已有 {done_count} → 目标 {target_total}),"
f"注册线程={threads} mint_workers={mint_workers} mint_queue_max={mint_qmax} fast={fast}",
flush=True,
)
args.count = target_total
elif args.count == 0:
remaining = None
print(
f"[*] 配置加载完成,不限数量,注册线程={threads} mint_workers={mint_workers} mint_queue_max={mint_qmax} fast={fast}",
flush=True,
)
else:
remaining = max(0, args.count - done_count)
print(
f"[*] 配置加载完成,目标 {args.count} 个账号,注册线程={threads} "
f"mint_workers={mint_workers} mint_queue_max={mint_qmax} fast={fast}",
flush=True,
)
print(f"[*] accounts_file = {args.accounts_file}", flush=True)
if done_count > 0:
print(f"[*] 断点续跑:已完成 {done_count}", flush=True)
if remaining is not None and remaining <= 0:
print("[*] 所有账号已完成,无需继续(可用 --extra N 再注册)", flush=True)
return 0
log_thread = threading.Thread(target=_log_writer, daemon=True)
log_thread.start()
try:
reg.TabPool.init(reg.create_browser_options, log_callback=lambda m: log(0, m))
except Exception as exc:
print(f"[!] 浏览器初始化失败: {exc}", flush=True)
return 1
task_queue: queue.Queue = queue.Queue()
mint_queue: queue.Queue | None = queue.Queue() if not do_mint_inline else None
if mint_queue is not None:
mint_queue._reg_qmax = mint_qmax # type: ignore[attr-defined]
global _next_idx
_next_idx[0] = done_count + 1
if remaining is not None:
for i in range(done_count + 1, args.count + 1):
task_queue.put(i)
else:
for i in range(done_count + 1, done_count + threads * 5 + 1):
task_queue.put(i)
_next_idx[0] = done_count + threads * 5 + 1
forever = remaining is None
cfg = getattr(reg, "config", {}) or {}
# mint workers first (so queue consumers ready)
mint_threads: list[threading.Thread] = []
if mint_queue is not None and mint_workers > 0:
for i in range(1, mint_workers + 1):
wid = f"M{i}"
t = threading.Thread(
target=_mint_worker,
args=(wid, mint_queue, cfg),
daemon=True,
name=f"mint-{i}",
)
t.start()
mint_threads.append(t)
reg_threads: list[threading.Thread] = []
for wid in range(1, threads + 1):
t = threading.Thread(
target=_register_worker,
args=(wid, task_queue, args.count, args.accounts_file, mint_queue, forever, do_mint_inline),
daemon=True,
name=f"reg-{wid}",
)
t.start()
reg_threads.append(t)
try:
for t in reg_threads:
t.join()
except KeyboardInterrupt:
print("\n[!] 用户中断", flush=True)
# drain mint queue
if mint_queue is not None:
log(0, f"[cpa] 等待 mint 队列清空(qsize≈{mint_queue.qsize()})...")
mint_queue.join()
for _ in mint_threads:
mint_queue.put(_MINT_STOP)
for t in mint_threads:
t.join(timeout=600)
try:
reg.shutdown_browser()
except Exception:
pass
# stop side-effect pool
try:
pool = getattr(reg, "_side_effect_pool", None)
if pool is not None:
pool.shutdown(wait=False, cancel_futures=True)
except Exception:
pass
_log_queue.put(None)
log_thread.join(timeout=2)
with _stats_lock:
s = dict(_stats)
print(
f"=== 完成: 注册成功 {s.get('reg_success', 0)}, 注册失败 {s.get('reg_fail', 0)}, "
f"CPA成功 {s.get('mint_success', 0)}, CPA失败 {s.get('mint_fail', 0)}, "
f"CPA跳过 {s.get('mint_skip', 0)} ===",
flush=True,
)
return 0 if s.get("reg_success", 0) > 0 else 1
if __name__ == "__main__":
sys.exit(main())