feat: bot_flag 检测停机、注册间隔与 CLI 参数

注册成功后解析 access_token 的 bot_flag_source,命中则立即停止;
支持 register_interval_sec 与 argparse(count/concurrency/interval/hide-window);
邮箱 local-part 强制 z 起首,并关闭 AutomationControlled 特征。
This commit is contained in:
chaos committed 2026-07-16 21:00:35 +08:00
1 parent 986bc36f04
commit d0f06ac93c
7 files changed
+193 -22

No files matched your search

+132 -19
View File
@@ -17,6 +17,7 @@ import random
import re
import string
import json
import base64
from DrissionPage import Chromium, ChromiumOptions
from DrissionPage.errors import PageDisconnectedError
@@ -586,13 +587,17 @@ def create_browser_options(proxy=None):
options.set_argument("--disable-background-timer-throttling")
options.set_argument("--disable-backgrounding-occluded-windows")
options.set_argument("--disable-renderer-backgrounding")
# 反 bot:关闭 Blink AutomationControlled(否则 navigator.webdriver 在 JS 前就暴露)
options.set_argument("--disable-blink-features=AutomationControlled")
# 固定窗口尺寸,避免 Turnstile iframe 在 0 尺寸窗口里不渲染
# 轻微随机化位置即可,尺寸过大/过小都可能影响验证渲染
options.set_argument("--window-size=1280,900")
# 隐藏窗口(可选):仅移出屏幕。注意:部分环境下会让 Turnstile 永远 token=0
if config.get("hide_window", False):
options.set_argument("--window-position=-32000,-32000")
else:
options.set_argument("--window-position=40,40")
options.set_argument(f"--window-position={random.randint(40, 120)},{random.randint(40, 120)}")
# 代理:线程绑定(代理池)> 入参 > config.proxy
use_proxy = ""
@@ -1125,13 +1130,25 @@ _LAST_NAMES = (
)
_NAME_SEPARATORS = ("", ".", "_")
# z 开头的常见英文名字,用于生成 local-part 全部以 z 起首的邮箱
_Z_FIRST_NAMES = (
"zachary", "zack", "zackary", "zackery", "zane", "zayn", "zayne", "zavier",
"zaid", "zaiden", "zayden", "zaire", "zeke", "zephyr", "zeus", "zion",
"ziggy", "zed", "zeb", "zebulon", "zelig", "zeno", "zenon", "zaki",
"zoe", "zoey", "zoie", "zoya", "zora", "zara", "zaria", "zariah",
"zelda", "zena", "zenia", "zinnia", "ziva", "zola", "zainab", "zaynab",
"zahra", "zaida", "zanna", "zara", "zarah", "zaria", "zia", "zula",
)
def generate_username(length=10):
"""生成更像正常英文用户的邮箱名:姓名/词根 + 数字结尾。
length 仅作兼容参数(旧调用传 10),实际长度由名字与数字后缀决定,约 8–18。
强制 local-part 以 'z' 起首(first 从 z 开头姓名池抽取;last+first[0] 风格改为
z + last,仍保证首字符为 z)。
"""
first = secrets.choice(_FIRST_NAMES)
first = secrets.choice(_Z_FIRST_NAMES)
last = secrets.choice(_LAST_NAMES)
sep = secrets.choice(_NAME_SEPARATORS)
# 数字后缀:2–4 位更像真实用户(生日年份、学号尾号等)
@@ -1163,8 +1180,8 @@ def generate_username(length=10):
# first 首字母 + last + digits e.g. jwilson87
base = f"{first[0]}{last}"
else:
# last + first 首字母 + digits e.g. wilsonj87
base = f"{last}{first[0]}"
# z + last + digits e.g. zwilson87(原 last+first[0] 风格改造以保证 z 起首)
base = f"z{last}"
local = f"{base}{digits}".lower()
# 邮箱 local 只保留 [a-z0-9._],去掉连续分隔符
@@ -2500,7 +2517,7 @@ def fill_email_and_submit(session, timeout=45, log_callback=None, cancel_callbac
while time.time() < deadline:
raise_if_cancelled(cancel_callback)
filled = page.run_js(
"""
r"""
const email = arguments[0];
function isVisible(node) {
if (!node) return false;
@@ -3836,6 +3853,37 @@ def register_one(session, shared, worker_id, slot_no):
return email
def _check_bot_flag(email, cfg=None):
"""检查刚注册账号的 access_token 是否带 bot_flag_source。
返回 (is_bot: bool, detail: str)。
"""
cfg = cfg or config
try:
import cpa
out_dir = str(cfg.get("cpa_auth_dir", "./cpa_auths"))
fname = cpa.credential_file_name(email)
path = os.path.join(out_dir, fname)
if not os.path.exists(path):
return False, f"auth file not found: {path}"
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
at = str(data.get("access_token") or "")
if not at:
return False, "no access_token"
parts = at.split(".")
if len(parts) < 2:
return False, "invalid JWT"
seg = parts[1] + "=" * (-len(parts[1]) % 4)
payload = json.loads(base64.urlsafe_b64decode(seg))
bot = payload.get("bot_flag_source")
if bot == 1:
return True, "bot_flag_source=1 → 账号被标记为 bot"
return False, f"bot_flag_source={bot} (OK)"
except Exception as e:
return False, f"check error: {e}"
def register_worker(worker_id, shared):
"""单个并发 worker:自建独立浏览器,循环领取名额并注册。"""
@@ -3878,6 +3926,18 @@ def register_worker(worker_id, shared):
except Exception:
pass
log(f"[+] 注册成功: {email}(累计成功 {total})")
# ===== 注册后立即检查 bot 标记 =====
if config.get("stop_on_bot_flag", True):
is_bot, detail = _check_bot_flag(email)
if is_bot:
log(f"[!] ⛔ 检测到 bot 标记: {detail}")
log("[!] 立即停止所有注册(避免继续浪费账号)")
shared.stop()
raise RegistrationCancelled("检测到 bot_flag,停止注册")
else:
log(f"[Debug] bot 检查通过: {detail}")
break
except RegistrationCancelled:
raise
@@ -3928,6 +3988,13 @@ def register_worker(worker_id, shared):
break
if shared.should_stop():
break
# 注册间隔:控制注册速率,降低被标记为 bot 的风险
interval = float(config.get("register_interval_sec", 0) or 0)
if interval > 0:
log(f"[*] 等待 {interval:.0f}s 后开始下一个账号(register_interval_sec)")
sleep_with_cancel(interval, shared.should_stop)
if shared.should_stop():
break
# 下一账号:默认轮换出口并重启浏览器
session.restart()
except RegistrationCancelled:
@@ -3984,11 +4051,55 @@ def run_registration_concurrent(count, concurrency):
def main():
import argparse
parser = argparse.ArgumentParser(
description="Grok 注册机 - 批量注册 xAI/Grok 账号",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python grok_register_ttk.py # 交互模式
python grok_register_ttk.py --count 10 --concurrency 2 # 注册10个,2并发
python grok_register_ttk.py --count 50 --interval 120 # 每2分钟注册1个
python grok_register_ttk.py --no-stop-on-bot # 不自动停(调试用)
""",
)
parser.add_argument("--count", type=int, default=None,
help="注册数量(默认读取 config.json 或 1)")
parser.add_argument("--concurrency", type=int, default=None,
help="并发浏览器数(默认交互输入)")
parser.add_argument("--interval", type=int, default=None, dest="register_interval_sec",
help="注册间隔秒数(如 120=每2分钟1个,0=无间隔)")
parser.add_argument("--stop-on-bot", action="store_true", default=None, dest="stop_on_bot_flag",
help="检测到 bot_flag 立即停止(默认开启)")
parser.add_argument("--no-stop-on-bot", action="store_false", default=None, dest="stop_on_bot_flag",
help="不自动停止(调试用)")
parser.add_argument("--hide-window", action="store_true", default=None, dest="hide_window",
help="隐藏浏览器窗口(移到屏幕外,非 headless)")
parser.add_argument("--show-window", action="store_false", default=None, dest="hide_window",
help="显示浏览器窗口(默认)")
args = parser.parse_args()
load_config()
count = int(config.get("register_count", 1) or 1)
count = args.count if args.count is not None else int(config.get("register_count", 1) or 1)
# 命令行参数覆盖到 config(后续代码统一读 config)
if args.register_interval_sec is not None:
config["register_interval_sec"] = args.register_interval_sec
if args.stop_on_bot_flag is not None:
config["stop_on_bot_flag"] = args.stop_on_bot_flag
if args.hide_window is not None:
config["hide_window"] = args.hide_window
# 默认值
config.setdefault("register_interval_sec", 0)
config.setdefault("stop_on_bot_flag", True)
cli_log("[*] 已加载配置")
interval = config.get("register_interval_sec", 0)
cli_log(
f"[*] 当前邮箱服务商: {config.get('email_provider', 'duckmail')} | 目标注册数: {count}"
f"[*] 邮箱: {config.get('email_provider', 'duckmail')} | "
f"目标: {count} | 间隔: {'{}s'.format(interval) if interval else '无'} | "
f"bot检测: {'开' if config.get('stop_on_bot_flag') else '关'} | "
f"窗口: {'隐藏' if config.get('hide_window') else '显示'}"
)
try:
import proxy_pool
@@ -4005,18 +4116,20 @@ def main():
)
else:
cli_log("[*] CPA 导出: 关闭(仅写 accounts_*.txt)")
try:
raw = input("请输入并发数量(同时开几个浏览器,直接回车=1): ").strip()
except (KeyboardInterrupt, EOFError):
cli_log("[!] 已取消")
return
try:
concurrency = int(raw) if raw else 1
except ValueError:
cli_log("[!] 并发数量无效,使用 1")
concurrency = 1
if concurrency < 1:
concurrency = 1
concurrency = args.concurrency
if concurrency is None:
try:
raw = input("请输入并发数量(同时开几个浏览器,直接回车=1): ").strip()
except (KeyboardInterrupt, EOFError):
cli_log("[!] 已取消")
return
try:
concurrency = int(raw) if raw else 1
except ValueError:
cli_log("[!] 并发数量无效,使用 1")
concurrency = 1
concurrency = max(1, min(concurrency, count))
run_registration_concurrent(count, concurrency)