jerry 1 miesiąc temu
rodzic
commit
6a63e1b95b

+ 224 - 0
booker_builtin.py

@@ -0,0 +1,224 @@
+import os
+import time
+import json
+import threading
+import random
+import traceback
+import redis
+from typing import List, Dict, Callable
+
+from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType
+from vs_plg_factory import VSPlgFactory 
+from toolkit.thread_pool import ThreadPool 
+from toolkit.vs_cloud_api import VSCloudApi
+from toolkit.proxy_manager import ProxyManager
+
+class BuiltinBookerGCO:
+    """
+    非绑定模式 (公共内置账号池):
+    - 只维护全局 target_instances 数量的实例。
+    - 所有实例热机等待,发现信号后临时去云端 Pop 订单。
+    """
+    def __init__(self, cfg: GroupConfig, redis_conf: Dict, logger: Callable[[str], None] = None):
+        self.m_cfg = cfg
+        self.m_factory = VSPlgFactory()
+        self.m_logger = logger
+        self.m_tasks: List[Task] = []
+        self.m_lock = threading.RLock()
+        self.m_stop_event = threading.Event()
+        self.redis_client = redis.Redis(**redis_conf)
+        self.m_pending_builtin = 0
+
+    def _log(self, message):
+        if self.m_logger:
+            self.m_logger(f'[BUILTIN-BOOKER] [{self.m_cfg.identifier}] {message}')
+
+    def start(self):
+        if not self.m_cfg.enable:
+            return
+        self._log("Starting Built-in Booker...")
+        plugin_name = self.m_cfg.plugin_config.plugin_name
+        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()
+
+    def stop(self):
+        self._log("Stopping Booker...")
+        self.m_stop_event.set()
+
+    def _get_redis_key(self, routing_key: str) -> str:
+        return f"vs:signal:{routing_key}"
+
+    def _maintain_loop(self):
+        self._log("Maintain loop started.")
+        rng = random.Random()
+        while not self.m_stop_event.is_set():
+            wait_seconds = rng.randint(60, 300)
+            for _ in range(wait_seconds):
+                if self.m_stop_event.is_set():
+                    return
+            time.sleep(1.0)
+            
+            with self.m_lock:
+                tasks_to_check = list(self.m_tasks)
+            
+            healthy_tasks = []
+            for t in tasks_to_check:
+                try:
+                    t.instance.keep_alive()
+                    if t.instance.health_check():
+                        healthy_tasks.append(t)
+                except Exception as e:
+                    self._log(f"Instance keep-alive failed: {e}")
+            
+            with self.m_lock:
+                self.m_tasks = [t for t in self.m_tasks if t in healthy_tasks]
+
+    def _booking_trigger_loop(self):
+        self._log("Trigger loop started.")
+        while not self.m_stop_event.is_set():
+            try:
+                time.sleep(1.0)
+                now = time.time()
+                for apt_type in self.m_cfg.appointment_types:
+                    redis_key = self._get_redis_key(apt_type.routing_key)
+                    if not self.redis_client.get(redis_key):
+                        continue
+                    
+                    data = json.loads(self.redis_client.get(redis_key))
+                    query_result = VSQueryResult.model_validate(data['query_result'])
+                    query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
+                    
+                    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
+                            
+                            self._log(f"🚀 Triggering BOOK for {apt_type.routing_key}")
+                            task.next_run = now + self.m_cfg.booker.booking_cooldown
+                            ThreadPool.getInstance().enqueue(self._execute_book_job, task, query_result)
+            except Exception as e:
+                self._log(f"Trigger loop error: {e}")
+                time.sleep(2)
+
+    def _execute_book_job(self, task: Task, query_result: VSQueryResult):
+        queue_name = f"auto.{query_result.apt_type.routing_key}"
+        task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
+        if not task_data:
+            return 
+        
+        task_id = task_data['id']
+        order_id = task_data.get('order_id')
+        user_input = task_data.get('user_inputs', {})
+        booking_success = False
+        
+        try:
+            book_res = task.instance.book(query_result, user_input)
+            if book_res.success:
+                booking_success = True
+                self._log(f"✅ BOOK SUCCESS! Order: {order_id}")
+                
+                grab_info = {
+                    "account": book_res.account,
+                    "session_id": book_res.session_id,
+                    "urn": book_res.urn,
+                    "slot_date": book_res.book_date,
+                    "slot_time": book_res.book_time,
+                    "timestamp": int(time.time()),
+                    "payment_link": book_res.payment_link
+                }
+                VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
+                
+                # === 核心:成功次数判断 ===
+                task.successful_bookings += 1
+                max_b = self.m_cfg.booker.max_bookings_per_account
+                if max_b > 0 and task.successful_bookings >= max_b:
+                    self._log(f"Account reached max bookings ({max_b}). Destroying instance.")
+                    with self.m_lock:
+                        if task in self.m_tasks:
+                            self.m_tasks.remove(task)
+            else:
+                self._log(f"❌ BOOK FAILED for Order: {order_id}")
+
+        except Exception as e:
+            self._log(f"Exception during booking: {e}")
+        finally:
+            if not booking_success and task_id:
+                try:
+                    VSCloudApi.Instance().return_vas_task_to_queue(task_id)
+                except Exception as ex:
+                    self._log(f"Failed to return task: {ex}")
+
+    def _creator_loop(self):
+        self._log("Creator loop started.")
+        while not self.m_stop_event.is_set():
+            time.sleep(5.0)
+            with self.m_lock:
+                current = len(self.m_tasks)
+                if (current + self.m_pending_builtin) < self.m_cfg.booker.target_instances:
+                    self._spawn_worker()
+
+    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
+
+                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 * 60)
+                    if not acc:
+                        return
+                    plg_cfg.account.id = acc['id']
+                    plg_cfg.account.username = acc['username']
+                    plg_cfg.account.password = acc['password']
+
+                if self.m_cfg.need_proxy:
+                    proxy = ProxyManager.Instance().next(self.m_cfg.proxy_pool, lock_duration=self.m_cfg.proxy_lock_interval)
+                    if not proxy:
+                        return
+                    plg_cfg.proxy.id = proxy['id']
+                    plg_cfg.proxy.ip = proxy['ip']
+                    plg_cfg.proxy.port = proxy['port']
+                    plg_cfg.proxy.scheme = proxy['scheme']
+                    plg_cfg.proxy.username = proxy['username']
+                    plg_cfg.proxy.password = proxy['password']
+
+                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
+                        )
+                    )
+                self._log(f"+++ Built-in Booker spawned: {plg_cfg.account.username}")
+            except Exception as e:
+                err_str = str(e)
+                if "40401" in err_str or "Account not found" in err_str:
+                    return
+                
+                self._log(f"Spawn failed: {e}")
+            finally:
+                with self.m_lock:
+                    self.m_pending_builtin = max(0, self.m_pending_builtin - 1)
+        ThreadPool.getInstance().enqueue(_job)

+ 267 - 0
booker_order.py

@@ -0,0 +1,267 @@
+import os
+import time
+import json
+import threading
+import random
+import traceback
+import redis
+from typing import List, Dict, Callable
+
+from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType
+from vs_plg_factory import VSPlgFactory 
+from toolkit.thread_pool import ThreadPool 
+from toolkit.vs_cloud_api import VSCloudApi
+from toolkit.proxy_manager import ProxyManager
+
+class OrderBookerGCO:
+    """
+    绑定模式 (订单自带账号):
+    - 按城市队列维护热机配额。
+    - 绝对的 1 对 1 关系:一个实例绑定一个云端订单。
+    - 预订成功后,实例立即销毁。
+    """
+    def __init__(self, cfg: GroupConfig, redis_conf: Dict, logger: Callable[[str], None] = None):
+        self.m_cfg = cfg
+        self.m_factory = VSPlgFactory()
+        self.m_logger = logger
+        self.m_tasks: List[Task] = []
+        self.m_lock = threading.RLock()
+        self.m_stop_event = threading.Event()
+        self.redis_client = redis.Redis(**redis_conf)
+        self.m_pending_order_by_queue: Dict[str, int] = {}
+
+    def _log(self, message):
+        if self.m_logger:
+            self.m_logger(f'[ORDER-BOOKER] [{self.m_cfg.identifier}] {message}')
+
+    def start(self):
+        if not self.m_cfg.enable:
+            return
+        self._log("Starting Order Booker...")
+        plugin_name = self.m_cfg.plugin_config.plugin_name
+        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()
+
+    def stop(self):
+        self._log("Stopping Booker...")
+        self.m_stop_event.set()
+
+    def _get_redis_key(self, routing_key: str) -> str:
+        return f"vs:signal:{routing_key}"
+
+    def _safe_return_task(self, task_id: int, reason: str = ""):
+        """安全地将订单归还给云端队列,防止复活已被取消或已抢成功的订单"""
+        if not task_id:
+            return
+            
+        try:
+            task_data = VSCloudApi.Instance().get_vas_task(task_id)
+            if not task_data:
+                self._log(f"Task {task_id} not found in cloud, cannot return.")
+                return
+                
+            current_status = task_data.get('status', '')
+            # 如果订单已经被客户取消,或者已经成功,绝对不能还回队列!
+            if current_status in ['cancelled', 'grabbed', 'success']:
+                self._log(f"Task {task_id} is already '{current_status}'. Skipping return.")
+                return
+                
+            self._log(f"Returning task {task_id} to queue. Reason: {reason}")
+            VSCloudApi.Instance().return_vas_task_to_queue(task_id)
+            
+        except Exception as ex:
+            self._log(f"Failed to safely return task {task_id}: {ex}")
+
+    def _maintain_loop(self):
+        self._log("Maintain loop started.")
+        rng = random.Random()
+        while not self.m_stop_event.is_set():
+            wait_seconds = rng.randint(60, 300)
+            for _ in range(wait_seconds):
+                if self.m_stop_event.is_set():
+                    return
+            time.sleep(1.0)
+            
+            with self.m_lock:
+                tasks_to_check = list(self.m_tasks)
+            
+            healthy_tasks = []
+            dead_tasks = []
+            
+            for t in tasks_to_check:
+                try:
+                    t.instance.keep_alive()
+                    if t.instance.health_check(): 
+                        healthy_tasks.append(t)
+                    else:
+                        dead_tasks.append(t)
+                        self._log(f"♻️ Instance {t.task_ref} unhealthy, marking for removal.")
+                except Exception as e:
+                    dead_tasks.append(t)
+                    self._log(f"♻️ Instance {t.task_ref} keep-alive failed: {e}, marking for removal.")
+            
+            with self.m_lock:
+                self.m_tasks = [t for t in self.m_tasks if t in healthy_tasks]
+
+            # 实例死亡,调用安全归还函数
+            for t in dead_tasks:
+                if t.task_ref is not None:
+                    self._safe_return_task(t.task_ref, reason="Instance died during maintain_loop")
+
+    def _booking_trigger_loop(self):
+        self._log("Trigger loop started.")
+        while not self.m_stop_event.is_set():
+            try:
+                time.sleep(1.0)
+                now = time.time()
+                for apt_type in self.m_cfg.appointment_types:
+                    redis_key = self._get_redis_key(apt_type.routing_key)
+                    if not self.redis_client.get(redis_key): continue
+                    
+                    data = json.loads(self.redis_client.get(redis_key))
+                    query_result = VSQueryResult.model_validate(data['query_result'])
+                    query_result.apt_type = AppointmentType.model_validate(data['apt_type'])
+                    
+                    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
+                            
+                            self._log(f"🚀 Triggering BOOK for {apt_type.routing_key}")
+                            task.next_run = now + self.m_cfg.booker.booking_cooldown
+                            ThreadPool.getInstance().enqueue(self._execute_book_job, task, query_result)
+            except Exception as e:
+                self._log(f"Trigger loop error: {e}")
+                time.sleep(2)
+
+    def _execute_book_job(self, task: Task, query_result: VSQueryResult):
+        task_id = task.task_ref
+        if not task_id:
+            return
+
+        try:
+            task_data = VSCloudApi.Instance().get_vas_task(task_id)
+            if not task_data or task_data.get('status') in ['grabbed', 'cancelled']:
+                self._log(f"Bound Task {task_id} is no longer valid or already processed. Removing instance.")
+                with self.m_lock:
+                    if task in self.m_tasks: self.m_tasks.remove(task)
+                return
+            
+            order_id = task_data.get('order_id')
+            user_input = task_data.get('user_inputs', {})
+
+            book_res = task.instance.book(query_result, user_input)
+
+            if book_res.success:
+                self._log(f"✅ BOOK SUCCESS! Order: {order_id}. Destroying instance.")
+                grab_info = {
+                    "account": book_res.account,
+                    "session_id": book_res.session_id,
+                    "urn": book_res.urn,
+                    "slot_date": book_res.book_date,
+                    "slot_time": book_res.book_time,
+                    "timestamp": int(time.time()),
+                    "payment_link": book_res.payment_link
+                }
+                VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
+                
+                with self.m_lock:
+                    if task in self.m_tasks:
+                        self.m_tasks.remove(task)
+            else:
+                self._log(f"❌ BOOK FAILED for Order: {order_id}. Will retry on next signal.")
+
+        except Exception as e:
+            self._log(f"Exception during booking: {e}")
+
+    def _creator_loop(self):
+        self._log("Creator loop started.")
+        while not self.m_stop_event.is_set():
+            time.sleep(5.0)
+            with self.m_lock:
+                for apt in self.m_cfg.appointment_types:
+                    r_key = apt.routing_key
+                    active = sum(1 for t in self.m_tasks if getattr(t, 'source_queue', '') == r_key)
+                    pending = self.m_pending_order_by_queue.get(r_key, 0)
+                    
+                    if (active + pending) < self.m_cfg.booker.target_instances:
+                        self._spawn_worker(r_key)
+                        time.sleep(0.5)
+
+    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
+            
+        def _job():
+            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 
+                
+                task_id = task_data['id']
+                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 = ProxyManager.Instance().next(self.m_cfg.proxy_pool, lock_duration=self.m_cfg.proxy_lock_interval)
+                    if not proxy:
+                        return
+                    plg_cfg.proxy.id = proxy['id']
+                    plg_cfg.proxy.ip = proxy['ip']
+                    plg_cfg.proxy.port = proxy['port']
+                    plg_cfg.proxy.scheme = proxy['scheme']
+                    plg_cfg.proxy.username = proxy['username']
+                    plg_cfg.proxy.password = proxy['password']
+
+                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, 
+                            ource_queue=target_routing_key,
+                            book_allowed=True
+                        )
+                    )
+                success = True
+                self._log(f"+++ Order Booker spawned: {plg_cfg.account.username} (Target: {acceptable_keys})")
+            except Exception as e:
+                err_str = str(e)
+                if "40401" in err_str or "Account not found" in err_str:
+                    return
+                
+                self._log(f"Order Booker spawn failed: {e}")
+            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:
+                    self._safe_return_task(task_id, reason="Instance spawn/login failed")
+                    
+        ThreadPool.getInstance().enqueue(_job)

+ 0 - 438
config/accounts.json

@@ -1,438 +0,0 @@
-{
-    "au_fr": [
-        {
-            "id": 598,
-            "password": "0cjO4ts6H@",
-            "username": "au_fr_h74mby@gmail-app.com"
-        },
-        {
-            "id": 599,
-            "password": "gR1ORxYNA@",
-            "username": "au_fr_p9iq5r@gmail-app.com"
-        },
-        {
-            "id": 600,
-            "password": "fAl7VmBc8@",
-            "username": "au_fr_47p3jz@gmail-app.com"
-        },
-        {
-            "id": 601,
-            "password": "mey@D4ubD0",
-            "username": "au_fr_k99ky1@gmail-app.com"
-        },
-        {
-            "id": 602,
-            "password": "Xui@Zj0HkN",
-            "username": "au_fr_q87fl0@gmail-app.com"
-        },
-        {
-            "id": 603,
-            "password": "@kYfU9S5NI",
-            "username": "au_fr_4cgp29@gmail-app.com"
-        }
-    ],
-    "gb_it": [
-        {
-            "id": 556,
-            "password": "2v@H2UXx74",
-            "username": "gb_it_0msok0@gmail-app.com"
-        },
-        {
-            "id": 557,
-            "password": "@KMWaBf0w1",
-            "username": "gb_it_0wxymp@gmail-app.com"
-        },
-        {
-            "id": 559,
-            "password": "Rc7@RFfzW3",
-            "username": "gb_it_3j6nwd@gmail-app.com"
-        },
-        {
-            "id": 560,
-            "password": "LVUBntr4@5",
-            "username": "gb_it_b2xhnr@gmail-app.com"
-        },
-        {
-            "id": 561,
-            "password": "B4@Q56dgu7",
-            "username": "gb_it_0m93bm@gmail-app.com"
-        },
-        {
-            "id": 562,
-            "password": "5gQX7h3@VF",
-            "username": "gb_it_ajcwvx@gmail-app.com"
-        },
-        {
-            "id": 563,
-            "password": "6Kc@3Eh@eL",
-            "username": "gb_it_mfn0o4@gmail-app.com"
-        },
-        {
-            "id": 564,
-            "password": "Q9c2x@e6da",
-            "username": "gb_it_tf2ync@gmail-app.com"
-        },
-        {
-            "id": 565,
-            "password": "miz@7FRuk3",
-            "username": "gb_it_bn4j65@gmail-app.com"
-        },
-        {
-            "id": 566,
-            "password": "2s4@5VHYU5",
-            "username": "gb_it_wtlffb@gmail-app.com"
-        },
-        {
-            "id": 567,
-            "password": "R@3s9jgNeO",
-            "username": "gb_it_r5z9g5@gmail-app.com"
-        }
-    ],
-    "gb_nl": [
-        {
-            "id": 620,
-            "password": "INYl7Q@0x1",
-            "username": "gb_nl_l6nv77@gmail-app.com"
-        },
-        {
-            "id": 621,
-            "password": "n1@iw3DfNO",
-            "username": "gb_nl_ztsj28@gmail-app.com"
-        },
-        {
-            "id": 622,
-            "password": "w1RmZHL@7N",
-            "username": "gb_nl_ifp27n@gmail-app.com"
-        },
-        {
-            "id": 623,
-            "password": "uZ@5vtkwJc",
-            "username": "gb_nl_2sx13j@gmail-app.com"
-        },
-        {
-            "id": 624,
-            "password": "yZBEOqIZ@4",
-            "username": "gb_nl_t4wi47@gmail-app.com"
-        },
-        {
-            "id": 625,
-            "password": "5i@GYmqh1x",
-            "username": "gb_nl_rwzr3l@gmail-app.com"
-        }
-    ],
-    "gb_no": [
-        {
-            "id": 632,
-            "password": "qFYL4Kvi@7",
-            "username": "gb_no_8kwb4k@gmail-app.com"
-        },
-        {
-            "id": 633,
-            "password": "LWh26@b1FO",
-            "username": "gb_no_i5fmm5@gmail-app.com"
-        },
-        {
-            "id": 634,
-            "password": "Jn33M3j@Km",
-            "username": "gb_no_31n1v9@gmail-app.com"
-        },
-        {
-            "id": 635,
-            "password": "kysEA@69en",
-            "username": "gb_no_sktwxs@gmail-app.com"
-        },
-        {
-            "id": 636,
-            "password": "A@l8ZX7Vhp",
-            "username": "gb_no_xvh47y@gmail-app.com"
-        },
-        {
-            "id": 637,
-            "password": "2CE76@fMTg",
-            "username": "gb_no_a4xwpu@gmail-app.com"
-        },
-        {
-            "id": 638,
-            "password": "cb@M2j4Y6M",
-            "username": "gb_no_nl9vdl@gmail-app.com"
-        },
-        {
-            "id": 639,
-            "password": "Db0cn2w@2M",
-            "username": "gb_no_iaya2k@gmail-app.com"
-        },
-        {
-            "id": 640,
-            "password": "6iWb@C1vxo",
-            "username": "gb_no_5yumya@gmail-app.com"
-        },
-        {
-            "id": 641,
-            "password": "R@b@43ngZ9",
-            "username": "gb_no_io50uu@gmail-app.com"
-        },
-        {
-            "id": 642,
-            "password": "ePbSJ@x94U",
-            "username": "gb_no_glpu8h@gmail-app.com"
-        },
-        {
-            "id": 643,
-            "password": "u6k6eUTky@",
-            "username": "gb_no_np7oxv@gmail-app.com"
-        }
-    ],
-    "ie_at": [
-        {
-            "id": 592,
-            "password": "P1C7evb@LU",
-            "username": "ie_at_oxjrdp@gmail-app.com"
-        },
-        {
-            "id": 593,
-            "password": "CHt@saBY2S",
-            "username": "ie_at_cy6nst@gmail-app.com"
-        },
-        {
-            "id": 594,
-            "password": "12z8o@DmVc",
-            "username": "ie_at_ivov3v@gmail-app.com"
-        },
-        {
-            "id": 595,
-            "password": "e@iC0fPpxP",
-            "username": "ie_at_fatyy4@gmail-app.com"
-        },
-        {
-            "id": 596,
-            "password": "sLfpeK9@PV",
-            "username": "ie_at_us8e66@gmail-app.com"
-        },
-        {
-            "id": 597,
-            "password": "h@@Qes5WdX",
-            "username": "ie_at_2akgxq@gmail-app.com"
-        }
-    ],
-    "ie_dk": [
-        {
-            "id": 626,
-            "password": "7It@d0S038",
-            "username": "ie_dk_c72y0m@gmail-app.com"
-        },
-        {
-            "id": 627,
-            "password": "TrIt0@eYdM",
-            "username": "ie_dk_w5cr67@gmail-app.com"
-        },
-        {
-            "id": 628,
-            "password": "@7caSKB2l9",
-            "username": "ie_dk_p9eqxz@gmail-app.com"
-        },
-        {
-            "id": 629,
-            "password": "ieS@tLsY24",
-            "username": "ie_dk_4e8gmz@gmail-app.com"
-        },
-        {
-            "id": 630,
-            "password": "x@8ikj3AcL",
-            "username": "ie_dk_2xywt2@gmail-app.com"
-        },
-        {
-            "id": 631,
-            "password": "TmA@SCxRg8",
-            "username": "ie_dk_6c9gsf@gmail-app.com"
-        }
-    ],
-    "ie_fi": [
-        {
-            "id": 586,
-            "password": "FeuyJ@zl20",
-            "username": "ie_fi_1txaxx@gmail-app.com"
-        },
-        {
-            "id": 587,
-            "password": "v22xGkFm@w",
-            "username": "ie_fi_j2zeiw@gmail-app.com"
-        },
-        {
-            "id": 588,
-            "password": "Cw6J@sDpik",
-            "username": "ie_fi_ez7inp@gmail-app.com"
-        },
-        {
-            "id": 589,
-            "password": "wbP@H@Ox2t",
-            "username": "ie_fi_119ulr@gmail-app.com"
-        },
-        {
-            "id": 590,
-            "password": "PY71A7@Cds",
-            "username": "ie_fi_bxc1n3@gmail-app.com"
-        },
-        {
-            "id": 591,
-            "password": "@5RC9t3L1w",
-            "username": "ie_fi_subwbr@gmail-app.com"
-        }
-    ],
-    "ie_hu": [
-        {
-            "id": 604,
-            "password": "VKrh@0qVMN",
-            "username": "ie_hu_ulyvgq@gmail-app.com"
-        },
-        {
-            "id": 605,
-            "password": "T03M@NQuhu",
-            "username": "ie_hu_7gmp17@gmail-app.com"
-        },
-        {
-            "id": 606,
-            "password": "6fGJm281@C",
-            "username": "ie_hu_rkscu3@gmail-app.com"
-        },
-        {
-            "id": 607,
-            "password": "A5j@rKBrcL",
-            "username": "ie_hu_mxpot6@gmail-app.com"
-        },
-        {
-            "id": 608,
-            "password": "4KhzKA@Yxw",
-            "username": "ie_hu_wexkp7@gmail-app.com"
-        },
-        {
-            "id": 609,
-            "password": "5eE@Vws3HZ",
-            "username": "ie_hu_i3fcrs@gmail-app.com"
-        }
-    ],
-    "ie_is": [
-        {
-            "id": 574,
-            "password": "Keu5Rt@o9I",
-            "username": "ie_is_p7ahqa@gmail-app.com"
-        },
-        {
-            "id": 575,
-            "password": "@3fw7HwcSZ",
-            "username": "ie_is_h4yi4y@gmail-app.com"
-        },
-        {
-            "id": 576,
-            "password": "Or1duEJc5@",
-            "username": "ie_is_cwwyxz@gmail-app.com"
-        },
-        {
-            "id": 577,
-            "password": "SMeG3hnD@u",
-            "username": "ie_is_hxik14@gmail-app.com"
-        },
-        {
-            "id": 578,
-            "password": "@jE6IaiZFW",
-            "username": "ie_is_28vm9n@gmail-app.com"
-        },
-        {
-            "id": 579,
-            "password": "fDTgp9ecH@",
-            "username": "ie_is_nn9bjb@gmail-app.com"
-        }
-    ],
-    "ie_nl": [
-        {
-            "id": 423,
-            "password": "VS@OjoMs3f",
-            "username": "ie_nl_quqc36@gmail-app.com"
-        },
-        {
-            "id": 568,
-            "password": "0z@VzYvt9i",
-            "username": "ie_nl_vxzo9t@gmail-app.com"
-        },
-        {
-            "id": 569,
-            "password": "9@5J9UMxwK",
-            "username": "ie_nl_cluyap@gmail-app.com"
-        },
-        {
-            "id": 570,
-            "password": "Ry5Ts7RA@R",
-            "username": "ie_nl_yk3mah@gmail-app.com"
-        },
-        {
-            "id": 571,
-            "password": "Yw6La2Ig@C",
-            "username": "ie_nl_cx2v7z@gmail-app.com"
-        },
-        {
-            "id": 572,
-            "password": "vDrmFxv@1V",
-            "username": "ie_nl_y118ln@gmail-app.com"
-        },
-        {
-            "id": 573,
-            "password": "VaZ0@0I8vS",
-            "username": "ie_nl_x0rh02@gmail-app.com"
-        }
-    ],
-    "sg_fr": [
-        {
-            "id": 580,
-            "password": "y1wm@hf0Bn",
-            "username": "sg_fr_onoy9i@gmail-app.com"
-        },
-        {
-            "id": 581,
-            "password": "p7@@0NnOR8",
-            "username": "sg_fr_k6vuhb@gmail-app.com"
-        },
-        {
-            "id": 582,
-            "password": "D@4OCh2j3I",
-            "username": "sg_fr_cvq08b@gmail-app.com"
-        },
-        {
-            "id": 583,
-            "password": "xT01gsHw@j",
-            "username": "sg_fr_4cn9xj@gmail-app.com"
-        },
-        {
-            "id": 584,
-            "password": "@aprZh7NbA",
-            "username": "sg_fr_m32a2e@gmail-app.com"
-        },
-        {
-            "id": 585,
-            "password": "SR2hnX@ho5",
-            "username": "sg_fr_p0nz5l@gmail-app.com"
-        }
-    ],
-    "gb_fr": [
-        {
-            "id": 0,
-            "username": "romicloud@163.com",
-            "password": "Visafly@111",
-            "lock_until": 0
-        }
-    ],
-    "ie_es": [
-        {
-            "id": 0,
-            "username": "arket_zz@163.com",
-            "password": "dx4ua@!.X.i8Xn8",
-            "lock_until": 0
-        }
-    ],
-    "ie_it": [
-        {
-            "id": 0,
-            "username": "Khandpur1@gmail-app.com",
-            "password": "Visafly@111",
-            "lock_until": 0
-        }
-    ]
-}

+ 1179 - 0
config/config.json

@@ -0,0 +1,1179 @@
+{
+    "redis": {
+        "host": "45.137.220.138",
+        "port": 6379,
+        "db": 0,
+        "password": "STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8"
+    },
+    "group_list": [
+        {
+            "identifier": "vfs.ie.nl",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "session_max_life": 30,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.nl.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.nl.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.nl.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/nld/login",
+            "free_config": {
+                "mission_code": "nld",
+                "mission_name": "Netherlands",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.nl.tourist": {
+                        "center_name": "Netherlands Visa Application Center - Dublin",
+                        "address": "Cunningham House, 130 Francis Street, Dublin 8  D08 H48R",
+                        "vac_code": "NTDB",
+                        "category_name": "All Short stay Categories",
+                        "category_code": "TA",
+                        "subcategory_name": "Tourist",
+                        "subcategory_code": "To"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.sg.fr",
+            "debug": false,
+            "enable": true,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "local",
+            "proxy_cd": 5,
+            "session_max_life": 30,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "sg.fr.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "sg.fr.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.sin.fr.tourist",
+                    "city": "Singapore",
+                    "visa_type": "Tourist",
+                    "country": "France"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/sgp/en/fra/login",
+            "free_config": {
+                "mission_code": "fra",
+                "mission_name": "France",
+                "country_code": "sgp",
+                "country_name": "Singapore",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.sin.fr.tourist": {
+                        "center_name": "France Visa Application Center, Singapore",
+                        "address": "79 Anson Road #15-01 Singapore 079906",
+                        "vac_code": "FRSN",
+                        "category_name": "Short Stay",
+                        "category_code": "02",
+                        "subcategory_name": "Short Stay Tourist, Family Visit",
+                        "subcategory_code": "Six"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.au.fr",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "au.fr.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "au.fr.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.syd.fr.tourist",
+                    "city": "Sydney",
+                    "visa_type": "Tourist",
+                    "country": "France"
+                },
+                {
+                    "weight": 10,
+                    "routing_key": "slot.mel.fr.tourist",
+                    "city": "Melbourne",
+                    "visa_type": "Tourist",
+                    "country": "France"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/aus/en/fra/login",
+            "free_config": {
+                "mission_code": "fra",
+                "mission_name": "France",
+                "country_code": "aus",
+                "country_name": "Australia",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.syd.fr.tourist": {
+                        "center_name": "France Visa Application Center - Sydney",
+                        "address": "France Visa Application Center,Level 6, 88 Pitt Street,Sydney NSW 2000",
+                        "vac_code": "SYD",
+                        "category_name": "VISA",
+                        "category_code": "VISA",
+                        "subcategory_name": "Short Stay Schengen Visa",
+                        "subcategory_code": "ShortStaySchengenVisa"
+                    },
+                    "slot.mel.fr.tourist": {
+                        "center_name": "France Visa Application Center - Melbourne",
+                        "address": "Level 5 332 St. Kilda road level 5 Melbourne 3004",
+                        "vac_code": "MEL",
+                        "category_name": "VISA",
+                        "category_code": "VISA",
+                        "subcategory_name": "Short Stay Schengen Visa",
+                        "subcategory_code": "ShortStaySchengenVisa"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.gb.it",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.it.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.it.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 80,
+                    "routing_key": "slot.lon.it.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Italy"
+                },
+                {
+                    "weight": 20,
+                    "routing_key": "slot.man.it.tourist",
+                    "city": "Manchester",
+                    "visa_type": "Tourist",
+                    "country": "Italy"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/gbr/en/ita/login",
+            "free_config": {
+                "mission_code": "ita",
+                "mission_name": "Italy",
+                "country_code": "gbr",
+                "country_name": "United Kingdom",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.lon.it.tourist": {
+                        "center_name": "Italy Visa Application Centre, London",
+                        "address": "Ground floor, 8- 20  Pocock St London SE1 0BW , United Kingdom",
+                        "vac_code": "ILON",
+                        "category_name": "Italy UK VisaCategory",
+                        "category_code": "UKITVED",
+                        "subcategory_name": "Tourist/ Business/ EU Family",
+                        "subcategory_code": "TBE"
+                    },
+                    "slot.man.it.tourist": {
+                        "center_name": "Italy Visa Application Centre, Manchester",
+                        "address": "50 Devonshire Street North, M12 6JH",
+                        "vac_code": "IMAN",
+                        "category_name": "Italy UK VisaCategory",
+                        "category_code": "UKITVED",
+                        "subcategory_name": "Tourist/ Business/ EU Family",
+                        "subcategory_code": "TBE"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.gb.nl",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.nl.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.nl.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 90,
+                    "routing_key": "slot.lon.nl.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands"
+                },
+                {
+                    "weight": 10,
+                    "routing_key": "slot.man.nl.tourist",
+                    "city": "Manchester",
+                    "visa_type": "Tourist",
+                    "country": "Netherlands"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/gbr/en/nld/login",
+            "free_config": {
+                "mission_code": "nld",
+                "mission_name": "Netherland",
+                "country_code": "gbr",
+                "country_name": "United Kingdom",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.lon.nl.tourist": {
+                        "center_name": "Netherlands Visa application centre - London",
+                        "address": "66 Wilson Street, EC2A 2BT",
+                        "vac_code": "NAKN",
+                        "category_name": "Schengen Visa",
+                        "category_code": "Schengen Visa",
+                        "subcategory_name": "Tourism",
+                        "subcategory_code": "TA"
+                    },
+                    "slot.man.nl.tourist": {
+                        "center_name": "Netherlands Visa application centre - Manchester",
+                        "address": "50 Devonshire Street North, M12 6JH",
+                        "vac_code": "NAKT",
+                        "category_name": "Schengen Visa",
+                        "category_code": "Schengen Visa",
+                        "subcategory_name": "Tourism",
+                        "subcategory_code": "TA"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.gb.no",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.no.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.no.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.lon.no.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Norway"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/gbr/en/nor/login",
+            "free_config": {
+                "mission_code": "nor",
+                "mission_name": "Norway",
+                "country_code": "gbr",
+                "country_name": "United Kingdom",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.lon.no.tourist": {
+                        "center_name": "Norway Visa Application Centre, London",
+                        "address": "66 Wilson street, EC2A 2BT",
+                        "vac_code": "NLON",
+                        "category_name": "Schengen Visa C",
+                        "category_code": "SCHVISA",
+                        "subcategory_name": "Tourist Visa",
+                        "subcategory_code": "TOU"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.ie.at",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "local",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.at.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.at.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.at.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Austria"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/aut/login",
+            "free_config": {
+                "mission_code": "aut",
+                "mission_name": "Austria",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.at.tourist": {
+                        "center_name": "Austria / Switzerland / Liechtenstein/ Slovenia Visa Application Center, Dublin",
+                        "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                        "vac_code": "AUT-DUB",
+                        "category_name": "Other Visas",
+                        "category_code": "Default_Austria_Ireland ",
+                        "subcategory_name": "All Visas ",
+                        "subcategory_code": "TA"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.ie.dk",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.dk.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.dk.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.dk.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Denmark"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/dnk/login",
+            "free_config": {
+                "mission_code": "dnk",
+                "mission_name": "Denmark",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.dk.tourist": {
+                        "center_name": "Denmark Visa Application Center, Dublin ",
+                        "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                        "vac_code": "DIDUB",
+                        "category_name": "Schengen Visa",
+                        "category_code": "SV",
+                        "subcategory_name": "Tourism",
+                        "subcategory_code": "TV"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.ie.fi",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.fi.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.fi.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.fi.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Finland"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/fin/login",
+            "free_config": {
+                "mission_code": "fin",
+                "mission_name": "Finland",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.fi.tourist": {
+                        "center_name": "Application Centre, Dublin",
+                        "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
+                        "vac_code": "Dubb",
+                        "category_name": "VISA",
+                        "category_code": "S S",
+                        "subcategory_name": "Tourist Category",
+                        "subcategory_code": "Tourist Category"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.ie.hu",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.hu.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.hu.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.hu.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Hungary"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/hun/login",
+            "free_config": {
+                "mission_code": "hun",
+                "mission_name": "Hungary",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.hu.tourist": {
+                        "center_name": "Ireland Visa Application Center,Dublin",
+                        "address": "Cunningham House, 130 Francis Street Dublin",
+                        "vac_code": "DUB",
+                        "category_name": "Short Stay",
+                        "category_code": "SS",
+                        "subcategory_name": "Schengen Visa",
+                        "subcategory_code": "Schengen Visa"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.ie.is",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.is.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.is.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.is.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Iceland"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/irl/en/isl/login",
+            "free_config": {
+                "mission_code": "isl",
+                "mission_name": "Iceland",
+                "country_code": "irl",
+                "country_name": "Ireland",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.dub.is.tourist": {
+                        "center_name": "Iceland Visa Application Center- Dublin",
+                        "address": "Cunningham House, 130 Francis Street, Dublin, Ireland- DO8 H48R",
+                        "vac_code": "DUB",
+                        "category_name": "C-Visa",
+                        "category_code": "CVI",
+                        "subcategory_name": "Tourism",
+                        "subcategory_code": "OTT"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "vfs.gb.at",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "local",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.at.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.at.booker",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.lon.at.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Austria"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/gbr/en/aut/login",
+            "free_config": {
+                "mission_code": "aut",
+                "mission_name": "Austria",
+                "country_code": "gbr",
+                "country_name": "United Kingdom",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.lon.at.tourist": {
+                        "center_name": "Austria Visa Application Centre, London",
+                        "address": "66 Wilson Street, EC2A 2BT",
+                        "vac_code": "ADN",
+                        "category_name": "Visa  to  Austria",
+                        "category_code": "Visa  to  Austria",
+                        "subcategory_name": "Tourism",
+                        "subcategory_code": "TA"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "bls.ie.es",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "iproyal",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.es.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "order",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 1
+            },
+            "session_max_life": 60,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 180,
+                "random_max": 360
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "bls_plugin",
+                "plugin_bin": "bls_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.es.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Spain"
+                }
+            ],
+            "website": "https://ireland.blsspainglobal.com/Global/bls/visatypeverification",
+            "free_config": {
+                "domain": "ireland.blsspainglobal.com",
+                "ocr_model": "data/ctc.pth",
+                "apt_configs": {
+                    "slot.dub.es.tourist": {
+                        "location": "Dublin",
+                        "jurisdiction": null,
+                        "visa_type": "Schengen Visa/ Short Term Visa",
+                        "visa_subtype": "Tourist Visa",
+                        "appointment_type": "Individual",
+                        "appointment_category": "Normal",
+                        "mission_code": "EMBASSY_DUBLIN"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "bls.gb.es",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "iproyal",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.es.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "order",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 1
+            },
+            "session_max_life": 60,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "bls_plugin",
+                "plugin_bin": "bls_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.lon.es.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "Spain"
+                }
+            ],
+            "website": "https://uk.blsspainglobal.com/Global/bls/visatypeverification",
+            "free_config": {
+                "domain": "uk.blsspainglobal.com",
+                "ocr_model": "data/ocr.pth",
+                "apt_configs": {
+                    "slot.lon.es.tourist": {
+                        "location": "Dublin",
+                        "jurisdiction": "Greater London",
+                        "visa_type": "Short Term Visa(Maximum stay of 90 days)",
+                        "visa_subtype": "Tourist Visa",
+                        "appointment_type": "Individual",
+                        "appointment_category": "Normal",
+                        "mission_code": "LHR"
+                    }
+                }
+            }
+        },
+        {
+            "identifier": "tls.gb.fr",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "iproyal",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "gb.fr.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "order",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 1
+            },
+            "session_max_life": 30,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "tls_plugin",
+                "plugin_bin": "tls_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.lon.fr.tourist",
+                    "city": "London",
+                    "visa_type": "Tourist",
+                    "country": "France"
+                }
+            ],
+            "website": "https://visas-fr.tlscontact.com/country/gb/vac/gbLON2fr/",
+            "free_config": {
+                "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
+                "apt_config": {
+                    "code": "gbLON2fr",
+                    "country": "gb",
+                    "mission": "fr",
+                    "city": "London"
+                },
+                "interest_month": "02-2026",
+                "target_labels": [
+                    "",
+                    "pta"
+                ]
+            }
+        },
+        {
+            "identifier": "visametric.ie.de",
+            "debug": false,
+            "enable": false,
+            "need_account": false,
+            "need_proxy": true,
+            "proxy_pool": "iproyal",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "",
+                "account_pool_id": "",
+                "target_instances": 1,
+                "account_cd": 0,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "",
+                "account_pool_id": "",
+                "target_instances": 1,
+                "account_cd": 0,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 1
+            },
+            "session_max_life": 15,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "de_plugin",
+                "plugin_bin": "de_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.de.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Germany"
+                }
+            ],
+            "website": "https://ie-appointment.visametric.com/en",
+            "free_config": {
+                "base_url": "https://ie-appointment.visametric.com",
+                "consularid": 1
+            }
+        },
+        {
+            "identifier": "pernotami.ie.it",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "local",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.it.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "order",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 1
+            },
+            "session_max_life": 15,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "ita_plugin",
+                "plugin_bin": "ita_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.dub.it.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Italy"
+                }
+            ],
+            "website": "https://prenotami.esteri.it/Home",
+            "free_config": {
+                "capsolver_key": "03db1d1ff2f4a33e84ef1da99bd83336bed3710153525"
+            }
+        },
+        {
+            "identifier": "greekemba.ie.gr",
+            "debug": false,
+            "enable": false,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": "isp_all",
+            "proxy_cd": 5,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "ie.gr.sentinel",
+                "target_instances": 1,
+                "account_cd": 180,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "order",
+                "target_instances": 1,
+                "account_cd": 180,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 2
+            },
+            "session_max_life": 15,
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "grc_plugin",
+                "plugin_bin": "grc_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "slot": "slot.dub.gr.tourist",
+                    "city": "Dublin",
+                    "visa_type": "Tourist",
+                    "country": "Greece"
+                }
+            ],
+            "website": "https://www.supersaas.com/schedule/GreekEmbassyInDublin/Visas",
+            "free_config": {}
+        }
+    ]
+}

+ 0 - 958
config/groups.json

@@ -1,958 +0,0 @@
-[
-    {
-        "identifier": "VFS_IE_NL",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_nl",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.nl.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Netherlands"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/nld/login",
-        "free_config": {
-            "mission_code": "nld",
-            "mission_name": "Netherlands",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.nl.tourist": {
-                    "center_name": "Netherlands Visa Application Center - Dublin",
-                    "address": "Cunningham House, 130 Francis Street, Dublin 8  D08 H48R",
-                    "vac_code": "NTDB",
-                    "category_name": "All Short stay Categories",
-                    "category_code": "TA",
-                    "subcategory_name": "Tourist",
-                    "subcategory_code": "To"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_SG_FR",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "sg_fr",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.sin.fr.tourist",
-                "city": "Singapore",
-                "visa_type": "Tourist",
-                "country": "France"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/sgp/en/fra/login",
-        "free_config": {
-            "mission_code": "fra",
-            "mission_name": "France",
-            "country_code": "sgp",
-            "country_name": "Singapore",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.sin.fr.tourist": {
-                    "center_name": "France Visa Application Center, Singapore",
-                    "address": "79 Anson Road #15-01 Singapore 079906",
-                    "vac_code": "FRSN",
-                    "category_name": "Short Stay",
-                    "category_code": "02",
-                    "subcategory_name": "Short Stay Tourist, Family Visit",
-                    "subcategory_code": "Six"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_AU_FR",
-        "debug": false,
-        "enable": false,
-        "need_account": true,
-        "local_account_pool": "au_fr",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.syd.fr.tourist",
-                "city": "Sydney",
-                "visa_type": "Tourist",
-                "country": "France"
-            },
-            {
-                "weight": 10,
-                "routing_key": "slot.mel.fr.tourist",
-                "city": "Melbourne",
-                "visa_type": "Tourist",
-                "country": "France"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/aus/en/fra/login",
-        "free_config": {
-            "mission_code": "fra",
-            "mission_name": "France",
-            "country_code": "aus",
-            "country_name": "Australia",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.syd.fr.tourist": {
-                    "center_name": "France Visa Application Center - Sydney",
-                    "address": "France Visa Application Center,Level 6, 88 Pitt Street,Sydney NSW 2000",
-                    "vac_code": "SYD",
-                    "category_name": "VISA",
-                    "category_code": "VISA",
-                    "subcategory_name": "Short Stay Schengen Visa",
-                    "subcategory_code": "ShortStaySchengenVisa"
-                },
-                "slot.mel.fr.tourist": {
-                    "center_name": "France Visa Application Center - Melbourne",
-                    "address": "Level 5 332 St. Kilda road level 5 Melbourne 3004",
-                    "vac_code": "MEL",
-                    "category_name": "VISA",
-                    "category_code": "VISA",
-                    "subcategory_name": "Short Stay Schengen Visa",
-                    "subcategory_code": "ShortStaySchengenVisa"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_GB_IT",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "gb_it",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 80,
-                "routing_key": "slot.lon.it.tourist",
-                "city": "London",
-                "visa_type": "Tourist",
-                "country": "Italy"
-            },
-            {
-                "weight": 20,
-                "routing_key": "slot.man.it.tourist",
-                "city": "Manchester",
-                "visa_type": "Tourist",
-                "country": "Italy"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/gbr/en/ita/login",
-        "free_config": {
-            "mission_code": "ita",
-            "mission_name": "Italy",
-            "country_code": "gbr",
-            "country_name": "United Kingdom",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.lon.it.tourist": {
-                    "center_name": "Italy Visa Application Centre, London",
-                    "address": "Ground floor, 8- 20  Pocock St London SE1 0BW , United Kingdom",
-                    "vac_code": "ILON",
-                    "category_name": "Italy UK VisaCategory",
-                    "category_code": "UKITVED",
-                    "subcategory_name": "Tourist/ Business/ EU Family",
-                    "subcategory_code": "TBE"
-                },
-                "slot.man.it.tourist": {
-                    "center_name": "Italy Visa Application Centre, Manchester",
-                    "address": "50 Devonshire Street North, M12 6JH",
-                    "vac_code": "IMAN",
-                    "category_name": "Italy UK VisaCategory",
-                    "category_code": "UKITVED",
-                    "subcategory_name": "Tourist/ Business/ EU Family",
-                    "subcategory_code": "TBE"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_GB_NL",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "gb_nl",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 90,
-                "routing_key": "slot.lon.nl.tourist",
-                "city": "London",
-                "visa_type": "Tourist",
-                "country": "Netherlands"
-            },
-            {
-                "weight": 10,
-                "routing_key": "slot.man.nl.tourist",
-                "city": "Manchester",
-                "visa_type": "Tourist",
-                "country": "Netherlands"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/gbr/en/nld/login",
-        "free_config": {
-            "mission_code": "nld",
-            "mission_name": "Netherland",
-            "country_code": "gbr",
-            "country_name": "United Kingdom",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.lon.nl.tourist": {
-                    "center_name": "Netherlands Visa application centre - London",
-                    "address": "66 Wilson Street, EC2A 2BT",
-                    "vac_code": "NAKN",
-                    "category_name": "Schengen Visa",
-                    "category_code": "Schengen Visa",
-                    "subcategory_name": "Tourism",
-                    "subcategory_code": "TA"
-                },
-                "slot.man.nl.tourist": {
-                    "center_name": "Netherlands Visa application centre - Manchester",
-                    "address": "50 Devonshire Street North, M12 6JH",
-                    "vac_code": "NAKT",
-                    "category_name": "Schengen Visa",
-                    "category_code": "Schengen Visa",
-                    "subcategory_name": "Tourism",
-                    "subcategory_code": "TA"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_GB_NO",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "gb_no",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.lon.no.tourist",
-                "city": "London",
-                "visa_type": "Tourist",
-                "country": "Norway"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/gbr/en/nor/login",
-        "free_config": {
-            "mission_code": "nor",
-            "mission_name": "Norway",
-            "country_code": "gbr",
-            "country_name": "United Kingdom",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.lon.no.tourist": {
-                    "center_name": "Norway Visa Application Centre, London",
-                    "address": "66 Wilson street, EC2A 2BT",
-                    "vac_code": "NLON",
-                    "category_name": "Schengen Visa C",
-                    "category_code": "SCHVISA",
-                    "subcategory_name": "Tourist Visa",
-                    "subcategory_code": "TOU"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_IE_AT",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_at",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.at.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Austria"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/aut/login",
-        "free_config": {
-            "mission_code": "aut",
-            "mission_name": "Austria",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.at.tourist": {
-                    "center_name": "Austria / Switzerland / Liechtenstein/ Slovenia Visa Application Center, Dublin",
-                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
-                    "vac_code": "AUT-DUB",
-                    "category_name": "Other Visas",
-                    "category_code": "Default_Austria_Ireland ",
-                    "subcategory_name": "All Visas ",
-                    "subcategory_code": "TA"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_IE_DK",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_dk",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.dk.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Denmark"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/dnk/login",
-        "free_config": {
-            "mission_code": "dnk",
-            "mission_name": "Denmark",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.dk.tourist": {
-                    "center_name": "Denmark Visa Application Center, Dublin ",
-                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
-                    "vac_code": "DIDUB",
-                    "category_name": "Schengen Visa",
-                    "category_code": "SV",
-                    "subcategory_name": "Tourism",
-                    "subcategory_code": "TV"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_IE_FI",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_fi",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.fi.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Finland"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/fin/login",
-        "free_config": {
-            "mission_code": "fin",
-            "mission_name": "Finland",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.fi.tourist": {
-                    "center_name": "Application Centre, Dublin",
-                    "address": "Cunningham House, 130 Francis Street, Dublin 8 D08 H48R",
-                    "vac_code": "Dubb",
-                    "category_name": "VISA",
-                    "category_code": "S S",
-                    "subcategory_name": "Tourist Category",
-                    "subcategory_code": "Tourist Category"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_IE_HU",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_hu",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.hu.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Hungary"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/hun/login",
-        "free_config": {
-            "mission_code": "hun",
-            "mission_name": "Hungary",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.hu.tourist": {
-                    "center_name": "Ireland Visa Application Center,Dublin",
-                    "address": "Cunningham House, 130 Francis Street Dublin",
-                    "vac_code": "DUB",
-                    "category_name": "Short Stay",
-                    "category_code": "SS",
-                    "subcategory_name": "Schengen Visa",
-                    "subcategory_code": "Schengen Visa"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "VFS_IE_IS",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_is",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 180,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "vfs_plugin2",
-            "plugin_bin": "vfs_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.is.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Iceland"
-            }
-        ],
-        "website": "https://visa.vfsglobal.com/irl/en/isl/login",
-        "free_config": {
-            "mission_code": "isl",
-            "mission_name": "Iceland",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.is.tourist": {
-                    "center_name": "Iceland Visa Application Center- Dublin",
-                    "address": "Cunningham House, 130 Francis Street, Dublin, Ireland- DO8 H48R",
-                    "vac_code": "DUB",
-                    "category_name": "C-Visa",
-                    "category_code": "CVI",
-                    "subcategory_name": "Tourism",
-                    "subcategory_code": "OTT"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "BLS_IE_ES",
-        "debug": false,
-        "enable": true,
-        "need_account": true,
-        "local_account_pool": "ie_es",
-        "need_proxy": true,
-        "proxy_pool": "iproyal",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 30,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 60,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 180,
-            "random_max": 360
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "bls_plugin",
-            "plugin_bin": "bls_plugin.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.es.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Spain"
-            }
-        ],
-        "website": "https://ireland.blsspainglobal.com/Global/bls/visatypeverification",
-        "free_config": {
-            "domain": "ireland.blsspainglobal.com",
-            "ocr_model": "data/ctc.pth",
-            "apt_configs": {
-                "slot.dub.es.tourist": {
-                    "location": "Dublin",
-                    "jurisdiction": null,
-                    "visa_type": "Schengen Visa/ Short Term Visa",
-                    "visa_subtype": "Tourist Visa",
-                    "appointment_type": "Individual",
-                    "appointment_category": "Normal",
-                    "mission_code": "EMBASSY_DUBLIN"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "BLS_GB_ES",
-        "debug": false,
-        "enable": false,
-        "need_account": true,
-        "local_account_pool": "gb_es",
-        "need_proxy": true,
-        "proxy_pool": "iproyal",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 30,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 60,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "bls_plugin",
-            "plugin_bin": "bls_plugin.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.lon.es.tourist",
-                "city": "London",
-                "visa_type": "Tourist",
-                "country": "Spain"
-            }
-        ],
-        "website": "https://uk.blsspainglobal.com/Global/bls/visatypeverification",
-        "free_config": {
-            "domain": "uk.blsspainglobal.com",
-            "ocr_model": "data/ocr.pth",
-            "apt_configs": {
-                "slot.lon.es.tourist": {
-                    "location": "Dublin",
-                    "jurisdiction": "Greater London",
-                    "visa_type": "Short Term Visa(Maximum stay of 90 days)",
-                    "visa_subtype": "Tourist Visa",
-                    "appointment_type": "Individual",
-                    "appointment_category": "Normal",
-                    "mission_code": "LHR"
-                }
-            }
-        }
-    },
-    {
-        "identifier": "TLS_GB_FR",
-        "debug": false,
-        "enable": false,
-        "need_account": true,
-        "local_account_pool": "gb_fr",
-        "need_proxy": true,
-        "proxy_pool": "iproyal",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 30,
-        "order_account_routing": "auto.slot.lon.fr.tourist",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": true,
-        "session_max_life": 30,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "tls_plugin2",
-            "plugin_bin": "tls_plugin2.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.lon.fr.tourist",
-                "city": "London",
-                "visa_type": "Tourist",
-                "country": "France"
-            }
-        ],
-        "website": "https://visas-fr.tlscontact.com/country/gb/vac/gbLON2fr/",
-        "free_config": {
-            "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
-            "apt_config": {
-                "code": "gbLON2fr",
-                "country": "gb",
-                "mission": "fr",
-                "city": "London"
-            },
-            "interest_month": "02-2026",
-            "target_labels": [
-                "",
-                "pta"
-            ]
-        }
-    },
-    {
-        "identifier": "VISAMETRIC_IE_DE",
-        "debug": false,
-        "enable": false,
-        "need_account": false,
-        "local_account_pool": "",
-        "need_proxy": true,
-        "proxy_pool": "iproyal",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 30,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 15,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "de_plugin",
-            "plugin_bin": "de_plugin.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.de.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Germany"
-            }
-        ],
-        "website": "https://ie-appointment.visametric.com/en",
-        "free_config": {
-            "base_url": "https://ie-appointment.visametric.com",
-            "consularid": 1
-        }
-    },
-    {
-        "identifier": "PRENOTAMI_IE_IT",
-        "debug": false,
-        "enable": false,
-        "need_account": true,
-        "local_account_pool": "ie_it",
-        "need_proxy": true,
-        "proxy_pool": "local",
-        "proxy_lock_interval": 5,
-        "target_instances": 1,
-        "account_login_interval": 0,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 15,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "ita_plugin",
-            "plugin_bin": "ita_plugin.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.it.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Italy"
-            }
-        ],
-        "website": "https://prenotami.esteri.it/Home",
-        "free_config": {
-            "capsolver_key": "03db1d1ff2f4a33e84ef1da99bd83336bed3710153525"
-        }
-    },
-    {
-        "identifier": "GreekEmbassy_IE_GR",
-        "debug": false,
-        "enable": false,
-        "need_account": true,
-        "local_account_pool": "ie_gr",
-        "need_proxy": true,
-        "proxy_pool": "isp_all",
-        "proxy_lock_interval": 5,
-        "target_instances": 4,
-        "account_login_interval": 1,
-        "order_account_routing": "",
-        "order_account_online_limit": 0,
-        "account_bind_applicant": false,
-        "session_max_life": 15,
-        "query_wait": {
-            "mode": "Random",
-            "fixed_wait": 10,
-            "random_min": 60,
-            "random_max": 300
-        },
-        "plugin_config": {
-            "lib_path": "plugins",
-            "plugin_name": "grc_plugin",
-            "plugin_bin": "grc_plugin.py",
-            "plugin_proto": "IVSPlg"
-        },
-        "appointment_types": [
-            {
-                "weight": 10,
-                "routing_key": "slot.dub.gr.tourist",
-                "city": "Dublin",
-                "visa_type": "Tourist",
-                "country": "Greece"
-            }
-        ],
-        "website": "https://www.supersaas.com/schedule/GreekEmbassyInDublin/Visas",
-        "free_config": {}
-    }
-]

+ 0 - 188
core/app_manager.py

@@ -1,188 +0,0 @@
-# core/app_manager.py
-import json
-import os
-import threading
-from typing import Dict, List, Optional
-
-# 引入核心组件
-from vs_types import GroupConfig, NotFoundError, BizLogicError
-from gco_wrapper import GCOWrapper
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_WARN
-from core.plugin_reloader import reload_plugin_module
-
-class AppManager:
-    _instance = None
-    _lock = threading.Lock()
-
-    def __new__(cls):
-        with cls._lock:
-            if cls._instance is None:
-                cls._instance = super().__new__(cls)
-                cls._instance._init()
-            return cls._instance
-
-    @staticmethod
-    def Instance():
-        return AppManager()
-
-    def _init(self):
-        self.executors: Dict[str, GCOWrapper] = {}   # group_id -> GCOWrapper
-        self.configs: Dict[str, GroupConfig] = {}    # group_id -> GroupConfig
-        self.config_file = "config/groups.json"
-
-    def load_configs(self):
-        """读取并解析配置文件"""
-        if not os.path.exists(self.config_file):
-            raise NotFoundError(message=f'Config file not found: {self.config_file}')
-
-        with open(self.config_file, 'r', encoding='utf-8') as f:
-            data = json.load(f)
-        
-        for item in data:
-            # JSON -> GroupConfig 转换
-            grp_cfg = GroupConfig.from_json(item)
-            self.configs[grp_cfg.identifier] = grp_cfg
-            
-        VSC_INFO("app_mgr", f"Loaded {len(self.configs)} group configurations.")
-
-    def start_all(self):
-        """启动所有 enable=True 的组"""
-        for gid, cfg in self.configs.items():
-            if cfg.enable and gid not in self.executors:
-                self.start_group(gid)
-
-    def start_group(self, group_id: str):
-        with self._lock:
-            if group_id not in self.configs:
-                raise NotFoundError(message=f"Group {group_id} not found in config")
-            
-            if group_id in self.executors:
-                VSC_WARN("app_mgr", f"Group {group_id} is already running")
-                return
-
-            cfg = self.configs[group_id]
-            gco = GCOWrapper(cfg)
-            gco.load()
-            gco.start()
-            self.executors[group_id] = gco
-            VSC_INFO("app_mgr", f"Started group: {group_id}")
-
-    def stop_group(self, group_id: str):
-        with self._lock:
-            if group_id not in self.executors:
-                raise NotFoundError(message=f"Group {group_id} not found in config")
-            
-            VSC_INFO("app_mgr", f"Stopping group: {group_id}...")
-            self.executors[group_id].stop()
-            del self.executors[group_id]
-            VSC_INFO("app_mgr", f"Stopped group: {group_id}")
-
-    def restart_group(self, group_id: str):
-        self.stop_group(group_id)
-        self.start_group(group_id)
-
-    def ota_upgrade_plugin(self, plugin_name: str) -> List[str]:
-        """
-        OTA 热更新流程:
-        1. 找到所有使用该插件的运行中组
-        2. 停止这些组
-        3. 清除 Python 模块缓存 (热重载)
-        4. 重新启动这些组
-        """
-        affected_groups = []
-        with self._lock:
-            # 1. 查找受影响的组
-            for gid, exec in self.executors.items():
-                # 注意:这里我们访问 coord 私有成员,实际工程中建议添加 getter
-                # 假设 config 存储在 m_cfg
-                if exec.m_cfg.plugin_config.plugin_name == plugin_name:
-                    affected_groups.append(gid)
-        
-        VSC_INFO("app_mgr", f"OTA Update for '{plugin_name}'. Affected groups: {affected_groups}")
-        
-        # 2. 停止
-        for gid in affected_groups:
-            self.stop_group(gid)
-            
-        # 3. 卸载模块
-        reload_plugin_module(plugin_name)
-        
-        # 4. 重启
-        restarted = []
-        for gid in affected_groups:
-            if self.start_group(gid):
-                restarted.append(gid)
-                
-        return restarted
-    
-    def get_group_config(self, group_id: str):
-        with self._lock:
-            if group_id not in self.configs:
-                return False
-            group_config = self.configs.get(group_id)
-            return group_config.to_json()
-    
-    # ----------------- 更新配置(局部) -----------------
-    def ota_update_plugin_config(self, group_id: str, new_config_str: str):
-        """更新某个 Executor 的配置(只影响单个 Executor)"""
-        with self._lock:
-            new_config = GroupConfig.from_json(json.loads(new_config_str))
-            old_exe = self.executors.get(group_id)
-            if not old_exe:
-                # Executor 没运行,直接创建启动
-                self.configs[group_id] = new_config
-                exe = GCOWrapper(new_config)
-                exe.load()
-                exe.start()
-                self.executors[group_id] = exe
-                return
-
-            # 创建新 Executor(插件不变,只更新配置)
-            new_exe = GCOWrapper(new_config)
-            new_exe.load()
-            new_exe.start()
-            old_exe.stop()
-            self.executors[group_id] = new_exe
-            VSC_INFO("app_mgr", f"Config update applied for {group_id}")
-     
-
-    def get_status(self):
-        """获取所有组的状态"""
-        with self._lock:
-            status_list = []
-            for gid, cfg in self.configs.items():
-                running = gid in self.executors
-                status_list.append({
-                    "id": gid,
-                    "plugin": cfg.plugin_config.plugin_name,
-                    "running": running,
-                    "instances": cfg.target_instances if running else 0,
-                    "local_account_pool": cfg.local_account_pool,
-                    "proxies_pool": cfg.proxy_pool
-                })
-            return status_list
-    
-    def subscribe_executor_logs(self, group_id: str, callback):
-        """
-        订阅某个 Executor 的日志
-        callback: Callable[[str], None]
-        """
-        with self._lock:
-            if group_id not in self.executors:
-                raise ValueError(f"Executor {group_id} not running")
-
-            exe = self.executors[group_id]
-            exe.subscribe_logs(callback)
-
-
-    def unsubscribe_executor_logs(self, group_id: str, callback):
-        """
-        取消订阅日志
-        """
-        with self._lock:
-            exe = self.executors.get(group_id)
-            if not exe:
-                return
-            exe.unsubscribe_logs(callback)
-
-    

+ 0 - 37
core/plugin_reloader.py

@@ -1,37 +0,0 @@
-# core/plugin_reloader.py
-import sys
-import importlib
-from vs_log_macros import VSC_INFO, VSC_WARN
-
-def reload_plugin_module(plugin_name: str):
-    """
-    强制卸载并重载插件模块,实现 OTA 热更新
-    """
-    # 1. 在 sys.modules 中查找相关模块
-    # 插件可能导入了其他子模块,这里简单处理:移除主插件模块
-    # 如果插件有 __init__.py 且是一个包,逻辑类似
-    
-    modules_to_remove = []
-    for mod_name in sys.modules.keys():
-        # 假设插件名就是模块名,或者模块名以插件名开头
-        # 例如 plugin_name="bls_plugin", 模块可能是 "plugins.bls_plugin"
-        if plugin_name in mod_name:
-            modules_to_remove.append(mod_name)
-    
-    if not modules_to_remove:
-        VSC_WARN("reloader", f"Plugin module '{plugin_name}' not found in memory. First load?")
-        return
-
-    # 2. 移除模块
-    for mod in modules_to_remove:
-        try:
-            del sys.modules[mod]
-            VSC_INFO("reloader", f"Unloaded module: {mod}")
-        except Exception as e:
-            VSC_WARN("reloader", f"Failed to unload {mod}: {e}")
-            
-    # 3. 强制垃圾回收 (可选,依赖引用计数)
-    import gc
-    gc.collect()
-    
-    VSC_INFO("reloader", f"Plugin '{plugin_name}' reloaded (ready for fresh import).")

+ 1 - 1
data/proxy_states.json

@@ -1,3 +1,3 @@
 {
-    "isp_proxy::100011": 1769627541.849858
+    "local::100029": 1775414740.931227
 }

+ 26 - 5
docker-compose.yml

@@ -1,15 +1,36 @@
 version: '3.8'
 
 services:
-  visa-bot:
+  visa-sentinel:
     build: .
     image: coordinator:latest
-    container_name: coordinator
+    container_name: coordinator-sentinel
+    command: ["python3", "main_sentinel.py"]
+    restart: unless-stopped
+    shm_size: '2gb'
+    volumes:
+      - ./config:/app/config
+      - ./logs:/app/logs
+      - ./data:/app/data
+      - ./plugins:/app/plugins
+    environment:
+      - TZ=Asia/Shanghai
+      - DISPLAY=:99
+      - CHROME_BIN=/usr/bin/chromium
+    # 资源限制
+    deploy:
+      resources:
+        limits:
+          cpus: '2.0'
+          memory: 4G
+
+  visa-booker:
+    build: .
+    image: coordinator:latest
+    container_name: coordinator-booker
+    command: ["python3", "main_booker.py"]
     restart: unless-stopped
-    # 【关键】Chromium 在 Docker 中必须要有足够的共享内存
     shm_size: '2gb'
-    ports:
-      - "127.0.0.1:8002:8000" # 如果你有 FastAPI 服务
     volumes:
       - ./config:/app/config
       - ./logs:/app/logs

+ 7 - 3
entrypoint.sh

@@ -6,7 +6,6 @@ rm -f /tmp/.X99-lock
 rm -f /var/run/dbus/pid
 
 # 2. [新增] 启动 DBus 服务
-# 这解决了 Failed to connect to socket /run/dbus/system_bus_socket 错误
 mkdir -p /var/run/dbus
 dbus-daemon --config-file=/usr/share/dbus-1/system.conf --print-address &
 
@@ -17,5 +16,10 @@ Xvfb :99 -ac -screen 0 1920x1080x24 -nolisten tcp &
 echo "Waiting for Xvfb and DBus..."
 sleep 3
 
-# 4. 启动 Python
-python3 main_server.py
+# 4. 执行传递给容器的命令
+# 如果没有传命令,默认执行 python3 main_sentinel.py (为了兼容你以前的习惯)
+if [ $# -eq 0 ]; then
+    exec python3 main_sentinel.py
+else
+    exec "$@"
+fi

+ 0 - 12
front/index.html

@@ -1,12 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Visa Plugin Manager</title>
-  </head>
-  <body>
-    <div id="root"></div>
-    <script type="module" src="/src/main.jsx"></script>
-  </body>
-</html>

+ 0 - 6068
front/package-lock.json

@@ -1,6068 +0,0 @@
-{
-    "name": "visa-plugin-manager",
-    "version": "0.1.0",
-    "lockfileVersion": 3,
-    "requires": true,
-    "packages": {
-        "": {
-            "name": "visa-plugin-manager",
-            "version": "0.1.0",
-            "dependencies": {
-                "@ant-design/icons": "^5.6.1",
-                "@monaco-editor/react": "^4.7.0",
-                "@tanstack/react-query": "^5.90.16",
-                "antd": "^5.29.3",
-                "axios": "^1.13.2",
-                "react": "^18.2.0",
-                "react-dom": "^18.2.0"
-            },
-            "devDependencies": {
-                "@types/react": "^18.2.43",
-                "@types/react-dom": "^18.2.17",
-                "@vitejs/plugin-react": "^4.2.1",
-                "eslint": "^8.55.0",
-                "eslint-plugin-react": "^7.33.2",
-                "eslint-plugin-react-hooks": "^4.6.0",
-                "eslint-plugin-react-refresh": "^0.4.5",
-                "vite": "^5.0.8"
-            }
-        },
-        "node_modules/@ant-design/colors": {
-            "version": "7.2.1",
-            "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz",
-            "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@ant-design/fast-color": "^2.0.6"
-            }
-        },
-        "node_modules/@ant-design/cssinjs": {
-            "version": "1.24.0",
-            "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
-            "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.1",
-                "@emotion/hash": "^0.8.0",
-                "@emotion/unitless": "^0.7.5",
-                "classnames": "^2.3.1",
-                "csstype": "^3.1.3",
-                "rc-util": "^5.35.0",
-                "stylis": "^4.3.4"
-            },
-            "peerDependencies": {
-                "react": ">=16.0.0",
-                "react-dom": ">=16.0.0"
-            }
-        },
-        "node_modules/@ant-design/cssinjs-utils": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
-            "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
-            "license": "MIT",
-            "dependencies": {
-                "@ant-design/cssinjs": "^1.21.0",
-                "@babel/runtime": "^7.23.2",
-                "rc-util": "^5.38.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@ant-design/fast-color": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
-            "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.24.7"
-            },
-            "engines": {
-                "node": ">=8.x"
-            }
-        },
-        "node_modules/@ant-design/icons": {
-            "version": "5.6.1",
-            "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz",
-            "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
-            "license": "MIT",
-            "dependencies": {
-                "@ant-design/colors": "^7.0.0",
-                "@ant-design/icons-svg": "^4.4.0",
-                "@babel/runtime": "^7.24.8",
-                "classnames": "^2.2.6",
-                "rc-util": "^5.31.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "peerDependencies": {
-                "react": ">=16.0.0",
-                "react-dom": ">=16.0.0"
-            }
-        },
-        "node_modules/@ant-design/icons-svg": {
-            "version": "4.4.2",
-            "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
-            "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
-            "license": "MIT"
-        },
-        "node_modules/@ant-design/react-slick": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
-            "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.4",
-                "classnames": "^2.2.5",
-                "json2mq": "^0.2.0",
-                "resize-observer-polyfill": "^1.5.1",
-                "throttle-debounce": "^5.0.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0"
-            }
-        },
-        "node_modules/@babel/code-frame": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.28.6.tgz",
-            "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-validator-identifier": "^7.28.5",
-                "js-tokens": "^4.0.0",
-                "picocolors": "^1.1.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/compat-data": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.6.tgz",
-            "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/core": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.6.tgz",
-            "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/code-frame": "^7.28.6",
-                "@babel/generator": "^7.28.6",
-                "@babel/helper-compilation-targets": "^7.28.6",
-                "@babel/helper-module-transforms": "^7.28.6",
-                "@babel/helpers": "^7.28.6",
-                "@babel/parser": "^7.28.6",
-                "@babel/template": "^7.28.6",
-                "@babel/traverse": "^7.28.6",
-                "@babel/types": "^7.28.6",
-                "@jridgewell/remapping": "^2.3.5",
-                "convert-source-map": "^2.0.0",
-                "debug": "^4.1.0",
-                "gensync": "^1.0.0-beta.2",
-                "json5": "^2.2.3",
-                "semver": "^6.3.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/babel"
-            }
-        },
-        "node_modules/@babel/generator": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz",
-            "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/parser": "^7.28.6",
-                "@babel/types": "^7.28.6",
-                "@jridgewell/gen-mapping": "^0.3.12",
-                "@jridgewell/trace-mapping": "^0.3.28",
-                "jsesc": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-compilation-targets": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
-            "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/compat-data": "^7.28.6",
-                "@babel/helper-validator-option": "^7.27.1",
-                "browserslist": "^4.24.0",
-                "lru-cache": "^5.1.1",
-                "semver": "^6.3.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-globals": {
-            "version": "7.28.0",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
-            "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-module-imports": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
-            "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/traverse": "^7.28.6",
-                "@babel/types": "^7.28.6"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-module-transforms": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
-            "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-module-imports": "^7.28.6",
-                "@babel/helper-validator-identifier": "^7.28.5",
-                "@babel/traverse": "^7.28.6"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0"
-            }
-        },
-        "node_modules/@babel/helper-plugin-utils": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
-            "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-string-parser": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
-            "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.28.5",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
-            "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helper-validator-option": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
-            "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/helpers": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz",
-            "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/template": "^7.28.6",
-                "@babel/types": "^7.28.6"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/parser": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz",
-            "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.28.6"
-            },
-            "bin": {
-                "parser": "bin/babel-parser.js"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
-        "node_modules/@babel/plugin-transform-react-jsx-self": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
-            "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.27.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@babel/plugin-transform-react-jsx-source": {
-            "version": "7.27.1",
-            "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
-            "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-plugin-utils": "^7.27.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            },
-            "peerDependencies": {
-                "@babel/core": "^7.0.0-0"
-            }
-        },
-        "node_modules/@babel/runtime": {
-            "version": "7.28.4",
-            "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
-            "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/template": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz",
-            "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/code-frame": "^7.28.6",
-                "@babel/parser": "^7.28.6",
-                "@babel/types": "^7.28.6"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/traverse": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz",
-            "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/code-frame": "^7.28.6",
-                "@babel/generator": "^7.28.6",
-                "@babel/helper-globals": "^7.28.0",
-                "@babel/parser": "^7.28.6",
-                "@babel/template": "^7.28.6",
-                "@babel/types": "^7.28.6",
-                "debug": "^4.3.1"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@babel/types": {
-            "version": "7.28.6",
-            "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz",
-            "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/helper-string-parser": "^7.27.1",
-                "@babel/helper-validator-identifier": "^7.28.5"
-            },
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/@emotion/hash": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
-            "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
-            "license": "MIT"
-        },
-        "node_modules/@emotion/unitless": {
-            "version": "0.7.5",
-            "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz",
-            "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
-            "license": "MIT"
-        },
-        "node_modules/@esbuild/aix-ppc64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
-            "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
-            "cpu": [
-                "ppc64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "aix"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/android-arm": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
-            "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
-            "cpu": [
-                "arm"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "android"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/android-arm64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
-            "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "android"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/android-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
-            "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "android"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/darwin-arm64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
-            "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "darwin"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/darwin-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
-            "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "darwin"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/freebsd-arm64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
-            "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "freebsd"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/freebsd-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
-            "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "freebsd"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-arm": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
-            "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
-            "cpu": [
-                "arm"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-arm64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
-            "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-ia32": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
-            "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
-            "cpu": [
-                "ia32"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-loong64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
-            "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
-            "cpu": [
-                "loong64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-mips64el": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
-            "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
-            "cpu": [
-                "mips64el"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-ppc64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
-            "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
-            "cpu": [
-                "ppc64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-riscv64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
-            "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
-            "cpu": [
-                "riscv64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-s390x": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
-            "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
-            "cpu": [
-                "s390x"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/linux-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
-            "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/netbsd-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
-            "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "netbsd"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/openbsd-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
-            "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "openbsd"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/sunos-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
-            "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "sunos"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/win32-arm64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
-            "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/win32-ia32": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
-            "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
-            "cpu": [
-                "ia32"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@esbuild/win32-x64": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
-            "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ],
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@eslint-community/eslint-utils": {
-            "version": "4.9.1",
-            "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
-            "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "eslint-visitor-keys": "^3.4.3"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            },
-            "peerDependencies": {
-                "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
-            }
-        },
-        "node_modules/@eslint-community/regexpp": {
-            "version": "4.12.2",
-            "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
-            "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
-            }
-        },
-        "node_modules/@eslint/eslintrc": {
-            "version": "2.1.4",
-            "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
-            "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "ajv": "^6.12.4",
-                "debug": "^4.3.2",
-                "espree": "^9.6.0",
-                "globals": "^13.19.0",
-                "ignore": "^5.2.0",
-                "import-fresh": "^3.2.1",
-                "js-yaml": "^4.1.0",
-                "minimatch": "^3.1.2",
-                "strip-json-comments": "^3.1.1"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/@eslint/js": {
-            "version": "8.57.1",
-            "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz",
-            "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            }
-        },
-        "node_modules/@humanwhocodes/config-array": {
-            "version": "0.13.0",
-            "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
-            "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
-            "deprecated": "Use @eslint/config-array instead",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "@humanwhocodes/object-schema": "^2.0.3",
-                "debug": "^4.3.1",
-                "minimatch": "^3.0.5"
-            },
-            "engines": {
-                "node": ">=10.10.0"
-            }
-        },
-        "node_modules/@humanwhocodes/module-importer": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-            "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "engines": {
-                "node": ">=12.22"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/nzakas"
-            }
-        },
-        "node_modules/@humanwhocodes/object-schema": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
-            "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
-            "deprecated": "Use @eslint/object-schema instead",
-            "dev": true,
-            "license": "BSD-3-Clause"
-        },
-        "node_modules/@jridgewell/gen-mapping": {
-            "version": "0.3.13",
-            "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
-            "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@jridgewell/sourcemap-codec": "^1.5.0",
-                "@jridgewell/trace-mapping": "^0.3.24"
-            }
-        },
-        "node_modules/@jridgewell/remapping": {
-            "version": "2.3.5",
-            "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
-            "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@jridgewell/gen-mapping": "^0.3.5",
-                "@jridgewell/trace-mapping": "^0.3.24"
-            }
-        },
-        "node_modules/@jridgewell/resolve-uri": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
-            "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
-        "node_modules/@jridgewell/sourcemap-codec": {
-            "version": "1.5.5",
-            "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
-            "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/@jridgewell/trace-mapping": {
-            "version": "0.3.31",
-            "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
-            "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@jridgewell/resolve-uri": "^3.1.0",
-                "@jridgewell/sourcemap-codec": "^1.4.14"
-            }
-        },
-        "node_modules/@monaco-editor/loader": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.7.0.tgz",
-            "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
-            "license": "MIT",
-            "dependencies": {
-                "state-local": "^1.0.6"
-            }
-        },
-        "node_modules/@monaco-editor/react": {
-            "version": "4.7.0",
-            "resolved": "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.7.0.tgz",
-            "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
-            "license": "MIT",
-            "dependencies": {
-                "@monaco-editor/loader": "^1.5.0"
-            },
-            "peerDependencies": {
-                "monaco-editor": ">= 0.25.0 < 1",
-                "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-                "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
-            }
-        },
-        "node_modules/@nodelib/fs.scandir": {
-            "version": "2.1.5",
-            "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-            "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@nodelib/fs.stat": "2.0.5",
-                "run-parallel": "^1.1.9"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/@nodelib/fs.stat": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-            "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/@nodelib/fs.walk": {
-            "version": "1.2.8",
-            "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-            "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@nodelib/fs.scandir": "2.1.5",
-                "fastq": "^1.6.0"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/@rc-component/async-validator": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz",
-            "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.24.4"
-            },
-            "engines": {
-                "node": ">=14.x"
-            }
-        },
-        "node_modules/@rc-component/color-picker": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
-            "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
-            "license": "MIT",
-            "dependencies": {
-                "@ant-design/fast-color": "^2.0.6",
-                "@babel/runtime": "^7.23.6",
-                "classnames": "^2.2.6",
-                "rc-util": "^5.38.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/context": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz",
-            "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "rc-util": "^5.27.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/mini-decimal": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz",
-            "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.0"
-            },
-            "engines": {
-                "node": ">=8.x"
-            }
-        },
-        "node_modules/@rc-component/mutate-observer": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
-            "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.0",
-                "classnames": "^2.3.2",
-                "rc-util": "^5.24.4"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/portal": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz",
-            "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.0",
-                "classnames": "^2.3.2",
-                "rc-util": "^5.24.4"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/qrcode": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
-            "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.24.7"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/tour": {
-            "version": "1.15.1",
-            "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz",
-            "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.0",
-                "@rc-component/portal": "^1.0.0-9",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "^2.3.2",
-                "rc-util": "^5.24.4"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rc-component/trigger": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.1.tgz",
-            "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.23.2",
-                "@rc-component/portal": "^1.1.0",
-                "classnames": "^2.3.2",
-                "rc-motion": "^2.0.0",
-                "rc-resize-observer": "^1.3.1",
-                "rc-util": "^5.44.0"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/@rolldown/pluginutils": {
-            "version": "1.0.0-beta.27",
-            "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
-            "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/@rollup/rollup-android-arm-eabi": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
-            "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
-            "cpu": [
-                "arm"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "android"
-            ]
-        },
-        "node_modules/@rollup/rollup-android-arm64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
-            "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "android"
-            ]
-        },
-        "node_modules/@rollup/rollup-darwin-arm64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
-            "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "darwin"
-            ]
-        },
-        "node_modules/@rollup/rollup-darwin-x64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
-            "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "darwin"
-            ]
-        },
-        "node_modules/@rollup/rollup-freebsd-arm64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
-            "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "freebsd"
-            ]
-        },
-        "node_modules/@rollup/rollup-freebsd-x64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
-            "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "freebsd"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
-            "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
-            "cpu": [
-                "arm"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
-            "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
-            "cpu": [
-                "arm"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-arm64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
-            "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-arm64-musl": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
-            "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-loong64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
-            "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
-            "cpu": [
-                "loong64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-loong64-musl": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
-            "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
-            "cpu": [
-                "loong64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-ppc64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
-            "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
-            "cpu": [
-                "ppc64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-ppc64-musl": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
-            "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
-            "cpu": [
-                "ppc64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
-            "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
-            "cpu": [
-                "riscv64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-riscv64-musl": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
-            "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
-            "cpu": [
-                "riscv64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-s390x-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
-            "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
-            "cpu": [
-                "s390x"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-x64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
-            "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-linux-x64-musl": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
-            "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "linux"
-            ]
-        },
-        "node_modules/@rollup/rollup-openbsd-x64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
-            "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "openbsd"
-            ]
-        },
-        "node_modules/@rollup/rollup-openharmony-arm64": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
-            "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "openharmony"
-            ]
-        },
-        "node_modules/@rollup/rollup-win32-arm64-msvc": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
-            "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
-            "cpu": [
-                "arm64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ]
-        },
-        "node_modules/@rollup/rollup-win32-ia32-msvc": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
-            "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
-            "cpu": [
-                "ia32"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ]
-        },
-        "node_modules/@rollup/rollup-win32-x64-gnu": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
-            "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ]
-        },
-        "node_modules/@rollup/rollup-win32-x64-msvc": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
-            "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
-            "cpu": [
-                "x64"
-            ],
-            "dev": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "win32"
-            ]
-        },
-        "node_modules/@tanstack/query-core": {
-            "version": "5.90.16",
-            "resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.16.tgz",
-            "integrity": "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==",
-            "license": "MIT",
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/tannerlinsley"
-            }
-        },
-        "node_modules/@tanstack/react-query": {
-            "version": "5.90.16",
-            "resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.16.tgz",
-            "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@tanstack/query-core": "5.90.16"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/tannerlinsley"
-            },
-            "peerDependencies": {
-                "react": "^18 || ^19"
-            }
-        },
-        "node_modules/@types/babel__core": {
-            "version": "7.20.5",
-            "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
-            "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/parser": "^7.20.7",
-                "@babel/types": "^7.20.7",
-                "@types/babel__generator": "*",
-                "@types/babel__template": "*",
-                "@types/babel__traverse": "*"
-            }
-        },
-        "node_modules/@types/babel__generator": {
-            "version": "7.27.0",
-            "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
-            "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.0.0"
-            }
-        },
-        "node_modules/@types/babel__template": {
-            "version": "7.4.4",
-            "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz",
-            "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/parser": "^7.1.0",
-                "@babel/types": "^7.0.0"
-            }
-        },
-        "node_modules/@types/babel__traverse": {
-            "version": "7.28.0",
-            "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
-            "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/types": "^7.28.2"
-            }
-        },
-        "node_modules/@types/estree": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
-            "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/@types/prop-types": {
-            "version": "15.7.15",
-            "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
-            "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/@types/react": {
-            "version": "18.3.27",
-            "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.27.tgz",
-            "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@types/prop-types": "*",
-                "csstype": "^3.2.2"
-            }
-        },
-        "node_modules/@types/react-dom": {
-            "version": "18.3.7",
-            "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
-            "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
-            "dev": true,
-            "license": "MIT",
-            "peerDependencies": {
-                "@types/react": "^18.0.0"
-            }
-        },
-        "node_modules/@types/trusted-types": {
-            "version": "2.0.7",
-            "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
-            "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
-            "license": "MIT",
-            "optional": true,
-            "peer": true
-        },
-        "node_modules/@ungap/structured-clone": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
-            "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/@vitejs/plugin-react": {
-            "version": "4.7.0",
-            "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
-            "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@babel/core": "^7.28.0",
-                "@babel/plugin-transform-react-jsx-self": "^7.27.1",
-                "@babel/plugin-transform-react-jsx-source": "^7.27.1",
-                "@rolldown/pluginutils": "1.0.0-beta.27",
-                "@types/babel__core": "^7.20.5",
-                "react-refresh": "^0.17.0"
-            },
-            "engines": {
-                "node": "^14.18.0 || >=16.0.0"
-            },
-            "peerDependencies": {
-                "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
-            }
-        },
-        "node_modules/acorn": {
-            "version": "8.15.0",
-            "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
-            "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
-            "dev": true,
-            "license": "MIT",
-            "bin": {
-                "acorn": "bin/acorn"
-            },
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
-        "node_modules/acorn-jsx": {
-            "version": "5.3.2",
-            "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-            "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-            "dev": true,
-            "license": "MIT",
-            "peerDependencies": {
-                "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
-            }
-        },
-        "node_modules/ajv": {
-            "version": "6.12.6",
-            "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
-            "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "fast-json-stable-stringify": "^2.0.0",
-                "json-schema-traverse": "^0.4.1",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/antd": {
-            "version": "5.29.3",
-            "resolved": "https://registry.npmmirror.com/antd/-/antd-5.29.3.tgz",
-            "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
-            "license": "MIT",
-            "dependencies": {
-                "@ant-design/colors": "^7.2.1",
-                "@ant-design/cssinjs": "^1.23.0",
-                "@ant-design/cssinjs-utils": "^1.1.3",
-                "@ant-design/fast-color": "^2.0.6",
-                "@ant-design/icons": "^5.6.1",
-                "@ant-design/react-slick": "~1.1.2",
-                "@babel/runtime": "^7.26.0",
-                "@rc-component/color-picker": "~2.0.1",
-                "@rc-component/mutate-observer": "^1.1.0",
-                "@rc-component/qrcode": "~1.1.0",
-                "@rc-component/tour": "~1.15.1",
-                "@rc-component/trigger": "^2.3.0",
-                "classnames": "^2.5.1",
-                "copy-to-clipboard": "^3.3.3",
-                "dayjs": "^1.11.11",
-                "rc-cascader": "~3.34.0",
-                "rc-checkbox": "~3.5.0",
-                "rc-collapse": "~3.9.0",
-                "rc-dialog": "~9.6.0",
-                "rc-drawer": "~7.3.0",
-                "rc-dropdown": "~4.2.1",
-                "rc-field-form": "~2.7.1",
-                "rc-image": "~7.12.0",
-                "rc-input": "~1.8.0",
-                "rc-input-number": "~9.5.0",
-                "rc-mentions": "~2.20.0",
-                "rc-menu": "~9.16.1",
-                "rc-motion": "^2.9.5",
-                "rc-notification": "~5.6.4",
-                "rc-pagination": "~5.1.0",
-                "rc-picker": "~4.11.3",
-                "rc-progress": "~4.0.0",
-                "rc-rate": "~2.13.1",
-                "rc-resize-observer": "^1.4.3",
-                "rc-segmented": "~2.7.0",
-                "rc-select": "~14.16.8",
-                "rc-slider": "~11.1.9",
-                "rc-steps": "~6.0.1",
-                "rc-switch": "~4.1.0",
-                "rc-table": "~7.54.0",
-                "rc-tabs": "~15.7.0",
-                "rc-textarea": "~1.10.2",
-                "rc-tooltip": "~6.4.0",
-                "rc-tree": "~5.13.1",
-                "rc-tree-select": "~5.27.0",
-                "rc-upload": "~4.11.0",
-                "rc-util": "^5.44.4",
-                "scroll-into-view-if-needed": "^3.1.0",
-                "throttle-debounce": "^5.0.2"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/ant-design"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/argparse": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
-            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-            "dev": true,
-            "license": "Python-2.0"
-        },
-        "node_modules/array-buffer-byte-length": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
-            "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "is-array-buffer": "^3.0.5"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/array-includes": {
-            "version": "3.1.9",
-            "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz",
-            "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.4",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.24.0",
-                "es-object-atoms": "^1.1.1",
-                "get-intrinsic": "^1.3.0",
-                "is-string": "^1.1.1",
-                "math-intrinsics": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/array.prototype.findlast": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
-            "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.7",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.2",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.0.0",
-                "es-shim-unscopables": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/array.prototype.flat": {
-            "version": "1.3.3",
-            "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
-            "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.5",
-                "es-shim-unscopables": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/array.prototype.flatmap": {
-            "version": "1.3.3",
-            "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
-            "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.5",
-                "es-shim-unscopables": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/array.prototype.tosorted": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
-            "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.7",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.3",
-                "es-errors": "^1.3.0",
-                "es-shim-unscopables": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/arraybuffer.prototype.slice": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
-            "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "array-buffer-byte-length": "^1.0.1",
-                "call-bind": "^1.0.8",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.5",
-                "es-errors": "^1.3.0",
-                "get-intrinsic": "^1.2.6",
-                "is-array-buffer": "^3.0.4"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/async-function": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz",
-            "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/asynckit": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
-            "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-            "license": "MIT"
-        },
-        "node_modules/available-typed-arrays": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
-            "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "possible-typed-array-names": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/axios": {
-            "version": "1.13.2",
-            "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
-            "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
-            "license": "MIT",
-            "dependencies": {
-                "follow-redirects": "^1.15.6",
-                "form-data": "^4.0.4",
-                "proxy-from-env": "^1.1.0"
-            }
-        },
-        "node_modules/balanced-match": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
-            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/baseline-browser-mapping": {
-            "version": "2.9.14",
-            "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
-            "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "bin": {
-                "baseline-browser-mapping": "dist/cli.js"
-            }
-        },
-        "node_modules/brace-expansion": {
-            "version": "1.1.12",
-            "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz",
-            "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/browserslist": {
-            "version": "4.28.1",
-            "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
-            "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/browserslist"
-                },
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "license": "MIT",
-            "dependencies": {
-                "baseline-browser-mapping": "^2.9.0",
-                "caniuse-lite": "^1.0.30001759",
-                "electron-to-chromium": "^1.5.263",
-                "node-releases": "^2.0.27",
-                "update-browserslist-db": "^1.2.0"
-            },
-            "bin": {
-                "browserslist": "cli.js"
-            },
-            "engines": {
-                "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
-            }
-        },
-        "node_modules/call-bind": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz",
-            "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind-apply-helpers": "^1.0.0",
-                "es-define-property": "^1.0.0",
-                "get-intrinsic": "^1.2.4",
-                "set-function-length": "^1.2.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/call-bind-apply-helpers": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
-            "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "function-bind": "^1.1.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/call-bound": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
-            "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind-apply-helpers": "^1.0.2",
-                "get-intrinsic": "^1.3.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/callsites": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
-            "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/caniuse-lite": {
-            "version": "1.0.30001764",
-            "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
-            "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
-                },
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "license": "CC-BY-4.0"
-        },
-        "node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/classnames": {
-            "version": "2.5.1",
-            "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
-            "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
-            "license": "MIT"
-        },
-        "node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/combined-stream": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
-            "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-            "license": "MIT",
-            "dependencies": {
-                "delayed-stream": "~1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.8"
-            }
-        },
-        "node_modules/compute-scroll-into-view": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
-            "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
-            "license": "MIT"
-        },
-        "node_modules/concat-map": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
-            "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/convert-source-map": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
-            "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/copy-to-clipboard": {
-            "version": "3.3.3",
-            "resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
-            "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
-            "license": "MIT",
-            "dependencies": {
-                "toggle-selection": "^1.0.6"
-            }
-        },
-        "node_modules/cross-spawn": {
-            "version": "7.0.6",
-            "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
-            "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "path-key": "^3.1.0",
-                "shebang-command": "^2.0.0",
-                "which": "^2.0.1"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/csstype": {
-            "version": "3.2.3",
-            "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
-            "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
-            "license": "MIT"
-        },
-        "node_modules/data-view-buffer": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
-            "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "es-errors": "^1.3.0",
-                "is-data-view": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/data-view-byte-length": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
-            "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "es-errors": "^1.3.0",
-                "is-data-view": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/inspect-js"
-            }
-        },
-        "node_modules/data-view-byte-offset": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
-            "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "es-errors": "^1.3.0",
-                "is-data-view": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/dayjs": {
-            "version": "1.11.19",
-            "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
-            "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
-            "license": "MIT"
-        },
-        "node_modules/debug": {
-            "version": "4.4.3",
-            "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
-            "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "ms": "^2.1.3"
-            },
-            "engines": {
-                "node": ">=6.0"
-            },
-            "peerDependenciesMeta": {
-                "supports-color": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/deep-is": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
-            "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/define-data-property": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
-            "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-define-property": "^1.0.0",
-                "es-errors": "^1.3.0",
-                "gopd": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/define-properties": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz",
-            "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-data-property": "^1.0.1",
-                "has-property-descriptors": "^1.0.0",
-                "object-keys": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/delayed-stream": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
-            "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.4.0"
-            }
-        },
-        "node_modules/doctrine": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
-            "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "esutils": "^2.0.2"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
-        "node_modules/dompurify": {
-            "version": "3.2.7",
-            "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz",
-            "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
-            "license": "(MPL-2.0 OR Apache-2.0)",
-            "peer": true,
-            "optionalDependencies": {
-                "@types/trusted-types": "^2.0.7"
-            }
-        },
-        "node_modules/dunder-proto": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
-            "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
-            "license": "MIT",
-            "dependencies": {
-                "call-bind-apply-helpers": "^1.0.1",
-                "es-errors": "^1.3.0",
-                "gopd": "^1.2.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/electron-to-chromium": {
-            "version": "1.5.267",
-            "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
-            "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/es-abstract": {
-            "version": "1.24.1",
-            "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.1.tgz",
-            "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "array-buffer-byte-length": "^1.0.2",
-                "arraybuffer.prototype.slice": "^1.0.4",
-                "available-typed-arrays": "^1.0.7",
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.4",
-                "data-view-buffer": "^1.0.2",
-                "data-view-byte-length": "^1.0.2",
-                "data-view-byte-offset": "^1.0.1",
-                "es-define-property": "^1.0.1",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.1.1",
-                "es-set-tostringtag": "^2.1.0",
-                "es-to-primitive": "^1.3.0",
-                "function.prototype.name": "^1.1.8",
-                "get-intrinsic": "^1.3.0",
-                "get-proto": "^1.0.1",
-                "get-symbol-description": "^1.1.0",
-                "globalthis": "^1.0.4",
-                "gopd": "^1.2.0",
-                "has-property-descriptors": "^1.0.2",
-                "has-proto": "^1.2.0",
-                "has-symbols": "^1.1.0",
-                "hasown": "^2.0.2",
-                "internal-slot": "^1.1.0",
-                "is-array-buffer": "^3.0.5",
-                "is-callable": "^1.2.7",
-                "is-data-view": "^1.0.2",
-                "is-negative-zero": "^2.0.3",
-                "is-regex": "^1.2.1",
-                "is-set": "^2.0.3",
-                "is-shared-array-buffer": "^1.0.4",
-                "is-string": "^1.1.1",
-                "is-typed-array": "^1.1.15",
-                "is-weakref": "^1.1.1",
-                "math-intrinsics": "^1.1.0",
-                "object-inspect": "^1.13.4",
-                "object-keys": "^1.1.1",
-                "object.assign": "^4.1.7",
-                "own-keys": "^1.0.1",
-                "regexp.prototype.flags": "^1.5.4",
-                "safe-array-concat": "^1.1.3",
-                "safe-push-apply": "^1.0.0",
-                "safe-regex-test": "^1.1.0",
-                "set-proto": "^1.0.0",
-                "stop-iteration-iterator": "^1.1.0",
-                "string.prototype.trim": "^1.2.10",
-                "string.prototype.trimend": "^1.0.9",
-                "string.prototype.trimstart": "^1.0.8",
-                "typed-array-buffer": "^1.0.3",
-                "typed-array-byte-length": "^1.0.3",
-                "typed-array-byte-offset": "^1.0.4",
-                "typed-array-length": "^1.0.7",
-                "unbox-primitive": "^1.1.0",
-                "which-typed-array": "^1.1.19"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/es-define-property": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
-            "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-errors": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
-            "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-iterator-helpers": {
-            "version": "1.2.2",
-            "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz",
-            "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.4",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.24.1",
-                "es-errors": "^1.3.0",
-                "es-set-tostringtag": "^2.1.0",
-                "function-bind": "^1.1.2",
-                "get-intrinsic": "^1.3.0",
-                "globalthis": "^1.0.4",
-                "gopd": "^1.2.0",
-                "has-property-descriptors": "^1.0.2",
-                "has-proto": "^1.2.0",
-                "has-symbols": "^1.1.0",
-                "internal-slot": "^1.1.0",
-                "iterator.prototype": "^1.1.5",
-                "safe-array-concat": "^1.1.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-object-atoms": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
-            "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-set-tostringtag": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
-            "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "get-intrinsic": "^1.2.6",
-                "has-tostringtag": "^1.0.2",
-                "hasown": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-shim-unscopables": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
-            "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "hasown": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/es-to-primitive": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
-            "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-callable": "^1.2.7",
-                "is-date-object": "^1.0.5",
-                "is-symbol": "^1.0.4"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/esbuild": {
-            "version": "0.21.5",
-            "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz",
-            "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
-            "dev": true,
-            "hasInstallScript": true,
-            "license": "MIT",
-            "bin": {
-                "esbuild": "bin/esbuild"
-            },
-            "engines": {
-                "node": ">=12"
-            },
-            "optionalDependencies": {
-                "@esbuild/aix-ppc64": "0.21.5",
-                "@esbuild/android-arm": "0.21.5",
-                "@esbuild/android-arm64": "0.21.5",
-                "@esbuild/android-x64": "0.21.5",
-                "@esbuild/darwin-arm64": "0.21.5",
-                "@esbuild/darwin-x64": "0.21.5",
-                "@esbuild/freebsd-arm64": "0.21.5",
-                "@esbuild/freebsd-x64": "0.21.5",
-                "@esbuild/linux-arm": "0.21.5",
-                "@esbuild/linux-arm64": "0.21.5",
-                "@esbuild/linux-ia32": "0.21.5",
-                "@esbuild/linux-loong64": "0.21.5",
-                "@esbuild/linux-mips64el": "0.21.5",
-                "@esbuild/linux-ppc64": "0.21.5",
-                "@esbuild/linux-riscv64": "0.21.5",
-                "@esbuild/linux-s390x": "0.21.5",
-                "@esbuild/linux-x64": "0.21.5",
-                "@esbuild/netbsd-x64": "0.21.5",
-                "@esbuild/openbsd-x64": "0.21.5",
-                "@esbuild/sunos-x64": "0.21.5",
-                "@esbuild/win32-arm64": "0.21.5",
-                "@esbuild/win32-ia32": "0.21.5",
-                "@esbuild/win32-x64": "0.21.5"
-            }
-        },
-        "node_modules/escalade": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
-            "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/escape-string-regexp": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-            "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/eslint": {
-            "version": "8.57.1",
-            "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz",
-            "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
-            "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@eslint-community/eslint-utils": "^4.2.0",
-                "@eslint-community/regexpp": "^4.6.1",
-                "@eslint/eslintrc": "^2.1.4",
-                "@eslint/js": "8.57.1",
-                "@humanwhocodes/config-array": "^0.13.0",
-                "@humanwhocodes/module-importer": "^1.0.1",
-                "@nodelib/fs.walk": "^1.2.8",
-                "@ungap/structured-clone": "^1.2.0",
-                "ajv": "^6.12.4",
-                "chalk": "^4.0.0",
-                "cross-spawn": "^7.0.2",
-                "debug": "^4.3.2",
-                "doctrine": "^3.0.0",
-                "escape-string-regexp": "^4.0.0",
-                "eslint-scope": "^7.2.2",
-                "eslint-visitor-keys": "^3.4.3",
-                "espree": "^9.6.1",
-                "esquery": "^1.4.2",
-                "esutils": "^2.0.2",
-                "fast-deep-equal": "^3.1.3",
-                "file-entry-cache": "^6.0.1",
-                "find-up": "^5.0.0",
-                "glob-parent": "^6.0.2",
-                "globals": "^13.19.0",
-                "graphemer": "^1.4.0",
-                "ignore": "^5.2.0",
-                "imurmurhash": "^0.1.4",
-                "is-glob": "^4.0.0",
-                "is-path-inside": "^3.0.3",
-                "js-yaml": "^4.1.0",
-                "json-stable-stringify-without-jsonify": "^1.0.1",
-                "levn": "^0.4.1",
-                "lodash.merge": "^4.6.2",
-                "minimatch": "^3.1.2",
-                "natural-compare": "^1.4.0",
-                "optionator": "^0.9.3",
-                "strip-ansi": "^6.0.1",
-                "text-table": "^0.2.0"
-            },
-            "bin": {
-                "eslint": "bin/eslint.js"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/eslint-plugin-react": {
-            "version": "7.37.5",
-            "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
-            "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "array-includes": "^3.1.8",
-                "array.prototype.findlast": "^1.2.5",
-                "array.prototype.flatmap": "^1.3.3",
-                "array.prototype.tosorted": "^1.1.4",
-                "doctrine": "^2.1.0",
-                "es-iterator-helpers": "^1.2.1",
-                "estraverse": "^5.3.0",
-                "hasown": "^2.0.2",
-                "jsx-ast-utils": "^2.4.1 || ^3.0.0",
-                "minimatch": "^3.1.2",
-                "object.entries": "^1.1.9",
-                "object.fromentries": "^2.0.8",
-                "object.values": "^1.2.1",
-                "prop-types": "^15.8.1",
-                "resolve": "^2.0.0-next.5",
-                "semver": "^6.3.1",
-                "string.prototype.matchall": "^4.0.12",
-                "string.prototype.repeat": "^1.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            },
-            "peerDependencies": {
-                "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
-            }
-        },
-        "node_modules/eslint-plugin-react-hooks": {
-            "version": "4.6.2",
-            "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
-            "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=10"
-            },
-            "peerDependencies": {
-                "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
-            }
-        },
-        "node_modules/eslint-plugin-react-refresh": {
-            "version": "0.4.26",
-            "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
-            "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
-            "dev": true,
-            "license": "MIT",
-            "peerDependencies": {
-                "eslint": ">=8.40"
-            }
-        },
-        "node_modules/eslint-plugin-react/node_modules/doctrine": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
-            "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "dependencies": {
-                "esutils": "^2.0.2"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/eslint-scope": {
-            "version": "7.2.2",
-            "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
-            "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "dependencies": {
-                "esrecurse": "^4.3.0",
-                "estraverse": "^5.2.0"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/eslint-visitor-keys": {
-            "version": "3.4.3",
-            "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-            "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/espree": {
-            "version": "9.6.1",
-            "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
-            "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "dependencies": {
-                "acorn": "^8.9.0",
-                "acorn-jsx": "^5.3.2",
-                "eslint-visitor-keys": "^3.4.1"
-            },
-            "engines": {
-                "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-            },
-            "funding": {
-                "url": "https://opencollective.com/eslint"
-            }
-        },
-        "node_modules/esquery": {
-            "version": "1.7.0",
-            "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz",
-            "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
-            "dev": true,
-            "license": "BSD-3-Clause",
-            "dependencies": {
-                "estraverse": "^5.1.0"
-            },
-            "engines": {
-                "node": ">=0.10"
-            }
-        },
-        "node_modules/esrecurse": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
-            "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "dependencies": {
-                "estraverse": "^5.2.0"
-            },
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
-        "node_modules/estraverse": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
-            "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
-        "node_modules/esutils": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
-            "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/fast-deep-equal": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/fast-json-stable-stringify": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-            "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/fast-levenshtein": {
-            "version": "2.0.6",
-            "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-            "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/fastq": {
-            "version": "1.20.1",
-            "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
-            "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "reusify": "^1.0.4"
-            }
-        },
-        "node_modules/file-entry-cache": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
-            "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "flat-cache": "^3.0.4"
-            },
-            "engines": {
-                "node": "^10.12.0 || >=12.0.0"
-            }
-        },
-        "node_modules/find-up": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
-            "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "locate-path": "^6.0.0",
-                "path-exists": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/flat-cache": {
-            "version": "3.2.0",
-            "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
-            "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "flatted": "^3.2.9",
-                "keyv": "^4.5.3",
-                "rimraf": "^3.0.2"
-            },
-            "engines": {
-                "node": "^10.12.0 || >=12.0.0"
-            }
-        },
-        "node_modules/flatted": {
-            "version": "3.3.3",
-            "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
-            "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/follow-redirects": {
-            "version": "1.15.11",
-            "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
-            "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
-            "funding": [
-                {
-                    "type": "individual",
-                    "url": "https://github.com/sponsors/RubenVerborgh"
-                }
-            ],
-            "license": "MIT",
-            "engines": {
-                "node": ">=4.0"
-            },
-            "peerDependenciesMeta": {
-                "debug": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/for-each": {
-            "version": "0.3.5",
-            "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
-            "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-callable": "^1.2.7"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/form-data": {
-            "version": "4.0.5",
-            "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
-            "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
-            "license": "MIT",
-            "dependencies": {
-                "asynckit": "^0.4.0",
-                "combined-stream": "^1.0.8",
-                "es-set-tostringtag": "^2.1.0",
-                "hasown": "^2.0.2",
-                "mime-types": "^2.1.12"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/fs.realpath": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
-            "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/fsevents": {
-            "version": "2.3.3",
-            "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
-            "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-            "dev": true,
-            "hasInstallScript": true,
-            "license": "MIT",
-            "optional": true,
-            "os": [
-                "darwin"
-            ],
-            "engines": {
-                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-            }
-        },
-        "node_modules/function-bind": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
-            "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
-            "license": "MIT",
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/function.prototype.name": {
-            "version": "1.1.8",
-            "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
-            "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
-                "define-properties": "^1.2.1",
-                "functions-have-names": "^1.2.3",
-                "hasown": "^2.0.2",
-                "is-callable": "^1.2.7"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/functions-have-names": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz",
-            "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
-            "dev": true,
-            "license": "MIT",
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/generator-function": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz",
-            "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/gensync": {
-            "version": "1.0.0-beta.2",
-            "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
-            "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6.9.0"
-            }
-        },
-        "node_modules/get-intrinsic": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
-            "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
-            "license": "MIT",
-            "dependencies": {
-                "call-bind-apply-helpers": "^1.0.2",
-                "es-define-property": "^1.0.1",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.1.1",
-                "function-bind": "^1.1.2",
-                "get-proto": "^1.0.1",
-                "gopd": "^1.2.0",
-                "has-symbols": "^1.1.0",
-                "hasown": "^2.0.2",
-                "math-intrinsics": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/get-proto": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
-            "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
-            "license": "MIT",
-            "dependencies": {
-                "dunder-proto": "^1.0.1",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/get-symbol-description": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
-            "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "es-errors": "^1.3.0",
-                "get-intrinsic": "^1.2.6"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/glob": {
-            "version": "7.2.3",
-            "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
-            "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-            "deprecated": "Glob versions prior to v9 are no longer supported",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.1.1",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            },
-            "engines": {
-                "node": "*"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/glob-parent": {
-            "version": "6.0.2",
-            "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
-            "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "is-glob": "^4.0.3"
-            },
-            "engines": {
-                "node": ">=10.13.0"
-            }
-        },
-        "node_modules/globals": {
-            "version": "13.24.0",
-            "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
-            "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "type-fest": "^0.20.2"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/globalthis": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz",
-            "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-properties": "^1.2.1",
-                "gopd": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/gopd": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
-            "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/graphemer": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
-            "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/has-bigints": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz",
-            "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/has-property-descriptors": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
-            "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-define-property": "^1.0.0"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/has-proto": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz",
-            "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "dunder-proto": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/has-symbols": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
-            "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/has-tostringtag": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
-            "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
-            "license": "MIT",
-            "dependencies": {
-                "has-symbols": "^1.0.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/hasown": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
-            "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
-            "license": "MIT",
-            "dependencies": {
-                "function-bind": "^1.1.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/ignore": {
-            "version": "5.3.2",
-            "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
-            "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 4"
-            }
-        },
-        "node_modules/import-fresh": {
-            "version": "3.3.1",
-            "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
-            "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "parent-module": "^1.0.0",
-                "resolve-from": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/imurmurhash": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
-            "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.8.19"
-            }
-        },
-        "node_modules/inflight": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
-            "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-            "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "once": "^1.3.0",
-                "wrappy": "1"
-            }
-        },
-        "node_modules/inherits": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
-            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/internal-slot": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
-            "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "hasown": "^2.0.2",
-                "side-channel": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/is-array-buffer": {
-            "version": "3.0.5",
-            "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
-            "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
-                "get-intrinsic": "^1.2.6"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-async-function": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz",
-            "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "async-function": "^1.0.0",
-                "call-bound": "^1.0.3",
-                "get-proto": "^1.0.1",
-                "has-tostringtag": "^1.0.2",
-                "safe-regex-test": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-bigint": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz",
-            "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "has-bigints": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-boolean-object": {
-            "version": "1.2.2",
-            "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
-            "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "has-tostringtag": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-callable": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz",
-            "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-core-module": {
-            "version": "2.16.1",
-            "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
-            "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "hasown": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-data-view": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz",
-            "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "get-intrinsic": "^1.2.6",
-                "is-typed-array": "^1.1.13"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-date-object": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz",
-            "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "has-tostringtag": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-extglob": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
-            "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-finalizationregistry": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
-            "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-generator-function": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz",
-            "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.4",
-                "generator-function": "^2.0.0",
-                "get-proto": "^1.0.1",
-                "has-tostringtag": "^1.0.2",
-                "safe-regex-test": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-glob": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
-            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-extglob": "^2.1.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-map": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
-            "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-negative-zero": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
-            "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-number-object": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz",
-            "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "has-tostringtag": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-path-inside": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
-            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-regex": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
-            "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "gopd": "^1.2.0",
-                "has-tostringtag": "^1.0.2",
-                "hasown": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-set": {
-            "version": "2.0.3",
-            "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz",
-            "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-shared-array-buffer": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
-            "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-string": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz",
-            "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "has-tostringtag": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-symbol": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz",
-            "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "has-symbols": "^1.1.0",
-                "safe-regex-test": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-typed-array": {
-            "version": "1.1.15",
-            "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz",
-            "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "which-typed-array": "^1.1.16"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-weakmap": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
-            "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-weakref": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz",
-            "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/is-weakset": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz",
-            "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "get-intrinsic": "^1.2.6"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/isarray": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz",
-            "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/isexe": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
-            "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/iterator.prototype": {
-            "version": "1.1.5",
-            "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
-            "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-data-property": "^1.1.4",
-                "es-object-atoms": "^1.0.0",
-                "get-intrinsic": "^1.2.6",
-                "get-proto": "^1.0.0",
-                "has-symbols": "^1.1.0",
-                "set-function-name": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/js-tokens": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
-            "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-            "license": "MIT"
-        },
-        "node_modules/js-yaml": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
-            "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "argparse": "^2.0.1"
-            },
-            "bin": {
-                "js-yaml": "bin/js-yaml.js"
-            }
-        },
-        "node_modules/jsesc": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
-            "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
-            "dev": true,
-            "license": "MIT",
-            "bin": {
-                "jsesc": "bin/jsesc"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/json-buffer": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
-            "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/json-schema-traverse": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-            "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/json-stable-stringify-without-jsonify": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-            "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/json2mq": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz",
-            "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
-            "license": "MIT",
-            "dependencies": {
-                "string-convert": "^0.2.0"
-            }
-        },
-        "node_modules/json5": {
-            "version": "2.2.3",
-            "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
-            "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
-            "dev": true,
-            "license": "MIT",
-            "bin": {
-                "json5": "lib/cli.js"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/jsx-ast-utils": {
-            "version": "3.3.5",
-            "resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
-            "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "array-includes": "^3.1.6",
-                "array.prototype.flat": "^1.3.1",
-                "object.assign": "^4.1.4",
-                "object.values": "^1.1.6"
-            },
-            "engines": {
-                "node": ">=4.0"
-            }
-        },
-        "node_modules/keyv": {
-            "version": "4.5.4",
-            "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
-            "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "json-buffer": "3.0.1"
-            }
-        },
-        "node_modules/levn": {
-            "version": "0.4.1",
-            "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
-            "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "prelude-ls": "^1.2.1",
-                "type-check": "~0.4.0"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
-        "node_modules/locate-path": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
-            "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "p-locate": "^5.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/lodash.merge": {
-            "version": "4.6.2",
-            "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
-            "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/loose-envify": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
-            "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
-            "license": "MIT",
-            "dependencies": {
-                "js-tokens": "^3.0.0 || ^4.0.0"
-            },
-            "bin": {
-                "loose-envify": "cli.js"
-            }
-        },
-        "node_modules/lru-cache": {
-            "version": "5.1.1",
-            "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
-            "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "yallist": "^3.0.2"
-            }
-        },
-        "node_modules/marked": {
-            "version": "14.0.0",
-            "resolved": "https://registry.npmmirror.com/marked/-/marked-14.0.0.tgz",
-            "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
-            "license": "MIT",
-            "peer": true,
-            "bin": {
-                "marked": "bin/marked.js"
-            },
-            "engines": {
-                "node": ">= 18"
-            }
-        },
-        "node_modules/math-intrinsics": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
-            "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/mime-db": {
-            "version": "1.52.0",
-            "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
-            "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.6"
-            }
-        },
-        "node_modules/mime-types": {
-            "version": "2.1.35",
-            "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
-            "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-            "license": "MIT",
-            "dependencies": {
-                "mime-db": "1.52.0"
-            },
-            "engines": {
-                "node": ">= 0.6"
-            }
-        },
-        "node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/monaco-editor": {
-            "version": "0.55.1",
-            "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.55.1.tgz",
-            "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
-            "license": "MIT",
-            "peer": true,
-            "dependencies": {
-                "dompurify": "3.2.7",
-                "marked": "14.0.0"
-            }
-        },
-        "node_modules/ms": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
-            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/nanoid": {
-            "version": "3.3.11",
-            "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
-            "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "license": "MIT",
-            "bin": {
-                "nanoid": "bin/nanoid.cjs"
-            },
-            "engines": {
-                "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-            }
-        },
-        "node_modules/natural-compare": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
-            "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/node-releases": {
-            "version": "2.0.27",
-            "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
-            "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/object-assign": {
-            "version": "4.1.1",
-            "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
-            "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/object-inspect": {
-            "version": "1.13.4",
-            "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
-            "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object-keys": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz",
-            "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/object.assign": {
-            "version": "4.1.7",
-            "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz",
-            "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
-                "define-properties": "^1.2.1",
-                "es-object-atoms": "^1.0.0",
-                "has-symbols": "^1.1.0",
-                "object-keys": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object.entries": {
-            "version": "1.1.9",
-            "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz",
-            "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.4",
-                "define-properties": "^1.2.1",
-                "es-object-atoms": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/object.fromentries": {
-            "version": "2.0.8",
-            "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz",
-            "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.7",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.2",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/object.values": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz",
-            "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
-                "define-properties": "^1.2.1",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
-            "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "wrappy": "1"
-            }
-        },
-        "node_modules/optionator": {
-            "version": "0.9.4",
-            "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
-            "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "deep-is": "^0.1.3",
-                "fast-levenshtein": "^2.0.6",
-                "levn": "^0.4.1",
-                "prelude-ls": "^1.2.1",
-                "type-check": "^0.4.0",
-                "word-wrap": "^1.2.5"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
-        "node_modules/own-keys": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz",
-            "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "get-intrinsic": "^1.2.6",
-                "object-keys": "^1.1.1",
-                "safe-push-apply": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/p-limit": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
-            "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "yocto-queue": "^0.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/p-locate": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
-            "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "p-limit": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/parent-module": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
-            "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "callsites": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/path-exists": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
-            "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/path-is-absolute": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-            "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/path-key": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
-            "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/path-parse": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
-            "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/picocolors": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
-            "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/possible-typed-array-names": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
-            "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/postcss": {
-            "version": "8.5.6",
-            "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
-            "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/postcss/"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/postcss"
-                },
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "license": "MIT",
-            "dependencies": {
-                "nanoid": "^3.3.11",
-                "picocolors": "^1.1.1",
-                "source-map-js": "^1.2.1"
-            },
-            "engines": {
-                "node": "^10 || ^12 || >=14"
-            }
-        },
-        "node_modules/prelude-ls": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
-            "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
-        "node_modules/prop-types": {
-            "version": "15.8.1",
-            "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz",
-            "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "loose-envify": "^1.4.0",
-                "object-assign": "^4.1.1",
-                "react-is": "^16.13.1"
-            }
-        },
-        "node_modules/proxy-from-env": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-            "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-            "license": "MIT"
-        },
-        "node_modules/punycode": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
-            "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/queue-microtask": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
-            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "license": "MIT"
-        },
-        "node_modules/rc-cascader": {
-            "version": "3.34.0",
-            "resolved": "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz",
-            "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.25.7",
-                "classnames": "^2.3.1",
-                "rc-select": "~14.16.2",
-                "rc-tree": "~5.13.0",
-                "rc-util": "^5.43.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-checkbox": {
-            "version": "3.5.0",
-            "resolved": "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
-            "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.3.2",
-                "rc-util": "^5.25.2"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-collapse": {
-            "version": "3.9.0",
-            "resolved": "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz",
-            "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "2.x",
-                "rc-motion": "^2.3.4",
-                "rc-util": "^5.27.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-dialog": {
-            "version": "9.6.0",
-            "resolved": "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz",
-            "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "@rc-component/portal": "^1.0.0-8",
-                "classnames": "^2.2.6",
-                "rc-motion": "^2.3.0",
-                "rc-util": "^5.21.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-drawer": {
-            "version": "7.3.0",
-            "resolved": "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.3.0.tgz",
-            "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.23.9",
-                "@rc-component/portal": "^1.1.1",
-                "classnames": "^2.2.6",
-                "rc-motion": "^2.6.1",
-                "rc-util": "^5.38.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-dropdown": {
-            "version": "4.2.1",
-            "resolved": "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
-            "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.3",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "^2.2.6",
-                "rc-util": "^5.44.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.11.0",
-                "react-dom": ">=16.11.0"
-            }
-        },
-        "node_modules/rc-field-form": {
-            "version": "2.7.1",
-            "resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.1.tgz",
-            "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.0",
-                "@rc-component/async-validator": "^5.0.3",
-                "rc-util": "^5.32.2"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-image": {
-            "version": "7.12.0",
-            "resolved": "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz",
-            "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.2",
-                "@rc-component/portal": "^1.0.2",
-                "classnames": "^2.2.6",
-                "rc-dialog": "~9.6.0",
-                "rc-motion": "^2.6.2",
-                "rc-util": "^5.34.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-input": {
-            "version": "1.8.0",
-            "resolved": "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz",
-            "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.1",
-                "classnames": "^2.2.1",
-                "rc-util": "^5.18.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.0.0",
-                "react-dom": ">=16.0.0"
-            }
-        },
-        "node_modules/rc-input-number": {
-            "version": "9.5.0",
-            "resolved": "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz",
-            "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "@rc-component/mini-decimal": "^1.0.1",
-                "classnames": "^2.2.5",
-                "rc-input": "~1.8.0",
-                "rc-util": "^5.40.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-mentions": {
-            "version": "2.20.0",
-            "resolved": "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz",
-            "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.22.5",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "^2.2.6",
-                "rc-input": "~1.8.0",
-                "rc-menu": "~9.16.0",
-                "rc-textarea": "~1.10.0",
-                "rc-util": "^5.34.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-menu": {
-            "version": "9.16.1",
-            "resolved": "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz",
-            "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "2.x",
-                "rc-motion": "^2.4.3",
-                "rc-overflow": "^1.3.1",
-                "rc-util": "^5.27.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-motion": {
-            "version": "2.9.5",
-            "resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz",
-            "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.1",
-                "classnames": "^2.2.1",
-                "rc-util": "^5.44.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-notification": {
-            "version": "5.6.4",
-            "resolved": "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz",
-            "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "2.x",
-                "rc-motion": "^2.9.0",
-                "rc-util": "^5.20.1"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-overflow": {
-            "version": "1.5.0",
-            "resolved": "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz",
-            "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.1",
-                "classnames": "^2.2.1",
-                "rc-resize-observer": "^1.0.0",
-                "rc-util": "^5.37.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-pagination": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz",
-            "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.3.2",
-                "rc-util": "^5.38.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-picker": {
-            "version": "4.11.3",
-            "resolved": "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz",
-            "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.24.7",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "^2.2.1",
-                "rc-overflow": "^1.3.2",
-                "rc-resize-observer": "^1.4.0",
-                "rc-util": "^5.43.0"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "date-fns": ">= 2.x",
-                "dayjs": ">= 1.x",
-                "luxon": ">= 3.x",
-                "moment": ">= 2.x",
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            },
-            "peerDependenciesMeta": {
-                "date-fns": {
-                    "optional": true
-                },
-                "dayjs": {
-                    "optional": true
-                },
-                "luxon": {
-                    "optional": true
-                },
-                "moment": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/rc-progress": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz",
-            "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.2.6",
-                "rc-util": "^5.16.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-rate": {
-            "version": "2.13.1",
-            "resolved": "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz",
-            "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.2.5",
-                "rc-util": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-resize-observer": {
-            "version": "1.4.3",
-            "resolved": "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
-            "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.20.7",
-                "classnames": "^2.2.1",
-                "rc-util": "^5.44.1",
-                "resize-observer-polyfill": "^1.5.1"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-segmented": {
-            "version": "2.7.1",
-            "resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.1.tgz",
-            "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.1",
-                "classnames": "^2.2.1",
-                "rc-motion": "^2.4.4",
-                "rc-util": "^5.17.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.0.0",
-                "react-dom": ">=16.0.0"
-            }
-        },
-        "node_modules/rc-select": {
-            "version": "14.16.8",
-            "resolved": "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz",
-            "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "@rc-component/trigger": "^2.1.1",
-                "classnames": "2.x",
-                "rc-motion": "^2.0.1",
-                "rc-overflow": "^1.3.1",
-                "rc-util": "^5.16.1",
-                "rc-virtual-list": "^3.5.2"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": "*",
-                "react-dom": "*"
-            }
-        },
-        "node_modules/rc-slider": {
-            "version": "11.1.9",
-            "resolved": "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.9.tgz",
-            "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.2.5",
-                "rc-util": "^5.36.0"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-steps": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz",
-            "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.16.7",
-                "classnames": "^2.2.3",
-                "rc-util": "^5.16.1"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-switch": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz",
-            "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.21.0",
-                "classnames": "^2.2.1",
-                "rc-util": "^5.30.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-table": {
-            "version": "7.54.0",
-            "resolved": "https://registry.npmmirror.com/rc-table/-/rc-table-7.54.0.tgz",
-            "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "@rc-component/context": "^1.4.0",
-                "classnames": "^2.2.5",
-                "rc-resize-observer": "^1.1.0",
-                "rc-util": "^5.44.3",
-                "rc-virtual-list": "^3.14.2"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-tabs": {
-            "version": "15.7.0",
-            "resolved": "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.7.0.tgz",
-            "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.2",
-                "classnames": "2.x",
-                "rc-dropdown": "~4.2.0",
-                "rc-menu": "~9.16.0",
-                "rc-motion": "^2.6.2",
-                "rc-resize-observer": "^1.0.0",
-                "rc-util": "^5.34.1"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-textarea": {
-            "version": "1.10.2",
-            "resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz",
-            "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "^2.2.1",
-                "rc-input": "~1.8.0",
-                "rc-resize-observer": "^1.0.0",
-                "rc-util": "^5.27.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-tooltip": {
-            "version": "6.4.0",
-            "resolved": "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
-            "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.11.2",
-                "@rc-component/trigger": "^2.0.0",
-                "classnames": "^2.3.1",
-                "rc-util": "^5.44.3"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-tree": {
-            "version": "5.13.1",
-            "resolved": "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz",
-            "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.10.1",
-                "classnames": "2.x",
-                "rc-motion": "^2.0.1",
-                "rc-util": "^5.16.1",
-                "rc-virtual-list": "^3.5.1"
-            },
-            "engines": {
-                "node": ">=10.x"
-            },
-            "peerDependencies": {
-                "react": "*",
-                "react-dom": "*"
-            }
-        },
-        "node_modules/rc-tree-select": {
-            "version": "5.27.0",
-            "resolved": "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
-            "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.25.7",
-                "classnames": "2.x",
-                "rc-select": "~14.16.2",
-                "rc-tree": "~5.13.0",
-                "rc-util": "^5.43.0"
-            },
-            "peerDependencies": {
-                "react": "*",
-                "react-dom": "*"
-            }
-        },
-        "node_modules/rc-upload": {
-            "version": "4.11.0",
-            "resolved": "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.11.0.tgz",
-            "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.3",
-                "classnames": "^2.2.5",
-                "rc-util": "^5.2.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-util": {
-            "version": "5.44.4",
-            "resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz",
-            "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.18.3",
-                "react-is": "^18.2.0"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/rc-util/node_modules/react-is": {
-            "version": "18.3.1",
-            "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
-            "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
-            "license": "MIT"
-        },
-        "node_modules/rc-virtual-list": {
-            "version": "3.19.2",
-            "resolved": "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
-            "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
-            "license": "MIT",
-            "dependencies": {
-                "@babel/runtime": "^7.20.0",
-                "classnames": "^2.2.6",
-                "rc-resize-observer": "^1.0.0",
-                "rc-util": "^5.36.0"
-            },
-            "engines": {
-                "node": ">=8.x"
-            },
-            "peerDependencies": {
-                "react": ">=16.9.0",
-                "react-dom": ">=16.9.0"
-            }
-        },
-        "node_modules/react": {
-            "version": "18.3.1",
-            "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
-            "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
-            "license": "MIT",
-            "dependencies": {
-                "loose-envify": "^1.1.0"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/react-dom": {
-            "version": "18.3.1",
-            "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
-            "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
-            "license": "MIT",
-            "dependencies": {
-                "loose-envify": "^1.1.0",
-                "scheduler": "^0.23.2"
-            },
-            "peerDependencies": {
-                "react": "^18.3.1"
-            }
-        },
-        "node_modules/react-is": {
-            "version": "16.13.1",
-            "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
-            "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/react-refresh": {
-            "version": "0.17.0",
-            "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
-            "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/reflect.getprototypeof": {
-            "version": "1.0.10",
-            "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
-            "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.9",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.0.0",
-                "get-intrinsic": "^1.2.7",
-                "get-proto": "^1.0.1",
-                "which-builtin-type": "^1.2.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/regexp.prototype.flags": {
-            "version": "1.5.4",
-            "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
-            "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "define-properties": "^1.2.1",
-                "es-errors": "^1.3.0",
-                "get-proto": "^1.0.1",
-                "gopd": "^1.2.0",
-                "set-function-name": "^2.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/resize-observer-polyfill": {
-            "version": "1.5.1",
-            "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
-            "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
-            "license": "MIT"
-        },
-        "node_modules/resolve": {
-            "version": "2.0.0-next.5",
-            "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz",
-            "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-core-module": "^2.13.0",
-                "path-parse": "^1.0.7",
-                "supports-preserve-symlinks-flag": "^1.0.0"
-            },
-            "bin": {
-                "resolve": "bin/resolve"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/resolve-from": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
-            "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/reusify": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
-            "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "iojs": ">=1.0.0",
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/rimraf": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
-            "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-            "deprecated": "Rimraf versions prior to v4 are no longer supported",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "glob": "^7.1.3"
-            },
-            "bin": {
-                "rimraf": "bin.js"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/rollup": {
-            "version": "4.55.1",
-            "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.55.1.tgz",
-            "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "@types/estree": "1.0.8"
-            },
-            "bin": {
-                "rollup": "dist/bin/rollup"
-            },
-            "engines": {
-                "node": ">=18.0.0",
-                "npm": ">=8.0.0"
-            },
-            "optionalDependencies": {
-                "@rollup/rollup-android-arm-eabi": "4.55.1",
-                "@rollup/rollup-android-arm64": "4.55.1",
-                "@rollup/rollup-darwin-arm64": "4.55.1",
-                "@rollup/rollup-darwin-x64": "4.55.1",
-                "@rollup/rollup-freebsd-arm64": "4.55.1",
-                "@rollup/rollup-freebsd-x64": "4.55.1",
-                "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
-                "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
-                "@rollup/rollup-linux-arm64-gnu": "4.55.1",
-                "@rollup/rollup-linux-arm64-musl": "4.55.1",
-                "@rollup/rollup-linux-loong64-gnu": "4.55.1",
-                "@rollup/rollup-linux-loong64-musl": "4.55.1",
-                "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
-                "@rollup/rollup-linux-ppc64-musl": "4.55.1",
-                "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
-                "@rollup/rollup-linux-riscv64-musl": "4.55.1",
-                "@rollup/rollup-linux-s390x-gnu": "4.55.1",
-                "@rollup/rollup-linux-x64-gnu": "4.55.1",
-                "@rollup/rollup-linux-x64-musl": "4.55.1",
-                "@rollup/rollup-openbsd-x64": "4.55.1",
-                "@rollup/rollup-openharmony-arm64": "4.55.1",
-                "@rollup/rollup-win32-arm64-msvc": "4.55.1",
-                "@rollup/rollup-win32-ia32-msvc": "4.55.1",
-                "@rollup/rollup-win32-x64-gnu": "4.55.1",
-                "@rollup/rollup-win32-x64-msvc": "4.55.1",
-                "fsevents": "~2.3.2"
-            }
-        },
-        "node_modules/run-parallel": {
-            "version": "1.2.0",
-            "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
-            "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "license": "MIT",
-            "dependencies": {
-                "queue-microtask": "^1.2.2"
-            }
-        },
-        "node_modules/safe-array-concat": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
-            "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.2",
-                "get-intrinsic": "^1.2.6",
-                "has-symbols": "^1.1.0",
-                "isarray": "^2.0.5"
-            },
-            "engines": {
-                "node": ">=0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/safe-push-apply": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
-            "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "isarray": "^2.0.5"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/safe-regex-test": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
-            "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "es-errors": "^1.3.0",
-                "is-regex": "^1.2.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/scheduler": {
-            "version": "0.23.2",
-            "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
-            "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
-            "license": "MIT",
-            "dependencies": {
-                "loose-envify": "^1.1.0"
-            }
-        },
-        "node_modules/scroll-into-view-if-needed": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
-            "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
-            "license": "MIT",
-            "dependencies": {
-                "compute-scroll-into-view": "^3.0.2"
-            }
-        },
-        "node_modules/semver": {
-            "version": "6.3.1",
-            "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
-            "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
-            "dev": true,
-            "license": "ISC",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/set-function-length": {
-            "version": "1.2.2",
-            "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
-            "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-data-property": "^1.1.4",
-                "es-errors": "^1.3.0",
-                "function-bind": "^1.1.2",
-                "get-intrinsic": "^1.2.4",
-                "gopd": "^1.0.1",
-                "has-property-descriptors": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/set-function-name": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz",
-            "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-data-property": "^1.1.4",
-                "es-errors": "^1.3.0",
-                "functions-have-names": "^1.2.3",
-                "has-property-descriptors": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/set-proto": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz",
-            "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "dunder-proto": "^1.0.1",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/shebang-command": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
-            "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "shebang-regex": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/shebang-regex": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
-            "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/side-channel": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
-            "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "object-inspect": "^1.13.3",
-                "side-channel-list": "^1.0.0",
-                "side-channel-map": "^1.0.1",
-                "side-channel-weakmap": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/side-channel-list": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
-            "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "object-inspect": "^1.13.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/side-channel-map": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
-            "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "es-errors": "^1.3.0",
-                "get-intrinsic": "^1.2.5",
-                "object-inspect": "^1.13.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/side-channel-weakmap": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
-            "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "es-errors": "^1.3.0",
-                "get-intrinsic": "^1.2.5",
-                "object-inspect": "^1.13.3",
-                "side-channel-map": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/source-map-js": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
-            "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
-            "dev": true,
-            "license": "BSD-3-Clause",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/state-local": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz",
-            "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
-            "license": "MIT"
-        },
-        "node_modules/stop-iteration-iterator": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
-            "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "es-errors": "^1.3.0",
-                "internal-slot": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/string-convert": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
-            "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
-            "license": "MIT"
-        },
-        "node_modules/string.prototype.matchall": {
-            "version": "4.0.12",
-            "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
-            "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.3",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.6",
-                "es-errors": "^1.3.0",
-                "es-object-atoms": "^1.0.0",
-                "get-intrinsic": "^1.2.6",
-                "gopd": "^1.2.0",
-                "has-symbols": "^1.1.0",
-                "internal-slot": "^1.1.0",
-                "regexp.prototype.flags": "^1.5.3",
-                "set-function-name": "^2.0.2",
-                "side-channel": "^1.1.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/string.prototype.repeat": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
-            "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "define-properties": "^1.1.3",
-                "es-abstract": "^1.17.5"
-            }
-        },
-        "node_modules/string.prototype.trim": {
-            "version": "1.2.10",
-            "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
-            "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.2",
-                "define-data-property": "^1.1.4",
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.5",
-                "es-object-atoms": "^1.0.0",
-                "has-property-descriptors": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/string.prototype.trimend": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
-            "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.2",
-                "define-properties": "^1.2.1",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/string.prototype.trimstart": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
-            "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.7",
-                "define-properties": "^1.2.1",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/strip-json-comments": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-            "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/stylis": {
-            "version": "4.3.6",
-            "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
-            "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
-            "license": "MIT"
-        },
-        "node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/supports-preserve-symlinks-flag": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-            "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/text-table": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
-            "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-            "dev": true,
-            "license": "MIT"
-        },
-        "node_modules/throttle-debounce": {
-            "version": "5.0.2",
-            "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
-            "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
-            "license": "MIT",
-            "engines": {
-                "node": ">=12.22"
-            }
-        },
-        "node_modules/toggle-selection": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz",
-            "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
-            "license": "MIT"
-        },
-        "node_modules/type-check": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
-            "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "prelude-ls": "^1.2.1"
-            },
-            "engines": {
-                "node": ">= 0.8.0"
-            }
-        },
-        "node_modules/type-fest": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
-            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-            "dev": true,
-            "license": "(MIT OR CC0-1.0)",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/typed-array-buffer": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
-            "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "es-errors": "^1.3.0",
-                "is-typed-array": "^1.1.14"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            }
-        },
-        "node_modules/typed-array-byte-length": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
-            "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.8",
-                "for-each": "^0.3.3",
-                "gopd": "^1.2.0",
-                "has-proto": "^1.2.0",
-                "is-typed-array": "^1.1.14"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/typed-array-byte-offset": {
-            "version": "1.0.4",
-            "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
-            "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "available-typed-arrays": "^1.0.7",
-                "call-bind": "^1.0.8",
-                "for-each": "^0.3.3",
-                "gopd": "^1.2.0",
-                "has-proto": "^1.2.0",
-                "is-typed-array": "^1.1.15",
-                "reflect.getprototypeof": "^1.0.9"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/typed-array-length": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz",
-            "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bind": "^1.0.7",
-                "for-each": "^0.3.3",
-                "gopd": "^1.0.1",
-                "is-typed-array": "^1.1.13",
-                "possible-typed-array-names": "^1.0.0",
-                "reflect.getprototypeof": "^1.0.6"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/unbox-primitive": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
-            "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.3",
-                "has-bigints": "^1.0.2",
-                "has-symbols": "^1.1.0",
-                "which-boxed-primitive": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/update-browserslist-db": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
-            "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "opencollective",
-                    "url": "https://opencollective.com/browserslist"
-                },
-                {
-                    "type": "tidelift",
-                    "url": "https://tidelift.com/funding/github/npm/browserslist"
-                },
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/ai"
-                }
-            ],
-            "license": "MIT",
-            "dependencies": {
-                "escalade": "^3.2.0",
-                "picocolors": "^1.1.1"
-            },
-            "bin": {
-                "update-browserslist-db": "cli.js"
-            },
-            "peerDependencies": {
-                "browserslist": ">= 4.21.0"
-            }
-        },
-        "node_modules/uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "license": "BSD-2-Clause",
-            "dependencies": {
-                "punycode": "^2.1.0"
-            }
-        },
-        "node_modules/vite": {
-            "version": "5.4.21",
-            "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz",
-            "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "esbuild": "^0.21.3",
-                "postcss": "^8.4.43",
-                "rollup": "^4.20.0"
-            },
-            "bin": {
-                "vite": "bin/vite.js"
-            },
-            "engines": {
-                "node": "^18.0.0 || >=20.0.0"
-            },
-            "funding": {
-                "url": "https://github.com/vitejs/vite?sponsor=1"
-            },
-            "optionalDependencies": {
-                "fsevents": "~2.3.3"
-            },
-            "peerDependencies": {
-                "@types/node": "^18.0.0 || >=20.0.0",
-                "less": "*",
-                "lightningcss": "^1.21.0",
-                "sass": "*",
-                "sass-embedded": "*",
-                "stylus": "*",
-                "sugarss": "*",
-                "terser": "^5.4.0"
-            },
-            "peerDependenciesMeta": {
-                "@types/node": {
-                    "optional": true
-                },
-                "less": {
-                    "optional": true
-                },
-                "lightningcss": {
-                    "optional": true
-                },
-                "sass": {
-                    "optional": true
-                },
-                "sass-embedded": {
-                    "optional": true
-                },
-                "stylus": {
-                    "optional": true
-                },
-                "sugarss": {
-                    "optional": true
-                },
-                "terser": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/which": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
-            "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-            "dev": true,
-            "license": "ISC",
-            "dependencies": {
-                "isexe": "^2.0.0"
-            },
-            "bin": {
-                "node-which": "bin/node-which"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/which-boxed-primitive": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
-            "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-bigint": "^1.1.0",
-                "is-boolean-object": "^1.2.1",
-                "is-number-object": "^1.1.1",
-                "is-string": "^1.1.1",
-                "is-symbol": "^1.1.1"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/which-builtin-type": {
-            "version": "1.2.1",
-            "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
-            "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "call-bound": "^1.0.2",
-                "function.prototype.name": "^1.1.6",
-                "has-tostringtag": "^1.0.2",
-                "is-async-function": "^2.0.0",
-                "is-date-object": "^1.1.0",
-                "is-finalizationregistry": "^1.1.0",
-                "is-generator-function": "^1.0.10",
-                "is-regex": "^1.2.1",
-                "is-weakref": "^1.0.2",
-                "isarray": "^2.0.5",
-                "which-boxed-primitive": "^1.1.0",
-                "which-collection": "^1.0.2",
-                "which-typed-array": "^1.1.16"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/which-collection": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz",
-            "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "is-map": "^2.0.3",
-                "is-set": "^2.0.3",
-                "is-weakmap": "^2.0.2",
-                "is-weakset": "^2.0.3"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/which-typed-array": {
-            "version": "1.1.19",
-            "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz",
-            "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "available-typed-arrays": "^1.0.7",
-                "call-bind": "^1.0.8",
-                "call-bound": "^1.0.4",
-                "for-each": "^0.3.5",
-                "get-proto": "^1.0.1",
-                "gopd": "^1.2.0",
-                "has-tostringtag": "^1.0.2"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
-        "node_modules/word-wrap": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
-            "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/yallist": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
-            "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-            "dev": true,
-            "license": "ISC"
-        },
-        "node_modules/yocto-queue": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
-            "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-            "dev": true,
-            "license": "MIT",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        }
-    }
-}

+ 0 - 31
front/package.json

@@ -1,31 +0,0 @@
-{
-    "name": "visa-plugin-manager",
-    "private": true,
-    "version": "0.1.0",
-    "type": "module",
-    "scripts": {
-        "dev": "vite",
-        "build": "vite build",
-        "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
-        "preview": "vite preview"
-    },
-    "dependencies": {
-        "@ant-design/icons": "^5.6.1",
-        "@monaco-editor/react": "^4.7.0",
-        "@tanstack/react-query": "^5.90.16",
-        "antd": "^5.29.3",
-        "axios": "^1.13.2",
-        "react": "^18.2.0",
-        "react-dom": "^18.2.0"
-    },
-    "devDependencies": {
-        "@types/react": "^18.2.43",
-        "@types/react-dom": "^18.2.17",
-        "@vitejs/plugin-react": "^4.2.1",
-        "eslint": "^8.55.0",
-        "eslint-plugin-react": "^7.33.2",
-        "eslint-plugin-react-hooks": "^4.6.0",
-        "eslint-plugin-react-refresh": "^0.4.5",
-        "vite": "^5.0.8"
-    }
-}

+ 0 - 51
front/src/App.css

@@ -1,51 +0,0 @@
-#root {
-    margin: 0;
-    padding: 0;
-    width: 100%;
-    height: 100%;
-  }
-  
-  body {
-    margin: 0;
-    padding: 0;
-    background-color: #f5f5f5;
-    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
-  }
-  
-  /* 覆盖 Ant Design 的一些默认间距,如果需要的话 */
-  .ant-layout-header {
-    padding-inline: 24px !important;
-  }
-  
-  /* 滚动条样式优化 (可选) */
-  ::-webkit-scrollbar {
-    width: 8px;
-    height: 8px;
-  }
-  ::-webkit-scrollbar-thumb {
-    background: #c1c1c1;
-    border-radius: 4px;
-  }
-  ::-webkit-scrollbar-track {
-    background: #f1f1f1;
-  }
-
-  /* 让表格悬停时效果更好 */
-.ant-table-tbody > tr.ant-table-row:hover > td {
-    background: #fafafa !important;
-  }
-  
-  /* 表格内垂直居中 */
-  .align-middle td {
-    vertical-align: middle !important;
-  }
-  
-  /* 调整 Modal 的圆角,看起来更圆润 */
-  .ant-modal-content {
-    border-radius: 12px !important;
-    overflow: hidden;
-  }
-  
-  .ant-btn {
-    border-radius: 6px;
-  }

+ 0 - 25
front/src/App.jsx

@@ -1,25 +0,0 @@
-// 文件路径: src/App.jsx
-import React from 'react';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import Dashboard from './pages/Dashboard';
-import './App.css'; 
-
-// 创建 React Query 客户端
-const queryClient = new QueryClient({
-  defaultOptions: {
-    queries: {
-      retry: 1,
-      refetchOnWindowFocus: false,
-    },
-  },
-});
-
-function App() {
-  return (
-    <QueryClientProvider client={queryClient}>
-      <Dashboard />
-    </QueryClientProvider>
-  );
-}
-
-export default App;

+ 0 - 30
front/src/api/index.js

@@ -1,30 +0,0 @@
-import axios from 'axios';
-
-// 基础配置,配合 vite.config.js 的代理
-const api = axios.create({
-  baseURL: '/api',
-  timeout: 10000,
-});
-
-api.interceptors.response.use(
-  (response) => response.data, // 把 axios 的外壳去掉,直接返回 { code: 0, data: [...] }
-  (error) => Promise.reject(error)
-);
-
-export const getStatus = () => api.get('/status');
-
-export const startGroup = (group_id) => api.post('/start', { group_id });
-export const stopGroup = (group_id) => api.post('/stop', { group_id });
-export const restartGroup = (group_id) => api.post('/restart', { group_id });
-
-export const getGroupConfig = (group_id) => api.post('/group_config', { group_id });
-
-// 特殊处理:API 要求 new_config_str 是字符串
-export const updateConfig = (group_id, fullConfigObj) => {
-  return api.post('/ota/update_config', {
-    group_id,
-    new_config_str: JSON.stringify(fullConfigObj)
-  });
-};
-
-export const upgradePlugin = (data) => api.post('/ota/upgrade_plugin', data);

+ 0 - 119
front/src/components/ConfigModal.jsx

@@ -1,119 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Modal, message, Spin, Button, Typography } from 'antd';
-import { useQuery, useMutation } from '@tanstack/react-query';
-import Editor from "@monaco-editor/react";
-import { getGroupConfig, updateConfig } from '../api';
-
-const { Text } = Typography;
-
-const ConfigModal = ({ open, groupId, onClose }) => {
-  const [jsonContent, setJsonContent] = useState("");
-
-  // 1. 获取当前配置
-  const { data: configResp, isLoading } = useQuery({
-    queryKey: ['config', groupId],
-    queryFn: () => getGroupConfig(groupId),
-    enabled: !!groupId && open,
-    staleTime: 0, // 每次打开都重新获取
-  });
-
-  // 2. 数据回显:收到数据后直接转为格式化的 JSON 字符串
-  useEffect(() => {
-    if (configResp?.data) {
-      // JSON.stringify(data, null, 2) 会生成带缩进的漂亮格式
-      setJsonContent(JSON.stringify(configResp.data, null, 2));
-    }
-  }, [configResp]);
-
-  // 3. 保存操作
-  const updateMutation = useMutation({
-    mutationFn: (data) => updateConfig(groupId, data),
-    onSuccess: () => {
-      message.success('Configuration updated successfully!');
-      onClose();
-    },
-    onError: (err) => {
-      message.error('Failed to update: ' + (err.message || 'Unknown error'));
-    }
-  });
-
-  const handleSave = () => {
-    try {
-      // A. 校验 JSON 格式
-      const configObj = JSON.parse(jsonContent);
-
-      // B. 安全检查:确保 group_id 没有被用户误删或修改
-      // 如果用户在 JSON 里改了 group_id,这里强制覆盖回正确的 ID,或者报错
-      if (configObj.group_id && configObj.group_id !== groupId) {
-        message.warning(`Group ID mismatch. Reverting to original ID: ${groupId}`);
-      }
-      configObj.group_id = groupId;
-
-      // C. 提交
-      updateMutation.mutate(configObj);
-
-    } catch (e) {
-      // JSON 解析失败
-      message.error('Invalid JSON format. Please check your syntax.');
-    }
-  };
-
-  return (
-    <Modal
-      title={
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingRight: 30 }}>
-          <span>Config: <Text code>{groupId}</Text></span>
-          <Text type="secondary" style={{ fontSize: 12, fontWeight: 'normal' }}>
-            Raw JSON Mode
-          </Text>
-        </div>
-      }
-      open={open}
-      onCancel={onClose}
-      footer={[
-        <Button key="cancel" onClick={onClose}>
-          Cancel
-        </Button>,
-        <Button 
-          key="submit" 
-          type="primary" 
-          loading={updateMutation.isPending} 
-          onClick={handleSave}
-        >
-          Save Changes
-        </Button>
-      ]}
-      width={800}
-      centered
-      destroyOnClose
-      maskClosable={false}
-      bodyStyle={{ padding: 0 }} // 去掉内边距,让编辑器撑满
-    >
-      {isLoading ? (
-        <div style={{ textAlign: 'center', padding: '100px' }}>
-          <Spin size="large" tip="Loading configuration..." />
-        </div>
-      ) : (
-        <div style={{ height: '600px', borderTop: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0' }}>
-          <Editor
-            height="100%"
-            defaultLanguage="json"
-            value={jsonContent}
-            onChange={(val) => setJsonContent(val)}
-            theme="vs-light"
-            options={{ 
-              minimap: { enabled: true },      // 显示右侧缩略图
-              scrollBeyondLastLine: false,     // 滚动限制
-              fontSize: 13,                    // 字体大小
-              formatOnPaste: true,             // 粘贴自动格式化
-              automaticLayout: true,           // 自动适应容器大小
-              tabSize: 2                       // 缩进空格数
-            }}
-          />
-        </div>
-      )}
-    </Modal>
-  );
-};
-
-export default ConfigModal;

+ 0 - 156
front/src/components/CreateGroupModal.jsx

@@ -1,156 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Modal, Input, message, Button, Typography, Space } from 'antd';
-import { ThunderboltOutlined } from '@ant-design/icons';
-import { useMutation, useQueryClient } from '@tanstack/react-query';
-import Editor from "@monaco-editor/react";
-import { updateConfig } from '../api';
-
-const { Text } = Typography;
-
-// 提供一个默认的模版,方便用户直接修改,而不是从零开始写 JSON
-const DEFAULT_TEMPLATE = {
-    "identifier": "",
-    "debug": false,
-    "enable": false,
-    "need_account": false,
-    "local_account_pool": "",
-    "need_proxy": false,
-    "proxy_pool": "",
-    "target_instances": 0,
-    "account_login_interval": 0,
-    "order_account_routing": "",
-    "order_account_online_limit": 0,
-    "account_bind_applicant": false,
-    "query_wait": {
-        "mode": "Random",
-        "fixed_wait": 10,
-        "random_min": 60,
-        "random_max": 300
-    },
-    "plugin_config": {
-        "lib_path": "plugins",
-        "plugin_name": "vfs_plugin",
-        "plugin_bin": "vfs_plugin.py",
-        "plugin_proto": "IVSPlg"
-    },
-    "free_config": {
-    }
-};
-
-const CreateGroupModal = ({ open, onClose }) => {
-  const queryClient = useQueryClient();
-  
-  // 两个核心状态:ID 和 JSON内容
-  const [groupId, setGroupId] = useState("");
-  const [jsonContent, setJsonContent] = useState("");
-
-  // 每次打开弹窗时,重置数据
-  useEffect(() => {
-    if (open) {
-      setGroupId("");
-      // 格式化默认模版
-      setJsonContent(JSON.stringify(DEFAULT_TEMPLATE, null, 2));
-    }
-  }, [open]);
-
-  // 提交 Mutation
-  const createMutation = useMutation({
-    mutationFn: (data) => updateConfig(data.group_id, data.config),
-    onSuccess: () => {
-      message.success(`Group [${groupId}] created successfully!`);
-      queryClient.invalidateQueries(['status']);
-      onClose();
-    },
-    onError: (err) => {
-      message.error('Failed to create: ' + (err.message || 'Unknown error'));
-    }
-  });
-
-  const handleCreate = () => {
-    // 1. 基础校验
-    if (!groupId || groupId.trim() === "") {
-      message.error("Group ID is required!");
-      return;
-    }
-
-    try {
-      // 2. 解析 JSON
-      const configObj = JSON.parse(jsonContent);
-
-      // 3. 强制合并 Group ID (确保 JSON 内部的 ID 与输入框一致)
-      configObj.group_id = groupId.trim();
-      // 如果 identifier 没填,默认用 ID
-      if (!configObj.identifier) {
-        configObj.identifier = groupId.trim();
-      }
-
-      // 4. 发送请求
-      createMutation.mutate({
-        group_id: groupId.trim(),
-        config: configObj
-      });
-
-    } catch (e) {
-      message.error("Invalid JSON format. Please check syntax.");
-    }
-  };
-
-  return (
-    <Modal
-      title="Create New Group"
-      open={open}
-      onCancel={onClose}
-      footer={[
-        <Button key="cancel" onClick={onClose}>
-          Cancel
-        </Button>,
-        <Button 
-          key="submit" 
-          type="primary" 
-          loading={createMutation.isPending} 
-          onClick={handleCreate}
-        >
-          Create Group
-        </Button>
-      ]}
-      width={800}
-      centered
-      maskClosable={false}
-    >
-      <Space direction="vertical" style={{ width: '100%', marginBottom: 16 }}>
-        <div>
-          <Text strong>Group ID (Unique Key)</Text>
-          <Input 
-            size="large"
-            placeholder="e.g. VFS_CN_DE" 
-            prefix={<ThunderboltOutlined style={{ color: '#bfbfbf' }} />}
-            value={groupId}
-            onChange={(e) => setGroupId(e.target.value.toUpperCase())} // 强制大写(可选)
-            style={{ marginTop: 8 }}
-          />
-        </div>
-        
-        <div>
-          <Text strong>Configuration (JSON)</Text>
-          <div style={{ marginTop: 8, height: '450px', border: '1px solid #d9d9d9', borderRadius: 6, overflow: 'hidden' }}>
-            <Editor
-              height="100%"
-              defaultLanguage="json"
-              value={jsonContent}
-              onChange={setJsonContent}
-              theme="vs-light"
-              options={{ 
-                minimap: { enabled: false },
-                fontSize: 13,
-                formatOnPaste: true,
-                tabSize: 2
-              }}
-            />
-          </div>
-        </div>
-      </Space>
-    </Modal>
-  );
-};
-
-export default CreateGroupModal;

+ 0 - 125
front/src/components/LogViewer.jsx

@@ -1,125 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react';
-import { Modal, Button, Empty, Tag } from 'antd';
-import { PauseCircleOutlined, PlayCircleOutlined, DeleteOutlined } from '@ant-design/icons';
-
-const LogViewer = ({ open, groupId, onClose }) => {
-  const [logs, setLogs] = useState([]);
-  const [isConnected, setIsConnected] = useState(false);
-  const [isPaused, setIsPaused] = useState(false);
-  
-  const wsRef = useRef(null);
-  const logEndRef = useRef(null);
-
-// 建立 WebSocket 连接
-useEffect(() => {
-    if (open && groupId) {
-      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-      const host = window.location.host; 
-      const wsUrl = `${protocol}//${host}/ws/logs/${groupId}`;
-
-      const ws = new WebSocket(wsUrl);
-      wsRef.current = ws;
-
-      ws.onopen = () => {
-        setIsConnected(true);
-        setLogs(prev => [...prev, `[SYSTEM] Connected to log stream for ${groupId}...`]);
-      };
-
-      ws.onmessage = (event) => {
-        if (isPaused) return; 
-        let newLog = event.data;
-        setLogs(prev => {
-            const updated = [...prev, newLog];
-            if (updated.length > 1000) {
-                return updated.slice(updated.length - 1000);
-            }
-            return updated;
-        });
-      };
-
-      ws.onclose = () => {
-        setIsConnected(false);
-        setLogs(prev => [...prev, `[SYSTEM] Connection closed.`]);
-      };
-
-      ws.onerror = (err) => {
-        console.error("WS Error", err);
-        setLogs(prev => [...prev, `[SYSTEM] WebSocket Error.`]);
-      };
-
-      // 修复双重打印的核心:
-      return () => {
-        ws.onmessage = null; // 断开回调引用
-        ws.close();          // 强制关闭,不管状态是 0 还是 1
-      };
-    }
-  }, [open, groupId]);
-
-  // 自动滚动到底部
-  useEffect(() => {
-    if (!isPaused && logEndRef.current) {
-      logEndRef.current.scrollIntoView({ behavior: "smooth" });
-    }
-  }, [logs, isPaused]);
-
-  // 清空日志
-  const handleClear = () => setLogs([]);
-
-  return (
-    <Modal
-      title={
-        <div className="flex items-center gap-2">
-          <span>Real-time Logs: {groupId}</span>
-          <Tag color={isConnected ? "green" : "red"}>
-            {isConnected ? "LIVE" : "DISCONNECTED"}
-          </Tag>
-        </div>
-      }
-      open={open}
-      onCancel={onClose}
-      footer={null}
-      width={900}
-      bodyStyle={{ padding: 0 }}
-      destroyOnClose
-    >
-      {/* 工具栏 */}
-      <div style={{ padding: '8px 16px', background: '#f0f0f0', display: 'flex', gap: '8px', borderBottom: '1px solid #d9d9d9' }}>
-        <Button 
-            size="small" 
-            icon={isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />} 
-            onClick={() => setIsPaused(!isPaused)}
-        >
-            {isPaused ? "Resume Auto-scroll" : "Pause Output"}
-        </Button>
-        <Button size="small" icon={<DeleteOutlined />} onClick={handleClear}>Clear</Button>
-      </div>
-
-      {/* 终端界面 */}
-      <div style={{
-        backgroundColor: '#1e1e1e',
-        color: '#d4d4d4',
-        fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace",
-        fontSize: '12px',
-        height: '500px',
-        overflowY: 'auto',
-        padding: '16px',
-        whiteSpace: 'pre-wrap', // 保持换行
-        wordBreak: 'break-all'
-      }}>
-        {logs.length === 0 && <div className="text-gray-500 text-center mt-20">Waiting for logs...</div>}
-        
-        {logs.map((log, index) => (
-          <div key={index} style={{ marginBottom: '2px', borderBottom: '1px solid #333' }}>
-             {/* 简单的时间戳+内容显示,你也可以引入 ansi-to-react 处理颜色 */}
-             <span style={{color: '#888', marginRight: '8px'}}>{new Date().toLocaleTimeString()}</span>
-             {log}
-          </div>
-        ))}
-        {/* 锚点用于自动滚动 */}
-        <div ref={logEndRef} />
-      </div>
-    </Modal>
-  );
-};
-
-export default LogViewer;

+ 0 - 58
front/src/components/UpgradeModal.jsx

@@ -1,58 +0,0 @@
-import React from 'react';
-import { Modal, Form, Input, message } from 'antd';
-import { useMutation } from '@tanstack/react-query';
-import { upgradePlugin } from '../api';
-
-const UpgradeModal = ({ open, onClose }) => {
-  const [form] = Form.useForm();
-
-  const mutation = useMutation({
-    mutationFn: upgradePlugin,
-    onSuccess: (data) => {
-      message.success(`Upgrade successful. Restarted: ${data.data.restarted.join(', ')}`);
-      form.resetFields();
-      onClose();
-    },
-    onError: (err) => {
-      message.error('Upgrade failed');
-    }
-  });
-
-  const handleOk = async () => {
-    try {
-      const values = await form.validateFields();
-      mutation.mutate(values);
-    } catch (e) {
-      // Validate failed
-    }
-  };
-
-  return (
-    <Modal
-      title="OTA Plugin Upgrade"
-      open={open}
-      onOk={handleOk}
-      onCancel={onClose}
-      confirmLoading={mutation.isPending}
-    >
-      <Form form={form} layout="vertical">
-        <Form.Item 
-            name="plugin_name" 
-            label="Plugin Name" 
-            rules={[{ required: true, message: 'Please input plugin name' }]}
-        >
-          <Input placeholder="e.g. bls_spain" />
-        </Form.Item>
-        <Form.Item 
-            name="plugin_bin" 
-            label="Plugin Binary Path/Name" 
-            rules={[{ required: true, message: 'Please input binary path' }]}
-        >
-          <Input placeholder="e.g. plugins/bls_v2.py" />
-        </Form.Item>
-      </Form>
-    </Modal>
-  );
-};
-
-export default UpgradeModal;

+ 0 - 11
front/src/main.jsx

@@ -1,11 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App.jsx'
-import './App.css'
-// Ant Design 5.x 默认自动引入样式,不需要手动 import 'antd/dist/reset.css'
-
-ReactDOM.createRoot(document.getElementById('root')).render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>,
-)

+ 0 - 297
front/src/pages/Dashboard.jsx

@@ -1,297 +0,0 @@
-import React, { useState, useMemo } from 'react';
-import { 
-  Table, Button, Space, message, Layout, Typography, 
-  Card, Tooltip, Badge, Row, Col, Statistic, Dropdown, Avatar 
-} from 'antd';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { 
-  PlayCircleFilled, 
-  PauseCircleFilled, 
-  ReloadOutlined, 
-  SettingOutlined,
-  CloudUploadOutlined,
-  FileTextOutlined,
-  PlusOutlined,
-  MoreOutlined,
-  GlobalOutlined,
-  UserOutlined,
-  AppstoreOutlined,
-  ThunderboltFilled
-} from '@ant-design/icons';
-
-// API & Components
-import { getStatus, startGroup, stopGroup, restartGroup } from '../api';
-import ConfigModal from '../components/ConfigModal';
-import UpgradeModal from '../components/UpgradeModal';
-import LogViewer from '../components/LogViewer';
-import CreateGroupModal from '../components/CreateGroupModal';
-
-const { Header, Content } = Layout;
-const { Title, Text } = Typography;
-
-const Dashboard = () => {
-  const queryClient = useQueryClient();
-  
-  // State
-  const [configGroupId, setConfigGroupId] = useState(null);
-  const [logGroupId, setLogGroupId] = useState(null);
-  const [isUpgradeOpen, setIsUpgradeOpen] = useState(false);
-  const [isCreateOpen, setIsCreateOpen] = useState(false);
-
-  // 1. Data Fetching
-  const { data: statusResp, isLoading } = useQuery({
-    queryKey: ['status'],
-    queryFn: getStatus,
-    refetchInterval: 3000,
-    refetchOnWindowFocus: true,
-  });
-
-  const dataSource = statusResp?.data || [];
-
-  // 2. Statistics Calculation
-  const stats = useMemo(() => {
-    const total = dataSource.length;
-    const running = dataSource.filter(d => d.running).length;
-    const stopped = total - running;
-    const totalInstances = dataSource.reduce((acc, curr) => acc + (curr.instances || 0), 0);
-    return { total, running, stopped, totalInstances };
-  }, [dataSource]);
-
-  // 3. Actions
-  const actionMutation = useMutation({
-    mutationFn: ({ fn, id }) => fn(id),
-    onSuccess: () => {
-      message.success('Command sent successfully');
-      queryClient.invalidateQueries(['status']);
-    },
-    onError: (err) => message.error('Operation failed: ' + err.message),
-  });
-
-  const handleAction = (fn, id) => actionMutation.mutate({ fn, id });
-
-  // 4. Columns Definition
-  const columns = [
-    {
-      title: 'Identity',
-      key: 'identity',
-      width: 220,
-      render: (_, record) => (
-        <Space>
-           <Avatar shape="square" size="large" icon={<AppstoreOutlined />} style={{ backgroundColor: record.running ? '#e6f7ff' : '#f5f5f5', color: record.running ? '#1890ff' : '#ccc' }} />
-           <div style={{ display: 'flex', flexDirection: 'column' }}>
-             <Text strong style={{ fontSize: '15px' }}>{record.id}</Text>
-             <Text type="secondary" style={{ fontSize: '12px' }}>{record.plugin}</Text>
-           </div>
-        </Space>
-      )
-    },
-    {
-      title: 'Status',
-      key: 'status',
-      width: 120,
-      render: (_, record) => (
-         <Badge 
-            status={record.running ? "processing" : "default"} 
-            text={
-                <span style={{ 
-                    color: record.running ? '#52c41a' : '#999', 
-                    fontWeight: 500 
-                }}>
-                    {record.running ? 'Running' : 'Stopped'}
-                </span>
-            } 
-         />
-      ),
-    },
-    { 
-      title: 'Target', 
-      dataIndex: 'instances', 
-      key: 'instances', 
-      width: 100,
-      align: 'center',
-      render: (val) => <TagPill value={val} label="Inst" />
-    },
-    { 
-      title: 'Resources', 
-      key: 'pools',
-      render: (_, record) => (
-        <Space direction="vertical" size={0}>
-          <Space size={4}>
-            <UserOutlined style={{ color: '#8c8c8c', fontSize: '12px' }} />
-            <Text style={{ fontSize: '13px', color: record.local_account_pool ? '#595959' : '#d9d9d9' }}>
-               {record.local_account_pool || 'N/A'}
-            </Text>
-          </Space>
-          <Space size={4}>
-            <GlobalOutlined style={{ color: '#8c8c8c', fontSize: '12px' }} />
-             <Text style={{ fontSize: '13px', color: record.proxies_pool ? '#595959' : '#d9d9d9' }}>
-               {record.proxies_pool || 'N/A'}
-            </Text>
-          </Space>
-        </Space>
-      )
-    },
-    {
-      title: 'Actions',
-      key: 'action',
-      width: 200,
-      align: 'right',
-      render: (_, record) => {
-        // Dropdown Menu for secondary actions
-        const menuItems = [
-            { key: 'logs', label: 'View Logs', icon: <FileTextOutlined />, onClick: () => setLogGroupId(record.id) },
-            { key: 'config', label: 'Configuration', icon: <SettingOutlined />, onClick: () => setConfigGroupId(record.id) },
-        ];
-
-        return (
-            <Space size="small">
-                {record.running ? (
-                    <Tooltip title="Stop">
-                        <Button 
-                            type="text" 
-                            danger 
-                            icon={<PauseCircleFilled style={{ fontSize: '18px' }} />} 
-                            onClick={() => handleAction(stopGroup, record.id)} 
-                            style={{ background: '#fff1f0', border: '1px solid #ffa39e' }}
-                        />
-                    </Tooltip>
-                ) : (
-                    <Tooltip title="Start">
-                        <Button 
-                            type="text" 
-                            icon={<PlayCircleFilled style={{ fontSize: '18px', color: '#52c41a' }} />} 
-                            onClick={() => handleAction(startGroup, record.id)}
-                            style={{ background: '#f6ffed', border: '1px solid #b7eb8f' }}
-                        />
-                    </Tooltip>
-                )}
-                
-                <Tooltip title="Restart">
-                     <Button 
-                        icon={<ReloadOutlined />} 
-                        onClick={() => handleAction(restartGroup, record.id)} 
-                    />
-                </Tooltip>
-
-                <Dropdown menu={{ items: menuItems }} trigger={['click']}>
-                    <Button icon={<MoreOutlined />} />
-                </Dropdown>
-            </Space>
-        );
-      },
-    },
-  ];
-
-  return (
-    <Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
-      {/* 1. Modern Header */}
-      <Header style={{ 
-        background: '#fff', 
-        padding: '0 24px', 
-        boxShadow: '0 2px 8px #f0f1f2', 
-        display: 'flex', 
-        alignItems: 'center', 
-        justifyContent: 'space-between',
-        zIndex: 10
-      }}>
-        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
-            <div style={{ 
-                width: '36px', height: '36px', 
-                background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)', 
-                borderRadius: '8px', 
-                display: 'flex', alignItems: 'center', justifyContent: 'center',
-                color: '#fff', fontSize: '20px'
-            }}>
-                <ThunderboltFilled />
-            </div>
-            <Title level={4} style={{ margin: 0, fontWeight: 600 }}>Visa Manager</Title>
-        </div>
-
-        <Space size="middle">
-            <Button 
-                onClick={() => setIsUpgradeOpen(true)} 
-                icon={<CloudUploadOutlined />}
-            >
-                OTA Update
-            </Button>
-            <Button 
-                type="primary" 
-                icon={<PlusOutlined />} 
-                onClick={() => setIsCreateOpen(true)}
-                style={{ borderRadius: '6px', height: '36px', padding: '0 20px' }}
-            >
-                Create Group
-            </Button>
-        </Space>
-      </Header>
-
-      <Content style={{ padding: '24px', maxWidth: '1600px', margin: '0 auto', width: '100%' }}>
-        
-        {/* 2. Stats Overview Cards */}
-        <Row gutter={16} style={{ marginBottom: '24px' }}>
-            <Col span={6}>
-                <StatCard title="Total Groups" value={stats.total} icon={<AppstoreOutlined />} color="#1890ff" />
-            </Col>
-            <Col span={6}>
-                <StatCard title="Running" value={stats.running} icon={<PlayCircleFilled />} color="#52c41a" />
-            </Col>
-            <Col span={6}>
-                <StatCard title="Stopped" value={stats.stopped} icon={<PauseCircleFilled />} color="#ff4d4f" />
-            </Col>
-            <Col span={6}>
-                <StatCard title="Total Instances" value={stats.totalInstances} icon={<ThunderboltFilled />} color="#faad14" />
-            </Col>
-        </Row>
-
-        {/* 3. Main Data Table */}
-        <Card 
-            bordered={false} 
-            bodyStyle={{ padding: '0' }}
-            style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.04)', overflow: 'hidden' }}
-        >
-          <Table 
-            dataSource={dataSource} 
-            columns={columns} 
-            loading={isLoading} 
-            rowKey="id" 
-            pagination={false}
-            rowClassName="align-middle"
-            size="middle"
-          />
-        </Card>
-      </Content>
-
-      {/* Modals */}
-      {configGroupId && <ConfigModal groupId={configGroupId} open={!!configGroupId} onClose={() => setConfigGroupId(null)} />}
-      {logGroupId && <LogViewer groupId={logGroupId} open={!!logGroupId} onClose={() => setLogGroupId(null)} />}
-      <UpgradeModal open={isUpgradeOpen} onClose={() => setIsUpgradeOpen(false)} />
-      <CreateGroupModal open={isCreateOpen} onClose={() => setIsCreateOpen(false)} />
-    </Layout>
-  );
-};
-
-// --- Helper Components ---
-
-const StatCard = ({ title, value, icon, color }) => (
-    <Card bordered={false} style={{ borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.02)' }}>
-        <Statistic 
-            title={<span style={{ fontSize: '13px', fontWeight: 500, color: '#8c8c8c' }}>{title}</span>}
-            value={value}
-            valueStyle={{ fontWeight: 'bold', fontSize: '24px', color: '#262626' }}
-            prefix={<span style={{ color, marginRight: '8px', fontSize: '20px', position: 'relative', top: '2px' }}>{icon}</span>}
-        />
-    </Card>
-);
-
-const TagPill = ({ value, label }) => (
-    <div style={{ 
-        background: '#f5f5f5', borderRadius: '4px', 
-        padding: '2px 8px', display: 'inline-block',
-        fontSize: '12px', color: '#595959', border: '1px solid #d9d9d9'
-    }}>
-        <span style={{ fontWeight: 'bold', marginRight: '4px' }}>{value}</span> 
-        <span style={{ fontSize: '10px', color: '#8c8c8c' }}>{label}</span>
-    </div>
-);
-
-export default Dashboard;

+ 0 - 22
front/vite.config.js

@@ -1,22 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-
-export default defineConfig({
-  plugins: [react()],
-  server: {
-    proxy: {
-      // API 请求代理
-      '/api': {
-        target: 'http://127.0.0.1:8000',
-        changeOrigin: true,
-        rewrite: (path) => path.replace(/^\/api/, '')
-      },
-      // WebSocket 请求代理 (新增部分)
-      '/ws': {
-        target: 'ws://127.0.0.1:8000',
-        ws: true, // 开启 WebSocket 代理支持
-        changeOrigin: true
-      }
-    }
-  }
-})

+ 0 - 552
gco.py

@@ -1,552 +0,0 @@
-# gco.py
-import os
-import time
-import json
-import random
-import threading
-from datetime import datetime, timezone
-from typing import List, Dict, Tuple, Any, Optional, Callable
-from concurrent.futures import wait
-
-# 导入所有依赖
-from vs_types import GroupConfig, QueryWaitMode, VSPlgConfig, VSQueryResult, Task 
-from vs_plg_factory import VSPlgFactory 
-from toolkit.proxy_manager import ProxyManager 
-from toolkit.thread_pool import ThreadPool 
-from toolkit.vs_cloud_api import VSCloudApi
-import traceback
-
-
-class GCO:
-    """
-    @brief GCO 类
-    负责管理一个组内的签证插件实例,包括实例的创建、健康检查、
-    任务调度、查询和预订流程。
-    """
-    def __init__(self, cfg: GroupConfig, logger: Callable[[str], None] = None):
-        self.m_cfg = cfg
-        self.m_factory = VSPlgFactory() # 插件工厂实例
-        self.m_logger = logger
-        
-        self.m_tasks: List[Task] = [] # 存储所有运行中的任务实例
-        
-        self.m_stop_event = threading.Event() # 用于停止线程
-        self.m_lock = threading.RLock() # 保护共享资源 (m_tasks)
-        
-        self.m_monitor_thread: Optional[threading.Thread] = None
-        self.m_creator_thread: Optional[threading.Thread] = None
-        
-        self.m_pending_builtin = 0  # 正在创建中的内置账号数量
-        self.m_pending_orders = 0   # 正在创建中的订单账号数量
-
-    def start(self):
-        """
-        @brief 启动协调器,包括插件注册和线程启动。
-        """
-        if not self.m_cfg.enable:
-            self._log("Group is disabled, not starting.")
-            return
-
-        self._log("Starting coordinator...")
-        self.m_stop_event.clear()
-
-        # 注册插件
-        plugin_module_path = os.path.join(self.m_cfg.plugin_config.lib_path, f"{self.m_cfg.plugin_config.plugin_bin}")
-        
-        # === 修复点:更智能的类名推导逻辑 ===
-        # 将 snake_case (e.g., "concrete_plugin") 转换为 PascalCase (e.g., "ConcretePlugin")
-        plugin_name = self.m_cfg.plugin_config.plugin_name
-        class_name = "".join(part.title() for part in plugin_name.split('_'))
-        
-        # 调试日志:确认推导出的类名
-        self._log(f"Inferring class name for plugin {plugin_name}: {class_name}")
-
-        self.m_factory.register_plugin(plugin_name, 
-                                       plugin_module_path, 
-                                       class_name)
-
-        self.m_monitor_thread = threading.Thread(target=self._monitor_loop, name=f"Monitor-{self.m_cfg.identifier}")
-        self.m_creator_thread = threading.Thread(target=self._creator_loop, name=f"Creator-{self.m_cfg.identifier}")
-        
-        self.m_monitor_thread.start()
-        self.m_creator_thread.start()
-        self._log("Coordinator threads started.")
-
-    def stop(self):
-        """
-        @brief 停止协调器,等待所有线程结束。
-        """
-        self._log("Stopping coordinator...")
-        self.m_stop_event.set() # 发送停止信号
-        
-        if self.m_monitor_thread and self.m_monitor_thread.is_alive():
-            self.m_monitor_thread.join()
-        if self.m_creator_thread and self.m_creator_thread.is_alive():
-            self.m_creator_thread.join()
-        self._log("Coordinator stopped.")
-
-    def group_id(self) -> str:
-        """
-        @brief 获取分组ID。
-        """
-        return self.m_cfg.identifier
-    
-    def _log(self, message):
-        if self.m_logger:
-            self.m_logger(f'[gco] [{self.m_cfg.identifier}] {message}')
-
-    def _monitor_loop(self):
-        """
-        @brief 监控循环:定期检查实例健康状况,执行查询任务。
-               一旦发现号源,立即触发所有实例进行批量预订。
-        """
-        self._log("[START] monitor loop starting...")
-        rng = random.Random()
-        
-        while not self.m_stop_event.is_set():
-            try:
-                sleep_ms = 0.1 + rng.randint(0, 20) / 1000.0
-                time.sleep(sleep_ms)
-                
-                now = time.time()
-                
-                tasks_to_process = []
-                with self.m_lock:
-                    tasks_to_process = list(self.m_tasks)
-                
-                batch_booking_triggered = False
-
-                for task in tasks_to_process:
-                    with self.m_lock:
-                        if task not in self.m_tasks:
-                            continue 
-
-                    if not task.instance.health_check():
-                        continue 
-                    
-                    if now < task.next_run:
-                        continue
-                    
-                    apt_type = None
-                    try:
-                        apt_types = self.m_cfg.appointment_types
-                        if not apt_types:
-                            self._log(f"No matching appointment configuration found.")
-                            continue
-                        
-                        weights = [float(item.weight) for item in apt_types]
-                        apt_type = random.choices(apt_types, weights=weights, k=1)[0]
-                        
-                        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 
-                        
-                        VSCloudApi.Instance().slot_refresh_success(
-                            apt_type.routing_key
-                        )
-
-                        if result.success:
-                            self._log(f"🔥 Slot Found by [{task.instance.get_group_id()}]! Triggering BATCH BOOKING.")
-                           
-                            query_payload = result.to_snapshot_payload()
-                            query_payload["website"] = self.m_cfg.website
-                            query_payload["snapshot_source"] = 'worker'
-                            query_payload["snapshot_at"] = datetime.now(timezone.utc).isoformat()
-                            VSCloudApi.Instance().slot_snapshot_report(query_payload)
-
-                            futures = []
-                            for worker in tasks_to_process:
-                                f = ThreadPool.getInstance().enqueue(
-                                    self._on_query_result, 
-                                    worker, 
-                                    result
-                                )
-                                futures.append(f)
-                            
-                            for f in futures:
-                                try:
-                                    f.result() 
-                                except Exception as e:
-                                    pass
-
-                            batch_booking_triggered = True
-                            break 
-                        
-                        else:
-                            self._log(f"Query done by {task.instance.get_group_id()}, No availability")
-
-                    except Exception as e:
-                        traceback.print_exc()
-                        self._log(f"Exception during query: {e}")
-                        if apt_type:
-                            VSCloudApi.Instance().slot_refresh_fail(
-                                apt_type.routing_key,
-                                error=str(e)
-                            )
-
-                    if not batch_booking_triggered:
-                        interval = 30
-                        mode = task.qw_cfg.mode
-                        if mode == QueryWaitMode.Loop:
-                            interval = 0
-                        elif mode == QueryWaitMode.Fixed:
-                            interval = task.qw_cfg.fixed_wait
-                        elif mode == QueryWaitMode.Random:
-                            interval = rng.randint(task.qw_cfg.random_min, task.qw_cfg.random_max)
-                        
-                        task.next_run = time.time() + interval
-
-                if batch_booking_triggered:
-                    self._log(f"Batch booking finished. Resetting wait times.")
-                    now_ts = time.time()
-                    
-                    with self.m_lock:
-                        for t in self.m_tasks:
-                            interval = 30
-                            mode = t.qw_cfg.mode
-                            if mode == QueryWaitMode.Loop:
-                                interval = 1 # 稍微给点缓冲
-                            elif mode == QueryWaitMode.Fixed:
-                                interval = t.qw_cfg.fixed_wait
-                            elif mode == QueryWaitMode.Random:
-                                interval = rng.randint(t.qw_cfg.random_min, t.qw_cfg.random_max)
-                            
-                            t.next_run = now_ts + interval
-                    
-                    self._log(f"All workers cooldown reset. Resuming monitor loop.")
-
-                with self.m_lock:
-                    initial_size = len(self.m_tasks)
-                    self.m_tasks[:] = [t for t in self.m_tasks if t.instance.health_check()]
-                    if len(self.m_tasks) < initial_size:
-                        self._log(f"Removed {initial_size - len(self.m_tasks)} unhealthy instance(s). Remaining: {len(self.m_tasks)}")
-
-            except Exception as outer_e:
-                traceback.print_exc()
-                self._log(f"🔥 CRITICAL ERROR in monitor loop (recovered): {outer_e}")
-                # 遇到严重错误休眠 1 秒,防止死循环刷屏占满 CPU
-                time.sleep(1.0)
-
-        self._log("[STOP] monitor loop exiting...")
-
-    def _creator_loop(self):
-        """
-        @brief 创建者循环:双轨制并发控制 (精简高可用版)
-        """
-        self._log("[START] creator loop starting...")
-        
-        while not self.m_stop_event.is_set():
-            try:
-                time.sleep(1.0) 
-
-                current_builtin = 0
-                current_orders = 0
-                with self.m_lock:
-                    for t in self.m_tasks:
-                        if t.task_ref: current_orders += 1
-                        else: current_builtin += 1
-                    
-                    pending_builtin = self.m_pending_builtin
-                    pending_orders = self.m_pending_orders
-
-                needed_builtin = 0
-                if self.m_cfg.target_instances > 0:
-                    needed_builtin = self.m_cfg.target_instances - (current_builtin + pending_builtin)
-
-                needed_order = 0
-                if self.m_cfg.need_account and self.m_cfg.order_account_online_limit > 0:
-                    needed_order = self.m_cfg.order_account_online_limit - (current_orders + pending_orders)
-
-                if needed_builtin <= 0 and needed_order <= 0:
-                    continue
-
-                config_data = self._prepare_next_config(
-                    need_builtin=(needed_builtin > 0),
-                    need_order=(needed_order > 0),
-                )
-                
-                if not config_data:
-                    time.sleep(5.0)
-                    continue
-
-                plg_cfg, task_ref = config_data
-                
-                with self.m_lock:
-                    if task_ref: self.m_pending_orders += 1
-                    else: self.m_pending_builtin += 1
-                
-                try:
-                    ThreadPool.getInstance().enqueue(
-                        self._create_and_register_plg_worker, plg_cfg, task_ref
-                    )
-                    self._log(f"+++ Spawning {'Order' if task_ref else 'Builtin'}...")
-                except Exception as e:
-                    self._log(f"Enqueue failed, rolling back: {e}")
-                    with self.m_lock:
-                        if task_ref: self.m_pending_orders -= 1
-                        else: self.m_pending_builtin -= 1
-                
-                time.sleep(random.uniform(0.5, 1.0))
-
-            except Exception as outer_e:
-                traceback.print_exc()
-                self._log(f"🔥 Creator loop error: {outer_e}")
-                time.sleep(5.0)
-
-        self._log("[STOP] creator loop exiting...")
-
-    def _prepare_next_config(self, need_builtin: bool, need_order: bool) -> Optional[Tuple[VSPlgConfig, Optional[Dict[str, Any]]]]:
-        """
-        @brief 准备下一个插件实例的配置
-        """
-        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
-        
-        task_ref = None 
-        config_ready = False
-        pool_name = self.m_cfg.local_account_pool 
-
-        # =================================================================
-        # 1. 账号获取
-        # =================================================================
-        if not self.m_cfg.need_account:
-            # === 游客模式 (无需账号) ===
-            if need_builtin:
-                plg_cfg.account.id = 0
-                plg_cfg.account.username = "Guest"
-                config_ready = True
-                task_ref = None
-        else:
-            # === 标准模式 (需要账号) ===
-            
-            # A. 优先补充内置账号 (只要 target_instances 还有缺口)
-            if need_builtin:
-                try:
-                    # 获取并锁定账号
-                    account = VSCloudApi.Instance().get_next_account(
-                        pool_name,
-                        lock_duration=self.m_cfg.account_login_interval * 60
-                    )
-                except Exception as e:
-                    # self._log(f"Get built-in account failed: {e}")
-                    account = None
-
-                if account:
-                    plg_cfg.account.id = account["id"]
-                    plg_cfg.account.username = account["username"]
-                    plg_cfg.account.password = account["password"]
-                    plg_cfg.account.lock_until = account.get("lock_until", 0)
-                    config_ready = True
-                    task_ref = None
-                    self._log(f"Selected Built-in: {plg_cfg.account.username}")
-
-            # B. 次选补充订单账号 (如果内置不需要 或 池子空了)
-            # 只有当 limit > 0 时才尝试
-            if not config_ready and need_order and self.m_cfg.order_account_online_limit > 0:
-                try:
-                    routing_key = self.m_cfg.order_account_routing
-                    task_ref = VSCloudApi.Instance().get_vas_task_pop(routing_key)
-                    
-                    if task_ref:
-                        user_inputs = task_ref.get('user_inputs', {})
-                        plg_cfg.account.id = 0 # 临时账号
-                        plg_cfg.account.username = user_inputs.get(self.m_cfg.input_map_username, "")
-                        plg_cfg.account.password = user_inputs.get(self.m_cfg.input_map_password, "")
-                        
-                        if plg_cfg.account.username:
-                            config_ready = True
-                            self._log(f"Selected Order Acc: {plg_cfg.account.username}")
-                        else:
-                            VSCloudApi.Instance().return_vas_task_to_queue(task_ref['id'])
-                            return None
-                except Exception as e:
-                    pass
-                    #self._log(f"Get Order task exception, e={e}")
-
-        if not config_ready:
-            return None
-
-        # =================================================================
-        # 2. 代理配置
-        # =================================================================
-        if self.m_cfg.need_proxy:
-            # 轮询代理
-            proxy_lock_time = self.m_cfg.proxy_lock_interval
-            proxy = ProxyManager.Instance().next(self.m_cfg.proxy_pool, lock_duration=proxy_lock_time)
-            if not proxy:
-                try:
-                    if task_ref:
-                        VSCloudApi.Instance().return_vas_task_to_queue(task_ref['id'])
-                    return None
-                except Exception as e:
-                    self._log(f"Return Order task to queue exception, e={e}")
-
-            plg_cfg.proxy.id = proxy["id"]
-            plg_cfg.proxy.ip = proxy["ip"]
-            plg_cfg.proxy.port = proxy["port"]
-            plg_cfg.proxy.scheme = proxy["scheme"]
-            plg_cfg.proxy.username = proxy.get("username", "")
-            plg_cfg.proxy.password = proxy.get("password", "")
-            plg_cfg.proxy.lock_until = proxy.get("lock_until", 0)
-
-        return plg_cfg, task_ref
-
-    def _create_and_register_plg_worker(self, plg_cfg: VSPlgConfig, task_ref: Optional[Dict[str, Any]] = None):
-        """
-        @brief 异步创建工作线程
-        """
-        instance = None
-        creation_success = False 
-        
-        try:
-            # 1. 耗时操作:实例化 & 登录
-            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() # 可能耗时很久
-
-            # 2. 注册到任务列表
-            with self.m_lock:
-                # 既然允许短暂超发,这里直接添加,不做严格的拒绝并返回逻辑
-                
-                # 计算预定权限
-                book_allowed = False
-                if not self.m_cfg.account_bind_applicant:
-                    book_allowed = True
-                elif self.m_cfg.account_bind_applicant and task_ref:
-                    book_allowed = True
-                
-                new_task = Task(
-                    instance=instance,
-                    qw_cfg=self.m_cfg.query_wait,
-                    next_run=time.time(),
-                    task_ref=task_ref,
-                    book_allowed=book_allowed
-                )
-                self.m_tasks.append(new_task)
-                
-                creation_success = True
-                
-                p_type = "Order" if task_ref else "Built-in"
-                self._log(f"=== Instance Registered [{p_type}]. Total Active: {len(self.m_tasks)} ===")
-
-        except Exception as e:
-            self._log(f"Creation failed for {plg_cfg.account.username}: {e}")
-            
-        finally:
-            # -------------------------------------------------------------
-            # 3. [绝对核心] 必须扣减 Pending
-            # -------------------------------------------------------------
-            # 无论上面是否抛出异常,或者是否注册成功,Pending 必须释放
-            with self.m_lock:
-                if task_ref:
-                    self.m_pending_orders -= 1
-                    if self.m_pending_orders < 0: self.m_pending_orders = 0
-                else:
-                    self.m_pending_builtin -= 1
-                    if self.m_pending_builtin < 0: self.m_pending_builtin = 0
-            
-            # 4. 失败回滚
-            if not creation_success:
-                # 如果是云端订单,必须归还,否则丢单
-                if task_ref:
-                    self._log(f"Rolling back task {task_ref['id']}")
-                    try:
-                        VSCloudApi.Instance().return_vas_task_to_queue(task_ref['id'])
-                    except:
-                        pass
-
-    def _on_query_result(self, t: Task, query_result: VSQueryResult):
-        self._log(f"Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")
-        
-        if not t.book_allowed:
-            return
-
-        # -------------------------------------------------------
-        # 1. 准备任务数据 (Data Preparation)
-        # -------------------------------------------------------
-        task_data = t.task_ref
-        is_cloud_task = False # 标记:是否为云端临时取出的任务(需要回滚)
-
-        # 如果没有绑定本地任务,尝试从云端 Pop
-        if not task_data:
-            apt_type = query_result.apt_type
-            booking_routing_key = f'auto.{apt_type.routing_key}' if apt_type.routing_key else "default"
-            task_data = VSCloudApi.Instance().get_vas_task_pop(booking_routing_key)
-            
-            if not task_data:
-                # 这种情况属于:内置账号查到了号,但云端没有待处理订单,直接放弃
-                self._log(f"No pending task found for key {booking_routing_key}. Abandoning slot.")
-                return
-            
-            is_cloud_task = True
-            self._log(f"Picked up Cloud Task ID {task_data['id']} for booking...")
-
-        # 统一提取核心参数
-        task_id = task_data['id']
-        order_id = task_data.get('order_id')
-        user_input = task_data.get('user_inputs', {})
-
-        # -------------------------------------------------------
-        # 2. 执行预订 (Execution)
-        # -------------------------------------------------------
-        booking_success = False
-        
-        try:
-            # 统一调用,无需区分来源
-            book_res = t.instance.book(query_result, user_input)
-            
-            # -------------------------------------------------------
-            # 3. 成功处理 (Success Handling)
-            # -------------------------------------------------------
-            if book_res.success:
-                booking_success = True
-                self._log(f"✅ Booking SUCCESS! Order: {order_id}")
-                
-                current_grab_info = {
-                    "account": book_res.account,
-                    "session_id": book_res.session_id,
-                    "urn": book_res.urn,
-                    "slot_date": book_res.book_date,
-                    "slot_time": book_res.book_time,
-                    "timestamp": int(time.time()),
-                    "payment_link": book_res.payment_link,
-                }
-                
-                update_data = {
-                    "status": "grabbed",
-                    "grabbed_history": current_grab_info
-                }
-                
-                VSCloudApi.Instance().update_vas_task(task_id, update_data)
-                self._log(f"Task {task_id} marked as GRABBED.")
-            
-            else:
-                self._log(f"❌ Booking Failed for Order {order_id}")
-
-        except Exception as e:
-            self._log(f"Exception during booking for Order {order_id}: {e}")
-
-        finally:
-            # -------------------------------------------------------
-            # 4. 回滚机制 (Rollback / Return to Queue)
-            # -------------------------------------------------------
-            # 只有两个条件同时满足才回滚:
-            # 1. 任务来自云端队列 (is_cloud_task is True)
-            # 2. 预定没有成功 (booking_success is False) - 包括预定失败或发生异常
-            if is_cloud_task and not booking_success:
-                self._log(f"Returning Task {task_id} to queue (status=pending).")
-                try:
-                    VSCloudApi.Instance().return_vas_task_to_queue(task_id)
-                except Exception as ex:
-                    self._log(f"Failed to return task to queue: {ex}")
-

+ 20 - 12
gco_wrapper.py

@@ -1,12 +1,10 @@
 import threading
 import collections
 from enum import Enum, auto
-from typing import Callable
-from gco import GCO
+from typing import Callable, Type, Any, Dict
 from vs_types import GroupConfig
 from vs_log_macros import VSC_INFO
 
-
 class State(Enum):
     CREATED = auto()
     LOADED = auto()
@@ -16,14 +14,22 @@ class State(Enum):
     FAILED = auto()
 
 class GCOWrapper:
-    def __init__(self, gco_cfg: GroupConfig):
+    def __init__(self, gco_class: Type[Any], gco_cfg: GroupConfig, redis_conf: Dict[str, Any]):
+        """
+        @param gco_class: 传入你要包装的具体类 (SentinelGCO 或 BookerGCO)
+        @param gco_cfg: 分组配置
+        @param redis_conf: Redis 连接配置 (字典)
+        """
+        self.gco_class = gco_class
         self.m_cfg = gco_cfg
+        self.redis_conf = redis_conf
+        
         self._gco = None
         self._state = State.CREATED
         self._log_queue = collections.deque(maxlen=1000)
         self._log_subscribers = set()
         self._lock = threading.Lock()
-        self._push_callback: Callable = lambda t,d,s: None
+        self._push_callback: Callable = lambda t, d, s: None
         
     def set_push_callback(self, callback: Callable):
         self._push_callback = callback
@@ -54,8 +60,7 @@ class GCOWrapper:
 
     def _transition(self, from_state, to_state):
         if self._state != from_state:
-            raise Exception(f"invalid transition {self._state} -> {to_state}")
-        print(f"[STATE] {self._state.name} -> {to_state.name}")
+            raise Exception(f"invalid transition {self._state.name} -> {to_state.name}")
         self._state = to_state
 
     def load(self):
@@ -64,16 +69,19 @@ class GCOWrapper:
     def start(self):
         self._transition(State.LOADED, State.RUNNING)
         try:
-            self._gco = GCO(self.m_cfg, logger=self._log)
-            self._gco.start()  # 真正的 start 工作
+            # 动态实例化传入的类,并传入 Redis 配置
+            self._gco = self.gco_class(self.m_cfg, self.redis_conf, logger=self._log)
+            self._gco.start()  # 调用 Sentinel 或 Booker 的 start
         except Exception:
             self._state = State.FAILED
             raise
 
     def stop(self):
+        if self._state != State.RUNNING:
+            return
         self._transition(State.RUNNING, State.STOPPING)
         try:
-            self._gco.stop()  # 原逻辑 stop
+            if self._gco:
+                self._gco.stop()  # 调用 Sentinel 或 Booker 的 stop
         finally:
-            self._transition(State.STOPPING, State.STOPPED)
-
+            self._transition(State.STOPPING, State.STOPPED)

+ 45 - 0
logger_setup.py

@@ -0,0 +1,45 @@
+import os
+import logging
+from logging.handlers import RotatingFileHandler
+
+import vs_log_macros  # 导入你的宏模块
+
+def setup_app_logger(app_name: str, log_dir: str = "logs") -> logging.Logger:
+    """
+    配置并返回一个标准的 Logger,支持终端输出和文件滚动保存。
+    """
+    if not os.path.exists(log_dir):
+        os.makedirs(log_dir)
+
+    logger = logging.getLogger(app_name)
+    logger.setLevel(logging.INFO)
+    
+    # 避免重复添加 Handler
+    if not logger.handlers:
+        log_file = os.path.join(log_dir, f"{app_name.lower()}.log")
+        
+        # 1. 终端输出
+        console_handler = logging.StreamHandler()
+        console_handler.setLevel(logging.INFO)
+        
+        # 2. 文件输出
+        file_handler = RotatingFileHandler(
+            log_file, maxBytes=10 * 1024 * 1024, backupCount=10, encoding='utf-8'
+        )
+        file_handler.setLevel(logging.INFO)
+        
+        # 设置格式
+        formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
+        console_handler.setFormatter(formatter)
+        file_handler.setFormatter(formatter)
+        
+        logger.addHandler(console_handler)
+        logger.addHandler(file_handler)
+
+    # ==========================================
+    # 核心:将这个专属 Logger 注入到宏!
+    # 这样整个系统里所有的 VSC_INFO 都会输出到你的 app_name.log 里!
+    # ==========================================
+    vs_log_macros.set_active_logger(logger)
+
+    return logger

+ 0 - 107
main.py

@@ -1,107 +0,0 @@
-# main.py
-import time
-import os
-import json
-import logging
-
-# 导入必要模块
-from vs_types import VSPlgConfig, AppointmentType
-from plugins.vfs_plugin2 import VfsPlugin2
-from vs_log_macros import VSC_INFO, VSC_ERROR
-
-def vfs_test():
-    # 1. 准备 VFS 业务配置 (free_config)
-    # 这部分 JSON 对应 VfsPlugin 中读取的配置
-    
-    cfg_dict = {
-        "debug": False,
-        "account": {
-            "username": "ie_is_cwwyxz@gmail-app.com",
-            "password": "Or1duEJc5@"
-        },
-        "proxy": {
-            "scheme": "http",
-            "ip": "127.0.0.1",
-            "port": 7890
-        },
-        "free_config": {
-            "mission_code": "isl",
-            "mission_name": "Iceland",
-            "country_code": "irl",
-            "country_name": "Ireland",
-            "culture_code": "en-US",
-            "language": "en",
-            "apt_configs": {
-                "slot.dub.is.tourist": {
-                    "center_name": "Iceland Visa Application Center- Dublin",
-                    "address": "Cunningham House, 130 Francis Street, Dublin, Ireland- DO8 H48R",
-                    "vac_code": "DUB",
-                    "category_name": "C-Visa",
-                    "category_code": "CVI",
-                    "subcategory_name": "Tourism",
-                    "subcategory_code": "OTT"
-                }
-            }
-        },
-        "session_max_life": 30
-    }
-    
-    apt_type_dict =  {
-        "weight": 10,
-        "routing_key": "slot.dub.is.tourist",
-        "city": "Dublin",
-        "visa_type": "Tourist",
-        "country": "Iceland"
-    }
-    
-    user_inputs = {
-        "email": "hujiarui8@gmail.com",
-        "phone": "07843900464",
-        "gender": "female",
-        "birthday": "1995-12-15",
-        "last_name": "Hu",
-        "first_name": "Jiarui",
-        "nationality": "China",
-        "passport_no": "EP9863641",
-        "_admin_bypass": {
-            "at": "2026-02-08T14:52:59.454636",
-            "by": "usr-db8f814c",
-            "reason": "admin manual order",
-            "enabled": True
-        },
-        "expected_end_date": "2023-03-15",
-        "phone_country_code": "44",
-        "expected_start_date": "2023-02-25",
-        "passport_expiry_date": "2035-09-17",
-        "social_media_account": "淘宝-yayale95778240"
-    }
-    
-    apt_type = AppointmentType(**apt_type_dict)
-
-    config = VSPlgConfig(**cfg_dict)
-
-    # 7. 启动
-    try:
-        VSC_INFO("main", "========================================")
-        VSC_INFO("main", "   VFS Python Plugin Tester      ")
-        VSC_INFO("main", "========================================")
-        
-        vfs_plg = VfsPlugin2("test")
-        vfs_plg.set_config(config)
-        vfs_plg.create_session()
-        slot_info = vfs_plg.query(apt_type)
-        slot_info.apt_type = apt_type
-        vfs_plg.book(slot_info, user_inputs)
-        
-        time.sleep(3600)
-    except KeyboardInterrupt:
-        VSC_INFO("main", "Ctrl+C detected. Stopping...")
-    except Exception as e:
-        VSC_ERROR("main", "Unexpected Error: %s", str(e))
-    finally:
-        # 8. 停止
-        VSC_INFO("main", "Program finished.")
-        
-    
-if __name__ == "__main__":
-    vfs_test()

+ 48 - 0
main_booker.py

@@ -0,0 +1,48 @@
+import time
+import json
+from vs_types import GroupConfig
+from gco_wrapper import GCOWrapper
+from logger_setup import setup_app_logger
+
+# 导入拆分后的两个类
+from booker_builtin import BuiltinBookerGCO
+from booker_order import OrderBookerGCO
+
+def main():
+    app_logger = setup_app_logger("Booker")
+    app_logger.info("Booker Logger is ready! All VSC macros are hooked.")
+
+    with open("config/config.json", "r", encoding="utf-8") as f:
+        cfg_data = json.load(f)
+
+    redis_conf = cfg_data.get('redis')
+    groups_conf = cfg_data.get('group_list')
+    wrappers = []
+
+    for item in groups_conf:
+        cfg = GroupConfig.from_json(item)
+        if not cfg.enable: continue
+            
+        # === 核心:根据配置自动选择实现类 ===
+        if cfg.booker.account_source == "order":
+            gco_class = OrderBookerGCO
+            app_logger.info(f"[{cfg.identifier}] Mode: ORDER (Bound)")
+        else:
+            gco_class = BuiltinBookerGCO
+            app_logger.info(f"[{cfg.identifier}] Mode: BUILT-IN (Unbound)")
+
+        wrapper = GCOWrapper(gco_class=gco_class, gco_cfg=cfg, redis_conf=redis_conf)
+        wrapper.load()
+        wrapper.start()
+        wrappers.append(wrapper)
+    
+    app_logger.info(f"Successfully started {len(wrappers)} Booker groups. Press Ctrl+C to stop.")
+    
+    try:
+        while True: time.sleep(1)
+    except KeyboardInterrupt:
+        app_logger.info("Shutting down Bookers...")
+        for wrapper in wrappers: wrapper.stop()
+
+if __name__ == "__main__":
+    main()

+ 46 - 0
main_sentinel.py

@@ -0,0 +1,46 @@
+import time
+import json
+from vs_types import GroupConfig
+from gco_wrapper import GCOWrapper
+from sentinel import SentinelGCO
+from logger_setup import setup_app_logger
+
+def main():
+    app_logger = setup_app_logger("Sentinel")
+    app_logger.info("Sentinel Logger is ready! All VSC macros are hooked.")
+
+    # 读取配置文件
+    with open("config/config.json", "r", encoding="utf-8") as f:
+        cfg_data = json.load(f)
+
+    redis_conf = cfg_data.get('redis')
+    groups_conf = cfg_data.get('group_list')
+
+    # 用于保存所有启动的 wrapper,方便后续退出时清理
+    wrappers = []
+
+    # 遍历 JSON 数组中的每一个组别配置
+    for item in groups_conf:
+        cfg = GroupConfig.from_json(item)
+        if not cfg.enable:
+            app_logger.info(f"Group [{cfg.identifier}] is disabled. Skipping.")
+            continue
+            
+        app_logger.info(f"Starting wrapper for group [{cfg.identifier}]...")
+        wrapper = GCOWrapper(gco_class=SentinelGCO, gco_cfg=cfg, redis_conf=redis_conf)
+        wrapper.load()
+        wrapper.start()
+        wrappers.append(wrapper)
+    
+    app_logger.info(f"Successfully started {len(wrappers)} Sentinel groups. Press Ctrl+C to stop.")
+    
+    try:
+        while True:
+            time.sleep(1)
+    except KeyboardInterrupt:
+        app_logger.info("Shutting down Sentinels...")
+        for wrapper in wrappers:
+            wrapper.stop()
+
+if __name__ == "__main__":
+    main()

+ 0 - 43
main_server.py

@@ -1,43 +0,0 @@
-# main_server.py
-import threading
-import time
-import os
-from core.app_manager import AppManager
-from web.server import run_web_server
-from vs_log_macros import VSC_INFO
-
-def main():
-    # 1. 确保目录存在
-    if not os.path.exists("config"): os.makedirs("config")
-    if not os.path.exists("plugins"): os.makedirs("plugins")
-    
-    # 2. 初始化 AppManager
-    manager = AppManager.Instance()
-    
-    # 3. 加载配置
-    VSC_INFO("main", "Loading configurations...")
-    manager.load_configs()
-    
-    # 4. 自动启动 enabled 的组
-    VSC_INFO("main", "Starting enabled groups...")
-    manager.start_all()
-    
-    # 5. 启动 Web Server (在主线程运行,或者新线程)
-    #由于 uvicorn.run 是阻塞的,我们直接在主线程跑 Web,
-    # 后台的 GroupCoordinators 已经在各自的线程里跑了。
-    VSC_INFO("main", "Starting Web API on port 8000...")
-    try:
-        # run_web_server()
-        while True:
-            time.sleep(3600)
-    except KeyboardInterrupt:
-        pass
-    finally:
-        # 退出时清理
-        VSC_INFO("main", "Shutting down...")
-        # 停止所有组
-        for gid in list(manager.executors.keys()):
-            manager.stop_group(gid)
-
-if __name__ == "__main__":
-    main()

+ 3 - 0
plugins/bls_plugin.py

@@ -79,6 +79,9 @@ class BlsPlugin(IVSPlg):
         # 从配置中读取 OCR 服务地址,如果没有则使用默认
         if self.free_config.get("local_service_url"):
             self.local_service_url = self.free_config["local_service_url"]
+            
+    def keep_alive(self):
+        pass
 
     def health_check(self) -> bool:
         if not self.is_healthy:

+ 332 - 373
plugins/de_plugin.py

@@ -3,40 +3,52 @@ import json
 import random
 import re
 import os
+import uuid
+import shutil
 import base64
+import socket
 from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
-from urllib.parse import urljoin
+from urllib.parse import urljoin, urlparse, urlencode
 
-from curl_cffi import requests, const
-from bs4 import BeautifulSoup
+# DrissionPage 核心
+from DrissionPage import ChromiumPage, ChromiumOptions
 
-
-from vs_plg import IVSPlg 
-from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, TimeSlot, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.vs_cloud_api import VSCloudApi 
+from vs_plg import IVSPlg
+from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
+from toolkit.vs_cloud_api import VSCloudApi
+from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
+from toolkit.proxy_tunnel import ProxyTunnel
 from toolkit.ocr_engine import DddOcrEngine
 
 
+class BrowserResponse:
+    def __init__(self, result_dict):
+        result_dict = result_dict or {}
+        self.status_code = result_dict.get('status', 0)
+        self.text = result_dict.get('body', '')
+        self.headers = result_dict.get('headers', {})
+        self.url = result_dict.get('url', '')
+        self._json = None
+    def json(self):
+        if self._json is None:
+            if not self.text: return {}
+            try: self._json = json.loads(self.text)
+            except: self._json = {}
+        return self._json
+
 def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
-    # 转换日期到YYYY-MM-DD 固定格式
     dt = datetime.strptime(data_str, date_str_format)
     return dt.strftime("%Y-%m-%d")
 
 def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
-    """
-    将邮箱域名替换为指定域名(默认 gmail-app.com)
-    """
-    if "@" not in email:
-        raise ValueError(f"Invalid email: {email}")
-
+    if "@" not in email: raise ValueError(f"Invalid email: {email}")
     local_part, _ = email.rsplit("@", 1)
     return f"{local_part}@{new_domain}"
 
 class DePlugin(IVSPlg):
     """
-    Germany (Visametric) 签证预约插件
-    适配 Visametric Ireland -> Germany 流程
+    Germany (Visametric) 签证预约插件 (Browser + Tunnel Mode)
     """
 
     def __init__(self, group_id: str):
@@ -44,110 +56,210 @@ class DePlugin(IVSPlg):
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.logger = None
-        self.session: Optional[requests.Session] = None
         
-        # 状态
+        # 浏览器实例
+        self.page: Optional[ChromiumPage] = None
+        
+        # 资源隔离
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+        
+        if not os.path.exists(self.root_workspace):
+            os.makedirs(self.root_workspace)
+            
+        self.tunnel = None # 代理隧道
         self.is_healthy = True
+        self.session_create_time: float = 0
         
-        # 关键上下文变量 (从页面提取)
-        self.base_url = "https://ie-appointment.visametric.com" 
+        # 字符识别引擎
+        self.ocr_engine: Optional[DddOcrEngine] = None
+        
+        # 业务状态
+        self.base_url = "https://ie-appointment.visametric.com"
         self.csrf_token = ""
         self.personal_info_val = ""
         self.email_val_control = ""
-        
-        # 默认 OCR 服务地址
-        self.session_create_time: float = 0
 
     def get_group_id(self) -> str:
         return self.group_id
     
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
-
+        
+    def _log(self, message): 
+        if self.logger:
+            self.logger(f'[DePlugin] [{self.group_id}] {message}')
+        else:
+            print(f'[DePlugin] [{self.group_id}] {message}')
+        
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.free_config = config.free_config or {}
-            
         if self.free_config.get("base_url"):
             self.base_url = self.free_config["base_url"].rstrip('/')
+            
+    def keep_alive(self):
+        pass
 
     def health_check(self) -> bool:
         if not self.is_healthy:
             return False
-        if self.session is None:
+        if not self.page:
+            return False
+        try:
+            if not self.page.run_js("return 1;"):
+                return False
+        except:
             return False
         if self.config.session_max_life > 0:
-            current_time = time.time()
-            elapsed_time = current_time - self.session_create_time
-            if elapsed_time > self.config.session_max_life * 60:
-                self._log(f"Session Life ({int(elapsed_time)}s) out of max life limit ({self.config.session_max_life * 60}s), mark as unhealth session")
+            if time.time() - self.session_create_time > self.config.session_max_life * 60:
+                self._log("Session expired.")
                 return False
         return True
 
     def create_session(self):
         """
-        初始化会话:过盾 -> 获取 CSRF -> 识别图片验证码 -> 提交验证码 -> 获取上下文
+        创建会话:启动浏览器 -> 代理隧道 -> 过盾 -> 提取 Captcha -> 本地识别 -> 提交 -> 获取 Context
         """
-        # 1. 初始化 Session
-        curlopt = {
-            const.CurlOpt.MAXAGE_CONN: 1800,
-            const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: self.config.debug,
-        }
-
-        self.session = requests.Session(
-            proxy=self._get_proxy_url(),
-            impersonate="chrome124",
-            curl_options=curlopt,
-            use_thread_local_curl=False,
-            http_version=const.CurlHttpVersion.V2TLS
-        )
+        self._log(f"Initializing Session (ID: {self.instance_id})...")
         self.ocr_engine = DddOcrEngine()
-        # 2. 访问首页,获取 CSRF 和 Captcha 图片
-        # Visametric 首页通常有 Cloudflare
-        url_home = f"{self.base_url}/en"
+        co = ChromiumOptions()
+        # 端口分配 (Docker 适配)
+        def get_free_port():
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.bind(('', 0)); return s.getsockname()[1]
+        co.set_local_port(get_free_port())
         
-        # 尝试过盾
-        self._solve_cloudflare5S_challenge()
+        # 路径与隔离
+        co.set_user_data_path(self.user_data_path)
+        chrome_path = os.getenv("CHROME_BIN")
+        if chrome_path and os.path.exists(chrome_path):
+            co.set_paths(browser_path=chrome_path)
+            
+        # 代理隧道
+        if self.config.proxy and self.config.proxy.ip:
+            p = self.config.proxy
+            if p.username and p.password:
+                self._log(f"Starting Tunnel for {p.ip}...")
+                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
+                local_proxy = self.tunnel.start()
+                self._log(f"Tunnel started at {local_proxy}")
+                co.set_argument(f'--proxy-server={local_proxy}')
+            else:
+                proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
+                co.set_argument(f'--proxy-server={proxy_str}')
+        else:
+            self._log("[WARN] No proxy configured!")
+
+        # Docker 核心参数
+        co.headless(False)
+        co.set_argument('--no-sandbox')
+        co.set_argument('--disable-gpu')
+        co.set_argument('--disable-dev-shm-usage') 
+        co.set_argument('--window-size=1920,1080')
+        co.set_argument('--disable-blink-features=AutomationControlled')
+        co.set_argument('--ignore-certificate-errors')
+
+        try:
+            self.page = ChromiumPage(co)
+            
+            # 1. 访问首页
+            url_home = f"{self.base_url}/en"
+            self._log(f"Navigating to {url_home}")
+            self.page.get(url_home)
+            
+            # 2. Cloudflare 过盾
+            cf = CloudflareBypasser(self.page, log=self.config.debug)
+            if not cf.bypass(max_retry=15):
+                if "access denied" in self.page.title.lower():
+                    raise BizLogicError("Cloudflare Access Denied")
+                raise BizLogicError("Cloudflare bypass timeout")
+
+            # 3. 提取 CSRF 和 验证码
+            # 等待页面加载
+            meta_ele = self.page.ele('xpath://meta[@name="csrf-token"]', timeout=30)
+                
+            if not meta_ele:
+                # 截图调试,看看是不是还在 Cloudflare 或者加载失败
+                self.page.get_screenshot(path='csrf_not_found.jpg')
+                raise NotFoundError("CSRF Token meta tag not found (Page load failed?)")
+            
+            self.csrf_token = meta_ele.attr('content')
+            
+            # 提取验证码图片 (Visametric Base64)
+            html = self.page.html
+            match = re.search(r'"data:image/png;base64,"\s*\+\s*"(.*?)"', html)
+            if not match:
+                # 尝试直接找 img
+                try:
+                    img_ele = self.page.ele('xpath://img[contains(@src, "data:image")]')
+                    if img_ele:
+                        b64_src = img_ele.attr('src')
+                        captcha_b64 = b64_src.split(',')[1]
+                    else:
+                        raise NotFoundError("Captcha image not found")
+                except:
+                    raise NotFoundError("Captcha image not found (Regex failed)")
+            else:
+                captcha_b64 = match.group(1)
+            image_bytes = base64.b64decode(captcha_b64)
+            # 4. 识别验证码 (本地 OCR 服务)
+            captcha_code = self.ocr_engine.inference_captcha(image_bytes)
+
+            # 5. 提交验证码 (获取 PersonalInfo)
+            self._submit_captcha(captcha_code)
+            
+            self.session_create_time = time.time()
+            self._log("Session created successfully.")
 
-        default_headers = self._get_headers()
-        default_headers.pop("X-Requested-With")
+        except Exception as e:
+            self._log(f"Session Create Failed: {e}")
+            self.cleanup()
+            raise e
         
-        resp = self._perform_request('GET', url_home, headers=default_headers)
-        if self.config.debug:
-            self._save_debug_html(resp.text, prefix="VisaMetric_Home_Page")
-        html = resp.text
-        soup = BeautifulSoup(html, 'html.parser')
-        meta = soup.find('meta', {'name': 'csrf-token'})
-        if not meta:
-            raise NotFoundError(message='Missing csrf-token in html')
-        self.csrf_token = meta.get('content', '')
-
-        # 提取验证码图片 Base64, 正则匹配: "data:image/png;base64," + "..."
-        match = re.search(r'"data:image/png;base64,"\s*\+\s*"(.*?)"', html)
-        if not match:
-            raise NotFoundError(message="Captcha image not found")
-  
-        captcha_b64 = base64.b64decode(match.group(1))
-        
-        captcha_code = self.ocr_engine.inference_captcha(captcha_b64)
-        self._log(f"Captcha recognized: {captcha_code}")
-
-        # 4. 提交验证码 (/appointment-form)
-        # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
-        self._submit_captcha(captcha_code)
-        self.session_create_time = time.time()
-        self._log("Session created successfully.")
+    def get_booked_applicants(self)-> List[Dict]:
+        return []
 
-    def query(self, apt_type: AppointmentType) -> VSQueryResult:
+    def _submit_captcha(self, code):
         """
-        查询可用日期 (/getdate)
+        提交验证码,获取 personalinfo 和 emailValControl
         """
-        res = VSQueryResult()
-        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
-        consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
-        max_retries = self.free_config.get("slot_query_max_retries", 2)
+        url = f"{self.base_url}/en/appointment-form"
+        payload = {
+            '_token': self.csrf_token,
+            'cpJvnsControl': '',
+            'mailConfirmCode': code
+        }
+        
+        # 使用 Fetch 提交 (Form-UrlEncoded)
+        resp = self._perform_request('POST', url, data=payload, headers={
+            'X-Requested-With': 'XMLHttpRequest'
+        })
+        
+        # 解析返回的 HTML 片段
+        html = resp.text
+        
+        # 提取 personalinfo
+        match_pi = re.search(r"personalinfo:\s*'([^']*)'", html)
+        if match_pi: self.personal_info_val = match_pi.group(1)
+        
+        # 提取 emailValControl
+        match_ev = re.search(r"emailValControl:\s*'([^']*)'", html)
+        if match_ev: self.email_val_control = match_ev.group(1)
+            
+        if not self.personal_info_val:
+            raise NotFoundError(message="Personalinfo not found in captcha response")
+        
+        # 更新 CSRF (如果返回了新的)
+        m = re.search(r'name="csrf-token" content="([^"]+)"', html)
+        if m: self.csrf_token = m.group(1)
 
+    def query(self, apt_type: AppointmentType) -> VSQueryResult:
+        res = VSQueryResult()
+        res.success = False
+        
+        consular_id = self.free_config.get("consularid", "1")
         url = f"{self.base_url}/en/getdate"
         payload = {
             "consularid": consular_id,
@@ -157,27 +269,17 @@ class DePlugin(IVSPlg):
             "totalperson": "1"
         }
         
-        default_headers = self._get_headers()
-        default_headers['X-CSRF-TOKEN'] = self.csrf_token
-        
+        headers = {
+            'X-CSRF-TOKEN': self.csrf_token,
+            'X-Requested-With': 'XMLHttpRequest'
+        }
         
-        for attempt in range(1, max_retries + 1):
-            try:
-                resp = self._perform_request('POST', url, data=payload, headers=default_headers)
-                break  # ✅ 请求成功,跳出重试循环
-
-            except PermissionDeniedError:
-                self._log(f"Getdate blocked (403), attempt {attempt}/{max_retries}")
-
-                # 最后一次就不再绕盾了
-                if attempt >= max_retries:
-                    raise PermissionDeniedError()
-
-                self._solve_cloudflare5S_challenge()
-                self._log("Cloudflare bypass success, retrying...")
-                continue
+        try:
+            resp = self._perform_request('POST', url, data=payload, headers=headers, retry_count=1)
+        except Exception as e:
+            self._log(f"Query Error: {e}")
+            raise e
 
-        # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
         j = resp.json()
         dates = j.get("getDateEnable", [])
         
@@ -186,27 +288,18 @@ class DePlugin(IVSPlg):
             res.availability_status = AvailabilityStatus.Available
             earliest_date = dates[0]
             earliest_dt = datetime.strptime(earliest_date, "%d-%m-%Y")
-            # Visametric 返回 DD-MM-YYYY → 标准化为 YYYY-MM-DD
             res.earliest_date = earliest_dt
-
             res.availability = [
                 DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
                 for d in dates
             ]
-
         else:
-            # 查询成功,但没有可用日期
-            res.success = False
             res.availability_status = AvailabilityStatus.NoneAvailable
 
         return res
 
     def book(self, slot_info: VSQueryResult, user_inputs: Dict) -> VSBookResult:
-        """
-        执行预约:选择日期 -> 选择时间 -> 发邮件 -> 填表 -> 提交
-        """
         res = VSBookResult()
-        # 1. 筛选日期
         available_dates = [da.date for da in slot_info.availability]
         exp_start = user_inputs.get('expected_start_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
@@ -216,39 +309,32 @@ class DePlugin(IVSPlg):
             for da in slot_info.availability
         ]
         
-        valid_dates = self._filter_dates(available_dates_str, exp_start, exp_end)
+        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
         if not valid_dates:
-            raise NotFoundError(message="No dates match user constraints")
+            raise NotFoundError("No dates match constraints")
         
         target_date = random.choice(valid_dates)
         self._log(f"Selected date: {target_date}")
         
-        # 2. 获取时间 (/senddate)
+        # 1. 获取时间 Slot
         time_slot = self._get_slot_time(target_date)
-        self._log(f"Selected time: {time_slot['time']}")
-
-        # 3. 触发邮件流程 (Step 1: /jky45fgd)
+        
+        # 2. 发送邮件流程
         alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
         self._send_email_step1(alias_email)
-            
-        # 4. 触发邮件流程 (Step 2: /confirmCodeSendMail) 这一步会发送包含验证码的邮件 根据原代码逻辑: send_email("0") 触发发送
         self._send_email_step2("0")
-
-        # 5. 读取 OTP
-        # Visametric 邮件发送者
-        recipient = alias_email
-        otp_code = self._read_otp_email(recipient)
-            
-        # 6. 提交验证码并确认 (/personal/appointment/create)
+        
+        # 3. 读取 OTP
+        otp_code = self._read_otp_email(alias_email)
+        
+        # 4. 提交确认
         book_res_html = self._confirm_appointment(target_date, time_slot, user_inputs, otp_code, alias_email)
-            
+        
         if "complete all required fields" in book_res_html.lower():
-            raise BizLogicError(message='Comfirm appointment response <complete all required fields>')
-    
-        # 7. 提取结果
+            raise BizLogicError("Incomplete fields response")
+            
         match = re.search(r'https:\/\/checkout\.stripe\.com\/c\/pay\/[^\s"]+', book_res_html)
         
-        
         res.success = True
         res.fee_amount = 3000
         res.fee_currency = 'EUR'
@@ -257,80 +343,70 @@ class DePlugin(IVSPlg):
         
         if match:
             res.payment_link = match.group(0)
-            self._log(f"Payment Link Found: {res.payment_link}")
+            self._log(f"Payment Link: {res.payment_link}")
+            
         return res
 
     # ---------------------------------------------------------
     # 辅助方法
     # ---------------------------------------------------------
-    def _log(self, message):
-        if self.logger:
-            self.logger(f'[DePlugin] [{self.group_id}] {message}')
     
-    def _get_headers(self) -> Dict[str, str]:
-        """基础 Header"""
-        return {
-            "Accept": "*/*",
-            "Accept-Language": "en,zh-CN;q=0.9,zh;q=0.8",
-            "Origin": self.base_url,
-            "Referer": f"{self.base_url}/en/appointment-form", # 默认 Referer
-            "X-Requested-With": "XMLHttpRequest"
-        }
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
+        if not self.page:
+            raise BizLogicError("Browser not init")
         
-    def _get_proxy_url(self):
-        # 构造代理
-        proxy_url = ""
-        if self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+        req_url = url
+        if params:
+            sep = '&' if '?' in req_url else '?'
+            req_url += sep + urlencode(params)
+            
+        fetch_opts = { "method": method.upper(), "headers": headers or {}, "credentials": "include" }
+        
+        if json_data:
+            fetch_opts['body'] = json.dumps(json_data)
+            fetch_opts['headers']['Content-Type'] = 'application/json'
+        elif data:
+            if isinstance(data, dict):
+                fetch_opts['body'] = urlencode(data)
+                fetch_opts['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
             else:
-                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
-        return proxy_url
-    
-    def _save_debug_html(self, content: str, prefix: str = "debug"):
-        save_dir = "debug_pages"
-        if not os.path.exists(save_dir):
-            os.makedirs(save_dir)
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"{save_dir}/{prefix}_{timestamp}.html"
-        with open(filename, "w", encoding="utf-8") as f:
-            f.write(content)
-        self._log(f"HTML saved to: {filename}")
-
-    def _submit_captcha(self, code):
-        url = f"{self.base_url}/en/appointment-form"
-        payload = {
-            '_token': self.csrf_token,
-            'cpJvnsControl': '',
-            'mailConfirmCode': code
-        }
-        headers = self._get_headers()
-        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+                fetch_opts['body'] = data
+
+        js = f"""
+        return fetch("{req_url}", {json.dumps(fetch_opts)})
+        .then(async r => {{
+            const h = {{}}; r.headers.forEach((v, k) => h[k] = v);
+            return {{ status: r.status, body: await r.text(), headers: h, url: r.url }};
+        }}).catch(e => {{ return {{ status: 0, body: e.toString() }}; }});
+        """
         
-        resp = self._perform_request('POST', url, data=payload, headers=headers)
-        if self.config.debug:
-            self._save_debug_html(resp.text, prefix="VisaMetric_Make_Appointment_Page")
-        # 关键:提交验证码后,返回的 HTML 中包含了后续需要的加密参数        
-        match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
-        if match_pi:
-            self.personal_info_val = match_pi.group(1)
-        
-        # emailValControl: '...'
-        match_ev = re.search(r"emailValControl:\s*'([^']*)'", resp.text)
-        if match_ev:
-            self.email_val_control = match_ev.group(1)
-            
-        if not self.personal_info_val:
-            raise NotFoundError(message="Personalinfo not found in captcha response")
+        resp = BrowserResponse(self.page.run_js(js, timeout=60))
         
-        soup = BeautifulSoup(resp.text, 'html.parser')
-        meta = soup.find('meta', {'name': 'csrf-token'})
-        if not meta:
-            raise NotFoundError(message='Missing csrf-token in html')
-        self.csrf_token = meta.get('content', '')
-   
-    def _get_slot_time(self, date) -> Optional[Dict]:
+        if resp.status_code == 200:
+            return resp
+        elif resp.status_code == 403:
+            if "Just a moment" in resp.text and retry_count < 2:
+                self._log("Cloudflare 403. Refreshing...")
+                if self._refresh_firewall_session():
+                    return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
+            raise PermissionDeniedError(f"HTTP 403: {resp.text[:100]}")
+        elif resp.status_code == 429:
+            self.is_healthy = False
+            raise RateLimiteddError()
+        elif resp.status_code in [401, 419]:
+            self.is_healthy = False
+            raise SessionExpiredOrInvalidError()
+        else:
+            raise BizLogicError(f"HTTP {resp.status_code}: {resp.text[:100]}")
+
+    def _refresh_firewall_session(self):
+        try:
+            self.page.refresh()
+            cf = CloudflareBypasser(self.page, log=self.config.debug)
+            return cf.bypass(max_retry=10)
+        except: return False
+
+    def _get_slot_time(self, date) -> Dict:
         url = f"{self.base_url}/en/senddate"
         dt_m = datetime.strptime(date, "%Y-%m-%d")
         converted_date = dt_m.strftime("%d-%m-%Y")
@@ -343,234 +419,117 @@ class DePlugin(IVSPlg):
             "set_new_service_type_id": "1",
             "personalinfo": self.personal_info_val
         }
-        headers = self._get_headers()
-        headers['X-CSRF-TOKEN'] = self.csrf_token # 这里需要 CSRF
-        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
-        
+        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
         resp = self._perform_request('POST', url, data=payload, headers=headers)
-        soup = BeautifulSoup(resp.text, 'html.parser')
-        buttons = soup.find_all('button')
-        slots = []
-        for btn in buttons:
-            i_tag = btn.find('i')
-            if i_tag:
-                time_val = i_tag.next_sibling.strip()
-                slots.append({
-                    'time': time_val,
-                    'data_id': btn.get('data-id'),
-                    'data_all': btn.get('data-all')
-                })
-        if slots:
-            return random.choice(slots)
-        else:
-            raise NotFoundError(message='Not slot time available')
-   
+        
+        # 使用 Regex 提取 Slot
+        times = []
+        # pattern: data-id="123" ... <i>09:00</i>
+        for m in re.finditer(r'data-id="([^"]+)"[^>]*data-all="([^"]+)"[^>]*>.*?<i>(.*?)</i>', resp.text, re.DOTALL):
+            times.append({'data_id': m.group(1), 'data_all': m.group(2), 'time': m.group(3).strip()})
+            
+        if not times: raise NotFoundError("No time slots")
+        return random.choice(times)
 
     def _send_email_step1(self, email):
         url = f"{self.base_url}/en/jky45fgd"
-        payload = {
-            "emailCheck": email,
-            "personalinfo": self.personal_info_val
-        }
-        headers = self._get_headers()
-        headers['X-CSRF-TOKEN'] = self.csrf_token
-        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        payload = { "emailCheck": email, "personalinfo": self.personal_info_val }
+        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
         self._perform_request('POST', url, data=payload, headers=headers)
-        
 
     def _send_email_step2(self, code_val):
         url = f"{self.base_url}/en/confirmCodeSendMail"
-        payload = {
-            "confirmCode": code_val,
-            "emailValControl": self.email_val_control
-        }
-        headers = self._get_headers()
-        headers['X-CSRF-TOKEN'] = self.csrf_token
-        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
+        payload = { "confirmCode": code_val, "emailValControl": self.email_val_control }
+        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
         self._perform_request('POST', url, data=payload, headers=headers)
-    
+
     def _read_otp_email(self, recipient) -> str:
-        """
-        读取 OTP 邮件
-        """
         master_email = "visafly666@gmail.com"
         sender = 'Visametric - verify at visametric.com'
-        subject_keywords = 'Verification Code'
-        body_keywords = 'Verification code'
-
-        now_utc = datetime.utcnow()
-        formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
-
-        self._log(f"Waiting for OTP email sent after {formatted_utc_time}...")
-
-        # 3. 轮询查收
+        now_utc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
         for i in range(12):
-            content_out = VSCloudApi.Instance().fetch_mail_content(
-                master_email,
-                sender,
-                recipient,
-                subject_keywords,
-                body_keywords,
-                formatted_utc_time,
-                300
-            )
-
-            if content_out:
-                match = re.search(r'\b\d{6}\b', content_out)
-                if match:
-                    otp = match.group(0)
-                    self._log(f"OTP code found: {otp}")
-                    return otp
-            
+            c = VSCloudApi.Instance().fetch_mail_content(master_email, sender, recipient, 'Verification Code', 'Verification code', now_utc, 300)
+            if c:
+                m = re.search(r'\b\d{6}\b', c)
+                if m: return m.group(0)
             time.sleep(5)
-        raise NotFoundError(message="OTP email not found (timeout)")
+        raise NotFoundError("OTP timeout")
 
     def _confirm_appointment(self, date, slot_data, user_inputs, otp, alias_email):
         url = f"{self.base_url}/en/personal/appointment/create"
-        
-        # 处理日期格式 YYYY-MM-DD
-        def _get_dob(d_str):
-            try: return datetime.strptime(d_str[:10], "%Y-%m-%d")
+        def _get_dob(d):
+            try: return datetime.strptime(d[:10], "%Y-%m-%d")
             except: return datetime.now()
-            
         dob = _get_dob(user_inputs.get('birthday', ''))
         
         payload = {
             "_token": self.csrf_token,
             "country": str(self.free_config.get("consularid", "1")),
             "visitingcountry": str(self.free_config.get("consularid", "1")),
-            "city": "6", # Dublin? 需配置
+            "city": "6",
             "office": "1",
             "officetype": "1",
             "totalPerson": "1",
-            
             "name1": user_inputs.get('first_name', '').upper(),
             "surname1": user_inputs.get('last_name', '').upper(),
-            "nationality1": "2", # 假设值
-            
+            "nationality1": "2",
             "birthday1": str(dob.day),
             "birthmonth1": str(dob.month),
             "birthyear1": str(dob.year),
-            
             "passport1": user_inputs.get('passport_no'),
-            # 原代码 passport_expried 是 DD-MM-YYYY
             "passportExpirationDate1": datetime.strptime(user_inputs.get('passport_expiry_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
             "email1": alias_email,
             "phone1": user_inputs.get('phone_no'),
             "alternativephone1": "",
-            
-            # 其他 person 留空
-            "name2": "", "surname2": "", "nationality2": "0", "birthday2": "0", "birthmonth2": "0", "birthyear2": "0", "passport2": "", "passportExpirationDate2": "", "email2": alias_email, "phone2": user_inputs.get('phone_no'), "alternativephone2": "",
-            "name3": "", "surname3": "", "nationality3": "0", "birthday3": "0", "birthmonth3": "0", "birthyear3": "0", "passport3": "", "passportExpirationDate3": "", "email3": alias_email, "phone3": user_inputs.get('phone_no'), "alternativephone3": "",
-            "name4": "", "surname4": "", "nationality4": "0", "birthday4": "0", "birthmonth4": "0", "birthyear4": "0", "passport4": "", "passportExpirationDate4": "", "email4": alias_email, "phone4": user_inputs.get('phone_no'), "alternativephone4": "",
-
             "mailConfirmCode": otp,
             "ctval": slot_data['data_id'],
             "qtallvert": slot_data['data_all'],
-            
             "oldofficetype": "1",
             "oldtotalperson": "1",
             "rePaymentControl": "0",
-            
-            # 关键:View Set
-            "view_set_app_country": "Schengen - Tourism/Family&Friend Visit/Transit Visa/Other Purposes", # 需配置
+            "view_set_app_country": "Schengen - Tourism/Family&Friend Visit/Transit Visa/Other Purposes",
             "view_set_app_office": "Dublin",
             "view_set_app_service_type": "NORMAL",
-            
             "cargoactive": "0",
             "setnewcalendarstatus": "2",
             "availableDaycontrol": "0",
-            
             "travelStartDate": datetime.strptime(user_inputs.get('travel_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
             "personalapproveTerms": "1"
         }
         
-        headers = self._get_headers()
-        headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
-        
-        resp = self._perform_request('POST', url, data=payload, headers=headers)
-        return resp.text
-
-    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
-        """
-        根据用户的期望范围筛选可用日期
-        
-        :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
-        :param start_str: 用户期望开始日期 (YYYY-MM-DD)
-        :param end_str: 用户期望结束日期 (YYYY-MM-DD)
-        :return: 符合要求的日期列表
-        """
-        # 如果没有设置范围,则不过滤,返回所有日期
-        if not start_str or not end_str:
-            return dates
-            
-        valid_dates = []
-        # 截取前10位以防带有时分秒
-        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
-        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
-        
-        for date_str in dates:
-            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
-            # 比较范围 (闭区间)
-            if s_date <= curr_date <= e_date:
-                valid_dates.append(date_str)
-        random.shuffle(valid_dates)
-        return valid_dates
-    
-    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
-        """
-        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
-        1. 发送 OPTIONS 请求
-        2. 发送实际请求
-        """
-        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        if self.config.debug:
-            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
-        if resp.status_code == 200:
-            return resp
-        elif resp.status_code in [401, 419]:
-            self.is_healthy = False
-            raise SessionExpiredOrInvalidError()
-        elif resp.status_code == 403:
-            raise PermissionDeniedError()
-        elif resp.status_code == 429:
-            self.is_healthy = False
-            raise RateLimiteddError()
-        else:
-            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
-
-    def _solve_cloudflare5S_challenge(self):
-        """
-        解决 Cloudflare 5s 盾
-        """
-        self._log(f"Solving Cloudflare 5s...")
-        website_url = f'{self.base_url}/en'
+        # 补全空字段 (Person 2-4)
+        for i in range(2, 5):
+            payload.update({
+                f"name{i}": "", f"surname{i}": "", f"nationality{i}": "0", f"birthday{i}": "0", f"birthmonth{i}": "0", f"birthyear{i}": "0", f"passport{i}": "", f"passportExpirationDate{i}": "", f"email{i}": alias_email, f"phone{i}": user_inputs.get('phone_no'), f"alternativephone{i}": ""
+            })
+
+        headers = {'X-Requested-With': 'XMLHttpRequest'}
+        return self._perform_request('POST', url, data=payload, headers=headers).text
+
+    def _filter_dates(self, dates, start, end):
+        if not start or not end: return dates
+        valid = []
+        s = datetime.strptime(start[:10], "%Y-%m-%d")
+        e = datetime.strptime(end[:10], "%Y-%m-%d")
+        for d in dates:
+            c = datetime.strptime(d, "%Y-%m-%d")
+            if s <= c <= e: valid.append(d)
+        random.shuffle(valid)
+        return valid
+
+    def cleanup(self):
+        if self.page:
+            try: self.page.quit()
+            except: pass
+            self.page = None
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try: time.sleep(0.2); shutil.rmtree(self.root_workspace, ignore_errors=True); break
+                except: time.sleep(0.5)
+        if self.tunnel:
+            try: self.tunnel.stop()
+            except: pass
+            self.tunnel = None
         
-        # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
-        p = self.config.proxy
-        if p.username:
-            proxy_str = f"{p.ip}:{p.port}:{p.username}:{p.password}"
-        else:
-            proxy_str = f"{p.ip}:{p.port}"
-        # 2. 提交任务
-        task_id = VSCloudApi.Instance().create_task(
-            command="AntiCloudflareTask",
-            args={
-                "proxy": proxy_str,
-                "websiteUrl": website_url
-            }
-        )
-        
-        result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
-        task_result = result_data.get("result", {})
-        cookies_list = task_result.get('cookies', [])
-        for cookie in cookies_list:
-            if cookie['name'] in ['__cf_bm', 'cf_clearance']:
-                self.session.cookies.set(
-                    cookie['name'], 
-                    cookie['value'], 
-                    domain=cookie['domain'], 
-                    path='/'
-                )
-        self.session.headers['User-Agent'] = task_result.get('userAgent')
-        self._log("Cloudflare 5s challenge solved.")
+    def __del__(self):
+        self.cleanup()

+ 0 - 529
plugins/de_plugin2.py

@@ -1,529 +0,0 @@
-import time
-import json
-import random
-import re
-import os
-import uuid
-import shutil
-import base64
-import socket
-from datetime import datetime
-from typing import List, Dict, Optional, Any, Callable
-from urllib.parse import urljoin, urlparse, urlencode
-
-# DrissionPage 核心
-from DrissionPage import ChromiumPage, ChromiumOptions
-
-from vs_plg import IVSPlg
-from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.vs_cloud_api import VSCloudApi
-from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
-from toolkit.proxy_tunnel import ProxyTunnel
-from toolkit.ocr_engine import DddOcrEngine
-
-
-class BrowserResponse:
-    def __init__(self, result_dict):
-        result_dict = result_dict or {}
-        self.status_code = result_dict.get('status', 0)
-        self.text = result_dict.get('body', '')
-        self.headers = result_dict.get('headers', {})
-        self.url = result_dict.get('url', '')
-        self._json = None
-    def json(self):
-        if self._json is None:
-            if not self.text: return {}
-            try: self._json = json.loads(self.text)
-            except: self._json = {}
-        return self._json
-
-def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
-    dt = datetime.strptime(data_str, date_str_format)
-    return dt.strftime("%Y-%m-%d")
-
-def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
-    if "@" not in email: raise ValueError(f"Invalid email: {email}")
-    local_part, _ = email.rsplit("@", 1)
-    return f"{local_part}@{new_domain}"
-
-class DePlugin2(IVSPlg):
-    """
-    Germany (Visametric) 签证预约插件 (Browser + Tunnel Mode)
-    """
-
-    def __init__(self, group_id: str):
-        self.group_id = group_id
-        self.config: Optional[VSPlgConfig] = None
-        self.free_config: Dict[str, Any] = {}
-        self.logger = None
-        
-        # 浏览器实例
-        self.page: Optional[ChromiumPage] = None
-        
-        # 资源隔离
-        self.instance_id = uuid.uuid4().hex[:8]
-        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
-        self.user_data_path = os.path.join(self.root_workspace, "user_data")
-        
-        if not os.path.exists(self.root_workspace):
-            os.makedirs(self.root_workspace)
-            
-        self.tunnel = None # 代理隧道
-        self.is_healthy = True
-        self.session_create_time: float = 0
-        
-        # 字符识别引擎
-        self.ocr_engine: Optional[DddOcrEngine] = None
-        
-        # 业务状态
-        self.base_url = "https://ie-appointment.visametric.com"
-        self.csrf_token = ""
-        self.personal_info_val = ""
-        self.email_val_control = ""
-
-    def get_group_id(self) -> str:
-        return self.group_id
-    
-    def set_log(self, logger: Callable[[str], None]) -> None:
-        self.logger = logger
-        
-    def _log(self, message): 
-        if self.logger:
-            self.logger(f'[DePlugin] [{self.group_id}] {message}')
-        else:
-            print(f'[DePlugin] [{self.group_id}] {message}')
-        
-    def set_config(self, config: VSPlgConfig):
-        self.config = config
-        self.free_config = config.free_config or {}
-        if self.free_config.get("base_url"):
-            self.base_url = self.free_config["base_url"].rstrip('/')
-
-    def health_check(self) -> bool:
-        if not self.is_healthy:
-            return False
-        if not self.page:
-            return False
-        try:
-            if not self.page.run_js("return 1;"):
-                return False
-        except:
-            return False
-        if self.config.session_max_life > 0:
-            if time.time() - self.session_create_time > self.config.session_max_life * 60:
-                self._log("Session expired.")
-                return False
-        return True
-
-    def create_session(self):
-        """
-        创建会话:启动浏览器 -> 代理隧道 -> 过盾 -> 提取 Captcha -> 本地识别 -> 提交 -> 获取 Context
-        """
-        self._log(f"Initializing Session (ID: {self.instance_id})...")
-        self.ocr_engine = DddOcrEngine()
-        co = ChromiumOptions()
-        # 端口分配 (Docker 适配)
-        def get_free_port():
-            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-                s.bind(('', 0)); return s.getsockname()[1]
-        co.set_local_port(get_free_port())
-        
-        # 路径与隔离
-        co.set_user_data_path(self.user_data_path)
-        chrome_path = os.getenv("CHROME_BIN")
-        if chrome_path and os.path.exists(chrome_path):
-            co.set_paths(browser_path=chrome_path)
-            
-        # 代理隧道
-        if self.config.proxy and self.config.proxy.ip:
-            p = self.config.proxy
-            if p.username and p.password:
-                self._log(f"Starting Tunnel for {p.ip}...")
-                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
-                local_proxy = self.tunnel.start()
-                self._log(f"Tunnel started at {local_proxy}")
-                co.set_argument(f'--proxy-server={local_proxy}')
-            else:
-                proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
-                co.set_argument(f'--proxy-server={proxy_str}')
-        else:
-            self._log("[WARN] No proxy configured!")
-
-        # Docker 核心参数
-        co.headless(False)
-        co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
-        co.set_argument('--disable-dev-shm-usage') 
-        co.set_argument('--window-size=1920,1080')
-        co.set_argument('--disable-blink-features=AutomationControlled')
-        co.set_argument('--ignore-certificate-errors')
-
-        try:
-            self.page = ChromiumPage(co)
-            
-            # 1. 访问首页
-            url_home = f"{self.base_url}/en"
-            self._log(f"Navigating to {url_home}")
-            self.page.get(url_home)
-            
-            # 2. Cloudflare 过盾
-            cf = CloudflareBypasser(self.page, log=self.config.debug)
-            if not cf.bypass(max_retry=15):
-                if "access denied" in self.page.title.lower():
-                    raise BizLogicError("Cloudflare Access Denied")
-                raise BizLogicError("Cloudflare bypass timeout")
-
-            # 3. 提取 CSRF 和 验证码
-            # 等待页面加载
-            meta_ele = self.page.ele('xpath://meta[@name="csrf-token"]', timeout=30)
-                
-            if not meta_ele:
-                # 截图调试,看看是不是还在 Cloudflare 或者加载失败
-                self.page.get_screenshot(path='csrf_not_found.jpg')
-                raise NotFoundError("CSRF Token meta tag not found (Page load failed?)")
-            
-            self.csrf_token = meta_ele.attr('content')
-            
-            # 提取验证码图片 (Visametric Base64)
-            html = self.page.html
-            match = re.search(r'"data:image/png;base64,"\s*\+\s*"(.*?)"', html)
-            if not match:
-                # 尝试直接找 img
-                try:
-                    img_ele = self.page.ele('xpath://img[contains(@src, "data:image")]')
-                    if img_ele:
-                        b64_src = img_ele.attr('src')
-                        captcha_b64 = b64_src.split(',')[1]
-                    else:
-                        raise NotFoundError("Captcha image not found")
-                except:
-                    raise NotFoundError("Captcha image not found (Regex failed)")
-            else:
-                captcha_b64 = match.group(1)
-            image_bytes = base64.b64decode(captcha_b64)
-            # 4. 识别验证码 (本地 OCR 服务)
-            captcha_code = self.ocr_engine.inference_captcha(image_bytes)
-
-            # 5. 提交验证码 (获取 PersonalInfo)
-            self._submit_captcha(captcha_code)
-            
-            self.session_create_time = time.time()
-            self._log("Session created successfully.")
-
-        except Exception as e:
-            self._log(f"Session Create Failed: {e}")
-            self.cleanup()
-            raise e
-
-    def _submit_captcha(self, code):
-        """
-        提交验证码,获取 personalinfo 和 emailValControl
-        """
-        url = f"{self.base_url}/en/appointment-form"
-        payload = {
-            '_token': self.csrf_token,
-            'cpJvnsControl': '',
-            'mailConfirmCode': code
-        }
-        
-        # 使用 Fetch 提交 (Form-UrlEncoded)
-        resp = self._perform_request('POST', url, data=payload, headers={
-            'X-Requested-With': 'XMLHttpRequest'
-        })
-        
-        # 解析返回的 HTML 片段
-        html = resp.text
-        
-        # 提取 personalinfo
-        match_pi = re.search(r"personalinfo:\s*'([^']*)'", html)
-        if match_pi: self.personal_info_val = match_pi.group(1)
-        
-        # 提取 emailValControl
-        match_ev = re.search(r"emailValControl:\s*'([^']*)'", html)
-        if match_ev: self.email_val_control = match_ev.group(1)
-            
-        if not self.personal_info_val:
-            raise NotFoundError(message="Personalinfo not found in captcha response")
-        
-        # 更新 CSRF (如果返回了新的)
-        m = re.search(r'name="csrf-token" content="([^"]+)"', html)
-        if m: self.csrf_token = m.group(1)
-
-    def query(self, apt_type: AppointmentType) -> VSQueryResult:
-        res = VSQueryResult()
-        res.success = False
-        
-        consular_id = self.free_config.get("consularid", "1")
-        url = f"{self.base_url}/en/getdate"
-        payload = {
-            "consularid": consular_id,
-            "exitid": "1",
-            "servicetypeid": "1",
-            "calendarType": "2",
-            "totalperson": "1"
-        }
-        
-        headers = {
-            'X-CSRF-TOKEN': self.csrf_token,
-            'X-Requested-With': 'XMLHttpRequest'
-        }
-        
-        try:
-            resp = self._perform_request('POST', url, data=payload, headers=headers, retry_count=1)
-        except Exception as e:
-            self._log(f"Query Error: {e}")
-            raise e
-
-        j = resp.json()
-        dates = j.get("getDateEnable", [])
-        
-        if dates:
-            res.success = True
-            res.availability_status = AvailabilityStatus.Available
-            earliest_date = dates[0]
-            earliest_dt = datetime.strptime(earliest_date, "%d-%m-%Y")
-            res.earliest_date = earliest_dt
-            res.availability = [
-                DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
-                for d in dates
-            ]
-        else:
-            res.availability_status = AvailabilityStatus.NoneAvailable
-
-        return res
-
-    def book(self, slot_info: VSQueryResult, user_inputs: Dict) -> VSBookResult:
-        res = VSBookResult()
-        available_dates = [da.date for da in slot_info.availability]
-        exp_start = user_inputs.get('expected_start_date', '')
-        exp_end = user_inputs.get('expected_end_date', '')
-        
-        available_dates_str = [
-            da.date.strftime("%Y-%m-%d")
-            for da in slot_info.availability
-        ]
-        
-        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
-        if not valid_dates:
-            raise NotFoundError("No dates match constraints")
-        
-        target_date = random.choice(valid_dates)
-        self._log(f"Selected date: {target_date}")
-        
-        # 1. 获取时间 Slot
-        time_slot = self._get_slot_time(target_date)
-        
-        # 2. 发送邮件流程
-        alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
-        self._send_email_step1(alias_email)
-        self._send_email_step2("0")
-        
-        # 3. 读取 OTP
-        otp_code = self._read_otp_email(alias_email)
-        
-        # 4. 提交确认
-        book_res_html = self._confirm_appointment(target_date, time_slot, user_inputs, otp_code, alias_email)
-        
-        if "complete all required fields" in book_res_html.lower():
-            raise BizLogicError("Incomplete fields response")
-            
-        match = re.search(r'https:\/\/checkout\.stripe\.com\/c\/pay\/[^\s"]+', book_res_html)
-        
-        res.success = True
-        res.fee_amount = 3000
-        res.fee_currency = 'EUR'
-        res.book_date = target_date
-        res.book_time = time_slot['time']
-        
-        if match:
-            res.payment_link = match.group(0)
-            self._log(f"Payment Link: {res.payment_link}")
-            
-        return res
-
-    # ---------------------------------------------------------
-    # 辅助方法
-    # ---------------------------------------------------------
-    
-    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
-        if not self.page:
-            raise BizLogicError("Browser not init")
-        
-        req_url = url
-        if params:
-            sep = '&' if '?' in req_url else '?'
-            req_url += sep + urlencode(params)
-            
-        fetch_opts = { "method": method.upper(), "headers": headers or {}, "credentials": "include" }
-        
-        if json_data:
-            fetch_opts['body'] = json.dumps(json_data)
-            fetch_opts['headers']['Content-Type'] = 'application/json'
-        elif data:
-            if isinstance(data, dict):
-                fetch_opts['body'] = urlencode(data)
-                fetch_opts['headers']['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
-            else:
-                fetch_opts['body'] = data
-
-        js = f"""
-        return fetch("{req_url}", {json.dumps(fetch_opts)})
-        .then(async r => {{
-            const h = {{}}; r.headers.forEach((v, k) => h[k] = v);
-            return {{ status: r.status, body: await r.text(), headers: h, url: r.url }};
-        }}).catch(e => {{ return {{ status: 0, body: e.toString() }}; }});
-        """
-        
-        resp = BrowserResponse(self.page.run_js(js, timeout=60))
-        
-        if resp.status_code == 200:
-            return resp
-        elif resp.status_code == 403:
-            if "Just a moment" in resp.text and retry_count < 2:
-                self._log("Cloudflare 403. Refreshing...")
-                if self._refresh_firewall_session():
-                    return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
-            raise PermissionDeniedError(f"HTTP 403: {resp.text[:100]}")
-        elif resp.status_code == 429:
-            self.is_healthy = False
-            raise RateLimiteddError()
-        elif resp.status_code in [401, 419]:
-            self.is_healthy = False
-            raise SessionExpiredOrInvalidError()
-        else:
-            raise BizLogicError(f"HTTP {resp.status_code}: {resp.text[:100]}")
-
-    def _refresh_firewall_session(self):
-        try:
-            self.page.refresh()
-            cf = CloudflareBypasser(self.page, log=self.config.debug)
-            return cf.bypass(max_retry=10)
-        except: return False
-
-    def _get_slot_time(self, date) -> Dict:
-        url = f"{self.base_url}/en/senddate"
-        dt_m = datetime.strptime(date, "%Y-%m-%d")
-        converted_date = dt_m.strftime("%d-%m-%Y")
-        payload = {
-            "fulldate": converted_date,
-            "totalperson": "1",
-            "set_new_consular_id": self.free_config.get("consularid", "1"),
-            "set_new_exit_office_id": "1",
-            "calendarType": "2",
-            "set_new_service_type_id": "1",
-            "personalinfo": self.personal_info_val
-        }
-        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
-        resp = self._perform_request('POST', url, data=payload, headers=headers)
-        
-        # 使用 Regex 提取 Slot
-        times = []
-        # pattern: data-id="123" ... <i>09:00</i>
-        for m in re.finditer(r'data-id="([^"]+)"[^>]*data-all="([^"]+)"[^>]*>.*?<i>(.*?)</i>', resp.text, re.DOTALL):
-            times.append({'data_id': m.group(1), 'data_all': m.group(2), 'time': m.group(3).strip()})
-            
-        if not times: raise NotFoundError("No time slots")
-        return random.choice(times)
-
-    def _send_email_step1(self, email):
-        url = f"{self.base_url}/en/jky45fgd"
-        payload = { "emailCheck": email, "personalinfo": self.personal_info_val }
-        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
-        self._perform_request('POST', url, data=payload, headers=headers)
-
-    def _send_email_step2(self, code_val):
-        url = f"{self.base_url}/en/confirmCodeSendMail"
-        payload = { "confirmCode": code_val, "emailValControl": self.email_val_control }
-        headers = {'X-CSRF-TOKEN': self.csrf_token, 'X-Requested-With': 'XMLHttpRequest'}
-        self._perform_request('POST', url, data=payload, headers=headers)
-
-    def _read_otp_email(self, recipient) -> str:
-        master_email = "visafly666@gmail.com"
-        sender = 'Visametric - verify at visametric.com'
-        now_utc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
-        for i in range(12):
-            c = VSCloudApi.Instance().fetch_mail_content(master_email, sender, recipient, 'Verification Code', 'Verification code', now_utc, 300)
-            if c:
-                m = re.search(r'\b\d{6}\b', c)
-                if m: return m.group(0)
-            time.sleep(5)
-        raise NotFoundError("OTP timeout")
-
-    def _confirm_appointment(self, date, slot_data, user_inputs, otp, alias_email):
-        url = f"{self.base_url}/en/personal/appointment/create"
-        def _get_dob(d):
-            try: return datetime.strptime(d[:10], "%Y-%m-%d")
-            except: return datetime.now()
-        dob = _get_dob(user_inputs.get('birthday', ''))
-        
-        payload = {
-            "_token": self.csrf_token,
-            "country": str(self.free_config.get("consularid", "1")),
-            "visitingcountry": str(self.free_config.get("consularid", "1")),
-            "city": "6",
-            "office": "1",
-            "officetype": "1",
-            "totalPerson": "1",
-            "name1": user_inputs.get('first_name', '').upper(),
-            "surname1": user_inputs.get('last_name', '').upper(),
-            "nationality1": "2",
-            "birthday1": str(dob.day),
-            "birthmonth1": str(dob.month),
-            "birthyear1": str(dob.year),
-            "passport1": user_inputs.get('passport_no'),
-            "passportExpirationDate1": datetime.strptime(user_inputs.get('passport_expiry_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
-            "email1": alias_email,
-            "phone1": user_inputs.get('phone_no'),
-            "alternativephone1": "",
-            "mailConfirmCode": otp,
-            "ctval": slot_data['data_id'],
-            "qtallvert": slot_data['data_all'],
-            "oldofficetype": "1",
-            "oldtotalperson": "1",
-            "rePaymentControl": "0",
-            "view_set_app_country": "Schengen - Tourism/Family&Friend Visit/Transit Visa/Other Purposes",
-            "view_set_app_office": "Dublin",
-            "view_set_app_service_type": "NORMAL",
-            "cargoactive": "0",
-            "setnewcalendarstatus": "2",
-            "availableDaycontrol": "0",
-            "travelStartDate": datetime.strptime(user_inputs.get('travel_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
-            "personalapproveTerms": "1"
-        }
-        
-        # 补全空字段 (Person 2-4)
-        for i in range(2, 5):
-            payload.update({
-                f"name{i}": "", f"surname{i}": "", f"nationality{i}": "0", f"birthday{i}": "0", f"birthmonth{i}": "0", f"birthyear{i}": "0", f"passport{i}": "", f"passportExpirationDate{i}": "", f"email{i}": alias_email, f"phone{i}": user_inputs.get('phone_no'), f"alternativephone{i}": ""
-            })
-
-        headers = {'X-Requested-With': 'XMLHttpRequest'}
-        return self._perform_request('POST', url, data=payload, headers=headers).text
-
-    def _filter_dates(self, dates, start, end):
-        if not start or not end: return dates
-        valid = []
-        s = datetime.strptime(start[:10], "%Y-%m-%d")
-        e = datetime.strptime(end[:10], "%Y-%m-%d")
-        for d in dates:
-            c = datetime.strptime(d, "%Y-%m-%d")
-            if s <= c <= e: valid.append(d)
-        random.shuffle(valid)
-        return valid
-
-    def cleanup(self):
-        if self.page:
-            try: self.page.quit()
-            except: pass
-            self.page = None
-        if os.path.exists(self.root_workspace):
-            for _ in range(3):
-                try: time.sleep(0.2); shutil.rmtree(self.root_workspace, ignore_errors=True); break
-                except: time.sleep(0.5)
-        if self.tunnel:
-            try: self.tunnel.stop()
-            except: pass
-            self.tunnel = None
-        
-    def __del__(self):
-        self.cleanup()

+ 4 - 1
plugins/grc_plugin.py

@@ -40,6 +40,9 @@ class GrcPlugin(IVSPlg):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.free_config = config.free_config or {}
+        
+    def keep_alive(self):
+        pass
 
     def health_check(self) -> bool:
         if not self.is_healthy:
@@ -111,7 +114,7 @@ class GrcPlugin(IVSPlg):
             self._save_debug_html(resp.text, prefix='login_unknown_fail')
             self._log(f"Login check failed. Current URL: {resp.url}")
             raise BizLogicError(message='Login failed: Unknown response')
-        
+            
     def _get_daily_schedule(self, open_times, date_obj):
         """根据 open_times 获取当天的开始和结束分钟数"""
         if not open_times:

+ 3 - 0
plugins/ita_plugin.py

@@ -86,6 +86,9 @@ class ItaPlugin(IVSPlg):
         self.free_config = config.free_config or {}
         # Service ID (e.g., 1321 for Ireland, 5059 for Guangzhou)
         self._service_id = self.free_config.get('service_id', 0)
+        
+    def keep_alive(self):
+        pass
 
     def health_check(self) -> bool:
         if not self.is_healthy or not self.page:

+ 522 - 294
plugins/tls_plugin.py

@@ -3,22 +3,44 @@ import json
 import random
 import re
 import os
+import uuid
+import shutil
 from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
-from urllib.parse import urljoin, urlparse
-from requests_toolbelt import MultipartEncoder
+from urllib.parse import urljoin, urlparse, urlencode
 
-from curl_cffi import requests, const
-from bs4 import BeautifulSoup
+# DrissionPage 核心
+from DrissionPage import ChromiumPage, ChromiumOptions
 
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.vs_cloud_api import VSCloudApi
+from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
+from toolkit.proxy_tunnel import ProxyTunnel
+
+
+class BrowserResponse:
+    """模拟 requests.Response"""
+    def __init__(self, result_dict):
+        result_dict = result_dict or {}
+        self.status_code = result_dict.get('status', 0)
+        self.text = result_dict.get('body', '')
+        self.headers = result_dict.get('headers', {})
+        self.url = result_dict.get('url', '')
+        self._json = None
+
+    def json(self):
+        if self._json is None:
+            if not self.text:
+                return {}
+            try:
+                self._json = json.loads(self.text)
+            except:
+                self._json = {}
+        return self._json
 
 class TlsPlugin(IVSPlg):
     """
-    TLSContact 签证预约插件
-    适配法国签证 (FR) 流程
+    TLSContact 签证预约插件 (DrissionPage 版)
     """
 
     def __init__(self, group_id: str):
@@ -27,9 +49,26 @@ class TlsPlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         self.is_healthy = True
         self.logger = None
-        self.session: Optional[requests.Session] = None
+        
+        # 浏览器实例
+        self.page: Optional[ChromiumPage] = None
+        
         self.travel_group: Optional[Dict] = None
-        self.user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
+        
+        # --- [核心修改] 并发隔离与资源管理 ---
+        # 生成唯一实例 ID
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        # 定义子目录:代理插件目录 & 浏览器用户数据目录
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+    
+        # 确保根目录存在 (子目录由具体逻辑创建)
+        if not os.path.exists(self.root_workspace):
+            os.makedirs(self.root_workspace)
+            
+        # 持有隧道实例
+        self.tunnel = None
+        
         self.session_create_time: float = 0
 
     def get_group_id(self) -> str:
@@ -37,116 +76,242 @@ class TlsPlugin(IVSPlg):
     
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
+    
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
+        else:
+            print(f'[TlsPlugin] [{self.group_id}] {message}')
 
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.free_config = config.free_config or {}
+        
+    def keep_alive(self):
+        pass
 
     def health_check(self) -> bool:
         if not self.is_healthy:
             return False
-        if self.session is None:
+        if self.page is None:
             return False
+        try:
+            if not self.page.run_js("return 1;"):
+                return False
+        except:
+            return False
+            
         if self.config.session_max_life > 0:
             current_time = time.time()
             elapsed_time = current_time - self.session_create_time
             if elapsed_time > self.config.session_max_life * 60:
-                self._log(f"Session Life ({int(elapsed_time)}s) out of max life limit ({self.config.session_max_life * 60}s), mark as unhealth session")
+                self._log(f"Session expired.")
                 return False
         return True
+    
+    def _save_screenshot(self, name_prefix):
+        try:
+            timestamp = int(time.time())
+            filename = f"{self.instance_id}_{name_prefix}_{timestamp}.jpg"
+            save_path = os.path.join("data", filename)
+            os.makedirs("data", exist_ok=True)
+            
+            # [修改] 改为 full_page=False,防止页面结构异常导致截图失败
+            # 这样能截取到浏览器当前可视区域,最适合调试“卡住”的情况
+            self.page.get_screenshot(path=save_path, full_page=False)
+            
+            self._log(f"Screenshot saved to {save_path}")
+        except Exception as e:
+            self._log(f"Failed to save screenshot: {e}")
 
     def create_session(self):
         """
-        创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
-        """        
-        # 1. 初始化 Session
-        curlopt = {
-            const.CurlOpt.MAXAGE_CONN: 1800,
-            const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: self.config.debug,
-        }
+        全浏览器会话创建:过盾 -> JS注入登录 -> 原生跳转
+        """
+        self._log(f"Initializing Session (ID: {self.instance_id})...")
+        co = ChromiumOptions()
+        # -------------------------------------------------------------
+        # [核心修复] 解决 'not enough values to unpack'
+        # -------------------------------------------------------------
+        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
+        # 2. 我们手动随机生成一个端口
+        import random
+        import socket
         
-        self.session = requests.Session(
-            proxy=self._get_proxy_url(),
-            impersonate="chrome124",
-            curl_options=curlopt,
-            use_thread_local_curl=False,
-            http_version=const.CurlHttpVersion.V2TLS
-        )
-
-        apt_config = self.free_config.get('apt_config', {})
-        if not apt_config:
-            raise NotFoundError(message="apt_config not found in free config")
-
-        # 2. 解决 Cloudflare 5s 盾
-        self._solve_cloudflare5S_challenge()
-
-        # 3. 获取登录页面参数 (OIDC)
-        login_page = "https://visas-fr.tlscontact.com/en-us/login"
-        params = {
-            "issuerId": apt_config["code"],
-            "country": apt_config["country"], 
-            "vac": apt_config["code"],
-            "redirect": f"/en-us/country/{apt_config['country']}/vac/{apt_config['code']}"
-        }
-        headers = {
-            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
-            'Referer': f'https://visas-fr.tlscontact.com/en-us/country/{apt_config["country"]}/vac/{apt_config["code"]}',
-            'User-Agent': self.user_agent,
-        }
-        resp = self._perform_request("GET", login_page, headers=headers, params=params)
+        def get_free_port():
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.bind(('', 0))
+                return s.getsockname()[1]
         
-        if self.config.debug:
-            self._save_debug_html(resp.text, prefix='Tls_Login_Page')
-
-        # 解析 Keycloak 登录地址
-        soup = BeautifulSoup(resp.text, 'html.parser')
-        form = soup.find('form')
-        if not form:
-            raise NotFoundError(message="Login form not found")
-
-        action = form.get('action')
-        authenticate_url = action if action.startswith('http') else urljoin(resp.url, action)
-
-        # 4. 解决 ReCaptcha V2 (登录验证码)
-        api_token = self.free_config.get("capsolver_key", "")
-        if not api_token:
-            raise NotFoundError(message="Missing 'capsolver_key' in free_config, captcha might fail.")
+        debug_port = get_free_port()
+        self._log(f"Assigned Debug Port: {debug_port}")
         
-        rc_params = {
-            "type": "ReCaptchaV2TaskProxyLess", 
-            "page": authenticate_url,
-            "siteKey": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0",
-            "apiToken": api_token,
-            "proxy": self._get_proxy_url()
-        }
-        g_token = self._solve_recaptcha(rc_params)
-        # 5. 提交登录
-        payload = {
-            'username': self.config.account.username,
-            'password': self.config.account.password,
-            'g-recaptcha-response': g_token
-        }
-        headers['Content-Type'] = 'application/x-www-form-urlencoded'
-        resp = self._perform_request("POST", authenticate_url, headers=headers, data=payload)
-        if self.config.debug:
-            self._save_debug_html(resp.text, prefix='Tls_Travel_Groups_Page')
-        # 6. 解析 Travel Groups
-        self._check_page_is_session_expired_or_invalid("My travel group", resp.text)
-        groups = self._parse_travel_groups(resp.text)
+        # 3. 强制指定端口,DrissionPage 就会直接连接,不再解析日志
+        co.set_local_port(debug_port)
+        
+        # --- [关键配置] 设置独立的用户数据目录 ---
+        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
+        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
+        co.set_user_data_path(self.user_data_path)
+        
+        # --- 1. 指定浏览器路径 (适配 Docker) ---
+        chrome_path = os.getenv("CHROME_BIN")
+        if chrome_path and os.path.exists(chrome_path):
+            co.set_paths(browser_path=chrome_path)
+        
+        # --- [核心修改] 代理配置 ---
+        if self.config.proxy and self.config.proxy.ip:
+            p = self.config.proxy
+            
+            if p.username and p.password:
+                self._log(f"Starting Proxy Tunnel for {p.ip}...")
+                
+                # 1. 启动本地隧道
+                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
+                local_proxy = self.tunnel.start()
+                
+                self._log(f"Tunnel started at {local_proxy}")
+                
+                # 2. Chrome 连接本地免密端口
+                # 必须使用 --proxy-server 强制指定,绝对稳健
+                co.set_argument(f'--proxy-server={local_proxy}')
+                
+            else:
+                # 无密码代理,直接用
+                proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
+                co.set_argument(f'--proxy-server={proxy_str}')
+        else:
+            self._log("[WARN] No proxy configured!")
+
+        co.headless(False)
+        co.set_argument('--no-sandbox')
+        co.set_argument('--disable-gpu')
+        # Docker 默认 /dev/shm 只有 64MB,Chromium 很容易爆内存崩溃
+        co.set_argument('--disable-dev-shm-usage')
+        co.set_argument('--window-size=1920,1080')
+        co.set_argument('--disable-blink-features=AutomationControlled')
+
+        try:
+            self.page = ChromiumPage(co)
+            
+            apt_config = self.free_config.get('apt_config', {})
+            if not apt_config:
+                raise NotFoundError("apt_config config missing")
+
+            login_url = "https://visas-fr.tlscontact.com/en-us/login"
+            params = {
+                "issuerId": apt_config["code"],
+                "country": apt_config["country"],
+                "vac": apt_config["code"],
+                "redirect": f"/en-us/country/{apt_config['country']}/vac/{apt_config['code']}"
+            }
+            full_login_url = f"{login_url}?{urlencode(params)}"
+            
+            self._log(f"Navigating: {full_login_url}")
+            self.page.get(full_login_url)
+            
+            # --- Cloudflare 过盾 ---
+            cf = CloudflareBypasser(self.page, log=self.config.debug)
+            if not cf.bypass(max_retry=15):
+                raise BizLogicError("Cloudflare bypass timeout")
+            
+            wait_start = time.time()
+            while True:
+                # 获取页面 HTML,转小写
+                # 注意:如果此处报错 "页面被刷新",是 DrissionPage 的机制问题,
+                # 但你要求先不处理复杂错误,所以这里保持最简单的写法。
+                html = self.page.html.lower()
+                
+                # 检查是否在排队室 (法语或英语)
+                if "file d'attente" in html or "waiting room" in html:
+                    # 如果等太久(比如1小时),就强制停止
+                    if time.time() - wait_start > 6 * 60:
+                        self._log("Waiting room timeout (1h).")
+                        break
+                        
+                    self._log("In Waiting Room... Waiting for auto-refresh.")
+                    time.sleep(30) # 截图说页面会自动刷新,所以这里只sleep,不动浏览器
+                else:
+                    # 页面里没有“等候室”的字了,说明出来了
+                    break
+
+            # --- 登录页面检查 ---
+            if not self.page.ele('#email-input-field'):
+                self._log("Reloading Login Page...")
+                self.page.get(full_login_url)
+                if not self.page.wait.ele_displayed('#email-input-field', timeout=15):
+                    self._save_screenshot("login_load_fail")
+                    raise BizLogicError("Login form not loaded")
+
+            # --- JS 注入登录 ---
+            g_token = ""
+            if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
+                self._log("Solving ReCaptcha...")
+                rc_params = {
+                    "type": "ReCaptchaV2TaskProxyLess", "page": self.page.url,
+                    "siteKey": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0", 
+                    "apiToken": self.free_config.get("capsolver_key", "")
+                }
+                g_token = self._solve_recaptcha(rc_params)
+
+            username = self.config.account.username
+            password = self.config.account.password
+            
+            # 使用 JS 直接操作 DOM 并 click,让浏览器处理 302
+            js_login = f"""
+            var u = document.getElementById('email-input-field');
+            if(u) {{ u.value = "{username}"; u.dispatchEvent(new Event('input', {{bubbles:true}})); }}
+            
+            var p = document.getElementById('password-input-field');
+            if(p) {{ p.value = "{password}"; p.dispatchEvent(new Event('input', {{bubbles:true}})); }}
+            
+            var g = document.getElementById('g-recaptcha-response');
+            if(g) {{ g.value = "{g_token}"; }}
             
-        # 选择匹配城市的 Group
-        target_city = apt_config['city'].lower()
-        for g in groups:
-            if g['location'].lower() == target_city:
-                self.travel_group = g
-                break
-        
-        if not self.travel_group:
-            raise NotFoundError(message=f"No matched group found for city {target_city}")
-        self.session_create_time = time.time()
-        self._log(f"Session created successfully. Group: {self.travel_group['group_number']}")
+            var btn = document.getElementById('btn-login');
+            if(btn) {{ btn.click(); return true; }} else {{ return false; }}
+            """
+            
+            self._log("Submitting Login via JS...")
+            if not self.page.run_js(js_login): raise BizLogicError("Login button missing")
+
+            # --- 等待跳转 ---
+            self._log("Waiting for redirect...")
+            self.page.wait.url_change('login-actions', exclude=True, timeout=45)
+            
+            # 检查是否失败
+            if "login-actions" in self.page.url or "auth" in self.page.url:
+                err = "Unknown Login Error"
+                if "Invalid username" in self.page.html: err = "Invalid Credentials"
+                self._save_screenshot("login_submit_fail")
+                raise BizLogicError(f"Login Failed: {err}")
+
+            # --- 提取 Dashboard 信息 ---
+            self._log("Waiting for dashboard...")
+            self.page.wait.load_start()
+            time.sleep(5)
+            
+            html = self.page.html
+            self._check_page_is_session_expired_or_invalid("My travel group", html)
+            groups = self._parse_travel_groups(html)
+            
+            target_city = apt_config['city'].lower()
+            for g in groups:
+                if g['location'].lower() == target_city:
+                    self.travel_group = g
+                    break
+            
+            if not self.travel_group:
+                self._save_screenshot("group_not_found")
+                raise NotFoundError(f"Group not found for {target_city}")
+            
+            self.session_create_time = time.time()
+            self._log(f"Session Ready. Group: {self.travel_group['group_number']}")
+
+        except Exception as e:
+            self._log(f"Session Create Error: {e}")
+            self.cleanup()
+            raise e
 
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
@@ -154,45 +319,29 @@ class TlsPlugin(IVSPlg):
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
-        max_retries = self.free_config.get("max_retries", 2)
         
         url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
         params = {
             'location': apt_config["code"],
             'month': interest_month,
         }
-        headers = {
-            'accept': '*/*',
-            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
-            'referer': f'{url}?location={apt_config["code"]}',
-            'user-agent': self.user_agent,
-        }
-
-        for attempt in range(1, max_retries + 1):
-            try:
-                resp = self._perform_request("GET", url, headers=headers, params=params)
-                if self.config.debug:
-                    self._save_debug_html(resp.text, prefix='Tls_Query_Slot_Page')
-                break  # ✅ 请求成功,跳出重试循环
-
-            except PermissionDeniedError:
-                self._log(f"Query Appointment-booking blocked (403), attempt {attempt}/{max_retries}")
-                # 最后一次就不再绕盾了
-                if attempt >= max_retries:
-                    raise PermissionDeniedError()
-
-                self._solve_cloudflare5S_challenge()
-                self._log("Cloudflare bypass success, retrying...")
-                continue
+        
+        # DrissionPage 自动处理 Cloudflare,直接 fetch 即可
+        try:
+            resp = self._perform_request("GET", url, params=params, retry_count=1)
+        except Exception as e:
+            self._log(f"Query request failed: {e}")
+            raise e
 
         self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
 
-        # 3. 解析 Slots
+        # 解析 Slots
         all_slots = self._parse_appointment_slots(resp.text)
 
         target_labels = self.free_config.get("target_labels", ["", "pta"])
+        # 根据配置过滤
         available = [s for s in all_slots if s.get("label") in target_labels]
-
+        
         if available:
             res.success = True
             earliest_date = available[0]["date"]
@@ -212,38 +361,33 @@ class TlsPlugin(IVSPlg):
             res.availability_status = AvailabilityStatus.NoneAvailable
         return res
 
-
     def book(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
         res = VSBookResult()
         res.success = False
         
-        # 1. 基础信息提取
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         
         available_dates = [da.date for da in slot_info.availability]
         exp_start = user_inputs.get('expected_start_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
-        
         support_pta = user_inputs.get('support_pta', True)
 
         target_labels = ['']
-        
         if support_pta:
             target_labels.append('pta')
-        
         available_dates_str = [
             da.date.strftime("%Y-%m-%d")
             for da in slot_info.availability
         ]
-        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
+        valid_dates = self._filter_dates(available_dates_str, exp_start, exp_end)
         if not valid_dates:
             raise NotFoundError(message="No dates match user constraints")
         
         selected_date = None
         selected_time = None
         selected_label = None
-        # [关键修正] Label 处理
+        
         for d in valid_dates:
             for da in slot_info.availability:
                 if da.date == d:
@@ -253,9 +397,12 @@ class TlsPlugin(IVSPlg):
                             selected_time = t
                             selected_label = t.label
                             break
+            if selected_date: break
+            
+        if not selected_date:
+             raise NotFoundError(message="No suitable slot found")
 
-        # 2. 解决 ReCaptcha V3
-        # 动作必须是 "book"
+        # 2. 解决 ReCaptcha V3 (Action: book)
         page_url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking?location={apt_config["code"]}&month={selected_date[:7]}'
         
         api_token = self.free_config.get("capsolver_key", "")
@@ -265,84 +412,88 @@ class TlsPlugin(IVSPlg):
             "action": "book", 
             "siteKey": "6LcTpXcfAAAAAM3VojNhyV-F1z92ADJIvcSZ39Y9",
             "apiToken": api_token,
-            "proxy": self._get_proxy_url()
+            "proxy": self._get_proxy_url() # ProxyLess
         }
         g_token = self._solve_recaptcha(rc_params)
 
-        # 3. 构造 Payload (严格对齐你的 Curl Dump)
-        # Next.js Server Action ID (从你的 header 确认)
+        # 3. 构造 Next.js Payload
+        # 注意:在 JS 中构造 FormData 比在 Python 中拼 Multipart 更容易且不易出错
         ACTION_ID = "60d0616946df1fc4e7c094ca6a7a04f134d0be3d53"
+        url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
         
-        fields = {
-            '1_formGroupId': str(group_num),      # 修正:加了 form 前缀
-            '1_lang': 'en-us',
-            '1_process': 'APPOINTMENT',
-            '1_location': apt_config["code"],        # 例如 gbLON2fr
-            '1_date': selected_date,
-            '1_time': selected_time,
-            '1_appointmentLabel': selected_label,   # 修正:单数 Label,值为字符串 "pta" 或 "regular"
-            '1_captcha_token': g_token,           # 修正:下划线格式
-            '0': '[{"status":"IDLE"},"$K1"]'      # 对应 Next.js Action 的状态位
-        }
+        # State Tree 字符串
+        router_state = '%5B%22%22%2C%7B%22children%22%3A%5B%5B%22lang%22%2C%22en-us%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%5B%22groupId%22%2C%22'+str(group_num)+'%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%22workflow%22%2C%7B%22children%22%3A%5B%22appointment-booking%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D'
+
+        # 构造 JS 代码执行 fetch
+        # 使用 FormData 对象来处理 multipart
+        js_script = f"""
+        const url = "{url}";
+        const formData = new FormData();
         
-        m = MultipartEncoder(fields=fields)
-
-        # 4. 发送请求
-        url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
+        formData.append('1_formGroupId', '{group_num}');
+        formData.append('1_lang', 'en-us');
+        formData.append('1_process', 'APPOINTMENT');
+        formData.append('1_location', '{apt_config["code"]}');
+        formData.append('1_date', '{selected_date}');
+        formData.append('1_time', '{selected_time.time}');
+        formData.append('1_appointmentLabel', '{selected_label}');
+        formData.append('1_captcha_token', '{g_token}');
+        formData.append('0', '[{{"status":"IDLE"}},"$K1"]');
         
-        headers = {
-            'Next-Action': ACTION_ID,
-            'Referer': page_url,
-            'Origin': 'https://visas-fr.tlscontact.com',
-            'Accept': 'text/x-component',
-            'User-Agent': self.user_agent, # 确保和 curl_cffi 的 impersonate 一致
-            'Content-Type': m.content_type,
-            # 使用你 dump 里的 State Tree,虽然长,但最稳妥
-            'Next-Router-State-Tree': '%5B%22%22%2C%7B%22children%22%3A%5B%5B%22lang%22%2C%22en-us%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%5B%22groupId%22%2C%22'+str(group_num)+'%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%22workflow%22%2C%7B%22children%22%3A%5B%22appointment-booking%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D'
-        }
-
-        # 必须使用 curl_cffi 模拟浏览器指纹
-        resp = self.session.post(url, data=m.to_string(), headers=headers, allow_redirects=False)
+        const headers = {{
+            'Next-Action': '{ACTION_ID}',
+            'Next-Router-State-Tree': decodeURIComponent('{router_state}'),
+            'Accept': 'text/x-component'
+        }};
+        
+        return fetch(url, {{
+            method: 'POST',
+            headers: headers,
+            body: formData
+        }}).then(async response => {{
+            const text = await response.text();
+            const headers = {{}};
+            response.headers.forEach((value, key) => headers[key] = value);
+            return {{
+                status: response.status,
+                body: text,
+                headers: headers,
+                url: response.url
+            }};
+        }}).catch(err => {{
+            return {{ status: 0, body: err.toString(), headers: {{}}, url: url }};
+        }});
+        """
+        
+        self._log("Submitting booking request via JS Fetch...")
+        res_dict = self.page.run_js(js_script)
+        resp = BrowserResponse(res_dict)
 
-        if self.config.debug:
-            self._save_debug_html(resp.text, prefix='Tls_Book_Result')
+        # 4. 结果判定
+        # Next.js Server Action 重定向通常是 303,但 fetch 可能会自动跟随
+        # 如果 fetch 跟随了,url 会变;如果没跟随(Redirect mode: manual),status 是 303
+        
+        if resp.status_code == 303 or (resp.status_code == 200 and "appointment-confirmation" in resp.url):
+             self._log(f"Booking Success! URL: {resp.url}")
+             res.success = True
+             res.book_date = selected_date
+             res.book_time = selected_time
+             return res
 
-        # 5. 结果判定
-        if resp.status_code == 303:
-            location = resp.headers.get('Location', '')
-            self._log(f"Booking Success! Redirecting to: {location}")
-            res.success = True
-            res.book_date = selected_date
-            res.book_time = selected_time
-            return res
-            
-        elif resp.status_code == 200:
-            # Next.js 有时会在 200 中返回业务错误
+        if resp.status_code == 200:
             if "APPOINTMENT_LIMIT_REACHED" in resp.text:
-                self._log("Failed: 限制/无号")
+                 self._log("Failed: Appointment Limit Reached")
             elif "Invalid captcha" in resp.text:
-                self._log("Failed: 验证码错误")
+                 self._log("Failed: Invalid Captcha")
             else:
-                self._log(f"Booking Failed (200 OK but error content): {resp.text[:200]}")
+                 self._log(f"Booking Failed (Unknown 200): {resp.text[:200]}")
         else:
-            self._log(f'Booking Failed. Status: {resp.status_code}')
+            self._log(f"Booking Failed. Status: {resp.status_code}")
 
         return res
 
-    def _log(self, message):
-        if self.logger:
-            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
+    # --- 辅助方法 ---
     
-    def _save_debug_html(self, content: str, prefix: str = "debug"):
-        save_dir = "debug_pages"
-        if not os.path.exists(save_dir):
-            os.makedirs(save_dir)
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"{save_dir}/{prefix}_{timestamp}.html"
-        with open(filename, "w", encoding="utf-8") as f:
-            f.write(content)
-        self._log(f"HTML saved to: {filename}")
-            
     def _get_proxy_url(self):
         # 构造代理
         proxy_url = ""
@@ -353,76 +504,136 @@ class TlsPlugin(IVSPlg):
             else:
                 proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
         return proxy_url
-    
-    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, retry_count=0):
         """
-        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
-        1. 发送 OPTIONS 请求
-        2. 发送实际请求
+        在浏览器上下文中注入 JS 执行 Fetch
         """
-        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        if self.config.debug:
-            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
+        if not self.page:
+            raise BizLogicError("Browser not initialized")
+
+        if params:
+            from urllib.parse import urlencode
+            if '?' in url:
+                url += '&' + urlencode(params)
+            else:
+                url += '?' + urlencode(params)
+
+        fetch_options = {
+            "method": method.upper(),
+            "headers": headers or {},
+            "credentials": "include"
+        }
+
+        # Body 处理
+        if json_data:
+            fetch_options['body'] = json.dumps(json_data)
+            fetch_options['headers']['Content-Type'] = 'application/json'
+        elif data:
+             if isinstance(data, dict):
+                from urllib.parse import urlencode
+                fetch_options['body'] = urlencode(data)
+                fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
+             else:
+                 fetch_options['body'] = data
+
+        js_script = f"""
+        const url = "{url}";
+        const options = {json.dumps(fetch_options)};
+        
+        return fetch(url, options)
+            .then(async response => {{
+                const text = await response.text();
+                const headers = {{}};
+                response.headers.forEach((value, key) => headers[key] = value);
+                
+                return {{
+                    status: response.status,
+                    body: text,
+                    headers: headers,
+                    url: response.url
+                }};
+            }})
+            .catch(error => {{
+                return {{
+                    status: 0,
+                    body: error.toString(),
+                    headers: {{}},
+                    url: url
+                }};
+            }});
+        """
+        
+        res_dict = self.page.run_js(js_script, timeout=30)
+        resp = BrowserResponse(res_dict)
+        
         if resp.status_code == 200:
             return resp
         elif resp.status_code == 401:
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
         elif resp.status_code == 403:
-            raise PermissionDeniedError()
+            # [关键修改] 遇到 403 Forbidden,尝试绕盾并重试
+            # 最多重试 2 次
+            if retry_count < 2:
+                self._log(f"HTTP 403 Detected. Cloudflare session expired? Attempting refresh (Try {retry_count+1}/2)...")
+                
+                # 尝试刷新盾
+                if self._refresh_firewall_session():
+                    self._log("Firewall session refreshed. Retrying request...")
+                    # 递归重试
+                    return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
+                else:
+                    self._log("Failed to refresh firewall session.")
+            
+            # 如果重试失败,抛出异常
+            raise PermissionDeniedError(f"HTTP 403: {resp.text[:100]}")
         elif resp.status_code == 429:
             self.is_healthy = False
             raise RateLimiteddError()
         else:
+             # 如果是 0,可能是 fetch 报错
+            if resp.status_code == 0:
+                 raise BizLogicError(f"Network Error: {resp.text}")
+            # TLS 业务错误
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
+        
 
-    def _solve_cloudflare5S_challenge(self):
+    def _refresh_firewall_session(self) -> bool:
         """
-        解决 Cloudflare 5s 盾
+        主动刷新页面以触发 Cloudflare 挑战并尝试通过
         """
-        self._log(f"Solving Cloudflare 5s...")
-        apt_config = self.free_config.get('apt_config', {})
-        website_url = f'https://visas-fr.tlscontact.com/en-us/country/{apt_config["country"]}'
-        
-        # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
-        p = self.config.proxy
-        if p.username:
-            proxy_str = f"{p.ip}:{p.port}:{p.username}:{p.password}"
-        else:
-            proxy_str = f"{p.ip}:{p.port}"
-        # 2. 提交任务
-        task_id = VSCloudApi.Instance().create_task(
-            command="AntiCloudflareTask",
-            args={
-                "proxy": proxy_str,
-                "websiteUrl": website_url
-            }
-        )
-        
-        result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
-        task_result = result_data.get("result", {})
-        cookies_list = task_result.get('cookies', [])
-        for cookie in cookies_list:
-            if cookie['name'] in ['__cf_bm', 'cf_clearance']:
-                self.session.cookies.set(
-                    cookie['name'], 
-                    cookie['value'], 
-                    domain=cookie['domain'], 
-                    path='/'
-                )
-        ua = task_result.get('userAgent')
-        if ua:
-            self.user_agent = ua
-            self.session.headers['User-Agent'] = ua
-        self._log("Cloudflare 5s challenge solved.")
+        try:
+            # 1. 刷新当前页面 (通常 Dashboard 页)
+            # 这会强制浏览器重新进行 HTTP 请求,从而触发 Cloudflare 拦截页
+            self._log("Refreshing page to trigger Cloudflare...")
+            self.page.refresh()
+            
+            # 2. 调用 CloudflareBypasser
+            cf = CloudflareBypasser(self.page, log=self.config.debug)
+            
+            # 3. 尝试过盾 (尝试次数稍多一点,因为此时可能网络不稳定)
+            success = cf.bypass(max_retry=10)
+            
+            if success:
+                # 再次确认页面是否正常加载 (非 403 页面)
+                title = self.page.title.lower()
+                if "access denied" in title:
+                    return False
+                
+                # 等待 DOM 稍微稳定
+                time.sleep(2)
+                return True
+            
+            return False
+        except Exception as e:
+            self._log(f"Error during firewall refresh: {e}")
+            return False
 
     def _solve_recaptcha(self, params) -> str:
-        """
-        调用 Capsolver
-        """
+        """调用 VSCloudApi 解决 ReCaptcha"""
         key = params.get("apiToken")
-        if not key: 
-            raise NotFoundError(message="Api-token is required for recaptcha solver")
+        if not key: raise NotFoundError("Api-token required")
         
         submit_url = "https://api.capsolver.com/createTask"
         task = {
@@ -441,15 +652,19 @@ class TlsPlugin(IVSPlg):
             if p.username:
                 task["proxyLogin"] = p.username
                 task["proxyPassword"] = p.password
+            
+        # 注意:使用 DrissionPage 后,通常是 ProxyLess 模式
+        # 除非你想让 Capsolver 也用同样的代理(通常不需要,除非风控极严)
         
         payload = {"clientKey": key, "task": task}
-        r = requests.post(submit_url, json=payload, timeout=20)
+        import requests as req # 局部引用,避免混淆
+        r = req.post(submit_url, json=payload, timeout=20)
         if r.status_code != 200:
             raise BizLogicError(message="Failed to submit capsolver task")
         
         task_id = r.json().get("taskId")
         for _ in range(20):
-            r = requests.post("https://api.capsolver.com/getTaskResult", json={"clientKey": key, "taskId": task_id}, timeout=20)
+            r = req.post("https://api.capsolver.com/getTaskResult", json={"clientKey": key, "taskId": task_id}, timeout=20)
             if r.status_code == 200:
                 d = r.json()
                 if d.get("status") == "ready":
@@ -487,73 +702,86 @@ class TlsPlugin(IVSPlg):
                 for s in day.get('slots', []):
                     labels = s.get('labels', [])
                     lbl = ""
-                    stype = ""
-                    cost = ""
+                    # 简化逻辑:TLS label 列表
+                    if 'pta' in labels: lbl = 'pta'
+                    elif 'ptaw' in labels: lbl = 'ptaw'
+                    elif '' in labels or not labels: lbl = ''
                     
-                    if 'pta' in labels:
-                        lbl = 'pta'
-                        stype = "Prime"
-                    elif 'ptaw' in labels:
-                        lbl = 'ptaw'
-                        stype = "Prime Weekend"
-                    elif '' in labels:
-                        lbl = ''
-                        stype = "Standard"
-                    
-                    if lbl or not labels: 
-                            slots.append({
-                                'date': d_str,
-                                'time': s.get('time'),
-                                'label': lbl,
-                                'type': stype,
-                                'cost': cost
-                            })
-            return slots
-        else:
-            self._log('Parsed appointment slot page, but not found availableAppointments')
+                    slots.append({
+                        'date': d_str,
+                        'time': s.get('time'),
+                        'label': lbl
+                    })
         return slots
   
     def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
         if not html:
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
-    
-        if keyword not in html: 
-            if 'redirected automatically' in html.lower():
-                self.is_healthy = False
-                raise SessionExpiredOrInvalidError()
-            if 'login' in html.lower() and 'password' in html.lower():
+        
+        # 将 html 转小写检查
+        html_lower = html.lower()
+        if keyword.lower() not in html_lower: 
+            if 'redirected automatically' in html_lower:
                 self.is_healthy = False
-                raise SessionExpiredOrInvalidError()
-            if 'session expired!' in html.lower() and 'for security reasons, your session has expired. please log in again to continue.' in html.lower() and 'you will be redirected automatically in 10 seconds.' in html.lower():
+                raise SessionExpiredOrInvalidError("Redirected automatically")
+            if 'login' in html_lower and 'password' in html_lower:
                 self.is_healthy = False
-                raise SessionExpiredOrInvalidError()
-            if 'temporarily blocked!' in html.lower() and 'Your session has been temporarily suspended due to the high number of your access to this page.' in html.lower() and 'You can try to access your account again in 2 hours.' in html.lower():
+                raise SessionExpiredOrInvalidError("Redirected to login")
+            if 'session expired' in html_lower:
                 self.is_healthy = False
-                raise SessionExpiredOrInvalidError()
+                raise SessionExpiredOrInvalidError("Session expired")
             
     def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
-        """
-        根据用户的期望范围筛选可用日期
-        
-        :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
-        :param start_str: 用户期望开始日期 (YYYY-MM-DD)
-        :param end_str: 用户期望结束日期 (YYYY-MM-DD)
-        :return: 符合要求的日期列表
-        """
-        # 如果没有设置范围,则不过滤,返回所有日期
         if not start_str or not end_str:
             return dates
-            
         valid_dates = []
-        # 截取前10位以防带有时分秒
         s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
         e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
-        
         for date_str in dates:
             curr_date = datetime.strptime(date_str, "%Y-%m-%d")
-            # 比较范围 (闭区间)
             if s_date <= curr_date <= e_date:
                 valid_dates.append(date_str)
         random.shuffle(valid_dates)
-        return valid_dates
+        return valid_dates
+    
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        # 1. 关闭浏览器
+        if self.page:
+            try:
+                self.page.quit() # 这会关闭 Chrome 进程
+            except Exception:
+                pass # 忽略已关闭的错误
+            self.page = None
+        
+        # 2. 删除文件
+        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    # 如果删除失败(通常是Windows文件占用),重试
+                    self._log(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
+            if os.path.exists(self.root_workspace):
+                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+        # 3. [新增] 关闭代理隧道
+        if self.tunnel:
+            try: self.tunnel.stop()
+            except: pass
+            self.tunnel = None
+        
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()

+ 0 - 784
plugins/tls_plugin2.py

@@ -1,784 +0,0 @@
-import time
-import json
-import random
-import re
-import os
-import uuid
-import shutil
-from datetime import datetime
-from typing import List, Dict, Optional, Any, Callable
-from urllib.parse import urljoin, urlparse, urlencode
-
-# DrissionPage 核心
-from DrissionPage import ChromiumPage, ChromiumOptions
-
-from vs_plg import IVSPlg
-from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
-from toolkit.proxy_tunnel import ProxyTunnel
-
-
-class BrowserResponse:
-    """模拟 requests.Response"""
-    def __init__(self, result_dict):
-        result_dict = result_dict or {}
-        self.status_code = result_dict.get('status', 0)
-        self.text = result_dict.get('body', '')
-        self.headers = result_dict.get('headers', {})
-        self.url = result_dict.get('url', '')
-        self._json = None
-
-    def json(self):
-        if self._json is None:
-            if not self.text:
-                return {}
-            try:
-                self._json = json.loads(self.text)
-            except:
-                self._json = {}
-        return self._json
-
-class TlsPlugin2(IVSPlg):
-    """
-    TLSContact 签证预约插件 (DrissionPage 版)
-    """
-
-    def __init__(self, group_id: str):
-        self.group_id = group_id
-        self.config: Optional[VSPlgConfig] = None
-        self.free_config: Dict[str, Any] = {}
-        self.is_healthy = True
-        self.logger = None
-        
-        # 浏览器实例
-        self.page: Optional[ChromiumPage] = None
-        
-        self.travel_group: Optional[Dict] = None
-        
-        # --- [核心修改] 并发隔离与资源管理 ---
-        # 生成唯一实例 ID
-        self.instance_id = uuid.uuid4().hex[:8]
-        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
-        # 定义子目录:代理插件目录 & 浏览器用户数据目录
-        self.user_data_path = os.path.join(self.root_workspace, "user_data")
-    
-        # 确保根目录存在 (子目录由具体逻辑创建)
-        if not os.path.exists(self.root_workspace):
-            os.makedirs(self.root_workspace)
-            
-        # 持有隧道实例
-        self.tunnel = None
-        
-        self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
-    
-    def set_log(self, logger: Callable[[str], None]) -> None:
-        self.logger = logger
-    
-    def _log(self, message):
-        if self.logger:
-            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
-        else:
-            print(f'[TlsPlugin] [{self.group_id}] {message}')
-
-    def set_config(self, config: VSPlgConfig):
-        self.config = config
-        self.free_config = config.free_config or {}
-
-    def health_check(self) -> bool:
-        if not self.is_healthy:
-            return False
-        if self.page is None:
-            return False
-        try:
-            if not self.page.run_js("return 1;"):
-                return False
-        except:
-            return False
-            
-        if self.config.session_max_life > 0:
-            current_time = time.time()
-            elapsed_time = current_time - self.session_create_time
-            if elapsed_time > self.config.session_max_life * 60:
-                self._log(f"Session expired.")
-                return False
-        return True
-    
-    def _save_screenshot(self, name_prefix):
-        try:
-            timestamp = int(time.time())
-            filename = f"{self.instance_id}_{name_prefix}_{timestamp}.jpg"
-            save_path = os.path.join("data", filename)
-            os.makedirs("data", exist_ok=True)
-            
-            # [修改] 改为 full_page=False,防止页面结构异常导致截图失败
-            # 这样能截取到浏览器当前可视区域,最适合调试“卡住”的情况
-            self.page.get_screenshot(path=save_path, full_page=False)
-            
-            self._log(f"Screenshot saved to {save_path}")
-        except Exception as e:
-            self._log(f"Failed to save screenshot: {e}")
-
-    def create_session(self):
-        """
-        全浏览器会话创建:过盾 -> JS注入登录 -> 原生跳转
-        """
-        self._log(f"Initializing Session (ID: {self.instance_id})...")
-        co = ChromiumOptions()
-        # -------------------------------------------------------------
-        # [核心修复] 解决 'not enough values to unpack'
-        # -------------------------------------------------------------
-        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
-        # 2. 我们手动随机生成一个端口
-        import random
-        import socket
-        
-        def get_free_port():
-            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-                s.bind(('', 0))
-                return s.getsockname()[1]
-        
-        debug_port = get_free_port()
-        self._log(f"Assigned Debug Port: {debug_port}")
-        
-        # 3. 强制指定端口,DrissionPage 就会直接连接,不再解析日志
-        co.set_local_port(debug_port)
-        
-        # --- [关键配置] 设置独立的用户数据目录 ---
-        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
-        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
-        co.set_user_data_path(self.user_data_path)
-        
-        # --- 1. 指定浏览器路径 (适配 Docker) ---
-        chrome_path = os.getenv("CHROME_BIN")
-        if chrome_path and os.path.exists(chrome_path):
-            co.set_paths(browser_path=chrome_path)
-        
-        # --- [核心修改] 代理配置 ---
-        if self.config.proxy and self.config.proxy.ip:
-            p = self.config.proxy
-            
-            if p.username and p.password:
-                self._log(f"Starting Proxy Tunnel for {p.ip}...")
-                
-                # 1. 启动本地隧道
-                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
-                local_proxy = self.tunnel.start()
-                
-                self._log(f"Tunnel started at {local_proxy}")
-                
-                # 2. Chrome 连接本地免密端口
-                # 必须使用 --proxy-server 强制指定,绝对稳健
-                co.set_argument(f'--proxy-server={local_proxy}')
-                
-            else:
-                # 无密码代理,直接用
-                proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
-                co.set_argument(f'--proxy-server={proxy_str}')
-        else:
-            self._log("[WARN] No proxy configured!")
-
-        co.headless(False)
-        co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
-        # Docker 默认 /dev/shm 只有 64MB,Chromium 很容易爆内存崩溃
-        co.set_argument('--disable-dev-shm-usage')
-        co.set_argument('--window-size=1920,1080')
-        co.set_argument('--disable-blink-features=AutomationControlled')
-
-        try:
-            self.page = ChromiumPage(co)
-            
-            apt_config = self.free_config.get('apt_config', {})
-            if not apt_config:
-                raise NotFoundError("apt_config config missing")
-
-            login_url = "https://visas-fr.tlscontact.com/en-us/login"
-            params = {
-                "issuerId": apt_config["code"],
-                "country": apt_config["country"],
-                "vac": apt_config["code"],
-                "redirect": f"/en-us/country/{apt_config['country']}/vac/{apt_config['code']}"
-            }
-            full_login_url = f"{login_url}?{urlencode(params)}"
-            
-            self._log(f"Navigating: {full_login_url}")
-            self.page.get(full_login_url)
-            
-            # --- Cloudflare 过盾 ---
-            cf = CloudflareBypasser(self.page, log=self.config.debug)
-            if not cf.bypass(max_retry=15):
-                raise BizLogicError("Cloudflare bypass timeout")
-            
-            wait_start = time.time()
-            while True:
-                # 获取页面 HTML,转小写
-                # 注意:如果此处报错 "页面被刷新",是 DrissionPage 的机制问题,
-                # 但你要求先不处理复杂错误,所以这里保持最简单的写法。
-                html = self.page.html.lower()
-                
-                # 检查是否在排队室 (法语或英语)
-                if "file d'attente" in html or "waiting room" in html:
-                    # 如果等太久(比如1小时),就强制停止
-                    if time.time() - wait_start > 6 * 60:
-                        self._log("Waiting room timeout (1h).")
-                        break
-                        
-                    self._log("In Waiting Room... Waiting for auto-refresh.")
-                    time.sleep(30) # 截图说页面会自动刷新,所以这里只sleep,不动浏览器
-                else:
-                    # 页面里没有“等候室”的字了,说明出来了
-                    break
-
-            # --- 登录页面检查 ---
-            if not self.page.ele('#email-input-field'):
-                self._log("Reloading Login Page...")
-                self.page.get(full_login_url)
-                if not self.page.wait.ele_displayed('#email-input-field', timeout=15):
-                    self._save_screenshot("login_load_fail")
-                    raise BizLogicError("Login form not loaded")
-
-            # --- JS 注入登录 ---
-            g_token = ""
-            if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
-                self._log("Solving ReCaptcha...")
-                rc_params = {
-                    "type": "ReCaptchaV2TaskProxyLess", "page": self.page.url,
-                    "siteKey": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0", 
-                    "apiToken": self.free_config.get("capsolver_key", "")
-                }
-                g_token = self._solve_recaptcha(rc_params)
-
-            username = self.config.account.username
-            password = self.config.account.password
-            
-            # 使用 JS 直接操作 DOM 并 click,让浏览器处理 302
-            js_login = f"""
-            var u = document.getElementById('email-input-field');
-            if(u) {{ u.value = "{username}"; u.dispatchEvent(new Event('input', {{bubbles:true}})); }}
-            
-            var p = document.getElementById('password-input-field');
-            if(p) {{ p.value = "{password}"; p.dispatchEvent(new Event('input', {{bubbles:true}})); }}
-            
-            var g = document.getElementById('g-recaptcha-response');
-            if(g) {{ g.value = "{g_token}"; }}
-            
-            var btn = document.getElementById('btn-login');
-            if(btn) {{ btn.click(); return true; }} else {{ return false; }}
-            """
-            
-            self._log("Submitting Login via JS...")
-            if not self.page.run_js(js_login): raise BizLogicError("Login button missing")
-
-            # --- 等待跳转 ---
-            self._log("Waiting for redirect...")
-            self.page.wait.url_change('login-actions', exclude=True, timeout=45)
-            
-            # 检查是否失败
-            if "login-actions" in self.page.url or "auth" in self.page.url:
-                err = "Unknown Login Error"
-                if "Invalid username" in self.page.html: err = "Invalid Credentials"
-                self._save_screenshot("login_submit_fail")
-                raise BizLogicError(f"Login Failed: {err}")
-
-            # --- 提取 Dashboard 信息 ---
-            self._log("Waiting for dashboard...")
-            self.page.wait.load_start()
-            time.sleep(5)
-            
-            html = self.page.html
-            self._check_page_is_session_expired_or_invalid("My travel group", html)
-            groups = self._parse_travel_groups(html)
-            
-            target_city = apt_config['city'].lower()
-            for g in groups:
-                if g['location'].lower() == target_city:
-                    self.travel_group = g
-                    break
-            
-            if not self.travel_group:
-                self._save_screenshot("group_not_found")
-                raise NotFoundError(f"Group not found for {target_city}")
-            
-            self.session_create_time = time.time()
-            self._log(f"Session Ready. Group: {self.travel_group['group_number']}")
-
-        except Exception as e:
-            self._log(f"Session Create Error: {e}")
-            self.cleanup()
-            raise e
-
-    def query(self, apt_type: AppointmentType) -> VSQueryResult:
-        res = VSQueryResult()
-        res.success = False
-        apt_config = self.free_config.get('apt_config', {})
-        group_num = self.travel_group['group_number']
-        interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
-        
-        url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
-        params = {
-            'location': apt_config["code"],
-            'month': interest_month,
-        }
-        
-        # DrissionPage 自动处理 Cloudflare,直接 fetch 即可
-        try:
-            resp = self._perform_request("GET", url, params=params, retry_count=1)
-        except Exception as e:
-            self._log(f"Query request failed: {e}")
-            raise e
-
-        self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
-
-        # 解析 Slots
-        all_slots = self._parse_appointment_slots(resp.text)
-
-        target_labels = self.free_config.get("target_labels", ["", "pta"])
-        # 根据配置过滤
-        available = [s for s in all_slots if s.get("label") in target_labels]
-        
-        if available:
-            res.success = True
-            earliest_date = available[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 available:
-                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()]
-        else:
-            res.success = False
-            res.availability_status = AvailabilityStatus.NoneAvailable
-        return res
-
-    def book(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
-        res = VSBookResult()
-        res.success = False
-        
-        apt_config = self.free_config.get('apt_config', {})
-        group_num = self.travel_group['group_number']
-        
-        available_dates = [da.date for da in slot_info.availability]
-        exp_start = user_inputs.get('expected_start_date', '')
-        exp_end = user_inputs.get('expected_end_date', '')
-        support_pta = user_inputs.get('support_pta', True)
-
-        target_labels = ['']
-        if support_pta:
-            target_labels.append('pta')
-        available_dates_str = [
-            da.date.strftime("%Y-%m-%d")
-            for da in slot_info.availability
-        ]
-        valid_dates = self._filter_dates(available_dates_str, exp_start, exp_end)
-        if not valid_dates:
-            raise NotFoundError(message="No dates match user constraints")
-        
-        selected_date = None
-        selected_time = None
-        selected_label = None
-        
-        for d in valid_dates:
-            for da in slot_info.availability:
-                if da.date == d:
-                    for t in da.times:
-                        if t.label in target_labels:
-                            selected_date = d
-                            selected_time = t
-                            selected_label = t.label
-                            break
-            if selected_date: break
-            
-        if not selected_date:
-             raise NotFoundError(message="No suitable slot found")
-
-        # 2. 解决 ReCaptcha V3 (Action: book)
-        page_url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking?location={apt_config["code"]}&month={selected_date[:7]}'
-        
-        api_token = self.free_config.get("capsolver_key", "")
-        rc_params = {
-            "type": "ReCaptchaV3Task",
-            "page": page_url,
-            "action": "book", 
-            "siteKey": "6LcTpXcfAAAAAM3VojNhyV-F1z92ADJIvcSZ39Y9",
-            "apiToken": api_token,
-            "proxy": self._get_proxy_url() # ProxyLess
-        }
-        g_token = self._solve_recaptcha(rc_params)
-
-        # 3. 构造 Next.js Payload
-        # 注意:在 JS 中构造 FormData 比在 Python 中拼 Multipart 更容易且不易出错
-        ACTION_ID = "60d0616946df1fc4e7c094ca6a7a04f134d0be3d53"
-        url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
-        
-        # State Tree 字符串
-        router_state = '%5B%22%22%2C%7B%22children%22%3A%5B%5B%22lang%22%2C%22en-us%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%5B%22groupId%22%2C%22'+str(group_num)+'%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%22workflow%22%2C%7B%22children%22%3A%5B%22appointment-booking%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D'
-
-        # 构造 JS 代码执行 fetch
-        # 使用 FormData 对象来处理 multipart
-        js_script = f"""
-        const url = "{url}";
-        const formData = new FormData();
-        
-        formData.append('1_formGroupId', '{group_num}');
-        formData.append('1_lang', 'en-us');
-        formData.append('1_process', 'APPOINTMENT');
-        formData.append('1_location', '{apt_config["code"]}');
-        formData.append('1_date', '{selected_date}');
-        formData.append('1_time', '{selected_time.time}');
-        formData.append('1_appointmentLabel', '{selected_label}');
-        formData.append('1_captcha_token', '{g_token}');
-        formData.append('0', '[{{"status":"IDLE"}},"$K1"]');
-        
-        const headers = {{
-            'Next-Action': '{ACTION_ID}',
-            'Next-Router-State-Tree': decodeURIComponent('{router_state}'),
-            'Accept': 'text/x-component'
-        }};
-        
-        return fetch(url, {{
-            method: 'POST',
-            headers: headers,
-            body: formData
-        }}).then(async response => {{
-            const text = await response.text();
-            const headers = {{}};
-            response.headers.forEach((value, key) => headers[key] = value);
-            return {{
-                status: response.status,
-                body: text,
-                headers: headers,
-                url: response.url
-            }};
-        }}).catch(err => {{
-            return {{ status: 0, body: err.toString(), headers: {{}}, url: url }};
-        }});
-        """
-        
-        self._log("Submitting booking request via JS Fetch...")
-        res_dict = self.page.run_js(js_script)
-        resp = BrowserResponse(res_dict)
-
-        # 4. 结果判定
-        # Next.js Server Action 重定向通常是 303,但 fetch 可能会自动跟随
-        # 如果 fetch 跟随了,url 会变;如果没跟随(Redirect mode: manual),status 是 303
-        
-        if resp.status_code == 303 or (resp.status_code == 200 and "appointment-confirmation" in resp.url):
-             self._log(f"Booking Success! URL: {resp.url}")
-             res.success = True
-             res.book_date = selected_date
-             res.book_time = selected_time
-             return res
-
-        if resp.status_code == 200:
-            if "APPOINTMENT_LIMIT_REACHED" in resp.text:
-                 self._log("Failed: Appointment Limit Reached")
-            elif "Invalid captcha" in resp.text:
-                 self._log("Failed: Invalid Captcha")
-            else:
-                 self._log(f"Booking Failed (Unknown 200): {resp.text[:200]}")
-        else:
-            self._log(f"Booking Failed. Status: {resp.status_code}")
-
-        return res
-
-    # --- 辅助方法 ---
-    
-    def _get_proxy_url(self):
-        # 构造代理
-        proxy_url = ""
-        if self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
-        return proxy_url
-
-    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
-        """
-        在浏览器上下文中注入 JS 执行 Fetch
-        """
-        if not self.page:
-            raise BizLogicError("Browser not initialized")
-
-        if params:
-            from urllib.parse import urlencode
-            if '?' in url:
-                url += '&' + urlencode(params)
-            else:
-                url += '?' + urlencode(params)
-
-        fetch_options = {
-            "method": method.upper(),
-            "headers": headers or {},
-            "credentials": "include"
-        }
-
-        # Body 处理
-        if json_data:
-            fetch_options['body'] = json.dumps(json_data)
-            fetch_options['headers']['Content-Type'] = 'application/json'
-        elif data:
-             if isinstance(data, dict):
-                from urllib.parse import urlencode
-                fetch_options['body'] = urlencode(data)
-                fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
-             else:
-                 fetch_options['body'] = data
-
-        js_script = f"""
-        const url = "{url}";
-        const options = {json.dumps(fetch_options)};
-        
-        return fetch(url, options)
-            .then(async response => {{
-                const text = await response.text();
-                const headers = {{}};
-                response.headers.forEach((value, key) => headers[key] = value);
-                
-                return {{
-                    status: response.status,
-                    body: text,
-                    headers: headers,
-                    url: response.url
-                }};
-            }})
-            .catch(error => {{
-                return {{
-                    status: 0,
-                    body: error.toString(),
-                    headers: {{}},
-                    url: url
-                }};
-            }});
-        """
-        
-        res_dict = self.page.run_js(js_script, timeout=30)
-        resp = BrowserResponse(res_dict)
-        
-        if resp.status_code == 200:
-            return resp
-        elif resp.status_code == 401:
-            self.is_healthy = False
-            raise SessionExpiredOrInvalidError()
-        elif resp.status_code == 403:
-            # [关键修改] 遇到 403 Forbidden,尝试绕盾并重试
-            # 最多重试 2 次
-            if retry_count < 2:
-                self._log(f"HTTP 403 Detected. Cloudflare session expired? Attempting refresh (Try {retry_count+1}/2)...")
-                
-                # 尝试刷新盾
-                if self._refresh_firewall_session():
-                    self._log("Firewall session refreshed. Retrying request...")
-                    # 递归重试
-                    return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
-                else:
-                    self._log("Failed to refresh firewall session.")
-            
-            # 如果重试失败,抛出异常
-            raise PermissionDeniedError(f"HTTP 403: {resp.text[:100]}")
-        elif resp.status_code == 429:
-            self.is_healthy = False
-            raise RateLimiteddError()
-        else:
-             # 如果是 0,可能是 fetch 报错
-            if resp.status_code == 0:
-                 raise BizLogicError(f"Network Error: {resp.text}")
-            # TLS 业务错误
-            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
-        
-
-    def _refresh_firewall_session(self) -> bool:
-        """
-        主动刷新页面以触发 Cloudflare 挑战并尝试通过
-        """
-        try:
-            # 1. 刷新当前页面 (通常 Dashboard 页)
-            # 这会强制浏览器重新进行 HTTP 请求,从而触发 Cloudflare 拦截页
-            self._log("Refreshing page to trigger Cloudflare...")
-            self.page.refresh()
-            
-            # 2. 调用 CloudflareBypasser
-            cf = CloudflareBypasser(self.page, log=self.config.debug)
-            
-            # 3. 尝试过盾 (尝试次数稍多一点,因为此时可能网络不稳定)
-            success = cf.bypass(max_retry=10)
-            
-            if success:
-                # 再次确认页面是否正常加载 (非 403 页面)
-                title = self.page.title.lower()
-                if "access denied" in title:
-                    return False
-                
-                # 等待 DOM 稍微稳定
-                time.sleep(2)
-                return True
-            
-            return False
-        except Exception as e:
-            self._log(f"Error during firewall refresh: {e}")
-            return False
-
-    def _solve_recaptcha(self, params) -> str:
-        """调用 VSCloudApi 解决 ReCaptcha"""
-        key = params.get("apiToken")
-        if not key: raise NotFoundError("Api-token required")
-        
-        submit_url = "https://api.capsolver.com/createTask"
-        task = {
-            "type": params.get("type"),
-            "websiteURL": params.get("page"),
-            "websiteKey": params.get("siteKey"),
-        }
-        if params.get("action"):
-            task["pageAction"] = params.get("action")
-            
-        if params.get("proxy"):
-            p = urlparse(params.get("proxy"))
-            task["proxyType"] = p.scheme
-            task["proxyAddress"] = p.hostname
-            task["proxyPort"] = p.port
-            if p.username:
-                task["proxyLogin"] = p.username
-                task["proxyPassword"] = p.password
-            
-        # 注意:使用 DrissionPage 后,通常是 ProxyLess 模式
-        # 除非你想让 Capsolver 也用同样的代理(通常不需要,除非风控极严)
-        
-        payload = {"clientKey": key, "task": task}
-        import requests as req # 局部引用,避免混淆
-        r = req.post(submit_url, json=payload, timeout=20)
-        if r.status_code != 200:
-            raise BizLogicError(message="Failed to submit capsolver task")
-        
-        task_id = r.json().get("taskId")
-        for _ in range(20):
-            r = req.post("https://api.capsolver.com/getTaskResult", json={"clientKey": key, "taskId": task_id}, timeout=20)
-            if r.status_code == 200:
-                d = r.json()
-                if d.get("status") == "ready":
-                    return d["solution"]["gRecaptchaResponse"]
-            time.sleep(3)
-        raise BizLogicError(message="Capsolver task timeout")
-
-    def _parse_travel_groups(self, html: str) -> List[Dict]:
-        groups = []
-        js_pattern = r'\\"travelGroups\\":\s*(\[.*?\]),\\"availableCountriesToCreateGroups'
-        js_match = re.search(js_pattern, html, re.DOTALL)
-        if js_match:
-            json_str = js_match.group(1).replace(r'\"', '"')
-            data = json.loads(json_str)
-            for g in data:
-                groups.append({
-                    'group_name': g.get('groupName'),
-                    'group_number': g.get('formGroupId'),
-                    'location': g.get('vacName')
-                })
-        else:
-            self._log('Parsed travel group page, but not found travelGroups')
-        return groups
-
-    def _parse_appointment_slots(self, html: str) -> List[Dict]:
-        slots = []
-        pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
-        match = re.search(pattern, html, re.DOTALL)
-        
-        if match:
-            json_str = match.group(1).replace(r'\"', '"')
-            data = json.loads(json_str)
-            for day in data:
-                d_str = day.get('day')
-                for s in day.get('slots', []):
-                    labels = s.get('labels', [])
-                    lbl = ""
-                    # 简化逻辑:TLS label 列表
-                    if 'pta' in labels: lbl = 'pta'
-                    elif 'ptaw' in labels: lbl = 'ptaw'
-                    elif '' in labels or not labels: lbl = ''
-                    
-                    slots.append({
-                        'date': d_str,
-                        'time': s.get('time'),
-                        'label': lbl
-                    })
-        return slots
-  
-    def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
-        if not html:
-            self.is_healthy = False
-            raise SessionExpiredOrInvalidError()
-        
-        # 将 html 转小写检查
-        html_lower = html.lower()
-        if keyword.lower() not in html_lower: 
-            if 'redirected automatically' in html_lower:
-                self.is_healthy = False
-                raise SessionExpiredOrInvalidError("Redirected automatically")
-            if 'login' in html_lower and 'password' in html_lower:
-                self.is_healthy = False
-                raise SessionExpiredOrInvalidError("Redirected to login")
-            if 'session expired' in html_lower:
-                self.is_healthy = False
-                raise SessionExpiredOrInvalidError("Session expired")
-            
-    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
-        if not start_str or not end_str:
-            return dates
-        valid_dates = []
-        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
-        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
-        for date_str in dates:
-            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
-            if s_date <= curr_date <= e_date:
-                valid_dates.append(date_str)
-        random.shuffle(valid_dates)
-        return valid_dates
-    
-    # --- 资源清理核心方法 ---
-    def cleanup(self):
-        """
-        销毁浏览器并彻底删除临时文件
-        """
-        # 1. 关闭浏览器
-        if self.page:
-            try:
-                self.page.quit() # 这会关闭 Chrome 进程
-            except Exception:
-                pass # 忽略已关闭的错误
-            self.page = None
-        
-        # 2. 删除文件
-        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
-        if os.path.exists(self.root_workspace):
-            for _ in range(3):
-                try:
-                    time.sleep(0.2)
-                    shutil.rmtree(self.root_workspace, ignore_errors=True)
-                    break
-                except Exception as e:
-                    # 如果删除失败(通常是Windows文件占用),重试
-                    self._log(f"Cleanup retry: {e}")
-                    time.sleep(0.5)
-            
-            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
-            if os.path.exists(self.root_workspace):
-                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
-        # 3. [新增] 关闭代理隧道
-        if self.tunnel:
-            try: self.tunnel.stop()
-            except: pass
-            self.tunnel = None
-        
-    def __del__(self):
-        """
-        析构函数:当对象被垃圾回收时自动调用
-        """
-        self.cleanup()

Plik diff jest za duży
+ 950 - 321
plugins/vfs_plugin.py


+ 0 - 1551
plugins/vfs_plugin2.py

@@ -1,1551 +0,0 @@
-# plugins/vfs_plugin2.py
-
-import os
-import time
-import json
-import random
-import base64
-import uuid
-import shutil
-import re
-import urllib.parse
-from datetime import datetime
-from typing import Dict, Any, Optional, List, Tuple, Callable
-
-# DrissionPage 核心引入
-from DrissionPage import ChromiumPage, ChromiumOptions
-from DrissionPage.common import Settings
-
-# 加密库
-from cryptography.hazmat.primitives import serialization, hashes
-from cryptography.hazmat.primitives.asymmetric import padding
-from cryptography.hazmat.backends import default_backend
-
-from vs_plg import IVSPlg 
-from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.vs_cloud_api import VSCloudApi
-from toolkit.proxy_tunnel import ProxyTunnel
-from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
-
-
-# ----------------- 静态常量与辅助数据 -----------------
-
-VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
-LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
-t59GV321kN0iquYRHrdh3ygfDDHlS9rROQeBqRga0ncSADtbLMrBPqXJjPCoV76y
-t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
-1e5wXLfvpj9w1QsiSWOSJxLOyICz/9ByxXycQQFdNmjnnnwco9Gt/Mi33NYH71j0
-5oXIjklFC4lvJqaqSY5lS7Vwb9oCt9zX9J0Yz4z4e/3V+0jgRnWOFGofyks4FKe2
-GQIDAQAB
------END PUBLIC KEY-----"""
-
-COUNTRY_MAP = {
-    "afghanistan": "AFG", "albania": "ALB", "algeria": "DZA", "andorra": "AND",  "angola": "AGO",
-    "antigua and barbuda": "ATG", "argentina": "ARG", "armenia": "ARM", "australia": "AUS", "austria": "AUT",
-    "azerbaijan": "AZE", "bahamas": "BHS", "bahrain": "BHR", "bangladesh": "BGD", "barbados": "BRB", "belarus": "BLR",
-    "belgium": "BEL", "belize": "BLZ", "benin": "BEN", "bhutan": "BTN", "bolivia": "BOL", "bosnia and herzegovina": "BIH",
-    "botswana": "BWA", "brazil": "BRA", "brunei": "BRN", "bulgaria": "BGR", "burkina faso": "BFA", "burundi": "BDI",
-    "cabo verde": "CPV", "cambodia": "KHM", "cameroon": "CMR", "canada": "CAN", "central african republic": "CAF",
-    "chad": "TCD", "chile": "CHL", "china": "CHN", "colombia": "COL", "comoros": "COM", "congo (brazzaville)": "COG",
-    "congo (kinshasa)": "COD", "costa rica": "CRI", "croatia": "HRV", "cuba": "CUB", "cyprus": "CYP", "czech republic": "CZE",
-    "denmark": "DNK", "djibouti": "DJI", "dominica": "DMA", "dominican republic": "DOM", "ecuador": "ECU", "egypt": "EGY",
-    "el salvador": "SLV", "equatorial guinea": "GNQ", "eritrea": "ERI", "estonia": "EST", "eswatini": "SWZ", "ethiopia": "ETH",
-    "fiji": "FJI", "finland": "FIN", "france": "FRA", "gabon": "GAB", "gambia": "GMB", "georgia": "GEO", "germany": "DEU",
-    "ghana": "GHA", "greece": "GRC", "grenada": "GRD", "guatemala": "GTM", "guinea": "GIN", "guinea-bissau": "GNB", "guyana": "GUY",
-    "haiti": "HTI", "honduras": "HND", "hungary": "HUN", "iceland": "ISL", "india": "IND", "indonesia": "IDN", "iran": "IRN",
-    "iraq": "IRQ", "ireland": "IRL", "israel": "ISR", "italy": "ITA", "jamaica": "JAM", "japan": "JPN", "jordan": "JOR",
-    "kazakhstan": "KAZ", "kenya": "KEN", "kiribati": "KIR", "korea, north": "PRK", "korea, south": "KOR", "kuwait": "KWT",
-    "kyrgyzstan": "KGZ", "laos": "LAO", "latvia": "LVA", "lebanon": "LBN", "lesotho": "LSO", "liberia": "LBR", "libya": "LBY",
-    "liechtenstein": "LIE", "lithuania": "LTU", "luxembourg": "LUX", "madagascar": "MDG", "malawi": "MWI", "malaysia": "MYS",
-    "maldives": "MDV", "mali": "MLI", "malta": "MLT", "marshall islands": "MHL", "mauritania": "MRT", "mauritius": "MUS",
-    "mexico": "MEX", "micronesia": "FSM", "moldova": "MDA", "monaco": "MCO", "mongolia": "MNG", "montenegro": "MNE", "morocco": "MAR",
-    "mozambique": "MOZ", "myanmar": "MMR", "namibia": "NAM", "nauru": "NRU", "nepal": "NPL", "netherlands": "NLD", "new zealand": "NZL",
-    "nicaragua": "NIC", "niger": "NER", "nigeria": "NGA", "north macedonia": "MKD", "norway": "NOR", "oman": "OMN", "pakistan": "PAK",
-    "palau": "PLW", "panama": "PAN", "papua new guinea": "PNG", "paraguay": "PRY", "peru": "PER", "philippines": "PHL", "poland": "POL",
-    "portugal": "PRT", "qatar": "QAT", "romania": "ROU", "russia": "RUS", "rwanda": "RWA", "saudi arabia": "SAU", "senegal": "SEN",
-    "serbia": "SRB", "seychelles": "SYC", "sierra leone": "SLE", "singapore": "SGP", "slovakia": "SVK", "slovenia": "SVN",
-    "solomon islands": "SLB", "somalia": "SOM", "south africa": "ZAF", "spain": "ESP", "sri lanka": "LKA", "sudan": "SDN",
-    "suriname": "SUR", "sweden": "SWE", "switzerland": "CHE", "syria": "SYR", "tajikistan": "TJK", "tanzania": "TZA", "thailand": "THA",
-    "timor-leste": "TLS", "togo": "TGO", "tonga": "TON", "tunisia": "TUN", "turkey": "TUR", "turkmenistan": "TKM", "uganda": "UGA",
-    "ukraine": "UKR", "united arab emirates": "ARE", "united kingdom": "GBR", "united states": "USA", "uruguay": "URY", "uzbekistan": "UZB",
-    "vanuatu": "VUT", "venezuela": "VEN", "vietnam": "VNM", "yemen": "YEM", "zambia": "ZMB", "zimbabwe": "ZWE"
-}
-
-def get_country_iso3(name: str) -> str:
-    return COUNTRY_MAP.get(name.lower(), "CHN")
-
-def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
-    try:
-        dt = datetime.strptime(data_str, date_str_format)
-        return dt.strftime(target_format)
-    except:
-        return data_str
-    
-def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
-    """
-    将邮箱域名替换为指定域名(默认 gmail-app.com)
-    """
-    if "@" not in email:
-        raise ValueError(f"Invalid email: {email}")
-
-    local_part, _ = email.rsplit("@", 1)
-    return f"{local_part}@{new_domain}"
-
-# --- 模拟 Requests Response 对象 ---
-class BrowserResponse:
-    def __init__(self, result_dict):
-        result_dict = result_dict or {}
-        self.status_code = result_dict.get('status', 0)
-        self.text = result_dict.get('body', '')
-        self.headers = result_dict.get('headers', {})
-        self.url = result_dict.get('url', '')
-        self._json = None
-
-    def json(self):
-        if self._json is None:
-            if not self.text:
-                return {}
-            try:
-                self._json = json.loads(self.text)
-            except:
-                self._json = {}
-        return self._json
-
-    @property
-    def content(self):
-        return self.text.encode('utf-8')
-
-class VfsPlugin2(IVSPlg):
-    def __init__(self, group_id: str):
-        self.group_id = group_id
-        self.config: Optional[VSPlgConfig] = None
-        self.free_config: Dict[str, Any] = {}
-        self.logger = None
-        
-        # 替换 requests.Session 为 DrissionPage
-        self.page: Optional[ChromiumPage] = None
-        
-        self.jwt_token: str = ""
-        self.real_ip: str = ""
-        self.is_healthy: bool = True
-        
-        self.center_conf = None
-        self.category_conf: Dict = {}
-        self.subcategory_conf: Dict = {}
-        
-        self.booking_wait_applied = False
-        
-        self.public_key = serialization.load_pem_public_key(
-            VFS_PUBLIC_KEY_PEM.encode(),
-            backend=default_backend()
-        )
-        
-        # --- [核心修改] 并发隔离与资源管理 ---
-        # 生成唯一实例 ID
-        self.instance_id = uuid.uuid4().hex[:8]
-        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
-        self.user_data_path = os.path.join(self.root_workspace, "user_data")
-        
-        # 持有隧道实例
-        self.tunnel = None
-        
-        # 确保根目录存在 (子目录由具体逻辑创建)
-        if not os.path.exists(self.root_workspace):
-            os.makedirs(self.root_workspace)
-            
-        self.session_create_time: float = 0
-
-    def get_group_id(self) -> str:
-        return self.group_id
-
-    def set_config(self, config: VSPlgConfig):
-        self.config = config
-        self.free_config = config.free_config or {}
-        
-    def set_log(self, logger: Callable[[str], None]) -> None:
-        self.logger = logger
-    
-    def _log(self, message):
-        if self.logger:
-            self.logger(f'[VfsPlugin] [{self.group_id}] {message}')
-        else:
-            print(f'[VfsPlugin] [{self.group_id}] {message}')
-
-    def health_check(self) -> bool:
-        if not self.is_healthy:
-            return False
-        if self.page is None:
-            return False
-        # 检查页面是否还活着
-        try:
-            if not self.page.run_js("return 1;"):
-                return False
-        except:
-            return False
-            
-        if self.config.session_max_life > 0:
-            current_time = time.time()
-            elapsed_time = current_time - self.session_create_time
-            if elapsed_time > self.config.session_max_life * 60:
-                self._log(f"Session expired.")
-                return False
-        return True
-
-    def create_session(self) -> None:
-        """
-        使用 DrissionPage 创建会话:
-        1. 启动浏览器
-        2. 导航到登录页
-        3. 自动过盾并提取 Token (集成 CloudflareBypasser)
-        4. JS fetch 登录
-        """
-        self._log(f"Initializing Session (ID: {self.instance_id})...")
-        
-        # 0. 配置浏览器
-        co = ChromiumOptions()
-        # -------------------------------------------------------------
-        # [核心修复] 解决 'not enough values to unpack'
-        # -------------------------------------------------------------
-        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
-        # 2. 我们手动随机生成一个端口
-        import random
-        import socket
-        
-        def get_free_port():
-            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-                s.bind(('', 0))
-                return s.getsockname()[1]
-        
-        debug_port = get_free_port()
-        self._log(f"Assigned Debug Port: {debug_port}")
-        
-        # 3. 强制指定端口,DrissionPage 就会直接连接,不再解析日志
-        co.set_local_port(debug_port)
-        
-        # --- [关键配置] 设置独立的用户数据目录 ---
-        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
-        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
-        co.set_user_data_path(self.user_data_path)
-        
-        # --- 1. 指定浏览器路径 (适配 Docker) ---
-        chrome_path = os.getenv("CHROME_BIN")
-        if chrome_path and os.path.exists(chrome_path):
-            co.set_paths(browser_path=chrome_path)
-        
-        # --- [核心修改] 代理配置 ---
-        if self.config.proxy and self.config.proxy.ip:
-            p = self.config.proxy
-            
-            if p.username and p.password:
-                self._log(f"Starting Proxy Tunnel for {p.ip}...")
-                
-                # 1. 启动本地隧道
-                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
-                local_proxy = self.tunnel.start()
-                
-                self._log(f"Tunnel started at {local_proxy}")
-                
-                # 2. Chrome 连接本地免密端口
-                # 必须使用 --proxy-server 强制指定,绝对稳健
-                co.set_argument(f'--proxy-server={local_proxy}')
-                
-            else:
-                # 无密码代理,直接用
-                proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
-                co.set_argument(f'--proxy-server={proxy_str}')
-        else:
-            self._log("[WARN] No proxy configured!")
-            
-        co.headless(False) 
-        co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
-        # Docker 默认 /dev/shm 只有 64MB,Chromium 很容易爆内存崩溃
-        co.set_argument('--disable-dev-shm-usage') 
-        
-        co.set_argument('--window-size=1920,1080')
-        co.set_argument('--disable-blink-features=AutomationControlled')
-
-        try:
-            self.page = ChromiumPage(co)
-            
-            # 1. 导航到登录页面
-            mission = self.free_config.get("mission_code", "")
-            country = self.free_config.get("country_code", "")
-            lang = self.free_config.get("language", "en")
-            
-            if not mission or not country:
-                raise BizLogicError("Missing mission/country code config")
-
-            login_page_url = f"https://visa.vfsglobal.com/{country}/{lang}/{mission}/login"
-            self._log(f"Navigating to {login_page_url}...")
-            
-            self.page.get(login_page_url)
-            
-            # -------------------------------------------------------------
-            # [核心修改] 2. 智能 Cloudflare 过盾逻辑
-            # -------------------------------------------------------------
-            self._log("Handling Cloudflare challenge...")
-            
-            # 初始化过盾助手
-            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-            cf_token = ""
-            
-            # 循环检测 (40秒超时)
-            for i in range(40):
-                time.sleep(1)
-                
-                # A. 优先处理 Cookie 遮挡 (VFS 必须步骤)
-                # 如果不关掉 cookie banner,验证码可能点不到
-                self._handle_cookie_banner()
-                
-                # B. 尝试从 DOM 获取 Token (无感验证可能自动通过)
-                try:
-                    ele = self.page.ele('@name=cf-turnstile-response')
-                    if ele and ele.value:
-                        cf_token = ele.value
-                        self._log("Cloudflare Turnstile token extracted.")
-                        break
-                except:
-                    pass
-                
-                # C. 如果前 3 秒没自动出 Token,开始尝试点击
-                if i > 2:
-                    try:
-                        # 开启 DFS 深度搜索模式 (防止 Shadow DOM 嵌套太深找不到)
-                        # 在第 10 秒后开启深度搜索,前期用快速搜索
-                        use_dfs = False
-                        cf_bypasser.click_verification_button(is_dfs=use_dfs)
-                    except Exception as e:
-                        # 点击错误忽略,继续下一轮
-                        pass
-                
-                # D. 检查是否已经看到了登录框 (有时候 Token 提取慢了,但页面已经变了)
-                if self.page.ele('tag:form') or self.page.ele('#mat-input-0'):
-                    self._log("Login form detected.")
-                    # 继续尝试提取一次 Token,如果实在没有也不要死循环
-                    if i > 5 and not cf_token:
-                        self._log("Form visible but token not found yet...")
-            
-            # -------------------------------------------------------------
-            
-            if not cf_token:
-                # 最后尝试一次强取
-                try:
-                    cf_token = self.page.ele('@name=cf-turnstile-response').value
-                except:
-                    pass
-            
-            if not cf_token:
-                self._log("[WARN] Could not extract Turnstile token.")
-                raise BizLogicError(f"Could not extract Turnstile token.")
-
-            # 3. 准备登录 API 参数
-            email = self.config.account.username
-            password = self.config.account.password
-            enc_password = self._encrypt_password(password)
-            
-            client_src = self._get_client_source()
-            orange_src = self._get_orange_source(email)
-            
-            url = "https://lift-api.vfsglobal.com/user/login"
-            headers = self._get_common_headers(with_auth=False)
-            headers.update({
-                "clientsource": client_src,
-                "orangex": orange_src
-            })
-            
-            data = {
-                "username": email,
-                "password": enc_password,
-                "missioncode": mission,
-                "countrycode": country,
-                "languageCode": "en-US",
-                "captcha_version": "cloudflare-v1",
-                "captcha_api_key": cf_token 
-            }
-            
-            self._log("Sending Login Request via Browser Fetch...")
-            resp = self._perform_request("POST", url, headers=headers, json_data=data)
-            resp_json = resp.json()
-
-            # 分支 1: 登录成功
-            if resp_json.get('accessToken'):
-                self.jwt_token = resp_json["accessToken"]
-                self._log("Login successful, JWT obtained.")
-            
-            # 分支 2: OTP
-            elif resp_json.get("enableOTPAuthentication"):
-                self._log("Login requires OTP.")
-                # 注意:_submit_login_otp 内部也会调用 _refresh_turnstile_token
-                # 所以这里旧的 cf_token 其实用处不大,传过去也没事
-                otp = self._read_otp_email()
-                self._submit_login_otp(cf_token, otp)
-            
-            else:
-                raise BizLogicError(f"Login failed: {resp.text[:200]}")
-
-            self.session_create_time = time.time()
-            try:
-                self.real_ip = self._get_realnetwork_ip()
-            except:
-                self.real_ip = "0.0.0.0"
-                
-        except Exception as e:
-            self._log(f"Create Session Failed: {e}")
-            self.cleanup()
-            raise e
-
-    def query(self, apt_type: AppointmentType) -> VSQueryResult:
-        """查询可预约 Slot"""
-        result = VSQueryResult()
-        apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
-        try:
-            self._fetch_configurations(apt_config)
-            query_result = self._query_earliest_slot(apt_config)
-            result.success = False
-            result.availability_status = AvailabilityStatus.NoneAvailable
-            if query_result:
-                result.success = True
-                if "WaitList" in query_result:
-                    result.availability_status = AvailabilityStatus.Waitlist
-                else:
-                    earliest_dt = datetime.strptime(query_result, "%Y-%m-%d")
-                    result.availability_status = AvailabilityStatus.Available
-                    result.earliest_date = earliest_dt
-                    result.availability = [DateAvailability(date=earliest_dt, times=[])]
-                self._log(f"Slot Found! -> {query_result}")
-            else:
-                self._log("No slots available.")
-                
-        except Exception as e:
-            self._log(f"Query Error: {e}")
-            raise e
-            
-        return result
-
-    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None, retry_count=0):
-        """
-        核心方法:在 DrissionPage 浏览器上下文中注入 JS 执行 fetch
-        并记录详细的 Traffic 日志用于分析
-        """
-        if not self.page:
-            raise BizLogicError("Browser session not initialized")
-
-        # ---------------------------------------------------------
-        # 1. 预处理 URL (构造最终请求地址)
-        # ---------------------------------------------------------
-        req_url = url
-        if params:
-            # 确保引用了 urllib
-            import urllib.parse
-            sep = '&' if '?' in req_url else '?'
-            req_url += sep + urllib.parse.urlencode(params)
-
-        # ---------------------------------------------------------
-        # 2. 构造 Body 和 Fetch 选项
-        # ---------------------------------------------------------
-        final_headers = headers or {}
-        
-        fetch_options = {
-            "method": method.upper(),
-            "headers": final_headers,
-            "credentials": "include" # 关键:带上浏览器 Cookie
-        }
-        
-        # 用于日志记录的 Body 内容(字符串形式)
-        log_body = "None"
-
-        if json_data:
-            json_str = json.dumps(json_data)
-            fetch_options['body'] = json_str
-            fetch_options['headers']['Content-Type'] = 'application/json'
-            log_body = json_str
-        elif data:
-            if isinstance(data, dict):
-                import urllib.parse
-                encoded_data = urllib.parse.urlencode(data)
-                fetch_options['body'] = encoded_data
-                fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
-                log_body = encoded_data
-            else:
-                fetch_options['body'] = data
-                log_body = str(data)
-
-        # ---------------------------------------------------------
-        # [日志] 记录请求数据
-        # ---------------------------------------------------------
-        self._log(f"┌── [TRAFFIC REQUEST] {method} {req_url}")
-        self._log(f"├── Headers: {json.dumps(final_headers)}")
-        self._log(f"└── Body: {log_body}")
-
-        # ---------------------------------------------------------
-        # 3. 注入 JS 执行 Fetch
-        # ---------------------------------------------------------
-        js_script = f"""
-        const url = "{req_url}";
-        const options = {json.dumps(fetch_options)};
-        
-        const startTime = Date.now();
-        
-        return fetch(url, options)
-            .then(async response => {{
-                const text = await response.text();
-                const headers = {{}};
-                response.headers.forEach((value, key) => headers[key] = value);
-                const endTime = Date.now();
-                
-                return {{
-                    status: response.status,
-                    body: text,
-                    headers: headers,
-                    url: response.url,
-                    duration: endTime - startTime
-                }};
-            }})
-            .catch(error => {{
-                return {{
-                    status: 0,
-                    body: error.toString(),
-                    headers: {{}},
-                    url: url,
-                    duration: Date.now() - startTime
-                }};
-            }});
-        """
-        
-        try:
-            # run_js 直接返回 return 的对象
-            # 适当增加超时时间,防止网络慢导致 Python 侧报错
-            res_dict = self.page.run_js(js_script, timeout=60)
-        except Exception as e:
-            self._log(f"[TRAFFIC ERROR] JS Execution failed: {e}")
-            raise BizLogicError(f"Browser JS Execution Error: {e}")
-
-        resp = BrowserResponse(res_dict)
-        
-        # ---------------------------------------------------------
-        # [日志] 记录响应数据
-        # ---------------------------------------------------------
-        duration = res_dict.get('duration', 0)
-        # 截取过长的响应体,避免日志文件爆炸 (保留前 1000 字符)
-        # 如果需要完整分析,可以去掉 [:1000]
-        resp_preview = resp.text[:1000] + "..." if len(resp.text) > 1000 else resp.text
-        
-        self._log(f"┌── [TRAFFIC RESPONSE] Status: {resp.status_code} | Time: {duration}ms")
-        self._log(f"└── Body: {resp_preview}")
-
-        # ---------------------------------------------------------
-        # 4. 统一处理状态码
-        # ---------------------------------------------------------
-        if resp.status_code == 200:
-            return resp
-            
-        elif resp.status_code == 401:
-            self.is_healthy = False
-            raise SessionExpiredOrInvalidError(f"401 Unauthorized: {resp.text[:100]}")
-            
-        elif resp.status_code == 403:
-            # 检查是否是 Cloudflare 拦截
-            if "Just a moment" in resp.text or "cloudflare" in resp.text.lower():
-                self._log(f"[TRAFFIC] HTTP 403 (Cloudflare) detected. Re-verifying (Try {retry_count+1}/3)...")
-                
-                if retry_count < 3:
-                    # 调用过盾逻辑
-                    new_token = self._refresh_turnstile_token()
-                    
-                    if new_token:
-                        self._log("[TRAFFIC] In-page verification success. Retrying...")
-                        
-                        # 如果原请求包含验证码字段,更新它
-                        if json_data and "captcha_api_key" in json_data:
-                            json_data["captcha_api_key"] = new_token
-                            
-                        # 递归重试
-                        return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
-            
-            # 如果不是 CF 或者重试耗尽
-            raise PermissionDeniedError(f"HTTP 403 Forbidden: {resp.text[:100]}")
-            
-        elif resp.status_code == 429:
-            self.is_healthy = False
-            raise RateLimiteddError(f"429 Rate Limit: {resp.text[:100]}")
-            
-        elif resp.status_code == 0:
-            raise BizLogicError(f"Network Error (Fetch Failed): {resp.text}")
-            
-        else:
-            # 允许 400 业务错误通过,交给上层解析 (例如登录失败)
-            if url.endswith("/login") and resp.status_code == 400:
-                return resp
-            
-            # 其他错误视为业务逻辑异常
-            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
-
-    def _handle_cookie_banner(self):
-        """
-        处理 OneTrust Cookie 遮挡
-        策略:尝试点击“接受所有”,如果点不到就直接移除 DOM
-        """
-        try:
-            # 使用 JS 处理最快,且不会因为元素运动报错
-            js = """
-            try {
-                // 1. 尝试点击 '接受所有' 按钮
-                var acceptBtn = document.getElementById('onetrust-accept-btn-handler');
-                if (acceptBtn) {
-                    acceptBtn.click();
-                    return true;
-                }
-                
-                // 2. 如果没有按钮,或者还在遮挡,直接把整个 banner 删掉
-                var banner = document.getElementById('onetrust-banner-sdk');
-                if (banner) {
-                    banner.style.display = 'none'; // 隐藏
-                    banner.remove(); // 或者移除
-                    return true;
-                }
-            } catch(e) {}
-            return false;
-            """
-            self.page.run_js(js)
-        except:
-            pass
-
-    def _get_proxy_url(self):
-        if self.config.proxy and self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                return f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                return f"{s.scheme}://{s.ip}:{s.port}"
-        return None
-    
-    def _get_realnetwork_ip(self):
-        """
-        通过新建标签页获取 IP
-        解决 CORS 403 问题:新标签页请求属于 Top-Level Navigation,
-        不带 Origin: visa.vfsglobal.com,也不带 credentials,符合 ipify 规则。
-        """
-        try:
-            # 1. 新建一个标签页 (后台静默打开)
-            tab = self.page.new_tab("https://api.ipify.org/?format=json")
-            
-            # 2. 获取页面内容 (DrissionPage 会自动等待页面加载)
-            # ipify 返回的是纯 JSON 文本,通常在 body 或 pre 标签里
-            if tab.ele('tag:pre'):
-                json_text = tab.ele('tag:pre').text
-            else:
-                json_text = tab.ele('tag:body').text
-            
-            # 3. 提取 IP
-            ip = json.loads(json_text)['ip']
-            
-            # 4. 务必关闭标签页,释放资源
-            tab.close()
-            
-            self._log(f"Real Network IP: {ip}")
-            return ip
-            
-        except Exception as e:
-            self._log(f"[WARN] Failed to check IP via new tab: {e}")
-            # 尝试清理可能没关掉的标签页
-            try:
-                if self.page.tabs_count > 1:
-                    tab.close()
-            except:
-                pass
-            return "0.0.0.0"
-
-    def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
-        # DrissionPage 浏览器会自动带上 Origin, Referer, User-Agent, Sec-CH-UA 等
-        # 这里只需要补充业务特定的 Headers
-        mission = self.free_config.get("mission_code", "")
-        country = self.free_config.get("country_code", "")
-        lang = self.free_config.get("language", "en")
-        route = f"{country}/{lang}/{mission}"
-        
-        h = {
-            "accept": "application/json, text/plain, */*",
-            # "origin": ... 浏览器自动处理
-            # "referer": ... 浏览器自动处理
-            "route": route
-        }
-        
-        # 即使是浏览器环境,VFS 也需要这两个加密参数
-        # 注意:这里可能需要从 JS 获取,或者保持 Python 生成
-        # 如果 Python 生成的总是报错,可以考虑把加密逻辑移到 JS 里跑
-        h["clientsource"] = self._get_client_source()
-        
-        if with_auth and self.jwt_token:
-            h["authorize"] = self.jwt_token
-            
-        return h
-
-    def _encrypt_password(self, password: str) -> str:
-        ciphertext = self.public_key.encrypt(
-            password.encode(),
-            padding.OAEP(
-                mgf=padding.MGF1(algorithm=hashes.SHA256()),
-                algorithm=hashes.SHA256(),
-                label=None
-            )
-        )
-        return base64.b64encode(ciphertext).decode()
-
-    def _get_orange_source(self, email: str) -> str:
-        timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
-        payload = f"{email};{timestamp}"
-        return self._encrypt_password(payload)
-
-    def _get_client_source(self) -> str:
-        timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
-        payload = f"GA;{timestamp}Z"
-        return self._encrypt_password(payload)
-
-    def _query_earliest_slot(self, apt_config) -> Optional[str]:
-        url = "https://lift-api.vfsglobal.com/appointment/CheckIsSlotAvailable"
-        data = {
-            "missioncode": self.free_config.get("mission_code"),
-            "countrycode": self.free_config.get("country_code"),
-            "vacCode": apt_config.get("vac_code"),
-            "visaCategoryCode": apt_config.get("subcategory_code"),
-            "roleName": "Individual",
-            "loginUser": self.config.account.username,
-            "payCode": ""
-        }
-        headers = self._get_common_headers(with_auth=True)
-        # fetch 不需要显式 content-type application/json,json_data会自动处理
-        
-        # DrissionPage 不需要手动处理 403 绕盾,因为浏览器本身就在盾后面
-        resp = self._perform_request("POST", url, headers=headers, json_data=data, retry_count=2)
-        
-        if "WaitList" in resp.text:
-            return "WaitList"
-
-        j = resp.json()
-        if j.get("earliestSlotLists"):
-            raw_date = j["earliestSlotLists"][0]["date"]
-            return to_yyyymmdd(raw_date, "%m/%d/%Y %H:%M:%S")
-        return ""
-
-    def _fetch_configurations(self, apt_config: Dict[str, Any]):
-        if not self.center_conf:
-            self.center_conf = self._query_center()
-
-        vac_code = apt_config.get("vac_code")
-        category_code = apt_config.get("category_code")
-        
-        if category_code not in self.category_conf:
-            visa_categories = self._query_visa_category(vac_code)
-            found = False
-            for vc in visa_categories:
-                if vc.get("code") == category_code:
-                    self.category_conf[category_code] = vc
-                    found = True
-                    break
-            if not found:
-                self._log(f"WARN: Category {category_code} not found")
-
-        sub_category_code = apt_config.get("subcategory_code")
-        if sub_category_code not in self.subcategory_conf:
-            visa_subcategories = self._query_visa_sub_category(vac_code, category_code)
-            found = False
-            for svc in visa_subcategories:
-                if svc.get("code") == sub_category_code:
-                    self.subcategory_conf[sub_category_code] = svc
-                    found = True
-                    break
-            if not found:
-                self._log(f"WARN: SubCategory {sub_category_code} not found")
-
-    def _query_center(self) -> List:
-        mission = self.free_config.get("mission_code")
-        country = self.free_config.get("country_code")
-        url = f"https://lift-api.vfsglobal.com/master/center/{mission}/{country}/en-US"
-        headers = self._get_common_headers(with_auth=False)
-        resp = self._perform_request("GET", url, headers=headers)
-        return resp.json()
-   
-    def _query_visa_category(self, center_code: str) -> List:
-        mission = self.free_config.get("mission_code")
-        country = self.free_config.get("country_code")
-        enc_center = urllib.parse.quote(center_code)
-        url = f"https://lift-api.vfsglobal.com/master/visacategory/{mission}/{country}/{enc_center}/en-US"
-        headers = self._get_common_headers(with_auth=False)
-        resp = self._perform_request("GET", url, headers=headers)
-        return resp.json()
- 
-    def _query_visa_sub_category(self, center_code: str, category_code: str) -> List:
-        mission = self.free_config.get("mission_code")
-        country = self.free_config.get("country_code")
-        enc_center = urllib.parse.quote(center_code)
-        enc_cat = urllib.parse.quote(category_code)
-        url = f"https://lift-api.vfsglobal.com/master/subvisacategory/{mission}/{country}/{enc_center}/{enc_cat}/en-US"
-        headers = self._get_common_headers(with_auth=False)
-        resp = self._perform_request("GET", url, headers=headers)
-        return resp.json()            
-
-    def _read_otp_email(self) -> str:
-        # 保持原样,这部分使用云API读取邮件,不依赖本地网络库
-        master_email = "visafly666@gmail.com"
-        recipient = self.config.account.username
-        sender = "donotreply at vfshelpline.com"
-        subject_keywords = "One Time Password"
-        body_keywords = "OTP"
-        now_utc = datetime.utcnow()
-        formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
-        self._log(f"Waiting for OTP email...")
-        for i in range(12):
-            content_out = VSCloudApi.Instance().fetch_mail_content(
-                master_email, sender, recipient, subject_keywords, body_keywords, formatted_utc_time, 300
-            )
-            if content_out:
-                match = re.search(r'\b\d{6}\b', content_out)
-                if match:
-                    return match.group(0)
-            time.sleep(5)
-        raise NotFoundError(message="OTP email not found")
-
-    def _submit_login_otp(self, old_cf_token: str, otp: str):
-        self._log("Submitting Login OTP...")
-        
-        # --- [新增] 必须刷新 Token ---
-        # 旧的 old_cf_token 已经在第一步登录时失效了
-        new_cf_token = self._refresh_turnstile_token()
-        # ---------------------------
-
-        email = self.config.account.username
-        password = self.config.account.password
-        enc_password = self._encrypt_password(password)
-        mission = self.free_config.get("mission_code", "")
-        country = self.free_config.get("country_code", "")
-        
-        client_src = self._get_client_source()
-        orange_src = self._get_orange_source(email)
-        
-        url = "https://lift-api.vfsglobal.com/user/login"
-        headers = self._get_common_headers(with_auth=False)
-        headers.update({
-            "clientsource": client_src,
-            "orangex": orange_src
-        })
-        
-        data = {
-            "username": email,
-            "password": enc_password,
-            "missioncode": mission,
-            "countrycode": country,
-            "languageCode": "en-US",
-            "captcha_version": "cloudflare-v1",
-            "captcha_api_key": new_cf_token,
-            "otp": otp 
-        }
-        
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        resp_json = resp.json()
-        
-        if resp_json.get("accessToken"):
-            self.jwt_token = resp_json["accessToken"]
-            self._log("OTP Login successful.")
-            return
-
-        # 增加错误详情日志
-        error_desc = resp_json.get("description", resp.text)
-        raise PermissionDeniedError(message=f"OTP Login Failed: {error_desc}")
-    
-    def _refresh_turnstile_token(self) -> str:
-        """
-        强制刷新 Cloudflare Turnstile 并获取新 Token (集成 CloudflareBypasser 版)
-        """
-        self._log("Refreshing Cloudflare Turnstile token...")
-        
-        # 1. JS 强制重置 (保持不变)
-        js_reset = """
-        try {
-            var input = document.querySelector('input[name="cf-turnstile-response"]');
-            if (input) input.value = "";
-            window.turnstile.reset(); 
-        } catch(e) {
-            console.log("Turnstile reset error:", e);
-        }
-        """
-        self.page.run_js(js_reset)
-        
-        # 2. 初始化过盾助手
-        # 假设 CloudflareBypasser 类已在当前文件中定义
-        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-        
-        # 3. 轮询等待 (30秒)
-        for i in range(60): 
-            time.sleep(0.5)
-            
-            # A. 检查 Token 是否已生成
-            # 使用 DrissionPage 的方式获取 value 比较稳定
-            try:
-                ele = self.page.ele('@name=cf-turnstile-response')
-                if ele and ele.value:
-                    self._log("Turnstile token refreshed successfully.")
-                    return ele.value
-            except:
-                pass
-            
-            # B. 尝试点击验证框
-            # 策略:前2秒等待,之后开始尝试点击
-            if i > 4: 
-                # [重要] VFS 经常有 Cookie 弹窗遮挡,先尝试清理一下
-                self._handle_cookie_banner()
-                
-                try:
-                    # 使用 CloudflareBypasser 的高级点击逻辑
-                    # is_dfs=True 表示如果普通搜索找不到,就递归搜索 iframe (更耗时但更强)
-                    # 我们在尝试 10 次 (5秒) 后开启 DFS 模式
-                    use_dfs = (i > 14)
-                    
-                    cf_bypasser.click_verification_button(is_dfs=use_dfs)
-                except Exception as e:
-                    # 点击过程报错不要中断主循环
-                    pass
-        
-        raise BizLogicError("Failed to refresh Cloudflare Turnstile token (Timeout)")
-
-    def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
-        """
-        执行完整的预约流程
-        """
-        self._log("Starting booking process...")
-        
-        # 1. 准备数据
-        user_email = user_inputs.get('email')
-        # 生成别名邮箱 (防止邮箱被 VFS 黑名单)
-        user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
-        
-        res = VSBookResult()
-        app_type = slot_info.apt_type
-        # 如果没有 earliest_date,默认从今天开始
-        from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
-        
-        apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
-        
-        if not apt_config:
-            raise NotFoundError(message="Book: Config missing for this routing key.")
-
-        # 确保配置已加载 (SubCategory 等)
-        self._fetch_configurations(apt_config)
-
-        sub_cc = apt_config.get("subcategory_code")
-        sub_conf = self.subcategory_conf.get(sub_cc, {})
-
-        # 3. OCR 识别 / 文档上传 (如果需要)
-        # 上传结果存入 user_inputs 供后续使用
-        ocr_enabled = sub_conf.get("isOCREnable", False)
-        if ocr_enabled:
-            self._log("OCR Enabled, uploading documents...")
-            upload_res = self._upload_applicant_documents(apt_config, user_inputs)
-            user_inputs["applicant_image"] = upload_res.get("passportImageFilename")
-            user_inputs["applicant_image_data"] = upload_res.get("passportImageFileBytes")
-            user_inputs["guid"] = upload_res.get("uploadDocumentGUID")
-
-        enable_reference_number = sub_conf.get("enableReferenceNumber", False)
-
-        # 4. 添加申请人 (核心步骤 1)
-        final_urn = None
-        is_waitlist = (slot_info.availability_status == AvailabilityStatus.Waitlist)
-        
-        if not self.booking_wait_applied:
-            self._log("pre-booking wait: sleeping 20s before booking to avoid risk control")
-            time.sleep(20)
-            self.booking_wait_applied = True
-        
-        # 重试机制:添加申请人有时候会因为并发冲突失败
-        MAX_RETRY = 3
-        for i in range(MAX_RETRY):
-            try:
-                final_urn = self._add_primary_applicant(apt_config, user_inputs, is_waitlist, ocr_enabled, enable_reference_number)
-                if final_urn:
-                    break
-            except Exception as e:
-                self._log(f"Add Applicant retry {i+1}/{MAX_RETRY}: {e}")
-                time.sleep(2)
-        
-        if not final_urn:
-            raise BizLogicError(message="Failed to add primary applicant (Slot likely taken or API error)")
-
-        self._log(f"Applicant Added. URN: {final_urn}")
-
-        # 5. 申请人 OTP 验证 (核心步骤 2 - 视配置而定)
-        otp_enabled = sub_conf.get("isApplicantOTPEnabled", False)
-        if otp_enabled:
-            self._log("Applicant OTP Required.")
-            if not self._applicant_otp_send(apt_config, final_urn):
-                raise BizLogicError(message='Applicant OTP send failed')
-            
-            # 复用之前的读邮件逻辑
-            otp_code = self._read_otp_email()
-            if not self._applicant_otp_verify(apt_config, final_urn, otp_code):
-                raise BizLogicError(message='Applicant OTP verify failed')
-
-        # 6. Waitlist 模式直接返回
-        if is_waitlist:
-            if self._confirm_waitlist(apt_config, final_urn):
-                res.success = True
-                res.urn = final_urn
-                res.account = self.config.account.username
-                self._log("Waitlist confirmed.")
-                return res
-            raise BizLogicError(message='Confirm waitlist failed')
-
-        # 7. 寻找具体的时间槽 (核心步骤 3)
-        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})")
-        
-        selected_slot_id = ""
-        selected_slot_date = ""
-        selected_slot_time_range = ""
-        
-        all_ads = set()
-        forbidden_dates = set()
-        found_slot = False
-        
-        for m_str in months:
-            self._log(f"Checking calendar for {m_str}...")
-            # 查询日历
-            ads = self._query_slot_calendar(apt_config, final_urn, m_str)
-            
-            # 去重
-            new_ads = [d for d in ads if d not in all_ads]
-            all_ads.update(new_ads)
-            
-            # 尝试选中一个日期
-            # 这里做一个简单循环,如果选中日期没时间了,就换一个日期
-            for _ in range(3):
-                avail_candidates = [d for d in list(all_ads) if d not in forbidden_dates]
-                # 根据用户期望过滤
-                sel_dates = self._filter_dates(avail_candidates, expected_start, expected_end)
-                
-                if not sel_dates:
-                    break # 当前月没有符合要求的日期,去下一个月
-                
-                tmp_date = sel_dates[0] # 取第一个(通常 _filter_dates 里已经 shuffle 过了)
-                forbidden_dates.add(tmp_date) # 标记为已尝试
-                
-                # 关键:Audit Log (锁定日期)
-                # VFS 要求在查 timeslot 之前必须先发这个请求
-                if not self._saveuseractionaudit(apt_config, final_urn, tmp_date):
-                    self._log(f"Audit failed for {tmp_date}, skipping...")
-                    time.sleep(1)
-                    continue
-                
-                # 查询具体时间
-                ats = self._query_slot_time(apt_config, final_urn, tmp_date)
-                if not ats:
-                    self._log(f"No timeslots for {tmp_date}")
-                    continue
-                
-                # 随机选一个时间
-                sel_tm = random.choice(ats)
-                
-                selected_slot_id = sel_tm.get("allocationId")
-                selected_slot_date = tmp_date
-                selected_slot_time_range = sel_tm.get("slot")
-                
-                found_slot = True
-                break
-            
-            if found_slot:
-                break
-                
-        if not found_slot:
-            self._log("No valid slots found after scanning.") 
-            res.success = False
-            return res
-
-        self._log(f"Slot Selected: {selected_slot_date} {selected_slot_time_range} (ID: {selected_slot_id})")
-
-        # 8. 服务与费用 (核心步骤 4)
-        self._submit_no_addition_service(final_urn)
-        amount, currency = self._query_fee(apt_config, final_urn)
-        
-        # 9. 最终提交
-        self._log("Submitting schedule...")
-        schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
-        
-        if not schedule_res.get("IsAppointmentBooked"):
-            self._log(f"Booking failed: {schedule_res}") 
-            res.success = False
-            return res
-             
-        # 10. 构造成功结果
-        res.success = True
-        res.account = self.config.account.username
-        res.book_date = selected_slot_date
-        res.book_time = selected_slot_time_range
-        res.urn = final_urn
-        res.fee_amount = int(amount * 100)
-        res.fee_currency = currency
-        
-        # 11. 处理支付链接
-        if schedule_res.get("IsPaymentRequired", False):
-            payload = schedule_res.get("payLoad", "")
-            if payload:
-                self._log("Processing payment link...")
-                payment_url = self._pay_request(payload)
-                if payment_url:
-                    res.payment_link = payment_url
-                    
-        return res
-
-    # -------------------------------------------------------------
-    # 辅助方法实现 (DrissionPage 适配版)
-    # -------------------------------------------------------------
-
-    def _upload_applicant_documents(self, apt_config, user_inputs) -> Dict:
-        """上传图片:先下载外部图片,再通过浏览器上传到 VFS"""
-        import requests as standard_requests # 使用标准库下载外部资源
-        
-        url = "https://lift-api.vfsglobal.com/appointment/UploadApplicantDocument"
-        passport_url = user_inputs.get("passport_image_url")
-        if not passport_url:
-            raise NotFoundError(message="Missing passport_image_url")
-
-        # 下载图片 (不走代理或走系统代理,不使用 DrissionPage,因为是外部链接)
-        try:
-            img_resp = standard_requests.get(passport_url, timeout=30)
-            if img_resp.status_code != 200:
-                raise BizLogicError(message=f"Failed to download passport image: {img_resp.status_code}")
-            b64_str = base64.b64encode(img_resp.content).decode('utf-8')
-        except Exception as e:
-            raise BizLogicError(message=f"Image download error: {e}")
-  
-        headers = self._get_common_headers(with_auth=True)
-        # DrissionPage fetch 不需要显式 content-type application/json,json_data会自动处理
-        
-        data = {
-            "missioncode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "languageCode": "en-US",
-            "visaCategoryCode": apt_config.get("subcategory_code"),
-            "fileBytes": b64_str,
-            "selfiImageFileBytes": ""
-        }
-        
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        result = resp.json()
-        
-        # 补充返回数据供后续使用
-        result["passportImageFilename"] = "passport_img.jpg"
-        result["passportImageFileBytes"] = b64_str
-        return result
-   
-    def _add_primary_applicant(self, apt_config: Dict[str, Any], user_inputs: Dict[str, Any], 
-                             is_waitlist: bool, ocr_enabled: bool, enable_ref: bool) -> str:
-        """构造申请人 payload 并提交"""
-        url = "https://lift-api.vfsglobal.com/appointment/applicants"
-        headers = self._get_common_headers(with_auth=True)
-
-        gender_str = str(user_inputs.get("gender", "")).lower()
-        gender_code = 1 if gender_str == "male" else 2
-
-        raw_dial = user_inputs.get("phone_country_code", "86")
-        dial_code = str(raw_dial)
-
-        # 日期格式转换 YYYY-MM-DD -> DD/MM/YYYY
-        def _to_ddmmyyyy(d_str):
-            try:
-                return datetime.strptime(str(d_str), "%Y-%m-%d").strftime("%d/%m/%Y")
-            except:
-                return str(d_str)
-
-        dob = _to_ddmmyyyy(user_inputs.get("birthday", ""))
-        ppt_exp = _to_ddmmyyyy(user_inputs.get("passport_expiry_date", ""))
-
-        applicant = {
-            "urn": "",
-            "arn": "",
-            "loginUser": self.config.account.username,
-            "firstName": str(user_inputs.get("first_name", "")).upper(),
-            "middleName": "",
-            "lastName": str(user_inputs.get("last_name", "")).upper(),
-            "employerFirstName": "",
-            "employerLastName": "",
-            "salutation": "",
-            "Subclasscode": None,
-            "VisaToken": None,
-            "centerClassCode": None,
-            "dateOfApplication": None,
-            "selectedSubvisaCategory": None,
-            "gender": gender_code,
-            "contactNumber": str(user_inputs.get("phone", "")).lstrip("0"),
-            "dialCode": dial_code,
-            "employerContactNumber": "",
-            "employerDialCode": "",
-            "emailId": str(user_inputs.get("alias_email", "")).upper(),
-            "employerEmailId": "",
-            "passportNumber": str(user_inputs.get("passport_no", "")).upper(),
-            "confirmPassportNumber": "",
-            "passportExpirtyDate": ppt_exp,
-            "dateOfBirth": dob,
-            "nationalId": None,
-            "nationalityCode": get_country_iso3(str(user_inputs.get("nationality", ""))),
-            "state": user_inputs.get("state"),
-            "city": user_inputs.get("city"),
-            "addressline1": user_inputs.get("addressline1"),
-            "addressline2": user_inputs.get("addressline2"),
-            "pincode": None,
-            "isEndorsedChild": False,
-            "applicantType": 0,
-            "vlnNumber": None,
-            "applicantGroupId": 0,
-            "parentPassportNumber": "",
-            "parentPassportExpiry": "",
-            "dateOfDeparture": None,
-            "entryType": "",
-            "eoiVisaType": "",
-            "passportType": "",
-            "vfsReferenceNumber": "",
-            "familyReunificationCerificateNumber": "",
-            "PVRequestRefNumber": "",
-            "PVStatus": "",
-            "PVStatusDescription": "",
-            "PVCanAllowRetry": True,
-            "PVisVerified": False,
-            "eefRegistrationNumber": "",
-            "isAutoRefresh": True,
-            "helloVerifyNumber": "",
-            "OfflineCClink": "",
-            "idenfystatuscheck": False,
-            "vafStatus": None,
-            "SpecialAssistance": "",
-            "AdditionalRefNo": None,
-            "juridictionCode": "",
-            "canInitiateVAF": False,
-            "canEditVAF": False,
-            "canDeleteVAF": False,
-            "canDownloadVAF": False,
-            "Retryleft": "",
-            # 这里的 IP 应该已经在 create_session 时获取到了
-            "ipAddress": self.real_ip
-        }
-
-        if enable_ref:
-            applicant["referenceNumber"] = str(user_inputs.get("cover_letter_id", ""))
-        else:
-            applicant["referenceNumber"] = None
-
-        if ocr_enabled:
-            applicant["applicantImage"] = str(user_inputs.get("applicant_image", ""))
-            applicant["applicantImageData"] = str(user_inputs.get("applicant_image_data", ""))
-            applicant["GUID"] = str(user_inputs.get("guid", ""))
-
-        payload = {
-            "countryCode": self.free_config.get("country_code"),
-            "missionCode": self.free_config.get("mission_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategory_code"),
-            "applicantList": [applicant],
-            "languageCode": "en-US",
-            "isWaitlist": is_waitlist,
-            "isEdit": False,
-            "feeEntryTypeCode": None, "feeExemptionTypeCode": None, 
-            "feeExemptionDetailsCode": None, "juridictionCode": None, "regionCode": None
-        }
-
-        resp = self._perform_request("POST", url, headers=headers, json_data=payload)
-        return resp.json().get("urn")
-    
-    def _applicant_otp_send(self, apt_config, urn) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
-        headers = self._get_common_headers(with_auth=True)
-        data = {
-            "urn": urn,
-            "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "OTP": "",
-            "otpAction": "GENERATE",
-            "languageCode": "en-US"
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json().get("isOTPGenerated", False)
-
-    def _applicant_otp_verify(self, apt_config, urn, otp) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
-        headers = self._get_common_headers(with_auth=True)
-        # VFS 这里的 header 有时需要 datacenter,原代码有就加上
-        headers["datacenter"] = "GERMANY" 
-        data = {
-            "urn": urn,
-            "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "OTP": otp,
-            "otpAction": "VALIDATE",
-            "languageCode": "en-US"
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json().get("isOTPValidated", False)
-        
-    def _query_slot_calendar(self, apt_config, urn, from_date) -> List:
-        url = "https://lift-api.vfsglobal.com/appointment/calendar"
-        headers = self._get_common_headers(with_auth=True)
-        
-        # 将 YYYY-MM-DD 转为 DD/MM/YYYY 用于 API
-        dt_m = datetime.strptime(from_date, "%Y-%m-%d")
-        converted_date = dt_m.strftime("%d/%m/%Y")
-        
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategory_code"),
-            "fromDate": converted_date,
-            "urn": urn,
-            "payCode": ""
-        }
-        
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        calendars = resp.json().get("calendars")
-        ads_out = []
-        if calendars:
-            for item in calendars:
-                # API 返回可能是 MM/DD/YYYY 或 DD/MM/YYYY,VFS 比较乱
-                # 通常是 MM/DD/YYYY
-                raw = item.get("date")
-                ads_out.append(to_yyyymmdd(raw, "%m/%d/%Y"))
-        return ads_out
-     
-    def _query_slot_time(self, apt_config, urn, slot_date) -> List:
-        url = "https://lift-api.vfsglobal.com/appointment/timeslot"
-        headers = self._get_common_headers(with_auth=True)
-        
-        dt_m = datetime.strptime(slot_date, "%Y-%m-%d")
-        converted_date = dt_m.strftime("%d/%m/%Y")
-        
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "visaCategoryCode": apt_config.get("subcategory_code"),
-            "slotDate": converted_date,
-            "urn": urn
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json().get("slots", [])
-
-    def _saveuseractionaudit(self, apt_config, urn, earliest_date) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/saveuseractionaudit"
-        headers = self._get_common_headers(with_auth=True)
-        
-        dt = datetime.strptime(earliest_date, "%Y-%m-%d")
-
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "urn": urn,
-            "firstEarliestSlotDate": dt.strftime("%d/%m/%Y"),
-            "action": "schedule",
-            "ipAddress": self.real_ip,
-            "eadAppointmentDetail": dt.strftime("%Y-%m-%dT%H:%M:%S")
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json().get("isSavedSuccess", False)
-        
-    def _submit_no_addition_service(self, urn):
-        url = "https://lift-api.vfsglobal.com/vas/mapvas"
-        headers = self._get_common_headers(with_auth=True)
-        data = {
-            "loginUser": self.config.account.username,
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "urn": urn,
-            "applicants": []
-        }
-        # 只要不报错即可
-        self._perform_request("POST", url, headers=headers, json_data=data)
-
-    def _query_fee(self, apt_config, urn) -> Tuple[float, str]:
-        url = "https://lift-api.vfsglobal.com/appointment/fees"
-        headers = self._get_common_headers(with_auth=True)
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "urn": urn,
-            "languageCode": "en-US"
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        j = resp.json()
-        total = j.get("totalamount", 0.0)
-        currency = "EUR"
-        if j.get("feeDetails"):
-            currency = j["feeDetails"][0].get("currency", "EUR")
-        return total, currency
-
-    def _schedule(self, apt_config, urn, amount, currency, slot_id) -> Dict:
-        url = "https://lift-api.vfsglobal.com/appointment/schedule"
-        headers = self._get_common_headers(with_auth=True)
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "urn": urn,
-            "notificationType": "none",
-            "paymentdetails": {
-                "paymentmode": "Online",
-                "RequestRefNo": "",
-                "clientId": "",
-                "merchantId": "",
-                "amount":  amount,
-                "currency": currency
-            },
-            "allocationId": str(slot_id),
-            "CanVFSReachoutToApplicant": True
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json()
-
-    def _pay_request(self, payload) -> str:
-        """
-        解析支付重定向 URL (DrissionPage 新标签页版)
-        """
-        # 初始 URL,通常是一个 Redirect 接口
-        start_url = f"https://online.vfsglobal.com/PG-Component/Payment/PayRequest?payLoad={payload}"
-        final_url = ""
-        
-        try:
-            self._log("Resolving payment redirect...")
-            # 使用新标签页去跑,以免当前会话状态丢失
-            pay_tab = self.page.new_tab(start_url)
-            
-            # 等待跳转完成 (通常会跳到 Stripe, WorldPay 或其他支付网关)
-            # 等待直到 URL 不再是 PayRequest
-            pay_tab.wait.url_change(start_url, timeout=15)
-            
-            final_url = pay_tab.url
-            self._log(f"Payment URL resolved: {final_url}")
-            
-            # 关闭标签页
-            pay_tab.close()
-            
-        except Exception as e:
-            self._log(f"[WARN] Failed to resolve payment URL: {e}")
-            try:
-                pay_tab.close()
-            except:
-                pass
-                
-        return final_url
-
-    def _confirm_waitlist(self, apt_config: Dict[str, Any], urn: str) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/ConfirmWaitlist"
-        headers = self._get_common_headers(with_auth=True)
-        data = {
-            "missionCode": self.free_config.get("mission_code"),
-            "countryCode": self.free_config.get("country_code"),
-            "centerCode": apt_config.get("vac_code"),
-            "loginUser": self.config.account.username,
-            "urn": urn,
-            "notificationType": "none",
-            "CanVFSReachoutToApplicant": True
-        }
-        resp = self._perform_request("POST", url, headers=headers, json_data=data)
-        return resp.json().get("isConfirmed", False)
-
-    def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
-        if not start_str or not end_str:
-            return dates
-        valid_dates = []
-        try:
-            s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
-            e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
-            for date_str in dates:
-                curr_date = datetime.strptime(date_str, "%Y-%m-%d")
-                if s_date <= curr_date <= e_date:
-                    valid_dates.append(date_str)
-            random.shuffle(valid_dates)
-            return valid_dates
-        except:
-            return dates
-
-    def _get_filtered_covered_months(self, start_date, end_date, from_date) -> List[str]:
-        fmt = "%Y-%m-%d"
-        try:
-            dt_start = datetime.strptime(start_date, fmt) if start_date else datetime.now()
-            dt_end = datetime.strptime(end_date, fmt) if end_date else datetime.now().replace(year=datetime.now().year + 1)
-            try:
-                dt_from = datetime.strptime(from_date, fmt)
-            except:
-                dt_from = datetime.now()
-        except:
-            return []
-
-        dt_start = dt_start.replace(day=1)
-        dt_end = dt_end.replace(day=1)
-        dt_from = dt_from.replace(day=1)
-        curr = max(dt_start, dt_from)
-        
-        months = []
-        while curr <= dt_end:
-            months.append(curr.strftime(fmt))
-            if curr.month == 12:
-                curr = curr.replace(year=curr.year + 1, month=1)
-            else:
-                curr = curr.replace(month=curr.month + 1)
-        return months
-    
-    # --- 资源清理核心方法 ---
-    def cleanup(self):
-        """
-        销毁浏览器并彻底删除临时文件
-        """
-        # 1. 关闭浏览器
-        if self.page:
-            try:
-                self.page.quit() # 这会关闭 Chrome 进程
-            except Exception:
-                pass # 忽略已关闭的错误
-            self.page = None
-        
-        # 2. 删除文件
-        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
-        if os.path.exists(self.root_workspace):
-            for _ in range(3):
-                try:
-                    time.sleep(0.2)
-                    shutil.rmtree(self.root_workspace, ignore_errors=True)
-                    break
-                except Exception as e:
-                    # 如果删除失败(通常是Windows文件占用),重试
-                    self._log(f"Cleanup retry: {e}")
-                    time.sleep(0.5)
-            
-            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
-            if os.path.exists(self.root_workspace):
-                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
-                 
-        # 3. [新增] 关闭代理隧道
-        if self.tunnel:
-            try: self.tunnel.stop()
-            except: pass
-            self.tunnel = None
-                 
-    def __del__(self):
-        """
-        析构函数:当对象被垃圾回收时自动调用
-        """
-        self.cleanup()

+ 172 - 0
sentinel.py

@@ -0,0 +1,172 @@
+import os
+import time
+import json
+import random
+import threading
+import redis
+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.proxy_manager import ProxyManager
+
+class SentinelGCO:
+    def __init__(self, cfg: GroupConfig, redis_conf: Dict, logger: Callable[[str], None] = None):
+        self.m_cfg = cfg
+        self.m_factory = VSPlgFactory()
+        self.m_logger = logger
+        self.m_tasks: List[Task] = []
+        self.m_lock = threading.RLock()
+        self.m_stop_event = threading.Event()
+        
+        self.redis_client = redis.Redis(**redis_conf)
+        self.m_pending_builtin = 0
+
+    def _log(self, message):
+        if self.m_logger:
+            self.m_logger(f'[SENTINEL] [{self.m_cfg.identifier}] {message}')
+
+    def start(self):
+        if not self.m_cfg.enable:
+            return
+        self._log("Starting Sentinel...")
+        plugin_name = self.m_cfg.plugin_config.plugin_name
+        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()
+
+    def stop(self):
+        self._log("Stopping Sentinel...")
+        self.m_stop_event.set()
+
+    def _get_redis_key(self, routing_key: str) -> str:
+        return f"vs:signal:{routing_key}"
+
+    def _monitor_loop(self):
+        self._log("Monitor loop started.")
+        rng = random.Random()
+        
+        while not self.m_stop_event.is_set():
+            try:
+                time.sleep(0.5)
+                now = time.time()
+                
+                with self.m_lock:
+                    active_tasks = [t for t in self.m_tasks if t.instance.health_check()]
+                    self.m_tasks = active_tasks
+                
+                for task in active_tasks:
+                    if now < task.next_run:
+                        continue
+                    
+                    apt_types = self.m_cfg.appointment_types
+                    if not apt_types:
+                        continue
+                    weights = [float(item.weight) for item in apt_types]
+                    apt_type = random.choices(apt_types, weights=weights, k=1)[0]
+                    
+                    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
+                        VSCloudApi.Instance().slot_refresh_success(apt_type.routing_key)
+
+                        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": now
+                            }
+                            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"])
+                            
+                        interval = 30
+                        mode = task.qw_cfg.mode
+                        if mode == QueryWaitMode.Loop:
+                            interval = 1
+                        elif mode == QueryWaitMode.Fixed:
+                            interval = task.qw_cfg.fixed_wait
+                        elif mode == QueryWaitMode.Random:
+                            interval = rng.randint(task.qw_cfg.random_min, task.qw_cfg.random_max)
+                        task.next_run = time.time() + interval
+
+                    except Exception as e:
+                        self._log(f"Query exception: {e}")
+                        VSCloudApi.Instance().slot_refresh_fail(apt_type.routing_key, error=str(e))
+                        task.next_run = time.time() + 10
+            except Exception as e:
+                self._log(f"Monitor loop error: {e}")
+                time.sleep(2)
+
+    def _creator_loop(self):
+        self._log("Creator loop started.")
+        while not self.m_stop_event.is_set():
+            time.sleep(2)
+            with self.m_lock:
+                current = len(self.m_tasks)
+                pending = self.m_pending_builtin
+            
+            if (current + pending) < self.m_cfg.sentinel.target_instances:
+                self._spawn_sentinel_worker()
+
+    def _spawn_sentinel_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
+                
+                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 * 60)
+                    if not acc: return
+                    plg_cfg.account.id = acc['id']
+                    plg_cfg.account.username = acc['username']
+                    plg_cfg.account.password = acc['password']
+                
+                if self.m_cfg.need_proxy:
+                    proxy = ProxyManager.Instance().next(self.m_cfg.proxy_pool, lock_duration=self.m_cfg.proxy_lock_interval)
+                    if not proxy:
+                        return
+                    plg_cfg.proxy.id = proxy['id']
+                    plg_cfg.proxy.ip = proxy['ip']
+                    plg_cfg.proxy.port = proxy['port']
+                    plg_cfg.proxy.scheme = proxy['scheme']
+                    plg_cfg.proxy.username = proxy['username']
+                    plg_cfg.proxy.password = proxy['password']
+
+                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()))
+                self._log(f"+++ Sentinel spawned: {plg_cfg.account.username}")
+
+            except Exception as e:
+                err_str = str(e)
+                if "40401" in err_str or "Account not found" in err_str:
+                    return
+                self._log(f"Sentinel spawn failed: {e}")
+            finally:
+                with self.m_lock:
+                    self.m_pending_builtin = max(0, self.m_pending_builtin - 1)
+
+        ThreadPool.getInstance().enqueue(_job)

+ 5 - 0
toolkit/vs_cloud_api.py

@@ -75,6 +75,11 @@ class VSCloudApi:
             return result.get("data", {})
         else:
             raise BizLogicError(message=f"Get vas task pop biz error: {result.get('message')}")
+    
+    def get_vas_task(self, task_id: int) -> dict:
+        # 例如:请求 GET /api/v1/tasks/{task_id}
+        # 返回包含 user_inputs 的完整字典数据
+        pass
 
     def update_vas_task(self, 
                         task_id: int, 

+ 0 - 111
upload_accounts.py

@@ -1,111 +0,0 @@
-import json
-import requests
-import os
-import time
-
-# --- 配置部分 ---
-# 服务器地址 (根据你的实际情况修改)
-SERVER_URL = "http://45.137.220.138:8888"
-# API 端点
-API_ENDPOINT = f"{SERVER_URL}/api/account/add"
-# 本地账号文件路径
-ACCOUNT_FILE = "registered_accounts.json"
-
-def post_account(pool_name, username, password, extra_data):
-    """发送单个账号数据到服务器"""
-    payload = {
-        "pool_name": pool_name,
-        "username": username,
-        "password": password,
-        "extra_data": extra_data
-    }
-    headers = {
-        "authorization": "Bearer tok_e1696f7d20f14e0f9a0b30e116aab396"
-    }
-    try:
-        response = requests.post(API_ENDPOINT, headers=headers, json=payload, timeout=5)
-        if response.status_code == 200:
-            res_json = response.json()
-            print(f"✅ [{pool_name}] {username} -> {res_json.get('message', 'Success')}")
-            return True
-        else:
-            print(f"❌ [{pool_name}] {username} -> 上传失败 (HTTP {response.status_code}): {response.text}")
-            return False
-    except Exception as e:
-        print(f"❌ [{pool_name}] {username} -> 异常: {e}")
-        return False
-
-def process_dict_format(data):
-    """处理旧格式: {'pool_name': [accounts...]}"""
-    print("📋 检测到格式: 字典 (Pool -> List)")
-    count = 0
-    for pool_name, accounts in data.items():
-        for acc in accounts:
-            username = acc.get("username")
-            password = acc.get("password")
-            # 把 ID 放入 extra_data
-            extra = {"original_id": acc.get("id"), "source": "dict_upload"}
-            
-            if username and pool_name:
-                if post_account(pool_name, username, password, extra):
-                    count += 1
-    return count
-
-def process_list_format(data):
-    """处理新格式: [{'pool_name': '...', ...}, ...]"""
-    print("📋 检测到格式: 列表 (Flat List)")
-    count = 0
-    for acc in data:
-        # 1. 提取核心字段
-        pool_name = acc.get("pool_name")
-        username = acc.get("username")
-        password = acc.get("password")
-        
-        # 2. 提取剩余字段作为 extra_data (country_code, phone 等)
-        # 我们把除了上面三个字段以外的所有字段都塞进 extra_data
-        exclude_keys = {"pool_name", "username", "password"}
-        extra = {k: v for k, v in acc.items() if k not in exclude_keys}
-        
-        # 标记来源
-        extra["source"] = "list_upload"
-
-        if not pool_name:
-            print(f"⚠️  跳过数据: 缺少 pool_name -> {acc}")
-            continue
-            
-        if not username:
-            print(f"⚠️  跳过数据: 缺少 username -> {acc}")
-            continue
-
-        if post_account(pool_name, username, password, extra):
-            count += 1
-    return count
-
-def main():
-    if not os.path.exists(ACCOUNT_FILE):
-        print(f"[错误] 找不到文件: {ACCOUNT_FILE}")
-        return
-
-    try:
-        with open(ACCOUNT_FILE, 'r', encoding='utf-8') as f:
-            data = json.load(f)
-    except json.JSONDecodeError:
-        print(f"[错误] JSON 格式无效")
-        return
-
-    success_count = 0
-    
-    # 智能识别结构
-    if isinstance(data, dict):
-        success_count = process_dict_format(data)
-    elif isinstance(data, list):
-        success_count = process_list_format(data)
-    else:
-        print("❌ 未知的 JSON 结构,必须是对象或数组")
-
-    print("\n" + "="*30)
-    print(f"📊 任务完成,成功上传: {success_count} 个")
-    print("="*30)
-
-if __name__ == "__main__":
-    main()

+ 4 - 4
vfs_registration_bot.py

@@ -496,15 +496,15 @@ def generate_account_details(config, pool_name):
 def main():
     # 配置
     config = {
-        "pool_name": "ie_dk",
+        "pool_name": "gb_at",
         "email_domain": "gmail-app", 
         "master_email": "visafly666@gmail.com",
         "proxy_url": "http://127.0.0.1:7890",
         "target_count": 30,
         "phone_country_code": 353,
-        "country_code": "irl",
-        "mission_code": "dnk",
-        "website": "https://visa.vfsglobal.com/irl/en/dnk/register",
+        "country_code": "gbr",
+        "mission_code": "aut",
+        "website": "https://visa.vfsglobal.com/gbr/en/aut/register",
     }
     
     bot = VFSRegistrationBot(config)

+ 11 - 59
vs_log_macros.py

@@ -1,71 +1,23 @@
-# vs_log_macros.py
 import logging
-from logging.handlers import RotatingFileHandler
-import os
-import sys
 
-# --- 日志配置 ---
+# 内部持有一个 logger 实例,默认是 Root Logger(防止在没初始化前报错)
+_active_logger = logging.getLogger()
 
-# 1. 确保日志目录存在
-LOG_DIR = "logs"
-if not os.path.exists(LOG_DIR):
-    os.makedirs(LOG_DIR)
+def set_active_logger(logger: logging.Logger):
+    """供外部日志配置工具调用,用于重定向所有宏的输出"""
+    global _active_logger
+    _active_logger = logger
 
-LOG_FILE = os.path.join(LOG_DIR, "vs_app.log")
-
-# 2. 定义日志格式 (包含时间)
-# 格式示例:[2023-10-27 10:30:01] [INFO] [MainThread] [TAG] Message...
-LOG_FORMAT = '[%(asctime)s][%(levelname)s][%(threadName)s]%(message)s'
-DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
-
-# 3. 获取日志级别
-log_level_str = os.environ.get("VSC_LOG_LEVEL", "INFO").upper()
-log_level = getattr(logging, log_level_str, logging.INFO)
-
-# 4. 配置日志滚动参数 (支持通过环境变量覆盖)
-# 默认单个日志文件大小: 10 MB (10 * 1024 * 1024 bytes)
-MAX_BYTES = int(os.environ.get("VSC_LOG_MAX_BYTES", 10 * 1024 * 1024))
-# 默认保留的备份文件数量: 5
-BACKUP_COUNT = int(os.environ.get("VSC_LOG_BACKUP_COUNT", 10))
-
-# 5. 创建 handlers
-# 控制台 handler
-console_handler = logging.StreamHandler(sys.stdout)
-
-# 滚动文件 handler
-rolling_file_handler = RotatingFileHandler(
-    filename=LOG_FILE,
-    mode='a',
-    maxBytes=MAX_BYTES,
-    backupCount=BACKUP_COUNT,
-    encoding='utf-8'
-)
-
-# 6. 配置 logging (同时输出到控制台和滚动文件)
-logging.basicConfig(
-    level=log_level,
-    format=LOG_FORMAT,
-    datefmt=DATE_FORMAT,
-    handlers=[
-        console_handler,
-        rolling_file_handler
-    ]
-)
-
-# --- 宏定义 ---
+# --- 宏定义 (调用签名完全保持原样) ---
 
 def VSC_INFO(tag, message, *args):
-    """
-    Usage: VSC_INFO("network", "Connected to %s:%d", ip, port)
-    Output: [Time][INFO][Thread][network]Connected to 127.0.0.1:80
-    """
-    logging.info(f"[{tag}]{message}", *args)
+    _active_logger.info(f"[{tag}] {message}", *args)
 
 def VSC_DEBUG(tag, message, *args):
-    logging.debug(f"[{tag}]{message}", *args)
+    _active_logger.debug(f"[{tag}] {message}", *args)
 
 def VSC_WARN(tag, message, *args):
-    logging.warning(f"[{tag}]{message}", *args)
+    _active_logger.warning(f"[{tag}] {message}", *args)
 
 def VSC_ERROR(tag, message, *args):
-    logging.error(f"[{tag}]{message}", *args)
+    _active_logger.error(f"[{tag}] {message}", *args)

+ 8 - 4
vs_plg.py

@@ -1,5 +1,5 @@
 # vs_plg.py
-from typing import Callable
+from typing import Callable, List, Dict
 from abc import ABC, abstractmethod
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult
 
@@ -19,9 +19,6 @@ class IVSPlg(ABC):
         """
         pass
 
-    # set_log_callback 在Python中通常通过配置logging模块或注入logger实例实现
-    # 这里为了简化,不显式提供,直接使用全局logging
-
     @abstractmethod
     def create_session(self) -> None:
         """
@@ -54,6 +51,13 @@ class IVSPlg(ABC):
         @return 分组 ID 字符串
         """
         pass
+    
+    @abstractmethod
+    def keep_alive(self):
+        """
+        @brief 会话保活
+        """
+        pass
 
     @abstractmethod
     def health_check(self) -> bool:

+ 44 - 159
vs_types.py

@@ -1,11 +1,8 @@
-# vs_types.py
 import json
 from datetime import datetime
 from pydantic import BaseModel, Field
-from pydantic.generics import GenericModel
-from enum import Enum, auto
-from typing import Generic, TypeVar, Optional, List, Any, Dict
-
+from enum import Enum
+from typing import Optional, List, Any, Dict
 
 class BizException(Exception):
     def __init__(
@@ -53,7 +50,7 @@ class PermissionDeniedError(BizException):
 class BizLogicError(BizException):
     def __init__(self, message="Business logic error"):
         super().__init__(code=40001, message=message)
-        
+
 class AvailabilityStatus(str, Enum):
     NoneAvailable = "NoAvailable"
     Available = "Available"
@@ -64,78 +61,57 @@ class QueryWaitMode(str, Enum):
     Fixed = "Fixed"
     Random = "Random"
 
-# --- Structs ---
-
 class QueryWaitConfig(BaseModel):
     mode: QueryWaitMode = QueryWaitMode.Loop
     fixed_wait: int = 0
     random_min: int = 0
     random_max: int = 0
 
-    @classmethod
-    def from_json(cls, data: Dict[str, Any]) -> "QueryWaitConfig":
-        return cls.model_validate(data)
-
-    def to_json(self) -> Dict[str, Any]:
-        return self.model_dump()
-
 class PluginConfig(BaseModel):
-    lib_path: str = Field(default="plugins", description="插件目录")
-    plugin_name: str = Field(default="", description="插件注册名")
-    plugin_bin: str = Field(default="", description="插件文件名 / Python 模块")
-    plugin_proto: str = Field(default="IVSPlg", description="插件接口协议")
-
-    @classmethod
-    def from_json(cls, data: Dict[str, Any]) -> "PluginConfig":
-        """
-        支持 dict / JSON decode 后的数据
-        """
-        return cls.model_validate(data)
-
-    def to_json(self) -> Dict[str, Any]:
-        """
-        输出标准 JSON dict(Enum 自动转字符串)
-        """
-        return self.model_dump()
+    lib_path: str = Field(default="plugins")
+    plugin_name: str = Field(default="")
+    plugin_bin: str = Field(default="")
+    plugin_proto: str = Field(default="IVSPlg")
 
 class AppointmentType(BaseModel):
     weight: int = 0
-
     routing_key: str = ''
     country: str = ''
     city: str = ''
     visa_type: str = ''
 
-    def normalized(self) -> dict:
-        return self.model_dump()
+# === 哨兵配置 ===
+class SentinelConfig(BaseModel):
+    account_source: str = "built-in"
+    account_pool_id: str = ""
+    target_instances: int = 1
+    account_cd: int = 180
+    signal_ttl: int = 180 
+
+# === Booker配置 ===
+class BookerConfig(BaseModel):
+    account_source: str = "built-in" # "built-in" 或 "order"
+    account_pool_id: str = ""        # 仅在 built-in 模式下使用
+    target_instances: int = 1        # built-in下为全局限制; order下为单队列限制
+    account_cd: int = 180
+    booking_cooldown: float = 10.0   # 触发冷却
+    max_bookings_per_account: int = 1
 
 class GroupConfig(BaseModel):
+    identifier: str = ""
     debug: bool = False
     enable: bool = False
-    identifier: str = ""
-
     need_account: bool = False
-    local_account_pool: str = ""
-
     need_proxy: bool = False
     proxy_pool: str = ""
     proxy_lock_interval: int = 5
-
-    account_login_interval: int = 0
-    target_instances: int = 1
-    
-    order_account_routing: str = ""
-    order_account_online_limit: int = 0
+    session_max_life: int = 30
     
-    input_map_username: str = "username"
-    input_map_password: str = "password"
-    
-    account_bind_applicant: bool = False
-    
-    session_max_life: int = 15
+    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 = ""
@@ -145,25 +121,6 @@ class GroupConfig(BaseModel):
     def from_json(cls, data: Dict[str, Any]) -> "GroupConfig":
         return cls.model_validate(data)
 
-    def to_json(self) -> Dict[str, Any]:
-        return self.model_dump()
-
-    def to_json_str(self) -> str:
-        return json.dumps(self.model_dump(), indent=2, ensure_ascii=False)
-
-    # ---------------------------
-    # 🔥 强烈推荐:用于热更新 / diff
-    # ---------------------------
-
-    def normalized(self) -> Dict[str, Any]:
-        """
-        用于配置比较 / 热更新判断
-        - Enum -> value
-        - 去掉运行期无关字段(如果有)
-        """
-        return self.model_dump()
-
-
 class VSAccount(BaseModel):
     id: int = 0
     username: str = ""
@@ -197,37 +154,21 @@ class DateAvailability(BaseModel):
 class VSQueryResult(BaseModel):
     success: bool = False
     apt_type: AppointmentType = Field(default_factory=AppointmentType)
-    
     availability_status: AvailabilityStatus = AvailabilityStatus.NoneAvailable
     earliest_date: Optional[datetime] = None
     availability: List[DateAvailability] = Field(default_factory=list)
     
-    model_config = {
-        "validate_assignment": True
-    }
+    model_config = {"validate_assignment": True}
     
     def to_snapshot_payload(self) -> dict:
-        """
-        直接用于 slot_snapshot/report
-        """
         def format_avail_item(item: DateAvailability) -> dict:
-            # 基础字典
             data = {}
-            
-            # 处理日期:转为 YYYY-MM-DD 字符串
             if item.date:
-                # 如果是 datetime 对象,先转 date() 去掉时分秒,再转 ISO 字符串
-                # 或者直接 strftime("%Y-%m-%d")
                 data["date"] = item.date.strftime("%Y-%m-%d")
             else:
                 data["date"] = None
-            
-            # 处理 times:只有当列表不为空时才放入字典
-            # 这能解决 "times": [] 这种多余字段的问题
             if item.times:
-                # 假设 TimeSlot 里的字段是简单的,直接 dump 即可
                 data["times"] = [t.model_dump() for t in item.times]
-                
             return data
         
         return {
@@ -236,11 +177,7 @@ class VSQueryResult(BaseModel):
             "city": self.apt_type.city,
             "visa_type": self.apt_type.visa_type,
             "availability_status": self.availability_status.value,
-            "earliest_date": (
-                self.earliest_date.strftime("%Y-%m-%d")
-                if self.earliest_date
-                else None
-            ),
+            "earliest_date": self.earliest_date.strftime("%Y-%m-%d") if self.earliest_date else None,
             "availability": [format_avail_item(a) for a in self.availability],
             "snapshot_source": "worker",
         }
@@ -252,76 +189,24 @@ class VSBookResult(BaseModel):
     account: str = ""
     book_date: str = ""
     book_time: str = ""
-    fee_amount: int = 0
-    fee_currency: str = ""
     payment_link: str = ""
 
-# --- 内部任务结构 ---
+# === 内部任务对象 ===
 class Task(BaseModel):
-    instance: Any              # IVSPlg 实例(运行时对象)
+    instance: Any              
     qw_cfg: QueryWaitConfig
-    next_run: float = 0.0      # Unix timestamp
+    next_run: float = 0.0      
     book_allowed: bool = True
-    task_ref: Optional[Dict[str, Any]] = None
-    class Config:
-        arbitrary_types_allowed = True
-        underscore_attrs_are_private = True
-
-# server 相关TYPES
-# === 数据模型 ===
-class GroupControl(BaseModel):
-    group_id: str
-
-class UpgradePluginRequest(BaseModel):
-    plugin_name: str
-    plugin_bin: str
-
-class UpdateConfigRequest(BaseModel):
-    group_id: str
-    new_config_str: str
-    
-class PluginStatusOut(BaseModel):
-    id: str
-    plugin: str
-    running: bool
-    instances: int
-    local_account_pool: str
-    proxies_pool: str
+    # 订单模式下,保存绑定的 Task ID
+    task_ref: Optional[int] = None
+    # 允许预订的 routing_key 列表
+    acceptable_routing_keys: List[str] = Field(default_factory=list)
+    # 来源标识(用于配额统计)
+    source_queue: str = ""
     
-class PluginRestarted(BaseModel):
-    restarted: List[str]
+    successful_bookings: int = 0
     
-T = TypeVar("T")
-
-class ApiResponse(BaseModel, Generic[T]):
-    code: int = 0
-    message: str = "success"
-    data: Optional[T] = None
-
-def _to_serializable(data: Any):
-    """自动将 ORM / Pydantic / list 转换为可序列化对象"""
-    if isinstance(data, BaseModel):
-        return data.model_dump()
-    if hasattr(data, "__table__"):  # SQLAlchemy ORM
-        return {
-            c.name: getattr(data, c.name)
-            for c in data.__table__.columns
-        }
-    if isinstance(data, list):
-        return [_to_serializable(i) for i in data]
-    return data
-
-def success(data=None, message: str = "success"):
-    return ApiResponse(
-        code=0,
-        message=message,
-        data=_to_serializable(data)
-    )
-
-def fail(message: str, code: int = 1):
-    return ApiResponse(
-        code=code,
-        message=message,
-        data=None
-    )
-    
+    model_config = {
+        "underscore_attrs_are_private": True,
+        "arbitrary_types_allowed": True
+    }

+ 0 - 138
web/server.py

@@ -1,138 +0,0 @@
-# web/server.py
-import asyncio
-from typing import List, Any, Dict
-from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
-from fastapi.responses import JSONResponse
-
-from vs_types import (
-    success,
-    fail,
-    BizException,
-    ApiResponse,
-    GroupControl,
-    UpgradePluginRequest,
-    UpdateConfigRequest,
-    PluginStatusOut,
-    GroupConfig,
-    PluginRestarted
-)
-from core.app_manager import AppManager
-from vs_log_macros import VSC_INFO, VSC_ERROR
-
-
-app = FastAPI(title="Visa Plugin Manager")
-
-
-# -----------------------
-# Exception Handlers
-# -----------------------
-@app.exception_handler(BizException)
-async def biz_exception_handler(request: Request, exc: BizException):
-    return JSONResponse(
-        status_code=exc.http_status,
-        content={
-            "code": exc.code,
-            "message": exc.message,
-            "data": exc.extra,
-        },
-    )
-
-
-@app.exception_handler(Exception)
-async def unhandled_exception_handler(request: Request, exc: Exception):
-    return JSONResponse(
-        status_code=500,
-        content={
-            "code": 50000,
-            "message": "Internal Server Error",
-            "data": None,
-        },
-    )
-
-@app.get("/status", response_model=ApiResponse[List[PluginStatusOut]])
-def get_status():
-    plugin_status = AppManager.Instance().get_status()
-    return success(data=plugin_status)
-
-@app.post("/start", response_model=ApiResponse)
-def start_group(payload: GroupControl):
-    AppManager.Instance().start_group(payload.group_id)
-    return success(message=f"Group {payload.group_id} started")
-
-@app.post("/stop", response_model=ApiResponse)
-def stop_group(payload: GroupControl):
-    AppManager.Instance().stop_group(payload.group_id)
-    return success(message=f"Group {payload.group_id} stopped")
-
-@app.post("/restart", response_model=ApiResponse)
-def restart_group(payload: GroupControl):
-    AppManager.Instance().restart_group(payload.group_id)
-    return success(message=f"Group {payload.group_id} restarted")
-
-@app.post("/group_config", response_model=ApiResponse[GroupConfig])
-def get_group_config(payload: GroupControl):
-    config = AppManager.Instance().get_group_config(payload.group_id)
-    return success(data=config)
-
-@app.post("/ota/upgrade_plugin", response_model=ApiResponse[PluginRestarted])
-def ota_upgrade(payload: UpgradePluginRequest):
-    restarted = AppManager.Instance().ota_upgrade_plugin(payload.plugin_name)
-    return success(message=f"Plugin {payload.plugin_name} reloaded", data=restarted)
-    
-@app.post("/ota/update_config")
-def ota_update(payload: UpdateConfigRequest):
-    AppManager.Instance().ota_update_plugin_config(payload.group_id, payload.new_config_str)
-    return success(message=f"Plugin {payload.group_id} config updated")
-    
-@app.websocket("/ws/logs/{group_id}")
-async def websocket_logs_batch(ws: WebSocket, group_id: str):
-    await ws.accept()
-    queue = asyncio.Queue(maxsize=5000)
-    loop = asyncio.get_running_loop()
-
-    def _put_msg_in_loop(msg: str):
-        try:
-            queue.put_nowait(msg)
-        except asyncio.QueueFull:
-            pass
-
-    def log_callback(msg: str):
-        loop.call_soon_threadsafe(_put_msg_in_loop, msg)
-
-    AppManager.Instance().subscribe_executor_logs(group_id, log_callback)
-
-    try:
-        buffer = []
-        while True:
-            # 阻塞等待第一条消息
-            msg = await queue.get()
-            buffer.append(msg)
-
-            # 尝试非阻塞获取队列中剩余的消息(最多取 50 条,避免包太大)
-            # 这样可以将瞬间涌入的日志合并成一个包发送
-            for _ in range(50):
-                if queue.empty():
-                    break
-                try:
-                    buffer.append(queue.get_nowait())
-                except asyncio.QueueEmpty:
-                    break
-            
-            # 合并发送 (根据前端需求,可以用换行符拼接,或者发送 JSON 数组)
-            if buffer:
-                await ws.send_text("\n".join(buffer))
-                buffer.clear()
-    except WebSocketDisconnect:
-        # 这是 FastAPI/Starlette 封装的标准异常
-        # 当客户端断开连接时会抛出这个
-        print(f"Client {group_id} disconnected")
-    except Exception as e:
-        # 处理其他未知的系统错误
-        print(f"Error: {e}")
-    finally:
-        AppManager.Instance().unsubscribe_executor_logs(group_id, log_callback)
-
-def run_web_server(host="0.0.0.0", port=8000):
-    import uvicorn
-    # log_level warning 减少控制台刷屏
-    uvicorn.run(app, host=host, port=port, log_level="info")

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików