jerry 3 месяцев назад
Родитель
Сommit
c6bd7dd623

+ 0 - 30
chrome_proxy_auth_plugin/background.js

@@ -1,30 +0,0 @@
-
-    var config = {
-        mode: "fixed_servers",
-        rules: {
-            singleProxy: {
-                scheme: "http",
-                host: "91.193.255.210",
-                port: parseInt(12323)
-            },
-            bypassList: ["localhost"]
-        }
-    };
-
-    chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
-
-    function callbackFn(details) {
-        return {
-            authCredentials: {
-                username: "14ae212b29a2a",
-                password: "d160bc0854"
-            }
-        };
-    }
-
-    chrome.webRequest.onAuthRequired.addListener(
-        callbackFn,
-        {urls: ["<all_urls>"]},
-        ['blocking']
-    );
-    

+ 0 - 20
chrome_proxy_auth_plugin/manifest.json

@@ -1,20 +0,0 @@
-
-    {
-        "version": "1.0.0",
-        "manifest_version": 2,
-        "name": "Chrome Proxy Auth Extension",
-        "permissions": [
-            "proxy",
-            "tabs",
-            "unlimitedStorage",
-            "storage",
-            "<all_urls>",
-            "webRequest",
-            "webRequestBlocking"
-        ],
-        "background": {
-            "scripts": ["background.js"]
-        },
-        "minimum_chrome_version": "22.0.0"
-    }
-    

+ 3 - 3
config/groups.json

@@ -235,11 +235,11 @@
     {
     {
         "identifier": "VFS_GB_NL",
         "identifier": "VFS_GB_NL",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_nl",
         "local_account_pool": "gb_nl",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "iproyal",
+        "proxy_pool": "proxy_cheap",
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 30,
         "account_login_interval": 30,
         "order_account_routing": "",
         "order_account_routing": "",
@@ -702,7 +702,7 @@
     {
     {
         "identifier": "TLS_GB_FR",
         "identifier": "TLS_GB_FR",
         "debug": false,
         "debug": false,
-        "enable": true,
+        "enable": false,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_fr",
         "local_account_pool": "gb_fr",
         "need_proxy": true,
         "need_proxy": true,

+ 92 - 2
config/proxies.json

@@ -259,7 +259,7 @@
     ],
     ],
     "proxy_cheap": [
     "proxy_cheap": [
         {
         {
-            "id": 100029,
+            "id": 100001,
             "ip": "95.135.130.175",
             "ip": "95.135.130.175",
             "lock_until": 0,
             "lock_until": 0,
             "password": "hmuROCk1FDebCnL",
             "password": "hmuROCk1FDebCnL",
@@ -268,13 +268,103 @@
             "username": "GB6o2vBrXFjz4ya"
             "username": "GB6o2vBrXFjz4ya"
         },
         },
         {
         {
-            "id": 100030,
+            "id": 100002,
             "ip": "95.135.130.29",
             "ip": "95.135.130.29",
             "lock_until": 0,
             "lock_until": 0,
             "password": "WmqFTSvRvtxChIT",
             "password": "WmqFTSvRvtxChIT",
             "port": 43740,
             "port": 43740,
             "scheme": "http",
             "scheme": "http",
             "username": "JUcjydi0HKZzWC6"
             "username": "JUcjydi0HKZzWC6"
+        },
+        {
+            "id": 100003,
+            "ip": "95.135.130.105",
+            "lock_until": 0,
+            "password": "a9udkCOGYZKGkLS",
+            "port": 47342,
+            "scheme": "http",
+            "username": "1mtj2c6xoLfrOLm"
+        },
+        {
+            "id": 100004,
+            "ip": "95.135.130.157",
+            "lock_until": 0,
+            "password": "q1UNK1gmiQdxJ1g",
+            "port": 42290,
+            "scheme": "http",
+            "username": "alxuf86deeI898d"
+        },
+        {
+            "id": 100005,
+            "ip": "95.135.130.167",
+            "lock_until": 0,
+            "password": "bmKmauMV5CuOCrh",
+            "port": 48333,
+            "scheme": "http",
+            "username": "gPaIdSyKsp2TnQ1"
+        },
+        {
+            "id": 100006,
+            "ip": "95.135.130.181",
+            "lock_until": 0,
+            "password": "0HU9RGLRTNNwecf",
+            "port": 46253,
+            "scheme": "http",
+            "username": "XpSiGAiz3zwAyi1"
+        },
+        {
+            "id": 100007,
+            "ip": "95.135.130.192",
+            "lock_until": 0,
+            "password": "anYmNO4luxcm22m",
+            "port": 41255,
+            "scheme": "http",
+            "username": "xRGDPswifGmmbog"
+        },
+        {
+            "id": 100008,
+            "ip": "95.135.130.203",
+            "lock_until": 0,
+            "password": "qXEZWLa74q8Awdx",
+            "port": 49012,
+            "scheme": "http",
+            "username": "jHATN4mVM6kcltO"
+        },
+        {
+            "id": 100009,
+            "ip": "95.135.130.206",
+            "lock_until": 0,
+            "password": "MnL8pmNAdpLo0h7",
+            "port": 47504,
+            "scheme": "http",
+            "username": "aLoTyl9YbSvdxbD"
+        },
+        {
+            "id": 100010,
+            "ip": "95.135.130.33",
+            "lock_until": 0,
+            "password": "slxIVWHqzZpkSPY",
+            "port": 44720,
+            "scheme": "http",
+            "username": "EnnPVteJalQvk7E"
+        },
+        {
+            "id": 100011,
+            "ip": "95.135.130.53",
+            "lock_until": 0,
+            "password": "5fS8EMqdto9JnO9",
+            "port": 44764,
+            "scheme": "http",
+            "username": "QeLVYhf0Pbz5BEe"
+        },
+        {
+            "id": 100012,
+            "ip": "95.135.130.66",
+            "lock_until": 0,
+            "password": "4HzLag9JYiPTZ5J",
+            "port": 46470,
+            "scheme": "http",
+            "username": "c1zHamB7LAOeNdb"
         }
         }
     ],
     ],
     "local": [
     "local": [

+ 128 - 18
plugins/ita_plugin.py

@@ -1,6 +1,8 @@
 import time
 import time
 import json
 import json
 import random
 import random
+import uuid
+import shutil
 import re
 import re
 import os
 import os
 import base64
 import base64
@@ -18,23 +20,73 @@ from toolkit.vs_cloud_api import VSCloudApi
 # ==========================================
 # ==========================================
 # 1. 辅助函数:代理插件 & 响应封装
 # 1. 辅助函数:代理插件 & 响应封装
 # ==========================================
 # ==========================================
-def create_proxy_auth_extension(ip, port, username, password, plugin_path="./chrome_proxy_auth_plugin"):
-    if not os.path.exists(plugin_path): os.makedirs(plugin_path)
+def create_proxy_auth_extension(ip, port, username, password, plugin_path):
+    """
+    创建一个 Chrome 插件来自动处理代理认证
+    """
+    if not os.path.exists(plugin_path):
+        os.makedirs(plugin_path)
+
+    # 1. manifest.json
     manifest_json = """
     manifest_json = """
     {
     {
-        "version": "1.0.0", "manifest_version": 2, "name": "Chrome Proxy Auth",
-        "permissions": ["proxy", "tabs", "unlimitedStorage", "storage", "<all_urls>", "webRequest", "webRequestBlocking"],
-        "background": {"scripts": ["background.js"]}, "minimum_chrome_version": "22.0.0"
-    }"""
+        "version": "1.0.0",
+        "manifest_version": 2,
+        "name": "Chrome Proxy Auth Extension",
+        "permissions": [
+            "proxy",
+            "tabs",
+            "unlimitedStorage",
+            "storage",
+            "<all_urls>",
+            "webRequest",
+            "webRequestBlocking"
+        ],
+        "background": {
+            "scripts": ["background.js"]
+        },
+        "minimum_chrome_version": "22.0.0"
+    }
+    """
+
+    # 2. background.js
     background_js = f"""
     background_js = f"""
-    var config = {{mode: "fixed_servers", rules: {{singleProxy: {{scheme: "http", host: "{ip}", port: parseInt({port})}}, bypassList: ["localhost"]}}}};
+    var config = {{
+        mode: "fixed_servers",
+        rules: {{
+            singleProxy: {{
+                scheme: "http",
+                host: "{ip}",
+                port: parseInt({port})
+            }},
+            bypassList: ["localhost"]
+        }}
+    }};
+
     chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
     chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
-    chrome.webRequest.onAuthRequired.addListener(function(details) {{
-        return {{authCredentials: {{username: "{username}", password: "{password}"}}}};
-    }}, {{urls: ["<all_urls>"]}}, ['blocking']);
+
+    function callbackFn(details) {{
+        return {{
+            authCredentials: {{
+                username: "{username}",
+                password: "{password}"
+            }}
+        }};
+    }}
+
+    chrome.webRequest.onAuthRequired.addListener(
+        callbackFn,
+        {{urls: ["<all_urls>"]}},
+        ['blocking']
+    );
     """
     """
-    with open(os.path.join(plugin_path, "manifest.json"), "w") as f: f.write(manifest_json)
-    with open(os.path.join(plugin_path, "background.js"), "w") as f: f.write(background_js)
+
+    with open(os.path.join(plugin_path, "manifest.json"), "w") as f:
+        f.write(manifest_json)
+    
+    with open(os.path.join(plugin_path, "background.js"), "w") as f:
+        f.write(background_js)
+
     return os.path.abspath(plugin_path)
     return os.path.abspath(plugin_path)
 
 
 class BrowserResponse:
 class BrowserResponse:
@@ -64,11 +116,25 @@ class ItaPlugin(IVSPlg):
         self.is_healthy = True
         self.is_healthy = True
         self.logger = None
         self.logger = None
         self.page: Optional[ChromiumPage] = None
         self.page: Optional[ChromiumPage] = None
-        self.session_create_time: float = 0
         
         
         # Prenotami 特有配置
         # Prenotami 特有配置
         self._service_id = 0 
         self._service_id = 0 
         self._host = 'https://prenotami.esteri.it'
         self._host = 'https://prenotami.esteri.it'
+        
+                
+        # --- [核心修改] 并发隔离与资源管理 ---
+        # 生成唯一实例 ID
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        # 定义子目录:代理插件目录 & 浏览器用户数据目录
+        self.proxy_ext_path = os.path.join(self.root_workspace, "proxy_ext")
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+        
+        # 确保根目录存在 (子目录由具体逻辑创建)
+        if not os.path.exists(self.root_workspace):
+            os.makedirs(self.root_workspace)
+        
+        self.session_create_time: float = 0
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
@@ -116,11 +182,21 @@ class ItaPlugin(IVSPlg):
         co = ChromiumOptions()
         co = ChromiumOptions()
         co.auto_port()
         co.auto_port()
         
         
+        # --- [关键配置] 设置独立的用户数据目录 ---
+        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
+        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
+        co.set_user_data_path(self.user_data_path)
+        
         if self.config.proxy and self.config.proxy.ip:
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
             p = self.config.proxy
             if p.username and p.password:
             if p.username and p.password:
                 self._log(f"Configuring authenticated proxy: {p.ip}:{p.port}")
                 self._log(f"Configuring authenticated proxy: {p.ip}:{p.port}")
-                co.add_extension(create_proxy_auth_extension(p.ip, p.port, p.username, p.password))
+                # [关键调用] 生成该实例独享的插件
+                plugin_path = create_proxy_auth_extension(
+                    p.ip, p.port, p.username, p.password, 
+                    self.proxy_ext_path # 传入唯一路径
+                )
+                co.add_extension(plugin_path)
             else:
             else:
                 co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
                 co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
 
 
@@ -170,9 +246,7 @@ class ItaPlugin(IVSPlg):
 
 
         except Exception as e:
         except Exception as e:
             self._log(f"Create Session Failed: {e}")
             self._log(f"Create Session Failed: {e}")
-            if self.page:
-                self.page.quit()
-                self.page = None
+            self.cleanup()
             raise e
             raise e
 
 
     def _handle_login_captcha(self):
     def _handle_login_captcha(self):
@@ -558,4 +632,40 @@ class ItaPlugin(IVSPlg):
                         'remain': remain
                         'remain': remain
                     })
                     })
         except: pass
         except: pass
-        return slots
+        return slots
+    
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        # 1. 关闭浏览器
+        if self.page:
+            try:
+                self.page.quit() # 这会关闭 Chrome 进程
+            except Exception:
+                pass # 忽略已关闭的错误
+            self.page = None
+        
+        # 2. 删除文件
+        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    # 如果删除失败(通常是Windows文件占用),重试
+                    if self.logger: self.logger(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
+            if os.path.exists(self.root_workspace) and self.logger:
+                 self.logger(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+                 
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()

+ 106 - 32
plugins/tls_plugin2.py

@@ -3,6 +3,8 @@ import json
 import random
 import random
 import re
 import re
 import os
 import os
+import uuid
+import shutil
 from datetime import datetime
 from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
 from typing import List, Dict, Optional, Any, Callable
 from urllib.parse import urljoin, urlparse, urlencode
 from urllib.parse import urljoin, urlparse, urlencode
@@ -15,41 +17,73 @@ from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatu
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.vs_cloud_api import VSCloudApi
 
 
-# --- 辅助函数:创建代理插件 ---
-def create_proxy_auth_extension(ip, port, username, password, plugin_path="./chrome_proxy_auth_plugin"):
+def create_proxy_auth_extension(ip, port, username, password, plugin_path):
+    """
+    创建一个 Chrome 插件来自动处理代理认证
+    """
     if not os.path.exists(plugin_path):
     if not os.path.exists(plugin_path):
         os.makedirs(plugin_path)
         os.makedirs(plugin_path)
 
 
+    # 1. manifest.json
     manifest_json = """
     manifest_json = """
     {
     {
         "version": "1.0.0",
         "version": "1.0.0",
         "manifest_version": 2,
         "manifest_version": 2,
         "name": "Chrome Proxy Auth Extension",
         "name": "Chrome Proxy Auth Extension",
-        "permissions": ["proxy", "tabs", "unlimitedStorage", "storage", "<all_urls>", "webRequest", "webRequestBlocking"],
-        "background": {"scripts": ["background.js"]},
+        "permissions": [
+            "proxy",
+            "tabs",
+            "unlimitedStorage",
+            "storage",
+            "<all_urls>",
+            "webRequest",
+            "webRequestBlocking"
+        ],
+        "background": {
+            "scripts": ["background.js"]
+        },
         "minimum_chrome_version": "22.0.0"
         "minimum_chrome_version": "22.0.0"
     }
     }
     """
     """
+
+    # 2. background.js
     background_js = f"""
     background_js = f"""
     var config = {{
     var config = {{
         mode: "fixed_servers",
         mode: "fixed_servers",
         rules: {{
         rules: {{
-            singleProxy: {{scheme: "http", host: "{ip}", port: parseInt({port})}},
+            singleProxy: {{
+                scheme: "http",
+                host: "{ip}",
+                port: parseInt({port})
+            }},
             bypassList: ["localhost"]
             bypassList: ["localhost"]
         }}
         }}
     }};
     }};
+
     chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
     chrome.proxy.settings.set({{value: config, scope: "regular"}}, function() {{}});
+
     function callbackFn(details) {{
     function callbackFn(details) {{
-        return {{authCredentials: {{username: "{username}", password: "{password}"}}}};
+        return {{
+            authCredentials: {{
+                username: "{username}",
+                password: "{password}"
+            }}
+        }};
     }}
     }}
+
     chrome.webRequest.onAuthRequired.addListener(
     chrome.webRequest.onAuthRequired.addListener(
-        callbackFn, {{urls: ["<all_urls>"]}}, ['blocking']
+        callbackFn,
+        {{urls: ["<all_urls>"]}},
+        ['blocking']
     );
     );
     """
     """
+
     with open(os.path.join(plugin_path, "manifest.json"), "w") as f:
     with open(os.path.join(plugin_path, "manifest.json"), "w") as f:
         f.write(manifest_json)
         f.write(manifest_json)
+    
     with open(os.path.join(plugin_path, "background.js"), "w") as f:
     with open(os.path.join(plugin_path, "background.js"), "w") as f:
         f.write(background_js)
         f.write(background_js)
+
     return os.path.abspath(plugin_path)
     return os.path.abspath(plugin_path)
 
 
 
 
@@ -89,8 +123,20 @@ class TlsPlugin2(IVSPlg):
         self.page: Optional[ChromiumPage] = None
         self.page: Optional[ChromiumPage] = None
         
         
         self.travel_group: Optional[Dict] = None
         self.travel_group: Optional[Dict] = None
+        
+        # --- [核心修改] 并发隔离与资源管理 ---
+        # 生成唯一实例 ID
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        # 定义子目录:代理插件目录 & 浏览器用户数据目录
+        self.proxy_ext_path = os.path.join(self.root_workspace, "proxy_ext")
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+        
+        # 确保根目录存在 (子目录由具体逻辑创建)
+        if not os.path.exists(self.root_workspace):
+            os.makedirs(self.root_workspace)
+        
         self.session_create_time: float = 0
         self.session_create_time: float = 0
-        self.real_ip: str = "0.0.0.0"
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
@@ -131,15 +177,25 @@ class TlsPlugin2(IVSPlg):
         """
         """
         全浏览器会话创建:过盾 -> JS注入登录 -> 原生跳转
         全浏览器会话创建:过盾 -> JS注入登录 -> 原生跳转
         """
         """
-        self._log("Initializing Browser Session (Full Browser Mode)...")
+        self._log(f"Initializing Session (ID: {self.instance_id})...")
         co = ChromiumOptions()
         co = ChromiumOptions()
         co.auto_port()
         co.auto_port()
         
         
+        # --- [关键配置] 设置独立的用户数据目录 ---
+        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
+        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
+        co.set_user_data_path(self.user_data_path)
+        
         if self.config.proxy and self.config.proxy.ip:
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
             p = self.config.proxy
             if p.username and p.password:
             if p.username and p.password:
                 self._log(f"Proxy: {p.ip}:{p.port} (Auth)")
                 self._log(f"Proxy: {p.ip}:{p.port} (Auth)")
-                co.add_extension(create_proxy_auth_extension(p.ip, p.port, p.username, p.password))
+                # [关键调用] 生成该实例独享的插件
+                plugin_path = create_proxy_auth_extension(
+                    p.ip, p.port, p.username, p.password, 
+                    self.proxy_ext_path # 传入唯一路径
+                )
+                co.add_extension(plugin_path)
             else:
             else:
                 co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
                 co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
 
 
@@ -233,15 +289,15 @@ class TlsPlugin2(IVSPlg):
                     self.travel_group = g
                     self.travel_group = g
                     break
                     break
             
             
-            if not self.travel_group: raise NotFoundError(f"Group not found for {target_city}")
+            if not self.travel_group:
+                raise NotFoundError(f"Group not found for {target_city}")
             
             
             self.session_create_time = time.time()
             self.session_create_time = time.time()
-            self.real_ip = self._get_realnetwork_ip()
             self._log(f"Session Ready. Group: {self.travel_group['group_number']}")
             self._log(f"Session Ready. Group: {self.travel_group['group_number']}")
 
 
         except Exception as e:
         except Exception as e:
             self._log(f"Session Create Error: {e}")
             self._log(f"Session Create Error: {e}")
-            if self.page: self.page.quit(); self.page = None
+            self.cleanup()
             raise e
             raise e
 
 
     def query(self) -> VSQueryResult:
     def query(self) -> VSQueryResult:
@@ -550,24 +606,6 @@ class TlsPlugin2(IVSPlg):
             self._log(f"Error during firewall refresh: {e}")
             self._log(f"Error during firewall refresh: {e}")
             return False
             return False
 
 
-    def _get_realnetwork_ip(self):
-        """新标签页获取 IP,规避 CORS"""
-        try:
-            tab = self.page.new_tab("https://api.ipify.org/?format=json")
-            if tab.ele('tag:pre'):
-                json_text = tab.ele('tag:pre').text
-            else:
-                json_text = tab.ele('tag:body').text
-            ip = json.loads(json_text)['ip']
-            tab.close()
-            return ip
-        except Exception:
-            # 尝试清理
-            try:
-                if self.page.tabs_count > 1: self.page.close_tabs(self.page.tabs[-1])
-            except: pass
-            return "0.0.0.0"
-
     def _solve_recaptcha(self, params) -> str:
     def _solve_recaptcha(self, params) -> str:
         """调用 VSCloudApi 解决 ReCaptcha"""
         """调用 VSCloudApi 解决 ReCaptcha"""
         key = params.get("apiToken")
         key = params.get("apiToken")
@@ -672,4 +710,40 @@ class TlsPlugin2(IVSPlg):
             if s_date <= curr_date <= e_date:
             if s_date <= curr_date <= e_date:
                 valid_dates.append(date_str)
                 valid_dates.append(date_str)
         random.shuffle(valid_dates)
         random.shuffle(valid_dates)
-        return valid_dates
+        return valid_dates
+    
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        # 1. 关闭浏览器
+        if self.page:
+            try:
+                self.page.quit() # 这会关闭 Chrome 进程
+            except Exception:
+                pass # 忽略已关闭的错误
+            self.page = None
+        
+        # 2. 删除文件
+        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    # 如果删除失败(通常是Windows文件占用),重试
+                    if self.logger: self.logger(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
+            if os.path.exists(self.root_workspace) and self.logger:
+                 self.logger(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+                 
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()

+ 187 - 80
plugins/vfs_plugin2.py

@@ -5,6 +5,8 @@ import time
 import json
 import json
 import random
 import random
 import base64
 import base64
+import uuid
+import shutil
 import re
 import re
 import urllib.parse
 import urllib.parse
 from datetime import datetime
 from datetime import datetime
@@ -21,7 +23,9 @@ from cryptography.hazmat.backends import default_backend
 
 
 from vs_plg import IVSPlg 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.vs_cloud_api import VSCloudApi 
+from toolkit.vs_cloud_api import VSCloudApi
+from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
+
 
 
 # ----------------- 静态常量与辅助数据 -----------------
 # ----------------- 静态常量与辅助数据 -----------------
 
 
@@ -35,11 +39,36 @@ t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
 GQIDAQAB
 GQIDAQAB
 -----END PUBLIC KEY-----"""
 -----END PUBLIC KEY-----"""
 
 
-# (Country Map 省略以节省篇幅,请保持原样)
 COUNTRY_MAP = {
 COUNTRY_MAP = {
     "afghanistan": "AFG", "albania": "ALB", "algeria": "DZA", "andorra": "AND",  "angola": "AGO",
     "afghanistan": "AFG", "albania": "ALB", "algeria": "DZA", "andorra": "AND",  "angola": "AGO",
-    "china": "CHN", "united kingdom": "GBR", "netherlands": "NLD", 
-    # ... 请保留你原来的完整映射 ...
+    "antigua and barbuda": "ATG", "argentina": "ARG", "armenia": "ARM", "australia": "AUS", "austria": "AUT",
+    "azerbaijan": "AZE", "bahamas": "BHS", "bahrain": "BHR", "bangladesh": "BGD", "barbados": "BRB", "belarus": "BLR",
+    "belgium": "BEL", "belize": "BLZ", "benin": "BEN", "bhutan": "BTN", "bolivia": "BOL", "bosnia and herzegovina": "BIH",
+    "botswana": "BWA", "brazil": "BRA", "brunei": "BRN", "bulgaria": "BGR", "burkina faso": "BFA", "burundi": "BDI",
+    "cabo verde": "CPV", "cambodia": "KHM", "cameroon": "CMR", "canada": "CAN", "central african republic": "CAF",
+    "chad": "TCD", "chile": "CHL", "china": "CHN", "colombia": "COL", "comoros": "COM", "congo (brazzaville)": "COG",
+    "congo (kinshasa)": "COD", "costa rica": "CRI", "croatia": "HRV", "cuba": "CUB", "cyprus": "CYP", "czech republic": "CZE",
+    "denmark": "DNK", "djibouti": "DJI", "dominica": "DMA", "dominican republic": "DOM", "ecuador": "ECU", "egypt": "EGY",
+    "el salvador": "SLV", "equatorial guinea": "GNQ", "eritrea": "ERI", "estonia": "EST", "eswatini": "SWZ", "ethiopia": "ETH",
+    "fiji": "FJI", "finland": "FIN", "france": "FRA", "gabon": "GAB", "gambia": "GMB", "georgia": "GEO", "germany": "DEU",
+    "ghana": "GHA", "greece": "GRC", "grenada": "GRD", "guatemala": "GTM", "guinea": "GIN", "guinea-bissau": "GNB", "guyana": "GUY",
+    "haiti": "HTI", "honduras": "HND", "hungary": "HUN", "iceland": "ISL", "india": "IND", "indonesia": "IDN", "iran": "IRN",
+    "iraq": "IRQ", "ireland": "IRL", "israel": "ISR", "italy": "ITA", "jamaica": "JAM", "japan": "JPN", "jordan": "JOR",
+    "kazakhstan": "KAZ", "kenya": "KEN", "kiribati": "KIR", "korea, north": "PRK", "korea, south": "KOR", "kuwait": "KWT",
+    "kyrgyzstan": "KGZ", "laos": "LAO", "latvia": "LVA", "lebanon": "LBN", "lesotho": "LSO", "liberia": "LBR", "libya": "LBY",
+    "liechtenstein": "LIE", "lithuania": "LTU", "luxembourg": "LUX", "madagascar": "MDG", "malawi": "MWI", "malaysia": "MYS",
+    "maldives": "MDV", "mali": "MLI", "malta": "MLT", "marshall islands": "MHL", "mauritania": "MRT", "mauritius": "MUS",
+    "mexico": "MEX", "micronesia": "FSM", "moldova": "MDA", "monaco": "MCO", "mongolia": "MNG", "montenegro": "MNE", "morocco": "MAR",
+    "mozambique": "MOZ", "myanmar": "MMR", "namibia": "NAM", "nauru": "NRU", "nepal": "NPL", "netherlands": "NLD", "new zealand": "NZL",
+    "nicaragua": "NIC", "niger": "NER", "nigeria": "NGA", "north macedonia": "MKD", "norway": "NOR", "oman": "OMN", "pakistan": "PAK",
+    "palau": "PLW", "panama": "PAN", "papua new guinea": "PNG", "paraguay": "PRY", "peru": "PER", "philippines": "PHL", "poland": "POL",
+    "portugal": "PRT", "qatar": "QAT", "romania": "ROU", "russia": "RUS", "rwanda": "RWA", "saudi arabia": "SAU", "senegal": "SEN",
+    "serbia": "SRB", "seychelles": "SYC", "sierra leone": "SLE", "singapore": "SGP", "slovakia": "SVK", "slovenia": "SVN",
+    "solomon islands": "SLB", "somalia": "SOM", "south africa": "ZAF", "spain": "ESP", "sri lanka": "LKA", "sudan": "SDN",
+    "suriname": "SUR", "sweden": "SWE", "switzerland": "CHE", "syria": "SYR", "tajikistan": "TJK", "tanzania": "TZA", "thailand": "THA",
+    "timor-leste": "TLS", "togo": "TGO", "tonga": "TON", "tunisia": "TUN", "turkey": "TUR", "turkmenistan": "TKM", "uganda": "UGA",
+    "ukraine": "UKR", "united arab emirates": "ARE", "united kingdom": "GBR", "united states": "USA", "uruguay": "URY", "uzbekistan": "UZB",
+    "vanuatu": "VUT", "venezuela": "VEN", "vietnam": "VNM", "yemen": "YEM", "zambia": "ZMB", "zimbabwe": "ZWE"
 }
 }
 
 
 def get_country_iso3(name: str) -> str:
 def get_country_iso3(name: str) -> str:
@@ -52,7 +81,7 @@ def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%
     except:
     except:
         return data_str
         return data_str
 
 
-def create_proxy_auth_extension(ip, port, username, password, plugin_path="./chrome_proxy_auth_plugin"):
+def create_proxy_auth_extension(ip, port, username, password, plugin_path):
     """
     """
     创建一个 Chrome 插件来自动处理代理认证
     创建一个 Chrome 插件来自动处理代理认证
     """
     """
@@ -167,6 +196,19 @@ class VfsPlugin2(IVSPlg):
             VFS_PUBLIC_KEY_PEM.encode(),
             VFS_PUBLIC_KEY_PEM.encode(),
             backend=default_backend()
             backend=default_backend()
         )
         )
+        
+        # --- [核心修改] 并发隔离与资源管理 ---
+        # 生成唯一实例 ID
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        # 定义子目录:代理插件目录 & 浏览器用户数据目录
+        self.proxy_ext_path = os.path.join(self.root_workspace, "proxy_ext")
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+        
+        # 确保根目录存在 (子目录由具体逻辑创建)
+        if not os.path.exists(self.root_workspace):
+            os.makedirs(self.root_workspace)
+            
         self.session_create_time: float = 0
         self.session_create_time: float = 0
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
@@ -210,49 +252,45 @@ class VfsPlugin2(IVSPlg):
         使用 DrissionPage 创建会话:
         使用 DrissionPage 创建会话:
         1. 启动浏览器
         1. 启动浏览器
         2. 导航到登录页
         2. 导航到登录页
-        3. 自动过盾并提取 Token
+        3. 自动过盾并提取 Token (集成 CloudflareBypasser)
         4. JS fetch 登录
         4. JS fetch 登录
         """
         """
-        self._log("Initializing Browser Session...")
+        self._log(f"Initializing Session (ID: {self.instance_id})...")
         
         
         # 0. 配置浏览器
         # 0. 配置浏览器
         co = ChromiumOptions()
         co = ChromiumOptions()
-        co.auto_port() # 自动分配端口
+        co.auto_port() 
+        
+        # --- [关键配置] 设置独立的用户数据目录 ---
+        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
+        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
+        co.set_user_data_path(self.user_data_path)
         
         
+        # 代理配置
         if self.config.proxy and self.config.proxy.ip:
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
             p = self.config.proxy
-        
-        # 情况 A: 有账号密码 -> 使用插件方案
-        if p.username and p.password:
-            self._log(f"Configuring authenticated proxy: {p.ip}:{p.port}")
-            plugin_path = create_proxy_auth_extension(
-                ip=p.ip,
-                port=p.port,
-                username=p.username,
-                password=p.password
-            )
-            co.add_extension(plugin_path)
-        
-        # 情况 B: 无账号密码 (IP白名单模式) -> 直接设置
-        else:
-            self._log(f"Configuring standard proxy: {p.ip}:{p.port}")
-            co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
+            if p.username and p.password:
+                self._log(f"Configuring authenticated proxy: {p.ip}:{p.port}")
+                # [关键调用] 生成该实例独享的插件
+                plugin_path = create_proxy_auth_extension(
+                    p.ip, p.port, p.username, p.password, 
+                    self.proxy_ext_path # 传入唯一路径
+                )
+                co.add_extension(plugin_path)
+            else:
+                self._log(f"Configuring standard proxy: {p.ip}:{p.port}")
+                co.set_proxy(f"{p.scheme}://{p.ip}:{p.port}")
             
             
-        # 无头模式 (生产环境建议 True, 调试 False)
-        # co.headless(True) 
-        co.headless(False) # 调试时设为 False 方便观察
-        
-        # 反爬参数
+        co.headless(False) 
         co.set_argument('--no-sandbox')
         co.set_argument('--no-sandbox')
         co.set_argument('--disable-gpu')
         co.set_argument('--disable-gpu')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--window-size=1920,1080')
-        # 禁用自动化特征
         co.set_argument('--disable-blink-features=AutomationControlled')
         co.set_argument('--disable-blink-features=AutomationControlled')
 
 
         try:
         try:
             self.page = ChromiumPage(co)
             self.page = ChromiumPage(co)
             
             
-            # 1. 导航到登录页面 (建立 Context)
+            # 1. 导航到登录页面
             mission = self.free_config.get("mission_code", "")
             mission = self.free_config.get("mission_code", "")
             country = self.free_config.get("country_code", "")
             country = self.free_config.get("country_code", "")
             lang = self.free_config.get("language", "en")
             lang = self.free_config.get("language", "en")
@@ -265,37 +303,64 @@ class VfsPlugin2(IVSPlg):
             
             
             self.page.get(login_page_url)
             self.page.get(login_page_url)
             
             
-            # 2. 等待 Cloudflare 验证通过
-            # DrissionPage 会自动处理 Turnstile,我们只需要等待结果出现
-            # 通常 CF 的 widget 会生成一个 hidden input name="cf-turnstile-response"
-            self._log("Waiting for Cloudflare challenge...")
+            # -------------------------------------------------------------
+            # [核心修改] 2. 智能 Cloudflare 过盾逻辑
+            # -------------------------------------------------------------
+            self._log("Handling Cloudflare challenge...")
             
             
-            # 最多等待 30 秒
+            # 初始化过盾助手
+            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
             cf_token = ""
             cf_token = ""
-            for _ in range(10):
-                # 间隔 1 秒
+            
+            # 循环检测 (40秒超时)
+            for i in range(40):
                 time.sleep(1)
                 time.sleep(1)
+                
+                # A. 优先处理 Cookie 遮挡 (VFS 必须步骤)
+                # 如果不关掉 cookie banner,验证码可能点不到
                 self._handle_cookie_banner()
                 self._handle_cookie_banner()
-                # 尝试从 DOM 获取 Token
+                
+                # B. 尝试从 DOM 获取 Token (无感验证可能自动通过)
                 try:
                 try:
-                    # 检查是否有 cf-turnstile-response 元素且有值
-                    ele = self.page.ele('xpath://input[@name="cf-turnstile-response"]')
+                    ele = self.page.ele('@name=cf-turnstile-response')
                     if ele and ele.value:
                     if ele and ele.value:
                         cf_token = ele.value
                         cf_token = ele.value
-                        self._log("Cloudflare Turnstile token extracted from DOM.")
+                        self._log("Cloudflare Turnstile token extracted.")
                         break
                         break
                 except:
                 except:
                     pass
                     pass
                 
                 
-                # 也可以检查是否已经看到了登录框 (id="mat-input-0" 或 form)
-                if self.page.ele('xpath://form'):
+                # C. 如果前 3 秒没自动出 Token,开始尝试点击
+                if i > 2:
+                    try:
+                        # 开启 DFS 深度搜索模式 (防止 Shadow DOM 嵌套太深找不到)
+                        # 在第 10 秒后开启深度搜索,前期用快速搜索
+                        use_dfs = (i > 10)
+                        
+                        cf_bypasser.click_verification_button(is_dfs=use_dfs)
+                    except Exception as e:
+                        # 点击错误忽略,继续下一轮
+                        pass
+                
+                # D. 检查是否已经看到了登录框 (有时候 Token 提取慢了,但页面已经变了)
+                if self.page.ele('tag:form') or self.page.ele('#mat-input-0'):
                     self._log("Login form detected.")
                     self._log("Login form detected.")
-                    # 即使 form 出来了,有时候 token 还在生成,稍微再等一下
+                    # 继续尝试提取一次 Token,如果实在没有也不要死循环
+                    if i > 5 and not cf_token:
+                        self._log("Form visible but token not found yet...")
+            
+            # -------------------------------------------------------------
+            
+            if not cf_token:
+                # 最后尝试一次强取
+                try:
+                    cf_token = self.page.ele('@name=cf-turnstile-response').value
+                except:
+                    pass
             
             
-            # 如果没拿到 token,尝试直接继续,或者报错
-            # 注意:有些 VFS 页面可能没有显式的 turnstile,而是隐式的
             if not cf_token:
             if not cf_token:
-                self._log("[WARN] Could not extract Turnstile token. Trying to proceed anyway...")
+                self._log("[WARN] Could not extract Turnstile token.")
+                raise BizLogicError(f"Could not extract Turnstile token.")
 
 
             # 3. 准备登录 API 参数
             # 3. 准备登录 API 参数
             email = self.config.account.username
             email = self.config.account.username
@@ -309,8 +374,7 @@ class VfsPlugin2(IVSPlg):
             headers = self._get_common_headers(with_auth=False)
             headers = self._get_common_headers(with_auth=False)
             headers.update({
             headers.update({
                 "clientsource": client_src,
                 "clientsource": client_src,
-                "orangex": orange_src,
-                # DrissionPage fetch 不需要 content-type,json参数会自动加
+                "orangex": orange_src
             })
             })
             
             
             data = {
             data = {
@@ -320,7 +384,7 @@ class VfsPlugin2(IVSPlg):
                 "countrycode": country,
                 "countrycode": country,
                 "languageCode": "en-US",
                 "languageCode": "en-US",
                 "captcha_version": "cloudflare-v1",
                 "captcha_version": "cloudflare-v1",
-                "captcha_api_key": cf_token  # 填入提取到的 Token
+                "captcha_api_key": cf_token 
             }
             }
             
             
             self._log("Sending Login Request via Browser Fetch...")
             self._log("Sending Login Request via Browser Fetch...")
@@ -335,6 +399,8 @@ class VfsPlugin2(IVSPlg):
             # 分支 2: OTP
             # 分支 2: OTP
             elif resp_json.get("enableOTPAuthentication"):
             elif resp_json.get("enableOTPAuthentication"):
                 self._log("Login requires OTP.")
                 self._log("Login requires OTP.")
+                # 注意:_submit_login_otp 内部也会调用 _refresh_turnstile_token
+                # 所以这里旧的 cf_token 其实用处不大,传过去也没事
                 otp = self._read_otp_email()
                 otp = self._read_otp_email()
                 self._submit_login_otp(cf_token, otp)
                 self._submit_login_otp(cf_token, otp)
             
             
@@ -342,7 +408,6 @@ class VfsPlugin2(IVSPlg):
                 raise BizLogicError(f"Login failed: {resp.text[:200]}")
                 raise BizLogicError(f"Login failed: {resp.text[:200]}")
 
 
             self.session_create_time = time.time()
             self.session_create_time = time.time()
-            # 获取真实IP (用于日志)
             try:
             try:
                 self.real_ip = self._get_realnetwork_ip()
                 self.real_ip = self._get_realnetwork_ip()
             except:
             except:
@@ -350,9 +415,7 @@ class VfsPlugin2(IVSPlg):
                 
                 
         except Exception as e:
         except Exception as e:
             self._log(f"Create Session Failed: {e}")
             self._log(f"Create Session Failed: {e}")
-            if self.page:
-                self.page.quit()
-                self.page = None
+            self.cleanup()
             raise e
             raise e
 
 
     def query(self) -> VSQueryResult:
     def query(self) -> VSQueryResult:
@@ -372,7 +435,7 @@ class VfsPlugin2(IVSPlg):
             result.availability_status = AvailabilityStatus.NoneAvailable
             result.availability_status = AvailabilityStatus.NoneAvailable
             result.visa_type = apt_config.get("visa_type", "")
             result.visa_type = apt_config.get("visa_type", "")
             result.city = apt_config.get("city", "")
             result.city = apt_config.get("city", "")
-            
+            result.routing_key = apt_config.get("routing_key", "")
             if earliest_date:
             if earliest_date:
                 result.success = True
                 result.success = True
                 if "WaitList" in earliest_date:
                 if "WaitList" in earliest_date:
@@ -757,12 +820,11 @@ class VfsPlugin2(IVSPlg):
     
     
     def _refresh_turnstile_token(self) -> str:
     def _refresh_turnstile_token(self) -> str:
         """
         """
-        强制刷新 Cloudflare Turnstile 并获取新 Token (增强版)
+        强制刷新 Cloudflare Turnstile 并获取新 Token (集成 CloudflareBypasser 版)
         """
         """
         self._log("Refreshing Cloudflare Turnstile token...")
         self._log("Refreshing Cloudflare Turnstile token...")
         
         
-        # 1. JS 强制重置
-        # 加上 try-catch 防止页面没有 turnstile 对象导致崩溃
+        # 1. JS 强制重置 (保持不变)
         js_reset = """
         js_reset = """
         try {
         try {
             var input = document.querySelector('input[name="cf-turnstile-response"]');
             var input = document.querySelector('input[name="cf-turnstile-response"]');
@@ -774,32 +836,41 @@ class VfsPlugin2(IVSPlg):
         """
         """
         self.page.run_js(js_reset)
         self.page.run_js(js_reset)
         
         
-        # 2. 轮询等待 (增加到 30 秒)
-        # 策略:检测 Token -> 如果没有且有 iframe -> 点击 iframe 触发验证
-        for i in range(60): # 60 * 0.5s = 30s
+        # 2. 初始化过盾助手
+        # 假设 CloudflareBypasser 类已在当前文件中定义
+        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
+        
+        # 3. 轮询等待 (30秒)
+        for i in range(60): 
             time.sleep(0.5)
             time.sleep(0.5)
             
             
-            # A. 尝试直接获取 Token (使用 JS 获取更稳定)
-            token = self.page.run_js('return document.querySelector("input[name=\'cf-turnstile-response\']")?.value')
-            if token:
-                self._log("Turnstile token refreshed successfully.")
-                return token
+            # A. 检查 Token 是否已生成
+            # 使用 DrissionPage 的方式获取 value 比较稳定
+            try:
+                ele = self.page.ele('@name=cf-turnstile-response')
+                if ele and ele.value:
+                    self._log("Turnstile token refreshed successfully.")
+                    return ele.value
+            except:
+                pass
             
             
-            # B. 如果等待了 3 秒还没结果,尝试寻找 iframe 并点击
-            # Cloudflare 有时需要用户点一下 "Verify you are human"
-            if i > 6 and (i % 5 == 0): # 每隔 2.5 秒尝试点一次
+            # B. 尝试点击验证框
+            # 策略:前2秒等待,之后开始尝试点击
+            if i > 4: 
+                # [重要] VFS 经常有 Cookie 弹窗遮挡,先尝试清理一下
+                self._handle_cookie_banner()
+                
                 try:
                 try:
-                    # 查找包含 turnstile 或 cloudflare 的 iframe
-                    # VFS 页面通常只有一个
-                    cf_iframe = self.page.ele('xpath://iframe[contains(@src, "turnstile") or contains(@src, "cloudflare")]')
-                    if cf_iframe:
-                        # 尝试点击 iframe 的中心位置
-                        # self._log("Clicking Cloudflare widget to activate...")
-                        cf_iframe.click(by_js=True) 
-                except Exception:
+                    # 使用 CloudflareBypasser 的高级点击逻辑
+                    # is_dfs=True 表示如果普通搜索找不到,就递归搜索 iframe (更耗时但更强)
+                    # 我们在尝试 10 次 (5秒) 后开启 DFS 模式
+                    use_dfs = (i > 14)
+                    
+                    cf_bypasser.click_verification_button(is_dfs=use_dfs)
+                except Exception as e:
+                    # 点击过程报错不要中断主循环
                     pass
                     pass
         
         
-        # 如果超时,为了调试,打印一下当前页面源码的一部分或截图(可选)
         raise BizLogicError("Failed to refresh Cloudflare Turnstile token (Timeout)")
         raise BizLogicError("Failed to refresh Cloudflare Turnstile token (Timeout)")
 
 
     # -------------------------------------------------------------
     # -------------------------------------------------------------
@@ -1373,4 +1444,40 @@ class VfsPlugin2(IVSPlg):
                 curr = curr.replace(year=curr.year + 1, month=1)
                 curr = curr.replace(year=curr.year + 1, month=1)
             else:
             else:
                 curr = curr.replace(month=curr.month + 1)
                 curr = curr.replace(month=curr.month + 1)
-        return months
+        return months
+    
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        # 1. 关闭浏览器
+        if self.page:
+            try:
+                self.page.quit() # 这会关闭 Chrome 进程
+            except Exception:
+                pass # 忽略已关闭的错误
+            self.page = None
+        
+        # 2. 删除文件
+        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    # 如果删除失败(通常是Windows文件占用),重试
+                    if self.logger: self.logger(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
+            if os.path.exists(self.root_workspace) and self.logger:
+                 self.logger(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+                 
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()