fix: harden auto-login selectors and poll auth status
Some checks failed
CI / lint-and-compile (push) Has been cancelled
CI / lint-and-compile (pull_request) Has been cancelled

This commit is contained in:
root
2026-04-17 14:40:28 +08:00
parent b621c4aca7
commit 5f0c1866a6
2 changed files with 57 additions and 3 deletions

View File

@@ -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)

View File

@@ -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)),
)