소스 검색

feat: update

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

+ 50 - 0
config/proxies.json

@@ -195,6 +195,56 @@
             "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": [
         {
             "id": 100021,

+ 10 - 5
gco.py

@@ -15,6 +15,7 @@ from toolkit.account_manager import AccountManager
 from toolkit.proxy_manager import ProxyManager 
 from toolkit.thread_pool import ThreadPool 
 from toolkit.vs_cloud_api import VSCloudApi
+import traceback
 
 
 class GCO:
@@ -153,17 +154,20 @@ class GCO:
                     )
                     # 这里的 task 充当了“哨兵”的角色
                     result = task.instance.query(apt_type)
+                    result.apt_type = apt_type 
                     VSCloudApi.Instance().slot_refresh_success(
                         apt_type.routing_key
                     )
                     if result.success:
                         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. 准备并发任务
                         # 我们使用刚刚快照的 tasks_to_process,或者重新获取一次全量列表
@@ -196,6 +200,7 @@ class GCO:
                         self._log(f"Query done by {task.instance.get_group_id()}, No availability")
 
                 except Exception as e:
+                    traceback.print_exc()
                     self._log(f"Exception during query: {e}")
                     if apt_type:
                         VSCloudApi.Instance().slot_refresh_fail(

+ 7 - 6
plugins/bls_plugin.py

@@ -198,9 +198,7 @@ class BlsPlugin(IVSPlg):
     # 2. 查询流程 (Query)
     # =========================================================================
     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)
         domain = self.free_config.get("domain")
 
@@ -263,11 +261,13 @@ class BlsPlugin(IVSPlg):
             if dates:
                 res.success = True
                 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 = [
                     DateAvailability(
-                        date=d,
+                        date=datetime.strptime(d, "%Y-%m-%d"),
                         times=[],
                     )
                     for d in dates
@@ -344,7 +344,8 @@ class BlsPlugin(IVSPlg):
         ma_form['EmailVerificationCode'] = otp_code
 
         # 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
         slot_url = f"https://{domain}/Global/blsappointment/GetAvailableSlotsByDate"
         # 构造复杂的 query params... 省略部分非关键参数

+ 10 - 8
plugins/de_plugin.py

@@ -144,7 +144,6 @@ class DePlugin(IVSPlg):
         查询可用日期 (/getdate)
         """
         res = VSQueryResult()
-        res.apt_type = apt_type
         # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
         consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
         max_retries = self.free_config.get("slot_query_max_retries", 2)
@@ -185,15 +184,13 @@ class DePlugin(IVSPlg):
         if dates:
             res.success = True
             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
-            res.earliest_date = to_yyyymmdd(dates[0], "%d-%m-%Y")
+            res.earliest_date = earliest_dt
 
             res.availability = [
-                DateAvailability(
-                    date=to_yyyymmdd(d, "%d-%m-%Y"),
-                    times=[],
-                )
+                DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
                 for d in dates
             ]
 
@@ -214,7 +211,12 @@ class DePlugin(IVSPlg):
         exp_start = user_inputs.get('expected_start_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:
             raise NotFoundError(message="No dates match user constraints")
         

+ 9 - 2
plugins/de_plugin2.py

@@ -280,9 +280,11 @@ class DePlugin2(IVSPlg):
         if dates:
             res.success = True
             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 = [
-                DateAvailability(date=to_yyyymmdd(d, "%d-%m-%Y"), times=[])
+                DateAvailability(date=datetime.strptime(d, "%d-%m-%Y"), times=[])
                 for d in dates
             ]
         else:
@@ -296,6 +298,11 @@ class DePlugin2(IVSPlg):
         exp_start = user_inputs.get('expected_start_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)
         if not valid_dates:
             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)
             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 = []
-                    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
 
     # -------------------------------------------------------------
@@ -352,7 +352,8 @@ class ItaPlugin(IVSPlg):
         if not slot_info.availability:
             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]
         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.is_healthy = True
         self.logger = None
-        # 会话相关
         self.session: Optional[requests.Session] = 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"
@@ -152,7 +151,6 @@ class TlsPlugin(IVSPlg):
     def query(self, apt_type: AppointmentType) -> VSQueryResult:
         res = VSQueryResult()
         res.success = False
-        res.apt_type = apt_type
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
@@ -197,26 +195,18 @@ class TlsPlugin(IVSPlg):
 
         if available:
             res.success = True
+            earliest_date = available[0]["date"]
+            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
             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:
-                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:
             res.success = False
             res.availability_status = AvailabilityStatus.NoneAvailable
@@ -242,6 +232,10 @@ class TlsPlugin(IVSPlg):
         if support_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)
         if not valid_dates:
             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:
         res = VSQueryResult()
         res.success = False
-        res.apt_type = apt_type
         apt_config = self.free_config.get('apt_config', {})
         group_num = self.travel_group['group_number']
         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"])
         # 根据配置过滤
         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:
             res.success = True
+            earliest_date = available[0]["date"]
+            earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
             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:
-                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", "")))
                 )
             res.availability = [DateAvailability(date=d, times=slots) for d, slots in date_map.items()]
@@ -338,8 +335,11 @@ class TlsPlugin2(IVSPlg):
         target_labels = ['']
         if support_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:
             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:
         """查询可预约 Slot"""
         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)
-        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:
-                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
 
     def book(self, slot_info: VSQueryResult, user_inputs) -> VSBookResult:
@@ -240,9 +240,9 @@ class VfsPlugin(IVSPlg):
         res = VSBookResult()
         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:
             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:
         """查询可预约 Slot"""
         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:
             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.availability_status = AvailabilityStatus.NoneAvailable
-            result.apt_type = apt_config
-            if earliest_date:
+            if query_result:
                 result.success = True
-                if "WaitList" in earliest_date:
+                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_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:
                 self._log("No slots available.")
                 
@@ -927,9 +925,9 @@ class VfsPlugin2(IVSPlg):
         res = VSBookResult()
         app_type = slot_info.apt_type
         # 如果没有 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:
             raise NotFoundError(message="Book: Config missing for this routing key.")

+ 30 - 5
vs_types.py

@@ -1,5 +1,6 @@
 # vs_types.py
 import json
+from datetime import datetime
 from pydantic import BaseModel, Field
 from pydantic.generics import GenericModel
 from enum import Enum, auto
@@ -190,7 +191,7 @@ class TimeSlot(BaseModel):
     label: str = ""
 
 class DateAvailability(BaseModel):
-    date: str = ""
+    date: Optional[datetime] = None
     times: List[TimeSlot] = Field(default_factory=list)
 
 class VSQueryResult(BaseModel):
@@ -198,13 +199,37 @@ class VSQueryResult(BaseModel):
     apt_type: AppointmentType = Field(default_factory=AppointmentType)
     
     availability_status: AvailabilityStatus = AvailabilityStatus.NoneAvailable
-    earliest_date: str = ""
+    earliest_date: Optional[datetime] = None
     availability: List[DateAvailability] = Field(default_factory=list)
     
+    model_config = {
+        "validate_assignment": True
+    }
+    
     def to_snapshot_payload(self) -> dict:
         """
         直接用于 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 {
             "routing_key": self.apt_type.routing_key,
             "country": self.apt_type.country,
@@ -212,11 +237,11 @@ class VSQueryResult(BaseModel):
             "visa_type": self.apt_type.visa_type,
             "availability_status": self.availability_status.value,
             "earliest_date": (
-                self.earliest_date.isoformat()
+                self.earliest_date.strftime("%Y-%m-%d")
                 if self.earliest_date
                 else None
             ),
-            "availability": [a.model_dump() for a in self.availability],
+            "availability": [format_avail_item(a) for a in self.availability],
             "snapshot_source": "worker",
         }
    
@@ -276,7 +301,7 @@ class ApiResponse(BaseModel, Generic[T]):
 def _to_serializable(data: Any):
     """自动将 ORM / Pydantic / list 转换为可序列化对象"""
     if isinstance(data, BaseModel):
-        return data.dict()
+        return data.model_dump()
     if hasattr(data, "__table__"):  # SQLAlchemy ORM
         return {
             c.name: getattr(data, c.name)