Hujiarui 2 ماه پیش
والد
کامیت
8bde71a77a
8فایلهای تغییر یافته به همراه360 افزوده شده و 499 حذف شده
  1. 71 126
      booker_builtin.py
  2. 69 159
      booker_order.py
  3. 65 59
      plugins/tls_plugin.py
  4. 9 6
      plugins/vfs_plugin.py
  5. 79 132
      sentinel.py
  6. 3 0
      toolkit/vs_cloud_api.py
  7. 58 8
      utils/fake_utils.py
  8. 6 9
      vs_types.py

+ 71 - 126
booker_builtin.py

@@ -9,7 +9,6 @@ from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentT
 from vs_plg_factory import VSPlgFactory 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
-from toolkit.backoff import ExponentialBackoff
 from utils.safe_redis_cli import SafeRedisClient
 
 
@@ -27,13 +26,7 @@ class BuiltinBookerGCO:
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
         self.redis_client = SafeRedisClient(redis_conf, self.m_logger)
-        self.m_pending_builtin = 0
-        
         self.m_tracker_key = f"vs:worker:tasks_tracker:{self.m_cfg.identifier}"
-        self.group_backoff = ExponentialBackoff(base_delay=60.0, max_delay=10*60.0, factor=2.0)
-        self.task_backoff = ExponentialBackoff(base_delay=5*60.0, max_delay=2*60*60.0, factor=2.0)
-        self.m_last_spawn_time = 0.0
-        self.heartbeat_ttl = 300
 
     def _log(self, message):
         if self.m_logger:
@@ -49,7 +42,6 @@ class BuiltinBookerGCO:
         class_name = "".join(part.title() for part in plugin_name.split('_'))
         plugin_path = os.path.join(self.m_cfg.plugin_config.lib_path, self.m_cfg.plugin_config.plugin_bin)
         self.m_factory.register_plugin(plugin_name, plugin_path, class_name)
-
         threading.Thread(target=self._booking_trigger_loop, daemon=True).start()
         threading.Thread(target=self._creator_loop, daemon=True).start()
         threading.Thread(target=self._maintain_loop, daemon=True).start()
@@ -120,11 +112,9 @@ class BuiltinBookerGCO:
                         t.instance.keep_alive()
                         if t.instance.health_check():
                             healthy_tasks.append(t)
-                            next_delay = random.randint(60, 180) 
-                            t.next_remote_ping = now + next_delay
+                            t.next_remote_ping = now + random.gauss(self.m_cfg.booker.keep_alive, 5)
                         else:
                             dead_tasks.append(t)
-                            self._log(f"♻️ Instance unhealthy. Will be removed.")
                     else:
                         healthy_tasks.append(t)
                 
@@ -192,7 +182,6 @@ class BuiltinBookerGCO:
         task_id = None
         task_data = None
         booking_success = False
-        is_rate_limited = False
         
         try:
             task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
@@ -200,7 +189,7 @@ class BuiltinBookerGCO:
                 return 
             task_id = task_data['id']
             order_id = task_data.get('order_id')
-            self.redis_client.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() + 30.0})
                         
             user_input = task_data.get('user_inputs', {})
             book_res = task.instance.book(query_result, user_input)
@@ -218,22 +207,28 @@ class BuiltinBookerGCO:
                     "timestamp": int(time.time()),
                     "payment_link": book_res.payment_link
                 }
-                VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
-                push_content = (
-                    f"🎉 【预定成功通知】\n"
-                    f"━━━━━━━━━━━━━━━\n"
-                    f"订单编号: {order_id}\n"
-                    f"预约账号: {book_res.account}\n"
-                    f"预约日期: {book_res.book_date}\n"
-                    f"预约时间: {book_res.book_time}\n"
-                    f"预约编号: {book_res.urn}\n"
-                    f"支付链接: {book_res.payment_link if book_res.payment_link else '无需支付/暂无'}\n"
-                    f"━━━━━━━━━━━━━━━\n"
-                )
-                VSCloudApi.Instance().push_weixin_text(push_content)
+                
+                def _update_cloud_success():
+                    try:
+                        VSCloudApi.Instance().update_vas_task(str(task_id), {"status": "grabbed", "grabbed_history": grab_info})
+                        push_content = (
+                            f"🎉 【预定成功通知】\n"
+                            f"━━━━━━━━━━━━━━━\n"
+                            f"订单编号: {order_id}\n"
+                            f"预约账号: {book_res.account}\n"
+                            f"预约日期: {book_res.book_date}\n"
+                            f"预约时间: {book_res.book_time}\n"
+                            f"预约编号: {book_res.urn}\n"
+                            f"支付链接: {book_res.payment_link if book_res.payment_link else '无需支付/暂无'}\n"
+                            f"━━━━━━━━━━━━━━━\n"
+                        )
+                        VSCloudApi.Instance().push_weixin_text(push_content)
+                    except Exception as e:
+                        self._log(f"Failed to update success state to cloud: {e}")
+
+                ThreadPool.getInstance().enqueue(_update_cloud_success)
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 
-                # === 核心:成功次数判断 ===
                 task.successful_bookings += 1
                 max_b = self.m_cfg.booker.max_bookings_per_account
                 if max_b > 0 and task.successful_bookings >= max_b:
@@ -241,7 +236,6 @@ class BuiltinBookerGCO:
                     self._remove_task(task, "max bookings reached")
             else:
                 self._log(f"❌ BOOK FAILED for Order: {order_id}")
-
         except Exception as e:
             err_str = str(e)
             self._log(f"Exception during booking: {err_str}")
@@ -250,119 +244,70 @@ class BuiltinBookerGCO:
                 "Rate limited" in err_str
             ]
             if any(rate_limited_indicators):
-                is_rate_limited = True
                 self._remove_task(task, "booking rate limited")
-                if task_data and task_id is not None:
-                    task_meta = task_data.get('meta') or {} 
-                    t_fails = task_meta.get('booking_failures', 0) + 1
-                    task_meta['booking_failures'] = t_fails
-                    
-                    def _update_cloud_meta():
-                        try:
-                            VSCloudApi.Instance().update_vas_task(str(task_id), {"meta": task_meta})
-                        except Exception as cloud_err:
-                            self._log(f"Failed to update task meta: {cloud_err}")
-                    ThreadPool.getInstance().enqueue(_update_cloud_meta) 
-                        
-                    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.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
-            
-        finally:
-           if not booking_success and task_id is not None and not is_rate_limited:
-                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.")
                 
     def _creator_loop(self):
         self._log("Creator loop started.")
-        spawn_interval = 10.0
-        group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
         while not self.m_stop_event.is_set():
             try:
                 time.sleep(1.0)
-                if self.redis_client.exists(group_cd_key):
-                    continue
                 with self.m_lock:
                     current = len(self.m_tasks)
-                    pending = self.m_pending_builtin
                     target = self.m_cfg.booker.target_instances
-                if (current + pending) < target:
-                    now = time.time()
-                    if now - self.m_last_spawn_time >= spawn_interval:
-                        self.m_last_spawn_time = now 
-                        self._spawn_worker()
+                if current < target:
+                    self._spawn_worker()
             except Exception as e:
                 self._log(f'Creator loop exception: {e}')
 
     def _spawn_worker(self):
-        with self.m_lock:
-            self.m_pending_builtin += 1
-            
-        def _job():
-            try:
-                plg_cfg = VSPlgConfig()
-                plg_cfg.debug = self.m_cfg.debug
-                plg_cfg.free_config = self.m_cfg.free_config
-                plg_cfg.session_max_life = self.m_cfg.session_max_life
+        instance = None
+        success = False
+        plg_cfg = None
+        
+        try:
+            plg_cfg = VSPlgConfig()
+            plg_cfg.debug = self.m_cfg.debug
+            plg_cfg.free_config = self.m_cfg.free_config
+            plg_cfg.session_max_life = self.m_cfg.session_max_life
 
-                if self.m_cfg.need_account:
-                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.booker.account_pool_id, self.m_cfg.booker.account_cd)
-                    plg_cfg.account = type(plg_cfg.account)(**acc)
+            if self.m_cfg.need_account:
+                acc = VSCloudApi.Instance().get_next_account(self.m_cfg.booker.account_pool_id, self.m_cfg.booker.account_cd)
+                plg_cfg.account = type(plg_cfg.account)(**acc)
 
-                if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
-                    plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
+            if self.m_cfg.need_proxy:
+                proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
+                plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
 
-                instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
-                instance.set_log(self.m_logger)
-                instance.set_config(plg_cfg)
-                instance.create_session()
-                
-                with self.m_lock:
-                    all_keys = [apt.routing_key for apt in self.m_cfg.appointment_types]
-                    self.m_tasks.append(
-                        Task(
-                            instance=instance,
-                            qw_cfg=self.m_cfg.query_wait,
-                            next_run=time.time(), 
-                            task_ref=None,
-                            acceptable_routing_keys=all_keys,
-                            source_queue="built-in",
-                            book_allowed=True,
-                            next_remote_ping = time.time() + random.randint(60, 180) 
-                        )
+            instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
+            instance.set_log(self.m_logger)
+            instance.set_config(plg_cfg)
+            instance.create_session()
+            
+            with self.m_lock:
+                all_keys = [apt.routing_key for apt in self.m_cfg.appointment_types]
+                self.m_tasks.append(
+                    Task(
+                        instance=instance,
+                        next_run=time.time(), 
+                        task_ref=None,
+                        acceptable_routing_keys=all_keys,
+                        source_queue="built-in",
+                        book_allowed=True,
+                        next_remote_ping = time.time() + random.gauss(self.m_cfg.booker.keep_alive, 5) 
                     )
-                    
-                    group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    self.redis_client.delete(group_fail_key)
-                    
-                self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
-            except Exception as e:
-                err_str = str(e)
-                resource_not_found_indicators = [
-                    "40401" in err_str,
-                    "Account not found" in err_str,
-                    "Proxy not found" in err_str,
-                ]
-                if any(resource_not_found_indicators):
-                    return
-                
-                self._log(f"Spawn failed: {e}")
-                
-                rate_limited_indicators = [
-                    "42901" in err_str,
-                    "Rate limited" in err_str
-                ]
-                if any(rate_limited_indicators):
-                    group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
-                    
-                    g_fails = self.redis_client.incr(group_fail_key)
-                    g_cd = self.group_backoff.calculate(g_fails)
-                    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.")
-
-            finally:
-                with self.m_lock:
-                    self.m_pending_builtin = max(0, self.m_pending_builtin - 1)
-        ThreadPool.getInstance().enqueue(_job)
+                )
+            self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
+        except Exception as e:
+            err_str = str(e)
+            self._log(f"Spawn failed: {err_str}")
+            rate_limited_indicators = [
+                "42901" in err_str,
+                "Rate limited" in err_str
+            ]
+            if any(rate_limited_indicators):
+                if plg_cfg and plg_cfg.account.username != "Guest":
+                    VSCloudApi.lock_account(plg_cfg.account.id, self.m_cfg.login_backoff)
+        finally:
+            if not success:
+                if instance:
+                    instance.cleanup()

+ 69 - 159
booker_order.py

@@ -3,14 +3,12 @@ import time
 import json
 import threading
 import random
-from datetime import datetime
-from typing import List, Dict, Callable, Any
+from typing import List, Dict, Callable
 
 from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType, AvailabilityStatus
 from vs_plg_factory import VSPlgFactory 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
-from toolkit.backoff import ExponentialBackoff
 from utils.safe_redis_cli import SafeRedisClient
 
 
@@ -28,17 +26,9 @@ class OrderBookerGCO:
         self.m_tasks: List[Task] = []
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
-
         self.redis_client = SafeRedisClient(redis_conf, self.m_logger)
-        
-        self.m_pending_order_by_queue: Dict[str, int] = {}        
-        self.m_last_spawn_times: Dict[str, float] = {}
         self.m_task_data_cache: Dict[str, dict] = {}
-        
         self.m_tracker_key = f"vs:worker:tasks_tracker:{self.m_cfg.identifier}"
-        self.queue_backoff = ExponentialBackoff(base_delay=1*60.0, max_delay=10*60.0, factor=2.0)
-        self.account_backoff = ExponentialBackoff(base_delay=5*60.0, max_delay=2*60*60.0, factor=2.0)
-        self.task_backoff = ExponentialBackoff(base_delay=10, max_delay=30*60.0, factor=2.0)
         self.heartbeat_ttl = 2*60.0
 
     def _log(self, message):
@@ -131,25 +121,20 @@ class OrderBookerGCO:
                         t.instance.keep_alive()
                         if t.instance.health_check(): 
                             healthy_tasks.append(t)
-                            next_delay = random.randint(55, 65) 
-                            t.next_remote_ping = now + next_delay
-                            self._log(f"🛡️ Task={t.task_ref} keep-alive success. Next ping in {next_delay}s.")
+                            t.next_remote_ping = now + random.gauss(self.m_cfg.booker.keep_alive, 5)
                         else:
                             dead_tasks.append(t)
-                            self._log(f"♻️ Instance for task={t.task_ref} unhealthy.")
                     else:
                         healthy_tasks.append(t)
                 
                 if healthy_tasks:
                     new_deadline = time.time() + self.heartbeat_ttl
-                    mapping = {str(t.task_ref): new_deadline for t in healthy_tasks if t.task_ref is not None}
+                    mapping = {str(t.task_ref): new_deadline for t in healthy_tasks}
                     self.redis_client.bulk_zadd(self.m_tracker_key, mapping)
-                    # self._log(f"💓 Heartbeat sent. Renewed {len(healthy_tasks)} tasks.")
 
                 if dead_tasks:
-                    mapping = {str(t.task_ref): 0 for t in dead_tasks if t.task_ref is not None}
+                    mapping = {str(t.task_ref): 0 for t in dead_tasks}
                     self.redis_client.bulk_zadd(self.m_tracker_key, mapping)
-                    self._log(f"🗑️ Handed over {len(dead_tasks)} dead tasks to Sweeper.")
                 
                 if dead_tasks:
                     with self.m_lock:
@@ -203,10 +188,6 @@ class OrderBookerGCO:
         task_id = task.task_ref
         task_data = self.m_task_data_cache.get(str(task_id), {})
         user_input = task_data.get('user_inputs', {})
-        expected_start_date = (
-            user_input.get('expected_start_date')
-            or '2000-01-01'
-        )
         expected_end_date = (
             user_input.get('expected_end_date')
             or '2100-01-01'
@@ -307,7 +288,6 @@ class OrderBookerGCO:
                         self._log(f"Failed to update success state to cloud: {e}")
                 
                 ThreadPool.getInstance().enqueue(_update_cloud_success)
-
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 self._remove_task(task, "booking success")
             else:
@@ -322,159 +302,89 @@ class OrderBookerGCO:
             ]
             if any(rate_limited_indicators):
                 self._remove_task(task, "booking rate limited")
-                if task_data and task_id is not None:
-                    task_meta = task_data.get('meta', {})
-                    t_fails = task_meta.get('booking_failures', 0) + 1
-                    task_meta['booking_failures'] = t_fails
-                    
-                    def _update_cloud_meta():
-                        try:
-                            VSCloudApi.Instance().update_vas_task(str(task_id), {"meta": task_meta})
-                        except Exception as cloud_err:
-                            self._log(f"Failed to update task meta: {cloud_err}")
-                    ThreadPool.getInstance().enqueue(_update_cloud_meta)   
-                    
-                    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.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})
                     
     def _creator_loop(self):
         self._log("Creator loop started.")
-        spawn_interval = 10.0
         while not self.m_stop_event.is_set():
             try:
                 time.sleep(1)
-                now = time.time()
                 for apt in self.m_cfg.appointment_types:
                     r_key = apt.routing_key
-                    queue_cd_key = f"vs:queue:cooldown:{r_key}"
-                
-                    if self.redis_client.exists(queue_cd_key):
-                        continue
-
                     with self.m_lock:
                         active = sum(1 for t in self.m_tasks if t.source_queue == r_key)
-                        pending = self.m_pending_order_by_queue.get(r_key, 0)
                         target = self.m_cfg.booker.target_instances
-                    
-                    if (active + pending) < target:
-                        last_spawn = self.m_last_spawn_times.get(r_key, 0.0)
-                        if now - last_spawn >= spawn_interval:
-                            self.m_last_spawn_times[r_key] = now 
-                            self._spawn_worker(r_key)
+                    if active < target:
+                        self._spawn_worker(r_key)
             except Exception as e:
                 self._log(f'Creator loop exception:{e}')
 
     def _spawn_worker(self, target_routing_key: str):
-        with self.m_lock: 
-            self.m_pending_order_by_queue[target_routing_key] = self.m_pending_order_by_queue.get(target_routing_key, 0) + 1
+        instance = None
+        success = False
+        task_id = None
+        try:
+            queue_name = f"auto.{target_routing_key}"
+            task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
+            if not task_data:
+                return 
             
-        def _job():
-            success = False
-            task_id = None
-            is_rate_limited = False
+            task_id = task_data['id']
             
-            try:
-                queue_name = f"auto.{target_routing_key}"
-                task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
-                if not task_data:
-                    return 
-                
-                task_id = task_data['id']
-                
-                with self.m_lock:
-                    self.m_task_data_cache[str(task_id)] = task_data
-                
-                self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + 5*60.0})
-                user_inputs = task_data.get('user_inputs', {})
-                
-                plg_cfg = VSPlgConfig()
-                plg_cfg.debug = self.m_cfg.debug
-                plg_cfg.free_config = self.m_cfg.free_config
-                plg_cfg.session_max_life = self.m_cfg.session_max_life
-                plg_cfg.account.username = user_inputs.get("username", "")
-                plg_cfg.account.password = user_inputs.get("password", "")
-                if not plg_cfg.account.username:
-                    return
-                
-                acceptable_keys = [target_routing_key]
-                if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
-                    plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
+            with self.m_lock:
+                self.m_task_data_cache[str(task_id)] = task_data
+            
+            self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + 8*60.0})
+            user_inputs = task_data.get('user_inputs', {})
+            
+            plg_cfg = VSPlgConfig()
+            plg_cfg.debug = self.m_cfg.debug
+            plg_cfg.free_config = self.m_cfg.free_config
+            plg_cfg.session_max_life = self.m_cfg.session_max_life
+            plg_cfg.account.username = user_inputs.get("username", "")
+            plg_cfg.account.password = user_inputs.get("password", "")
+            if not plg_cfg.account.username:
+                return
+            
+            acceptable_keys = [target_routing_key]
+            if self.m_cfg.need_proxy:
+                proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
+                plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
 
-                instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
-                instance.set_log(self.m_logger)
-                instance.set_config(plg_cfg)
-                instance.create_session() 
-                
-                with self.m_lock:
-                    self.m_tasks.append(
-                        Task(
-                            instance=instance,
-                            qw_cfg=self.m_cfg.query_wait,
-                            next_run=time.time(), 
-                            task_ref=task_id,
-                            acceptable_routing_keys=acceptable_keys, 
-                            source_queue=target_routing_key,
-                            book_allowed=True,
-                            next_remote_ping=time.time() + random.randint(55, 65)   
-                        )
+            instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
+            instance.set_log(self.m_logger)
+            instance.set_config(plg_cfg)
+            instance.create_session() 
+            
+            with self.m_lock:
+                self.m_tasks.append(
+                    Task(
+                        instance=instance,
+                        next_run=time.time(), 
+                        task_ref=task_id,
+                        acceptable_routing_keys=acceptable_keys, 
+                        source_queue=target_routing_key,
+                        book_allowed=True,
+                        next_remote_ping=time.time() + random.gauss(self.m_cfg.booker.keep_alive, 5)
                     )
-                    
-                success = True
-                queue_fail_key = f"vs:queue:failures:{target_routing_key}"
-                self.redis_client.delete(queue_fail_key)                    
-                self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
-            except Exception as e:
-                err_str = str(e)
-                resource_not_found_indicators = [
-                    "40401" in err_str,
-                    "Account not found" in err_str,
-                    "Proxy not found" in err_str
-                ]
-                if any(resource_not_found_indicators):
-                    return
-                
-                self._log(f"Order Booker spawn failed: {e}")
-                
-                rate_limited_indicators = [
-                    "42901" in err_str,
-                    "Rate limited" in err_str
-                ]
-                if any(rate_limited_indicators):
-                    is_rate_limited = True
-                    queue_fail_key = f"vs:queue:failures:{target_routing_key}"
-                    queue_cd_key = f"vs:queue:cooldown:{target_routing_key}"
-                    
-                    q_fails = self.redis_client.incr(queue_fail_key)
-                    q_cd = self.queue_backoff.calculate(q_fails)
-                    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.")
-
-                    if task_id is not None:
-                        task_meta = task_data.get('meta') or {}
-                        t_fails = task_meta.get('spawn_failures', 0) + 1
-                        task_meta['spawn_failures'] = t_fails
-                        
-                        def _update_cloud_meta():
-                            try:
-                                VSCloudApi.Instance().update_vas_task(str(task_id), {"meta": task_meta})
-                            except Exception as cloud_err:
-                                self._log(f"Failed to update task meta: {cloud_err}")
-                        ThreadPool.getInstance().enqueue(_update_cloud_meta) 
-                        
-                        t_cd = self.account_backoff.calculate(t_fails)
-                        self._log(f"⏳ Task={task_id} (Attempt {t_fails}) suspended for {t_cd:.1f}s.")
-                        self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + t_cd})  
-         
-            finally:
-                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)
-                
-                if not success and task_id is not None and not is_rate_limited:
-                    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.")
+                )
+            success = True             
+            self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
+        except Exception as e:
+            err_str = str(e)
+            self._log(f"Order Booker spawn failed: {err_str}")
+            rate_limited_indicators = [
+                "42901" in err_str,
+                "Rate limited" in err_str
+            ]
+            if any(rate_limited_indicators):
+                if task_id is not None:
+                    self.redis_client.zadd(self.m_tracker_key, {str(task_id): time.time() + self.m_cfg.login_backoff})  
+        
+        finally:
+            if not success:
+                if task_id:
                     with self.m_lock:
                         self.m_task_data_cache.pop(str(task_id), None)
-                        
-        ThreadPool.getInstance().enqueue(_job)
+                if instance:
+                    instance.cleanup()
+                        

+ 65 - 59
plugins/tls_plugin.py

@@ -66,7 +66,6 @@ class TlsPlugin(IVSPlg):
         if not os.path.exists(self.root_workspace):
             os.makedirs(self.root_workspace)
         
-        self.is_busy = False
         self.tunnel = None
         self.session_create_time: float = 0
 
@@ -120,8 +119,6 @@ class TlsPlugin(IVSPlg):
         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:
@@ -362,6 +359,17 @@ class TlsPlugin(IVSPlg):
                     self.page.wait.load_start(timeout=3)
                     continue
                 
+                # 改签的情况
+                if '/workflow/application-summary' in current_url:
+                    change_btn_sel = 'tag:button@@text():Change'
+                    self.page.wait.ele_displayed(change_btn_sel, timeout=10)
+                    self.page.ele(change_btn_sel).click(by_js=True)
+                    confirm_btn_sel = 'tag:button@@text():Yes'
+                    self.page.wait.ele_displayed(confirm_btn_sel, timeout=5)
+                    self.page.ele(confirm_btn_sel).click(by_js=True)
+                    self.page.wait.load_start(timeout=5)
+                    continue
+                
                 # 遇到没有申请人的拦截页 (致命错误退出条件)
                 no_applicant_indicators = [
                     "Add a new applicant" in current_html_content,
@@ -515,63 +523,59 @@ class TlsPlugin(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
         res.success = False
-        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)
+        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)
+        
+        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! size={len(slots)}")
+        else:
+            self._log("No slots available.")
+            res.success = False
+            res.availability_status = AvailabilityStatus.NoneAvailable
             
-            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! size={len(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
+        # 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
         return res
 
     def book(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
@@ -896,6 +900,8 @@ class TlsPlugin(IVSPlg):
             if not dialog:
                 return False
             target_btn = dialog.ele('tag:button@text():Save')
+            if not target_btn:
+                target_btn = dialog.ele('tag:button@text():Accept')
             if target_btn:
                 self.mouse.human_click_ele(target_btn)
                 self._log(f"Handle cookie window success")

+ 9 - 6
plugins/vfs_plugin.py

@@ -179,9 +179,8 @@ class VfsPlugin(IVSPlg):
                 self._get_application,
                 self._query_center,
             ]
-
-            resp = random.choice(keep_alive_funcs)()
-            self._log(f'keep_alive request, resp={resp}')
+            random.choice(keep_alive_funcs)()
+            self._log(f'keep_alive request, status ok')
         except Exception as e:
             self.is_healthy = False
             self._log(f'keep_alive failed: {e}')
@@ -835,6 +834,13 @@ class VfsPlugin(IVSPlg):
         app_type = slot_info.apt_type
         from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         
+        expected_start = user_inputs.get("expected_start_date", "2000-01-01")
+        expected_end = user_inputs.get("expected_end_date", "2100-01-01")
+        if from_date >= expected_end:
+            self._log("No valid slots in date preference.") 
+            res.success = False
+            return res
+        
         apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
         
         if not apt_config:
@@ -900,9 +906,6 @@ class VfsPlugin(IVSPlg):
                 self._log("Waitlist confirmed.")
                 return res
             raise BizLogicError(message='Confirm waitlist failed')
-
-        expected_start = user_inputs.get("expected_start_date", "")
-        expected_end = user_inputs.get("expected_end_date", "")
         
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
         self._log(f"Scanning months: {months} (Start looking from: {from_date})")

+ 79 - 132
sentinel.py

@@ -7,9 +7,7 @@ from typing import List, Dict, Callable
 
 from vs_types import GroupConfig, VSPlgConfig, Task, QueryWaitMode
 from vs_plg_factory import VSPlgFactory 
-from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
-from toolkit.backoff import ExponentialBackoff
 from utils.safe_redis_cli import SafeRedisClient
 
 class SentinelGCO:
@@ -20,13 +18,7 @@ class SentinelGCO:
         self.m_tasks: List[Task] = []
         self.m_lock = threading.RLock()
         self.m_stop_event = threading.Event()
-        
         self.redis_client = SafeRedisClient(redis_conf, self.m_logger)
-        self.m_pending_builtin = 0
-        
-        self.group_backoff = ExponentialBackoff(base_delay=60.0, max_delay=3600.0, factor=2.0)
-        self.m_last_spawn_time = 0.0
-        self.m_spawn_interval = 120
         self.m_last_group_query_time = 0.0
 
     def _log(self, message):
@@ -37,13 +29,13 @@ class SentinelGCO:
             
     def _get_average_interval(self) -> float:
         """计算当前组平均的查询间隔(秒)"""
-        mode = self.m_cfg.query_wait.mode
+        mode = self.m_cfg.sentinel.query_wait.mode
         if mode == QueryWaitMode.Loop:
             return 1.0
         elif mode == QueryWaitMode.Fixed:
-            return float(self.m_cfg.query_wait.fixed_wait)
+            return float(self.m_cfg.sentinel.query_wait.fixed_wait)
         elif mode == QueryWaitMode.Random:
-            return (self.m_cfg.query_wait.random_min + self.m_cfg.query_wait.random_max) / 2.0
+            return (self.m_cfg.sentinel.query_wait.random_min + self.m_cfg.sentinel.query_wait.random_max) / 2.0
         return 30.0
     
     def update_config(self, new_cfg: GroupConfig):
@@ -68,7 +60,6 @@ class SentinelGCO:
         class_name = "".join(part.title() for part in plugin_name.split('_'))
         plugin_path = os.path.join(self.m_cfg.plugin_config.lib_path, self.m_cfg.plugin_config.plugin_bin)
         self.m_factory.register_plugin(plugin_name, plugin_path, class_name)
-
         threading.Thread(target=self._monitor_loop, daemon=True, name="Sentinel-Monitor").start()
         threading.Thread(target=self._creator_loop, daemon=True, name="Sentinel-Creator").start()
 
@@ -116,10 +107,6 @@ class SentinelGCO:
                 active_tasks = []
                 dead_tasks = []
                 for t in tasks_to_check:
-                    if t.is_querying:
-                        active_tasks.append(t)
-                        continue
-                    
                     if t.instance.health_check():
                         active_tasks.append(t)
                     else:
@@ -146,10 +133,7 @@ class SentinelGCO:
                 for task in active_tasks:
                     if now < task.next_run:
                         continue
-                    
-                    if task.is_querying:
-                        continue
-                    
+                
                     if now - self.m_last_group_query_time < global_gap:
                         break
 
@@ -168,132 +152,95 @@ class SentinelGCO:
                     elif mode == QueryWaitMode.Random:
                         interval = random.randint(task.qw_cfg.random_min, task.qw_cfg.random_max)
 
-                    task.is_querying = True 
                     self.m_last_group_query_time = now
-
-                    def _query_job(current_task=task, a_type=apt_type, wait_gap=interval):
-                        try:
-                            VSCloudApi.Instance().slot_refresh_start(a_type.routing_key, country=a_type.country, city=a_type.city, visa_type=a_type.visa_type)
-                            result = current_task.instance.query(a_type)
-                            result.apt_type = a_type
-                            if result.success:
-                                ttl = self.m_cfg.sentinel.signal_ttl
-                                self._log(f"🔥 SLOT FOUND! Writing signal to Redis (TTL: {ttl}s)")
-                                payload = {
-                                    "group_id": self.m_cfg.identifier,
-                                    "apt_type": a_type.model_dump(),
-                                    "query_result": result.to_snapshot_payload(),
-                                    "timestamp": time.time()
-                                }
-                                redis_key = self._get_redis_key(a_type.routing_key)
-                                self.redis_client.setex(redis_key, ttl, json.dumps(payload))
-                                payload["query_result"]["website"] = self.m_cfg.website
-                                VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
-                            VSCloudApi.Instance().slot_refresh_success(a_type.routing_key)
-                        except Exception as e:
-                            self._log(f"Query exception: {e}")
-                            VSCloudApi.Instance().slot_refresh_fail(a_type.routing_key, error=str(e))
-                        finally:
-                            current_task.next_run = time.time() + wait_gap
-                            current_task.is_querying = False
-                    ThreadPool.getInstance().enqueue(_query_job)
+                    try:
+                        VSCloudApi.Instance().slot_refresh_start(
+                            apt_type.routing_key, 
+                            country=apt_type.country, 
+                            city=apt_type.city, 
+                            visa_type=apt_type.visa_type
+                        )
+                        result = task.instance.query(apt_type)
+                        result.apt_type = apt_type
+                        if result.success:
+                            ttl = self.m_cfg.sentinel.signal_ttl
+                            self._log(f"🔥 SLOT FOUND! Writing signal to Redis (TTL: {ttl}s)")
+                            payload = {
+                                "group_id": self.m_cfg.identifier,
+                                "apt_type": apt_type.model_dump(),
+                                "query_result": result.to_snapshot_payload(),
+                                "timestamp": time.time()
+                            }
+                            redis_key = self._get_redis_key(apt_type.routing_key)
+                            self.redis_client.setex(redis_key, ttl, json.dumps(payload))
+                            payload["query_result"]["website"] = self.m_cfg.website
+                            VSCloudApi.Instance().slot_snapshot_report(payload["query_result"])
+                        VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
+                    except Exception as e:
+                        self._log(f"Query exception: {e}")
+                        VSCloudApi.Instance().slot_refresh_fail(apt_type.routing_key, error=str(e))
+                    finally:
+                        task.next_run = time.time() + interval
                     break
-
             except Exception as e:
                 self._log(f"Monitor loop error: {e}")
 
     def _creator_loop(self):
         self._log("Creator loop started.")
-        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.wait(1.0):
             try:
-                time.sleep(1)
-                if self.redis_client.exists(group_cd_key):
-                    continue
                 with self.m_lock:
                     current = len(self.m_tasks)
-                    pending = self.m_pending_builtin
                     target = self.m_cfg.sentinel.target_instances
                 
-                if (current + pending) < target:
-                    now = time.time()
-                    if now - self.m_last_spawn_time >= self.m_spawn_interval:
-                        with self.m_lock:
-                            self.m_last_spawn_time = now
-                        self._log(f"Staggered spawn triggered. Next spawn in {self.m_spawn_interval:.1f}s")
-                        self._spawn_sentinel_worker()
+                if current < target:
+                    self._spawn_sentinel_worker()
             except Exception as e:
-                self._log(f'Creator loop exception:{e}')
+                self._log(f'Creator loop exception: {e}')
 
     def _spawn_sentinel_worker(self):
-        with self.m_lock:
-            self.m_pending_builtin += 1
+        instance = None
+        success = False
+        plg_cfg = None
+        try:
+            plg_cfg = VSPlgConfig()
+            plg_cfg.debug = self.m_cfg.debug
+            plg_cfg.free_config = self.m_cfg.free_config
+            plg_cfg.session_max_life = self.m_cfg.session_max_life
             
-        def _job():
-            instance = None
-            success = False
-            try:
-                plg_cfg = VSPlgConfig()
-                plg_cfg.debug = self.m_cfg.debug
-                plg_cfg.free_config = self.m_cfg.free_config
-                plg_cfg.session_max_life = self.m_cfg.session_max_life
-                
-                if not self.m_cfg.need_account:
-                    plg_cfg.account.id = 0
-                    plg_cfg.account.username = "Guest"
-                else:
-                    acc = VSCloudApi.Instance().get_next_account(self.m_cfg.sentinel.account_pool_id, self.m_cfg.sentinel.account_cd)
-                    plg_cfg.account = type(plg_cfg.account)(**acc)
-                
-                if self.m_cfg.need_proxy:
-                    proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
-                    plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
-
-                instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
-                instance.set_log(self.m_logger)
-                instance.set_config(plg_cfg)
-                instance.create_session()
-                
-                with self.m_lock:
-                    self.m_tasks.append(
-                        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}"
-                self.redis_client.delete(group_fail_key)
-                
-                success = True
-                self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
-
-            except Exception as e:
-                err_str = str(e)
-                resource_not_found_indicators = [
-                    "40401" in err_str,
-                    "Account not found" in err_str,
-                    "Proxy not found" in err_str,
-                ]
-                if any(resource_not_found_indicators):
-                    return
-                
-                self._log(f"Spawn failed: {e}")
-                
-                rate_limited_indicators = [
-                    "42901" in err_str,
-                    "Rate limited" in err_str
-                ]
-                if any(rate_limited_indicators):
-                    group_fail_key = f"vs:group:failures:{self.m_cfg.identifier}"
-                    group_cd_key = f"vs:group:cooldown:{self.m_cfg.identifier}"
-                    
-                    g_fails = self.redis_client.incr(group_fail_key)
-                    g_cd = self.group_backoff.calculate(g_fails)
-                    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.")
-                    
-            finally:
-                if not success and instance is not None:
-                    instance.cleanup()
-                with self.m_lock:
-                    self.m_pending_builtin = max(0, self.m_pending_builtin - 1)
+            if not self.m_cfg.need_account:
+                plg_cfg.account.id = 0
+                plg_cfg.account.username = "Guest"
+            else:
+                acc = VSCloudApi.Instance().get_next_account(self.m_cfg.sentinel.account_pool_id, self.m_cfg.sentinel.account_cd)
+                plg_cfg.account = type(plg_cfg.account)(**acc)
+            
+            if self.m_cfg.need_proxy:
+                proxy = VSCloudApi.Instance().get_next_proxy(self.m_cfg.proxy_pool, self.m_cfg.proxy_cd)
+                plg_cfg.proxy = type(plg_cfg.proxy)(**proxy)
+
+            instance = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
+            instance.set_log(self.m_logger)
+            instance.set_config(plg_cfg)
+            instance.create_session()
+            
+            with self.m_lock:
+                self.m_tasks.append(
+                    Task(instance=instance, qw_cfg=self.m_cfg.sentinel.query_wait, next_run=time.time(), book_allowed=False)
+                )
 
-        ThreadPool.getInstance().enqueue(_job)
+            success = True
+            self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
+        except Exception as e:
+            err_str = str(e)
+            self._log(f"Spawn failed: {err_str}")
+            rate_limited_indicators = [
+                "42901" in err_str,
+                "Rate limited" in err_str
+            ]
+            if any(rate_limited_indicators):
+                if plg_cfg and plg_cfg.account.username != "Guest":
+                    VSCloudApi.lock_account(plg_cfg.account.id, self.m_cfg.login_backoff)
+        finally:
+            if not success and instance is not None:
+                instance.cleanup()

+ 3 - 0
toolkit/vs_cloud_api.py

@@ -282,6 +282,9 @@ class VSCloudApi:
         else:
             raise BizLogicError(message=f"Get next account biz error: {result.get('message')}")
         
+    def lock_account(self, account_id: int, lock_duration: int = 3600):
+        pass
+        
     def get_next_proxy(self, pools: List[str], proxy_cd: int = 60):
         if configure.TEST_PROXY:
             return configure.TEST_PROXY 

+ 58 - 8
utils/fake_utils.py

@@ -15,7 +15,7 @@ PASSPORT_TYPE = "Ordinary passport"
 
 # 国家专属配置字典,将差异化数据隔离,极大提升可维护性
 COUNTRY_CONFIGS = {
-    "CN": {
+    "CN-Shanghai": {
         "nat_code": None,  # randomuser 不支持 CN,置为 None
         "pool_name": "tls.cn.sha.fr.sentinel",
         "location": "Shanghai",
@@ -30,9 +30,46 @@ COUNTRY_CONFIGS = {
         # -----------------------------------------------
         "default_first_name": "San",
         "default_last_name": "Zhang",
-        "default_phone": "13800000000"
+        "default_phone": "13800000000",
+        "fra1": "FRA1SH",
     },
-    "GB": {
+    "CN-Hangzhou": {
+        "nat_code": None,  # randomuser 不支持 CN,置为 None
+        "pool_name": "tls.cn.hgh.fr.sentinel",
+        "location": "Hangzhou",
+        "province_residence": "Jiangsu",
+        "nationality": "China",
+        "phone_country_code": "86",
+        # --- 本地化生成规则(针对 API 不支持的国家) ---
+        "local_first_names": ["Wei", "Fang", "Jian", "Hui", "Lei", "Ting", "Peng", "Xia", "Bin", "Jie", "San", "Ming"],
+        "local_last_names": ["Wang", "Li", "Zhang", "Liu", "Chen", "Yang", "Huang", "Zhao", "Wu", "Zhou"],
+        "phone_prefix": ["138", "139", "150", "151", "180", "189"], # 中国手机号前缀
+        "phone_length": 11,
+        # -----------------------------------------------
+        "default_first_name": "San",
+        "default_last_name": "Zhang",
+        "default_phone": "13800000000",
+        "fra1": "FRA1HN",
+    },
+    "CN-Beijing": {
+        "nat_code": None,  # randomuser 不支持 CN,置为 None
+        "pool_name": "tls.cn.bjs.fr.sentinel",
+        "location": "Beijing",
+        "province_residence": "Beijing",
+        "nationality": "China",
+        "phone_country_code": "86",
+        # --- 本地化生成规则(针对 API 不支持的国家) ---
+        "local_first_names": ["Wei", "Fang", "Jian", "Hui", "Lei", "Ting", "Peng", "Xia", "Bin", "Jie", "San", "Ming"],
+        "local_last_names": ["Wang", "Li", "Zhang", "Liu", "Chen", "Yang", "Huang", "Zhao", "Wu", "Zhou"],
+        "phone_prefix": ["138", "139", "150", "151", "180", "189"], # 中国手机号前缀
+        "phone_length": 11,
+        # -----------------------------------------------
+        "default_first_name": "San",
+        "default_last_name": "Zhang",
+        "default_phone": "13800000000",
+        "fra1": "FRA1PB",
+    },
+    "GB-London": {
         "nat_code": "gb",  # API 支持 GB,直接依赖 API 生成姓名
         "pool_name": "tls.gb.lon.fr.sentinel",
         "location": "London",
@@ -41,7 +78,20 @@ COUNTRY_CONFIGS = {
         "phone_country_code": "44",
         "default_first_name": "James",
         "default_last_name": "Smith",
-        "default_phone": "7400000000"
+        "default_phone": "7400000000",
+        "fra1": "FRA1LO",
+    },
+        "IE-Dublin": {
+        "nat_code": "ie",  # API 支持 GB,直接依赖 API 生成姓名
+        "pool_name": "tls.ie.dub.fr.sentinel",
+        "location": "Dublin",
+        "province_residence": "Dublin",
+        "nationality": "Ireland",
+        "phone_country_code": "353",
+        "default_first_name": "James",
+        "default_last_name": "Smith",
+        "default_phone": "0895224562",
+        "fra1": "FRA1DB",
     }
 }
 
@@ -120,7 +170,7 @@ def generate_random_account_detail(country_code: str = "CN") -> Dict[str, Any]:
     """
     基于 randomuser 和指定国家配置生成随机账户信息。
     """
-    config = COUNTRY_CONFIGS.get(country_code.upper())
+    config = COUNTRY_CONFIGS.get(country_code)
     if not config:
         raise ValueError(f"Unsupported country code: {country_code}")
 
@@ -144,8 +194,8 @@ def generate_random_account_detail(country_code: str = "CN") -> Dict[str, Any]:
     app_form_suffix = "".join(str(random.randint(0, 9)) for _ in range(11))
     passport_no = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2)) + \
                   "".join(random.choices("0123456789", k=7))
-
-    # LO 伦敦 DB 都柏林 PB 北京 SH 上海
+    fra1_prefix = config.get('fra1')
+    # LO 伦敦 DB 都柏林 PB 北京 SH 上海 HN 杭州
     # 4. 组装组装并返回
     return {
         "pool_name": config["pool_name"],
@@ -154,7 +204,7 @@ def generate_random_account_detail(country_code: str = "CN") -> Dict[str, Any]:
         "location": config["location"],
         "visa_type": VISA_TYPE,
         "travel_purpose": TRAVEL_PURPOSE,
-        "application_form_id": f"FRA1LO{app_form_suffix}",
+        "application_form_id": f"{fra1_prefix}{app_form_suffix}",
         "last_name": last_name,
         "first_name": first_name,
         "gender": gender,

+ 6 - 9
vs_types.py

@@ -87,13 +87,15 @@ class SentinelConfig(BaseModel):
     target_instances: int = 1
     account_cd: int = 180*60        # 单位 秒
     signal_ttl: int = 180           # 单位 秒
+    query_wait: QueryWaitConfig = Field(default_factory=QueryWaitConfig)
 
 # === Booker配置 ===
 class BookerConfig(BaseModel):
     account_source: str = "built-in" # "built-in" 或 "order"
-    account_pool_id: str = ""        # 仅在 built-in 模式下使用
+    account_pool_id: str = ""        # 仅在 built-in 模式下使用 账号池名字
+    account_cd: int = 180*60         # 仅在 built-in 模式下使用 账号冷却时间 单位 秒
     target_instances: int = 1        # built-in下为全局限制; order下为单队列限制
-    account_cd: int = 180*60         # 单位 秒
+    keep_alive: int = 60             # 每60秒保活一次
     booking_cooldown: float = 10.0   # 单位 秒
     max_bookings_per_account: int = 1
 
@@ -106,14 +108,11 @@ class GroupConfig(BaseModel):
     proxy_pool: List[str] = Field(default_factory=list)
     proxy_cd: int = 5*60                # 单位 秒
     session_max_life: int = 30*60       # 单位 秒
-    
+    login_backoff: int = 1800           # 创建会话Rate limited 后账号自动退避 秒
     sentinel: SentinelConfig = Field(default_factory=SentinelConfig)
     booker: BookerConfig = Field(default_factory=BookerConfig)
-
-    query_wait: QueryWaitConfig = Field(default_factory=QueryWaitConfig)
     plugin_config: PluginConfig = Field(default_factory=PluginConfig)
     appointment_types: List[AppointmentType] = Field(default_factory=list)
-
     website: str = ""
     free_config: Dict[str, Any] = Field(default_factory=dict)
 
@@ -194,7 +193,7 @@ class VSBookResult(BaseModel):
 # === 内部任务对象 ===
 class Task(BaseModel):
     instance: Any              
-    qw_cfg: QueryWaitConfig
+    qw_cfg: Optional[QueryWaitConfig] = None,
     next_run: float = 0.0      
     book_allowed: bool = True
     # 订单模式下,保存绑定的 Task ID
@@ -206,8 +205,6 @@ class Task(BaseModel):
     successful_bookings: int = 0
     # 下一次允许心跳时间
     next_remote_ping: float = 0.0
-    # 是否正在查询
-    is_querying: bool = False
     
     model_config = {
         "underscore_attrs_are_private": True,