Hujiarui 2 ماه پیش
والد
کامیت
e54e6499a2

+ 0 - 2
booker_builtin.py

@@ -181,7 +181,6 @@ class BuiltinBookerGCO:
 
         task_id = None
         task_data = None
-        booking_success = False
         
         try:
             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)
             
             if book_res.success:
-                booking_success = True
                 self._log(f"✅ BOOK SUCCESS! Order: {order_id}")
                 
                 grab_info = {

+ 0 - 3
plugins/bel_plugin.py

@@ -73,9 +73,6 @@ class BelPlugin(IVSPlg):
         self.tunnel = None # 代理隧道
         self.is_healthy = True
         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:
         self.logger = logger

+ 0 - 3
plugins/bls_plugin.py

@@ -60,9 +60,6 @@ class BlsPlugin(IVSPlg):
         # OCR 服务地址默认值
         self.local_service_url: str = ""
         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:
         self.logger = logger

+ 0 - 3
plugins/de_plugin.py

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

+ 0 - 3
plugins/grc_plugin.py

@@ -38,9 +38,6 @@ class GrcPlugin(IVSPlg):
         self.rp_id = None
         self.token = None
         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:
         self.logger = logger

+ 0 - 4
plugins/ita_plugin.py

@@ -69,11 +69,7 @@ class ItaPlugin(IVSPlg):
             
         # 持有隧道实例
         self.tunnel = None
-        
         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:
         self.logger = logger

+ 0 - 3
plugins/pol_plugin.py

@@ -72,9 +72,6 @@ class PolPlugin(IVSPlg):
         self.tunnel = None # 代理隧道
         self.is_healthy = True
         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:
         self.logger = logger

+ 251 - 264
plugins/tls_plugin.py

@@ -68,9 +68,6 @@ class TlsPlugin(IVSPlg):
         
         self.tunnel = None
         self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
     
     def set_log(self, logger: Callable[[str], None]):
         self.logger = logger
@@ -237,288 +234,278 @@ class TlsPlugin(IVSPlg):
         co.set_argument(f"--fingerprint-platform={fp_platform}")
         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)
                     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)
                     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
-                    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)
-                    
-            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:
         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 DrissionPage import ChromiumPage, ChromiumOptions
-from DrissionPage.common import Settings
-
 from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.backends import default_backend
@@ -137,27 +135,18 @@ class VfsPlugin(IVSPlg):
             VFS_PUBLIC_KEY_PEM.encode(),
             backend=default_backend()
         )
-        
-        # --- [核心修改] 并发隔离与资源管理 ---
-        # 生成唯一实例 ID
+
         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.user_data_path = os.path.join(self.root_workspace, "user_data")
         
         self.vfs_api_domain = 'lift-api.vfsglobal.com'
-        
-        # 持有隧道实例
-        self.tunnel = None
-        
-        # 确保根目录存在 (子目录由具体逻辑创建)
         if not os.path.exists(self.root_workspace):
             os.makedirs(self.root_workspace)
             
+        self.tunnel = None
         self.session_create_time: float = 0
 
-    def get_group_id(self) -> str:
-        return self.group_id
-
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.free_config = config.free_config or {}
@@ -269,139 +258,126 @@ class VfsPlugin(IVSPlg):
         co.set_argument('--disable-dev-shm-usage') 
         co.set_argument('--window-size=1920,1080')
         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:
-                    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
             
-            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:
-                self.real_ip = self._get_realnetwork_ip()
+                cf_token = self.page.ele('@name=cf-turnstile-response').value
             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:
         """查询可预约 Slot"""
         result = VSQueryResult()
         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:
-                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
 
     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:
             js = """
             try {
-                // 1. 尝试点击 '接受所有' 按钮
                 var acceptBtn = document.getElementById('onetrust-accept-btn-handler');
                 if (acceptBtn) {
                     acceptBtn.click();
                     return true;
                 }
                 
-                // 2. 如果没有按钮,或者还在遮挡,直接把整个 banner 删掉
                 var banner = document.getElementById('onetrust-banner-sdk');
                 if (banner) {
-                    banner.style.display = 'none'; // 隐藏
-                    banner.remove(); // 或者移除
+                    banner.style.display = 'none';
+                    banner.remove();
                     return true;
                 }
             } catch(e) {}
@@ -557,15 +531,6 @@ class VfsPlugin(IVSPlg):
             self.page.run_js(js)
         except:
             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):
         """
@@ -1401,7 +1366,6 @@ class VfsPlugin(IVSPlg):
                 curr = curr.replace(month=curr.month + 1)
         return months
     
-    # --- 资源清理核心方法 ---
     def cleanup(self):
         """
         销毁浏览器并彻底删除临时文件

+ 7 - 6
tls_registration_bot.py

@@ -47,7 +47,7 @@ class TlsRegistrator:
         # 隔离的用户数据目录
         self.instance_id = uuid.uuid4().hex[:8]
         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.page = None
         self.mouse = None
@@ -206,7 +206,7 @@ class TlsRegistrator:
         """执行自动注册"""
         email = self.account_detail.get('email')
         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'
         if not self.page.wait.ele_displayed(btn_selector, timeout=3):
             register_btn = self.page.ele("tag:a@@href:registration")
@@ -609,13 +609,13 @@ class TlsRegistrator:
         if self.page:
             try: self.page.quit()
             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):
     """单个注册任务的工作线程函数"""
-    account_detail = generate_random_account_detail('GB')
+    account_detail = generate_random_account_detail('CN-Hangzhou')
     bot = None
     try:
         bot = TlsRegistrator(
@@ -640,6 +640,7 @@ def register_worker(proxy_config, tls_url, capsolver_key):
     except Exception as e:
         print(f"[ERROR] 注册失败 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
         bot.save_screenshot('tls_registration_failed')
+        time.sleep(180)
         return False
         
     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
 
 # --- 配置部分 ---
-API_URL = 'https://api.text.skin/api/proxy/create'
+API_URL = 'https://visafly.top/api/proxy/create'
 BEARER_TOKEN = 'tok_e946329a60ff45ba807f3f41b0e8b7fc'    # 替换为你的真实 Token
 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.")
             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
 
     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.")
                 if button.states.is_displayed and button.states.is_enabled:
                     button.click()
-                    time.sleep(1)  # 确保事件触发
+                    time.sleep(1)
                 else:
                     self.log_message("Button is not clickable.")
             else:

+ 19 - 1
utils/fake_utils.py

@@ -69,6 +69,24 @@ COUNTRY_CONFIGS = {
         "default_phone": "13800000000",
         "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": {
         "nat_code": "gb",  # API 支持 GB,直接依赖 API 生成姓名
         "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)) + \
                   "".join(random.choices("0123456789", k=7))
     fra1_prefix = config.get('fra1')
-    # LO 伦敦 DB 都柏林 PB 北京 SH 上海 HN 杭州
+    # LO 伦敦 DB 都柏林 PB 北京 SH 上海 HN 杭州 CB 广州
     # 4. 组装组装并返回
     return {
         "pool_name": config["pool_name"],

+ 7 - 15
vs_plg.py

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