瀏覽代碼

feat: upadte

Hujiarui 2 月之前
父節點
當前提交
47de4d22bb
共有 10 個文件被更改,包括 443 次插入286 次删除
  1. 27 11
      booker_order.py
  2. 80 14
      config/config.json
  3. 3 1
      deps.txt
  4. 12 1
      gco_wrapper.py
  5. 10 2
      plugins/tls_plugin.py
  6. 22 20
      plugins/vfs_plugin.py
  7. 16 0
      sentinel.py
  8. 10 10
      test/test_publish_slot.py
  9. 2 2
      tls_registration_bot.py
  10. 261 225
      vfs_registration_bot.py

+ 27 - 11
booker_order.py

@@ -6,7 +6,7 @@ import random
 import redis
 from typing import List, Dict, Callable, Any, Optional
 
-from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType
+from vs_types import GroupConfig, VSPlgConfig, Task, VSQueryResult, AppointmentType, AvailabilityStatus
 from vs_plg_factory import VSPlgFactory 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
@@ -190,12 +190,34 @@ class OrderBookerGCO:
                 except Exception:
                     pass
                 time.sleep(0.5)
+    
+    def _is_date_of_interest(self, task, query_result) -> bool:
+        """
+        判断 query_result 中的可用日期,
+        是否在 task 的意向日期范围内。
+        """
+
+        if query_result.availability_status != AvailabilityStatus.Available:
+            return True
+        task_id = task.task_ref
+        task_data = self.m_task_data_cache.get(str(task_id), {})
+        user_input = task_data.get('user_inputs', {})
+        expected_start_date = (
+            user_input.get('expected_start_date')
+            or '2000-01-01'
+        )
+        expected_end_date = (
+            user_input.get('expected_end_date')
+            or '2100-01-01'
+        )
+        available_date = query_result.earliest_date
+        return available_date <= expected_end_date
 
     def _booking_trigger_loop(self):
         self._log("Trigger loop started.")
         while not self.m_stop_event.is_set():
             try:
-                time.sleep(1.0)
+                time.sleep(0.1)
                 now = time.time()
                 for apt_type in self.m_cfg.appointment_types:
                     redis_key = self._get_redis_key(apt_type.routing_key)
@@ -218,6 +240,8 @@ class OrderBookerGCO:
                                 continue
                             if apt_type.routing_key not in task.acceptable_routing_keys:
                                 continue
+                            if not self._is_date_of_interest(task, query_result):
+                                continue
                             
                             task.next_run = now + self.m_cfg.booker.booking_cooldown
                             matching_tasks.append(task)
@@ -242,14 +266,6 @@ class OrderBookerGCO:
         try:
             with self.m_lock:
                 task_data = self.m_task_data_cache.get(str(task_id))
-            
-            if not task_data:
-                self._log(f"Cache miss for {task_id}, fetching from cloud...")
-                task_data = VSCloudApi.Instance().get_vas_task(str(task_id))
-                if task_data:
-                    with self.m_lock:
-                        self.m_task_data_cache[str(task_id)] = task_data
-
             if not task_data or task_data.get('status') in ['grabbed', 'pause', 'completed', 'cancelled']:
                 self._log(f"Bound Task={task_id} is no longer valid or already processed. Removing instance.")
                 self._remove_task(task, "bound task no longer valid")
@@ -402,7 +418,7 @@ class OrderBookerGCO:
                             acceptable_routing_keys=acceptable_keys, 
                             source_queue=target_routing_key,
                             book_allowed=True,
-                            next_remote_ping=time.time() + random.randint(55, 65) 
+                            next_remote_ping=time.time() + random.randint(55, 65)   
                         )
                     )
                     queue_fail_key = f"vs:queue:failures:{target_routing_key}"

+ 80 - 14
config/config.json

@@ -988,7 +988,7 @@
                 "account_source": "order",
                 "target_instances": 2,
                 "account_cd": 1800,
-                "booking_cooldown": 10,
+                "booking_cooldown": 3,
                 "max_bookings_per_account": 1
             },
             "query_wait": {
@@ -1027,25 +1027,25 @@
         },
         {
             "identifier": "tls.cn.bjs.fr",
-            "debug": false,
-            "enable": true,
+            "debug": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
-            "proxy_pool": ["decodo"],
+            "proxy_pool": ["proxy-cheap-good"],
             "proxy_cd": 900,
             "session_max_life": 1800,
             "sentinel": {
                 "account_source": "built-in",
                 "account_pool_id": "tls.cn.bjs.fr.sentinel",
-                "target_instances": 3,
+                "target_instances": 1,
                 "account_cd": 1800,
                 "signal_ttl": 30
             },
             "booker": {
                 "account_source": "order",
-                "target_instances": 3,
+                "target_instances": 1,
                 "account_cd": 1800,
-                "booking_cooldown": 10,
+                "booking_cooldown": 3,
                 "max_bookings_per_account": 1
             },
             "query_wait": {
@@ -1084,25 +1084,25 @@
         },
         {
             "identifier": "tls.cn.sha.fr",
-            "debug": false,
-            "enable": true,
+            "debug": true,
+            "enable": false,
             "need_account": true,
             "need_proxy": true,
-            "proxy_pool": ["decodo"],
+            "proxy_pool": ["proxy-cheap-good"],
             "proxy_cd": 900,
             "session_max_life": 1800,
             "sentinel": {
                 "account_source": "built-in",
                 "account_pool_id": "tls.cn.sha.fr.sentinel",
-                "target_instances": 3,
+                "target_instances": 1,
                 "account_cd": 1800,
                 "signal_ttl": 30
             },
             "booker": {
                 "account_source": "order",
-                "target_instances": 3,
+                "target_instances": 1,
                 "account_cd": 1800,
-                "booking_cooldown": 10,
+                "booking_cooldown": 3,
                 "max_bookings_per_account": 1
             },
             "query_wait": {
@@ -1159,7 +1159,7 @@
                 "account_source": "order",
                 "target_instances": 1,
                 "account_cd": 1800,
-                "booking_cooldown": 10,
+                "booking_cooldown": 3,
                 "max_bookings_per_account": 1
             },
             "query_wait": {
@@ -1196,6 +1196,72 @@
                 }
             }
         },
+        {
+            "identifier": "vfs.cn.at",
+            "debug": false,
+            "enable": true,
+            "need_account": true,
+            "need_proxy": true,
+            "proxy_pool": ["proxy-cheap"],
+            "proxy_cd": 300,
+            "session_max_life": 1800,
+            "sentinel": {
+                "account_source": "built-in",
+                "account_pool_id": "cn.at.sentinel",
+                "target_instances": 1,
+                "account_cd": 10800,
+                "signal_ttl": 30
+            },
+            "booker": {
+                "account_source": "built-in",
+                "account_pool_id": "cn.at.booker",
+                "target_instances": 1,
+                "account_cd": 10800,
+                "booking_cooldown": 10,
+                "max_bookings_per_account": 8
+            },
+            "query_wait": {
+                "mode": "Random",
+                "fixed_wait": 10,
+                "random_min": 60,
+                "random_max": 300
+            },
+            "plugin_config": {
+                "lib_path": "plugins",
+                "plugin_name": "vfs_plugin",
+                "plugin_bin": "vfs_plugin.py",
+                "plugin_proto": "IVSPlg"
+            },
+            "appointment_types": [
+                {
+                    "weight": 10,
+                    "routing_key": "slot.can.at.tourist",
+                    "city": "Guangzhou",
+                    "visa_type": "Tourist",
+                    "country": "Austria"
+                }
+            ],
+            "website": "https://visa.vfsglobal.com/chn/en/aut/login",
+            "free_config": {
+                "mission_code": "aut",
+                "mission_name": "Austria",
+                "country_code": "chn",
+                "country_name": "China",
+                "culture_code": "en-US",
+                "language": "en",
+                "apt_configs": {
+                    "slot.can.at.tourist": {
+                        "center_name": "Austria Visa Application Center, Guangzhou",
+                        "address": "7/F, GAL Tower, No. 78, Pazhou Avenue, Haizhu District",
+                        "vac_code": "Gua",
+                        "category_name": "Visa Type C-Schengen",
+                        "category_code": "VTC",
+                        "subcategory_name": "Tourism (90 days)",
+                        "subcategory_code": "TR"
+                    }
+                }
+            }
+        },
         {
             "identifier": "e-konsulat.ie.pl",
             "debug": false,

+ 3 - 1
deps.txt

@@ -1,4 +1,6 @@
 mihomo-linux-amd64.gz|https://github.com/MetaCubeX/mihomo/releases/download/v1.19.24/mihomo-linux-amd64-v3-v1.19.24.gz
 mihomo-windows-amd64.zip|https://github.com/MetaCubeX/mihomo/releases/download/v1.19.24/mihomo-windows-amd64-v3-v1.19.24.zip
 ungoogled-chromium-144.0.7559.132-1-x86_64_linux.tar.xz|https://github.com/adryfish/fingerprint-chromium/releases/download/144.0.7559.132/ungoogled-chromium-144.0.7559.132-1-x86_64_linux.tar.xz
-ungoogled-chromium_144.0.7559.132-1.1_windows_x64.zip|https://github.com/adryfish/fingerprint-chromium/releases/download/144.0.7559.132/ungoogled-chromium_144.0.7559.132-1.1_windows_x64.zip
+ungoogled-chromium_144.0.7559.132-1.1_windows_x64.zip|https://github.com/adryfish/fingerprint-chromium/releases/download/144.0.7559.132/ungoogled-chromium_144.0.7559.132-1.1_windows_x64.zip
+cloakbrowser-linux-x64.tar.gz|https://github.com/CloakHQ/CloakBrowser/releases/download/chromium-v146.0.7680.177.5/cloakbrowser-linux-x64.tar.gz
+cloakbrowser-windows-x64.zip|https://github.com/CloakHQ/CloakBrowser/releases/download/chromium-v146.0.7680.177.5/cloakbrowser-windows-x64.zip

+ 12 - 1
gco_wrapper.py

@@ -84,4 +84,15 @@ class GCOWrapper:
             if self._gco:
                 self._gco.stop()  # 调用 Sentinel 或 Booker 的 stop
         finally:
-            self._transition(State.STOPPING, State.STOPPED)
+            self._transition(State.STOPPING, State.STOPPED)
+            
+    # ================= 新增热更新方法 =================
+    def update_config(self, new_cfg: GroupConfig):
+        """
+        动态更新当前 Group 的配置,并透传给底层具体执行的 GCO 实例
+        """
+        with self._lock:
+            self.m_cfg = new_cfg
+            
+        if self._gco:
+            self._gco.update_config(new_cfg)

+ 10 - 2
plugins/tls_plugin.py

@@ -313,6 +313,8 @@ class TlsPlugin(IVSPlg):
             session_created = False
             has_submitted_login = False
             
+            other_langs = ['fr-fr', 'ar-ar', 'cb-ph', 'id-id', 'km-kh', 'mk-mk', 'ru-ru', 'sq-al', 'th-th', 'tl-ph', 'uz-uz', 'vi-vn', 'zh-cn', 'hy-am', 'zh-hk']
+            
             for step in range(max_steps):
                 self.page.wait.doc_loaded()
                 time.sleep(0.5)
@@ -328,6 +330,14 @@ class TlsPlugin(IVSPlg):
                 if any(cloudflare_blocked_indicators):
                     raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
                 
+                # 如果语言不匹配, 切换语言到英语
+                matched_lang = next((lang for lang in other_langs if lang in current_url), None)
+                if matched_lang:
+                    current_url = current_url.replace(matched_lang, 'en-us')
+                    self.page.get(current_url)
+                    self.page.wait.load_start(timeout=3)
+                    continue
+                     
                 # 状态 1:到达终极目标页面 (成功退出条件)
                 if "appointment-booking" in current_url or self.page.ele('tag:button@text():Book your appointment', timeout=1):
                     btn_selector = 'tag:button@text():Book your appointment'            
@@ -512,9 +522,7 @@ class TlsPlugin(IVSPlg):
                         TimeSlot(time=s["time"], label=str(s.get("label", "")))
                     )
                 res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
-                self._log(f"Slot Found! -> {slots}")
             else:
-                self._log("No slots available.")
                 res.success = False
                 res.availability_status = AvailabilityStatus.NoneAvailable
                 

+ 22 - 20
plugins/vfs_plugin.py

@@ -144,6 +144,8 @@ class VfsPlugin(IVSPlg):
         self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
         self.user_data_path = os.path.join(self.root_workspace, "user_data")
         
+        self.vfs_api_domain = 'lift-api.vfsglobal.com'
+        
         # 持有隧道实例
         self.tunnel = None
         
@@ -159,6 +161,8 @@ class VfsPlugin(IVSPlg):
     def set_config(self, config: VSPlgConfig):
         self.config = config
         self.free_config = config.free_config or {}
+        if self.free_config.get('country_code') == 'chn':
+            self.vfs_api_domain = 'lift-apicn.vfsglobal.com'
         
     def set_log(self, logger: Callable[[str], None]) -> None:
         self.logger = logger
@@ -323,7 +327,7 @@ class VfsPlugin(IVSPlg):
             client_src = self._get_client_source()
             orange_src = self._get_orange_source(email)
             
-            url = "https://lift-api.vfsglobal.com/user/login"
+            url = f"https://{self.vfs_api_domain}/user/login"
             headers = self._get_common_headers(with_auth=False)
             headers.update({
                 "clientsource": client_src,
@@ -634,7 +638,7 @@ class VfsPlugin(IVSPlg):
         return self._encrypt_password(payload)
 
     def _query_earliest_slot(self, apt_config) -> Optional[str]:
-        url = "https://lift-api.vfsglobal.com/appointment/CheckIsSlotAvailable"
+        url = f"https://{self.vfs_api_domain}/appointment/CheckIsSlotAvailable"
         data = {
             "missioncode": self.free_config.get("mission_code"),
             "countrycode": self.free_config.get("country_code"),
@@ -689,7 +693,7 @@ class VfsPlugin(IVSPlg):
     def _query_center(self) -> List:
         mission = self.free_config.get("mission_code")
         country = self.free_config.get("country_code")
-        url = f"https://lift-api.vfsglobal.com/master/center/{mission}/{country}/en-US"
+        url = f"https://{self.vfs_api_domain}/master/center/{mission}/{country}/en-US"
         headers = self._get_common_headers(with_auth=False)
         resp = self._perform_request("GET", url, headers=headers)
         return resp.json()
@@ -698,7 +702,7 @@ class VfsPlugin(IVSPlg):
         mission = self.free_config.get("mission_code")
         country = self.free_config.get("country_code")
         enc_center = urllib.parse.quote(center_code)
-        url = f"https://lift-api.vfsglobal.com/master/visacategory/{mission}/{country}/{enc_center}/en-US"
+        url = f"https://{self.vfs_api_domain}/master/visacategory/{mission}/{country}/{enc_center}/en-US"
         headers = self._get_common_headers(with_auth=False)
         resp = self._perform_request("GET", url, headers=headers)
         return resp.json()
@@ -708,7 +712,7 @@ class VfsPlugin(IVSPlg):
         country = self.free_config.get("country_code")
         enc_center = urllib.parse.quote(center_code)
         enc_cat = urllib.parse.quote(category_code)
-        url = f"https://lift-api.vfsglobal.com/master/subvisacategory/{mission}/{country}/{enc_center}/{enc_cat}/en-US"
+        url = f"https://{self.vfs_api_domain}/master/subvisacategory/{mission}/{country}/{enc_center}/{enc_cat}/en-US"
         headers = self._get_common_headers(with_auth=False)
         resp = self._perform_request("GET", url, headers=headers)
         return resp.json()            
@@ -748,7 +752,7 @@ class VfsPlugin(IVSPlg):
         client_src = self._get_client_source()
         orange_src = self._get_orange_source(email)
         
-        url = "https://lift-api.vfsglobal.com/user/login"
+        url = f"https://{self.vfs_api_domain}/user/login"
         headers = self._get_common_headers(with_auth=False)
         headers.update({
             "clientsource": client_src,
@@ -974,8 +978,6 @@ class VfsPlugin(IVSPlg):
         res.book_date = selected_slot_date
         res.book_time = selected_slot_time_range
         res.urn = final_urn
-        res.fee_amount = int(amount * 100)
-        res.fee_currency = currency
         
         if schedule_res.get("IsPaymentRequired", False):
             payload = schedule_res.get("payLoad", "")
@@ -988,7 +990,7 @@ class VfsPlugin(IVSPlg):
         return res
 
     def _get_application(self):
-        url = 'https://lift-api.vfsglobal.com/appointment/application'
+        url = f"https://{self.vfs_api_domain}/appointment/application"
         headers = self._get_common_headers(with_auth=True)
         data = {
             'countryCode': self.free_config.get("country_code"),
@@ -1003,7 +1005,7 @@ class VfsPlugin(IVSPlg):
         """上传图片:先下载外部图片,再通过浏览器上传到 VFS"""
         import requests as standard_requests # 使用标准库下载外部资源
         
-        url = "https://lift-api.vfsglobal.com/appointment/UploadApplicantDocument"
+        url = f"https://{self.vfs_api_domain}/appointment/UploadApplicantDocument"
         passport_url = user_inputs.get("passport_image_url")
         if not passport_url:
             raise NotFoundError(message="Missing passport_image_url")
@@ -1039,7 +1041,7 @@ class VfsPlugin(IVSPlg):
     def _add_primary_applicant(self, apt_config: Dict[str, Any], user_inputs: Dict[str, Any], 
                              is_waitlist: bool, ocr_enabled: bool, enable_ref: bool) -> str:
         """构造申请人 payload 并提交"""
-        url = "https://lift-api.vfsglobal.com/appointment/applicants"
+        url = f"https://{self.vfs_api_domain}/appointment/applicants"
         headers = self._get_common_headers(with_auth=True)
 
         gender_str = str(user_inputs.get("gender", "")).lower()
@@ -1158,7 +1160,7 @@ class VfsPlugin(IVSPlg):
         return urn
     
     def _applicant_otp_send(self, apt_config, urn) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
+        url = f"https://{self.vfs_api_domain}/appointment/applicantotp"
         headers = self._get_common_headers(with_auth=True)
         data = {
             "urn": urn,
@@ -1174,7 +1176,7 @@ class VfsPlugin(IVSPlg):
         return resp.json().get("isOTPGenerated", False)
 
     def _applicant_otp_verify(self, apt_config, urn, otp) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/applicantotp"
+        url = f"https://{self.vfs_api_domain}/appointment/applicantotp"
         headers = self._get_common_headers(with_auth=True)
         # VFS 这里的 header 有时需要 datacenter,原代码有就加上
         headers["datacenter"] = "GERMANY" 
@@ -1192,7 +1194,7 @@ class VfsPlugin(IVSPlg):
         return resp.json().get("isOTPValidated", False)
         
     def _query_slot_calendar(self, apt_config, urn, from_date) -> List:
-        url = "https://lift-api.vfsglobal.com/appointment/calendar"
+        url = f"https://{self.vfs_api_domain}/appointment/calendar"
         headers = self._get_common_headers(with_auth=True)
         
         # 将 YYYY-MM-DD 转为 DD/MM/YYYY 用于 API
@@ -1222,7 +1224,7 @@ class VfsPlugin(IVSPlg):
         return ads_out
      
     def _query_slot_time(self, apt_config, urn, slot_date) -> List:
-        url = "https://lift-api.vfsglobal.com/appointment/timeslot"
+        url = f"https://{self.vfs_api_domain}/appointment/timeslot"
         headers = self._get_common_headers(with_auth=True)
         
         dt_m = datetime.strptime(slot_date, "%Y-%m-%d")
@@ -1241,7 +1243,7 @@ class VfsPlugin(IVSPlg):
         return resp.json().get("slots", [])
 
     def _saveuseractionaudit(self, apt_config, urn, earliest_date) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/saveuseractionaudit"
+        url = f"https://{self.vfs_api_domain}/appointment/saveuseractionaudit"
         headers = self._get_common_headers(with_auth=True)
         
         dt = datetime.strptime(earliest_date, "%Y-%m-%d")
@@ -1261,7 +1263,7 @@ class VfsPlugin(IVSPlg):
         return resp.json().get("isSavedSuccess", False)
         
     def _submit_no_addition_service(self, urn):
-        url = "https://lift-api.vfsglobal.com/vas/mapvas"
+        url = f"https://{self.vfs_api_domain}/vas/mapvas"
         headers = self._get_common_headers(with_auth=True)
         data = {
             "loginUser": self.config.account.username,
@@ -1273,7 +1275,7 @@ class VfsPlugin(IVSPlg):
         self._perform_request("POST", url, headers=headers, json_data=data)
 
     def _query_fee(self, apt_config, urn) -> Tuple[float, str]:
-        url = "https://lift-api.vfsglobal.com/appointment/fees"
+        url = f"https://{self.vfs_api_domain}/appointment/fees"
         headers = self._get_common_headers(with_auth=True)
         data = {
             "missionCode": self.free_config.get("mission_code"),
@@ -1292,7 +1294,7 @@ class VfsPlugin(IVSPlg):
         return total, currency
 
     def _schedule(self, apt_config, urn, amount, currency, slot_id) -> Dict:
-        url = "https://lift-api.vfsglobal.com/appointment/schedule"
+        url = f"https://{self.vfs_api_domain}/appointment/schedule"
         headers = self._get_common_headers(with_auth=True)
         data = {
             "missionCode": self.free_config.get("mission_code"),
@@ -1346,7 +1348,7 @@ class VfsPlugin(IVSPlg):
         return final_url
 
     def _confirm_waitlist(self, apt_config: Dict[str, Any], urn: str) -> bool:
-        url = "https://lift-api.vfsglobal.com/appointment/ConfirmWaitlist"
+        url = f"https://{self.vfs_api_domain}/appointment/ConfirmWaitlist"
         headers = self._get_common_headers(with_auth=True)
         data = {
             "missionCode": self.free_config.get("mission_code"),

+ 16 - 0
sentinel.py

@@ -44,6 +44,22 @@ class SentinelGCO:
         elif mode == QueryWaitMode.Random:
             return (self.m_cfg.query_wait.random_min + self.m_cfg.query_wait.random_max) / 2.0
         return 30.0
+    
+    def update_config(self, new_cfg: GroupConfig):
+        """
+        动态更新配置。侵入性小,仅替换配置对象。
+        现有的 creator_loop 和 monitor_loop 下一次循环读取时即生效。
+        """
+        with self.m_lock:
+            # 如果开关发生了变化(例如后端禁用了该组)
+            if self.m_cfg.enable and not new_cfg.enable:
+                self._log("Config dynamically updated: Group DISABLED. Will stop creating new tasks.")
+            elif not self.m_cfg.enable and new_cfg.enable:
+                self._log("Config dynamically updated: Group ENABLED.")
+            else:
+                self._log("Config dynamically updated: Parameters refreshed.")
+                
+            self.m_cfg = new_cfg
 
     def start(self):
         if not self.m_cfg.enable:

+ 10 - 10
test/test_publish_slot.py

@@ -11,32 +11,32 @@ r = redis.Redis(
 )
 
 # 使用的键名(原 Channel 名称)
-key = "vs:signal:slot.lon.fr.tourist"
+key = "vs:signal:slot.bjs.fr.tourist"
 
 # 消息体
 message = {
-    "group_id": "tls.gb.fr",
+    "group_id": "tls.cn.bjs.fr",
     "apt_type": {
         "weight": 10,
-        "routing_key": "slot.lon.fr.tourist",
-        "city": "London",
+        "routing_key": "slot.bjs.fr.tourist",
+        "city": "Beijing",
         "visa_type": "Tourist",
         "country": "France"
     },
     "query_result": {
-        "routing_key": "slot.lon.fr.tourist",
+        "routing_key": "slot.bjs.fr.tourist",
         "country": "France",
-        "city": "London",
+        "city": "Beijing",
         "visa_type": "Tourist",
         "availability_status": "Available",
-        "earliest_date": "2026-06-05",
+        "earliest_date": "2026-06-06",
         "availability": [
             {
-                "date": "2026-06-05",
+                "date": "2026-06-06",
                 "times": [
                     {
-                        "time": "14:00",
-                        "label": ""
+                        "time": "16:30",
+                        "label": "pta"
                     }
                 ]
             }

+ 2 - 2
tls_registration_bot.py

@@ -241,11 +241,11 @@ class TlsRegistrator:
         self._log("提交注册...")
         btn_e = self.page.ele(btn_selector)
         time.sleep(random.uniform(0.3, 0.6))
-
+        
         self.mouse.human_click_ele(btn_e)
 
         self._log("正在等待验证结果 (最多10秒)...")
-        success_dialog = self.page.wait.ele_displayed('tag:h1@text():Check your email inbox', timeout=10)
+        success_dialog = self.page.wait.ele_displayed('tag:h1@text():Check your email inbox', timeout=15)
         
         if not success_dialog:
             self.page.get_screenshot("failed_submit.png") 

+ 261 - 225
vfs_registration_bot.py

@@ -1,12 +1,16 @@
 import os
+import re
+import uuid
 import random
 import socket
 import json
 import time
 import string
+import shutil
 import logging
 import base64
 import requests
+import argparse
 from datetime import datetime, timezone
 from urllib.parse import urlparse, urlencode
 
@@ -15,11 +19,16 @@ from cryptography.hazmat.primitives import serialization, hashes
 from cryptography.hazmat.primitives.asymmetric import padding
 from cryptography.hazmat.backends import default_backend
 
+# 假设这些是你本地的依赖
+import configure
 from DrissionPage import ChromiumPage, ChromiumOptions
-from vs_types import RateLimiteddError, BizLogicError 
+from vs_types import RateLimiteddError, BizLogicError
+from toolkit.mihomo_tunnel import MihomoTunnel
 from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
 
-# --- 配置日志 ---
+# ==========================================
+# 日志与常量配置
+# ==========================================
 logging.basicConfig(
     level=logging.INFO,
     format='%(asctime)s [%(levelname)s] %(message)s',
@@ -27,7 +36,6 @@ logging.basicConfig(
 )
 logger = logging.getLogger("VFSRegistrar")
 
-# --- 常量 ---
 VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
 LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
@@ -38,73 +46,26 @@ t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
 GQIDAQAB
 -----END PUBLIC KEY-----"""
 
-def upload_account_to_server(account):
-    """
-    将注册成功的账号上报到中心服务器
-    """
-    api_url = 'https://api.text.skin/api/account/add'
-    api_token = 'tok_e946329a60ff45ba807f3f41b0e8b7fc'  # 你的 Bearer Token
-
-    # 构造请求头
-    headers = {
-        'accept': 'application/json',
-        'Authorization': f'Bearer {api_token}',
-        'Content-Type': 'application/json'
-    }
-
-    # 构造 extra_data (存放 VFS 特有的国家、领区、手机号信息)
-    extra_payload = {
-        "country_code": account.get("country_code"),
-        "mission_code": account.get("mission_code"),
-        "phone_country_code": account.get("phone_country_code"),
-        "phone_number": account.get("phone_number"),
-        "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-    }
-
-    # 构造主 Payload
-    payload = {
-        "pool_name": account.get("pool_name", "default_pool"),
-        "username": account.get("username"),
-        "password": account.get("password"),
-        "extra_data": extra_payload
-    }
-
-    try:
-        logger.info(f"Uploading account {account['username']} to server...")
-        resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
-        
-        if resp.status_code == 200:
-            logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
-            return True
-        else:
-            logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
-            return False
-    except Exception as e:
-        logger.error(f"❌ [API Upload Error]: {e}")
-        return False
-
 
+# ==========================================
+# 辅助工具类
+# ==========================================
 class VFSHelper:
-    """工具方法的静态类"""
-    
     @staticmethod
     def generate_mobile_number(country_code=353, e164_format=False):
-        if country_code == 353: # Ireland
+        if country_code == 353:  # Ireland
             prefix = random.choice(['83', '85', '86', '87', '89'])
             number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
             return f"+353{number}" if e164_format else number
-            
-        elif country_code == 44: # UK
+        elif country_code == 44:  # UK
             prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
             number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
             return f"+44{number}" if e164_format else number
-
-        elif country_code == 86: # China
+        elif country_code == 86:  # China
             prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
             prefix = random.choice(prefixes)
             number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
             return f"+86{number}" if e164_format else number
-        
         return "".join([str(random.randint(0, 9)) for _ in range(10)])
 
     @staticmethod
@@ -139,6 +100,7 @@ class VFSHelper:
         payload = f"GA;{timestamp}Z"
         return VFSHelper.encrypt_password(payload)
 
+
 class BrowserResponse:
     """标准化浏览器响应"""
     def __init__(self, result_dict):
@@ -157,12 +119,78 @@ class BrowserResponse:
                 self._json = {}
         return self._json
 
+
+# ==========================================
+# API 交互与代理工具
+# ==========================================
+def load_proxies(pool_name):
+    """从 config/proxies.json 读取对应的代理池"""
+    config_path = os.path.join(os.path.dirname(__file__), 'config', 'proxies.json')
+    try:
+        with open(config_path, 'r', encoding='utf-8') as f:
+            data = json.load(f)
+            proxies = data.get(pool_name, [])
+            if not proxies:
+                raise ValueError(f"代理池 '{pool_name}' 为空或不存在!")
+            return proxies
+    except Exception as e:
+        logger.error(f"读取代理配置文件失败: {e}")
+        exit(1)
+
+def upload_account_to_server(account, api_url, api_token):
+    """将注册成功的账号上报到中心服务器"""
+    headers = {
+        'accept': 'application/json',
+        'Authorization': f'Bearer {api_token}',
+        'Content-Type': 'application/json'
+    }
+
+    payload = {
+        "pool_name": account.get("pool_name"),
+        "username": account.get("username"),
+        "password": account.get("password"),
+        "extra_data": {
+            "country_code": account.get("country_code"),
+            "mission_code": account.get("mission_code"),
+            "phone_country_code": account.get("phone_country_code"),
+            "phone_number": account.get("phone_number"),
+            "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        }
+    }
+
+    try:
+        logger.info(f"Uploading account {account['username']} to server...")
+        resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
+        if resp.status_code == 200:
+            logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
+            return True
+        else:
+            logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
+            return False
+    except Exception as e:
+        logger.error(f"❌ [API Upload Error]: {e}")
+        return False
+
+
+# ==========================================
+# 核心自动化类
+# ==========================================
 class VFSRegistrationBot:
-    def __init__(self, config):
-        self.config = config
-        self.page = None
-        self.proxy_url = config.get("proxy_url")
+    def __init__(self, vfs_url, proxy, email_api_token, master_email):
+        self.vfs_url = vfs_url
+        self.proxy_config = proxy
+        self.email_api_token = email_api_token
+        self.master_email = master_email
         
+        self.page = None
+        self.tunnel = None
+        self.instance_id = uuid.uuid4().hex[:8]
+        self.workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"reg_session_{self.instance_id}"))
+        self.active_proxy_url = None  # 供 request 请求复用
+
+    def _log(self, msg):
+        logger.info(f"[TLS-Reg-{self.instance_id}] {msg}")
+
     def _init_browser(self):
         """初始化浏览器配置"""
         co = ChromiumOptions()
@@ -174,30 +202,51 @@ class VFSRegistrationBot:
             
         co.set_local_port(port)
         
-        chrome_path = os.getenv("CHROME_BIN")
-        if chrome_path:
+        chrome_path = configure.CHROME_PATH or os.getenv("CHROME_BIN")
+        if chrome_path and os.path.exists(chrome_path):
             co.set_paths(browser_path=chrome_path)
 
-        if self.proxy_url:
-            co.set_argument(f'--proxy-server={self.proxy_url}')
+        if self.proxy_config and self.proxy_config.get("ip"):
+            p = self.proxy_config
+            if p.get('username') and p.get('password'):
+                self._log(f"Starting Proxy Tunnel for {p.get('ip')}...")
+                exit_node = {
+                    "name": "ExitNode",
+                    "type": p.get('proto'),
+                    "server": p.get('ip'),
+                    "port": p.get('port'),
+                    "username": p.get('username'),
+                    "password": p.get('password')
+                }
+                relay_node = random.choice(configure.MIHOMO_RELAY_NODES) if configure.MIHOMO_RELAY_NODES else None
+                mihomo_path = configure.MIHOMO_BIN_PATH or os.getenv("MIHOMO_BIN")
+                if not mihomo_path:
+                    raise BizLogicError('Mihomo path is null. Set mihomo bin path in configure or os env')
+                
+                self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
+                self.active_proxy_url = self.tunnel.start()
+                self._log(f"Tunnel started at {self.active_proxy_url}")
+                co.set_argument(f'--proxy-server={self.active_proxy_url}')
+            else:
+                self.active_proxy_url = f"{p.get('proto')}://{p.get('ip')}:{p.get('port')}"
+                co.set_argument(f'--proxy-server={self.active_proxy_url}')
+        else:
+            self._log("[WARN] No proxy configured!")
 
-        co.headless(False) # VFS 验证码通常需要有头模式
+        co.headless(False)
         co.set_argument('--no-sandbox')
         co.set_argument('--disable-gpu')
         co.set_argument('--disable-dev-shm-usage')
         co.set_argument('--window-size=1920,1080')
         co.set_argument('--disable-blink-features=AutomationControlled')
         
-        # 创建页面对象
         self.page = ChromiumPage(co)
-        # 设置超时
         self.page.set.timeouts(15)
 
     def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
         """注入JS执行Fetch请求,绕过部分指纹检测"""
         if not self.page:
             raise BizLogicError("Browser not initialized")
-
         if retry_count > 3:
             raise BizLogicError("Max retries exceeded for request")
 
@@ -214,12 +263,9 @@ class VFSRegistrationBot:
             fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
             fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
 
-        logger.debug(f"Request: {method} {url}")
-
         js_script = f"""
         const url = "{url}";
         const options = {json.dumps(fetch_options)};
-        
         return fetch(url, options)
             .then(async response => {{
                 const text = await response.text();
@@ -242,7 +288,6 @@ class VFSRegistrationBot:
             if resp.status_code == 200:
                 return resp
             
-            # 处理 Cloudflare 403 拦截
             if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
                 logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
                 new_token = self._refresh_turnstile()
@@ -253,28 +298,24 @@ class VFSRegistrationBot:
             if resp.status_code == 429:
                 raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
                 
-            return resp # 返回其他状态码供调用者处理 (如 400 业务错误)
-            
+            return resp
         except Exception as e:
             logger.error(f"JS Execution Error: {e}")
             raise BizLogicError(f"Fetch failed: {e}")
 
     def _handle_cookie_banner(self):
-        """处理 Cookie 弹窗"""
         try:
-            js = """
+            self.page.run_js("""
             var btn = document.getElementById('onetrust-accept-btn-handler');
             if(btn) { btn.click(); return true; }
             var banner = document.getElementById('onetrust-banner-sdk');
             if(banner) { banner.remove(); return true; }
-            """
-            self.page.run_js(js)
+            """)
         except:
             pass
 
     def _refresh_turnstile(self):
-        """刷新并获取 Cloudflare Token"""
-        logger.info("Attempting to refresh Turnstile token...")
+        self._log("Attempting to refresh Turnstile token...")
         try:
             self.page.run_js('try{window.turnstile.reset()}catch(e){}')
             cf_bypasser = CloudflareBypasser(self.page, log=True)
@@ -284,39 +325,33 @@ class VFSRegistrationBot:
                 try:
                     ele = self.page.ele('@name=cf-turnstile-response')
                     if ele and ele.value:
-                        logger.info("Turnstile token obtained.")
+                        self._log("Turnstile token obtained.")
                         return ele.value
                 except:
                     pass
-                
                 if i > 5:
-                    try:
-                        cf_bypasser.click_verification_button(is_dfs=False)
-                    except:
-                        pass
+                    try: cf_bypasser.click_verification_button(is_dfs=False)
+                    except: pass
         except Exception as e:
             logger.error(f"Turnstile refresh failed: {e}")
         return None
 
     def _wait_for_activation_link(self, username, max_wait_sec=60):
-        """轮询获取激活链接"""
-        logger.info(f"Waiting for email to {username}...")
+        self._log(f"Waiting for email to {username}...")
         start_time = time.time()
-        
-        master_email = self.config.get("master_email", "visafly666@gmail.com")
-        
-        # 配置代理
-        proxies = {
-            "http": self.proxy_url,
-            "https": self.proxy_url,
-        } if self.proxy_url else None
+
+        url = "https://api.text.skin/api/email-authorizations/fetch"
+        headers = {
+            "Authorization": f"Bearer {self.email_api_token}",
+            "Content-Type": "application/json",
+            "Accept": "application/json, text/plain, */*"
+        }
 
         while time.time() - start_time < max_wait_sec:
             try:
                 utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
-                
                 params = {
-                    "email": master_email,
+                    "email": self.master_email,
                     "sender": 'donotreply at vfsglobal.com',
                     "recipient": username,
                     "subjectKeywords": 'Welcome',
@@ -325,112 +360,76 @@ class VFSRegistrationBot:
                     "expiry": str(60)
                 }
         
-                url = f"https://api.text.skin/api/email-authorizations/fetch"
-                headers =  {
-                    "Authorization": "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc",
-                    "Content-Type": "application/json",
-                    "Accept": "application/json, text/plain, */*"
-                }
-
-                # 发送请求,添加代理和超时
-                # 注意:如果 body 为空,建议传 json={} 而不是 data=""
-                resp = requests.post(url, headers=headers, params=params, json={}, proxies=proxies, timeout=60)
+                # 已经去掉 proxies=proxies 参数
+                resp = requests.post(url, headers=headers, params=params, json={}, timeout=60)
                 
-                # --- 关键改进:先检查状态码 ---
                 if resp.status_code != 200:
-                    logger.warning(f"Email API returned status {resp.status_code}. Body: {resp.text[:100]}")
+                    logger.warning(f"Email API returned status {resp.status_code}.")
                     time.sleep(15)
                     continue
 
-                # --- 关键改进:安全解析 JSON ---
                 try:
                     result = resp.json()
                 except json.JSONDecodeError:
-                    logger.error(f"Failed to decode JSON. Response was: {resp.text[:200]}")
                     time.sleep(15)
                     continue
 
                 if result.get('code') != 0:
-                    # 这里的错误通常是业务逻辑错误(如:邮件还没到)
                     logger.debug(f"API Message: {result.get('message')}")
                 else:
-                    data = result.get('data', {})
-                    content = data.get('body', "")
-        
+                    content = result.get('data', {}).get('body', "")
                     if content:
                         soup = BeautifulSoup(content, "html.parser")
                         link = soup.find("a", string="ActivateAccount")
                         if link:
-                            raw_link = link["href"]
-                            clean_url = raw_link.replace(" ", "").replace("\n", "").replace("\r", "").strip()
-                            return clean_url
-                
+                            return link["href"].replace(" ", "").replace("\n", "").replace("\r", "").strip()
             except Exception as e:
                 logger.warning(f"Error fetching email: {e}")
             
             time.sleep(15)
-            logger.info("Checking email again...")
+            self._log("Checking email again...")
             
         return None
 
     def register(self, account):
-        """执行单个账号注册"""
-        website = self.config['website']
-        
         try:
             self._init_browser()
-            logger.info(f"Opening {website}")
+            self._log(f"Opening {self.vfs_url}")
             
-            # 1. 设置超时
             self.page.set.timeouts(page_load=30, script=30)
-            
-            # 2. 尝试打开页面
             try:
-                self.page.get(website, retry=0, timeout=30)
+                self.page.get(self.vfs_url, retry=0, timeout=30)
             except Exception:
-                # 超时强制停止,防止卡死
-                logger.warning(f"Page load timed out (Stopped manually). Checking URL...")
+                logger.warning("Page load timed out. Stopping loading manually.")
                 self.page.stop_loading()
             
-            # 1. 过盾
             cf_token = None
             cf_bypasser = CloudflareBypasser(self.page, log=True)
             
             for _ in range(40):
                 time.sleep(1)
-                
-                current_url = self.page.url
-                if "page-not-found" in current_url:
-                    logger.error(f"❌ [BLOCKED] Redirected to 'Page Not Found' during check. Aborting.")
+                if "page-not-found" in self.page.url:
+                    logger.error("❌ [BLOCKED] Redirected to 'Page Not Found'. Aborting.")
                     return False
-                
-                # 如果页面标题变成 403 Forbidden
                 if "403" in self.page.title and "Just a moment" not in self.page.title:
-                    logger.error(f"❌ [BLOCKED] 403 Forbidden detected. Aborting.")
+                    logger.error("❌ [BLOCKED] 403 Forbidden detected. Aborting.")
                     return False
                 
                 self._handle_cookie_banner()
                 
-                # 尝试获取 Token
                 try:
                     ele = self.page.ele('@name=cf-turnstile-response')
                     if ele and ele.value:
                         cf_token = ele.value
-                        if cf_token:
-                            break
-                except:
-                    pass
+                        break
+                except: pass
                 
-                # 尝试点击
-                try:
-                    cf_bypasser.click_verification_button(is_dfs=False)
-                except:
-                    pass
+                try: cf_bypasser.click_verification_button(is_dfs=False)
+                except: pass
             
             if not cf_token:
                 raise BizLogicError("Failed to obtain initial Cloudflare token")
 
-            # 2. 构造注册请求
             post_data = {
                 'emailid': account['username'],
                 'password': VFSHelper.encrypt_password(account['password']),
@@ -456,48 +455,43 @@ class VFSRegistrationBot:
                 'clientsource': VFSHelper.get_client_source(),
             }
 
-            logger.info(f"Submitting registration for {account['username']}")
+            self._log(f"Submitting registration for {account['username']}")
+            
+            country_code = account.get('country_code', '').lower()
+            if country_code == 'chn':
+                api_domain = 'https://lift-apicn.vfsglobal.com'
+            else:
+                api_domain = 'https://lift-api.vfsglobal.com'
+                
+            register_endpoint = f"{api_domain}/user/registration"
+            self._log(f"Using API Endpoint: {register_endpoint}")
+
             resp = self._perform_js_fetch(
                 'POST', 
-                'https://lift-api.vfsglobal.com/user/registration', 
+                register_endpoint,
                 headers=headers, 
                 json_data=post_data
             )
-            logger.info(f"Registration response: {resp.text}")
+            
+            self._log(f'register resp={resp.text}')
             resp_data = resp.json()
+            
             if resp_data.get("code") == "200":
-                logger.info("Registration API success. Waiting for email...")
-                
+                self._log("Registration API success. Waiting for email...")
                 activate_link = self._wait_for_activation_link(account['username'])
+                
                 if activate_link:
-                    logger.info(f"Activating account: {activate_link}")
-                    # 在当前浏览器上下文中打开链接,保持环境一致性
-                    # === 关键步骤:打开新标签页并验证结果 ===
-                    # 打开新标签页
+                    self._log(f"Activating account: {activate_link}")
                     activate_tab = self.page.new_tab(activate_link)
-                    
                     try:
-                        # 等待页面加载并查找 "Activation Successful" 文本
-                        # timeout=30 表示最多等待 30 秒
-                        logger.info("Waiting for 'Activation Successful' message on page...")
-                        
-                        # DrissionPage 查找包含特定文本的元素
                         success_ele = activate_tab.ele('Activation Successful', timeout=30)
-                        
                         if success_ele:
-                            logger.info(f"✅ Account {account['username']} activated successfully (Verified).")
+                            logger.info(f"✅ Account {account['username']} activated successfully.")
                             return True
                         else:
-                            # 如果没找到成功提示,尝试读取页面内容找错误原因
-                            body_text = activate_tab.ele('tag:body').text[:200]
-                            logger.error(f"Activation verification failed. Page text: {body_text}")
+                            logger.error("Activation verification failed. Success text not found.")
                             return False
-                            
-                    except Exception as e:
-                        logger.error(f"Error checking activation status: {e}")
-                        return False
                     finally:
-                        # 无论成功失败,关闭激活标签页,切回主标签
                         activate_tab.close()
                 else:
                     logger.error("Timeout waiting for activation email.")
@@ -506,75 +500,117 @@ class VFSRegistrationBot:
 
         except Exception as e:
             logger.error(f"Registration process exception: {e}", exc_info=True)
-        finally:
-            if self.page:
-                try:
-                    self.page.quit()
-                except:
-                    pass
         return False
+    
+    def cleanup(self):
+        self._log("Cleaning up resources...")
+        if self.page:
+            try: self.page.quit()
+            except: pass
+        if self.tunnel:
+            try: self.tunnel.stop()
+            except: pass
+        if os.path.exists(self.workspace):
+            time.sleep(1)
+            shutil.rmtree(self.workspace, ignore_errors=True)
+
+
+# ==========================================
+# 主流程 & 命令行参数解析
+# ==========================================
+def parse_arguments():
+    parser = argparse.ArgumentParser(description="VFS Global 自动注册机器人")
+    
+    # 核心参数
+    parser.add_argument('--target', type=int, default=20, help="需要注册的目标账号数量")
+    parser.add_argument('--vfs-url', type=str, required=True, help="VFS 注册页面的完整 URL, 如: https://visa.vfsglobal.com/chn/en/aut/register")
+    parser.add_argument('--proxy-pool', type=str, default='proxy-cheap', help="使用的代理池名称 (在 proxies.json 中定义)")
+    
+    # 账号生成相关参数
+    parser.add_argument('--account-prefix', type=str, default="cn_at", help="生成的邮箱前缀")
+    parser.add_argument('--account-pool', type=str, default="cn.at.sentinel", help="上报到服务端的账号池名称")
+    parser.add_argument('--phone-country', type=int, default=86, help="生成的手机号国家代码")
+    parser.add_argument('--email-domain', type=str, default="text.skin", help="邮箱域名")
+    
+    # API 与 上报配置
+    parser.add_argument('--upload-url', type=str, default="https://api.text.skin/api/account/add", help="账号上报服务器 URL")
+    parser.add_argument('--upload-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="账号上报服务器 API Token")
+    parser.add_argument('--email-token', type=str, default="tok_e946329a60ff45ba807f3f41b0e8b7fc", help="获取邮件的 API Token")
+    parser.add_argument('--master-email', type=str, default="hujiarui8@gmail.com", help="接收验证邮件的主邮箱")
+
+    return parser.parse_args()
 
-# --- 主流程 ---
 
-def generate_account_details(config):
-    """生成账号数据字典"""
-    account_prefix = config.get('account_prefix', 'vfs')
-    pool_name = config.get('pool_name', 'vfs')
+def generate_account_details(args):
+    """根据命令行参数和 URL 生成账号数据字典"""
+    # 从 URL 中提取 from_country 和 to_country
+    match = re.search(r'vfsglobal\.com/([^/]+)/[^/]+/([^/]+)/register', args.vfs_url)
+    if not match:
+        raise ValueError(f"无法从 URL 解析国家代码: {args.vfs_url}")
+        
+    from_country = match.group(1)
+    to_country = match.group(2)
+    
     rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
-    username = f"{account_prefix}_{rand_suffix}@{config['email_domain']}.com"
-    phone = VFSHelper.generate_mobile_number(config['phone_country_code'])
+    username = f"{args.account_prefix}_{rand_suffix}@{args.email_domain}"
+    phone = VFSHelper.generate_mobile_number(args.phone_country)
     
     return {
-        'pool_name': pool_name,
-        'country_code': config['country_code'],
-        'mission_code': config['mission_code'],
-        'phone_country_code': config['phone_country_code'],
+        'pool_name': args.account_pool,
+        'country_code': from_country,
+        'mission_code': to_country,
+        'phone_country_code': args.phone_country,
         'phone_number': phone,
         'username': username,
         'password': VFSHelper.generate_password(),
     }
 
+
 def main():
-    # 配置
-    config = {
-        "pool_name": "ie.nl.sentinel",
-        "account_prefix": "ie_nl",
-        "email_domain": "gmail-app", 
-        "master_email": "visafly666@gmail.com",
-        "proxy_url": "http://127.0.0.1:7890",
-        "target_count": 10,
-        "phone_country_code": 353,
-        "country_code": "irl",
-        "mission_code": "nld",
-        "website": "https://visa.vfsglobal.com/irl/en/nld/register",
-    }
+    args = parse_arguments()
+    proxies = load_proxies(args.proxy_pool)
+    logger.info(f"[*] 成功加载代理数量: {len(proxies)} 个")
     
-    bot = VFSRegistrationBot(config)
     success_accounts = []
+    logger.info(">>> Starting Registration Bot <<<")
     
-    print(">>> Starting Registration Bot <<<")
-    
-    while len(success_accounts) < config['target_count']:
-        account = generate_account_details(config)
-        logger.info(f"Processing Account: {account['username']}")
-        
-        is_success = bot.register(account)
+    while len(success_accounts) < args.target:
+        proxy_config = random.choice(proxies)
+        account_detail = generate_account_details(args)
         
-        if is_success:
-            success_accounts.append(account)
-            logger.info(f"Progress: {len(success_accounts)}/{config['target_count']}")
+        logger.info(f"Processing Account: {account_detail['username']}")
+        bot = None
+        try:
+            bot = VFSRegistrationBot(
+                vfs_url=args.vfs_url,
+                proxy=proxy_config,
+                email_api_token=args.email_token,
+                master_email=args.master_email
+            )
+            is_success = bot.register(account_detail)
             
-            upload_account_to_server(account)
-            # 保存结果到文件,防止中途退出丢失
-            with open("registered_accounts.json", "w", encoding='utf-8') as f:
-                json.dump(success_accounts, f, indent=4, ensure_ascii=False)
-        else:
-            logger.warning("Retrying with new account details...")
+            if is_success:
+                success_accounts.append(account_detail)
+                logger.info(f"Progress: {len(success_accounts)}/{args.target}")
+                
+                upload_account_to_server(account_detail, args.upload_url, args.upload_token)
+                
+                # 实时保存结果,防止中途退出丢失
+                with open("registered_accounts.json", "w", encoding='utf-8') as f:
+                    json.dump(success_accounts, f, indent=4, ensure_ascii=False)
+            else:
+                logger.warning("Registration failed. Retrying with new account details...")
 
-        # 稍微暂停,避免请求过于频繁
-        time.sleep(5)
+            time.sleep(5)  # 避免请求过于频繁
+            
+        except Exception as e:
+            logger.error(f"[ERROR] 注册发生致命错误 | 代理 IP: {proxy_config.get('ip')} | 异常信息: {e}")
+            
+        finally:
+            if bot:
+                bot.cleanup()
 
-    print(">>> All tasks completed <<<")
+    logger.info(f">>> All tasks completed. Registered {len(success_accounts)} accounts. <<<")
 
 if __name__ == "__main__":
     main()