|
|
@@ -17,7 +17,7 @@ from vs_plg import IVSPlg
|
|
|
from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
|
|
|
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
|
from toolkit.mihomo_tunnel import MihomoTunnel
|
|
|
-from utils.mouse import HumanMouse
|
|
|
+from utils.mouse import HumanMouse, MOUSE_PROFILES
|
|
|
from utils.keyboard import HumanKeyboard
|
|
|
from utils.fingerprint_utils import FingerprintGenerator
|
|
|
|
|
|
@@ -65,8 +65,8 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
if not os.path.exists(self.root_workspace):
|
|
|
os.makedirs(self.root_workspace)
|
|
|
-
|
|
|
- self.last_refresh_time = time.time()
|
|
|
+
|
|
|
+ self.is_busy = False
|
|
|
self.tunnel = None
|
|
|
self.session_create_time: float = 0
|
|
|
|
|
|
@@ -87,42 +87,19 @@ class TlsPlugin(IVSPlg):
|
|
|
self.free_config = config.free_config or {}
|
|
|
|
|
|
def keep_alive(self):
|
|
|
- """
|
|
|
- 统一保活机制:
|
|
|
- - 距离上次刷新超过 10 分钟:执行完整页面刷新并检查 Session。
|
|
|
- - 否则:随机发送 Fetch 小请求保活。
|
|
|
- """
|
|
|
- if time.time() - self.last_refresh_time >= 60*10:
|
|
|
- try:
|
|
|
- self._log("Cut all connections...")
|
|
|
- self.tunnel.cut_all_connections()
|
|
|
- self._log("refresh page...")
|
|
|
- self.page.refresh()
|
|
|
- self.page.wait.load_start(timeout=2)
|
|
|
- self.page.wait.doc_loaded()
|
|
|
- time.sleep(random.uniform(1, 3))
|
|
|
- self._check_page_is_session_expired_or_invalid('Book your appointment', html=self.page.html)
|
|
|
- self.last_refresh_time = time.time()
|
|
|
- self._log("refresh page finished")
|
|
|
- except Exception as e:
|
|
|
- self._log(f"refresh page error: {str(e)}")
|
|
|
- self.is_healthy = False
|
|
|
- else:
|
|
|
- choice = random.choice(['home', 'travel_groups'])
|
|
|
- headers = {}
|
|
|
- if choice == 'home':
|
|
|
- url = "https://visas-fr.tlscontact.com/"
|
|
|
- elif choice == 'travel_groups':
|
|
|
- url = "https://visas-fr.tlscontact.com/en-us/travel-groups"
|
|
|
- headers = {"cache-control": "max-age=0"}
|
|
|
- try:
|
|
|
- self._log(f"send keep alive fetch request ({choice})")
|
|
|
- self._perform_request("GET", url, headers=headers)
|
|
|
- except Exception as e:
|
|
|
- self._log(f"send keep alive fetch error: {str(e)}")
|
|
|
- self.is_healthy = False
|
|
|
+ try:
|
|
|
+ self.page.refresh()
|
|
|
+ self.page.wait.load_start(timeout=2)
|
|
|
+ self.page.wait.doc_loaded()
|
|
|
+ time.sleep(random.uniform(1, 3))
|
|
|
+ self._check_page_is_session_expired_or_invalid('Book your appointment', html = self.page.html)
|
|
|
+ self.simulate_random_human_mouse_move()
|
|
|
+ except SessionExpiredOrInvalidError as e:
|
|
|
+ self.is_healthy = False
|
|
|
+ except Exception as e:
|
|
|
+ self._log(f"Unexpected error in keep_alive: {e}")
|
|
|
|
|
|
- def simulate_random_human_clicks(self, min_x=300, max_x=800, min_y=400, max_y=600, min_clicks=1, max_clicks=2):
|
|
|
+ def simulate_random_human_mouse_move(self, min_x=100, max_x=800, min_y=100, max_y=800, min_points=1, max_points=2):
|
|
|
"""
|
|
|
在指定区域内模拟人类随机移动鼠标并点击数次。
|
|
|
|
|
|
@@ -130,23 +107,21 @@ class TlsPlugin(IVSPlg):
|
|
|
:param max_x: X坐标最大范围
|
|
|
:param min_y: Y坐标最小范围
|
|
|
:param max_y: Y坐标最大范围
|
|
|
- :param min_clicks: 随便点击的最少次数
|
|
|
- :param max_clicks: 随便点击的最多次数
|
|
|
+ :param min_point: 随便移动的最少次数
|
|
|
+ :param max_point: 随便移动的最多次数
|
|
|
"""
|
|
|
- click_count = random.randint(min_clicks, max_clicks)
|
|
|
- self._log(f"Starting random human simulation: will click {click_count} times in the area.")
|
|
|
- for i in range(click_count):
|
|
|
+ move_cnt = random.randint(min_points, max_points)
|
|
|
+ self._log(f"Starting random human simulation: will move {move_cnt} times in the area.")
|
|
|
+ for i in range(move_cnt):
|
|
|
rand_x = random.randint(min_x, max_x)
|
|
|
rand_y = random.randint(min_y, max_y)
|
|
|
- self._log(f"[{i+1}/{click_count}] Moving mouse to ({rand_x}, {rand_y}) and clicking")
|
|
|
- self.mouse.click(rand_x, rand_y, humanize=True)
|
|
|
- if i < click_count - 1:
|
|
|
- sleep_time = random.uniform(0.5, 1.8)
|
|
|
- self._log(f"Resting for {sleep_time:.2f} seconds before next click...")
|
|
|
- time.sleep(sleep_time)
|
|
|
- self._log("Random human clicks simulation completed.")
|
|
|
+ self._log(f"[{i+1}/{move_cnt}] Moving mouse to ({rand_x}, {rand_y})")
|
|
|
+ self.mouse.move(rand_x, rand_y, humanize=True)
|
|
|
+ self._log("Random human move simulation completed.")
|
|
|
|
|
|
def health_check(self) -> bool:
|
|
|
+ if self.is_busy:
|
|
|
+ return True
|
|
|
if not self.is_healthy:
|
|
|
return False
|
|
|
if self.page is None:
|
|
|
@@ -322,7 +297,11 @@ class TlsPlugin(IVSPlg):
|
|
|
cf_bypasser.handle_waiting_room()
|
|
|
|
|
|
self._log("Init humanize tools...")
|
|
|
- self.mouse = HumanMouse(self.page, debug=self.config.debug)
|
|
|
+
|
|
|
+ profile_name = random.choice(list(MOUSE_PROFILES.keys()))
|
|
|
+ self._log(f"[HumanMouse] current mouse profiles: {profile_name}")
|
|
|
+
|
|
|
+ self.mouse = HumanMouse(self.page, timing=MOUSE_PROFILES[profile_name], debug=self.config.debug)
|
|
|
self.keyboard = HumanKeyboard(self.page)
|
|
|
viewport_width = self.page.rect.viewport_size[0]
|
|
|
viewport_height = self.page.rect.viewport_size[1]
|
|
|
@@ -511,60 +490,63 @@ class TlsPlugin(IVSPlg):
|
|
|
def query(self, apt_type: AppointmentType) -> VSQueryResult:
|
|
|
res = VSQueryResult()
|
|
|
res.success = False
|
|
|
-
|
|
|
- slots = []
|
|
|
- self._log(f"Executing silent JS fetch...")
|
|
|
- resp = self._perform_request("GET", self.page.url, retry_count=1)
|
|
|
- self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
|
|
|
- slots = self._parse_appointment_slots(resp.text)
|
|
|
-
|
|
|
- if slots:
|
|
|
- res.success = True
|
|
|
- earliest_date = slots[0]["date"]
|
|
|
- earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
|
|
|
- res.availability_status = AvailabilityStatus.Available
|
|
|
- res.earliest_date = earliest_dt
|
|
|
- date_map: dict[datetime, list[TimeSlot]] = {}
|
|
|
- for s in slots:
|
|
|
- date_str = s["date"]
|
|
|
- dt = datetime.strptime(date_str, "%Y-%m-%d")
|
|
|
- date_map.setdefault(dt, []).append(
|
|
|
- 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! -> {slots}")
|
|
|
- else:
|
|
|
- self._log("No slots available.")
|
|
|
- res.success = False
|
|
|
- res.availability_status = AvailabilityStatus.NoneAvailable
|
|
|
+ self.is_busy = True
|
|
|
+ try:
|
|
|
+ slots = []
|
|
|
+ self._log(f"Executing silent JS fetch...")
|
|
|
+ resp = self._perform_request("GET", self.page.url, retry_count=0)
|
|
|
+ self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
|
|
|
+ slots = self._parse_appointment_slots(resp.text)
|
|
|
|
|
|
- # TODO(TEST): 临时测试预约提交
|
|
|
- if configure.TLS_TEST_BOOK_AFTER_QUERY:
|
|
|
- test_date = "2026-06-10"
|
|
|
- test_time = "09:00"
|
|
|
- test_label = ""
|
|
|
- test_dt = datetime.strptime(test_date, "%Y-%m-%d")
|
|
|
- query_res = VSQueryResult()
|
|
|
- query_res.success = True
|
|
|
- query_res.availability_status = AvailabilityStatus.Available
|
|
|
- query_res.earliest_date = test_dt
|
|
|
- query_res.availability = [
|
|
|
- DateAvailability(
|
|
|
- date=test_dt,
|
|
|
- times=[TimeSlot(time=test_time, label=test_label)]
|
|
|
- )
|
|
|
- ]
|
|
|
- self._log(f"[TEST] using fixed June slot: {test_date} {test_time} {test_label}")
|
|
|
- test_userinput = {
|
|
|
- "support_pta": False,
|
|
|
- "expected_end_date": "2100-01-01",
|
|
|
- "expected_start_date": "2000-01-01"
|
|
|
- }
|
|
|
- try:
|
|
|
- self.book(query_res, test_userinput)
|
|
|
- except Exception as e:
|
|
|
- self._log(f"[TEST] book() after query failed: {e}")
|
|
|
- self.is_healthy = False
|
|
|
+ if slots:
|
|
|
+ res.success = True
|
|
|
+ earliest_date = slots[0]["date"]
|
|
|
+ earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
|
|
|
+ res.availability_status = AvailabilityStatus.Available
|
|
|
+ res.earliest_date = earliest_dt
|
|
|
+ date_map: dict[datetime, list[TimeSlot]] = {}
|
|
|
+ for s in slots:
|
|
|
+ date_str = s["date"]
|
|
|
+ dt = datetime.strptime(date_str, "%Y-%m-%d")
|
|
|
+ date_map.setdefault(dt, []).append(
|
|
|
+ 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! -> {slots}")
|
|
|
+ else:
|
|
|
+ self._log("No slots available.")
|
|
|
+ res.success = False
|
|
|
+ res.availability_status = AvailabilityStatus.NoneAvailable
|
|
|
+
|
|
|
+ # TODO(TEST): 临时测试预约提交
|
|
|
+ if configure.TLS_TEST_BOOK_AFTER_QUERY:
|
|
|
+ test_date = "2026-06-10"
|
|
|
+ test_time = "09:00"
|
|
|
+ test_label = ""
|
|
|
+ test_dt = datetime.strptime(test_date, "%Y-%m-%d")
|
|
|
+ query_res = VSQueryResult()
|
|
|
+ query_res.success = True
|
|
|
+ query_res.availability_status = AvailabilityStatus.Available
|
|
|
+ query_res.earliest_date = test_dt
|
|
|
+ query_res.availability = [
|
|
|
+ DateAvailability(
|
|
|
+ date=test_dt,
|
|
|
+ times=[TimeSlot(time=test_time, label=test_label)]
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ self._log(f"[TEST] using fixed June slot: {test_date} {test_time} {test_label}")
|
|
|
+ test_userinput = {
|
|
|
+ "support_pta": False,
|
|
|
+ "expected_end_date": "2100-01-01",
|
|
|
+ "expected_start_date": "2000-01-01"
|
|
|
+ }
|
|
|
+ try:
|
|
|
+ self.book(query_res, test_userinput)
|
|
|
+ except Exception as e:
|
|
|
+ self._log(f"[TEST] book() after query failed: {e}")
|
|
|
+ self.is_healthy = False
|
|
|
+ finally:
|
|
|
+ self.is_busy = False
|
|
|
return res
|
|
|
|
|
|
def book_bak(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
|
|
|
@@ -954,6 +936,7 @@ class TlsPlugin(IVSPlg):
|
|
|
try:
|
|
|
self._log("Refreshing page to trigger Cloudflare...")
|
|
|
self.page.refresh()
|
|
|
+ time.sleep(5)
|
|
|
cf = CloudflareBypasser(self.page, log=self.config.debug)
|
|
|
success = cf.bypass(max_retry=6)
|
|
|
|