jerry 3 недель назад
Родитель
Сommit
d16bdfe3c4
7 измененных файлов с 80 добавлено и 173 удалено
  1. 12 0
      booker_builtin.py
  2. 12 0
      booker_order.py
  3. 24 9
      plugins/grc_plugin.py
  4. 7 1
      plugins/tls_plugin.py
  5. 20 159
      plugins/vfs_plugin.py
  6. 1 0
      tls_registration_bot.py
  7. 4 4
      vs_plg.py

+ 12 - 0
booker_builtin.py

@@ -185,6 +185,18 @@ class BuiltinBookerGCO:
                     "payment_link": book_res.payment_link
                     "payment_link": book_res.payment_link
                 }
                 }
                 VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
                 VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
+                push_content = (
+                    f"🎉 【预定成功通知】\n"
+                    f"━━━━━━━━━━━━━━━\n"
+                    f"订单编号: {order_id}\n"
+                    f"预约账号: {book_res.account}\n"
+                    f"预约日期: {book_res.book_date}\n"
+                    f"预约时间: {book_res.book_time}\n"
+                    f"预约编号: {book_res.urn}\n"
+                    f"支付链接: {book_res.payment_link if book_res.payment_link else '无需支付/暂无'}\n"
+                    f"━━━━━━━━━━━━━━━\n"
+                )
+                VSCloudApi.Instance().push_weixin_text(push_content)
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 
                 
                 # === 核心:成功次数判断 ===
                 # === 核心:成功次数判断 ===

+ 12 - 0
booker_order.py

@@ -199,6 +199,18 @@ class OrderBookerGCO:
                     "payment_link": book_res.payment_link
                     "payment_link": book_res.payment_link
                 }
                 }
                 VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
                 VSCloudApi.Instance().update_vas_task(task_id, {"status": "grabbed", "grabbed_history": grab_info})
+                push_content = (
+                    f"🎉 【预定成功通知】\n"
+                    f"━━━━━━━━━━━━━━━\n"
+                    f"订单编号: {order_id}\n"
+                    f"预约账号: {book_res.account}\n"
+                    f"预约日期: {book_res.book_date}\n"
+                    f"预约时间: {book_res.book_time}\n"
+                    f"预约编号: {book_res.urn}\n"
+                    f"支付链接: {book_res.payment_link if book_res.payment_link else '无需支付/暂无'}\n"
+                    f"━━━━━━━━━━━━━━━\n"
+                )
+                VSCloudApi.Instance().push_weixin_text(push_content)
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 self.redis_client.zrem(self.m_tracker_key, task_id)
                 
                 
                 with self.m_lock:
                 with self.m_lock:

+ 24 - 9
plugins/grc_plugin.py

@@ -11,6 +11,20 @@ from curl_cffi import requests, const
 from vs_plg import IVSPlg
 from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 
 
+MODERN_BROWSERS: List[str] = [
+    # Chrome (124+)
+    "chrome124", "chrome131", "chrome133a", "chrome136", "chrome142", "chrome145", "chrome146",
+    "chrome131_android",
+    
+    # Safari (18+)
+    "safari180", "safari184", "safari260", "safari2601",
+    "safari180_ios", "safari184_ios", "safari260_ios",
+    
+    # Firefox (133+)
+    "firefox133", "firefox135", "firefox144", "firefox147",
+    
+    "tor145"
+]
 
 
 class GrcPlugin(IVSPlg):
 class GrcPlugin(IVSPlg):
     """
     """
@@ -42,10 +56,7 @@ class GrcPlugin(IVSPlg):
         self.free_config = config.free_config or {}
         self.free_config = config.free_config or {}
         
         
     def keep_alive(self):
     def keep_alive(self):
-        home_page = "https://www.supersaas.com/schedule/GreekEmbassyInDublin/Visas"
-        resp = self._perform_request("GET", home_page)
-        if f'Signed in as {self.config.account.username.lower()}' not in resp.text:
-            self.is_healthy = False
+        pass
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         if not self.is_healthy:
         if not self.is_healthy:
@@ -68,9 +79,12 @@ class GrcPlugin(IVSPlg):
             const.CurlOpt.VERBOSE: self.config.debug,
             const.CurlOpt.VERBOSE: self.config.debug,
         }
         }
         
         
+        chosen_browser = random.choice(MODERN_BROWSERS)
+        self._log(f"Using browser fingerprint: {chosen_browser}")
+
         self.session = requests.Session(
         self.session = requests.Session(
             proxy=self._get_proxy_url(),
             proxy=self._get_proxy_url(),
-            impersonate="chrome124",
+            impersonate=chosen_browser,
             curl_options=curlopt,
             curl_options=curlopt,
             use_thread_local_curl=False,
             use_thread_local_curl=False,
             http_version=const.CurlHttpVersion.V2TLS
             http_version=const.CurlHttpVersion.V2TLS
@@ -321,7 +335,9 @@ class GrcPlugin(IVSPlg):
         res = VSBookResult()
         res = VSBookResult()
         res.success = False
         res.success = False
 
 
-        # --- 1. 筛选并收集所有可用 Slot ---
+        # 重新登录一次,避免session expired
+        self.create_session()
+        
         exp_start = user_inputs.get('expected_start_date', '')
         exp_start = user_inputs.get('expected_start_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         
         
@@ -412,6 +428,7 @@ class GrcPlugin(IVSPlg):
                     self._log(f"Slot {start_time_str} unavailable ({err_reason}). Trying next...")
                     self._log(f"Slot {start_time_str} unavailable ({err_reason}). Trying next...")
                     continue
                     continue
 
 
+                time.sleep(random.uniform(10, 30))
                 # ==========================
                 # ==========================
                 # Request step 2: 提交表单
                 # Request step 2: 提交表单
                 # ==========================
                 # ==========================
@@ -525,6 +542,4 @@ class GrcPlugin(IVSPlg):
             self.is_healthy = False
             self.is_healthy = False
             raise RateLimiteddError()
             raise RateLimiteddError()
         else:
         else:
-            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
-            
-    #[修复点 3]:删除了原有代码最下方重复的 def _filter_dates 方法
+            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")

+ 7 - 1
plugins/tls_plugin.py

@@ -85,7 +85,13 @@ class TlsPlugin(IVSPlg):
         self.free_config = config.free_config or {}
         self.free_config = config.free_config or {}
         
         
     def keep_alive(self):
     def keep_alive(self):
-        pass
+        try:
+            resp = self._perform_request("GET", self.page.url, retry_count=1)
+            self._check_page_is_session_expired_or_invalid('Book your appointment', html = resp.text)
+        except SessionExpiredOrInvalidError as e:
+            self.is_healthy = False
+        except Exception as e:
+            pass
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         if not self.is_healthy:
         if not self.is_healthy:

+ 20 - 159
plugins/vfs_plugin.py

@@ -8,15 +8,14 @@ import base64
 import uuid
 import uuid
 import shutil
 import shutil
 import re
 import re
+import socket
 import urllib.parse
 import urllib.parse
 from datetime import datetime
 from datetime import datetime
 from typing import Dict, Any, Optional, List, Tuple, Callable
 from typing import Dict, Any, Optional, List, Tuple, Callable
 
 
-# DrissionPage 核心引入
 from DrissionPage import ChromiumPage, ChromiumOptions
 from DrissionPage import ChromiumPage, ChromiumOptions
 from DrissionPage.common import Settings
 from DrissionPage.common import Settings
 
 
-# 加密库
 from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.backends import default_backend
@@ -28,8 +27,6 @@ from toolkit.proxy_tunnel import ProxyTunnel
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 
 
 
 
-# ----------------- 静态常量与辅助数据 -----------------
-
 VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
 VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
 LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
 LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
@@ -123,7 +120,6 @@ class VfsPlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
         self.logger = None
         self.logger = None
         
         
-        # 替换 requests.Session 为 DrissionPage
         self.page: Optional[ChromiumPage] = None
         self.page: Optional[ChromiumPage] = None
         
         
         self.jwt_token: str = ""
         self.jwt_token: str = ""
@@ -208,14 +204,7 @@ class VfsPlugin(IVSPlg):
         4. JS fetch 登录
         4. JS fetch 登录
         """
         """
         self._log(f"Initializing Session (ID: {self.instance_id})...")
         self._log(f"Initializing Session (ID: {self.instance_id})...")
-        
-        # 0. 配置浏览器
         co = ChromiumOptions()
         co = ChromiumOptions()
-        # -------------------------------------------------------------
-        # [核心修复] 解决 'not enough values to unpack'
-        # -------------------------------------------------------------
-        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
-        # 2. 我们手动随机生成一个端口
         import random
         import random
         import socket
         import socket
         
         
@@ -227,38 +216,24 @@ class VfsPlugin(IVSPlg):
         debug_port = get_free_port()
         debug_port = get_free_port()
         self._log(f"Assigned Debug Port: {debug_port}")
         self._log(f"Assigned Debug Port: {debug_port}")
         
         
-        # 3. 强制指定端口,DrissionPage 就会直接连接,不再解析日志
         co.set_local_port(debug_port)
         co.set_local_port(debug_port)
-        
-        # --- [关键配置] 设置独立的用户数据目录 ---
-        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
-        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
         co.set_user_data_path(self.user_data_path)
         co.set_user_data_path(self.user_data_path)
         
         
-        # --- 1. 指定浏览器路径 (适配 Docker) ---
         chrome_path = os.getenv("CHROME_BIN")
         chrome_path = os.getenv("CHROME_BIN")
         if chrome_path and os.path.exists(chrome_path):
         if chrome_path and os.path.exists(chrome_path):
             co.set_paths(browser_path=chrome_path)
             co.set_paths(browser_path=chrome_path)
         
         
-        # --- [核心修改] 代理配置 ---
         if self.config.proxy and self.config.proxy.ip:
         if self.config.proxy and self.config.proxy.ip:
             p = self.config.proxy
             p = self.config.proxy
             
             
             if p.username and p.password:
             if p.username and p.password:
                 self._log(f"Starting Proxy Tunnel for {p.ip}...")
                 self._log(f"Starting Proxy Tunnel for {p.ip}...")
-                
-                # 1. 启动本地隧道
                 self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
                 self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
                 local_proxy = self.tunnel.start()
                 local_proxy = self.tunnel.start()
-                
                 self._log(f"Tunnel started at {local_proxy}")
                 self._log(f"Tunnel started at {local_proxy}")
-                
-                # 2. Chrome 连接本地免密端口
-                # 必须使用 --proxy-server 强制指定,绝对稳健
                 co.set_argument(f'--proxy-server={local_proxy}')
                 co.set_argument(f'--proxy-server={local_proxy}')
                 
                 
             else:
             else:
-                # 无密码代理,直接用
                 proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
                 proxy_str = f"{p.scheme}://{p.ip}:{p.port}"
                 co.set_argument(f'--proxy-server={proxy_str}')
                 co.set_argument(f'--proxy-server={proxy_str}')
         else:
         else:
@@ -267,16 +242,13 @@ class VfsPlugin(IVSPlg):
         co.headless(False) 
         co.headless(False) 
         co.set_argument('--no-sandbox')
         co.set_argument('--no-sandbox')
         co.set_argument('--disable-gpu')
         co.set_argument('--disable-gpu')
-        # Docker 默认 /dev/shm 只有 64MB,Chromium 很容易爆内存崩溃
         co.set_argument('--disable-dev-shm-usage') 
         co.set_argument('--disable-dev-shm-usage') 
-        
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
         co.set_argument('--disable-blink-features=AutomationControlled')
 
 
         try:
         try:
             self.page = ChromiumPage(co)
             self.page = ChromiumPage(co)
             
             
-            # 1. 导航到登录页面
             mission = self.free_config.get("mission_code", "")
             mission = self.free_config.get("mission_code", "")
             country = self.free_config.get("country_code", "")
             country = self.free_config.get("country_code", "")
             lang = self.free_config.get("language", "en")
             lang = self.free_config.get("language", "en")
@@ -289,24 +261,14 @@ class VfsPlugin(IVSPlg):
             
             
             self.page.get(login_page_url)
             self.page.get(login_page_url)
             
             
-            # -------------------------------------------------------------
-            # [核心修改] 2. 智能 Cloudflare 过盾逻辑
-            # -------------------------------------------------------------
             self._log("Handling Cloudflare challenge...")
             self._log("Handling Cloudflare challenge...")
             
             
-            # 初始化过盾助手
             cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
             cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
             cf_token = ""
             cf_token = ""
             
             
-            # 循环检测 (40秒超时)
             for i in range(40):
             for i in range(40):
                 time.sleep(1)
                 time.sleep(1)
-                
-                # A. 优先处理 Cookie 遮挡 (VFS 必须步骤)
-                # 如果不关掉 cookie banner,验证码可能点不到
                 self._handle_cookie_banner()
                 self._handle_cookie_banner()
-                
-                # B. 尝试从 DOM 获取 Token (无感验证可能自动通过)
                 try:
                 try:
                     ele = self.page.ele('@name=cf-turnstile-response')
                     ele = self.page.ele('@name=cf-turnstile-response')
                     if ele and ele.value:
                     if ele and ele.value:
@@ -316,28 +278,21 @@ class VfsPlugin(IVSPlg):
                 except:
                 except:
                     pass
                     pass
                 
                 
-                # C. 如果前 3 秒没自动出 Token,开始尝试点击
                 if i > 2:
                 if i > 2:
                     try:
                     try:
-                        # 开启 DFS 深度搜索模式 (防止 Shadow DOM 嵌套太深找不到)
-                        # 在第 10 秒后开启深度搜索,前期用快速搜索
                         use_dfs = False
                         use_dfs = False
                         cf_bypasser.click_verification_button(is_dfs=use_dfs)
                         cf_bypasser.click_verification_button(is_dfs=use_dfs)
                     except Exception as e:
                     except Exception as e:
-                        # 点击错误忽略,继续下一轮
                         pass
                         pass
                 
                 
-                # D. 检查是否已经看到了登录框 (有时候 Token 提取慢了,但页面已经变了)
                 if self.page.ele('tag:form') or self.page.ele('#mat-input-0'):
                 if self.page.ele('tag:form') or self.page.ele('#mat-input-0'):
                     self._log("Login form detected.")
                     self._log("Login form detected.")
-                    # 继续尝试提取一次 Token,如果实在没有也不要死循环
                     if i > 5 and not cf_token:
                     if i > 5 and not cf_token:
                         self._log("Form visible but token not found yet...")
                         self._log("Form visible but token not found yet...")
             
             
             # -------------------------------------------------------------
             # -------------------------------------------------------------
             
             
             if not cf_token:
             if not cf_token:
-                # 最后尝试一次强取
                 try:
                 try:
                     cf_token = self.page.ele('@name=cf-turnstile-response').value
                     cf_token = self.page.ele('@name=cf-turnstile-response').value
                 except:
                 except:
@@ -347,7 +302,6 @@ class VfsPlugin(IVSPlg):
                 self._log("[WARN] Could not extract Turnstile token.")
                 self._log("[WARN] Could not extract Turnstile token.")
                 raise BizLogicError(f"Could not extract Turnstile token.")
                 raise BizLogicError(f"Could not extract Turnstile token.")
 
 
-            # 3. 准备登录 API 参数
             email = self.config.account.username
             email = self.config.account.username
             password = self.config.account.password
             password = self.config.account.password
             enc_password = self._encrypt_password(password)
             enc_password = self._encrypt_password(password)
@@ -378,16 +332,12 @@ class VfsPlugin(IVSPlg):
             resp = self._perform_request("POST", url, headers=headers, json_data=data)
             resp = self._perform_request("POST", url, headers=headers, json_data=data)
             resp_json = resp.json()
             resp_json = resp.json()
 
 
-            # 分支 1: 登录成功
             if resp_json.get('accessToken'):
             if resp_json.get('accessToken'):
                 self.jwt_token = resp_json["accessToken"]
                 self.jwt_token = resp_json["accessToken"]
                 self._log("Login successful, JWT obtained.")
                 self._log("Login successful, JWT obtained.")
             
             
-            # 分支 2: OTP
             elif resp_json.get("enableOTPAuthentication"):
             elif resp_json.get("enableOTPAuthentication"):
                 self._log("Login requires OTP.")
                 self._log("Login requires OTP.")
-                # 注意:_submit_login_otp 内部也会调用 _refresh_turnstile_token
-                # 所以这里旧的 cf_token 其实用处不大,传过去也没事
                 otp = self._read_otp_email(sent_at=sent_at)
                 otp = self._read_otp_email(sent_at=sent_at)
                 self._submit_login_otp(cf_token, otp)
                 self._submit_login_otp(cf_token, otp)
             
             
@@ -442,19 +392,11 @@ class VfsPlugin(IVSPlg):
         if not self.page:
         if not self.page:
             raise BizLogicError("Browser session not initialized")
             raise BizLogicError("Browser session not initialized")
 
 
-        # ---------------------------------------------------------
-        # 1. 预处理 URL (构造最终请求地址)
-        # ---------------------------------------------------------
         req_url = url
         req_url = url
         if params:
         if params:
-            # 确保引用了 urllib
-            import urllib.parse
             sep = '&' if '?' in req_url else '?'
             sep = '&' if '?' in req_url else '?'
             req_url += sep + urllib.parse.urlencode(params)
             req_url += sep + urllib.parse.urlencode(params)
 
 
-        # ---------------------------------------------------------
-        # 2. 构造 Body 和 Fetch 选项
-        # ---------------------------------------------------------
         final_headers = headers or {}
         final_headers = headers or {}
         
         
         fetch_options = {
         fetch_options = {
@@ -463,7 +405,6 @@ class VfsPlugin(IVSPlg):
             "credentials": "include" # 关键:带上浏览器 Cookie
             "credentials": "include" # 关键:带上浏览器 Cookie
         }
         }
         
         
-        # 用于日志记录的 Body 内容(字符串形式)
         log_body = "None"
         log_body = "None"
 
 
         if json_data:
         if json_data:
@@ -473,7 +414,6 @@ class VfsPlugin(IVSPlg):
             log_body = json_str
             log_body = json_str
         elif data:
         elif data:
             if isinstance(data, dict):
             if isinstance(data, dict):
-                import urllib.parse
                 encoded_data = urllib.parse.urlencode(data)
                 encoded_data = urllib.parse.urlencode(data)
                 fetch_options['body'] = encoded_data
                 fetch_options['body'] = encoded_data
                 fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
                 fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
@@ -482,16 +422,10 @@ class VfsPlugin(IVSPlg):
                 fetch_options['body'] = data
                 fetch_options['body'] = data
                 log_body = str(data)
                 log_body = str(data)
 
 
-        # ---------------------------------------------------------
-        # [日志] 记录请求数据
-        # ---------------------------------------------------------
         self._log(f"┌── [TRAFFIC REQUEST] {method} {req_url}")
         self._log(f"┌── [TRAFFIC REQUEST] {method} {req_url}")
         self._log(f"├── Headers: {json.dumps(final_headers)}")
         self._log(f"├── Headers: {json.dumps(final_headers)}")
         self._log(f"└── Body: {log_body}")
         self._log(f"└── Body: {log_body}")
 
 
-        # ---------------------------------------------------------
-        # 3. 注入 JS 执行 Fetch
-        # ---------------------------------------------------------
         js_script = f"""
         js_script = f"""
         const url = "{req_url}";
         const url = "{req_url}";
         const options = {json.dumps(fetch_options)};
         const options = {json.dumps(fetch_options)};
@@ -525,8 +459,6 @@ class VfsPlugin(IVSPlg):
         """
         """
         
         
         try:
         try:
-            # run_js 直接返回 return 的对象
-            # 适当增加超时时间,防止网络慢导致 Python 侧报错
             res_dict = self.page.run_js(js_script, timeout=60)
             res_dict = self.page.run_js(js_script, timeout=60)
         except Exception as e:
         except Exception as e:
             self._log(f"[TRAFFIC ERROR] JS Execution failed: {e}")
             self._log(f"[TRAFFIC ERROR] JS Execution failed: {e}")
@@ -534,20 +466,12 @@ class VfsPlugin(IVSPlg):
 
 
         resp = BrowserResponse(res_dict)
         resp = BrowserResponse(res_dict)
         
         
-        # ---------------------------------------------------------
-        # [日志] 记录响应数据
-        # ---------------------------------------------------------
         duration = res_dict.get('duration', 0)
         duration = res_dict.get('duration', 0)
-        # 截取过长的响应体,避免日志文件爆炸 (保留前 1000 字符)
-        # 如果需要完整分析,可以去掉 [:1000]
         resp_preview = resp.text[:1000] + "..." if len(resp.text) > 1000 else resp.text
         resp_preview = resp.text[:1000] + "..." if len(resp.text) > 1000 else resp.text
         
         
         self._log(f"┌── [TRAFFIC RESPONSE] Status: {resp.status_code} | Time: {duration}ms")
         self._log(f"┌── [TRAFFIC RESPONSE] Status: {resp.status_code} | Time: {duration}ms")
         self._log(f"└── Body: {resp_preview}")
         self._log(f"└── Body: {resp_preview}")
 
 
-        # ---------------------------------------------------------
-        # 4. 统一处理状态码
-        # ---------------------------------------------------------
         if resp.status_code == 200:
         if resp.status_code == 200:
             return resp
             return resp
             
             
@@ -556,25 +480,20 @@ class VfsPlugin(IVSPlg):
             raise SessionExpiredOrInvalidError(f"401 Unauthorized: {resp.text[:100]}")
             raise SessionExpiredOrInvalidError(f"401 Unauthorized: {resp.text[:100]}")
             
             
         elif resp.status_code == 403:
         elif resp.status_code == 403:
-            # 检查是否是 Cloudflare 拦截
             if "Just a moment" in resp.text or "cloudflare" in resp.text.lower():
             if "Just a moment" in resp.text or "cloudflare" in resp.text.lower():
                 self._log(f"[TRAFFIC] HTTP 403 (Cloudflare) detected. Re-verifying (Try {retry_count+1}/3)...")
                 self._log(f"[TRAFFIC] HTTP 403 (Cloudflare) detected. Re-verifying (Try {retry_count+1}/3)...")
                 
                 
                 if retry_count < 3:
                 if retry_count < 3:
-                    # 调用过盾逻辑
                     new_token = self._refresh_turnstile_token()
                     new_token = self._refresh_turnstile_token()
                     
                     
                     if new_token:
                     if new_token:
                         self._log("[TRAFFIC] In-page verification success. Retrying...")
                         self._log("[TRAFFIC] In-page verification success. Retrying...")
                         
                         
-                        # 如果原请求包含验证码字段,更新它
                         if json_data and "captcha_api_key" in json_data:
                         if json_data and "captcha_api_key" in json_data:
                             json_data["captcha_api_key"] = new_token
                             json_data["captcha_api_key"] = new_token
                             
                             
-                        # 递归重试
                         return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
                         return self._perform_request(method, url, headers, data, json_data, params, retry_count+1)
             
             
-            # 如果不是 CF 或者重试耗尽
             raise PermissionDeniedError(f"HTTP 403 Forbidden: {resp.text[:100]}")
             raise PermissionDeniedError(f"HTTP 403 Forbidden: {resp.text[:100]}")
             
             
         elif resp.status_code == 429:
         elif resp.status_code == 429:
@@ -585,11 +504,9 @@ class VfsPlugin(IVSPlg):
             raise BizLogicError(f"Network Error (Fetch Failed): {resp.text}")
             raise BizLogicError(f"Network Error (Fetch Failed): {resp.text}")
             
             
         else:
         else:
-            # 允许 400 业务错误通过,交给上层解析 (例如登录失败)
             if url.endswith("/login") and resp.status_code == 400:
             if url.endswith("/login") and resp.status_code == 400:
                 return resp
                 return resp
             
             
-            # 其他错误视为业务逻辑异常
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
 
 
     def _handle_cookie_banner(self):
     def _handle_cookie_banner(self):
@@ -638,20 +555,15 @@ class VfsPlugin(IVSPlg):
         不带 Origin: visa.vfsglobal.com,也不带 credentials,符合 ipify 规则。
         不带 Origin: visa.vfsglobal.com,也不带 credentials,符合 ipify 规则。
         """
         """
         try:
         try:
-            # 1. 新建一个标签页 (后台静默打开)
             tab = self.page.new_tab("https://api.ipify.org/?format=json")
             tab = self.page.new_tab("https://api.ipify.org/?format=json")
             
             
-            # 2. 获取页面内容 (DrissionPage 会自动等待页面加载)
-            # ipify 返回的是纯 JSON 文本,通常在 body 或 pre 标签里
             if tab.ele('tag:pre'):
             if tab.ele('tag:pre'):
                 json_text = tab.ele('tag:pre').text
                 json_text = tab.ele('tag:pre').text
             else:
             else:
                 json_text = tab.ele('tag:body').text
                 json_text = tab.ele('tag:body').text
             
             
-            # 3. 提取 IP
             ip = json.loads(json_text)['ip']
             ip = json.loads(json_text)['ip']
             
             
-            # 4. 务必关闭标签页,释放资源
             tab.close()
             tab.close()
             
             
             self._log(f"Real Network IP: {ip}")
             self._log(f"Real Network IP: {ip}")
@@ -659,7 +571,6 @@ class VfsPlugin(IVSPlg):
             
             
         except Exception as e:
         except Exception as e:
             self._log(f"[WARN] Failed to check IP via new tab: {e}")
             self._log(f"[WARN] Failed to check IP via new tab: {e}")
-            # 尝试清理可能没关掉的标签页
             try:
             try:
                 if self.page.tabs_count > 1:
                 if self.page.tabs_count > 1:
                     tab.close()
                     tab.close()
@@ -677,14 +588,9 @@ class VfsPlugin(IVSPlg):
         
         
         h = {
         h = {
             "accept": "application/json, text/plain, */*",
             "accept": "application/json, text/plain, */*",
-            # "origin": ... 浏览器自动处理
-            # "referer": ... 浏览器自动处理
             "route": route
             "route": route
         }
         }
         
         
-        # 即使是浏览器环境,VFS 也需要这两个加密参数
-        # 注意:这里可能需要从 JS 获取,或者保持 Python 生成
-        # 如果 Python 生成的总是报错,可以考虑把加密逻辑移到 JS 里跑
         h["clientsource"] = self._get_client_source()
         h["clientsource"] = self._get_client_source()
         
         
         if with_auth and self.jwt_token:
         if with_auth and self.jwt_token:
@@ -725,9 +631,6 @@ class VfsPlugin(IVSPlg):
             "payCode": ""
             "payCode": ""
         }
         }
         headers = self._get_common_headers(with_auth=True)
         headers = self._get_common_headers(with_auth=True)
-        # fetch 不需要显式 content-type application/json,json_data会自动处理
-        
-        # DrissionPage 不需要手动处理 403 绕盾,因为浏览器本身就在盾后面
         resp = self._perform_request("POST", url, headers=headers, json_data=data, retry_count=2)
         resp = self._perform_request("POST", url, headers=headers, json_data=data, retry_count=2)
         
         
         if "WaitList" in resp.text:
         if "WaitList" in resp.text:
@@ -819,8 +722,6 @@ class VfsPlugin(IVSPlg):
     def _submit_login_otp(self, old_cf_token: str, otp: str):
     def _submit_login_otp(self, old_cf_token: str, otp: str):
         self._log("Submitting Login OTP...")
         self._log("Submitting Login OTP...")
         
         
-        # --- [新增] 必须刷新 Token ---
-        # 旧的 old_cf_token 已经在第一步登录时失效了
         new_cf_token = self._refresh_turnstile_token()
         new_cf_token = self._refresh_turnstile_token()
         # ---------------------------
         # ---------------------------
 
 
@@ -859,7 +760,6 @@ class VfsPlugin(IVSPlg):
             self._log("OTP Login successful.")
             self._log("OTP Login successful.")
             return
             return
 
 
-        # 增加错误详情日志
         error_desc = resp_json.get("description", resp.text)
         error_desc = resp_json.get("description", resp.text)
         raise PermissionDeniedError(message=f"OTP Login Failed: {error_desc}")
         raise PermissionDeniedError(message=f"OTP Login Failed: {error_desc}")
     
     
@@ -869,7 +769,6 @@ class VfsPlugin(IVSPlg):
         """
         """
         self._log("Refreshing Cloudflare Turnstile token...")
         self._log("Refreshing Cloudflare Turnstile token...")
         
         
-        # 1. JS 强制重置 (保持不变)
         js_reset = """
         js_reset = """
         try {
         try {
             var input = document.querySelector('input[name="cf-turnstile-response"]');
             var input = document.querySelector('input[name="cf-turnstile-response"]');
@@ -881,16 +780,11 @@ class VfsPlugin(IVSPlg):
         """
         """
         self.page.run_js(js_reset)
         self.page.run_js(js_reset)
         
         
-        # 2. 初始化过盾助手
-        # 假设 CloudflareBypasser 类已在当前文件中定义
         cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
         cf_bypasser = CloudflareBypasser(self.page, log=self.config.debug)
-        
-        # 3. 轮询等待 (30秒)
+
         for i in range(60): 
         for i in range(60): 
             time.sleep(0.5)
             time.sleep(0.5)
             
             
-            # A. 检查 Token 是否已生成
-            # 使用 DrissionPage 的方式获取 value 比较稳定
             try:
             try:
                 ele = self.page.ele('@name=cf-turnstile-response')
                 ele = self.page.ele('@name=cf-turnstile-response')
                 if ele and ele.value:
                 if ele and ele.value:
@@ -899,18 +793,11 @@ class VfsPlugin(IVSPlg):
             except:
             except:
                 pass
                 pass
             
             
-            # B. 尝试点击验证框
-            # 策略:前2秒等待,之后开始尝试点击
             if i > 4: 
             if i > 4: 
-                # [重要] VFS 经常有 Cookie 弹窗遮挡,先尝试清理一下
                 self._handle_cookie_banner()
                 self._handle_cookie_banner()
                 
                 
                 try:
                 try:
-                    # 使用 CloudflareBypasser 的高级点击逻辑
-                    # is_dfs=True 表示如果普通搜索找不到,就递归搜索 iframe (更耗时但更强)
-                    # 我们在尝试 10 次 (5秒) 后开启 DFS 模式
-                    use_dfs = (i > 14)
-                    
+                    use_dfs = (i > 14) 
                     cf_bypasser.click_verification_button(is_dfs=use_dfs)
                     cf_bypasser.click_verification_button(is_dfs=use_dfs)
                 except Exception as e:
                 except Exception as e:
                     # 点击过程报错不要中断主循环
                     # 点击过程报错不要中断主循环
@@ -924,14 +811,11 @@ class VfsPlugin(IVSPlg):
         """
         """
         self._log("Starting booking process...")
         self._log("Starting booking process...")
         
         
-        # 1. 准备数据
         user_email = user_inputs.get('email')
         user_email = user_inputs.get('email')
-        # 生成别名邮箱 (防止邮箱被 VFS 黑名单)
         user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
         user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
         
         
         res = VSBookResult()
         res = VSBookResult()
         app_type = slot_info.apt_type
         app_type = slot_info.apt_type
-        # 如果没有 earliest_date,默认从今天开始
         from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         
         
         apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
         apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
@@ -939,14 +823,11 @@ class VfsPlugin(IVSPlg):
         if not apt_config:
         if not apt_config:
             raise NotFoundError(message="Book: Config missing for this routing key.")
             raise NotFoundError(message="Book: Config missing for this routing key.")
 
 
-        # 确保配置已加载 (SubCategory 等)
         self._fetch_configurations(apt_config)
         self._fetch_configurations(apt_config)
 
 
         sub_cc = apt_config.get("subcategory_code")
         sub_cc = apt_config.get("subcategory_code")
         sub_conf = self.subcategory_conf.get(sub_cc, {})
         sub_conf = self.subcategory_conf.get(sub_cc, {})
 
 
-        # 3. OCR 识别 / 文档上传 (如果需要)
-        # 上传结果存入 user_inputs 供后续使用
         ocr_enabled = sub_conf.get("isOCREnable", False)
         ocr_enabled = sub_conf.get("isOCREnable", False)
         if ocr_enabled:
         if ocr_enabled:
             self._log("OCR Enabled, uploading documents...")
             self._log("OCR Enabled, uploading documents...")
@@ -957,7 +838,6 @@ class VfsPlugin(IVSPlg):
 
 
         enable_reference_number = sub_conf.get("enableReferenceNumber", False)
         enable_reference_number = sub_conf.get("enableReferenceNumber", False)
 
 
-        # 4. 添加申请人 (核心步骤 1)
         final_urn = None
         final_urn = None
         is_waitlist = (slot_info.availability_status == AvailabilityStatus.Waitlist)
         is_waitlist = (slot_info.availability_status == AvailabilityStatus.Waitlist)
         
         
@@ -966,23 +846,23 @@ class VfsPlugin(IVSPlg):
             time.sleep(20)
             time.sleep(20)
             self.booking_wait_applied = True
             self.booking_wait_applied = True
         
         
-        # 重试机制:添加申请人有时候会因为并发冲突失败
-        MAX_RETRY = 4
+        MAX_RETRY = 2
         for i in range(MAX_RETRY):
         for i in range(MAX_RETRY):
             try:
             try:
                 final_urn = self._add_primary_applicant(apt_config, user_inputs, is_waitlist, ocr_enabled, enable_reference_number)
                 final_urn = self._add_primary_applicant(apt_config, user_inputs, is_waitlist, ocr_enabled, enable_reference_number)
-                if final_urn:
-                    break
-            except Exception as e:
-                self._log(f"Add Applicant retry {i+1}/{MAX_RETRY}: {e}")
-            time.sleep(5.0)
+                break
+            except BizLogicError as e:
+                err_msg = str(e)
+                self._log(f"Add Applicant retry {i+1}/{MAX_RETRY}: {err_msg}")
+                if 'Capping has exceeded' in err_msg:
+                    raise e
+            time.sleep(10.0)
         
         
         if not final_urn:
         if not final_urn:
             raise BizLogicError(message="Failed to add primary applicant (Slot likely taken or API error)")
             raise BizLogicError(message="Failed to add primary applicant (Slot likely taken or API error)")
 
 
         self._log(f"Applicant Added. URN: {final_urn}")
         self._log(f"Applicant Added. URN: {final_urn}")
 
 
-        # 5. 申请人 OTP 验证 (核心步骤 2 - 视配置而定)
         otp_enabled = sub_conf.get("isApplicantOTPEnabled", False)
         otp_enabled = sub_conf.get("isApplicantOTPEnabled", False)
         if otp_enabled:
         if otp_enabled:
             self._log("Applicant OTP Required.")
             self._log("Applicant OTP Required.")
@@ -991,12 +871,10 @@ class VfsPlugin(IVSPlg):
             if not self._applicant_otp_send(apt_config, final_urn):
             if not self._applicant_otp_send(apt_config, final_urn):
                 raise BizLogicError(message='Applicant OTP send failed')
                 raise BizLogicError(message='Applicant OTP send failed')
             
             
-            # 复用之前的读邮件逻辑
             otp_code = self._read_otp_email(sent_at=sent_at)
             otp_code = self._read_otp_email(sent_at=sent_at)
             if not self._applicant_otp_verify(apt_config, final_urn, otp_code):
             if not self._applicant_otp_verify(apt_config, final_urn, otp_code):
                 raise BizLogicError(message='Applicant OTP verify failed')
                 raise BizLogicError(message='Applicant OTP verify failed')
 
 
-        # 6. Waitlist 模式直接返回
         if is_waitlist:
         if is_waitlist:
             if self._confirm_waitlist(apt_config, final_urn):
             if self._confirm_waitlist(apt_config, final_urn):
                 res.success = True
                 res.success = True
@@ -1006,11 +884,9 @@ class VfsPlugin(IVSPlg):
                 return res
                 return res
             raise BizLogicError(message='Confirm waitlist failed')
             raise BizLogicError(message='Confirm waitlist failed')
 
 
-        # 7. 寻找具体的时间槽 (核心步骤 3)
         expected_start = user_inputs.get("expected_start_date", "")
         expected_start = user_inputs.get("expected_start_date", "")
         expected_end = user_inputs.get("expected_end_date", "")
         expected_end = user_inputs.get("expected_end_date", "")
         
         
-        # 计算需要扫描的月份
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
         months = self._get_filtered_covered_months(expected_start, expected_end, from_date)
         self._log(f"Scanning months: {months} (Start looking from: {from_date})")
         self._log(f"Scanning months: {months} (Start looking from: {from_date})")
         
         
@@ -1024,40 +900,31 @@ class VfsPlugin(IVSPlg):
         
         
         for m_str in months:
         for m_str in months:
             self._log(f"Checking calendar for {m_str}...")
             self._log(f"Checking calendar for {m_str}...")
-            # 查询日历
             ads = self._query_slot_calendar(apt_config, final_urn, m_str)
             ads = self._query_slot_calendar(apt_config, final_urn, m_str)
             
             
-            # 去重
             new_ads = [d for d in ads if d not in all_ads]
             new_ads = [d for d in ads if d not in all_ads]
             all_ads.update(new_ads)
             all_ads.update(new_ads)
             
             
-            # 尝试选中一个日期
-            # 这里做一个简单循环,如果选中日期没时间了,就换一个日期
             for _ in range(3):
             for _ in range(3):
                 avail_candidates = [d for d in list(all_ads) if d not in forbidden_dates]
                 avail_candidates = [d for d in list(all_ads) if d not in forbidden_dates]
-                # 根据用户期望过滤
                 sel_dates = self._filter_dates(avail_candidates, expected_start, expected_end)
                 sel_dates = self._filter_dates(avail_candidates, expected_start, expected_end)
                 
                 
                 if not sel_dates:
                 if not sel_dates:
-                    break # 当前月没有符合要求的日期,去下一个月
-                
-                tmp_date = sel_dates[0] # 取第一个(通常 _filter_dates 里已经 shuffle 过了)
-                forbidden_dates.add(tmp_date) # 标记为已尝试
+                    break
                 
                 
-                # 关键:Audit Log (锁定日期)
-                # VFS 要求在查 timeslot 之前必须先发这个请求
+                tmp_date = sel_dates[0]
+                forbidden_dates.add(tmp_date)
+
                 if not self._saveuseractionaudit(apt_config, final_urn, tmp_date):
                 if not self._saveuseractionaudit(apt_config, final_urn, tmp_date):
                     self._log(f"Audit failed for {tmp_date}, skipping...")
                     self._log(f"Audit failed for {tmp_date}, skipping...")
                     time.sleep(1)
                     time.sleep(1)
                     continue
                     continue
                 
                 
-                # 查询具体时间
                 ats = self._query_slot_time(apt_config, final_urn, tmp_date)
                 ats = self._query_slot_time(apt_config, final_urn, tmp_date)
                 if not ats:
                 if not ats:
                     self._log(f"No timeslots for {tmp_date}")
                     self._log(f"No timeslots for {tmp_date}")
                     continue
                     continue
                 
                 
-                # 随机选一个时间
                 sel_tm = random.choice(ats)
                 sel_tm = random.choice(ats)
                 
                 
                 selected_slot_id = sel_tm.get("allocationId")
                 selected_slot_id = sel_tm.get("allocationId")
@@ -1077,11 +944,9 @@ class VfsPlugin(IVSPlg):
 
 
         self._log(f"Slot Selected: {selected_slot_date} {selected_slot_time_range} (ID: {selected_slot_id})")
         self._log(f"Slot Selected: {selected_slot_date} {selected_slot_time_range} (ID: {selected_slot_id})")
 
 
-        # 8. 服务与费用 (核心步骤 4)
         self._submit_no_addition_service(final_urn)
         self._submit_no_addition_service(final_urn)
         amount, currency = self._query_fee(apt_config, final_urn)
         amount, currency = self._query_fee(apt_config, final_urn)
         
         
-        # 9. 最终提交
         self._log("Submitting schedule...")
         self._log("Submitting schedule...")
         schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
         schedule_res = self._schedule(apt_config, final_urn, amount, currency, selected_slot_id)
         
         
@@ -1090,7 +955,6 @@ class VfsPlugin(IVSPlg):
             res.success = False
             res.success = False
             return res
             return res
              
              
-        # 10. 构造成功结果
         res.success = True
         res.success = True
         res.account = self.config.account.username
         res.account = self.config.account.username
         res.book_date = selected_slot_date
         res.book_date = selected_slot_date
@@ -1099,7 +963,6 @@ class VfsPlugin(IVSPlg):
         res.fee_amount = int(amount * 100)
         res.fee_amount = int(amount * 100)
         res.fee_currency = currency
         res.fee_currency = currency
         
         
-        # 11. 处理支付链接
         if schedule_res.get("IsPaymentRequired", False):
         if schedule_res.get("IsPaymentRequired", False):
             payload = schedule_res.get("payLoad", "")
             payload = schedule_res.get("payLoad", "")
             if payload:
             if payload:
@@ -1131,7 +994,6 @@ class VfsPlugin(IVSPlg):
         if not passport_url:
         if not passport_url:
             raise NotFoundError(message="Missing passport_image_url")
             raise NotFoundError(message="Missing passport_image_url")
 
 
-        # 下载图片 (不走代理或走系统代理,不使用 DrissionPage,因为是外部链接)
         try:
         try:
             img_resp = standard_requests.get(passport_url, timeout=30)
             img_resp = standard_requests.get(passport_url, timeout=30)
             if img_resp.status_code != 200:
             if img_resp.status_code != 200:
@@ -1141,7 +1003,6 @@ class VfsPlugin(IVSPlg):
             raise BizLogicError(message=f"Image download error: {e}")
             raise BizLogicError(message=f"Image download error: {e}")
   
   
         headers = self._get_common_headers(with_auth=True)
         headers = self._get_common_headers(with_auth=True)
-        # DrissionPage fetch 不需要显式 content-type application/json,json_data会自动处理
         
         
         data = {
         data = {
             "missioncode": self.free_config.get("mission_code"),
             "missioncode": self.free_config.get("mission_code"),
@@ -1157,7 +1018,6 @@ class VfsPlugin(IVSPlg):
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
         resp = self._perform_request("POST", url, headers=headers, json_data=data)
         result = resp.json()
         result = resp.json()
         
         
-        # 补充返回数据供后续使用
         result["passportImageFilename"] = "passport_img.jpg"
         result["passportImageFilename"] = "passport_img.jpg"
         result["passportImageFileBytes"] = b64_str
         result["passportImageFileBytes"] = b64_str
         return result
         return result
@@ -1277,7 +1137,11 @@ class VfsPlugin(IVSPlg):
         }
         }
 
 
         resp = self._perform_request("POST", url, headers=headers, json_data=payload)
         resp = self._perform_request("POST", url, headers=headers, json_data=payload)
-        return resp.json().get("urn")
+        urn = resp.json().get("urn")
+        if not urn:
+            err_msg = resp.json().get('error')
+            raise BizLogicError(message=str(err_msg))
+        return urn
     
     
     def _applicant_otp_send(self, apt_config, urn) -> bool:
     def _applicant_otp_send(self, apt_config, urn) -> bool:
         url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
         url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
@@ -1392,7 +1256,6 @@ class VfsPlugin(IVSPlg):
             "urn": urn,
             "urn": urn,
             "applicants": []
             "applicants": []
         }
         }
-        # 只要不报错即可
         self._perform_request("POST", url, headers=headers, json_data=data)
         self._perform_request("POST", url, headers=headers, json_data=data)
 
 
     def _query_fee(self, apt_config, urn) -> Tuple[float, str]:
     def _query_fee(self, apt_config, urn) -> Tuple[float, str]:
@@ -1442,7 +1305,6 @@ class VfsPlugin(IVSPlg):
         """
         """
         解析支付重定向 URL (DrissionPage 新标签页版)
         解析支付重定向 URL (DrissionPage 新标签页版)
         """
         """
-        # 初始 URL,通常是一个 Redirect 接口
         start_url = f"https://online.vfsglobal.com/PG-Component/Payment/PayRequest?payLoad={payload}"
         start_url = f"https://online.vfsglobal.com/PG-Component/Payment/PayRequest?payLoad={payload}"
         final_url = ""
         final_url = ""
         
         
@@ -1458,7 +1320,6 @@ class VfsPlugin(IVSPlg):
             final_url = pay_tab.url
             final_url = pay_tab.url
             self._log(f"Payment URL resolved: {final_url}")
             self._log(f"Payment URL resolved: {final_url}")
             
             
-            # 关闭标签页
             pay_tab.close()
             pay_tab.close()
             
             
         except Exception as e:
         except Exception as e:

+ 1 - 0
tls_registration_bot.py

@@ -32,6 +32,7 @@ def generate_random_account_detail() -> Dict:
         "location": "London",
         "location": "London",
         "visa_type": "Short stay (<90 days) - Tourism",
         "visa_type": "Short stay (<90 days) - Tourism",
         "travel_purpose": "Tourism / Private visit",
         "travel_purpose": "Tourism / Private visit",
+        # FRA1LO20260411910
         "application_form_id": "FRA" + "".join(str(random.randint(0, 9)) for _ in range(14)),
         "application_form_id": "FRA" + "".join(str(random.randint(0, 9)) for _ in range(14)),
         "last_name": "Smith",
         "last_name": "Smith",
         "first_name": "James",
         "first_name": "James",

+ 4 - 4
vs_plg.py

@@ -55,21 +55,21 @@ class IVSPlg(ABC):
     @abstractmethod
     @abstractmethod
     def keep_alive(self):
     def keep_alive(self):
         """
         """
-        @brief 会话保活
+        @brief 会话保活, 该函数不允许抛异常
         """
         """
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         """
         """
-        @brief 健康检查,用于检测 API 服务是否正常
-        @return true 表示健康状态良好false 表示存在问题
+        @brief 健康检查,用于检测 API 服务是否正常, 该函数不允许抛异常
+        @return true 表示健康状态良好,false 表示存在问题
         """
         """
         pass
         pass
     
     
     @abstractmethod
     @abstractmethod
     def set_log(self, logger: Callable[[str], None]) -> None:
     def set_log(self, logger: Callable[[str], None]) -> None:
         """
         """
-        @brief 设置日志输出工具
+        @brief 设置日志输出工具, 该函数不允许抛异常
         """
         """
         pass
         pass