fix: harden auto-login selectors and poll auth status
This commit is contained in:
@@ -2,7 +2,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
@@ -16,12 +19,18 @@ class AutoLoginManager:
|
|||||||
headless: bool,
|
headless: bool,
|
||||||
timeout_sec: int,
|
timeout_sec: int,
|
||||||
max_retry: int,
|
max_retry: int,
|
||||||
|
verify_logged_in: Callable[[], Awaitable[bool]] | None = None,
|
||||||
|
verify_timeout_sec: int = 60,
|
||||||
|
debug_dir: str = "/tmp/lingma-auto-login",
|
||||||
):
|
):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
self.timeout_sec = timeout_sec
|
self.timeout_sec = timeout_sec
|
||||||
self.max_retry = max_retry
|
self.max_retry = max_retry
|
||||||
|
self.verify_logged_in = verify_logged_in
|
||||||
|
self.verify_timeout_sec = verify_timeout_sec
|
||||||
|
self.debug_dir = debug_dir
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._task: asyncio.Task | None = None
|
self._task: asyncio.Task | None = None
|
||||||
self._state = "idle"
|
self._state = "idle"
|
||||||
@@ -63,6 +72,7 @@ class AutoLoginManager:
|
|||||||
for attempt in range(1, self.max_retry + 1):
|
for attempt in range(1, self.max_retry + 1):
|
||||||
try:
|
try:
|
||||||
await self._auto_login_once(login_url)
|
await self._auto_login_once(login_url)
|
||||||
|
await self._wait_logged_in_after_browser()
|
||||||
self._state = "success"
|
self._state = "success"
|
||||||
self._last_finished_at = time.time()
|
self._last_finished_at = time.time()
|
||||||
return
|
return
|
||||||
@@ -89,7 +99,7 @@ class AutoLoginManager:
|
|||||||
await page.goto(login_url, wait_until="domcontentloaded", timeout=30000)
|
await page.goto(login_url, wait_until="domcontentloaded", timeout=30000)
|
||||||
|
|
||||||
# Try common login selectors.
|
# Try common login selectors.
|
||||||
await self._fill_if_visible(page, [
|
user_filled = await self._fill_if_visible(page, [
|
||||||
'input[type="email"]',
|
'input[type="email"]',
|
||||||
'input[name="loginId"]',
|
'input[name="loginId"]',
|
||||||
'input[name="username"]',
|
'input[name="username"]',
|
||||||
@@ -98,19 +108,27 @@ class AutoLoginManager:
|
|||||||
'input[placeholder*="邮箱"]',
|
'input[placeholder*="邮箱"]',
|
||||||
], self.username)
|
], self.username)
|
||||||
|
|
||||||
await self._fill_if_visible(page, [
|
pwd_filled = await self._fill_if_visible(page, [
|
||||||
'input[type="password"]',
|
'input[type="password"]',
|
||||||
'input[name="password"]',
|
'input[name="password"]',
|
||||||
'input[placeholder*="密码"]',
|
'input[placeholder*="密码"]',
|
||||||
], self.password)
|
], self.password)
|
||||||
|
|
||||||
await self._click_if_visible(page, [
|
clicked = await self._click_if_visible(page, [
|
||||||
'button:has-text("登录")',
|
'button:has-text("登录")',
|
||||||
'button:has-text("登 录")',
|
'button:has-text("登 录")',
|
||||||
'button:has-text("Login")',
|
'button:has-text("Login")',
|
||||||
'button[type="submit"]',
|
'button[type="submit"]',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if not (user_filled and pwd_filled and clicked):
|
||||||
|
debug_file = await self._dump_debug_page(page)
|
||||||
|
raise RuntimeError(
|
||||||
|
"login form selectors not matched "
|
||||||
|
f"(user={user_filled}, password={pwd_filled}, click={clicked}), "
|
||||||
|
f"debug={debug_file}"
|
||||||
|
)
|
||||||
|
|
||||||
# Wait for redirect / callback activity.
|
# Wait for redirect / callback activity.
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
url = page.url
|
url = page.url
|
||||||
@@ -147,3 +165,31 @@ class AutoLoginManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def _wait_logged_in_after_browser(self):
|
||||||
|
if self.verify_logged_in is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < self.verify_timeout_sec:
|
||||||
|
try:
|
||||||
|
ok = await self.verify_logged_in()
|
||||||
|
if ok:
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
raise RuntimeError("auth status still not logged in after browser flow")
|
||||||
|
|
||||||
|
async def _dump_debug_page(self, page) -> str:
|
||||||
|
Path(self.debug_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
ts = int(time.time())
|
||||||
|
png = Path(self.debug_dir) / f"login-{ts}.png"
|
||||||
|
html = Path(self.debug_dir) / f"login-{ts}.html"
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await page.screenshot(path=str(png), full_page=True)
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
content = await page.content()
|
||||||
|
html.write_text(content, encoding="utf-8")
|
||||||
|
return str(png if png.exists() else html)
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ def auth_guard(request: Request):
|
|||||||
require_bearer(request, settings.api_keys)
|
require_bearer(request, settings.api_keys)
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_logged_in() -> bool:
|
||||||
|
assert lingma is not None
|
||||||
|
st = await lingma.auth_status()
|
||||||
|
return bool(st and st.get("id"))
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def on_startup():
|
async def on_startup():
|
||||||
global lingma, auto_login
|
global lingma, auto_login
|
||||||
@@ -52,6 +58,8 @@ async def on_startup():
|
|||||||
headless=settings.auto_login_headless,
|
headless=settings.auto_login_headless,
|
||||||
timeout_sec=settings.auto_login_timeout,
|
timeout_sec=settings.auto_login_timeout,
|
||||||
max_retry=settings.auto_login_max_retry,
|
max_retry=settings.auto_login_max_retry,
|
||||||
|
verify_logged_in=_is_logged_in,
|
||||||
|
verify_timeout_sec=max(30, min(180, settings.auto_login_timeout)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user