tls_registration_gui.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. import sys
  2. import time
  3. import json
  4. import os
  5. import re
  6. import uuid
  7. import socket
  8. import threading
  9. import random
  10. import requests
  11. from datetime import datetime
  12. from typing import Optional, Dict
  13. from PyQt5.QtWidgets import (
  14. QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  15. QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QPushButton,
  16. QTableWidget, QTableWidgetItem, QAbstractItemView, QTextEdit,
  17. QHeaderView, QMessageBox
  18. )
  19. from PyQt5.QtCore import pyqtSignal, QObject, Qt
  20. from DrissionPage.common import Keys
  21. from DrissionPage import ChromiumPage, ChromiumOptions
  22. import configure
  23. from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
  24. from toolkit.vs_cloud_api import VSCloudApi
  25. from toolkit.mihomo_tunnel import MihomoTunnel
  26. from vs_types import BizLogicError
  27. from utils.mouse import HumanMouse
  28. from utils.keyboard import HumanKeyboard
  29. from utils.fingerprint_utils import FingerprintGenerator
  30. from utils.fake_utils import generate_random_account_detail
  31. def load_proxies(pool_name):
  32. """从 config/proxies.json 读取对应的代理池"""
  33. config_path = os.path.join(os.path.dirname(__file__), 'config', 'proxies.json')
  34. try:
  35. with open(config_path, 'r', encoding='utf-8') as f:
  36. data = json.load(f)
  37. proxies = data.get(pool_name, [])
  38. if not proxies:
  39. raise ValueError(f"代理池 '{pool_name}' 为空或不存在!")
  40. return proxies
  41. except Exception as e:
  42. raise Exception(f"读取代理配置文件失败: {e}")
  43. # ================= 用于跨线程更新UI的信号类 =================
  44. class GuiSignals(QObject):
  45. log_signal = pyqtSignal(str) # 传递日志文本
  46. status_signal = pyqtSignal(int, str) # 传递行号和状态文本
  47. # ================= 核心业务类 =================
  48. class TlsRegistrator:
  49. def __init__(self, tls_url, proxy_config: Optional[Dict]=None, capsolver_key: Optional[str]=None, signals: GuiSignals=None):
  50. self.proxy_config = proxy_config
  51. self.capsolver_key = capsolver_key
  52. self.account_detail = None # 动态绑定
  53. self.signals = signals # 绑定UI信号
  54. self.instance_id = uuid.uuid4().hex[:8]
  55. self.tls_url = tls_url
  56. self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
  57. self.page = None
  58. self.mouse = None
  59. self.keyboard = None
  60. self.tunnel = None
  61. def _log(self, msg):
  62. now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  63. log_str = f"[{now}][Bot-{self.instance_id}] {msg}"
  64. if self.signals:
  65. self.signals.log_signal.emit(log_str)
  66. else:
  67. print(log_str)
  68. def _get_free_port(self):
  69. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  70. s.bind(('', 0))
  71. return s.getsockname()[1]
  72. def save_screenshot(self, name_prefix):
  73. try:
  74. timestamp = int(time.time())
  75. filename = f"{self.instance_id}_{name_prefix}_{timestamp}.jpg"
  76. save_path = os.path.join("data", filename)
  77. os.makedirs("data", exist_ok=True)
  78. self.page.get_screenshot(path=save_path, full_page=False)
  79. self._log(f"Screenshot saved to {save_path}")
  80. except Exception as e:
  81. self._log(f"Failed to save screenshot: {e}")
  82. def init_browser(self):
  83. self._log("Initializing browser...")
  84. co = ChromiumOptions()
  85. port = self._get_free_port()
  86. co.set_local_port(port)
  87. co.set_user_data_path(self.workspace)
  88. chrome_path = configure.CHROME_PATH or os.getenv("CHROME_BIN")
  89. if chrome_path and os.path.exists(chrome_path):
  90. co.set_paths(browser_path=chrome_path)
  91. if self.proxy_config and self.proxy_config.get("ip"):
  92. p = self.proxy_config
  93. if p.get('username') and p.get('password'):
  94. self._log(f"Starting Proxy Tunnel for {p.get('ip')}...")
  95. exit_node = {
  96. "name": "ExitNode", "type": p.get('proto'),
  97. "server": p.get('ip'), "port": p.get('port'),
  98. "username": p.get('username'), "password": p.get('password')
  99. }
  100. relay_node = random.choice(configure.MIHOMO_RELAY_NODES) if configure.MIHOMO_RELAY_NODES else None
  101. mihomo_path = configure.MIHOMO_BIN_PATH or os.getenv("MIHOMO_BIN")
  102. if not mihomo_path:
  103. raise BizLogicError(message='Mihomo path is null')
  104. self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
  105. local_proxy = self.tunnel.start()
  106. co.set_argument(f'--proxy-server={local_proxy}')
  107. else:
  108. proxy_str = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}"
  109. co.set_argument(f'--proxy-server={proxy_str}')
  110. else:
  111. self._log("[WARN] No proxy configured!")
  112. fingerprint_gen = FingerprintGenerator()
  113. specific_fp = fingerprint_gen.generate(self.instance_id)
  114. co.headless(False)
  115. co.set_argument('--no-sandbox')
  116. co.set_argument('--disable-dev-shm-usage')
  117. co.set_argument('--window-size=1920,1080')
  118. co.set_argument('--disable-blink-features=AutomationControlled')
  119. co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
  120. co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
  121. co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
  122. self.page = ChromiumPage(co)
  123. self.page.get(self.tls_url)
  124. time.sleep(5)
  125. cf_bypasser = CloudflareBypasser(self.page, log=True)
  126. cf_bypasser.bypass(max_retry=8)
  127. time.sleep(3)
  128. cf_bypasser.handle_waiting_room()
  129. self._log("正在初始化拟人化工具...")
  130. self.mouse = HumanMouse(self.page, debug=False)
  131. self.keyboard = HumanKeyboard(self.page)
  132. viewport_width = self.page.rect.viewport_size[0]
  133. viewport_height = self.page.rect.viewport_size[1]
  134. init_x = random.randint(10, viewport_width - 10)
  135. init_y = random.randint(10, viewport_height - 10)
  136. self.mouse.move(init_x, init_y)
  137. self._log("浏览器初始化完成。")
  138. def solve_captcha(self, page_url: str, task_type: str, site_key: str, use_proxy=False, action: str=None, api_domain: str=None) -> str:
  139. if not self.capsolver_key: raise ValueError("Capsolver API key missing")
  140. task = {"type": task_type, "websiteURL": page_url, "websiteKey": site_key}
  141. if api_domain: task["apiDomain"] = api_domain
  142. if use_proxy:
  143. proxy = self.proxy_config
  144. task["proxyType"] = proxy.get('proto', 'http')
  145. task["proxyAddress"] = proxy.get('ip')
  146. task["proxyPort"] = int(proxy.get('port'))
  147. if proxy.get('username'):
  148. task["proxyLogin"] = proxy.get('username')
  149. task["proxyPassword"] = proxy.get('password')
  150. if action: task["pageAction"] = action
  151. payload = {"clientKey": self.capsolver_key, "task": task}
  152. res = requests.post("https://api.capsolver.com/createTask", json=payload, timeout=20)
  153. if res.status_code != 200 or res.json().get("errorId") != 0:
  154. raise Exception(f"Failed to create capsolver task: {res.text}")
  155. task_id = res.json().get("taskId")
  156. for _ in range(30):
  157. r = requests.post("https://api.capsolver.com/getTaskResult", json={"clientKey": self.capsolver_key, "taskId": task_id}, timeout=20)
  158. data = r.json()
  159. if data.get("status") == "ready":
  160. return data["solution"].get("gRecaptchaResponse") or data["solution"].get("token")
  161. time.sleep(3)
  162. raise Exception("Capsolver task timeout")
  163. def register(self):
  164. email = self.account_detail.get('email')
  165. password = self.account_detail.get('pwd')
  166. self._log(f"开始执行自动注册: {email}")
  167. btn_selector = '#submit'
  168. if not self.page.wait.ele_displayed(btn_selector, timeout=3):
  169. register_btn = self.page.ele("tag:a@@href:registration")
  170. if register_btn: self.mouse.human_click_ele(register_btn)
  171. time.sleep(3)
  172. if not self.page.wait.ele_displayed(btn_selector, timeout=10):
  173. raise BizLogicError(message=f"Can't find selector={btn_selector}")
  174. time.sleep(random.uniform(0.5, 1))
  175. self._log("正在填写邮箱和密码...")
  176. self.mouse.human_click_ele(self.page.ele('#email'))
  177. self.keyboard.type_text(email, humanize=True)
  178. time.sleep(random.uniform(0.2, 0.5))
  179. self.mouse.human_click_ele(self.page.ele('#password'))
  180. self.keyboard.type_text(password, humanize=True)
  181. time.sleep(random.uniform(0.2, 0.5))
  182. self.mouse.human_click_ele(self.page.ele('#confirm-password'))
  183. self.keyboard.type_text(password, humanize=True)
  184. time.sleep(random.uniform(0.2, 0.5))
  185. self._log("正在勾选必选条款...")
  186. for checkbox_id in ['#terms-and-conditions', '#biometric-data', '#privacy-notice']:
  187. self.mouse.human_click_ele(self.page.ele(checkbox_id).next())
  188. time.sleep(random.uniform(0.3, 0.6))
  189. self._log("提交注册...")
  190. time.sleep(random.uniform(0.3, 0.6))
  191. self.mouse.human_click_ele(self.page.ele(btn_selector))
  192. self._log("正在等待验证结果 (最多15秒)...")
  193. success_dialog = self.page.wait.ele_displayed('tag:h1@text():Check your email inbox', timeout=15)
  194. if not success_dialog:
  195. self.save_screenshot("failed_submit.png")
  196. raise BizLogicError(message='Failed to submit account registration')
  197. self._log("✅ 操作成功!已弹出提示:Check your email inbox")
  198. return True
  199. def activate(self, sent_at=None):
  200. email = self.account_detail.get('email')
  201. if not sent_at:
  202. sent_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
  203. self._log("获取激活邮件...")
  204. content_out = VSCloudApi.Instance().fetch_mail_content(
  205. email='hujiarui8@gmail.com', sender='TLSContact', recipient=email,
  206. subject_keywords='TLSContact', body_keywords='', sent_date=sent_at, expiry=600
  207. )
  208. match = re.search(r'https://\S+', content_out)
  209. activate_link = match.group(0) if match else None
  210. if not activate_link:
  211. raise Exception("未找到激活链接")
  212. self.page.get(activate_link)
  213. btn_selector = "#activation-pending-button"
  214. if not self.page.wait.ele_displayed(btn_selector, timeout=10):
  215. raise BizLogicError(message=f"Wait ele={btn_selector} timeout")
  216. self.page.ele(btn_selector).click()
  217. time.sleep(3)
  218. self._log("✅ 激活成功!")
  219. def make_account_useful(self):
  220. def fill_date_field(page, selector, date_str):
  221. if not date_str:
  222. return
  223. ele = page.ele(selector)
  224. ele.scroll.to_see(center=True)
  225. js_detect_format = """
  226. const parts = new Intl.DateTimeFormat().formatToParts(new Date(2023, 11, 31));
  227. let format = [];
  228. for (let part of parts) {
  229. if (part.type === 'year') format.push('Y');
  230. if (part.type === 'month') format.push('M');
  231. if (part.type === 'day') format.push('D');
  232. }
  233. return format;
  234. """
  235. date_format = page.run_js(js_detect_format)
  236. year, month, day = date_str.split('-')
  237. date_dict = {'Y': year, 'M': month.zfill(2), 'D': day.zfill(2)}
  238. ele.click()
  239. time.sleep(0.1)
  240. page.actions.type(Keys.LEFT * 3)
  241. time.sleep(0.1)
  242. for i, char in enumerate(date_format):
  243. val = date_dict[char]
  244. page.actions.type(val)
  245. time.sleep(0.1)
  246. if char == 'Y':
  247. if i < 2:
  248. page.actions.type(Keys.RIGHT)
  249. time.sleep(0.1)
  250. # email = self.account_detail.get('email')
  251. # password = self.account_detail.get('pwd')
  252. # location = self.account_detail.get('location')
  253. # self._log("=== 开始执行自动填表流程 ===")
  254. # btn_selector = 'tag:button@@text():Login'
  255. # if not self.page.wait.ele_displayed(btn_selector, timeout=3):
  256. # login_btn = self.page.ele("tag:a@@href:login")
  257. # if login_btn: self.mouse.human_click_ele(login_btn)
  258. # time.sleep(3)
  259. # if not self.page.wait.ele_displayed(btn_selector, timeout=10):
  260. # raise BizLogicError(message=f"Can't find selector={btn_selector}")
  261. # recpatchav2_token = ""
  262. # if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
  263. # self._log("Solving ReCaptcha...")
  264. # recpatchav2_token = self.solve_captcha(self.page.url, "ReCaptchaV2TaskProxyLess", "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0")
  265. # input_ele = self.page.ele('tag:label@@text():Email').next()
  266. # self.mouse.human_click_ele(input_ele)
  267. # time.sleep(random.uniform(0.2, 0.6))
  268. # self.keyboard.type_text(email, humanize=True)
  269. # time.sleep(random.uniform(0.5, 1.2))
  270. # input_ele = self.page.ele('tag:label@@text():Password').next()
  271. # self.mouse.human_click_ele(input_ele)
  272. # time.sleep(random.uniform(0.2, 0.6))
  273. # self.keyboard.type_text(password, humanize=True)
  274. # if recpatchav2_token:
  275. # inject_recpatchav2_token_js = f"""
  276. # var g = document.getElementById('g-recaptcha-response');
  277. # if(g) {{ g.value = "{recpatchav2_token}"; }}
  278. # """
  279. # self._log("Inject ReCaptchaV2 Token via JS...")
  280. # self.page.run_js(inject_recpatchav2_token_js)
  281. # time.sleep(random.uniform(0.5, 1.0))
  282. # self._log("Submitting Login...")
  283. # time.sleep(random.uniform(0.3, 0.8))
  284. # login_btn = self.page.ele('tag:button@@text():Login')
  285. # self.mouse.human_click_ele(login_btn)
  286. # self._log("Waiting for dashboard redirect...")
  287. # self.page.wait.url_change('login-actions', exclude=True, timeout=45)
  288. # time.sleep(4)
  289. # if "login-actions" in self.page.url or "auth" in self.page.url:
  290. # raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
  291. # self._log("Waiting for dashboard...")
  292. # self.page.wait.load_start()
  293. # time.sleep(5)
  294. # self._log("Parsing Dashboard for Travel Group...")
  295. # html = self.page.html
  296. # js_pattern = r'\\"travelGroups\\":\s*(\[.*?\]),\\"availableCountriesToCreateGroups'
  297. # js_match = re.search(js_pattern, html, re.DOTALL)
  298. # groups = []
  299. # if js_match:
  300. # json_str = js_match.group(1).replace(r'\"', '"')
  301. # groups = json.loads(json_str)
  302. # travel_group = None
  303. # for g in groups:
  304. # if location in g.get('vacName', ''):
  305. # travel_group = g
  306. # break
  307. # if not travel_group:
  308. # raise BizLogicError(message=f"Travel Group not found for {location}")
  309. # formgroup_id = travel_group.get('formGroupId')
  310. # self._log(f"Waiting for group button to render: {formgroup_id}")
  311. # btn_selector = f'tag:a@@data-testid=btn-select-group'
  312. # self._log(f"Select group_id={formgroup_id}...")
  313. # self.mouse.human_click_ele(self.page.ele(btn_selector))
  314. # self._log("Waiting for url redirect...")
  315. # self.page.wait.url_change('travel-groups', exclude=True, timeout=45)
  316. # time.sleep(2)
  317. # if "travel-groups" in self.page.url or "auth" in self.page.url:
  318. # raise BizLogicError(message="Redirect to service-level Failed!")
  319. # btn_selector_add = 'tag:button@@data-testid=btn-add-applicant'
  320. # btn_selector_max = 'tag:button@@data-testid=btn-max-number-of-applicants'
  321. # error_selector = 'tag:h2@text():Something went wrong'
  322. # for attempt in range(2):
  323. # try:
  324. # btn_selector = btn_selector_add
  325. # if not self.page.wait.ele_displayed(btn_selector, timeout=10):
  326. # btn_selector = btn_selector_max
  327. # if not self.page.ele(btn_selector):
  328. # raise BizLogicError(message=f"Can't find selector={btn_selector}")
  329. # add_btn = self.page.ele(btn_selector)
  330. # add_btn.scroll.to_see(center=True)
  331. # time.sleep(random.uniform(0.6, 0.8))
  332. # add_btn.click()
  333. # if self.page.wait.ele_displayed(error_selector, timeout=5):
  334. # raise BizLogicError("Page shows 'Something went wrong'")
  335. # break
  336. # except Exception as e:
  337. # if attempt == 0:
  338. # self.page.refresh()
  339. # time.sleep(5)
  340. # else:
  341. # raise BizLogicError(message=f"Click add applicant failed after retry: {e}")
  342. self._log("开始填充表单字段...")
  343. visa_type = self.account_detail.get("visa_type")
  344. if visa_type:
  345. btn = self.page.ele('tag:button@@data-testid=input-visa-type')
  346. btn.scroll.to_see(center=True)
  347. btn.click()
  348. time.sleep(0.5)
  349. self.page.ele(f'tag:span@@text():{visa_type}').click(by_js=True)
  350. time.sleep(0.5)
  351. tavel_purpose = self.account_detail.get("travel_purpose")
  352. if tavel_purpose:
  353. btn = self.page.ele('tag:button@@data-testid=input-travel-purpose')
  354. btn.scroll.to_see(center=True)
  355. btn.click()
  356. time.sleep(0.5)
  357. self.page.ele(f'tag:span@@text():{tavel_purpose}').click(by_js=True)
  358. time.sleep(0.5)
  359. application_form_id = self.account_detail.get('application_form_id')
  360. if application_form_id:
  361. ele = self.page.ele('tag:input@@data-testid=f_cai')
  362. ele.scroll.to_see(center=True)
  363. ele.input(application_form_id)
  364. last_name = self.account_detail.get('last_name')
  365. if last_name:
  366. ele = self.page.ele('tag:input@@data-testid=f_pers_surnames')
  367. ele.scroll.to_see(center=True)
  368. ele.input(last_name.upper())
  369. first_name = self.account_detail.get('first_name')
  370. if first_name:
  371. ele = self.page.ele('tag:input@@data-testid=f_pers_givennames')
  372. ele.scroll.to_see(center=True)
  373. ele.input(first_name.upper())
  374. gender = self.account_detail.get('gender')
  375. if gender:
  376. try:
  377. gender = gender.capitalize()
  378. ele = self.page.ele(f'tag:label@@text():{gender}')
  379. ele.scroll.to_see(center=True)
  380. ele.click()
  381. except Exception as e:
  382. self._log(e)
  383. fill_date_field(self.page, 'tag:input@@data-testid=f_pers_birth_date', self.account_detail.get('birthday'))
  384. nationality = self.account_detail.get('nationality')
  385. if nationality:
  386. nationality = nationality.title()
  387. btn = self.page.ele('tag:label@@for=f_pers_nationality').next()
  388. btn.scroll.to_see(center=True)
  389. btn.click()
  390. time.sleep(0.5)
  391. self.page.ele(f'tag:li@@role=option@@text():{nationality}').click(by_js=True)
  392. time.sleep(0.5)
  393. province_residence = self.account_detail.get('province_residence')
  394. if province_residence:
  395. try:
  396. province_residence = province_residence.title()
  397. btn = self.page.ele('tag:label@@for=f_pers_province').next()
  398. btn.scroll.to_see(center=True)
  399. btn.click()
  400. time.sleep(0.5)
  401. self.page.ele(f'tag:li@@role=option@@text():{province_residence}').click(by_js=True)
  402. time.sleep(0.5)
  403. except Exception as e:
  404. self._log(e)
  405. passport_type = self.account_detail.get('passport_type')
  406. if passport_type:
  407. btn = self.page.ele('tag:label@@for=f_identity_type').next()
  408. btn.scroll.to_see(center=True)
  409. btn.click()
  410. time.sleep(0.5)
  411. self.page.ele(f'tag:li@@role=option@@text():{passport_type}').click(by_js=True)
  412. time.sleep(0.5)
  413. passport_no = self.account_detail.get('passport_no')
  414. if passport_no:
  415. ele = self.page.ele('tag:input@@data-testid=f_pass_num')
  416. ele.scroll.to_see(center=True)
  417. ele.input(passport_no.upper())
  418. passport_issue_date = self.account_detail.get('passport_issue_date')
  419. if passport_issue_date:
  420. try:
  421. fill_date_field(self.page, 'tag:input@@data-testid=fi_passport_issue_date', passport_issue_date)
  422. time.sleep(0.5)
  423. except Exception as e:
  424. self._log(e)
  425. passport_expiry_date = self.account_detail.get('passport_expiry_date')
  426. if passport_expiry_date:
  427. try:
  428. fill_date_field(self.page, 'tag:input@@data-testid=fi_passport_expiry_date', passport_expiry_date)
  429. time.sleep(0.5)
  430. except Exception as e:
  431. self._log(e)
  432. phone_country_code = self.account_detail.get('phone_country_code')
  433. phone_number = self.account_detail.get('phone_number')
  434. if phone_country_code:
  435. div = self.page.ele('tag:label@@for=f_pers_mobile_phone').next()
  436. btn = div.ele('tag:button')
  437. btn.scroll.to_see(center=True)
  438. btn.click()
  439. time.sleep(0.5)
  440. self.page.ele(f'tag:li@@role=option@@text():+{phone_country_code}').click(by_js=True)
  441. time.sleep(0.5)
  442. div.ele('tag:input@@type:tel').input(phone_number)
  443. fill_date_field(self.page, 'tag:input@@data-testid=fi_trav_origin_departure_date', self.account_detail.get('departure_origin_date'))
  444. fill_date_field(self.page, 'tag:input@@data-testid=f_trav_departure_date', self.account_detail.get('arrival_schengen_area_date'))
  445. fill_date_field(self.page, 'tag:input@@data-testid=f_trav_arrival_date', self.account_detail.get('departure_schengen_area_date'))
  446. submit_btn = self.page.ele('tag:button@@data-testid=btn-submit')
  447. submit_btn.scroll.to_see(center=True)
  448. time.sleep(1)
  449. submit_btn.click()
  450. time.sleep(6)
  451. confirm_btn = self.page.ele('tag:button@@text():Confirm')
  452. if confirm_btn:
  453. confirm_btn.scroll.to_see(center=True)
  454. confirm_btn.click()
  455. time.sleep(6)
  456. self._log("✅ 自动填表提交成功!")
  457. def upload_account_to_server(self):
  458. api_url = 'https://api.text.skin/api/account/add'
  459. api_token = 'tok_e946329a60ff45ba807f3f41b0e8b7fc'
  460. headers = {
  461. 'accept': 'application/json',
  462. 'Authorization': f'Bearer {api_token}',
  463. 'Content-Type': 'application/json'
  464. }
  465. payload = {
  466. "pool_name": self.account_detail.get("pool_name", "default_pool"),
  467. "username": self.account_detail.get("email"),
  468. "password": self.account_detail.get("pwd"),
  469. "extra_data": self.account_detail
  470. }
  471. try:
  472. self._log(f"Uploading account {self.account_detail['email']} to server...")
  473. resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
  474. if resp.status_code == 200:
  475. self._log(f"✅ [API Upload Success] Server responded: {resp.text}")
  476. return True
  477. else:
  478. self._log(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
  479. return False
  480. except Exception as e:
  481. self._log(f"❌ [API Upload Error]: {e}")
  482. return False
  483. def cleanup(self):
  484. self._log("Cleaning up resources...")
  485. if self.page:
  486. try: self.page.quit()
  487. except: pass
  488. # ================= PyQt5 GUI 主窗口 =================
  489. class MainWindow(QMainWindow):
  490. def __init__(self):
  491. super().__init__()
  492. self.setWindowTitle("TLS 批量注册工具 v1.0 (PyQt5)")
  493. self.resize(1000, 700)
  494. self.bot = None
  495. self.profiles = []
  496. self.capsolver_key = os.getenv("CAPSOLVER_KEY", "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A")
  497. # 实例化信号类并绑定槽函数
  498. self.signals = GuiSignals()
  499. self.signals.log_signal.connect(self.append_log)
  500. self.signals.status_signal.connect(self.update_table_status)
  501. self.init_ui()
  502. def init_ui(self):
  503. main_widget = QWidget()
  504. main_layout = QVBoxLayout(main_widget)
  505. # 1. 基础设置区域
  506. group_settings = QGroupBox("1. 基础设置")
  507. layout_settings = QGridLayout()
  508. layout_settings.addWidget(QLabel("地区 (如 CN-Beijing):"), 0, 0)
  509. self.input_region = QLineEdit("CN-Beijing")
  510. layout_settings.addWidget(self.input_region, 0, 1)
  511. layout_settings.addWidget(QLabel("代理池名 (从proxies.json读取):"), 0, 2)
  512. self.input_pool = QLineEdit("local")
  513. layout_settings.addWidget(self.input_pool, 0, 3)
  514. layout_settings.addWidget(QLabel("目标 URL:"), 1, 0)
  515. self.input_url = QLineEdit("https://visas-fr.tlscontact.com/en-us/country/gb/vac/gbLON2fr")
  516. layout_settings.addWidget(self.input_url, 1, 1, 1, 3)
  517. group_settings.setLayout(layout_settings)
  518. main_layout.addWidget(group_settings)
  519. # 2. 生成区域
  520. group_gen = QGroupBox("2. 生成虚拟信息")
  521. layout_gen = QHBoxLayout()
  522. layout_gen.addWidget(QLabel("生成数量 (1-100):"))
  523. self.spin_count = QSpinBox()
  524. self.spin_count.setRange(1, 100)
  525. self.spin_count.setValue(5)
  526. layout_gen.addWidget(self.spin_count)
  527. self.btn_generate = QPushButton("生成虚拟信息")
  528. self.btn_generate.setStyleSheet("background-color: lightblue;")
  529. self.btn_generate.clicked.connect(self.action_generate)
  530. layout_gen.addWidget(self.btn_generate)
  531. layout_gen.addStretch()
  532. group_gen.setLayout(layout_gen)
  533. main_layout.addWidget(group_gen)
  534. # 3. 表格展示区域
  535. self.table = QTableWidget()
  536. self.table.setColumnCount(5)
  537. self.table.setHorizontalHeaderLabels(["序号", "邮箱", "密码", "姓名", "当前状态"])
  538. self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
  539. self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
  540. self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
  541. main_layout.addWidget(self.table)
  542. # 4. 浏览器操作区域
  543. group_actions = QGroupBox("3. 浏览器操作 (需先在表格中点击选中一行)")
  544. layout_actions = QHBoxLayout()
  545. self.btn_start = QPushButton("[全局] 启动浏览器")
  546. self.btn_start.clicked.connect(self.action_start_browser)
  547. layout_actions.addWidget(self.btn_start)
  548. self.btn_register = QPushButton("1. 自动注册并激活")
  549. self.btn_register.setStyleSheet("background-color: #d0f0c0;")
  550. self.btn_register.clicked.connect(self.action_register)
  551. layout_actions.addWidget(self.btn_register)
  552. self.btn_fill = QPushButton("2. 自动填表")
  553. self.btn_fill.setStyleSheet("background-color: #ffd700;")
  554. self.btn_fill.clicked.connect(self.action_fill)
  555. layout_actions.addWidget(self.btn_fill)
  556. self.btn_upload = QPushButton("3. 上报并标记已用")
  557. self.btn_upload.setStyleSheet("background-color: #ffb6c1;")
  558. self.btn_upload.clicked.connect(self.action_upload)
  559. layout_actions.addWidget(self.btn_upload)
  560. self.btn_close = QPushButton("[全局] 关闭浏览器")
  561. self.btn_close.setStyleSheet("color: red;")
  562. self.btn_close.clicked.connect(self.action_close_browser)
  563. layout_actions.addWidget(self.btn_close)
  564. group_actions.setLayout(layout_actions)
  565. main_layout.addWidget(group_actions)
  566. # 5. 日志区域
  567. group_log = QGroupBox("运行日志")
  568. layout_log = QVBoxLayout()
  569. self.text_log = QTextEdit()
  570. self.text_log.setReadOnly(True)
  571. layout_log.addWidget(self.text_log)
  572. group_log.setLayout(layout_log)
  573. main_layout.addWidget(group_log)
  574. self.setCentralWidget(main_widget)
  575. # ---------------- 槽函数:UI 更新 ----------------
  576. def append_log(self, text):
  577. self.text_log.append(text)
  578. self.text_log.moveCursor(self.text_log.textCursor().End)
  579. def update_table_status(self, row, status):
  580. item = QTableWidgetItem(status)
  581. item.setTextAlignment(Qt.AlignCenter)
  582. self.table.setItem(row, 4, item)
  583. # ---------------- 业务逻辑触发 ----------------
  584. def run_in_thread(self, target_func, *args):
  585. t = threading.Thread(target=target_func, args=args, daemon=True)
  586. t.start()
  587. def action_generate(self):
  588. count = self.spin_count.value()
  589. region = self.input_region.text().strip()
  590. pool = self.input_pool.text().strip()
  591. self.append_log(f"开始生成 {count} 个 {region} 地区的虚拟信息...")
  592. self.profiles.clear()
  593. self.table.setRowCount(0)
  594. for i in range(count):
  595. try:
  596. acc = generate_random_account_detail(region)
  597. acc['pool_name'] = pool
  598. self.profiles.append(acc)
  599. self.table.insertRow(i)
  600. self.table.setItem(i, 0, QTableWidgetItem(str(i+1)))
  601. self.table.setItem(i, 1, QTableWidgetItem(acc.get('email', '')))
  602. self.table.setItem(i, 2, QTableWidgetItem(acc.get('pwd', '')))
  603. name = f"{acc.get('first_name','')} {acc.get('last_name','')}"
  604. self.table.setItem(i, 3, QTableWidgetItem(name))
  605. self.update_table_status(i, "就绪")
  606. except Exception as e:
  607. self.append_log(f"生成时出错: {e}")
  608. self.append_log("✅ 虚拟信息生成完毕!")
  609. def action_start_browser(self):
  610. pool = self.input_pool.text().strip()
  611. url = self.input_url.text().strip()
  612. def _task():
  613. try:
  614. self.signals.log_signal.emit(f"正在读取代理池 {pool} ...")
  615. proxies = load_proxies(pool)
  616. proxy = random.choice(proxies)
  617. self.signals.log_signal.emit(f"已选取代理: {proxy.get('ip')}")
  618. self.bot = TlsRegistrator(
  619. tls_url=url,
  620. proxy_config=proxy,
  621. capsolver_key=self.capsolver_key,
  622. signals=self.signals
  623. )
  624. self.bot.init_browser()
  625. except Exception as e:
  626. self.signals.log_signal.emit(f"❌ 浏览器启动失败: {e}")
  627. self.run_in_thread(_task)
  628. def _get_selected_row(self):
  629. row = self.table.currentRow()
  630. if row < 0:
  631. QMessageBox.warning(self, "警告", "请先在表格中点击选中需要操作的虚拟账号!")
  632. return None
  633. return row
  634. def action_register(self):
  635. row = self._get_selected_row()
  636. if row is None: return
  637. if not self.bot:
  638. QMessageBox.warning(self, "警告", "请先启动浏览器!")
  639. return
  640. profile = self.profiles[row]
  641. def _task():
  642. try:
  643. self.bot.account_detail = profile
  644. self.signals.status_signal.emit(row, "注册中...")
  645. sent_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
  646. self.bot.register()
  647. self.signals.status_signal.emit(row, "激活中...")
  648. self.bot.activate(sent_at=sent_at)
  649. self.signals.status_signal.emit(row, "注册激活完成")
  650. except Exception as e:
  651. self.signals.log_signal.emit(f"❌ 注册过程异常: {e}")
  652. self.signals.status_signal.emit(row, "注册失败")
  653. self.run_in_thread(_task)
  654. def action_fill(self):
  655. row = self._get_selected_row()
  656. if row is None: return
  657. if not self.bot:
  658. QMessageBox.warning(self, "警告", "请先启动浏览器!")
  659. return
  660. profile = self.profiles[row]
  661. def _task():
  662. try:
  663. self.bot.account_detail = profile
  664. self.signals.status_signal.emit(row, "填表中...")
  665. self.bot.make_account_useful()
  666. self.signals.status_signal.emit(row, "填表完成")
  667. except Exception as e:
  668. self.signals.log_signal.emit(f"❌ 填表过程异常: {e}")
  669. self.signals.status_signal.emit(row, "填表失败")
  670. self.run_in_thread(_task)
  671. def action_upload(self):
  672. row = self._get_selected_row()
  673. if row is None: return
  674. if not self.bot: return
  675. profile = self.profiles[row]
  676. def _task():
  677. try:
  678. self.bot.account_detail = profile
  679. self.signals.status_signal.emit(row, "上报中...")
  680. success = self.bot.upload_account_to_server()
  681. if success:
  682. self.signals.status_signal.emit(row, "已使用(上报成功)")
  683. else:
  684. self.signals.status_signal.emit(row, "上报失败")
  685. except Exception as e:
  686. self.signals.log_signal.emit(f"❌ 上报异常: {e}")
  687. self.signals.status_signal.emit(row, "上报异常")
  688. self.run_in_thread(_task)
  689. def action_close_browser(self):
  690. if self.bot:
  691. self.bot.cleanup()
  692. self.bot = None
  693. self.append_log("✅ 浏览器已关闭。")
  694. def closeEvent(self, event):
  695. """主窗口关闭时,确保清理资源"""
  696. if self.bot:
  697. self.bot.cleanup()
  698. event.accept()
  699. if __name__ == "__main__":
  700. app = QApplication(sys.argv)
  701. window = MainWindow()
  702. window.show()
  703. sys.exit(app.exec_())