|
@@ -41,11 +41,12 @@ def _now_ms() -> int:
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_landing_state(page, log: Callable[[str], None]) -> dict:
|
|
def detect_landing_state(page, log: Callable[[str], None]) -> dict:
|
|
|
- """返回 { kind: 'checkoutweb'|'login_email_only'|'login_email_password'|'unknown', email_count, password_count, url }"""
|
|
|
|
|
|
|
+ """返回 { kind: 'checkoutweb'|'create_with_c2p'|'login_email_only'|'login_email_password'|'unknown',
|
|
|
|
|
+ email_count, password_count, has_c2p, url }"""
|
|
|
url = page.url or ""
|
|
url = page.url or ""
|
|
|
if "/checkoutweb/" in url:
|
|
if "/checkoutweb/" in url:
|
|
|
log(f"[paypal:detect] 已在 /checkoutweb/ url={url}")
|
|
log(f"[paypal:detect] 已在 /checkoutweb/ url={url}")
|
|
|
- return {"kind": "checkoutweb", "email_count": 0, "password_count": 0, "url": url}
|
|
|
|
|
|
|
+ return {"kind": "checkoutweb", "email_count": 0, "password_count": 0, "has_c2p": False, "url": url}
|
|
|
|
|
|
|
|
try:
|
|
try:
|
|
|
info = page.evaluate(
|
|
info = page.evaluate(
|
|
@@ -63,10 +64,21 @@ def detect_landing_state(page, log: Callable[[str], None]) -> dict:
|
|
|
const passwords = Array.from(document.querySelectorAll(
|
|
const passwords = Array.from(document.querySelectorAll(
|
|
|
'input#password, input[name="password"], input[type="password"], input[autocomplete="current-password"]'
|
|
'input#password, input[name="password"], input[type="password"], input[autocomplete="current-password"]'
|
|
|
)).filter(visible);
|
|
)).filter(visible);
|
|
|
|
|
+ const bodyText = (document.body && document.body.innerText || '').replace(/\s+/g, ' ').slice(0, 800);
|
|
|
|
|
+ // 检测 "Continue to Payment" 中间页特征
|
|
|
|
|
+ const hasC2P = Array.from(document.querySelectorAll(
|
|
|
|
|
+ 'button, input[type="submit"], [role="button"]'
|
|
|
|
|
+ )).filter(visible).some((el) => {
|
|
|
|
|
+ const t = (el.innerText || el.getAttribute('aria-label') || '').trim();
|
|
|
|
|
+ return /continue\s+to\s+payment|continue\s+to\s+pay|继续(?:到)?(?:支付|付款)|前往(?:支付|付款)/i.test(t);
|
|
|
|
|
+ });
|
|
|
|
|
+ const hasCreateAccount = /create\s+(?:a\s+)?paypal\s+account|create\s+(?:an?\s+)?account/i.test(bodyText);
|
|
|
return {
|
|
return {
|
|
|
emailCount: emails.length,
|
|
emailCount: emails.length,
|
|
|
passwordCount: passwords.length,
|
|
passwordCount: passwords.length,
|
|
|
- bodyText: (document.body && document.body.innerText || '').replace(/\s+/g, ' ').slice(0, 600),
|
|
|
|
|
|
|
+ hasC2P: hasC2P,
|
|
|
|
|
+ hasCreateAccount: hasCreateAccount,
|
|
|
|
|
+ bodyText: bodyText,
|
|
|
};
|
|
};
|
|
|
}"""
|
|
}"""
|
|
|
) or {}
|
|
) or {}
|
|
@@ -76,14 +88,22 @@ def detect_landing_state(page, log: Callable[[str], None]) -> dict:
|
|
|
|
|
|
|
|
email_count = int(info.get("emailCount") or 0)
|
|
email_count = int(info.get("emailCount") or 0)
|
|
|
password_count = int(info.get("passwordCount") or 0)
|
|
password_count = int(info.get("passwordCount") or 0)
|
|
|
|
|
+ has_c2p = bool(info.get("hasC2P"))
|
|
|
|
|
+ has_create = bool(info.get("hasCreateAccount"))
|
|
|
body_preview = info.get("bodyText") or ""
|
|
body_preview = info.get("bodyText") or ""
|
|
|
- log(f"[paypal:detect] url={url} email_count={email_count} password_count={password_count} body_preview={body_preview[:160]!r}")
|
|
|
|
|
|
|
+ log(f"[paypal:detect] url={url} email_count={email_count} password_count={password_count} has_c2p={has_c2p} has_create={has_create} body_preview={body_preview[:160]!r}")
|
|
|
|
|
+
|
|
|
|
|
+ # 优先识别 "Create Account + Continue to Payment" 中间页
|
|
|
|
|
+ if has_c2p and has_create and password_count == 0:
|
|
|
|
|
+ return {"kind": "create_with_c2p", "email_count": email_count, "password_count": password_count, "has_c2p": True, "url": url}
|
|
|
|
|
+ if has_c2p and password_count == 0 and email_count >= 1:
|
|
|
|
|
+ return {"kind": "create_with_c2p", "email_count": email_count, "password_count": password_count, "has_c2p": True, "url": url}
|
|
|
|
|
|
|
|
if password_count >= 1:
|
|
if password_count >= 1:
|
|
|
- return {"kind": "login_email_password", "email_count": email_count, "password_count": password_count, "url": url}
|
|
|
|
|
|
|
+ return {"kind": "login_email_password", "email_count": email_count, "password_count": password_count, "has_c2p": has_c2p, "url": url}
|
|
|
if email_count >= 1:
|
|
if email_count >= 1:
|
|
|
- return {"kind": "login_email_only", "email_count": email_count, "password_count": password_count, "url": url}
|
|
|
|
|
- return {"kind": "unknown", "email_count": email_count, "password_count": password_count, "url": url}
|
|
|
|
|
|
|
+ return {"kind": "login_email_only", "email_count": email_count, "password_count": password_count, "has_c2p": has_c2p, "url": url}
|
|
|
|
|
+ return {"kind": "unknown", "email_count": email_count, "password_count": password_count, "has_c2p": has_c2p, "url": url}
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_create_account_action(page, log: Callable[[str], None]):
|
|
def find_create_account_action(page, log: Callable[[str], None]):
|
|
@@ -437,10 +457,14 @@ def ensure_checkoutweb(
|
|
|
def should_try_login_next_first(state: dict) -> bool:
|
|
def should_try_login_next_first(state: dict) -> bool:
|
|
|
kind = state.get("kind") or ""
|
|
kind = state.get("kind") or ""
|
|
|
url = (state.get("url") or "").lower()
|
|
url = (state.get("url") or "").lower()
|
|
|
- if kind == "login_email_only":
|
|
|
|
|
- return True
|
|
|
|
|
|
|
+ # 有 Continue to Payment 特征的页面不走 Login Next,走中间页处理
|
|
|
|
|
+ if state.get("has_c2p"):
|
|
|
|
|
+ return False
|
|
|
|
|
+ # /pay 页面无论单/双输入框,都先尝试填 email + Next
|
|
|
if "paypal.com/pay" in url and int(state.get("email_count") or 0) >= 1:
|
|
if "paypal.com/pay" in url and int(state.get("email_count") or 0) >= 1:
|
|
|
return True
|
|
return True
|
|
|
|
|
+ if kind == "login_email_only":
|
|
|
|
|
+ return True
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
page.wait_for_load_state("domcontentloaded", timeout=30000)
|
|
page.wait_for_load_state("domcontentloaded", timeout=30000)
|
|
@@ -451,6 +475,15 @@ def ensure_checkoutweb(
|
|
|
stage("PayPal 已直达 /checkoutweb/")
|
|
stage("PayPal 已直达 /checkoutweb/")
|
|
|
return "already"
|
|
return "already"
|
|
|
|
|
|
|
|
|
|
+ # 优先处理 "Create Account + Continue to Payment" 中间页
|
|
|
|
|
+ if state.get("kind") == "create_with_c2p" or state.get("has_c2p"):
|
|
|
|
|
+ stage("PayPal 落地为 Create Account + Continue to Payment 中间页")
|
|
|
|
|
+ if handle_create_account_intermediate(page, fallback_email=fallback_email, log=log):
|
|
|
|
|
+ stage("通过 Continue to Payment 进入 /checkoutweb/")
|
|
|
|
|
+ return "create_c2p"
|
|
|
|
|
+ # 中间页处理失败,继续尝试其他路径
|
|
|
|
|
+ log("[paypal] Continue to Payment 中间页处理失败,尝试其他路径")
|
|
|
|
|
+
|
|
|
# 路径 A:/pay 或 email-only 落地页 → 先尝试邮箱 + Next
|
|
# 路径 A:/pay 或 email-only 落地页 → 先尝试邮箱 + Next
|
|
|
if should_try_login_next_first(state):
|
|
if should_try_login_next_first(state):
|
|
|
stage("PayPal /pay 登录页:先尝试 Login Next 路径")
|
|
stage("PayPal /pay 登录页:先尝试 Login Next 路径")
|
|
@@ -461,18 +494,27 @@ def ensure_checkoutweb(
|
|
|
log(f"[paypal:login] 填邮箱失败: {exc!r}")
|
|
log(f"[paypal:login] 填邮箱失败: {exc!r}")
|
|
|
|
|
|
|
|
if click_login_next(page, log):
|
|
if click_login_next(page, log):
|
|
|
- deadline = time.time() + 12
|
|
|
|
|
|
|
+ deadline = time.time() + 15
|
|
|
|
|
+ password_seen_count = 0
|
|
|
while time.time() < deadline:
|
|
while time.time() < deadline:
|
|
|
- page.wait_for_timeout(500)
|
|
|
|
|
|
|
+ page.wait_for_timeout(800)
|
|
|
cur = detect_landing_state(page, log)
|
|
cur = detect_landing_state(page, log)
|
|
|
if cur["kind"] == "checkoutweb":
|
|
if cur["kind"] == "checkoutweb":
|
|
|
stage("通过 Login Next 进入 /checkoutweb/")
|
|
stage("通过 Login Next 进入 /checkoutweb/")
|
|
|
return "login"
|
|
return "login"
|
|
|
- if cur["kind"] == "login_email_password":
|
|
|
|
|
- log("[paypal:login] 检测到 password 输入框,登录路径不可行,将切到 Create Account")
|
|
|
|
|
|
|
+ # 检测到 C2P 中间页,立刻跳出去处理
|
|
|
|
|
+ if cur.get("has_c2p") or cur["kind"] == "create_with_c2p":
|
|
|
|
|
+ log("[paypal:login] Login Next 后检测到 Continue to Payment 中间页,切到 C2P 处理")
|
|
|
break
|
|
break
|
|
|
|
|
+ if cur["kind"] == "login_email_password":
|
|
|
|
|
+ password_seen_count += 1
|
|
|
|
|
+ if password_seen_count >= 3:
|
|
|
|
|
+ log("[paypal:login] 多次检测到 password 输入框,登录路径不可行,将切到 Create Account")
|
|
|
|
|
+ break
|
|
|
|
|
+ else:
|
|
|
|
|
+ password_seen_count = 0
|
|
|
else:
|
|
else:
|
|
|
- log("[paypal:login] 12s 内未进 /checkoutweb/,将切到 Create Account 兜底")
|
|
|
|
|
|
|
+ log("[paypal:login] 15s 内未进 /checkoutweb/,将切到 Create Account 兜底")
|
|
|
else:
|
|
else:
|
|
|
log("[paypal:login] 没找到可点的 Login Next,将切到 Create Account")
|
|
log("[paypal:login] 没找到可点的 Login Next,将切到 Create Account")
|
|
|
|
|
|
|
@@ -481,6 +523,14 @@ def ensure_checkoutweb(
|
|
|
stage("Login Next 之后已进 /checkoutweb/")
|
|
stage("Login Next 之后已进 /checkoutweb/")
|
|
|
return "login"
|
|
return "login"
|
|
|
|
|
|
|
|
|
|
+ # Login Next 后页面变成了 C2P 中间页,直接处理
|
|
|
|
|
+ if state2.get("has_c2p") or state2["kind"] == "create_with_c2p":
|
|
|
|
|
+ stage("Login Next 后进入 Create Account + Continue to Payment 中间页")
|
|
|
|
|
+ if handle_create_account_intermediate(page, fallback_email=fallback_email, log=log):
|
|
|
|
|
+ stage("通过 Continue to Payment 进入 /checkoutweb/")
|
|
|
|
|
+ return "create_c2p"
|
|
|
|
|
+ log("[paypal] Continue to Payment 中间页处理失败")
|
|
|
|
|
+
|
|
|
# 路径 B:Create Account(之后可能还有"邮箱 + Continue to Payment"中间页)
|
|
# 路径 B:Create Account(之后可能还有"邮箱 + Continue to Payment"中间页)
|
|
|
stage(f"PayPal 落地为 {state2['kind']},尝试点击 Create Account")
|
|
stage(f"PayPal 落地为 {state2['kind']},尝试点击 Create Account")
|
|
|
if click_create_account(page, log):
|
|
if click_create_account(page, log):
|