This commit is contained in:
Your Name
2026-07-28 11:49:36 +08:00
parent f99a4edf83
commit ac406a5f99
5 changed files with 132 additions and 11 deletions
+18 -3
View File
@@ -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(
+7 -1
View File
@@ -161,7 +161,13 @@ async def validate_im_session(
auth = DouyinAuth.from_im_session(session)
# 优先用已持久化的 my_uid,避免每次都发起网络 query_my_uiduid_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()