import sys import time import json import os import re import uuid import socket import threading import random import requests from datetime import datetime from typing import Optional, Dict from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QPushButton, QTableWidget, QTableWidgetItem, QAbstractItemView, QTextEdit, QHeaderView, QMessageBox ) from PyQt5.QtCore import pyqtSignal, QObject, Qt from DrissionPage.common import Keys from DrissionPage import ChromiumPage, ChromiumOptions import configure from utils.cloudflare_bypass_for_scraping import CloudflareBypasser from toolkit.vs_cloud_api import VSCloudApi from toolkit.mihomo_tunnel import MihomoTunnel from vs_types import BizLogicError from utils.mouse import HumanMouse from utils.keyboard import HumanKeyboard from utils.fingerprint_utils import FingerprintGenerator from utils.fake_utils import generate_random_account_detail 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: raise Exception(f"读取代理配置文件失败: {e}") # ================= 用于跨线程更新UI的信号类 ================= class GuiSignals(QObject): log_signal = pyqtSignal(str) # 传递日志文本 status_signal = pyqtSignal(int, str) # 传递行号和状态文本 # ================= 核心业务类 ================= class TlsRegistrator: def __init__(self, tls_url, proxy_config: Optional[Dict]=None, capsolver_key: Optional[str]=None, signals: GuiSignals=None): self.proxy_config = proxy_config self.capsolver_key = capsolver_key self.account_detail = None # 动态绑定 self.signals = signals # 绑定UI信号 self.instance_id = uuid.uuid4().hex[:8] self.tls_url = tls_url self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}")) self.page = None self.mouse = None self.keyboard = None self.tunnel = None def _log(self, msg): now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_str = f"[{now}][Bot-{self.instance_id}] {msg}" if self.signals: self.signals.log_signal.emit(log_str) else: print(log_str) def _get_free_port(self): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('', 0)) return s.getsockname()[1] def save_screenshot(self, name_prefix): try: timestamp = int(time.time()) filename = f"{self.instance_id}_{name_prefix}_{timestamp}.jpg" save_path = os.path.join("data", filename) os.makedirs("data", exist_ok=True) self.page.get_screenshot(path=save_path, full_page=False) self._log(f"Screenshot saved to {save_path}") except Exception as e: self._log(f"Failed to save screenshot: {e}") def init_browser(self): self._log("Initializing browser...") co = ChromiumOptions() port = self._get_free_port() co.set_local_port(port) co.set_user_data_path(self.workspace) 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) 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(message='Mihomo path is null') self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node) local_proxy = self.tunnel.start() co.set_argument(f'--proxy-server={local_proxy}') else: proxy_str = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}" co.set_argument(f'--proxy-server={proxy_str}') else: self._log("[WARN] No proxy configured!") fingerprint_gen = FingerprintGenerator() specific_fp = fingerprint_gen.generate(self.instance_id) co.headless(False) co.set_argument('--no-sandbox') co.set_argument('--disable-dev-shm-usage') co.set_argument('--window-size=1920,1080') co.set_argument('--disable-blink-features=AutomationControlled') co.set_argument(f"--fingerprint={specific_fp.get('seed')}") co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}") co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}") self.page = ChromiumPage(co) self.page.get(self.tls_url) time.sleep(5) cf_bypasser = CloudflareBypasser(self.page, log=True) cf_bypasser.bypass(max_retry=8) time.sleep(3) cf_bypasser.handle_waiting_room() self._log("正在初始化拟人化工具...") self.mouse = HumanMouse(self.page, debug=False) self.keyboard = HumanKeyboard(self.page) viewport_width = self.page.rect.viewport_size[0] viewport_height = self.page.rect.viewport_size[1] init_x = random.randint(10, viewport_width - 10) init_y = random.randint(10, viewport_height - 10) self.mouse.move(init_x, init_y) self._log("浏览器初始化完成。") def solve_captcha(self, page_url: str, task_type: str, site_key: str, use_proxy=False, action: str=None, api_domain: str=None) -> str: if not self.capsolver_key: raise ValueError("Capsolver API key missing") task = {"type": task_type, "websiteURL": page_url, "websiteKey": site_key} if api_domain: task["apiDomain"] = api_domain if use_proxy: proxy = self.proxy_config task["proxyType"] = proxy.get('proto', 'http') task["proxyAddress"] = proxy.get('ip') task["proxyPort"] = int(proxy.get('port')) if proxy.get('username'): task["proxyLogin"] = proxy.get('username') task["proxyPassword"] = proxy.get('password') if action: task["pageAction"] = action payload = {"clientKey": self.capsolver_key, "task": task} res = requests.post("https://api.capsolver.com/createTask", json=payload, timeout=20) if res.status_code != 200 or res.json().get("errorId") != 0: raise Exception(f"Failed to create capsolver task: {res.text}") task_id = res.json().get("taskId") for _ in range(30): r = requests.post("https://api.capsolver.com/getTaskResult", json={"clientKey": self.capsolver_key, "taskId": task_id}, timeout=20) data = r.json() if data.get("status") == "ready": return data["solution"].get("gRecaptchaResponse") or data["solution"].get("token") time.sleep(3) raise Exception("Capsolver task timeout") def register(self): email = self.account_detail.get('email') password = self.account_detail.get('pwd') self._log(f"开始执行自动注册: {email}") btn_selector = '#submit' if not self.page.wait.ele_displayed(btn_selector, timeout=3): register_btn = self.page.ele("tag:a@@href:registration") if register_btn: self.mouse.human_click_ele(register_btn) time.sleep(3) if not self.page.wait.ele_displayed(btn_selector, timeout=10): raise BizLogicError(message=f"Can't find selector={btn_selector}") time.sleep(random.uniform(0.5, 1)) self._log("正在填写邮箱和密码...") self.mouse.human_click_ele(self.page.ele('#email')) self.keyboard.type_text(email, humanize=True) time.sleep(random.uniform(0.2, 0.5)) self.mouse.human_click_ele(self.page.ele('#password')) self.keyboard.type_text(password, humanize=True) time.sleep(random.uniform(0.2, 0.5)) self.mouse.human_click_ele(self.page.ele('#confirm-password')) self.keyboard.type_text(password, humanize=True) time.sleep(random.uniform(0.2, 0.5)) self._log("正在勾选必选条款...") for checkbox_id in ['#terms-and-conditions', '#biometric-data', '#privacy-notice']: self.mouse.human_click_ele(self.page.ele(checkbox_id).next()) time.sleep(random.uniform(0.3, 0.6)) self._log("提交注册...") time.sleep(random.uniform(0.3, 0.6)) self.mouse.human_click_ele(self.page.ele(btn_selector)) self._log("正在等待验证结果 (最多15秒)...") success_dialog = self.page.wait.ele_displayed('tag:h1@text():Check your email inbox', timeout=15) if not success_dialog: self.save_screenshot("failed_submit.png") raise BizLogicError(message='Failed to submit account registration') self._log("✅ 操作成功!已弹出提示:Check your email inbox") return True def activate(self, sent_at=None): email = self.account_detail.get('email') if not sent_at: sent_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") self._log("获取激活邮件...") content_out = VSCloudApi.Instance().fetch_mail_content( email='hujiarui8@gmail.com', sender='TLSContact', recipient=email, subject_keywords='TLSContact', body_keywords='', sent_date=sent_at, expiry=600 ) match = re.search(r'https://\S+', content_out) activate_link = match.group(0) if match else None if not activate_link: raise Exception("未找到激活链接") self.page.get(activate_link) btn_selector = "#activation-pending-button" if not self.page.wait.ele_displayed(btn_selector, timeout=10): raise BizLogicError(message=f"Wait ele={btn_selector} timeout") self.page.ele(btn_selector).click() time.sleep(3) self._log("✅ 激活成功!") def make_account_useful(self): def fill_date_field(page, selector, date_str): if not date_str: return ele = page.ele(selector) ele.scroll.to_see(center=True) js_detect_format = """ const parts = new Intl.DateTimeFormat().formatToParts(new Date(2023, 11, 31)); let format = []; for (let part of parts) { if (part.type === 'year') format.push('Y'); if (part.type === 'month') format.push('M'); if (part.type === 'day') format.push('D'); } return format; """ date_format = page.run_js(js_detect_format) year, month, day = date_str.split('-') date_dict = {'Y': year, 'M': month.zfill(2), 'D': day.zfill(2)} ele.click() time.sleep(0.1) page.actions.type(Keys.LEFT * 3) time.sleep(0.1) for i, char in enumerate(date_format): val = date_dict[char] page.actions.type(val) time.sleep(0.1) if char == 'Y': if i < 2: page.actions.type(Keys.RIGHT) time.sleep(0.1) # email = self.account_detail.get('email') # password = self.account_detail.get('pwd') # location = self.account_detail.get('location') # self._log("=== 开始执行自动填表流程 ===") # btn_selector = 'tag:button@@text():Login' # if not self.page.wait.ele_displayed(btn_selector, timeout=3): # login_btn = self.page.ele("tag:a@@href:login") # if login_btn: self.mouse.human_click_ele(login_btn) # time.sleep(3) # if not self.page.wait.ele_displayed(btn_selector, timeout=10): # raise BizLogicError(message=f"Can't find selector={btn_selector}") # recpatchav2_token = "" # if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'): # self._log("Solving ReCaptcha...") # recpatchav2_token = self.solve_captcha(self.page.url, "ReCaptchaV2TaskProxyLess", "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0") # input_ele = self.page.ele('tag:label@@text():Email').next() # self.mouse.human_click_ele(input_ele) # time.sleep(random.uniform(0.2, 0.6)) # self.keyboard.type_text(email, humanize=True) # time.sleep(random.uniform(0.5, 1.2)) # input_ele = self.page.ele('tag:label@@text():Password').next() # self.mouse.human_click_ele(input_ele) # time.sleep(random.uniform(0.2, 0.6)) # self.keyboard.type_text(password, humanize=True) # if recpatchav2_token: # inject_recpatchav2_token_js = f""" # var g = document.getElementById('g-recaptcha-response'); # if(g) {{ g.value = "{recpatchav2_token}"; }} # """ # self._log("Inject ReCaptchaV2 Token via JS...") # self.page.run_js(inject_recpatchav2_token_js) # time.sleep(random.uniform(0.5, 1.0)) # self._log("Submitting Login...") # time.sleep(random.uniform(0.3, 0.8)) # login_btn = self.page.ele('tag:button@@text():Login') # self.mouse.human_click_ele(login_btn) # self._log("Waiting for dashboard redirect...") # self.page.wait.url_change('login-actions', exclude=True, timeout=45) # time.sleep(4) # if "login-actions" in self.page.url or "auth" in self.page.url: # raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.") # self._log("Waiting for dashboard...") # self.page.wait.load_start() # time.sleep(5) # self._log("Parsing Dashboard for Travel Group...") # html = self.page.html # js_pattern = r'\\"travelGroups\\":\s*(\[.*?\]),\\"availableCountriesToCreateGroups' # js_match = re.search(js_pattern, html, re.DOTALL) # groups = [] # if js_match: # json_str = js_match.group(1).replace(r'\"', '"') # groups = json.loads(json_str) # travel_group = None # for g in groups: # if location in g.get('vacName', ''): # travel_group = g # break # if not travel_group: # raise BizLogicError(message=f"Travel Group not found for {location}") # formgroup_id = travel_group.get('formGroupId') # self._log(f"Waiting for group button to render: {formgroup_id}") # btn_selector = f'tag:a@@data-testid=btn-select-group' # self._log(f"Select group_id={formgroup_id}...") # self.mouse.human_click_ele(self.page.ele(btn_selector)) # self._log("Waiting for url redirect...") # self.page.wait.url_change('travel-groups', exclude=True, timeout=45) # time.sleep(2) # if "travel-groups" in self.page.url or "auth" in self.page.url: # raise BizLogicError(message="Redirect to service-level Failed!") # btn_selector_add = 'tag:button@@data-testid=btn-add-applicant' # btn_selector_max = 'tag:button@@data-testid=btn-max-number-of-applicants' # error_selector = 'tag:h2@text():Something went wrong' # for attempt in range(2): # try: # btn_selector = btn_selector_add # if not self.page.wait.ele_displayed(btn_selector, timeout=10): # btn_selector = btn_selector_max # if not self.page.ele(btn_selector): # raise BizLogicError(message=f"Can't find selector={btn_selector}") # add_btn = self.page.ele(btn_selector) # add_btn.scroll.to_see(center=True) # time.sleep(random.uniform(0.6, 0.8)) # add_btn.click() # if self.page.wait.ele_displayed(error_selector, timeout=5): # raise BizLogicError("Page shows 'Something went wrong'") # break # except Exception as e: # if attempt == 0: # self.page.refresh() # time.sleep(5) # else: # raise BizLogicError(message=f"Click add applicant failed after retry: {e}") self._log("开始填充表单字段...") visa_type = self.account_detail.get("visa_type") if visa_type: btn = self.page.ele('tag:button@@data-testid=input-visa-type') btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:span@@text():{visa_type}').click(by_js=True) time.sleep(0.5) tavel_purpose = self.account_detail.get("travel_purpose") if tavel_purpose: btn = self.page.ele('tag:button@@data-testid=input-travel-purpose') btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:span@@text():{tavel_purpose}').click(by_js=True) time.sleep(0.5) application_form_id = self.account_detail.get('application_form_id') if application_form_id: ele = self.page.ele('tag:input@@data-testid=f_cai') ele.scroll.to_see(center=True) ele.input(application_form_id) last_name = self.account_detail.get('last_name') if last_name: ele = self.page.ele('tag:input@@data-testid=f_pers_surnames') ele.scroll.to_see(center=True) ele.input(last_name.upper()) first_name = self.account_detail.get('first_name') if first_name: ele = self.page.ele('tag:input@@data-testid=f_pers_givennames') ele.scroll.to_see(center=True) ele.input(first_name.upper()) gender = self.account_detail.get('gender') if gender: try: gender = gender.capitalize() ele = self.page.ele(f'tag:label@@text():{gender}') ele.scroll.to_see(center=True) ele.click() except Exception as e: self._log(e) fill_date_field(self.page, 'tag:input@@data-testid=f_pers_birth_date', self.account_detail.get('birthday')) nationality = self.account_detail.get('nationality') if nationality: nationality = nationality.title() btn = self.page.ele('tag:label@@for=f_pers_nationality').next() btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:li@@role=option@@text():{nationality}').click(by_js=True) time.sleep(0.5) province_residence = self.account_detail.get('province_residence') if province_residence: try: province_residence = province_residence.title() btn = self.page.ele('tag:label@@for=f_pers_province').next() btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:li@@role=option@@text():{province_residence}').click(by_js=True) time.sleep(0.5) except Exception as e: self._log(e) passport_type = self.account_detail.get('passport_type') if passport_type: btn = self.page.ele('tag:label@@for=f_identity_type').next() btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:li@@role=option@@text():{passport_type}').click(by_js=True) time.sleep(0.5) passport_no = self.account_detail.get('passport_no') if passport_no: ele = self.page.ele('tag:input@@data-testid=f_pass_num') ele.scroll.to_see(center=True) ele.input(passport_no.upper()) passport_issue_date = self.account_detail.get('passport_issue_date') if passport_issue_date: try: fill_date_field(self.page, 'tag:input@@data-testid=fi_passport_issue_date', passport_issue_date) time.sleep(0.5) except Exception as e: self._log(e) passport_expiry_date = self.account_detail.get('passport_expiry_date') if passport_expiry_date: try: fill_date_field(self.page, 'tag:input@@data-testid=fi_passport_expiry_date', passport_expiry_date) time.sleep(0.5) except Exception as e: self._log(e) phone_country_code = self.account_detail.get('phone_country_code') phone_number = self.account_detail.get('phone_number') if phone_country_code: div = self.page.ele('tag:label@@for=f_pers_mobile_phone').next() btn = div.ele('tag:button') btn.scroll.to_see(center=True) btn.click() time.sleep(0.5) self.page.ele(f'tag:li@@role=option@@text():+{phone_country_code}').click(by_js=True) time.sleep(0.5) div.ele('tag:input@@type:tel').input(phone_number) fill_date_field(self.page, 'tag:input@@data-testid=fi_trav_origin_departure_date', self.account_detail.get('departure_origin_date')) fill_date_field(self.page, 'tag:input@@data-testid=f_trav_departure_date', self.account_detail.get('arrival_schengen_area_date')) fill_date_field(self.page, 'tag:input@@data-testid=f_trav_arrival_date', self.account_detail.get('departure_schengen_area_date')) submit_btn = self.page.ele('tag:button@@data-testid=btn-submit') submit_btn.scroll.to_see(center=True) time.sleep(1) submit_btn.click() time.sleep(6) confirm_btn = self.page.ele('tag:button@@text():Confirm') if confirm_btn: confirm_btn.scroll.to_see(center=True) confirm_btn.click() time.sleep(6) self._log("✅ 自动填表提交成功!") def upload_account_to_server(self): api_url = 'https://api.text.skin/api/account/add' api_token = 'tok_e946329a60ff45ba807f3f41b0e8b7fc' headers = { 'accept': 'application/json', 'Authorization': f'Bearer {api_token}', 'Content-Type': 'application/json' } payload = { "pool_name": self.account_detail.get("pool_name", "default_pool"), "username": self.account_detail.get("email"), "password": self.account_detail.get("pwd"), "extra_data": self.account_detail } try: self._log(f"Uploading account {self.account_detail['email']} to server...") resp = requests.post(api_url, json=payload, headers=headers, timeout=10) if resp.status_code == 200: self._log(f"✅ [API Upload Success] Server responded: {resp.text}") return True else: self._log(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}") return False except Exception as e: self._log(f"❌ [API Upload Error]: {e}") return False def cleanup(self): self._log("Cleaning up resources...") if self.page: try: self.page.quit() except: pass # ================= PyQt5 GUI 主窗口 ================= class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("TLS 批量注册工具 v1.0 (PyQt5)") self.resize(1000, 700) self.bot = None self.profiles = [] self.capsolver_key = os.getenv("CAPSOLVER_KEY", "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A") # 实例化信号类并绑定槽函数 self.signals = GuiSignals() self.signals.log_signal.connect(self.append_log) self.signals.status_signal.connect(self.update_table_status) self.init_ui() def init_ui(self): main_widget = QWidget() main_layout = QVBoxLayout(main_widget) # 1. 基础设置区域 group_settings = QGroupBox("1. 基础设置") layout_settings = QGridLayout() layout_settings.addWidget(QLabel("地区 (如 CN-Beijing):"), 0, 0) self.input_region = QLineEdit("CN-Beijing") layout_settings.addWidget(self.input_region, 0, 1) layout_settings.addWidget(QLabel("代理池名 (从proxies.json读取):"), 0, 2) self.input_pool = QLineEdit("local") layout_settings.addWidget(self.input_pool, 0, 3) layout_settings.addWidget(QLabel("目标 URL:"), 1, 0) self.input_url = QLineEdit("https://visas-fr.tlscontact.com/en-us/country/gb/vac/gbLON2fr") layout_settings.addWidget(self.input_url, 1, 1, 1, 3) group_settings.setLayout(layout_settings) main_layout.addWidget(group_settings) # 2. 生成区域 group_gen = QGroupBox("2. 生成虚拟信息") layout_gen = QHBoxLayout() layout_gen.addWidget(QLabel("生成数量 (1-100):")) self.spin_count = QSpinBox() self.spin_count.setRange(1, 100) self.spin_count.setValue(5) layout_gen.addWidget(self.spin_count) self.btn_generate = QPushButton("生成虚拟信息") self.btn_generate.setStyleSheet("background-color: lightblue;") self.btn_generate.clicked.connect(self.action_generate) layout_gen.addWidget(self.btn_generate) layout_gen.addStretch() group_gen.setLayout(layout_gen) main_layout.addWidget(group_gen) # 3. 表格展示区域 self.table = QTableWidget() self.table.setColumnCount(5) self.table.setHorizontalHeaderLabels(["序号", "邮箱", "密码", "姓名", "当前状态"]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) main_layout.addWidget(self.table) # 4. 浏览器操作区域 group_actions = QGroupBox("3. 浏览器操作 (需先在表格中点击选中一行)") layout_actions = QHBoxLayout() self.btn_start = QPushButton("[全局] 启动浏览器") self.btn_start.clicked.connect(self.action_start_browser) layout_actions.addWidget(self.btn_start) self.btn_register = QPushButton("1. 自动注册并激活") self.btn_register.setStyleSheet("background-color: #d0f0c0;") self.btn_register.clicked.connect(self.action_register) layout_actions.addWidget(self.btn_register) self.btn_fill = QPushButton("2. 自动填表") self.btn_fill.setStyleSheet("background-color: #ffd700;") self.btn_fill.clicked.connect(self.action_fill) layout_actions.addWidget(self.btn_fill) self.btn_upload = QPushButton("3. 上报并标记已用") self.btn_upload.setStyleSheet("background-color: #ffb6c1;") self.btn_upload.clicked.connect(self.action_upload) layout_actions.addWidget(self.btn_upload) self.btn_close = QPushButton("[全局] 关闭浏览器") self.btn_close.setStyleSheet("color: red;") self.btn_close.clicked.connect(self.action_close_browser) layout_actions.addWidget(self.btn_close) group_actions.setLayout(layout_actions) main_layout.addWidget(group_actions) # 5. 日志区域 group_log = QGroupBox("运行日志") layout_log = QVBoxLayout() self.text_log = QTextEdit() self.text_log.setReadOnly(True) layout_log.addWidget(self.text_log) group_log.setLayout(layout_log) main_layout.addWidget(group_log) self.setCentralWidget(main_widget) # ---------------- 槽函数:UI 更新 ---------------- def append_log(self, text): self.text_log.append(text) self.text_log.moveCursor(self.text_log.textCursor().End) def update_table_status(self, row, status): item = QTableWidgetItem(status) item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 4, item) # ---------------- 业务逻辑触发 ---------------- def run_in_thread(self, target_func, *args): t = threading.Thread(target=target_func, args=args, daemon=True) t.start() def action_generate(self): count = self.spin_count.value() region = self.input_region.text().strip() pool = self.input_pool.text().strip() self.append_log(f"开始生成 {count} 个 {region} 地区的虚拟信息...") self.profiles.clear() self.table.setRowCount(0) for i in range(count): try: acc = generate_random_account_detail(region) acc['pool_name'] = pool self.profiles.append(acc) self.table.insertRow(i) self.table.setItem(i, 0, QTableWidgetItem(str(i+1))) self.table.setItem(i, 1, QTableWidgetItem(acc.get('email', ''))) self.table.setItem(i, 2, QTableWidgetItem(acc.get('pwd', ''))) name = f"{acc.get('first_name','')} {acc.get('last_name','')}" self.table.setItem(i, 3, QTableWidgetItem(name)) self.update_table_status(i, "就绪") except Exception as e: self.append_log(f"生成时出错: {e}") self.append_log("✅ 虚拟信息生成完毕!") def action_start_browser(self): pool = self.input_pool.text().strip() url = self.input_url.text().strip() def _task(): try: self.signals.log_signal.emit(f"正在读取代理池 {pool} ...") proxies = load_proxies(pool) proxy = random.choice(proxies) self.signals.log_signal.emit(f"已选取代理: {proxy.get('ip')}") self.bot = TlsRegistrator( tls_url=url, proxy_config=proxy, capsolver_key=self.capsolver_key, signals=self.signals ) self.bot.init_browser() except Exception as e: self.signals.log_signal.emit(f"❌ 浏览器启动失败: {e}") self.run_in_thread(_task) def _get_selected_row(self): row = self.table.currentRow() if row < 0: QMessageBox.warning(self, "警告", "请先在表格中点击选中需要操作的虚拟账号!") return None return row def action_register(self): row = self._get_selected_row() if row is None: return if not self.bot: QMessageBox.warning(self, "警告", "请先启动浏览器!") return profile = self.profiles[row] def _task(): try: self.bot.account_detail = profile self.signals.status_signal.emit(row, "注册中...") sent_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") self.bot.register() self.signals.status_signal.emit(row, "激活中...") self.bot.activate(sent_at=sent_at) self.signals.status_signal.emit(row, "注册激活完成") except Exception as e: self.signals.log_signal.emit(f"❌ 注册过程异常: {e}") self.signals.status_signal.emit(row, "注册失败") self.run_in_thread(_task) def action_fill(self): row = self._get_selected_row() if row is None: return if not self.bot: QMessageBox.warning(self, "警告", "请先启动浏览器!") return profile = self.profiles[row] def _task(): try: self.bot.account_detail = profile self.signals.status_signal.emit(row, "填表中...") self.bot.make_account_useful() self.signals.status_signal.emit(row, "填表完成") except Exception as e: self.signals.log_signal.emit(f"❌ 填表过程异常: {e}") self.signals.status_signal.emit(row, "填表失败") self.run_in_thread(_task) def action_upload(self): row = self._get_selected_row() if row is None: return if not self.bot: return profile = self.profiles[row] def _task(): try: self.bot.account_detail = profile self.signals.status_signal.emit(row, "上报中...") success = self.bot.upload_account_to_server() if success: self.signals.status_signal.emit(row, "已使用(上报成功)") else: self.signals.status_signal.emit(row, "上报失败") except Exception as e: self.signals.log_signal.emit(f"❌ 上报异常: {e}") self.signals.status_signal.emit(row, "上报异常") self.run_in_thread(_task) def action_close_browser(self): if self.bot: self.bot.cleanup() self.bot = None self.append_log("✅ 浏览器已关闭。") def closeEvent(self, event): """主窗口关闭时,确保清理资源""" if self.bot: self.bot.cleanup() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())