Hujiarui 1 mesiac pred
rodič
commit
06a929149f

+ 2 - 2
booker_standalone.py

@@ -137,9 +137,9 @@ class BookerStandalone:
         self.m_instance.set_log(self.m_logger)
         self.m_instance.set_config(plg_cfg)
         
-        self.m_instance.create_session()
-        self.m_last_login_time = time.time()
         self.m_next_query_time = time.time() + self._get_wait_interval()
+        self.m_last_login_time = time.time()
+        self.m_instance.create_session()
         self._log("Session created successfully.")
 
     def start(self):

+ 57 - 0
config/config_tls.json.example

@@ -0,0 +1,57 @@
+{
+  "redis": {
+    "host": "text.skin",
+    "port": 6379,
+    "db": 0,
+    "password": "STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8"
+  },
+  "booker_standalone_config": {
+    "debug": true,
+    "plugin_config": {
+      "lib_path": "plugins",
+      "plugin_name": "tls_plugin",
+      "plugin_bin": "tls_plugin.py",
+      "plugin_proto": "IVSPlg"
+    },
+    "account": {
+      "username": "18305503085@163.com",
+      "password": "Zhangww@201207"
+    },
+    "proxy_pool": ["proxy-cheap"],
+    "query_wait": {
+      "mode": "Random",
+      "fixed_wait": 10,
+      "random_min": 55,
+      "random_max": 65
+    },
+    "login_interval": 1800,
+    "session_max_life": 3600,
+    "active_time_start": "00:00",
+    "active_time_end": "23:30",
+    "appointment_types": [
+      {
+        "weight": 10,
+        "routing_key": "slot.sha.fr.tourist",
+        "city": "Shanghai",
+        "visa_type": "Tourist",
+        "country": "France"
+      }
+    ],
+    "user_preferences": {
+      "support_pta": false,
+      "expected_end_date": "2024-07-01",
+      "expected_start_date": "2024-07-20"
+    },
+    "free_config": {
+      "tls_url": "https://visas-fr.tlscontact.com/en-us/country/cn/vac/cnSHA2fr",
+      "location": "Shanghai",
+      "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
+      "login_captcha": {
+        "solve_advance": false,
+        "site_key": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0",
+        "page_url": "https://i2-auth.visas-fr.tlscontact.com",
+        "task": "ReCaptchaV2TaskProxyLess"
+      }
+    }
+  }
+}

+ 66 - 0
config/config_usa.json.example

@@ -0,0 +1,66 @@
+{
+  "redis": {
+    "host": "text.skin",
+    "port": 6379,
+    "db": 0,
+    "password": "STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8"
+  },
+  "booker_standalone_config": {
+    "debug": true,
+    "plugin_config": {
+      "lib_path": "plugins",
+      "plugin_name": "usa_plugin",
+      "plugin_bin": "usa_plugin.py",
+      "plugin_proto": "IVSPlg"
+    },
+    "account": {
+      "username": "Max0105888",
+      "password": "Zl265498"
+    },
+    "proxy_pool": [
+      "proxy-cheap"
+    ],
+    "query_wait": {
+      "mode": "Random",
+      "fixed_wait": 10,
+      "random_min": 60,
+      "random_max": 600
+    },
+    "login_interval": 3600,
+    "session_max_life": 3600,
+    "active_time_start": "00:00",
+    "active_time_end": "23:30",
+    "appointment_types": [
+      {
+        "weight": 10,
+        "routing_key": "slot.wuh.us.tourist",
+        "city": "Wuhan",
+        "visa_type": "Tourist",
+        "country": "USA"
+      }
+    ],
+    "user_preferences": {
+      "expected_date_start": "2026-06-01",
+      "expected_date_end": "2026-08-05"
+    },
+    "free_config": {
+      "applicant": "jinhao ma",
+      "usa_url": "https://www.usvisascheduling.com",
+      "location": "WUHAN",
+      "security": {
+        "1_quest": {
+          "q": "What is your mother's surname?",
+          "a": "zhou"
+        },
+        "2_quest": {
+          "q": "What is your least favorite food?",
+          "a": "kfc"
+        },
+        "3_quest": {
+          "q": "Who was your childhood hero?",
+          "a": "123456"
+        }
+      }
+    }
+  }
+}

+ 94 - 0
config/config_vfs.json.example

@@ -0,0 +1,94 @@
+{
+    "redis": {
+        "host": "text.skin",
+        "port": 6379,
+        "db": 0,
+        "password": "STEs2x6ML0U1HlpE9SojM6YU7QPhqzY8"
+    },
+    "booker_standalone_config": {
+        "debug": true,
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "vfs_plugin",
+            "plugin_bin": "vfs_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "account": {
+            "username": "italyvisa888@text.skin",
+            "password": "Visafly@111"
+        },
+        "proxy_pool": [
+            "proxy-cheap"
+        ],
+        "query_wait": {
+            "mode": "Random",
+            "fixed_wait": 10,
+            "random_min": 120,
+            "random_max": 180
+        },
+        "login_interval": 7200,
+        "session_max_life": 1800,
+        "active_time_start": "00:00",
+        "active_time_end": "23:30",
+        "appointment_types": [
+            {
+                "weight": 0,
+                "routing_key": "slot.csx.it.tourist",
+                "city": "Changsha",
+                "visa_type": "Tourist",
+                "country": "Italy"
+            },
+            {
+                "weight": 10,
+                "routing_key": "slot.bjs.it.tourist",
+                "city": "Beijing",
+                "visa_type": "Tourist",
+                "country": "Italy"
+            }
+        ],
+        "user_preferences": {
+            "email": "italyvisa666@text.skin",
+            "phone": "019074224604",
+            "gender": "female",
+            "birthday": "1997-09-09",
+            "last_name": "ZHAO",
+            "first_name": "LINJIE",
+            "nationality": "China",
+            "passport_no": "EE4571001",
+            "expected_end_date": "2026-07-03",
+            "phone_country_code": "86",
+            "expected_start_date": "2026-06-12",
+            "passport_expiry_date": "2028-10-08",
+            "social_media_account": "13370148298",
+            "passport_image_url": "data/passport.jpg"
+        },
+        "free_config": {
+            "mission_code": "ita",
+            "mission_name": "Italy",
+            "country_code": "chn",
+            "country_name": "China",
+            "culture_code": "en-US",
+            "language": "en",
+            "apt_configs": {
+                "slot.csx.it.tourist": {
+                    "center_name": "Italy Visa Application Center, Changsha",
+                    "address": "1F, Changsha Rongchuang Steigenberger Hotel, No. 3-9, Xueyuan Road, Changsha County, Changsha City",
+                    "vac_code": "CGA",
+                    "category_name": "SchengenVisa",
+                    "category_code": "Schengen Visa",
+                    "subcategory_name": "C visa / Schengen visa",
+                    "subcategory_code": "Schengen "
+                },
+                "slot.bjs.it.tourist": {
+                    "center_name": "Italy Visa Application Center, Beijing",
+                    "address": "East 101, Floor B1, Block C, Guanghua Lu SOHO II, No. 9 Guanghua Lu, Chaoyang District",
+                    "vac_code": "BJII",
+                    "category_name": "Beijing Italy",
+                    "category_code": "Beijing Italy",
+                    "subcategory_name": "Standard Beijing Italy",
+                    "subcategory_code": "BJ Italy Standard"
+                }
+            }
+        }
+    }
+}

+ 320 - 327
plugins/ita_plugin.py

@@ -7,16 +7,17 @@ import shutil
 import re
 import os
 import base64
+from concurrent.futures import ThreadPoolExecutor
 from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
 from urllib.parse import urlencode, urlparse
 
-# DrissionPage 核心
 from DrissionPage import ChromiumPage, ChromiumOptions
 
+import configure
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
-from toolkit.proxy_tunnel import ProxyTunnel
+from toolkit.mihomo_tunnel import MihomoTunnel
 from toolkit.vs_cloud_api import VSCloudApi
 from utils.mouse import HumanMouse
 from utils.keyboard import HumanKeyboard
@@ -53,21 +54,14 @@ class ItaPlugin(IVSPlg):
         
         # Prenotami 特有配置
         self._service_id = 0 
-        self._host = 'https://prenotami.esteri.it'
+        self.ita_url = 'https://prenotami.esteri.it'
         
-                
-        # --- [核心修改] 并发隔离与资源管理 ---
-        # 生成唯一实例 ID
         self.instance_id = uuid.uuid4().hex[:8]
         self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
-        # 定义子目录:代理插件目录 & 浏览器用户数据目录
         self.user_data_path = os.path.join(self.root_workspace, "user_data")
         
-        # 确保根目录存在 (子目录由具体逻辑创建)
         if not os.path.exists(self.root_workspace):
             os.makedirs(self.root_workspace)
-            
-        # 持有隧道实例
         self.tunnel = None
         self.session_create_time: float = 0
     
@@ -103,19 +97,12 @@ class ItaPlugin(IVSPlg):
                 return False
         return True
 
-    # -------------------------------------------------------------
-    # 1. Create Session (Login)
-    # -------------------------------------------------------------
     def create_session(self):
         """
-        全浏览器登录流程:
-        1. 启动浏览器
-        2. 解决 ReCaptcha
-        3. 登录并维持 Session
+        全浏览器会话创建:过盾 -> JS注入登录 -> 状态机自动路由导航 -> 到达目标页
         """
         self._log(f"Initializing Session (ID: {self.instance_id})...")
         co = ChromiumOptions()
-        
         def get_free_port():
             with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                 s.bind(('', 0))
@@ -123,115 +110,245 @@ class ItaPlugin(IVSPlg):
         
         debug_port = get_free_port()
         self._log(f"Assigned Debug Port: {debug_port}")
+        self._log(f"Account id={self.config.account.id}, proxy id={self.config.proxy.id}")
+    
+        co.set_local_port(debug_port)       
         co.set_user_data_path(self.user_data_path)
         
-        chrome_path = os.getenv("CHROME_BIN")
+        chrome_path = configure.CHROME_PATH
+        if not chrome_path:
+            chrome_path = os.getenv("CHROME_BIN")
         if chrome_path and os.path.exists(chrome_path):
             co.set_paths(browser_path=chrome_path)
         
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
-            
             if p.username and p.password:
                 self._log(f"Starting Proxy Tunnel for {p.ip}...")
-                
-                self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
+                exit_node = {
+                    "name": "ExitNode",
+                    "type": p.proto,
+                    "server": p.ip,
+                    "port": p.port,
+                    "username": p.username,
+                    "password": p.password
+                }
+                relay_node = None
+                if configure.MIHOMO_RELAY_NODES:
+                    relay_node = random.choice(configure.MIHOMO_RELAY_NODES)
+                mihomo_path = configure.MIHOMO_BIN_PATH
+                if not mihomo_path:
+                    mihomo_path = os.getenv("MIHOMO_BIN")
+                if not mihomo_path:
+                    raise BizLogicError(message='Mihomo path is null, You need set mihomo bin path in configure or os env')
+                self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
                 local_proxy = self.tunnel.start()
-                
                 self._log(f"Tunnel started at {local_proxy}")
                 co.set_argument(f'--proxy-server={local_proxy}')
-                
             else:
                 proxy_str = f"{p.proto}://{p.ip}:{p.port}"
                 co.set_argument(f'--proxy-server={proxy_str}')
         else:
             self._log("[WARN] No proxy configured!")
 
-        fingerprint_gen = FingerprintGenerator()
-        specific_fp = fingerprint_gen.generate(self.config.account.username)
-        self._log(f'browser fingerprint={specific_fp}')
+        specific_fp = FingerprintGenerator().generate(self.config.account.username)
+        fp_seed = specific_fp.get("seed")
+        fp_platform = specific_fp.get("platform")
+        fp_brand = specific_fp.get("brand")
+        self._log(f'browser fingerprint seed={fp_seed}')
 
-        co.headless(False) 
+        co.headless(False)
         co.set_argument('--no-sandbox')
-        co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
-        co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
-        co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
-        co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
-        try:
-            self.page = ChromiumPage(co)
-            login_url = f"{self._host}/Home"
-            self._log(f"Navigating to {login_url}")
-            self.page.get(login_url)
+        co.set_argument(f"--fingerprint={fp_seed}")
+        co.set_argument(f"--fingerprint-platform={fp_platform}")
+        co.set_argument(f"--fingerprint-brand={fp_brand}")
+        
+        self.page = ChromiumPage(co)
+        if self.config.debug:
+            self.page.get('https://example.com')
+            js_script = """
+            function getFingerprint() {
+                let webglVendor = 'Unknown';
+                let webglRenderer = 'Unknown';
+                try {
+                    let canvas = document.createElement('canvas');
+                    let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+                    if (gl) {
+                        let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
+                        if (debugInfo) {
+                            webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
+                            webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
+                        }
+                    }
+                } catch(e) {}
+
+                return {
+                    "User-Agent": navigator.userAgent,
+                    "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
+                    "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
+                    "CPU Cores": navigator.hardwareConcurrency,
+                    "Language": navigator.language,
+                    "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
+                    "WebGL Vendor": webglVendor,
+                    "WebGL Renderer": webglRenderer
+                };
+            }
+            return getFingerprint();
+            """
+            fp_data = self.page.run_js(js_script)
+            self._log("================ 预检浏览器指纹数据 ================")
+            self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
+            self._log("====================================================")
             
-            self._log("Init humanize tools...")
-            self.mouse = HumanMouse(self.page, debug=True)
-            self.keyboard = HumanKeyboard(self.page)
-            self._log("Random mouse start position...")
-            viewport_width = self.page.rect.viewport_size[0]
-            viewport_height = self.page.rect.viewport_size[1]
-            init_x = random.randint(10, viewport_width - 10)
-            init_y = random.randint(10, viewport_height - 10)
-            self.mouse.move(init_x, init_y) 
+        self.page = ChromiumPage(co)
+        ita_url = self.ita_url
+        self._log(f"Navigating to {ita_url}")
+        self.page.get(ita_url)
+        
+        self._log("Init humanize tools...")
+        self.mouse = HumanMouse(self.page, debug=True)
+        self.keyboard = HumanKeyboard(self.page)
+        self._log("Random mouse start position...")
+        viewport_width = self.page.rect.viewport_size[0]
+        viewport_height = self.page.rect.viewport_size[1]
+        init_x = random.randint(10, viewport_width - 10)
+        init_y = random.randint(10, viewport_height - 10)
+        self.mouse.move(init_x, init_y) 
+        
+        switch_en_btn = self.page.ele('tag:a@@href=/Language/ChangeLanguage?lang=2')
+        self.mouse.human_click_ele(switch_en_btn)
+        self.page.wait.load_start() 
+        time.sleep(5)
+
+        max_steps = 15
+        stuck_counter = 0
+        last_url = ""
+        session_created = False
+        has_submitted_login = False
+        
+        username = self.config.account.username
+        password = self.config.account.password
+        
+        for step in range(max_steps):
+            self.page.wait.doc_loaded()
+            time.sleep(1)
             
-            # 等待登录框
-            if not self.page.wait.ele_displayed('#login-email', timeout=20):
-                raise BizLogicError("Login page not loaded")
-
-            # 填充用户名密码
-            self.mouse.human_click_ele(self.page.ele('#login-email'))
-            self.keyboard.type_text(self.config.account.username)
+            current_url = self.page.url
+            current_title = self.page.title.lower()
+            current_html_content = self.page.html
+            self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
             
-            self.mouse.human_click_ele(self.page.ele('#login-password'))
-            self.keyboard.type_text(self.config.account.password)
+            # --- [异常处理与反爬对抗层] ---
+            if current_url == last_url:
+                stuck_counter += 1
+            else:
+                last_url = current_url
+                stuck_counter = 0
+                
+            if stuck_counter >= 3:
+                self._log("[WARN] Page stucked, try to refresh...")
+                self.page.refresh()
+                self._random_sleep(5, 8)
+                stuck_counter = 0
+                continue
             
-            # 先定位
-            self._log("Locating Login button...")
-            login_btn = self.page.ele('#captcha-trigger')
+            server_error_indicators = ["502 Bad Gateway", "503 Service Temporarily Unavailable", "error 1020"]
+            if any(err in current_html_content for err in server_error_indicators):
+                self._log(f"[WARN] Server network error, try to refresh (Step: {step})...")
+                self.page.refresh()
+                self._random_sleep(10, 15)
+                continue
             
-            self.mouse.human_click_ele(login_btn)
-            self._log("Login button clicked.")
+            # Cloudflare 拦截
+            cloudflare_blocked_indicators = [
+                "Sorry, you have been blocked",
+                "You are being rate limited",
+                "Cloudflare Ray ID"
+            ]
+            if any(indicator in current_html_content for indicator in cloudflare_blocked_indicators):
+                raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
             
-            # 等待 URL 变化或特定元素出现
-            # 成功通常跳转到 /UserArea, 失败则留在 /Home
-            end_time = time.time() + 45
-            login_success = False
+            # 遇到五秒盾先绕盾 (保留你原有的逻辑)
+            if "just a moment" in current_title or "cloudflare" in current_title:
+                # 假设你有 cf_bypasser 实例
+                # if not cf_bypasser.bypass(max_retry=3): continue
+                self._log("[State] CF 5-second shield detected. Waiting...")
+                time.sleep(5)
+                continue
             
-            while time.time() < end_time:
+            # 状态 0: 成功到达目标预约页面 (Target Page)
+            if "/Services" in current_url and self.page.ele('.app-menu', timeout=1):
+                self._log("🎉 Successfully reached the Booking Services page! Session created successfully!")                
+                self.session_create_time = time.time()
+                session_created = True
+                break
+
+            # 状态 1: 登录前的初始首页
+            elif self.page.ele('#pingid-button', timeout=1):
+                self._log("[State] Initial Landing Page detected. Clicking login button...")
+                login_btn = self.page.ele('#pingid-button')
+                self.mouse.human_click_ele(login_btn)
+                self._log("Redirecting to PingID...")
+                self._random_sleep(3, 5) # 替代 time.sleep(5)
+                continue
+
+            # 状态 2: 真正的表单登录页 (PingID)
+            elif self.page.ele('@name=callback_1', timeout=1):
+                self._log("[State] PingID Login Form detected. Submitting credentials...")
+                
+                print("正在输入账号...")
+                user_input = self.page.ele('@name=callback_1')
+                self.mouse.human_click_ele(user_input)
+                user_input.input(username, clear=True)
+                time.sleep(1) # 模拟人手停顿
+                
+                print("正在输入密码...")
+                pwd_input = self.page.ele('@type=password')
+                self.mouse.human_click_ele(pwd_input)
+                pwd_input.input(password, clear=True)
                 time.sleep(1)
-                curr_url = self.page.url
                 
-                # 成功特征
-                if "/UserArea" in curr_url or "/Services" in curr_url:
-                    login_success = True
-                    break
+                print("正在点击登录按钮...")
+                submit_btn = self.page.ele('tag:button@type=submit') 
+                self.mouse.human_click_ele(submit_btn)
                 
-                # 失败特征
-                if self.page.ele('.validation-summary-errors') or self.page.ele('.field-validation-error'):
-                    err_text = self.page.ele('.validation-summary-errors').text if self.page.ele('.validation-summary-errors') else "Unknown validation error"
-                    raise PermissionDeniedError(f"Login Failed: {err_text}")
+                has_submitted_login = True
+                self._log("Login form submitted. Waiting for dashboard to load...")
+                self._random_sleep(5, 7)
+                continue
+
+            # 状态 3: 登录成功后的主控制台 (导航栏页面)
+            elif self.page.ele('.app-menu', timeout=1):
+                self._log("[State] Dashboard Menu detected.")
                 
-                # 检查是否有弹窗错误
-                if "Home" in curr_url and self.page.ele('#logoutForm'):
-                     # 有时候虽然在 Home 但出现了 Logout 按钮,也算成功
-                     login_success = True
-                     break
-
-            if not login_success:
-                # 截图保留现场
-                # self.page.get_screenshot(path="login_fail.jpg")
-                raise BizLogicError("Login Failed: Timeout waiting for redirect (Captcha score too low?)")
-
-            self._log("Login Successful.")
-            
-            self.session_create_time = time.time()
+                switch_en_btn = self.page.ele('tag:a@@href=/Language/ChangeLanguage?lang=2', timeout=0.5)
+                if switch_en_btn:
+                    self._log("Found 'English' language switch button. Clicking to change language...")
+                    self.mouse.human_click_ele(switch_en_btn)
+                    self._random_sleep(4, 6)
+                    continue
+                
+                # 动作 B: 如果没有英文切换按钮(说明已经是英文或不存在),则查找并点击 Book 按钮
+                book_btn = self.page.ele('@href=/Services', timeout=0.5)
+                if book_btn:
+                    self._log("Found 'Book' menu element. Clicking to proceed to reservation...")
+                    self.mouse.human_click_ele(book_btn)
+                    self._random_sleep(4, 6)
+                    continue
+                
+                self._log("[WARN] On Dashboard, but neither 'English' nor 'Book' button was found. Waiting...")
+                time.sleep(2)
+                continue
 
-        except Exception as e:
-            self._log(f"Create Session Failed: {e}")
-            self.cleanup()
-            raise e
+            else:
+                self._log("[State] In unknown or transitional state. Waiting for next polling cycle...")
+                time.sleep(2)
+                
+        if not session_created:
+            raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
 
     # -------------------------------------------------------------
     # 2. Query Availability
@@ -241,101 +358,119 @@ class ItaPlugin(IVSPlg):
         res.success = False
         res.availability_status = AvailabilityStatus.NoneAvailable
         
-        if not self._service_id:
-            raise BizLogicError("Service ID not configured")
-
-        # 1. 检查 Slot 是否可用 (Check Availability Endpoint)
-        check_url = f"{self._host}/Services/Booking/{self._service_id}"
-        
-        # 使用 Fetch 发起检查请求
-        resp = self._perform_request("GET", check_url, headers={
-            "Referer": f"{self._host}/Services",
-            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
-        })
-        
-        # 302 跳转处理逻辑
-        if resp.status_code == 200:
-            # 200 表示进入了预约页,有号
-            self._log("Slot Check: 200 OK (Availability Detected)")
-            pass 
-        elif "BookingCalendar" in resp.url: # 或者是被重定向到了 Calendar
-             self._log("Slot Check: Redirected to Calendar (Availability Detected)")
-             pass
+        # 假设要预约的服务和到访原因
+        TARGET_SERVICE = "National and Schengen Visas"
+        REASON_FOR_VISIT = "Tourism" # 对应 value 42 的选项文本
+
+        # ==========================================
+        # 步骤 1:在服务列表中寻找并点击目标服务的 Book
+        # ==========================================
+        print("等待服务列表加载...")
+        self.page.wait.eles_loaded('@aria-controls=dataTableServices', timeout=10)
+
+        print(f"正在查找 [{TARGET_SERVICE}] 的 Book 按钮...")
+        # 使用 XPath:找包含 TARGET_SERVICE 文本的行(tr),再找该行里包含 Book 的超链接(a)
+        target_book_btn_xpath = f'xpath://tr[contains(., "{TARGET_SERVICE}")]//a[contains(text(), "Book")]'
+        book_btn = self.page.ele(target_book_btn_xpath)
+
+        if not book_btn:
+            print(f"未找到 {TARGET_SERVICE} 的 Book 按钮,可能当前无号,程序退出。")
+            # 这里可以根据你的逻辑 return 或者 raise Exception
         else:
-            # 被重定向回 Home 或 Service,说明没号或 Session 过期
-            if "Home" in resp.url or "Login" in resp.url:
-                self.is_healthy = False
-                raise SessionExpiredOrInvalidError("Session expired during query")
-            self._log("Slot Check: No availability (Redirected back)")
-            return res
-
-        # 2. 查询月份 (Query Month)
-        # 默认查询当月,或者配置的月份
-        tar_dates = self.free_config.get("target_dates", [])
-        if not tar_dates:
-            # 默认查下个月
-            next_month = datetime.now().replace(day=28) + datetime.timedelta(days=4)
-            tar_dates = [next_month.strftime("%Y-%m-%d")]
-
-        all_slots = []
-        
-        # Prenotami 需要先 retrieve server info
-        self._perform_request("GET", f"{self._host}/BookingCalendar/RetrieveServerInfo")
-
-        for date_str in tar_dates:
-            # 构造月份格式 2026-01-05 -> 2026-01-01 (API 需要)
-            try:
-                dt = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
-            except:
-                try:
-                    dt = datetime.strptime(date_str, "%Y-%m-%d")
-                except:
-                    dt = datetime.now()
-            
-            # API 需要格式: 2025-11-05T... 格式的字符串作为 selectedDay
-            # 实际上 RetrieveCalendarAvailability 只需要由前端日历控件触发的格式
-            
-            # 查询日历 API
-            cal_url = f"{self._host}/BookingCalendar/RetrieveCalendarAvailability"
-            cal_payload = {
-                "_Servizio": str(self._service_id),
-                "selectedDay": date_str # 原样传配置里的 ISO 串
-            }
+            self.mouse.human_click_ele(book_btn)
+            self.page.wait.load_start()
+            time.sleep(3)
+
+
+        # ==========================================
+        # 步骤 2:填写预约表单
+        # ==========================================
+        print("等待预约表单加载...")
+        self.page.wait.eles_loaded('#bookingForm', timeout=10)
+
+        print("1. 选择预约类型...")
+        typeofbooking_ddl = self.page.ele('#typeofbookingddl')
+        # DrissionPage select 方法直接按文本选中,会自动触发网页的 JS 联动
+        typeofbooking_ddl.select('Individual booking')
+        time.sleep(1)
+
+        print("2. 选择到访原因...")
+        reason_ddl = self.page.ele('#ddls_0')
+        reason_ddl.select(REASON_FOR_VISIT)
+        time.sleep(1)
+
+        print("3. 填写备注...")
+        notes_input = self.page.ele('#BookingNotes')
+        notes_input.clear()
+        notes_input.input('N/A') # 选填,填入 N/A 或者留空
+        time.sleep(1)
+
+        # ==========================================
+        # 步骤 3:处理 OTP 验证码
+        # ==========================================
+        print("4. 点击发送 OTP 验证码...")
+        otp_send_btn = self.page.ele('#otp-send')
+        self.mouse.human_click_ele(otp_send_btn)
+
+        # 这是一个 AJAX 请求,会转圈圈。我们需要等待成功提示出现
+        print("等待验证码发送成功的提示...")
+        self.page.wait.ele_displayed('#IdOtpSent', timeout=15) # 等待绿字 "New code sent!" 显示
+        print("验证码已发送!")
+
+        print("5. 填写默认验证码 123456 ...")
+        otp_input = self.page.ele('#otp-input')
+        self.mouse.human_click_ele(otp_input)
+        otp_input.input('123456')
+        time.sleep(1)
+
+        # ==========================================
+        # 步骤 4:勾选隐私政策并提交
+        # ==========================================
+        print("6. 勾选隐私政策...")
+        privacy_checkbox = self.page.ele('#PrivacyCheck')
+        # 判断一下如果没勾上才去点,防止重复点击取消了
+        if not privacy_checkbox.states.is_checked:
+            self.mouse.human_click_ele(privacy_checkbox)
+        time.sleep(1)
+
+        print("7. 点击 Forward 提交表单...")
+        forward_btn = self.page.ele('#btnAvanti')
+        self.mouse.human_click_ele(forward_btn)
+
+        # 提交后页面会跳转,等待加载开始
+        self.page.wait.load_start()
+        print("表单已提交!当前页面:", self.page.url)
+
+        time.sleep(5)
+
+        valid_dates = []
             
-            resp_cal = self._perform_request("POST", cal_url, json_data=cal_payload)
-            
-            if resp_cal.status_code != 200: continue
-            
-            # 解析有效日期
-            valid_days = self._parse_valid_days(resp_cal.text)
-            self._log(f"Valid days for {date_str}: {valid_days}")
-            
-            if valid_dates:
-                res.success = True
-                res.availability_status = AvailabilityStatus.Available
-                earliest_date = valid_dates[0]
-                earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
-                res.earliest_date = earliest_dt
-                for day in valid_days:
-                    # 查询具体 Slot
-                    slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
-                    slot_payload = {
-                        "selectedDay": day, # YYYY-MM-DD
-                        "idService": str(self._service_id)
-                    }
-                    resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
-                    
-                    time_slots = self._parse_time_slots(resp_slot.text)
-                    ts_list = []
-                    if time_slots:
-                        # 转换结构
-                        for ts in time_slots:
-                            # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
-                            ts_list.append(TimeSlot(
-                                time=f"{ts['start']} - {ts['end']}",
-                                label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
-                            ))
-                    res.availability.append(DateAvailability(date=datetime.strptime(day, "%d-%m-%Y"), times=ts_list))
+        if valid_dates:
+            res.success = True
+            res.availability_status = AvailabilityStatus.Available
+            earliest_date = valid_dates[0]
+            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
+            res.earliest_date = earliest_dt
+            for day in valid_days:
+                # 查询具体 Slot
+                slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
+                slot_payload = {
+                    "selectedDay": day, # YYYY-MM-DD
+                    "idService": str(self._service_id)
+                }
+                resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
+                
+                time_slots = self._parse_time_slots(resp_slot.text)
+                ts_list = []
+                if time_slots:
+                    # 转换结构
+                    for ts in time_slots:
+                        # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
+                        ts_list.append(TimeSlot(
+                            time=f"{ts['start']} - {ts['end']}",
+                            label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
+                        ))
+                res.availability.append(DateAvailability(date=datetime.strptime(day, "%d-%m-%Y"), times=ts_list))
         return res
 
     # -------------------------------------------------------------
@@ -507,77 +642,6 @@ class ItaPlugin(IVSPlg):
             self._log(f"Final Booking Failed: {resp_final.status_code}")
             
         return res
-
-    # -------------------------------------------------------------
-    # 4. Helpers
-    # -------------------------------------------------------------
-    
-    def _get_proxy_url(self):
-            # 构造代理
-        proxy_url = ""
-        if self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                proxy_url = f"{s.proto}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                proxy_url = f"{s.proto}://{s.ip}:{s.port}"
-        return proxy_url
-
-    def _solve_and_inject_prenotami_captcha(self):
-        """
-        专门处理 Prenotami 的 ReCaptcha Enterprise
-        """
-        self._log("Solving ReCaptcha Enterprise (Action: LOGIN)...")
-        
-        api_token = self.free_config.get("capsolver_key", "")
-        if not api_token:
-            raise BizLogicError("Capsolver Key is required for Prenotami")
-
-        # 从 HTML 源码中提取的信息
-        site_key = "6LdkwrIqAAAAAC4NX-g_j7lEx9vh1rg94ZL2cFfY"
-        page_url = self.page.url
-        
-        # 注意:Prenotami 的这个 Key 其实是混合模式,
-        # 虽然它是 V3 (Enterprise),但很多打码平台用 V2 接口也能解,或者必须用 V3 Enterprise 接口
-        # 建议先尝试 ReCaptchaV3EnterpriseTaskProxyLess
-        
-        # 修正为最标准的 V3 Enterprise 配置
-        rc_params = {
-            "type": "ReCaptchaV3EnterpriseTaskProxyless",
-            "page": page_url,
-            "siteKey": site_key,
-            "action": "LOGIN", # 关键参数
-            "minScore": 0.7,   # 要求高分
-            "apiToken": api_token,
-            # "proxy": self._get_proxy_url()
-        }
-        
-        g_token = self._solve_recaptcha(rc_params)
-        self._log(f"Captcha Solved. Token length: {len(g_token)}")
-        
-        hook_js = f"""
-            // 1. 填充隐藏域 (双重保险)
-            var input = document.getElementById('g-recaptcha-response');
-            if(input) {{
-                input.value = "{g_token}";
-            }}
-
-            // 2. 劫持 grecaptcha.execute 和 grecaptcha.enterprise.execute
-            // 无论网页用哪个版本,都拦截下来
-            var mockExecute = function() {{
-                console.log("Recaptcha execution intercepted!");
-                return Promise.resolve("{g_token}");
-            }};
-
-            if (window.grecaptcha) {{
-                window.grecaptcha.execute = mockExecute;
-                if (window.grecaptcha.enterprise) {{
-                    window.grecaptcha.enterprise.execute = mockExecute;
-                }}
-            }}
-        """
-        self._log("Injecting ReCaptcha Hook...")
-        self.page.run_js(hook_js) 
     
     def _perform_request(self, method, url, headers=None, data=None, json_data=None):
         """JS Fetch Wrapper"""
@@ -604,77 +668,6 @@ class ItaPlugin(IVSPlg):
         """
         return BrowserResponse(self.page.run_js(js, timeout=60)) # 文件上传可能较慢,给60s
 
-    def _solve_recaptcha(self, params) -> str:
-        """
-        调用 YesCaptcha API 识别
-        """
-        client_key = params.get("apiToken")
-        
-        # 1. 选择任务类型
-        # 根据文档:RecaptchaV3TaskProxylessM1S7 强制 0.7 分,适合登录
-        task_type = "RecaptchaV3TaskProxyless" # 默认
-        if params.get("minScore") == 0.7:
-            task_type = "RecaptchaV3TaskProxylessM1S7"
-        elif params.get("minScore") == 0.9:
-            task_type = "RecaptchaV3TaskProxylessM1S9"
-            
-        # 2. 构造创建任务请求
-        create_url = "https://api.yescaptcha.com/createTask"
-        create_data = {
-            "clientKey": client_key,
-            "task": {
-                "type": task_type,
-                "websiteURL": params.get("page"),
-                "websiteKey": params.get("siteKey"),
-                "pageAction": params.get("action") # YesCaptcha 要求的字段名是 pageAction
-            }
-        }
-        
-        import requests as req
-        try:
-            # 发送创建任务请求
-            r = req.post(create_url, json=create_data, timeout=20)
-            if r.status_code != 200:
-                raise BizLogicError(f"YesCaptcha Create Failed: {r.text}")
-            
-            res_json = r.json()
-            if res_json.get("errorId") != 0:
-                raise BizLogicError(f"YesCaptcha Error: {res_json.get('errorDescription')}")
-                
-            task_id = res_json.get("taskId")
-            if not task_id:
-                raise BizLogicError("YesCaptcha returned no taskId")
-            
-            # 3. 轮询获取结果
-            result_url = "https://api.yescaptcha.com/getTaskResult"
-            for _ in range(30): # 最多等 60-90秒
-                time.sleep(3)
-                
-                r = req.post(result_url, json={"clientKey": client_key, "taskId": task_id}, timeout=20)
-                d = r.json()
-                
-                # 识别中
-                if d.get("status") == "processing":
-                    continue
-                
-                # 识别成功
-                if d.get("status") == "ready":
-                    solution = d.get("solution", {})
-                    token = solution.get("gRecaptchaResponse")
-                    if token:
-                        return token
-                    else:
-                        raise BizLogicError("YesCaptcha ready but no token found")
-                
-                # 识别失败
-                if d.get("errorId") != 0:
-                    raise BizLogicError(f"YesCaptcha Task Failed: {d.get('errorDescription')}")
-                    
-        except Exception as e:
-            raise BizLogicError(f"Captcha Solver Exception: {e}")
-            
-        raise BizLogicError("YesCaptcha timeout")
-
     def _parse_valid_days(self, text):
         # 提取 DateLibere (YYYY-MM-DD)
         # 格式: {"DateLibere":"22/10/2024 00:00:00","SlotLiberi":1,"SlotRimanenti":1}

+ 7 - 2
plugins/tls_plugin.py

@@ -300,7 +300,7 @@ class TlsPlugin(IVSPlg):
         init_y = random.randint(10, viewport_height - 10)
         self.mouse.move(init_x, init_y) 
 
-        max_steps = 10
+        max_steps = 15
         stuck_counter = 0
         last_url = ""
         session_created = False
@@ -373,6 +373,11 @@ class TlsPlugin(IVSPlg):
                 time.sleep(3)
                 continue
             
+            if "waitting room" in current_title:
+                cf_bypasser.handle_waiting_room()
+                time.sleep(3)
+                continue
+            
             # 如果语言不匹配, 切换语言到英语
             matched_lang = next((lang for lang in other_langs if lang in current_url), None)
             if matched_lang:
@@ -418,7 +423,7 @@ class TlsPlugin(IVSPlg):
                 raise BizLogicError(message="No applicant added. Cannot proceed to booking.") 
             
             # 首页/登录入口页 -> 需要点击进入登录
-            if current_url == tls_url:
+            if tls_url in current_url:
                 if self.page.ele("tag:a@@href:login", timeout=1) and not self.page.ele('tag:label@@text():Email', timeout=1):
                     self._log("State: Login Portal. Clicking login link...")
                     login_link = self.page.ele("tag:a@@href:login")

+ 19 - 11
plugins/usa_plugin.py

@@ -207,20 +207,17 @@ class UsaPlugin(IVSPlg):
         usa_url = self.free_config.get('usa_url', '') 
         self._log(f"Navigating: {usa_url}")
         self.page.get(usa_url)
-        # 初始访问页面,等待长延时,防止过快触发后续操作
-        self._random_sleep(30, 45)
+        self._random_sleep(3, 5)
         
         if 'Attention Required! | Cloudflare' in self.page.title and 'Sorry, you have been blocked' in self.page.html:
             self._log(f'Block by cloudflare, try refresh...')
             self.page.refresh()
-            self._random_sleep(30, 45) # 刷新动作属于高危操作,加长延时
             self.page.wait.doc_loaded()
         
         cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
         if not cf_bypasser.bypass(max_retry=6):
             raise BizLogicError("Cloudflare bypass timeout")
-        # 绕过盾后,休眠一段时间再处理 waiting room
-        self._random_sleep(15, 30) 
+        self._random_sleep(3, 5) 
         cf_bypasser.handle_waiting_room()
         
         self._log("Init humanize tools...")
@@ -283,8 +280,10 @@ class UsaPlugin(IVSPlg):
             
             # 遇到五秒盾先绕盾
             if "just a moment" in current_title:
-                cf_bypasser.bypass(max_retry=3)
-                self._random_sleep(20, 40)
+                if not cf_bypasser.bypass(max_retry=3):
+                    continue
+                self._random_sleep(3, 5)
+                cf_bypasser.handle_waiting_room()
                 continue
             
             if self.page.ele('#post_select', timeout=1):
@@ -339,6 +338,13 @@ class UsaPlugin(IVSPlg):
                 self._log("Login form submitted. Waiting for the next step to load...")
                 self._random_sleep(30, 40)
                 continue
+            
+            elif self.page.ele('#continue_application', timeout=1):
+                schedule_btn = self.page.ele('#continue_application', timeout=0.5)
+                self._log("Currently in first-time booking mode, clicking to proceed...")
+                self.mouse.human_click_ele(schedule_btn)
+                self._random_sleep(30, 40)
+            
 
             # 状态 3: 预约主页(控制台) -> 选择首签或改签
             elif self.page.ele('#atlas-sidebar', timeout=1):
@@ -376,10 +382,12 @@ class UsaPlugin(IVSPlg):
         
         self.page.refresh()
         
-        cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-        if not cf_bypasser.bypass(max_retry=6):
-            raise BizLogicError("Cloudflare bypass timeout")
-        cf_bypasser.handle_waiting_room()
+        if "just a moment" in self.page.title:
+            cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
+            if not cf_bypasser.bypass(max_retry=5):
+                raise BizLogicError("Cloudflare bypass timeout")
+            self._random_sleep(3, 5)
+            cf_bypasser.handle_waiting_room()
         
         current_url = self.page.url.lower()
         if 'auth' in current_url or 'login' in current_url:

+ 40 - 14
plugins/vfs_plugin.py

@@ -23,6 +23,7 @@ from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.mihomo_tunnel import MihomoTunnel
+from toolkit.captcha_breaker import recognize_vfs_captcha_with_qwen
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 
 
@@ -273,8 +274,15 @@ class VfsPlugin(IVSPlg):
         self._log("Handling Cloudflare challenge...")
         
         cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-        cf_token = ""
         
+        if 'Attention Required! | Cloudflare' in self.page.title and 'Sorry, you have been blocked' in self.page.html:
+            self._log(f'Block by cloudflare, try refresh...')
+            self.page.refresh()
+            self.page.wait.doc_loaded()
+        time.sleep(random.uniform(3, 5))
+        cf_bypasser.handle_waiting_room()
+        
+        cf_token = ""
         for i in range(40):
             time.sleep(1)
             self._handle_cookie_banner()
@@ -687,17 +695,32 @@ class VfsPlugin(IVSPlg):
         recipient = self.config.account.username
         subject_keywords = "One Time Password"
         body_keywords = "OTP"
+        
         if not sent_at:
             now_utc = datetime.utcnow()
             sent_at = now_utc.strftime("%Y-%m-%d %H:%M:%S")
+            
         self._log(f"Waiting for OTP email...")
         content_out = VSCloudApi.Instance().fetch_mail_content(
-            master_email, sender, recipient, subject_keywords, body_keywords, sent_at, 300
+            master_email, sender, recipient, subject_keywords, body_keywords, sent_at, 300, only_text=False
         )
         if content_out:
-            match = re.search(r'\b\d{6}\b', content_out)
+            parts = content_out.split("\n")
+            text_parts = [p for p in parts if not p.startswith("data:image")]
+            email_text = " ".join(text_parts)
+            image_urls = [p for p in parts if p.startswith("data:image")]
+            match = re.search(r'\b\d{6}\b', email_text)
             if match:
+                self._log("Found OTP from email text via Regex.")
                 return match.group(0)
+            if image_urls:
+                self._log("OTP not found in text, trying LLM image recognition...")
+                target_image_b64 = image_urls[0]
+                
+                otp_code = recognize_vfs_captcha_with_qwen(target_image_b64)
+                if otp_code:
+                    self._log(f"Successfully extracted OTP from image: {otp_code}")
+                    return otp_code
         raise BizLogicError(message="OTP code not found")
 
 
@@ -968,22 +991,25 @@ class VfsPlugin(IVSPlg):
         return resp.json()
 
     def _upload_applicant_documents(self, apt_config, user_inputs) -> Dict:
-        """上传图片:先下载外部图片,再通过浏览器上传到 VFS"""
-        import requests as standard_requests # 使用标准库下载外部资源
-        
+        """上传图片:根据前缀判断是下载外部图片还是读取本地文件,并上传到 VFS"""
+        import requests as standard_requests
         url = f"https://{self.vfs_api_domain}/appointment/UploadApplicantDocument"
-        passport_url = user_inputs.get("passport_image_url")
-        if not passport_url:
+        passport_path_or_url = user_inputs.get("passport_image_url")
+        if not passport_path_or_url:
             raise NotFoundError(message="Missing passport_image_url")
 
-        try:
-            img_resp = standard_requests.get(passport_url, timeout=30)
+        if passport_path_or_url.startswith("http://") or passport_path_or_url.startswith("https://"):
+            img_resp = standard_requests.get(passport_path_or_url, timeout=30)
             if img_resp.status_code != 200:
                 raise BizLogicError(message=f"Failed to download passport image: {img_resp.status_code}")
-            b64_str = base64.b64encode(img_resp.content).decode('utf-8')
-        except Exception as e:
-            raise BizLogicError(message=f"Image download error: {e}")
-  
+            img_data = img_resp.content
+        else:
+            if not os.path.exists(passport_path_or_url):
+                raise BizLogicError(message=f"Local passport image file not found: {passport_path_or_url}")
+            with open(passport_path_or_url, "rb") as f:
+                img_data = f.read()
+        b64_str = base64.b64encode(img_data).decode('utf-8')
+            
         headers = self._get_common_headers(with_auth=True)
         
         data = {

+ 38 - 3
toolkit/captcha_breaker.py

@@ -2,6 +2,8 @@ import base64
 import os
 from openai import OpenAI
 
+QWEN_API_KEY = "sk-893e895724c6403d81374e515ffaf427"
+
 def encode_image_to_base64(image_path):
     with open(image_path, "rb") as image_file:
         return base64.b64encode(image_file.read()).decode('utf-8')
@@ -40,10 +42,43 @@ def recognize_captcha_with_qwen(image_path, api_key):
     except Exception as e:
         print(f"Qwen 识别错误: {e}")
         return None
+    
+def recognize_vfs_captcha_with_qwen(base64_image):
+    # 利用 OpenAI 的包,调用阿里云的兼容 API 接口
+    client = OpenAI(
+        api_key=QWEN_API_KEY,
+        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
+    )
+        
+    prompt = "图片中有多组验证码,其中OTP箭头指向的验证码为真,其余为假。只输出真实验证码的内容,不要任何多余的汉字或标点符号。"
+
+    try:
+        response = client.chat.completions.create(
+            model="qwen-vl-max", # 也可以用更便宜的 qwen-vl-plus
+            messages=[
+                {
+                    "role": "user",
+                    "content":[
+                        {"type": "text", "text": prompt},
+                        {
+                            "type": "image_url",
+                            "image_url": {
+                                "url": base64_image
+                            }
+                        }
+                    ]
+                }
+            ],
+            temperature=0.0
+        )
+        return response.choices[0].message.content.strip()
+    except Exception as e:
+        print(f"Qwen 识别错误: {e}")
+        return None
 
 if __name__ == "__main__":
     # 填入阿里云百炼 (DashScope) 的 API-KEY
-    API_KEY = "sk-893e895724c6403d81374e515ffaf427"
-    IMAGE_PATH = "captcha.png"
+
+    IMAGE_PATH = "/home/jerry/workspace/coordinator/data/275c31f009e612b65eb92cb52bb1d98.png"
     
-    print(f"Qwen-VL 识别结果: {recognize_captcha_with_qwen(IMAGE_PATH, API_KEY)}")
+    print(f"Qwen-VL 识别结果: {recognize_vfs_captcha_with_qwen(IMAGE_PATH, QWEN_API_KEY)}")

+ 4 - 2
toolkit/vs_cloud_api.py

@@ -333,7 +333,8 @@ class VSCloudApi:
         subject_keywords: str,
         body_keywords: str,
         sent_date: str,
-        expiry: int
+        expiry: int,
+        only_text: bool = True,
     ) -> Optional[str]:
         """
         获取邮件内容
@@ -345,7 +346,8 @@ class VSCloudApi:
             "subjectKeywords": subject_keywords,
             "bodyKeywords": body_keywords,
             "sentDate": sent_date,
-            "expiry": str(expiry)
+            "expiry": str(expiry),
+            "onlyText": only_text,
         }
         
         url = f"{self.base_url}/api/email-authorizations/fetch"

+ 1 - 1
utils/cloudflare_bypass_for_scraping.py

@@ -110,7 +110,7 @@ class CloudflareBypasser:
                 html = self.driver.html.lower()
                 if "cloudflare" not in html:
                     break
-                if "file d'attente" in html or "waiting room" in html:
+                if "file d'attente" in html or "waitting room" in html:
                     if time.time() - wait_start > 60 * 60:
                         self.log_message("Waiting room timeout (1h).")
                         break