jerry 1 mesiac pred
rodič
commit
8ad13bcf01
4 zmenil súbory, kde vykonal 126 pridanie a 80 odobranie
  1. 18 19
      booker_order.py
  2. 103 56
      plugins/tls_plugin.py
  3. 4 4
      toolkit/vs_cloud_api.py
  4. 1 1
      vs_types.py

+ 18 - 19
booker_order.py

@@ -54,24 +54,24 @@ class OrderBookerGCO:
     def _get_redis_key(self, routing_key: str) -> str:
         return f"vs:signal:{routing_key}"
 
-    def _safe_return_task(self, order_id: str, reason: str = ""):
+    def _safe_return_task(self, task_id: int, reason: str = ""):
         """安全地将订单归还给云端队列,防止复活已被取消或已抢成功的订单"""
-        if not order_id:
+        if not task_id:
             return
             
         try:
-            task_data = VSCloudApi.Instance().get_vas_task(order_id)
+            task_data = VSCloudApi.Instance().get_vas_task(task_id)
             if not task_data:
-                self._log(f"Task {order_id} not found in cloud, cannot return.")
+                self._log(f"Task={task_id} not found in cloud, cannot return.")
                 return
-            task_id = task_data['id']
+            order_id = task_data['order_id']
             current_status = task_data.get('status', '')
             # 如果订单已经被客户取消,或者已经成功,绝对不能还回队列!
             if current_status in ['cancelled', 'grabbed', 'success']:
-                self._log(f"Task {order_id} is already '{current_status}'. Skipping return.")
+                self._log(f"Task={task_id} is already '{current_status}'. Skipping return.")
                 return
                 
-            self._log(f"Returning task {order_id} to queue. Reason: {reason}")
+            self._log(f"Returning task={task_id} to queue. Reason: {reason}")
             VSCloudApi.Instance().return_vas_task_to_queue(task_id)
             
         except Exception as ex:
@@ -100,10 +100,10 @@ class OrderBookerGCO:
                         healthy_tasks.append(t)
                     else:
                         dead_tasks.append(t)
-                        self._log(f"♻️ Instance {t.task_ref} unhealthy, marking for removal.")
+                        self._log(f"♻️ Instance for task={t.task_ref} unhealthy, marking for removal.")
                 except Exception as e:
                     dead_tasks.append(t)
-                    self._log(f"♻️ Instance {t.task_ref} keep-alive failed: {e}, marking for removal.")
+                    self._log(f"♻️ Instance for task={t.task_ref} keep-alive failed: {e}, marking for removal.")
             
             with self.m_lock:
                 self.m_tasks = [t for t in self.m_tasks if t in healthy_tasks]
@@ -160,19 +160,19 @@ class OrderBookerGCO:
                 time.sleep(2)
 
     def _execute_book_job(self, task: Task, query_result: VSQueryResult):
-        order_id = task.task_ref
-        if not order_id:
+        task_id = task.task_ref
+        if not task_id:
             return
 
         try:
-            task_data = VSCloudApi.Instance().get_vas_task(order_id)
+            task_data = VSCloudApi.Instance().get_vas_task(task_id)
             if not task_data or task_data.get('status') in ['grabbed', 'cancelled']:
-                self._log(f"Bound Task {order_id} is no longer valid or already processed. Removing instance.")
+                self._log(f"Bound Task={task_id} is no longer valid or already processed. Removing instance.")
                 with self.m_lock:
                     if task in self.m_tasks: self.m_tasks.remove(task)
                 return
             
-            task_id = task_data.get('id')
+            order_id = task_data.get('order_id')
             user_input = task_data.get('user_inputs', {})
 
             book_res = task.instance.book(query_result, user_input)
@@ -219,7 +219,7 @@ class OrderBookerGCO:
             
         def _job():
             success = False
-            order_id = None
+            task_id = None
             try:
                 queue_name = f"auto.{target_routing_key}"
                 task_data = VSCloudApi.Instance().get_vas_task_pop(queue_name)
@@ -227,7 +227,6 @@ class OrderBookerGCO:
                     return 
                 
                 task_id = task_data['id']
-                order_id = task_data['order_id']
                 user_inputs = task_data.get('user_inputs', {})
                 
                 plg_cfg = VSPlgConfig()
@@ -261,7 +260,7 @@ class OrderBookerGCO:
                             instance=instance,
                             qw_cfg=self.m_cfg.query_wait,
                             next_run=time.time(), 
-                            task_ref=order_id,
+                            task_ref=task_id,
                             acceptable_routing_keys=acceptable_keys, 
                             source_queue=target_routing_key,
                             book_allowed=True
@@ -280,7 +279,7 @@ class OrderBookerGCO:
                     self.m_pending_order_by_queue[target_routing_key] = max(0, self.m_pending_order_by_queue[target_routing_key] - 1)
                 
                 # 创建/登录失败,调用安全归还函数
-                if not success and order_id is not None:
-                    self._safe_return_task(order_id, reason="Instance spawn/login failed")
+                if not success and task_id is not None:
+                    self._safe_return_task(task_id, reason="Instance spawn/login failed")
                     
         ThreadPool.getInstance().enqueue(_job)

+ 103 - 56
plugins/tls_plugin.py

@@ -279,6 +279,7 @@ class TlsPlugin(IVSPlg):
             self._log("Waiting for redirect...")
             self.page.wait.url_change('login-actions', exclude=True, timeout=45)
             
+            time.sleep(3)
             # 检查是否失败
             if "login-actions" in self.page.url or "auth" in self.page.url:
                 err = "Unknown Login Error"
@@ -356,7 +357,9 @@ 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! -> {available}")
         else:
+            self._log("No slots available.")
             res.success = False
             res.availability_status = AvailabilityStatus.NoneAvailable
         return res
@@ -377,29 +380,26 @@ class TlsPlugin(IVSPlg):
             target_labels.append('pta')
 
         # 获取所有可用的日期字符串用于过滤
-        available_dates_str = [
+        available_dates_str =[
             da.date.strftime("%Y-%m-%d")
             for da in slot_info.availability if da.date
         ]
         
-        # 1. 过滤出符合用户日期范围要求的日期
+        # ---------------------------------------------------------
+        # 第一步:过滤出符合用户日期范围要求的日期,并随机选择一个 slot
+        # ---------------------------------------------------------
         valid_dates_list = self._filter_dates(available_dates_str, exp_start, exp_end)
         if not valid_dates_list:
             raise NotFoundError(message="No dates match user constraints")
         
-        # --- 修改部分:计算所有可行的 slot ---
-        all_possible_slots = []
-        
+        all_possible_slots =[]
         for da in slot_info.availability:
             if not da.date:
                 continue
                 
             date_str = da.date.strftime("%Y-%m-%d")
-            
-            # 如果该日期在用户过滤后的合法日期列表中
             if date_str in valid_dates_list:
                 for t in da.times:
-                    # 检查标签是否符合要求
                     if t.label in target_labels:
                         all_possible_slots.append({
                             "date": date_str,
@@ -410,21 +410,76 @@ class TlsPlugin(IVSPlg):
         if not all_possible_slots:
             raise NotFoundError(message="No suitable slot found (after label filtering)")
 
-        # 随机选择一个 slot
         selected_slot = random.choice(all_possible_slots)
         selected_date = selected_slot["date"]
-        selected_time = selected_slot["time_obj"]  # 这是一个 TimeSlot 对象
+        selected_time = selected_slot["time_obj"]  # TimeSlot 对象
         selected_label = selected_slot["label"]
 
         self._log(f"Found {len(all_possible_slots)} valid slots. Randomly selected: {selected_date} {selected_time.time}")
-        # --- 修改结束 ---
-
-        # 2. 解决 ReCaptcha V3
-        page_url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking?location={apt_config["code"]}&month={selected_date[:7]}'
         
+        # 基础 URL 和路由状态 (Next.js 专用)
+        base_url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
+        router_state = f'%5B%22%22%2C%7B%22children%22%3A%5B%5B%22lang%22%2C%22en-us%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%5B%22groupId%22%2C%22{group_num}%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%22workflow%22%2C%7B%22children%22%3A%5B%22appointment-booking%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D'
+
+        # ---------------------------------------------------------
+        # 第二步:调用 getBasketCost 获取订单金额 (预定前置条件)
+        # ---------------------------------------------------------
+        self._log("Fetching basket cost...")
+        getBasketCost_ACTION_ID = "40124cc90acef520d4fd2daf60ad3c8e21fc2c11d8"
+        
+        payload =[{
+            "groupId": str(group_num),
+            "lang": "en-us",
+            "labels": [selected_label]
+        }]
+        body_data_str = json.dumps(payload)
+        
+        getBasketCost_js_script = f"""
+        const url = "{base_url}";
+        const headers = {{
+            'Next-Action': '{getBasketCost_ACTION_ID}',
+            'Next-Router-State-Tree': decodeURIComponent('{router_state}'),
+            'Accept': 'text/x-component',
+            'Accept-Language': 'zh-CN,zh;q=0.9',
+            'Content-Type': 'text/plain;charset=UTF-8'
+        }};
+        const bodyData = '{body_data_str}';
+        
+        return fetch(url, {{ method: 'POST', headers: headers, body: bodyData }})
+            .then(async response => {{
+                const text = await response.text();
+                const headers = {{}};
+                response.headers.forEach((value, key) => headers[key] = value);
+                return {{ status: response.status, body: text, headers: headers, url: response.url }};
+            }}).catch(err => {{
+                return {{ status: 0, body: err.toString(), headers: {{}}, url: url }};
+            }});
+        """
+        
+        cost_res_dict = self.page.run_js(getBasketCost_js_script)
+        cost_resp = BrowserResponse(cost_res_dict)
+
+        if cost_resp.status_code != 200:
+            self._log(f"Failed to get basket cost! Status: {cost_resp.status_code}. Aborting booking.")
+            return res
+
+        # 尝试解析并打印金额信息,例如总价和币种
+        cost_match = re.search(r'"total":"([^"]+)","currency":"([^"]+)"', cost_resp.text)
+        if cost_match:
+            total_cost, currency = cost_match.groups()
+            self._log(f"Basket cost checked successfully: {total_cost} {currency}")
+        else:
+            self._log("Basket cost checked successfully (could not parse exact amount).")
+
+        # ---------------------------------------------------------
+        # 第三步:解决 ReCaptcha V3
+        # ---------------------------------------------------------
+        self._log("Solving ReCaptcha V3...")
+        page_url = f'{base_url}?location={apt_config["code"]}&month={selected_date[:7]}'
         api_token = self.free_config.get("capsolver_key", "")
+        
         rc_params = {
-            "type": "ReCaptchaV3Task",
+            "type": "ReCaptchaV3TaskProxyLess",
             "page": page_url,
             "action": "book", 
             "siteKey": "6LcTpXcfAAAAAM3VojNhyV-F1z92ADJIvcSZ39Y9",
@@ -433,14 +488,14 @@ class TlsPlugin(IVSPlg):
         }
         g_token = self._solve_recaptcha(rc_params)
 
-        # 3. 构造 Next.js Payload
-        ACTION_ID = "6043cfd107081bc817cbb11a8c0db17d3a063401be"
-        url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
+        # ---------------------------------------------------------
+        # 第四步:提交正式的 Appointment Booking 请求
+        # ---------------------------------------------------------
+        self._log("Submitting booking request via JS Fetch...")
+        bookAppointment_ACTION_ID = "6043cfd107081bc817cbb11a8c0db17d3a063401be"
         
-        router_state = '%5B%22%22%2C%7B%22children%22%3A%5B%5B%22lang%22%2C%22en-us%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%5B%22groupId%22%2C%22'+str(group_num)+'%22%2C%22d%22%5D%2C%7B%22children%22%3A%5B%22workflow%22%2C%7B%22children%22%3A%5B%22appointment-booking%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D%7D%2Cnull%2Cnull%5D'
-
-        js_script = f"""
-        const url = "{url}";
+        bookAppointment_js_script = f"""
+        const url = "{base_url}";
         const formData = new FormData();
         
         formData.append('1_formGroupId', '{group_num}');
@@ -450,44 +505,37 @@ class TlsPlugin(IVSPlg):
         formData.append('1_date', '{selected_date}');
         formData.append('1_time', '{selected_time.time}');
         formData.append('1_appointmentLabel', '{selected_label}');
-        formData.append('1_captcha_token', '{g_token}');
+        formData.append('1_captchaToken', '{g_token}');
         formData.append('0', '[{{"status":"IDLE"}},"$K1"]');
         
         const headers = {{
-            'Next-Action': '{ACTION_ID}',
+            'Next-Action': '{bookAppointment_ACTION_ID}',
             'Next-Router-State-Tree': decodeURIComponent('{router_state}'),
             'Accept': 'text/x-component'
         }};
         
-        return fetch(url, {{
-            method: 'POST',
-            headers: headers,
-            body: formData
-        }}).then(async response => {{
-            const text = await response.text();
-            const headers = {{}};
-            response.headers.forEach((value, key) => headers[key] = value);
-            return {{
-                status: response.status,
-                body: text,
-                headers: headers,
-                url: response.url
-            }};
-        }}).catch(err => {{
-            return {{ status: 0, body: err.toString(), headers: {{}}, url: url }};
-        }});
+        return fetch(url, {{ method: 'POST', headers: headers, body: formData }})
+            .then(async response => {{
+                const text = await response.text();
+                const headers = {{}};
+                response.headers.forEach((value, key) => headers[key] = value);
+                return {{ status: response.status, body: text, headers: headers, url: response.url }};
+            }}).catch(err => {{
+                return {{ status: 0, body: err.toString(), headers: {{}}, url: url }};
+            }});
         """
         
-        self._log("Submitting booking request via JS Fetch...")
-        res_dict = self.page.run_js(js_script)
-        resp = BrowserResponse(res_dict)
+        book_res_dict = self.page.run_js(bookAppointment_js_script)
+        resp = BrowserResponse(book_res_dict)
 
-        # 4. 结果判定
+        # ---------------------------------------------------------
+        # 第五步:结果判定
+        # ---------------------------------------------------------
         if resp.status_code == 303 or (resp.status_code == 200 and "appointment-confirmation" in resp.url):
             self._log(f"Booking Success! URL: {resp.url}")
             res.success = True
             res.book_date = selected_date
-            res.book_time = selected_time
+            res.book_time = selected_time.time
             return res
 
         if resp.status_code == 200:
@@ -605,8 +653,7 @@ class TlsPlugin(IVSPlg):
                  raise BizLogicError(f"Network Error: {resp.text}")
             # TLS 业务错误
             raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
-        
-
+    
     def _refresh_firewall_session(self) -> bool:
         """
         主动刷新页面以触发 Cloudflare 挑战并尝试通过
@@ -652,14 +699,14 @@ class TlsPlugin(IVSPlg):
         if params.get("action"):
             task["pageAction"] = params.get("action")
             
-        if params.get("proxy"):
-            p = urlparse(params.get("proxy"))
-            task["proxyType"] = p.scheme
-            task["proxyAddress"] = p.hostname
-            task["proxyPort"] = p.port
-            if p.username:
-                task["proxyLogin"] = p.username
-                task["proxyPassword"] = p.password
+        # if params.get("proxy"):
+        #     p = urlparse(params.get("proxy"))
+        #     task["proxyType"] = p.scheme
+        #     task["proxyAddress"] = p.hostname
+        #     task["proxyPort"] = p.port
+        #     if p.username:
+        #         task["proxyLogin"] = p.username
+        #         task["proxyPassword"] = p.password
             
         # 注意:使用 DrissionPage 后,通常是 ProxyLess 模式
         # 除非你想让 Capsolver 也用同样的代理(通常不需要,除非风控极严)

+ 4 - 4
toolkit/vs_cloud_api.py

@@ -76,21 +76,21 @@ class VSCloudApi:
         else:
             raise BizLogicError(message=f"Get vas task pop biz error: {result.get('message')}")
     
-    def get_vas_task(self, order_id: str) -> dict:
+    def get_vas_task(self, task_id: str) -> dict:
         # 例如:请求 GET /api/v1/tasks/{task_id}
         # curl -X 'GET' \
         # 'http://45.137.220.138:8888/api/vas/task/get_by_order?order_id=ORD-20260306212306-7e604df8' \
         # -H 'accept: application/json' \
         # -H 'Authorization: Bearer tok_c9be86aa78274939a3c008db31ce9d22'
-        url = f"{self.base_url}/api/vas/task/get_by_order"
-        params = {"order_id": order_id}
+        url = f"{self.base_url}/api/vas/task/detail"
+        params = {"task_id": task_id}
         headers = self._get_headers()
         resp = self._perform_request('GET', url, params=params, headers=headers)
         result = resp.json()
         if result.get("code") == 0:
             return result.get("data", {})
         else:
-            raise BizLogicError(message=f"Get Task for order_id={order_id} error: {result.get('message')}")
+            raise BizLogicError(message=f"Get Task={task_id} error: {result.get('message')}")
 
     def update_vas_task(self, 
                         task_id: int, 

+ 1 - 1
vs_types.py

@@ -199,7 +199,7 @@ class Task(BaseModel):
     next_run: float = 0.0      
     book_allowed: bool = True
     # 订单模式下,保存绑定的 Task ID
-    task_ref: Optional[str] = None
+    task_ref: Optional[int] = None
     # 允许预订的 routing_key 列表
     acceptable_routing_keys: List[str] = Field(default_factory=list)
     # 来源标识(用于配额统计)