fix: complete remaining registration hardening

This commit is contained in:
github-actions[bot]
2026-07-14 16:13:09 +00:00
parent 0f97680c36
commit 478efecc32
9 changed files with 501 additions and 1576 deletions
+113 -210
View File
@@ -1,12 +1,6 @@
# -*- coding: utf-8 -*-
"""Shared registration workflow used by both GUI and CLI."""
"""Shared registration workflow used by both GUI and CLI adapters."""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Type
def _noop(*_args, **_kwargs):
return None
from typing import Any, Callable, Dict, Optional, Tuple
@dataclass
@@ -15,31 +9,26 @@ class RegistrationCallbacks:
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]
start_browser: Callable[[], None]
restart_browser: Callable[[], 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]
open_signup_page: Callable[[], None]
fill_email_and_submit: Callable[[], Tuple[str, str]]
save_mail_credential: Callable[[str, str], bool]
fill_code_and_submit: Callable[[str, str], str]
fill_profile_and_submit: Callable[[], Dict[str, Any]]
wait_for_sso_cookie: Callable[[], str]
enable_nsfw: Callable[[str], Tuple[bool, str]]
persist_account_line: Callable[[str, str, str], None]
queue_unsaved_result: Callable[[Dict[str, Any], str], bool]
add_tokens: Callable[[str, str], Dict[str, Dict[str, Any]]]
export_cpa: Callable[[str, str, str], Dict[str, Any]]
cleanup: Callable[[str], None]
sleep: Callable[[float], None]
cancelled_exception: type
retry_exception: type
@dataclass
@@ -53,101 +42,69 @@ class RegistrationResult:
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
pending_saved: bool = False
save_error: str = ""
token_pools: Dict[str, Any] = field(default_factory=dict)
pools: Dict[str, 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
processed_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)
results: list = field(default_factory=list)
def register_one_account(
callbacks: RegistrationCallbacks,
operations: RegistrationOperations,
enable_nsfw: bool = True,
max_mail_retry: int = 3,
) -> RegistrationResult:
def register_one_account(callbacks, ops, enable_nsfw=True, max_mail_retry=3):
email = ""
dev_token = ""
code = ""
mail_ok = False
for mail_try in range(1, max_mail_retry + 1):
if callbacks.cancelled():
raise ops.cancelled_exception()
callbacks.log(f"[*] 1. 打开注册页 (尝试 {mail_try}/{max_mail_retry})")
operations.open_signup_page(callbacks.log, callbacks.cancelled)
ops.open_signup_page()
callbacks.log("[*] 2. 创建邮箱并提交")
email, dev_token = operations.fill_email_and_submit(
callbacks.log, callbacks.cancelled
)
email, dev_token = ops.fill_email_and_submit()
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")
)
if not ops.save_mail_credential(email, dev_token):
callbacks.log("[!] 邮箱凭据保存失败,注册继续,但已明确记录该异常")
callbacks.log("[*] 3. 拉取验证码")
try:
code = operations.fill_code_and_submit(
email, dev_token, callbacks.log, callbacks.cancelled
)
code = ops.fill_code_and_submit(email, dev_token)
mail_ok = True
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)
if ("未收到验证码" in message or "验证码" in message) and mail_try < max_mail_retry:
callbacks.log(f"[!] 本邮箱未取到验证码,自动更换新邮箱重试: {message}")
ops.restart_browser()
ops.sleep(1)
continue
raise
else:
if not mail_ok:
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', '')}"
)
profile = ops.fill_profile_and_submit()
callbacks.log(f"[*] 资料已填: {profile.get('given_name')} {profile.get('family_name')}")
callbacks.log("[*] 5. 等待 sso cookie")
sso = operations.wait_for_sso_cookie(callbacks.log, callbacks.cancelled)
sso = ops.wait_for_sso_cookie()
if enable_nsfw:
callbacks.log("[*] 6. 开启 NSFW")
nsfw_ok, nsfw_message = operations.enable_nsfw_for_token(
sso, callbacks.log
)
nsfw_ok, nsfw_msg = ops.enable_nsfw(sso)
if nsfw_ok:
callbacks.log(f"[+] NSFW 开启成功: {nsfw_message}")
callbacks.log(f"[+] NSFW 开启成功: {nsfw_msg}")
else:
callbacks.log(f"[!] NSFW 未开启,继续保存账号: {nsfw_message}")
callbacks.log(f"[!] NSFW 未开启,继续保存账号: {nsfw_msg}")
return RegistrationResult(
ok=True,
email=email,
@@ -157,157 +114,103 @@ def register_one_account(
)
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
def persist_account_result(result, callbacks, ops):
try:
ops.persist_account_line(result.email, result.password, result.sso)
saved = True
save_error = ""
pending_saved = False
except Exception as exc:
saved = False
save_error = str(exc)
pending_saved = ops.queue_unsaved_result(
{
"email": result.email,
"password": result.password,
"sso": result.sso,
"profile": result.profile,
},
save_error,
)
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
)
callbacks.log(f"[!] 账号已注册但主结果文件保存失败: {save_error}")
if pending_saved:
callbacks.log("[!] 未保存账号已写入 pending 队列,等待人工重试")
else:
callbacks.log("[!] pending 队列也写入失败,请立即复制当前账号信息")
pools = ops.add_tokens(result.sso, result.email)
for name, state in pools.items():
if state.get("enabled") and not state.get("ok"):
callbacks.log(f"[!] grok2api {name} 入池失败: {state.get('error')}")
cpa = ops.export_cpa(result.email, result.password, result.sso)
return OutputResult(
registered=True,
saved=True,
save_attempts=used_attempts,
token_pools=token_pools,
saved=saved,
pending_saved=pending_saved,
save_error=save_error,
pools=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)
def run_batch(count, callbacks, observer, ops, enable_nsfw=True, cleanup_interval=5,
max_slot_retry=3, max_mail_retry=3):
result = BatchResult()
retry_count_for_slot = 0
index = 0
operations.start_browser(callbacks.log)
ops.start_browser()
callbacks.log("[*] 浏览器已启动")
try:
while index < count:
while result.processed_count < count:
if callbacks.cancelled():
batch.cancelled = True
result.cancelled = True
break
callbacks.log(f"--- 开始第 {index + 1}/{count} 个账号 ---")
callbacks.log(f"--- 开始第 {result.processed_count + 1}/{count} 个账号 ---")
account = None
output = None
try:
registration = register_one_account(
callbacks,
operations,
enable_nsfw=enable_nsfw,
account = register_one_account(
callbacks, ops, enable_nsfw=enable_nsfw,
max_mail_retry=max_mail_retry,
)
output = persist_account_result(
registration,
output_context,
callbacks,
operations,
)
batch.results.append(registration)
batch.outputs.append(output)
observer.on_result(registration, output)
output = persist_account_result(account, callbacks, ops)
result.results.append({"registration": account, "output": output})
retry_count_for_slot = 0
index += 1
result.processed_count += 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} 个账号,执行定期清理",
)
result.success_count += 1
callbacks.log(f"[+] 注册并保存成功: {account.email}")
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
result.fail_count += 1
callbacks.log(f"[-] 注册成功但持久化未完成: {account.email}")
if result.success_count > 0 and result.success_count % cleanup_interval == 0 and result.processed_count < count:
ops.cleanup(f"已成功 {result.success_count} 个账号,执行定期清理")
except ops.cancelled_exception:
result.cancelled = True
callbacks.log("[!] 注册被停止")
break
except operations.retry_error as exc:
except ops.retry_exception 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}"
)
callbacks.log(f"[!] 当前账号流程卡住,重试第 {retry_count_for_slot}/{max_slot_retry} 次: {exc}")
else:
batch.fail_count += 1
result.fail_count += 1
result.processed_count += 1
retry_count_for_slot = 0
index += 1
callbacks.log(
f"[-] 当前账号已达到最大重试次数,跳过: {exc}"
)
callbacks.log(f"[-] 当前账号已达到最大重试次数,跳过: {exc}")
except Exception as exc:
batch.fail_count += 1
result.fail_count += 1
result.processed_count += 1
retry_count_for_slot = 0
index += 1
callbacks.log(f"[-] 注册失败: {exc}")
finally:
observer.on_stats(
batch.success_count, batch.fail_count
)
observer(result, account, output)
if callbacks.cancelled():
batch.cancelled = True
result.cancelled = True
break
if operations.browser_missing():
operations.start_browser(callbacks.log)
if ops.browser_missing():
ops.start_browser()
else:
operations.restart_browser(callbacks.log)
operations.sleep_with_cancel(1, callbacks.cancelled)
ops.restart_browser()
ops.sleep(1)
finally:
operations.cleanup_runtime_memory(callbacks.log, "任务结束")
return batch
ops.cleanup("任务结束")
return result