jerry 2 săptămâni în urmă
părinte
comite
c75e65be3d
3 a modificat fișierele cu 253 adăugiri și 205 ștergeri
  1. 224 197
      plugins/tls_plugin.py
  2. 6 6
      plugins/vfs_plugin.py
  3. 23 2
      sentinel.py

+ 224 - 197
plugins/tls_plugin.py

@@ -8,7 +8,7 @@ import shutil
 import socket
 from datetime import datetime
 from typing import List, Dict, Optional, Any, Callable
-from urllib.parse import urljoin, urlparse, urlencode
+from urllib.parse import urljoin, urlparse, urlencode, parse_qs
 
 # DrissionPage 核心
 from DrissionPage import ChromiumPage, ChromiumOptions
@@ -58,7 +58,7 @@ class TlsPlugin(IVSPlg):
         self.keyboard = None
         self.page: Optional[ChromiumPage] = None    
         self.travel_group: Optional[Dict] = None
-        
+
         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")
@@ -126,7 +126,7 @@ class TlsPlugin(IVSPlg):
 
     def create_session(self):
         """
-        全浏览器会话创建:过盾 -> JS注入登录 -> 原生跳转
+        全浏览器会话创建:过盾 -> JS注入登录 -> 状态机自动路由导航 -> 到达目标页
         """
         self._log(f"Initializing Session (ID: {self.instance_id})...")
         co = ChromiumOptions()
@@ -148,16 +148,12 @@ class TlsPlugin(IVSPlg):
         
         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)
                 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}')
@@ -170,15 +166,17 @@ class TlsPlugin(IVSPlg):
 
         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)
+            
+            # --- 预检指纹信息 ---
             if self.config.debug:
                 self.page.get('https://example.com')
                 js_script = """
@@ -210,160 +208,168 @@ class TlsPlugin(IVSPlg):
                 }
                 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("====================================================")
 
+            # --- 初始化访问与过盾 ---
             tls_url = self.free_config.get('tls_url', '')
             self._log(f"Navigating: {tls_url}")
             self.page.get(tls_url)
             time.sleep(5)
+            
             cf_bypasser = CloudflareBypasser(self.page, log=True)
             if not cf_bypasser.bypass(max_retry=15):
                 raise BizLogicError("Cloudflare bypass timeout")
             time.sleep(3)
             cf_bypasser.handle_waiting_room()
             
+            # --- 初始化人类行为模拟工具 ---
             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) 
-            
-            btn_selector = 'tag:button@@text():Login'
-            if not self.page.wait.ele_displayed(btn_selector, timeout=3):
-                login_btn = self.page.ele("tag:a@@href:login")
-                self.mouse.human_click_ele(login_btn)
-                time.sleep(3)
-            if not self.page.wait.ele_displayed(btn_selector, timeout=10):
-                raise BizLogicError(message=f"Can't find selector={btn_selector}")
-            time.sleep(random.uniform(0.5, 1))
-
-            # recaptchav2_token = ""
-            # if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
-            #     self._log("Solving ReCaptcha...")
-            #     rc_params = {
-            #         "type": "ReCaptchaV2TaskProxyLess",
-            #         "page": self.page.url,
-            #         "siteKey": "6LcDpXcfAAAAAM7wOEsF_38DNsL20tTvPTKxpyn0", 
-            #         "apiToken": self.free_config.get("capsolver_key", "")
-            #     }
-            #     recaptchav2_token = self._solve_recaptcha(rc_params)
-
-            username = self.config.account.username
-            password = self.config.account.password
-            
-            input_ele = self.page.ele('tag:label@@text():Email').next()
-            self.mouse.human_click_ele(input_ele)
-            time.sleep(random.uniform(0.2, 0.6))
-            self.keyboard.type_text(username, humanize=True)
-            
-            time.sleep(random.uniform(0.5, 1.2)) 
-        
-            input_ele = self.page.ele('tag:label@@text():Password').next()
-            self.mouse.human_click_ele(input_ele)
-            time.sleep(random.uniform(0.2, 0.6))
-            self.keyboard.type_text(password, humanize=True)
-            
-            # if recaptchav2_token:
-            #     inject_recaptchav2_token_js = f"""
-            #     var g = document.getElementById('g-recaptcha-response');
-            #     if(g) {{ g.value = "{recaptchav2_token}"; }}
-            #     """
-            #     self._log("Inject ReCaptchaV2 Token via JS...")
-            #     self.page.run_js(inject_recaptchav2_token_js)
-            #     time.sleep(random.uniform(0.5, 1.0))
-            
-            self._log("Submitting Login...")
-            time.sleep(random.uniform(0.3, 0.8))
-            login_btn = self.page.ele('tag:button@@text():Login')
-            self.mouse.human_click_ele(login_btn)
-
-            self._log("Waiting for redirect...")
-            self.page.wait.url_change('login-actions', exclude=True, timeout=45)
-            
-            time.sleep(3)
-            if "login-actions" in self.page.url or "auth" in self.page.url:
-                raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
-            
-            self.page.wait.load_start()
-            time.sleep(5)
-            
-            # groups = self._parse_travel_groups(self.page.html)
-            # location = self.free_config.get('location')
-            # for g in groups:
-            #     if g['location'] == location:
-            #         self.travel_group = g
-            #         break
-            
-            # if not self.travel_group:
-            #     self._save_screenshot("group_not_found")
-            #     raise NotFoundError(f"Group not found for {location}")
-        
-            # formgroup_id = self.travel_group.get('group_number')
 
-            # btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
-            # self._log(f"Waiting for visible button to render: {formgroup_id}...")
+            max_steps = 20 
+            session_created = False
+            has_submitted_login = False
             
-            # self.page.wait.eles_loaded(btn_selector, timeout=15)
-            
-            # buttons = self.page.eles(btn_selector)
-            # select_btn = None
-            # for btn in reversed(buttons):
-            #     try:
-            #         w, h = btn.rect.size
-            #         if w > 0 and h > 0:
-            #             select_btn = btn
-            #             break
-            #     except Exception:
-            #         continue
-            # if not select_btn:
-            #     self._save_screenshot("visible_button_not_found")
-            #     raise BizLogicError(f"Can't find any visible Select button for group {formgroup_id}")
-            # time.sleep(random.uniform(0.5, 1.2))
-            # self.mouse.human_click_ele(select_btn)
-            
-            # self._log("Waiting for url redirect...")
-            # self.page.wait.url_change('travel-groups', exclude=True, timeout=45)
-            # time.sleep(2)
-
-            # if "travel-groups" in self.page.url or "auth" in self.page.url:
-            #     raise BizLogicError(message="Redirect to service-level Failed!")
-            
-            # no_applicant_indicators = [
-            #     "Add a new applicant" in self.page.html,
-            #     "You have not yet added an applicant. Please click the button below to add one." in self.page.html,
-            #     "applicants-information" in self.page.url
-            # ]
-            # if any(no_applicant_indicators):
-            #     raise BizLogicError(message=f"No applicant added") 
-            
-            btn_selector = '#book-appointment-btn'
-            self._log(f"Waiting for selector={btn_selector} to render...")
-            if not self.page.wait.ele_displayed(btn_selector, timeout=15):
-                raise BizLogicError(message=f"Waiting for selector={btn_selector} timeout")
-            self.mouse.human_click_ele(self.page.ele(btn_selector))
-
-            time.sleep(3)
-            # self._log("Waiting for url redirect...")
-            # self.page.wait.url_change('service-level', exclude=True, timeout=45)
-            # time.sleep(2)
-
-            # if "service-level" in self.page.url or "auth" in self.page.url:
-            #     raise BizLogicError(message="Redirect to appointment-booking Failed!")
+            for step in range(max_steps):
+                self.page.wait.load_start()
+                current_url = self.page.url
+                self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
+                
+                # 状态 1:到达终极目标页面 (成功退出条件)
+                if "appointment-booking" in current_url or self.page.ele('tag:button@text():Book your appointment', timeout=1):
+                    btn_selector = 'tag:button@text():Book your appointment'            
+                    if self.page.wait.ele_displayed(btn_selector, timeout=10):
+                        self.session_create_time = time.time()
+                        self._log("✅ Login & Navigation Success! Reached appointment-booking.")
+                        session_created = True
+                        break
+                
+                # 状态 2:遇到没有申请人的拦截页 (致命错误退出条件)
+                no_applicant_indicators = [
+                    "Add a new applicant" in self.page.html,
+                    "You have not yet added an applicant" in self.page.html,
+                    "applicants-information" in current_url
+                ]
+                if any(no_applicant_indicators):
+                    raise BizLogicError(message="No applicant added. Cannot proceed to booking.") 
+                
+                # 状态 3:首页/登录入口页 -> 需要点击进入登录
+                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")
+                    self.mouse.human_click_ele(login_link)
+                    time.sleep(3)
+                    continue
+                
+                # 状态 4:真正的登录表单页
+                if self.page.ele('tag:label@@text():Email', timeout=1) and not has_submitted_login:
+                    self._log("State: Login Form. Processing credentials and Captcha...")
+                    
+                    recaptchav2_token = ""
+                    if self.page.ele('.g-recaptcha') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
+                        rec_iframe = self.page.ele('xpath://iframe[contains(@src, "recaptcha")]')
+                        rec_iframe_src = rec_iframe.attr('src')
+                        rec_parsed = urlparse(rec_iframe_src)
+                        rec_params = parse_qs(rec_parsed.query)
+                        rec_sitekey = rec_params.get("k", [None])[0]
+                        rec_size = rec_params.get("size", [None])[0]
+                        
+                        if 'normal' == rec_size:
+                            self._log(f"Solving ReCaptcha sitekey={rec_sitekey}...")
+                            rc_params = {
+                                "type": "ReCaptchaV2TaskProxyLess",
+                                "page": current_url,
+                                "siteKey": rec_sitekey, 
+                                "apiToken": self.free_config.get("capsolver_key", "")
+                            }
+                            recaptchav2_token = self._solve_recaptcha(rc_params)
 
-            btn_selector = 'tag:button@text():Book your appointment'            
-            if not self.page.wait.ele_displayed(btn_selector, timeout=10):
-                raise BizLogicError(message=f"Waiting for selector={btn_selector} timeout")
+                    username = self.config.account.username
+                    password = self.config.account.password
+                    
+                    input_ele = self.page.ele('tag:label@@text():Email').next()
+                    self.mouse.human_click_ele(input_ele)
+                    time.sleep(random.uniform(0.2, 0.6))
+                    self.keyboard.type_text(username, humanize=True)
+                    time.sleep(random.uniform(0.5, 1.2)) 
+                
+                    input_ele = self.page.ele('tag:label@@text():Password').next()
+                    self.mouse.human_click_ele(input_ele)
+                    time.sleep(random.uniform(0.2, 0.6))
+                    self.keyboard.type_text(password, humanize=True)
+                    
+                    # 注入 Token
+                    if recaptchav2_token:
+                        inject_js = f"var g = document.getElementById('g-recaptcha-response'); if(g) {{ g.value = '{recaptchav2_token}'; }}"
+                        self.page.run_js(inject_js)
+                        time.sleep(random.uniform(0.5, 1.0))
+                    
+                    self._log("Submitting Login...")
+                    login_btn = self.page.ele('tag:button@@text():Login')
+                    self.mouse.human_click_ele(login_btn)
+                    has_submitted_login = True
+                    time.sleep(3)
+                    continue
+                
+                # 状态 5:Travel Groups 页面
+                if "travel-groups" in current_url:
+                    self._log("State: Travel Groups. Selecting targeted group...")
+                    groups = self._parse_travel_groups(self.page.html)
+                    location = self.free_config.get('location')
+                    self.travel_group = next((g for g in groups if location in g['location']), None)
+                    
+                    if not self.travel_group:
+                        self._save_screenshot("group_not_found")
+                        raise NotFoundError(f"Group not found for {location}")
                 
-            self.session_create_time = time.time()
-            self._log(f"✅ Login & Navigation Success!")
+                    formgroup_id = self.travel_group.get('group_number')
+                    btn_selector = f'tag:button@@name=formGroupId@@value={formgroup_id}'
+                    
+                    if self.page.wait.eles_loaded(btn_selector, timeout=10):
+                        buttons = self.page.eles(btn_selector)
+                        select_btn = next((btn for btn in reversed(buttons) if btn.rect.size[0] > 0 and btn.rect.size[1] > 0), None)
+                        
+                        if select_btn:
+                            time.sleep(random.uniform(0.5, 1.2))
+                            self.mouse.human_click_ele(select_btn)
+                            time.sleep(3)
+                            continue
+                        else:
+                            self._log("[WARN] Select button found but not visible.")
+                    else:
+                        self._log(f"[WARN] Wait timeout for group button {formgroup_id}")
+                
+                # 状态 6:中间过渡页,需点击 "Book Appointment" 继续往下走
+                if self.page.ele('#book-appointment-btn', timeout=1):
+                    self._log("State: Intermediate Dashboard. Clicking Book Appointment button...")
+                    self.mouse.human_click_ele(self.page.ele('#book-appointment-btn'))
+                    time.sleep(3)
+                    continue
+                
+                # 状态 7:登录失败校验 或 未知加载状态
+                if "login-actions" in current_url and has_submitted_login:
+                    self._log("Waiting on login-actions... (Might be authenticating or invalid credentials)")
+                    time.sleep(2)
+                    if self.page.ele('text:Invalid username or password', timeout=1): # 假设网页上有错误提示
+                        raise BizLogicError(message="Login Failed! Invalid credentials or Captcha rejected.")
+                    continue
+                
+                # 兜底:未匹配到明确状态,等待页面渲染或重定向
+                self._log("State: Transitioning or Unknown. Waiting 2 seconds...")
+                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}")
 
         except Exception as e:
             self._log(f"Session Create Error: {e}")
@@ -383,8 +389,6 @@ class TlsPlugin(IVSPlg):
         target_month_num = target_date_obj.month
         
         slots = []
-        all_slots = []
-        
         current_selected_ele = self.page.ele('@data-testid=btn-current-month-available')
         current_month_text = current_selected_ele.text.strip() if current_selected_ele else ""
 
@@ -392,76 +396,36 @@ class TlsPlugin(IVSPlg):
 
         if not is_on_target_month:
             self._log(f"Current is '{current_month_text}', navigating to '{target_month_text}'...")
-            for _ in range(12):
-                target_btn_xpath = f'xpath://a[contains(@href, "month={interest_month}")]'
-                target_btn = self.page.ele(target_btn_xpath)
-                
-                if target_btn:
-                    target_btn.click(by_js=True)
-                    time.sleep(3) 
+            reached_target = False
+            for step in range(12):
+                current_ele = self.page.ele('@data-testid=btn-current-month-available', timeout=2)
+                if current_ele and current_ele.text.strip().lower() == target_month_text.lower():
+                    self._log(f"✅ Successfully navigated to target month: '{target_month_text}'!")
+                    reached_target = True
                     break
                 
-                next_btn = self.page.ele('@data-testid=btn-next-month-available')
-                if next_btn:
+                next_btn = self.page.ele('@data-testid=btn-next-month-available', timeout=2)
+                
+                if next_btn and next_btn.tag.lower() == 'button':
+                    self._log(f"Clicking next month: {next_btn.text.strip()} ...")
                     next_btn.click(by_js=True)
-                    time.sleep(2) 
+                    time.sleep(2.5)
                 else:
-                    self._log("Warning: Cannot find target month or 'Next Month' button.")
+                    self._log("⚠️ Reached the end of the calendar or 'Next Month' is disabled.")
                     break
 
+            if not reached_target:
+                self._log(f"❌ Could not navigate to target month: {target_month_text}. Stop parsing.")
+                res.success = False
+                res.availability_status = AvailabilityStatus.NoneAvailable
+                return res
             self._log("Extracting slots from DOM using robust data-testid features...")
-            
-            day_blocks_xpath = '//div[p and div//button[contains(@data-testid, "slot")]]'
-            day_blocks = self.page.eles(f'xpath:{day_blocks_xpath}')
-            
-            for block in day_blocks:
-                # 1. 提取日期:只要是这个 block 下的 p 标签,必定是 "Mon 01" 这种
-                p_ele = block.ele('tag:p')
-                if not p_ele:
-                    continue
-                
-                # 直接从 p 标签的纯文本里抽取出数字,忽略前面的字母
-                day_match = re.search(r'\d+', p_ele.text)
-                if not day_match:
-                    continue
-                day_str = day_match.group()
-                
-                full_date = f"{target_year}-{target_month_num:02d}-{int(day_str):02d}"
-                
-                # 2. 提取可用按钮:利用 data-testid 前缀匹配
-                # 完美过滤掉 btn-unavailable-slot (灰色的不可用按钮)
-                available_btns = block.eles('xpath:.//button[starts-with(@data-testid, "btn-available-slot")]')
-                
-                for btn in available_btns:
-                    # 提取时间:无视内部各种 span 的变动,只要 html 里有 00:00 这种格式就被截取
-                    time_match = re.search(r'\d{2}:\d{2}', btn.html)
-                    if not time_match:
-                        continue
-                    time_str = time_match.group()
-                    
-                    # 提取 Label:完全依赖测试工程师留下的 testid
-                    test_id = btn.attr('data-testid') or ""
-                    if 'prime' in test_id and 'weekend' in test_id:
-                        lbl = 'ptaw'
-                    elif 'prime' in test_id:
-                        lbl = 'pta'
-                    else:
-                        lbl = '' 
-                        
-                    all_slots.append({
-                        'date': full_date,
-                        'time': time_str,
-                        'label': lbl
-                    })
-
+            slots = self._scan_dom_for_slots(target_year, target_month_num)
         else:
             self._log(f"Already on '{target_month_text}'. Executing silent JS fetch...") 
             resp = self._perform_request("GET", self.page.url, retry_count=1)
             self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
-            all_slots = self._parse_appointment_slots(resp.text)
-
-        target_labels = self.free_config.get("target_labels", ["", "pta"])
-        slots = [s for s in all_slots if s.get("label") in target_labels]
+            slots = self._parse_appointment_slots(resp.text)
         
         if slots:
             res.success = True
@@ -646,7 +610,70 @@ class TlsPlugin(IVSPlg):
             except Exception:
                 pass
         return res
-    
+
+    def _scan_dom_for_slots(self, target_year: int, target_month_num: int) -> list[dict]:
+        """
+        DOM-based slot scanning — 结合区块结构与类名/属性推断标签
+        """
+        slots = []
+        
+        day_blocks_xpath = '//div[p and div//button[contains(@data-testid, "slot")]]'
+        day_blocks = self.page.eles(f'xpath:{day_blocks_xpath}')
+        
+        for block in day_blocks:
+            p_ele = block.ele('tag:p')
+            if not p_ele: continue
+            
+            day_match = re.search(r'\d+', p_ele.text)
+            if not day_match: continue
+            day_str = day_match.group()
+            full_date = f"{target_year}-{target_month_num:02d}-{int(day_str):02d}"
+            
+            btn_selectors = [
+                'xpath:.//button[starts-with(@data-testid, "btn-available-slot")]',
+                'xpath:.//button[contains(@class, "available")]',
+                'xpath:.//div[contains(@class, "time-slot") and not(contains(@class, "unavailable"))]'
+            ]
+            
+            available_elements = []
+            for sel in btn_selectors:
+                elements = block.eles(sel)
+                if elements:
+                    available_elements.extend(elements)
+                    break
+            seen_times = set()
+            for el in available_elements:
+                classes = (el.attr("class") or "").lower()
+                test_id = (el.attr("data-testid") or "").lower()
+                combined_attrs = f"{classes} {test_id}"
+                if "disabled" in combined_attrs or "unavailable" in combined_attrs:
+                    continue
+                time_match = re.search(r'\d{2}:\d{2}', el.html)
+                if not time_match: continue
+                time_str = time_match.group()
+                
+                if time_str in seen_times:
+                    continue
+                seen_times.add(time_str)
+                label = ''  
+                if 'prime' in combined_attrs and 'weekend' in combined_attrs:
+                    label = 'ptaw'
+                elif 'prime' in combined_attrs or 'premium' in combined_attrs:
+                    label = 'pta'
+                elif any(k in combined_attrs for k in ['regular', 'standard', 'default']):
+                    label = ''
+                else:
+                    label = '' 
+                
+                slots.append({
+                    'date': full_date,
+                    'time': time_str,
+                    'label': label,
+                    'source': 'dom'
+                })
+                
+        return slots
+
     def _get_proxy_url(self):
         # 构造代理
         proxy_url = ""

+ 6 - 6
plugins/vfs_plugin.py

@@ -316,7 +316,7 @@ class VfsPlugin(IVSPlg):
                 time.sleep(1)
                 self._handle_cookie_banner()
                 try:
-                    ele = self.page.ele('@name=cf-turnstile-response')
+                    ele = self.page.ele('@name=cf-turnstile-response', timeout=1)
                     if ele and ele.value:
                         cf_token = ele.value
                         self._log("Cloudflare Turnstile token extracted.")
@@ -327,11 +327,11 @@ class VfsPlugin(IVSPlg):
                 if i > 2:
                     try:
                         use_dfs = False
-                        cf_bypasser.click_verification_button(is_dfs=use_dfs)
+                        cf_bypasser.click_verification_button(use_dfs)
                     except Exception as e:
                         pass
                 
-                if self.page.ele('tag:form') or self.page.ele('#mat-input-0'):
+                if self.page.ele('tag:form', timeout=0.5) or self.page.ele('#mat-input-0', timeout=0.5):
                     self._log("Login form detected.")
                     if i > 5 and not cf_token:
                         self._log("Form visible but token not found yet...")
@@ -832,7 +832,7 @@ class VfsPlugin(IVSPlg):
             time.sleep(0.5)
             
             try:
-                ele = self.page.ele('@name=cf-turnstile-response')
+                ele = self.page.ele('@name=cf-turnstile-response', timeout=1)
                 if ele and ele.value:
                     self._log("Turnstile token refreshed successfully.")
                     return ele.value
@@ -843,8 +843,8 @@ class VfsPlugin(IVSPlg):
                 self._handle_cookie_banner()
                 
                 try:
-                    use_dfs = (i > 14) 
-                    cf_bypasser.click_verification_button(is_dfs=use_dfs)
+                    use_dfs = False
+                    cf_bypasser.click_verification_button(use_dfs)
                 except Exception as e:
                     # 点击过程报错不要中断主循环
                     pass

+ 23 - 2
sentinel.py

@@ -26,10 +26,22 @@ class SentinelGCO:
         
         # 1. 全局建连退避:起步 1 分钟,封顶 1 小时 (保护登录接口)
         self.group_backoff = ExponentialBackoff(base_delay=60.0, max_delay=3600.0, factor=2.0)
+        self.m_last_spawn_time = 0.0
 
     def _log(self, message):
         if self.m_logger:
             self.m_logger(f'[SENTINEL] [{self.m_cfg.identifier}] {message}')
+            
+    def _get_average_interval(self) -> float:
+        """计算当前组平均的查询间隔(秒)"""
+        mode = self.m_cfg.query_wait.mode
+        if mode == QueryWaitMode.Loop:
+            return 1.0
+        elif mode == QueryWaitMode.Fixed:
+            return float(self.m_cfg.query_wait.fixed_wait)
+        elif mode == QueryWaitMode.Random:
+            return (self.m_cfg.query_wait.random_min + self.m_cfg.query_wait.random_max) / 2.0
+        return 30.0
 
     def start(self):
         if not self.m_cfg.enable:
@@ -125,9 +137,18 @@ class SentinelGCO:
                 
                 current = len(self.m_tasks)
                 pending = self.m_pending_builtin
+                target = self.m_cfg.sentinel.target_instances
             
-            if (current + pending) < self.m_cfg.sentinel.target_instances:
-                self._spawn_sentinel_worker()
+            if (current + pending) < target:
+                now = time.time()
+                avg_interval = self._get_average_interval()
+                stagger_delay = avg_interval / max(1, target)
+                stagger_delay = max(10.0, stagger_delay)
+                if now - self.m_last_spawn_time >= stagger_delay:
+                    with self.m_lock:
+                        self.m_last_spawn_time = now
+                    self._log(f"Staggered spawn triggered. Next spawn in {stagger_delay:.1f}s")
+                    self._spawn_sentinel_worker()
 
     def _spawn_sentinel_worker(self):
         with self.m_lock: