更新
This commit is contained in:
+18
-3
@@ -770,13 +770,18 @@ def _get_account_cookie_data(account: Account) -> Optional[str]:
|
||||
return read_cookie_file(account.id)
|
||||
|
||||
|
||||
async def _build_cookie_response(account: Account, cookie_data: Optional[str] = None) -> AccountCookieResponse:
|
||||
async def _build_cookie_response(
|
||||
account: Account,
|
||||
cookie_data: Optional[str] = None,
|
||||
*,
|
||||
runtime_check: bool = True,
|
||||
) -> AccountCookieResponse:
|
||||
cookie_data = cookie_data if cookie_data is not None else _get_account_cookie_data(account)
|
||||
summary = cookie_summary(cookie_data)
|
||||
im_detail = await build_cookie_credential_detail(
|
||||
cookie_data,
|
||||
account.im_session_data,
|
||||
runtime_check=True,
|
||||
runtime_check=runtime_check,
|
||||
)
|
||||
return AccountCookieResponse(
|
||||
account_id=account.id,
|
||||
@@ -1608,7 +1613,17 @@ async def get_account_cookie(
|
||||
cookie_data = _get_account_cookie_data(account)
|
||||
if cookie_data and purpose != "management":
|
||||
await _require_desktop_login_sec_user_id(account, db)
|
||||
return await _build_cookie_response(account, cookie_data)
|
||||
return await _build_cookie_response(
|
||||
account,
|
||||
cookie_data,
|
||||
# Opening the edit dialog is a read-only management action. A live
|
||||
# Douyin credential probe can take many seconds and, with hundreds of
|
||||
# hosted accounts, would wait behind recurring background traffic.
|
||||
# The dialog only needs the locally stored credential fields; users
|
||||
# can still request an authoritative online check with the explicit
|
||||
# "recheck credential" action.
|
||||
runtime_check=purpose != "management",
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
|
||||
@@ -161,7 +161,13 @@ async def validate_im_session(
|
||||
auth = DouyinAuth.from_im_session(session)
|
||||
# 优先用已持久化的 my_uid,避免每次都发起网络 query_my_uid(uid_tt 是加密串,
|
||||
# int() 解析必然失败而回退到网络请求;该请求偶发失败会误判为“未就绪”)。
|
||||
uid = session.my_uid or auth.get_uid()
|
||||
uid = session.my_uid
|
||||
if not uid:
|
||||
# get_uid() may fall back to a synchronous HTTP request with a
|
||||
# multi-second timeout. Keep that work off FastAPI's event loop
|
||||
# so a manual credential recheck cannot freeze account editing or
|
||||
# unrelated API requests.
|
||||
uid = await asyncio.to_thread(auth.get_uid)
|
||||
if not uid:
|
||||
return False, "服务端未认可当前 Cookie(无法获取用户 UID)"
|
||||
if not auth.is_sign_ready():
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import threading
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from rpa_engine.credential import validate_im_session
|
||||
|
||||
|
||||
class CredentialResponsivenessTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_uid_lookup_does_not_block_event_loop(self):
|
||||
event_loop_thread_id = threading.get_ident()
|
||||
lookup_thread_ids = []
|
||||
|
||||
def get_uid():
|
||||
lookup_thread_ids.append(threading.get_ident())
|
||||
return 123456
|
||||
|
||||
session = SimpleNamespace(
|
||||
my_uid=0,
|
||||
can_direct_im=lambda: True,
|
||||
)
|
||||
auth = SimpleNamespace(
|
||||
get_uid=get_uid,
|
||||
is_sign_ready=lambda: True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"rpa_engine.credential.ensure_frontier_ws",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"rpa_engine.credential.DouyinAuth.from_im_session",
|
||||
return_value=auth,
|
||||
),
|
||||
):
|
||||
result = await validate_im_session(
|
||||
session,
|
||||
_bypass_global_limit=True,
|
||||
)
|
||||
|
||||
self.assertTrue(result[0])
|
||||
self.assertEqual(len(lookup_thread_ids), 1)
|
||||
self.assertNotEqual(
|
||||
lookup_thread_ids[0],
|
||||
event_loop_thread_id,
|
||||
"the synchronous UID lookup ran on the event-loop thread",
|
||||
)
|
||||
self.assertEqual(session.my_uid, 123456)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -171,7 +171,52 @@ class DesktopLoginSecUserIdTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertIs(result, response)
|
||||
db.execute.assert_not_awaited()
|
||||
build.assert_awaited_once_with(account, "cookie-json")
|
||||
build.assert_awaited_once_with(
|
||||
account,
|
||||
"cookie-json",
|
||||
runtime_check=False,
|
||||
)
|
||||
|
||||
async def test_cookie_response_forwards_static_management_check(self):
|
||||
account = self._account()
|
||||
summary = {
|
||||
"cookie_count": 2,
|
||||
"cookie_valid": True,
|
||||
"cookie_expired": False,
|
||||
"reason": "ok",
|
||||
"expires_at": None,
|
||||
"key_names": ["sessionid"],
|
||||
}
|
||||
detail = {
|
||||
"has_sessionid": True,
|
||||
"sessionid": "sid",
|
||||
"sessionid_ss": "",
|
||||
"im_ready": False,
|
||||
"im_status": "static",
|
||||
"can_skip_browser": False,
|
||||
"should_reset": False,
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(main, "cookie_summary", return_value=summary),
|
||||
patch.object(
|
||||
main,
|
||||
"build_cookie_credential_detail",
|
||||
new=AsyncMock(return_value=detail),
|
||||
) as build_detail,
|
||||
):
|
||||
result = await main._build_cookie_response(
|
||||
account,
|
||||
"cookie-json",
|
||||
runtime_check=False,
|
||||
)
|
||||
|
||||
self.assertTrue(result.has_sessionid)
|
||||
build_detail.assert_awaited_once_with(
|
||||
"cookie-json",
|
||||
None,
|
||||
runtime_check=False,
|
||||
)
|
||||
|
||||
async def test_legacy_cookie_request_is_guarded_for_existing_desktop_clients(self):
|
||||
account = self._account()
|
||||
|
||||
Reference in New Issue
Block a user