Răsfoiți Sursa

feat: update

jerry 4 luni în urmă
părinte
comite
e7fc2e98d0
12 a modificat fișierele cu 232 adăugiri și 125 ștergeri
  1. 1 1
      config/accounts.json
  2. 40 20
      config/groups.json
  3. 2 0
      core/app_manager.py
  4. 8 7
      group_coordinator.py
  5. 69 14
      plugins/bls_plugin.py
  6. 20 8
      plugins/de_plugin.py
  7. 65 41
      plugins/tls_plugin.py
  8. 3 3
      plugins/vfs_plugin.py
  9. 13 22
      server.py
  10. 4 1
      utils/browser_util.py
  11. 4 8
      vs_plg.py
  12. 3 0
      vs_types.py

+ 1 - 1
config/accounts.json

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

+ 40 - 20
config/groups.json

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

+ 2 - 0
core/app_manager.py

@@ -60,8 +60,10 @@ class AppManager:
 
 
                 grp_cfg = GroupConfig(
                 grp_cfg = GroupConfig(
                     identifier=item["identifier"],
                     identifier=item["identifier"],
+                    debug=item.get("debug", False),
                     enable=item.get("enable", False),
                     enable=item.get("enable", False),
                     need_account=item.get("need_account", False),
                     need_account=item.get("need_account", False),
+                    account_built_in=item.get("account_built_in", True),
                     account_pool=item.get("account_pool", ""),
                     account_pool=item.get("account_pool", ""),
                     need_proxy=item.get("need_proxy", False),
                     need_proxy=item.get("need_proxy", False),
                     proxy_pool=item.get("proxy_pool", ""),
                     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.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.")
         VSC_INFO("coordinator", f"GroupCoordinator for {self.m_cfg.identifier} initialized.")
 
 
@@ -152,7 +152,7 @@ class GroupCoordinator:
                         self.on_query_result(task.instance, result)
                         self.on_query_result(task.instance, result)
                         is_booking_triggered = True
                         is_booking_triggered = True
                     else:
                     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:
                 except Exception as e:
                     VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {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...")
         VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Preparing plugin configuration...")
         plg_cfg = VSPlgConfig()
         plg_cfg = VSPlgConfig()
+        plg_cfg.debug = self.m_cfg.debug
         
         
         # 账号配置
         # 账号配置
         if self.m_cfg.need_account:
         if self.m_cfg.need_account:
@@ -296,8 +297,8 @@ class GroupCoordinator:
         # @brief 创建并初始化单个插件实例。
         # @brief 创建并初始化单个插件实例。
         # 这个方法在 creator_loop 的线程池中执行。
         # 这个方法在 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 = self.m_factory.create(self.m_cfg.identifier, self.m_cfg.plugin_config.plugin_name)
             inst.set_config(plg_cfg)
             inst.set_config(plg_cfg)
             inst.create_session()
             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)
                     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.")
             VSC_INFO("coordinator", f"[{self.m_cfg.identifier}] Plugin instance created and session established.")
             return inst
             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):
     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...")
         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 re
 import os
 import os
+import uuid
 import base64
 import base64
 import time
 import time
 import json
 import json
@@ -61,10 +62,10 @@ class BlsPlugin(IVSPlg):
     def create_session(self):
     def create_session(self):
         self.session = requests.Session(
         self.session = requests.Session(
             proxy=self._get_proxy_url(),
             proxy=self._get_proxy_url(),
-            impersonate="chrome131",
+            impersonate="chrome124",
             curl_options={
             curl_options={
                 const.CurlOpt.MAXAGE_CONN: 1800,
                 const.CurlOpt.MAXAGE_CONN: 1800,
-                const.CurlOpt.VERBOSE: False
+                const.CurlOpt.VERBOSE: self.config.debug
             }
             }
         )
         )
         domain = self.free_config.get("domain")
         domain = self.free_config.get("domain")
@@ -80,11 +81,11 @@ class BlsPlugin(IVSPlg):
         }
         }
         
         
         resp = self._perform_request('GET', login_url, headers=headers)
         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')
         soup = BeautifulSoup(resp.text, 'html.parser')
         form_data = self._extract_hidden_fields(soup)
         form_data = self._extract_hidden_fields(soup)
         
         
-        
         real_user = None
         real_user = None
         real_pass = None
         real_pass = None
     
     
@@ -130,6 +131,9 @@ class BlsPlugin(IVSPlg):
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         }
         resp = self._perform_request('GET', url_vtv, headers=headers)
         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'))
         form_vtv = self._extract_hidden_fields(BeautifulSoup(resp.text, 'html.parser'))
         captcha_token = self._solve_bls_captcha()
         captcha_token = self._solve_bls_captcha()
@@ -148,10 +152,17 @@ class BlsPlugin(IVSPlg):
         url_vt = f"https://{domain}/Global/bls/visatype?data={data_val}"
         url_vt = f"https://{domain}/Global/bls/visatype?data={data_val}"
         
         
         vt_resp = self._perform_request('GET', url_vt, headers=headers)
         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)
         # 这里需要极其复杂的 JS 变量提取 (JS Arrays -> Match Name -> Get ID)
         vt_payload = self._construct_visatype_payload(vt_resp.text, BeautifulSoup(vt_resp.text, 'html.parser'))
         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)
         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()['success']:
             if not vt_res.json()['available']:
             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)}"
         url_ma = f"https://{domain}/Global/blsAppointment/ManageAppointment?{urlencode(self.book_params)}"
         
         
         resp_ma = self._perform_request('GET', url_ma, headers=headers)
         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 =(.*?);")
         avail_str = self._extract_js_var(resp_ma.text, "var availDates", r"var availDates =(.*?);")
         if avail_str:
         if avail_str:
             avail_json = json.loads(avail_str)
             avail_json = json.loads(avail_str)
             # 提取日期
             # 提取日期
             dates = [x['DateText'] for x in avail_json['ad'] if x['SingleSlotAvailable']]
             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:
             if dates:
                 res.success = True
                 res.success = True
                 res.availability_status = AvailabilityStatus.Available
                 res.availability_status = AvailabilityStatus.Available
@@ -334,7 +344,7 @@ class BlsPlugin(IVSPlg):
         if not os.path.exists(save_dir):
         if not os.path.exists(save_dir):
             os.makedirs(save_dir)
             os.makedirs(save_dir)
         timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         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:
         with open(filename, "w", encoding="utf-8") as f:
             f.write(content)
             f.write(content)
         VSC_INFO("bls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
         VSC_INFO("bls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
@@ -345,9 +355,10 @@ class BlsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         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)
         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:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:
@@ -373,6 +384,10 @@ class BlsPlugin(IVSPlg):
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         }
         resp = self._perform_request("GET", url, headers=headers)
         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')
         soup = BeautifulSoup(resp.text, 'html.parser')
         resp = requests.post(
         resp = requests.post(
             f'{self.local_service_url}/browser/visable_captchas', 
             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"
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
         headers["X-Requested-With"] = "XMLHttpRequest"
         headers["X-Requested-With"] = "XMLHttpRequest"
         resp = self._perform_request('POST', submit_url, headers=headers, data=form)
         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:
         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:
     def _extract_hidden_fields(self, soup) -> Dict:
         params = {}
         params = {}
@@ -816,6 +844,33 @@ class BlsPlugin(IVSPlg):
         # 超时处理
         # 超时处理
         raise NotFoundError(f"OTP email not found within {wait_sec}s")
         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):
     def _save_http_session(self, page_url):
         """
         """
         提取 cookies, local_storage, 存入 VSCloudApi
         提取 cookies, local_storage, 存入 VSCloudApi

+ 20 - 8
plugins/de_plugin.py

@@ -45,7 +45,7 @@ class DePlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
         
         
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = None
-        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/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
         self.is_healthy = True
@@ -86,7 +86,7 @@ class DePlugin(IVSPlg):
         curlopt = {
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
         }
 
 
         self.session = requests.Session(
         self.session = requests.Session(
@@ -108,7 +108,8 @@ class DePlugin(IVSPlg):
         default_headers.pop("X-Requested-With")
         default_headers.pop("X-Requested-With")
         
         
         resp = self._perform_request('GET', url_home, headers=default_headers)
         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
         html = resp.text
         soup = BeautifulSoup(html, 'html.parser')
         soup = BeautifulSoup(html, 'html.parser')
         meta = soup.find('meta', {'name': 'csrf-token'})
         meta = soup.find('meta', {'name': 'csrf-token'})
@@ -146,7 +147,7 @@ class DePlugin(IVSPlg):
         """
         """
         res = VSQueryResult()
         res = VSQueryResult()
 
 
-        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置,或者使用原代码的默认值
+        # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         max_retries = self.free_config.get("slot_query_max_retries", 2)
         max_retries = self.free_config.get("slot_query_max_retries", 2)
 
 
@@ -287,6 +288,16 @@ class DePlugin(IVSPlg):
             else:
             else:
                 proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
                 proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
         return proxy_url
         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):
     def _submit_captcha(self, code):
         url = f"{self.base_url}/en/appointment-form"
         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'
         headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
         
         
         resp = self._perform_request('POST', url, data=payload, headers=headers)
         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 中包含了后续需要的加密参数        
         # 关键:提交验证码后,返回的 HTML 中包含了后续需要的加密参数        
         match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
         match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
         if match_pi:
         if match_pi:
@@ -512,12 +524,12 @@ class DePlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         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)
         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:
         if resp.status_code == 200:
             return resp
             return resp
-        elif resp.status_code == 401:
+        elif resp.status_code in [401, 419]:
             self.is_healthy = False
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
             raise SessionExpiredOrInvalidError()
         elif resp.status_code == 403:
         elif resp.status_code == 403:

+ 65 - 41
plugins/tls_plugin.py

@@ -44,27 +44,6 @@ class TlsPlugin(IVSPlg):
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         return self.is_healthy
         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):
     def create_session(self):
         """
         """
         创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
         创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
@@ -73,7 +52,7 @@ class TlsPlugin(IVSPlg):
         curlopt = {
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
         }
         
         
         self.session = requests.Session(
         self.session = requests.Session(
@@ -106,7 +85,9 @@ class TlsPlugin(IVSPlg):
             'User-Agent': self.user_agent,
             'User-Agent': self.user_agent,
         }
         }
         resp = self._perform_request("GET", login_page, headers=headers, params=params)
         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 登录地址
         # 解析 Keycloak 登录地址
         soup = BeautifulSoup(resp.text, 'html.parser')
         soup = BeautifulSoup(resp.text, 'html.parser')
@@ -138,8 +119,10 @@ class TlsPlugin(IVSPlg):
         }
         }
         headers['Content-Type'] = 'application/x-www-form-urlencoded'
         headers['Content-Type'] = 'application/x-www-form-urlencoded'
         resp = self._perform_request("POST", authenticate_url, headers=headers, data=payload)
         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
         # 6. 解析 Travel Groups
+        self._check_page_is_session_expired_or_invalid("My travel group", resp.text)
         groups = self._parse_travel_groups(resp.text)
         groups = self._parse_travel_groups(resp.text)
             
             
         # 选择匹配城市的 Group
         # 选择匹配城市的 Group
@@ -150,7 +133,7 @@ class TlsPlugin(IVSPlg):
                 break
                 break
         
         
         if not self.travel_group:
         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'])
         VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
 
 
     def query(self) -> VSQueryResult:
     def query(self) -> VSQueryResult:
@@ -159,8 +142,8 @@ class TlsPlugin(IVSPlg):
         embassy = self.free_config.get('center', {})
         embassy = self.free_config.get('center', {})
         group_num = self.travel_group['group_number']
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
         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'
         url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
         params = {
         params = {
             'location': embassy["code"],
             'location': embassy["code"],
@@ -173,15 +156,29 @@ class TlsPlugin(IVSPlg):
             'user-agent': self.user_agent,
             '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
         # 3. 解析 Slots
         all_slots = self._parse_appointment_slots(resp.text)
         all_slots = self._parse_appointment_slots(resp.text)
@@ -286,7 +283,8 @@ class TlsPlugin(IVSPlg):
         body = "".join(body_parts).encode("utf-8")
         body = "".join(body_parts).encode("utf-8")
         
         
         resp = self.session.post(url, params=params, headers=headers, data=body, allow_redirects=False)
         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: 
         if resp.status_code == 303: 
             res.success = True
             res.success = True
             res.book_date = target_date
             res.book_date = target_date
@@ -297,15 +295,36 @@ class TlsPlugin(IVSPlg):
             res.success = False            
             res.success = False            
         return res
         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):
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
         """
         """
         统一 HTTP 请求封装,严格复刻 C++ 逻辑:
         统一 HTTP 请求封装,严格复刻 C++ 逻辑:
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         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)
         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:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         elif resp.status_code == 401:
@@ -414,7 +433,7 @@ class TlsPlugin(IVSPlg):
 
 
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
     def _parse_appointment_slots(self, html: str) -> List[Dict]:
         slots = []
         slots = []
-        pattern = r'availableAppointments\\?":\s*(\[.*?\])(?:,\\?"|\},)'
+        pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
         match = re.search(pattern, html, re.DOTALL)
         match = re.search(pattern, html, re.DOTALL)
         
         
         if match:
         if match:
@@ -446,15 +465,17 @@ class TlsPlugin(IVSPlg):
                                 'type': stype,
                                 'type': stype,
                                 'cost': cost
                                 'cost': cost
                             })
                             })
+            return slots
         else:
         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
         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:
         if not html:
             self.is_healthy = False
             self.is_healthy = False
             raise SessionExpiredOrInvalidError()
             raise SessionExpiredOrInvalidError()
-        if 'availableAppointments' not in html: 
+    
+        if keyword not in html: 
             if 'redirected automatically' in html.lower():
             if 'redirected automatically' in html.lower():
                 self.is_healthy = False
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()
                 raise SessionExpiredOrInvalidError()
@@ -462,5 +483,8 @@ class TlsPlugin(IVSPlg):
                 self.is_healthy = False
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()
                 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():
             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
                 self.is_healthy = False
                 raise SessionExpiredOrInvalidError()
                 raise SessionExpiredOrInvalidError()

+ 3 - 3
plugins/vfs_plugin.py

@@ -97,7 +97,7 @@ class VfsPlugin(IVSPlg):
         curlopt = {
         curlopt = {
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXAGE_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
-            const.CurlOpt.VERBOSE: False,
+            const.CurlOpt.VERBOSE: self.config.debug,
         }
         }
 
 
         self.session = requests.Session(
         self.session = requests.Session(
@@ -740,7 +740,6 @@ class VfsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         # --- 1. 发送 OPTIONS 请求 ---
         # --- 1. 发送 OPTIONS 请求 ---
         try:
         try:
             # OPTIONS 请求使用相同的 URL 和 headers (部分 header 如 content-length 会被自动处理)
             # 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)
         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:
         if resp.status_code == 200:
             return resp
             return resp
         elif resp.status_code == 401:
         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 import FastAPI, Body, Request, Query, HTTPException
 from fastapi.responses import JSONResponse
 from fastapi.responses import JSONResponse
 from fastapi.concurrency import run_in_threadpool
 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
 from toolkit.ocr_engine import PyTorchEngine, DddOcrEngine
 
 
 
 
 # ================= 全局资源 =================
 # ================= 全局资源 =================
-# 全局浏览器对象
-GLOBAL_PAGE = None 
 # 异步锁,用于互斥控制
 # 异步锁,用于互斥控制
 BROWSER_LOCK = asyncio.Lock()
 BROWSER_LOCK = asyncio.Lock()
 # OCR 引擎字典
 # OCR 引擎字典
@@ -28,9 +26,10 @@ def _sync_get_visatype_ids(tmp_file: str):
     """
     """
     result = {"status": "failed", "message": ""}
     result = {"status": "failed", "message": ""}
     try:
     try:
+        browser = attach_browser()
         html_file_path = Path(tmp_file).resolve()
         html_file_path = Path(tmp_file).resolve()
         file_url = f'file://{html_file_path}'
         file_url = f'file://{html_file_path}'
-        GLOBAL_PAGE.get(file_url)
+        browser.get(file_url)
         
         
         jur_id = None
         jur_id = None
         loc_id = None
         loc_id = None
@@ -39,32 +38,32 @@ def _sync_get_visatype_ids(tmp_file: str):
         cat_id = None
         cat_id = None
         
         
          # 匹配 ID
          # 匹配 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:
         for app_category_label in app_category_labels:
             if app_category_label.states.has_rect and app_category_label.tag == 'label':
             if app_category_label.states.has_rect and app_category_label.tag == 'label':
                 eid = app_category_label.after('tag:input').attr('id')
                 eid = app_category_label.after('tag:input').attr('id')
                 cat_id = int(''.join(filter(str.isdigit, eid)))
                 cat_id = int(''.join(filter(str.isdigit, eid)))
                 break
                 break
-        jurisdiction_labels = GLOBAL_PAGE.eles(f'Jurisdiction', timeout=1)
+        jurisdiction_labels = browser.eles(f'Jurisdiction', timeout=1)
         if jurisdiction_labels:
         if jurisdiction_labels:
             for jurisdiction_label in jurisdiction_labels:
             for jurisdiction_label in jurisdiction_labels:
                 if jurisdiction_label.states.has_rect and jurisdiction_label.tag == 'label':
                 if jurisdiction_label.states.has_rect and jurisdiction_label.tag == 'label':
                     eid = jurisdiction_label.after('tag:input').attr('id')
                     eid = jurisdiction_label.after('tag:input').attr('id')
                     jur_id = int(''.join(filter(str.isdigit, eid)))
                     jur_id = int(''.join(filter(str.isdigit, eid)))
                     break
                     break
-        location_labels = GLOBAL_PAGE.eles(f'Location', timeout=1)
+        location_labels = browser.eles(f'Location', timeout=1)
         for location_label in location_labels:
         for location_label in location_labels:
             if location_label.states.has_rect and location_label.tag == 'label':
             if location_label.states.has_rect and location_label.tag == 'label':
                 eid = location_label.after('tag:input', index=2).attr('id')
                 eid = location_label.after('tag:input', index=2).attr('id')
                 loc_id = int(''.join(filter(str.isdigit, eid)))
                 loc_id = int(''.join(filter(str.isdigit, eid)))
                 break
                 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:
         for visa_type_label in visa_type_labels:
             if visa_type_label.states.has_rect and visa_type_label.tag == 'label':
             if visa_type_label.states.has_rect and visa_type_label.tag == 'label':
                 eid = visa_type_label.after('tag:input').attr('id')
                 eid = visa_type_label.after('tag:input').attr('id')
                 type_id = int(''.join(filter(str.isdigit, eid)))
                 type_id = int(''.join(filter(str.isdigit, eid)))
                 break
                 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:
         for visa_subtype_label in visa_subtype_labels:
             if visa_subtype_label.states.has_rect and visa_subtype_label.tag == 'label':
             if visa_subtype_label.states.has_rect and visa_subtype_label.tag == 'label':
                 eid = visa_subtype_label.after('tag:input').attr('id')
                 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": ""}
     result = {"status": "failed", "message": ""}
     try:
     try:
+        browser = attach_browser()
         images_ids = []
         images_ids = []
         html_file_path = Path(tmp_file).resolve()
         html_file_path = Path(tmp_file).resolve()
         file_url = f'file://{html_file_path}'
         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:
         if not captions_ele:
             raise Exception('Captions elements not found')
             raise Exception('Captions elements not found')
         caption_eles = captions_ele.children()
         caption_eles = captions_ele.children()
@@ -107,7 +107,7 @@ def _sync_get_visable_image_ids(tmp_file: str):
                 caption_text = caption.text
                 caption_text = caption.text
 
 
         number = re.findall(r'\d+', caption_text)[0]
         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()
         captcha_image_eles = captcha_images_ele.children()
         for captcha_image in captcha_image_eles:
         for captcha_image in captcha_image_eles:
             img = captcha_image.ele('.captcha-img')
             img = captcha_image.ele('.captcha-img')
@@ -138,21 +138,12 @@ async def lifespan(app: FastAPI):
     
     
     # --- 启动 DrissionPage ---
     # --- 启动 DrissionPage ---
     print("--- Starting DrissionPage ---")
     print("--- Starting DrissionPage ---")
-    global GLOBAL_PAGE
-
-    
     # 创建浏览器对象,连接浏览器
     # 创建浏览器对象,连接浏览器
-    GLOBAL_PAGE = get_browser()
+    open_browser()
     
     
     yield
     yield
     
     
     # --- 关闭资源 ---
     # --- 关闭资源 ---
-    print("--- Shutting Down ---")
-    if GLOBAL_PAGE:
-        try:
-            GLOBAL_PAGE.quit() # 关闭浏览器
-        except:
-            pass
     engines.clear()
     engines.clear()
 
 
 app = FastAPI(lifespan=lifespan)
 app = FastAPI(lifespan=lifespan)

+ 4 - 1
utils/browser_util.py

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

+ 4 - 8
vs_plg.py

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

+ 3 - 0
vs_types.py

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