jerry 4 mesi fa
parent
commit
70ae104786
9 ha cambiato i file con 766 aggiunte e 633 eliminazioni
  1. 63 14
      config/groups.json
  2. 2 2
      group_coordinator.py
  3. 124 111
      plugins/bls_plugin.py
  4. 78 55
      plugins/de_plugin.py
  5. 3 3
      plugins/tls_plugin.py
  6. 0 312
      predict_server.py
  7. 259 0
      server.py
  8. 119 0
      toolkit/ocr_engine.py
  9. 118 136
      toolkit/vs_cloud_api.py

+ 63 - 14
config/groups.json

@@ -602,7 +602,7 @@
     },
     },
     {
     {
         "identifier": "BLS_IE_ES",
         "identifier": "BLS_IE_ES",
-        "enable": true,
+        "enable": false,
         "need_account": true,
         "need_account": true,
         "account_pool": "ie_es",
         "account_pool": "ie_es",
         "need_proxy": true,
         "need_proxy": true,
@@ -621,19 +621,65 @@
         },
         },
         "free_config": {
         "free_config": {
             "domain": "ireland.blsspainglobal.com", 
             "domain": "ireland.blsspainglobal.com", 
-            "ocr_service_url": "http://127.0.0.1:8085/predict/bls?model=pytorch", 
-            
-            "location": "Dublin",
-            "jurisdiction": null,
-            "visaType": "Schengen Visa/ Short Term Visa",
-            "visaSubType": "Tourist Visa",
-            "appointmentCategory": "Normal"
+            "local_service_url": "http://127.0.0.1:8085", 
+            "query_selector": {
+                "location": "Dublin",
+                "jurisdiction": null,
+                "visa_type": "Schengen Visa/ Short Term Visa",
+                "visa_subtype": "Tourist Visa",
+                "appointment_type": "Individual",
+                "appointment_category": "Normal",
+                "mission_code": "EMBASSY_DUBLIN"
+            },
+            "city": "Dublin",
+            "country": "Spain",
+            "visa_type": "Tourist",
+            "routing_key": "slot.dub.es.tourist",
+            "website": "https://ireland.blsspainglobal.com/Global/bls/visatypeverification"
         }
         }
     },
     },
     {
     {
-        "identifier": "TLS_GB_FR",
+        "identifier": "BLS_GB_ES",
         "enable": false,
         "enable": false,
         "need_account": true,
         "need_account": true,
+        "account_pool": "gb_es",
+        "need_proxy": true,
+        "proxy_pool": "local",
+        "target_instances": 1,
+        "query_wait": {
+            "mode": 2,
+            "random_min": 60,
+            "random_max": 300
+        },
+        "plugin_config": {
+            "lib_path": "plugins",
+            "plugin_name": "bls_plugin",
+            "plugin_bin": "bls_plugin.py",
+            "plugin_proto": "IVSPlg"
+        },
+        "free_config": {
+            "domain": "uk.blsspainglobal.com", 
+            "local_service_url": "http://127.0.0.1:8085",
+            "query_selector": {
+                "location": "Dublin",
+                "jurisdiction": "Greater London",
+                "visa_type": "Short Term Visa(Maximum stay of 90 days)",
+                "visa_subtype": "Tourist Visa",
+                "appointment_type": "Individual",
+                "appointment_category": "Normal",
+                "mission_code": "LHR"
+            },
+            "city": "London",
+            "country": "Spain",
+            "visa_type": "Tourist",
+            "routing_key": "slot.lon.es.tourist",
+            "website": "https://uk.blsspainglobal.com/Global/bls/visatypeverification"
+        }
+    },
+    {
+        "identifier": "TLS_GB_FR",
+        "enable": true,
+        "need_account": true,
         "account_pool": "gb_fr",
         "account_pool": "gb_fr",
         "need_proxy": true,
         "need_proxy": true,
         "proxy_pool": "ireland_proxies",
         "proxy_pool": "ireland_proxies",
@@ -657,12 +703,10 @@
                 "mission": "fr",
                 "mission": "fr",
                 "city": "London"
                 "city": "London"
             },
             },
-            "embassy_code": "gbLON2fr",
-            "country_code": "gb",
-            "mission_code": "fr",
             "city": "London",
             "city": "London",
             "country": "France",
             "country": "France",
             "visa_type": "Tourist",
             "visa_type": "Tourist",
+            "routing_key": "slot.lon.fr.tourist",
             "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
             "capsolver_key": "CAP-5441DD341DD3CC2FAEF0BE6FE493EE9A",
             "interest_month": "02-2026",
             "interest_month": "02-2026",
             "target_labels": ["", "pta"],
             "target_labels": ["", "pta"],
@@ -691,8 +735,13 @@
         "free_config": {
         "free_config": {
             "verbose": 0,
             "verbose": 0,
             "base_url": "https://ie-appointment.visametric.com",
             "base_url": "https://ie-appointment.visametric.com",
-            "ocr_service_url": "http://127.0.0.1:8085/predict/visametric", 
-            "consularid": 1
+            "local_service_url": "http://127.0.0.1:8085",
+            "consularid": 1,
+            "city": "Dublin",
+            "country": "Germany",
+            "visa_type": "Tourist",
+            "routing_key": "slot.dub.de.tourist",
+            "website": "https://ie-appointment.visametric.com/en"
         }
         }
     }
     }
 ]
 ]

+ 2 - 2
group_coordinator.py

@@ -154,7 +154,7 @@ class GroupCoordinator:
                     else:
                     else:
                         VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Query failed, No availability found")
                         VSC_DEBUG("coordinator", f"[{self.m_cfg.identifier}] Query failed, No availability found")
                 except Exception as e:
                 except Exception as e:
-                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e.message}")
+                    VSC_ERROR("coordinator", f"[{self.m_cfg.identifier}] Exception during query: {e}")
 
 
                 # 计算下次运行时间
                 # 计算下次运行时间
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
                 # 如果刚刚触发了抢票(无论成功失败),建议强制加长一点冷却时间,防止反爬
@@ -376,7 +376,7 @@ class GroupCoordinator:
                 if task_id is not None:
                 if task_id is not None:
                     VSC_WARN("coordinator", f"[{inst.get_group_id()}] Returning Task {task_id} to queue (status=pending).")
                     VSC_WARN("coordinator", f"[{inst.get_group_id()}] Returning Task {task_id} to queue (status=pending).")
                     try:
                     try:
-                        VSCloudApi.Instance().update_vas_task(task_id, {"status": "pending"})
+                        VSCloudApi.Instance().return_vas_task_to_queue(task_id)
                     except Exception as ex:
                     except Exception as ex:
                         VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Failed to return task to queue: {ex}")
                         VSC_ERROR("coordinator", f"[{inst.get_group_id()}] Failed to return task to queue: {ex}")
 
 

+ 124 - 111
plugins/bls_plugin.py

@@ -22,7 +22,6 @@ from vs_plg import IVSPlg
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
 from toolkit.vs_cloud_api import VSCloudApi 
-from utils.browser_util import get_browser
 
 
 class BlsPlugin(IVSPlg):
 class BlsPlugin(IVSPlg):
     """
     """
@@ -40,8 +39,7 @@ class BlsPlugin(IVSPlg):
         self.is_healthy = True
         self.is_healthy = True
         
         
         # OCR 服务地址默认值
         # OCR 服务地址默认值
-        self.ocr_service_url = "http://127.0.0.1:8085/predict/bls?model=pytorch"
-        self.browser = get_browser()
+        self.local_service_url = ""
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
@@ -54,8 +52,8 @@ class BlsPlugin(IVSPlg):
             self.free_config = {}
             self.free_config = {}
         
         
         # 从配置中读取 OCR 服务地址,如果没有则使用默认
         # 从配置中读取 OCR 服务地址,如果没有则使用默认
-        if self.free_config.get("ocr_service_url"):
-            self.ocr_service_url = self.free_config["ocr_service_url"]
+        if self.free_config.get("local_service_url"):
+            self.local_service_url = self.free_config["local_service_url"]
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         return self.is_healthy
         return self.is_healthy
@@ -86,15 +84,18 @@ class BlsPlugin(IVSPlg):
         soup = BeautifulSoup(resp.text, 'html.parser')
         soup = BeautifulSoup(resp.text, 'html.parser')
         form_data = self._extract_hidden_fields(soup)
         form_data = self._extract_hidden_fields(soup)
         
         
+        
+        real_user = None
+        real_pass = None
+    
         # 解析动态 ID (UserId1, Password1 等)
         # 解析动态 ID (UserId1, Password1 等)
         for inp in soup.find_all('input'):
         for inp in soup.find_all('input'):
-            iid = inp.get('id', '')
-            if 'UserId' in iid and re.search(r'\d+', iid):
-                form_data["UserIdKey"] = iid # 暂存 Key
-                form_data["UserId"] = re.search(r'\d+', iid).group(0)
-            if 'Password' in iid and re.search(r'\d+', iid):
-                form_data["PasswordKey"] = iid # 暂存 Key
-                form_data["Password"] = re.search(r'\d+', iid).group(0)
+            name = inp.get('name', '')
+            if inp.has_attr('required'):
+                if 'UserId' in name:
+                    real_user = name
+                elif 'Password' in name:
+                    real_pass = name
         
         
         # 解析 data 参数 (用于验证码)
         # 解析 data 参数 (用于验证码)
         data_val = self._extract_js_var(resp.text, "iframeOpenUrl", r"data=([^']+)")
         data_val = self._extract_js_var(resp.text, "iframeOpenUrl", r"data=([^']+)")
@@ -108,11 +109,8 @@ class BlsPlugin(IVSPlg):
         payload["X-Requested-With"] = "XMLHttpRequest"
         payload["X-Requested-With"] = "XMLHttpRequest"
         payload["CaptchaData"] = captcha_token
         payload["CaptchaData"] = captcha_token
         # 填入账号密码
         # 填入账号密码
-        if "UserIdKey" in form_data:
-            payload[form_data["UserIdKey"]] = self.config.account.username
-        if "PasswordKey" in form_data:
-            payload[form_data["PasswordKey"]] = self.config.account.password
-        
+        payload[real_user] = self.config.account.username
+        payload[real_pass] = self.config.account.password
         login_resp = self._perform_request('POST', submit_url, data=payload, headers=headers)
         login_resp = self._perform_request('POST', submit_url, data=payload, headers=headers)
         if login_resp.json()['success']:
         if login_resp.json()['success']:
             return
             return
@@ -176,14 +174,20 @@ class BlsPlugin(IVSPlg):
             avail_json = json.loads(avail_str)
             avail_json = json.loads(avail_str)
             # 提取日期
             # 提取日期
             dates = [x['DateText'] for x in avail_json['ad'] if x['SingleSlotAvailable']]
             dates = [x['DateText'] for x in avail_json['ad'] if x['SingleSlotAvailable']]
-            
+            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 dates:
             if dates:
                 res.success = True
                 res.success = True
                 res.availability_status = AvailabilityStatus.Available
                 res.availability_status = AvailabilityStatus.Available
                 res.earliest_date = dates[0]
                 res.earliest_date = dates[0]
                 for d in dates:
                 for d in dates:
-                    da = VSQueryResult.DateAvailability(date=d)
-                    da.times.append(VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available"))
+                    da = VSQueryResult.DateAvailability()
+                    da.date = d
+                    da.times = [] 
+                    time_slot = VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available")
+                    da.times.append(time_slot)
                     res.availability.append(da)
                     res.availability.append(da)
             else:
             else:
                 res.success = False
                 res.success = False
@@ -239,7 +243,11 @@ class BlsPlugin(IVSPlg):
         otp_code = self._read_otp_email(wait_sec=30)
         otp_code = self._read_otp_email(wait_sec=30)
             
             
         # 验证 OTP
         # 验证 OTP
-        verify_payload = {"Code": otp_code, "Value": ma_form.get('EmailCode'), "Id": ma_form.get('Id')}
+        verify_payload = {
+            "Code": otp_code,
+            "Value": ma_form.get('EmailCode'),
+            "Id": ma_form.get('Id')
+        }
         
         
         headers['requestverificationtoken'] = req_token
         headers['requestverificationtoken'] = req_token
         v_resp = self._perform_request('POST', f"https://{domain}/Global/blsappointment/VerifyEmail", data=verify_payload, headers=headers)
         v_resp = self._perform_request('POST', f"https://{domain}/Global/blsappointment/VerifyEmail", data=verify_payload, headers=headers)
@@ -337,7 +345,7 @@ class BlsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        print(f'[perform request] {method} {url}')
+        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         VSC_INFO('bls_plg', resp.text)
         VSC_INFO('bls_plg', resp.text)
         if resp.status_code == 200:
         if resp.status_code == 200:
@@ -365,68 +373,54 @@ class BlsPlugin(IVSPlg):
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
             'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
         }
         }
         resp = self._perform_request("GET", url, headers=headers)
         resp = self._perform_request("GET", url, headers=headers)
+        soup = BeautifulSoup(resp.text, 'html.parser')
+        resp = requests.post(
+            f'{self.local_service_url}/browser/visable_captchas', 
+            data=resp.text, 
+            headers={"Content-Type": "text/plain"},
+            timeout=10
+        )
+        result = resp.json()
+        if result.get('status') != 'success':
+            raise BizLogicError(message='Broswer task failed')
         
         
-        with open("tmp.html", 'w') as f:
-            f.write(resp.text)
-        
+        numbers = result['data']['number']
+        image_ids = result['data']['image_ids']
         selected_ids = []
         selected_ids = []
-        html_file_path = Path("tmp.html").resolve()
-        file_url = f'file://{html_file_path}'
-        self.browser.get(file_url)
-        captions_ele = self.browser.ele('xpath://*[@id="captcha-main-div"]/div/div[1]', timeout=5)
-        if not captions_ele:
-            raise NotFoundError(message='Captions elements not found')
-        caption_eles = captions_ele.children()
-        caption_text = ''
-        for caption in caption_eles:
-            if not caption.states.is_covered:
-                caption_text = caption.text
-
-        numbers = re.findall(r'\d+', caption_text)[0]
-        captcha_images_ele = self.browser.ele('xpath://*[@id="captcha-main-div"]/div/div[2]')
-        captcha_image_eles = captcha_images_ele.children()
-        rect_dict = {}
-        for captcha_image in captcha_image_eles:
-            img = captcha_image.ele('.captcha-img')
-            if img.states.has_rect:
-                rect_dict[img._backend_id] = img.states.has_rect
-        for captcha_image in captcha_image_eles:
-            img = captcha_image.ele('.captcha-img')
-            if img.states.has_rect and img.states.is_covered == False:
-                img_src = img.attr('src')
-                if img_src and img_src.startswith('data:image'):
-                    base64_data = re.sub('^data:image/.+;base64,', '', img_src)
-                    img_bytes = base64.b64decode(base64_data)
-                    
-                    ocr_resp = requests.post(
-                        self.ocr_service_url, 
-                        data=img_bytes, 
-                        headers={"Content-Type": "application/octet-stream"},
-                        timeout=5
-                    )
-                    if ocr_resp.status_code == 200:
-                        res_json = ocr_resp.json()
-                        ocr_res = res_json.get('data', '').replace('$', '')[:3]
-                        
-                        VSC_INFO("bls_plg", f'ocr captcha id={captcha_image.attr("id")} result={ocr_res}, target={numbers}')
-                        
-                        if ocr_res == numbers:
-                            eid = captcha_image.attr('id')
-                            selected_ids.append(eid)
-                    else:
-                        raise BizLogicError(message='Captcha server response error')
-          
+        for sid in image_ids: 
+            div = soup.find("div", id=sid)
+            img = div.find("img")
+            src = img.get("src")
+            base64_data = src.split("base64,", 1)[1]
+            img_bytes = base64.b64decode(base64_data)
+            ocr_resp = requests.post(
+                f'{self.local_service_url}/predict/bls?model=pytorch', 
+                data=img_bytes, 
+                headers={"Content-Type": "application/octet-stream"},
+                timeout=5
+            )
+            if ocr_resp.status_code == 200:
+                res_json = ocr_resp.json()
+                ocr_res = res_json.get('data', '').replace('$', '')[:3]
+                VSC_INFO("bls_plg", f'ocr captcha id={sid} result={ocr_res}, target={numbers}')
+                if ocr_res == numbers:
+                    selected_ids.append(sid)
+            else:
+                raise BizLogicError(message='Captcha server response error')
         if not selected_ids:
         if not selected_ids:
             raise BizLogicError(message='Captcha selected ids is empty')
             raise BizLogicError(message='Captcha selected ids is empty')
-        VSC_INFO("bls_plg", f'select_ids={selected_ids}')
-        soup = BeautifulSoup(resp.text, 'html.parser')
+        
         # 3. 提交选中结果
         # 3. 提交选中结果
+        VSC_INFO("bls_plg", f'select_ids={selected_ids}')
         form = self._extract_hidden_fields(soup)
         form = self._extract_hidden_fields(soup)
         form['SelectedImages'] = ",".join(selected_ids)
         form['SelectedImages'] = ",".join(selected_ids)
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
         submit_url = f"https://{domain}/Global/{'CaptchaPublic' if data else 'NewCaptcha'}/SubmitCaptcha"
         headers["X-Requested-With"] = "XMLHttpRequest"
         headers["X-Requested-With"] = "XMLHttpRequest"
         resp = self._perform_request('POST', submit_url, headers=headers, data=form)
         resp = self._perform_request('POST', submit_url, headers=headers, data=form)
-        return resp.json()['captcha']
+        if data:
+            return resp.json()['captcha']
+        else:
+            return resp.json()['cd']
 
 
     def _extract_hidden_fields(self, soup) -> Dict:
     def _extract_hidden_fields(self, soup) -> Dict:
         params = {}
         params = {}
@@ -450,10 +444,10 @@ class BlsPlugin(IVSPlg):
         """
         """
         构造 VisaType 提交参数 (对应原代码 parse_visatype_form)
         构造 VisaType 提交参数 (对应原代码 parse_visatype_form)
         """
         """
-        # 1. 基础表单参数 (__RequestVerificationToken 等)
+        # 基础表单参数 (__RequestVerificationToken 等)
         params = self._extract_hidden_fields(soup)
         params = self._extract_hidden_fields(soup)
         
         
-        # 2. 提取页面中的 JS 数据变量
+        # 提取页面中的 JS 数据变量
         def get_js_data(var_name):
         def get_js_data(var_name):
             try:
             try:
                 # 匹配 var name = [...]; 结构
                 # 匹配 var name = [...]; 结构
@@ -464,86 +458,105 @@ class BlsPlugin(IVSPlg):
             except Exception as e:
             except Exception as e:
                 VSC_DEBUG("bls_plg", f"Failed to parse JS var {var_name}: {e}")
                 VSC_DEBUG("bls_plg", f"Failed to parse JS var {var_name}: {e}")
             return []
             return []
-
+        
+        # 读取配置
+        query_selector = self.free_config.get("query_selector", {})
+        cfg_jur = query_selector.get("jurisdiction")
+        cfg_loc = query_selector.get("location")
+        cfg_type = query_selector.get("visa_type")
+        cfg_subtype = query_selector.get("visa_subtype")
+        cfg_cat = query_selector.get("appointment_category")
+        
+        jur_value = None
+        loc_value = None
+        type_value = None
+        subtype_value = None
+        cat_value = None
+        
+        resp = requests.post(
+            f'{self.local_service_url}/browser/visatype_visable', 
+            data=html, 
+            headers={"Content-Type": "text/plain"},
+            timeout=10
+        )
+        result = resp.json()
+        if result.get('status') != 'success':
+            raise BizLogicError(message='Broswer task failed')
+        
+        jur_id = result['data']['jur_id']
+        loc_id = result['data']['loc_id']
+        type_id = result['data']['type_id']
+        subtype_id = result['data']['subtype_id']
+        cat_id = result['data']['cat_id']
+  
         jurisdiction_list = get_js_data("jurisdictionData")
         jurisdiction_list = get_js_data("jurisdictionData")
         location_list = get_js_data("locationData")
         location_list = get_js_data("locationData")
         visa_type_list = get_js_data("visaIdData")
         visa_type_list = get_js_data("visaIdData")
         visa_subtype_list = get_js_data("visasubIdData")
         visa_subtype_list = get_js_data("visasubIdData")
         app_category_list = get_js_data("AppointmentCategoryIdData")
         app_category_list = get_js_data("AppointmentCategoryIdData")
 
 
-        # 3. 读取配置
-        cfg_jur = self.free_config.get("jurisdiction")
-        cfg_loc = self.free_config.get("location")
-        cfg_type = self.free_config.get("visaType")
-        cfg_subtype = self.free_config.get("visaSubType")
-        cfg_cat = self.free_config.get("appointmentCategory", "Normal")
-
-        # 4. 匹配 ID
-        jur_id = None
-        loc_id = None
-        type_id = None
-        subtype_id = None
-        cat_id = None
-
+        # 4. 匹配 Value
         # (A) Appointment Category
         # (A) Appointment Category
         for item in app_category_list:
         for item in app_category_list:
             if item.get("Name") == cfg_cat:
             if item.get("Name") == cfg_cat:
-                cat_id = item.get("Id")
+                cat_value = item.get("Id")
                 break
                 break
         
         
         # (B) Jurisdiction (如果配置了)
         # (B) Jurisdiction (如果配置了)
         if cfg_jur and jurisdiction_list:
         if cfg_jur and jurisdiction_list:
             for item in jurisdiction_list:
             for item in jurisdiction_list:
                 if item.get("Name") == cfg_jur:
                 if item.get("Name") == cfg_jur:
-                    jur_id = item.get("Id")
+                    jur_value = item.get("Id")
                     break
                     break
 
 
         # (C) Location
         # (C) Location
         for item in location_list:
         for item in location_list:
             if item.get("Name") == cfg_loc:
             if item.get("Name") == cfg_loc:
-                loc_id = item.get("Id")
+                loc_value = item.get("Id")
                 break
                 break
         
         
         # (D) Visa Type (需匹配 LocationId)
         # (D) Visa Type (需匹配 LocationId)
-        if loc_id:
+        if loc_value:
             for item in visa_type_list:
             for item in visa_type_list:
                 # 比较 Name 和 LocationId
                 # 比较 Name 和 LocationId
-                if item.get("Name") == cfg_type and str(item.get("LocationId")) == str(loc_id):
-                    type_id = item.get("Id")
+                if item.get("Name") == cfg_type and str(item.get("LocationId")) == str(loc_value):
+                    type_value = item.get("Id")
                     break
                     break
         
         
         # (E) Visa SubType (需匹配 VisaType Value)
         # (E) Visa SubType (需匹配 VisaType Value)
-        if type_id:
+        if type_value:
             for item in visa_subtype_list:
             for item in visa_subtype_list:
                 # BLS 逻辑: visasubIdData 中的 Value 字段对应 VisaTypeId
                 # BLS 逻辑: visasubIdData 中的 Value 字段对应 VisaTypeId
-                if item.get("Name") == cfg_subtype and str(item.get("Value")) == str(type_id):
-                    subtype_id = item.get("Id")
+                if item.get("Name") == cfg_subtype and str(item.get("Value")) == str(type_value):
+                    subtype_value = item.get("Id")
                     break
                     break
 
 
         # 5. 构造动态参数 & 校验
         # 5. 构造动态参数 & 校验
-        if not cat_id:
+        if not cat_value:
             raise NotFoundError(message=f"Config: AppCategory '{cfg_cat}' not found")
             raise NotFoundError(message=f"Config: AppCategory '{cfg_cat}' not found")
-        params[f"AppointmentCategoryId{cat_id}"] = cat_id
+        params[f"AppointmentCategoryId{cat_id}"] = cat_value
 
 
         if cfg_jur:
         if cfg_jur:
-            if not jur_id:
+            if not jur_value:
                 raise NotFoundError(message=f"Config: Jurisdiction '{cfg_jur}' not found")
                 raise NotFoundError(message=f"Config: Jurisdiction '{cfg_jur}' not found")
-            params[f"JurisdictionId{jur_id}"] = jur_id
+            params[f"JurisdictionId{jur_id}"] = jur_value
 
 
-        if not loc_id:
+        if not loc_value:
             raise NotFoundError(message=f"Config: Location '{cfg_loc}' not found")
             raise NotFoundError(message=f"Config: Location '{cfg_loc}' not found")
-        params[f"Location{loc_id}"] = loc_id
+        params[f"Location{loc_id}"] = loc_value
 
 
-        if not type_id:
+        if not type_value:
             raise NotFoundError(message=f"Config: VisaType '{cfg_type}' not found for Loc '{cfg_loc}'")
             raise NotFoundError(message=f"Config: VisaType '{cfg_type}' not found for Loc '{cfg_loc}'")
-        params[f"VisaType{type_id}"] = type_id
+        params[f"VisaType{type_id}"] = type_value
 
 
-        if not subtype_id:
+        if not subtype_value:
             raise NotFoundError(message=f"Config: VisaSubType '{cfg_subtype}' not found")
             raise NotFoundError(message=f"Config: VisaSubType '{cfg_subtype}' not found")
-        params[f"VisaSubType{subtype_id}"] = subtype_id
+        params[f"VisaSubType{subtype_id}"] = subtype_value
 
 
         # 固定参数
         # 固定参数
-        params["AppointmentFor1"] = "Individual"
+        for k in list(params.keys()):
+            if k.startswith("AppointmentFor"):
+                params[k] = "Individual"
         
         
         # 6. 构造 ResponseData (行为轨迹模拟)
         # 6. 构造 ResponseData (行为轨迹模拟)
         # BLS 后端会校验这个字段,模拟用户选择下拉框的时间间隔
         # BLS 后端会校验这个字段,模拟用户选择下拉框的时间间隔

+ 78 - 55
plugins/de_plugin.py

@@ -12,13 +12,17 @@ from curl_cffi import requests, const
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup
 
 
 
 
-# 框架依赖
 from vs_plg import IVSPlg 
 from vs_plg import IVSPlg 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from vs_log_macros import VSC_INFO, VSC_ERROR, VSC_DEBUG, VSC_WARN 
 from toolkit.vs_cloud_api import VSCloudApi 
 from toolkit.vs_cloud_api import VSCloudApi 
 
 
 
 
+def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
+    # 转换日期到YYYY-MM-DD 固定格式
+    dt = datetime.strptime(data_str, date_str_format)
+    return dt.strftime("%Y-%m-%d")
+
 def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
 def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
     """
     """
     将邮箱域名替换为指定域名(默认 gmail-app.com)
     将邮箱域名替换为指定域名(默认 gmail-app.com)
@@ -53,7 +57,7 @@ class DePlugin(IVSPlg):
         self.email_val_control = ""
         self.email_val_control = ""
         
         
         # 默认 OCR 服务地址
         # 默认 OCR 服务地址
-        self.ocr_service_url = "http://127.0.0.1:8085/predict/visametric"
+        self.local_service_url = "http://127.0.0.1:8085"
 
 
     def get_group_id(self) -> str:
     def get_group_id(self) -> str:
         return self.group_id
         return self.group_id
@@ -68,8 +72,8 @@ class DePlugin(IVSPlg):
         if self.free_config.get("base_url"):
         if self.free_config.get("base_url"):
             self.base_url = self.free_config["base_url"].rstrip('/')
             self.base_url = self.free_config["base_url"].rstrip('/')
             
             
-        if self.free_config.get("ocr_service_url"):
-            self.ocr_service_url = self.free_config["ocr_service_url"]
+        if self.free_config.get("local_service_url"):
+            self.local_service_url = self.free_config["local_service_url"]
 
 
     def health_check(self) -> bool:
     def health_check(self) -> bool:
         return self.is_healthy
         return self.is_healthy
@@ -84,17 +88,9 @@ class DePlugin(IVSPlg):
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
             const.CurlOpt.MAXLIFETIME_CONN: 1800,
             const.CurlOpt.VERBOSE: False,
             const.CurlOpt.VERBOSE: False,
         }
         }
-        
-        proxy_url = ""
-        if self.config.proxy.ip:
-            s = self.config.proxy
-            if s.username:
-                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
-            else:
-                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
 
 
         self.session = requests.Session(
         self.session = requests.Session(
-            proxy=proxy_url,
+            proxy=self._get_proxy_url(),
             impersonate="chrome124",
             impersonate="chrome124",
             curl_options=curlopt,
             curl_options=curlopt,
             use_thread_local_curl=False,
             use_thread_local_curl=False,
@@ -129,7 +125,7 @@ class DePlugin(IVSPlg):
 
 
         # 3. 识别验证码
         # 3. 识别验证码
         resp = requests.post(
         resp = requests.post(
-            self.ocr_service_url, 
+            f'{self.local_service_url}/predict/visametric', 
             data=captcha_b64, 
             data=captcha_b64, 
             headers={"Content-Type": "application/octet-stream"},
             headers={"Content-Type": "application/octet-stream"},
             timeout=10
             timeout=10
@@ -152,7 +148,8 @@ class DePlugin(IVSPlg):
 
 
         # 构造 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)
+
         url = f"{self.base_url}/en/getdate"
         url = f"{self.base_url}/en/getdate"
         payload = {
         payload = {
             "consularid": consular_id,
             "consularid": consular_id,
@@ -163,25 +160,51 @@ class DePlugin(IVSPlg):
         }
         }
         
         
         default_headers = self._get_headers()
         default_headers = self._get_headers()
-        resp = self._perform_request('POST', url, data=payload, headers=default_headers)
+        default_headers['X-CSRF-TOKEN'] = self.csrf_token
+        
+        
+        for attempt in range(1, max_retries + 1):
+            try:
+                resp = self._perform_request('POST', url, data=payload, headers=default_headers)
+                break  # ✅ 请求成功,跳出重试循环
+
+            except PermissionDeniedError:
+                VSC_WARN(
+                    "de_plg",
+                    "[Visamtric] getdate blocked (403), attempt %d/%d",
+                    attempt, max_retries
+                )
+
+                # 最后一次就不再绕盾了
+                if attempt >= max_retries:
+                    raise PermissionDeniedError()
+
+                self._solve_cloudflare5S_challenge()
+                VSC_INFO("de_plg", "[Visamtric] Cloudflare bypass success, retrying...")
+                continue
 
 
         # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
         # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
         j = resp.json()
         j = resp.json()
         dates = j.get("getDateEnable", [])
         dates = j.get("getDateEnable", [])
         
         
+        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 dates:
         if dates:
             res.success = True
             res.success = True
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
-            # Visametric 返回 DD-MM-YYYY, 标准化为 DD/MM/YYYY
-            res.earliest_date = dates[0].replace("-", "/") 
-            
+            # Visametric 返回 DD-MM-YYYY, 标准化为 YYYY-MM-DD
+            res.earliest_date = to_yyyymmdd(dates[0], '%d-%m-%Y')
             for d in dates:
             for d in dates:
                 da = VSQueryResult.DateAvailability()
                 da = VSQueryResult.DateAvailability()
-                da.date = d.replace("-", "/")
-                da.times.append(VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available"))
+                da.date = to_yyyymmdd(d, '%d-%m-%Y')
+                da.times = [] 
+                time_slot = VSQueryResult.DateAvailability.TimeSlot(time="00:00", label="Available")
+                da.times.append(time_slot)
                 res.availability.append(da)
                 res.availability.append(da)
         else:
         else:
-            res.success = False
+            res.success = False # 获取成功,只是没号,所以 success 依然是 True
             res.availability_status = AvailabilityStatus.NoneAvailable
             res.availability_status = AvailabilityStatus.NoneAvailable
         return res
         return res
 
 
@@ -190,9 +213,8 @@ class DePlugin(IVSPlg):
         执行预约:选择日期 -> 选择时间 -> 发邮件 -> 填表 -> 提交
         执行预约:选择日期 -> 选择时间 -> 发邮件 -> 填表 -> 提交
         """
         """
         res = VSBookResult()
         res = VSBookResult()
-        
         # 1. 筛选日期
         # 1. 筛选日期
-        available_dates = [da.date.replace("/", "-") for da in slot_info.availability]
+        available_dates = [da.date for da in slot_info.availability]
         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', '')
         
         
@@ -254,6 +276,17 @@ class DePlugin(IVSPlg):
             "Referer": f"{self.base_url}/en/appointment-form", # 默认 Referer
             "Referer": f"{self.base_url}/en/appointment-form", # 默认 Referer
             "X-Requested-With": "XMLHttpRequest"
             "X-Requested-With": "XMLHttpRequest"
         }
         }
+        
+    def _get_proxy_url(self):
+        # 构造代理
+        proxy_url = ""
+        if self.config.proxy.ip:
+            s = self.config.proxy
+            if s.username:
+                proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
+            else:
+                proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
+        return proxy_url
 
 
     def _submit_captcha(self, code):
     def _submit_captcha(self, code):
         url = f"{self.base_url}/en/appointment-form"
         url = f"{self.base_url}/en/appointment-form"
@@ -279,11 +312,19 @@ class DePlugin(IVSPlg):
             
             
         if not self.personal_info_val:
         if not self.personal_info_val:
             raise NotFoundError(message="Personalinfo not found in captcha response")
             raise NotFoundError(message="Personalinfo not found in captcha response")
+        
+        soup = BeautifulSoup(resp.text, 'html.parser')
+        meta = soup.find('meta', {'name': 'csrf-token'})
+        if not meta:
+            raise NotFoundError(message='Missing csrf-token in html')
+        self.csrf_token = meta.get('content', '')
    
    
     def _get_slot_time(self, date) -> Optional[Dict]:
     def _get_slot_time(self, date) -> Optional[Dict]:
         url = f"{self.base_url}/en/senddate"
         url = f"{self.base_url}/en/senddate"
+        dt_m = datetime.strptime(date, "%Y-%m-%d")
+        converted_date = dt_m.strftime("%d-%m-%Y")
         payload = {
         payload = {
-            "fulldate": date,
+            "fulldate": converted_date,
             "totalperson": "1",
             "totalperson": "1",
             "set_new_consular_id": self.free_config.get("consularid", "1"),
             "set_new_consular_id": self.free_config.get("consularid", "1"),
             "set_new_exit_office_id": "1",
             "set_new_exit_office_id": "1",
@@ -443,7 +484,7 @@ class DePlugin(IVSPlg):
         """
         """
         根据用户的期望范围筛选可用日期
         根据用户的期望范围筛选可用日期
         
         
-        :param dates: API 返回的可用日期列表 (通常是 DD-MM-YYYY 或 DD/MM/YYYY)
+        :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
         :param start_str: 用户期望开始日期 (YYYY-MM-DD)
         :param start_str: 用户期望开始日期 (YYYY-MM-DD)
         :param end_str: 用户期望结束日期 (YYYY-MM-DD)
         :param end_str: 用户期望结束日期 (YYYY-MM-DD)
         :return: 符合要求的日期列表
         :return: 符合要求的日期列表
@@ -453,34 +494,16 @@ class DePlugin(IVSPlg):
             return dates
             return dates
             
             
         valid_dates = []
         valid_dates = []
-        try:
-            # 1. 解析用户期望的范围 (通常是 YYYY-MM-DD)
-            # 截取前10位以防带有时分秒
-            s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
-            e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
-            
-            for date_str in dates:
-                try:
-                    # 2. 解析 API 返回的日期
-                    # Visametric 通常返回 DD-MM-YYYY,但为了稳健,尝试两种常见分隔符
-                    # 替换 / 为 - 统一格式处理
-                    clean_date_str = date_str.replace("/", "-")
-                    curr_date = datetime.strptime(clean_date_str, "%d-%m-%Y")
-                    
-                    # 3. 比较范围 (闭区间)
-                    if s_date <= curr_date <= e_date:
-                        valid_dates.append(date_str)
-                except ValueError:
-                    # 如果某个日期格式解析失败,跳过该日期,不影响其他
-                    VSC_DEBUG("gmy_plg", f"Date parse error for slot: {date_str}")
-                    continue
-                    
-        except ValueError as e:
-            # 如果用户配置的日期格式不对,记录警告并返回所有日期(或者空,视业务需求)
-            # 这里选择返回所有日期,避免因配置错误导致一直无法下单
-            VSC_WARN("gmy_plg", f"User date range format error: {e}. Returning all slots.")
-            return dates
-
+        # 截取前10位以防带有时分秒
+        s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
+        e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
+        
+        for date_str in dates:
+            curr_date = datetime.strptime(date_str, "%Y-%m-%d")
+            # 比较范围 (闭区间)
+            if s_date <= curr_date <= e_date:
+                valid_dates.append(date_str)
+        random.shuffle(valid_dates)
         return valid_dates
         return valid_dates
     
     
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
     def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
@@ -489,7 +512,7 @@ class DePlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        print(f'[perform request] {method} {url}')
+        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         VSC_INFO('tls_plg', resp.text)
         VSC_INFO('tls_plg', resp.text)
         if resp.status_code == 200:
         if resp.status_code == 200:

+ 3 - 3
plugins/tls_plugin.py

@@ -192,8 +192,7 @@ class TlsPlugin(IVSPlg):
         res.city = self.free_config.get('city', '')
         res.city = self.free_config.get('city', '')
         res.country = self.free_config.get('country', '')
         res.country = self.free_config.get('country', '')
         res.visa_type = self.free_config.get('visa_type', '')
         res.visa_type = self.free_config.get('visa_type', '')
-        res.availability_status = AvailabilityStatus.NoneAvailable
-
+        res.routing_key = self.free_config.get('routing_key', '')
         if available:
         if available:
             res.success = True
             res.success = True
             res.availability_status = AvailabilityStatus.Available
             res.availability_status = AvailabilityStatus.Available
@@ -214,6 +213,7 @@ class TlsPlugin(IVSPlg):
                 res.availability.append(da)
                 res.availability.append(da)
         else:
         else:
             res.success = False
             res.success = False
+            res.availability_status = AvailabilityStatus.NoneAvailable
         return res
         return res
 
 
 
 
@@ -303,7 +303,7 @@ class TlsPlugin(IVSPlg):
         1. 发送 OPTIONS 请求
         1. 发送 OPTIONS 请求
         2. 发送实际请求
         2. 发送实际请求
         """
         """
-        print(f'[perform request] {method} {url}')
+        print(f'[perform request] {method} {url} {data} {json_data} {params}')
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
         VSC_INFO('tls_plg', resp.text)
         VSC_INFO('tls_plg', resp.text)
         if resp.status_code == 200:
         if resp.status_code == 200:

+ 0 - 312
predict_server.py

@@ -1,312 +0,0 @@
-import os
-# 屏蔽 ONNX Runtime 的警告日志
-os.environ["ORT_LOGGING_LEVEL"] = "3"
-
-import json
-import string
-import socket
-import traceback
-import io  # 新增
-from http.server import BaseHTTPRequestHandler, HTTPServer
-from io import BytesIO
-from collections import OrderedDict
-from urllib.parse import urlparse, parse_qs
-
-# 图像处理依赖
-import cv2
-import numpy as np
-from PIL import Image, ImageFilter # 新增 ImageFilter
-
-# 深度学习依赖
-import torch
-from torch import nn
-from torchvision import transforms
-
-# ddddocr 依赖
-try:
-    import ddddocr
-    HAS_DDDDOCR = True
-except ImportError:
-    print("[WARNING] ddddocr not installed. Run 'pip install ddddocr'")
-    HAS_DDDDOCR = False
-
-# ================= 核心优化:图像去噪 (BLS专用) =================
-def advanced_denoise(image_bytes):
-    """
-    针对 BLS 验证码的去噪流程
-    """
-    try:
-        nparr = np.frombuffer(image_bytes, np.uint8)
-        img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
-        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-        gray_blur = cv2.medianBlur(gray, 3)
-        binary = cv2.adaptiveThreshold(
-            gray_blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
-            cv2.THRESH_BINARY, 11, 2
-        )
-        contours, _ = cv2.findContours(255 - binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
-        clean_img = np.ones(binary.shape, dtype="uint8") * 255
-        for cnt in contours:
-            area = cv2.contourArea(cnt)
-            if 30 < area < 1000:
-                cv2.drawContours(clean_img, [cnt], -1, 0, -1)
-        return Image.fromarray(clean_img)
-    except Exception as e:
-        print(f"[Denoise] Error: {e}")
-        return Image.open(BytesIO(image_bytes))
-
-# ================= PyTorch 模型结构 =================
-class Model(nn.Module):
-    def __init__(self, n_classes, input_shape=(3, 64, 128)):
-        super(Model, self).__init__()
-        self.input_shape = input_shape
-        channels = [32, 64, 128, 256, 256]
-        layers = [2, 2, 2, 2, 2]
-        kernels = [3, 3, 3, 3, 3]
-        pools = [2, 2, 2, 2, (2, 1)]
-        modules = OrderedDict()
-        
-        def cba(name, in_channels, out_channels, kernel_size):
-            modules[f'conv{name}'] = nn.Conv2d(in_channels, out_channels, kernel_size,
-                                               padding=(1, 1) if kernel_size == 3 else 0)
-            modules[f'bn{name}'] = nn.BatchNorm2d(out_channels)
-            modules[f'relu{name}'] = nn.ReLU(inplace=True)
-        
-        last_channel = 3
-        for block, (n_channel, n_layer, n_kernel, k_pool) in enumerate(zip(channels, layers, kernels, pools)):
-            for layer in range(1, n_layer + 1):
-                cba(f'{block+1}{layer}', last_channel, n_channel, n_kernel)
-                last_channel = n_channel
-            modules[f'pool{block + 1}'] = nn.MaxPool2d(k_pool)
-        modules[f'dropout'] = nn.Dropout(0.25, inplace=True)
-        
-        self.cnn = nn.Sequential(modules)
-        self.lstm = nn.LSTM(input_size=self.infer_features(), hidden_size=128, num_layers=2, bidirectional=True)
-        self.fc = nn.Linear(in_features=256, out_features=n_classes)
-    
-    def infer_features(self):
-        x = torch.zeros((1,)+self.input_shape)
-        x = self.cnn(x)
-        x = x.reshape(x.shape[0], -1, x.shape[-1])
-        return x.shape[1]
-
-    def forward(self, x):
-        x = self.cnn(x)
-        x = x.reshape(x.shape[0], -1, x.shape[-1])
-        x = x.permute(2, 0, 1)
-        x, _ = self.lstm(x)
-        x = self.fc(x)
-        return x
-
-# ================= 引擎1: PyTorch =================
-class PyTorchEngine:
-    def __init__(self, model_path):
-        self.num_classes = 12
-        self.characters = '-' + string.digits + '$'
-        self.width = 150
-        self.hight = 80
-        self.model = Model(self.num_classes, input_shape=(3, self.hight, self.width))
-        
-        if os.path.exists(model_path):
-            self.model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
-            self.model.eval()
-            print(f"[PyTorch] Model loaded successfully from {model_path}")
-            self.ready = True
-        else:
-            print(f"[PyTorch] Warning: Model file not found at {model_path}")
-            self.ready = False
-
-        self.transforms_func = transforms.Compose([
-            transforms.Resize((self.hight, self.width)),
-            transforms.ToTensor()
-        ])
-        
-    def decode(self, sequence):
-        a = ''.join([self.characters[x] for x in sequence])
-        s = []
-        last = None
-        for x in a:
-            if x != last:
-                s.append(x)
-                last = x
-        s2 = ''.join([x for x in s if x != self.characters[0]])
-        return s2
-
-    def inference_bytes(self, image_bytes):
-        if not self.ready:
-            return "Error: Model not loaded"
-        try:
-            # === 恢复:直接使用 PIL 打开图片,移除 advanced_denoise ===
-            image = Image.open(BytesIO(image_bytes))
-            image = image.convert('RGB')
-            
-            if self.transforms_func is not None:
-                image = self.transforms_func(image)
-            
-            with torch.no_grad():
-                output = self.model(image.unsqueeze(0).cpu())
-                
-            output_argmax = output.detach().permute(1, 0, 2).argmax(dim=-1)
-            predict_label = self.decode(output_argmax[0])
-            return predict_label
-        except Exception as e:
-            print(f"[PyTorch] Inference error: {e}")
-            return ""
-
-# ================= 引擎2: DDDDOCR =================
-class DddOcrEngine:
-    def __init__(self):
-        if HAS_DDDDOCR:
-            self.ocr = ddddocr.DdddOcr(show_ad=False, beta=True)
-            print("[DDDDOCR] Initialized successfully")
-            self.ready = True
-        else:
-            print("[DDDDOCR] Library missing")
-            self.ready = False
-
-    def inference_bytes(self, image_bytes):
-        """ 原有的 VFCode 识别逻辑 """
-        if not self.ready:
-            return "Error: ddddocr not installed"
-        try:
-            # 1. VF 专用预处理
-            img_pil = advanced_denoise(image_bytes)
-            
-            # 2. 转 bytes 传给 ddddocr
-            img_byte_arr = BytesIO()
-            img_pil.save(img_byte_arr, format='PNG')
-            processed_bytes = img_byte_arr.getvalue()
-            
-            # 3. 识别
-            res = self.ocr.classification(processed_bytes)
-            return res
-        except Exception as e:
-            print(f"[DDDDOCR] Inference error: {e}")
-            return ""
-
-    def inference_captcha(self, image_bytes):
-        """ 
-        [新增] 适配你提供的预处理逻辑
-        路径: /predict/visametric
-        """
-        if not self.ready:
-            return "Error: ddddocr not installed"
-        try:
-            # 1. 打开图片
-            image = Image.open(io.BytesIO(image_bytes))
-
-            # 2. 自定义预处理: 灰度 -> 中值滤波 -> 二值化
-            gray_img = image.convert("L").filter(ImageFilter.MedianFilter(size=3))
-            binary_img = gray_img.point(lambda p: 255 if p > 128 else 0)
-
-            # 3. 转 bytes 并识别
-            with io.BytesIO() as img_buffer:
-                binary_img.save(img_buffer, format="PNG")
-                processed_bytes = img_buffer.getvalue()
-                return self.ocr.classification(processed_bytes)
-
-        except Exception as e:
-            print(f"[DDDDOCR-Captcha] Inference error: {e}")
-            return ""
-
-# ================= HTTP 处理 =================
-engines = {}
-
-class RequestHandler(BaseHTTPRequestHandler):
-    def _send_response(self, status, content_type, content):
-        self.send_response(status)
-        self.send_header('Content-type', content_type)
-        self.end_headers()
-        self.wfile.write(content)
-
-    def log_message(self, format, *args):
-        return
-
-    def do_POST(self):
-        parsed_path = urlparse(self.path)
-        path = parsed_path.path
-        query_params = parse_qs(parsed_path.query)
-
-        # 获取 Content-Length
-        try:
-            content_length = int(self.headers.get('Content-Length', 0))
-            if content_length == 0:
-                self._send_response(400, 'application/json', json.dumps({'code': 400, 'msg': 'Empty body'}).encode())
-                return
-            file_content = self.rfile.read(content_length)
-        except Exception:
-            self._send_response(400, 'application/json', json.dumps({'code': 400, 'msg': 'Read body failed'}).encode())
-            return
-
-        result_string = ""
-        
-        try:
-            # === 路由 1: 原有的 VFCode 识别 ===
-            if path == '/predict/bls':
-                model_type = query_params.get('model', ['ddddocr'])[0]
-                if model_type == 'ddddocr':
-                    if 'ddddocr' in engines:
-                        result_string = engines['ddddocr'].inference_bytes(file_content)
-                    else:
-                        result_string = "Error: ddddocr not available"
-                else:
-                    if 'pytorch' in engines:
-                        result_string = engines['pytorch'].inference_bytes(file_content)
-                    else:
-                        result_string = "Error: pytorch model not available"
-                
-                print(f"[VFCode] [{model_type}] Result: {result_string}")
-
-            # === 路由 2: 新增的通用 Captcha 识别 ===
-            elif path == '/predict/visametric':
-                if 'ddddocr' in engines:
-                    # 使用新增的预处理逻辑
-                    result_string = engines['ddddocr'].inference_captcha(file_content)
-                else:
-                    result_string = "Error: ddddocr not available"
-                
-                print(f"[Captcha] Result: {result_string}")
-
-            else:
-                self._send_response(404, 'text/plain', b'Not Found')
-                return
-
-            # 返回成功响应
-            response = {
-                'data': result_string,
-                'msg': "success",
-                'code': 200
-            }
-            self._send_response(200, 'application/json', json.dumps(response).encode())
-
-        except Exception as e:
-            traceback.print_exc()
-            response = {'data': '', 'msg': 'failed', 'code': 500}
-            self._send_response(500, 'application/json', json.dumps(response).encode())
-
-if __name__ == '__main__':
-    MODEL_PATH = 'data/ctc.pth'
-    PORT = 8085
-    
-    # 初始化 PyTorch 引擎
-    pytorch_engine = PyTorchEngine(MODEL_PATH)
-    if pytorch_engine.ready:
-        engines['pytorch'] = pytorch_engine
-        
-    # 初始化 DDDDOCR 引擎
-    ddd_engine = DddOcrEngine()
-    if ddd_engine.ready:
-        engines['ddddocr'] = ddd_engine
-    
-    server_address = ('0.0.0.0', PORT)
-    httpd = HTTPServer(server_address, RequestHandler)
-    print(f'OCR Server running on port {PORT}...')
-    print(f'Routes available:')
-    print(f'  POST /predict/bls?model=ddddocr|pytorch')
-    print(f'  POST /predict/visametric (Uses specific preprocessing)')
-    
-    try:
-        httpd.serve_forever()
-    except KeyboardInterrupt:
-        pass
-    httpd.server_close()

+ 259 - 0
server.py

@@ -0,0 +1,259 @@
+
+import re
+
+import asyncio
+import uvicorn
+import tempfile
+from pathlib import Path
+from contextlib import asynccontextmanager
+from fastapi import FastAPI, Body, Request, Query, HTTPException
+from fastapi.responses import JSONResponse
+from fastapi.concurrency import run_in_threadpool
+from utils.browser_util import get_browser
+from toolkit.ocr_engine import PyTorchEngine, DddOcrEngine
+
+
+# ================= 全局资源 =================
+# 全局浏览器对象
+GLOBAL_PAGE = None 
+# 异步锁,用于互斥控制
+BROWSER_LOCK = asyncio.Lock()
+# OCR 引擎字典
+engines = {}
+
+def _sync_get_visatype_ids(tmp_file: str):
+    """
+    这是实际执行浏览器操作的同步函数。
+    它会在独立的线程中运行,不会阻塞服务器。
+    """
+    result = {"status": "failed", "message": ""}
+    try:
+        html_file_path = Path(tmp_file).resolve()
+        file_url = f'file://{html_file_path}'
+        GLOBAL_PAGE.get(file_url)
+        
+        jur_id = None
+        loc_id = None
+        type_id = None
+        subtype_id = None
+        cat_id = None
+        
+         # 匹配 ID
+        app_category_labels = GLOBAL_PAGE.eles(f'Appointment Category', timeout=1)
+        for app_category_label in app_category_labels:
+            if app_category_label.states.has_rect and app_category_label.tag == 'label':
+                eid = app_category_label.after('tag:input').attr('id')
+                cat_id = int(''.join(filter(str.isdigit, eid)))
+                break
+        jurisdiction_labels = GLOBAL_PAGE.eles(f'Jurisdiction', timeout=1)
+        if jurisdiction_labels:
+            for jurisdiction_label in jurisdiction_labels:
+                if jurisdiction_label.states.has_rect and jurisdiction_label.tag == 'label':
+                    eid = jurisdiction_label.after('tag:input').attr('id')
+                    jur_id = int(''.join(filter(str.isdigit, eid)))
+                    break
+        location_labels = GLOBAL_PAGE.eles(f'Location', timeout=1)
+        for location_label in location_labels:
+            if location_label.states.has_rect and location_label.tag == 'label':
+                eid = location_label.after('tag:input', index=2).attr('id')
+                loc_id = int(''.join(filter(str.isdigit, eid)))
+                break
+        visa_type_labels = GLOBAL_PAGE.eles(f'Visa Type', timeout=1)
+        for visa_type_label in visa_type_labels:
+            if visa_type_label.states.has_rect and visa_type_label.tag == 'label':
+                eid = visa_type_label.after('tag:input').attr('id')
+                type_id = int(''.join(filter(str.isdigit, eid)))
+                break
+        visa_subtype_labels = GLOBAL_PAGE.eles(f'Visa Sub Type', timeout=1)
+        for visa_subtype_label in visa_subtype_labels:
+            if visa_subtype_label.states.has_rect and visa_subtype_label.tag == 'label':
+                eid = visa_subtype_label.after('tag:input').attr('id')
+                subtype_id = int(''.join(filter(str.isdigit, eid)))
+                break
+        data = {
+            "jur_id": jur_id,
+            "loc_id": loc_id,
+            "type_id": type_id,
+            "subtype_id": subtype_id,
+            "cat_id": cat_id,
+        }
+        result["status"] = "success"
+        result['data'] = data
+
+    except Exception as e:
+        result["message"] = str(e)
+        print(f"[DrissionPage] Error: {e}")
+            
+    return result 
+
+def _sync_get_visable_image_ids(tmp_file: str):
+    """
+    这是实际执行浏览器操作的同步函数。
+    它会在独立的线程中运行,不会阻塞服务器。
+    """
+    result = {"status": "failed", "message": ""}
+    try:
+        images_ids = []
+        html_file_path = Path(tmp_file).resolve()
+        file_url = f'file://{html_file_path}'
+        GLOBAL_PAGE.get(file_url)
+        captions_ele = GLOBAL_PAGE.ele('xpath://*[@id="captcha-main-div"]/div/div[1]', timeout=5)
+        if not captions_ele:
+            raise Exception('Captions elements not found')
+        caption_eles = captions_ele.children()
+        caption_text = ''
+        for caption in caption_eles:
+            if not caption.states.is_covered:
+                caption_text = caption.text
+
+        number = re.findall(r'\d+', caption_text)[0]
+        captcha_images_ele = GLOBAL_PAGE.ele('xpath://*[@id="captcha-main-div"]/div/div[2]')
+        captcha_image_eles = captcha_images_ele.children()
+        for captcha_image in captcha_image_eles:
+            img = captcha_image.ele('.captcha-img')
+            if img.states.has_rect and img.states.is_covered == False:
+                img_src = img.attr('src')
+                if img_src and img_src.startswith('data:image'):
+                    images_ids.append(captcha_image.attr('id'))
+        data = {
+            "number": number,
+            "image_ids": images_ids,
+        }
+        result["status"] = "success"
+        result['data'] = data
+
+    except Exception as e:
+        result["message"] = str(e)
+        print(f"[DrissionPage] Error: {e}")
+            
+    return result
+
+# ================= 2. 生命周期管理 =================
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    # --- 启动 OCR (伪代码,请保留你之前的逻辑) ---
+    print("--- Loading OCR Models ---")
+    engines['pytorch'] = PyTorchEngine('data/ctc.pth')
+    engines['ddddocr'] = DddOcrEngine()
+    
+    # --- 启动 DrissionPage ---
+    print("--- Starting DrissionPage ---")
+    global GLOBAL_PAGE
+
+    
+    # 创建浏览器对象,连接浏览器
+    GLOBAL_PAGE = get_browser()
+    
+    yield
+    
+    # --- 关闭资源 ---
+    print("--- Shutting Down ---")
+    if GLOBAL_PAGE:
+        try:
+            GLOBAL_PAGE.quit() # 关闭浏览器
+        except:
+            pass
+    engines.clear()
+
+app = FastAPI(lifespan=lifespan)
+
+# ================= 3. 浏览器接口 (带忙碌检测) =================
+@app.post("/browser/visable_captchas")
+async def browser_get_data(html_content: str = Body(..., media_type="text/plain")
+):
+    # 1. 非阻塞检查:锁是否被占用
+    if BROWSER_LOCK.locked():
+        return JSONResponse(
+            status_code=503,
+            content={
+                "code": 503, 
+                "status": "busy", 
+                "msg": "Browser is busy. One task at a time."
+            }
+        )
+
+    # 2. 获取锁
+    async with BROWSER_LOCK:
+        print(f"[Browser] Processing")
+        # 3. 写入临时 HTML 文件
+        with tempfile.NamedTemporaryFile(
+            mode="w+",
+            suffix=".html",
+            delete=True,
+            encoding="utf-8"
+        ) as f:
+            f.write(html_content)
+            f.flush()
+            # 3. 核心:将同步的 DrissionPage 代码扔到线程池运行
+            # 这样主线程(处理 OCR 请求的线程)不会被卡死
+            result = await run_in_threadpool(_sync_get_visable_image_ids, f.name)
+        
+        return result
+    
+# ================= 3. 浏览器接口 (带忙碌检测) =================
+@app.post("/browser/visatype_visable")
+async def browser_get_data(html_content: str = Body(..., media_type="text/plain")
+):
+    # 1. 非阻塞检查:锁是否被占用
+    if BROWSER_LOCK.locked():
+        return JSONResponse(
+            status_code=503,
+            content={
+                "code": 503, 
+                "status": "busy", 
+                "msg": "Browser is busy. One task at a time."
+            }
+        )
+
+    # 2. 获取锁
+    async with BROWSER_LOCK:
+        print(f"[Browser] Processing")
+        # 3. 写入临时 HTML 文件
+        with tempfile.NamedTemporaryFile(
+            mode="w+",
+            suffix=".html",
+            delete=True,
+            encoding="utf-8"
+        ) as f:
+            f.write(html_content)
+            f.flush()
+            # 3. 核心:将同步的 DrissionPage 代码扔到线程池运行
+            # 这样主线程(处理 OCR 请求的线程)不会被卡死
+            result = await run_in_threadpool(_sync_get_visatype_ids, f.name)
+        
+        return result
+
+# ================= 路由 2: OCR 识别 (BLS) =================
+@app.post("/predict/bls")
+async def predict_bls(request: Request, model: str = Query("ddddocr", enum=["ddddocr", "pytorch"])):
+    """ 处理 BLS 验证码 """
+    try:
+        image_bytes = await request.body()
+        if not image_bytes:
+            raise HTTPException(status_code=400, detail="Empty body")
+
+        if model == 'ddddocr':
+            res = engines['ddddocr'].inference_bytes(image_bytes)
+        else:
+            res = engines['pytorch'].inference_bytes(image_bytes)
+            
+        return {"code": 200, "msg": "success", "data": res, "engine": model}
+    except Exception as e:
+        return JSONResponse(status_code=500, content={"code": 500, "msg": str(e), "data": ""})
+
+# ================= 路由 3: OCR 识别 (Visametric) =================
+@app.post("/predict/visametric")
+async def predict_visametric(request: Request):
+    """ 处理 Visametric 验证码 (特殊预处理) """
+    try:
+        image_bytes = await request.body()
+        res = engines['ddddocr'].inference_captcha(image_bytes)
+        return {"code": 200, "msg": "success", "data": res}
+    except Exception as e:
+        return JSONResponse(status_code=500, content={"code": 500, "msg": str(e), "data": ""})
+
+if __name__ == '__main__':
+    # 运行服务
+    # host='0.0.0.0' 允许局域网访问
+    print("API Documentation: http://127.0.0.1:8085/docs")
+    uvicorn.run(app, host='0.0.0.0', port=8085)

+ 119 - 0
toolkit/ocr_engine.py

@@ -0,0 +1,119 @@
+import os
+
+# 屏蔽 ONNX Runtime 警告
+os.environ["ORT_LOGGING_LEVEL"] = "3"
+import string
+import io
+import torch
+import ddddocr
+from torch import nn
+from collections import OrderedDict
+from torchvision import transforms
+from PIL import Image, ImageFilter
+from io import BytesIO
+
+# ================= PyTorch 模型结构 (保留原有逻辑) =================
+class Model(nn.Module):
+    def __init__(self, n_classes, input_shape=(3, 64, 128)):
+        super(Model, self).__init__()
+        self.input_shape = input_shape
+        channels = [32, 64, 128, 256, 256]
+        layers = [2, 2, 2, 2, 2]
+        kernels = [3, 3, 3, 3, 3]
+        pools = [2, 2, 2, 2, (2, 1)]
+        modules = OrderedDict()
+        
+        def cba(name, in_channels, out_channels, kernel_size):
+            modules[f'conv{name}'] = nn.Conv2d(in_channels, out_channels, kernel_size,
+                                               padding=(1, 1) if kernel_size == 3 else 0)
+            modules[f'bn{name}'] = nn.BatchNorm2d(out_channels)
+            modules[f'relu{name}'] = nn.ReLU(inplace=True)
+        
+        last_channel = 3
+        for block, (n_channel, n_layer, n_kernel, k_pool) in enumerate(zip(channels, layers, kernels, pools)):
+            for layer in range(1, n_layer + 1):
+                cba(f'{block+1}{layer}', last_channel, n_channel, n_kernel)
+                last_channel = n_channel
+            modules[f'pool{block + 1}'] = nn.MaxPool2d(k_pool)
+        modules[f'dropout'] = nn.Dropout(0.25, inplace=True)
+        
+        self.cnn = nn.Sequential(modules)
+        self.lstm = nn.LSTM(input_size=self.infer_features(), hidden_size=128, num_layers=2, bidirectional=True)
+        self.fc = nn.Linear(in_features=256, out_features=n_classes)
+    
+    def infer_features(self):
+        x = torch.zeros((1,)+self.input_shape)
+        x = self.cnn(x)
+        x = x.reshape(x.shape[0], -1, x.shape[-1])
+        return x.shape[1]
+
+    def forward(self, x):
+        x = self.cnn(x)
+        x = x.reshape(x.shape[0], -1, x.shape[-1])
+        x = x.permute(2, 0, 1)
+        x, _ = self.lstm(x)
+        x = self.fc(x)
+        return x
+
+# ================= 引擎封装 =================
+class PyTorchEngine:
+    def __init__(self, model_path):
+        self.num_classes = 12
+        self.characters = '-' + string.digits + '$'
+        self.width = 150
+        self.hight = 80
+        self.model = Model(self.num_classes, input_shape=(3, self.hight, self.width))
+        self.ready = False
+        self.transforms_func = transforms.Compose([
+            transforms.Resize((self.hight, self.width)),
+            transforms.ToTensor()
+        ])
+        if os.path.exists(model_path):
+            self.model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
+            self.model.eval()
+            self.ready = True
+            print(f"[PyTorch] Loaded: {model_path}")
+
+    def decode(self, sequence):
+        a = ''.join([self.characters[x] for x in sequence])
+        s = []
+        last = None
+        for x in a:
+            if x != last:
+                s.append(x)
+                last = x
+        return ''.join([x for x in s if x != self.characters[0]])
+
+    def inference_bytes(self, image_bytes):
+        if not self.ready: return "Error: Model not loaded"
+        try:
+            image = Image.open(BytesIO(image_bytes)).convert('RGB')
+            image = self.transforms_func(image)
+            with torch.no_grad():
+                output = self.model(image.unsqueeze(0).cpu())
+            output_argmax = output.detach().permute(1, 0, 2).argmax(dim=-1)
+            return self.decode(output_argmax[0])
+        except Exception as e:
+            return f"Error: {str(e)}"
+
+class DddOcrEngine:
+    def __init__(self):
+        self.ocr = ddddocr.DdddOcr(show_ad=False, beta=True)
+           
+
+    def inference_bytes(self, image_bytes):
+        try:
+            return self.ocr.classification(image_bytes)
+        except Exception as e:
+            return f"Error: {e}"
+
+    def inference_captcha(self, image_bytes):
+        try:
+            image = Image.open(io.BytesIO(image_bytes))
+            gray_img = image.convert("L").filter(ImageFilter.MedianFilter(size=3))
+            binary_img = gray_img.point(lambda p: 255 if p > 128 else 0)
+            buf = io.BytesIO()
+            binary_img.save(buf, format="PNG")
+            return self.ocr.classification(buf.getvalue())
+        except Exception as e:
+            return f"Error: {e}"

+ 118 - 136
toolkit/vs_cloud_api.py

@@ -4,7 +4,8 @@ import json
 import time
 import time
 import urllib.parse
 import urllib.parse
 from typing import Dict, Any, Optional
 from typing import Dict, Any, Optional
-from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_DEBUG
+from vs_types import NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError 
+from vs_log_macros import VSC_ERROR, VSC_INFO, VSC_WARN, VSC_DEBUG
 
 
 class VSCloudApi:
 class VSCloudApi:
     """
     """
@@ -32,6 +33,26 @@ class VSCloudApi:
             "Content-Type": content_type,
             "Content-Type": content_type,
             "Accept": "application/json, text/plain, */*"
             "Accept": "application/json, text/plain, */*"
         }
         }
+        
+    def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
+        """
+        统一 HTTP 请求封装,严格复刻 C++ 逻辑:
+        1. 发送 OPTIONS 请求
+        2. 发送实际请求
+        """
+        print(f'[perform request] {method} {url} {data} {json_data} {params}')
+        resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params)
+        VSC_INFO('vs_cloud', resp.text)
+        if resp.status_code == 200:
+            return resp
+        elif resp.status_code == 401:
+            raise SessionExpiredOrInvalidError()
+        elif resp.status_code == 403:
+            raise PermissionDeniedError()
+        elif resp.status_code == 429:
+            raise RateLimiteddError()
+        else:
+            raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
 
 
     # =========================================================================
     # =========================================================================
     #  VAS Task Management (新增 API)
     #  VAS Task Management (新增 API)
@@ -39,8 +60,8 @@ class VSCloudApi:
 
 
     def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
     def get_vas_task_pop(self, routing_key: str) -> Optional[Dict]:
         """
         """
-        获取任务列表
-        API: GET /api/vas/task/list
+        获取任务信息
+        API: GET /api/vas/task/pop
         """
         """
         url = f"{self.base_url}/api/vas/task/pop"
         url = f"{self.base_url}/api/vas/task/pop"
         params = {
         params = {
@@ -49,19 +70,12 @@ class VSCloudApi:
 
 
         headers = self._get_headers()
         headers = self._get_headers()
         
         
-        try:
-            resp = self.session.get(url, params=params, headers=headers, timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"get_vas_task_pop biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"get_vas_task_pop failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"get_vas_task_pop exception: {e}")
-        return False
+        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 vas task pop biz error: {result.get('message')}")
 
 
     def update_vas_task(self, 
     def update_vas_task(self, 
                         task_id: int, 
                         task_id: int, 
@@ -74,21 +88,27 @@ class VSCloudApi:
         url = f"{self.base_url}/api/vas/task/update"
         url = f"{self.base_url}/api/vas/task/update"
         params = {"id": task_id}
         params = {"id": task_id}
         headers = self._get_headers()
         headers = self._get_headers()
-        
-        try:
-            resp = self.session.post(url, params=params, json=update_data, headers=headers, timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"update_vas_task failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"update_vas_task exception: {e}")
-                
-        return None
+        resp = self._perform_request('POST', url, params=params, json_data=update_data, headers=headers)
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Update vas task biz error: {result.get('message')}")
+    
+    def return_vas_task_to_queue(self, task_id: int):
+        """
+        重入队列
+        API: POST /api/vas/task/return_to_queue?task_id=1
+        """
+        url = f"{self.base_url}/api/vas/task/return_to_queue"
+        params = {"task_id": task_id}
+        headers = self._get_headers()
+        resp = self._perform_request('POST', url, params=params, headers=headers)
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Return vas task to queue biz error: {result.get('message')}")
 
 
     def submit_anti_turnstile_task(self, proxy: str, website_url: str) -> Optional[Dict]:
     def submit_anti_turnstile_task(self, proxy: str, website_url: str) -> Optional[Dict]:
         """
         """
@@ -108,37 +128,25 @@ class VSCloudApi:
             "status": 0
             "status": 0
         }
         }
 
 
-        try:
-            resp = self.session.post(url, headers=headers, json=payload, timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"submit_anti_turnstile_task failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"submit_anti_turnstile_task exception: {e}")
-        return None
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Submit anti turnstile task biz error: {result.get('message')}")
+
 
 
     def get_anti_turnstile_result(self, task_id: str) -> Optional[Dict]:
     def get_anti_turnstile_result(self, task_id: str) -> Optional[Dict]:
         """获取反 Turnstile 结果"""
         """获取反 Turnstile 结果"""
         url = f"{self.base_url}/api/tasks/{task_id}"
         url = f"{self.base_url}/api/tasks/{task_id}"
         headers = self._get_headers()
         headers = self._get_headers()
-        try:
-            resp = self.session.get(url, headers=headers, timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"get_anti_turnstile_result exception: {e}")
-        return None
+        resp = self._perform_request('GET', url, headers=headers)
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Get anti turnstile result biz error: {result.get('message')}")
+ 
     
     
     def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
     def submit_anticloudflare_task(self, proxy: str, website_url: str) -> Optional[Dict]:
         """
         """
@@ -156,19 +164,12 @@ class VSCloudApi:
             "status": 0
             "status": 0
         }
         }
         headers = self._get_headers()
         headers = self._get_headers()
-        try:
-            response = self.session.post(url, headers=headers, json=data, timeout=30)
-            if response.status_code == 200:
-                result = response.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {response.status_code} {response.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"submit_anticloudflare_task exception: {e}")
-        return None
+        resp = self._perform_request('POST', url, headers=headers, json_data=data)
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Submit anticloudflare task biz error: {result.get('message')}")
     
     
     def get_anticloudflare_result(self, task_id, retry_interval=5, max_retries=20) -> Optional[Dict]:
     def get_anticloudflare_result(self, task_id, retry_interval=5, max_retries=20) -> Optional[Dict]:
         """
         """
@@ -176,29 +177,33 @@ class VSCloudApi:
         """
         """
         url = f"{self.base_url}/api/tasks/{task_id}"
         url = f"{self.base_url}/api/tasks/{task_id}"
         headers = self._get_headers()
         headers = self._get_headers()
-        try:
-            for attempt in range(max_retries):
-                response = self.session.get(url, headers=headers, timeout=30)
-                
-                if response.status_code == 200:
-                    result = response.json()
-                    if result.get("code") == 0:
-                        data = result.get("data", {})
-                        # status 2 表示成功
-                        if data.get("status") == 2:
-                            return data
-                        elif data.get("status") == 3:
-                            VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
-                            return None
-                        else:
-                            time.sleep(retry_interval)
+        
+        for attempt in range(1, max_retries + 1):
+            try:
+                resp = self._perform_request('GET', url, headers=headers)
+                result = resp.json()
+                if result.get("code") == 0:
+                    data = result.get("data", {})
+                    # status 2 表示成功
+                    if data.get("status") == 2:
+                        return data
+                    elif data.get("status") == 3:
+                        VSC_ERROR("vs_cloud", f"AntiCloudflareTask failed: {data.get('result')}")
+                        return None
                     else:
                     else:
-                        VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-                else:
-                    VSC_ERROR("vs_cloud", f"get_anti_turnstile_result failed: {response.status_code} {response.text[:100]}")
-            VSC_ERROR("vs_cloud", "Max retries reached, AntiCloudflareTask not completed.")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"get_anticloudflare_result exception: {e}")
+                        time.sleep(retry_interval)
+            except Exception as e:
+                VSC_WARN(
+                    "vs_cloud",
+                    "Get anticloudflare result exception, attempt %d/%d",
+                    attempt, max_retries
+                )
+
+                if attempt >= max_retries:
+                    break
+
+                time.sleep(retry_interval)
+                continue
         return None
         return None
 
 
     def create_http_session(
     def create_http_session(
@@ -222,20 +227,13 @@ class VSCloudApi:
             "page": page,
             "page": page,
             "session_id": session_id
             "session_id": session_id
         }
         }
-        
-        try:
-            resp = self.session.post(url, headers=headers, json=payload, timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get("code") == 0:
-                    return result.get("data", {})
-                else:
-                    VSC_ERROR("vs_cloud", f"update_vas_task biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"create_http_session failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"create_http_session exception: {e}")
-        return None
+        resp = self._perform_request('POST', url, headers=headers, json_data=payload)
+
+        result = resp.json()
+        if result.get("code") == 0:
+            return result.get("data", {})
+        else:
+            raise BizLogicError(message=f"Create http session biz error: {result.get('message')}")
 
 
     def fetch_mail_content(
     def fetch_mail_content(
         self,
         self,
@@ -262,21 +260,13 @@ class VSCloudApi:
         
         
         url = f"{self.base_url}/api/email-authorizations/fetch"
         url = f"{self.base_url}/api/email-authorizations/fetch"
         headers = self._get_headers()
         headers = self._get_headers()
-        try:
-            resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get('code', 0) == 0:
-                    data = result.get('data', {})
-                    return data.get('body', "")
-                else:
-                    VSC_ERROR("vs_cloud", f"fetch_mail_content biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"fetch_mail_content failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"fetch_mail_content exception: {e}")
-            
-        return None
+        resp = self._perform_request('POST', url, headers=headers, params=params, data="")
+        result = resp.json()
+        if result.get('code') == 0:
+            data = result.get('data', {})
+            return data.get('body', '')
+        else:
+            raise BizLogicError(message=f"Fetch mail content biz error: {result.get('message')}")
 
 
     def fetch_mail_content_from_top(
     def fetch_mail_content_from_top(
         self,
         self,
@@ -299,18 +289,10 @@ class VSCloudApi:
         
         
         url = f"{self.base_url}/api/email-authorizations/fetch-top"
         url = f"{self.base_url}/api/email-authorizations/fetch-top"
         headers = self._get_headers()
         headers = self._get_headers()
-        
-        try:
-            resp = self.session.post(url, headers=headers, params=params, data="", timeout=30)
-            if resp.status_code == 200:
-                result = resp.json()
-                if result.get('code', 0) == 0:
-                    data = result.get('data', {})
-                    return data.get('body', "")
-                else:
-                    VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top biz error: {result.get('message')}")
-            else:
-                VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top failed: {resp.status_code} {resp.text[:100]}")
-        except Exception as e:
-            VSC_ERROR("vs_cloud", f"fetch_mail_content_from_top exception: {e}")
-        return None
+        resp = self._perform_request('POST', url, headers=headers, params=params, data="")
+        result = resp.json()
+        if result.get('code') == 0:
+            data = result.get('data', {})
+            return data.get('body', "")
+        else:
+            raise BizLogicError(message=f"Fetch mail content from top biz error: {result.get('message')}")