Bladeren bron

feat: update

Hujiarui 2 maanden geleden
bovenliggende
commit
e54e6499a2

+ 0 - 2
booker_builtin.py

@@ -181,7 +181,6 @@ class BuiltinBookerGCO:
 
 
         task_id = None
         task_id = None
         task_data = None
         task_data = None
-        booking_success = False
         
         
         try:
         try:
             task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
             task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
@@ -195,7 +194,6 @@ class BuiltinBookerGCO:
             book_res = task.instance.book(query_result, user_input)
             book_res = task.instance.book(query_result, user_input)
             
             
             if book_res.success:
             if book_res.success:
-                booking_success = True
                 self._log(f"✅ BOOK SUCCESS! Order: {order_id}")
                 self._log(f"✅ BOOK SUCCESS! Order: {order_id}")
                 
                 
                 grab_info = {
                 grab_info = {

+ 0 - 3
plugins/bel_plugin.py

@@ -73,9 +73,6 @@ class BelPlugin(IVSPlg):
         self.tunnel = None # 代理隧道
         self.tunnel = None # 代理隧道
         self.is_healthy = True
         self.is_healthy = True
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 0 - 3
plugins/bls_plugin.py

@@ -60,9 +60,6 @@ class BlsPlugin(IVSPlg):
         # OCR 服务地址默认值
         # OCR 服务地址默认值
         self.local_service_url: str = ""
         self.local_service_url: str = ""
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 0 - 3
plugins/de_plugin.py

@@ -80,9 +80,6 @@ class DePlugin(IVSPlg):
         self.csrf_token = ""
         self.csrf_token = ""
         self.personal_info_val = ""
         self.personal_info_val = ""
         self.email_val_control = ""
         self.email_val_control = ""
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 0 - 3
plugins/grc_plugin.py

@@ -38,9 +38,6 @@ class GrcPlugin(IVSPlg):
         self.rp_id = None
         self.rp_id = None
         self.token = None
         self.token = None
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 0 - 4
plugins/ita_plugin.py

@@ -69,11 +69,7 @@ class ItaPlugin(IVSPlg):
             
             
         # 持有隧道实例
         # 持有隧道实例
         self.tunnel = None
         self.tunnel = None
-        
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 0 - 3
plugins/pol_plugin.py

@@ -72,9 +72,6 @@ class PolPlugin(IVSPlg):
         self.tunnel = None # 代理隧道
         self.tunnel = None # 代理隧道
         self.is_healthy = True
         self.is_healthy = True
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
         self.logger = logger

+ 251 - 264
plugins/tls_plugin.py

@@ -68,9 +68,6 @@ class TlsPlugin(IVSPlg):
         
         
         self.tunnel = None
         self.tunnel = None
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     
     def set_log(self, logger: Callable[[str], None]):
     def set_log(self, logger: Callable[[str], None]):
         self.logger = logger
         self.logger = logger
@@ -237,288 +234,278 @@ class TlsPlugin(IVSPlg):
         co.set_argument(f"--fingerprint-platform={fp_platform}")
         co.set_argument(f"--fingerprint-platform={fp_platform}")
         co.set_argument(f"--fingerprint-brand={fp_brand}")
         co.set_argument(f"--fingerprint-brand={fp_brand}")
         
         
-        try:
-            self.page = ChromiumPage(co)
-            
-            # --- 预检指纹信息 ---
-            if self.config.debug:
-                self.page.get('https://example.com')
-                js_script = """
-                function getFingerprint() {
-                    let webglVendor = 'Unknown';
-                    let webglRenderer = 'Unknown';
-                    try {
-                        let canvas = document.createElement('canvas');
-                        let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
-                        if (gl) {
-                            let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
-                            if (debugInfo) {
-                                webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
-                                webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
-                            }
+        self.page = ChromiumPage(co)
+        if self.config.debug:
+            self.page.get('https://example.com')
+            js_script = """
+            function getFingerprint() {
+                let webglVendor = 'Unknown';
+                let webglRenderer = 'Unknown';
+                try {
+                    let canvas = document.createElement('canvas');
+                    let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+                    if (gl) {
+                        let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
+                        if (debugInfo) {
+                            webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
+                            webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
                         }
                         }
-                    } catch(e) {}
-
-                    return {
-                        "User-Agent": navigator.userAgent,
-                        "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
-                        "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
-                        "CPU Cores": navigator.hardwareConcurrency,
-                        "Language": navigator.language,
-                        "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
-                        "WebGL Vendor": webglVendor,
-                        "WebGL Renderer": webglRenderer
-                    };
-                }
-                return getFingerprint();
-                """
-                fp_data = self.page.run_js(js_script)
-                self._log("================ 预检浏览器指纹数据 ================")
-                self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
-                self._log("====================================================")
-
-            # --- 初始化访问与过盾 ---
-            tls_url = self.free_config.get('tls_url', '')
-            self._log(f"Navigating: {tls_url}")
-            self.page.get(tls_url)
-            time.sleep(5)
-            if 'Attention Required! | Cloudflare' in self.page.title and 'Sorry, you have been blocked' in self.page.html:
-                self._log(f'Block by cloudflare, try refresh...')
-                self.page.refresh()
-                self.page.wait.load_start(timeout=2)
-                self.page.wait.doc_loaded()
-            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-            if not cf_bypasser.bypass(max_retry=6):
-                raise BizLogicError("Cloudflare bypass timeout")
-            time.sleep(3)
-            cf_bypasser.handle_waiting_room()
+                    }
+                } catch(e) {}
+
+                return {
+                    "User-Agent": navigator.userAgent,
+                    "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
+                    "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
+                    "CPU Cores": navigator.hardwareConcurrency,
+                    "Language": navigator.language,
+                    "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
+                    "WebGL Vendor": webglVendor,
+                    "WebGL Renderer": webglRenderer
+                };
+            }
+            return getFingerprint();
+            """
+            fp_data = self.page.run_js(js_script)
+            self._log("================ 预检浏览器指纹数据 ================")
+            self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
+            self._log("====================================================")
+
+        # --- 初始化访问与过盾 ---
+        tls_url = self.free_config.get('tls_url', '')
+        self._log(f"Navigating: {tls_url}")
+        self.page.get(tls_url)
+        time.sleep(5)
+        if 'Attention Required! | Cloudflare' in self.page.title and 'Sorry, you have been blocked' in self.page.html:
+            self._log(f'Block by cloudflare, try refresh...')
+            self.page.refresh()
+            self.page.wait.load_start(timeout=2)
+            self.page.wait.doc_loaded()
+        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
+        if not cf_bypasser.bypass(max_retry=6):
+            raise BizLogicError("Cloudflare bypass timeout")
+        time.sleep(3)
+        cf_bypasser.handle_waiting_room()
+        
+        self._log("Init humanize tools...")
+        
+        profile_name = random.choice(list(MOUSE_PROFILES.keys()))
+        self._log(f"[HumanMouse] current mouse profiles: {profile_name}") 
+        
+        self.mouse = HumanMouse(self.page, timing=MOUSE_PROFILES[profile_name], debug=self.config.debug)
+        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) 
+
+        max_steps = 10 
+        session_created = False
+        has_submitted_login = False
+        
+        other_langs = ['fr-fr', 'ar-ar', 'cb-ph', 'id-id', 'km-kh', 'mk-mk', 'ru-ru', 'sq-al', 'th-th', 'tl-ph', 'uz-uz', 'vi-vn', 'zh-cn', 'hy-am', 'zh-hk']
+        
+        for step in range(max_steps):
+            self.page.wait.doc_loaded()
+            time.sleep(1)
             
             
-            self._log("Init humanize tools...")
+            current_url = self.page.url
+            current_title = self.page.title.lower()
+            current_html_content = self.page.html
+            self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
             
             
-            profile_name = random.choice(list(MOUSE_PROFILES.keys()))
-            self._log(f"[HumanMouse] current mouse profiles: {profile_name}") 
+            cloudflare_blocked_indicators = [
+                "Sorry, you have been blocked" in current_html_content,
+                "You are being rate limited" in current_html_content,
+                "Cloudflare Ray ID" in current_html_content
+            ]
+            if any(cloudflare_blocked_indicators):
+                raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
             
             
-            self.mouse = HumanMouse(self.page, timing=MOUSE_PROFILES[profile_name], debug=self.config.debug)
-            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) 
-
-            max_steps = 10 
-            session_created = False
-            has_submitted_login = False
+            # 遇到五秒盾先绕盾
+            if "just a moment" in current_title:
+                cf_bypasser.bypass(max_retry=3)
+                time.sleep(3)
+                continue
             
             
-            other_langs = ['fr-fr', 'ar-ar', 'cb-ph', 'id-id', 'km-kh', 'mk-mk', 'ru-ru', 'sq-al', 'th-th', 'tl-ph', 'uz-uz', 'vi-vn', 'zh-cn', 'hy-am', 'zh-hk']
+            # 如果语言不匹配, 切换语言到英语
+            matched_lang = next((lang for lang in other_langs if lang in current_url), None)
+            if matched_lang:
+                current_url = current_url.replace(matched_lang, 'en-us')
+                self.page.get(current_url)
+                self.page.wait.load_start(timeout=3)
+                continue
+                    
+            # 到达终极目标页面 (成功退出条件)
+            if "appointment-booking" in current_url or self.page.ele('tag:button@text():Book your appointment', timeout=1):
+                btn_selector = 'tag:button@text():Book your appointment'            
+                if self.page.wait.ele_displayed(btn_selector, timeout=10):
+                    self._handle_cookie_dialog()
+                    self.session_create_time = time.time()
+                    self._log("✅ Login & Navigation Success! Reached appointment-booking.")
+                    session_created = True
+                    break
             
             
-            for step in range(max_steps):
-                self.page.wait.doc_loaded()
-                time.sleep(1)
-                
-                current_url = self.page.url
-                current_title = self.page.title.lower()
-                current_html_content = self.page.html
-                self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
-                
-                cloudflare_blocked_indicators = [
-                    "Sorry, you have been blocked" in current_html_content,
-                    "You are being rate limited" in current_html_content,
-                    "Cloudflare Ray ID" in current_html_content
-                ]
-                if any(cloudflare_blocked_indicators):
-                    raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
-                
-                # 遇到五秒盾先绕盾
-                if "just a moment" in current_title:
-                    cf_bypasser.bypass(max_retry=3)
-                    time.sleep(3)
-                    continue
-                
-                # 如果语言不匹配, 切换语言到英语
-                matched_lang = next((lang for lang in other_langs if lang in current_url), None)
-                if matched_lang:
-                    current_url = current_url.replace(matched_lang, 'en-us')
-                    self.page.get(current_url)
+            # 页面发生react渲染错误
+            if 'a client-side exception has occurred while loading' in current_html_content:
+                self.page.refresh()
+                self.page.wait.load_start(timeout=3)
+                continue
+            
+            # 改签的情况
+            if '/workflow/application-summary' in current_url:
+                change_btn_sel = 'tag:button@@text():Change'
+                self.page.wait.ele_displayed(change_btn_sel, timeout=10)
+                self.page.ele(change_btn_sel).click(by_js=True)
+                confirm_btn_sel = 'tag:button@@text():Yes'
+                self.page.wait.ele_displayed(confirm_btn_sel, timeout=5)
+                self.page.ele(confirm_btn_sel).click(by_js=True)
+                self.page.wait.load_start(timeout=5)
+                continue
+            
+            # 遇到没有申请人的拦截页 (致命错误退出条件)
+            no_applicant_indicators = [
+                "Add a new applicant" in current_html_content,
+                "You have not yet added an applicant" in current_html_content,
+                "applicants-information" in current_url
+            ]
+            if any(no_applicant_indicators):
+                raise BizLogicError(message="No applicant added. Cannot proceed to booking.") 
+            
+            # 首页/登录入口页 -> 需要点击进入登录
+            if current_url == tls_url:
+                if self.page.ele("tag:a@@href:login", timeout=1) and not self.page.ele('tag:label@@text():Email', timeout=1):
+                    self._log("State: Login Portal. Clicking login link...")
+                    login_link = self.page.ele("tag:a@@href:login")
+                    self.mouse.human_click_ele(login_link)
+                    
                     self.page.wait.load_start(timeout=3)
                     self.page.wait.load_start(timeout=3)
                     continue
                     continue
-                     
-                # 到达终极目标页面 (成功退出条件)
-                if "appointment-booking" in current_url or self.page.ele('tag:button@text():Book your appointment', timeout=1):
-                    btn_selector = 'tag:button@text():Book your appointment'            
-                    if self.page.wait.ele_displayed(btn_selector, timeout=10):
-                        self._handle_cookie_dialog()
-                        self.session_create_time = time.time()
-                        self._log("✅ Login & Navigation Success! Reached appointment-booking.")
-                        session_created = True
-                        break
-                
-                # 页面发生react渲染错误
-                if 'a client-side exception has occurred while loading' in current_html_content:
-                    self.page.refresh()
+
+                if self.page.ele("tag:svg@@data-testid=user-button", timeout=1):
+                    self._log("State: Already login, logout now...")
+                    user_btn = self.page.ele("tag:svg@@data-testid=user-button")
+                    self.mouse.human_click_ele(user_btn)
+                    time.sleep(1.5)
+                    logout_btn = self.page.ele("#logout")
+                    self.mouse.human_click_ele(logout_btn)
+                    
+                    self.page.wait.load_start(timeout=3)
+                    self.page.get(tls_url)
                     self.page.wait.load_start(timeout=3)
                     self.page.wait.load_start(timeout=3)
                     continue
                     continue
+            
+            # 真正的登录表单页
+            if self.page.ele('tag:label@@text():Email', timeout=1) and not has_submitted_login:
+                self._log("State: Login Form. Processing credentials and Captcha...")
                 
                 
-                # 改签的情况
-                if '/workflow/application-summary' in current_url:
-                    change_btn_sel = 'tag:button@@text():Change'
-                    self.page.wait.ele_displayed(change_btn_sel, timeout=10)
-                    self.page.ele(change_btn_sel).click(by_js=True)
-                    confirm_btn_sel = 'tag:button@@text():Yes'
-                    self.page.wait.ele_displayed(confirm_btn_sel, timeout=5)
-                    self.page.ele(confirm_btn_sel).click(by_js=True)
-                    self.page.wait.load_start(timeout=5)
-                    continue
-                
-                # 遇到没有申请人的拦截页 (致命错误退出条件)
-                no_applicant_indicators = [
-                    "Add a new applicant" in current_html_content,
-                    "You have not yet added an applicant" in current_html_content,
-                    "applicants-information" in current_url
-                ]
-                if any(no_applicant_indicators):
-                    raise BizLogicError(message="No applicant added. Cannot proceed to booking.") 
-                
-                # 首页/登录入口页 -> 需要点击进入登录
-                if current_url == tls_url:
-                    if self.page.ele("tag:a@@href:login", timeout=1) and not self.page.ele('tag:label@@text():Email', timeout=1):
-                        self._log("State: Login Portal. Clicking login link...")
-                        login_link = self.page.ele("tag:a@@href:login")
-                        self.mouse.human_click_ele(login_link)
-                        
-                        self.page.wait.load_start(timeout=3)
-                        continue
+                recaptchav2_token = ""
+                if not captcha_future and (self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')):
+                    rec_iframe = self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')
+                    rec_iframe_src = rec_iframe.attr('src')
+                    rec_parsed = urlparse(rec_iframe_src)
+                    rec_params = parse_qs(rec_parsed.query)
+                    rec_sitekey = rec_params.get("k", [None])[0]
+                    rec_size = rec_params.get("size", [None])[0]
+                    
+                    if 'normal' == rec_size:
+                        self._log(f"Found dynamic sitekey={rec_sitekey}. Starting async Captcha solver...")
+                        rc_params = {
+                            "type": "ReCaptchaV2TaskProxyLess",
+                            "page": current_url,
+                            "siteKey": rec_sitekey, 
+                            "apiToken": self.free_config.get("capsolver_key", "")
+                        }
+                        captcha_future = captcha_executor.submit(self._solve_recaptcha, rc_params)
 
 
-                    if self.page.ele("tag:svg@@data-testid=user-button", timeout=1):
-                        self._log("State: Already login, logout now...")
-                        user_btn = self.page.ele("tag:svg@@data-testid=user-button")
-                        self.mouse.human_click_ele(user_btn)
-                        time.sleep(1.5)
-                        logout_btn = self.page.ele("#logout")
-                        self.mouse.human_click_ele(logout_btn)
-                        
-                        self.page.wait.load_start(timeout=3)
-                        self.page.get(tls_url)
-                        self.page.wait.load_start(timeout=3)
-                        continue
+                username = self.config.account.username
+                password = self.config.account.password
                 
                 
-                # 真正的登录表单页
-                if self.page.ele('tag:label@@text():Email', timeout=1) and not has_submitted_login:
-                    self._log("State: Login Form. Processing credentials and Captcha...")
-                    
-                    recaptchav2_token = ""
-                    if not captcha_future and (self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')):
-                        rec_iframe = self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')
-                        rec_iframe_src = rec_iframe.attr('src')
-                        rec_parsed = urlparse(rec_iframe_src)
-                        rec_params = parse_qs(rec_parsed.query)
-                        rec_sitekey = rec_params.get("k", [None])[0]
-                        rec_size = rec_params.get("size", [None])[0]
-                        
-                        if 'normal' == rec_size:
-                            self._log(f"Found dynamic sitekey={rec_sitekey}. Starting async Captcha solver...")
-                            rc_params = {
-                                "type": "ReCaptchaV2TaskProxyLess",
-                                "page": current_url,
-                                "siteKey": rec_sitekey, 
-                                "apiToken": self.free_config.get("capsolver_key", "")
-                            }
-                            captcha_future = captcha_executor.submit(self._solve_recaptcha, rc_params)
-
-                    username = self.config.account.username
-                    password = self.config.account.password
-                    
-                    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(username, humanize=True)
-                    time.sleep(random.uniform(0.5, 1.2)) 
+                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(username, 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)
+                
+                # 注入 Token
+                if captcha_future:
+                    self._log("Waiting for background Captcha result...")
+                    try:
+                        # 设一个合理的超时,防止死锁
+                        recaptchav2_token = captcha_future.result(timeout=120) 
+                        self._log("Background Captcha solved successfully!")
+                    except Exception as e:
+                        raise BizLogicError(f"Captcha solving failed or timed out: {e}")
                 
                 
-                    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)
-                    
                     # 注入 Token
                     # 注入 Token
-                    if captcha_future:
-                        self._log("Waiting for background Captcha result...")
-                        try:
-                            # 设一个合理的超时,防止死锁
-                            recaptchav2_token = captcha_future.result(timeout=120) 
-                            self._log("Background Captcha solved successfully!")
-                        except Exception as e:
-                            raise BizLogicError(f"Captcha solving failed or timed out: {e}")
-                    
-                        # 注入 Token
-                        if recaptchav2_token:
-                            inject_js = f"var g = document.getElementById('g-recaptcha-response'); if(g) {{ g.value = '{recaptchav2_token}'; }}"
-                            self.page.run_js(inject_js)
-                            time.sleep(random.uniform(0.5, 1.0))
-                    
-                    self._log("Submitting Login...")
-                    login_btn = self.page.ele('tag:button@@text():Login')
-                    self.mouse.human_click_ele(login_btn)
-                    has_submitted_login = True
-                    
-                    self.page.wait.load_start(timeout=5)
-                    continue
+                    if recaptchav2_token:
+                        inject_js = f"var g = document.getElementById('g-recaptcha-response'); if(g) {{ g.value = '{recaptchav2_token}'; }}"
+                        self.page.run_js(inject_js)
+                        time.sleep(random.uniform(0.5, 1.0))
                 
                 
-                # Travel Groups 页面
-                if "travel-groups" in current_url:
-                    self._log("State: Travel Groups. Selecting targeted group...")
-                    groups = self._parse_travel_groups(current_html_content)
-                    location = self.free_config.get('location')
-                    self.travel_group = next((g for g in groups if location in g['location']), None)
-                    
-                    if not self.travel_group or not self.travel_group.get("submitted"):
-                        self._save_screenshot("group_not_found")
-                        raise NotFoundError(f"Group not found for {location}")
+                self._log("Submitting Login...")
+                login_btn = self.page.ele('tag:button@@text():Login')
+                self.mouse.human_click_ele(login_btn)
+                has_submitted_login = True
                 
                 
-                    formgroup_id = self.travel_group.get('group_number')
-                    btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
-                    
-                    if self.page.wait.eles_loaded(btn_selector, timeout=10):
-                        buttons = self.page.eles(btn_selector)
-                        select_btn = next((btn for btn in reversed(buttons) if btn.rect.size[0] > 0 and btn.rect.size[1] > 0), None)
-                        
-                        if select_btn:
-                            time.sleep(random.uniform(0.5, 1.2))
-                            self.mouse.human_click_ele(select_btn)
-                            self.page.wait.load_start(timeout=3)
-                            continue
-                        else:
-                            self._log("[WARN] Select button found but not visible.")
-                    else:
-                        self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
+                self.page.wait.load_start(timeout=5)
+                continue
+            
+            # Travel Groups 页面
+            if "travel-groups" in current_url:
+                self._log("State: Travel Groups. Selecting targeted group...")
+                groups = self._parse_travel_groups(current_html_content)
+                location = self.free_config.get('location')
+                self.travel_group = next((g for g in groups if location in g['location']), None)
                 
                 
-                # 中间过渡页,需点击 "Book Appointment" 继续往下走
-                if self.page.ele('#book-appointment-btn', timeout=1):
-                    self._log("State: Intermediate Dashboard. Clicking Book Appointment button...")
-                    self.mouse.human_click_ele(self.page.ele('#book-appointment-btn'))
-                    
-                    self.page.wait.load_start(timeout=3)
-                    continue
+                if not self.travel_group or not self.travel_group.get("submitted"):
+                    self._save_screenshot("group_not_found")
+                    raise NotFoundError(f"Group not found for {location}")
+            
+                formgroup_id = self.travel_group.get('group_number')
+                btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
                 
                 
-                # 登录失败校验 或 未知加载状态
-                if "login-actions" in current_url and has_submitted_login:
-                    self._log("Waiting on login-actions... (Might be authenticating or invalid credentials)")
-                    time.sleep(2)
-                    if self.page.ele('text:Invalid username or password', timeout=1): # 假设网页上有错误提示
-                        raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
-                    continue
+                if self.page.wait.eles_loaded(btn_selector, timeout=10):
+                    buttons = self.page.eles(btn_selector)
+                    select_btn = next((btn for btn in reversed(buttons) if btn.rect.size[0] > 0 and btn.rect.size[1] > 0), None)
+                    
+                    if select_btn:
+                        time.sleep(random.uniform(0.5, 1.2))
+                        self.mouse.human_click_ele(select_btn)
+                        self.page.wait.load_start(timeout=3)
+                        continue
+                    else:
+                        self._log("[WARN] Select button found but not visible.")
+                else:
+                    self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
+            
+            # 中间过渡页,需点击 "Book Appointment" 继续往下走
+            if self.page.ele('#book-appointment-btn', timeout=1):
+                self._log("State: Intermediate Dashboard. Clicking Book Appointment button...")
+                self.mouse.human_click_ele(self.page.ele('#book-appointment-btn'))
                 
                 
-                self._log("State: Transitioning or Unknown. Waiting 2 seconds...")
+                self.page.wait.load_start(timeout=3)
+                continue
+            
+            # 登录失败校验 或 未知加载状态
+            if "login-actions" in current_url and has_submitted_login:
+                self._log("Waiting on login-actions... (Might be authenticating or invalid credentials)")
                 time.sleep(2)
                 time.sleep(2)
-                    
-            if not session_created:
-                raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
-
-        except Exception as e:
-            self._log(f"Session Create Error: {e}")
-            if self.config.debug:
-                self._save_screenshot("create_session_except")
-            self.cleanup()
-            raise e
+                if self.page.ele('text:Invalid username or password', timeout=1): # 假设网页上有错误提示
+                    raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
+                continue
+            
+            self._log("State: Transitioning or Unknown. Waiting 2 seconds...")
+            time.sleep(2)
+                
+        if not session_created:
+            raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
 
 
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
         res = VSQueryResult()

+ 110 - 146
plugins/vfs_plugin.py

@@ -14,8 +14,6 @@ from datetime import datetime
 from typing import Dict, Any, Optional, List, Tuple, Callable
 from typing import Dict, Any, Optional, List, Tuple, Callable
 
 
 from DrissionPage import ChromiumPage, ChromiumOptions
 from DrissionPage import ChromiumPage, ChromiumOptions
-from DrissionPage.common import Settings
-
 from cryptography.hazmat.primitives import serialization, hashes
 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
@@ -137,27 +135,18 @@ class VfsPlugin(IVSPlg):
             VFS_PUBLIC_KEY_PEM.encode(),
             VFS_PUBLIC_KEY_PEM.encode(),
             backend=default_backend()
             backend=default_backend()
         )
         )
-        
-        # --- [核心修改] 并发隔离与资源管理 ---
-        # 生成唯一实例 ID
+
         self.instance_id = uuid.uuid4().hex[:8]
         self.instance_id = uuid.uuid4().hex[:8]
         self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
         self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
         self.user_data_path = os.path.join(self.root_workspace, "user_data")
         self.user_data_path = os.path.join(self.root_workspace, "user_data")
         
         
         self.vfs_api_domain = 'lift-api.vfsglobal.com'
         self.vfs_api_domain = 'lift-api.vfsglobal.com'
-        
-        # 持有隧道实例
-        self.tunnel = None
-        
-        # 确保根目录存在 (子目录由具体逻辑创建)
         if not os.path.exists(self.root_workspace):
         if not os.path.exists(self.root_workspace):
             os.makedirs(self.root_workspace)
             os.makedirs(self.root_workspace)
             
             
+        self.tunnel = None
         self.session_create_time: float = 0
         self.session_create_time: float = 0
 
 
-    def get_group_id(self) -> str:
-        return self.group_id
-
     def set_config(self, config: VSPlgConfig):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.config = config
         self.free_config = config.free_config or {}
         self.free_config = config.free_config or {}
@@ -269,139 +258,126 @@ class VfsPlugin(IVSPlg):
         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')
-        try:
-            self.page = ChromiumPage(co)
-            mission = self.free_config.get("mission_code", "")
-            country = self.free_config.get("country_code", "")
-            lang = self.free_config.get("language", "en")
-            
-            if not mission or not country:
-                raise BizLogicError("Missing mission/country code config")
+        self.page = ChromiumPage(co)
+        mission = self.free_config.get("mission_code", "")
+        country = self.free_config.get("country_code", "")
+        lang = self.free_config.get("language", "en")
+        
+        if not mission or not country:
+            raise BizLogicError("Missing mission/country code config")
 
 
-            login_page_url = f"https://visa.vfsglobal.com/{country}/{lang}/{mission}/login"
-            self._log(f"Navigating to {login_page_url}...")
-            
-            self.page.get(login_page_url)
-            
-            self._log("Handling Cloudflare challenge...")
-            
-            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-            cf_token = ""
-            
-            for i in range(40):
-                time.sleep(1)
-                self._handle_cookie_banner()
-                try:
-                    ele = self.page.ele('@name=cf-turnstile-response', timeout=1)
-                    if ele and ele.value:
-                        cf_token = ele.value
-                        self._log("Cloudflare Turnstile token extracted.")
-                        break
-                except:
-                    pass
-                
-                if i > 2:
-                    try:
-                        use_dfs = False
-                        cf_bypasser.click_verification_button(use_dfs)
-                    except Exception as e:
-                        pass
-                
-                if self.page.ele('tag:form', timeout=0.5) or self.page.ele('#mat-input-0', timeout=0.5):
-                    self._log("Login form detected.")
-                    if i > 5 and not cf_token:
-                        self._log("Form visible but token not found yet...")
-            
-            # -------------------------------------------------------------
+        login_page_url = f"https://visa.vfsglobal.com/{country}/{lang}/{mission}/login"
+        self._log(f"Navigating to {login_page_url}...")
+        
+        self.page.get(login_page_url)
+        
+        self._log("Handling Cloudflare challenge...")
+        
+        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
+        cf_token = ""
+        
+        for i in range(40):
+            time.sleep(1)
+            self._handle_cookie_banner()
+            try:
+                ele = self.page.ele('@name=cf-turnstile-response', timeout=1)
+                if ele and ele.value:
+                    cf_token = ele.value
+                    self._log("Cloudflare Turnstile token extracted.")
+                    break
+            except:
+                pass
             
             
-            if not cf_token:
+            if i > 2:
                 try:
                 try:
-                    cf_token = self.page.ele('@name=cf-turnstile-response').value
-                except:
+                    use_dfs = False
+                    cf_bypasser.click_verification_button(use_dfs)
+                except Exception as e:
                     pass
                     pass
             
             
-            if not cf_token:
-                self._log("[WARN] Could not extract Turnstile token.")
-                raise BizLogicError(f"Could not extract Turnstile token.")
-
-            email = self.config.account.username
-            password = self.config.account.password
-            enc_password = self._encrypt_password(password)
-            
-            client_src = self._get_client_source()
-            orange_src = self._get_orange_source(email)
-            
-            url = f"https://{self.vfs_api_domain}/user/login"
-            headers = self._get_common_headers(with_auth=False)
-            headers.update({
-                "clientsource": client_src,
-                "orangex": orange_src
-            })
-            
-            data = {
-                "username": email,
-                "password": enc_password,
-                "missioncode": mission,
-                "countrycode": country,
-                "languageCode": "en-US",
-                "captcha_version": "cloudflare-v1",
-                "captcha_api_key": cf_token 
-            }
-            
-            now_utc = datetime.utcnow()
-            sent_at = now_utc.strftime("%Y-%m-%d %H:%M:%S")
-            self._log("Sending Login Request via Browser Fetch...")
-            resp = self._perform_request("POST", url, headers=headers, json_data=data)
-            resp_json = resp.json()
-
-            if resp_json.get('accessToken'):
-                self.jwt_token = resp_json["accessToken"]
-                self._log("Login successful, JWT obtained.")
-            
-            elif resp_json.get("enableOTPAuthentication"):
-                self._log("Login requires OTP.")
-                otp = self._read_otp_email(sent_at=sent_at)
-                self._submit_login_otp(cf_token, otp)
-            
-            else:
-                raise BizLogicError(f"Login failed: {resp.text[:200]}")
+            if self.page.ele('tag:form', timeout=0.5) or self.page.ele('#mat-input-0', timeout=0.5):
+                self._log("Login form detected.")
+                if i > 5 and not cf_token:
+                    self._log("Form visible but token not found yet...")
 
 
-            self.session_create_time = time.time()
+        if not cf_token:
             try:
             try:
-                self.real_ip = self._get_realnetwork_ip()
+                cf_token = self.page.ele('@name=cf-turnstile-response').value
             except:
             except:
-                self.real_ip = "0.0.0.0"
-                
-        except Exception as e:
-            self._log(f"Create Session Failed: {e}")
-            raise e
+                pass
+        
+        if not cf_token:
+            self._log("[WARN] Could not extract Turnstile token.")
+            raise BizLogicError(f"Could not extract Turnstile token.")
+
+        email = self.config.account.username
+        password = self.config.account.password
+        enc_password = self._encrypt_password(password)
+        
+        client_src = self._get_client_source()
+        orange_src = self._get_orange_source(email)
+        
+        url = f"https://{self.vfs_api_domain}/user/login"
+        headers = self._get_common_headers(with_auth=False)
+        headers.update({
+            "clientsource": client_src,
+            "orangex": orange_src
+        })
+        
+        data = {
+            "username": email,
+            "password": enc_password,
+            "missioncode": mission,
+            "countrycode": country,
+            "languageCode": "en-US",
+            "captcha_version": "cloudflare-v1",
+            "captcha_api_key": cf_token 
+        }
+        
+        now_utc = datetime.utcnow()
+        sent_at = now_utc.strftime("%Y-%m-%d %H:%M:%S")
+        self._log("Sending Login Request via Browser Fetch...")
+        resp = self._perform_request("POST", url, headers=headers, json_data=data)
+        resp_json = resp.json()
+
+        if resp_json.get('accessToken'):
+            self.jwt_token = resp_json["accessToken"]
+            self._log("Login successful, JWT obtained.")
+        
+        elif resp_json.get("enableOTPAuthentication"):
+            self._log("Login requires OTP.")
+            otp = self._read_otp_email(sent_at=sent_at)
+            self._submit_login_otp(cf_token, otp)
+        
+        else:
+            raise BizLogicError(f"Login failed: {resp.text[:200]}")
+
+        self.session_create_time = time.time()
+        try:
+            self.real_ip = self._get_realnetwork_ip()
+        except:
+            self.real_ip = "0.0.0.0"
 
 
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         """查询可预约 Slot"""
         """查询可预约 Slot"""
         result = VSQueryResult()
         result = VSQueryResult()
         apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
         apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
-        try:
-            self._fetch_configurations(apt_config)
-            query_result = self._query_earliest_slot(apt_config)
-            result.success = False
-            result.availability_status = AvailabilityStatus.NoneAvailable
-            if query_result:
-                result.success = True
-                if "WaitList" in query_result:
-                    result.availability_status = AvailabilityStatus.Waitlist
-                else:
-                    earliest_dt = datetime.strptime(query_result, "%Y-%m-%d")
-                    result.availability_status = AvailabilityStatus.Available
-                    result.earliest_date = earliest_dt
-                    result.availability = [DateAvailability(date=earliest_dt, times=[])]
-                self._log(f"Slot Found! -> {query_result}")
+        self._fetch_configurations(apt_config)
+        query_result = self._query_earliest_slot(apt_config)
+        result.success = False
+        result.availability_status = AvailabilityStatus.NoneAvailable
+        if query_result:
+            result.success = True
+            if "WaitList" in query_result:
+                result.availability_status = AvailabilityStatus.Waitlist
             else:
             else:
-                self._log("No slots available.")
-                
-        except Exception as e:
-            self._log(f"Query Error: {e}")
-            raise e
-            
+                earliest_dt = datetime.strptime(query_result, "%Y-%m-%d")
+                result.availability_status = AvailabilityStatus.Available
+                result.earliest_date = earliest_dt
+                result.availability = [DateAvailability(date=earliest_dt, times=[])]
+            self._log(f"Slot Found! -> {query_result}")
+        else:
+            self._log("No slots available.")
         return result
         return result
 
 
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
@@ -537,18 +513,16 @@ class VfsPlugin(IVSPlg):
         try:
         try:
             js = """
             js = """
             try {
             try {
-                // 1. 尝试点击 '接受所有' 按钮
                 var acceptBtn = document.getElementById('onetrust-accept-btn-handler');
                 var acceptBtn = document.getElementById('onetrust-accept-btn-handler');
                 if (acceptBtn) {
                 if (acceptBtn) {
                     acceptBtn.click();
                     acceptBtn.click();
                     return true;
                     return true;
                 }
                 }
                 
                 
-                // 2. 如果没有按钮,或者还在遮挡,直接把整个 banner 删掉
                 var banner = document.getElementById('onetrust-banner-sdk');
                 var banner = document.getElementById('onetrust-banner-sdk');
                 if (banner) {
                 if (banner) {
-                    banner.style.display = 'none'; // 隐藏
-                    banner.remove(); // 或者移除
+                    banner.style.display = 'none';
+                    banner.remove();
                     return true;
                     return true;
                 }
                 }
             } catch(e) {}
             } catch(e) {}
@@ -557,15 +531,6 @@ class VfsPlugin(IVSPlg):
             self.page.run_js(js)
             self.page.run_js(js)
         except:
         except:
             pass
             pass
-
-    def _get_proxy_url(self):
-        if self.config.proxy and self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                return f"{s.proto}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                return f"{s.proto}://{s.ip}:{s.port}"
-        return None
     
     
     def _get_realnetwork_ip(self):
     def _get_realnetwork_ip(self):
         """
         """
@@ -1401,7 +1366,6 @@ class VfsPlugin(IVSPlg):
                 curr = curr.replace(month=curr.month + 1)
                 curr = curr.replace(month=curr.month + 1)
         return months
         return months
     
     
-    # --- 资源清理核心方法 ---
     def cleanup(self):
     def cleanup(self):
         """
         """
         销毁浏览器并彻底删除临时文件
         销毁浏览器并彻底删除临时文件

+ 7 - 6
tls_registration_bot.py

@@ -47,7 +47,7 @@ class TlsRegistrator:
         # 隔离的用户数据目录
         # 隔离的用户数据目录
         self.instance_id = uuid.uuid4().hex[:8]
         self.instance_id = uuid.uuid4().hex[:8]
         self.tls_url = tls_url
         self.tls_url = tls_url
-        # self.instance_id = '18d389e9'
+        self.instance_id = '18d389e9'
         self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
         self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
         self.page = None
         self.page = None
         self.mouse = None
         self.mouse = None
@@ -206,7 +206,7 @@ class TlsRegistrator:
         """执行自动注册"""
         """执行自动注册"""
         email = self.account_detail.get('email')
         email = self.account_detail.get('email')
         password = self.account_detail.get('pwd')
         password = self.account_detail.get('pwd')
-        self._log(f'Account Detail:{self.account_detail}')
+        self._log(f'Account Detail:{json.dumps(self.account_detail)}')
         btn_selector = '#submit'
         btn_selector = '#submit'
         if not self.page.wait.ele_displayed(btn_selector, timeout=3):
         if not self.page.wait.ele_displayed(btn_selector, timeout=3):
             register_btn = self.page.ele("tag:a@@href:registration")
             register_btn = self.page.ele("tag:a@@href:registration")
@@ -609,13 +609,13 @@ class TlsRegistrator:
         if self.page:
         if self.page:
             try: self.page.quit()
             try: self.page.quit()
             except: pass
             except: pass
-        if os.path.exists(self.workspace):
-            time.sleep(1) # 等待文件锁释放
-            shutil.rmtree(self.workspace, ignore_errors=True)
+        # if os.path.exists(self.workspace):
+        #     time.sleep(1) # 等待文件锁释放
+        #     shutil.rmtree(self.workspace, ignore_errors=True)
             
             
 def register_worker(proxy_config, tls_url, capsolver_key):
 def register_worker(proxy_config, tls_url, capsolver_key):
     """单个注册任务的工作线程函数"""
     """单个注册任务的工作线程函数"""
-    account_detail = generate_random_account_detail('GB')
+    account_detail = generate_random_account_detail('CN-Hangzhou')
     bot = None
     bot = None
     try:
     try:
         bot = TlsRegistrator(
         bot = TlsRegistrator(
@@ -640,6 +640,7 @@ def register_worker(proxy_config, tls_url, capsolver_key):
     except Exception as e:
     except Exception as e:
         print(f"[ERROR] 注册失败 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
         print(f"[ERROR] 注册失败 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
         bot.save_screenshot('tls_registration_failed')
         bot.save_screenshot('tls_registration_failed')
+        time.sleep(180)
         return False
         return False
         
         
     finally:
     finally:

+ 838 - 0
tls_registration_gui.py

@@ -0,0 +1,838 @@
+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_())

+ 1 - 1
tools/proxy_uploader.py

@@ -2,7 +2,7 @@ import requests
 from datetime import datetime, timezone
 from datetime import datetime, timezone
 
 
 # --- 配置部分 ---
 # --- 配置部分 ---
-API_URL = 'https://api.text.skin/api/proxy/create'
+API_URL = 'https://visafly.top/api/proxy/create'
 BEARER_TOKEN = 'tok_e946329a60ff45ba807f3f41b0e8b7fc'    # 替换为你的真实 Token
 BEARER_TOKEN = 'tok_e946329a60ff45ba807f3f41b0e8b7fc'    # 替换为你的真实 Token
 FILE_PATH = '../config/decodo.txt'   # 替换为你实际的文本文件名 (例如 proxies.txt)
 FILE_PATH = '../config/decodo.txt'   # 替换为你实际的文本文件名 (例如 proxies.txt)
 
 

+ 13 - 9
utils/cloudflare_bypass_for_scraping.py

@@ -16,14 +16,18 @@ class CloudflareBypasser:
             self.log_message(f"Max depth {max_depth} reached during iframe search.")
             self.log_message(f"Max depth {max_depth} reached during iframe search.")
             return None
             return None
 
 
-        if ele.shadow_root:
-            if ele.shadow_root.child().tag == "iframe":
-                return ele.shadow_root.child()
-        else:
-            for child in ele.children():
-                result = self.search_recursively_shadow_root_with_iframe(child, depth + 1, max_depth)
-                if result:
-                    return result
+        try:
+            if ele.shadow_root:
+                child = ele.shadow_root.child()
+                if child and child.tag == "iframe":
+                    return child
+            else:
+                for child in ele.children():
+                    result = self.search_recursively_shadow_root_with_iframe(child, depth + 1, max_depth)
+                    if result:
+                        return result
+        except Exception as e:
+            pass 
         return None
         return None
 
 
     def search_recursively_shadow_root_with_cf_input(self,ele):
     def search_recursively_shadow_root_with_cf_input(self,ele):
@@ -73,7 +77,7 @@ class CloudflareBypasser:
                 self.log_message("Verification button found. Attempting to click.")
                 self.log_message("Verification button found. Attempting to click.")
                 if button.states.is_displayed and button.states.is_enabled:
                 if button.states.is_displayed and button.states.is_enabled:
                     button.click()
                     button.click()
-                    time.sleep(1)  # 确保事件触发
+                    time.sleep(1)
                 else:
                 else:
                     self.log_message("Button is not clickable.")
                     self.log_message("Button is not clickable.")
             else:
             else:

+ 19 - 1
utils/fake_utils.py

@@ -69,6 +69,24 @@ COUNTRY_CONFIGS = {
         "default_phone": "13800000000",
         "default_phone": "13800000000",
         "fra1": "FRA1PB",
         "fra1": "FRA1PB",
     },
     },
+    "CN-Guangzhou": {
+        "nat_code": None,  # randomuser 不支持 CN,置为 None
+        "pool_name": "tls.cn.can.fr.sentinel",
+        "location": "Guangzhou",
+        "province_residence": "Guangdong",
+        "nationality": "China",
+        "phone_country_code": "86",
+        # --- 本地化生成规则(针对 API 不支持的国家) ---
+        "local_first_names": ["Wei", "Fang", "Jian", "Hui", "Lei", "Ting", "Peng", "Xia", "Bin", "Jie", "San", "Ming"],
+        "local_last_names": ["Wang", "Li", "Zhang", "Liu", "Chen", "Yang", "Huang", "Zhao", "Wu", "Zhou"],
+        "phone_prefix": ["138", "139", "150", "151", "180", "189"], # 中国手机号前缀
+        "phone_length": 11,
+        # -----------------------------------------------
+        "default_first_name": "San",
+        "default_last_name": "Zhang",
+        "default_phone": "13800000000",
+        "fra1": "FRA1CB",
+    },
     "GB-London": {
     "GB-London": {
         "nat_code": "gb",  # API 支持 GB,直接依赖 API 生成姓名
         "nat_code": "gb",  # API 支持 GB,直接依赖 API 生成姓名
         "pool_name": "tls.gb.lon.fr.sentinel",
         "pool_name": "tls.gb.lon.fr.sentinel",
@@ -195,7 +213,7 @@ def generate_random_account_detail(country_code: str = "CN") -> Dict[str, Any]:
     passport_no = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2)) + \
     passport_no = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2)) + \
                   "".join(random.choices("0123456789", k=7))
                   "".join(random.choices("0123456789", k=7))
     fra1_prefix = config.get('fra1')
     fra1_prefix = config.get('fra1')
-    # LO 伦敦 DB 都柏林 PB 北京 SH 上海 HN 杭州
+    # LO 伦敦 DB 都柏林 PB 北京 SH 上海 HN 杭州 CB 广州
     # 4. 组装组装并返回
     # 4. 组装组装并返回
     return {
     return {
         "pool_name": config["pool_name"],
         "pool_name": config["pool_name"],

+ 7 - 15
vs_plg.py

@@ -18,6 +18,13 @@ class IVSPlg(ABC):
         @param config 签证 API 配置对象
         @param config 签证 API 配置对象
         """
         """
         pass
         pass
+    
+    @abstractmethod
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        """
+        @brief 设置日志输出工具, 该函数不允许抛异常
+        """
+        pass
 
 
     @abstractmethod
     @abstractmethod
     def create_session(self) -> None:
     def create_session(self) -> None:
@@ -43,14 +50,6 @@ class IVSPlg(ABC):
         @return VSBookResult 预约结果
         @return VSBookResult 预约结果
         """
         """
         pass
         pass
-
-    @abstractmethod
-    def get_group_id(self) -> str:
-        """
-        @brief 获取当前 API 实例所属的分组 ID
-        @return 分组 ID 字符串
-        """
-        pass
     
     
     @abstractmethod
     @abstractmethod
     def keep_alive(self):
     def keep_alive(self):
@@ -67,13 +66,6 @@ class IVSPlg(ABC):
         """
         """
         pass
         pass
     
     
-    @abstractmethod
-    def set_log(self, logger: Callable[[str], None]) -> None:
-        """
-        @brief 设置日志输出工具, 该函数不允许抛异常
-        """
-        pass
-    
     @abstractmethod
     @abstractmethod
     def cleanup(self) -> None:
     def cleanup(self) -> None:
         """
         """