vfs_registration_bot.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import os
  2. import re
  3. import uuid
  4. import random
  5. import socket
  6. import json
  7. import time
  8. import string
  9. import shutil
  10. import logging
  11. import base64
  12. import requests
  13. import argparse
  14. from datetime import datetime, timezone
  15. from urllib.parse import urlparse, urlencode
  16. from bs4 import BeautifulSoup
  17. from cryptography.hazmat.primitives import serialization, hashes
  18. from cryptography.hazmat.primitives.asymmetric import padding
  19. from cryptography.hazmat.backends import default_backend
  20. # 假设这些是你本地的依赖
  21. import configure
  22. from DrissionPage import ChromiumPage, ChromiumOptions
  23. from vs_types import RateLimiteddError, BizLogicError
  24. from toolkit.mihomo_tunnel import MihomoTunnel
  25. from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
  26. # ==========================================
  27. # 日志与常量配置
  28. # ==========================================
  29. logging.basicConfig(
  30. level=logging.INFO,
  31. format='%(asctime)s [%(levelname)s] %(message)s',
  32. datefmt='%H:%M:%S'
  33. )
  34. logger = logging.getLogger("VFSRegistrar")
  35. VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
  36. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
  37. LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
  38. t59GV321kN0iquYRHrdh3ygfDDHlS9rROQeBqRga0ncSADtbLMrBPqXJjPCoV76y
  39. t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
  40. 1e5wXLfvpj9w1QsiSWOSJxLOyICz/9ByxXycQQFdNmjnnnwco9Gt/Mi33NYH71j0
  41. 5oXIjklFC4lvJqaqSY5lS7Vwb9oCt9zX9J0Yz4z4e/3V+0jgRnWOFGofyks4FKe2
  42. GQIDAQAB
  43. -----END PUBLIC KEY-----"""
  44. # ==========================================
  45. # 辅助工具类
  46. # ==========================================
  47. class VFSHelper:
  48. @staticmethod
  49. def generate_mobile_number(country_code=353, e164_format=False):
  50. if country_code == 353: # Ireland
  51. prefix = random.choice(['83', '85', '86', '87', '89'])
  52. number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
  53. return f"+353{number}" if e164_format else number
  54. elif country_code == 44: # UK
  55. prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
  56. number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
  57. return f"+44{number}" if e164_format else number
  58. elif country_code == 86: # China
  59. prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
  60. prefix = random.choice(prefixes)
  61. number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
  62. return f"+86{number}" if e164_format else number
  63. return "".join([str(random.randint(0, 9)) for _ in range(10)])
  64. @staticmethod
  65. def generate_password(length=12):
  66. chars = string.ascii_letters + string.digits + "@#$%"
  67. while True:
  68. pwd = ''.join(random.choices(chars, k=length))
  69. if (any(c.islower() for c in pwd) and
  70. any(c.isupper() for c in pwd) and
  71. any(c.isdigit() for c in pwd) and
  72. any(c in "@#$%" for c in pwd)):
  73. return pwd
  74. @staticmethod
  75. def encrypt_password(password: str) -> str:
  76. public_key = serialization.load_pem_public_key(
  77. VFS_PUBLIC_KEY_PEM.encode(), backend=default_backend()
  78. )
  79. ciphertext = public_key.encrypt(
  80. password.encode(),
  81. padding.OAEP(
  82. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  83. algorithm=hashes.SHA256(),
  84. label=None
  85. )
  86. )
  87. return base64.b64encode(ciphertext).decode()
  88. @staticmethod
  89. def get_client_source() -> str:
  90. timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
  91. payload = f"GA;{timestamp}Z"
  92. return VFSHelper.encrypt_password(payload)
  93. class BrowserResponse:
  94. """标准化浏览器响应"""
  95. def __init__(self, result_dict):
  96. result_dict = result_dict or {}
  97. self.status_code = result_dict.get('status', 0)
  98. self.text = result_dict.get('body', '')
  99. self.headers = result_dict.get('headers', {})
  100. self.url = result_dict.get('url', '')
  101. self._json = None
  102. def json(self):
  103. if self._json is None:
  104. try:
  105. self._json = json.loads(self.text) if self.text else {}
  106. except json.JSONDecodeError:
  107. self._json = {}
  108. return self._json
  109. # ==========================================
  110. # API 交互与代理工具
  111. # ==========================================
  112. def load_proxies(pool_name):
  113. """从 config/proxies.json 读取对应的代理池"""
  114. config_path = os.path.join(os.path.dirname(__file__), 'config', 'proxies.json')
  115. try:
  116. with open(config_path, 'r', encoding='utf-8') as f:
  117. data = json.load(f)
  118. proxies = data.get(pool_name, [])
  119. if not proxies:
  120. raise ValueError(f"代理池 '{pool_name}' 为空或不存在!")
  121. return proxies
  122. except Exception as e:
  123. logger.error(f"读取代理配置文件失败: {e}")
  124. exit(1)
  125. def upload_account_to_server(account, api_url, api_token):
  126. """将注册成功的账号上报到中心服务器"""
  127. headers = {
  128. 'accept': 'application/json',
  129. 'Authorization': f'Bearer {api_token}',
  130. 'Content-Type': 'application/json'
  131. }
  132. payload = {
  133. "pool_name": account.get("pool_name"),
  134. "username": account.get("username"),
  135. "password": account.get("password"),
  136. "extra_data": {
  137. "country_code": account.get("country_code"),
  138. "mission_code": account.get("mission_code"),
  139. "phone_country_code": account.get("phone_country_code"),
  140. "phone_number": account.get("phone_number"),
  141. "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  142. }
  143. }
  144. try:
  145. logger.info(f"Uploading account {account['username']} to server...")
  146. resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
  147. if resp.status_code == 200:
  148. logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
  149. return True
  150. else:
  151. logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
  152. return False
  153. except Exception as e:
  154. logger.error(f"❌ [API Upload Error]: {e}")
  155. return False
  156. # ==========================================
  157. # 核心自动化类
  158. # ==========================================
  159. class VFSRegistrationBot:
  160. def __init__(self, vfs_url, proxy, email_api_token, master_email):
  161. self.vfs_url = vfs_url
  162. self.proxy_config = proxy
  163. self.email_api_token = email_api_token
  164. self.master_email = master_email
  165. self.page = None
  166. self.tunnel = None
  167. self.instance_id = uuid.uuid4().hex[:8]
  168. self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
  169. self.active_proxy_url = None # 供 request 请求复用
  170. def _log(self, msg):
  171. logger.info(f"[TLS-Reg-{self.instance_id}] {msg}")
  172. def _init_browser(self):
  173. """初始化浏览器配置"""
  174. co = ChromiumOptions()
  175. # 查找可用端口
  176. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  177. s.bind(('', 0))
  178. port = s.getsockname()[1]
  179. co.set_local_port(port)
  180. chrome_path = configure.CHROME_PATH or os.getenv("CHROME_BIN")
  181. if chrome_path and os.path.exists(chrome_path):
  182. co.set_paths(browser_path=chrome_path)
  183. if self.proxy_config and self.proxy_config.get("ip"):
  184. p = self.proxy_config
  185. if p.get('username') and p.get('password'):
  186. self._log(f"Starting Proxy Tunnel for {p.get('ip')}...")
  187. exit_node = {
  188. "name": "ExitNode",
  189. "type": p.get('proto'),
  190. "server": p.get('ip'),
  191. "port": p.get('port'),
  192. "username": p.get('username'),
  193. "password": p.get('password')
  194. }
  195. relay_node = random.choice(configure.MIHOMO_RELAY_NODES) if configure.MIHOMO_RELAY_NODES else None
  196. mihomo_path = configure.MIHOMO_BIN_PATH or os.getenv("MIHOMO_BIN")
  197. if not mihomo_path:
  198. raise BizLogicError('Mihomo path is null. Set mihomo bin path in configure or os env')
  199. self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
  200. self.active_proxy_url = self.tunnel.start()
  201. self._log(f"Tunnel started at {self.active_proxy_url}")
  202. co.set_argument(f'--proxy-server={self.active_proxy_url}')
  203. else:
  204. self.active_proxy_url = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}"
  205. co.set_argument(f'--proxy-server={self.active_proxy_url}')
  206. else:
  207. self._log("[WARN] No proxy configured!")
  208. co.headless(False)
  209. co.set_argument('--no-sandbox')
  210. co.set_argument('--disable-gpu')
  211. co.set_argument('--disable-dev-shm-usage')
  212. co.set_argument('--window-size=1920,1080')
  213. co.set_argument('--disable-blink-features=AutomationControlled')
  214. self.page = ChromiumPage(co)
  215. self.page.set.timeouts(15)
  216. def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
  217. """注入JS执行Fetch请求,绕过部分指纹检测"""
  218. if not self.page:
  219. raise BizLogicError("Browser not initialized")
  220. if retry_count > 3:
  221. raise BizLogicError("Max retries exceeded for request")
  222. fetch_options = {
  223. "method": method.upper(),
  224. "headers": headers or {},
  225. "credentials": "include"
  226. }
  227. if json_data:
  228. fetch_options['body'] = json.dumps(json_data)
  229. fetch_options['headers']['Content-Type'] = 'application/json'
  230. elif data:
  231. fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
  232. fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
  233. js_script = f"""
  234. const url = "{url}";
  235. const options = {json.dumps(fetch_options)};
  236. return fetch(url, options)
  237. .then(async response => {{
  238. const text = await response.text();
  239. const headers = {{}};
  240. response.headers.forEach((value, key) => headers[key] = value);
  241. return {{
  242. status: response.status,
  243. body: text,
  244. headers: headers,
  245. url: response.url
  246. }};
  247. }})
  248. .catch(err => ({{ status: 0, body: err.toString() }}));
  249. """
  250. try:
  251. res_dict = self.page.run_js(js_script, timeout=60)
  252. resp = BrowserResponse(res_dict)
  253. if resp.status_code == 200:
  254. return resp
  255. if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
  256. logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
  257. new_token = self._refresh_turnstile()
  258. if new_token and json_data and "captcha_api_key" in json_data:
  259. json_data["captcha_api_key"] = new_token
  260. return self._perform_js_fetch(method, url, headers, data, json_data, retry_count + 1)
  261. if resp.status_code == 429:
  262. raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
  263. return resp
  264. except Exception as e:
  265. logger.error(f"JS Execution Error: {e}")
  266. raise BizLogicError(f"Fetch failed: {e}")
  267. def _handle_cookie_banner(self):
  268. try:
  269. self.page.run_js("""
  270. var btn = document.getElementById('onetrust-accept-btn-handler');
  271. if(btn) { btn.click(); return true; }
  272. var banner = document.getElementById('onetrust-banner-sdk');
  273. if(banner) { banner.remove(); return true; }
  274. """)
  275. except:
  276. pass
  277. def _refresh_turnstile(self):
  278. self._log("Attempting to refresh Turnstile token...")
  279. try:
  280. self.page.run_js('try{window.turnstile.reset()}catch(e){}')
  281. cf_bypasser = CloudflareBypasser(self.page, log=True)
  282. for i in range(30):
  283. time.sleep(1)
  284. try:
  285. ele = self.page.ele('@name=cf-turnstile-response')
  286. if ele and ele.value:
  287. self._log("Turnstile token obtained.")
  288. return ele.value
  289. except:
  290. pass
  291. if i > 5:
  292. try: cf_bypasser.click_verification_button(is_dfs=False)
  293. except: pass
  294. except Exception as e:
  295. logger.error(f"Turnstile refresh failed: {e}")
  296. return None
  297. def _wait_for_activation_link(self, username, max_wait_sec=60):
  298. self._log(f"Waiting for email to {username}...")
  299. start_time = time.time()
  300. url = "https://api.text.skin/api/email-authorizations/fetch"
  301. headers = {
  302. "Authorization": f"Bearer {self.email_api_token}",
  303. "Content-Type": "application/json",
  304. "Accept": "application/json, text/plain, */*"
  305. }
  306. while time.time() - start_time < max_wait_sec:
  307. try:
  308. utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
  309. params = {
  310. "email": self.master_email,
  311. "sender": 'donotreply at vfsglobal.com',
  312. "recipient": username,
  313. "subjectKeywords": 'Welcome',
  314. "bodyKeywords": 'ActivateAccount',
  315. "sentDate": utc_now_str,
  316. "expiry": str(60)
  317. }
  318. # 已经去掉 proxies=proxies 参数
  319. resp = requests.post(url, headers=headers, params=params, json={}, timeout=60)
  320. if resp.status_code != 200:
  321. logger.warning(f"Email API returned status {resp.status_code}.")
  322. time.sleep(15)
  323. continue
  324. try:
  325. result = resp.json()
  326. except json.JSONDecodeError:
  327. time.sleep(15)
  328. continue
  329. if result.get('code') != 0:
  330. logger.debug(f"API Message: {result.get('message')}")
  331. else:
  332. content = result.get('data', {}).get('body', "")
  333. if content:
  334. soup = BeautifulSoup(content, "html.parser")
  335. link = soup.find("a", string="ActivateAccount")
  336. if link:
  337. return link["href"].replace(" ", "").replace("\n", "").replace("\r", "").strip()
  338. except Exception as e:
  339. logger.warning(f"Error fetching email: {e}")
  340. time.sleep(15)
  341. self._log("Checking email again...")
  342. return None
  343. def register(self, account):
  344. try:
  345. self._init_browser()
  346. self._log(f"Opening {self.vfs_url}")
  347. self.page.set.timeouts(page_load=30, script=30)
  348. try:
  349. self.page.get(self.vfs_url, retry=0, timeout=30)
  350. except Exception:
  351. logger.warning("Page load timed out. Stopping loading manually.")
  352. self.page.stop_loading()
  353. cf_token = None
  354. cf_bypasser = CloudflareBypasser(self.page, log=True)
  355. for _ in range(40):
  356. time.sleep(1)
  357. if "page-not-found" in self.page.url:
  358. logger.error("❌ [BLOCKED] Redirected to 'Page Not Found'. Aborting.")
  359. return False
  360. if "403" in self.page.title and "Just a moment" not in self.page.title:
  361. logger.error("❌ [BLOCKED] 403 Forbidden detected. Aborting.")
  362. return False
  363. self._handle_cookie_banner()
  364. try:
  365. ele = self.page.ele('@name=cf-turnstile-response')
  366. if ele and ele.value:
  367. cf_token = ele.value
  368. break
  369. except: pass
  370. try: cf_bypasser.click_verification_button(is_dfs=False)
  371. except: pass
  372. if not cf_token:
  373. raise BizLogicError("Failed to obtain initial Cloudflare token")
  374. post_data = {
  375. 'emailid': account['username'],
  376. 'password': VFSHelper.encrypt_password(account['password']),
  377. 'confirmPassword': VFSHelper.encrypt_password(account['password']),
  378. 'processPerDataAgreed': True,
  379. 'intTransPerDataAgreed': True,
  380. 'termAndConditionAgreed': True,
  381. 'missioncode': account['mission_code'],
  382. 'countrycode': account['country_code'],
  383. 'languageCode': 'en',
  384. 'dialcode': str(account['phone_country_code']),
  385. 'contact': account['phone_number'],
  386. 'captcha_version': 'cloudflare-v1',
  387. 'captcha_api_key': cf_token,
  388. 'cultureCode': 'en-US',
  389. 'IsSpecialUser': False,
  390. }
  391. headers = {
  392. 'content-type': 'application/json;charset=utf-8',
  393. 'accept': 'application/json, text/plain, */*',
  394. 'route': f"{account['country_code']}/en/{account['mission_code']}",
  395. 'clientsource': VFSHelper.get_client_source(),
  396. }
  397. self._log(f"Submitting registration for {account['username']}")
  398. country_code = account.get('country_code', '').lower()
  399. if country_code == 'chn':
  400. api_domain = 'https://lift-apicn.vfsglobal.com'
  401. else:
  402. api_domain = 'https://lift-api.vfsglobal.com'
  403. register_endpoint = f"{api_domain}/user/registration"
  404. self._log(f"Using API Endpoint: {register_endpoint}")
  405. resp = self._perform_js_fetch(
  406. 'POST',
  407. register_endpoint,
  408. headers=headers,
  409. json_data=post_data
  410. )
  411. self._log(f'register resp={resp.text}')
  412. resp_data = resp.json()
  413. if resp_data.get("code") == "200":
  414. self._log("Registration API success. Waiting for email...")
  415. activate_link = self._wait_for_activation_link(account['username'])
  416. if activate_link:
  417. self._log(f"Activating account: {activate_link}")
  418. activate_tab = self.page.new_tab(activate_link)
  419. try:
  420. success_ele = activate_tab.ele('Activation Successful', timeout=30)
  421. if success_ele:
  422. logger.info(f"✅ Account {account['username']} activated successfully.")
  423. return True
  424. else:
  425. logger.error("Activation verification failed. Success text not found.")
  426. return False
  427. finally:
  428. activate_tab.close()
  429. else:
  430. logger.error("Timeout waiting for activation email.")
  431. else:
  432. logger.error(f"Registration failed: {resp_data}")
  433. except Exception as e:
  434. logger.error(f"Registration process exception: {e}", exc_info=True)
  435. return False
  436. def cleanup(self):
  437. self._log("Cleaning up resources...")
  438. if self.page:
  439. try: self.page.quit()
  440. except: pass
  441. if self.tunnel:
  442. try: self.tunnel.stop()
  443. except: pass
  444. if os.path.exists(self.workspace):
  445. time.sleep(1)
  446. shutil.rmtree(self.workspace, ignore_errors=True)
  447. # ==========================================
  448. # 主流程 & 命令行参数解析
  449. # ==========================================
  450. def parse_arguments():
  451. parser = argparse.ArgumentParser(description="VFS Global 自动注册机器人")
  452. # 核心参数
  453. parser.add_argument('--target', type=int, default=20, help="需要注册的目标账号数量")
  454. parser.add_argument('--vfs-url', type=str, required=True, help="VFS 注册页面的完整 URL, 如: https://visa.vfsglobal.com/chn/en/aut/register")
  455. parser.add_argument('--proxy-pool', type=str, default='proxy-cheap', help="使用的代理池名称 (在 proxies.json 中定义)")
  456. # 账号生成相关参数
  457. parser.add_argument('--account-prefix', type=str, default="cn_at", help="生成的邮箱前缀")
  458. parser.add_argument('--account-pool', type=str, default="cn.at.sentinel", help="上报到服务端的账号池名称")
  459. parser.add_argument('--phone-country', type=int, default=86, help="生成的手机号国家代码")
  460. parser.add_argument('--email-domain', type=str, default="text.skin", help="邮箱域名")
  461. # API 与 上报配置
  462. parser.add_argument('--upload-url', type=str, default="https://api.text.skin/api/account/add", help="账号上报服务器 URL")
  463. parser.add_argument('--upload-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="账号上报服务器 API Token")
  464. parser.add_argument('--email-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="获取邮件的 API Token")
  465. parser.add_argument('--master-email', type=str, default="hujiarui8@gmail.com", help="接收验证邮件的主邮箱")
  466. return parser.parse_args()
  467. def generate_account_details(args):
  468. """根据命令行参数和 URL 生成账号数据字典"""
  469. # 从 URL 中提取 from_country 和 to_country
  470. match = re.search(r'vfsglobal\.com/([^/]+)/[^/]+/([^/]+)/register', args.vfs_url)
  471. if not match:
  472. raise ValueError(f"无法从 URL 解析国家代码: {args.vfs_url}")
  473. from_country = match.group(1)
  474. to_country = match.group(2)
  475. rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
  476. username = f"{args.account_prefix}_{rand_suffix}@{args.email_domain}"
  477. phone = VFSHelper.generate_mobile_number(args.phone_country)
  478. return {
  479. 'pool_name': args.account_pool,
  480. 'country_code': from_country,
  481. 'mission_code': to_country,
  482. 'phone_country_code': args.phone_country,
  483. 'phone_number': phone,
  484. 'username': username,
  485. 'password': VFSHelper.generate_password(),
  486. }
  487. def main():
  488. args = parse_arguments()
  489. proxies = load_proxies(args.proxy_pool)
  490. logger.info(f"[*] 成功加载代理数量: {len(proxies)} 个")
  491. success_accounts = []
  492. logger.info(">>> Starting Registration Bot <<<")
  493. while len(success_accounts) < args.target:
  494. proxy_config = random.choice(proxies)
  495. account_detail = generate_account_details(args)
  496. logger.info(f"Processing Account: {account_detail['username']}")
  497. bot = None
  498. try:
  499. bot = VFSRegistrationBot(
  500. vfs_url=args.vfs_url,
  501. proxy=proxy_config,
  502. email_api_token=args.email_token,
  503. master_email=args.master_email
  504. )
  505. is_success = bot.register(account_detail)
  506. if is_success:
  507. success_accounts.append(account_detail)
  508. logger.info(f"Progress: {len(success_accounts)}/{args.target}")
  509. upload_account_to_server(account_detail, args.upload_url, args.upload_token)
  510. # 实时保存结果,防止中途退出丢失
  511. with open("registered_accounts.json", "w", encoding='utf-8') as f:
  512. json.dump(success_accounts, f, indent=4, ensure_ascii=False)
  513. else:
  514. logger.warning("Registration failed. Retrying with new account details...")
  515. time.sleep(5) # 避免请求过于频繁
  516. except Exception as e:
  517. logger.error(f"[ERROR] 注册发生致命错误 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
  518. finally:
  519. if bot:
  520. bot.cleanup()
  521. logger.info(f">>> All tasks completed. Registered {len(success_accounts)} accounts. <<<")
  522. if __name__ == "__main__":
  523. main()