|
|
@@ -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()
|