|
@@ -7,16 +7,17 @@ import shutil
|
|
|
import re
|
|
import re
|
|
|
import os
|
|
import os
|
|
|
import base64
|
|
import base64
|
|
|
|
|
+from concurrent.futures import ThreadPoolExecutor
|
|
|
from datetime import datetime
|
|
from datetime import datetime
|
|
|
from typing import List, Dict, Optional, Any, Callable
|
|
from typing import List, Dict, Optional, Any, Callable
|
|
|
from urllib.parse import urlencode, urlparse
|
|
from urllib.parse import urlencode, urlparse
|
|
|
|
|
|
|
|
-# DrissionPage 核心
|
|
|
|
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
|
|
|
|
|
|
|
|
+import configure
|
|
|
from vs_plg import IVSPlg
|
|
from vs_plg import IVSPlg
|
|
|
from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
|
|
from vs_types import VSPlgConfig, AppointmentType, VSQueryResult, VSBookResult, AvailabilityStatus, TimeSlot, DateAvailability, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
|
|
|
-from toolkit.proxy_tunnel import ProxyTunnel
|
|
|
|
|
|
|
+from toolkit.mihomo_tunnel import MihomoTunnel
|
|
|
from toolkit.vs_cloud_api import VSCloudApi
|
|
from toolkit.vs_cloud_api import VSCloudApi
|
|
|
from utils.mouse import HumanMouse
|
|
from utils.mouse import HumanMouse
|
|
|
from utils.keyboard import HumanKeyboard
|
|
from utils.keyboard import HumanKeyboard
|
|
@@ -53,21 +54,14 @@ class ItaPlugin(IVSPlg):
|
|
|
|
|
|
|
|
# Prenotami 特有配置
|
|
# Prenotami 特有配置
|
|
|
self._service_id = 0
|
|
self._service_id = 0
|
|
|
- self._host = 'https://prenotami.esteri.it'
|
|
|
|
|
|
|
+ self.ita_url = 'https://prenotami.esteri.it'
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- # --- [核心修改] 并发隔离与资源管理 ---
|
|
|
|
|
- # 生成唯一实例 ID
|
|
|
|
|
self.instance_id = uuid.uuid4().hex[:8]
|
|
self.instance_id = uuid.uuid4().hex[:8]
|
|
|
self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
|
|
self.root_workspace = os.path.abspath(os.path.join("data/temp_browser_data", f"{self.group_id}.{self.instance_id}"))
|
|
|
- # 定义子目录:代理插件目录 & 浏览器用户数据目录
|
|
|
|
|
self.user_data_path = os.path.join(self.root_workspace, "user_data")
|
|
self.user_data_path = os.path.join(self.root_workspace, "user_data")
|
|
|
|
|
|
|
|
- # 确保根目录存在 (子目录由具体逻辑创建)
|
|
|
|
|
if not os.path.exists(self.root_workspace):
|
|
if not os.path.exists(self.root_workspace):
|
|
|
os.makedirs(self.root_workspace)
|
|
os.makedirs(self.root_workspace)
|
|
|
-
|
|
|
|
|
- # 持有隧道实例
|
|
|
|
|
self.tunnel = None
|
|
self.tunnel = None
|
|
|
self.session_create_time: float = 0
|
|
self.session_create_time: float = 0
|
|
|
|
|
|
|
@@ -103,19 +97,12 @@ class ItaPlugin(IVSPlg):
|
|
|
return False
|
|
return False
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
|
- # -------------------------------------------------------------
|
|
|
|
|
- # 1. Create Session (Login)
|
|
|
|
|
- # -------------------------------------------------------------
|
|
|
|
|
def create_session(self):
|
|
def create_session(self):
|
|
|
"""
|
|
"""
|
|
|
- 全浏览器登录流程:
|
|
|
|
|
- 1. 启动浏览器
|
|
|
|
|
- 2. 解决 ReCaptcha
|
|
|
|
|
- 3. 登录并维持 Session
|
|
|
|
|
|
|
+ 全浏览器会话创建:过盾 -> JS注入登录 -> 状态机自动路由导航 -> 到达目标页
|
|
|
"""
|
|
"""
|
|
|
self._log(f"Initializing Session (ID: {self.instance_id})...")
|
|
self._log(f"Initializing Session (ID: {self.instance_id})...")
|
|
|
co = ChromiumOptions()
|
|
co = ChromiumOptions()
|
|
|
-
|
|
|
|
|
def get_free_port():
|
|
def get_free_port():
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
|
s.bind(('', 0))
|
|
s.bind(('', 0))
|
|
@@ -123,115 +110,245 @@ class ItaPlugin(IVSPlg):
|
|
|
|
|
|
|
|
debug_port = get_free_port()
|
|
debug_port = get_free_port()
|
|
|
self._log(f"Assigned Debug Port: {debug_port}")
|
|
self._log(f"Assigned Debug Port: {debug_port}")
|
|
|
|
|
+ self._log(f"Account id={self.config.account.id}, proxy id={self.config.proxy.id}")
|
|
|
|
|
+
|
|
|
|
|
+ co.set_local_port(debug_port)
|
|
|
co.set_user_data_path(self.user_data_path)
|
|
co.set_user_data_path(self.user_data_path)
|
|
|
|
|
|
|
|
- chrome_path = os.getenv("CHROME_BIN")
|
|
|
|
|
|
|
+ chrome_path = configure.CHROME_PATH
|
|
|
|
|
+ if not chrome_path:
|
|
|
|
|
+ chrome_path = os.getenv("CHROME_BIN")
|
|
|
if chrome_path and os.path.exists(chrome_path):
|
|
if chrome_path and os.path.exists(chrome_path):
|
|
|
co.set_paths(browser_path=chrome_path)
|
|
co.set_paths(browser_path=chrome_path)
|
|
|
|
|
|
|
|
if self.config.proxy and self.config.proxy.ip:
|
|
if self.config.proxy and self.config.proxy.ip:
|
|
|
p = self.config.proxy
|
|
p = self.config.proxy
|
|
|
-
|
|
|
|
|
if p.username and p.password:
|
|
if p.username and p.password:
|
|
|
self._log(f"Starting Proxy Tunnel for {p.ip}...")
|
|
self._log(f"Starting Proxy Tunnel for {p.ip}...")
|
|
|
-
|
|
|
|
|
- self.tunnel = ProxyTunnel(p.ip, p.port, p.username, p.password)
|
|
|
|
|
|
|
+ exit_node = {
|
|
|
|
|
+ "name": "ExitNode",
|
|
|
|
|
+ "type": p.proto,
|
|
|
|
|
+ "server": p.ip,
|
|
|
|
|
+ "port": p.port,
|
|
|
|
|
+ "username": p.username,
|
|
|
|
|
+ "password": p.password
|
|
|
|
|
+ }
|
|
|
|
|
+ relay_node = None
|
|
|
|
|
+ if configure.MIHOMO_RELAY_NODES:
|
|
|
|
|
+ relay_node = random.choice(configure.MIHOMO_RELAY_NODES)
|
|
|
|
|
+ mihomo_path = configure.MIHOMO_BIN_PATH
|
|
|
|
|
+ if not mihomo_path:
|
|
|
|
|
+ mihomo_path = os.getenv("MIHOMO_BIN")
|
|
|
|
|
+ if not mihomo_path:
|
|
|
|
|
+ raise BizLogicError(message='Mihomo path is null, You need set mihomo bin path in configure or os env')
|
|
|
|
|
+ self.tunnel = MihomoTunnel(mihomo_path, exit_node=exit_node, relay_node=relay_node)
|
|
|
local_proxy = self.tunnel.start()
|
|
local_proxy = self.tunnel.start()
|
|
|
-
|
|
|
|
|
self._log(f"Tunnel started at {local_proxy}")
|
|
self._log(f"Tunnel started at {local_proxy}")
|
|
|
co.set_argument(f'--proxy-server={local_proxy}')
|
|
co.set_argument(f'--proxy-server={local_proxy}')
|
|
|
-
|
|
|
|
|
else:
|
|
else:
|
|
|
proxy_str = f"{p.proto}://{p.ip}:{p.port}"
|
|
proxy_str = f"{p.proto}://{p.ip}:{p.port}"
|
|
|
co.set_argument(f'--proxy-server={proxy_str}')
|
|
co.set_argument(f'--proxy-server={proxy_str}')
|
|
|
else:
|
|
else:
|
|
|
self._log("[WARN] No proxy configured!")
|
|
self._log("[WARN] No proxy configured!")
|
|
|
|
|
|
|
|
- fingerprint_gen = FingerprintGenerator()
|
|
|
|
|
- specific_fp = fingerprint_gen.generate(self.config.account.username)
|
|
|
|
|
- self._log(f'browser fingerprint={specific_fp}')
|
|
|
|
|
|
|
+ specific_fp = FingerprintGenerator().generate(self.config.account.username)
|
|
|
|
|
+ fp_seed = specific_fp.get("seed")
|
|
|
|
|
+ fp_platform = specific_fp.get("platform")
|
|
|
|
|
+ fp_brand = specific_fp.get("brand")
|
|
|
|
|
+ self._log(f'browser fingerprint seed={fp_seed}')
|
|
|
|
|
|
|
|
- co.headless(False)
|
|
|
|
|
|
|
+ co.headless(False)
|
|
|
co.set_argument('--no-sandbox')
|
|
co.set_argument('--no-sandbox')
|
|
|
- co.set_argument('--disable-gpu')
|
|
|
|
|
co.set_argument('--disable-dev-shm-usage')
|
|
co.set_argument('--disable-dev-shm-usage')
|
|
|
co.set_argument('--window-size=1920,1080')
|
|
co.set_argument('--window-size=1920,1080')
|
|
|
co.set_argument('--disable-blink-features=AutomationControlled')
|
|
co.set_argument('--disable-blink-features=AutomationControlled')
|
|
|
- co.set_argument(f"--fingerprint={specific_fp.get('seed')}")
|
|
|
|
|
- co.set_argument(f"--fingerprint-platform={specific_fp.get('platform')}")
|
|
|
|
|
- co.set_argument(f"--fingerprint-brand={specific_fp.get('brand')}")
|
|
|
|
|
- try:
|
|
|
|
|
- self.page = ChromiumPage(co)
|
|
|
|
|
- login_url = f"{self._host}/Home"
|
|
|
|
|
- self._log(f"Navigating to {login_url}")
|
|
|
|
|
- self.page.get(login_url)
|
|
|
|
|
|
|
+ co.set_argument(f"--fingerprint={fp_seed}")
|
|
|
|
|
+ co.set_argument(f"--fingerprint-platform={fp_platform}")
|
|
|
|
|
+ co.set_argument(f"--fingerprint-brand={fp_brand}")
|
|
|
|
|
+
|
|
|
|
|
+ self.page = ChromiumPage(co)
|
|
|
|
|
+ if self.config.debug:
|
|
|
|
|
+ self.page.get('https://example.com')
|
|
|
|
|
+ js_script = """
|
|
|
|
|
+ function getFingerprint() {
|
|
|
|
|
+ let webglVendor = 'Unknown';
|
|
|
|
|
+ let webglRenderer = 'Unknown';
|
|
|
|
|
+ try {
|
|
|
|
|
+ let canvas = document.createElement('canvas');
|
|
|
|
|
+ let gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
|
|
|
+ if (gl) {
|
|
|
|
|
+ let debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
|
|
|
+ if (debugInfo) {
|
|
|
|
|
+ webglVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
|
|
|
+ webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch(e) {}
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "User-Agent": navigator.userAgent,
|
|
|
|
|
+ "Platform": navigator.userAgentData ? navigator.userAgentData.platform : navigator.platform,
|
|
|
|
|
+ "Brands": navigator.userAgentData ? navigator.userAgentData.brands.map(b => b.brand).join(', ') : 'Not Supported',
|
|
|
|
|
+ "CPU Cores": navigator.hardwareConcurrency,
|
|
|
|
|
+ "Language": navigator.language,
|
|
|
|
|
+ "Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
|
|
|
+ "WebGL Vendor": webglVendor,
|
|
|
|
|
+ "WebGL Renderer": webglRenderer
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return getFingerprint();
|
|
|
|
|
+ """
|
|
|
|
|
+ fp_data = self.page.run_js(js_script)
|
|
|
|
|
+ self._log("================ 预检浏览器指纹数据 ================")
|
|
|
|
|
+ self._log(json.dumps(fp_data, indent=4, ensure_ascii=False))
|
|
|
|
|
+ self._log("====================================================")
|
|
|
|
|
|
|
|
- self._log("Init humanize tools...")
|
|
|
|
|
- self.mouse = HumanMouse(self.page, debug=True)
|
|
|
|
|
- self.keyboard = HumanKeyboard(self.page)
|
|
|
|
|
- self._log("Random mouse start position...")
|
|
|
|
|
- viewport_width = self.page.rect.viewport_size[0]
|
|
|
|
|
- viewport_height = self.page.rect.viewport_size[1]
|
|
|
|
|
- init_x = random.randint(10, viewport_width - 10)
|
|
|
|
|
- init_y = random.randint(10, viewport_height - 10)
|
|
|
|
|
- self.mouse.move(init_x, init_y)
|
|
|
|
|
|
|
+ self.page = ChromiumPage(co)
|
|
|
|
|
+ ita_url = self.ita_url
|
|
|
|
|
+ self._log(f"Navigating to {ita_url}")
|
|
|
|
|
+ self.page.get(ita_url)
|
|
|
|
|
+
|
|
|
|
|
+ self._log("Init humanize tools...")
|
|
|
|
|
+ self.mouse = HumanMouse(self.page, debug=True)
|
|
|
|
|
+ self.keyboard = HumanKeyboard(self.page)
|
|
|
|
|
+ self._log("Random mouse start position...")
|
|
|
|
|
+ viewport_width = self.page.rect.viewport_size[0]
|
|
|
|
|
+ viewport_height = self.page.rect.viewport_size[1]
|
|
|
|
|
+ init_x = random.randint(10, viewport_width - 10)
|
|
|
|
|
+ init_y = random.randint(10, viewport_height - 10)
|
|
|
|
|
+ self.mouse.move(init_x, init_y)
|
|
|
|
|
+
|
|
|
|
|
+ switch_en_btn = self.page.ele('tag:a@@href=/Language/ChangeLanguage?lang=2')
|
|
|
|
|
+ self.mouse.human_click_ele(switch_en_btn)
|
|
|
|
|
+ self.page.wait.load_start()
|
|
|
|
|
+ time.sleep(5)
|
|
|
|
|
+
|
|
|
|
|
+ max_steps = 15
|
|
|
|
|
+ stuck_counter = 0
|
|
|
|
|
+ last_url = ""
|
|
|
|
|
+ session_created = False
|
|
|
|
|
+ has_submitted_login = False
|
|
|
|
|
+
|
|
|
|
|
+ username = self.config.account.username
|
|
|
|
|
+ password = self.config.account.password
|
|
|
|
|
+
|
|
|
|
|
+ for step in range(max_steps):
|
|
|
|
|
+ self.page.wait.doc_loaded()
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
|
|
|
- # 等待登录框
|
|
|
|
|
- if not self.page.wait.ele_displayed('#login-email', timeout=20):
|
|
|
|
|
- raise BizLogicError("Login page not loaded")
|
|
|
|
|
-
|
|
|
|
|
- # 填充用户名密码
|
|
|
|
|
- self.mouse.human_click_ele(self.page.ele('#login-email'))
|
|
|
|
|
- self.keyboard.type_text(self.config.account.username)
|
|
|
|
|
|
|
+ current_url = self.page.url
|
|
|
|
|
+ current_title = self.page.title.lower()
|
|
|
|
|
+ current_html_content = self.page.html
|
|
|
|
|
+ self._log(f"--- [Router Step {step+1}] Current URL: {current_url} ---")
|
|
|
|
|
|
|
|
- self.mouse.human_click_ele(self.page.ele('#login-password'))
|
|
|
|
|
- self.keyboard.type_text(self.config.account.password)
|
|
|
|
|
|
|
+ # --- [异常处理与反爬对抗层] ---
|
|
|
|
|
+ if current_url == last_url:
|
|
|
|
|
+ stuck_counter += 1
|
|
|
|
|
+ else:
|
|
|
|
|
+ last_url = current_url
|
|
|
|
|
+ stuck_counter = 0
|
|
|
|
|
+
|
|
|
|
|
+ if stuck_counter >= 3:
|
|
|
|
|
+ self._log("[WARN] Page stucked, try to refresh...")
|
|
|
|
|
+ self.page.refresh()
|
|
|
|
|
+ self._random_sleep(5, 8)
|
|
|
|
|
+ stuck_counter = 0
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- # 先定位
|
|
|
|
|
- self._log("Locating Login button...")
|
|
|
|
|
- login_btn = self.page.ele('#captcha-trigger')
|
|
|
|
|
|
|
+ server_error_indicators = ["502 Bad Gateway", "503 Service Temporarily Unavailable", "error 1020"]
|
|
|
|
|
+ if any(err in current_html_content for err in server_error_indicators):
|
|
|
|
|
+ self._log(f"[WARN] Server network error, try to refresh (Step: {step})...")
|
|
|
|
|
+ self.page.refresh()
|
|
|
|
|
+ self._random_sleep(10, 15)
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- self.mouse.human_click_ele(login_btn)
|
|
|
|
|
- self._log("Login button clicked.")
|
|
|
|
|
|
|
+ # Cloudflare 拦截
|
|
|
|
|
+ cloudflare_blocked_indicators = [
|
|
|
|
|
+ "Sorry, you have been blocked",
|
|
|
|
|
+ "You are being rate limited",
|
|
|
|
|
+ "Cloudflare Ray ID"
|
|
|
|
|
+ ]
|
|
|
|
|
+ if any(indicator in current_html_content for indicator in cloudflare_blocked_indicators):
|
|
|
|
|
+ raise BizLogicError(message="Blocked by Cloudflare WAF. Need to change IP or browser fingerprint.")
|
|
|
|
|
|
|
|
- # 等待 URL 变化或特定元素出现
|
|
|
|
|
- # 成功通常跳转到 /UserArea, 失败则留在 /Home
|
|
|
|
|
- end_time = time.time() + 45
|
|
|
|
|
- login_success = False
|
|
|
|
|
|
|
+ # 遇到五秒盾先绕盾 (保留你原有的逻辑)
|
|
|
|
|
+ if "just a moment" in current_title or "cloudflare" in current_title:
|
|
|
|
|
+ # 假设你有 cf_bypasser 实例
|
|
|
|
|
+ # if not cf_bypasser.bypass(max_retry=3): continue
|
|
|
|
|
+ self._log("[State] CF 5-second shield detected. Waiting...")
|
|
|
|
|
+ time.sleep(5)
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- while time.time() < end_time:
|
|
|
|
|
|
|
+ # 状态 0: 成功到达目标预约页面 (Target Page)
|
|
|
|
|
+ if "/Services" in current_url and self.page.ele('.app-menu', timeout=1):
|
|
|
|
|
+ self._log("🎉 Successfully reached the Booking Services page! Session created successfully!")
|
|
|
|
|
+ self.session_create_time = time.time()
|
|
|
|
|
+ session_created = True
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # 状态 1: 登录前的初始首页
|
|
|
|
|
+ elif self.page.ele('#pingid-button', timeout=1):
|
|
|
|
|
+ self._log("[State] Initial Landing Page detected. Clicking login button...")
|
|
|
|
|
+ login_btn = self.page.ele('#pingid-button')
|
|
|
|
|
+ self.mouse.human_click_ele(login_btn)
|
|
|
|
|
+ self._log("Redirecting to PingID...")
|
|
|
|
|
+ self._random_sleep(3, 5) # 替代 time.sleep(5)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 状态 2: 真正的表单登录页 (PingID)
|
|
|
|
|
+ elif self.page.ele('@name=callback_1', timeout=1):
|
|
|
|
|
+ self._log("[State] PingID Login Form detected. Submitting credentials...")
|
|
|
|
|
+
|
|
|
|
|
+ print("正在输入账号...")
|
|
|
|
|
+ user_input = self.page.ele('@name=callback_1')
|
|
|
|
|
+ self.mouse.human_click_ele(user_input)
|
|
|
|
|
+ user_input.input(username, clear=True)
|
|
|
|
|
+ time.sleep(1) # 模拟人手停顿
|
|
|
|
|
+
|
|
|
|
|
+ print("正在输入密码...")
|
|
|
|
|
+ pwd_input = self.page.ele('@type=password')
|
|
|
|
|
+ self.mouse.human_click_ele(pwd_input)
|
|
|
|
|
+ pwd_input.input(password, clear=True)
|
|
|
time.sleep(1)
|
|
time.sleep(1)
|
|
|
- curr_url = self.page.url
|
|
|
|
|
|
|
|
|
|
- # 成功特征
|
|
|
|
|
- if "/UserArea" in curr_url or "/Services" in curr_url:
|
|
|
|
|
- login_success = True
|
|
|
|
|
- break
|
|
|
|
|
|
|
+ print("正在点击登录按钮...")
|
|
|
|
|
+ submit_btn = self.page.ele('tag:button@type=submit')
|
|
|
|
|
+ self.mouse.human_click_ele(submit_btn)
|
|
|
|
|
|
|
|
- # 失败特征
|
|
|
|
|
- if self.page.ele('.validation-summary-errors') or self.page.ele('.field-validation-error'):
|
|
|
|
|
- err_text = self.page.ele('.validation-summary-errors').text if self.page.ele('.validation-summary-errors') else "Unknown validation error"
|
|
|
|
|
- raise PermissionDeniedError(f"Login Failed: {err_text}")
|
|
|
|
|
|
|
+ has_submitted_login = True
|
|
|
|
|
+ self._log("Login form submitted. Waiting for dashboard to load...")
|
|
|
|
|
+ self._random_sleep(5, 7)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 状态 3: 登录成功后的主控制台 (导航栏页面)
|
|
|
|
|
+ elif self.page.ele('.app-menu', timeout=1):
|
|
|
|
|
+ self._log("[State] Dashboard Menu detected.")
|
|
|
|
|
|
|
|
- # 检查是否有弹窗错误
|
|
|
|
|
- if "Home" in curr_url and self.page.ele('#logoutForm'):
|
|
|
|
|
- # 有时候虽然在 Home 但出现了 Logout 按钮,也算成功
|
|
|
|
|
- login_success = True
|
|
|
|
|
- break
|
|
|
|
|
-
|
|
|
|
|
- if not login_success:
|
|
|
|
|
- # 截图保留现场
|
|
|
|
|
- # self.page.get_screenshot(path="login_fail.jpg")
|
|
|
|
|
- raise BizLogicError("Login Failed: Timeout waiting for redirect (Captcha score too low?)")
|
|
|
|
|
-
|
|
|
|
|
- self._log("Login Successful.")
|
|
|
|
|
-
|
|
|
|
|
- self.session_create_time = time.time()
|
|
|
|
|
|
|
+ switch_en_btn = self.page.ele('tag:a@@href=/Language/ChangeLanguage?lang=2', timeout=0.5)
|
|
|
|
|
+ if switch_en_btn:
|
|
|
|
|
+ self._log("Found 'English' language switch button. Clicking to change language...")
|
|
|
|
|
+ self.mouse.human_click_ele(switch_en_btn)
|
|
|
|
|
+ self._random_sleep(4, 6)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 动作 B: 如果没有英文切换按钮(说明已经是英文或不存在),则查找并点击 Book 按钮
|
|
|
|
|
+ book_btn = self.page.ele('@href=/Services', timeout=0.5)
|
|
|
|
|
+ if book_btn:
|
|
|
|
|
+ self._log("Found 'Book' menu element. Clicking to proceed to reservation...")
|
|
|
|
|
+ self.mouse.human_click_ele(book_btn)
|
|
|
|
|
+ self._random_sleep(4, 6)
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ self._log("[WARN] On Dashboard, but neither 'English' nor 'Book' button was found. Waiting...")
|
|
|
|
|
+ time.sleep(2)
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- self._log(f"Create Session Failed: {e}")
|
|
|
|
|
- self.cleanup()
|
|
|
|
|
- raise e
|
|
|
|
|
|
|
+ else:
|
|
|
|
|
+ self._log("[State] In unknown or transitional state. Waiting for next polling cycle...")
|
|
|
|
|
+ time.sleep(2)
|
|
|
|
|
+
|
|
|
|
|
+ if not session_created:
|
|
|
|
|
+ raise BizLogicError(f"Failed to reach appointment-booking after {max_steps} navigation steps. Stuck at: {self.page.url}")
|
|
|
|
|
|
|
|
# -------------------------------------------------------------
|
|
# -------------------------------------------------------------
|
|
|
# 2. Query Availability
|
|
# 2. Query Availability
|
|
@@ -241,101 +358,119 @@ class ItaPlugin(IVSPlg):
|
|
|
res.success = False
|
|
res.success = False
|
|
|
res.availability_status = AvailabilityStatus.NoneAvailable
|
|
res.availability_status = AvailabilityStatus.NoneAvailable
|
|
|
|
|
|
|
|
- if not self._service_id:
|
|
|
|
|
- raise BizLogicError("Service ID not configured")
|
|
|
|
|
-
|
|
|
|
|
- # 1. 检查 Slot 是否可用 (Check Availability Endpoint)
|
|
|
|
|
- check_url = f"{self._host}/Services/Booking/{self._service_id}"
|
|
|
|
|
-
|
|
|
|
|
- # 使用 Fetch 发起检查请求
|
|
|
|
|
- resp = self._perform_request("GET", check_url, headers={
|
|
|
|
|
- "Referer": f"{self._host}/Services",
|
|
|
|
|
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- # 302 跳转处理逻辑
|
|
|
|
|
- if resp.status_code == 200:
|
|
|
|
|
- # 200 表示进入了预约页,有号
|
|
|
|
|
- self._log("Slot Check: 200 OK (Availability Detected)")
|
|
|
|
|
- pass
|
|
|
|
|
- elif "BookingCalendar" in resp.url: # 或者是被重定向到了 Calendar
|
|
|
|
|
- self._log("Slot Check: Redirected to Calendar (Availability Detected)")
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ # 假设要预约的服务和到访原因
|
|
|
|
|
+ TARGET_SERVICE = "National and Schengen Visas"
|
|
|
|
|
+ REASON_FOR_VISIT = "Tourism" # 对应 value 42 的选项文本
|
|
|
|
|
+
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ # 步骤 1:在服务列表中寻找并点击目标服务的 Book
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ print("等待服务列表加载...")
|
|
|
|
|
+ self.page.wait.eles_loaded('@aria-controls=dataTableServices', timeout=10)
|
|
|
|
|
+
|
|
|
|
|
+ print(f"正在查找 [{TARGET_SERVICE}] 的 Book 按钮...")
|
|
|
|
|
+ # 使用 XPath:找包含 TARGET_SERVICE 文本的行(tr),再找该行里包含 Book 的超链接(a)
|
|
|
|
|
+ target_book_btn_xpath = f'xpath://tr[contains(., "{TARGET_SERVICE}")]//a[contains(text(), "Book")]'
|
|
|
|
|
+ book_btn = self.page.ele(target_book_btn_xpath)
|
|
|
|
|
+
|
|
|
|
|
+ if not book_btn:
|
|
|
|
|
+ print(f"未找到 {TARGET_SERVICE} 的 Book 按钮,可能当前无号,程序退出。")
|
|
|
|
|
+ # 这里可以根据你的逻辑 return 或者 raise Exception
|
|
|
else:
|
|
else:
|
|
|
- # 被重定向回 Home 或 Service,说明没号或 Session 过期
|
|
|
|
|
- if "Home" in resp.url or "Login" in resp.url:
|
|
|
|
|
- self.is_healthy = False
|
|
|
|
|
- raise SessionExpiredOrInvalidError("Session expired during query")
|
|
|
|
|
- self._log("Slot Check: No availability (Redirected back)")
|
|
|
|
|
- return res
|
|
|
|
|
-
|
|
|
|
|
- # 2. 查询月份 (Query Month)
|
|
|
|
|
- # 默认查询当月,或者配置的月份
|
|
|
|
|
- tar_dates = self.free_config.get("target_dates", [])
|
|
|
|
|
- if not tar_dates:
|
|
|
|
|
- # 默认查下个月
|
|
|
|
|
- next_month = datetime.now().replace(day=28) + datetime.timedelta(days=4)
|
|
|
|
|
- tar_dates = [next_month.strftime("%Y-%m-%d")]
|
|
|
|
|
-
|
|
|
|
|
- all_slots = []
|
|
|
|
|
-
|
|
|
|
|
- # Prenotami 需要先 retrieve server info
|
|
|
|
|
- self._perform_request("GET", f"{self._host}/BookingCalendar/RetrieveServerInfo")
|
|
|
|
|
-
|
|
|
|
|
- for date_str in tar_dates:
|
|
|
|
|
- # 构造月份格式 2026-01-05 -> 2026-01-01 (API 需要)
|
|
|
|
|
- try:
|
|
|
|
|
- dt = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
|
|
|
- except:
|
|
|
|
|
- try:
|
|
|
|
|
- dt = datetime.strptime(date_str, "%Y-%m-%d")
|
|
|
|
|
- except:
|
|
|
|
|
- dt = datetime.now()
|
|
|
|
|
-
|
|
|
|
|
- # API 需要格式: 2025-11-05T... 格式的字符串作为 selectedDay
|
|
|
|
|
- # 实际上 RetrieveCalendarAvailability 只需要由前端日历控件触发的格式
|
|
|
|
|
-
|
|
|
|
|
- # 查询日历 API
|
|
|
|
|
- cal_url = f"{self._host}/BookingCalendar/RetrieveCalendarAvailability"
|
|
|
|
|
- cal_payload = {
|
|
|
|
|
- "_Servizio": str(self._service_id),
|
|
|
|
|
- "selectedDay": date_str # 原样传配置里的 ISO 串
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ self.mouse.human_click_ele(book_btn)
|
|
|
|
|
+ self.page.wait.load_start()
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ # 步骤 2:填写预约表单
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ print("等待预约表单加载...")
|
|
|
|
|
+ self.page.wait.eles_loaded('#bookingForm', timeout=10)
|
|
|
|
|
+
|
|
|
|
|
+ print("1. 选择预约类型...")
|
|
|
|
|
+ typeofbooking_ddl = self.page.ele('#typeofbookingddl')
|
|
|
|
|
+ # DrissionPage select 方法直接按文本选中,会自动触发网页的 JS 联动
|
|
|
|
|
+ typeofbooking_ddl.select('Individual booking')
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ print("2. 选择到访原因...")
|
|
|
|
|
+ reason_ddl = self.page.ele('#ddls_0')
|
|
|
|
|
+ reason_ddl.select(REASON_FOR_VISIT)
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ print("3. 填写备注...")
|
|
|
|
|
+ notes_input = self.page.ele('#BookingNotes')
|
|
|
|
|
+ notes_input.clear()
|
|
|
|
|
+ notes_input.input('N/A') # 选填,填入 N/A 或者留空
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ # 步骤 3:处理 OTP 验证码
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ print("4. 点击发送 OTP 验证码...")
|
|
|
|
|
+ otp_send_btn = self.page.ele('#otp-send')
|
|
|
|
|
+ self.mouse.human_click_ele(otp_send_btn)
|
|
|
|
|
+
|
|
|
|
|
+ # 这是一个 AJAX 请求,会转圈圈。我们需要等待成功提示出现
|
|
|
|
|
+ print("等待验证码发送成功的提示...")
|
|
|
|
|
+ self.page.wait.ele_displayed('#IdOtpSent', timeout=15) # 等待绿字 "New code sent!" 显示
|
|
|
|
|
+ print("验证码已发送!")
|
|
|
|
|
+
|
|
|
|
|
+ print("5. 填写默认验证码 123456 ...")
|
|
|
|
|
+ otp_input = self.page.ele('#otp-input')
|
|
|
|
|
+ self.mouse.human_click_ele(otp_input)
|
|
|
|
|
+ otp_input.input('123456')
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ # 步骤 4:勾选隐私政策并提交
|
|
|
|
|
+ # ==========================================
|
|
|
|
|
+ print("6. 勾选隐私政策...")
|
|
|
|
|
+ privacy_checkbox = self.page.ele('#PrivacyCheck')
|
|
|
|
|
+ # 判断一下如果没勾上才去点,防止重复点击取消了
|
|
|
|
|
+ if not privacy_checkbox.states.is_checked:
|
|
|
|
|
+ self.mouse.human_click_ele(privacy_checkbox)
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+
|
|
|
|
|
+ print("7. 点击 Forward 提交表单...")
|
|
|
|
|
+ forward_btn = self.page.ele('#btnAvanti')
|
|
|
|
|
+ self.mouse.human_click_ele(forward_btn)
|
|
|
|
|
+
|
|
|
|
|
+ # 提交后页面会跳转,等待加载开始
|
|
|
|
|
+ self.page.wait.load_start()
|
|
|
|
|
+ print("表单已提交!当前页面:", self.page.url)
|
|
|
|
|
+
|
|
|
|
|
+ time.sleep(5)
|
|
|
|
|
+
|
|
|
|
|
+ valid_dates = []
|
|
|
|
|
|
|
|
- resp_cal = self._perform_request("POST", cal_url, json_data=cal_payload)
|
|
|
|
|
-
|
|
|
|
|
- if resp_cal.status_code != 200: continue
|
|
|
|
|
-
|
|
|
|
|
- # 解析有效日期
|
|
|
|
|
- valid_days = self._parse_valid_days(resp_cal.text)
|
|
|
|
|
- self._log(f"Valid days for {date_str}: {valid_days}")
|
|
|
|
|
-
|
|
|
|
|
- if valid_dates:
|
|
|
|
|
- res.success = True
|
|
|
|
|
- res.availability_status = AvailabilityStatus.Available
|
|
|
|
|
- earliest_date = valid_dates[0]
|
|
|
|
|
- earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
|
|
|
|
|
- res.earliest_date = earliest_dt
|
|
|
|
|
- for day in valid_days:
|
|
|
|
|
- # 查询具体 Slot
|
|
|
|
|
- slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
|
|
|
|
|
- slot_payload = {
|
|
|
|
|
- "selectedDay": day, # YYYY-MM-DD
|
|
|
|
|
- "idService": str(self._service_id)
|
|
|
|
|
- }
|
|
|
|
|
- resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
|
|
|
|
|
-
|
|
|
|
|
- time_slots = self._parse_time_slots(resp_slot.text)
|
|
|
|
|
- ts_list = []
|
|
|
|
|
- if time_slots:
|
|
|
|
|
- # 转换结构
|
|
|
|
|
- for ts in time_slots:
|
|
|
|
|
- # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
|
|
|
|
|
- ts_list.append(TimeSlot(
|
|
|
|
|
- time=f"{ts['start']} - {ts['end']}",
|
|
|
|
|
- label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
|
|
|
|
|
- ))
|
|
|
|
|
- res.availability.append(DateAvailability(date=datetime.strptime(day, "%d-%m-%Y"), times=ts_list))
|
|
|
|
|
|
|
+ if valid_dates:
|
|
|
|
|
+ res.success = True
|
|
|
|
|
+ res.availability_status = AvailabilityStatus.Available
|
|
|
|
|
+ earliest_date = valid_dates[0]
|
|
|
|
|
+ earliest_dt = datetime.strptime(earliest_date, "%Y-%m-%d")
|
|
|
|
|
+ res.earliest_date = earliest_dt
|
|
|
|
|
+ for day in valid_days:
|
|
|
|
|
+ # 查询具体 Slot
|
|
|
|
|
+ slot_url = f"{self._host}/BookingCalendar/RetrieveTimeSlots"
|
|
|
|
|
+ slot_payload = {
|
|
|
|
|
+ "selectedDay": day, # YYYY-MM-DD
|
|
|
|
|
+ "idService": str(self._service_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ resp_slot = self._perform_request("POST", slot_url, json_data=slot_payload)
|
|
|
|
|
+
|
|
|
|
|
+ time_slots = self._parse_time_slots(resp_slot.text)
|
|
|
|
|
+ ts_list = []
|
|
|
|
|
+ if time_slots:
|
|
|
|
|
+ # 转换结构
|
|
|
|
|
+ for ts in time_slots:
|
|
|
|
|
+ # ts: {'id': 123, 'start': '10:00', 'end': '10:30', 'remain': 1}
|
|
|
|
|
+ ts_list.append(TimeSlot(
|
|
|
|
|
+ time=f"{ts['start']} - {ts['end']}",
|
|
|
|
|
+ label=str(ts['id']) # 将 ID 存入 label 以便 book 使用
|
|
|
|
|
+ ))
|
|
|
|
|
+ res.availability.append(DateAvailability(date=datetime.strptime(day, "%d-%m-%Y"), times=ts_list))
|
|
|
return res
|
|
return res
|
|
|
|
|
|
|
|
# -------------------------------------------------------------
|
|
# -------------------------------------------------------------
|
|
@@ -507,77 +642,6 @@ class ItaPlugin(IVSPlg):
|
|
|
self._log(f"Final Booking Failed: {resp_final.status_code}")
|
|
self._log(f"Final Booking Failed: {resp_final.status_code}")
|
|
|
|
|
|
|
|
return res
|
|
return res
|
|
|
-
|
|
|
|
|
- # -------------------------------------------------------------
|
|
|
|
|
- # 4. Helpers
|
|
|
|
|
- # -------------------------------------------------------------
|
|
|
|
|
-
|
|
|
|
|
- def _get_proxy_url(self):
|
|
|
|
|
- # 构造代理
|
|
|
|
|
- proxy_url = ""
|
|
|
|
|
- if self.config.proxy.ip:
|
|
|
|
|
- s = self.config.proxy
|
|
|
|
|
- if s.username:
|
|
|
|
|
- proxy_url = f"{s.proto}://{s.username}:{s.password}@{s.ip}:{s.port}"
|
|
|
|
|
- else:
|
|
|
|
|
- proxy_url = f"{s.proto}://{s.ip}:{s.port}"
|
|
|
|
|
- return proxy_url
|
|
|
|
|
-
|
|
|
|
|
- def _solve_and_inject_prenotami_captcha(self):
|
|
|
|
|
- """
|
|
|
|
|
- 专门处理 Prenotami 的 ReCaptcha Enterprise
|
|
|
|
|
- """
|
|
|
|
|
- self._log("Solving ReCaptcha Enterprise (Action: LOGIN)...")
|
|
|
|
|
-
|
|
|
|
|
- api_token = self.free_config.get("capsolver_key", "")
|
|
|
|
|
- if not api_token:
|
|
|
|
|
- raise BizLogicError("Capsolver Key is required for Prenotami")
|
|
|
|
|
-
|
|
|
|
|
- # 从 HTML 源码中提取的信息
|
|
|
|
|
- site_key = "6LdkwrIqAAAAAC4NX-g_j7lEx9vh1rg94ZL2cFfY"
|
|
|
|
|
- page_url = self.page.url
|
|
|
|
|
-
|
|
|
|
|
- # 注意:Prenotami 的这个 Key 其实是混合模式,
|
|
|
|
|
- # 虽然它是 V3 (Enterprise),但很多打码平台用 V2 接口也能解,或者必须用 V3 Enterprise 接口
|
|
|
|
|
- # 建议先尝试 ReCaptchaV3EnterpriseTaskProxyLess
|
|
|
|
|
-
|
|
|
|
|
- # 修正为最标准的 V3 Enterprise 配置
|
|
|
|
|
- rc_params = {
|
|
|
|
|
- "type": "ReCaptchaV3EnterpriseTaskProxyless",
|
|
|
|
|
- "page": page_url,
|
|
|
|
|
- "siteKey": site_key,
|
|
|
|
|
- "action": "LOGIN", # 关键参数
|
|
|
|
|
- "minScore": 0.7, # 要求高分
|
|
|
|
|
- "apiToken": api_token,
|
|
|
|
|
- # "proxy": self._get_proxy_url()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- g_token = self._solve_recaptcha(rc_params)
|
|
|
|
|
- self._log(f"Captcha Solved. Token length: {len(g_token)}")
|
|
|
|
|
-
|
|
|
|
|
- hook_js = f"""
|
|
|
|
|
- // 1. 填充隐藏域 (双重保险)
|
|
|
|
|
- var input = document.getElementById('g-recaptcha-response');
|
|
|
|
|
- if(input) {{
|
|
|
|
|
- input.value = "{g_token}";
|
|
|
|
|
- }}
|
|
|
|
|
-
|
|
|
|
|
- // 2. 劫持 grecaptcha.execute 和 grecaptcha.enterprise.execute
|
|
|
|
|
- // 无论网页用哪个版本,都拦截下来
|
|
|
|
|
- var mockExecute = function() {{
|
|
|
|
|
- console.log("Recaptcha execution intercepted!");
|
|
|
|
|
- return Promise.resolve("{g_token}");
|
|
|
|
|
- }};
|
|
|
|
|
-
|
|
|
|
|
- if (window.grecaptcha) {{
|
|
|
|
|
- window.grecaptcha.execute = mockExecute;
|
|
|
|
|
- if (window.grecaptcha.enterprise) {{
|
|
|
|
|
- window.grecaptcha.enterprise.execute = mockExecute;
|
|
|
|
|
- }}
|
|
|
|
|
- }}
|
|
|
|
|
- """
|
|
|
|
|
- self._log("Injecting ReCaptcha Hook...")
|
|
|
|
|
- self.page.run_js(hook_js)
|
|
|
|
|
|
|
|
|
|
def _perform_request(self, method, url, headers=None, data=None, json_data=None):
|
|
def _perform_request(self, method, url, headers=None, data=None, json_data=None):
|
|
|
"""JS Fetch Wrapper"""
|
|
"""JS Fetch Wrapper"""
|
|
@@ -604,77 +668,6 @@ class ItaPlugin(IVSPlg):
|
|
|
"""
|
|
"""
|
|
|
return BrowserResponse(self.page.run_js(js, timeout=60)) # 文件上传可能较慢,给60s
|
|
return BrowserResponse(self.page.run_js(js, timeout=60)) # 文件上传可能较慢,给60s
|
|
|
|
|
|
|
|
- def _solve_recaptcha(self, params) -> str:
|
|
|
|
|
- """
|
|
|
|
|
- 调用 YesCaptcha API 识别
|
|
|
|
|
- """
|
|
|
|
|
- client_key = params.get("apiToken")
|
|
|
|
|
-
|
|
|
|
|
- # 1. 选择任务类型
|
|
|
|
|
- # 根据文档:RecaptchaV3TaskProxylessM1S7 强制 0.7 分,适合登录
|
|
|
|
|
- task_type = "RecaptchaV3TaskProxyless" # 默认
|
|
|
|
|
- if params.get("minScore") == 0.7:
|
|
|
|
|
- task_type = "RecaptchaV3TaskProxylessM1S7"
|
|
|
|
|
- elif params.get("minScore") == 0.9:
|
|
|
|
|
- task_type = "RecaptchaV3TaskProxylessM1S9"
|
|
|
|
|
-
|
|
|
|
|
- # 2. 构造创建任务请求
|
|
|
|
|
- create_url = "https://api.yescaptcha.com/createTask"
|
|
|
|
|
- create_data = {
|
|
|
|
|
- "clientKey": client_key,
|
|
|
|
|
- "task": {
|
|
|
|
|
- "type": task_type,
|
|
|
|
|
- "websiteURL": params.get("page"),
|
|
|
|
|
- "websiteKey": params.get("siteKey"),
|
|
|
|
|
- "pageAction": params.get("action") # YesCaptcha 要求的字段名是 pageAction
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- import requests as req
|
|
|
|
|
- try:
|
|
|
|
|
- # 发送创建任务请求
|
|
|
|
|
- r = req.post(create_url, json=create_data, timeout=20)
|
|
|
|
|
- if r.status_code != 200:
|
|
|
|
|
- raise BizLogicError(f"YesCaptcha Create Failed: {r.text}")
|
|
|
|
|
-
|
|
|
|
|
- res_json = r.json()
|
|
|
|
|
- if res_json.get("errorId") != 0:
|
|
|
|
|
- raise BizLogicError(f"YesCaptcha Error: {res_json.get('errorDescription')}")
|
|
|
|
|
-
|
|
|
|
|
- task_id = res_json.get("taskId")
|
|
|
|
|
- if not task_id:
|
|
|
|
|
- raise BizLogicError("YesCaptcha returned no taskId")
|
|
|
|
|
-
|
|
|
|
|
- # 3. 轮询获取结果
|
|
|
|
|
- result_url = "https://api.yescaptcha.com/getTaskResult"
|
|
|
|
|
- for _ in range(30): # 最多等 60-90秒
|
|
|
|
|
- time.sleep(3)
|
|
|
|
|
-
|
|
|
|
|
- r = req.post(result_url, json={"clientKey": client_key, "taskId": task_id}, timeout=20)
|
|
|
|
|
- d = r.json()
|
|
|
|
|
-
|
|
|
|
|
- # 识别中
|
|
|
|
|
- if d.get("status") == "processing":
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- # 识别成功
|
|
|
|
|
- if d.get("status") == "ready":
|
|
|
|
|
- solution = d.get("solution", {})
|
|
|
|
|
- token = solution.get("gRecaptchaResponse")
|
|
|
|
|
- if token:
|
|
|
|
|
- return token
|
|
|
|
|
- else:
|
|
|
|
|
- raise BizLogicError("YesCaptcha ready but no token found")
|
|
|
|
|
-
|
|
|
|
|
- # 识别失败
|
|
|
|
|
- if d.get("errorId") != 0:
|
|
|
|
|
- raise BizLogicError(f"YesCaptcha Task Failed: {d.get('errorDescription')}")
|
|
|
|
|
-
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- raise BizLogicError(f"Captcha Solver Exception: {e}")
|
|
|
|
|
-
|
|
|
|
|
- raise BizLogicError("YesCaptcha timeout")
|
|
|
|
|
-
|
|
|
|
|
def _parse_valid_days(self, text):
|
|
def _parse_valid_days(self, text):
|
|
|
# 提取 DateLibere (YYYY-MM-DD)
|
|
# 提取 DateLibere (YYYY-MM-DD)
|
|
|
# 格式: {"DateLibere":"22/10/2024 00:00:00","SlotLiberi":1,"SlotRimanenti":1}
|
|
# 格式: {"DateLibere":"22/10/2024 00:00:00","SlotLiberi":1,"SlotRimanenti":1}
|