fix(creator): 扫码成功却一直停在二维码上 + 运营改为左右布局
【扫码不完成】根因是判据本身。原先读页面里的 window.__INITIAL_STATE__,而那是 页面加载那一刻的快照:监控那边的同一探针能用,是因为那台浏览器的页面加载时就已经 登录了;而扫码是加载之后才登录的 —— SPA 内部确实登进去了,但初始快照不会翻转, 于是检测永远等不到。 改成拿 cookie 直接问创作者后台 /api/galaxy/user/info 我是谁。实测这个判据很干净: 游客也会拿到 a1(所以签名算得出来),但接口直接回 401 无登录信息;只有真正登录了 才返回 user_id。所以「有 a1」什么都证明不了,后台认了才算。 顺带按 5 秒节流 —— 前端每 2 秒问一次,没必要每次都打后台接口。 【弹窗不关】成功后不自动关闭,停在二维码上会让人以为没成功。现在显示账号卡片与原话 提示,1.6 秒后自动关闭并提供一个「完成」按钮。 【已完成结果会残留】take_cookie 取走 cookie 就拆会话,而在飞的轮询会看到 _current 为空 回报 idle,把已显示的成功能擦掉。现在把结果记在模块里重复返回,关闭弹窗时清掉 —— 否则下次打开会立刻显示上次的成功。 【布局】按用户要求改成左右两栏(左账号列表、右数据面板),与监控统一,取消二级菜单。 未选过时默认选中第一个,右栏不会一开始就是空的。 测试:tests/test_creator_login.py 新增 7 例,含「游客会话永不完成」「接口不打满每次轮询」 「临时上下文用完必须关掉」。
This commit is contained in:
+52
-32
@@ -31,7 +31,6 @@
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
@@ -39,6 +38,7 @@ from playwright.async_api import async_playwright
|
||||
from tools import utils
|
||||
|
||||
from ..monitor.platforms import PLATFORM_XHS
|
||||
from .client import CreatorApiError, CreatorClient
|
||||
|
||||
QR_TTL_SECONDS = 300
|
||||
|
||||
@@ -52,32 +52,26 @@ LOGIN_URL = "https://www.xiaohongshu.com"
|
||||
QR_SELECTOR = "xpath=//img[@class='qrcode-img']"
|
||||
LOGIN_BUTTON_SELECTOR = "xpath=//*[@id='app']/div[1]/div[2]/div[1]/ul/div[1]/button"
|
||||
|
||||
# 与 api/monitor/qrlogin.py 同一套判据:主站在 window.__INITIAL_STATE__ 里报告登录态,
|
||||
# 而 user.loggedIn 是 Vue 的响应式引用,要 .value 解包才是布尔值。
|
||||
LOGIN_STATE_PROBE = """
|
||||
() => {
|
||||
try {
|
||||
const user = (window.__INITIAL_STATE__ || {}).user;
|
||||
if (!user) return JSON.stringify({ known: false });
|
||||
let loggedIn = user.loggedIn;
|
||||
if (loggedIn && typeof loggedIn === 'object' && 'value' in loggedIn) loggedIn = loggedIn.value;
|
||||
let info = null;
|
||||
try { info = user.userInfo || null; } catch (e) { info = null; }
|
||||
const text = (v) => (v === null || v === undefined ? null : String(v));
|
||||
return JSON.stringify({
|
||||
known: true,
|
||||
loggedIn: Boolean(loggedIn),
|
||||
nickname: info ? text(info.nickname) : null
|
||||
});
|
||||
} catch (e) {
|
||||
return JSON.stringify({ known: false, error: String(e) });
|
||||
}
|
||||
}
|
||||
"""
|
||||
# 判据不读页面状态,而是拿 cookie 直接问创作者后台"我是谁"。
|
||||
#
|
||||
# **为什么不用页面状态**:`window.__INITIAL_STATE__` 是**页面加载那一刻的快照**。
|
||||
# 监控那边的同一个探针能用,是因为那台浏览器的页面加载时就已经登录了,快照里
|
||||
# loggedIn 就是 true。而扫码是"页面加载之后才登录的"—— SPA 内部确实登进去了,
|
||||
# 但那个初始快照不会翻转,于是检测永远等不到,界面就一直停在二维码上。
|
||||
#
|
||||
# `user/info` 则是权威的:实测**游客也会拿到 a1**(所以签名算得出来),但接口直接
|
||||
# 回 401「无登录信息」;只有真正登录了才返回 user_id。所以"有 a1"什么都证明不了,
|
||||
# "后台认这份身份"才是。
|
||||
LOGIN_CHECK_INTERVAL_SECONDS = 5.0
|
||||
|
||||
_lock = asyncio.Lock()
|
||||
_current: Optional["AccountLoginSession"] = None
|
||||
|
||||
# 完成后的快照。会话一旦被取走 cookie 就会拆掉,而前端可能还有一个在飞的轮询——
|
||||
# 那个请求若看到 _current 为空就会回报 idle,把已经显示出来的成功状态又擦掉。
|
||||
# 把结果留在这里,重复轮询就稳定得多。
|
||||
_last_result: Optional[Dict[str, Any]] = None
|
||||
|
||||
_playwright: Any = None
|
||||
|
||||
|
||||
@@ -132,6 +126,7 @@ class AccountLoginSession:
|
||||
self.platform = PLATFORM_XHS
|
||||
self._context = context
|
||||
self._page = page
|
||||
self._last_login_check = 0.0
|
||||
|
||||
@property
|
||||
def elapsed(self) -> float:
|
||||
@@ -145,23 +140,36 @@ class AccountLoginSession:
|
||||
self.message = "二维码已超时,请重新获取"
|
||||
return
|
||||
|
||||
# 前端每 2 秒问一次,但没必要每次都去打后台接口 —— 一次真实的网络往返
|
||||
# 去确认一个通常还没发生的事件是浪费。
|
||||
now = time.time()
|
||||
if now - self._last_login_check < LOGIN_CHECK_INTERVAL_SECONDS:
|
||||
return
|
||||
self._last_login_check = now
|
||||
|
||||
try:
|
||||
raw = await self._page.evaluate(LOGIN_STATE_PROBE)
|
||||
state = json.loads(raw) if isinstance(raw, str) else {}
|
||||
cookies = await self._context.cookies()
|
||||
except Exception:
|
||||
self.status = STATUS_ERROR
|
||||
self.message = "二维码所在页面已被关闭,请重新获取"
|
||||
self.message = "登录窗口已被关闭,请重新获取"
|
||||
return
|
||||
|
||||
if not state.get("logged_in"):
|
||||
cookie = "; ".join(f"{c['name']}={c['value']}" for c in cookies)
|
||||
try:
|
||||
info = await CreatorClient(cookie).fetch_user_info()
|
||||
except CreatorApiError:
|
||||
# 还没登录(或者刚扫、后端还没认),继续等。
|
||||
return
|
||||
|
||||
# 登录成功:从**这个临时上下文**取 cookie。取完上下文就丢弃,
|
||||
if not info.get("user_id"):
|
||||
return
|
||||
|
||||
# 登录成功:cookie 取自**这个临时上下文**,取完上下文就丢弃,
|
||||
# 所以不会残留、也不会影响别的账号。
|
||||
cookies = await self._context.cookies()
|
||||
self.cookie = "; ".join(f"{c['name']}={c['value']}" for c in cookies)
|
||||
self.cookie = cookie
|
||||
self.account = info
|
||||
self.status = STATUS_SUCCESS
|
||||
self.message = "已获取登录态,正在识别账号…"
|
||||
self.message = f"登录成功:{info.get('nickname') or info['user_id']}"
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return {
|
||||
@@ -191,9 +199,10 @@ async def _teardown_locked() -> None:
|
||||
|
||||
async def start() -> Dict[str, Any]:
|
||||
"""开一个临时上下文,打开登录页,取回二维码。"""
|
||||
global _current
|
||||
global _current, _last_result
|
||||
|
||||
async with _lock:
|
||||
_last_result = None
|
||||
await _teardown_locked()
|
||||
|
||||
try:
|
||||
@@ -234,6 +243,8 @@ async def start() -> Dict[str, Any]:
|
||||
async def status() -> Dict[str, Any]:
|
||||
async with _lock:
|
||||
if _current is None:
|
||||
if _last_result is not None:
|
||||
return _last_result
|
||||
return {
|
||||
"status": STATUS_IDLE,
|
||||
"message": "",
|
||||
@@ -246,6 +257,13 @@ async def status() -> Dict[str, Any]:
|
||||
return _current.snapshot()
|
||||
|
||||
|
||||
async def remember_result(snapshot: Dict[str, Any]) -> None:
|
||||
"""记住已完成的扫码结果,供后续轮询重复返回。"""
|
||||
global _last_result
|
||||
async with _lock:
|
||||
_last_result = snapshot
|
||||
|
||||
|
||||
async def take_cookie() -> Optional[str]:
|
||||
"""取走已登录的 cookie 并结束会话。
|
||||
|
||||
@@ -262,7 +280,9 @@ async def take_cookie() -> Optional[str]:
|
||||
|
||||
|
||||
async def cancel() -> Dict[str, Any]:
|
||||
global _last_result
|
||||
async with _lock:
|
||||
_last_result = None
|
||||
await _teardown_locked()
|
||||
return {
|
||||
"status": STATUS_IDLE,
|
||||
|
||||
Reference in New Issue
Block a user