refactor: add shared registration workflow
This commit is contained in:
@@ -0,0 +1,313 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Shared registration workflow used by both GUI and CLI."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Callable, Dict, List, Type
|
||||||
|
|
||||||
|
|
||||||
|
def _noop(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegistrationCallbacks:
|
||||||
|
log: Callable[[str], None]
|
||||||
|
cancelled: Callable[[], bool]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegistrationObserver:
|
||||||
|
on_stats: Callable[[int, int], None] = _noop
|
||||||
|
on_result: Callable[["RegistrationResult", "OutputResult"], None] = _noop
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegistrationOperations:
|
||||||
|
start_browser: Callable[[Callable[[str], None]], None]
|
||||||
|
browser_missing: Callable[[], bool]
|
||||||
|
restart_browser: Callable[[Callable[[str], None]], None]
|
||||||
|
cleanup_runtime_memory: Callable[[Callable[[str], None], str], None]
|
||||||
|
sleep_with_cancel: Callable[[float, Callable[[], bool]], None]
|
||||||
|
open_signup_page: Callable[[Callable[[str], None], Callable[[], bool]], None]
|
||||||
|
fill_email_and_submit: Callable[[Callable[[str], None], Callable[[], bool]], Any]
|
||||||
|
save_mail_credential: Callable[[str, str], Dict[str, Any]]
|
||||||
|
fill_code_and_submit: Callable[[str, str, Callable[[str], None], Callable[[], bool]], str]
|
||||||
|
fill_profile_and_submit: Callable[[Callable[[str], None], Callable[[], bool]], Dict[str, Any]]
|
||||||
|
wait_for_sso_cookie: Callable[[Callable[[str], None], Callable[[], bool]], str]
|
||||||
|
enable_nsfw_for_token: Callable[[str, Callable[[str], None]], Any]
|
||||||
|
save_account_result: Callable[["RegistrationResult", str], Dict[str, Any]]
|
||||||
|
add_token_pools: Callable[["RegistrationResult", Callable[[str], None]], Dict[str, Any]]
|
||||||
|
export_cpa: Callable[["RegistrationResult", Callable[[str], None], Callable[[], bool]], Dict[str, Any]]
|
||||||
|
cancelled_error: Type[BaseException]
|
||||||
|
retry_error: Type[BaseException]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegistrationResult:
|
||||||
|
ok: bool
|
||||||
|
email: str = ""
|
||||||
|
password: str = ""
|
||||||
|
sso: str = ""
|
||||||
|
profile: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
error: str = ""
|
||||||
|
retryable: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OutputContext:
|
||||||
|
accounts_output_file: str
|
||||||
|
save_attempts: int = 3
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OutputResult:
|
||||||
|
registered: bool
|
||||||
|
saved: bool
|
||||||
|
save_attempts: int = 0
|
||||||
|
save_error: str = ""
|
||||||
|
token_pools: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
cpa: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BatchResult:
|
||||||
|
requested: int
|
||||||
|
success_count: int = 0
|
||||||
|
fail_count: int = 0
|
||||||
|
cancelled: bool = False
|
||||||
|
results: List[RegistrationResult] = field(default_factory=list)
|
||||||
|
outputs: List[OutputResult] = field(default_factory=list)
|
||||||
|
pending_outputs: List[RegistrationResult] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def register_one_account(
|
||||||
|
callbacks: RegistrationCallbacks,
|
||||||
|
operations: RegistrationOperations,
|
||||||
|
enable_nsfw: bool = True,
|
||||||
|
max_mail_retry: int = 3,
|
||||||
|
) -> RegistrationResult:
|
||||||
|
email = ""
|
||||||
|
dev_token = ""
|
||||||
|
code = ""
|
||||||
|
for mail_try in range(1, max_mail_retry + 1):
|
||||||
|
callbacks.log(f"[*] 1. 打开注册页 (尝试 {mail_try}/{max_mail_retry})")
|
||||||
|
operations.open_signup_page(callbacks.log, callbacks.cancelled)
|
||||||
|
callbacks.log("[*] 2. 创建邮箱并提交")
|
||||||
|
email, dev_token = operations.fill_email_and_submit(
|
||||||
|
callbacks.log, callbacks.cancelled
|
||||||
|
)
|
||||||
|
callbacks.log(f"[*] 邮箱: {email}")
|
||||||
|
callbacks.log(f"[Debug] 邮箱credential(jwt): {dev_token}")
|
||||||
|
credential_status = operations.save_mail_credential(email, dev_token)
|
||||||
|
if not credential_status.get("ok"):
|
||||||
|
callbacks.log(
|
||||||
|
"[!] 邮箱凭据保存失败,注册流程继续但已记录警告: "
|
||||||
|
+ str(credential_status.get("error") or "unknown error")
|
||||||
|
)
|
||||||
|
callbacks.log("[*] 3. 拉取验证码")
|
||||||
|
try:
|
||||||
|
code = operations.fill_code_and_submit(
|
||||||
|
email, dev_token, callbacks.log, callbacks.cancelled
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except operations.cancelled_error:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
message = str(exc)
|
||||||
|
if (
|
||||||
|
("未收到验证码" in message or "验证码" in message)
|
||||||
|
and mail_try < max_mail_retry
|
||||||
|
):
|
||||||
|
callbacks.log(
|
||||||
|
f"[!] 本邮箱未取到验证码,自动更换新邮箱重试: {message}"
|
||||||
|
)
|
||||||
|
operations.restart_browser(callbacks.log)
|
||||||
|
operations.sleep_with_cancel(1, callbacks.cancelled)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise RuntimeError("验证码阶段失败,已达到最大重试次数")
|
||||||
|
|
||||||
|
callbacks.log(f"[*] 验证码: {code}")
|
||||||
|
callbacks.log("[*] 4. 填写资料")
|
||||||
|
profile = operations.fill_profile_and_submit(
|
||||||
|
callbacks.log, callbacks.cancelled
|
||||||
|
)
|
||||||
|
callbacks.log(
|
||||||
|
f"[*] 资料已填: {profile.get('given_name', '')} "
|
||||||
|
f"{profile.get('family_name', '')}"
|
||||||
|
)
|
||||||
|
callbacks.log("[*] 5. 等待 sso cookie")
|
||||||
|
sso = operations.wait_for_sso_cookie(callbacks.log, callbacks.cancelled)
|
||||||
|
if enable_nsfw:
|
||||||
|
callbacks.log("[*] 6. 开启 NSFW")
|
||||||
|
nsfw_ok, nsfw_message = operations.enable_nsfw_for_token(
|
||||||
|
sso, callbacks.log
|
||||||
|
)
|
||||||
|
if nsfw_ok:
|
||||||
|
callbacks.log(f"[+] NSFW 开启成功: {nsfw_message}")
|
||||||
|
else:
|
||||||
|
callbacks.log(f"[!] NSFW 未开启,继续保存账号: {nsfw_message}")
|
||||||
|
return RegistrationResult(
|
||||||
|
ok=True,
|
||||||
|
email=email,
|
||||||
|
password=str(profile.get("password") or ""),
|
||||||
|
sso=sso,
|
||||||
|
profile=profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def persist_account_result(
|
||||||
|
result: RegistrationResult,
|
||||||
|
context: OutputContext,
|
||||||
|
callbacks: RegistrationCallbacks,
|
||||||
|
operations: RegistrationOperations,
|
||||||
|
) -> OutputResult:
|
||||||
|
attempts = max(int(context.save_attempts), 1)
|
||||||
|
save_error = ""
|
||||||
|
saved = False
|
||||||
|
used_attempts = 0
|
||||||
|
for attempt in range(1, attempts + 1):
|
||||||
|
used_attempts = attempt
|
||||||
|
status = operations.save_account_result(
|
||||||
|
result, context.accounts_output_file
|
||||||
|
)
|
||||||
|
if status.get("ok"):
|
||||||
|
saved = True
|
||||||
|
break
|
||||||
|
save_error = str(status.get("error") or "unknown error")
|
||||||
|
callbacks.log(
|
||||||
|
f"[!] 保存账号结果失败 ({attempt}/{attempts}): {save_error}"
|
||||||
|
)
|
||||||
|
if attempt < attempts:
|
||||||
|
operations.sleep_with_cancel(
|
||||||
|
min(0.5 * attempt, 1.5), callbacks.cancelled
|
||||||
|
)
|
||||||
|
|
||||||
|
if not saved:
|
||||||
|
return OutputResult(
|
||||||
|
registered=True,
|
||||||
|
saved=False,
|
||||||
|
save_attempts=used_attempts,
|
||||||
|
save_error=save_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
token_pools = operations.add_token_pools(result, callbacks.log)
|
||||||
|
for target in ("local", "remote"):
|
||||||
|
target_result = token_pools.get(target) or {}
|
||||||
|
if target_result.get("enabled") and not target_result.get("ok"):
|
||||||
|
callbacks.log(
|
||||||
|
f"[!] grok2api {target} 入池失败,账号已安全保存: "
|
||||||
|
f"{target_result.get('error') or 'unknown error'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
cpa = operations.export_cpa(
|
||||||
|
result, callbacks.log, callbacks.cancelled
|
||||||
|
)
|
||||||
|
return OutputResult(
|
||||||
|
registered=True,
|
||||||
|
saved=True,
|
||||||
|
save_attempts=used_attempts,
|
||||||
|
token_pools=token_pools,
|
||||||
|
cpa=cpa,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_batch(
|
||||||
|
count: int,
|
||||||
|
callbacks: RegistrationCallbacks,
|
||||||
|
observer: RegistrationObserver,
|
||||||
|
operations: RegistrationOperations,
|
||||||
|
output_context: OutputContext,
|
||||||
|
enable_nsfw: bool = True,
|
||||||
|
cleanup_interval: int = 5,
|
||||||
|
max_slot_retry: int = 3,
|
||||||
|
) -> BatchResult:
|
||||||
|
batch = BatchResult(requested=count)
|
||||||
|
retry_count_for_slot = 0
|
||||||
|
index = 0
|
||||||
|
operations.start_browser(callbacks.log)
|
||||||
|
callbacks.log("[*] 浏览器已启动")
|
||||||
|
try:
|
||||||
|
while index < count:
|
||||||
|
if callbacks.cancelled():
|
||||||
|
batch.cancelled = True
|
||||||
|
break
|
||||||
|
callbacks.log(f"--- 开始第 {index + 1}/{count} 个账号 ---")
|
||||||
|
try:
|
||||||
|
registration = register_one_account(
|
||||||
|
callbacks,
|
||||||
|
operations,
|
||||||
|
enable_nsfw=enable_nsfw,
|
||||||
|
)
|
||||||
|
output = persist_account_result(
|
||||||
|
registration,
|
||||||
|
output_context,
|
||||||
|
callbacks,
|
||||||
|
operations,
|
||||||
|
)
|
||||||
|
batch.results.append(registration)
|
||||||
|
batch.outputs.append(output)
|
||||||
|
observer.on_result(registration, output)
|
||||||
|
retry_count_for_slot = 0
|
||||||
|
index += 1
|
||||||
|
if output.saved:
|
||||||
|
batch.success_count += 1
|
||||||
|
callbacks.log(
|
||||||
|
f"[+] 注册并保存成功: {registration.email}"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
cleanup_interval > 0
|
||||||
|
and batch.success_count % cleanup_interval == 0
|
||||||
|
and index < count
|
||||||
|
):
|
||||||
|
operations.cleanup_runtime_memory(
|
||||||
|
callbacks.log,
|
||||||
|
f"已成功 {batch.success_count} 个账号,执行定期清理",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
batch.fail_count += 1
|
||||||
|
batch.pending_outputs.append(registration)
|
||||||
|
callbacks.log(
|
||||||
|
"[-] 账号已注册但结果未能持久化,未计入成功并加入"
|
||||||
|
f"待重试队列: {registration.email}: "
|
||||||
|
f"{output.save_error}"
|
||||||
|
)
|
||||||
|
except operations.cancelled_error:
|
||||||
|
batch.cancelled = True
|
||||||
|
callbacks.log("[!] 注册被停止")
|
||||||
|
break
|
||||||
|
except operations.retry_error as exc:
|
||||||
|
retry_count_for_slot += 1
|
||||||
|
if retry_count_for_slot <= max_slot_retry:
|
||||||
|
callbacks.log(
|
||||||
|
"[!] 当前账号流程卡住,重试第 "
|
||||||
|
f"{retry_count_for_slot}/{max_slot_retry} 次: {exc}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
batch.fail_count += 1
|
||||||
|
retry_count_for_slot = 0
|
||||||
|
index += 1
|
||||||
|
callbacks.log(
|
||||||
|
f"[-] 当前账号已达到最大重试次数,跳过: {exc}"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
batch.fail_count += 1
|
||||||
|
retry_count_for_slot = 0
|
||||||
|
index += 1
|
||||||
|
callbacks.log(f"[-] 注册失败: {exc}")
|
||||||
|
finally:
|
||||||
|
observer.on_stats(
|
||||||
|
batch.success_count, batch.fail_count
|
||||||
|
)
|
||||||
|
if callbacks.cancelled():
|
||||||
|
batch.cancelled = True
|
||||||
|
break
|
||||||
|
if operations.browser_missing():
|
||||||
|
operations.start_browser(callbacks.log)
|
||||||
|
else:
|
||||||
|
operations.restart_browser(callbacks.log)
|
||||||
|
operations.sleep_with_cancel(1, callbacks.cancelled)
|
||||||
|
finally:
|
||||||
|
operations.cleanup_runtime_memory(callbacks.log, "任务结束")
|
||||||
|
return batch
|
||||||
Reference in New Issue
Block a user