Răsfoiți Sursa

feat: update

jerry 4 luni în urmă
părinte
comite
9b58edcfa1

+ 3 - 1
.gitignore

@@ -1,2 +1,4 @@
 __pycache__
-debug_pages
+debug_pages
+logs
+.DS_Store

+ 7 - 1
config/groups.json

@@ -1,7 +1,7 @@
 [
     {
         "identifier": "VFS_DUBLIN_NL",
-        "enable": true,
+        "enable": false,
         "need_account": true,
         "account_pool": "ie_nl",
         "need_proxy": true,
@@ -99,6 +99,12 @@
         },
         "free_config": {
             "verbose": 0,
+            "center": {
+                "code": "gbLON2fr",
+                "country": "gb",
+                "mission": "fr",
+                "city": "London"
+            },
             "embassy_code": "gbLON2fr",
             "country_code": "gb",
             "mission_code": "fr",

+ 60 - 87
group_coordinator.py

@@ -5,19 +5,18 @@ import json
 import random
 import threading
 from typing import List, Optional
-from concurrent.futures import ThreadPoolExecutor, wait
+from concurrent.futures import wait
 
 # 导入所有依赖
-from vs_types import GroupConfig, QueryWaitMode, VSPlgConfig, VSQueryResult, Task # type: ignore
-from vs_plg import IVSPlg # type: ignore
-from vs_plg_factory import VSPlgFactory # type: ignore
-from toolkit.account_manager import AccountManager # type: ignore
-from toolkit.proxy_manager import ProxyManager # type: ignore
-from toolkit.binding_manager import BindingManager # type: ignore
-from toolkit.thread_pool import ThreadPool # type: ignore
+from vs_types import GroupConfig, QueryWaitMode, VSPlgConfig, VSQueryResult, Task 
+from vs_plg import IVSPlg 
+from vs_plg_factory import VSPlgFactory 
+from toolkit.account_manager import AccountManager 
+from toolkit.proxy_manager import ProxyManager 
+from toolkit.binding_manager import BindingManager 
+from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
-from vs_types import VSError, VSErrorCategory
-from vs_log_macros import VSC_INFO, VSC_DEBUG, VSC_WARN, VSC_ERROR # type: ignore
+from vs_log_macros import VSC_INFO, VSC_DEBUG, VSC_WARN, VSC_ERROR 
 
 
 class GroupCoordinator:
@@ -41,7 +40,7 @@ class GroupCoordinator:
         # 预订操作的线程池,独立于任务调度
         self.book_executor = ThreadPool(max_workers=5).getInstance() # 使用我们封装的ThreadPool
 
-        VSC_INFO("coordinator", "GroupCoordinator for '%s' initialized.", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"GroupCoordinator for {self.m_cfg.identifier} initialized.")
 
     def set_push_callback(self, cb):
         """
@@ -49,17 +48,17 @@ class GroupCoordinator:
         Python中可以直接传递可调用对象。
         """
         self.push_callback_ = cb
-        VSC_INFO("coordinator", "Push callback set for group '%s'.", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Push callback set for group {self.m_cfg.identifier}.")
 
     def start(self):
         """
         @brief 启动协调器,包括插件注册和线程启动。
         """
         if not self.m_cfg.enable:
-            VSC_WARN("coordinator", "Group '%s' is disabled, not starting.", self.m_cfg.identifier)
+            VSC_WARN("coordinator", f"Group {self.m_cfg.identifier} is disabled, not starting.")
             return
 
-        VSC_INFO("coordinator", "Starting coordinator for group '%s'...", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Starting coordinator for group {self.m_cfg.identifier}...")
         self.m_stop_event.clear()
 
         # 注册插件
@@ -71,7 +70,7 @@ class GroupCoordinator:
         class_name = "".join(part.title() for part in plugin_name.split('_'))
         
         # 调试日志:确认推导出的类名
-        VSC_DEBUG("coordinator", "Inferring class name for plugin '%s': '%s'", plugin_name, class_name)
+        VSC_DEBUG("coordinator", f"Inferring class name for plugin {plugin_name}: {class_name}")
 
         self.m_factory.register_plugin(plugin_name, 
                                        plugin_module_path, 
@@ -82,13 +81,13 @@ class GroupCoordinator:
         
         self.m_monitor_thread.start()
         self.m_creator_thread.start()
-        VSC_INFO("coordinator", "Coordinator for group '%s' threads started.", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} threads started.")
 
     def stop(self):
         """
         @brief 停止协调器,等待所有线程结束。
         """
-        VSC_INFO("coordinator", "Stopping coordinator for group '%s'...", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Stopping coordinator for group {self.m_cfg.identifier}...")
         self.m_stop_event.set() # 发送停止信号
         
         if self.m_monitor_thread and self.m_monitor_thread.is_alive():
@@ -98,16 +97,16 @@ class GroupCoordinator:
         
         # 关闭预订线程池
         self.book_executor.shutdown(wait=True)
-        VSC_INFO("coordinator", "Coordinator for group '%s' stopped.", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} stopped.")
 
     def restart(self):
         """
         @brief 重启协调器。
         """
-        VSC_INFO("coordinator", "Restarting coordinator for group '%s'...", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Restarting coordinator for group {self.m_cfg.identifier}...", )
         self.stop()
         self.start()
-        VSC_INFO("coordinator", "Coordinator for group '%s' restarted.", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} restarted.")
 
     def group_id(self) -> str:
         """
@@ -119,7 +118,7 @@ class GroupCoordinator:
         """
         @brief 监控循环:定期检查实例健康状况,执行查询任务,并根据结果触发预订。
         """
-        VSC_INFO("coordinator", "[START] monitor loop starting for group %s", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"[START] monitor loop starting for group {self.m_cfg.identifier}")
         rng = random.Random()
         
         while not self.m_stop_event.is_set():
@@ -153,17 +152,15 @@ class GroupCoordinator:
                         self.on_query_result(task.instance, result)
                         is_booking_triggered = True
                     else:
-                        error = task.instance.get_last_error()
-                        if error.category != VSErrorCategory.EMPTY: # 忽略 No availability found
-                            VSC_DEBUG("coordinator", "[%s] Query failed, msg=%s", self.m_cfg.identifier, error.message)
+                        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Query failed, No availability found")
                 except Exception as e:
-                    VSC_ERROR("coordinator", "[%s] Exception during query: %s", self.m_cfg.identifier, str(e))
+                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
 
                 # 计算下次运行时间
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
                 if is_booking_triggered:
                     interval = rng.randint(30, 60) # 抢完票休息 30-60 秒
-                    VSC_INFO("coordinator", "[%s] Booking attempted, entering cooldown for %d sec.", self.m_cfg.identifier, interval)
+                    VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Booking attempted, entering cooldown for {interval} sec.")
                 else:
                     interval = 30
                     mode = task.qw_cfg.mode
@@ -181,16 +178,15 @@ class GroupCoordinator:
                 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:
-                    VSC_WARN("coordinator", "[%s] Removed %d unhealthy instance(s). Remaining: %d", 
-                             self.m_cfg.identifier, initial_size - len(self.m_tasks), len(self.m_tasks))
+                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Removed {initial_size - len(self.m_tasks)} unhealthy instance(s). Remaining: {len(self.m_tasks)}")
 
-        VSC_INFO("coordinator", "[STOP] monitor loop exiting for group %s", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"[STOP] monitor loop exiting for group {self.m_cfg.identifier}")
 
     def creator_loop(self):
         """
         @brief 创建者循环:根据目标实例数量,创建和补充新的插件实例。
         """
-        VSC_INFO("coordinator", "[START] creator loop starting for group %s", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"[START] creator loop starting for group {self.m_cfg.identifier}")
         
         while not self.m_stop_event.is_set():
             time.sleep(0.1) # 避免空转太快
@@ -201,13 +197,12 @@ class GroupCoordinator:
                 diff = self.m_cfg.target_instances - current_instances_count
             
             if diff > 0:
-                VSC_INFO("coordinator", "[%s] Need to create %d new instance(s). Current: %d, Target: %d", 
-                         self.m_cfg.identifier, diff, current_instances_count, self.m_cfg.target_instances)
+                VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Need to create {diff} new instance(s). Current: {current_instances_count}, Target: {self.m_cfg.target_instances}")
                 
                 # 准备配置
                 plg_cfg = self._make_plg_config()
                 if not plg_cfg:
-                    VSC_WARN("coordinator", "[%s] Failed to prepare plugin configuration, sleeping 30s.", self.m_cfg.identifier)
+                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Failed to prepare plugin configuration, sleeping 30s.")
                     time.sleep(30) # 等待资源 (账户/代理) 恢复
                     continue
 
@@ -225,37 +220,36 @@ class GroupCoordinator:
                                 next_run=time.time() # 立即执行第一次查询
                             )
                             self.m_tasks.append(new_task)
-                            VSC_INFO("coordinator", "[%s] New instance added. Total instances: %d", 
-                                     self.m_cfg.identifier, len(self.m_tasks))
+                            VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] New instance added. Total instances: {len(self.m_tasks)}")
                         else:
-                            VSC_DEBUG("coordinator", "[%s] Target instances already met, discarding newly created instance.", self.m_cfg.identifier)
+                            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Target instances already met, discarding newly created instance.")
                 else:
-                    VSC_WARN("coordinator", "[%s] Failed to create plugin instance.", self.m_cfg.identifier)
+                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Failed to create plugin instance.")
                     # 可以在这里添加重试逻辑或错误处理
 
             # 模拟创建间隔,避免瞬间创建过多实例
             time.sleep(random.uniform(1.0, 5.0))
 
-        VSC_INFO("coordinator", "[STOP] creator loop exiting for group %s", self.m_cfg.identifier)
+        VSC_INFO("coordinator", f"[STOP] creator loop exiting for group {self.m_cfg.identifier}")
 
     def _make_plg_config(self) -> Optional[VSPlgConfig]:
         """
         @brief 准备插件配置 (账号、代理等)。
         """
-        VSC_DEBUG("coordinator", "[%s] Preparing plugin configuration...", self.m_cfg.identifier)
+        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Preparing plugin configuration...")
         plg_cfg = VSPlgConfig()
         
         # 账号配置
         if self.m_cfg.need_account:
             account = AccountManager.Instance().get_next_account(self.m_cfg.account_pool)
             if not account:
-                VSC_WARN("coordinator", "[%s] No available accounts for pool '%s'", self.m_cfg.identifier, self.m_cfg.account_pool)
+                VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available accounts for pool {self.m_cfg.account_pool}")
                 return None
             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", "")
-            VSC_DEBUG("coordinator", "[%s] Using account ID %d, username %s", self.m_cfg.identifier, plg_cfg.account.id, plg_cfg.account.username)
+            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Using account ID {plg_cfg.account.id}, username {plg_cfg.account.username}")
 
         # 代理配置
         if self.m_cfg.need_proxy:
@@ -266,23 +260,22 @@ class GroupCoordinator:
                     bounded_ids = BindingManager.Instance().get_bounded_proxies_ids(self.m_cfg.account_pool, self.m_cfg.proxy_pool)
                     proxy = ProxyManager.Instance().get_unbind_proxy(self.m_cfg.proxy_pool, bounded_ids)
                     if not proxy:
-                        VSC_WARN("coordinator", "[%s] No available unbind proxy in pool '%s'", self.m_cfg.identifier, self.m_cfg.proxy_pool)
+                        VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available unbind proxy in pool {self.m_cfg.proxy_pool}")
                         return None
                     BindingManager.Instance().create_binding(
                         self.m_cfg.account_pool, plg_cfg.account.id,
                         self.m_cfg.proxy_pool, proxy["id"], "dynamic")
-                    VSC_INFO("coordinator", "[%s] Created dynamic binding: account %d -> proxy %d", 
-                             self.m_cfg.identifier, plg_cfg.account.id, proxy["id"])
-                else: # 已经有绑定代理,直接获取
+                    VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Created dynamic binding: account {plg_cfg.account.id} -> proxy {proxy['id']}")
+                else:
                     all_proxies_in_pool = ProxyManager.Instance()._proxies.get(self.m_cfg.proxy_pool, [])
                     proxy = next((p for p in all_proxies_in_pool if p["id"] == proxy_id), None)
                     if not proxy:
-                         VSC_ERROR("coordinator", "[%s] Bounded proxy ID %d not found in pool %s", self.m_cfg.identifier, proxy_id, self.m_cfg.proxy_pool)
+                         VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Bounded proxy ID {proxy_id} not found in pool {self.m_cfg.proxy_pool}")
                          return None
             else:
                 proxy = ProxyManager.Instance().get_next_proxy(self.m_cfg.proxy_pool)
                 if not proxy:
-                    VSC_WARN("coordinator", "[%s] No available proxy in pool '%s'", self.m_cfg.identifier, self.m_cfg.proxy_pool)
+                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available proxy in pool {self.m_cfg.proxy_pool}")
                     return None
 
             plg_cfg.proxy.id = proxy["id"]
@@ -292,11 +285,10 @@ class GroupCoordinator:
             plg_cfg.proxy.username = proxy.get("username", "")
             plg_cfg.proxy.password = proxy.get("password", "")
             plg_cfg.proxy.lock_until = proxy.get("lock_until", "")
-            VSC_DEBUG("coordinator", "[%s] Using proxy ID %d, IP %s:%d", 
-                      self.m_cfg.identifier, plg_cfg.proxy.id, plg_cfg.proxy.ip, plg_cfg.proxy.port)
+            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Using proxy ID {plg_cfg.proxy.id}, IP {plg_cfg.proxy.ip}:{plg_cfg.proxy.port}")
 
         plg_cfg.free_config = self.m_cfg.free_config
-        VSC_DEBUG("coordinator", "[%s] Plugin configuration prepared.", self.m_cfg.identifier)
+        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Plugin configuration prepared.")
         return plg_cfg
 
     def _create_instance(self, plg_cfg: VSPlgConfig) -> Optional[IVSPlg]:
@@ -304,32 +296,22 @@ class GroupCoordinator:
         @brief 创建并初始化单个插件实例。
         这个方法在 creator_loop 的线程池中执行。
         """
-        VSC_DEBUG("coordinator", "[%s] Creating plugin instance (plugin=%s)...", self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
+        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
         try:
             inst = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
             inst.set_config(plg_cfg)
-
-            success = inst.create_session()
-            
-            # 无论成功失败都锁定账号
+            inst.create_session()
             if self.m_cfg.need_account and self.m_cfg.account_login_interval > 0:
                 AccountManager.Instance().lock_account(
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
-
-            if not success:
-                error = inst.get_last_error()
-                VSC_ERROR("coordinator", "[%s] Create session failed, msg=%s", self.m_cfg.identifier, error.message)
-                return None
-            
-            VSC_INFO("coordinator", "[%s] Plugin instance created and session established.", self.m_cfg.identifier)
+            VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
             return inst
         except Exception as e:
-            VSC_ERROR("coordinator", "[%s] Error creating plugin instance: %s", self.m_cfg.identifier, str(e))
-            return None
+            VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
+        return None
 
     def on_query_result(self, sptr: IVSPlg, query_result: VSQueryResult):
-        VSC_INFO("coordinator", "[%s] Query result received: %s. BLOCKING monitor loop for booking...", 
-                 self.m_cfg.identifier, str(query_result))
+        VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")
         
         # 定义内部预订任务
         def book_task(inst: IVSPlg, result: VSQueryResult):
@@ -342,14 +324,14 @@ class GroupCoordinator:
                 task = VSCloudApi.Instance().get_vas_task_pop(booking_routing_key)
                 
                 if not task:
-                    VSC_WARN("coordinator", "[%s] No pending task found for key '%s'. Abandoning slot.", 
-                             inst.get_group_id(), booking_routing_key)
+                    VSC_WARN("coordinator", f"[{inst.get_group_id()}] No pending task found for key {booking_routing_key}. Abandoning slot.")
                     return 
 
                 task_id = task['id']
+                order_id = task['order_id']
                 user_input = task.get('user_inputs', {})
                 
-                VSC_INFO("coordinator", "[%s] Picked up Task ID %s for booking...", inst.get_group_id(), task_id)
+                VSC_INFO("coordinator", f"[{inst.get_group_id()}] Picked up Task ID {task_id} for booking...")
 
                 # 2. 执行预订
                 # 注意:插件的 book 方法需要接收 user_input
@@ -357,22 +339,21 @@ class GroupCoordinator:
 
                 # 3. 处理结果
                 if book_res.success:
-                    VSC_INFO("coordinator", "[%s] Booking SUCCESS! Order: '%s'", 
-                             inst.get_group_id(), book_res.order_id)
+                    VSC_INFO("coordinator", f"[{inst.get_group_id()}] Booking SUCCESS! Order: {order_id}")
                     
                     # 推送通知
                     if hasattr(self, 'push_callback_') and self.push_callback_:
-                        self.push_callback_(100, f"Booking Success: {book_res.order_id}".encode('utf-8'), 0) 
+                        self.push_callback_(100, f"Booking Success: {order_id}".encode('utf-8'), 0) 
                     
                     # 4. 成功逻辑:更新任务状态为 grabbed
                     # 构造历史记录详情对象
                     history_detail = {
+                        "account": book_res.account,
+                        "session_id": book_res.session_id,
                         "slot_date": book_res.book_date,
                         "slot_time": book_res.book_time,
-                        "account": inst.config.account.username if inst.config else "unknown",
                         "timestamp": int(time.time()),
                         "payment_link": book_res.payment_link,
-                        "order_id": book_res.order_id
                     }
 
                     update_data = {
@@ -383,28 +364,20 @@ class GroupCoordinator:
                     }
                     
                     VSCloudApi.Instance().update_vas_task(task_id, update_data)
-                    VSC_INFO("coordinator", "[%s] Task %s marked as GRABBED.", inst.get_group_id(), task_id)
-                    
+                    VSC_INFO("coordinator", f"[{inst.get_group_id()}] Task {task_id} marked as GRABBED.")
                     # 成功后 task_id 置空,防止 finally 块再次将其重置为 pending
                     task_id = None 
-
-                else:
-                    # 失败逻辑
-                    error = inst.get_last_error()
-                    VSC_ERROR("coordinator", "[%s] Booking FAILED. Msg=%s", inst.get_group_id(), error.message)
-                    # task_id 仍然存在,将由 finally 块处理回滚
-
-            except Exception as e:
-                VSC_ERROR("coordinator", "[%s] Exception during booking: %s", inst.get_group_id(), str(e))
+            # except Exception as e:
+            #     VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Exception during booking: {e}")
             
             finally:
                 # 5. Return to Queue (回滚机制)
                 if task_id is not None:
-                    VSC_WARN("coordinator", "[%s] Returning Task %s to queue (status=pending).", inst.get_group_id(), task_id)
+                    VSC_WARN("coordinator", f"[{inst.get_group_id()}] Returning Task {task_id} to queue (status=pending).")
                     try:
                         VSCloudApi.Instance().update_vas_task(task_id, {"status": "pending"})
                     except Exception as ex:
-                        VSC_ERROR("coordinator", "[%s] Failed to return task to queue: %s", inst.get_group_id(), str(ex))
+                        VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Failed to return task to queue: {ex}")
 
         futures = []
         f = self.book_executor.enqueue(book_task, sptr, query_result)

+ 3 - 3
main.py

@@ -5,9 +5,9 @@ import json
 import logging
 
 # 导入必要模块
-from vs_types import GroupConfig, QueryWaitMode, PluginConfig, QueryWaitConfig # type: ignore
-from group_coordinator import GroupCoordinator # type: ignore
-from vs_log_macros import VSC_INFO, VSC_ERROR # type: ignore
+from vs_types import GroupConfig, QueryWaitMode, PluginConfig, QueryWaitConfig
+from group_coordinator import GroupCoordinator
+from vs_log_macros import VSC_INFO, VSC_ERROR
 
 def vfs_test():
     # 0. 检查目录结构

+ 559 - 215
plugins/bls_plugin.py

@@ -1,24 +1,27 @@
 import re
+import os
 import base64
 import time
 import json
 import random
 import string
+from datetime import datetime, timedelta
 from pathlib import Path
 from urllib.parse import urlparse, parse_qs, urlencode
 from typing import Dict, List, Optional, Any
 
-try:
-    from curl_cffi import requests, const
-    from bs4 import BeautifulSoup
-except ImportError:
-    raise ImportError("Missing dependencies. Run: pip install curl-cffi beautifulsoup4")
+from curl_cffi import requests, const
+from bs4 import BeautifulSoup
+
+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, VSError # type: ignore
-from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus # type: ignore
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN # type: ignore
-from toolkit.vs_cloud_api import VSCloudApi # type: ignore
+from vs_plg import IVSPlg, VSError 
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
+from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
+from toolkit.vs_cloud_api import VSCloudApi 
 from utils.browser_util import get_browser
 
 class BlsPlugin(IVSPlg):
@@ -58,36 +61,28 @@ class BlsPlugin(IVSPlg):
     def health_check(self) -> bool:
         return self.is_healthy
 
-    def get_last_error(self) -> VSError:
-        return self.last_error
-
-    def _set_error(self, code: int, message: str):
-        self.last_error = VSError(code, message)
-        VSC_ERROR("bls_plg", "[%s] Error %d: %s", self.group_id, code, message)
-        if code in [401, 403]: self.is_healthy = False
-
-    # =========================================================================
-    # 1. 登录流程 (Login)
-    # =========================================================================
-    def create_session(self) -> bool:
-        VSC_INFO("bls_plg", "[%s] Creating session...", self.group_id)
-        self.is_healthy = True
-        
-        # 初始化 Session
+    def create_session(self):
         self.session = requests.Session(
             proxy=self._get_proxy_url(),
             impersonate="chrome131",
-            curl_options={const.CurlOpt.MAXAGE_CONN: 1800, const.CurlOpt.VERBOSE: False}
+            curl_options={
+                const.CurlOpt.MAXAGE_CONN: 1800,
+                const.CurlOpt.VERBOSE: False
+            }
         )
         domain = self.free_config.get("domain")
         if not domain:
-            return False
+            raise NotFoundError(message="Required field [domain] in free config")
 
         # 1.1 获取登录页 & 解析参数
-        url = f"https://{domain}/Global/account/login"
-        resp = self._request("GET", url)
-        if not resp:
-            return False
+        login_url = f"https://{domain}/Global/account/login"
+        
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+        }
+        
+        resp = self._perform_request('GET', login_url, headers=headers)
         
         soup = BeautifulSoup(resp.text, 'html.parser')
         form_data = self._extract_hidden_fields(soup)
@@ -107,8 +102,6 @@ class BlsPlugin(IVSPlg):
         
         # 1.2 处理验证码
         captcha_token = self._solve_bls_captcha(data_val)
-        if not captcha_token:
-            return False
         
         # 1.3 提交登录
         submit_url = f"https://{domain}/Global/account/loginsubmit"
@@ -116,16 +109,15 @@ class BlsPlugin(IVSPlg):
         payload["X-Requested-With"] = "XMLHttpRequest"
         payload["CaptchaData"] = captcha_token
         # 填入账号密码
-        if "UserIdKey" in form_data: payload[form_data["UserIdKey"]] = self.config.account.username
-        if "PasswordKey" in form_data: payload[form_data["PasswordKey"]] = self.config.account.password
+        if "UserIdKey" in form_data:
+            payload[form_data["UserIdKey"]] = self.config.account.username
+        if "PasswordKey" in form_data:
+            payload[form_data["PasswordKey"]] = self.config.account.password
         
-        login_res = self._request("POST", submit_url, data=payload)
-        if login_res and login_res.json().get('success'):
-            VSC_INFO("bls_plg", "[%s] Login Successful", self.group_id)
-            return True
-            
-        self._set_error(2000, "Login Failed")
-        return False
+        login_resp = self._perform_request('POST', submit_url, data=payload, headers=headers)
+        if login_resp.json()['success']:
+            return
+        raise BizLogicError(message='Login failed')
 
     # =========================================================================
     # 2. 查询流程 (Query)
@@ -133,52 +125,52 @@ class BlsPlugin(IVSPlg):
     def query(self) -> VSQueryResult:
         res = VSQueryResult()
         domain = self.free_config.get("domain")
-        if not self.session: return res
 
-        # 2.1 签证类型验证 (VisaTypeVerification)
+        # 2.1 签证类型验证
         url_vtv = f"https://{domain}/Global/bls/visatypeverification"
-        resp = self._request("GET", url_vtv)
-        if not resp: return res
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+        }
+        resp = self._perform_request('GET', url_vtv, headers=headers)
         
         form_vtv = self._extract_hidden_fields(BeautifulSoup(resp.text, 'html.parser'))
         captcha_token = self._solve_bls_captcha()
-        if not captcha_token: return res
         
         form_vtv['CaptchaData'] = captcha_token
         form_vtv["X-Requested-With"] = "XMLHttpRequest"
         
-        vtv_res = self._request("POST", f"https://{domain}/Global/bls/VisaTypeVerification", data=form_vtv)
-        if not vtv_res or not vtv_res.json().get('success'): return res
+        vtv_resp = self._perform_request('POST', f"https://{domain}/Global/bls/VisaTypeVerification", data=form_vtv, headers=headers)
+        if not vtv_resp.json()['success']:
+            raise BizLogicError(message='Submit VisaTypeVerification Failed')
         
-        # 2.2 签证类型选择 (VisaType)
-        return_url = vtv_res.json()['returnUrl'] # 包含 data=xxx
+        # 2.2 签证类型选择
+        return_url = vtv_resp.json()['returnUrl'] # 包含 data=xxx
         data_val = re.search(r"data=([^&]+)", return_url).group(1)
         
         url_vt = f"https://{domain}/Global/bls/visatype?data={data_val}"
-        resp_vt = self._request("GET", url_vt)
-        if not resp_vt: return res
+        
+        vt_resp = self._perform_request('GET', url_vt, headers=headers)
         
         # 这里需要极其复杂的 JS 变量提取 (JS Arrays -> Match Name -> Get ID)
-        # 为了缩减篇幅,假设 _construct_visatype_payload 封装了这些逻辑
-        vt_payload = self._construct_visatype_payload(resp_vt.text, BeautifulSoup(resp_vt.text, 'html.parser'))
-        if not vt_payload: return res
+        vt_payload = self._construct_visatype_payload(vt_resp.text, BeautifulSoup(vt_resp.text, 'html.parser'))
         
-        vt_res = self._request("POST", f"https://{domain}/Global/bls/VisaType", data=vt_payload)
-        if not vt_res or not vt_res.json().get('success'):
-            if vt_res and not vt_res.json().get('available'):
-                res.success = True
+        vt_res = self._perform_request('POST', f"https://{domain}/Global/bls/VisaType", data=vt_payload, headers=headers)
+        if not vt_res.json()['success']:
+            if not vt_res.json()['available']:
+                res.success = False
                 res.availability_status = AvailabilityStatus.NoneAvailable
             return res
 
-        # 2.3 获取预约参数 (Book Params)
+        # 2.3 获取预约参数
         final_url = vt_res.json()['returnUrl']
         q_params = parse_qs(urlparse(final_url).query)
         self.book_params = {k: v[0] for k, v in q_params.items()}
         
-        # 2.4 查询日历 (ManageAppointment)
+        # 2.4 查询日历
         url_ma = f"https://{domain}/Global/blsAppointment/ManageAppointment?{urlencode(self.book_params)}"
-        resp_ma = self._request("GET", url_ma)
-        if not resp_ma: return res
+        
+        resp_ma = self._perform_request('GET', url_ma, headers=headers)
         
         avail_str = self._extract_js_var(resp_ma.text, "var availDates", r"var availDates =(.*?);")
         if avail_str:
@@ -195,60 +187,66 @@ class BlsPlugin(IVSPlg):
                     da.times.append(VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available"))
                     res.availability.append(da)
             else:
-                res.success = True
+                res.success = False
                 res.availability_status = AvailabilityStatus.NoneAvailable
+            return res
                 
-        return res
+        raise BizLogicError(message='Query page not found required field [var availDates]')
 
-    # =========================================================================
-    # 3. 预约流程 (Book)
-    # =========================================================================
-    def book(self, slot_info: VSQueryResult) -> VSBookResult:
+    def book(self, slot_info: VSQueryResult, user_inputs: Dict) -> VSBookResult:
         res = VSBookResult()
         domain = self.free_config.get("domain")
-        if not self.book_params: return res
-        
-        uinfo = self.free_config.get("user_info", {})
         
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+        }
         # 3.1 获取 Manage Page (为了 Token 和 JS 变量)
         url_ma = f"https://{domain}/Global/blsAppointment/ManageAppointment?{urlencode(self.book_params)}"
-        resp_ma = self._request("GET", url_ma)
-        if not resp_ma: return res
+        
+        resp_ma = self._perform_request('GET', url_ma, headers=headers)
         ma_soup = BeautifulSoup(resp_ma.text, 'html.parser')
         ma_form = self._extract_hidden_fields(ma_soup)
         req_token = ma_form.get('__RequestVerificationToken')
         
         # 3.2 上传照片
-        if 'passport_image_url' in uinfo:
-            photo_bytes = requests.get(uinfo['passport_image_url']).content
-            boundary = "----WebKitFormBoundary" + "".join(random.choices(string.ascii_letters + string.digits, k=16))
-            upload_headers = {
-                "content-type": f"multipart/form-data; boundary={boundary}",
-                "requestverificationtoken": req_token,
-                "x-requested-with": "XMLHttpRequest",
-            }
-            body = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"photo.jpg\"\r\n"
-                    f"Content-Type: image/jpeg\r\n\r\n").encode("utf-8") + photo_bytes + f"\r\n--{boundary}--\r\n".encode("utf-8")
-            
-            up_res = self.session.post(f"https://{domain}/Global/query/UploadProfileImage", headers=upload_headers, data=body)
-            if up_res.status_code == 200:
-                ma_form['ApplicantPhotoId'] = up_res.json()['fileId']
+        if 'passport_image_url' not in user_inputs:
+            raise NotFoundError()
+        
+        photo_bytes = requests.get(user_inputs['passport_image_url']).content
+        boundary = "----WebKitFormBoundary" + "".join(random.choices(string.ascii_letters + string.digits, k=16))
+        upload_headers = {
+            "content-type": f"multipart/form-data; boundary={boundary}",
+            "requestverificationtoken": req_token,
+            "x-requested-with": "XMLHttpRequest",
+        }
+        body = (f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"photo.jpg\"\r\n"
+                f"Content-Type: image/jpeg\r\n\r\n").encode("utf-8") + photo_bytes + f"\r\n--{boundary}--\r\n".encode("utf-8")
+        
+        up_resp = self.session.post(f"https://{domain}/Global/query/UploadProfileImage", headers=upload_headers, data=body)
+        if up_resp.status_code !=200:
+            raise BizLogicError(message='Upload Passport Image failed')
+      
+        ma_form['ApplicantPhotoId'] = up_resp.json()['fileId']
 
         # 3.3 邮箱 OTP 流程
         data_val = self._extract_js_var(resp_ma.text, "win.iframeOpenUrl", r"data=([^&]+)")
+        
         # 发送 OTP
-        self._request("GET", f"https://{domain}/Global/blsappointment/SendAppointmentVerificationCode?code={data_val}", headers={"X-Requested-With": "XMLHttpRequest"})
+        headers["X-Requested-With"] = "XMLHttpRequest"
+        self._perform_request('GET', f"https://{domain}/Global/blsappointment/SendAppointmentVerificationCode?code={data_val}", headers=headers)
         
         # 读取 OTP (Wait 30s max)
         otp_code = self._read_otp_email(wait_sec=30)
-        if not otp_code: 
-            self._set_error(3004, "OTP timeout")
-            return res
             
         # 验证 OTP
         verify_payload = {"Code": otp_code, "Value": ma_form.get('EmailCode'), "Id": ma_form.get('Id')}
-        v_res = self._request("POST", f"https://{domain}/Global/blsappointment/VerifyEmail", data=verify_payload, headers={"requestverificationtoken": req_token})
-        if not v_res or not v_res.json().get('success'): return res
+        
+        headers['requestverificationtoken'] = req_token
+        v_resp = self._perform_request('POST', f"https://{domain}/Global/blsappointment/VerifyEmail", data=verify_payload, headers=headers)
+        headers.pop('requestverificationtoken')
+        if not v_resp.json().get('success'):
+            raise BizLogicError(message='Email verification failed')
         
         ma_form['EmailVerified'] = 'True'
         ma_form['EmailVerificationCode'] = otp_code
@@ -268,11 +266,15 @@ class BlsPlugin(IVSPlg):
             "dataSource": ma_form.get("DataSource"),
             "missionId": ma_form.get("MissionId")
         }
-        slots_res = self._request("POST", slot_url, params=slot_params, headers={"requestverificationtoken": req_token})
-        if not slots_res: return res
         
-        slots_data = sorted(slots_res.json(), key=lambda x: -x["Count"]) # 选剩余最多的
-        if not slots_data or slots_data[0]['Count'] <= 0: return res
+        headers['requestverificationtoken'] = req_token  
+        slots_resp = self._perform_request('POST', slot_url, params=slot_params, headers=headers)
+        headers.pop('requestverificationtoken')
+        slots_data = sorted(slots_resp.json(), key=lambda x: -x["Count"]) # 选剩余最多的
+        if not slots_data or slots_data[0]['Count'] <= 0:
+            VSC_WARN('bls', 'Available slot times not found')
+            res.success = False
+            return res
         
         target_time = slots_data[0]['Name']
         ma_form['ServerAppointmentDate'] = target_date
@@ -285,53 +287,61 @@ class BlsPlugin(IVSPlg):
 
         # 3.5 再次验证码 & 提交 ManageAppointment
         captcha_token = self._solve_bls_captcha(data_val)
-        if not captcha_token: return res
         ma_form['CaptchaData'] = captcha_token
         
-        final_ma_res = self._request("POST", f"https://{domain}/Global/BLSAppointment/ManageAppointment", data=ma_form)
-        if not final_ma_res: return res
+        final_ma_resp = self._perform_request('POST', f"https://{domain}/Global/BLSAppointment/ManageAppointment", data=ma_form, headers=headers)
         
-        appt_model_id = final_ma_res.json().get('model', {}).get('Id')
-        if not appt_model_id: return res
+        appt_model_id = final_ma_resp.json().get('model', {}).get('Id')
+        if not appt_model_id:
+            raise NotFoundError(message='Appointment model id not found')
 
         # 3.6 填写申请表 (VisaAppointmentForm)
         # 获取页面 -> 解析 JS 变量 -> 映射 UserInfo -> 提交
         # 这里逻辑较深,核心是映射。简化为提交一个空的 applicants JSON,实际需完整映射。
         # 假设 _fill_applicant_form 做了这些工作
-        if self._submit_final_form(appt_model_id, uinfo, self.book_params, req_token):
-            # 成功,返回 Liveness 链接
-            res.success = True
-            res.session_id = self._generate_id()
-            res.order_id = res.session_id
-            res.payment_link = f"https://{domain}/Global/BlsAppointment/livenessView?id={appt_model_id}"
-            
-            # 将 Session 信息存入 Cloud 以便前端接管
-            self._save_session_to_cloud(res.session_id, res.payment_link)
-            VSC_INFO("bls_plg", "[%s] Book Success. Liveness URL: %s", self.group_id, res.payment_link)
-            
+        self._submit_final_form(appt_model_id, user_inputs, self.book_params, req_token)
+        # 成功,返回 Liveness 链接
+        Liveness_page = f"https://{domain}/Global/BlsAppointment/livenessView?id={appt_model_id}"
+        session_data = self._save_http_session(Liveness_page)
+        res.success = True
+        res.account = self.config.account.username
+        res.session_id = session_data['session_id']
+        res.book_date = target_date
+        res.book_time = target_time
+        VSC_INFO("bls_plg", "[%s] Book Success. Liveness URL: %s", self.group_id, res.payment_link)
         return res
-
-    # =========================================================================
-    # 辅助函数 (Helpers)
-    # =========================================================================
-
-    def _request(self, method, url, **kwargs):
-        print(f'_request {method} {url}')
-        headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
-            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
-        }
-        if 'headers' in kwargs: headers.update(kwargs['headers'])
-        kwargs['headers'] = headers
-        
-        try:
-            resp = self.session.request(method, url, timeout=60, **kwargs)
-            if resp.status_code == 401: self._set_error(401, "Unauthorized")
-            elif resp.status_code in [403, 429]: self._set_error(resp.status_code, "Blocked")
-            if resp.status_code == 200: return resp
-        except Exception as e:
-            VSC_WARN("bls_plg", f"Request Error: {e}")
-        return None
+    
+    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}_{self.group_id}_{timestamp}.html"
+        with open(filename, "w", encoding="utf-8") as f:
+            f.write(content)
+        VSC_INFO("bls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+    
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
+        """
+        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
+        1. 发送 OPTIONS 请求
+        2. 发送实际请求
+        """
+        print(f'[perform request] {method} {url}')
+        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
+        VSC_INFO('bls_plg', resp.text)
+        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()
+        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_bls_captcha(self, data='') -> Optional[str]:
         """
@@ -340,10 +350,11 @@ class BlsPlugin(IVSPlg):
         domain = self.free_config.get("domain")
         url = f"https://{domain}/Global/NewCaptcha/GenerateCaptcha"
         if data: url = f"https://{domain}/Global/CaptchaPublic/GenerateCaptcha?data={data}"
-        
-        resp = self._request("GET", url)
-        if not resp:
-            return None
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+        }
+        resp = self._perform_request("GET", url, headers=headers)
         
         with open("tmp.html", 'w') as f:
             f.write(resp.text)
@@ -354,7 +365,7 @@ class BlsPlugin(IVSPlg):
         self.browser.get(file_url)
         captions_ele = self.browser.ele('xpath://*[@id="captcha-main-div"]/div/div[1]', timeout=5)
         if not captions_ele:
-            return None
+            raise NotFoundError(message='Captions elements not found')
         caption_eles = captions_ele.children()
         caption_text = ''
         for caption in caption_eles:
@@ -377,40 +388,35 @@ class BlsPlugin(IVSPlg):
                     base64_data = re.sub('^data:image/.+;base64,', '', img_src)
                     img_bytes = base64.b64decode(base64_data)
                     
-                    # === 远程调用 Start ===
-                    try:
-                        # 直接发送二进制 body
-                        ocr_resp = requests.post(
-                            self.ocr_service_url, 
-                            data=img_bytes, 
-                            headers={"Content-Type": "application/octet-stream"},
-                            timeout=5
-                        )
-                        if ocr_resp.status_code == 200:
-                            res_json = ocr_resp.json()
-                            ocr_res = res_json.get('data', '').replace('$', '')[:3]
-                            
-                            VSC_INFO("bls_plg", f'ocr captcha id={captcha_image.attr("id")} result={ocr_res}, target={numbers}')
-                            
-                            if ocr_res == numbers:
-                                eid = captcha_image.attr('id')
-                                selected_ids.append(eid)
-                    except Exception as e:
-                        VSC_WARN("bls_plg", f"OCR Service Failed: {e}")
+                    ocr_resp = requests.post(
+                        self.ocr_service_url, 
+                        data=img_bytes, 
+                        headers={"Content-Type": "application/octet-stream"},
+                        timeout=5
+                    )
+                    if ocr_resp.status_code == 200:
+                        res_json = ocr_resp.json()
+                        ocr_res = res_json.get('data', '').replace('$', '')[:3]
+                        
+                        VSC_INFO("bls_plg", f'ocr captcha id={captcha_image.attr("id")} result={ocr_res}, target={numbers}')
+                        
+                        if ocr_res == numbers:
+                            eid = captcha_image.attr('id')
+                            selected_ids.append(eid)
+                    else:
+                        raise BizLogicError(message='Captcha server response error')
+          
         if not selected_ids:
-            return None 
+            raise BizLogicError(message='Captcha selected ids is empty')
         VSC_INFO("bls_plg", f'select_ids={selected_ids}')
         soup = BeautifulSoup(resp.text, 'html.parser')
-
         # 3. 提交选中结果
         form = self._extract_hidden_fields(soup)
         form['SelectedImages'] = ",".join(selected_ids)
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
-        
-        res = self._request("POST", submit_url, data=form, headers={"X-Requested-With": "XMLHttpRequest"})
-        if res and res.json().get('captcha'):
-            return res.json()['captcha']
-        return None
+        headers["X-Requested-With"] = "XMLHttpRequest"
+        resp = self._perform_request('POST', submit_url, headers=headers, data=form)
+        return resp.json()['captcha']
 
     def _extract_hidden_fields(self, soup) -> Dict:
         params = {}
@@ -419,6 +425,8 @@ class BlsPlugin(IVSPlg):
             for inp in form.find_all("input"):
                 name = inp.get("name")
                 if name: params[name] = inp.get("value", "")
+        else:
+            VSC_WARN('bls_plg', 'Form element not found')
         return params
 
     def _extract_js_var(self, html, context, pattern):
@@ -428,57 +436,393 @@ class BlsPlugin(IVSPlg):
             if match: return match.group(1)
         return ""
 
-    def _construct_visatype_payload(self, html, soup):
-        # 简化版:提取 ID 逻辑。实际需根据 free_config 的 VisaType 名称匹配 JS 数组中的 ID
-        # 这里仅展示结构,核心是利用 self.free_config['visaType'] 等去匹配
+    def _construct_visatype_payload(self, html: str, soup: BeautifulSoup) -> Optional[Dict]:
+        """
+        构造 VisaType 提交参数 (对应原代码 parse_visatype_form)
+        """
+        # 1. 基础表单参数 (__RequestVerificationToken 等)
         params = self._extract_hidden_fields(soup)
         
-        # Helper inner function to find ID from JS array
-        def find_id(var_name, target_name, key="Name", val_key="Id"):
-            json_str = self._extract_js_var(html, f"var {var_name}", rf"var {var_name}\s*=\s*(.*?);")
-            if json_str:
-                try:
-                    data = json.loads(json_str)
-                    for item in data:
-                        if item.get(key) == target_name: return item.get(val_key)
-                except: pass
-            return None
-
-        # 示例:Jurisdiction
-        if self.free_config.get('jurisdiction'):
-            jid = find_id("jurisdictionData", self.free_config['jurisdiction'])
-            if jid: params[f'JurisdictionId{jid}'] = jid # 这里的 Key 也是动态的,BLS 特色
+        # 2. 提取页面中的 JS 数据变量
+        def get_js_data(var_name):
+            try:
+                # 匹配 var name = [...]; 结构
+                pattern = f"var {var_name}\\s*=\\s*(.*?);"
+                match = re.search(pattern, html, re.DOTALL)
+                if match:
+                    return json.loads(match.group(1))
+            except Exception as e:
+                VSC_DEBUG("bls_plg", f"Failed to parse JS var {var_name}: {e}")
+            return []
+
+        jurisdiction_list = get_js_data("jurisdictionData")
+        location_list = get_js_data("locationData")
+        visa_type_list = get_js_data("visaIdData")
+        visa_subtype_list = get_js_data("visasubIdData")
+        app_category_list = get_js_data("AppointmentCategoryIdData")
+
+        # 3. 读取配置
+        cfg_jur = self.free_config.get("jurisdiction")
+        cfg_loc = self.free_config.get("location")
+        cfg_type = self.free_config.get("visaType")
+        cfg_subtype = self.free_config.get("visaSubType")
+        cfg_cat = self.free_config.get("appointmentCategory", "Normal")
+
+        # 4. 匹配 ID
+        jur_id = None
+        loc_id = None
+        type_id = None
+        subtype_id = None
+        cat_id = None
+
+        # (A) Appointment Category
+        for item in app_category_list:
+            if item.get("Name") == cfg_cat:
+                cat_id = item.get("Id")
+                break
+        
+        # (B) Jurisdiction (如果配置了)
+        if cfg_jur and jurisdiction_list:
+            for item in jurisdiction_list:
+                if item.get("Name") == cfg_jur:
+                    jur_id = item.get("Id")
+                    break
+
+        # (C) Location
+        for item in location_list:
+            if item.get("Name") == cfg_loc:
+                loc_id = item.get("Id")
+                break
+        
+        # (D) Visa Type (需匹配 LocationId)
+        if loc_id:
+            for item in visa_type_list:
+                # 比较 Name 和 LocationId
+                if item.get("Name") == cfg_type and str(item.get("LocationId")) == str(loc_id):
+                    type_id = item.get("Id")
+                    break
+        
+        # (E) Visa SubType (需匹配 VisaType Value)
+        if type_id:
+            for item in visa_subtype_list:
+                # BLS 逻辑: visasubIdData 中的 Value 字段对应 VisaTypeId
+                if item.get("Name") == cfg_subtype and str(item.get("Value")) == str(type_id):
+                    subtype_id = item.get("Id")
+                    break
+
+        # 5. 构造动态参数 & 校验
+        if not cat_id:
+            raise NotFoundError(message=f"Config: AppCategory '{cfg_cat}' not found")
+        params[f"AppointmentCategoryId{cat_id}"] = cat_id
+
+        if cfg_jur:
+            if not jur_id:
+                raise NotFoundError(message=f"Config: Jurisdiction '{cfg_jur}' not found")
+            params[f"JurisdictionId{jur_id}"] = jur_id
+
+        if not loc_id:
+            raise NotFoundError(message=f"Config: Location '{cfg_loc}' not found")
+        params[f"Location{loc_id}"] = loc_id
+
+        if not type_id:
+            raise NotFoundError(message=f"Config: VisaType '{cfg_type}' not found for Loc '{cfg_loc}'")
+        params[f"VisaType{type_id}"] = type_id
+
+        if not subtype_id:
+            raise NotFoundError(message=f"Config: VisaSubType '{cfg_subtype}' not found")
+        params[f"VisaSubType{subtype_id}"] = subtype_id
+
+        # 固定参数
+        params["AppointmentFor1"] = "Individual"
+        
+        # 6. 构造 ResponseData (行为轨迹模拟)
+        # BLS 后端会校验这个字段,模拟用户选择下拉框的时间间隔
+        response_data = []
+        current_time = datetime.utcnow()
+        
+        def add_trace(prefix, val_id):
+            nonlocal current_time
+            # 模拟 1-3 秒的操作间隔
+            duration = random.randint(1000, 3000)
+            gap = random.randint(500, 1500)
             
-        # ... 对 Location, VisaType, VisaSubType 重复此逻辑 ...
-        
+            start = current_time
+            end = start + timedelta(milliseconds=duration)
+            
+            # BLS 时间格式: 2023-10-27T10:00:00.123Z
+            fmt = "%Y-%m-%dT%H:%M:%S.%f"
+            
+            response_data.append({
+                "Id": f"{prefix}{val_id}",
+                "Start": start.strftime(fmt)[:-3] + "Z",
+                "End": end.strftime(fmt)[:-3] + "Z",
+                "Total": duration,
+                "Selected": True
+            })
+            current_time = end + timedelta(milliseconds=gap)
+
+        # 按顺序添加轨迹
+        add_trace("AppointmentCategoryId", cat_id)
+        if jur_id: add_trace("JurisdictionId", jur_id)
+        add_trace("Location", loc_id)
+        add_trace("VisaType", type_id)
+        add_trace("VisaSubType", subtype_id)
+
+        params["ResponseData"] = json.dumps(response_data)
         params["X-Requested-With"] = "XMLHttpRequest"
-        params["ResponseData"] = "[]" # 必须字段
+        
         return params
 
-    def _submit_final_form(self, model_id, uinfo, book_params, token):
-        # 1. Get Form HTML -> 2. Parse JS Data -> 3. Map uinfo -> 4. Post
-        # 略,参考原代码 parse_application_form_excel 和 _fix_applicant_data
-        # 这是一个纯数据映射过程
-        return True
+    def _submit_final_form(self, model_id: str, user_inputs: Dict, book_params: Dict, token: str):
+        """
+        提交最终签证申请表 (VisaAppointmentForm)
+        对应原代码的: get_visa_appointment_form_html -> parse -> fix_data -> submit
+        """
+        domain = self.free_config.get("domain")
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+        }
+        # 1. 获取表单页面 (为了提取 JS 变量映射表)
+        url_get = f"https://{domain}/Global/BlsAppointment/VisaAppointmentForm?appointmentId={model_id}"
+        # 构造 Referer
+        ref_query = urlencode(book_params)
+        referer = f"Global/blsAppointment/ManageAppointment?{ref_query}"
+        
+        headers['X-Requested-With'] = "XMLHttpRequest"
+        headers['Referer'] = f"https://{domain}/{referer}"
+        resp = self._perform_request('GET', url_get, headers=headers)
+        headers.pop['X-Requested-With']
+        headers.pop['Referer']
+        
+        html = resp.text
+        soup = BeautifulSoup(resp.text, 'html.parser')
+        
+        # 2. 提取基础隐藏域 (包含 __RequestVerificationToken 等)
+        form_data = self._extract_hidden_fields(soup)
+        
+        # 3. 提取下拉菜单数据源 (JS Variables)
+        # BLS 的页面里有很多 var countryData = [...]; 这种数据
+        def get_list(name):
+            val = self._extract_js_var(html, f"var {name}", rf"var {name}\s*=\s*(.*?);")
+            return json.loads(val) if val else []
+
+        # 提取关键数据源
+        country_data = get_list("countryData")
+        gender_data = get_list("genderData")
+        marital_data = get_list("maritalStatusData")
+        occupation_data = get_list("occupationData")
+        # passport_type_data = get_list("passportTypeData") # 通常默认 Ordinary
+        
+        # 4. 辅助函数:根据文本找 ID
+        def find_id(data_list, text_val, default=None):
+            if not text_val: return default
+            text_val = str(text_val).lower().strip()
+            for item in data_list:
+                if str(item.get("Name")).lower() == text_val:
+                    return item.get("Id")
+            return default
+
+        # 5. 准备日期 (YYYY-MM-DD)
+        # uinfo 中的日期可能是不同格式,需统一
+        def fmt_date(d_str):
+            try:
+                # 尝试解析常见格式
+                for fmt in ["%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"]:
+                    try:
+                        return datetime.strptime(d_str, fmt).strftime("%Y-%m-%d")
+                    except: pass
+            except: pass
+            return d_str # 原样返回 fallback
+
+        dob = fmt_date(user_inputs.get("birthday", ""))
+        ppt_issue = fmt_date(user_inputs.get("passport_issue_date", ""))
+        ppt_expiry = fmt_date(user_inputs.get("passport_expiry_date", ""))
+        
+        # 自动计算行程日期 (如果未提供,默认一个月后)
+        try:
+            travel_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
+        except: travel_date = ""
+
+        # 6. 构造申请人详细数据对象 (JSON)
+        # 注意:这里的字段名必须严格匹配 BLS 后端实体定义
+        applicant_detail = {
+            "ApplicantSerialNo": "1",
+            "ApplicantId": form_data.get("applicantId", "0"), # 从页面隐藏域提取
+            "Id": form_data.get("applicantId", "0"),
+            "ParentId": form_data.get("Id", model_id), # 关联的 Appointment ID
+            
+            # 基本信息
+            "FirstName": user_inputs.get("first_name", ""),
+            "SurName": user_inputs.get("last_name", ""),
+            "LastName": user_inputs.get("last_name", ""),
+            "SurnameAtBirth": user_inputs.get("last_name", ""), # 默认同名
+            "GenderId": find_id(gender_data, user_inputs.get("gender"), "1"), # 默认 Male
+            "MaritalStatusId": find_id(marital_data, user_inputs.get("marital_status", "Single"), "1"),
+            "ServerDateOfBirth": dob,
+            
+            # 国籍/出生地
+            "PlaceOfBirth": user_inputs.get("place_of_birth", "-"),
+            "CountryOfBirthId": find_id(country_data, user_inputs.get("nationality"), "0"),
+            "NationalityAtBirthId": find_id(country_data, user_inputs.get("nationality"), "0"),
+            "NationalityId": find_id(country_data, user_inputs.get("nationality"), "0"),
+            
+            # 护照信息
+            "PassportType": "Ordinary Passport", # 默认
+            "PassportNo": user_inputs.get("passport_no", ""),
+            "ServerPassportIssueDate": ppt_issue,
+            "ServerPassportExpiryDate": ppt_expiry,
+            "IssuePlace": user_inputs.get("place_of_issue", "-"),
+            "IssueCountryId": find_id(country_data, user_inputs.get("nationality"), "0"),
+            
+            # 联系方式 (必填占位符)
+            "HomeAddressLine1": "-",
+            "HomeAddressCity": "-",
+            "HomeAddressPostalCode": "-",
+            "HomeAddressContactNumber": user_inputs.get("phone", "-"),
+            "HomeAddressCountryId": find_id(country_data, user_inputs.get("nationality"), "0"),
+            "EmployerName": "-",
+            "EmployerAddress": "-",
+            
+            # 职业
+            "CurrentOccupationId": find_id(occupation_data, user_inputs.get("occupation", "Others"), "20"), 
+            
+            # 行程信息 (部分写死为常规旅游)
+            "PurposeOfJourneyId": "Tourism", 
+            "MemberStateDestinationId": "Spain",
+            "MemberStateFirstEntryId": "Spain",
+            "NumberOfEntriesRequested": "Multiple Entries",
+            "IntendedStayDuration": "5",
+            "ServerTravelDate": travel_date,
+            "ServerIntendedDateOfArrival": travel_date,
+            "ServerIntendedDateOfDeparture": travel_date, # 简化
+            
+            # 费用承担
+            "CostCoveredById": "By the Applicant himself / herself",
+            "MeansOfSupportId": "Cash",
+            
+            # 杂项
+            "IsMinor": False,
+            "IsVisaIssuedBefore": False,
+            "BlsInvitingAuthority": "1", # 这里的 1 通常代表 "No" 或者特定枚举
+            "PreviousFingerPrintStatus": "2", # 2 通常代表 No
+            
+            # 邀请人信息 (旅游通常填酒店或空)
+            "InvitingAuthorityName": "-",
+            "InvitingAddress": "-",
+            "InvitingCity": "-",
+            "InvitingEmail": "no-reply@example.com"
+        }
+
+        # 7. 更新表单数据
+        # ApplicantsDetailsList 需要是一个 JSON 字符串
+        form_data['ApplicantsDetailsList'] = json.dumps([applicant_detail])
+        
+        # 补全其他可能需要的字段
+        form_data['PreviousFingerPrintStatus_0'] = "2"
+        form_data['BlsInvitingAuthority_0'] = "1"
+        form_data["X-Requested-With"] = "XMLHttpRequest"
+
+        # 8. 提交
+        # 注意:提交地址通常和 manage appointment 相同,或者是特定的 Save 接口
+        # 根据你的原代码,是 Global/BLSAppointment/ManageAppointment
+        url_post = f"https://{domain}/Global/BLSAppointment/ManageAppointment"
+        
+        # Headers 需要 Token
+        headers = {
+            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
+            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+            "Referer": f"https://{domain}/{referer}",
+            "X-Requested-With": "XMLHttpRequest",
+            "requestverificationtoken": token
+        }
 
-    def _read_otp_email(self, wait_sec=30):
-        # 轮询 Cloud API
-        for _ in range(wait_sec // 5):
+        # 这里的 form_data['params'] 逻辑在 _extract_hidden_fields 可能会有差异
+        # 确保 form_data 是扁平的字典
+        submit_resp = self._perform_request('POST', url_post, data=form_data, headers=headers)
+        
+        if submit_resp.json().get('success'):
+            VSC_INFO("bls_plg", "[%s] Final Form Submitted Successfully.", self.group_id)
+            return True
+        raise BizLogicError(message='Submit application form failed')
+
+    def _read_otp_email(self, wait_sec: int = 60) -> str:
+        """
+        读取 BLS 的 OTP 邮件
+        """
+        master_email = "visafly666@gmail.com"
+        recipient = self.config.account.username
+        sender = "Info@blsinternational.com"
+        subject_keywords = "BLS"
+        body_keywords = "verification code"
+
+        # 设置时间起点 (UTC)
+        now_utc = datetime.utcnow()
+        formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
+
+        VSC_INFO("bls_plg", "[%s] Waiting for OTP from %s...", self.group_id, sender)
+
+        # 轮询查收, 每 5 秒查一次
+        attempts = wait_sec // 5
+        for i in range(attempts):
+            
+            # 调用云端接口获取邮件内容
+            # expiry=300 表示邮件有效搜索窗口为 5 分钟
+            content_out = VSCloudApi.Instance().fetch_mail_content(
+                master_email,
+                sender,
+                recipient,
+                subject_keywords,
+                body_keywords,
+                formatted_utc_time,
+                300
+            )
+
+            # 正则匹配 6 位数字验证码
+            match = re.search(r'\b\d{6}\b', content_out)
+            if match:
+                otp = match.group(0)
+                VSC_INFO("bls_plg", "[%s] OTP code found: %s", self.group_id, otp)
+                return otp
+            
+            # 等待下一次轮询
             time.sleep(5)
-            # content = VSCloudApi.Instance().fetch_mail_content(...)
-            # ...
-            pass
-        return "123456" # Mock
-
-    def _save_session_to_cloud(self, sid, url):
-        cookies = json.dumps(requests.utils.dict_from_cookiejar(self.session.cookies))
-        VSCloudApi.Instance().create_http_session(sid, cookies, "", "", "", url, {})
-
-    def _get_proxy_url(self):
-        p = self.config.proxy
-        if not p.ip: return ""
-        if p.username: return f"{p.scheme}://{p.username}:{p.password}@{p.ip}:{p.port}"
-        return f"{p.scheme}://{p.ip}:{p.port}"
-
-    def _generate_id(self):
-        return "".join(random.choices(string.ascii_letters + string.digits, k=8))
+            if i % 2 == 0:
+                VSC_DEBUG("bls_plg", "[%s] OTP not received yet, retrying...", self.group_id)
+
+        # 超时处理
+        raise NotFoundError(f"OTP email not found within {wait_sec}s")
+    
+    def _save_http_session(self, page_url):
+        """
+        提取 cookies, local_storage, 存入 VSCloudApi
+        """
+        cookies_dict = {}
+        # 方式 1: curl_cffi 的 cookies 对象通常支持 get_dict()
+        if hasattr(self.session.cookies, "get_dict"):
+            cookies_dict = self.session.cookies.get_dict()
+        else:
+            # 方式 2: 迭代 (兼容标准 CookieJar)
+            for c in self.session.cookies:
+                cookies_dict[c.name] = c.value
+
+        cookies_str = json.dumps(cookies_dict)
+        
+        # 简单生成 SessionID hash
+        ua_str = self.user_agent or "unknown_ua"
+        raw = cookies_str + ua_str + page_url
+        
+        session_id = hashes.Hash(hashes.SHA256(), backend=default_backend())
+        session_id.update(raw.encode())
+        sid = session_id.finalize().hex()
+            
+        proxy_str = ""
+        if self.config.proxy.ip:
+            proxy_str = f"{self.config.proxy.scheme}://"
+        if self.config.proxy.username:
+            proxy_str += f"{self.config.proxy.username}:{self.config.proxy.password}@"
+        proxy_str += f"{self.config.proxy.ip}:{self.config.proxy.port}"
+        
+        return VSCloudApi.Instance().create_http_session(
+            sid, cookies_str, "", ua_str, proxy_str, page_url
+        )

+ 3 - 3
plugins/concrete_plugin.py

@@ -2,9 +2,9 @@
 import time
 import random
 from typing import Dict, List, Optional, Any
-from vs_plg import IVSPlg, VSError # type: ignore
-from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, QueryWaitMode # type: ignore
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN # type: ignore
+from vs_plg import IVSPlg, VSError
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, QueryWaitMode
+from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN
 
 class ConcretePlugin(IVSPlg):
     """

+ 236 - 413
plugins/tls_plugin.py

@@ -10,11 +10,10 @@ from urllib.parse import urljoin, urlparse
 from curl_cffi import requests, const
 from bs4 import BeautifulSoup
 
-# 框架依赖
-from vs_plg import IVSPlg # type: ignore
-from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, VSError, VSErrorCategory # type: ignore
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN # type: ignore
-from toolkit.vs_cloud_api import VSCloudApi # type: ignore
+from vs_plg import IVSPlg
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus,  NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
+from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN
+from toolkit.vs_cloud_api import VSCloudApi
 
 class TlsPlugin(IVSPlg):
     """
@@ -26,14 +25,11 @@ class TlsPlugin(IVSPlg):
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
-        
+        self.is_healthy = True
         # 会话相关
         self.session: Optional[requests.Session] = None
         self.travel_group: Optional[Dict] = None
         self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
-        
-        # 状态
-        self.last_error = VSError(VSErrorCategory.OK, "")
 
     def get_group_id(self) -> str:
         return self.group_id
@@ -46,32 +42,17 @@ class TlsPlugin(IVSPlg):
             self.free_config = {}
 
     def health_check(self) -> bool:
-        return self.last_error.is_healthy()
-
-    def get_last_error(self) -> VSError:
-        return self.last_error
-
-    def _set_error(self, category: VSErrorCategory, message: str, details: str = ""):
-        self.last_error = VSError(category, message, details)
-        # 日志依然可以打印详细信息
-        if category == VSErrorCategory.OK:
-            return
-        elif category in [VSErrorCategory.FATAL, VSErrorCategory.RELOGIN]:
-            VSC_ERROR("vfs_plg", "[%s] %s: %s (%s)", self.group_id, category.name, message, details)
-        else:
-            VSC_WARN("vfs_plg", "[%s] %s: %s", self.group_id, category.name, message)
+        return self.is_healthy
 
     def _save_debug_html(self, content: str, prefix: str = "debug"):
-        try:
-            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}_{self.group_id}_{timestamp}.html"
-            with open(filename, "w", encoding="utf-8") as f:
-                f.write(content)
-            VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
-        except Exception as e:
-            VSC_WARN("tls_plg", "[%s] Failed to save debug HTML: %s", self.group_id, str(e))
+        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}_{self.group_id}_{timestamp}.html"
+        with open(filename, "w", encoding="utf-8") as f:
+            f.write(content)
+        VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
             
     def _get_proxy_url(self):
         # 构造代理
@@ -84,16 +65,10 @@ class TlsPlugin(IVSPlg):
                 proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
         return proxy_url
 
-    # ---------------------------------------------------------
-    # 核心接口实现
-    # ---------------------------------------------------------
-
-    def create_session(self) -> bool:
+    def create_session(self):
         """
         创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
-        """
-        VSC_INFO("tls_plg", "[%s] Creating session...", self.group_id)
-        
+        """        
         # 1. 初始化 Session
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
@@ -109,15 +84,12 @@ class TlsPlugin(IVSPlg):
             http_version=const.CurlHttpVersion.V2TLS
         )
 
-        embassy = self._get_embassy_config()
+        embassy = self.free_config.get('center', {})
         if not embassy:
-            self._set_error(VSErrorCategory.Fatal, "Invalid embassy config")
-            return False
+            raise NotFoundError(message="center not found in free config")
 
         # 2. 解决 Cloudflare 5s 盾
-        if not self._solve_cloudflare5S_challenge():
-            self._set_error(VSErrorCategory.Fatal, "Cloudflare challenge failed")
-            return False
+        self._solve_cloudflare5S_challenge()
 
         # 3. 获取登录页面参数 (OIDC)
         login_page = "https://visas-fr.tlscontact.com/en-us/login"
@@ -133,30 +105,22 @@ class TlsPlugin(IVSPlg):
             'Referer': f'https://visas-fr.tlscontact.com/en-us/country/{embassy["country"]}/vac/{embassy["code"]}',
             'User-Agent': self.user_agent,
         }
+        resp = self._perform_request("GET", login_page, headers=headers, params=params)
+        self._save_debug_html(resp.text, 'Login_Page')
 
-        try:
-            resp = self.session.get(login_page, headers=headers, params=params)
-            if resp.status_code != 200:
-                self._set_error(VSErrorCategory.Fatal, f"Get Login Page Failed: {resp.status_code}")
-                return False
-            
-            # 解析 Keycloak 登录地址
-            soup = BeautifulSoup(resp.text, 'html.parser')
-            form = soup.find('form')
-            if not form:
-                self._set_error(VSErrorCategory.Fatal, "Login form not found")
-                return False
-            action = form.get('action')
-            authenticate_url = action if action.startswith('http') else urljoin(resp.url, action)
-
-        except Exception as e:
-            self._set_error(VSErrorCategory.Fatal, f"Network error during login init: {e}")
-            return False
+        # 解析 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:
-            VSC_WARN("tls_plg", "Missing 'capsolver_key' in free_config, captcha might fail.")
+            raise NotFoundError(message="Missing 'capsolver_key' in free_config, captcha might fail.")
         
         rc_params = {
             "type": "ReCaptchaV2TaskProxyLess", 
@@ -166,10 +130,6 @@ class TlsPlugin(IVSPlg):
             "proxy": self._get_proxy_url()
         }
         g_token = self._solve_recaptcha(rc_params)
-        if not g_token:
-            self._set_error(VSErrorCategory.Fatal, "Failed to solve Login Recaptcha")
-            return False
-
         # 5. 提交登录
         payload = {
             'username': self.config.account.username,
@@ -177,45 +137,26 @@ class TlsPlugin(IVSPlg):
             'g-recaptcha-response': g_token
         }
         headers['Content-Type'] = 'application/x-www-form-urlencoded'
-        
-        try:
-            resp = self.session.post(authenticate_url, headers=headers, data=payload)
-            if resp.status_code != 200:
-                self._set_error(VSErrorCategory.Fatal, f"Login Submit Failed: {resp.status_code}")
-                return False
-            
-            # 6. 解析 Travel Groups
-            groups = self._parse_travel_groups(resp.text)
-            if not groups:
-                if "Invalid username or password" in resp.text:
-                    self._set_error(VSErrorCategory.Fatal, "Invalid username or password")
-                else:
-                    self._set_error(VSErrorCategory.Fatal, "No Travel Groups found after login")
-                return False
+        resp = self._perform_request("POST", authenticate_url, headers=headers, data=payload)
+        self._save_debug_html(resp.text, 'Travel_Groups_Page')
+        # 6. 解析 Travel Groups
+        groups = self._parse_travel_groups(resp.text)
             
-            # 选择匹配城市的 Group
-            target_city = embassy['city'].lower()
-            for g in groups:
-                if g['location'].lower() == target_city:
-                    self.travel_group = g
-                    break
-            
-            if not self.travel_group:
-                self._set_error(VSErrorCategory.Fatal, f"No group found for city {target_city}")
-                return False
-
-            VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
-            self._set_error(VSErrorCategory.OK, "Login Success")
-            return True
-
-        except Exception as e:
-            self._set_error(VSErrorCategory.Fatal, f"Login exception: {e}")
-            return False
+        # 选择匹配城市的 Group
+        target_city = embassy['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 group found for city {target_city}")
+        VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
 
     def query(self) -> VSQueryResult:
         res = VSQueryResult()
         res.success = False
-        embassy = self._get_embassy_config()
+        embassy = self.free_config.get('center', {})
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
         
@@ -232,141 +173,58 @@ class TlsPlugin(IVSPlg):
             'user-agent': self.user_agent,
         }
 
-        try:
-            resp_text = None
-            max_retries = 3
-            for attempt in range(1, max_retries + 1):
-                resp = self.session.get(url, params=params, headers=headers)
-                # self._save_debug_html(resp.text, f"query_slot_page")
-                # 1. Cloudflare 403 → 过盾 → 重试
-                if resp.status_code == 403:
-                    VSC_WARN(
-                        "tls_plg",
-                        "[%s] Query 403 Forbidden (attempt %d/%d). Solving Cloudflare...",
-                        self.group_id, attempt, max_retries
-                    )
-                    
-                    if attempt < max_retries and self._solve_cloudflare5S_challenge():
-                        continue  # 重新请求
-                    else:
-                        self._set_error(
-                            VSErrorCategory.WAF,
-                            "Cloudflare 5s challenge failed"
-                        )
-                        return res
-                    
-                if resp.status_code == 429:
-                    self._set_error(
-                        VSErrorCategory.Fatal,
-                        "Query Limited Rate (429)"
-                    )
-                    return res
-                    
-                # 检测session 有效性
-                if resp.status_code == 401 or self._is_session_expired_page(resp.text):
-                    VSC_WARN(
-                        "tls_plg",
-                        "[%s] Session expired. URL: %s",
-                        self.group_id, resp.url
-                    )
-                    self._set_error(VSErrorCategory.RELOGIN, "Session expired")
-                    return res
-
-                # 状态码不对
-                if resp.status_code != 200:
-                    self._set_error(
-                        VSErrorCategory.ABORT,
-                        f"Query failed status: {resp.status_code}"
-                    )
-                    return res
-
-                # 检测关键词
-                is_valid_content = "availableAppointments" in resp.text
-                if not is_valid_content:
-                    self._set_error(
-                        VSErrorCategory.ABORT,
-                        f"Query unexpected result"
-                    )
-                    return res
-                
-                # === 成功拿到有效内容,跳出重试循环 ===
-                resp_text = resp.text
-                break
-
-            if not resp_text:
-                # for 循环正常结束(未 break)
-                self._set_error(
-                    VSErrorCategory.Fatal,
-                    "Query failed after max retries"
-                )
-                return res
-
-            # 3. 解析 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]
-
-            res.city = self.free_config.get('city', '')
-            res.country = self.free_config.get('country', '')
-            res.visa_type = self.free_config.get('visa_type', '')
-            res.availability_status = AvailabilityStatus.NoneAvailable
-
-            if available:
-                res.success = True
-
-                res.availability_status = AvailabilityStatus.Available
-                res.earliest_date = available[0]['date']
-
-                date_map = {}
-                for s in available:
-                    d = s['date']
-                    date_map.setdefault(d, [])
-                    ts = VSQueryResult.DateAvailability.TimeSlot()
-                    ts.time = s['time']
-                    ts.label = f"{s['type']}"
-                    date_map[d].append(ts)
-
-                for d, slots in date_map.items():
-                    da = VSQueryResult.DateAvailability()
-                    da.date = d
-                    da.times = slots
-                    res.availability.append(da)
-
-                VSC_INFO(
-                    "tls_plg",
-                    "[%s] Found %d slots",
-                    self.group_id, len(available)
-                )
-                self._set_error(VSErrorCategory.OK, "Slots found")
-            else:
-                VSC_DEBUG(
-                    "tls_plg",
-                    "[%s] Query OK, but no matching slots.",
-                    self.group_id
-                )
-                self._set_error(VSErrorCategory.EMPTY, "No matching slots")
-
-        except Exception as e:
-            self._set_error(
-                VSErrorCategory.ABORT,
-                f"Query exception: {e}"
-            )
-
+        resp = self._perform_request("GET", url, headers=headers, params=params)
+        
+        self._save_debug_html(resp.text, 'Query_Slot_Page')
+        self._check_session_expired_page(resp.text)
+
+        # 检测关键词
+        if not "availableAppointments" in resp.text:
+            raise NotFoundError(message='Query result not found availableAppointments')
+
+
+        # 3. 解析 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]
+
+        res.city = self.free_config.get('city', '')
+        res.country = self.free_config.get('country', '')
+        res.visa_type = self.free_config.get('visa_type', '')
+        res.availability_status = AvailabilityStatus.NoneAvailable
+
+        if available:
+            res.success = True
+            res.availability_status = AvailabilityStatus.Available
+            res.earliest_date = available[0]['date']
+            date_map = {}
+            for s in available:
+                d = s['date']
+                date_map.setdefault(d, [])
+                ts = VSQueryResult.DateAvailability.TimeSlot()
+                ts.time = s['time']
+                ts.label = f"{s['type']}"
+                date_map[d].append(ts)
+
+            for d, slots in date_map.items():
+                da = VSQueryResult.DateAvailability()
+                da.date = d
+                da.times = slots
+                res.availability.append(da)
+        else:
+            res.success = False
         return res
 
 
-    def book(self, slot_info: VSQueryResult, user_input: dict = None) -> VSBookResult:
-        """
-        预约
-        """
+    def book(self, slot_info: VSQueryResult, user_input: Dict = None) -> VSBookResult:
         res = VSBookResult()
         res.success = False
         target_date = slot_info.availability[0].date
         target_time = slot_info.availability[0].times[0].time
         target_label = "" 
         
-        embassy = self._get_embassy_config()
+        embassy = self.free_config.get('center', {})
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
         
@@ -383,9 +241,6 @@ class TlsPlugin(IVSPlg):
             "proxy": self._get_proxy_url()
         }
         g_token = self._solve_recaptcha(rc_params)
-        if not g_token:
-            self._set_error(VSErrorCategory.ABORT, "Failed to solve Booking Recaptcha")
-            return res
 
         # 2. 构造请求
         url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
@@ -409,7 +264,6 @@ class TlsPlugin(IVSPlg):
             random.choices("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", k=16)
         )
         headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
-        
         form_fields = {
             '1_formGroupId': str(group_num),
             '1_lang': 'en-us',
@@ -431,213 +285,182 @@ class TlsPlugin(IVSPlg):
         body_parts.append(f"--{boundary}--\r\n")
         body = "".join(body_parts).encode("utf-8")
         
-        try:
-            resp = self.session.post(url, params=params, headers=headers, data=body)
-            if resp.status_code == 303: 
-                res.success = True
-                res.book_date = target_date
-                res.book_time = target_time
-                self._set_error(VSErrorCategory.OK, "Booking Success")
-                VSC_INFO("tls_plg", "[%s] Book Success (303 Redirect)!", self.group_id)
-                return res
-            else:
-                self._set_error(VSErrorCategory.ABORT, f"Book Failed: {resp.status_code}")
-        except Exception as e:
-            self._set_error(VSErrorCategory.ABORT, f"Book exception: {e}")
-
+        resp = self.session.post(url, params=params, headers=headers, data=body, allow_redirects=False)
+        self._save_debug_html(resp.text, 'Book_Appointment_Page')
+        if resp.status_code == 303: 
+            res.success = True
+            res.book_date = target_date
+            res.book_time = target_time
+            return res
+        else:
+            VSC_WARN('tls_plg', 'Expected Status is 303, but got {resp.status_code}')
+            res.success = False            
         return res
+    
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
+        """
+        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
+        1. 发送 OPTIONS 请求
+        2. 发送实际请求
+        """
+        print(f'[perform request] {method} {url}')
+        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
+        VSC_INFO('tls_plg', resp.text)
+        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()
+        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 _get_embassy_config(self) -> Dict:
-        if "embassy_code" in self.free_config:
-             return {
-                 "code": self.free_config.get("embassy_code"),
-                 "country": self.free_config.get("country_code"),
-                 "mission": self.free_config.get("mission_code", "fr"),
-                 "city": self.free_config.get("city")
-             }
-        return {}
-
-    def _solve_cloudflare5S_challenge(self) -> bool:
+    def _solve_cloudflare5S_challenge(self):
         """
         解决 Cloudflare 5s 盾
-        使用 VSCloudApi 的 submit_anticloudflare_task
         """
-        VSC_INFO("tls_plg", "[%s] Solving Cloudflare 5s...", self.group_id)
-        embassy = self._get_embassy_config()
+        VSC_INFO("tls_plg", f"[{self.group_id}] Solving Cloudflare 5s...")
+        embassy = self.free_config.get('center', {})
         website_url = f'https://visas-fr.tlscontact.com/en-us/country/{embassy["country"]}'
         
-        # 1. 格式化代理字符串
-        # 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
-        # self.config.proxy 结构体里的数据
+        # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
         p = self.config.proxy
-        if not p.ip:
-            VSC_ERROR("tls_plg", "Proxy is required for Cloudflare challenge")
-            return False
-            
-        # 构造 user:pass@ip:port 用于 urlparse (方便解析) 或者直接拼接
-        # 你的独立脚本中是: f'{parsed_proxy.hostname}:{parsed_proxy.port}:{parsed_proxy.username}:{parsed_proxy.password}'
-        # VSPlgConfig 中的 proxy 对象字段: ip, port, username, password
-        
         if p.username:
             proxy_str = f"{p.ip}:{p.port}:{p.username}:{p.password}"
         else:
             proxy_str = f"{p.ip}:{p.port}"
-            
         # 2. 提交任务
         task = VSCloudApi.Instance().submit_anticloudflare_task(proxy_str, website_url)
-        if not task or not task.get('id'):
-            VSC_ERROR("tls_plg", "Failed to submit AntiCloudflareTask")
-            return False
-            
-        # 2. 等待结果
+        # 3. 等待结果
         task_id = str(task['id'])
         result = VSCloudApi.Instance().get_anticloudflare_result(task_id)
-        
-        if result:
-            try:
-                parsed = json.loads(result.get('result', '{}'))
-                cookies_list = parsed.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 = parsed.get('userAgent')
-                if ua:
-                    self.user_agent = ua
-                    self.session.headers['User-Agent'] = ua
-                
-                VSC_INFO("tls_plg", "[%s] Cloudflare 5s challenge solved.", self.group_id)
-                return True
-            except Exception as e:
-                VSC_ERROR("tls_plg", f"Failed to parse Cloudflare result: {e}")
-        
-        return False
+        parsed = json.loads(result.get('result', '{}'))
+        cookies_list = parsed.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 = parsed.get('userAgent')
+        if ua:
+            self.user_agent = ua
+            self.session.headers['User-Agent'] = ua
+        VSC_INFO("tls_plg", "[%s] Cloudflare 5s challenge solved.", self.group_id)
 
-    def _solve_recaptcha(self, params) -> Optional[str]:
+    def _solve_recaptcha(self, params) -> str:
         """
         调用 Capsolver
         """
-        try:
-            key = params.get("apiToken")
-            if not key: 
-                VSC_ERROR("tls_plg", "Api-token is required for recaptcha solver")
-                return None
-            
-            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
-            
-            payload = {"clientKey": key, "task": task}
-            r = requests.post(submit_url, json=payload, timeout=20)
-            if r.status_code != 200:
-                VSC_ERROR("tls_plg", "Failed to submit capsolver task")
-                return None
-            
-            task_id = r.json().get("taskId")
-            if not task_id:
-                VSC_ERROR("tls_plg", "Failed to get taskId")
-                return None
-            
-            # Query
-            for _ in range(20):
-                r = requests.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)
-        except Exception as e:
-            VSC_ERROR("tls_plg", f"Capsolver error: {e}")
+        key = params.get("apiToken")
+        if not key: 
+            raise NotFoundError(message="Api-token is required for recaptcha solver")
+        
+        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")
             
-        return None
+        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
+        
+        payload = {"clientKey": key, "task": task}
+        r = requests.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)
+            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 = []
-        try:
-            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')
-                    })
-        except:
-            pass
+        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:
+            VSC_WARN('tls_plg', 'Parsed travel group page, but not found travelGroups')
         return groups
 
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
         slots = []
-        try:
-            # 增强正则
-            pattern = r'availableAppointments\\?":\s*(\[.*?\])(?:,\\?"|\},)'
-            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 = ""
-                        stype = ""
-                        cost = ""
-                        
-                        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
-                             })
-        except Exception as e:
-            VSC_DEBUG("tls_plg", f"Slot parse error: {e}")
-            pass
+        pattern = r'availableAppointments\\?":\s*(\[.*?\])(?:,\\?"|\},)'
+        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 = ""
+                    stype = ""
+                    cost = ""
+                    
+                    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
+                            })
+        else:
+            VSC_WARN("tls_plg", 'Parsed appointment slots page, but not found availableAppointments')
         return slots
 
-    def _is_session_expired_page(self, html: str) -> bool:
-        if not html: return False
+    def _check_session_expired_page(self, html: str) -> bool:
+        if not html:
+            self.is_healthy = False
+            raise SessionExpiredOrInvalidError()
         if 'availableAppointments' not in html: 
-            if 'redirected automatically' in html.lower(): return True
-            if 'login' in html.lower() and 'password' in html.lower(): return True
-            return True
-        return False
+            if 'redirected automatically' in html.lower():
+                self.is_healthy = False
+                raise SessionExpiredOrInvalidError()
+            if 'login' in html.lower() and 'password' 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():
+                self.is_healthy = False
+                raise SessionExpiredOrInvalidError()

Fișier diff suprimat deoarece este prea mare
+ 232 - 522
plugins/vfs_plugin.py


+ 1 - 1
predict_server.py

@@ -269,7 +269,7 @@ class RequestHandler(BaseHTTPRequestHandler):
             self._send_response(404, 'text/plain', b'Not Found')
 
 if __name__ == '__main__':
-    MODEL_PATH = 'data/ocr.pth'
+    MODEL_PATH = 'data/ctc.pth'
     PORT = 8085
     
     # 1. PyTorch

+ 1 - 1
toolkit/account_manager.py

@@ -4,7 +4,7 @@ import json
 import os
 import random
 from typing import Optional, Dict, Any, List
-from vs_log_macros import VSC_DEBUG, VSC_WARN, VSC_INFO, VSC_ERROR # type: ignore
+from vs_log_macros import VSC_DEBUG, VSC_WARN, VSC_INFO, VSC_ERROR
 
 class AccountManager:
     """

+ 1 - 1
toolkit/binding_manager.py

@@ -2,7 +2,7 @@ import threading
 import json
 import os
 from typing import List, Optional, Tuple, Dict, Any
-from vs_log_macros import VSC_DEBUG, VSC_INFO, VSC_WARN, VSC_ERROR # type: ignore
+from vs_log_macros import VSC_DEBUG, VSC_INFO, VSC_WARN, VSC_ERROR
 
 class BindingManager:
     """

+ 1 - 1
toolkit/proxy_manager.py

@@ -4,7 +4,7 @@ import json
 import os
 import random
 from typing import List, Optional, Dict, Any
-from vs_log_macros import VSC_DEBUG, VSC_WARN, VSC_INFO, VSC_ERROR # type: ignore
+from vs_log_macros import VSC_DEBUG, VSC_WARN, VSC_INFO, VSC_ERROR
 
 class ProxyManager:
     """

+ 1 - 1
toolkit/thread_pool.py

@@ -1,7 +1,7 @@
 # toolkit/thread_pool.py
 import threading
 from concurrent.futures import ThreadPoolExecutor, Future
-from vs_log_macros import VSC_INFO, VSC_ERROR # type: ignore
+from vs_log_macros import VSC_INFO, VSC_ERROR
 
 class ThreadPool:
     """

+ 1 - 1
toolkit/vs_cloud_api.py

@@ -4,7 +4,7 @@ import json
 import time
 import urllib.parse
 from typing import Dict, Any, Optional
-from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_DEBUG # type: ignore
+from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_DEBUG
 
 class VSCloudApi:
     """

+ 1 - 1
vs_plg.py

@@ -1,6 +1,6 @@
 # vs_plg.py
 from abc import ABC, abstractmethod
-from vs_types import VSPlgConfig, VSQueryResult, VSBookResult # type: ignore
+from vs_types import VSPlgConfig, VSQueryResult, VSBookResult
 
 # ================== 接口类 ==================
 class IVSPlg(ABC):

+ 2 - 2
vs_plg_factory.py

@@ -2,8 +2,8 @@
 import importlib.util
 import sys
 import os
-from vs_plg import IVSPlg # type: ignore
-from vs_log_macros import VSC_ERROR, VSC_INFO # type: ignore
+from vs_plg import IVSPlg
+from vs_log_macros import VSC_ERROR, VSC_INFO
 
 class VSPlgFactory:
     """

+ 8 - 5
vs_types.py

@@ -22,6 +22,14 @@ class NotFoundError(BizException):
     def __init__(self, message="Resource not found"):
         super().__init__(code=40401, message=message, http_status=404)
 
+class RateLimiteddError(BizException):
+    def __init__(self, message="Rate limited"):
+        super().__init__(code=42901, message=message, http_status=429)
+        
+class SessionExpiredOrInvalidError(BizException):
+    def __init__(self, message="Session expired or invalid"):
+        super().__init__(code=40101, message=message, http_status=401)
+
 class PermissionDeniedError(BizException):
     def __init__(self, message="Permission denied"):
         super().__init__(code=40301, message=message, http_status=403)
@@ -127,13 +135,8 @@ class VSQueryResult:
 @dataclass
 class VSBookResult:
     success: bool = False
-    order_id: str = ""
     session_id: str = ""
-    user_inputs: str = ""
     account: str = ""
-    visa_type: str = ""
-    city: str = ""
-    country: str = ""
     book_date: str = ""
     book_time: str = ""
     fee_amount: int = 0

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff