瀏覽代碼

feat: update

Hujiarui 2 月之前
父節點
當前提交
961416f316
共有 4 個文件被更改,包括 72 次插入209 次删除
  1. 3 4
      main_booker.py
  2. 6 7
      main_sentinel.py
  3. 51 186
      plugins/tls_plugin.py
  4. 12 12
      toolkit/vs_cloud_api.py

+ 3 - 4
main_booker.py

@@ -13,7 +13,9 @@ from booker_order import OrderBookerGCO
 from toolkit.vs_cloud_api import VSCloudApi
 
 
-def load_config(path):
+def load_config(path: str):
+    if not os.path.exists(path):
+        return {}
     with open(path, "r", encoding="utf-8") as f:
         return json.load(f)
 
@@ -80,9 +82,6 @@ def main():
         while True:
             time.sleep(30)  # 每 3 秒检查一次文件状态
             
-            if not os.path.exists(config_path):
-                continue
-                
             try:
                 new_cfg_data = VSCloudApi.Instance().get_dynamic_config(config_name=CONF_NAME)
                 new_version = new_cfg_data.get('version')

+ 6 - 7
main_sentinel.py

@@ -13,6 +13,8 @@ from toolkit.vs_cloud_api import VSCloudApi
 
 
 def load_config(path: str):
+    if not os.path.exists(path):
+        return {}
     with open(path, "r", encoding="utf-8") as f:
         return json.load(f)
 
@@ -79,16 +81,13 @@ def main():
         while True:
             time.sleep(30)  # 每 3 秒检查一次文件状态
             
-            if not os.path.exists(config_path):
-                continue
-                
             try:
                 new_cfg_data = VSCloudApi.Instance().get_dynamic_config(config_name=CONF_NAME)
                 new_version = new_cfg_data.get('version')
                 # 如果 API 返回的没有 version 字段,或者版本号没变,则跳过
                 if new_version == current_version:
                     continue
-
+                
                 app_logger.info("Config file modification detected! Reloading configurations...")
                 save_config(config_path, new_cfg_data)
                 current_version = new_version
@@ -123,7 +122,7 @@ def main():
                         # 情况 3: 这个组完全从 JSON 中被删除了
                         app_logger.info(f"Group [{current_id}] deleted from config. Stopping and removing...")
                         wrapper.stop()
-                        
+                
                 # 更新当前正在运行的 wrappers 列表
                 wrappers = surviving_wrappers
                 
@@ -138,7 +137,7 @@ def main():
                         # 只有启用的组才会被启动
                         if not cfg.enable:
                             continue
-                            
+                        
                         app_logger.info(f"Dynamically starting wrapper for NEW group [{cfg.identifier}]...")
                         new_wrapper = GCOWrapper(
                             gco_class=SentinelGCO,
@@ -151,7 +150,7 @@ def main():
                             wrappers.append(new_wrapper)
                         except Exception as e:
                             app_logger.error(f"Failed to dynamically start new group [{cfg.identifier}]: {e}")
-                                
+            
             except json.JSONDecodeError:
                 # 捕获异常:防止后端写入文件的中间状态导致 JSON 格式错误而崩溃
                 app_logger.warning("Config file is currently invalid JSON (maybe still writing), skipping this reload.")

+ 51 - 186
plugins/tls_plugin.py

@@ -317,19 +317,27 @@ class TlsPlugin(IVSPlg):
             
             for step in range(max_steps):
                 self.page.wait.doc_loaded()
-                time.sleep(0.5)
+                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 self.page.html,
-                    "You are being rate limited" in self.page.html,
-                    "Cloudflare Ray ID" in self.page.html
+                    "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:
@@ -338,26 +346,33 @@ class TlsPlugin(IVSPlg):
                     self.page.wait.load_start(timeout=3)
                     continue
                      
-                # 状态 1:到达终极目标页面 (成功退出条件)
+                # 到达终极目标页面 (成功退出条件)
                 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
                 
-                # 状态 2:遇到没有申请人的拦截页 (致命错误退出条件)
+                # 页面发生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
+                
+                # 遇到没有申请人的拦截页 (致命错误退出条件)
                 no_applicant_indicators = [
-                    "Add a new applicant" in self.page.html,
-                    "You have not yet added an applicant" in self.page.html,
+                    "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:
-                    # 状态 3:首页/登录入口页 -> 需要点击进入登录
                     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")
@@ -379,7 +394,7 @@ class TlsPlugin(IVSPlg):
                         self.page.wait.load_start(timeout=3)
                         continue
                 
-                # 状态 4:真正的登录表单页
+                # 真正的登录表单页
                 if self.page.ele('tag:label@@text():Email', timeout=1) and not has_submitted_login:
                     self._log("State: Login Form. Processing credentials and Captcha...")
                     
@@ -440,10 +455,10 @@ class TlsPlugin(IVSPlg):
                     self.page.wait.load_start(timeout=5)
                     continue
                 
-                # 状态 5:Travel Groups 页面
+                # Travel Groups 页面
                 if "travel-groups" in current_url:
                     self._log("State: Travel Groups. Selecting targeted group...")
-                    groups = self._parse_travel_groups(self.page.html)
+                    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)
                     
@@ -468,7 +483,7 @@ class TlsPlugin(IVSPlg):
                     else:
                         self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
                 
-                # 状态 6:中间过渡页,需点击 "Book Appointment" 继续往下走
+                # 中间过渡页,需点击 "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'))
@@ -476,7 +491,7 @@ class TlsPlugin(IVSPlg):
                     self.page.wait.load_start(timeout=3)
                     continue
                 
-                # 状态 7:登录失败校验 或 未知加载状态
+                # 登录失败校验 或 未知加载状态
                 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)
@@ -522,7 +537,9 @@ class TlsPlugin(IVSPlg):
                         TimeSlot(time=s["time"], label=str(s.get("label", "")))
                     )
                 res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
+                self._log(f"Slot Found! size={len(slots)}")
             else:
+                self._log("No slots available.")
                 res.success = False
                 res.availability_status = AvailabilityStatus.NoneAvailable
                 
@@ -556,178 +573,6 @@ class TlsPlugin(IVSPlg):
         finally:
             self.is_busy = False
         return res
-    
-    def book_bak(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
-        res = VSBookResult()
-        res.success = False
-        
-        exp_start = user_inputs.get('expected_start_date', '')
-        exp_end = user_inputs.get('expected_end_date', '')
-        support_pta = user_inputs.get('support_pta', True)
-
-        target_labels = ['']
-        if support_pta:
-            target_labels.append('pta')
-
-        available_dates_str =[
-            da.date.strftime("%Y-%m-%d")
-            for da in slot_info.availability if da.date
-        ]
-        
-        valid_dates_list = self._filter_dates(available_dates_str, exp_start, exp_end)
-        if not valid_dates_list:
-            raise NotFoundError(message="No dates match user constraints")
-        
-        all_possible_slots =[]
-        for da in slot_info.availability:
-            if not da.date:
-                continue
-                
-            date_str = da.date.strftime("%Y-%m-%d")
-            if date_str in valid_dates_list:
-                for t in da.times:
-                    if t.label in target_labels:
-                        all_possible_slots.append({
-                            "date": date_str,
-                            "time_obj": t,
-                            "label": t.label
-                        })
-        
-        if not all_possible_slots:
-            raise NotFoundError(message="No suitable slot found (after label filtering)")
-
-        selected_slot = random.choice(all_possible_slots)
-        selected_date = selected_slot["date"]
-        selected_time = selected_slot["time_obj"]
-        selected_label = selected_slot["label"]
-
-        self._log(f"Found {len(all_possible_slots)} valid slots. selected slot: {selected_date} {selected_time.time} {selected_label}")
-        self.page.listen.start('/workflow/appointment-booking', method='POST')
-        page_url = self.page.url
-        location = self.travel_group.get('location')
-        mapper = {
-            "London": "gbLON2fr",
-            "Dublin": "ieDUB2fr"
-        }
-        location_id = mapper.get(location)
-        form_group_id =self.travel_group.get('group_number')
-        
-        api_token = self.free_config.get("capsolver_key", "")
-        rc_params = {
-            "type": "ReCaptchaV3Task",
-            "page": page_url,
-            "action": "book", 
-            "siteKey": "6LcTpXcfAAAAAM3VojNhyV-F1z92ADJIvcSZ39Y9",
-            "apiToken": api_token,
-            "proxy": True
-        }
-        g_token = self._solve_recaptcha(rc_params)
-
-        ACTION_ID = "6033ac5e6e4ac04f59a4b74a9c5dd312876dd46bd9"
-        
-        js_script = f"""
-        (async function() {{
-            const url = "{page_url}";
-            const groupId = "{form_group_id}";
-            const locationId = "{location_id}";
-            const selectedDate = "{selected_date}";
-            const selectedTime = "{selected_time.time}";
-            const selectedLabel = "{selected_label}";
-            const captchaToken = "{g_token}";
-            const actionId = "{ACTION_ID}";
-            
-            let castleToken = '';
-            try {{
-                const castleModule = window.__webpack_require__(93773);
-                if (castleModule && castleModule.createRequestToken) {{
-                    castleToken = await castleModule.createRequestToken();
-                    console.log('Castle token obtained:', castleToken);
-                }}
-            }} catch(e) {{
-                console.error('Failed to get Castle token:', e);
-            }}
-            
-            let routerStateTree = '';
-            try {{
-                const routerModule = window.__webpack_require__(11807);
-                const routerState = routerModule.getCurrentAppRouterState();
-                if (routerState && routerState.tree) {{
-                    const prepareModule = window.__webpack_require__(16378);
-                    routerStateTree = prepareModule.prepareFlightRouterStateForRequest(routerState.tree);
-                    console.log('Router state tree obtained');
-                }}
-            }} catch(e) {{
-                console.error('Failed to get router state:', e);
-            }}
-            
-            const formData = new FormData();
-            formData.append('1_formGroupId', groupId);
-            formData.append('1_lang', 'en-us');
-            formData.append('1_process', 'APPOINTMENT');
-            formData.append('1_location', locationId);
-            formData.append('1_date', selectedDate);
-            formData.append('1_time', selectedTime);
-            formData.append('1_appointmentLabel', selectedLabel);
-            formData.append('1_castleRequestToken', castleToken);
-            formData.append('1_captchaToken', captchaToken);
-            formData.append('0', '[{{"status":"IDLE"}},"$K1"]');
-
-            const headers = {{
-                'Accept': 'text/x-component',
-                'Next-Action': actionId,
-                'Next-Router-State-Tree': routerStateTree,
-            }};
-            
-            const response = await fetch(url, {{
-                method: 'POST',
-                headers: headers,
-                body: formData,
-                credentials: 'include'
-            }});
-            
-            const text = await response.text();
-            const responseHeaders = {{}};
-            response.headers.forEach((value, key) => {{
-                responseHeaders[key] = value;
-            }});
-            const result = {{
-                status: response.status,
-                body: text,
-                headers: responseHeaders,
-                url: response.url,
-                redirected: response.redirected,
-                ok: response.ok
-            }};
-            return result;
-        }})();
-        """
-        
-        self._log("Submitting booking request via JS Fetch...")
-        self.page.run_js(js_script)
-        
-        packet = self.page.listen.wait(timeout=15)
-        if not packet:
-            raise BizLogicError(message='Listening data failed')
-        
-        self.page.listen.stop()
-        self._log(f"URL: {packet.url}")
-        self._log(f"POST Body: {packet.request.postData}")
-        self._log(f"POST Stat: {packet.response.status}")
-        self._log(f"POST head: {packet.response.headers}")
-        self._log(f"POST Resp: {packet.response.raw_body}")
-        redirect_location = packet.response.headers.get('location', '') or ''
-        appointment_confirmation_indicators = [
-            "order-summary" in redirect_location,
-            "partner-services" in redirect_location,
-            "appointment-confirmation" in redirect_location,
-        ]
-        if any(appointment_confirmation_indicators):
-            self._log(f"Booking Success!")
-            res.success = True
-            res.book_date = selected_date
-            res.book_time = selected_time.time
-            return res
-        return res
 
     def book(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
         res = VSBookResult()
@@ -1041,6 +886,26 @@ class TlsPlugin(IVSPlg):
                             'label': lbl
                         })
         return slots
+    
+    def _handle_cookie_dialog(self, timeout: int = 3) -> bool:
+        """
+        检测并处理 Osano Cookie 弹窗
+        """
+        try:
+            dialog = self.page.ele('@aria-label=Cookie Consent Banner', timeout=timeout)
+            if not dialog:
+                return False
+            target_btn = dialog.ele('tag:button@text():Save')
+            if target_btn:
+                self.mouse.human_click_ele(target_btn)
+                self._log(f"Handle cookie window success")
+                return True
+            else:
+                self._log(f"Found cookie window, but button not found")
+                return False
+        except Exception as e:
+            self._log(f"Handle cookie window exception: {e}")
+            return False
   
     def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
         if not html:

+ 12 - 12
toolkit/vs_cloud_api.py

@@ -97,7 +97,7 @@ class VSCloudApi:
         url = f"{self.base_url}/api/vas/task/update"
         params = {"id": task_id}
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
+        resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -111,7 +111,7 @@ class VSCloudApi:
         url = f"{self.base_url}/api/vas/task/return_to_queue"
         params = {"task_id": task_id}
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, params=params, headers=headers)
+        resp = self._perform_request('POST', url, params=params, headers=headers, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -125,7 +125,7 @@ class VSCloudApi:
         url = f"{self.base_url}/api/wechat/send_no_token"
         payload = {"message": text}
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, json_data=payload, headers=headers)
+        resp = self._perform_request('POST', url, json_data=payload, headers=headers, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -143,7 +143,7 @@ class VSCloudApi:
             "args": args,
             "status": 0
         }
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             data = result.get("data", {})
@@ -165,7 +165,7 @@ class VSCloudApi:
             if time.time() - start_time > timeout:
                 raise BizLogicError(message=f"Wait for task result timeout ({timeout}s). TaskID: {task_id}")
             try:
-                resp = self._perform_request('GET', url, headers=headers)
+                resp = self._perform_request('GET', url, headers=headers, timeout=10)
                 result = resp.json()
                 if result.get("code") != 0:
                     raise BizLogicError(message=f"API Error fetching task: {result.get('message')}")
@@ -206,7 +206,7 @@ class VSCloudApi:
             "page": page,
             "session_id": session_id
         }
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -230,7 +230,7 @@ class VSCloudApi:
             "snapshot_source": snapshot_source,
         }
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -244,7 +244,7 @@ class VSCloudApi:
             "snapshot_source": snapshot_source,
         }
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -259,7 +259,7 @@ class VSCloudApi:
             "error": error,
         }
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -301,7 +301,7 @@ class VSCloudApi:
     def slot_snapshot_report(self, query_payload: Dict[str, Any] = {}):
         url = f"{self.base_url}/api/slots/report"
         headers = self._get_headers()
-        resp = self._perform_request("POST", url, headers=headers, json_data=query_payload)
+        resp = self._perform_request("POST", url, headers=headers, json_data=query_payload, timeout=10)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
@@ -333,7 +333,7 @@ class VSCloudApi:
         
         url = f"{self.base_url}/api/email-authorizations/fetch"
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, params=params, data="")
+        resp = self._perform_request('POST', url, headers=headers, params=params, data="", timeout=5*60)
         result = resp.json()
         if result.get('code') == 0:
             data = result.get('data', {})
@@ -361,7 +361,7 @@ class VSCloudApi:
         }
         url = f"{self.base_url}/api/email-authorizations/fetch-top"
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, params=params, data="")
+        resp = self._perform_request('POST', url, headers=headers, params=params, data="", timeout=5*60)
         result = resp.json()
         if result.get('code') == 0:
             data = result.get('data', {})