diff --git a/app/auto_login.py b/app/auto_login.py index 4d09c3c..54155d9 100644 --- a/app/auto_login.py +++ b/app/auto_login.py @@ -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) diff --git a/app/main.py b/app/main.py index 0139fc4..9259425 100644 --- a/app/main.py +++ b/app/main.py @@ -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)), )