|
@@ -16,7 +16,7 @@ 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 utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
|
|
|
-from toolkit.proxy_tunnel import ProxyTunnel
|
|
|
|
|
|
|
+from toolkit.mihomo_tunnel import MihomoTunnel
|
|
|
from utils.mouse import HumanMouse
|
|
from utils.mouse import HumanMouse
|
|
|
from utils.keyboard import HumanKeyboard
|
|
from utils.keyboard import HumanKeyboard
|
|
|
from utils.fingerprint_utils import FingerprintGenerator
|
|
from utils.fingerprint_utils import FingerprintGenerator
|
|
@@ -193,9 +193,26 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
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
|
|
|
|
|
+ self._log(f'Current proxy id={p.id}')
|
|
|
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}')
|
|
@@ -486,6 +503,34 @@ class TlsPlugin(IVSPlg):
|
|
|
self._log("No slots available.")
|
|
self._log("No slots available.")
|
|
|
res.success = False
|
|
res.success = False
|
|
|
res.availability_status = AvailabilityStatus.NoneAvailable
|
|
res.availability_status = AvailabilityStatus.NoneAvailable
|
|
|
|
|
+
|
|
|
|
|
+ # TODO(TEST): 临时测试预约提交
|
|
|
|
|
+ if configure.TLS_TEST_BOOK_AFTER_QUERY:
|
|
|
|
|
+ test_date = "2026-06-10"
|
|
|
|
|
+ test_time = "09:00"
|
|
|
|
|
+ test_label = ""
|
|
|
|
|
+ test_dt = datetime.strptime(test_date, "%Y-%m-%d")
|
|
|
|
|
+ query_res = VSQueryResult()
|
|
|
|
|
+ query_res.success = True
|
|
|
|
|
+ query_res.availability_status = AvailabilityStatus.Available
|
|
|
|
|
+ query_res.earliest_date = test_dt
|
|
|
|
|
+ query_res.availability = [
|
|
|
|
|
+ DateAvailability(
|
|
|
|
|
+ date=test_dt,
|
|
|
|
|
+ times=[TimeSlot(time=test_time, label=test_label)]
|
|
|
|
|
+ )
|
|
|
|
|
+ ]
|
|
|
|
|
+ self._log(f"[TEST] using fixed June slot: {test_date} {test_time} {test_label}")
|
|
|
|
|
+ test_userinput = {
|
|
|
|
|
+ "support_pta": False,
|
|
|
|
|
+ "expected_end_date": "2100-01-01",
|
|
|
|
|
+ "expected_start_date": "2000-01-01"
|
|
|
|
|
+ }
|
|
|
|
|
+ try:
|
|
|
|
|
+ self.book(query_res, test_userinput)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ self._log(f"[TEST] book() after query failed: {e}")
|
|
|
|
|
+ self.is_healthy = False
|
|
|
return res
|
|
return res
|
|
|
|
|
|
|
|
def book_bak(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
|
|
def book_bak(self, slot_info: VSQueryResult, user_inputs: Dict = None) -> VSBookResult:
|
|
@@ -636,7 +681,7 @@ class TlsPlugin(IVSPlg):
|
|
|
self._log("Submitting booking request via JS Fetch...")
|
|
self._log("Submitting booking request via JS Fetch...")
|
|
|
self.page.run_js(js_script)
|
|
self.page.run_js(js_script)
|
|
|
|
|
|
|
|
- packet = self.page.listen.wait(timeout=10)
|
|
|
|
|
|
|
+ packet = self.page.listen.wait(timeout=15)
|
|
|
if not packet:
|
|
if not packet:
|
|
|
raise BizLogicError(message='Listening data failed')
|
|
raise BizLogicError(message='Listening data failed')
|
|
|
|
|
|
|
@@ -873,24 +918,15 @@ class TlsPlugin(IVSPlg):
|
|
|
主动刷新页面以触发 Cloudflare 挑战并尝试通过
|
|
主动刷新页面以触发 Cloudflare 挑战并尝试通过
|
|
|
"""
|
|
"""
|
|
|
try:
|
|
try:
|
|
|
- # 1. 刷新当前页面 (通常 Dashboard 页)
|
|
|
|
|
- # 这会强制浏览器重新进行 HTTP 请求,从而触发 Cloudflare 拦截页
|
|
|
|
|
self._log("Refreshing page to trigger Cloudflare...")
|
|
self._log("Refreshing page to trigger Cloudflare...")
|
|
|
self.page.refresh()
|
|
self.page.refresh()
|
|
|
-
|
|
|
|
|
- # 2. 调用 CloudflareBypasser
|
|
|
|
|
cf = CloudflareBypasser(self.page, log=self.config.debug)
|
|
cf = CloudflareBypasser(self.page, log=self.config.debug)
|
|
|
-
|
|
|
|
|
- # 3. 尝试过盾 (尝试次数稍多一点,因为此时可能网络不稳定)
|
|
|
|
|
- success = cf.bypass(max_retry=10)
|
|
|
|
|
|
|
+ success = cf.bypass(max_retry=6)
|
|
|
|
|
|
|
|
if success:
|
|
if success:
|
|
|
- # 再次确认页面是否正常加载 (非 403 页面)
|
|
|
|
|
title = self.page.title.lower()
|
|
title = self.page.title.lower()
|
|
|
if "access denied" in title:
|
|
if "access denied" in title:
|
|
|
return False
|
|
return False
|
|
|
-
|
|
|
|
|
- # 等待 DOM 稍微稳定
|
|
|
|
|
time.sleep(2)
|
|
time.sleep(2)
|
|
|
return True
|
|
return True
|
|
|
|
|
|
|
@@ -1015,16 +1051,13 @@ class TlsPlugin(IVSPlg):
|
|
|
"""
|
|
"""
|
|
|
销毁浏览器并彻底删除临时文件
|
|
销毁浏览器并彻底删除临时文件
|
|
|
"""
|
|
"""
|
|
|
- # 1. 关闭浏览器
|
|
|
|
|
if self.page:
|
|
if self.page:
|
|
|
try:
|
|
try:
|
|
|
- self.page.quit(force=True) # 这会关闭 Chrome 进程
|
|
|
|
|
|
|
+ self.page.quit(force=True)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
- pass # 忽略已关闭的错误
|
|
|
|
|
|
|
+ pass
|
|
|
self.page = None
|
|
self.page = None
|
|
|
|
|
|
|
|
- # 2. 删除文件
|
|
|
|
|
- # 注意:Chrome 关闭后可能需要几百毫秒释放文件锁,稍微等待
|
|
|
|
|
if os.path.exists(self.root_workspace):
|
|
if os.path.exists(self.root_workspace):
|
|
|
for _ in range(3):
|
|
for _ in range(3):
|
|
|
try:
|
|
try:
|
|
@@ -1032,14 +1065,11 @@ class TlsPlugin(IVSPlg):
|
|
|
shutil.rmtree(self.root_workspace, ignore_errors=True)
|
|
shutil.rmtree(self.root_workspace, ignore_errors=True)
|
|
|
break
|
|
break
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- # 如果删除失败(通常是Windows文件占用),重试
|
|
|
|
|
self._log(f"Cleanup retry: {e}")
|
|
self._log(f"Cleanup retry: {e}")
|
|
|
time.sleep(0.5)
|
|
time.sleep(0.5)
|
|
|
|
|
|
|
|
- # 如果依然存在,打印警告(虽然 ignore_errors=True 会掩盖报错,但可以 check exists)
|
|
|
|
|
if os.path.exists(self.root_workspace):
|
|
if os.path.exists(self.root_workspace):
|
|
|
self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
|
|
self._log(f"[WARN] Failed to fully remove workspace: {self.root_workspace}")
|
|
|
- # 3. [新增] 关闭代理隧道
|
|
|
|
|
if self.tunnel:
|
|
if self.tunnel:
|
|
|
try: self.tunnel.stop()
|
|
try: self.tunnel.stop()
|
|
|
except: pass
|
|
except: pass
|