Bladeren bron

feat: update

Hujiarui 2 maanden geleden
bovenliggende
commit
56a1870f0a
4 gewijzigde bestanden met toevoegingen van 162 en 79 verwijderingen
  1. 1 1
      booker_standalone.py
  2. 2 2
      main_standalone.py
  3. 157 76
      plugins/usa_plugin.py
  4. 2 0
      utils/cloudflare_bypass_for_scraping.py

+ 1 - 1
booker_standalone.py

@@ -139,6 +139,7 @@ class BookerStandalone:
         
         self.m_instance.create_session()
         self.m_last_login_time = time.time()
+        self.m_next_query_time = time.time() + self._get_wait_interval()
         self._log("Session created successfully.")
 
     def start(self):
@@ -178,7 +179,6 @@ class BookerStandalone:
                 apt_type = random.choices(apt_types, weights=weights, k=1)[0]
 
                 self._log(f"Querying slots for {apt_type.routing_key}...")
-
                 query_result = self.m_instance.query(apt_type)
                 query_result.apt_type = apt_type
                 self.m_next_query_time = time.time() + self._get_wait_interval()

+ 2 - 2
main_standalone.py

@@ -42,11 +42,11 @@ def main():
     wrapper = GCOWrapper(gco_class=gco_class, gco_cfg=cfg, redis_conf=redis_conf)
     wrapper.load()
     wrapper.start()
-    
     app_logger.info(f"Successfully started booker. Press Ctrl+C to stop.")
     
     try:
-        while True: time.sleep(1)
+        while True:
+            time.sleep(1)
     except KeyboardInterrupt:
         app_logger.info("Shutting down Bookers...")
         wrapper.stop()

+ 157 - 76
plugins/usa_plugin.py

@@ -78,6 +78,14 @@ class UsaPlugin(IVSPlg):
         else:
             print(f'[UsaPlugin] [{self.group_id}] [{self.instance_id}] {message}')
 
+    def _random_sleep(self, min_sec=30, max_sec=60):
+        """
+        核心防限速控制:模拟人类阅读和操作的长时间停顿,确保网络请求间隔在30-60秒
+        """
+        sleep_time = random.uniform(min_sec, max_sec)
+        self._log(f"Anti-Rate-Limit: Sleeping for {sleep_time:.2f} seconds...")
+        time.sleep(sleep_time)
+
     def _get_free_port(self):
         with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
             s.bind(('', 0))
@@ -199,18 +207,20 @@ class UsaPlugin(IVSPlg):
         usa_url = self.free_config.get('usa_url', '') 
         self._log(f"Navigating: {usa_url}")
         self.page.get(usa_url)
-        time.sleep(5)
+        # 初始访问页面,等待长延时,防止过快触发后续操作
+        self._random_sleep(30, 45)
         
         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._random_sleep(30, 45) # 刷新动作属于高危操作,加长延时
             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)
+        # 绕过盾后,休眠一段时间再处理 waiting room
+        self._random_sleep(15, 30) 
         cf_bypasser.handle_waiting_room()
         
         self._log("Init humanize tools...")
@@ -235,34 +245,32 @@ class UsaPlugin(IVSPlg):
         
         for step in range(max_steps):
             self.page.wait.doc_loaded()
-            time.sleep(1)
+            time.sleep(1) # 这个用于等待页面DOM渲染,保留短时,因为不是发请求
             
             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} ---")
             
+            # --- [异常处理层] ---
             if current_url == last_url:
                 stuck_counter += 1
             else:
                 last_url = current_url
                 stuck_counter = 0
-            
-            # --- [异常处理层] ---
             if stuck_counter >= 3:
                 self._log("[WARN] Page stucked, try to refresh...")
                 self.page.refresh()
-                self.page.wait.load_start(timeout=5)
+                self._random_sleep(30, 40)
                 stuck_counter = 0
                 continue
             
-            server_error_indicators = ["502 Bad Gateway", "503 Service Temporarily Unavailable"]
             # 网络出现故障,直接重试
+            server_error_indicators = ["502 Bad Gateway", "503 Service Temporarily Unavailable"]
             if any(err in current_html_content for err in server_error_indicators):
                 self._log(f"[WARN] Server network error, try to refresh (Step: {step})...")
-                time.sleep(2)
                 self.page.refresh()
-                self.page.wait.load_start(timeout=5)
+                self._random_sleep(30, 40)
                 continue
             
             cloudflare_blocked_indicators = [
@@ -276,7 +284,7 @@ class UsaPlugin(IVSPlg):
             # 遇到五秒盾先绕盾
             if "just a moment" in current_title:
                 cf_bypasser.bypass(max_retry=3)
-                time.sleep(3)
+                self._random_sleep(20, 40)
                 continue
             
             if self.page.ele('#post_select', timeout=1):
@@ -289,79 +297,90 @@ class UsaPlugin(IVSPlg):
             elif self.page.ele('xpath://input[starts-with(@id, "kba") and contains(@id, "_response")]', timeout=1):
                 self._log("[State] Security question verification detected. Filling in answers...")
                 answer_eles = self.page.eles('xpath://input[starts-with(@id, "kba") and contains(@id, "_response")]')
-                
                 for ans_ele in answer_eles:
                     ele_id = ans_ele.attr('id')
                     match = re.search(r'kba(\d+)_response', ele_id)
                     if match:
                         q_num = match.group(1)
                         config_key = f"{q_num}_quest"
-                        
                         q_data = security.get(config_key)
-                
                         ans_text = q_data.get('a')
-                        ans_ele.input(ans_text)
+                        self.mouse.human_click_ele(ans_ele)
+                        self.keyboard.type_text(ans_text, humanize=True)
                         self._log(f"-> Find input {ele_id}, successfully filled in the answer for question {q_num}.")
-          
-
-                self.page.ele('#continue').click()
+                        
+                continue_btn = self.page.ele('#continue')
+                self.mouse.human_click_ele(continue_btn)
                 self._log("Security answers submitted. Waiting for redirection...")
-                time.sleep(3)
+                self._random_sleep(30, 40) 
                 continue
 
             # 状态 1: 登录页面
             elif self.page.ele('#signInName', timeout=1):
                 self._log("[State] Login page detected. Submitting credentials...")
-                
-                username_input = self.page.ele('#signInName')
+                username_selector = '#signInName'
+                username_input = self.page.ele(username_selector)
+                self.mouse.human_click_ele(username_input)
                 username_input.clear()
-                username_input.input(username)
+                self.keyboard.type_text(username, humanize=True)
+                self._random_sleep(3, 5) 
                 
-                password_input = self.page.ele('#password')
+                password_selector = '#password'
+                password_input = self.page.ele(password_selector)
+                self.mouse.human_click_ele(password_input)
                 password_input.clear()
-                password_input.input(password)
+                self.keyboard.type_text(password, humanize=True)
+                self._random_sleep(3, 5) 
                 
-                self.page.ele('#continue').click()
+                continue_btn_selector = '#continue'
+                continue_btn = self.page.ele(continue_btn_selector)
+                self.mouse.human_click_ele(continue_btn)                
                 has_submitted_login = True
                 self._log("Login form submitted. Waiting for the next step to load...")
-                time.sleep(3)
+                self._random_sleep(30, 40)
                 continue
 
             # 状态 3: 预约主页(控制台) -> 选择首签或改签
             elif self.page.ele('#atlas-sidebar', timeout=1):
                 self._log("[State] At the main booking dashboard. Looking for navigation button...")
-                
-                reschedule_btn = self.page.ele('#reschedule_appointment', timeout=0.5)
+                reschedule_btn_selector = '#reschedule_appointment'
+                schedule_btn_selector = '#schedule_appointment'
+                reschedule_btn = self.page.ele(reschedule_btn_selector, timeout=0.5)
                 if reschedule_btn:
-                    self._log("-> Detected [Reschedule Appointment]. Currently in rescheduling mode, clicking to proceed...")
-                    reschedule_btn.click()
+                    self._log("Currently in rescheduling mode, clicking to proceed...")
+                    self.mouse.human_click_ele(reschedule_btn)
+                    self._random_sleep(30, 40)
                 else:
-                    schedule_btn = self.page.ele('xpath://ul[@id="atlas-sidebar"]//a[text()="安排预约" or text()="New Appointment" or text()="Schedule Appointment"]', timeout=0.5)
+                    schedule_btn = self.page.ele(schedule_btn_selector, timeout=0.5)
                     if schedule_btn:
-                        self._log("-> Detected [Schedule Appointment]. Currently in first-time booking mode, clicking to proceed...")
-                        schedule_btn.click()
+                        self._log("Currently in first-time booking mode, clicking to proceed...")
+                        self.mouse.human_click_ele(schedule_btn)
+                        self._random_sleep(30, 40)
                     else:
-                        self._log("-> [WARN] Sidebar found, but no 'Schedule' or 'Reschedule' button detected. The page may still be loading...")
-                        
-                time.sleep(3)
+                        self._log("Not found schedule or reschedule button. The page may still be loading...")
+                        time.sleep(2)
                 continue
 
             else:
-                self._log("[State] In unknown or transitional state. No matching UI elements found. Waiting for next polling cycle...")
+                self._log("[State] In unknown or transitional state. Waiting for next polling cycle...")
                 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:
         """查询可用的签证预约信息"""
         self._log("Querying available slots...")
         res = VSQueryResult()
         res.success = False
-        # 1. 刷新页面以获取最新数据
+        
         self.page.refresh()
-        time.sleep(3)
+        
+        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
+        if not cf_bypasser.bypass(max_retry=6):
+            raise BizLogicError("Cloudflare bypass timeout")
+        cf_bypasser.handle_waiting_room()
+        
         current_url = self.page.url.lower()
         if 'auth' in current_url or 'login' in current_url:
             self.is_healthy = False
@@ -375,28 +394,62 @@ class UsaPlugin(IVSPlg):
         self.page.ele(f"xpath://label[text()='{applicant}']", timeout=60)
         post_select = self.page.ele('#post_select', timeout=60)
         
-        # 3. 选择领事馆
+        # ==================== 新增:网络监听逻辑 ====================
+        
+        # 开启监听目标 API
+        target_api = 'get-family-consular-schedule-days'
+        self.page.listen.start(target_api)
+        
+        # 3. 选择领事馆 (此操作会触发上述 API 的 AJAX 请求)
         self.page.ele(f"xpath://select[@id='post_select']/option[@value='{location_id}']", timeout=60)
         post_select.select.by_value(location_id) 
         
-        # 4. 等待日历加载
-        self.page.ele('xpath://p[@id="datepicker-message"]', timeout=60)        
-        # 5. 抓取所有可用日期
-        available_dates = []
-        day_cells = self.page.eles("css:td[data-handler='selectDay'].greenday")
+        # 4. 等待拦截 API 响应 (设置超时时间)
+        self._log("Waiting for schedule dates API response...")
+        packet = self.page.listen.wait(timeout=30)
+        self.page.listen.stop()
         
-        for cell in day_cells:
-            day = cell.ele("css:a.ui-state-default").text
-            month = int(cell.attr("data-month")) + 1
-            year = int(cell.attr("data-year"))
-            available_dates.append(date(year, month, int(day)).isoformat())
+        if not packet:
+            raise BizLogicError("Timeout waiting for schedule API response")
+            
+        status_code = packet.response.status
+        raw_resp = packet.response.raw_body
+        self._log(f"API Response Status: {status_code}")
+        
+        # 处理 HTTP 返回码不是200的情况
+        if status_code != 200:
+            if status_code == 403:
+                raise PermissionDeniedError(f"HTTP 403: {raw_resp[:512]}")
+            if status_code == 429:
+                self.is_healthy = False
+                raise RateLimiteddError(f"HTTP 429: {raw_resp[:512]}")
+            raise BizLogicError(f"HTTP {status_code} error. resp={raw_resp[0:512]}")
+
+        # 5. 解析返回的数据
+        available_dates = []
+        match = re.search(r'(\{.*\})', raw_resp, re.DOTALL)
+        if match:
+            json_str = match.group(1)
+            data = json.loads(json_str)
+        else:
+            data = json.loads(raw_resp)
+
+        schedule_days = data.get("ScheduleDays", [])
+        if schedule_days:
+            for day_obj in schedule_days:
+                date_str = day_obj.get("Date")
+                if date_str:
+                    available_dates.append(date_str)
 
+        # 6. 处理最终结果 (保持与你原有返回结构一致)
         if available_dates:
+            # 确保日期是有序的
+            available_dates.sort()
+            
             res.success = True
             res.availability_status = AvailabilityStatus.Available
             earliest_date = available_dates[0]
-            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
-            res.earliest_date = earliest_dt
+            res.earliest_date = datetime.strptime(earliest_date, "%Y-%m-%d")
             res.availability = [
                 DateAvailability(date=datetime.strptime(d, "%Y-%m-%d"), times=[])
                 for d in available_dates
@@ -414,8 +467,8 @@ class UsaPlugin(IVSPlg):
         res = VSBookResult()
         res.success = False
         
-        exp_start = user_inputs.get('expected_start_date', '')
-        exp_end = user_inputs.get('expected_end_date', '')
+        exp_start = user_inputs.get('expected_date_start', '')
+        exp_end = user_inputs.get('expected_date_end', '')
 
         available_dates_str =[
             da.date.strftime("%Y-%m-%d")
@@ -426,37 +479,65 @@ class UsaPlugin(IVSPlg):
         if not valid_dates_list:
             raise NotFoundError(message="No dates match user constraints")
         
-        selected_date = random.choice(valid_dates_list)
+        selected_slot_date = random.choice(valid_dates_list)
+        book_date_obj = datetime.strptime(selected_slot_date, "%Y-%m-%d").date()
+        
+        # jQuery UI Datepicker 月份是 0-11
+        target_day = str(book_date_obj.day)
+        target_month = str(book_date_obj.month - 1) 
+        target_year = str(book_date_obj.year)
+        
+        self._log(f"Target booking date: {selected_slot_date}. Navigating calendar...")
+        
+        # 1. 适配不在当前日历的情况:通过下拉框选择年份和月份
+        year_select = self.page.ele('.ui-datepicker-year', timeout=10)  # 查找年份下拉框
+        if year_select and year_select.value != target_year:
+            self._log(f"Changing year to {target_year}")
+            year_select.select.by_value(target_year)
+            self._random_sleep(0.5, 1)
+
+        month_select = self.page.ele('.ui-datepicker-month', timeout=10) # 查找月份下拉框
+        if month_select and month_select.value != target_month:
+            self._log(f"Changing month to {target_month}")
+            month_select.select.by_value(target_month)
+            self._random_sleep(0.5, 1)
 
-        book_date_obj = datetime.strptime(selected_date, "%Y-%m-%d").date()
-        day_to_click = str(book_date_obj.day)
-        month_to_click = str(book_date_obj.month - 1)
-        year_to_click = str(book_date_obj.year)
+        # 2. 点击目标日期
+        self._log(f"Clicking date {selected_slot_date}...")
+        target_cell_selector = f"xpath://td[@data-year='{target_year}' and @data-month='{target_month}']//a[text()='{target_day}']"
         
-        target_cell_xpath = f"xpath://td[@data-year='{year_to_click}' and @data-month='{month_to_click}']//a[text()='{day_to_click}']"
+        target_date_cell = self.page.ele(target_cell_selector, timeout=10)
+        if not target_date_cell:
+             raise BizLogicError(f"Target date element not found for {selected_slot_date} after navigating.")
+             
+        self.mouse.human_click_ele(target_date_cell)
         
-        # 1. 点击目标日期
-        self._log(f"Clicking date {selected_date}...")
-        self.page.ele(target_cell_xpath, timeout=30).click(by_js=True)
+        self._log("Waiting for available times to load...")
+        self._random_sleep(3, 5)
 
-        # 2. 等待并选择时间
+        # 3. 等待并选择时间
         self._log("Selecting earliest available time...")
-        first_time_radio = self.page.ele("css:#time_select input[name='schedule-entries']", timeout=30)
-        booked_time = first_time_radio.parent().text.strip()
-        first_time_radio.click()
-
-        # 3. 提交
-        self._log("Submitting booking...")
-        submit_button = self.page.ele("#submitbtn", timeout=30)
-        submit_button.click()
+        slot_time_selector = "css:#time_select input[name='schedule-entries']"
+        first_time_radio = self.page.ele(slot_time_selector, timeout=15)
+        if not first_time_radio:
+            raise BizLogicError("Failed to load time slots after clicking date. Might be rate limited or slots gone.")
+            
+        selected_slot_time = first_time_radio.parent().text.strip()
+        self.mouse.human_click_ele(first_time_radio)
         
+        # 4. 点击提交
+        self._random_sleep(1, 2)
+        self._log("Submitting booking...")
+        submit_appointment_selector = "#submitbtn"
+        submit_button = self.page.ele(submit_appointment_selector, timeout=10)
+        self.mouse.human_click_ele(submit_button)
         self._log("Booking submitted successfully!")
-        
+
         # 构造返回结果
         res = VSBookResult()
         res.success = True
-        res.book_date = selected_date
-        res.book_time = booked_time
+        res.book_date = selected_slot_date
+        res.book_time = selected_slot_time
         res.account = self.config.account.username
         return res
             

+ 2 - 0
utils/cloudflare_bypass_for_scraping.py

@@ -108,6 +108,8 @@ class CloudflareBypasser:
         while True:
             try:
                 html = self.driver.html.lower()
+                if "cloudflare" not in html:
+                    break
                 if "file d'attente" in html or "waiting room" in html:
                     if time.time() - wait_start > 60 * 60:
                         self.log_message("Waiting room timeout (1h).")