|
|
@@ -263,164 +263,165 @@ class TlsPlugin(IVSPlg):
|
|
|
has_submitted_login = False
|
|
|
|
|
|
for step in range(max_steps):
|
|
|
- self.page.wait.load_start()
|
|
|
- current_url = self.page.url
|
|
|
- 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
|
|
|
- ]
|
|
|
- if any(cloudflare_blocked_indicators):
|
|
|
- raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
|
|
|
-
|
|
|
- # 状态 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.session_create_time = time.time()
|
|
|
- self._log("✅ Login & Navigation Success! Reached appointment-booking.")
|
|
|
- session_created = True
|
|
|
- break
|
|
|
-
|
|
|
- # 状态 2:遇到没有申请人的拦截页 (致命错误退出条件)
|
|
|
- no_applicant_indicators = [
|
|
|
- "Add a new applicant" in self.page.html,
|
|
|
- "You have not yet added an applicant" in self.page.html,
|
|
|
- "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")
|
|
|
- self.mouse.human_click_ele(login_link)
|
|
|
- time.sleep(3)
|
|
|
- continue
|
|
|
- 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)
|
|
|
- time.sleep(1.5)
|
|
|
- self.page.get(tls_url)
|
|
|
- time.sleep(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...")
|
|
|
+ try:
|
|
|
+ self.page.wait.load_start()
|
|
|
+ current_url = self.page.url
|
|
|
+ self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
|
|
|
|
|
|
- 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
|
|
|
+ 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
|
|
|
+ ]
|
|
|
+ if any(cloudflare_blocked_indicators):
|
|
|
+ raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
|
|
|
|
|
|
- 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)
|
|
|
+ # 状态 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.session_create_time = time.time()
|
|
|
+ self._log("✅ Login & Navigation Success! Reached appointment-booking.")
|
|
|
+ session_created = True
|
|
|
+ break
|
|
|
|
|
|
- # 注入 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}")
|
|
|
+ # 状态 2:遇到没有申请人的拦截页 (致命错误退出条件)
|
|
|
+ no_applicant_indicators = [
|
|
|
+ "Add a new applicant" in self.page.html,
|
|
|
+ "You have not yet added an applicant" in self.page.html,
|
|
|
+ "applicants-information" in current_url
|
|
|
+ ]
|
|
|
+ if any(no_applicant_indicators):
|
|
|
+ raise BizLogicError(message="No applicant added. Cannot proceed to booking.")
|
|
|
|
|
|
- # 注入 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))
|
|
|
+ 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")
|
|
|
+ self.mouse.human_click_ele(login_link)
|
|
|
+ time.sleep(3)
|
|
|
+ continue
|
|
|
+ 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)
|
|
|
+ time.sleep(1.5)
|
|
|
+ self.page.get(tls_url)
|
|
|
+ time.sleep(3)
|
|
|
+ continue
|
|
|
|
|
|
- self._log("Submitting Login...")
|
|
|
- login_btn = self.page.ele('tag:button@@text():Login')
|
|
|
- self.mouse.human_click_ele(login_btn)
|
|
|
- has_submitted_login = True
|
|
|
- time.sleep(3)
|
|
|
- continue
|
|
|
-
|
|
|
- # 状态 5:Travel Groups 页面
|
|
|
- if "travel-groups" in current_url:
|
|
|
- self._log("State: Travel Groups. Selecting targeted group...")
|
|
|
- groups = self._parse_travel_groups(self.page.html)
|
|
|
- location = self.free_config.get('location')
|
|
|
- self.travel_group = next((g for g in groups if location in g['location']), None)
|
|
|
+ # 状态 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...")
|
|
|
+
|
|
|
+ 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))
|
|
|
|
|
|
- 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}'
|
|
|
+ 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
|
|
|
+ time.sleep(3)
|
|
|
+ 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)
|
|
|
+ # 状态 5:Travel Groups 页面
|
|
|
+ if "travel-groups" in current_url:
|
|
|
+ self._log("State: Travel Groups. Selecting targeted group...")
|
|
|
+ groups = self._parse_travel_groups(self.page.html)
|
|
|
+ location = self.free_config.get('location')
|
|
|
+ self.travel_group = next((g for g in groups if location in g['location']), None)
|
|
|
|
|
|
- if select_btn:
|
|
|
- time.sleep(random.uniform(0.5, 1.2))
|
|
|
- self.mouse.human_click_ele(select_btn)
|
|
|
- time.sleep(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 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)
|
|
|
+ time.sleep(3)
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ self._log("[WARN] Select button found but not visible.")
|
|
|
else:
|
|
|
- self._log("[WARN] Select button found but not visible.")
|
|
|
- else:
|
|
|
- self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
|
|
|
-
|
|
|
- # 状态 6:中间过渡页,需点击 "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'))
|
|
|
- time.sleep(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)")
|
|
|
+ self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
|
|
|
+
|
|
|
+ # 状态 6:中间过渡页,需点击 "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'))
|
|
|
+ time.sleep(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)
|
|
|
+ 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 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)
|
|
|
-
|
|
|
- # 如果循环耗尽还没到达目标
|
|
|
+ except Exception as e:
|
|
|
+ self._log(f'Step {step} exception: {e}')
|
|
|
+
|
|
|
if not session_created:
|
|
|
raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
|
|
|
|