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:
1 parent
986bc36f04
commit
d0f06ac93c
7 files changed
+193
-22
No files matched your search
+132
-19
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user