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 contextlib
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable
|
||||
|
||||
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
||||
from playwright.async_api import async_playwright
|
||||
@@ -16,12 +19,18 @@ class AutoLoginManager:
|
||||
headless: bool,
|
||||
timeout_sec: 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.password = password
|
||||
self.headless = headless
|
||||
self.timeout_sec = timeout_sec
|
||||
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._task: asyncio.Task | None = None
|
||||
self._state = "idle"
|
||||
@@ -63,6 +72,7 @@ class AutoLoginManager:
|
||||
for attempt in range(1, self.max_retry + 1):
|
||||
try:
|
||||
await self._auto_login_once(login_url)
|
||||
await self._wait_logged_in_after_browser()
|
||||
self._state = "success"
|
||||
self._last_finished_at = time.time()
|
||||
return
|
||||
@@ -89,7 +99,7 @@ class AutoLoginManager:
|
||||
await page.goto(login_url, wait_until="domcontentloaded", timeout=30000)
|
||||
|
||||
# Try common login selectors.
|
||||
await self._fill_if_visible(page, [
|
||||
user_filled = await self._fill_if_visible(page, [
|
||||
'input[type="email"]',
|
||||
'input[name="loginId"]',
|
||||
'input[name="username"]',
|
||||
@@ -98,19 +108,27 @@ class AutoLoginManager:
|
||||
'input[placeholder*="邮箱"]',
|
||||
], self.username)
|
||||
|
||||
await self._fill_if_visible(page, [
|
||||
pwd_filled = await self._fill_if_visible(page, [
|
||||
'input[type="password"]',
|
||||
'input[name="password"]',
|
||||
'input[placeholder*="密码"]',
|
||||
], self.password)
|
||||
|
||||
await self._click_if_visible(page, [
|
||||
clicked = await self._click_if_visible(page, [
|
||||
'button:has-text("登录")',
|
||||
'button:has-text("登 录")',
|
||||
'button:has-text("Login")',
|
||||
'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.
|
||||
while time.time() < deadline:
|
||||
url = page.url
|
||||
@@ -147,3 +165,31 @@ class AutoLoginManager:
|
||||
except Exception:
|
||||
continue
|
||||
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)
|
||||
|
||||
|
||||
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")
|
||||
async def on_startup():
|
||||
global lingma, auto_login
|
||||
@@ -52,6 +58,8 @@ async def on_startup():
|
||||
headless=settings.auto_login_headless,
|
||||
timeout_sec=settings.auto_login_timeout,
|
||||
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