|
@@ -80,9 +80,9 @@ def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%
|
|
|
except:
|
|
except:
|
|
|
return data_str
|
|
return data_str
|
|
|
|
|
|
|
|
-def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
|
|
|
|
|
|
|
+def get_alias_email(email: str, new_domain: str = "text.skin") -> str:
|
|
|
"""
|
|
"""
|
|
|
- 将邮箱域名替换为指定域名(默认 gmail-app.com)
|
|
|
|
|
|
|
+ 将邮箱域名替换为指定域名(默认 text.skin)
|
|
|
"""
|
|
"""
|
|
|
if "@" not in email:
|
|
if "@" not in email:
|
|
|
raise ValueError(f"Invalid email: {email}")
|
|
raise ValueError(f"Invalid email: {email}")
|
|
@@ -175,17 +175,22 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
def keep_alive(self):
|
|
def keep_alive(self):
|
|
|
try:
|
|
try:
|
|
|
- applicants = self._get_application()
|
|
|
|
|
- self._log(f'keep_alive request, get applicant resp={applicants}')
|
|
|
|
|
- except:
|
|
|
|
|
|
|
+ keep_alive_funcs = [
|
|
|
|
|
+ self._get_application,
|
|
|
|
|
+ self._query_center,
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ resp = random.choice(keep_alive_funcs)()
|
|
|
|
|
+ self._log(f'keep_alive request, resp={resp}')
|
|
|
|
|
+ except Exception as e:
|
|
|
self.is_healthy = False
|
|
self.is_healthy = False
|
|
|
|
|
+ self._log(f'keep_alive failed: {e}')
|
|
|
|
|
|
|
|
def health_check(self) -> bool:
|
|
def health_check(self) -> bool:
|
|
|
if not self.is_healthy:
|
|
if not self.is_healthy:
|
|
|
return False
|
|
return False
|
|
|
if self.page is None:
|
|
if self.page is None:
|
|
|
return False
|
|
return False
|
|
|
- # 检查页面是否还活着
|
|
|
|
|
try:
|
|
try:
|
|
|
if not self.page.run_js("return 1;"):
|
|
if not self.page.run_js("return 1;"):
|
|
|
return False
|
|
return False
|
|
@@ -370,8 +375,6 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
self._log(f"Create Session Failed: {e}")
|
|
self._log(f"Create Session Failed: {e}")
|
|
|
- time.sleep(3600)
|
|
|
|
|
- self.cleanup()
|
|
|
|
|
raise e
|
|
raise e
|
|
|
|
|
|
|
|
def query(self, apt_type: AppointmentType) -> VSQueryResult:
|
|
def query(self, apt_type: AppointmentType) -> VSQueryResult:
|
|
@@ -533,7 +536,6 @@ class VfsPlugin(IVSPlg):
|
|
|
策略:尝试点击“接受所有”,如果点不到就直接移除 DOM
|
|
策略:尝试点击“接受所有”,如果点不到就直接移除 DOM
|
|
|
"""
|
|
"""
|
|
|
try:
|
|
try:
|
|
|
- # 使用 JS 处理最快,且不会因为元素运动报错
|
|
|
|
|
js = """
|
|
js = """
|
|
|
try {
|
|
try {
|
|
|
// 1. 尝试点击 '接受所有' 按钮
|
|
// 1. 尝试点击 '接受所有' 按钮
|
|
@@ -597,8 +599,7 @@ class VfsPlugin(IVSPlg):
|
|
|
return "0.0.0.0"
|
|
return "0.0.0.0"
|
|
|
|
|
|
|
|
def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
|
|
def _get_common_headers(self, with_auth=True) -> Dict[str, str]:
|
|
|
- # DrissionPage 浏览器会自动带上 Origin, Referer, User-Agent, Sec-CH-UA 等
|
|
|
|
|
- # 这里只需要补充业务特定的 Headers
|
|
|
|
|
|
|
+ # DrissionPage 浏览器会自动带上 Origin, Referer, User-Agent, Sec-CH-UA 等,这里只需要补充业务特定的 Headers
|
|
|
mission = self.free_config.get("mission_code", "")
|
|
mission = self.free_config.get("mission_code", "")
|
|
|
country = self.free_config.get("country_code", "")
|
|
country = self.free_config.get("country_code", "")
|
|
|
lang = self.free_config.get("language", "en")
|
|
lang = self.free_config.get("language", "en")
|
|
@@ -719,7 +720,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
def _read_otp_email(self, sent_at='', sender='VFS Global') -> str:
|
|
def _read_otp_email(self, sent_at='', sender='VFS Global') -> str:
|
|
|
# 保持原样,这部分使用云API读取邮件,不依赖本地网络库
|
|
# 保持原样,这部分使用云API读取邮件,不依赖本地网络库
|
|
|
- master_email = "visafly666@gmail.com"
|
|
|
|
|
|
|
+ master_email = "hujiarui8@gmail.com"
|
|
|
recipient = self.config.account.username
|
|
recipient = self.config.account.username
|
|
|
subject_keywords = "One Time Password"
|
|
subject_keywords = "One Time Password"
|
|
|
body_keywords = "OTP"
|
|
body_keywords = "OTP"
|
|
@@ -741,7 +742,6 @@ class VfsPlugin(IVSPlg):
|
|
|
self._log("Submitting Login OTP...")
|
|
self._log("Submitting Login OTP...")
|
|
|
|
|
|
|
|
new_cf_token = self._refresh_turnstile_token()
|
|
new_cf_token = self._refresh_turnstile_token()
|
|
|
- # ---------------------------
|
|
|
|
|
|
|
|
|
|
email = self.config.account.username
|
|
email = self.config.account.username
|
|
|
password = self.config.account.password
|
|
password = self.config.account.password
|
|
@@ -818,7 +818,6 @@ class VfsPlugin(IVSPlg):
|
|
|
use_dfs = False
|
|
use_dfs = False
|
|
|
cf_bypasser.click_verification_button(use_dfs)
|
|
cf_bypasser.click_verification_button(use_dfs)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- # 点击过程报错不要中断主循环
|
|
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
raise BizLogicError("Failed to refresh Cloudflare Turnstile token (Timeout)")
|
|
raise BizLogicError("Failed to refresh Cloudflare Turnstile token (Timeout)")
|
|
@@ -830,7 +829,7 @@ class VfsPlugin(IVSPlg):
|
|
|
self._log("Starting booking process...")
|
|
self._log("Starting booking process...")
|
|
|
|
|
|
|
|
user_email = user_inputs.get('email')
|
|
user_email = user_inputs.get('email')
|
|
|
- user_inputs['alias_email'] = get_alias_email(user_email, new_domain="gmail-app.com")
|
|
|
|
|
|
|
+ user_inputs['alias_email'] = get_alias_email(user_email, new_domain="text.skin")
|
|
|
|
|
|
|
|
res = VSBookResult()
|
|
res = VSBookResult()
|
|
|
app_type = slot_info.apt_type
|
|
app_type = slot_info.apt_type
|
|
@@ -1217,7 +1216,6 @@ class VfsPlugin(IVSPlg):
|
|
|
ads_out = []
|
|
ads_out = []
|
|
|
if calendars:
|
|
if calendars:
|
|
|
for item in calendars:
|
|
for item in calendars:
|
|
|
- # API 返回可能是 MM/DD/YYYY 或 DD/MM/YYYY,VFS 比较乱
|
|
|
|
|
# 通常是 MM/DD/YYYY
|
|
# 通常是 MM/DD/YYYY
|
|
|
raw = item.get("date")
|
|
raw = item.get("date")
|
|
|
ads_out.append(to_yyyymmdd(raw, "%m/%d/%Y"))
|
|
ads_out.append(to_yyyymmdd(raw, "%m/%d/%Y"))
|
|
@@ -1326,11 +1324,7 @@ class VfsPlugin(IVSPlg):
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
self._log("Resolving payment redirect...")
|
|
self._log("Resolving payment redirect...")
|
|
|
- # 使用新标签页去跑,以免当前会话状态丢失
|
|
|
|
|
pay_tab = self.page.new_tab(start_url)
|
|
pay_tab = self.page.new_tab(start_url)
|
|
|
-
|
|
|
|
|
- # 等待跳转完成 (通常会跳到 Stripe, WorldPay 或其他支付网关)
|
|
|
|
|
- # 等待直到 URL 不再是 PayRequest
|
|
|
|
|
pay_tab.wait.url_change(start_url, timeout=15)
|
|
pay_tab.wait.url_change(start_url, timeout=15)
|
|
|
|
|
|
|
|
final_url = pay_tab.url
|
|
final_url = pay_tab.url
|
|
@@ -1409,16 +1403,13 @@ class VfsPlugin(IVSPlg):
|
|
|
"""
|
|
"""
|
|
|
销毁浏览器并彻底删除临时文件
|
|
销毁浏览器并彻底删除临时文件
|
|
|
"""
|
|
"""
|
|
|
- # 1. 关闭浏览器
|
|
|
|
|
if self.page:
|
|
if self.page:
|
|
|
try:
|
|
try:
|
|
|
- self.page.quit() # 这会关闭 Chrome 进程
|
|
|
|
|
|
|
+ self.page.quit()
|
|
|
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:
|
|
@@ -1426,15 +1417,12 @@ class VfsPlugin(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
|