Browse Source

faet: update

jerry 4 tháng trước cách đây
mục cha
commit
71c2982d61
17 tập tin đã thay đổi với 549 bổ sung439 xóa
  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__
 debug_pages
 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 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 core.plugin_reloader import reload_plugin_module
 
@@ -26,8 +26,8 @@ class AppManager:
         return AppManager()
 
     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"
 
     def load_configs(self):
@@ -42,38 +42,8 @@ class AppManager:
             
             for item in data:
                 # 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
                 
             VSC_INFO("app_mgr", f"Loaded {len(self.configs)} group configurations.")
@@ -84,51 +54,51 @@ class AppManager:
     def start_all(self):
         """启动所有 enable=True 的组"""
         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)
 
     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:
-        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:
         self.stop_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 热更新流程:
         1. 找到所有使用该插件的运行中组
@@ -139,10 +109,10 @@ class AppManager:
         affected_groups = []
         
         # 1. 查找受影响的组
-        for gid, coord in self.coordinators.items():
+        for gid, exec in self.executors.items():
             # 注意:这里我们访问 coord 私有成员,实际工程中建议添加 getter
             # 假设 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)
         
         VSC_INFO("app_mgr", f"OTA Update for '{plugin_name}'. Affected groups: {affected_groups}")
@@ -161,12 +131,46 @@ class AppManager:
                 restarted.append(gid)
                 
         return restarted
+    
+    def get_group_config(self, group_id: str):
+        with self._lock:
+            if group_id not in self.configs:
+                return False
+            group_config = self.configs.get(group_id)
+            return group_config.to_json()
+    
+    # ----------------- 更新配置(局部) -----------------
+    def ota_update_plugin_config(self, group_id: str, new_config_str: str) -> 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):
         """获取所有组的状态"""
         status_list = []
         for gid, cfg in self.configs.items():
-            running = gid in self.coordinators
+            running = gid in self.executors
             status_list.append({
                 "id": gid,
                 "plugin": cfg.plugin_config.plugin_name,
@@ -174,4 +178,27 @@ class AppManager:
                 "instances": cfg.target_instances if running else 0,
                 "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 time
 import json
 import random
 import threading
-from typing import List, Optional
+from typing import List, Optional, Callable
 from concurrent.futures import wait
 
 # 导入所有依赖
@@ -16,18 +16,18 @@ from toolkit.proxy_manager import ProxyManager
 from toolkit.binding_manager import BindingManager 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
-from vs_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_factory = VSPlgFactory() # 插件工厂实例
+        self.m_logger = logger
         
         self.m_tasks: List[Task] = [] # 存储所有运行中的任务实例
         
@@ -36,29 +36,16 @@ class GroupCoordinator:
         
         self.m_monitor_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):
         """
         @brief 启动协调器,包括插件注册和线程启动。
         """
         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
 
-        VSC_INFO("coordinator", f"Starting coordinator for group {self.m_cfg.identifier}...")
+        self._log("Starting coordinator...")
         self.m_stop_event.clear()
 
         # 注册插件
@@ -70,55 +57,47 @@ class GroupCoordinator:
         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, 
                                        plugin_module_path, 
                                        class_name)
 
-        self.m_monitor_thread = threading.Thread(target=self.monitor_loop, name=f"Monitor-{self.m_cfg.identifier}")
-        self.m_creator_thread = threading.Thread(target=self.creator_loop, name=f"Creator-{self.m_cfg.identifier}")
+        self.m_monitor_thread = threading.Thread(target=self._monitor_loop, name=f"Monitor-{self.m_cfg.identifier}")
+        self.m_creator_thread = threading.Thread(target=self._creator_loop, name=f"Creator-{self.m_cfg.identifier}")
         
         self.m_monitor_thread.start()
         self.m_creator_thread.start()
-        VSC_INFO("coordinator", f"Coordinator for group {self.m_cfg.identifier} threads started.")
+        self._log("Coordinator threads started.")
 
     def stop(self):
         """
         @brief 停止协调器,等待所有线程结束。
         """
-        VSC_INFO("coordinator", f"Stopping coordinator for group {self.m_cfg.identifier}...")
+        self._log("Stopping coordinator...")
         self.m_stop_event.set() # 发送停止信号
         
         if self.m_monitor_thread and self.m_monitor_thread.is_alive():
             self.m_monitor_thread.join()
         if self.m_creator_thread and self.m_creator_thread.is_alive():
             self.m_creator_thread.join()
-        
-        # 关闭预订线程池
-        self.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:
         """
         @brief 获取分组ID。
         """
         return self.m_cfg.identifier
+    
+    def _log(self, message):
+        if self.m_logger:
+            self.m_logger(f'[gco] [{self.m_cfg.identifier}] {message}')
 
-    def monitor_loop(self):
+    def _monitor_loop(self):
         """
         @brief 监控循环:定期检查实例健康状况,执行查询任务,并根据结果触发预订。
         """
-        VSC_INFO("coordinator", f"[START] monitor loop starting for group {self.m_cfg.identifier}")
+        self._log("[START] monitor loop starting...")
         rng = random.Random()
         
         while not self.m_stop_event.is_set():
@@ -148,19 +127,19 @@ class GroupCoordinator:
                 try:
                     result = task.instance.query()
                     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
                     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:
-                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
+                    self._log(f"Exception during query: {e}")
 
                 # 计算下次运行时间
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
                 if is_booking_triggered:
                     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:
                     interval = 30
                     mode = task.qw_cfg.mode
@@ -178,15 +157,15 @@ class GroupCoordinator:
                 initial_size = len(self.m_tasks)
                 self.m_tasks[:] = [t for t in self.m_tasks if t.instance.health_check()]
                 if len(self.m_tasks) < initial_size:
-                    VSC_WARN("coordinator", 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 创建者循环:根据目标实例数量,创建和补充新的插件实例。
         """
-        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():
             time.sleep(0.1) # 避免空转太快
@@ -197,12 +176,12 @@ class GroupCoordinator:
                 diff = self.m_cfg.target_instances - current_instances_count
             
             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()
                 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) # 等待资源 (账户/代理) 恢复
                     continue
 
@@ -220,23 +199,23 @@ class GroupCoordinator:
                                 next_run=time.time() # 立即执行第一次查询
                             )
                             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:
-                            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:
-                    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))
 
-        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]:
         """
         @brief 准备插件配置 (账号、代理等)。
         """
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Preparing plugin configuration...")
+        self._log("Preparing plugin configuration...")
         plg_cfg = VSPlgConfig()
         plg_cfg.debug = self.m_cfg.debug
         
@@ -244,13 +223,13 @@ class GroupCoordinator:
         if self.m_cfg.need_account:
             account = AccountManager.Instance().get_next_account(self.m_cfg.account_pool)
             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
             plg_cfg.account.id = account["id"]
             plg_cfg.account.username = account["username"]
             plg_cfg.account.password = account["password"]
             plg_cfg.account.lock_until = account.get("lock_until", "")
-            VSC_DEBUG("coordinator", 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:
@@ -261,22 +240,22 @@ class GroupCoordinator:
                     bounded_ids = BindingManager.Instance().get_bounded_proxies_ids(self.m_cfg.account_pool, self.m_cfg.proxy_pool)
                     proxy = ProxyManager.Instance().get_unbind_proxy(self.m_cfg.proxy_pool, bounded_ids)
                     if not proxy:
-                        VSC_WARN("coordinator", 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
                     BindingManager.Instance().create_binding(
                         self.m_cfg.account_pool, plg_cfg.account.id,
                         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:
                     all_proxies_in_pool = ProxyManager.Instance()._proxies.get(self.m_cfg.proxy_pool, [])
                     proxy = next((p for p in all_proxies_in_pool if p["id"] == proxy_id), None)
                     if not proxy:
-                         VSC_ERROR("coordinator", 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:
                 proxy = ProxyManager.Instance().get_next_proxy(self.m_cfg.proxy_pool)
                 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
 
             plg_cfg.proxy.id = proxy["id"]
@@ -286,10 +265,10 @@ class GroupCoordinator:
             plg_cfg.proxy.username = proxy.get("username", "")
             plg_cfg.proxy.password = proxy.get("password", "")
             plg_cfg.proxy.lock_until = proxy.get("lock_until", "")
-            VSC_DEBUG("coordinator", 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
-        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Plugin configuration prepared.")
+        self._log("Plugin configuration prepared.")
         return plg_cfg
 
     def _create_instance(self, plg_cfg: VSPlgConfig) -> Optional[IVSPlg]:
@@ -297,22 +276,23 @@ class GroupCoordinator:
         # @brief 创建并初始化单个插件实例。
         # 这个方法在 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:
             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.create_session()
             if self.m_cfg.need_account and self.m_cfg.account_login_interval > 0:
                 AccountManager.Instance().lock_account(
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
-            VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
+            self._log("Plugin instance created and session established.")
             return inst
         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
 
-    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):
@@ -325,14 +305,14 @@ class GroupCoordinator:
                 task = VSCloudApi.Instance().get_vas_task_pop(booking_routing_key)
                 
                 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 
 
                 task_id = task['id']
                 order_id = task['order_id']
                 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. 执行预订
                 # 注意:插件的 book 方法需要接收 user_input
@@ -340,11 +320,7 @@ class GroupCoordinator:
 
                 # 3. 处理结果
                 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
                     # 包含后端需要的关键信息
@@ -366,23 +342,23 @@ class GroupCoordinator:
                     }
                     
                     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 = None 
             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:
                 # 5. Return to Queue (回滚机制)
                 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:
                         VSCloudApi.Instance().return_vas_task_to_queue(task_id)
                     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 = []
-        f = self.book_executor.enqueue(book_task, sptr, query_result)
+        f = ThreadPool.getInstance().enqueue(book_task, sptr, query_result)
         futures.append(f)
         
         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 group_coordinator import GroupCoordinator
+from gco import GCO
 from vs_log_macros import VSC_INFO, VSC_ERROR
 
 def vfs_test():
@@ -80,22 +80,14 @@ def vfs_test():
     )
 
     # 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. 启动
     try:
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "   VFS Python Plugin Tester      ")
         VSC_INFO("main", "========================================")
         
-        coordinator.start()
+        gco.start()
         
         time.sleep(3600)
     except KeyboardInterrupt:
@@ -104,7 +96,7 @@ def vfs_test():
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
     finally:
         # 8. 停止
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Program finished.")
         
         
@@ -162,22 +154,14 @@ def tls_test():
     )
 
     # 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. 启动
     try:
         VSC_INFO("main", "========================================")
         VSC_INFO("main", "    TLS Python Plugin Tester      ")
         VSC_INFO("main", "========================================")
         
-        coordinator.start()
+        gco.start()
         
         time.sleep(3600)
     except KeyboardInterrupt:
@@ -186,7 +170,7 @@ def tls_test():
         VSC_ERROR("main", "Unexpected Error: %s", str(e))
     finally:
         # 8. 停止
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Program finished.")
         
 def bls_test():
@@ -254,19 +238,11 @@ def bls_test():
     )
 
     # 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. 启动运行
     try:
         VSC_INFO("main", ">>> Starting BLS Plugin Test...")
-        coordinator.start()
+        gco.start()
         
         time.sleep(3600)
     except KeyboardInterrupt:
@@ -274,7 +250,7 @@ def bls_test():
     except Exception as e:
         VSC_ERROR("main", f"Unexpected Error: {e}")
     finally:
-        coordinator.stop()
+        gco.stop()
         VSC_INFO("main", "Test Finished.")
 
 if __name__ == "__main__":

+ 1 - 1
main_server.py

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

+ 24 - 20
plugins/bls_plugin.py

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

+ 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 base64
 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 curl_cffi import requests, const
@@ -14,7 +14,6 @@ from bs4 import BeautifulSoup
 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
 
 
@@ -43,7 +42,7 @@ class DePlugin(IVSPlg):
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
-        
+        self.logger = 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"
         
@@ -61,13 +60,13 @@ class DePlugin(IVSPlg):
 
     def get_group_id(self) -> str:
         return self.group_id
+    
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
     def set_config(self, config: VSPlgConfig):
         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"):
             self.base_url = self.free_config["base_url"].rstrip('/')
@@ -134,12 +133,12 @@ class DePlugin(IVSPlg):
         if resp.status_code != 200:
             raise BizLogicError(message='Captcha ocr server failed')
         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)
         # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
         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:
         """
@@ -170,18 +169,14 @@ class DePlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
 
             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:
                     raise PermissionDeniedError()
 
                 self._solve_cloudflare5S_challenge()
-                VSC_INFO("de_plg", "[Visamtric] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
 
         # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
@@ -224,11 +219,11 @@ class DePlugin(IVSPlg):
             raise NotFoundError(message="No dates match user constraints")
         
         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)
         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)
         alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
@@ -260,12 +255,15 @@ class DePlugin(IVSPlg):
         
         if match:
             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
 
     # ---------------------------------------------------------
     # 辅助方法
     # ---------------------------------------------------------
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[DePlugin] [{self.group_id}] {message}')
     
     def _get_headers(self) -> Dict[str, str]:
         """基础 Header"""
@@ -297,7 +295,7 @@ class DePlugin(IVSPlg):
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
             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):
         url = f"{self.base_url}/en/appointment-form"
@@ -402,7 +400,7 @@ class DePlugin(IVSPlg):
         now_utc = datetime.utcnow()
         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. 轮询查收
         for i in range(12):
@@ -420,7 +418,7 @@ class DePlugin(IVSPlg):
                 match = re.search(r'\b\d{6}\b', content_out)
                 if match:
                     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
             
             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)
         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:
             return resp
         elif resp.status_code in [401, 419]:
@@ -544,7 +542,7 @@ class DePlugin(IVSPlg):
         """
         解决 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'
         
         # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
@@ -572,4 +570,4 @@ class DePlugin(IVSPlg):
         if ua:
             self.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 os
 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 curl_cffi import requests, const
@@ -12,7 +12,6 @@ from bs4 import BeautifulSoup
 
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN
 from toolkit.vs_cloud_api import VSCloudApi
 
 class TlsPlugin(IVSPlg):
@@ -26,6 +25,7 @@ class TlsPlugin(IVSPlg):
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
         self.is_healthy = True
+        self.logger = None
         # 会话相关
         self.session: Optional[requests.Session] = None
         self.travel_group: Optional[Dict] = None
@@ -33,13 +33,13 @@ class TlsPlugin(IVSPlg):
 
     def get_group_id(self) -> str:
         return self.group_id
+    
+    def set_log(self, logger: Callable[[str], None]) -> None:
+        self.logger = logger
 
     def set_config(self, config: VSPlgConfig):
         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:
         return self.is_healthy
@@ -134,7 +134,7 @@ class TlsPlugin(IVSPlg):
         
         if not self.travel_group:
             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:
         res = VSQueryResult()
@@ -164,18 +164,13 @@ class TlsPlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
 
             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:
                     raise PermissionDeniedError()
 
                 self._solve_cloudflare5S_challenge()
-                VSC_INFO("tls_plg", "[TLS] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
 
         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
             return res
         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            
         return res
+
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
     
     def _save_debug_html(self, content: str, prefix: str = "debug"):
         save_dir = "debug_pages"
@@ -303,7 +302,7 @@ class TlsPlugin(IVSPlg):
         filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
-        VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+        self._log(f"HTML saved to: {filename}")
             
     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)
         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:
             return resp
         elif resp.status_code == 401:
@@ -342,7 +341,7 @@ class TlsPlugin(IVSPlg):
         """
         解决 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', {})
         website_url = f'https://visas-fr.tlscontact.com/en-us/country/{embassy["country"]}'
         
@@ -371,7 +370,7 @@ class TlsPlugin(IVSPlg):
         if ua:
             self.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:
         """
@@ -428,7 +427,7 @@ class TlsPlugin(IVSPlg):
                     'location': g.get('vacName')
                 })
         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
 
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
@@ -467,7 +466,7 @@ class TlsPlugin(IVSPlg):
                             })
             return slots
         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
   
     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 urllib.parse
 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
 # 加密库
@@ -16,7 +16,6 @@ from cryptography.hazmat.backends import default_backend
 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
 
 # ----------------- 静态常量与辅助数据 -----------------
@@ -61,7 +60,8 @@ class VfsPlugin(IVSPlg):
         self.group_id = group_id
         self.config: Optional[VSPlgConfig] = None
         self.free_config: Dict[str, Any] = {}
-
+        self.logger = None
+        
         self.session: Optional[requests.Session] = None
         
         self.jwt_token = ""
@@ -84,10 +84,10 @@ class VfsPlugin(IVSPlg):
 
     def set_config(self, config: VSPlgConfig):
         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:
         return self.is_healthy
@@ -148,12 +148,12 @@ class VfsPlugin(IVSPlg):
         resp_json = resp.json()
         if resp_json.get('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
         
         # OTP 处理
         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()
             self._submit_login_otp(None, otp)
             return
@@ -178,14 +178,10 @@ class VfsPlugin(IVSPlg):
             if "WaitList" in earliest_date:
                 result.success = True
                 result.availability_status = AvailabilityStatus.Waitlist
-                VSC_INFO("vfs_plg", "[%s] Found WaitList.", self.group_id)
             else:
                 result.success = True
                 result.availability_status = AvailabilityStatus.Available
                 result.earliest_date = earliest_date
-                
-                VSC_INFO("vfs_plg", "[%s] Found Slot: %s", self.group_id, earliest_date)
-                
                 day_info = VSQueryResult.DateAvailability()
                 day_info.date = earliest_date
                 result.availability.append(day_info)
@@ -222,7 +218,7 @@ class VfsPlugin(IVSPlg):
         ocr_enabled = sub_conf.get("isOCREnable", False)
         
         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)
             user_inputs["applicant_image"] = upload_res.get("passportImageFilename")
             user_inputs["applicant_image_data"] = upload_res.get("passportImageFileBytes") # Base64
@@ -245,20 +241,20 @@ class VfsPlugin(IVSPlg):
                     raise NotFoundError(message="URN not found")
                 break
             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)
                 add_primary_retry += 1
         
         if not final_urn:
             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_enabled = sub_conf.get("isApplicantOTPEnabled", False)
         
         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):
                 raise BizLogicError(message='applicant otp send failed')
             
@@ -281,7 +277,7 @@ class VfsPlugin(IVSPlg):
         
         # 计算需要扫描的月份, 如果 expected_start/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_date = ""
@@ -341,12 +337,11 @@ class VfsPlugin(IVSPlg):
                 break
                 
         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
             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)
         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)
         
         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
             return res
              
@@ -380,6 +375,10 @@ class VfsPlugin(IVSPlg):
                     res.session_id = saved_session['session_id']
         return res
     
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[VfsPlugin] [{self.group_id}] {message}')
+    
     def _get_proxy_url(self):
             # 构造代理
         proxy_url = ""
@@ -753,12 +752,12 @@ class VfsPlugin(IVSPlg):
             
         except Exception as e:
             # 记录警告但不中断流程,防止仅仅是 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)
         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:
             return resp
         elif resp.status_code == 401:
@@ -808,7 +807,7 @@ class VfsPlugin(IVSPlg):
         proxy_str = self._get_proxy_url()
         
         # 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)
         if not task_out:
             raise BizLogicError(message="Failed to submit captcha task to Cloud API")
@@ -852,7 +851,7 @@ class VfsPlugin(IVSPlg):
 
                 # B. 设置 Cookies
                 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:
                         # 兼容不同的 cookie 格式
                         c_name = cookie.get("name")
@@ -868,7 +867,7 @@ class VfsPlugin(IVSPlg):
                                 path=c_path
                             )
                 
-                VSC_INFO("vfs_plg", "[%s] Cloudflare challenge passed.", self.group_id)
+                self._log("Cloudflare challenge passed.")
                 return token
 
             elif status == 3: # Failed
@@ -934,18 +933,14 @@ class VfsPlugin(IVSPlg):
                 break  # ✅ 请求成功,跳出重试循环
 
             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:
                     raise PermissionDeniedError()
 
                 self._handle_cloudflare_challenge()
-                VSC_INFO("vfs_plg", "[VFS] Cloudflare bypass success, retrying...")
+                self._log("Cloudflare bypass success, retrying...")
                 continue
 
         # ====== 正常解析响应 ======
@@ -1039,7 +1034,7 @@ class VfsPlugin(IVSPlg):
         now_utc = datetime.utcnow()
         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. 轮询查收
         for i in range(12):
@@ -1057,7 +1052,7 @@ class VfsPlugin(IVSPlg):
                 match = re.search(r'\b\d{6}\b', content_out)
                 if match:
                     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
             
             time.sleep(5)
@@ -1067,7 +1062,7 @@ class VfsPlugin(IVSPlg):
         """
         提交 OTP 验证码进行登录
         """
-        VSC_INFO("vfs_plg", "[%s] Submitting Login OTP...", self.group_id)
+        self._log("Submitting Login OTP...")
 
         # 1. 准备基础数据
         email = self.config.account.username
@@ -1092,7 +1087,7 @@ class VfsPlugin(IVSPlg):
         
         # 为了稳健,如果传入为空,尝试重新获取。
         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()
 
         data = {
@@ -1110,7 +1105,7 @@ class VfsPlugin(IVSPlg):
         resp_json = resp.json()
         if 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
         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]]:
         """
         获取一个未绑定(且未锁定)的代理。
-        用于 GroupCoordinator 的 IP 绑定逻辑。
+        用于 GCO 的 IP 绑定逻辑。
         """
         with self._proxy_lock:
             proxies = self._proxies.get(pool_name, [])

+ 2 - 4
toolkit/vs_cloud_api.py

@@ -13,13 +13,12 @@ class VSCloudApi:
     用于对接云端服务 (打码、邮件、Session存储、任务调度)
     """
     _instance = None
-
     def __new__(cls, *args, **kwargs):
         if cls._instance is None:
             cls._instance = super(VSCloudApi, cls).__new__(cls)
             # 初始化默认配置
             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()
         return cls._instance
 
@@ -40,9 +39,8 @@ class VSCloudApi:
         1. 发送 OPTIONS 请求
         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)
-        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:
             return resp
         elif resp.status_code == 401:

+ 8 - 0
vs_plg.py

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

+ 90 - 1
vs_types.py

@@ -1,4 +1,5 @@
 # vs_types.py
+import json
 from dataclasses import dataclass, field
 from enum import Enum, auto
 from typing import List, Optional, Any, Dict
@@ -12,10 +13,23 @@ class BizException(Exception):
         http_status: int = 400,
         extra: Optional[Dict[str, Any]] = None,
     ):
+        super().__init__(message)
         self.code = code
         self.message = message
         self.http_status = http_status
         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):
@@ -56,6 +70,23 @@ class QueryWaitConfig:
     fixed_wait: int = 0  # 仅在 Fixed 模式下使用
     random_min: 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
 class PluginConfig:
@@ -63,6 +94,23 @@ class PluginConfig:
     plugin_name: str = ""        # 插件注册名
     plugin_bin: str = ""         # 动态库文件名/Python文件名
     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
 class GroupConfig:
@@ -80,7 +128,48 @@ class GroupConfig:
     
     query_wait: QueryWaitConfig = field(default_factory=QueryWaitConfig)
     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
 class VSAccount:

+ 46 - 10
web/server.py

@@ -1,7 +1,8 @@
 # web/server.py
 import os
+import asyncio
 from pathlib import Path
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
 from fastapi.staticfiles import StaticFiles
 from fastapi.responses import FileResponse
 from pydantic import BaseModel
@@ -46,20 +47,19 @@ async def read_index():
 class GroupControl(BaseModel):
     group_id: str
 
-class OTARequest(BaseModel):
+class UpgradePluginRequest(BaseModel):
     plugin_name: str
+    plugin_bin: str
 
+class UpdateConfigRequest(BaseModel):
+    group_id: str
+    new_config_str: str
 # === API 接口 ===
 
 @app.get("/status")
 def 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")
 def start_group(payload: GroupControl):
     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"}
     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:
-        restarted = AppManager.Instance().ota_update_plugin(payload.plugin_name)
+        restarted = AppManager.Instance().ota_upgrade_plugin(payload.plugin_name)
         return {
             "message": f"Plugin {payload.plugin_name} reloaded",
             "restarted_groups": restarted
         }
     except Exception as 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):
     import uvicorn