Ver Fonte

feat: update

jerry há 4 meses atrás
pai
commit
e7fc2e98d0

+ 1 - 1
config/accounts.json

@@ -492,7 +492,7 @@
     "gb_fr": [
         {
             "id": 0,
-            "username":"arket_zz@163.com",
+            "username":"romicloud@163.com",
             "password": "Visafly@111",
             "lock_until": 0
         }

+ 40 - 20
config/groups.json

@@ -1,7 +1,9 @@
 [
     {
         "identifier": "VFS_IE_NL",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_nl",
         "need_proxy": true,
@@ -21,7 +23,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "nld",
             "mission_name": "Netherlands",
             "country_code": "irl",
@@ -51,7 +52,9 @@
     },
     {
         "identifier": "VFS_SG_FR",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "sg_fr",
         "need_proxy": true,
@@ -71,7 +74,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "fra",
             "mission_name": "France",
             "country_code": "sgp",
@@ -101,7 +103,9 @@
     },
     {
         "identifier": "VFS_AU_FR",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "au_fr",
         "need_proxy": true,
@@ -121,7 +125,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "fra",
             "mission_name": "France",
             "country_code": "aus",
@@ -167,7 +170,9 @@
     },
     {
         "identifier": "VFS_GB_IT",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "gb_it",
         "need_proxy": true,
@@ -187,7 +192,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "ita",
             "mission_name": "Italy",
             "country_code": "gbr",
@@ -235,7 +239,9 @@
     },
     {
         "identifier": "VFS_GB_NL",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "gb_nl",
         "need_proxy": true,
@@ -255,7 +261,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "nld",
             "mission_name": "Netherland",
             "country_code": "gbr",
@@ -302,7 +307,9 @@
     },
     {
         "identifier": "VFS_GB_NO",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "gb_no",
         "need_proxy": true,
@@ -322,7 +329,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "nor",
             "mission_name": "Norway",
             "country_code": "gbr",
@@ -352,7 +358,9 @@
     },
     {
         "identifier": "VFS_IE_AT",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_at",
         "need_proxy": true,
@@ -372,7 +380,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "aut",
             "mission_name": "Austria",
             "country_code": "irl",
@@ -402,7 +409,9 @@
     },
     {
         "identifier": "VFS_IE_DK",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_dk",
         "need_proxy": true,
@@ -422,7 +431,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "dnk",
             "mission_name": "Denmark",
             "country_code": "irl",
@@ -452,7 +460,9 @@
     },
     {
         "identifier": "VFS_IE_FI",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_fi",
         "need_proxy": true,
@@ -472,7 +482,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "fin",
             "mission_name": "Finland",
             "country_code": "irl",
@@ -502,7 +511,9 @@
     },
     {
         "identifier": "VFS_IE_HU",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_hu",
         "need_proxy": true,
@@ -522,7 +533,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "hun",
             "mission_name": "Hungary",
             "country_code": "irl",
@@ -552,7 +562,9 @@
     },
     {
         "identifier": "VFS_IE_IS",
+        "debug": false,
         "enable": false,
+        "account_built_in": true,
         "need_account": true,
         "account_pool": "ie_is",
         "need_proxy": true,
@@ -572,7 +584,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "mission_code": "isl",
             "mission_name": "Iceland",
             "country_code": "irl",
@@ -602,8 +613,10 @@
     },
     {
         "identifier": "BLS_IE_ES",
-        "enable": false,
+        "debug": false,
+        "enable": true,
         "need_account": true,
+        "account_built_in": true,
         "account_pool": "ie_es",
         "need_proxy": true,
         "proxy_pool": "local",
@@ -620,8 +633,8 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "domain": "ireland.blsspainglobal.com", 
-            "local_service_url": "http://127.0.0.1:8085", 
+            "domain": "ireland.blsspainglobal.com",
+            "local_service_url": "http://127.0.0.1:8085",
             "query_selector": {
                 "location": "Dublin",
                 "jurisdiction": null,
@@ -640,8 +653,10 @@
     },
     {
         "identifier": "BLS_GB_ES",
+        "debug": false,
         "enable": false,
         "need_account": true,
+        "account_built_in": false,
         "account_pool": "gb_es",
         "need_proxy": true,
         "proxy_pool": "local",
@@ -658,7 +673,7 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "domain": "uk.blsspainglobal.com", 
+            "domain": "uk.blsspainglobal.com",
             "local_service_url": "http://127.0.0.1:8085",
             "query_selector": {
                 "location": "Dublin",
@@ -678,8 +693,10 @@
     },
     {
         "identifier": "TLS_GB_FR",
+        "debug": false,
         "enable": true,
         "need_account": true,
+        "account_built_in": true,
         "account_pool": "gb_fr",
         "need_proxy": true,
         "proxy_pool": "ireland_proxies",
@@ -696,7 +713,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "center": {
                 "code": "gbLON2fr",
                 "country": "gb",
@@ -708,15 +724,20 @@
             "visa_type": "Tourist",
             "routing_key": "slot.lon.fr.tourist",
             "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
-            "interest_month": "02-2026",
-            "target_labels": ["", "pta"],
+            "interest_month": "01-2026",
+            "target_labels": [
+                "",
+                "pta"
+            ],
             "website": "https://visas-fr.tlscontact.com/country/gb/vac/gbLON2fr/"
         }
     },
     {
         "identifier": "VISAMETRIC_IE_DE",
-        "enable": false,
+        "debug": false,
+        "enable": true,
         "need_account": false,
+        "account_built_in": false,
         "account_pool": "",
         "need_proxy": true,
         "proxy_pool": "ireland_proxies",
@@ -733,7 +754,6 @@
             "plugin_proto": "IVSPlg"
         },
         "free_config": {
-            "verbose": 0,
             "base_url": "https://ie-appointment.visametric.com",
             "local_service_url": "http://127.0.0.1:8085",
             "consularid": 1,

+ 2 - 0
core/app_manager.py

@@ -60,8 +60,10 @@ class AppManager:
 
                 grp_cfg = GroupConfig(
                     identifier=item["identifier"],
+                    debug=item.get("debug", False),
                     enable=item.get("enable", False),
                     need_account=item.get("need_account", False),
+                    account_built_in=item.get("account_built_in", True),
                     account_pool=item.get("account_pool", ""),
                     need_proxy=item.get("need_proxy", False),
                     proxy_pool=item.get("proxy_pool", ""),

+ 8 - 7
group_coordinator.py

@@ -38,7 +38,7 @@ class GroupCoordinator:
         self.m_creator_thread: Optional[threading.Thread] = None
         
         # 预订操作的线程池,独立于任务调度
-        self.book_executor = ThreadPool(max_workers=5).getInstance() # 使用我们封装的ThreadPool
+        self.book_executor = ThreadPool(max_workers=5).getInstance()
 
         VSC_INFO("coordinator", f"GroupCoordinator for {self.m_cfg.identifier} initialized.")
 
@@ -152,7 +152,7 @@ class GroupCoordinator:
                         self.on_query_result(task.instance, result)
                         is_booking_triggered = True
                     else:
-                        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Query failed, No availability found")
+                        VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query done, No availability found")
                 except Exception as e:
                     VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
 
@@ -238,6 +238,7 @@ class GroupCoordinator:
         """
         VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Preparing plugin configuration...")
         plg_cfg = VSPlgConfig()
+        plg_cfg.debug = self.m_cfg.debug
         
         # 账号配置
         if self.m_cfg.need_account:
@@ -296,8 +297,8 @@ class GroupCoordinator:
         # @brief 创建并初始化单个插件实例。
         # 这个方法在 creator_loop 的线程池中执行。
         # """
-        # VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
-        # try:
+        VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Creating plugin instance (plugin={self.m_cfg.plugin_config.plugin_name})...")
+        try:
             inst = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
             inst.set_config(plg_cfg)
             inst.create_session()
@@ -306,9 +307,9 @@ class GroupCoordinator:
                     self.m_cfg.account_pool, plg_cfg.account.id, self.m_cfg.account_login_interval * 60)
             VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
             return inst
-        # except Exception as e:
-        #     VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
-        # return None
+        except Exception as e:
+            VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Error creating plugin instance: {e}")
+        return None
 
     def on_query_result(self, sptr: IVSPlg, query_result: VSQueryResult):
         VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Query result received: {str(query_result)}. BLOCKING monitor loop for booking...")

+ 69 - 14
plugins/bls_plugin.py

@@ -1,5 +1,6 @@
 import re
 import os
+import uuid
 import base64
 import time
 import json
@@ -61,10 +62,10 @@ class BlsPlugin(IVSPlg):
     def create_session(self):
         self.session = requests.Session(
             proxy=self._get_proxy_url(),
-            impersonate="chrome131",
+            impersonate="chrome124",
             curl_options={
                 const.CurlOpt.MAXAGE_CONN: 1800,
-                const.CurlOpt.VERBOSE: False
+                const.CurlOpt.VERBOSE: self.config.debug
             }
         )
         domain = self.free_config.get("domain")
@@ -80,11 +81,11 @@ class BlsPlugin(IVSPlg):
         }
         
         resp = self._perform_request('GET', login_url, headers=headers)
-        
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="Bls_Login_Page")
         soup = BeautifulSoup(resp.text, 'html.parser')
         form_data = self._extract_hidden_fields(soup)
         
-        
         real_user = None
         real_pass = None
     
@@ -130,6 +131,9 @@ class BlsPlugin(IVSPlg):
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         resp = self._perform_request('GET', url_vtv, headers=headers)
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="Bls_Visatypeverification_Page")
+        self._check_resp_is_session_expired_or_invalid('APPLICATION PROCESS', resp)
         
         form_vtv = self._extract_hidden_fields(BeautifulSoup(resp.text, 'html.parser'))
         captcha_token = self._solve_bls_captcha()
@@ -148,10 +152,17 @@ class BlsPlugin(IVSPlg):
         url_vt = f"https://{domain}/Global/bls/visatype?data={data_val}"
         
         vt_resp = self._perform_request('GET', url_vt, headers=headers)
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="Bls_Visatype_Page")
+        self._check_resp_is_session_expired_or_invalid('APPLICATION PROCESS', resp)
         
         # 这里需要极其复杂的 JS 变量提取 (JS Arrays -> Match Name -> Get ID)
         vt_payload = self._construct_visatype_payload(vt_resp.text, BeautifulSoup(vt_resp.text, 'html.parser'))
         
+        res.city = self.free_config.get('city', '')
+        res.country = self.free_config.get('country', '')
+        res.visa_type = self.free_config.get('visa_type', '')
+        res.routing_key = self.free_config.get('routing_key', '')
         vt_res = self._perform_request('POST', f"https://{domain}/Global/bls/VisaType", data=vt_payload, headers=headers)
         if not vt_res.json()['success']:
             if not vt_res.json()['available']:
@@ -168,16 +179,15 @@ class BlsPlugin(IVSPlg):
         url_ma = f"https://{domain}/Global/blsAppointment/ManageAppointment?{urlencode(self.book_params)}"
         
         resp_ma = self._perform_request('GET', url_ma, headers=headers)
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="Bls_ManageAppointment_Page")
+        self._check_resp_is_session_expired_or_invalid('APPLICATION PROCESS', resp)
         
         avail_str = self._extract_js_var(resp_ma.text, "var availDates", r"var availDates =(.*?);")
         if avail_str:
             avail_json = json.loads(avail_str)
             # 提取日期
             dates = [x['DateText'] for x in avail_json['ad'] if x['SingleSlotAvailable']]
-            res.city = self.free_config.get('city', '')
-            res.country = self.free_config.get('country', '')
-            res.visa_type = self.free_config.get('visa_type', '')
-            res.routing_key = self.free_config.get('routing_key', '')
             if dates:
                 res.success = True
                 res.availability_status = AvailabilityStatus.Available
@@ -334,7 +344,7 @@ class BlsPlugin(IVSPlg):
         if not os.path.exists(save_dir):
             os.makedirs(save_dir)
         timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"{save_dir}/{prefix}_{self.group_id}_{timestamp}.html"
+        filename = f"{save_dir}/{prefix}_{timestamp}.html"
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
         VSC_INFO("bls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
@@ -345,9 +355,10 @@ class BlsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
+
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        VSC_INFO('bls_plg', resp.text)
+        if self.config.debug:
+            VSC_INFO('bls_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
             return resp
         elif resp.status_code == 401:
@@ -373,6 +384,10 @@ class BlsPlugin(IVSPlg):
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         resp = self._perform_request("GET", url, headers=headers)
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="Bls_Captcha_Page")
+        self._check_resp_is_session_expired_or_invalid('Please select all boxes with number', resp)    
+        
         soup = BeautifulSoup(resp.text, 'html.parser')
         resp = requests.post(
             f'{self.local_service_url}/browser/visable_captchas', 
@@ -417,10 +432,23 @@ class BlsPlugin(IVSPlg):
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
         headers["X-Requested-With"] = "XMLHttpRequest"
         resp = self._perform_request('POST', submit_url, headers=headers, data=form)
-        if data:
-            return resp.json()['captcha']
+        j = resp.json()
+        if j.get('success'):
+            if data:
+                return resp.json()['captcha']
+            else:
+                return resp.json()['cd']
         else:
-            return resp.json()['cd']
+            # 存盘所有错误验证码后续进行数据分析
+            VSC_WARN('bls_plg', 'Captcha Selection Invalid, Saving important data to data/bls_captcha')
+            for img in soup.select("img.captcha-img"):
+                src = img.get("src", "")
+                if not src.startswith("data:image"):
+                    continue
+                b64 = src.split("base64,", 1)[1]
+                with open(f'data/bls_captcha/{uuid.uuid4().hex}.jpg', "wb") as fp:
+                    fp.write(base64.b64decode(b64))
+            raise BizLogicError(message="Sovle captcha failed")
 
     def _extract_hidden_fields(self, soup) -> Dict:
         params = {}
@@ -816,6 +844,33 @@ class BlsPlugin(IVSPlg):
         # 超时处理
         raise NotFoundError(f"OTP email not found within {wait_sec}s")
     
+    def _check_resp_is_session_expired_or_invalid(self, keyword, resp) -> bool:
+        """
+        检测是否发生了 Session 过期
+        """
+        # 1. 检查最终 URL 是否包含登录页特征
+        # 这里的判断依据是你提供的日志:Redirect to /Global/Account/LogIn
+        if "/Account/LogIn" in resp.url or "/Account/Login" in resp.url:
+            self.is_healthy = False
+            raise SessionExpiredOrInvalidError()
+        
+        # 2. (备用) 如果 _perform_request 禁止了重定向,检查 302 Location
+        if resp.status_code == 302:
+            location = resp.headers.get("Location", "")
+            if "/Account/LogIn" in location or "/Account/Login" in location:
+                self.is_healthy = False
+                raise SessionExpiredOrInvalidError()
+            
+        resp_text = resp.text
+        if not resp_text:
+            self.is_healthy = False
+            raise SessionExpiredOrInvalidError()
+    
+        if keyword not in resp_text: 
+            if 'your session has expired, please login again.' in resp_text.lower():
+                self.is_healthy = False
+                raise SessionExpiredOrInvalidError()
+    
     def _save_http_session(self, page_url):
         """
         提取 cookies, local_storage, 存入 VSCloudApi

+ 20 - 8
plugins/de_plugin.py

@@ -45,7 +45,7 @@ class DePlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         
         self.session: Optional[requests.Session] = None
-        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
         
         # 状态
         self.is_healthy = True
@@ -86,7 +86,7 @@ class DePlugin(IVSPlg):
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
 
         self.session = requests.Session(
@@ -108,7 +108,8 @@ class DePlugin(IVSPlg):
         default_headers.pop("X-Requested-With")
         
         resp = self._perform_request('GET', url_home, headers=default_headers)
-        
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="VisaMetric_Home_Page")
         html = resp.text
         soup = BeautifulSoup(html, 'html.parser')
         meta = soup.find('meta', {'name': 'csrf-token'})
@@ -146,7 +147,7 @@ class DePlugin(IVSPlg):
         """
         res = VSQueryResult()
 
-        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置,或者使用原代码的默认值
+        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         max_retries = self.free_config.get("slot_query_max_retries", 2)
 
@@ -287,6 +288,16 @@ class DePlugin(IVSPlg):
             else:
                 proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
         return proxy_url
+    
+    def _save_debug_html(self, content: str, prefix: str = "debug"):
+        save_dir = "debug_pages"
+        if not os.path.exists(save_dir):
+            os.makedirs(save_dir)
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        filename = f"{save_dir}/{prefix}_{timestamp}.html"
+        with open(filename, "w", encoding="utf-8") as f:
+            f.write(content)
+        VSC_INFO("de_plg", "[%s] HTML saved to: %s", self.group_id, filename)
 
     def _submit_captcha(self, code):
         url = f"{self.base_url}/en/appointment-form"
@@ -299,7 +310,8 @@ class DePlugin(IVSPlg):
         headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
         
         resp = self._perform_request('POST', url, data=payload, headers=headers)
-  
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix="VisaMetric_Make_Appointment_Page")
         # 关键:提交验证码后,返回的 HTML 中包含了后续需要的加密参数        
         match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
         if match_pi:
@@ -512,12 +524,12 @@ class DePlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        VSC_INFO('tls_plg', resp.text)
+        if self.config.debug:
+            VSC_INFO('de_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
             return resp
-        elif resp.status_code == 401:
+        elif resp.status_code in [401, 419]:
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
         elif resp.status_code == 403:

+ 65 - 41
plugins/tls_plugin.py

@@ -44,27 +44,6 @@ class TlsPlugin(IVSPlg):
     def health_check(self) -> bool:
         return self.is_healthy
 
-    def _save_debug_html(self, content: str, prefix: str = "debug"):
-        save_dir = "debug_pages"
-        if not os.path.exists(save_dir):
-            os.makedirs(save_dir)
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        filename = f"{save_dir}/{prefix}_{self.group_id}_{timestamp}.html"
-        with open(filename, "w", encoding="utf-8") as f:
-            f.write(content)
-        VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
-            
-    def _get_proxy_url(self):
-        # 构造代理
-        proxy_url = ""
-        if self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
-        return proxy_url
-
     def create_session(self):
         """
         创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
@@ -73,7 +52,7 @@ class TlsPlugin(IVSPlg):
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
         
         self.session = requests.Session(
@@ -106,7 +85,9 @@ class TlsPlugin(IVSPlg):
             'User-Agent': self.user_agent,
         }
         resp = self._perform_request("GET", login_page, headers=headers, params=params)
-        self._save_debug_html(resp.text, 'Login_Page')
+        
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix='Tls_Login_Page')
 
         # 解析 Keycloak 登录地址
         soup = BeautifulSoup(resp.text, 'html.parser')
@@ -138,8 +119,10 @@ class TlsPlugin(IVSPlg):
         }
         headers['Content-Type'] = 'application/x-www-form-urlencoded'
         resp = self._perform_request("POST", authenticate_url, headers=headers, data=payload)
-        self._save_debug_html(resp.text, 'Travel_Groups_Page')
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix='Tls_Travel_Groups_Page')
         # 6. 解析 Travel Groups
+        self._check_page_is_session_expired_or_invalid("My travel group", resp.text)
         groups = self._parse_travel_groups(resp.text)
             
         # 选择匹配城市的 Group
@@ -150,7 +133,7 @@ class TlsPlugin(IVSPlg):
                 break
         
         if not self.travel_group:
-            raise NotFoundError(message=f"No group found for city {target_city}")
+            raise NotFoundError(message=f"No matched group found for city {target_city}")
         VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
 
     def query(self) -> VSQueryResult:
@@ -159,8 +142,8 @@ class TlsPlugin(IVSPlg):
         embassy = self.free_config.get('center', {})
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
+        max_retries = self.free_config.get("max_retries", 2)
         
-
         url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
         params = {
             'location': embassy["code"],
@@ -173,15 +156,29 @@ class TlsPlugin(IVSPlg):
             'user-agent': self.user_agent,
         }
 
-        resp = self._perform_request("GET", url, headers=headers, params=params)
-        
-        self._save_debug_html(resp.text, 'Query_Slot_Page')
-        self._check_session_expired_page(resp.text)
+        for attempt in range(1, max_retries + 1):
+            try:
+                resp = self._perform_request("GET", url, headers=headers, params=params)
+                if self.config.debug:
+                    self._save_debug_html(resp.text, prefix='Tls_Query_Slot_Page')
+                break  # ✅ 请求成功,跳出重试循环
+
+            except PermissionDeniedError:
+                VSC_WARN(
+                    "tls_plg",
+                    "[TLS] Query Appointment-booking blocked (403), attempt %d/%d",
+                    attempt, max_retries
+                )
+
+                # 最后一次就不再绕盾了
+                if attempt >= max_retries:
+                    raise PermissionDeniedError()
 
-        # 检测关键词
-        if not "availableAppointments" in resp.text:
-            raise NotFoundError(message='Query result not found availableAppointments')
+                self._solve_cloudflare5S_challenge()
+                VSC_INFO("tls_plg", "[TLS] Cloudflare bypass success, retrying...")
+                continue
 
+        self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
 
         # 3. 解析 Slots
         all_slots = self._parse_appointment_slots(resp.text)
@@ -286,7 +283,8 @@ class TlsPlugin(IVSPlg):
         body = "".join(body_parts).encode("utf-8")
         
         resp = self.session.post(url, params=params, headers=headers, data=body, allow_redirects=False)
-        self._save_debug_html(resp.text, 'Book_Appointment_Page')
+        if self.config.debug:
+            self._save_debug_html(resp.text, prefix='Tls_Book_Appointment_Page')
         if resp.status_code == 303: 
             res.success = True
             res.book_date = target_date
@@ -297,15 +295,36 @@ class TlsPlugin(IVSPlg):
             res.success = False            
         return res
     
+    def _save_debug_html(self, content: str, prefix: str = "debug"):
+        save_dir = "debug_pages"
+        if not os.path.exists(save_dir):
+            os.makedirs(save_dir)
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        filename = f"{save_dir}/{prefix}_{timestamp}.html"
+        with open(filename, "w", encoding="utf-8") as f:
+            f.write(content)
+        VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
+            
+    def _get_proxy_url(self):
+        # 构造代理
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+        return proxy_url
+    
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
         """
         统一 HTTP 请求封装,严格复刻 C++ 逻辑:
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        VSC_INFO('tls_plg', resp.text)
+        if self.config.debug:
+            VSC_INFO('tls_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
             return resp
         elif resp.status_code == 401:
@@ -414,7 +433,7 @@ class TlsPlugin(IVSPlg):
 
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
         slots = []
-        pattern = r'availableAppointments\\?":\s*(\[.*?\])(?:,\\?"|\},)'
+        pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
         match = re.search(pattern, html, re.DOTALL)
         
         if match:
@@ -446,15 +465,17 @@ class TlsPlugin(IVSPlg):
                                 'type': stype,
                                 'cost': cost
                             })
+            return slots
         else:
-            VSC_WARN("tls_plg", 'Parsed appointment slots page, but not found availableAppointments')
+            VSC_WARN('tls_plg', 'Parsed appointment slot page, but not found availableAppointments')
         return slots
-
-    def _check_session_expired_page(self, html: str) -> bool:
+  
+    def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
         if not html:
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
-        if 'availableAppointments' not in html: 
+    
+        if keyword not in html: 
             if 'redirected automatically' in html.lower():
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()
@@ -462,5 +483,8 @@ class TlsPlugin(IVSPlg):
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()
             if 'session expired!' in html.lower() and 'for security reasons, your session has expired. please log in again to continue.' in html.lower() and 'you will be redirected automatically in 10 seconds.' in html.lower():
+                self.is_healthy = False
+                raise SessionExpiredOrInvalidError()
+            if 'temporarily blocked!' in html.lower() and 'Your session has been temporarily suspended due to the high number of your access to this page.' in html.lower() and 'You can try to access your account again in 2 hours.' in html.lower():
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()

+ 3 - 3
plugins/vfs_plugin.py

@@ -97,7 +97,7 @@ class VfsPlugin(IVSPlg):
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
 
         self.session = requests.Session(
@@ -740,7 +740,6 @@ class VfsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         # --- 1. 发送 OPTIONS 请求 ---
         try:
             # OPTIONS 请求使用相同的 URL 和 headers (部分 header 如 content-length 会被自动处理)
@@ -758,7 +757,8 @@ class VfsPlugin(IVSPlg):
 
 
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
-        VSC_INFO('vfs_plg', resp.text)
+        if self.config.debug:
+            VSC_INFO('vfs_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
         if resp.status_code == 200:
             return resp
         elif resp.status_code == 401:

+ 13 - 22
server.py

@@ -9,13 +9,11 @@ from contextlib import asynccontextmanager
 from fastapi import FastAPI, Body, Request, Query, HTTPException
 from fastapi.responses import JSONResponse
 from fastapi.concurrency import run_in_threadpool
-from utils.browser_util import get_browser
+from utils.browser_util import open_browser, attach_browser
 from toolkit.ocr_engine import PyTorchEngine, DddOcrEngine
 
 
 # ================= 全局资源 =================
-# 全局浏览器对象
-GLOBAL_PAGE = None 
 # 异步锁,用于互斥控制
 BROWSER_LOCK = asyncio.Lock()
 # OCR 引擎字典
@@ -28,9 +26,10 @@ def _sync_get_visatype_ids(tmp_file: str):
     """
     result = {"status": "failed", "message": ""}
     try:
+        browser = attach_browser()
         html_file_path = Path(tmp_file).resolve()
         file_url = f'file://{html_file_path}'
-        GLOBAL_PAGE.get(file_url)
+        browser.get(file_url)
         
         jur_id = None
         loc_id = None
@@ -39,32 +38,32 @@ def _sync_get_visatype_ids(tmp_file: str):
         cat_id = None
         
          # 匹配 ID
-        app_category_labels = GLOBAL_PAGE.eles(f'Appointment Category', timeout=1)
+        app_category_labels = browser.eles(f'Appointment Category', timeout=1)
         for app_category_label in app_category_labels:
             if app_category_label.states.has_rect and app_category_label.tag == 'label':
                 eid = app_category_label.after('tag:input').attr('id')
                 cat_id = int(''.join(filter(str.isdigit, eid)))
                 break
-        jurisdiction_labels = GLOBAL_PAGE.eles(f'Jurisdiction', timeout=1)
+        jurisdiction_labels = browser.eles(f'Jurisdiction', timeout=1)
         if jurisdiction_labels:
             for jurisdiction_label in jurisdiction_labels:
                 if jurisdiction_label.states.has_rect and jurisdiction_label.tag == 'label':
                     eid = jurisdiction_label.after('tag:input').attr('id')
                     jur_id = int(''.join(filter(str.isdigit, eid)))
                     break
-        location_labels = GLOBAL_PAGE.eles(f'Location', timeout=1)
+        location_labels = browser.eles(f'Location', timeout=1)
         for location_label in location_labels:
             if location_label.states.has_rect and location_label.tag == 'label':
                 eid = location_label.after('tag:input', index=2).attr('id')
                 loc_id = int(''.join(filter(str.isdigit, eid)))
                 break
-        visa_type_labels = GLOBAL_PAGE.eles(f'Visa Type', timeout=1)
+        visa_type_labels = browser.eles(f'Visa Type', timeout=1)
         for visa_type_label in visa_type_labels:
             if visa_type_label.states.has_rect and visa_type_label.tag == 'label':
                 eid = visa_type_label.after('tag:input').attr('id')
                 type_id = int(''.join(filter(str.isdigit, eid)))
                 break
-        visa_subtype_labels = GLOBAL_PAGE.eles(f'Visa Sub Type', timeout=1)
+        visa_subtype_labels = browser.eles(f'Visa Sub Type', timeout=1)
         for visa_subtype_label in visa_subtype_labels:
             if visa_subtype_label.states.has_rect and visa_subtype_label.tag == 'label':
                 eid = visa_subtype_label.after('tag:input').attr('id')
@@ -93,11 +92,12 @@ def _sync_get_visable_image_ids(tmp_file: str):
     """
     result = {"status": "failed", "message": ""}
     try:
+        browser = attach_browser()
         images_ids = []
         html_file_path = Path(tmp_file).resolve()
         file_url = f'file://{html_file_path}'
-        GLOBAL_PAGE.get(file_url)
-        captions_ele = GLOBAL_PAGE.ele('xpath://*[@id="captcha-main-div"]/div/div[1]', timeout=5)
+        browser.get(file_url)
+        captions_ele = browser.ele('xpath://*[@id="captcha-main-div"]/div/div[1]', timeout=5)
         if not captions_ele:
             raise Exception('Captions elements not found')
         caption_eles = captions_ele.children()
@@ -107,7 +107,7 @@ def _sync_get_visable_image_ids(tmp_file: str):
                 caption_text = caption.text
 
         number = re.findall(r'\d+', caption_text)[0]
-        captcha_images_ele = GLOBAL_PAGE.ele('xpath://*[@id="captcha-main-div"]/div/div[2]')
+        captcha_images_ele = browser.ele('xpath://*[@id="captcha-main-div"]/div/div[2]')
         captcha_image_eles = captcha_images_ele.children()
         for captcha_image in captcha_image_eles:
             img = captcha_image.ele('.captcha-img')
@@ -138,21 +138,12 @@ async def lifespan(app: FastAPI):
     
     # --- 启动 DrissionPage ---
     print("--- Starting DrissionPage ---")
-    global GLOBAL_PAGE
-
-    
     # 创建浏览器对象,连接浏览器
-    GLOBAL_PAGE = get_browser()
+    open_browser()
     
     yield
     
     # --- 关闭资源 ---
-    print("--- Shutting Down ---")
-    if GLOBAL_PAGE:
-        try:
-            GLOBAL_PAGE.quit() # 关闭浏览器
-        except:
-            pass
     engines.clear()
 
 app = FastAPI(lifespan=lifespan)

+ 4 - 1
utils/browser_util.py

@@ -1,7 +1,7 @@
 from DrissionPage import ChromiumPage, ChromiumOptions
 
 
-def get_browser():
+def open_browser():
     options = ChromiumOptions()
     options.set_local_port(28888)
     options.set_argument(arg='--disable-notifications', value=True)
@@ -22,5 +22,8 @@ def get_browser():
     driver.set.window.max()
     return driver
 
+def attach_browser():
+    return ChromiumPage(addr_or_opts=28888)
+
 def quit_browser(driver):
     driver.quit()

+ 4 - 8
vs_plg.py

@@ -24,15 +24,14 @@ class IVSPlg(ABC):
     @abstractmethod
     def create_session(self) -> None:
         """
-        @brief 创建一个新的会话
-        @return true 表示会话创建成功,false 表示失败
+        @brief 创建一个新的会话, 抛异常则创建失败
         """
         pass
 
     @abstractmethod
     def query(self) -> VSQueryResult:
         """
-        @brief 查询可用的签证预约信息
+        @brief 查询可用的签证预约信息, 抛异常则查询失败
         @return VSQueryResult 查询结果
         """
         pass
@@ -40,7 +39,7 @@ class IVSPlg(ABC):
     @abstractmethod
     def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
         """
-        @brief 进行预约操作
+        @brief 进行预约操作, 抛异常则预定失败
         @param slot_info 查询得到的可用时段信息
         @param user_inputs 用户输入的预约信息
         @return VSBookResult 预约结果
@@ -61,7 +60,4 @@ class IVSPlg(ABC):
         @brief 健康检查,用于检测 API 服务是否正常
         @return true 表示健康状态良好,false 表示存在问题
         """
-        pass
-
-# Python中不需要显式地C接口导出,动态加载模块通常通过importlib或直接import完成
-# CreatePlg 函数逻辑将在 VSPlgFactory 中实现
+        pass

+ 3 - 0
vs_types.py

@@ -66,9 +66,11 @@ class PluginConfig:
 
 @dataclass
 class GroupConfig:
+    debug: bool = False
     enable: bool = False
     identifier: str = ""
     need_account: bool = False
+    account_built_in: bool = True
     account_pool: str = ""
     need_proxy: bool = False
     proxy_pool: str = ""
@@ -99,6 +101,7 @@ class VSProxy:
 
 @dataclass
 class VSPlgConfig:
+    debug: bool = False
     account: VSAccount = field(default_factory=VSAccount)
     proxy: VSProxy = field(default_factory=VSProxy)
     free_config: str = ""