|
|
@@ -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:
|