|
@@ -1,12 +1,16 @@
|
|
|
import os
|
|
import os
|
|
|
|
|
+import re
|
|
|
|
|
+import uuid
|
|
|
import random
|
|
import random
|
|
|
import socket
|
|
import socket
|
|
|
import json
|
|
import json
|
|
|
import time
|
|
import time
|
|
|
import string
|
|
import string
|
|
|
|
|
+import shutil
|
|
|
import logging
|
|
import logging
|
|
|
import base64
|
|
import base64
|
|
|
import requests
|
|
import requests
|
|
|
|
|
+import argparse
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
from urllib.parse import urlparse, urlencode
|
|
from urllib.parse import urlparse, urlencode
|
|
|
|
|
|
|
@@ -15,11 +19,16 @@ from cryptography.hazmat.primitives import serialization, hashes
|
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
|
|
|
|
|
|
+# 假设这些是你本地的依赖
|
|
|
|
|
+import configure
|
|
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
|
-from vs_types import RateLimiteddError, BizLogicError
|
|
|
|
|
|
|
+from vs_types import RateLimiteddError, BizLogicError
|
|
|
|
|
+from toolkit.mihomo_tunnel import MihomoTunnel
|
|
|
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
|
|
|
|
|
|
-# --- 配置日志 ---
|
|
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+# 日志与常量配置
|
|
|
|
|
+# ==========================================
|
|
|
logging.basicConfig(
|
|
logging.basicConfig(
|
|
|
level=logging.INFO,
|
|
level=logging.INFO,
|
|
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
|
@@ -27,7 +36,6 @@ logging.basicConfig(
|
|
|
)
|
|
)
|
|
|
logger = logging.getLogger("VFSRegistrar")
|
|
logger = logging.getLogger("VFSRegistrar")
|
|
|
|
|
|
|
|
-# --- 常量 ---
|
|
|
|
|
VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
|
|
VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
|
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
|
|
|
LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
|
|
LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
|
|
@@ -38,73 +46,26 @@ t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
|
|
|
GQIDAQAB
|
|
GQIDAQAB
|
|
|
-----END PUBLIC KEY-----"""
|
|
-----END PUBLIC KEY-----"""
|
|
|
|
|
|
|
|
-def upload_account_to_server(account):
|
|
|
|
|
- """
|
|
|
|
|
- 将注册成功的账号上报到中心服务器
|
|
|
|
|
- """
|
|
|
|
|
- api_url = 'https://api.text.skin/api/account/add'
|
|
|
|
|
- api_token = 'tok_e946329a60ff45ba807f3f41b0e8b7fc' # 你的 Bearer Token
|
|
|
|
|
-
|
|
|
|
|
- # 构造请求头
|
|
|
|
|
- headers = {
|
|
|
|
|
- 'accept': 'application/json',
|
|
|
|
|
- 'Authorization': f'Bearer {api_token}',
|
|
|
|
|
- 'Content-Type': 'application/json'
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- # 构造 extra_data (存放 VFS 特有的国家、领区、手机号信息)
|
|
|
|
|
- extra_payload = {
|
|
|
|
|
- "country_code": account.get("country_code"),
|
|
|
|
|
- "mission_code": account.get("mission_code"),
|
|
|
|
|
- "phone_country_code": account.get("phone_country_code"),
|
|
|
|
|
- "phone_number": account.get("phone_number"),
|
|
|
|
|
- "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- # 构造主 Payload
|
|
|
|
|
- payload = {
|
|
|
|
|
- "pool_name": account.get("pool_name", "default_pool"),
|
|
|
|
|
- "username": account.get("username"),
|
|
|
|
|
- "password": account.get("password"),
|
|
|
|
|
- "extra_data": extra_payload
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- try:
|
|
|
|
|
- logger.info(f"Uploading account {account['username']} to server...")
|
|
|
|
|
- resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
|
|
|
|
|
-
|
|
|
|
|
- if resp.status_code == 200:
|
|
|
|
|
- logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
|
|
|
|
|
- return True
|
|
|
|
|
- else:
|
|
|
|
|
- logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
|
|
|
|
|
- return False
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error(f"❌ [API Upload Error]: {e}")
|
|
|
|
|
- return False
|
|
|
|
|
-
|
|
|
|
|
|
|
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+# 辅助工具类
|
|
|
|
|
+# ==========================================
|
|
|
class VFSHelper:
|
|
class VFSHelper:
|
|
|
- """工具方法的静态类"""
|
|
|
|
|
-
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
def generate_mobile_number(country_code=353, e164_format=False):
|
|
def generate_mobile_number(country_code=353, e164_format=False):
|
|
|
- if country_code == 353: # Ireland
|
|
|
|
|
|
|
+ if country_code == 353: # Ireland
|
|
|
prefix = random.choice(['83', '85', '86', '87', '89'])
|
|
prefix = random.choice(['83', '85', '86', '87', '89'])
|
|
|
number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
|
|
number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
|
|
|
return f"+353{number}" if e164_format else number
|
|
return f"+353{number}" if e164_format else number
|
|
|
-
|
|
|
|
|
- elif country_code == 44: # UK
|
|
|
|
|
|
|
+ elif country_code == 44: # UK
|
|
|
prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
|
|
prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
|
|
|
number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
|
|
number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
|
|
|
return f"+44{number}" if e164_format else number
|
|
return f"+44{number}" if e164_format else number
|
|
|
-
|
|
|
|
|
- elif country_code == 86: # China
|
|
|
|
|
|
|
+ elif country_code == 86: # China
|
|
|
prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
|
|
prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
|
|
|
prefix = random.choice(prefixes)
|
|
prefix = random.choice(prefixes)
|
|
|
number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
|
|
number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
|
|
|
return f"+86{number}" if e164_format else number
|
|
return f"+86{number}" if e164_format else number
|
|
|
-
|
|
|
|
|
return "".join([str(random.randint(0, 9)) for _ in range(10)])
|
|
return "".join([str(random.randint(0, 9)) for _ in range(10)])
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
@@ -139,6 +100,7 @@ class VFSHelper:
|
|
|
payload = f"GA;{timestamp}Z"
|
|
payload = f"GA;{timestamp}Z"
|
|
|
return VFSHelper.encrypt_password(payload)
|
|
return VFSHelper.encrypt_password(payload)
|
|
|
|
|
|
|
|
|
|
+
|
|
|
class BrowserResponse:
|
|
class BrowserResponse:
|
|
|
"""标准化浏览器响应"""
|
|
"""标准化浏览器响应"""
|
|
|
def __init__(self, result_dict):
|
|
def __init__(self, result_dict):
|
|
@@ -157,12 +119,78 @@ class BrowserResponse:
|
|
|
self._json = {}
|
|
self._json = {}
|
|
|
return self._json
|
|
return self._json
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+# API 交互与代理工具
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+def load_proxies(pool_name):
|
|
|
|
|
+ """从 config/proxies.json 读取对应的代理池"""
|
|
|
|
|
+ config_path = os.path.join(os.path.dirname(__file__), 'config', 'proxies.json')
|
|
|
|
|
+ try:
|
|
|
|
|
+ with open(config_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ data = json.load(f)
|
|
|
|
|
+ proxies = data.get(pool_name, [])
|
|
|
|
|
+ if not proxies:
|
|
|
|
|
+ raise ValueError(f"代理池 '{pool_name}' 为空或不存在!")
|
|
|
|
|
+ return proxies
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"读取代理配置文件失败: {e}")
|
|
|
|
|
+ exit(1)
|
|
|
|
|
+
|
|
|
|
|
+def upload_account_to_server(account, api_url, api_token):
|
|
|
|
|
+ """将注册成功的账号上报到中心服务器"""
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'accept': 'application/json',
|
|
|
|
|
+ 'Authorization': f'Bearer {api_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "pool_name": account.get("pool_name"),
|
|
|
|
|
+ "username": account.get("username"),
|
|
|
|
|
+ "password": account.get("password"),
|
|
|
|
|
+ "extra_data": {
|
|
|
|
|
+ "country_code": account.get("country_code"),
|
|
|
|
|
+ "mission_code": account.get("mission_code"),
|
|
|
|
|
+ "phone_country_code": account.get("phone_country_code"),
|
|
|
|
|
+ "phone_number": account.get("phone_number"),
|
|
|
|
|
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ logger.info(f"Uploading account {account['username']} to server...")
|
|
|
|
|
+ resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
|
|
|
|
|
+ if resp.status_code == 200:
|
|
|
|
|
+ logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
|
|
|
|
|
+ return True
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"❌ [API Upload Error]: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+# 核心自动化类
|
|
|
|
|
+# ==========================================
|
|
|
class VFSRegistrationBot:
|
|
class VFSRegistrationBot:
|
|
|
- def __init__(self, config):
|
|
|
|
|
- self.config = config
|
|
|
|
|
- self.page = None
|
|
|
|
|
- self.proxy_url = config.get("proxy_url")
|
|
|
|
|
|
|
+ def __init__(self, vfs_url, proxy, email_api_token, master_email):
|
|
|
|
|
+ self.vfs_url = vfs_url
|
|
|
|
|
+ self.proxy_config = proxy
|
|
|
|
|
+ self.email_api_token = email_api_token
|
|
|
|
|
+ self.master_email = master_email
|
|
|
|
|
|
|
|
|
|
+ self.page = None
|
|
|
|
|
+ self.tunnel = None
|
|
|
|
|
+ self.instance_id = uuid.uuid4().hex[:8]
|
|
|
|
|
+ self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
|
|
|
|
|
+ self.active_proxy_url = None # 供 request 请求复用
|
|
|
|
|
+
|
|
|
|
|
+ def _log(self, msg):
|
|
|
|
|
+ logger.info(f"[TLS-Reg-{self.instance_id}] {msg}")
|
|
|
|
|
+
|
|
|
def _init_browser(self):
|
|
def _init_browser(self):
|
|
|
"""初始化浏览器配置"""
|
|
"""初始化浏览器配置"""
|
|
|
co = ChromiumOptions()
|
|
co = ChromiumOptions()
|
|
@@ -174,30 +202,51 @@ class VFSRegistrationBot:
|
|
|
|
|
|
|
|
co.set_local_port(port)
|
|
co.set_local_port(port)
|
|
|
|
|
|
|
|
- chrome_path = os.getenv("CHROME_BIN")
|
|
|
|
|
- if chrome_path:
|
|
|
|
|
|
|
+ chrome_path = configure.CHROME_PATH or os.getenv("CHROME_BIN")
|
|
|
|
|
+ if chrome_path and os.path.exists(chrome_path):
|
|
|
co.set_paths(browser_path=chrome_path)
|
|
co.set_paths(browser_path=chrome_path)
|
|
|
|
|
|
|
|
- if self.proxy_url:
|
|
|
|
|
- co.set_argument(f'--proxy-server={self.proxy_url}')
|
|
|
|
|
|
|
+ if self.proxy_config and self.proxy_config.get("ip"):
|
|
|
|
|
+ p = self.proxy_config
|
|
|
|
|
+ if p.get('username') and p.get('password'):
|
|
|
|
|
+ self._log(f"Starting Proxy Tunnel for {p.get('ip')}...")
|
|
|
|
|
+ exit_node = {
|
|
|
|
|
+ "name": "ExitNode",
|
|
|
|
|
+ "type": p.get('proto'),
|
|
|
|
|
+ "server": p.get('ip'),
|
|
|
|
|
+ "port": p.get('port'),
|
|
|
|
|
+ "username": p.get('username'),
|
|
|
|
|
+ "password": p.get('password')
|
|
|
|
|
+ }
|
|
|
|
|
+ relay_node = random.choice(configure.MIHOMO_RELAY_NODES) if configure.MIHOMO_RELAY_NODES else None
|
|
|
|
|
+ mihomo_path = configure.MIHOMO_BIN_PATH or os.getenv("MIHOMO_BIN")
|
|
|
|
|
+ if not mihomo_path:
|
|
|
|
|
+ raise BizLogicError('Mihomo path is null. Set mihomo bin path in configure or os env')
|
|
|
|
|
+
|
|
|
|
|
+ self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
|
|
|
|
|
+ self.active_proxy_url = self.tunnel.start()
|
|
|
|
|
+ self._log(f"Tunnel started at {self.active_proxy_url}")
|
|
|
|
|
+ co.set_argument(f'--proxy-server={self.active_proxy_url}')
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.active_proxy_url = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}"
|
|
|
|
|
+ co.set_argument(f'--proxy-server={self.active_proxy_url}')
|
|
|
|
|
+ else:
|
|
|
|
|
+ self._log("[WARN] No proxy configured!")
|
|
|
|
|
|
|
|
- co.headless(False) # VFS 验证码通常需要有头模式
|
|
|
|
|
|
|
+ co.headless(False)
|
|
|
co.set_argument('--no-sandbox')
|
|
co.set_argument('--no-sandbox')
|
|
|
co.set_argument('--disable-gpu')
|
|
co.set_argument('--disable-gpu')
|
|
|
co.set_argument('--disable-dev-shm-usage')
|
|
co.set_argument('--disable-dev-shm-usage')
|
|
|
co.set_argument('--window-size=1920,1080')
|
|
co.set_argument('--window-size=1920,1080')
|
|
|
co.set_argument('--disable-blink-features=AutomationControlled')
|
|
co.set_argument('--disable-blink-features=AutomationControlled')
|
|
|
|
|
|
|
|
- # 创建页面对象
|
|
|
|
|
self.page = ChromiumPage(co)
|
|
self.page = ChromiumPage(co)
|
|
|
- # 设置超时
|
|
|
|
|
self.page.set.timeouts(15)
|
|
self.page.set.timeouts(15)
|
|
|
|
|
|
|
|
def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
|
|
def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
|
|
|
"""注入JS执行Fetch请求,绕过部分指纹检测"""
|
|
"""注入JS执行Fetch请求,绕过部分指纹检测"""
|
|
|
if not self.page:
|
|
if not self.page:
|
|
|
raise BizLogicError("Browser not initialized")
|
|
raise BizLogicError("Browser not initialized")
|
|
|
-
|
|
|
|
|
if retry_count > 3:
|
|
if retry_count > 3:
|
|
|
raise BizLogicError("Max retries exceeded for request")
|
|
raise BizLogicError("Max retries exceeded for request")
|
|
|
|
|
|
|
@@ -214,12 +263,9 @@ class VFSRegistrationBot:
|
|
|
fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
|
|
fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
|
|
|
fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
|
|
|
|
|
|
- logger.debug(f"Request: {method} {url}")
|
|
|
|
|
-
|
|
|
|
|
js_script = f"""
|
|
js_script = f"""
|
|
|
const url = "{url}";
|
|
const url = "{url}";
|
|
|
const options = {json.dumps(fetch_options)};
|
|
const options = {json.dumps(fetch_options)};
|
|
|
-
|
|
|
|
|
return fetch(url, options)
|
|
return fetch(url, options)
|
|
|
.then(async response => {{
|
|
.then(async response => {{
|
|
|
const text = await response.text();
|
|
const text = await response.text();
|
|
@@ -242,7 +288,6 @@ class VFSRegistrationBot:
|
|
|
if resp.status_code == 200:
|
|
if resp.status_code == 200:
|
|
|
return resp
|
|
return resp
|
|
|
|
|
|
|
|
- # 处理 Cloudflare 403 拦截
|
|
|
|
|
if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
|
|
if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
|
|
|
logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
|
|
logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
|
|
|
new_token = self._refresh_turnstile()
|
|
new_token = self._refresh_turnstile()
|
|
@@ -253,28 +298,24 @@ class VFSRegistrationBot:
|
|
|
if resp.status_code == 429:
|
|
if resp.status_code == 429:
|
|
|
raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
|
|
raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
|
|
|
|
|
|
|
|
- return resp # 返回其他状态码供调用者处理 (如 400 业务错误)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ return resp
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"JS Execution Error: {e}")
|
|
logger.error(f"JS Execution Error: {e}")
|
|
|
raise BizLogicError(f"Fetch failed: {e}")
|
|
raise BizLogicError(f"Fetch failed: {e}")
|
|
|
|
|
|
|
|
def _handle_cookie_banner(self):
|
|
def _handle_cookie_banner(self):
|
|
|
- """处理 Cookie 弹窗"""
|
|
|
|
|
try:
|
|
try:
|
|
|
- js = """
|
|
|
|
|
|
|
+ self.page.run_js("""
|
|
|
var btn = document.getElementById('onetrust-accept-btn-handler');
|
|
var btn = document.getElementById('onetrust-accept-btn-handler');
|
|
|
if(btn) { btn.click(); return true; }
|
|
if(btn) { btn.click(); return true; }
|
|
|
var banner = document.getElementById('onetrust-banner-sdk');
|
|
var banner = document.getElementById('onetrust-banner-sdk');
|
|
|
if(banner) { banner.remove(); return true; }
|
|
if(banner) { banner.remove(); return true; }
|
|
|
- """
|
|
|
|
|
- self.page.run_js(js)
|
|
|
|
|
|
|
+ """)
|
|
|
except:
|
|
except:
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
def _refresh_turnstile(self):
|
|
def _refresh_turnstile(self):
|
|
|
- """刷新并获取 Cloudflare Token"""
|
|
|
|
|
- logger.info("Attempting to refresh Turnstile token...")
|
|
|
|
|
|
|
+ self._log("Attempting to refresh Turnstile token...")
|
|
|
try:
|
|
try:
|
|
|
self.page.run_js('try{window.turnstile.reset()}catch(e){}')
|
|
self.page.run_js('try{window.turnstile.reset()}catch(e){}')
|
|
|
cf_bypasser = CloudflareBypasser(self.page, log=True)
|
|
cf_bypasser = CloudflareBypasser(self.page, log=True)
|
|
@@ -284,39 +325,33 @@ class VFSRegistrationBot:
|
|
|
try:
|
|
try:
|
|
|
ele = self.page.ele('@name=cf-turnstile-response')
|
|
ele = self.page.ele('@name=cf-turnstile-response')
|
|
|
if ele and ele.value:
|
|
if ele and ele.value:
|
|
|
- logger.info("Turnstile token obtained.")
|
|
|
|
|
|
|
+ self._log("Turnstile token obtained.")
|
|
|
return ele.value
|
|
return ele.value
|
|
|
except:
|
|
except:
|
|
|
pass
|
|
pass
|
|
|
-
|
|
|
|
|
if i > 5:
|
|
if i > 5:
|
|
|
- try:
|
|
|
|
|
- cf_bypasser.click_verification_button(is_dfs=False)
|
|
|
|
|
- except:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ try: cf_bypasser.click_verification_button(is_dfs=False)
|
|
|
|
|
+ except: pass
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"Turnstile refresh failed: {e}")
|
|
logger.error(f"Turnstile refresh failed: {e}")
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
def _wait_for_activation_link(self, username, max_wait_sec=60):
|
|
def _wait_for_activation_link(self, username, max_wait_sec=60):
|
|
|
- """轮询获取激活链接"""
|
|
|
|
|
- logger.info(f"Waiting for email to {username}...")
|
|
|
|
|
|
|
+ self._log(f"Waiting for email to {username}...")
|
|
|
start_time = time.time()
|
|
start_time = time.time()
|
|
|
-
|
|
|
|
|
- master_email = self.config.get("master_email", "visafly666@gmail.com")
|
|
|
|
|
-
|
|
|
|
|
- # 配置代理
|
|
|
|
|
- proxies = {
|
|
|
|
|
- "http": self.proxy_url,
|
|
|
|
|
- "https": self.proxy_url,
|
|
|
|
|
- } if self.proxy_url else None
|
|
|
|
|
|
|
+
|
|
|
|
|
+ url = "https://api.text.skin/api/email-authorizations/fetch"
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ "Authorization": f"Bearer {self.email_api_token}",
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ "Accept": "application/json, text/plain, */*"
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
while time.time() - start_time < max_wait_sec:
|
|
while time.time() - start_time < max_wait_sec:
|
|
|
try:
|
|
try:
|
|
|
utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
|
utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
-
|
|
|
|
|
params = {
|
|
params = {
|
|
|
- "email": master_email,
|
|
|
|
|
|
|
+ "email": self.master_email,
|
|
|
"sender": 'donotreply at vfsglobal.com',
|
|
"sender": 'donotreply at vfsglobal.com',
|
|
|
"recipient": username,
|
|
"recipient": username,
|
|
|
"subjectKeywords": 'Welcome',
|
|
"subjectKeywords": 'Welcome',
|
|
@@ -325,112 +360,76 @@ class VFSRegistrationBot:
|
|
|
"expiry": str(60)
|
|
"expiry": str(60)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- url = f"https://api.text.skin/api/email-authorizations/fetch"
|
|
|
|
|
- headers = {
|
|
|
|
|
- "Authorization": "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc",
|
|
|
|
|
- "Content-Type": "application/json",
|
|
|
|
|
- "Accept": "application/json, text/plain, */*"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- # 发送请求,添加代理和超时
|
|
|
|
|
- # 注意:如果 body 为空,建议传 json={} 而不是 data=""
|
|
|
|
|
- resp = requests.post(url, headers=headers, params=params, json={}, proxies=proxies, timeout=60)
|
|
|
|
|
|
|
+ # 已经去掉 proxies=proxies 参数
|
|
|
|
|
+ resp = requests.post(url, headers=headers, params=params, json={}, timeout=60)
|
|
|
|
|
|
|
|
- # --- 关键改进:先检查状态码 ---
|
|
|
|
|
if resp.status_code != 200:
|
|
if resp.status_code != 200:
|
|
|
- logger.warning(f"Email API returned status {resp.status_code}. Body: {resp.text[:100]}")
|
|
|
|
|
|
|
+ logger.warning(f"Email API returned status {resp.status_code}.")
|
|
|
time.sleep(15)
|
|
time.sleep(15)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- # --- 关键改进:安全解析 JSON ---
|
|
|
|
|
try:
|
|
try:
|
|
|
result = resp.json()
|
|
result = resp.json()
|
|
|
except json.JSONDecodeError:
|
|
except json.JSONDecodeError:
|
|
|
- logger.error(f"Failed to decode JSON. Response was: {resp.text[:200]}")
|
|
|
|
|
time.sleep(15)
|
|
time.sleep(15)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
if result.get('code') != 0:
|
|
if result.get('code') != 0:
|
|
|
- # 这里的错误通常是业务逻辑错误(如:邮件还没到)
|
|
|
|
|
logger.debug(f"API Message: {result.get('message')}")
|
|
logger.debug(f"API Message: {result.get('message')}")
|
|
|
else:
|
|
else:
|
|
|
- data = result.get('data', {})
|
|
|
|
|
- content = data.get('body', "")
|
|
|
|
|
-
|
|
|
|
|
|
|
+ content = result.get('data', {}).get('body', "")
|
|
|
if content:
|
|
if content:
|
|
|
soup = BeautifulSoup(content, "html.parser")
|
|
soup = BeautifulSoup(content, "html.parser")
|
|
|
link = soup.find("a", string="ActivateAccount")
|
|
link = soup.find("a", string="ActivateAccount")
|
|
|
if link:
|
|
if link:
|
|
|
- raw_link = link["href"]
|
|
|
|
|
- clean_url = raw_link.replace(" ", "").replace("\n", "").replace("\r", "").strip()
|
|
|
|
|
- return clean_url
|
|
|
|
|
-
|
|
|
|
|
|
|
+ return link["href"].replace(" ", "").replace("\n", "").replace("\r", "").strip()
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.warning(f"Error fetching email: {e}")
|
|
logger.warning(f"Error fetching email: {e}")
|
|
|
|
|
|
|
|
time.sleep(15)
|
|
time.sleep(15)
|
|
|
- logger.info("Checking email again...")
|
|
|
|
|
|
|
+ self._log("Checking email again...")
|
|
|
|
|
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
def register(self, account):
|
|
def register(self, account):
|
|
|
- """执行单个账号注册"""
|
|
|
|
|
- website = self.config['website']
|
|
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
self._init_browser()
|
|
self._init_browser()
|
|
|
- logger.info(f"Opening {website}")
|
|
|
|
|
|
|
+ self._log(f"Opening {self.vfs_url}")
|
|
|
|
|
|
|
|
- # 1. 设置超时
|
|
|
|
|
self.page.set.timeouts(page_load=30, script=30)
|
|
self.page.set.timeouts(page_load=30, script=30)
|
|
|
-
|
|
|
|
|
- # 2. 尝试打开页面
|
|
|
|
|
try:
|
|
try:
|
|
|
- self.page.get(website, retry=0, timeout=30)
|
|
|
|
|
|
|
+ self.page.get(self.vfs_url, retry=0, timeout=30)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- # 超时强制停止,防止卡死
|
|
|
|
|
- logger.warning(f"Page load timed out (Stopped manually). Checking URL...")
|
|
|
|
|
|
|
+ logger.warning("Page load timed out. Stopping loading manually.")
|
|
|
self.page.stop_loading()
|
|
self.page.stop_loading()
|
|
|
|
|
|
|
|
- # 1. 过盾
|
|
|
|
|
cf_token = None
|
|
cf_token = None
|
|
|
cf_bypasser = CloudflareBypasser(self.page, log=True)
|
|
cf_bypasser = CloudflareBypasser(self.page, log=True)
|
|
|
|
|
|
|
|
for _ in range(40):
|
|
for _ in range(40):
|
|
|
time.sleep(1)
|
|
time.sleep(1)
|
|
|
-
|
|
|
|
|
- current_url = self.page.url
|
|
|
|
|
- if "page-not-found" in current_url:
|
|
|
|
|
- logger.error(f"❌ [BLOCKED] Redirected to 'Page Not Found' during check. Aborting.")
|
|
|
|
|
|
|
+ if "page-not-found" in self.page.url:
|
|
|
|
|
+ logger.error("❌ [BLOCKED] Redirected to 'Page Not Found'. Aborting.")
|
|
|
return False
|
|
return False
|
|
|
-
|
|
|
|
|
- # 如果页面标题变成 403 Forbidden
|
|
|
|
|
if "403" in self.page.title and "Just a moment" not in self.page.title:
|
|
if "403" in self.page.title and "Just a moment" not in self.page.title:
|
|
|
- logger.error(f"❌ [BLOCKED] 403 Forbidden detected. Aborting.")
|
|
|
|
|
|
|
+ logger.error("❌ [BLOCKED] 403 Forbidden detected. Aborting.")
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
self._handle_cookie_banner()
|
|
self._handle_cookie_banner()
|
|
|
|
|
|
|
|
- # 尝试获取 Token
|
|
|
|
|
try:
|
|
try:
|
|
|
ele = self.page.ele('@name=cf-turnstile-response')
|
|
ele = self.page.ele('@name=cf-turnstile-response')
|
|
|
if ele and ele.value:
|
|
if ele and ele.value:
|
|
|
cf_token = ele.value
|
|
cf_token = ele.value
|
|
|
- if cf_token:
|
|
|
|
|
- break
|
|
|
|
|
- except:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ break
|
|
|
|
|
+ except: pass
|
|
|
|
|
|
|
|
- # 尝试点击
|
|
|
|
|
- try:
|
|
|
|
|
- cf_bypasser.click_verification_button(is_dfs=False)
|
|
|
|
|
- except:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ try: cf_bypasser.click_verification_button(is_dfs=False)
|
|
|
|
|
+ except: pass
|
|
|
|
|
|
|
|
if not cf_token:
|
|
if not cf_token:
|
|
|
raise BizLogicError("Failed to obtain initial Cloudflare token")
|
|
raise BizLogicError("Failed to obtain initial Cloudflare token")
|
|
|
|
|
|
|
|
- # 2. 构造注册请求
|
|
|
|
|
post_data = {
|
|
post_data = {
|
|
|
'emailid': account['username'],
|
|
'emailid': account['username'],
|
|
|
'password': VFSHelper.encrypt_password(account['password']),
|
|
'password': VFSHelper.encrypt_password(account['password']),
|
|
@@ -456,48 +455,43 @@ class VFSRegistrationBot:
|
|
|
'clientsource': VFSHelper.get_client_source(),
|
|
'clientsource': VFSHelper.get_client_source(),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- logger.info(f"Submitting registration for {account['username']}")
|
|
|
|
|
|
|
+ self._log(f"Submitting registration for {account['username']}")
|
|
|
|
|
+
|
|
|
|
|
+ country_code = account.get('country_code', '').lower()
|
|
|
|
|
+ if country_code == 'chn':
|
|
|
|
|
+ api_domain = 'https://lift-apicn.vfsglobal.com'
|
|
|
|
|
+ else:
|
|
|
|
|
+ api_domain = 'https://lift-api.vfsglobal.com'
|
|
|
|
|
+
|
|
|
|
|
+ register_endpoint = f"{api_domain}/user/registration"
|
|
|
|
|
+ self._log(f"Using API Endpoint: {register_endpoint}")
|
|
|
|
|
+
|
|
|
resp = self._perform_js_fetch(
|
|
resp = self._perform_js_fetch(
|
|
|
'POST',
|
|
'POST',
|
|
|
- 'https://lift-api.vfsglobal.com/user/registration',
|
|
|
|
|
|
|
+ register_endpoint,
|
|
|
headers=headers,
|
|
headers=headers,
|
|
|
json_data=post_data
|
|
json_data=post_data
|
|
|
)
|
|
)
|
|
|
- logger.info(f"Registration response: {resp.text}")
|
|
|
|
|
|
|
+
|
|
|
|
|
+ self._log(f'register resp={resp.text}')
|
|
|
resp_data = resp.json()
|
|
resp_data = resp.json()
|
|
|
|
|
+
|
|
|
if resp_data.get("code") == "200":
|
|
if resp_data.get("code") == "200":
|
|
|
- logger.info("Registration API success. Waiting for email...")
|
|
|
|
|
-
|
|
|
|
|
|
|
+ self._log("Registration API success. Waiting for email...")
|
|
|
activate_link = self._wait_for_activation_link(account['username'])
|
|
activate_link = self._wait_for_activation_link(account['username'])
|
|
|
|
|
+
|
|
|
if activate_link:
|
|
if activate_link:
|
|
|
- logger.info(f"Activating account: {activate_link}")
|
|
|
|
|
- # 在当前浏览器上下文中打开链接,保持环境一致性
|
|
|
|
|
- # === 关键步骤:打开新标签页并验证结果 ===
|
|
|
|
|
- # 打开新标签页
|
|
|
|
|
|
|
+ self._log(f"Activating account: {activate_link}")
|
|
|
activate_tab = self.page.new_tab(activate_link)
|
|
activate_tab = self.page.new_tab(activate_link)
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
- # 等待页面加载并查找 "Activation Successful" 文本
|
|
|
|
|
- # timeout=30 表示最多等待 30 秒
|
|
|
|
|
- logger.info("Waiting for 'Activation Successful' message on page...")
|
|
|
|
|
-
|
|
|
|
|
- # DrissionPage 查找包含特定文本的元素
|
|
|
|
|
success_ele = activate_tab.ele('Activation Successful', timeout=30)
|
|
success_ele = activate_tab.ele('Activation Successful', timeout=30)
|
|
|
-
|
|
|
|
|
if success_ele:
|
|
if success_ele:
|
|
|
- logger.info(f"✅ Account {account['username']} activated successfully (Verified).")
|
|
|
|
|
|
|
+ logger.info(f"✅ Account {account['username']} activated successfully.")
|
|
|
return True
|
|
return True
|
|
|
else:
|
|
else:
|
|
|
- # 如果没找到成功提示,尝试读取页面内容找错误原因
|
|
|
|
|
- body_text = activate_tab.ele('tag:body').text[:200]
|
|
|
|
|
- logger.error(f"Activation verification failed. Page text: {body_text}")
|
|
|
|
|
|
|
+ logger.error("Activation verification failed. Success text not found.")
|
|
|
return False
|
|
return False
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- logger.error(f"Error checking activation status: {e}")
|
|
|
|
|
- return False
|
|
|
|
|
finally:
|
|
finally:
|
|
|
- # 无论成功失败,关闭激活标签页,切回主标签
|
|
|
|
|
activate_tab.close()
|
|
activate_tab.close()
|
|
|
else:
|
|
else:
|
|
|
logger.error("Timeout waiting for activation email.")
|
|
logger.error("Timeout waiting for activation email.")
|
|
@@ -506,75 +500,117 @@ class VFSRegistrationBot:
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.error(f"Registration process exception: {e}", exc_info=True)
|
|
logger.error(f"Registration process exception: {e}", exc_info=True)
|
|
|
- finally:
|
|
|
|
|
- if self.page:
|
|
|
|
|
- try:
|
|
|
|
|
- self.page.quit()
|
|
|
|
|
- except:
|
|
|
|
|
- pass
|
|
|
|
|
return False
|
|
return False
|
|
|
|
|
+
|
|
|
|
|
+ def cleanup(self):
|
|
|
|
|
+ self._log("Cleaning up resources...")
|
|
|
|
|
+ if self.page:
|
|
|
|
|
+ try: self.page.quit()
|
|
|
|
|
+ except: pass
|
|
|
|
|
+ if self.tunnel:
|
|
|
|
|
+ try: self.tunnel.stop()
|
|
|
|
|
+ except: pass
|
|
|
|
|
+ if os.path.exists(self.workspace):
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+ shutil.rmtree(self.workspace, ignore_errors=True)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+# 主流程 & 命令行参数解析
|
|
|
|
|
+# ==========================================
|
|
|
|
|
+def parse_arguments():
|
|
|
|
|
+ parser = argparse.ArgumentParser(description="VFS Global 自动注册机器人")
|
|
|
|
|
+
|
|
|
|
|
+ # 核心参数
|
|
|
|
|
+ parser.add_argument('--target', type=int, default=20, help="需要注册的目标账号数量")
|
|
|
|
|
+ parser.add_argument('--vfs-url', type=str, required=True, help="VFS 注册页面的完整 URL, 如: https://visa.vfsglobal.com/chn/en/aut/register")
|
|
|
|
|
+ parser.add_argument('--proxy-pool', type=str, default='proxy-cheap', help="使用的代理池名称 (在 proxies.json 中定义)")
|
|
|
|
|
+
|
|
|
|
|
+ # 账号生成相关参数
|
|
|
|
|
+ parser.add_argument('--account-prefix', type=str, default="cn_at", help="生成的邮箱前缀")
|
|
|
|
|
+ parser.add_argument('--account-pool', type=str, default="cn.at.sentinel", help="上报到服务端的账号池名称")
|
|
|
|
|
+ parser.add_argument('--phone-country', type=int, default=86, help="生成的手机号国家代码")
|
|
|
|
|
+ parser.add_argument('--email-domain', type=str, default="text.skin", help="邮箱域名")
|
|
|
|
|
+
|
|
|
|
|
+ # API 与 上报配置
|
|
|
|
|
+ parser.add_argument('--upload-url', type=str, default="https://api.text.skin/api/account/add", help="账号上报服务器 URL")
|
|
|
|
|
+ parser.add_argument('--upload-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="账号上报服务器 API Token")
|
|
|
|
|
+ parser.add_argument('--email-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="获取邮件的 API Token")
|
|
|
|
|
+ parser.add_argument('--master-email', type=str, default="hujiarui8@gmail.com", help="接收验证邮件的主邮箱")
|
|
|
|
|
+
|
|
|
|
|
+ return parser.parse_args()
|
|
|
|
|
|
|
|
-# --- 主流程 ---
|
|
|
|
|
|
|
|
|
|
-def generate_account_details(config):
|
|
|
|
|
- """生成账号数据字典"""
|
|
|
|
|
- account_prefix = config.get('account_prefix', 'vfs')
|
|
|
|
|
- pool_name = config.get('pool_name', 'vfs')
|
|
|
|
|
|
|
+def generate_account_details(args):
|
|
|
|
|
+ """根据命令行参数和 URL 生成账号数据字典"""
|
|
|
|
|
+ # 从 URL 中提取 from_country 和 to_country
|
|
|
|
|
+ match = re.search(r'vfsglobal\.com/([^/]+)/[^/]+/([^/]+)/register', args.vfs_url)
|
|
|
|
|
+ if not match:
|
|
|
|
|
+ raise ValueError(f"无法从 URL 解析国家代码: {args.vfs_url}")
|
|
|
|
|
+
|
|
|
|
|
+ from_country = match.group(1)
|
|
|
|
|
+ to_country = match.group(2)
|
|
|
|
|
+
|
|
|
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
|
- username = f"{account_prefix}_{rand_suffix}@{config['email_domain']}.com"
|
|
|
|
|
- phone = VFSHelper.generate_mobile_number(config['phone_country_code'])
|
|
|
|
|
|
|
+ username = f"{args.account_prefix}_{rand_suffix}@{args.email_domain}"
|
|
|
|
|
+ phone = VFSHelper.generate_mobile_number(args.phone_country)
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- 'pool_name': pool_name,
|
|
|
|
|
- 'country_code': config['country_code'],
|
|
|
|
|
- 'mission_code': config['mission_code'],
|
|
|
|
|
- 'phone_country_code': config['phone_country_code'],
|
|
|
|
|
|
|
+ 'pool_name': args.account_pool,
|
|
|
|
|
+ 'country_code': from_country,
|
|
|
|
|
+ 'mission_code': to_country,
|
|
|
|
|
+ 'phone_country_code': args.phone_country,
|
|
|
'phone_number': phone,
|
|
'phone_number': phone,
|
|
|
'username': username,
|
|
'username': username,
|
|
|
'password': VFSHelper.generate_password(),
|
|
'password': VFSHelper.generate_password(),
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
def main():
|
|
def main():
|
|
|
- # 配置
|
|
|
|
|
- config = {
|
|
|
|
|
- "pool_name": "ie.nl.sentinel",
|
|
|
|
|
- "account_prefix": "ie_nl",
|
|
|
|
|
- "email_domain": "gmail-app",
|
|
|
|
|
- "master_email": "visafly666@gmail.com",
|
|
|
|
|
- "proxy_url": "http://127.0.0.1:7890",
|
|
|
|
|
- "target_count": 10,
|
|
|
|
|
- "phone_country_code": 353,
|
|
|
|
|
- "country_code": "irl",
|
|
|
|
|
- "mission_code": "nld",
|
|
|
|
|
- "website": "https://visa.vfsglobal.com/irl/en/nld/register",
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ args = parse_arguments()
|
|
|
|
|
+ proxies = load_proxies(args.proxy_pool)
|
|
|
|
|
+ logger.info(f"[*] 成功加载代理数量: {len(proxies)} 个")
|
|
|
|
|
|
|
|
- bot = VFSRegistrationBot(config)
|
|
|
|
|
success_accounts = []
|
|
success_accounts = []
|
|
|
|
|
+ logger.info(">>> Starting Registration Bot <<<")
|
|
|
|
|
|
|
|
- print(">>> Starting Registration Bot <<<")
|
|
|
|
|
-
|
|
|
|
|
- while len(success_accounts) < config['target_count']:
|
|
|
|
|
- account = generate_account_details(config)
|
|
|
|
|
- logger.info(f"Processing Account: {account['username']}")
|
|
|
|
|
-
|
|
|
|
|
- is_success = bot.register(account)
|
|
|
|
|
|
|
+ while len(success_accounts) < args.target:
|
|
|
|
|
+ proxy_config = random.choice(proxies)
|
|
|
|
|
+ account_detail = generate_account_details(args)
|
|
|
|
|
|
|
|
- if is_success:
|
|
|
|
|
- success_accounts.append(account)
|
|
|
|
|
- logger.info(f"Progress: {len(success_accounts)}/{config['target_count']}")
|
|
|
|
|
|
|
+ logger.info(f"Processing Account: {account_detail['username']}")
|
|
|
|
|
+ bot = None
|
|
|
|
|
+ try:
|
|
|
|
|
+ bot = VFSRegistrationBot(
|
|
|
|
|
+ vfs_url=args.vfs_url,
|
|
|
|
|
+ proxy=proxy_config,
|
|
|
|
|
+ email_api_token=args.email_token,
|
|
|
|
|
+ master_email=args.master_email
|
|
|
|
|
+ )
|
|
|
|
|
+ is_success = bot.register(account_detail)
|
|
|
|
|
|
|
|
- upload_account_to_server(account)
|
|
|
|
|
- # 保存结果到文件,防止中途退出丢失
|
|
|
|
|
- with open("registered_accounts.json", "w", encoding='utf-8') as f:
|
|
|
|
|
- json.dump(success_accounts, f, indent=4, ensure_ascii=False)
|
|
|
|
|
- else:
|
|
|
|
|
- logger.warning("Retrying with new account details...")
|
|
|
|
|
|
|
+ if is_success:
|
|
|
|
|
+ success_accounts.append(account_detail)
|
|
|
|
|
+ logger.info(f"Progress: {len(success_accounts)}/{args.target}")
|
|
|
|
|
+
|
|
|
|
|
+ upload_account_to_server(account_detail, args.upload_url, args.upload_token)
|
|
|
|
|
+
|
|
|
|
|
+ # 实时保存结果,防止中途退出丢失
|
|
|
|
|
+ with open("registered_accounts.json", "w", encoding='utf-8') as f:
|
|
|
|
|
+ json.dump(success_accounts, f, indent=4, ensure_ascii=False)
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning("Registration failed. Retrying with new account details...")
|
|
|
|
|
|
|
|
- # 稍微暂停,避免请求过于频繁
|
|
|
|
|
- time.sleep(5)
|
|
|
|
|
|
|
+ time.sleep(5) # 避免请求过于频繁
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"[ERROR] 注册发生致命错误 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ finally:
|
|
|
|
|
+ if bot:
|
|
|
|
|
+ bot.cleanup()
|
|
|
|
|
|
|
|
- print(">>> All tasks completed <<<")
|
|
|
|
|
|
|
+ logger.info(f">>> All tasks completed. Registered {len(success_accounts)} accounts. <<<")
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if __name__ == "__main__":
|
|
|
main()
|
|
main()
|