Browse Source

faet: update

jerry 4 months ago
parent
commit
71c2982d61
17 changed files with 549 additions and 439 deletions
  1. 2 1
      .gitignore
  2. 101 74
      core/app_manager.py
  3. 59 83
      gco.py
  4. 79 0
      gco_wrapper.py
  5. 10 34
      main.py
  6. 1 1
      main_server.py
  7. 24 20
      plugins/bls_plugin.py
  8. 0 127
      plugins/concrete_plugin.py
  9. 22 24
      plugins/de_plugin.py
  10. 20 21
      plugins/tls_plugin.py
  11. 33 38
      plugins/vfs_plugin.py
  12. 51 0
      test/regex_test.py
  13. 1 1
      toolkit/proxy_manager.py
  14. 2 4
      toolkit/vs_cloud_api.py
  15. 8 0
      vs_plg.py
  16. 90 1
      vs_types.py
  17. 46 10
      web/server.py

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
 __pycache__
 __pycache__
 debug_pages
 debug_pages
 logs
 logs
-.DS_Store
+.DS_Store
+*.jpg

+ 101 - 74
core/app_manager.py

@@ -5,8 +5,8 @@ import threading
 from typing import Dict, List, Optional
 from typing import Dict, List, Optional
 
 
 # 引入核心组件
 # 引入核心组件
-from vs_types import GroupConfig, QueryWaitConfig, PluginConfig, QueryWaitMode
-from group_coordinator import GroupCoordinator
+from vs_types import GroupConfig
+from gco_wrapper import GCOWrapper
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_WARN
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_WARN
 from core.plugin_reloader import reload_plugin_module
 from core.plugin_reloader import reload_plugin_module
 
 
@@ -26,8 +26,8 @@ class AppManager:
         return AppManager()
         return AppManager()
 
 
     def _init(self):
     def _init(self):
-        self.coordinators: Dict[str, GroupCoordinator] = {}
-        self.configs: Dict[str, GroupConfig] = {}
+        self.executors: Dict[str, GCOWrapper] = {}   # group_id -> GCOWrapper
+        self.configs: Dict[str, GroupConfig] = {}    # group_id -> GroupConfig
         self.config_file = "config/groups.json"
         self.config_file = "config/groups.json"
 
 
     def load_configs(self):
     def load_configs(self):
@@ -42,38 +42,8 @@ class AppManager:
             
             
             for item in data:
             for item in data:
                 # JSON -> GroupConfig 转换
                 # JSON -> GroupConfig 转换
-                qw_data = item.get("query_wait", {})
-                qw_cfg = QueryWaitConfig(
-                    mode=QueryWaitMode(qw_data.get("mode", 0)),
-                    fixed_wait=qw_data.get("fixed_wait", 0),
-                    random_min=qw_data.get("random_min", 0),
-                    random_max=qw_data.get("random_max", 0)
-                )
-
-                plg_data = item.get("plugin_config", {})
-                plg_cfg = PluginConfig(
-                    lib_path=plg_data.get("lib_path", "plugins"),
-                    plugin_name=plg_data.get("plugin_name", ""),
-                    plugin_bin=plg_data.get("plugin_bin", ""),
-                    plugin_proto=plg_data.get("plugin_proto", "IVSPlg")
-                )
-
-                grp_cfg = GroupConfig(
-                    identifier=item["identifier"],
-                    debug=item.get("debug", False),
-                    enable=item.get("enable", False),
-                    need_account=item.get("need_account", False),
-                    account_built_in=item.get("account_built_in", True),
-                    account_pool=item.get("account_pool", ""),
-                    need_proxy=item.get("need_proxy", False),
-                    proxy_pool=item.get("proxy_pool", ""),
-                    target_instances=item.get("target_instances", 1),
-                    account_login_interval=item.get("account_login_interval", 0),
-                    query_wait=qw_cfg,
-                    plugin_config=plg_cfg,
-                    free_config=json.dumps(item.get("free_config", "{}"))
-                )
-                
+                grp_cfg = GroupConfig.from_json(item)
+                print(grp_cfg.free_config)
                 self.configs[grp_cfg.identifier] = grp_cfg
                 self.configs[grp_cfg.identifier] = grp_cfg
                 
                 
             VSC_INFO("app_mgr", f"Loaded {len(self.configs)} group configurations.")
             VSC_INFO("app_mgr", f"Loaded {len(self.configs)} group configurations.")
@@ -84,51 +54,51 @@ class AppManager:
     def start_all(self):
     def start_all(self):
         """启动所有 enable=True 的组"""
         """启动所有 enable=True 的组"""
         for gid, cfg in self.configs.items():
         for gid, cfg in self.configs.items():
-            if cfg.enable and gid not in self.coordinators:
+            if cfg.enable and gid not in self.executors:
                 self.start_group(gid)
                 self.start_group(gid)
 
 
     def start_group(self, group_id: str) -> bool:
     def start_group(self, group_id: str) -> bool:
-        if group_id not in self.configs:
-            VSC_ERROR("app_mgr", f"Group {group_id} not found in config")
-            return False
-        
-        if group_id in self.coordinators:
-            VSC_WARN("app_mgr", f"Group {group_id} is already running")
-            return True
-
-        cfg = self.configs[group_id]
-        try:
-            coord = GroupCoordinator(cfg)
-            # 设置推送回调,这里可以连接到 WebSocket
-            coord.set_push_callback(lambda t, d, s: print(f"[{group_id} PUSH] {d.decode()}"))
+        with self._lock:
+            if group_id not in self.configs:
+                VSC_ERROR("app_mgr", f"Group {group_id} not found in config")
+                return False
             
             
-            coord.start()
-            self.coordinators[group_id] = coord
-            VSC_INFO("app_mgr", f"Started group: {group_id}")
-            return True
-        except Exception as e:
-            VSC_ERROR("app_mgr", f"Failed to start group {group_id}: {e}")
-            return False
+            if group_id in self.executors:
+                VSC_WARN("app_mgr", f"Group {group_id} is already running")
+                return True
+
+            cfg = self.configs[group_id]
+            try:
+                gco = GCOWrapper(cfg)
+                gco.load()
+                gco.start()
+                self.executors[group_id] = gco
+                VSC_INFO("app_mgr", f"Started group: {group_id}")
+                return True
+            except Exception as e:
+                VSC_ERROR("app_mgr", f"Failed to start group {group_id}: {e}")
+                return False
 
 
     def stop_group(self, group_id: str) -> bool:
     def stop_group(self, group_id: str) -> bool:
-        if group_id not in self.coordinators:
-            return False
-        
-        VSC_INFO("app_mgr", f"Stopping group: {group_id}...")
-        try:
-            self.coordinators[group_id].stop()
-            del self.coordinators[group_id]
-            VSC_INFO("app_mgr", f"Stopped group: {group_id}")
-            return True
-        except Exception as e:
-            VSC_ERROR("app_mgr", f"Error stopping group {group_id}: {e}")
-            return False
+        with self._lock:
+            if group_id not in self.executors:
+                return False
+            
+            VSC_INFO("app_mgr", f"Stopping group: {group_id}...")
+            try:
+                self.executors[group_id].stop()
+                del self.executors[group_id]
+                VSC_INFO("app_mgr", f"Stopped group: {group_id}")
+                return True
+            except Exception as e:
+                VSC_ERROR("app_mgr", f"Error stopping group {group_id}: {e}")
+                return False
 
 
     def restart_group(self, group_id: str) -> bool:
     def restart_group(self, group_id: str) -> bool:
         self.stop_group(group_id)
         self.stop_group(group_id)
         return self.start_group(group_id)
         return self.start_group(group_id)
 
 
-    def ota_update_plugin(self, plugin_name: str) -> List[str]:
+    def ota_upgrade_plugin(self, plugin_name: str) -> List[str]:
         """
         """
         OTA 热更新流程:
         OTA 热更新流程:
         1. 找到所有使用该插件的运行中组
         1. 找到所有使用该插件的运行中组
@@ -139,10 +109,10 @@ class AppManager:
         affected_groups = []
         affected_groups = []
         
         
         # 1. 查找受影响的组
         # 1. 查找受影响的组
-        for gid, coord in self.coordinators.items():
+        for gid, exec in self.executors.items():
             # 注意:这里我们访问 coord 私有成员,实际工程中建议添加 getter
             # 注意:这里我们访问 coord 私有成员,实际工程中建议添加 getter
             # 假设 config 存储在 m_cfg
             # 假设 config 存储在 m_cfg
-            if coord.m_cfg.plugin_config.plugin_name == plugin_name:
+            if exec.m_cfg.plugin_config.plugin_name == plugin_name:
                 affected_groups.append(gid)
                 affected_groups.append(gid)
         
         
         VSC_INFO("app_mgr", f"OTA Update for '{plugin_name}'. Affected groups: {affected_groups}")
         VSC_INFO("app_mgr", f"OTA Update for '{plugin_name}'. Affected groups: {affected_groups}")
@@ -161,12 +131,46 @@ class AppManager:
                 restarted.append(gid)
                 restarted.append(gid)
                 
                 
         return restarted
         return restarted
+    
+    def get_group_config(self, group_id: str):
+        with self._lock:
+            if group_id not in self.configs:
+                return False
+            group_config = self.configs.get(group_id)
+            return group_config.to_json()
+    
+    # ----------------- 更新配置(局部) -----------------
+    def ota_update_plugin_config(self, group_id: str, new_config_str: str) -> bool:
+        """更新某个 Executor 的配置(只影响单个 Executor)"""
+        new_config = GroupConfig.from_json(json.loads(new_config_str))
+        with self._lock:
+            old_exe = self.executors.get(group_id)
+            if not old_exe:
+                # Executor 没运行,直接创建启动
+                exe = GCOWrapper(new_config)
+                exe.load()
+                exe.start()
+                self.executors[group_id] = exe
+                return True
+
+            # 创建新 Executor(插件不变,只更新配置)
+            new_exe = GCOWrapper(new_config)
+            new_exe.load()
+            try:
+                new_exe.start()
+                old_exe.stop()
+                self.executors[group_id] = new_exe
+                VSC_INFO("app_mgr", f"Config update applied for {group_id}")
+                return True
+            except Exception as e:
+                VSC_ERROR("app_mgr", f"Failed to update config for {group_id}: {e}")
+                return False
 
 
     def get_status(self):
     def get_status(self):
         """获取所有组的状态"""
         """获取所有组的状态"""
         status_list = []
         status_list = []
         for gid, cfg in self.configs.items():
         for gid, cfg in self.configs.items():
-            running = gid in self.coordinators
+            running = gid in self.executors
             status_list.append({
             status_list.append({
                 "id": gid,
                 "id": gid,
                 "plugin": cfg.plugin_config.plugin_name,
                 "plugin": cfg.plugin_config.plugin_name,
@@ -174,4 +178,27 @@ class AppManager:
                 "instances": cfg.target_instances if running else 0,
                 "instances": cfg.target_instances if running else 0,
                 "account_pool": cfg.account_pool
                 "account_pool": cfg.account_pool
             })
             })
-        return status_list
+        return status_list
+    
+    def subscribe_executor_logs(self, group_id: str, callback):
+        """
+        订阅某个 Executor 的日志
+        callback: Callable[[str], None]
+        """
+        if group_id not in self.executors:
+            raise ValueError(f"Executor {group_id} not running")
+
+        exe = self.executors[group_id]
+        exe.subscribe_logs(callback)
+
+
+    def unsubscribe_executor_logs(self, group_id: str, callback):
+        """
+        取消订阅日志
+        """
+        exe = self.executors.get(group_id)
+        if not exe:
+            return
+        exe.unsubscribe_logs(callback)
+
+    

+ 59 - 83
group_coordinator.py → gco.py

@@ -1,10 +1,10 @@
-# group_coordinator.py
+# gco.py
 import os
 import os
 import time
 import time
 import json
 import json
 import random
 import random
 import threading
 import threading
-from typing import List, Optional
+from typing import List, Optional, Callable
 from concurrent.futures import wait
 from concurrent.futures import wait
 
 
 # 导入所有依赖
 # 导入所有依赖
@@ -16,18 +16,18 @@ from toolkit.proxy_manager import ProxyManager
 from toolkit.binding_manager import BindingManager 
 from toolkit.binding_manager import BindingManager 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.vs_cloud_api import VSCloudApi
-from vs_log_macros import VSC_INFO, VSC_DEBUG, VSC_WARN, VSC_ERROR 
 
 
 
 
-class GroupCoordinator:
+class GCO:
     """
     """
-    @brief GroupCoordinator
+    @brief GCO
     负责管理一个组内的签证插件实例,包括实例的创建、健康检查、
     负责管理一个组内的签证插件实例,包括实例的创建、健康检查、
     任务调度、查询和预订流程。
     任务调度、查询和预订流程。
     """
     """
-    def __init__(self, cfg: GroupConfig):
+    def __init__(self, cfg: GroupConfig, logger: Callable[[str], None] = None):
         self.m_cfg = cfg
         self.m_cfg = cfg
         self.m_factory = VSPlgFactory() # 插件工厂实例
         self.m_factory = VSPlgFactory() # 插件工厂实例
+        self.m_logger = logger
         
         
         self.m_tasks: List[Task] = [] # 存储所有运行中的任务实例
         self.m_tasks: List[Task] = [] # 存储所有运行中的任务实例
         
         
@@ -36,29 +36,16 @@ class GroupCoordinator:
         
         
         self.m_monitor_thread: Optional[threading.Thread] = None
         self.m_monitor_thread: Optional[threading.Thread] = None
         self.m_creator_thread: Optional[threading.Thread] = None
         self.m_creator_thread: Optional[threading.Thread] = None
-        
-        # 预订操作的线程池,独立于任务调度
-        self.book_executor = ThreadPool(max_workers=5).getInstance()
-
-        VSC_INFO("coordinator", f"GroupCoordinator for {self.m_cfg.identifier} initialized.")
-
-    def set_push_callback(self, cb):
-        """
-        @brief 设置推送回调函数 (C++中的PushCallback)
-        Python中可以直接传递可调用对象。
-        """
-        self.push_callback_ = cb
-        VSC_INFO("coordinator", f"Push callback set for group {self.m_cfg.identifier}.")
 
 
     def start(self):
     def start(self):
         """
         """
         @brief 启动协调器,包括插件注册和线程启动。
         @brief 启动协调器,包括插件注册和线程启动。
         """
         """
         if not self.m_cfg.enable:
         if not self.m_cfg.enable:
-            VSC_WARN("coordinator", f"Group {self.m_cfg.identifier} is disabled, not starting.")
+            self._log("Group is disabled, not starting.")
             return
             return
 
 
-        VSC_INFO("coordinator", f"Starting coordinator for group {self.m_cfg.identifier}...")
+        self._log("Starting coordinator...")
         self.m_stop_event.clear()
         self.m_stop_event.clear()
 
 
         # 注册插件
         # 注册插件
@@ -70,55 +57,47 @@ class GroupCoordinator:
         class_name = "".join(part.title() for part in plugin_name.split('_'))
         class_name = "".join(part.title() for part in plugin_name.split('_'))
         
         
         # 调试日志:确认推导出的类名
         # 调试日志:确认推导出的类名
-        VSC_DEBUG("coordinator", f"Inferring class name for plugin {plugin_name}: {class_name}")
+        self._log(f"Inferring class name for plugin {plugin_name}: {class_name}")
 
 
         self.m_factory.register_plugin(plugin_name, 
         self.m_factory.register_plugin(plugin_name, 
                                        plugin_module_path, 
                                        plugin_module_path, 
                                        class_name)
                                        class_name)
 
 
-        self.m_monitor_thread = threading.Thread(target=self.monitor_loop, name=f"Monitor-{self.m_cfg.identifier}")
-        self.m_creator_thread = threading.Thread(target=self.creator_loop, name=f"Creator-{self.m_cfg.identifier}")
+        self.m_monitor_thread = threading.Thread(target=self._monitor_loop, name=f"Monitor-{self.m_cfg.identifier}")
+        self.m_creator_thread = threading.Thread(target=self._creator_loop, name=f"Creator-{self.m_cfg.identifier}")
         
         
         self.m_monitor_thread.start()
         self.m_monitor_thread.start()
         self.m_creator_thread.start()
         self.m_creator_thread.start()
-        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} threads started.")
+        self._log("Coordinator threads started.")
 
 
     def stop(self):
     def stop(self):
         """
         """
         @brief 停止协调器,等待所有线程结束。
         @brief 停止协调器,等待所有线程结束。
         """
         """
-        VSC_INFO("coordinator", f"Stopping coordinator for group {self.m_cfg.identifier}...")
+        self._log("Stopping coordinator...")
         self.m_stop_event.set() # 发送停止信号
         self.m_stop_event.set() # 发送停止信号
         
         
         if self.m_monitor_thread and self.m_monitor_thread.is_alive():
         if self.m_monitor_thread and self.m_monitor_thread.is_alive():
             self.m_monitor_thread.join()
             self.m_monitor_thread.join()
         if self.m_creator_thread and self.m_creator_thread.is_alive():
         if self.m_creator_thread and self.m_creator_thread.is_alive():
             self.m_creator_thread.join()
             self.m_creator_thread.join()
-        
-        # 关闭预订线程池
-        self.book_executor.shutdown(wait=True)
-        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} stopped.")
-
-    def restart(self):
-        """
-        @brief 重启协调器。
-        """
-        VSC_INFO("coordinator", f"Restarting coordinator for group {self.m_cfg.identifier}...", )
-        self.stop()
-        self.start()
-        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} restarted.")
+        self._log("Coordinator stopped.")
 
 
     def group_id(self) -> str:
     def group_id(self) -> str:
         """
         """
         @brief 获取分组ID。
         @brief 获取分组ID。
         """
         """
         return self.m_cfg.identifier
         return self.m_cfg.identifier
+    
+    def _log(self, message):
+        if self.m_logger:
+            self.m_logger(f'[gco] [{self.m_cfg.identifier}] {message}')
 
 
-    def monitor_loop(self):
+    def _monitor_loop(self):
         """
         """
         @brief 监控循环:定期检查实例健康状况,执行查询任务,并根据结果触发预订。
         @brief 监控循环:定期检查实例健康状况,执行查询任务,并根据结果触发预订。
         """
         """
-        VSC_INFO("coordinator", f"[START] monitor loop starting for group {self.m_cfg.identifier}")
+        self._log("[START] monitor loop starting...")
         rng = random.Random()
         rng = random.Random()
         
         
         while not self.m_stop_event.is_set():
         while not self.m_stop_event.is_set():
@@ -148,19 +127,19 @@ class GroupCoordinator:
                 try:
                 try:
                     result = task.instance.query()
                     result = task.instance.query()
                     if result.success:
                     if result.success:
-                        # === 关键修改:on_query_result 现在会阻塞直到抢票结束 ===
-                        self.on_query_result(task.instance, result)
+                        # === 关键修改:_on_query_result 现在会阻塞直到抢票结束 ===
+                        self._on_query_result(task.instance, result)
                         is_booking_triggered = True
                         is_booking_triggered = True
                     else:
                     else:
-                        VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query done, No availability found")
+                        self._log("Query done, No availability found")
                 except Exception as e:
                 except Exception as e:
-                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
+                    self._log(f"Exception during query: {e}")
 
 
                 # 计算下次运行时间
                 # 计算下次运行时间
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
                 if is_booking_triggered:
                 if is_booking_triggered:
                     interval = rng.randint(30, 60) # 抢完票休息 30-60 秒
                     interval = rng.randint(30, 60) # 抢完票休息 30-60 秒
-                    VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Booking attempted, entering cooldown for {interval} sec.")
+                    self._log(f"Booking attempted, entering cooldown for {interval} sec.")
                 else:
                 else:
                     interval = 30
                     interval = 30
                     mode = task.qw_cfg.mode
                     mode = task.qw_cfg.mode
@@ -178,15 +157,15 @@ class GroupCoordinator:
                 initial_size = len(self.m_tasks)
                 initial_size = len(self.m_tasks)
                 self.m_tasks[:] = [t for t in self.m_tasks if t.instance.health_check()]
                 self.m_tasks[:] = [t for t in self.m_tasks if t.instance.health_check()]
                 if len(self.m_tasks) < initial_size:
                 if len(self.m_tasks) < initial_size:
-                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Removed {initial_size - len(self.m_tasks)} unhealthy instance(s). Remaining: {len(self.m_tasks)}")
+                    self._log(f"Removed {initial_size - len(self.m_tasks)} unhealthy instance(s). Remaining: {len(self.m_tasks)}")
 
 
-        VSC_INFO("coordinator", f"[STOP] monitor loop exiting for group {self.m_cfg.identifier}")
+        self._log("[STOP] monitor loop exiting...")
 
 
-    def creator_loop(self):
+    def _creator_loop(self):
         """
         """
         @brief 创建者循环:根据目标实例数量,创建和补充新的插件实例。
         @brief 创建者循环:根据目标实例数量,创建和补充新的插件实例。
         """
         """
-        VSC_INFO("coordinator", f"[START] creator loop starting for group {self.m_cfg.identifier}")
+        self._log("[START] creator loop starting...")
         
         
         while not self.m_stop_event.is_set():
         while not self.m_stop_event.is_set():
             time.sleep(0.1) # 避免空转太快
             time.sleep(0.1) # 避免空转太快
@@ -197,12 +176,12 @@ class GroupCoordinator:
                 diff = self.m_cfg.target_instances - current_instances_count
                 diff = self.m_cfg.target_instances - current_instances_count
             
             
             if diff > 0:
             if diff > 0:
-                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}")
+                self._log(f"Need to create {diff} new instance(s). Current: {current_instances_count}")
                 
                 
                 # 准备配置
                 # 准备配置
                 plg_cfg = self._make_plg_config()
                 plg_cfg = self._make_plg_config()
                 if not plg_cfg:
                 if not plg_cfg:
-                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Failed to prepare plugin configuration, sleeping 30s.")
+                    self._log("Failed to prepare plugin configuration, sleeping 30s.")
                     time.sleep(30) # 等待资源 (账户/代理) 恢复
                     time.sleep(30) # 等待资源 (账户/代理) 恢复
                     continue
                     continue
 
 
@@ -220,23 +199,23 @@ class GroupCoordinator:
                                 next_run=time.time() # 立即执行第一次查询
                                 next_run=time.time() # 立即执行第一次查询
                             )
                             )
                             self.m_tasks.append(new_task)
                             self.m_tasks.append(new_task)
-                            VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] New instance added. Total instances: {len(self.m_tasks)}")
+                            self._log(f"New instance added. Total instances: {len(self.m_tasks)}")
                         else:
                         else:
-                            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Target instances already met, discarding newly created instance.")
+                            self._log("Target instances already met, discarding newly created instance.")
                 else:
                 else:
-                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] Failed to create plugin instance.")
+                    self._log("Failed to create plugin instance.")
                     # 可以在这里添加重试逻辑或错误处理
                     # 可以在这里添加重试逻辑或错误处理
 
 
             # 模拟创建间隔,避免瞬间创建过多实例
             # 模拟创建间隔,避免瞬间创建过多实例
             time.sleep(random.uniform(1.0, 5.0))
             time.sleep(random.uniform(1.0, 5.0))
 
 
-        VSC_INFO("coordinator", f"[STOP] creator loop exiting for group {self.m_cfg.identifier}")
+        self._log("[STOP] creator loop exiting...")
 
 
     def _make_plg_config(self) -> Optional[VSPlgConfig]:
     def _make_plg_config(self) -> Optional[VSPlgConfig]:
         """
         """
         @brief 准备插件配置 (账号、代理等)。
         @brief 准备插件配置 (账号、代理等)。
         """
         """
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Preparing plugin configuration...")
+        self._log("Preparing plugin configuration...")
         plg_cfg = VSPlgConfig()
         plg_cfg = VSPlgConfig()
         plg_cfg.debug = self.m_cfg.debug
         plg_cfg.debug = self.m_cfg.debug
         
         
@@ -244,13 +223,13 @@ class GroupCoordinator:
         if self.m_cfg.need_account:
         if self.m_cfg.need_account:
             account = AccountManager.Instance().get_next_account(self.m_cfg.account_pool)
             account = AccountManager.Instance().get_next_account(self.m_cfg.account_pool)
             if not account:
             if not account:
-                VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available accounts for pool {self.m_cfg.account_pool}")
+                self._log(f"No available accounts for pool {self.m_cfg.account_pool}")
                 return None
                 return None
             plg_cfg.account.id = account["id"]
             plg_cfg.account.id = account["id"]
             plg_cfg.account.username = account["username"]
             plg_cfg.account.username = account["username"]
             plg_cfg.account.password = account["password"]
             plg_cfg.account.password = account["password"]
             plg_cfg.account.lock_until = account.get("lock_until", "")
             plg_cfg.account.lock_until = account.get("lock_until", "")
-            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Using account ID {plg_cfg.account.id}, username {plg_cfg.account.username}")
+            self._log(f"Using account ID {plg_cfg.account.id}, username {plg_cfg.account.username}")
 
 
         # 代理配置
         # 代理配置
         if self.m_cfg.need_proxy:
         if self.m_cfg.need_proxy:
@@ -261,22 +240,22 @@ class GroupCoordinator:
                     bounded_ids = BindingManager.Instance().get_bounded_proxies_ids(self.m_cfg.account_pool, self.m_cfg.proxy_pool)
                     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)
                     proxy = ProxyManager.Instance().get_unbind_proxy(self.m_cfg.proxy_pool, bounded_ids)
                     if not proxy:
                     if not proxy:
-                        VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available unbind proxy in pool {self.m_cfg.proxy_pool}")
+                        self._log(f"No available unbind proxy in pool {self.m_cfg.proxy_pool}")
                         return None
                         return None
                     BindingManager.Instance().create_binding(
                     BindingManager.Instance().create_binding(
                         self.m_cfg.account_pool, plg_cfg.account.id,
                         self.m_cfg.account_pool, plg_cfg.account.id,
                         self.m_cfg.proxy_pool, proxy["id"], "dynamic")
                         self.m_cfg.proxy_pool, proxy["id"], "dynamic")
-                    VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Created dynamic binding: account {plg_cfg.account.id} -> proxy {proxy['id']}")
+                    self._log(f"Created dynamic binding: account {plg_cfg.account.id} -> proxy {proxy['id']}")
                 else:
                 else:
                     all_proxies_in_pool = ProxyManager.Instance()._proxies.get(self.m_cfg.proxy_pool, [])
                     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)
                     proxy = next((p for p in all_proxies_in_pool if p["id"] == proxy_id), None)
                     if not proxy:
                     if not proxy:
-                         VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Bounded proxy ID {proxy_id} not found in pool {self.m_cfg.proxy_pool}")
-                         return None
+                        self._log(f"Bounded proxy ID {proxy_id} not found in pool {self.m_cfg.proxy_pool}")
+                        return None
             else:
             else:
                 proxy = ProxyManager.Instance().get_next_proxy(self.m_cfg.proxy_pool)
                 proxy = ProxyManager.Instance().get_next_proxy(self.m_cfg.proxy_pool)
                 if not proxy:
                 if not proxy:
-                    VSC_WARN("coordinator", f"[{self.m_cfg.identifier}] No available proxy in pool {self.m_cfg.proxy_pool}")
+                    self._log(f"No available proxy in pool {self.m_cfg.proxy_pool}")
                     return None
                     return None
 
 
             plg_cfg.proxy.id = proxy["id"]
             plg_cfg.proxy.id = proxy["id"]
@@ -286,10 +265,10 @@ class GroupCoordinator:
             plg_cfg.proxy.username = proxy.get("username", "")
             plg_cfg.proxy.username = proxy.get("username", "")
             plg_cfg.proxy.password = proxy.get("password", "")
             plg_cfg.proxy.password = proxy.get("password", "")
             plg_cfg.proxy.lock_until = proxy.get("lock_until", "")
             plg_cfg.proxy.lock_until = proxy.get("lock_until", "")
-            VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Using proxy ID {plg_cfg.proxy.id}, IP {plg_cfg.proxy.ip}:{plg_cfg.proxy.port}")
+            self._log(f"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
         plg_cfg.free_config = self.m_cfg.free_config
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Plugin configuration prepared.")
+        self._log("Plugin configuration prepared.")
         return plg_cfg
         return plg_cfg
 
 
     def _create_instance(self, plg_cfg: VSPlgConfig) -> Optional[IVSPlg]:
     def _create_instance(self, plg_cfg: VSPlgConfig) -> Optional[IVSPlg]:
@@ -297,22 +276,23 @@ class GroupCoordinator:
         # @brief 创建并初始化单个插件实例。
         # @brief 创建并初始化单个插件实例。
         # 这个方法在 creator_loop 的线程池中执行。
         # 这个方法在 creator_loop 的线程池中执行。
         # """
         # """
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
+        self._log(f"Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
         try:
         try:
             inst = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
             inst = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
+            inst.set_log(self.m_logger)
             inst.set_config(plg_cfg)
             inst.set_config(plg_cfg)
             inst.create_session()
             inst.create_session()
             if self.m_cfg.need_account and self.m_cfg.account_login_interval > 0:
             if self.m_cfg.need_account and self.m_cfg.account_login_interval > 0:
                 AccountManager.Instance().lock_account(
                 AccountManager.Instance().lock_account(
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
-            VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
+            self._log("Plugin instance created and session established.")
             return inst
             return inst
         except Exception as e:
         except Exception as e:
-            VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
+            self._log(f"Error creating plugin instance: {e}")
         return None
         return None
 
 
-    def on_query_result(self, sptr: IVSPlg, query_result: VSQueryResult):
-        VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")
+    def _on_query_result(self, sptr: IVSPlg, query_result: VSQueryResult):
+        self._log(f"Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")
         
         
         # 定义内部预订任务
         # 定义内部预订任务
         def book_task(inst: IVSPlg, result: VSQueryResult):
         def book_task(inst: IVSPlg, result: VSQueryResult):
@@ -325,14 +305,14 @@ class GroupCoordinator:
                 task = VSCloudApi.Instance().get_vas_task_pop(booking_routing_key)
                 task = VSCloudApi.Instance().get_vas_task_pop(booking_routing_key)
                 
                 
                 if not task:
                 if not task:
-                    VSC_WARN("coordinator", f"[{inst.get_group_id()}] No pending task found for key {booking_routing_key}. Abandoning slot.")
+                    self._log(f"No pending task found for key {booking_routing_key}. Abandoning slot.")
                     return 
                     return 
 
 
                 task_id = task['id']
                 task_id = task['id']
                 order_id = task['order_id']
                 order_id = task['order_id']
                 user_input = task.get('user_inputs', {})
                 user_input = task.get('user_inputs', {})
                 
                 
-                VSC_INFO("coordinator", f"[{inst.get_group_id()}] Picked up Task ID {task_id} for booking...")
+                self._log(f"Picked up Task ID {task_id} for booking...")
 
 
                 # 2. 执行预订
                 # 2. 执行预订
                 # 注意:插件的 book 方法需要接收 user_input
                 # 注意:插件的 book 方法需要接收 user_input
@@ -340,11 +320,7 @@ class GroupCoordinator:
 
 
                 # 3. 处理结果
                 # 3. 处理结果
                 if book_res.success:
                 if book_res.success:
-                    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: {order_id}".encode('utf-8'), 0) 
+                    self._log(f" Booking SUCCESS! Order: {order_id}")
                     
                     
                     # 4. 成功逻辑:更新任务状态为 grabbed
                     # 4. 成功逻辑:更新任务状态为 grabbed
                     # 包含后端需要的关键信息
                     # 包含后端需要的关键信息
@@ -366,23 +342,23 @@ class GroupCoordinator:
                     }
                     }
                     
                     
                     VSCloudApi.Instance().update_vas_task(task_id, update_data)
                     VSCloudApi.Instance().update_vas_task(task_id, update_data)
-                    VSC_INFO("coordinator", f"[{inst.get_group_id()}] Task {task_id} marked as GRABBED.")
+                    self._log(f"Task {task_id} marked as GRABBED.")
                     # 成功后 task_id 置空,防止 finally 块再次将其重置为 pending
                     # 成功后 task_id 置空,防止 finally 块再次将其重置为 pending
                     task_id = None 
                     task_id = None 
             except Exception as e:
             except Exception as e:
-                VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Exception during booking: {e.message}")
+                self._log(f"Exception during booking: {e}")
             
             
             finally:
             finally:
                 # 5. Return to Queue (回滚机制)
                 # 5. Return to Queue (回滚机制)
                 if task_id is not None:
                 if task_id is not None:
-                    VSC_WARN("coordinator", f"[{inst.get_group_id()}] Returning Task {task_id} to queue (status=pending).")
+                    self._log(f"Returning Task {task_id} to queue (status=pending).")
                     try:
                     try:
                         VSCloudApi.Instance().return_vas_task_to_queue(task_id)
                         VSCloudApi.Instance().return_vas_task_to_queue(task_id)
                     except Exception as ex:
                     except Exception as ex:
-                        VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Failed to return task to queue: {ex}")
+                        self.log(f"Failed to return task to queue: {ex}")
 
 
         futures = []
         futures = []
-        f = self.book_executor.enqueue(book_task, sptr, query_result)
+        f = ThreadPool.getInstance().enqueue(book_task, sptr, query_result)
         futures.append(f)
         futures.append(f)
         
         
         wait(futures)
         wait(futures)

+ 79 - 0
gco_wrapper.py

@@ -0,0 +1,79 @@
+import threading
+import collections
+from enum import Enum, auto
+from typing import Callable
+from gco import GCO
+from vs_types import GroupConfig
+from vs_log_macros import VSC_INFO
+
+
+class State(Enum):
+    CREATED = auto()
+    LOADED = auto()
+    RUNNING = auto()
+    STOPPING = auto()
+    STOPPED = auto()
+    FAILED = auto()
+
+class GCOWrapper:
+    def __init__(self, gco_cfg: GroupConfig):
+        self.m_cfg = gco_cfg
+        self._gco = None
+        self._state = State.CREATED
+        self._log_queue = collections.deque(maxlen=1000)
+        self._log_subscribers = set()
+        self._lock = threading.Lock()
+        self._push_callback: Callable = lambda t,d,s: None
+        
+    def set_push_callback(self, callback: Callable):
+        self._push_callback = callback
+        
+    def _log(self, msg: str):
+        with self._lock:
+            VSC_INFO("-", msg)
+            self._log_queue.append(msg)
+            for sub in self._log_subscribers:
+                try:
+                    sub(msg)
+                except Exception:
+                    pass
+        # 原有 push callback
+        self._push_callback("LOG", msg.encode(), self._state)
+        
+    def get_logs(self, last_n=50):
+        with self._lock:
+            return list(self._log_queue)[-last_n:]
+
+    def subscribe_logs(self, callback: Callable):
+        with self._lock:
+            self._log_subscribers.add(callback)
+
+    def unsubscribe_logs(self, callback: Callable):
+        with self._lock:
+            self._log_subscribers.discard(callback)
+
+    def _transition(self, from_state, to_state):
+        if self._state != from_state:
+            raise Exception(f"invalid transition {self._state} -> {to_state}")
+        print(f"[STATE] {self._state.name} -> {to_state.name}")
+        self._state = to_state
+
+    def load(self):
+        self._transition(State.CREATED, State.LOADED)
+
+    def start(self):
+        self._transition(State.LOADED, State.RUNNING)
+        try:
+            self._gco = GCO(self.m_cfg, logger=self._log)
+            self._gco.start()  # 真正的 start 工作
+        except Exception:
+            self._state = State.FAILED
+            raise
+
+    def stop(self):
+        self._transition(State.RUNNING, State.STOPPING)
+        try:
+            self._gco.stop()  # 原逻辑 stop
+        finally:
+            self._transition(State.STOPPING, State.STOPPED)
+

+ 10 - 34
main.py

@@ -6,7 +6,7 @@ import logging
 
 
 # 导入必要模块
 # 导入必要模块
 from vs_types import GroupConfig, QueryWaitMode, PluginConfig, QueryWaitConfig
 from vs_types import GroupConfig, QueryWaitMode, PluginConfig, QueryWaitConfig
-from group_coordinator import GroupCoordinator
+from gco import GCO
 from vs_log_macros import VSC_INFO, VSC_ERROR
 from vs_log_macros import VSC_INFO, VSC_ERROR
 
 
 def vfs_test():
 def vfs_test():
@@ -80,22 +80,14 @@ def vfs_test():
     )
     )
 
 
     # 5. 创建协调器
     # 5. 创建协调器
-    coordinator = GroupCoordinator(group_config)
-
-    # 6. 设置回调 (模拟 WebSocket 推送或日志记录)
-    def my_push_callback(type_code: int, data: bytes, size: int):
-        msg = data.decode('utf-8')
-        VSC_INFO("main", ">>> [PUSH CALLBACK] Type: %d, Message: %s", type_code, msg)
-    
-    coordinator.set_push_callback(my_push_callback)
-
+    gco = GCO(group_config)
     # 7. 启动
     # 7. 启动
     try:
     try:
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "   VFS Python Plugin Tester      ")
         VSC_INFO("main", "   VFS Python Plugin Tester      ")
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "========================================")
         
         
-        coordinator.start()
+        gco.start()
         
         
         time.sleep(3600)
         time.sleep(3600)
     except KeyboardInterrupt:
     except KeyboardInterrupt:
@@ -104,7 +96,7 @@ def vfs_test():
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
     finally:
     finally:
         # 8. 停止
         # 8. 停止
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Program finished.")
         VSC_INFO("main", "Program finished.")
         
         
         
         
@@ -162,22 +154,14 @@ def tls_test():
     )
     )
 
 
     # 5. 创建协调器
     # 5. 创建协调器
-    coordinator = GroupCoordinator(group_config)
-
-    # 6. 设置回调 (模拟 WebSocket 推送或日志记录)
-    def my_push_callback(type_code: int, data: bytes, size: int):
-        msg = data.decode('utf-8')
-        VSC_INFO("main", ">>> [PUSH CALLBACK] Type: %d, Message: %s", type_code, msg)
-    
-    coordinator.set_push_callback(my_push_callback)
-
+    gco = GCO(group_config)
     # 7. 启动
     # 7. 启动
     try:
     try:
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "    TLS Python Plugin Tester      ")
         VSC_INFO("main", "    TLS Python Plugin Tester      ")
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "========================================")
         
         
-        coordinator.start()
+        gco.start()
         
         
         time.sleep(3600)
         time.sleep(3600)
     except KeyboardInterrupt:
     except KeyboardInterrupt:
@@ -186,7 +170,7 @@ def tls_test():
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
     finally:
     finally:
         # 8. 停止
         # 8. 停止
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Program finished.")
         VSC_INFO("main", "Program finished.")
         
         
 def bls_test():
 def bls_test():
@@ -254,19 +238,11 @@ def bls_test():
     )
     )
 
 
     # 6. 创建协调器
     # 6. 创建协调器
-    coordinator = GroupCoordinator(group_config)
-
-    # 7. 设置回调 (监听抢票成功消息)
-    def push_callback(type_code, data, size):
-        msg = data.decode('utf-8')
-        VSC_INFO("CALLBACK", f"Received Push: Type={type_code}, Msg={msg}")
-    
-    coordinator.set_push_callback(push_callback)
-
+    gco = GCO(group_config)
     # 8. 启动运行
     # 8. 启动运行
     try:
     try:
         VSC_INFO("main", ">>> Starting BLS Plugin Test...")
         VSC_INFO("main", ">>> Starting BLS Plugin Test...")
-        coordinator.start()
+        gco.start()
         
         
         time.sleep(3600)
         time.sleep(3600)
     except KeyboardInterrupt:
     except KeyboardInterrupt:
@@ -274,7 +250,7 @@ def bls_test():
     except Exception as e:
     except Exception as e:
         VSC_ERROR("main", f"Unexpected Error: {e}")
         VSC_ERROR("main", f"Unexpected Error: {e}")
     finally:
     finally:
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Test Finished.")
         VSC_INFO("main", "Test Finished.")
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":

+ 1 - 1
main_server.py

@@ -27,7 +27,7 @@ def main():
     # 后台的 GroupCoordinators 已经在各自的线程里跑了。
     # 后台的 GroupCoordinators 已经在各自的线程里跑了。
     VSC_INFO("main", "Starting Web API on port 8000...")
     VSC_INFO("main", "Starting Web API on port 8000...")
     try:
     try:
-        # run_web_server()
+        run_web_server()
         while True:
         while True:
             time.sleep(3600)
             time.sleep(3600)
     except KeyboardInterrupt:
     except KeyboardInterrupt:

+ 24 - 20
plugins/bls_plugin.py

@@ -9,7 +9,7 @@ import string
 from datetime import datetime, timedelta
 from datetime import datetime, timedelta
 from pathlib import Path
 from pathlib import Path
 from urllib.parse import urlparse, parse_qs, urlencode
 from urllib.parse import urlparse, parse_qs, urlencode
-from typing import Dict, List, Optional, Any
+from typing import Dict, List, Optional, Any, Callable
 
 
 from curl_cffi import requests, const
 from curl_cffi import requests, const
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup
@@ -21,7 +21,6 @@ from cryptography.hazmat.backends import default_backend
 # 框架依赖
 # 框架依赖
 from vs_plg import IVSPlg 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
 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 toolkit.vs_cloud_api import VSCloudApi 
 
 
 class BlsPlugin(IVSPlg):
 class BlsPlugin(IVSPlg):
@@ -33,6 +32,8 @@ class BlsPlugin(IVSPlg):
         self.group_id = group_id
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
+        self.logger = None
+        
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = None
         
         
         # 运行时状态
         # 运行时状态
@@ -44,14 +45,13 @@ class BlsPlugin(IVSPlg):
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
+    
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
 
     def set_config(self, config: VSPlgConfig):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.config = config
-        try:
-            self.free_config = json.loads(config.free_config) if config.free_config else {}
-        except:
-            self.free_config = {}
-        
+        self.free_config = config.free_config or {}
         # 从配置中读取 OCR 服务地址,如果没有则使用默认
         # 从配置中读取 OCR 服务地址,如果没有则使用默认
         if self.free_config.get("local_service_url"):
         if self.free_config.get("local_service_url"):
             self.local_service_url = self.free_config["local_service_url"]
             self.local_service_url = self.free_config["local_service_url"]
@@ -289,7 +289,7 @@ class BlsPlugin(IVSPlg):
         headers.pop('requestverificationtoken')
         headers.pop('requestverificationtoken')
         slots_data = sorted(slots_resp.json(), key=lambda x: -x["Count"]) # 选剩余最多的
         slots_data = sorted(slots_resp.json(), key=lambda x: -x["Count"]) # 选剩余最多的
         if not slots_data or slots_data[0]['Count'] <= 0:
         if not slots_data or slots_data[0]['Count'] <= 0:
-            VSC_WARN('bls', 'Available slot times not found')
+            self._log('Available slot times not found')
             res.success = False
             res.success = False
             return res
             return res
         
         
@@ -325,9 +325,13 @@ class BlsPlugin(IVSPlg):
         res.session_id = session_data['session_id']
         res.session_id = session_data['session_id']
         res.book_date = target_date
         res.book_date = target_date
         res.book_time = target_time
         res.book_time = target_time
-        VSC_INFO("bls_plg", "[%s] Book Success. Liveness URL: %s", self.group_id, res.payment_link)
+        self._log(f"Book Success. Liveness URL: {res.payment_link}")
         return res
         return res
     
     
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[BlsPlugin] [{self.group_id}] {message}')
+    
     def _get_proxy_url(self):
     def _get_proxy_url(self):
             # 构造代理
             # 构造代理
         proxy_url = ""
         proxy_url = ""
@@ -347,7 +351,7 @@ class BlsPlugin(IVSPlg):
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
             f.write(content)
-        VSC_INFO("bls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+        self._log(f"HTML saved to: {filename}")
     
     
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
         """
         """
@@ -358,7 +362,7 @@ class BlsPlugin(IVSPlg):
 
 
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         if self.config.debug:
         if self.config.debug:
-            VSC_INFO('bls_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
+            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:
@@ -417,7 +421,7 @@ class BlsPlugin(IVSPlg):
             if ocr_resp.status_code == 200:
             if ocr_resp.status_code == 200:
                 res_json = ocr_resp.json()
                 res_json = ocr_resp.json()
                 ocr_res = res_json.get('data', '').replace('$', '')[:3]
                 ocr_res = res_json.get('data', '').replace('$', '')[:3]
-                VSC_INFO("bls_plg", f'ocr captcha id={sid} result={ocr_res}, target={numbers}')
+                self._log(f'ocr captcha id={sid} result={ocr_res}, target={numbers}')
                 if ocr_res == numbers:
                 if ocr_res == numbers:
                     selected_ids.append(sid)
                     selected_ids.append(sid)
             else:
             else:
@@ -426,7 +430,7 @@ class BlsPlugin(IVSPlg):
             raise BizLogicError(message='Captcha selected ids is empty')
             raise BizLogicError(message='Captcha selected ids is empty')
         
         
         # 3. 提交选中结果
         # 3. 提交选中结果
-        VSC_INFO("bls_plg", f'select_ids={selected_ids}')
+        self._log(f'select_ids={selected_ids}')
         form = self._extract_hidden_fields(soup)
         form = self._extract_hidden_fields(soup)
         form['SelectedImages'] = ",".join(selected_ids)
         form['SelectedImages'] = ",".join(selected_ids)
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
@@ -440,7 +444,7 @@ class BlsPlugin(IVSPlg):
                 return resp.json()['cd']
                 return resp.json()['cd']
         else:
         else:
             # 存盘所有错误验证码后续进行数据分析
             # 存盘所有错误验证码后续进行数据分析
-            VSC_WARN('bls_plg', 'Captcha Selection Invalid, Saving important data to data/bls_captcha')
+            self._log('Captcha Selection Invalid, Saving important data to data/bls_captcha')
             for img in soup.select("img.captcha-img"):
             for img in soup.select("img.captcha-img"):
                 src = img.get("src", "")
                 src = img.get("src", "")
                 if not src.startswith("data:image"):
                 if not src.startswith("data:image"):
@@ -458,7 +462,7 @@ class BlsPlugin(IVSPlg):
                 name = inp.get("name")
                 name = inp.get("name")
                 if name: params[name] = inp.get("value", "")
                 if name: params[name] = inp.get("value", "")
         else:
         else:
-            VSC_WARN('bls_plg', 'Form element not found')
+            self._log('Form element not found')
         return params
         return params
 
 
     def _extract_js_var(self, html, context, pattern):
     def _extract_js_var(self, html, context, pattern):
@@ -484,7 +488,7 @@ class BlsPlugin(IVSPlg):
                 if match:
                 if match:
                     return json.loads(match.group(1))
                     return json.loads(match.group(1))
             except Exception as e:
             except Exception as e:
-                VSC_DEBUG("bls_plg", f"Failed to parse JS var {var_name}: {e}")
+                self._log(f"Failed to parse JS var {var_name}: {e}")
             return []
             return []
         
         
         # 读取配置
         # 读取配置
@@ -793,7 +797,7 @@ class BlsPlugin(IVSPlg):
         submit_resp = self._perform_request('POST', url_post, data=form_data, headers=headers)
         submit_resp = self._perform_request('POST', url_post, data=form_data, headers=headers)
         
         
         if submit_resp.json().get('success'):
         if submit_resp.json().get('success'):
-            VSC_INFO("bls_plg", "[%s] Final Form Submitted Successfully.", self.group_id)
+            self._log("Final Form Submitted Successfully.")
             return True
             return True
         raise BizLogicError(message='Submit application form failed')
         raise BizLogicError(message='Submit application form failed')
 
 
@@ -811,7 +815,7 @@ class BlsPlugin(IVSPlg):
         now_utc = datetime.utcnow()
         now_utc = datetime.utcnow()
         formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
         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)
+        self._log(f"Waiting for OTP from {sender}...")
 
 
         # 轮询查收, 每 5 秒查一次
         # 轮询查收, 每 5 秒查一次
         attempts = wait_sec // 5
         attempts = wait_sec // 5
@@ -833,13 +837,13 @@ class BlsPlugin(IVSPlg):
             match = re.search(r'\b\d{6}\b', content_out)
             match = re.search(r'\b\d{6}\b', content_out)
             if match:
             if match:
                 otp = match.group(0)
                 otp = match.group(0)
-                VSC_INFO("bls_plg", "[%s] OTP code found: %s", self.group_id, otp)
+                self._log("OTP code found: {otp}")
                 return otp
                 return otp
             
             
             # 等待下一次轮询
             # 等待下一次轮询
             time.sleep(5)
             time.sleep(5)
             if i % 2 == 0:
             if i % 2 == 0:
-                VSC_DEBUG("bls_plg", "[%s] OTP not received yet, retrying...", self.group_id)
+                self._log("OTP not received yet, retrying...")
 
 
         # 超时处理
         # 超时处理
         raise NotFoundError(f"OTP email not found within {wait_sec}s")
         raise NotFoundError(f"OTP email not found within {wait_sec}s")

+ 0 - 127
plugins/concrete_plugin.py

@@ -1,127 +0,0 @@
-# plugins/concrete_plugin.py
-import time
-import random
-from typing import Dict, List, Optional, Any
-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):
-    """
-    @brief 具体的签证插件实现示例
-    实现了 IVSPlg 接口,模拟签证查询和预订逻辑。
-    """
-    def __init__(self, group_id: str):
-        self._group_id = group_id
-        self._config: Optional[VSPlgConfig] = None
-        self._is_healthy = True
-        self._last_error: VSError = VSError(0, "No error")
-        VSC_INFO("plugin", "[%s] ConcretePlugin initialized.", self._group_id)
-
-    def set_config(self, config: VSPlgConfig):
-        """
-        @brief 设置 API 的配置信息
-        @param config 签证 API 配置对象
-        """
-        self._config = config
-        VSC_DEBUG("plugin", "[%s] Config set: Account ID=%d, Proxy IP=%s:%d",
-                  self._group_id, config.account.id, config.proxy.ip, config.proxy.port)
-
-    def create_session(self) -> bool:
-        """
-        @brief 创建一个新的会话
-        模拟登录耗时,有概率失败。
-        """
-        VSC_INFO("plugin", "[%s] Creating session...", self._group_id)
-        time.sleep(0.5) # 模拟耗时
-        if random.random() < 0.1: # 10% 概率创建失败
-            self._last_error = VSError(1001, "Session creation failed due to network error.")
-            VSC_ERROR("plugin", "[%s] Session creation failed.", self._group_id)
-            return False
-        
-        VSC_INFO("plugin", "[%s] Session created successfully.", self._group_id)
-        return True
-
-    def query(self) -> VSQueryResult:
-        """
-        @brief 查询可用的签证预约信息
-        模拟查询,有10%概率找到可预约时段。
-        """
-        VSC_DEBUG("plugin", "[%s] Performing query...", self._group_id)
-        # 模拟查询耗时
-        time.sleep(random.uniform(0.1, 0.5)) 
-
-        result = VSQueryResult()
-        if random.random() < 0.1: # 10% 概率找到可预约
-            result.success = True
-            result.availability_status = AvailabilityStatus.Available
-            result.earliest_date = "2025-10-01"
-            result.visa_type = "Shengen"
-            result.city = "Paris"
-            result.country = "France"
-            
-            # 模拟可用时间
-            slot1 = VSQueryResult.DateAvailability.TimeSlot(time="09:00", label="Standard")
-            slot2 = VSQueryResult.DateAvailability.TimeSlot(time="10:30", label="Premium")
-            date_avail = VSQueryResult.DateAvailability(date="2025-10-01", times=[slot1, slot2])
-            result.availability.append(date_avail)
-            
-            VSC_INFO("plugin", "[%s] Query found availability for %s", self._group_id, result.earliest_date)
-        else:
-            result.success = False
-            self._last_error = VSError(2001, "No availability found at this time.")
-            VSC_DEBUG("plugin", "[%s] Query found no availability.", self._group_id)
-        
-        return result
-
-    def book(self, slot_info: VSQueryResult) -> VSBookResult:
-        """
-        @brief 进行预约操作
-        模拟预订,有90%概率成功。
-        """
-        VSC_INFO("plugin", "[%s] Attempting to book based on query result...", self._group_id)
-        time.sleep(random.uniform(1.0, 2.0)) # 模拟预订耗时
-
-        book_result = VSBookResult()
-        if random.random() < 0.9: # 90% 概率预订成功
-            book_result.success = True
-            book_result.order_id = f"ORD-{self._group_id}-{random.randint(10000, 99999)}"
-            book_result.session_id = "SESSION-XYZ"
-            book_result.account = self._config.account.username if self._config else "N/A"
-            book_result.visa_type = slot_info.visa_type
-            book_result.city = slot_info.city
-            book_result.country = slot_info.country
-            book_result.book_date = slot_info.earliest_date
-            book_result.book_time = slot_info.availability[0].times[0].time if slot_info.availability else "N/A"
-            book_result.fee_amount = 10000 # 100 EUR
-            book_result.fee_currency = "EUR"
-            book_result.payment_link = "https://example.com/payment/xyz"
-            VSC_INFO("plugin", "[%s] Booking successful! Order ID: %s", self._group_id, book_result.order_id)
-        else:
-            book_result.success = False
-            self._last_error = VSError(3001, "Booking failed due to concurrent access or payment issue.")
-            VSC_ERROR("plugin", "[%s] Booking failed.", self._group_id)
-        
-        return book_result
-
-    def get_group_id(self) -> str:
-        """
-        @brief 获取当前 API 实例所属的分组 ID
-        """
-        return self._group_id
-
-    def health_check(self) -> bool:
-        """
-        @brief 健康检查,模拟1%的概率变为不健康状态
-        """
-        if random.random() < 0.01: # 1% 概率实例变得不健康
-            self._is_healthy = False
-            self._last_error = VSError(4001, "Instance became unhealthy unexpectedly.")
-            VSC_WARN("plugin", "[%s] Instance is now unhealthy.", self._group_id)
-        return self._is_healthy
-
-    def get_last_error(self) -> VSError:
-        """
-        @brief 获取最近一次操作的错误信息
-        """
-        return self._last_error

+ 22 - 24
plugins/de_plugin.py

@@ -5,7 +5,7 @@ import re
 import os
 import os
 import base64
 import base64
 from datetime import datetime
 from datetime import datetime
-from typing import List, Dict, Optional, Any
+from typing import List, Dict, Optional, Any, Callable
 from urllib.parse import urljoin
 from urllib.parse import urljoin
 
 
 from curl_cffi import requests, const
 from curl_cffi import requests, const
@@ -14,7 +14,6 @@ from bs4 import BeautifulSoup
 
 
 from vs_plg import IVSPlg 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 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 toolkit.vs_cloud_api import VSCloudApi 
 
 
 
 
@@ -43,7 +42,7 @@ class DePlugin(IVSPlg):
         self.group_id = group_id
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
-        
+        self.logger = None
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = 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.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"
         
         
@@ -61,13 +60,13 @@ class DePlugin(IVSPlg):
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
+    
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
 
     def set_config(self, config: VSPlgConfig):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.config = config
-        try:
-            self.free_config = json.loads(config.free_config) if config.free_config else {}
-        except:
-            self.free_config = {}
+        self.free_config = config.free_config or {}
             
             
         if self.free_config.get("base_url"):
         if self.free_config.get("base_url"):
             self.base_url = self.free_config["base_url"].rstrip('/')
             self.base_url = self.free_config["base_url"].rstrip('/')
@@ -134,12 +133,12 @@ class DePlugin(IVSPlg):
         if resp.status_code != 200:
         if resp.status_code != 200:
             raise BizLogicError(message='Captcha ocr server failed')
             raise BizLogicError(message='Captcha ocr server failed')
         captcha_code = resp.json().get('data', '').replace('$', '')
         captcha_code = resp.json().get('data', '').replace('$', '')
-        VSC_INFO("de_plg", "[%s] Captcha recognized: %s", self.group_id, captcha_code)
+        self._log(f"Captcha recognized: {captcha_code}")
 
 
         # 4. 提交验证码 (/appointment-form)
         # 4. 提交验证码 (/appointment-form)
         # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
         # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
         self._submit_captcha(captcha_code)
         self._submit_captcha(captcha_code)
-        VSC_INFO("de_plg", "[%s] Session created successfully.", self.group_id)
+        self._log("Session created successfully.")
 
 
     def query(self) -> VSQueryResult:
     def query(self) -> VSQueryResult:
         """
         """
@@ -170,18 +169,14 @@ class DePlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
                 break  # ✅ 请求成功,跳出重试循环
 
 
             except PermissionDeniedError:
             except PermissionDeniedError:
-                VSC_WARN(
-                    "de_plg",
-                    "[Visamtric] getdate blocked (403), attempt %d/%d",
-                    attempt, max_retries
-                )
+                self._log(f"Getdate blocked (403), attempt {attempt}/{max_retries}")
 
 
                 # 最后一次就不再绕盾了
                 # 最后一次就不再绕盾了
                 if attempt >= max_retries:
                 if attempt >= max_retries:
                     raise PermissionDeniedError()
                     raise PermissionDeniedError()
 
 
                 self._solve_cloudflare5S_challenge()
                 self._solve_cloudflare5S_challenge()
-                VSC_INFO("de_plg", "[Visamtric] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
                 continue
 
 
         # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
         # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
@@ -224,11 +219,11 @@ class DePlugin(IVSPlg):
             raise NotFoundError(message="No dates match user constraints")
             raise NotFoundError(message="No dates match user constraints")
         
         
         target_date = random.choice(valid_dates)
         target_date = random.choice(valid_dates)
-        VSC_INFO("de_plg", "[%s] Selected date: %s", self.group_id, target_date)
+        self._log(f"Selected date: {target_date}")
         
         
         # 2. 获取时间 (/senddate)
         # 2. 获取时间 (/senddate)
         time_slot = self._get_slot_time(target_date)
         time_slot = self._get_slot_time(target_date)
-        VSC_INFO("de_plg", "[%s] Selected time: %s", self.group_id, time_slot['time'])
+        self._log(f"Selected time: {time_slot['time']}")
 
 
         # 3. 触发邮件流程 (Step 1: /jky45fgd)
         # 3. 触发邮件流程 (Step 1: /jky45fgd)
         alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
         alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
@@ -260,12 +255,15 @@ class DePlugin(IVSPlg):
         
         
         if match:
         if match:
             res.payment_link = match.group(0)
             res.payment_link = match.group(0)
-            VSC_INFO("de_plg", "[%s] Payment Link Found: %s", self.group_id, res.payment_link)
+            self._log(f"Payment Link Found: {res.payment_link}")
         return res
         return res
 
 
     # ---------------------------------------------------------
     # ---------------------------------------------------------
     # 辅助方法
     # 辅助方法
     # ---------------------------------------------------------
     # ---------------------------------------------------------
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[DePlugin] [{self.group_id}] {message}')
     
     
     def _get_headers(self) -> Dict[str, str]:
     def _get_headers(self) -> Dict[str, str]:
         """基础 Header"""
         """基础 Header"""
@@ -297,7 +295,7 @@ class DePlugin(IVSPlg):
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
             f.write(content)
-        VSC_INFO("de_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+        self._log(f"HTML saved to: {filename}")
 
 
     def _submit_captcha(self, code):
     def _submit_captcha(self, code):
         url = f"{self.base_url}/en/appointment-form"
         url = f"{self.base_url}/en/appointment-form"
@@ -402,7 +400,7 @@ class DePlugin(IVSPlg):
         now_utc = datetime.utcnow()
         now_utc = datetime.utcnow()
         formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
         formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
 
 
-        VSC_INFO("de_plg", "[%s] Waiting for OTP email sent after %s...", self.group_id, formatted_utc_time)
+        self._log(f"Waiting for OTP email sent after {formatted_utc_time}...")
 
 
         # 3. 轮询查收
         # 3. 轮询查收
         for i in range(12):
         for i in range(12):
@@ -420,7 +418,7 @@ class DePlugin(IVSPlg):
                 match = re.search(r'\b\d{6}\b', content_out)
                 match = re.search(r'\b\d{6}\b', content_out)
                 if match:
                 if match:
                     otp = match.group(0)
                     otp = match.group(0)
-                    VSC_INFO("de_plg", "[%s] OTP code found: %s", self.group_id, otp)
+                    self._log(f"OTP code found: {otp}")
                     return otp
                     return otp
             
             
             time.sleep(5)
             time.sleep(5)
@@ -526,7 +524,7 @@ class DePlugin(IVSPlg):
         """
         """
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         if self.config.debug:
         if self.config.debug:
-            VSC_INFO('de_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
+            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code in [401, 419]:
         elif resp.status_code in [401, 419]:
@@ -544,7 +542,7 @@ class DePlugin(IVSPlg):
         """
         """
         解决 Cloudflare 5s 盾
         解决 Cloudflare 5s 盾
         """
         """
-        VSC_INFO("de_plg", f"[{self.group_id}] Solving Cloudflare 5s...")
+        self._log(f"Solving Cloudflare 5s...")
         website_url = f'{self.base_url}/en'
         website_url = f'{self.base_url}/en'
         
         
         # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
         # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
@@ -572,4 +570,4 @@ class DePlugin(IVSPlg):
         if ua:
         if ua:
             self.user_agent = ua
             self.user_agent = ua
             self.session.headers['User-Agent'] = ua
             self.session.headers['User-Agent'] = ua
-        VSC_INFO("de_plg", "[%s] Cloudflare 5s challenge solved.", self.group_id)
+        self._log("Cloudflare 5s challenge solved.")

+ 20 - 21
plugins/tls_plugin.py

@@ -4,7 +4,7 @@ import random
 import re
 import re
 import os
 import os
 from datetime import datetime
 from datetime import datetime
-from typing import List, Dict, Optional, Any
+from typing import List, Dict, Optional, Any, Callable
 from urllib.parse import urljoin, urlparse
 from urllib.parse import urljoin, urlparse
 
 
 from curl_cffi import requests, const
 from curl_cffi import requests, const
@@ -12,7 +12,6 @@ from bs4 import BeautifulSoup
 
 
 from vs_plg import IVSPlg
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 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 toolkit.vs_cloud_api import VSCloudApi
 
 
 class TlsPlugin(IVSPlg):
 class TlsPlugin(IVSPlg):
@@ -26,6 +25,7 @@ class TlsPlugin(IVSPlg):
         self.config: Optional[VSPlgConfig] = None
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
         self.is_healthy = True
         self.is_healthy = True
+        self.logger = None
         # 会话相关
         # 会话相关
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = None
         self.travel_group: Optional[Dict] = None
         self.travel_group: Optional[Dict] = None
@@ -33,13 +33,13 @@ class TlsPlugin(IVSPlg):
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
+    
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
 
     def set_config(self, config: VSPlgConfig):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.config = config
-        try:
-            self.free_config = json.loads(config.free_config) if config.free_config else {}
-        except:
-            self.free_config = {}
+        self.free_config = config.free_config or {}
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         return self.is_healthy
         return self.is_healthy
@@ -134,7 +134,7 @@ class TlsPlugin(IVSPlg):
         
         
         if not self.travel_group:
         if not self.travel_group:
             raise NotFoundError(message=f"No matched group found for city {target_city}")
             raise NotFoundError(message=f"No matched group found for city {target_city}")
-        VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
+        self._log(f"Session created. Group: {self.travel_group['group_number']}")
 
 
     def query(self) -> VSQueryResult:
     def query(self) -> VSQueryResult:
         res = VSQueryResult()
         res = VSQueryResult()
@@ -164,18 +164,13 @@ class TlsPlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
                 break  # ✅ 请求成功,跳出重试循环
 
 
             except PermissionDeniedError:
             except PermissionDeniedError:
-                VSC_WARN(
-                    "tls_plg",
-                    "[TLS] Query Appointment-booking blocked (403), attempt %d/%d",
-                    attempt, max_retries
-                )
-
+                self._log(f"Query Appointment-booking blocked (403), attempt {attempt}/{max_retries}")
                 # 最后一次就不再绕盾了
                 # 最后一次就不再绕盾了
                 if attempt >= max_retries:
                 if attempt >= max_retries:
                     raise PermissionDeniedError()
                     raise PermissionDeniedError()
 
 
                 self._solve_cloudflare5S_challenge()
                 self._solve_cloudflare5S_challenge()
-                VSC_INFO("tls_plg", "[TLS] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
                 continue
 
 
         self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
         self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
@@ -291,9 +286,13 @@ class TlsPlugin(IVSPlg):
             res.book_time = target_time
             res.book_time = target_time
             return res
             return res
         else:
         else:
-            VSC_WARN('tls_plg', 'Expected Status is 303, but got {resp.status_code}')
+            self._log(f'Expected Status is 303, but got {resp.status_code}')
             res.success = False            
             res.success = False            
         return res
         return res
+
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
     
     
     def _save_debug_html(self, content: str, prefix: str = "debug"):
     def _save_debug_html(self, content: str, prefix: str = "debug"):
         save_dir = "debug_pages"
         save_dir = "debug_pages"
@@ -303,7 +302,7 @@ class TlsPlugin(IVSPlg):
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
             f.write(content)
-        VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+        self._log(f"HTML saved to: {filename}")
             
             
     def _get_proxy_url(self):
     def _get_proxy_url(self):
         # 构造代理
         # 构造代理
@@ -324,7 +323,7 @@ class TlsPlugin(IVSPlg):
         """
         """
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         if self.config.debug:
         if self.config.debug:
-            VSC_INFO('tls_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
+            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:
@@ -342,7 +341,7 @@ class TlsPlugin(IVSPlg):
         """
         """
         解决 Cloudflare 5s 盾
         解决 Cloudflare 5s 盾
         """
         """
-        VSC_INFO("tls_plg", f"[{self.group_id}] Solving Cloudflare 5s...")
+        self._log(f"Solving Cloudflare 5s...")
         embassy = self.free_config.get('center', {})
         embassy = self.free_config.get('center', {})
         website_url = f'https://visas-fr.tlscontact.com/en-us/country/{embassy["country"]}'
         website_url = f'https://visas-fr.tlscontact.com/en-us/country/{embassy["country"]}'
         
         
@@ -371,7 +370,7 @@ class TlsPlugin(IVSPlg):
         if ua:
         if ua:
             self.user_agent = ua
             self.user_agent = ua
             self.session.headers['User-Agent'] = ua
             self.session.headers['User-Agent'] = ua
-        VSC_INFO("tls_plg", "[%s] Cloudflare 5s challenge solved.", self.group_id)
+        self._log("Cloudflare 5s challenge solved.")
 
 
     def _solve_recaptcha(self, params) -> str:
     def _solve_recaptcha(self, params) -> str:
         """
         """
@@ -428,7 +427,7 @@ class TlsPlugin(IVSPlg):
                     'location': g.get('vacName')
                     'location': g.get('vacName')
                 })
                 })
         else:
         else:
-            VSC_WARN('tls_plg', 'Parsed travel group page, but not found travelGroups')
+            self._log('Parsed travel group page, but not found travelGroups')
         return groups
         return groups
 
 
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
@@ -467,7 +466,7 @@ class TlsPlugin(IVSPlg):
                             })
                             })
             return slots
             return slots
         else:
         else:
-            VSC_WARN('tls_plg', 'Parsed appointment slot page, but not found availableAppointments')
+            self._log('Parsed appointment slot page, but not found availableAppointments')
         return slots
         return slots
   
   
     def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
     def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:

+ 33 - 38
plugins/vfs_plugin.py

@@ -6,7 +6,7 @@ import base64
 import re
 import re
 import urllib.parse
 import urllib.parse
 from datetime import datetime
 from datetime import datetime
-from typing import Dict, Any, Optional, List, Tuple
+from typing import Dict, Any, Optional, List, Tuple, Callable
 
 
 from curl_cffi import requests, const
 from curl_cffi import requests, const
 # 加密库
 # 加密库
@@ -16,7 +16,6 @@ from cryptography.hazmat.backends import default_backend
 
 
 from vs_plg import IVSPlg 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 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 toolkit.vs_cloud_api import VSCloudApi 
 
 
 # ----------------- 静态常量与辅助数据 -----------------
 # ----------------- 静态常量与辅助数据 -----------------
@@ -61,7 +60,8 @@ class VfsPlugin(IVSPlg):
         self.group_id = group_id
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
-
+        self.logger = None
+        
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = None
         
         
         self.jwt_token = ""
         self.jwt_token = ""
@@ -84,10 +84,10 @@ class VfsPlugin(IVSPlg):
 
 
     def set_config(self, config: VSPlgConfig):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.config = config
-        try:
-            self.free_config = json.loads(config.free_config) if config.free_config else {}
-        except:
-            self.free_config = {}
+        self.free_config = config.free_config or {}
+        
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         return self.is_healthy
         return self.is_healthy
@@ -148,12 +148,12 @@ class VfsPlugin(IVSPlg):
         resp_json = resp.json()
         resp_json = resp.json()
         if resp_json.get('accessToken', ''):
         if resp_json.get('accessToken', ''):
             self.jwt_token = resp_json["accessToken"]
             self.jwt_token = resp_json["accessToken"]
-            VSC_INFO("vfs_plg", "[%s] Login successful, JWT obtained.", self.group_id)
+            self._log("Login successful, JWT obtained.")
             return
             return
         
         
         # OTP 处理
         # OTP 处理
         if resp_json.get("enableOTPAuthentication"):
         if resp_json.get("enableOTPAuthentication"):
-            VSC_INFO("vfs_plg", "[%s] Login requires OTP.", self.group_id)
+            self._log("Login requires OTP.")
             otp = self._read_otp_email()
             otp = self._read_otp_email()
             self._submit_login_otp(None, otp)
             self._submit_login_otp(None, otp)
             return
             return
@@ -178,14 +178,10 @@ class VfsPlugin(IVSPlg):
             if "WaitList" in earliest_date:
             if "WaitList" in earliest_date:
                 result.success = True
                 result.success = True
                 result.availability_status = AvailabilityStatus.Waitlist
                 result.availability_status = AvailabilityStatus.Waitlist
-                VSC_INFO("vfs_plg", "[%s] Found WaitList.", self.group_id)
             else:
             else:
                 result.success = True
                 result.success = True
                 result.availability_status = AvailabilityStatus.Available
                 result.availability_status = AvailabilityStatus.Available
                 result.earliest_date = earliest_date
                 result.earliest_date = earliest_date
-                
-                VSC_INFO("vfs_plg", "[%s] Found Slot: %s", self.group_id, earliest_date)
-                
                 day_info = VSQueryResult.DateAvailability()
                 day_info = VSQueryResult.DateAvailability()
                 day_info.date = earliest_date
                 day_info.date = earliest_date
                 result.availability.append(day_info)
                 result.availability.append(day_info)
@@ -222,7 +218,7 @@ class VfsPlugin(IVSPlg):
         ocr_enabled = sub_conf.get("isOCREnable", False)
         ocr_enabled = sub_conf.get("isOCREnable", False)
         
         
         if ocr_enabled:
         if ocr_enabled:
-            VSC_INFO("vfs_plg", "[%s] OCR Enabled, uploading documents...", self.group_id)
+            self._log("OCR Enabled, uploading documents...")
             upload_res = self._upload_applicant_documents(apt_config, user_inputs, upload_res)
             upload_res = self._upload_applicant_documents(apt_config, user_inputs, upload_res)
             user_inputs["applicant_image"] = upload_res.get("passportImageFilename")
             user_inputs["applicant_image"] = upload_res.get("passportImageFilename")
             user_inputs["applicant_image_data"] = upload_res.get("passportImageFileBytes") # Base64
             user_inputs["applicant_image_data"] = upload_res.get("passportImageFileBytes") # Base64
@@ -245,20 +241,20 @@ class VfsPlugin(IVSPlg):
                     raise NotFoundError(message="URN not found")
                     raise NotFoundError(message="URN not found")
                 break
                 break
             except Exception as e:
             except Exception as e:
-                VSC_WARN("vfs_plg", "[%s] Add Applicant retry %d...", self.group_id, add_primary_retry)
+                self._log(f"Add Applicant retry {add_primary_retry}...")
                 time.sleep(10)
                 time.sleep(10)
                 add_primary_retry += 1
                 add_primary_retry += 1
         
         
         if not final_urn:
         if not final_urn:
             raise BizLogicError(message="Failed to add primary applicant (Slot likely taken)")
             raise BizLogicError(message="Failed to add primary applicant (Slot likely taken)")
 
 
-        VSC_INFO("vfs_plg", "[%s] Applicant Added. URN: %s", self.group_id, final_urn)
+        self._log("Applicant Added. URN: {final_urn}")
 
 
         # 申请人 OTP 验证 (核心步骤 2)
         # 申请人 OTP 验证 (核心步骤 2)
         otp_enabled = sub_conf.get("isApplicantOTPEnabled", False)
         otp_enabled = sub_conf.get("isApplicantOTPEnabled", False)
         
         
         if otp_enabled:
         if otp_enabled:
-            VSC_INFO("vfs_plg", "[%s] Applicant OTP Required.", self.group_id)
+            self._log("Applicant OTP Required.")
             if not self._applicant_otp_send(apt_config, final_urn):
             if not self._applicant_otp_send(apt_config, final_urn):
                 raise BizLogicError(message='applicant otp send failed')
                 raise BizLogicError(message='applicant otp send failed')
             
             
@@ -281,7 +277,7 @@ class VfsPlugin(IVSPlg):
         
         
         # 计算需要扫描的月份, 如果 expected_start/end 为空,默认使用 from_date 所在月
         # 计算需要扫描的月份, 如果 expected_start/end 为空,默认使用 from_date 所在月
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
-        VSC_INFO("vfs_plg", "[%s] Scanning months: %s (From: %s)", self.group_id, months, from_date)
+        self._log(f"Scanning months: {months} (From: {from_date})")
         
         
         selected_slot_id = ""
         selected_slot_id = ""
         selected_slot_date = ""
         selected_slot_date = ""
@@ -341,12 +337,11 @@ class VfsPlugin(IVSPlg):
                 break
                 break
                 
                 
         if not found_slot:
         if not found_slot:
-            VSC_INFO("vfs_plg", "[%s] No valid slots found.", self.group_id) 
+            self._log("No valid slots found.") 
             res.success = False
             res.success = False
             return res
             return res
 
 
-        VSC_INFO("vfs_plg", "[%s] Slot Selected: %s %s (ID: %s)", 
-                 self.group_id, selected_slot_date, selected_slot_time_range, selected_slot_id)
+        self._log(f"Slot Selected: {selected_slot_date} {selected_slot_time_range} (ID: {selected_slot_id})")
 
 
         # 服务、费用、最终预约 (核心步骤 4)
         # 服务、费用、最终预约 (核心步骤 4)
         self._submit_no_addition_service(final_urn)
         self._submit_no_addition_service(final_urn)
@@ -355,7 +350,7 @@ class VfsPlugin(IVSPlg):
         schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
         schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
         
         
         if not schedule_res.get("IsAppointmentBooked"):
         if not schedule_res.get("IsAppointmentBooked"):
-            VSC_INFO("vfs_plg", "[%s] IsAppointmentBooked is false", self.group_id) 
+            self._log(f"IsAppointmentBooked is false") 
             res.success = False
             res.success = False
             return res
             return res
              
              
@@ -380,6 +375,10 @@ class VfsPlugin(IVSPlg):
                     res.session_id = saved_session['session_id']
                     res.session_id = saved_session['session_id']
         return res
         return res
     
     
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[VfsPlugin] [{self.group_id}] {message}')
+    
     def _get_proxy_url(self):
     def _get_proxy_url(self):
             # 构造代理
             # 构造代理
         proxy_url = ""
         proxy_url = ""
@@ -753,12 +752,12 @@ class VfsPlugin(IVSPlg):
             
             
         except Exception as e:
         except Exception as e:
             # 记录警告但不中断流程,防止仅仅是 OPTIONS 失败导致误判
             # 记录警告但不中断流程,防止仅仅是 OPTIONS 失败导致误判
-            VSC_DEBUG("vfs_plg", "OPTIONS request failed (non-fatal): %s", str(e))
+            self._log(f"OPTIONS request failed (non-fatal): {str(e)}")
 
 
 
 
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         if self.config.debug:
         if self.config.debug:
-            VSC_INFO('vfs_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
+            self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:
@@ -808,7 +807,7 @@ class VfsPlugin(IVSPlg):
         proxy_str = self._get_proxy_url()
         proxy_str = self._get_proxy_url()
         
         
         # 2. 提交任务
         # 2. 提交任务
-        VSC_INFO("vfs_plg", "[%s] Submitting Turnstile task for %s...", self.group_id, website_url)
+        self._log(f"Submitting Turnstile task for {website_url}...")
         task_out = VSCloudApi.Instance().submit_anti_turnstile_task(proxy_str, website_url)
         task_out = VSCloudApi.Instance().submit_anti_turnstile_task(proxy_str, website_url)
         if not task_out:
         if not task_out:
             raise BizLogicError(message="Failed to submit captcha task to Cloud API")
             raise BizLogicError(message="Failed to submit captcha task to Cloud API")
@@ -852,7 +851,7 @@ class VfsPlugin(IVSPlg):
 
 
                 # B. 设置 Cookies
                 # B. 设置 Cookies
                 if cookies_list:
                 if cookies_list:
-                    VSC_DEBUG("vfs_plg", "[%s] Syncing %d cookies from Captcha solver...", self.group_id, len(cookies_list))
+                    self._log(f"Syncing {len(cookies_list)} cookies from Captcha solver...")
                     for cookie in cookies_list:
                     for cookie in cookies_list:
                         # 兼容不同的 cookie 格式
                         # 兼容不同的 cookie 格式
                         c_name = cookie.get("name")
                         c_name = cookie.get("name")
@@ -868,7 +867,7 @@ class VfsPlugin(IVSPlg):
                                 path=c_path
                                 path=c_path
                             )
                             )
                 
                 
-                VSC_INFO("vfs_plg", "[%s] Cloudflare challenge passed.", self.group_id)
+                self._log("Cloudflare challenge passed.")
                 return token
                 return token
 
 
             elif status == 3: # Failed
             elif status == 3: # Failed
@@ -934,18 +933,14 @@ class VfsPlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
                 break  # ✅ 请求成功,跳出重试循环
 
 
             except PermissionDeniedError:
             except PermissionDeniedError:
-                VSC_WARN(
-                    "vfs_plg",
-                    "[VFS] Earliest slot blocked (403), attempt %d/%d",
-                    attempt, max_retries
-                )
+                self._log(f"Earliest slot blocked (403), attempt {attempt}/{max_retries}")
 
 
                 # 最后一次就不再绕盾了
                 # 最后一次就不再绕盾了
                 if attempt >= max_retries:
                 if attempt >= max_retries:
                     raise PermissionDeniedError()
                     raise PermissionDeniedError()
 
 
                 self._handle_cloudflare_challenge()
                 self._handle_cloudflare_challenge()
-                VSC_INFO("vfs_plg", "[VFS] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
                 continue
 
 
         # ====== 正常解析响应 ======
         # ====== 正常解析响应 ======
@@ -1039,7 +1034,7 @@ class VfsPlugin(IVSPlg):
         now_utc = datetime.utcnow()
         now_utc = datetime.utcnow()
         formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
         formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
 
 
-        VSC_INFO("vfs_plg", "[%s] Waiting for OTP email sent after %s...", self.group_id, formatted_utc_time)
+        self._log(f"Waiting for OTP email sent after {formatted_utc_time}...")
 
 
         # 3. 轮询查收
         # 3. 轮询查收
         for i in range(12):
         for i in range(12):
@@ -1057,7 +1052,7 @@ class VfsPlugin(IVSPlg):
                 match = re.search(r'\b\d{6}\b', content_out)
                 match = re.search(r'\b\d{6}\b', content_out)
                 if match:
                 if match:
                     otp = match.group(0)
                     otp = match.group(0)
-                    VSC_INFO("vfs_plg", "[%s] OTP code found: %s", self.group_id, otp)
+                    self._log(f"OTP code found: {otp}")
                     return otp
                     return otp
             
             
             time.sleep(5)
             time.sleep(5)
@@ -1067,7 +1062,7 @@ class VfsPlugin(IVSPlg):
         """
         """
         提交 OTP 验证码进行登录
         提交 OTP 验证码进行登录
         """
         """
-        VSC_INFO("vfs_plg", "[%s] Submitting Login OTP...", self.group_id)
+        self._log("Submitting Login OTP...")
 
 
         # 1. 准备基础数据
         # 1. 准备基础数据
         email = self.config.account.username
         email = self.config.account.username
@@ -1092,7 +1087,7 @@ class VfsPlugin(IVSPlg):
         
         
         # 为了稳健,如果传入为空,尝试重新获取。
         # 为了稳健,如果传入为空,尝试重新获取。
         if not cf_token:
         if not cf_token:
-            VSC_DEBUG("vfs_plg", "[%s] CF Token is empty, regenerating for OTP...", self.group_id)
+            self._log("CF Token is empty, regenerating for OTP...")
             cf_token = self._handle_cloudflare_challenge()
             cf_token = self._handle_cloudflare_challenge()
 
 
         data = {
         data = {
@@ -1110,7 +1105,7 @@ class VfsPlugin(IVSPlg):
         resp_json = resp.json()
         resp_json = resp.json()
         if resp_json["accessToken"]:
         if resp_json["accessToken"]:
             self.jwt_token = resp_json["accessToken"]
             self.jwt_token = resp_json["accessToken"]
-            VSC_INFO("vfs_plg", "[%s] OTP Login successful, JWT obtained.", self.group_id)
+            self._log("OTP Login successful, JWT obtained.")
             return
             return
         raise PermissionDeniedError(message=resp.text)
         raise PermissionDeniedError(message=resp.text)
         
         

+ 51 - 0
test/regex_test.py

@@ -0,0 +1,51 @@
+import re
+import json
+from typing import List, Dict
+
+def _parse_appointment_slots(html: str) -> List[Dict]:
+    slots = []
+    pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
+    match = re.search(pattern, html, re.DOTALL)
+    
+    if match:
+        json_str = match.group(1).replace(r'\"', '"')
+        print(f'json_str={json_str}') 
+        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
+                        })
+        return slots
+    else:
+        print('Parsed appointment slot page, but not found availableAppointments')
+    return slots
+
+f = open('../debug_pages/Tls_Query_Slot_Page_20260110_221803.html', 'r')
+html_content = f.read()
+
+f.close()
+
+slots = _parse_appointment_slots(html_content)
+print(slots)

+ 1 - 1
toolkit/proxy_manager.py

@@ -95,7 +95,7 @@ class ProxyManager:
     def get_unbind_proxy(self, pool_name: str, bounded_ids: List[int]) -> Optional[Dict[str, Any]]:
     def get_unbind_proxy(self, pool_name: str, bounded_ids: List[int]) -> Optional[Dict[str, Any]]:
         """
         """
         获取一个未绑定(且未锁定)的代理。
         获取一个未绑定(且未锁定)的代理。
-        用于 GroupCoordinator 的 IP 绑定逻辑。
+        用于 GCO 的 IP 绑定逻辑。
         """
         """
         with self._proxy_lock:
         with self._proxy_lock:
             proxies = self._proxies.get(pool_name, [])
             proxies = self._proxies.get(pool_name, [])

+ 2 - 4
toolkit/vs_cloud_api.py

@@ -13,13 +13,12 @@ class VSCloudApi:
     用于对接云端服务 (打码、邮件、Session存储、任务调度)
     用于对接云端服务 (打码、邮件、Session存储、任务调度)
     """
     """
     _instance = None
     _instance = None
-
     def __new__(cls, *args, **kwargs):
     def __new__(cls, *args, **kwargs):
         if cls._instance is None:
         if cls._instance is None:
             cls._instance = super(VSCloudApi, cls).__new__(cls)
             cls._instance = super(VSCloudApi, cls).__new__(cls)
             # 初始化默认配置
             # 初始化默认配置
             cls._instance.base_url = "http://45.137.220.138:8888"
             cls._instance.base_url = "http://45.137.220.138:8888"
-            cls._instance.api_token = "Bearer tok_8cb26cf337cb405784eb346dfafb7f54"
+            cls._instance.api_token = "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc"
             cls._instance.session = requests.Session()
             cls._instance.session = requests.Session()
         return cls._instance
         return cls._instance
 
 
@@ -40,9 +39,8 @@ class VSCloudApi:
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
-        VSC_INFO('vs_cloud', resp.text)
+        VSC_DEBUG('vs_cloud', f'[perform request] {method} {url} {data} {json_data} {params} {resp.text}')
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:

+ 8 - 0
vs_plg.py

@@ -1,4 +1,5 @@
 # vs_plg.py
 # vs_plg.py
+from typing import Callable
 from abc import ABC, abstractmethod
 from abc import ABC, abstractmethod
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult
 
 
@@ -60,4 +61,11 @@ class IVSPlg(ABC):
         @brief 健康检查,用于检测 API 服务是否正常
         @brief 健康检查,用于检测 API 服务是否正常
         @return true 表示健康状态良好,false 表示存在问题
         @return true 表示健康状态良好,false 表示存在问题
         """
         """
+        pass
+    
+    @abstractmethod
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        """
+        @brief 设置日志输出工具
+        """
         pass
         pass

+ 90 - 1
vs_types.py

@@ -1,4 +1,5 @@
 # vs_types.py
 # vs_types.py
+import json
 from dataclasses import dataclass, field
 from dataclasses import dataclass, field
 from enum import Enum, auto
 from enum import Enum, auto
 from typing import List, Optional, Any, Dict
 from typing import List, Optional, Any, Dict
@@ -12,10 +13,23 @@ class BizException(Exception):
         http_status: int = 400,
         http_status: int = 400,
         extra: Optional[Dict[str, Any]] = None,
         extra: Optional[Dict[str, Any]] = None,
     ):
     ):
+        super().__init__(message)
         self.code = code
         self.code = code
         self.message = message
         self.message = message
         self.http_status = http_status
         self.http_status = http_status
         self.extra = extra
         self.extra = extra
+        
+    def __str__(self):
+        return (
+        f"{self.__class__.__name__}("
+        f"code={self.code}, "
+        f"http_status={self.http_status}, "
+        f"message='{self.message}', "
+        f"extra={self.extra})"
+    )
+
+    def __repr__(self):
+        return self.__str__()
 
 
 
 
 class NotFoundError(BizException):
 class NotFoundError(BizException):
@@ -56,6 +70,23 @@ class QueryWaitConfig:
     fixed_wait: int = 0  # 仅在 Fixed 模式下使用
     fixed_wait: int = 0  # 仅在 Fixed 模式下使用
     random_min: int = 0  # 仅在 Random 模式下使用
     random_min: int = 0  # 仅在 Random 模式下使用
     random_max: int = 0  # 仅在 Random 模式下使用
     random_max: int = 0  # 仅在 Random 模式下使用
+    
+    @classmethod
+    def from_json(cls, data: Dict[str, Any]):
+        return cls(
+            mode=data.get("mode", 0),
+            fixed_wait=data.get("fixed_wait", 0),
+            random_min=data.get("random_min", 0),
+            random_max=data.get("random_max", 0)
+        )
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "mode": self.mode,
+            "fixed_wait": self.fixed_wait,
+            "random_min": self.random_min,
+            "random_max": self.random_max
+        }
 
 
 @dataclass
 @dataclass
 class PluginConfig:
 class PluginConfig:
@@ -63,6 +94,23 @@ class PluginConfig:
     plugin_name: str = ""        # 插件注册名
     plugin_name: str = ""        # 插件注册名
     plugin_bin: str = ""         # 动态库文件名/Python文件名
     plugin_bin: str = ""         # 动态库文件名/Python文件名
     plugin_proto: str = ""       # 接口协议
     plugin_proto: str = ""       # 接口协议
+    
+    @classmethod
+    def from_json(cls, data: Dict[str, Any]):
+        return cls(
+            lib_path=data.get("lib_path", "plugins"),
+            plugin_name=data.get("plugin_name", ""),
+            plugin_bin=data.get("plugin_bin", ""),
+            plugin_proto=data.get("plugin_proto", "IVSPlg")
+        )
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "lib_path": self.lib_path,
+            "plugin_name": self.plugin_name,
+            "plugin_bin": self.plugin_bin,
+            "plugin_proto": self.plugin_proto
+        }
 
 
 @dataclass
 @dataclass
 class GroupConfig:
 class GroupConfig:
@@ -80,7 +128,48 @@ class GroupConfig:
     
     
     query_wait: QueryWaitConfig = field(default_factory=QueryWaitConfig)
     query_wait: QueryWaitConfig = field(default_factory=QueryWaitConfig)
     plugin_config: PluginConfig = field(default_factory=PluginConfig)
     plugin_config: PluginConfig = field(default_factory=PluginConfig)
-    free_config: str = "{}"
+    free_config: Dict[str, Any] = field(default_factory=dict)
+    
+    @classmethod
+    def from_json(cls, data: Dict[str, Any]):
+        return cls(
+            identifier=data.get("identifier", ""),
+            debug=data.get("debug", False),
+            enable=data.get("enable", False),
+            need_account=data.get("need_account", False),
+            account_built_in=data.get("account_built_in", True),
+            account_pool=data.get("account_pool", ""),
+            need_proxy=data.get("need_proxy", False),
+            proxy_pool=data.get("proxy_pool", ""),
+            need_ip_bind=data.get("need_ip_bind", False),
+            account_login_interval=data.get("account_login_interval", 30),
+            target_instances=data.get("target_instances", 1),
+            query_wait=QueryWaitConfig.from_json(data.get("query_wait", {})),
+            plugin_config=PluginConfig.from_json(data.get("plugin_config", {})),
+            free_config=data.get("free_config", {})
+        )
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "identifier": self.identifier,
+            "debug": self.debug,
+            "enable": self.enable,
+            "need_account": self.need_account,
+            "account_built_in": self.account_built_in,
+            "account_pool": self.account_pool,
+            "need_proxy": self.need_proxy,
+            "proxy_pool": self.proxy_pool,
+            "need_ip_bind": self.need_ip_bind,
+            "account_login_interval": self.account_login_interval,
+            "target_instances": self.target_instances,
+            "query_wait": self.query_wait.to_json(),
+            "plugin_config": self.plugin_config.to_json(),
+            "free_config": self.free_config
+        }
+
+    def to_json_str(self) -> str:
+        """直接输出 JSON 字符串"""
+        return json.dumps(self.to_json(), indent=2)
 
 
 @dataclass
 @dataclass
 class VSAccount:
 class VSAccount:

+ 46 - 10
web/server.py

@@ -1,7 +1,8 @@
 # web/server.py
 # web/server.py
 import os
 import os
+import asyncio
 from pathlib import Path
 from pathlib import Path
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
 from fastapi.responses import FileResponse
 from fastapi.responses import FileResponse
 from pydantic import BaseModel
 from pydantic import BaseModel
@@ -46,20 +47,19 @@ async def read_index():
 class GroupControl(BaseModel):
 class GroupControl(BaseModel):
     group_id: str
     group_id: str
 
 
-class OTARequest(BaseModel):
+class UpgradePluginRequest(BaseModel):
     plugin_name: str
     plugin_name: str
+    plugin_bin: str
 
 
+class UpdateConfigRequest(BaseModel):
+    group_id: str
+    new_config_str: str
 # === API 接口 ===
 # === API 接口 ===
 
 
 @app.get("/status")
 @app.get("/status")
 def get_status():
 def get_status():
     return {"data": AppManager.Instance().get_status()}
     return {"data": AppManager.Instance().get_status()}
 
 
-@app.post("/reload_config")
-def reload_config():
-    AppManager.Instance().load_configs()
-    return {"message": "Configuration reloaded"}
-
 @app.post("/start")
 @app.post("/start")
 def start_group(payload: GroupControl):
 def start_group(payload: GroupControl):
     if AppManager.Instance().start_group(payload.group_id):
     if AppManager.Instance().start_group(payload.group_id):
@@ -78,16 +78,52 @@ def restart_group(payload: GroupControl):
         return {"message": f"Group {payload.group_id} restarted"}
         return {"message": f"Group {payload.group_id} restarted"}
     raise HTTPException(status_code=400, detail="Failed to restart")
     raise HTTPException(status_code=400, detail="Failed to restart")
 
 
-@app.post("/ota")
-def ota_update(payload: OTARequest):
+@app.post("/group_config")
+def get_group_config(payload: GroupControl):
+    config = AppManager.Instance().get_group_config(payload.group_id)
+    if config:
+        return {"message": f"Group {payload.group_id} restarted", "data": config}
+    raise HTTPException(status_code=400, detail="Failed to get group config")
+
+@app.post("/ota/upgrade_plugin")
+def ota_update(payload: UpgradePluginRequest):
     try:
     try:
-        restarted = AppManager.Instance().ota_update_plugin(payload.plugin_name)
+        restarted = AppManager.Instance().ota_upgrade_plugin(payload.plugin_name)
         return {
         return {
             "message": f"Plugin {payload.plugin_name} reloaded",
             "message": f"Plugin {payload.plugin_name} reloaded",
             "restarted_groups": restarted
             "restarted_groups": restarted
         }
         }
     except Exception as e:
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
         raise HTTPException(status_code=500, detail=str(e))
+    
+@app.post("/ota/update_config")
+def ota_update(payload: UpdateConfigRequest):
+    try:
+        AppManager.Instance().ota_update_plugin_config(payload.group_id, payload.new_config_str)
+        return {
+            "message": f"Plugin {payload.group_id} config updated"
+        }
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+    
+@app.websocket("/ws/logs/{group_id}")
+async def websocket_logs(ws: WebSocket, group_id: str):
+    await ws.accept()
+
+    queue = asyncio.Queue()
+    loop = asyncio.get_running_loop()   # ⭐ 只在这里拿
+
+    def log_callback(msg: str):
+        loop.call_soon_threadsafe(queue.put_nowait, msg)
+
+    AppManager.Instance().subscribe_executor_logs(group_id, log_callback)
+
+    try:
+        while True:
+            msg = await queue.get()
+            await ws.send_text(msg)
+    finally:
+        AppManager.Instance().unsubscribe_executor_logs(group_id, log_callback)
 
 
 def run_web_server(host="0.0.0.0", port=8000):
 def run_web_server(host="0.0.0.0", port=8000):
     import uvicorn
     import uvicorn