jerry преди 3 месеца
родител
ревизия
324903836e
променени са 13 файла, в които са добавени 772 реда и са изтрити 368 реда
  1. 2 1
      .gitignore
  2. 8 0
      config/accounts.json
  3. 62 13
      config/groups.json
  4. 123 69
      config/proxies.json
  5. 22 6
      gco.py
  6. 231 62
      plugins/bls_plugin.py
  7. 12 12
      plugins/de_plugin.py
  8. 167 54
      plugins/ita_plugin.py
  9. 12 7
      plugins/tls_plugin.py
  10. 21 1
      plugins/tls_plugin2.py
  11. 42 69
      plugins/vfs_plugin.py
  12. 15 3
      plugins/vfs_plugin2.py
  13. 55 71
      toolkit/vs_cloud_api.py

+ 2 - 1
.gitignore

@@ -3,4 +3,5 @@ debug_pages
 logs
 .DS_Store
 *.jpg
-node_modules*
+node_modules*
+temp_browser_data

+ 8 - 0
config/accounts.json

@@ -504,5 +504,13 @@
             "password": "dx4ua@!.X.i8Xn8",
             "lock_until": 0
         }
+    ],
+    "ie_it": [
+        {
+            "id": 0,
+            "username":"Khandpur1@gmail-app.com",
+            "password": "Visafly@111",
+            "lock_until": 0
+        }
     ]
 }

+ 62 - 13
config/groups.json

@@ -6,7 +6,7 @@
         "need_account": true,
         "local_account_pool": "ie_nl",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -35,6 +35,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/nld/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.nl.tourist",
                     "center_name": "Netherlands Visa Application Center - Dublin",
                     "city": "Dublin",
@@ -57,7 +58,7 @@
         "need_account": true,
         "local_account_pool": "sg_fr",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -87,6 +88,7 @@
             "website": "https://visa.vfsglobal.com/sgp/en/fra/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.sin.fr.tourist",
                     "center_name": "France Visa Application Center, Singapore",
                     "city": "Singapore",
@@ -109,7 +111,7 @@
         "need_account": true,
         "local_account_pool": "au_fr",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -139,6 +141,7 @@
             "website": "https://visa.vfsglobal.com/aus/en/fra/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.syd.fr.tourist",
                     "center_name": "France Visa Application Center - Sydney",
                     "city": "Sydney",
@@ -152,6 +155,7 @@
                     "subcategory_code": "ShortStaySchengenVisa"
                 },
                 {
+                    "weight": 10,
                     "routing_key": "slot.mel.fr.tourist",
                     "center_name": "France Visa Application Center - Melbourne",
                     "city": "Melbourne",
@@ -174,7 +178,7 @@
         "need_account": true,
         "local_account_pool": "gb_it",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -204,6 +208,7 @@
             "language": "en",
             "appointment_types": [
                 {
+                    "weight": 80,
                     "routing_key": "slot.lon.it.tourist",
                     "center_name": "Italy Visa Application Centre, London",
                     "city": "London",
@@ -217,6 +222,7 @@
                     "subcategory_code": "TBE"
                 },
                 {
+                    "weight": 20,
                     "routing_key": "slot.man.it.tourist",
                     "center_name": "Italy Visa Application Centre, Manchester",
                     "city": "Manchester",
@@ -239,7 +245,7 @@
         "need_account": true,
         "local_account_pool": "gb_nl",
         "need_proxy": true,
-        "proxy_pool": "iproyal",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -269,6 +275,7 @@
             "website": "https://visa.vfsglobal.com/gbr/en/nld/login",
             "appointment_types": [
                 {
+                    "weight": 90,
                     "routing_key": "slot.lon.nl.tourist",
                     "center_name": "Netherlands Visa application centre - London",
                     "city": "London",
@@ -282,6 +289,7 @@
                     "subcategory_code": "TA"
                 },
                 {
+                    "weight": 10,
                     "routing_key": "slot.man.it.tourist",
                     "center_name": "Netherlands Visa application centre - Manchester",
                     "city": "Manchester",
@@ -304,7 +312,7 @@
         "need_account": true,
         "local_account_pool": "gb_no",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -334,6 +342,7 @@
             "website": "https://visa.vfsglobal.com/gbr/en/nor/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.lon.no.tourist",
                     "center_name": "Norway Visa Application Centre, London",
                     "city": "London",
@@ -356,7 +365,7 @@
         "need_account": true,
         "local_account_pool": "ie_at",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -386,6 +395,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/aut/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.at.tourist",
                     "center_name": "Austria / Switzerland / Liechtenstein/ Slovenia Visa Application Center, Dublin",
                     "city": "Dublin",
@@ -408,7 +418,7 @@
         "need_account": true,
         "local_account_pool": "ie_dk",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -438,6 +448,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/dnk/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.dk.tourist",
                     "center_name": "Denmark Visa Application Center, Dublin ",
                     "city": "Dublin",
@@ -460,7 +471,7 @@
         "need_account": true,
         "local_account_pool": "ie_fi",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -490,6 +501,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/fin/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.fi.tourist",
                     "center_name": "Application Centre, Dublin",
                     "city": "Dublin",
@@ -512,7 +524,7 @@
         "need_account": true,
         "local_account_pool": "ie_hu",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -542,6 +554,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/hun/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.hu.tourist",
                     "center_name": "Ireland Visa Application Center,Dublin",
                     "city": "Dublin",
@@ -564,7 +577,7 @@
         "need_account": true,
         "local_account_pool": "ie_is",
         "need_proxy": true,
-        "proxy_pool": "proxy_cheap",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "",
@@ -594,6 +607,7 @@
             "website": "https://visa.vfsglobal.com/irl/en/isl/login",
             "appointment_types": [
                 {
+                    "weight": 10,
                     "routing_key": "slot.dub.is.tourist",
                     "center_name": "Iceland Visa Application Center- Dublin",
                     "city": "Dublin",
@@ -706,7 +720,7 @@
         "need_account": true,
         "local_account_pool": "gb_fr",
         "need_proxy": true,
-        "proxy_pool": "iproyal",
+        "proxy_pool": "isp_proxy",
         "target_instances": 1,
         "account_login_interval": 30,
         "order_account_routing": "auto.slot.lon.fr.tourist",
@@ -773,7 +787,7 @@
         },
         "free_config": {
             "base_url": "https://ie-appointment.visametric.com",
-            "local_service_url": "http://127.0.0.1:8085",
+            "ocr_model": "data/ctc.pth",
             "consularid": 1,
             "city": "Dublin",
             "country": "Germany",
@@ -781,5 +795,40 @@
             "routing_key": "slot.dub.de.tourist",
             "website": "https://ie-appointment.visametric.com/en"
         }
+    },
+    {
+        "identifier": "PRENOTAMI_IE_IT",
+        "debug": false,
+        "enable": true,
+        "need_account": true,
+        "local_account_pool": "ie_it",
+        "need_proxy": true,
+        "proxy_pool": "iproyal",
+        "target_instances": 1,
+        "account_login_interval": 30,
+        "order_account_routing": "",
+        "order_account_online_limit": 0,
+        "account_bind_applicant": false,
+        "session_max_life": 15,
+        "query_wait": {
+            "mode": "Random",
+            "fixed_wait": 10,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "ita_plugin",
+            "plugin_bin": "ita_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "capsolver_key": "03db1d1ff2f4a33e84ef1da99bd83336bed3710153525",
+            "city": "Dublin",
+            "country": "Italy",
+            "visa_type": "Tourist",
+            "routing_key": "slot.dub.it.tourist",
+            "website": "https://prenotami.esteri.it/Home"
+        }
     }
 ]

+ 123 - 69
config/proxies.json

@@ -19,150 +19,204 @@
             "username": "user-visafly_zFNdf"
         }
     ],
-    "dc": [
+    "isp_proxy": [
         {
             "id": 100001,
-            "ip": "157.22.72.100",
+            "ip": "95.135.130.175",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "hmuROCk1FDebCnL",
+            "port": 46247,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "GB6o2vBrXFjz4ya"
         },
         {
             "id": 100002,
-            "ip": "193.202.9.27",
+            "ip": "95.135.130.29",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "WmqFTSvRvtxChIT",
+            "port": 43740,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "JUcjydi0HKZzWC6"
         },
         {
             "id": 100003,
-            "ip": "45.80.105.101",
+            "ip": "95.135.130.105",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "a9udkCOGYZKGkLS",
+            "port": 47342,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "1mtj2c6xoLfrOLm"
         },
         {
             "id": 100004,
-            "ip": "170.168.240.243",
+            "ip": "95.135.130.157",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "q1UNK1gmiQdxJ1g",
+            "port": 42290,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "alxuf86deeI898d"
         },
         {
             "id": 100005,
-            "ip": "45.140.206.239",
+            "ip": "95.135.130.167",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "bmKmauMV5CuOCrh",
+            "port": 48333,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "gPaIdSyKsp2TnQ1"
         },
         {
             "id": 100006,
-            "ip": "157.22.125.162",
+            "ip": "95.135.130.181",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "0HU9RGLRTNNwecf",
+            "port": 46253,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "XpSiGAiz3zwAyi1"
         },
         {
             "id": 100007,
-            "ip": "193.233.89.216",
+            "ip": "95.135.130.192",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "anYmNO4luxcm22m",
+            "port": 41255,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "xRGDPswifGmmbog"
         },
         {
             "id": 100008,
-            "ip": "157.22.74.157",
+            "ip": "95.135.130.203",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "qXEZWLa74q8Awdx",
+            "port": 49012,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "jHATN4mVM6kcltO"
         },
         {
             "id": 100009,
-            "ip": "170.168.174.129",
+            "ip": "95.135.130.206",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "MnL8pmNAdpLo0h7",
+            "port": 47504,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "aLoTyl9YbSvdxbD"
         },
         {
             "id": 100010,
-            "ip": "212.119.43.241",
+            "ip": "95.135.130.33",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "slxIVWHqzZpkSPY",
+            "port": 44720,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "EnnPVteJalQvk7E"
         },
         {
             "id": 100011,
-            "ip": "45.80.104.167",
+            "ip": "95.135.130.53",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "5fS8EMqdto9JnO9",
+            "port": 44764,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "QeLVYhf0Pbz5BEe"
         },
         {
             "id": 100012,
-            "ip": "45.148.233.17",
+            "ip": "95.135.130.66",
+            "lock_until": 0,
+            "password": "4HzLag9JYiPTZ5J",
+            "port": 46470,
+            "scheme": "http",
+            "username": "c1zHamB7LAOeNdb"
+        },
+        {
+            "id": 100021,
+            "ip": "89.33.195.129",
+            "lock_until": 0,
+            "password": "d160bc0854",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae212b29a2a"
+        },
+        {
+            "id": 100022,
+            "ip": "89.33.195.64",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "d160bc0854",
+            "port": 12323,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "14ae212b29a2a"
         },
         {
-            "id": 100013,
-            "ip": "185.61.223.103",
+            "id": 100023,
+            "ip": "89.33.195.93",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "d160bc0854",
+            "port": 12323,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "14ae212b29a2a"
         },
         {
-            "id": 100014,
-            "ip": "157.22.18.89",
+            "id": 100024,
+            "ip": "89.33.195.42",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "d160bc0854",
+            "port": 12323,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "14ae212b29a2a"
         },
         {
-            "id": 100015,
-            "ip": "212.119.45.239",
+            "id": 100025,
+            "ip": "95.170.29.126",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "d160bc0854",
+            "port": 12323,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "14ae212b29a2a"
         },
         {
-            "id": 100016,
-            "ip": "5.181.170.159",
+            "id": 100026,
+            "ip": "91.193.255.166",
             "lock_until": 0,
-            "password": "o4bQTdjF",
-            "port": 8080,
+            "password": "d160bc0854",
+            "port": 12323,
             "scheme": "http",
-            "username": "mix306YSSTWFF"
+            "username": "14ae212b29a2a"
+        },
+        {
+            "id": 100027,
+            "ip": "91.193.255.60",
+            "lock_until": 0,
+            "password": "d160bc0854",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae212b29a2a"
+        },
+        {
+            "id": 100028,
+            "ip": "91.193.255.210",
+            "lock_until": 0,
+            "password": "d160bc0854",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae212b29a2a"
+        },
+        {
+            "id": 100029,
+            "ip": "91.193.255.149",
+            "lock_until": 0,
+            "password": "d160bc0854",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae212b29a2a"
+        },
+        {
+            "id": 100030,
+            "ip": "91.193.255.245",
+            "lock_until": 0,
+            "password": "d160bc0854",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae212b29a2a"
         }
     ],
     "iproyal": [

+ 22 - 6
gco.py

@@ -190,17 +190,33 @@ class GCO:
                     
                     task.next_run = time.time() + interval
 
-            # === [批量冷却逻辑] ===
-            # 如果触发了批量抢票,所有人都需要进入冷却,防止接口被瞬间打死
+            # === [修正后的批量冷却逻辑] ===
+            # 抢票结束后,必须严格按照每个账号配置的频率进入冷却,防止集体429
             if batch_booking_triggered:
-                cooldown = rng.randint(30, 60) # 全员冷却 30-60 秒
-                self._log(f"Batch booking finished. All workers entering cooldown for {cooldown}s.")
+                self._log(f"Batch booking finished. Resetting wait times based on configurations.")
                 
+                # 重新获取当前时间(因为抢票过程消耗了时间)
                 now_ts = time.time()
+                
                 with self.m_lock:
-                    # 更新所有任务的下次运行时间
                     for t in self.m_tasks:
-                        t.next_run = now_ts + cooldown
+                        # 重新读取该任务的配置
+                        interval = 30 # 默认兜底
+                        mode = t.qw_cfg.mode
+                        
+                        if mode == QueryWaitMode.Loop:
+                            # 即使是 Loop 模式,在大规模抢票后建议给一个微小的缓冲(如1秒),避免死循环导致 CPU 飙升
+                            # 如果你的逻辑允许立刻重试,这里可以是 0
+                            interval = 1 
+                        elif mode == QueryWaitMode.Fixed:
+                            interval = t.qw_cfg.fixed_wait
+                        elif mode == QueryWaitMode.Random:
+                            interval = rng.randint(t.qw_cfg.random_min, t.qw_cfg.random_max)
+                        
+                        # 设置下次运行时间
+                        t.next_run = now_ts + interval
+                
+                self._log(f"All workers cooldown reset. Resuming monitor loop.")
 
             # 清理不健康实例
             with self.m_lock:

+ 231 - 62
plugins/bls_plugin.py

@@ -14,6 +14,9 @@ from typing import Dict, List, Optional, Any, Callable
 from curl_cffi import requests, const
 from bs4 import BeautifulSoup
 
+# DrissionPage 核心
+from DrissionPage import ChromiumPage, ChromiumOptions
+
 from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.backends import default_backend
@@ -21,7 +24,8 @@ from cryptography.hazmat.backends import default_backend
 # 框架依赖
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, DateAvailability, TimeSlot, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
-from toolkit.vs_cloud_api import VSCloudApi 
+from toolkit.vs_cloud_api import VSCloudApi
+from toolkit.ocr_engine import PyTorchEngine
 
 class BlsPlugin(IVSPlg):
     """
@@ -40,6 +44,19 @@ class BlsPlugin(IVSPlg):
         self.book_params: Dict = {} 
         self.is_healthy: bool = True
         
+        # 浏览器实例
+        self.page: Optional[ChromiumPage] = None
+        
+        # --- [核心修改] 并发隔离与资源管理 ---
+        # 生成唯一实例 ID
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.root_workspace = os.path.abspath(os.path.join("temp_browser_data", f"{self.group_id}_{self.instance_id}"))
+        # 定义子目录:代理插件目录 & 浏览器用户数据目录
+        self.user_data_path = os.path.join(self.root_workspace, "user_data")
+        
+        # 字符识别引擎
+        self.ocr_engine = Optional[PyTorchEngine] = None
+        
         # OCR 服务地址默认值
         self.local_service_url: str = ""
         self.session_create_time: float = 0
@@ -49,6 +66,12 @@ class BlsPlugin(IVSPlg):
     
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
+        
+    def _log(self, message):
+        if self.logger:
+            self.logger(f'[TlsPlugin] [{self.group_id}] {message}')
+        else:
+            print(f'[TlsPlugin] [{self.group_id}] {message}')
 
     def set_config(self, config: VSPlgConfig):
         self.config = config
@@ -66,11 +89,58 @@ class BlsPlugin(IVSPlg):
             current_time = time.time()
             elapsed_time = current_time - self.session_create_time
             if elapsed_time > self.config.session_max_life * 60:
-                self._log(f"Session Life ({int(elapsed_time)}s) out of max life limit ({self.config.session_max_life * 60}s), mark as unhealth session")
+                self._log(f"Session expired.")
                 return False
         return True
 
     def create_session(self):
+        self._log(f"Initializing Session (ID: {self.instance_id})...")
+        co = ChromiumOptions()
+        # -------------------------------------------------------------
+        # [核心修复] 解决 'not enough values to unpack'
+        # -------------------------------------------------------------
+        # 1. 不要用 co.auto_port(),因为它依赖解析 stdout,会被 DBus 报错干扰
+        # 2. 我们手动随机生成一个端口
+        import random
+        import socket
+        
+        def get_free_port():
+            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+                s.bind(('', 0))
+                return s.getsockname()[1]
+        
+        debug_port = get_free_port()
+        self._log(f"Assigned Debug Port: {debug_port}")
+        
+        # 3. 强制指定端口,DrissionPage 就会直接连接,不再解析日志
+        co.set_local_port(debug_port)
+        
+        # --- [关键配置] 设置独立的用户数据目录 ---
+        # 这样每个实例的 Cache, Cookies, LocalStorage 都是完全隔离的
+        # 同时也防止了多进程争抢同一个 Default 文件夹导致的崩溃
+        co.set_user_data_path(self.user_data_path)
+        
+        # --- 1. 指定浏览器路径 (适配 Docker) ---
+        chrome_path = os.getenv("CHROME_BIN")
+        if chrome_path and os.path.exists(chrome_path):
+            co.set_paths(browser_path=chrome_path)
+            
+        co.headless(False)
+        co.set_argument('--no-sandbox')
+        co.set_argument('--disable-gpu')
+        # Docker 默认 /dev/shm 只有 64MB,Chromium 很容易爆内存崩溃
+        co.set_argument('--disable-dev-shm-usage')
+        co.set_argument('--window-size=1920,1080')
+        co.set_argument('--disable-blink-features=AutomationControlled')
+
+        try:
+            self.page = ChromiumPage(co)
+        except Exception as e:
+            self._log(f"Session Create Error: {e}")
+            self.cleanup()
+            raise e
+        
+        self.ocr_engine = PyTorchEngine(self.free_config.get('ocr_model'))
         self.session = requests.Session(
             proxy=self._get_proxy_url(),
             impersonate="chrome124",
@@ -86,12 +156,7 @@ class BlsPlugin(IVSPlg):
         # 1.1 获取登录页 & 解析参数
         login_url = f"https://{domain}/Global/account/login"
         
-        headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
-            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
-        }
-        
-        resp = self._perform_request('GET', login_url, headers=headers)
+        resp = self._perform_request('GET', login_url)
         if self.config.debug:
             self._save_debug_html(resp.text, prefix="Bls_Login_Page")
         soup = BeautifulSoup(resp.text, 'html.parser')
@@ -139,7 +204,6 @@ class BlsPlugin(IVSPlg):
         # 2.1 签证类型验证
         url_vtv = f"https://{domain}/Global/bls/visatypeverification"
         headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         resp = self._perform_request('GET', url_vtv, headers=headers)
@@ -228,7 +292,6 @@ class BlsPlugin(IVSPlg):
         domain = self.free_config.get("domain")
         
         headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         # 3.1 获取 Manage Page (为了 Token 和 JS 变量)
@@ -345,10 +408,6 @@ class BlsPlugin(IVSPlg):
         self._log(f"Book Success. Liveness URL: {res.payment_link}")
         return res
     
-    def _log(self, message):
-        if self.logger:
-            self.logger(f'[BlsPlugin] [{self.group_id}] {message}')
-    
     def _get_proxy_url(self):
             # 构造代理
         proxy_url = ""
@@ -392,6 +451,62 @@ class BlsPlugin(IVSPlg):
             raise RateLimiteddError()
         else:
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
+        
+    def _extract_captcha_data(self, tmp_file):
+        # 1. 加载文件
+        html_file_path = Path(tmp_file).resolve()
+        self.page.get(f'file://{html_file_path}')
+
+        # 2. 定位主容器 (作为后续查找的基准,减少全局扫描)
+        main_div = self.page.ele('#captcha-main-div', timeout=5)
+        if not main_div:
+            raise BizLogicError(message='Captcha main container not found')
+
+        # --- 3. 提取提示数字 ---
+        # 假设结构是 main -> div -> div[1] (header)
+        # 使用相对 XPath 定位 header 区域
+        header_ele = main_div.ele('xpath:./div/div[1]')
+        caption_text = ""
+        
+        if header_ele:
+            # 遍历子元素寻找可见的提示语
+            for child in header_ele.children():
+                # 这里的 is_displayed 检查是否有大小,is_covered 检查是否被遮挡
+                if child.states.is_displayed and not child.states.is_covered:
+                    caption_text = child.text
+                    if caption_text: # 找到文本就跳出
+                        break
+        
+        # 安全提取数字
+        number_match = re.search(r'\d+', caption_text)
+        if not number_match:
+            # 如果没找到数字,返回错误或特定的 status
+            raise BizLogicError(message="No number found in caption")
+        
+        number = number_match.group()
+
+        # --- 4. 提取图片 ID ---
+        images_ids = []
+        
+        # 优化策略:直接查找所有 class 为 captcha-img 的图片元素
+        # 语法: tag:img @@ class:captcha-img
+        all_imgs = main_div.eles('tag:img@@class:captcha-img')
+        
+        for img in all_imgs:
+            # 1. 检查可见性 (有尺寸且未被遮挡)
+            if img.states.is_displayed and not img.states.is_covered:
+                # 2. 检查 src 属性
+                src = img.attr('src')
+                if src and src.startswith('data:image'):
+                    # 3. 获取父级元素的 ID (根据原逻辑,ID 在 img 的父级容器上)
+                    parent_id = img.parent().attr('id')
+                    if parent_id:
+                        images_ids.append(parent_id)
+        data = {
+            "number": number,
+            "image_ids": images_ids,
+        }
+        return data
 
     def _solve_bls_captcha(self, data='') -> Optional[str]:
         """
@@ -399,29 +514,22 @@ class BlsPlugin(IVSPlg):
         """
         domain = self.free_config.get("domain")
         url = f"https://{domain}/Global/NewCaptcha/GenerateCaptcha"
-        if data: url = f"https://{domain}/Global/CaptchaPublic/GenerateCaptcha?data={data}"
-        headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
-            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
-        }
+        if data:
+            url = f"https://{domain}/Global/CaptchaPublic/GenerateCaptcha?data={data}"
         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)    
         
+        tmpfile = os.path.join(self.root_workspace, "tmp.html")
+        with open(tmpfile, 'wb') as tfp:
+            tfp.write(resp.text)
+        
         soup = BeautifulSoup(resp.text, 'html.parser')
-        resp = requests.post(
-            f'{self.local_service_url}/browser/visable_captchas', 
-            data=resp.text, 
-            headers={"Content-Type": "text/plain"},
-            timeout=10
-        )
-        result = resp.json()
-        if result.get('status') != 'success':
-            raise BizLogicError(message='Broswer task failed')
+        extract_data = self._extract_captcha_data(tmpfile)
         
-        numbers = result['data']['number']
-        image_ids = result['data']['image_ids']
+        numbers = extract_data['number']
+        image_ids = extract_data['image_ids']
         selected_ids = []
         for sid in image_ids: 
             div = soup.find("div", id=sid)
@@ -429,20 +537,12 @@ class BlsPlugin(IVSPlg):
             src = img.get("src")
             base64_data = src.split("base64,", 1)[1]
             img_bytes = base64.b64decode(base64_data)
-            ocr_resp = requests.post(
-                f'{self.local_service_url}/predict/bls?model=pytorch', 
-                data=img_bytes, 
-                headers={"Content-Type": "application/octet-stream"},
-                timeout=5
-            )
-            if ocr_resp.status_code == 200:
-                res_json = ocr_resp.json()
-                ocr_res = res_json.get('data', '').replace('$', '')[:3]
-                self._log(f'ocr captcha id={sid} result={ocr_res}, target={numbers}')
-                if ocr_res == numbers:
-                    selected_ids.append(sid)
-            else:
-                raise BizLogicError(message='Captcha server response error')
+            
+            ocr_output = self.ocr_engine.inference_bytes(img_bytes)
+            ocr_res = ocr_output.replace('$', '')[:3]
+            self._log(f'ocr captcha id={sid} result={ocr_res}, target={numbers}')
+            if ocr_res == numbers:
+                selected_ids.append(sid)
         if not selected_ids:
             raise BizLogicError(message='Captcha selected ids is empty')
         
@@ -488,6 +588,32 @@ class BlsPlugin(IVSPlg):
             match = re.search(pattern, html)
             if match: return match.group(1)
         return ""
+    
+    def _find_id_by_label(self, label_text, input_index=1):
+        """
+        根据 Label 文本查找对应 Input 的 ID 数字
+        :param label_text: Label 包含的文本
+        :param input_index: Input 也是 Label 后的第几个 input (默认第1个)
+        :return: ID (int) or None
+        """
+        # 优化定位:直接查找包含特定文本的 label 标签
+        # syntax: 标签名:label @@ text:文本内容
+        labels = self.page.eles(f'tag:label@@text:{label_text}', timeout=1)
+        
+        for label in labels:
+            # 检查元素是否可见 (has_rect)
+            if label.states.has_rect:
+                # 获取 label 后的指定 input 元素
+                target_input = label.after('tag:input', index=input_index)
+                
+                if target_input:
+                    eid = target_input.attr('id')
+                    if eid:
+                        # 使用正则提取数字,比 filter 更快且易读
+                        match = re.search(r'\d+', eid)
+                        if match:
+                            return int(match.group())
+        return None
 
     def _construct_visatype_payload(self, html: str, soup: BeautifulSoup) -> Optional[Dict]:
         """
@@ -522,21 +648,30 @@ class BlsPlugin(IVSPlg):
         subtype_value = None
         cat_value = None
         
-        resp = requests.post(
-            f'{self.local_service_url}/browser/visatype_visable', 
-            data=html, 
-            headers={"Content-Type": "text/plain"},
-            timeout=10
-        )
-        result = resp.json()
-        if result.get('status') != 'success':
-            raise BizLogicError(message='Broswer task failed')
-        
-        jur_id = result['data']['jur_id']
-        loc_id = result['data']['loc_id']
-        type_id = result['data']['type_id']
-        subtype_id = result['data']['subtype_id']
-        cat_id = result['data']['cat_id']
+        tmpfile = os.path.join(self.root_workspace, "tmp.html")
+        with open(tmpfile, 'wb') as tfp:
+            tfp.write(resp.text)
+        
+        # 3. 配置映射关系: { 结果字段名: (Label文本, Input索引) }
+        # 注意:Location 原代码中 index=2,其余默认为 1
+        field_config = {
+            "cat_id":     ("Appointment Category", 1),
+            "jur_id":     ("Jurisdiction", 1),
+            "loc_id":     ("Location", 2), 
+            "type_id":    ("Visa Type", 1),
+            "subtype_id": ("Visa Sub Type", 1),
+        }
+        
+        # 4. 循环提取
+        data = {}
+        for key, (text, idx) in field_config.items():
+            data[key] = _find_id_by_label(text, idx)
+        
+        jur_id = data['jur_id']
+        loc_id = data['loc_id']
+        type_id = data['type_id']
+        subtype_id = data['subtype_id']
+        cat_id = data['cat_id']
   
         jurisdiction_list = get_js_data("jurisdictionData")
         location_list = get_js_data("locationData")
@@ -652,7 +787,6 @@ class BlsPlugin(IVSPlg):
         """
         domain = self.free_config.get("domain")
         headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         # 1. 获取表单页面 (为了提取 JS 变量映射表)
@@ -802,7 +936,6 @@ class BlsPlugin(IVSPlg):
         
         # Headers 需要 Token
         headers = {
-            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0 Safari/537.36',
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
             "Referer": f"https://{domain}/{referer}",
             "X-Requested-With": "XMLHttpRequest",
@@ -924,4 +1057,40 @@ class BlsPlugin(IVSPlg):
         
         return VSCloudApi.Instance().create_http_session(
             sid, cookies_str, "", ua_str, proxy_str, page_url
-        )
+        )
+        
+    # --- 资源清理核心方法 ---
+    def cleanup(self):
+        """
+        销毁浏览器并彻底删除临时文件
+        """
+        # 1. 关闭浏览器
+        if self.page:
+            try:
+                self.page.quit() # 这会关闭 Chrome 进程
+            except Exception:
+                pass # 忽略已关闭的错误
+            self.page = None
+        
+        # 2. 删除文件
+        # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
+        if os.path.exists(self.root_workspace):
+            for _ in range(3):
+                try:
+                    time.sleep(0.2)
+                    shutil.rmtree(self.root_workspace, ignore_errors=True)
+                    break
+                except Exception as e:
+                    # 如果删除失败(通常是Windows文件占用),重试
+                    self._log(f"Cleanup retry: {e}")
+                    time.sleep(0.5)
+            
+            # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
+            if os.path.exists(self.root_workspace):
+                 self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
+        
+    def __del__(self):
+        """
+        析构函数:当对象被垃圾回收时自动调用
+        """
+        self.cleanup()

+ 12 - 12
plugins/de_plugin.py

@@ -44,7 +44,6 @@ class DePlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         self.logger = 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/124.0.0.0 Safari/537.36"
         
         # 状态
         self.is_healthy = True
@@ -285,7 +284,6 @@ class DePlugin(IVSPlg):
     def _get_headers(self) -> Dict[str, str]:
         """基础 Header"""
         return {
-            "User-Agent": self.user_agent,
             "Accept": "*/*",
             "Accept-Language": "en,zh-CN;q=0.9,zh;q=0.8",
             "Origin": self.base_url,
@@ -569,12 +567,17 @@ class DePlugin(IVSPlg):
         else:
             proxy_str = f"{p.ip}:{p.port}"
         # 2. 提交任务
-        task = VSCloudApi.Instance().submit_anticloudflare_task(proxy_str, website_url)
-        # 3. 等待结果
-        task_id = str(task['id'])
-        result = VSCloudApi.Instance().get_anticloudflare_result(task_id)
-        parsed = json.loads(result.get('result', '{}'))
-        cookies_list = parsed.get('cookies', [])
+        task_id = VSCloudApi.Instance().create_task(
+            command="AntiCloudflareTask",
+            args={
+                "proxy": proxy_str,
+                "websiteUrl": website_url
+            }
+        )
+        
+        result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
+        task_result = result_data.get("result", {}).get("token")
+        cookies_list = task_result.get('cookies', [])
         for cookie in cookies_list:
             if cookie['name'] in ['__cf_bm', 'cf_clearance']:
                 self.session.cookies.set(
@@ -583,8 +586,5 @@ class DePlugin(IVSPlg):
                     domain=cookie['domain'], 
                     path='/'
                 )
-        ua = parsed.get('userAgent')
-        if ua:
-            self.user_agent = ua
-            self.session.headers['User-Agent'] = ua
+        self.session.headers['User-Agent'] = task_result.get('userAgent')
         self._log("Cloudflare 5s challenge solved.")

+ 167 - 54
plugins/ita_plugin.py

@@ -184,25 +184,50 @@ class ItaPlugin(IVSPlg):
             self.page.ele('#login-email').input(self.config.account.username)
             self.page.ele('#login-password').input(self.config.account.password)
             
-            # 解决 ReCaptcha V2
-            self._handle_login_captcha()
+            # 4. [核心修改] 解决 ReCaptcha V3 Enterprise 并注入
+            # Prenotami 使用的是 Enterprise V3, Action = 'LOGIN'
+            self._solve_and_inject_prenotami_captcha()
+
+            
+            # 5. [核心修改] 提交登录
+            # 不要点击 #captcha-trigger,因为它会触发网页自带的 Google 验证逻辑
+            # 我们直接触发表单提交,因为 Token 已经由我们注入了
+            self._log("Submitting login form via JS...")
+            self.page.run_js("document.getElementById('login-form').submit()")
             
-            # 提交登录
-            self._log("Submitting login...")
-            self.page.ele('xpath://*[@id="login-form"]/button').click()
+            # 等待 URL 变化或特定元素出现
+            # 成功通常跳转到 /UserArea, 失败则留在 /Home
+            end_time = time.time() + 45
+            login_success = False
             
-            # 等待登录成功 (通常会跳转到 /UserArea 或 /Services)
-            time.sleep(3)
-            if "Home" in self.page.url and not self.page.ele('#logoutForm'):
-                 # 检查是否有错误提示
-                 if self.page.ele('.alert-danger'):
-                     err = self.page.ele('.alert-danger').text
-                     raise PermissionDeniedError(f"Login Failed: {err}")
-                 raise BizLogicError("Login Failed: Unknown reason")
+            while time.time() < end_time:
+                time.sleep(1)
+                curr_url = self.page.url
+                
+                # 成功特征
+                if "/UserArea" in curr_url or "/Services" in curr_url:
+                    login_success = True
+                    break
+                
+                # 失败特征
+                if self.page.ele('.validation-summary-errors') or self.page.ele('.field-validation-error'):
+                    err_text = self.page.ele('.validation-summary-errors').text if self.page.ele('.validation-summary-errors') else "Unknown validation error"
+                    raise PermissionDeniedError(f"Login Failed: {err_text}")
+                
+                # 检查是否有弹窗错误
+                if "Home" in curr_url and self.page.ele('#logoutForm'):
+                     # 有时候虽然在 Home 但出现了 Logout 按钮,也算成功
+                     login_success = True
+                     break
+
+            if not login_success:
+                # 截图保留现场
+                # self.page.get_screenshot(path="login_fail.jpg")
+                raise BizLogicError("Login Failed: Timeout waiting for redirect (Captcha score too low?)")
 
             self._log("Login Successful.")
             
-            # 访问服务列表页以保活
+            # 访问服务页保活
             self.page.get(f"{self._host}/Services")
             
             self.session_create_time = time.time()
@@ -212,34 +237,6 @@ class ItaPlugin(IVSPlg):
             self.cleanup()
             raise e
 
-    def _handle_login_captcha(self):
-        """处理登录页面的 ReCaptcha"""
-        if self.page.ele('#recaptcha-anchor') or self.page.ele('xpath://iframe[contains(@src, "recaptcha")]'):
-            self._log("Solving ReCaptcha...")
-            api_token = self.free_config.get("capsolver_key", "")
-            if not api_token:
-                self._log("WARN: No capsolver_key, manual solve required.")
-                time.sleep(5)
-                return
-
-            site_key = "6LdkwrIqAAAAAC4NX-g_j7lEx9vh1rg94ZL2cFfY" # Prenotami Site Key
-            
-            rc_params = {
-                "type": "ReCaptchaV2TaskProxyLess", 
-                "page": self.page.url,
-                "siteKey": site_key, 
-                "apiToken": api_token
-            }
-            g_token = self._solve_recaptcha(rc_params)
-            
-            # 注入 Token
-            js = f"""
-            var el = document.getElementById('g-recaptcha-response');
-            if(el) {{ el.value = "{g_token}"; }}
-            """
-            self.page.run_js(js)
-            self._log("Captcha solved & injected.")
-
     # -------------------------------------------------------------
     # 2. Query Availability
     # -------------------------------------------------------------
@@ -517,6 +514,66 @@ class ItaPlugin(IVSPlg):
     # -------------------------------------------------------------
     # 4. Helpers
     # -------------------------------------------------------------
+    
+    def _get_proxy_url(self):
+            # 构造代理
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+        return proxy_url
+
+    def _solve_and_inject_prenotami_captcha(self):
+        """
+        专门处理 Prenotami 的 ReCaptcha Enterprise
+        """
+        self._log("Solving ReCaptcha Enterprise (Action: LOGIN)...")
+        
+        api_token = self.free_config.get("capsolver_key", "")
+        if not api_token:
+            raise BizLogicError("Capsolver Key is required for Prenotami")
+
+        # 从 HTML 源码中提取的信息
+        site_key = "6LdkwrIqAAAAAC4NX-g_j7lEx9vh1rg94ZL2cFfY"
+        page_url = self.page.url
+        
+        # 注意:Prenotami 的这个 Key 其实是混合模式,
+        # 虽然它是 V3 (Enterprise),但很多打码平台用 V2 接口也能解,或者必须用 V3 Enterprise 接口
+        # 建议先尝试 ReCaptchaV3EnterpriseTaskProxyLess
+        
+        # 修正为最标准的 V3 Enterprise 配置
+        rc_params = {
+            "type": "ReCaptchaV3EnterpriseTaskProxyless",
+            "page": page_url,
+            "siteKey": site_key,
+            "action": "LOGIN", # 关键参数
+            "minScore": 0.7,   # 要求高分
+            "apiToken": api_token,
+            # "proxy": self._get_proxy_url()
+        }
+        
+        g_token = self._solve_recaptcha(rc_params)
+        self._log(f"Captcha Solved. Token length: {len(g_token)}")
+        
+        # 注入 Token 到表单
+        # 页面逻辑是:$('#login-form').append('<input type="hidden" name="g-recaptcha-response" value="' + token + '" />');
+        js_inject = f"""
+        var form = document.getElementById('login-form');
+        // 移除旧的 input 防止重复
+        var old = document.getElementsByName('g-recaptcha-response');
+        if(old.length > 0) old[0].remove();
+        
+        var input = document.createElement('input');
+        input.type = 'hidden';
+        input.name = 'g-recaptcha-response';
+        input.value = "{g_token}";
+        form.appendChild(input);
+        """
+        self.page.run_js(js_inject)   
+    
     def _perform_request(self, method, url, headers=None, data=None, json_data=None):
         """JS Fetch Wrapper"""
         if not self.page: raise BizLogicError("Browser not init")
@@ -543,19 +600,75 @@ class ItaPlugin(IVSPlg):
         return BrowserResponse(self.page.run_js(js, timeout=60)) # 文件上传可能较慢,给60s
 
     def _solve_recaptcha(self, params) -> str:
-        # 复用通用的 Capsolver 逻辑
-        key = params.get("apiToken")
+        """
+        调用 YesCaptcha API 识别
+        """
+        client_key = params.get("apiToken")
+        
+        # 1. 选择任务类型
+        # 根据文档:RecaptchaV3TaskProxylessM1S7 强制 0.7 分,适合登录
+        task_type = "RecaptchaV3TaskProxyless" # 默认
+        if params.get("minScore") == 0.7:
+            task_type = "RecaptchaV3TaskProxylessM1S7"
+        elif params.get("minScore") == 0.9:
+            task_type = "RecaptchaV3TaskProxylessM1S9"
+            
+        # 2. 构造创建任务请求
+        create_url = "https://api.yescaptcha.com/createTask"
+        create_data = {
+            "clientKey": client_key,
+            "task": {
+                "type": task_type,
+                "websiteURL": params.get("page"),
+                "websiteKey": params.get("siteKey"),
+                "pageAction": params.get("action") # YesCaptcha 要求的字段名是 pageAction
+            }
+        }
+        
         import requests as req
-        task = { "type": params.get("type"), "websiteURL": params.get("page"), "websiteKey": params.get("siteKey") }
-        r = req.post("https://api.capsolver.com/createTask", json={"clientKey": key, "task": task}, timeout=20)
-        if r.status_code != 200: raise BizLogicError("Capsolver submit failed")
-        tid = r.json().get("taskId")
-        for _ in range(20):
-            r = req.post("https://api.capsolver.com/getTaskResult", json={"clientKey": key, "taskId": tid}, timeout=20)
-            if r.status_code == 200 and r.json().get("status") == "ready":
-                return r.json()["solution"]["gRecaptchaResponse"]
-            time.sleep(3)
-        raise BizLogicError("Capsolver timeout")
+        try:
+            # 发送创建任务请求
+            r = req.post(create_url, json=create_data, timeout=20)
+            if r.status_code != 200:
+                raise BizLogicError(f"YesCaptcha Create Failed: {r.text}")
+            
+            res_json = r.json()
+            if res_json.get("errorId") != 0:
+                raise BizLogicError(f"YesCaptcha Error: {res_json.get('errorDescription')}")
+                
+            task_id = res_json.get("taskId")
+            if not task_id:
+                raise BizLogicError("YesCaptcha returned no taskId")
+            
+            # 3. 轮询获取结果
+            result_url = "https://api.yescaptcha.com/getTaskResult"
+            for _ in range(30): # 最多等 60-90秒
+                time.sleep(3)
+                
+                r = req.post(result_url, json={"clientKey": client_key, "taskId": task_id}, timeout=20)
+                d = r.json()
+                
+                # 识别中
+                if d.get("status") == "processing":
+                    continue
+                
+                # 识别成功
+                if d.get("status") == "ready":
+                    solution = d.get("solution", {})
+                    token = solution.get("gRecaptchaResponse")
+                    if token:
+                        return token
+                    else:
+                        raise BizLogicError("YesCaptcha ready but no token found")
+                
+                # 识别失败
+                if d.get("errorId") != 0:
+                    raise BizLogicError(f"YesCaptcha Task Failed: {d.get('errorDescription')}")
+                    
+        except Exception as e:
+            raise BizLogicError(f"Captcha Solver Exception: {e}")
+            
+        raise BizLogicError("YesCaptcha timeout")
 
     def _parse_valid_days(self, text):
         # 提取 DateLibere (YYYY-MM-DD)

+ 12 - 7
plugins/tls_plugin.py

@@ -400,12 +400,17 @@ class TlsPlugin(IVSPlg):
         else:
             proxy_str = f"{p.ip}:{p.port}"
         # 2. 提交任务
-        task = VSCloudApi.Instance().submit_anticloudflare_task(proxy_str, website_url)
-        # 3. 等待结果
-        task_id = str(task['id'])
-        result = VSCloudApi.Instance().get_anticloudflare_result(task_id)
-        parsed = json.loads(result.get('result', '{}'))
-        cookies_list = parsed.get('cookies', [])
+        task_id = VSCloudApi.Instance().create_task(
+            command="AntiCloudflareTask",
+            args={
+                "proxy": proxy_str,
+                "websiteUrl": website_url
+            }
+        )
+        
+        result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
+        task_result = result_data.get("result", {}).get("token")
+        cookies_list = task_result.get('cookies', [])
         for cookie in cookies_list:
             if cookie['name'] in ['__cf_bm', 'cf_clearance']:
                 self.session.cookies.set(
@@ -414,7 +419,7 @@ class TlsPlugin(IVSPlg):
                     domain=cookie['domain'], 
                     path='/'
                 )
-        ua = parsed.get('userAgent')
+        ua = task_result.get('userAgent')
         if ua:
             self.user_agent = ua
             self.session.headers['User-Agent'] = ua

+ 21 - 1
plugins/tls_plugin2.py

@@ -369,7 +369,7 @@ class TlsPlugin2(IVSPlg):
             "action": "book", 
             "siteKey": "6LcTpXcfAAAAAM3VojNhyV-F1z92ADJIvcSZ39Y9",
             "apiToken": api_token,
-            # "proxy": self._get_proxy_url() # ProxyLess
+            "proxy": self._get_proxy_url() # ProxyLess
         }
         g_token = self._solve_recaptcha(rc_params)
 
@@ -450,6 +450,17 @@ class TlsPlugin2(IVSPlg):
         return res
 
     # --- 辅助方法 ---
+    
+    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, retry_count=0):
         """
@@ -590,6 +601,15 @@ class TlsPlugin2(IVSPlg):
         if params.get("action"):
             task["pageAction"] = params.get("action")
             
+        if params.get("proxy"):
+            p = urlparse(params.get("proxy"))
+            task["proxyType"] = p.scheme
+            task["proxyAddress"] = p.hostname
+            task["proxyPort"] = p.port
+            if p.username:
+                task["proxyLogin"] = p.username
+                task["proxyPassword"] = p.password
+            
         # 注意:使用 DrissionPage 后,通常是 ProxyLess 模式
         # 除非你想让 Capsolver 也用同样的代理(通常不需要,除非风控极严)
         

+ 42 - 69
plugins/vfs_plugin.py

@@ -857,77 +857,50 @@ class VfsPlugin(IVSPlg):
         
         # 2. 提交任务
         self._log(f"Submitting Turnstile task for {website_url}...")
-        task_out = VSCloudApi.Instance().submit_anti_turnstile_task(proxy_str, website_url)
-        if not task_out:
-            raise BizLogicError(message="Failed to submit captcha task to Cloud API")
-        
-        task_id = str(task_out.get("id"))
-        if not task_id:
-            raise BizLogicError(message="Cloud API returned invalid task ID")
-
-        # 3. 轮询结果 (超时时间 120秒)
-        timeout = 120
-        start_time = time.time()
-        
-        while time.time() - start_time < timeout:
-            result_out = VSCloudApi.Instance().get_anti_turnstile_result(task_id)
-            if not result_out:
-                time.sleep(3)
-                continue
-            
-            # status: 0=Pending, 1=Processing, 2=Success, 3=Failed
-            status = result_out.get("status", 0)
-            
-            if status == 2:
-                raw_result = result_out.get("result", "")
-           
-                if isinstance(raw_result, str):
-                    data = json.loads(raw_result)
-                else:
-                    data = raw_result
-
-                token = data.get("token")
-                ua = data.get("userAgent")
-                cookies_list = data.get("cookies", [])
-
-                if not token:
-                    raise BizLogicError("Captcha solved but token is empty")
-                
-                # A. 设置 User-Agent
-                if ua:
-                    self.user_agent = ua
-                    self.session.headers["User-Agent"] = ua
-
-                # B. 设置 Cookies
-                if cookies_list:
-                    self._log(f"Syncing {len(cookies_list)} cookies from Captcha solver...")
-                    for cookie in cookies_list:
-                        # 兼容不同的 cookie 格式
-                        c_name = cookie.get("name")
-                        c_value = cookie.get("value")
-                        c_domain = cookie.get("domain", "")
-                        c_path = cookie.get("path", "/")
-                        
-                        if c_name and c_value:
-                            self.session.cookies.set(
-                                name=c_name, 
-                                value=c_value, 
-                                domain=c_domain, 
-                                path=c_path
-                            )
-                
-                self._log("Cloudflare challenge passed.")
-                return token
+        
+        task_id = VSCloudApi.Instance().create_task(
+            command="AntiCloudflareTurnstileTask",
+            args={
+                "proxy": proxy_str,
+                "websiteUrl":website_url
+            }
+        )
 
-            elif status == 3: # Failed
-                err_msg = result_out.get("result", "Unknown error")
-                raise BizLogicError(message=f"Captcha task failed: {err_msg}")
-            
-            else:
-                # Pending / Processing
-                time.sleep(3)
+        result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
+        task_result = result_data.get("result", {})
+
+        token = task_result.get("token")
+        ua = task_result.get("userAgent")
+        cookies_list = task_result.get("cookies", [])
+
+        if not token:
+            raise BizLogicError("Captcha solved but token is empty")
+        
+        # A. 设置 User-Agent
+        if ua:
+            self.user_agent = ua
+            self.session.headers["User-Agent"] = ua
+
+        # B. 设置 Cookies
+        if cookies_list:
+            self._log(f"Syncing {len(cookies_list)} cookies from Captcha solver...")
+            for cookie in cookies_list:
+                # 兼容不同的 cookie 格式
+                c_name = cookie.get("name")
+                c_value = cookie.get("value")
+                c_domain = cookie.get("domain", "")
+                c_path = cookie.get("path", "/")
+                
+                if c_name and c_value:
+                    self.session.cookies.set(
+                        name=c_name, 
+                        value=c_value, 
+                        domain=c_domain, 
+                        path=c_path
+                    )
         
-        raise BizLogicError(message="Captcha task timeout (120s)")
+        self._log("Cloudflare challenge passed.")
+        return token
 
     def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
         mission = self.free_config.get("mission_code", "")

+ 15 - 3
plugins/vfs_plugin2.py

@@ -81,6 +81,16 @@ def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%
         return dt.strftime(target_format)
     except:
         return data_str
+    
+def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
+    """
+    将邮箱域名替换为指定域名(默认 gmail-app.com)
+    """
+    if "@" not in email:
+        raise ValueError(f"Invalid email: {email}")
+
+    local_part, _ = email.rsplit("@", 1)
+    return f"{local_part}@{new_domain}"
 
 # --- 模拟 Requests Response 对象 ---
 class BrowserResponse:
@@ -388,11 +398,12 @@ class VfsPlugin2(IVSPlg):
     def query(self) -> VSQueryResult:
         """查询可预约 Slot"""
         result = VSQueryResult() 
-        appt_types = self.free_config.get("appointment_types", [])
-        if not appt_types:
+        apt_types = self.free_config.get("appointment_types", [])
+        if not apt_types:
             raise NotFoundError(message="No matching appointment configuration found.")
         
-        apt_config = random.choice(appt_types)
+        weights = [float(item.get("weight", 1)) for item in apt_types]
+        apt_config = random.choices(apt_types, weights=weights, k=1)[0]
         
         try:
             self._fetch_configurations(apt_config)
@@ -402,6 +413,7 @@ class VfsPlugin2(IVSPlg):
             result.availability_status = AvailabilityStatus.NoneAvailable
             result.visa_type = apt_config.get("visa_type", "")
             result.city = apt_config.get("city", "")
+            result.country = apt_config.get("country", "")
             result.routing_key = apt_config.get("routing_key", "")
             if earliest_date:
                 result.success = True

+ 55 - 71
toolkit/vs_cloud_api.py

@@ -108,101 +108,85 @@ class VSCloudApi:
         else:
             raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
 
-    def submit_anti_turnstile_task(self, proxy: str, website_url: str) -> Optional[Dict]:
+    def create_task(self, command: str, args: Dict) -> str:
         """
-        提交反 Turnstile 任务
+        [核心] 创建任务
+        :param command: 任务指令 (e.g., "AntiCloudflareTurnstileTask", "AntiCloudflareTask")
+        :param args: 任务参数字典 (e.g., {"proxy": "...", "websiteUrl": "..."})
+        :return: task_id (直接返回任务ID,方便调用方)
         """
         url = f"{self.base_url}/api/tasks"
         headers = self._get_headers()
         
-        args = {
-            "proxy": proxy,
-            "websiteUrl": website_url
-        }
-        
         payload = {
-            "command": "AntiCloudflareTurnstileTask",
-            "args": args, 
+            "command": command,
+            "args": args,
             "status": 0
         }
 
+        # 发送请求
         resp = self._perform_request('POST', url, headers=headers, json_data=payload)
         result = resp.json()
-        if result.get("code") == 0:
-            return result.get("data", {})
-        else:
-            raise BizLogicError(message=f"Submit anti turnstile task biz error: {result.get('message')}")
-
-
-    def get_anti_turnstile_result(self, task_id: str) -> Optional[Dict]:
-        """获取反 Turnstile 结果"""
-        url = f"{self.base_url}/api/tasks/{task_id}"
-        headers = self._get_headers()
-        resp = self._perform_request('GET', url, headers=headers)
-        result = resp.json()
-        if result.get("code") == 0:
-            return result.get("data", {})
-        else:
-            raise BizLogicError(message=f"Get anti turnstile result biz error: {result.get('message')}")
- 
-    
-    def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
-        """
-        提交 AntiCloudflareTask (用于 TLSContact 5s 盾)
-        """
-        url = f"{self.base_url}/api/tasks" 
         
-        args = {
-            'proxy': proxy,
-            'websiteUrl': website_url
-        }
-        data = {
-            "command": "AntiCloudflareTask", 
-            "args": args,
-            "status": 0
-        }
-        headers = self._get_headers()
-        resp = self._perform_request('POST', url, headers=headers, json_data=data)
-        result = resp.json()
+        # 校验业务状态码
         if result.get("code") == 0:
-            return result.get("data", {})
+            data = result.get("data", {})
+            task_id = data.get("id")
+            if not task_id:
+                raise BizLogicError(message=f"Task created but no ID returned. Resp: {data}")
+            return str(task_id)
         else:
-            raise BizLogicError(message=f"Submit anticloudflare task biz error: {result.get('message')}")
-    
-    def get_anticloudflare_result(self, task_id, retry_interval=5, max_retries=20) -> Optional[Dict]:
+            raise BizLogicError(message=f"Create task failed ({command}): {result.get('message')}")
+
+    def get_task_result(self, task_id: str, timeout: int = 120, interval: int = 3) -> Dict:
         """
-        获取 AntiCloudflareTask 结果 (带轮询)
+        [核心] 轮询获取任务结果
+        :param task_id: 任务ID
+        :param timeout: 最大等待时间(秒)
+        :param interval: 轮询间隔(秒)
+        :return: 任务成功后的 data 字典 (包含 token/cookies 等)
         """
         url = f"{self.base_url}/api/tasks/{task_id}"
         headers = self._get_headers()
         
-        for attempt in range(1, max_retries + 1):
+        start_time = time.time()
+        
+        while True:
+            # 1. 检查是否超时
+            if time.time() - start_time > timeout:
+                raise BizLogicError(message=f"Wait for task result timeout ({timeout}s). TaskID: {task_id}")
+
             try:
+                # 2. 发起查询
                 resp = self._perform_request('GET', url, headers=headers)
                 result = resp.json()
-                if result.get("code") == 0:
-                    data = result.get("data", {})
-                    # status 2 表示成功
-                    if data.get("status") == 2:
-                        return data
-                    elif data.get("status") == 3:
-                        VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
-                        return None
-                    else:
-                        time.sleep(retry_interval)
+                
+                # 3. 校验 API 层面错误
+                if result.get("code") != 0:
+                    raise BizLogicError(message=f"API Error fetching task: {result.get('message')}")
+                
+                data = result.get("data", {})
+                status = data.get("status")
+                
+                # 4. 判断任务状态
+                if status == 2:  # 成功
+                    return data
+                
+                elif status == 3:  # 失败
+                    error_msg = data.get("result", "Unknown error")
+                    raise BizLogicError(message=f"Task execution failed: {error_msg}")
+                
+                # status 为 0 (Pending) 或 1 (Running),继续等待
+                
             except Exception as e:
-                VSC_WARN(
-                    "vs_cloud",
-                    "Get anticloudflare result exception, attempt %d/%d",
-                    attempt, max_retries
-                )
-
-                if attempt >= max_retries:
-                    break
+                # 如果是 BizLogicError 直接抛出,不重试
+                if isinstance(e, BizLogicError):
+                    raise e
+                # 网络波动等其他异常,记录日志并重试
+                VSC_WARN("vs_cloud", f"Polling exception: {str(e)}")
 
-                time.sleep(retry_interval)
-                continue
-        return None
+            # 等待下次轮询
+            time.sleep(interval)
 
     def create_http_session(
         self,