Hujiarui 2 bulan lalu
induk
melakukan
65b085cffb
1 mengubah file dengan 148 tambahan dan 32 penghapusan
  1. 148 32
      plugins/usa_plugin.py

+ 148 - 32
plugins/usa_plugin.py

@@ -78,14 +78,6 @@ 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))
@@ -207,20 +199,18 @@ class UsaPlugin(IVSPlg):
         usa_url = self.free_config.get('usa_url', '') 
         self._log(f"Navigating: {usa_url}")
         self.page.get(usa_url)
-        # 初始访问页面,等待长延时,防止过快触发后续操作
-        self._random_sleep(30, 45)
+        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._random_sleep(30, 45) # 刷新动作属于高危操作,加长延时
+            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")
-        # 绕过盾后,休眠一段时间再处理 waiting room
-        self._random_sleep(15, 30) 
+        time.sleep(3)
         cf_bypasser.handle_waiting_room()
         
         self._log("Init humanize tools...")
@@ -245,7 +235,7 @@ class UsaPlugin(IVSPlg):
         
         for step in range(max_steps):
             self.page.wait.doc_loaded()
-            time.sleep(1) # 这个用于等待页面DOM渲染,保留短时,因为不是发请求
+            time.sleep(1)
             
             current_url = self.page.url
             current_title = self.page.title.lower()
@@ -262,7 +252,7 @@ class UsaPlugin(IVSPlg):
             if stuck_counter >= 3:
                 self._log("[WARN] Page stucked, try to refresh...")
                 self.page.refresh()
-                self._random_sleep(30, 60) # 刷新操作触发请求,长延时
+                self.page.wait.load_start(timeout=5)
                 stuck_counter = 0
                 continue
             
@@ -270,8 +260,9 @@ class UsaPlugin(IVSPlg):
             # 网络出现故障,直接重试
             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._random_sleep(45, 60) # 遇到错误必须拉长延时防止被彻底拉黑
+                self.page.wait.load_start(timeout=5)
                 continue
             
             cloudflare_blocked_indicators = [
@@ -285,7 +276,7 @@ class UsaPlugin(IVSPlg):
             # 遇到五秒盾先绕盾
             if "just a moment" in current_title:
                 cf_bypasser.bypass(max_retry=3)
-                self._random_sleep(20, 40)
+                time.sleep(3)
                 continue
             
             if self.page.ele('#post_select', timeout=1):
@@ -315,8 +306,7 @@ class UsaPlugin(IVSPlg):
 
                 self.page.ele('#continue').click()
                 self._log("Security answers submitted. Waiting for redirection...")
-                # 提交密保问题,触发POST请求,长延时
-                self._random_sleep(30, 60) 
+                time.sleep(3)
                 continue
 
             # 状态 1: 登录页面
@@ -334,8 +324,7 @@ class UsaPlugin(IVSPlg):
                 self.page.ele('#continue').click()
                 has_submitted_login = True
                 self._log("Login form submitted. Waiting for the next step to load...")
-                # 提交登录表单,触发POST请求,必须长延时
-                self._random_sleep(30, 60)
+                time.sleep(3)
                 continue
 
             # 状态 3: 预约主页(控制台) -> 选择首签或改签
@@ -346,23 +335,20 @@ class UsaPlugin(IVSPlg):
                 if reschedule_btn:
                     self._log("-> Detected [Reschedule Appointment]. Currently in rescheduling mode, clicking to proceed...")
                     reschedule_btn.click()
-                    # 点击导航按钮,触发页面跳转GET请求,长延时
-                    self._random_sleep(30, 50)
                 else:
                     schedule_btn = self.page.ele('xpath://ul[@id="atlas-sidebar"]//a[text()="安排预约" or text()="New Appointment" or text()="Schedule Appointment"]', timeout=0.5)
                     if schedule_btn:
                         self._log("-> Detected [Schedule Appointment]. Currently in first-time booking mode, clicking to proceed...")
                         schedule_btn.click()
-                        # 点击导航按钮,触发页面跳转GET请求,长延时
-                        self._random_sleep(30, 50)
                     else:
                         self._log("-> [WARN] Sidebar found, but no 'Schedule' or 'Reschedule' button detected. The page may still be loading...")
-                        time.sleep(2) # 仅等待DOM渲染,不发请求,保持短时
+                        
+                time.sleep(3)
                 continue
 
             else:
                 self._log("[State] In unknown or transitional state. No matching UI elements found. Waiting for next polling cycle...")
-                time.sleep(2) # 仅等待DOM渲染,不发请求,保持短时
+                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}")
@@ -373,12 +359,9 @@ class UsaPlugin(IVSPlg):
         self._log("Querying available slots...")
         res = VSQueryResult()
         res.success = False
-        
         # 1. 刷新页面以获取最新数据
         self.page.refresh()
-        # 【关键修改】:刷新操作触发页面重载请求,必须执行长睡眠 
-        self._random_sleep(30, 60)
-        
+        time.sleep(3)
         current_url = self.page.url.lower()
         if 'auth' in current_url or 'login' in current_url:
             self.is_healthy = False
@@ -390,4 +373,137 @@ class UsaPlugin(IVSPlg):
 
         # 2. 等待页面元素
         self.page.ele(f"xpath://label[text()='{applicant}']", timeout=60)
-        post_select = self.page.ele('#po
+        post_select = self.page.ele('#post_select', timeout=60)
+        
+        # 3. 选择领事馆
+        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")
+        
+        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 available_dates:
+            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.availability = [
+                DateAvailability(date=datetime.strptime(d, "%Y-%m-%d"), times=[])
+                for d in available_dates
+            ]
+            self._log(f"Slot Found! earliest_date={earliest_date}, size={len(available_dates)}")
+        else:
+            res.success = False
+            res.availability_status = AvailabilityStatus.NoneAvailable
+            self._log("No slots available.")
+            
+        return res
+
+    def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
+        """进行预约操作"""
+        res = VSBookResult()
+        res.success = False
+        
+        exp_start = user_inputs.get('expected_start_date', '')
+        exp_end = user_inputs.get('expected_end_date', '')
+
+        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")
+        
+        selected_date = random.choice(valid_dates_list)
+
+        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)
+        
+        target_cell_xpath = f"xpath://td[@data-year='{year_to_click}' and @data-month='{month_to_click}']//a[text()='{day_to_click}']"
+        
+        # 1. 点击目标日期
+        self._log(f"Clicking date {selected_date}...")
+        self.page.ele(target_cell_xpath, timeout=30).click(by_js=True)
+
+        # 2. 等待并选择时间
+        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()
+        
+        self._log("Booking submitted successfully!")
+        
+        # 构造返回结果
+        res = VSBookResult()
+        res.success = True
+        res.book_date = selected_date
+        res.book_time = booked_time
+        res.account = self.config.account.username
+        return res
+            
+    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
+        if not start_str or not end_str:
+            return dates
+        valid_dates = []
+        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
+        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
+        for date_str in dates:
+            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
+            if s_date <= curr_date <= e_date:
+                valid_dates.append(date_str)
+        random.shuffle(valid_dates)
+        return valid_dates
+    
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        if self.page:
+            try:
+                self.page.quit(force=True)
+            except Exception:
+                pass
+            self.page = None
+        
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    self._log(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            if os.path.exists(self.root_workspace):
+                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+        if self.tunnel:
+            try: self.tunnel.stop()
+            except: pass
+            self.tunnel = None
+        
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()