|
@@ -78,6 +78,14 @@ class UsaPlugin(IVSPlg):
|
|
|
else:
|
|
else:
|
|
|
print(f'[UsaPlugin] [{self.group_id}] [{self.instance_id}] {message}')
|
|
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):
|
|
def _get_free_port(self):
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
|
s.bind(('', 0))
|
|
s.bind(('', 0))
|
|
@@ -199,18 +207,20 @@ class UsaPlugin(IVSPlg):
|
|
|
usa_url = self.free_config.get('usa_url', '')
|
|
usa_url = self.free_config.get('usa_url', '')
|
|
|
self._log(f"Navigating: {usa_url}")
|
|
self._log(f"Navigating: {usa_url}")
|
|
|
self.page.get(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:
|
|
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._log(f'Block by cloudflare, try refresh...')
|
|
|
self.page.refresh()
|
|
self.page.refresh()
|
|
|
- self.page.wait.load_start(timeout=2)
|
|
|
|
|
|
|
+ self._random_sleep(30, 45) # 刷新动作属于高危操作,加长延时
|
|
|
self.page.wait.doc_loaded()
|
|
self.page.wait.doc_loaded()
|
|
|
|
|
|
|
|
cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
|
|
cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
|
|
|
if not cf_bypasser.bypass(max_retry=6):
|
|
if not cf_bypasser.bypass(max_retry=6):
|
|
|
raise BizLogicError("Cloudflare bypass timeout")
|
|
raise BizLogicError("Cloudflare bypass timeout")
|
|
|
- time.sleep(3)
|
|
|
|
|
|
|
+ # 绕过盾后,休眠一段时间再处理 waiting room
|
|
|
|
|
+ self._random_sleep(15, 30)
|
|
|
cf_bypasser.handle_waiting_room()
|
|
cf_bypasser.handle_waiting_room()
|
|
|
|
|
|
|
|
self._log("Init humanize tools...")
|
|
self._log("Init humanize tools...")
|
|
@@ -235,34 +245,32 @@ class UsaPlugin(IVSPlg):
|
|
|
|
|
|
|
|
for step in range(max_steps):
|
|
for step in range(max_steps):
|
|
|
self.page.wait.doc_loaded()
|
|
self.page.wait.doc_loaded()
|
|
|
- time.sleep(1)
|
|
|
|
|
|
|
+ time.sleep(1) # 这个用于等待页面DOM渲染,保留短时,因为不是发请求
|
|
|
|
|
|
|
|
current_url = self.page.url
|
|
current_url = self.page.url
|
|
|
current_title = self.page.title.lower()
|
|
current_title = self.page.title.lower()
|
|
|
current_html_content = self.page.html
|
|
current_html_content = self.page.html
|
|
|
self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
|
|
self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
|
|
|
|
|
|
|
|
|
|
+ # --- [异常处理层] ---
|
|
|
if current_url == last_url:
|
|
if current_url == last_url:
|
|
|
stuck_counter += 1
|
|
stuck_counter += 1
|
|
|
else:
|
|
else:
|
|
|
last_url = current_url
|
|
last_url = current_url
|
|
|
stuck_counter = 0
|
|
stuck_counter = 0
|
|
|
-
|
|
|
|
|
- # --- [异常处理层] ---
|
|
|
|
|
if stuck_counter >= 3:
|
|
if stuck_counter >= 3:
|
|
|
self._log("[WARN] Page stucked, try to refresh...")
|
|
self._log("[WARN] Page stucked, try to refresh...")
|
|
|
self.page.refresh()
|
|
self.page.refresh()
|
|
|
- self.page.wait.load_start(timeout=5)
|
|
|
|
|
|
|
+ self._random_sleep(30, 40)
|
|
|
stuck_counter = 0
|
|
stuck_counter = 0
|
|
|
continue
|
|
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):
|
|
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})...")
|
|
self._log(f"[WARN] Server network error, try to refresh (Step: {step})...")
|
|
|
- time.sleep(2)
|
|
|
|
|
self.page.refresh()
|
|
self.page.refresh()
|
|
|
- self.page.wait.load_start(timeout=5)
|
|
|
|
|
|
|
+ self._random_sleep(30, 40)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
cloudflare_blocked_indicators = [
|
|
cloudflare_blocked_indicators = [
|
|
@@ -276,7 +284,7 @@ class UsaPlugin(IVSPlg):
|
|
|
# 遇到五秒盾先绕盾
|
|
# 遇到五秒盾先绕盾
|
|
|
if "just a moment" in current_title:
|
|
if "just a moment" in current_title:
|
|
|
cf_bypasser.bypass(max_retry=3)
|
|
cf_bypasser.bypass(max_retry=3)
|
|
|
- time.sleep(3)
|
|
|
|
|
|
|
+ self._random_sleep(20, 40)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
if self.page.ele('#post_select', timeout=1):
|
|
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):
|
|
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...")
|
|
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")]')
|
|
answer_eles = self.page.eles('xpath://input[starts-with(@id, "kba") and contains(@id, "_response")]')
|
|
|
-
|
|
|
|
|
for ans_ele in answer_eles:
|
|
for ans_ele in answer_eles:
|
|
|
ele_id = ans_ele.attr('id')
|
|
ele_id = ans_ele.attr('id')
|
|
|
match = re.search(r'kba(\d+)_response', ele_id)
|
|
match = re.search(r'kba(\d+)_response', ele_id)
|
|
|
if match:
|
|
if match:
|
|
|
q_num = match.group(1)
|
|
q_num = match.group(1)
|
|
|
config_key = f"{q_num}_quest"
|
|
config_key = f"{q_num}_quest"
|
|
|
-
|
|
|
|
|
q_data = security.get(config_key)
|
|
q_data = security.get(config_key)
|
|
|
-
|
|
|
|
|
ans_text = q_data.get('a')
|
|
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._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...")
|
|
self._log("Security answers submitted. Waiting for redirection...")
|
|
|
- time.sleep(3)
|
|
|
|
|
|
|
+ self._random_sleep(30, 40)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
# 状态 1: 登录页面
|
|
# 状态 1: 登录页面
|
|
|
elif self.page.ele('#signInName', timeout=1):
|
|
elif self.page.ele('#signInName', timeout=1):
|
|
|
self._log("[State] Login page detected. Submitting credentials...")
|
|
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.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.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
|
|
has_submitted_login = True
|
|
|
self._log("Login form submitted. Waiting for the next step to load...")
|
|
self._log("Login form submitted. Waiting for the next step to load...")
|
|
|
- time.sleep(3)
|
|
|
|
|
|
|
+ self._random_sleep(30, 40)
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
# 状态 3: 预约主页(控制台) -> 选择首签或改签
|
|
# 状态 3: 预约主页(控制台) -> 选择首签或改签
|
|
|
elif self.page.ele('#atlas-sidebar', timeout=1):
|
|
elif self.page.ele('#atlas-sidebar', timeout=1):
|
|
|
self._log("[State] At the main booking dashboard. Looking for navigation button...")
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
continue
|
|
|
|
|
|
|
|
else:
|
|
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)
|
|
time.sleep(2)
|
|
|
|
|
|
|
|
if not session_created:
|
|
if not session_created:
|
|
|
raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
|
|
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:
|
|
def query(self, apt_type: AppointmentType) -> VSQueryResult:
|
|
|
"""查询可用的签证预约信息"""
|
|
"""查询可用的签证预约信息"""
|
|
|
self._log("Querying available slots...")
|
|
self._log("Querying available slots...")
|
|
|
res = VSQueryResult()
|
|
res = VSQueryResult()
|
|
|
res.success = False
|
|
res.success = False
|
|
|
- # 1. 刷新页面以获取最新数据
|
|
|
|
|
|
|
+
|
|
|
self.page.refresh()
|
|
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()
|
|
current_url = self.page.url.lower()
|
|
|
if 'auth' in current_url or 'login' in current_url:
|
|
if 'auth' in current_url or 'login' in current_url:
|
|
|
self.is_healthy = False
|
|
self.is_healthy = False
|
|
@@ -375,28 +394,62 @@ class UsaPlugin(IVSPlg):
|
|
|
self.page.ele(f"xpath://label[text()='{applicant}']", timeout=60)
|
|
self.page.ele(f"xpath://label[text()='{applicant}']", timeout=60)
|
|
|
post_select = self.page.ele('#post_select', 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)
|
|
self.page.ele(f"xpath://select[@id='post_select']/option[@value='{location_id}']", timeout=60)
|
|
|
post_select.select.by_value(location_id)
|
|
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:
|
|
if available_dates:
|
|
|
|
|
+ # 确保日期是有序的
|
|
|
|
|
+ available_dates.sort()
|
|
|
|
|
+
|
|
|
res.success = True
|
|
res.success = True
|
|
|
res.availability_status = AvailabilityStatus.Available
|
|
res.availability_status = AvailabilityStatus.Available
|
|
|
earliest_date = available_dates[0]
|
|
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 = [
|
|
res.availability = [
|
|
|
DateAvailability(date=datetime.strptime(d, "%Y-%m-%d"), times=[])
|
|
DateAvailability(date=datetime.strptime(d, "%Y-%m-%d"), times=[])
|
|
|
for d in available_dates
|
|
for d in available_dates
|
|
@@ -414,8 +467,8 @@ class UsaPlugin(IVSPlg):
|
|
|
res = VSBookResult()
|
|
res = VSBookResult()
|
|
|
res.success = False
|
|
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 =[
|
|
available_dates_str =[
|
|
|
da.date.strftime("%Y-%m-%d")
|
|
da.date.strftime("%Y-%m-%d")
|
|
@@ -426,37 +479,65 @@ class UsaPlugin(IVSPlg):
|
|
|
if not valid_dates_list:
|
|
if not valid_dates_list:
|
|
|
raise NotFoundError(message="No dates match user constraints")
|
|
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...")
|
|
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!")
|
|
self._log("Booking submitted successfully!")
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
# 构造返回结果
|
|
# 构造返回结果
|
|
|
res = VSBookResult()
|
|
res = VSBookResult()
|
|
|
res.success = True
|
|
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
|
|
res.account = self.config.account.username
|
|
|
return res
|
|
return res
|
|
|
|
|
|