jerry 3 ماه پیش
والد
کامیت
218da4903d
12فایلهای تغییر یافته به همراه221 افزوده شده و 138 حذف شده
  1. 23 23
      config/groups.json
  2. 50 0
      config/proxies.json
  3. 10 5
      gco.py
  4. 7 6
      plugins/bls_plugin.py
  5. 10 8
      plugins/de_plugin.py
  6. 9 2
      plugins/de_plugin2.py
  7. 26 25
      plugins/ita_plugin.py
  8. 13 19
      plugins/tls_plugin.py
  9. 12 12
      plugins/tls_plugin2.py
  10. 21 21
      plugins/vfs_plugin.py
  11. 10 12
      plugins/vfs_plugin2.py
  12. 30 5
      vs_types.py

+ 23 - 23
config/groups.json

@@ -22,8 +22,8 @@
         },
         },
         "plugin_config": {
         "plugin_config": {
             "lib_path": "plugins",
             "lib_path": "plugins",
-            "plugin_name": "vfs_plugin",
-            "plugin_bin": "vfs_plugin.py",
+            "plugin_name": "vfs_plugin2",
+            "plugin_bin": "vfs_plugin2.py",
             "plugin_proto": "IVSPlg"
             "plugin_proto": "IVSPlg"
         },
         },
         "appointment_types": [
         "appointment_types": [
@@ -59,7 +59,7 @@
     {
     {
         "identifier": "VFS_SG_FR",
         "identifier": "VFS_SG_FR",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "sg_fr",
         "local_account_pool": "sg_fr",
         "need_proxy": true,
         "need_proxy": true,
@@ -189,11 +189,11 @@
     {
     {
         "identifier": "VFS_GB_IT",
         "identifier": "VFS_GB_IT",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_it",
         "local_account_pool": "gb_it",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "isp_proxy",
+        "proxy_pool": "iproyal-gb",
         "proxy_lock_interval": 5,
         "proxy_lock_interval": 5,
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 180,
         "account_login_interval": 180,
@@ -262,11 +262,11 @@
     {
     {
         "identifier": "VFS_GB_NL",
         "identifier": "VFS_GB_NL",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_nl",
         "local_account_pool": "gb_nl",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "isp_proxy",
+        "proxy_pool": "iproyal-gb",
         "proxy_lock_interval": 5,
         "proxy_lock_interval": 5,
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 180,
         "account_login_interval": 180,
@@ -296,7 +296,7 @@
             },
             },
             {
             {
                 "weight": 10,
                 "weight": 10,
-                "routing_key": "slot.man.it.tourist",
+                "routing_key": "slot.man.nl.tourist",
                 "city": "Manchester",
                 "city": "Manchester",
                 "visa_type": "Tourist",
                 "visa_type": "Tourist",
                 "country": "Netherlands"
                 "country": "Netherlands"
@@ -320,7 +320,7 @@
                     "subcategory_name": "Tourism",
                     "subcategory_name": "Tourism",
                     "subcategory_code": "TA"
                     "subcategory_code": "TA"
                 },
                 },
-                "slot.man.it.tourist": {
+                "slot.man.nl.tourist": {
                     "center_name": "Netherlands Visa application centre - Manchester",
                     "center_name": "Netherlands Visa application centre - Manchester",
                     "address": "50 Devonshire Street North, M12 6JH",
                     "address": "50 Devonshire Street North, M12 6JH",
                     "vac_code": "NAKT",
                     "vac_code": "NAKT",
@@ -335,11 +335,11 @@
     {
     {
         "identifier": "VFS_GB_NO",
         "identifier": "VFS_GB_NO",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_no",
         "local_account_pool": "gb_no",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "isp_proxy",
+        "proxy_pool": "iproyal-gb",
         "proxy_lock_interval": 5,
         "proxy_lock_interval": 5,
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 180,
         "account_login_interval": 180,
@@ -376,8 +376,8 @@
             "country_name": "United Kingdom",
             "country_name": "United Kingdom",
             "culture_code": "en-US",
             "culture_code": "en-US",
             "language": "en",
             "language": "en",
-            "apt_configs": [
-                {
+            "apt_configs": {
+                "slot.lon.no.tourist": {
                     "center_name": "Norway Visa Application Centre, London",
                     "center_name": "Norway Visa Application Centre, London",
                     "address": "66 Wilson street, EC2A 2BT",
                     "address": "66 Wilson street, EC2A 2BT",
                     "vac_code": "NLON",
                     "vac_code": "NLON",
@@ -386,13 +386,13 @@
                     "subcategory_name": "Tourist Visa",
                     "subcategory_name": "Tourist Visa",
                     "subcategory_code": "TOU"
                     "subcategory_code": "TOU"
                 }
                 }
-            ]
+            }
         }
         }
     },
     },
     {
     {
         "identifier": "VFS_IE_AT",
         "identifier": "VFS_IE_AT",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_at",
         "local_account_pool": "ie_at",
         "need_proxy": true,
         "need_proxy": true,
@@ -449,7 +449,7 @@
     {
     {
         "identifier": "VFS_IE_DK",
         "identifier": "VFS_IE_DK",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_dk",
         "local_account_pool": "ie_dk",
         "need_proxy": true,
         "need_proxy": true,
@@ -506,7 +506,7 @@
     {
     {
         "identifier": "VFS_IE_FI",
         "identifier": "VFS_IE_FI",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_fi",
         "local_account_pool": "ie_fi",
         "need_proxy": true,
         "need_proxy": true,
@@ -563,7 +563,7 @@
     {
     {
         "identifier": "VFS_IE_HU",
         "identifier": "VFS_IE_HU",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_hu",
         "local_account_pool": "ie_hu",
         "need_proxy": true,
         "need_proxy": true,
@@ -620,7 +620,7 @@
     {
     {
         "identifier": "VFS_IE_IS",
         "identifier": "VFS_IE_IS",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_is",
         "local_account_pool": "ie_is",
         "need_proxy": true,
         "need_proxy": true,
@@ -677,7 +677,7 @@
     {
     {
         "identifier": "BLS_IE_ES",
         "identifier": "BLS_IE_ES",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "ie_es",
         "local_account_pool": "ie_es",
         "need_proxy": true,
         "need_proxy": true,
@@ -734,7 +734,7 @@
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_es",
         "local_account_pool": "gb_es",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "iproyal",
+        "proxy_pool": "iproyal-gb",
         "proxy_lock_interval": 5,
         "proxy_lock_interval": 5,
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 30,
         "account_login_interval": 30,
@@ -783,11 +783,11 @@
     {
     {
         "identifier": "TLS_GB_FR",
         "identifier": "TLS_GB_FR",
         "debug": false,
         "debug": false,
-        "enable": false,
+        "enable": true,
         "need_account": true,
         "need_account": true,
         "local_account_pool": "gb_fr",
         "local_account_pool": "gb_fr",
         "need_proxy": true,
         "need_proxy": true,
-        "proxy_pool": "isp_proxy",
+        "proxy_pool": "iproyal-gb",
         "proxy_lock_interval": 5,
         "proxy_lock_interval": 5,
         "target_instances": 1,
         "target_instances": 1,
         "account_login_interval": 30,
         "account_login_interval": 30,

+ 50 - 0
config/proxies.json

@@ -195,6 +195,56 @@
             "username": "14ae212b29a2a"
             "username": "14ae212b29a2a"
         }
         }
     ],
     ],
+    "iproyal-gb": [
+        {
+            "id": 1,
+            "ip": "91.236.217.55",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        },
+        {
+            "id": 2,
+            "ip": "91.236.216.188",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        },
+        {
+            "id": 3,
+            "ip": "91.236.217.206",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        },
+        {
+            "id": 4,
+            "ip": "91.236.217.133",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        },
+        {
+            "id": 5,
+            "ip": "185.54.14.49",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        },
+        {
+            "id": 6,
+            "ip": "192.177.31.137",
+            "port": 12323,
+            "scheme": "http",
+            "username": "14ae6e75feb01",
+            "password": "6952ea93f8"
+        }
+    ],
     "iproyal": [
     "iproyal": [
         {
         {
             "id": 100021,
             "id": 100021,

+ 10 - 5
gco.py

@@ -15,6 +15,7 @@ from toolkit.account_manager import AccountManager
 from toolkit.proxy_manager import ProxyManager 
 from toolkit.proxy_manager import ProxyManager 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
 from toolkit.vs_cloud_api import VSCloudApi
+import traceback
 
 
 
 
 class GCO:
 class GCO:
@@ -153,17 +154,20 @@ class GCO:
                     )
                     )
                     # 这里的 task 充当了“哨兵”的角色
                     # 这里的 task 充当了“哨兵”的角色
                     result = task.instance.query(apt_type)
                     result = task.instance.query(apt_type)
+                    result.apt_type = apt_type 
                     VSCloudApi.Instance().slot_refresh_success(
                     VSCloudApi.Instance().slot_refresh_success(
                         apt_type.routing_key
                         apt_type.routing_key
                     )
                     )
                     if result.success:
                     if result.success:
                         self._log(f"🔥 Slot Found by [{task.instance.get_group_id()}]! Triggering BATCH BOOKING for {len(tasks_to_process)} workers.")
                         self._log(f"🔥 Slot Found by [{task.instance.get_group_id()}]! Triggering BATCH BOOKING for {len(tasks_to_process)} workers.")
                        
                        
-                        query_payload = result.to_snapshot_payload()
-                        query_payload["website"] = self.m_cfg.website
-                        query_payload["snapshot_source"] = 'worker'
-                        query_payload["snapshot_at"] = datetime.now(timezone.utc).isoformat()
-                        VSCloudApi.Instance().slot_snapshot_report(query_payload)
+                        # 上报Slot Snapshot
+                        # query_payload = result.to_snapshot_payload()
+                        # query_payload["website"] = self.m_cfg.website
+                        # query_payload["snapshot_source"] = 'worker'
+                        # query_payload["snapshot_at"] = datetime.now(timezone.utc).isoformat()
+                        # VSCloudApi.Instance().slot_snapshot_report(query_payload)
+                        
                         # === [核心修改]:一人发现,全员出击 ===
                         # === [核心修改]:一人发现,全员出击 ===
                         # 1. 准备并发任务
                         # 1. 准备并发任务
                         # 我们使用刚刚快照的 tasks_to_process,或者重新获取一次全量列表
                         # 我们使用刚刚快照的 tasks_to_process,或者重新获取一次全量列表
@@ -196,6 +200,7 @@ class GCO:
                         self._log(f"Query done by {task.instance.get_group_id()}, No availability")
                         self._log(f"Query done by {task.instance.get_group_id()}, No availability")
 
 
                 except Exception as e:
                 except Exception as e:
+                    traceback.print_exc()
                     self._log(f"Exception during query: {e}")
                     self._log(f"Exception during query: {e}")
                     if apt_type:
                     if apt_type:
                         VSCloudApi.Instance().slot_refresh_fail(
                         VSCloudApi.Instance().slot_refresh_fail(

+ 7 - 6
plugins/bls_plugin.py

@@ -198,9 +198,7 @@ class BlsPlugin(IVSPlg):
     # 2. 查询流程 (Query)
     # 2. 查询流程 (Query)
     # =========================================================================
     # =========================================================================
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
-        res = VSQueryResult()
-        res.apt_type = apt_type
-        
+        res = VSQueryResult()        
         apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
         apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
         domain = self.free_config.get("domain")
         domain = self.free_config.get("domain")
 
 
@@ -263,11 +261,13 @@ class BlsPlugin(IVSPlg):
             if dates:
             if dates:
                 res.success = True
                 res.success = True
                 res.availability_status = AvailabilityStatus.Available
                 res.availability_status = AvailabilityStatus.Available
-                res.earliest_date = dates[0]
+                earliest_date = dates[0]
+                earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
+                res.earliest_date = earliest_dt
 
 
                 res.availability = [
                 res.availability = [
                     DateAvailability(
                     DateAvailability(
-                        date=d,
+                        date=datetime.strptime(d, "%Y-%m-%d"),
                         times=[],
                         times=[],
                     )
                     )
                     for d in dates
                     for d in dates
@@ -344,7 +344,8 @@ class BlsPlugin(IVSPlg):
         ma_form['EmailVerificationCode'] = otp_code
         ma_form['EmailVerificationCode'] = otp_code
 
 
         # 3.4 锁定时间 (简单随机)
         # 3.4 锁定时间 (简单随机)
-        target_date = slot_info.earliest_date
+        target_dt = slot_info.earliest_date
+        target_date = target_dt.strftime("%Y-%m-%d")
         # Query Slots in Day
         # Query Slots in Day
         slot_url = f"https://{domain}/Global/blsappointment/GetAvailableSlotsByDate"
         slot_url = f"https://{domain}/Global/blsappointment/GetAvailableSlotsByDate"
         # 构造复杂的 query params... 省略部分非关键参数
         # 构造复杂的 query params... 省略部分非关键参数

+ 10 - 8
plugins/de_plugin.py

@@ -144,7 +144,6 @@ class DePlugin(IVSPlg):
         查询可用日期 (/getdate)
         查询可用日期 (/getdate)
         """
         """
         res = VSQueryResult()
         res = VSQueryResult()
-        res.apt_type = apt_type
         # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
         # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         max_retries = self.free_config.get("slot_query_max_retries", 2)
         max_retries = self.free_config.get("slot_query_max_retries", 2)
@@ -185,15 +184,13 @@ class DePlugin(IVSPlg):
         if dates:
         if dates:
             res.success = True
             res.success = True
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
-
+            earliest_date = dates[0]
+            earliest_dt = datetime.strptime(earliest_date, "%d-%m-%Y")
             # Visametric 返回 DD-MM-YYYY → 标准化为 YYYY-MM-DD
             # Visametric 返回 DD-MM-YYYY → 标准化为 YYYY-MM-DD
-            res.earliest_date = to_yyyymmdd(dates[0], "%d-%m-%Y")
+            res.earliest_date = earliest_dt
 
 
             res.availability = [
             res.availability = [
-                DateAvailability(
-                    date=to_yyyymmdd(d, "%d-%m-%Y"),
-                    times=[],
-                )
+                DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
                 for d in dates
                 for d in dates
             ]
             ]
 
 
@@ -214,7 +211,12 @@ class DePlugin(IVSPlg):
         exp_start = user_inputs.get('expected_start_date', '')
         exp_start = user_inputs.get('expected_start_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         
         
-        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
+        available_dates_str = [
+            da.date.strftime("%Y-%m-%d")
+            for da in slot_info.availability
+        ]
+        
+        valid_dates = self._filter_dates(available_dates_str, exp_start, exp_end)
         if not valid_dates:
         if not valid_dates:
             raise NotFoundError(message="No dates match user constraints")
             raise NotFoundError(message="No dates match user constraints")
         
         

+ 9 - 2
plugins/de_plugin2.py

@@ -280,9 +280,11 @@ class DePlugin2(IVSPlg):
         if dates:
         if dates:
             res.success = True
             res.success = True
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
-            res.earliest_date = to_yyyymmdd(dates[0], "%d-%m-%Y")
+            earliest_date = dates[0]
+            earliest_dt = datetime.strptime(earliest_date, "%d-%m-%Y")
+            res.earliest_date = earliest_dt
             res.availability = [
             res.availability = [
-                DateAvailability(date=to_yyyymmdd(d, "%d-%m-%Y"), times=[])
+                DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
                 for d in dates
                 for d in dates
             ]
             ]
         else:
         else:
@@ -296,6 +298,11 @@ class DePlugin2(IVSPlg):
         exp_start = user_inputs.get('expected_start_date', '')
         exp_start = user_inputs.get('expected_start_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         exp_end = user_inputs.get('expected_end_date', '')
         
         
+        available_dates_str = [
+            da.date.strftime("%Y-%m-%d")
+            for da in slot_info.availability
+        ]
+        
         valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
         valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
         if not valid_dates:
         if not valid_dates:
             raise NotFoundError("No dates match constraints")
             raise NotFoundError("No dates match constraints")

+ 26 - 25
plugins/ita_plugin.py

@@ -314,32 +314,32 @@ class ItaPlugin(IVSPlg):
             valid_days = self._parse_valid_days(resp_cal.text)
             valid_days = self._parse_valid_days(resp_cal.text)
             self._log(f"Valid days for {date_str}: {valid_days}")
             self._log(f"Valid days for {date_str}: {valid_days}")
             
             
-            for day in valid_days:
-                # 查询具体 Slot
-                slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
-                slot_payload = {
-                    "selectedDay": day, # YYYY-MM-DD
-                    "idService": str(self._service_id)
-                }
-                resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
-                
-                time_slots = self._parse_time_slots(resp_slot.text)
-                if time_slots:
-                    res.success = True
-                    res.availability_status = AvailabilityStatus.Available
-                    res.earliest_date = day
+            if valid_dates:
+                res.success = True
+                res.availability_status = AvailabilityStatus.Available
+                earliest_date = valid_dates[0]
+                earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
+                res.earliest_date = earliest_dt
+                for day in valid_days:
+                    # 查询具体 Slot
+                    slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
+                    slot_payload = {
+                        "selectedDay": day, # YYYY-MM-DD
+                        "idService": str(self._service_id)
+                    }
+                    resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
                     
                     
-                    # 转换结构
+                    time_slots = self._parse_time_slots(resp_slot.text)
                     ts_list = []
                     ts_list = []
-                    for ts in time_slots:
-                        # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
-                        ts_list.append(TimeSlot(
-                            time=f"{ts['start']} - {ts['end']}",
-                            label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
-                        ))
-                    
-                    res.availability.append(DateAvailability(date=day, times=ts_list))
-        
+                    if time_slots:
+                        # 转换结构
+                        for ts in time_slots:
+                            # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
+                            ts_list.append(TimeSlot(
+                                time=f"{ts['start']} - {ts['end']}",
+                                label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
+                            ))
+                    res.availability.append(DateAvailability(date=datetime.strptime(day, "%d-%m-%Y"), times=ts_list))
         return res
         return res
 
 
     # -------------------------------------------------------------
     # -------------------------------------------------------------
@@ -352,7 +352,8 @@ class ItaPlugin(IVSPlg):
         if not slot_info.availability:
         if not slot_info.availability:
             raise NotFoundError("No slots to book")
             raise NotFoundError("No slots to book")
             
             
-        target_date = slot_info.availability[0].date
+        target_dt = slot_info.availability[0].date
+        target_date = target_dt.strftime("%Y-%m-%d")
         # 取第一个时间段
         # 取第一个时间段
         target_slot = slot_info.availability[0].times[0]
         target_slot = slot_info.availability[0].times[0]
         slot_id = target_slot.label # 我们在 query 里把 ID 存在了 label
         slot_id = target_slot.label # 我们在 query 里把 ID 存在了 label

+ 13 - 19
plugins/tls_plugin.py

@@ -27,7 +27,6 @@ class TlsPlugin(IVSPlg):
         self.free_config: Dict[str, Any] = {}
         self.free_config: Dict[str, Any] = {}
         self.is_healthy = True
         self.is_healthy = True
         self.logger = None
         self.logger = None
-        # 会话相关
         self.session: Optional[requests.Session] = None
         self.session: Optional[requests.Session] = None
         self.travel_group: Optional[Dict] = None
         self.travel_group: Optional[Dict] = None
         self.user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
         self.user_agent: str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
@@ -152,7 +151,6 @@ class TlsPlugin(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
         res = VSQueryResult()
         res.success = False
         res.success = False
-        res.apt_type = apt_type
         apt_config = self.free_config.get('apt_config', {})
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
@@ -197,26 +195,18 @@ class TlsPlugin(IVSPlg):
 
 
         if available:
         if available:
             res.success = True
             res.success = True
+            earliest_date = available[0]["date"]
+            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
-            res.earliest_date = available[0]["date"]
-
-            date_map: dict[str, list[TimeSlot]] = {}
-
+            res.earliest_date = earliest_dt
+            date_map: dict[datetime, list[TimeSlot]] = {}
             for s in available:
             for s in available:
-                d = s["date"]
-
-                date_map.setdefault(d, []).append(
-                    TimeSlot(
-                        time=s["time"],
-                        label=str(s.get("label", "")),
-                    )
+                date_str = s["date"]
+                dt = datetime.strptime(date_str, "%Y-%m-%d")
+                date_map.setdefault(dt, []).append(
+                    TimeSlot(time=s["time"], label=str(s.get("label", "")))
                 )
                 )
-
-            res.availability = [
-                DateAvailability(date=d, times=slots)
-                for d, slots in date_map.items()
-            ]
-
+            res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
         else:
         else:
             res.success = False
             res.success = False
             res.availability_status = AvailabilityStatus.NoneAvailable
             res.availability_status = AvailabilityStatus.NoneAvailable
@@ -242,6 +232,10 @@ class TlsPlugin(IVSPlg):
         if support_pta:
         if support_pta:
             target_labels.append('pta')
             target_labels.append('pta')
         
         
+        available_dates_str = [
+            da.date.strftime("%Y-%m-%d")
+            for da in slot_info.availability
+        ]
         valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
         valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
         if not valid_dates:
         if not valid_dates:
             raise NotFoundError(message="No dates match user constraints")
             raise NotFoundError(message="No dates match user constraints")

+ 12 - 12
plugins/tls_plugin2.py

@@ -275,7 +275,6 @@ class TlsPlugin2(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
         res = VSQueryResult()
         res.success = False
         res.success = False
-        res.apt_type = apt_type
         apt_config = self.free_config.get('apt_config', {})
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
@@ -301,20 +300,18 @@ class TlsPlugin2(IVSPlg):
         target_labels = self.free_config.get("target_labels", ["", "pta"])
         target_labels = self.free_config.get("target_labels", ["", "pta"])
         # 根据配置过滤
         # 根据配置过滤
         available = [s for s in all_slots if s.get("label") in target_labels]
         available = [s for s in all_slots if s.get("label") in target_labels]
-
-        res.city = self.free_config.get('city', '')
-        res.country = self.free_config.get('country', '')
-        res.visa_type = self.free_config.get('visa_type', '')
-        res.routing_key = self.free_config.get('routing_key', '')
         
         
         if available:
         if available:
             res.success = True
             res.success = True
+            earliest_date = available[0]["date"]
+            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
-            res.earliest_date = available[0]["date"]
-            date_map: dict[str, list[TimeSlot]] = {}
+            res.earliest_date = earliest_dt
+            date_map: dict[datetime, list[TimeSlot]] = {}
             for s in available:
             for s in available:
-                d = s["date"]
-                date_map.setdefault(d, []).append(
+                date_str = s["date"]
+                dt = datetime.strptime(date_str, "%Y-%m-%d")
+                date_map.setdefault(dt, []).append(
                     TimeSlot(time=s["time"], label=str(s.get("label", "")))
                     TimeSlot(time=s["time"], label=str(s.get("label", "")))
                 )
                 )
             res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
             res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
@@ -338,8 +335,11 @@ class TlsPlugin2(IVSPlg):
         target_labels = ['']
         target_labels = ['']
         if support_pta:
         if support_pta:
             target_labels.append('pta')
             target_labels.append('pta')
-        
-        valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
+        available_dates_str = [
+            da.date.strftime("%Y-%m-%d")
+            for da in slot_info.availability
+        ]
+        valid_dates = self._filter_dates(available_dates_str, exp_start, exp_end)
         if not valid_dates:
         if not valid_dates:
             raise NotFoundError(message="No dates match user constraints")
             raise NotFoundError(message="No dates match user constraints")
         
         

+ 21 - 21
plugins/vfs_plugin.py

@@ -207,27 +207,27 @@ class VfsPlugin(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         """查询可预约 Slot"""
         """查询可预约 Slot"""
         result = VSQueryResult()
         result = VSQueryResult()
-        apt_config = self.free_config.get("app_configs", {}).get(apt_type.routing_key)
+        apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
         self._fetch_configurations(apt_config)
         self._fetch_configurations(apt_config)
-        earliest_date = self._query_earliest_slot(apt_config)
-        result.success = False
-        result.availability_status = AvailabilityStatus.NoneAvailable
-        result.apt_type = apt_type
-        if earliest_date:
-            result.success = True
-
-            if "WaitList" in earliest_date:
-                result.availability_status = AvailabilityStatus.Waitlist
+        try:
+            query_result = self._query_earliest_slot(apt_config)
+            result.success = False
+            result.availability_status = AvailabilityStatus.NoneAvailable
+            if query_result:
+                result.success = True
+                if "WaitList" in query_result:
+                    result.availability_status = AvailabilityStatus.Waitlist
+                else:
+                    earliest_dt = datetime.strptime(query_result, "%Y-%m-%d")
+                    result.availability_status = AvailabilityStatus.Available
+                    result.earliest_date = earliest_dt
+                    result.availability = [DateAvailability(date=earliest_dt, times=[])]
+                    self._log(f"Slot Found! -> {query_result}")
             else:
             else:
-                result.availability_status = AvailabilityStatus.Available
-                result.earliest_date = earliest_date
-
-                result.availability = [
-                    DateAvailability(
-                        date=earliest_date,
-                        times=[],
-                    )
-                ]
+                self._log("No slots available.")
+        except Exception as e:
+            self._log(f"Query Error: {e}")
+            raise e
         return result
         return result
 
 
     def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
     def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
@@ -240,9 +240,9 @@ class VfsPlugin(IVSPlg):
         res = VSBookResult()
         res = VSBookResult()
         app_type = slot_info.apt_type
         app_type = slot_info.apt_type
         
         
-        from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
+        from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         
         
-        apt_config = self.free_config.get("app_configs", {}).get(app_type.routing_key)
+        apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
         
         
         if not apt_config:
         if not apt_config:
             raise NotFoundError(message="Book: Config missing.")
             raise NotFoundError(message="Book: Config missing.")

+ 10 - 12
plugins/vfs_plugin2.py

@@ -397,24 +397,22 @@ class VfsPlugin2(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         """查询可预约 Slot"""
         """查询可预约 Slot"""
         result = VSQueryResult()
         result = VSQueryResult()
-        apt_config = self.free_config.get("app_configs", {}).get(apt_type.routing_key)
-        
+        apt_config = self.free_config.get("apt_configs", {}).get(apt_type.routing_key)
         try:
         try:
             self._fetch_configurations(apt_config)
             self._fetch_configurations(apt_config)
-            earliest_date = self._query_earliest_slot(apt_config)
-            
+            query_result = self._query_earliest_slot(apt_config)
             result.success = False
             result.success = False
             result.availability_status = AvailabilityStatus.NoneAvailable
             result.availability_status = AvailabilityStatus.NoneAvailable
-            result.apt_type = apt_config
-            if earliest_date:
+            if query_result:
                 result.success = True
                 result.success = True
-                if "WaitList" in earliest_date:
+                if "WaitList" in query_result:
                     result.availability_status = AvailabilityStatus.Waitlist
                     result.availability_status = AvailabilityStatus.Waitlist
                 else:
                 else:
+                    earliest_dt = datetime.strptime(query_result, "%Y-%m-%d")
                     result.availability_status = AvailabilityStatus.Available
                     result.availability_status = AvailabilityStatus.Available
-                    result.earliest_date = earliest_date
-                    result.availability = [DateAvailability(date=earliest_date, times=[])]
-                    self._log(f"Slot Found! Date: {earliest_date}")
+                    result.earliest_date = earliest_dt
+                    result.availability = [DateAvailability(date=earliest_dt, times=[])]
+                self._log(f"Slot Found! -> {query_result}")
             else:
             else:
                 self._log("No slots available.")
                 self._log("No slots available.")
                 
                 
@@ -927,9 +925,9 @@ class VfsPlugin2(IVSPlg):
         res = VSBookResult()
         res = VSBookResult()
         app_type = slot_info.apt_type
         app_type = slot_info.apt_type
         # 如果没有 earliest_date,默认从今天开始
         # 如果没有 earliest_date,默认从今天开始
-        from_date = slot_info.earliest_date if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
+        from_date = slot_info.earliest_date.strftime("%Y-%m-%d") if slot_info.earliest_date else datetime.now().strftime("%Y-%m-%d")
         
         
-        apt_config = self.free_config.get("app_configs", {}).get(app_type.routing_key)
+        apt_config = self.free_config.get("apt_configs", {}).get(app_type.routing_key)
         
         
         if not apt_config:
         if not apt_config:
             raise NotFoundError(message="Book: Config missing for this routing key.")
             raise NotFoundError(message="Book: Config missing for this routing key.")

+ 30 - 5
vs_types.py

@@ -1,5 +1,6 @@
 # vs_types.py
 # vs_types.py
 import json
 import json
+from datetime import datetime
 from pydantic import BaseModel, Field
 from pydantic import BaseModel, Field
 from pydantic.generics import GenericModel
 from pydantic.generics import GenericModel
 from enum import Enum, auto
 from enum import Enum, auto
@@ -190,7 +191,7 @@ class TimeSlot(BaseModel):
     label: str = ""
     label: str = ""
 
 
 class DateAvailability(BaseModel):
 class DateAvailability(BaseModel):
-    date: str = ""
+    date: Optional[datetime] = None
     times: List[TimeSlot] = Field(default_factory=list)
     times: List[TimeSlot] = Field(default_factory=list)
 
 
 class VSQueryResult(BaseModel):
 class VSQueryResult(BaseModel):
@@ -198,13 +199,37 @@ class VSQueryResult(BaseModel):
     apt_type: AppointmentType = Field(default_factory=AppointmentType)
     apt_type: AppointmentType = Field(default_factory=AppointmentType)
     
     
     availability_status: AvailabilityStatus = AvailabilityStatus.NoneAvailable
     availability_status: AvailabilityStatus = AvailabilityStatus.NoneAvailable
-    earliest_date: str = ""
+    earliest_date: Optional[datetime] = None
     availability: List[DateAvailability] = Field(default_factory=list)
     availability: List[DateAvailability] = Field(default_factory=list)
     
     
+    model_config = {
+        "validate_assignment": True
+    }
+    
     def to_snapshot_payload(self) -> dict:
     def to_snapshot_payload(self) -> dict:
         """
         """
         直接用于 slot_snapshot/report
         直接用于 slot_snapshot/report
         """
         """
+        def format_avail_item(item: DateAvailability) -> dict:
+            # 基础字典
+            data = {}
+            
+            # 处理日期:转为 YYYY-MM-DD 字符串
+            if item.date:
+                # 如果是 datetime 对象,先转 date() 去掉时分秒,再转 ISO 字符串
+                # 或者直接 strftime("%Y-%m-%d")
+                data["date"] = item.date.strftime("%Y-%m-%d")
+            else:
+                data["date"] = None
+            
+            # 处理 times:只有当列表不为空时才放入字典
+            # 这能解决 "times": [] 这种多余字段的问题
+            if item.times:
+                # 假设 TimeSlot 里的字段是简单的,直接 dump 即可
+                data["times"] = [t.model_dump() for t in item.times]
+                
+            return data
+        
         return {
         return {
             "routing_key": self.apt_type.routing_key,
             "routing_key": self.apt_type.routing_key,
             "country": self.apt_type.country,
             "country": self.apt_type.country,
@@ -212,11 +237,11 @@ class VSQueryResult(BaseModel):
             "visa_type": self.apt_type.visa_type,
             "visa_type": self.apt_type.visa_type,
             "availability_status": self.availability_status.value,
             "availability_status": self.availability_status.value,
             "earliest_date": (
             "earliest_date": (
-                self.earliest_date.isoformat()
+                self.earliest_date.strftime("%Y-%m-%d")
                 if self.earliest_date
                 if self.earliest_date
                 else None
                 else None
             ),
             ),
-            "availability": [a.model_dump() for a in self.availability],
+            "availability": [format_avail_item(a) for a in self.availability],
             "snapshot_source": "worker",
             "snapshot_source": "worker",
         }
         }
    
    
@@ -276,7 +301,7 @@ class ApiResponse(BaseModel, Generic[T]):
 def _to_serializable(data: Any):
 def _to_serializable(data: Any):
     """自动将 ORM / Pydantic / list 转换为可序列化对象"""
     """自动将 ORM / Pydantic / list 转换为可序列化对象"""
     if isinstance(data, BaseModel):
     if isinstance(data, BaseModel):
-        return data.dict()
+        return data.model_dump()
     if hasattr(data, "__table__"):  # SQLAlchemy ORM
     if hasattr(data, "__table__"):  # SQLAlchemy ORM
         return {
         return {
             c.name: getattr(data, c.name)
             c.name: getattr(data, c.name)