jerry 1 неделя назад
Родитель
Сommit
c608406f0b
8 измененных файлов с 276 добавлено и 366 удалено
  1. 49 91
      booker_builtin.py
  2. 51 95
      booker_order.py
  3. 2 2
      configure.py
  4. 145 148
      plugins/tls_plugin.py
  5. 6 7
      sentinel.py
  6. 6 9
      test/test_publish_slot.py
  7. 6 6
      toolkit/vs_cloud_api.py
  8. 11 8
      utils/cloudflare_bypass_for_scraping.py

+ 49 - 91
booker_builtin.py

@@ -26,19 +26,7 @@ class BuiltinBookerGCO:
         self.m_tasks: List[Task] = []
         self.m_tasks: List[Task] = []
         self.m_lock = threading.RLock()
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
         self.m_stop_event = threading.Event()
-        redis_common_kwargs = {
-            **redis_conf,
-            "socket_timeout": 5,
-            "socket_connect_timeout": 5,
-            # 会自动发送 PING
-            "health_check_interval": 15,
-            # TCP KeepAlive
-            "socket_keepalive": True,
-            "retry_on_timeout": True,
-            "decode_responses": False,
-        }
-        self.redis_com = redis.Redis(**redis_common_kwargs)
-        self.redis_sub = redis.Redis(**redis_common_kwargs)
+        self.redis_client = redis.Redis(**redis_conf)
         self.m_pending_builtin = 0
         self.m_pending_builtin = 0
         
         
         self.m_tracker_key = f"vs:worker:tasks_tracker:{self.m_cfg.identifier}"
         self.m_tracker_key = f"vs:worker:tasks_tracker:{self.m_cfg.identifier}"
@@ -141,80 +129,50 @@ class BuiltinBookerGCO:
                     self.m_tasks = [t for t in self.m_tasks if t in healthy_tasks]
                     self.m_tasks = [t for t in self.m_tasks if t in healthy_tasks]
 
 
     def _booking_trigger_loop(self):
     def _booking_trigger_loop(self):
-        self._log("Pub/Sub Trigger loop started.")
-        channel_to_routing_key = {}
-        for apt in self.m_cfg.appointment_types:
-            channel = self._get_redis_key(apt.routing_key)
-            channel_to_routing_key[channel] = apt.routing_key
-            
-        if not channel_to_routing_key:
-            self._log("No appointment types configured. Exiting trigger loop.")
-            return
-            
-        pubsub = None
+        self._log("Trigger loop started.")
         while not self.m_stop_event.is_set():
         while not self.m_stop_event.is_set():
             try:
             try:
-                if pubsub is None:
-                    pubsub = self.redis_sub.pubsub(ignore_subscribe_messages=False)
-                    channels_to_sub = list(channel_to_routing_key.keys())
-                    self._log(f"⏳ Sending SUBSCRIBE command to Redis for: {channels_to_sub}")
-                    pubsub.subscribe(*channels_to_sub)
-                message = pubsub.get_message(timeout=5.0)
-                if not message:
-                    continue
-                channel = message['channel']
-                if isinstance(channel, bytes):
-                    channel = channel.decode('utf-8')
-                if message['type'] == 'subscribe':
-                    active_subs = message['data']
-                    self._log(f"📡 [Redis ACK] Successfully subscribed to: {channel} (Active connection subs: {active_subs})")
-                    continue
-                if message['type'] != 'message':
-                    continue
-                raw_data = message['data']
-                if isinstance(raw_data, bytes):
-                    raw_data = raw_data.decode('utf-8')
-                routing_key = channel_to_routing_key.get(channel)
-                if not routing_key:
-                    continue
-                try:
-                    data = json.loads(raw_data)
-                    query_result = VSQueryResult.model_validate(data['query_result'])
-                    query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
-                except Exception as parse_err:
-                    self._log(f"Data parsing error for channel {channel}: {parse_err}")
-                    continue
+                time.sleep(1.0)
                 now = time.time()
                 now = time.time()
-                matching_tasks = []
-                with self.m_lock:
-                    for task in self.m_tasks:
-                        if now < task.next_run or not task.book_allowed:
-                            continue
-                        if routing_key not in task.acceptable_routing_keys:
-                            continue   
-                        task.next_run = now + self.m_cfg.booker.booking_cooldown
-                        matching_tasks.append(task)
-                if matching_tasks:
-                    for task in matching_tasks:
-                        self._log(f"🚀 Triggering BOOK for {routing_key} | Order Ref: {task.task_ref}")
-                        t = threading.Thread(
-                            target=self._execute_book_job, 
-                            args=(task, query_result), 
-                            daemon=True
-                        )
-                        t.start()
-            except Exception as e:
-                self._log(f"Trigger loop pub/sub error: {e}")
-                if pubsub:
+                for apt_type in self.m_cfg.appointment_types:
+                    redis_key = self._get_redis_key(apt_type.routing_key)
+                    raw_data = self.redis_client.get(redis_key)
+                    if not raw_data:
+                        continue
                     try:
                     try:
-                        pubsub.close()
-                    except:
-                        pass
-                    pubsub = None
+                        data = json.loads(raw_data)
+                        query_result = VSQueryResult.model_validate(data['query_result'])
+                        query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
+                    except Exception as parse_err:
+                        self._log(f"Data parsing error for {redis_key}: {parse_err}. Deleting corrupted signal.")
+                        self.redis_client.delete(redis_key)
+                        continue
+                    
+                    matching_tasks = []
+                    with self.m_lock:
+                        for task in self.m_tasks:
+                            if now < task.next_run or not task.book_allowed:
+                                continue
+                            if apt_type.routing_key not in task.acceptable_routing_keys:
+                                continue
+                            
+                            task.next_run = now + self.m_cfg.booker.booking_cooldown
+                            matching_tasks.append(task)
+                            
+                    if matching_tasks:
+                        threads = []
+                        for task in matching_tasks:
+                            self._log(f"🚀 Triggering BOOK for {apt_type.routing_key} | Order Ref: {task.task_ref}")
+                            t = threading.Thread(target=self._execute_book_job, args=(task, query_result))
+                            threads.append(t)
+                            t.start()
+                        
+                        for t in threads:
+                            t.join() 
+                    
+            except Exception as e:
+                self._log(f"Trigger loop error: {e}")
                 time.sleep(2)
                 time.sleep(2)
-        if pubsub:
-            pubsub.close()
-            self._log("Pub/Sub connection closed.")
 
 
     def _execute_book_job(self, task: Task, query_result: VSQueryResult):
     def _execute_book_job(self, task: Task, query_result: VSQueryResult):
         queue_name = f"auto.{query_result.apt_type.routing_key}"
         queue_name = f"auto.{query_result.apt_type.routing_key}"
@@ -230,7 +188,7 @@ class BuiltinBookerGCO:
                 return 
                 return 
             task_id = task_data['id']
             task_id = task_data['id']
             order_id = task_data.get('order_id')
             order_id = task_data.get('order_id')
-            self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + self.heartbeat_ttl})
+            self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + self.heartbeat_ttl})
                         
                         
             user_input = task_data.get('user_inputs', {})
             user_input = task_data.get('user_inputs', {})
             book_res = task.instance.book(query_result, user_input)
             book_res = task.instance.book(query_result, user_input)
@@ -261,7 +219,7 @@ class BuiltinBookerGCO:
                     f"━━━━━━━━━━━━━━━\n"
                     f"━━━━━━━━━━━━━━━\n"
                 )
                 )
                 VSCloudApi.Instance().push_weixin_text(push_content)
                 VSCloudApi.Instance().push_weixin_text(push_content)
-                self.redis_com.zrem(self.m_tracker_key, task_id)
+                self.redis_client.zrem(self.m_tracker_key, task_id)
                 
                 
                 # === 核心:成功次数判断 ===
                 # === 核心:成功次数判断 ===
                 task.successful_bookings += 1
                 task.successful_bookings += 1
@@ -294,11 +252,11 @@ class BuiltinBookerGCO:
                         
                         
                     t_cd = self.task_backoff.calculate(t_fails)
                     t_cd = self.task_backoff.calculate(t_fails)
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
+                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
             
             
         finally:
         finally:
            if not booking_success and task_id is not None and not is_rate_limited:
            if not booking_success and task_id is not None and not is_rate_limited:
-                self.redis_com.zadd(self.m_tracker_key, {str(task_id): 0})
+                self.redis_client.zadd(self.m_tracker_key, {str(task_id): 0})
                 self._log(f"♻️ Task={task_id} normal failure. Instantly handed over to Sweeper.")
                 self._log(f"♻️ Task={task_id} normal failure. Instantly handed over to Sweeper.")
                 
                 
     def _creator_loop(self):
     def _creator_loop(self):
@@ -307,7 +265,7 @@ class BuiltinBookerGCO:
         group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
         group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
         while not self.m_stop_event.is_set():
         while not self.m_stop_event.is_set():
             time.sleep(2.0)
             time.sleep(2.0)
-            if self.redis_com.exists(group_cd_key):
+            if self.redis_client.exists(group_cd_key):
                 continue
                 continue
             with self.m_lock:
             with self.m_lock:
                 current = len(self.m_tasks)
                 current = len(self.m_tasks)
@@ -361,12 +319,12 @@ class BuiltinBookerGCO:
                             acceptable_routing_keys=all_keys,
                             acceptable_routing_keys=all_keys,
                             source_queue="built-in",
                             source_queue="built-in",
                             book_allowed=True,
                             book_allowed=True,
-                            next_remote_ping = time.time() + random.randint(180, 300) 
+                            next_remote_ping = time.time() + random.randint(60, 180) 
                         )
                         )
                     )
                     )
                     
                     
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    self.redis_com.delete(group_fail_key)
+                    self.redis_client.delete(group_fail_key)
                     
                     
                 self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
                 self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
             except Exception as e:
             except Exception as e:
@@ -390,11 +348,11 @@ class BuiltinBookerGCO:
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     
                     
                     # 更新全局(机器组)失败次数
                     # 更新全局(机器组)失败次数
-                    g_fails = self.redis_com.incr(group_fail_key)
+                    g_fails = self.redis_client.incr(group_fail_key)
                     # 计算退避时间
                     # 计算退避时间
                     g_cd = self.group_backoff.calculate(g_fails)
                     g_cd = self.group_backoff.calculate(g_fails)
                     # 设置 Redis 全局冷却保护阀
                     # 设置 Redis 全局冷却保护阀
-                    self.redis_com.set(group_cd_key, "1", ex=int(g_cd))
+                    self.redis_client.set(group_cd_key, "1", ex=int(g_cd))
                     self._log(f"📉 [Rate Limited] Group '{self.m_cfg.identifier}' failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
                     self._log(f"📉 [Rate Limited] Group '{self.m_cfg.identifier}' failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
 
 
             finally:
             finally:

+ 51 - 95
booker_order.py

@@ -27,19 +27,7 @@ class OrderBookerGCO:
         self.m_lock = threading.RLock()
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
         self.m_stop_event = threading.Event()
 
 
-        redis_common_kwargs = {
-            **redis_conf,
-            "socket_timeout": 5,
-            "socket_connect_timeout": 5,
-            # 会自动发送 PING
-            "health_check_interval": 15,
-            # TCP KeepAlive
-            "socket_keepalive": True,
-            "retry_on_timeout": True,
-            "decode_responses": False,
-        }
-        self.redis_com = redis.Redis(**redis_common_kwargs)
-        self.redis_sub = redis.Redis(**redis_common_kwargs)
+        self.redis_client = redis.Redis(**redis_conf)
         self.m_pending_order_by_queue: Dict[str, int] = {}        
         self.m_pending_order_by_queue: Dict[str, int] = {}        
         self.m_last_spawn_times: Dict[str, float] = {}
         self.m_last_spawn_times: Dict[str, float] = {}
         self.m_task_data_cache: Dict[str, dict] = {}
         self.m_task_data_cache: Dict[str, dict] = {}
@@ -145,7 +133,7 @@ class OrderBookerGCO:
             
             
             if healthy_tasks:
             if healthy_tasks:
                 try:
                 try:
-                    pipeline = self.redis_com.pipeline()
+                    pipeline = self.redis_client.pipeline()
                     new_deadline = time.time() + self.heartbeat_ttl
                     new_deadline = time.time() + self.heartbeat_ttl
                     for t in healthy_tasks:
                     for t in healthy_tasks:
                         if t.task_ref is not None:
                         if t.task_ref is not None:
@@ -157,7 +145,7 @@ class OrderBookerGCO:
 
 
             if dead_tasks:
             if dead_tasks:
                 try:
                 try:
-                    pipeline = self.redis_com.pipeline()
+                    pipeline = self.redis_client.pipeline()
                     for t in dead_tasks:
                     for t in dead_tasks:
                         if t.task_ref is not None:
                         if t.task_ref is not None:
                             pipeline.zadd(self.m_tracker_key, {str(t.task_ref): 0})
                             pipeline.zadd(self.m_tracker_key, {str(t.task_ref): 0})
@@ -204,80 +192,48 @@ class OrderBookerGCO:
                 time.sleep(0.5)
                 time.sleep(0.5)
 
 
     def _booking_trigger_loop(self):
     def _booking_trigger_loop(self):
-        self._log("Pub/Sub Trigger loop started.")
-        channel_to_routing_key = {}
-        for apt in self.m_cfg.appointment_types:
-            channel = self._get_redis_key(apt.routing_key)
-            channel_to_routing_key[channel] = apt.routing_key
-            
-        if not channel_to_routing_key:
-            self._log("No appointment types configured. Exiting trigger loop.")
-            return
-            
-        pubsub = None
+        self._log("Trigger loop started.")
         while not self.m_stop_event.is_set():
         while not self.m_stop_event.is_set():
             try:
             try:
-                if pubsub is None:
-                    pubsub = self.redis_sub.pubsub(ignore_subscribe_messages=False)
-                    channels_to_sub = list(channel_to_routing_key.keys())
-                    self._log(f"⏳ Sending SUBSCRIBE command to Redis for: {channels_to_sub}")
-                    pubsub.subscribe(*channels_to_sub)
-                message = pubsub.get_message(timeout=5.0)
-                if not message:
-                    continue
-                channel = message['channel']
-                if isinstance(channel, bytes):
-                    channel = channel.decode('utf-8')
-                if message['type'] == 'subscribe':
-                    active_subs = message['data']
-                    self._log(f"📡 [Redis ACK] Successfully subscribed to: {channel} (Active connection subs: {active_subs})")
-                    continue
-                if message['type'] != 'message':
-                    continue
-                raw_data = message['data']
-                if isinstance(raw_data, bytes):
-                    raw_data = raw_data.decode('utf-8')
-                routing_key = channel_to_routing_key.get(channel)
-                if not routing_key:
-                    continue
-                try:
-                    data = json.loads(raw_data)
-                    query_result = VSQueryResult.model_validate(data['query_result'])
-                    query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
-                except Exception as parse_err:
-                    self._log(f"Data parsing error for channel {channel}: {parse_err}")
-                    continue
+                time.sleep(1.0)
                 now = time.time()
                 now = time.time()
-                matching_tasks = []
-                with self.m_lock:
-                    for task in self.m_tasks:
-                        if now < task.next_run or not task.book_allowed:
-                            continue
-                        if routing_key not in task.acceptable_routing_keys:
-                            continue   
-                        task.next_run = now + self.m_cfg.booker.booking_cooldown
-                        matching_tasks.append(task)
-                if matching_tasks:
-                    for task in matching_tasks:
-                        self._log(f"🚀 Triggering BOOK for {routing_key} | Order Ref: {task.task_ref}")
-                        t = threading.Thread(
-                            target=self._execute_book_job, 
-                            args=(task, query_result), 
-                            daemon=True
-                        )
-                        t.start()
-            except Exception as e:
-                self._log(f"Trigger loop pub/sub error: {e}")
-                if pubsub:
+                for apt_type in self.m_cfg.appointment_types:
+                    redis_key = self._get_redis_key(apt_type.routing_key)
+                    raw_data = self.redis_client.get(redis_key)
+                    if not raw_data:
+                        continue
                     try:
                     try:
-                        pubsub.close()
-                    except:
-                        pass
-                    pubsub = None
+                        data = json.loads(raw_data)
+                        query_result = VSQueryResult.model_validate(data['query_result'])
+                        query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
+                    except Exception as parse_err:
+                        self._log(f"Data parsing error for {redis_key}: {parse_err}. Deleting corrupted signal.")
+                        self.redis_client.delete(redis_key)
+                        continue
+                    
+                    matching_tasks = []
+                    with self.m_lock:
+                        for task in self.m_tasks:
+                            if now < task.next_run or not task.book_allowed:
+                                continue
+                            if apt_type.routing_key not in task.acceptable_routing_keys:
+                                continue
+                            
+                            task.next_run = now + self.m_cfg.booker.booking_cooldown
+                            matching_tasks.append(task)
+                            
+                    if matching_tasks:
+                        threads = []
+                        for task in matching_tasks:
+                            self._log(f"🚀 Triggering BOOK for {apt_type.routing_key} | Order Ref: {task.task_ref}")
+                            t = threading.Thread(target=self._execute_book_job, args=(task, query_result))
+                            threads.append(t)
+                            t.start()
+                        for t in threads:
+                            t.join() 
+            except Exception as e:
+                self._log(f"Trigger loop error: {e}")
                 time.sleep(2)
                 time.sleep(2)
-        if pubsub:
-            pubsub.close()
-            self._log("Pub/Sub connection closed.")
 
 
     def _execute_book_job(self, task: Task, query_result: VSQueryResult):
     def _execute_book_job(self, task: Task, query_result: VSQueryResult):
         task_id = task.task_ref
         task_id = task.task_ref
@@ -297,7 +253,7 @@ class OrderBookerGCO:
             if not task_data or task_data.get('status') in ['grabbed', 'pause', 'completed', 'cancelled']:
             if not task_data or task_data.get('status') in ['grabbed', 'pause', 'completed', 'cancelled']:
                 self._log(f"Bound Task={task_id} is no longer valid or already processed. Removing instance.")
                 self._log(f"Bound Task={task_id} is no longer valid or already processed. Removing instance.")
                 self._remove_task(task, "bound task no longer valid")
                 self._remove_task(task, "bound task no longer valid")
-                self.redis_com.zrem(self.m_tracker_key, task_id)
+                self.redis_client.zrem(self.m_tracker_key, task_id)
                 return
                 return
             
             
             order_id = task_data.get('order_id')
             order_id = task_data.get('order_id')
@@ -336,7 +292,7 @@ class OrderBookerGCO:
                 
                 
                 ThreadPool.getInstance().enqueue(_update_cloud_success)
                 ThreadPool.getInstance().enqueue(_update_cloud_success)
 
 
-                self.redis_com.zrem(self.m_tracker_key, task_id)
+                self.redis_client.zrem(self.m_tracker_key, task_id)
                 self._remove_task(task, "booking success")
                 self._remove_task(task, "booking success")
             else:
             else:
                 self._log(f"❌ BOOK FAILED for Order: {order_id}. Will retry on next signal.")
                 self._log(f"❌ BOOK FAILED for Order: {order_id}. Will retry on next signal.")
@@ -363,7 +319,7 @@ class OrderBookerGCO:
                     ThreadPool.getInstance().enqueue(_update_cloud_meta)   
                     ThreadPool.getInstance().enqueue(_update_cloud_meta)   
                     t_cd = self.task_backoff.calculate(t_fails)
                     t_cd = self.task_backoff.calculate(t_fails)
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
                     self._log(f"⏳ Task={task_id} (Booking Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
+                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
 
 
     def _creator_loop(self):
     def _creator_loop(self):
         self._log("Creator loop started.")
         self._log("Creator loop started.")
@@ -375,7 +331,7 @@ class OrderBookerGCO:
                 r_key = apt.routing_key
                 r_key = apt.routing_key
                 
                 
                 queue_cd_key = f"vs:queue:cooldown:{r_key}"
                 queue_cd_key = f"vs:queue:cooldown:{r_key}"
-                if self.redis_com.exists(queue_cd_key):
+                if self.redis_client.exists(queue_cd_key):
                     continue
                     continue
                 
                 
                 with self.m_lock:
                 with self.m_lock:
@@ -409,7 +365,7 @@ class OrderBookerGCO:
                 with self.m_lock:
                 with self.m_lock:
                     self.m_task_data_cache[str(task_id)] = task_data
                     self.m_task_data_cache[str(task_id)] = task_data
                 
                 
-                self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + 5*60.0})
+                self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + 5*60.0})
                 user_inputs = task_data.get('user_inputs', {})
                 user_inputs = task_data.get('user_inputs', {})
                 
                 
                 plg_cfg = VSPlgConfig()
                 plg_cfg = VSPlgConfig()
@@ -446,11 +402,11 @@ class OrderBookerGCO:
                             acceptable_routing_keys=acceptable_keys, 
                             acceptable_routing_keys=acceptable_keys, 
                             source_queue=target_routing_key,
                             source_queue=target_routing_key,
                             book_allowed=True,
                             book_allowed=True,
-                            next_remote_ping=time.time() + random.randint(180, 300) 
+                            next_remote_ping=time.time() + random.randint(60, 180) 
                         )
                         )
                     )
                     )
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
-                    self.redis_com.delete(queue_fail_key)                    
+                    self.redis_client.delete(queue_fail_key)                    
                 success = True
                 success = True
                 self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
                 self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
             except Exception as e:
             except Exception as e:
@@ -473,9 +429,9 @@ class OrderBookerGCO:
                     is_rate_limited = True
                     is_rate_limited = True
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"
                     queue_cd_key = f"vs:queue:cooldown:{target_routing_key}"
                     queue_cd_key = f"vs:queue:cooldown:{target_routing_key}"
-                    q_fails = self.redis_com.incr(queue_fail_key)
+                    q_fails = self.redis_client.incr(queue_fail_key)
                     q_cd = self.queue_backoff.calculate(q_fails)
                     q_cd = self.queue_backoff.calculate(q_fails)
-                    self.redis_com.set(queue_cd_key, "1", ex=int(q_cd))
+                    self.redis_client.set(queue_cd_key, "1", ex=int(q_cd))
                     self._log(f"📉 [Rate Limited] Queue '{target_routing_key}' failed {q_fails} times. Global Backoff: {q_cd:.1f}s.")
                     self._log(f"📉 [Rate Limited] Queue '{target_routing_key}' failed {q_fails} times. Global Backoff: {q_cd:.1f}s.")
                     if task_id is not None:
                     if task_id is not None:
                         task_meta = task_data.get('meta') or {}
                         task_meta = task_data.get('meta') or {}
@@ -489,13 +445,13 @@ class OrderBookerGCO:
                         
                         
                         t_cd = self.account_backoff.calculate(t_fails)
                         t_cd = self.account_backoff.calculate(t_fails)
                         self._log(f"⏳ Task={task_id} (Attempt {t_fails}) suspended for {t_cd:.1f}s.")
                         self._log(f"⏳ Task={task_id} (Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                        self.redis_com.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})       
+                        self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})       
             finally:
             finally:
                 with self.m_lock: 
                 with self.m_lock: 
                     self.m_pending_order_by_queue[target_routing_key] = max(0, self.m_pending_order_by_queue[target_routing_key] - 1)
                     self.m_pending_order_by_queue[target_routing_key] = max(0, self.m_pending_order_by_queue[target_routing_key] - 1)
                 
                 
                 if not success and task_id is not None and not is_rate_limited:
                 if not success and task_id is not None and not is_rate_limited:
-                    self.redis_com.zadd(self.m_tracker_key, {str(task_id): 0})
+                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): 0})
                     self._log(f"♻️ Task={task_id} failed normal spawn. Instantly handed over to Sweeper.")
                     self._log(f"♻️ Task={task_id} failed normal spawn. Instantly handed over to Sweeper.")
                     
                     
                     with self.m_lock:
                     with self.m_lock:

+ 2 - 2
configure.py

@@ -60,6 +60,6 @@ TEST_PROXY = None                        # 测试代理,这里配置以后ip
 CHROME_PATH = None                       # Chrome bin 的路径, 这个优先级最高,其次CHROME_BIN 的环境变量,最后系统默认值
 CHROME_PATH = None                       # Chrome bin 的路径, 这个优先级最高,其次CHROME_BIN 的环境变量,最后系统默认值
 
 
 CLASH_SWITCH_NODE = False                # 是否要启用Clash 轮换
 CLASH_SWITCH_NODE = False                # 是否要启用Clash 轮换
-CLASH_API_URL = "http://127.0.0.1:9097"  # Clash 本地 API
+CLASH_API_URL = "http://127.0.0.1:9090"  # Clash 本地 API
 CLASH_API_KEY = "esZnx8"                 # Clash API 密钥
 CLASH_API_KEY = "esZnx8"                 # Clash API 密钥
-CLASH_GROUP_NAME = "♻️ 手动切换"           # 要轮换的策略组名称
+CLASH_GROUP_NAME = "♻️ TLS-A"             # 要轮换的策略组名称 ♻️ TLS-A ♻️ TLS-B ♻️ TLS-C

+ 145 - 148
plugins/tls_plugin.py

@@ -263,164 +263,161 @@ class TlsPlugin(IVSPlg):
             has_submitted_login = False
             has_submitted_login = False
             
             
             for step in range(max_steps):
             for step in range(max_steps):
-                try:
-                    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
+                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...")
                     
                     
-                    # 状态 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]
                         
                         
-                        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)
+                        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)) 
+                    username = self.config.account.username
+                    password = self.config.account.password
                     
                     
-                        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
+                    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)
                     
                     
-                    # 状态 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 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}")
+                    # 注入 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}")
                     
                     
-                        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(f"[WARN] Wait timeout for group button {formgroup_id}")
+                        # 注入 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))
                     
                     
-                    # 状态 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
+                    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)
                     
                     
-                    # 状态 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
+                    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}'
                     
                     
-                    self._log("State: Transitioning or Unknown. Waiting 2 seconds...")
+                    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(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)
                     time.sleep(2)
-                except Exception as e:
-                    self._log(f'Step {step} exception: {e}')
+                    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 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}")

+ 6 - 7
sentinel.py

@@ -21,8 +21,7 @@ class SentinelGCO:
         self.m_lock = threading.RLock()
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
         self.m_stop_event = threading.Event()
         
         
-        self.redis_com = redis.Redis(**redis_conf)
-        self.redis_pub = redis.Redis(**redis_conf)
+        self.redis_client = redis.Redis(**redis_conf)
         self.m_pending_builtin = 0
         self.m_pending_builtin = 0
         
         
         # 1. 全局建连退避:起步 1 分钟,封顶 1 小时 (保护登录接口)
         # 1. 全局建连退避:起步 1 分钟,封顶 1 小时 (保护登录接口)
@@ -154,7 +153,7 @@ class SentinelGCO:
                                 "timestamp": now
                                 "timestamp": now
                             }
                             }
                             redis_key = self._get_redis_key(apt_type.routing_key)
                             redis_key = self._get_redis_key(apt_type.routing_key)
-                            self.redis_pub.publish(redis_key, json.dumps(payload))
+                            self.redis_client.setex(redis_key, ttl, json.dumps(payload))
                             payload["query_result"]["website"] = self.m_cfg.website
                             payload["query_result"]["website"] = self.m_cfg.website
                             VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
                             VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
                         VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
                         VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
@@ -173,7 +172,7 @@ class SentinelGCO:
             time.sleep(2)
             time.sleep(2)
             with self.m_lock:
             with self.m_lock:
                 
                 
-                if self.redis_com.exists(group_cd_key):
+                if self.redis_client.exists(group_cd_key):
                     continue
                     continue
                 
                 
                 current = len(self.m_tasks)
                 current = len(self.m_tasks)
@@ -232,7 +231,7 @@ class SentinelGCO:
                         Task(instance=instance,qw_cfg=self.m_cfg.query_wait,next_run=time.time(), book_allowed=False))
                         Task(instance=instance,qw_cfg=self.m_cfg.query_wait,next_run=time.time(), book_allowed=False))
                 
                 
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    self.redis_com.delete(group_fail_key)
+                    self.redis_client.delete(group_fail_key)
                 
                 
                 success = True
                 success = True
                 self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
                 self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
@@ -257,9 +256,9 @@ class SentinelGCO:
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
                     group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
                     
                     
-                    g_fails = self.redis_com.incr(group_fail_key)
+                    g_fails = self.redis_client.incr(group_fail_key)
                     g_cd = self.group_backoff.calculate(g_fails)
                     g_cd = self.group_backoff.calculate(g_fails)
-                    self.redis_com.set(group_cd_key, "1", ex=int(g_cd))
+                    self.redis_client.set(group_cd_key, "1", ex=int(g_cd))
                     self._log(f"📉 [Rate Limited] Sentinel Spawn failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
                     self._log(f"📉 [Rate Limited] Sentinel Spawn failed {g_fails} times. Global Backoff: {g_cd:.1f}s.")
                     
                     
             finally:
             finally:

+ 6 - 9
test/test_publish_slot.py

@@ -10,8 +10,8 @@ r = redis.Redis(
     decode_responses=True
     decode_responses=True
 )
 )
 
 
-# topic/channel
-channel = "vs:signal:slot.lon.fr.tourist"
+# 使用的键名(原 Channel 名称)
+key = "vs:signal:slot.lon.fr.tourist"
 
 
 # 消息体
 # 消息体
 message = {
 message = {
@@ -47,11 +47,8 @@ message = {
     "timestamp": 1777745853
     "timestamp": 1777745853
 }
 }
 
 
-# 发布
-receiver_count = r.publish(
-    channel,
-    json.dumps(message, ensure_ascii=False)
-)
+# 写入 Redis 键,有效期 30 秒
+r.setex(key, 30, json.dumps(message, ensure_ascii=False))
 
 
-print(f"发布成功 -> {channel}")
-print(f"收到订阅者数量: {receiver_count}")
+print(f"写入成功 -> 键: {key}")
+print(f"有效期: 30 秒")

+ 6 - 6
toolkit/vs_cloud_api.py

@@ -36,13 +36,13 @@ class VSCloudApi:
             "Accept": "application/json, text/plain, */*"
             "Accept": "application/json, text/plain, */*"
         }
         }
         
         
-    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, timeout=5*60):
         """
         """
         统一 HTTP 请求封装,严格复刻 C++ 逻辑:
         统一 HTTP 请求封装,严格复刻 C++ 逻辑:
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
+        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=timeout)
         VSC_DEBUG('vs_cloud', f'[perform request] {method} {url} {data} {json_data} {params} {resp.text}')
         VSC_DEBUG('vs_cloud', f'[perform request] {method} {url} {data} {json_data} {params} {resp.text}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
@@ -65,7 +65,7 @@ class VSCloudApi:
 
 
         headers = self._get_headers()
         headers = self._get_headers()
         
         
-        resp = self._perform_request('GET', url, params=params, headers=headers)
+        resp = self._perform_request('GET', url, params=params, headers=headers, timeout=10)
         result = resp.json()
         result = resp.json()
         if result.get("code") == 0:
         if result.get("code") == 0:
             return result.get("data", {})
             return result.get("data", {})
@@ -76,7 +76,7 @@ class VSCloudApi:
         url = f"{self.base_url}/api/vas/task/detail"
         url = f"{self.base_url}/api/vas/task/detail"
         params = {"task_id": task_id}
         params = {"task_id": task_id}
         headers = self._get_headers()
         headers = self._get_headers()
-        resp = self._perform_request('GET', url, params=params, headers=headers)
+        resp = self._perform_request('GET', url, params=params, headers=headers, timeout=10)
         result = resp.json()
         result = resp.json()
         if result.get("code") == 0:
         if result.get("code") == 0:
             return result.get("data", {})
             return result.get("data", {})
@@ -316,7 +316,7 @@ class VSCloudApi:
             "account_cd": account_cd
             "account_cd": account_cd
         }
         }
         headers = self._get_headers()
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         result = resp.json()
         if result.get("code") == 0:
         if result.get("code") == 0:
             return result.get("data", {})
             return result.get("data", {})
@@ -339,7 +339,7 @@ class VSCloudApi:
             "proxy_cd": proxy_cd
             "proxy_cd": proxy_cd
         }
         }
         headers = self._get_headers()
         headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload, timeout=10)
         result = resp.json()
         result = resp.json()
         if result.get("code") == 0:
         if result.get("code") == 0:
             return result.get("data", {})
             return result.get("data", {})

+ 11 - 8
utils/cloudflare_bypass_for_scraping.py

@@ -102,13 +102,16 @@ class CloudflareBypasser:
     def handle_waiting_room(self):
     def handle_waiting_room(self):
         wait_start = time.time()
         wait_start = time.time()
         while True:
         while True:
-            html = self.driver.html.lower()
-            if "file d'attente" in html or "waiting room" in html:
-                if time.time() - wait_start > 6 * 60:
-                    self.log_message("Waiting room timeout (1h).")
+            try:
+                html = self.driver.html.lower()
+                if "file d'attente" in html or "waiting room" in html:
+                    if time.time() - wait_start > 6 * 60:
+                        self.log_message("Waiting room timeout (1h).")
+                        break
+                    self.log_message("In Waiting Room... Waiting for auto-refresh.")
+                    time.sleep(10)
+                else:
                     break
                     break
-                self.log_message("In Waiting Room... Waiting for auto-refresh.")
-                time.sleep(10)
-            else:
-                break
+            except Exception as e:
+                self.log_message(f"Error checking html: {e}")