更新
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)
|
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)
|
cookie_data = cookie_data if cookie_data is not None else _get_account_cookie_data(account)
|
||||||
summary = cookie_summary(cookie_data)
|
summary = cookie_summary(cookie_data)
|
||||||
im_detail = await build_cookie_credential_detail(
|
im_detail = await build_cookie_credential_detail(
|
||||||
cookie_data,
|
cookie_data,
|
||||||
account.im_session_data,
|
account.im_session_data,
|
||||||
runtime_check=True,
|
runtime_check=runtime_check,
|
||||||
)
|
)
|
||||||
return AccountCookieResponse(
|
return AccountCookieResponse(
|
||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
@@ -1608,7 +1613,17 @@ async def get_account_cookie(
|
|||||||
cookie_data = _get_account_cookie_data(account)
|
cookie_data = _get_account_cookie_data(account)
|
||||||
if cookie_data and purpose != "management":
|
if cookie_data and purpose != "management":
|
||||||
await _require_desktop_login_sec_user_id(account, db)
|
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(
|
@app.get(
|
||||||
|
|||||||
@@ -161,7 +161,13 @@ async def validate_im_session(
|
|||||||
auth = DouyinAuth.from_im_session(session)
|
auth = DouyinAuth.from_im_session(session)
|
||||||
# 优先用已持久化的 my_uid,避免每次都发起网络 query_my_uid(uid_tt 是加密串,
|
# 优先用已持久化的 my_uid,避免每次都发起网络 query_my_uid(uid_tt 是加密串,
|
||||||
# int() 解析必然失败而回退到网络请求;该请求偶发失败会误判为“未就绪”)。
|
# 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:
|
if not uid:
|
||||||
return False, "服务端未认可当前 Cookie(无法获取用户 UID)"
|
return False, "服务端未认可当前 Cookie(无法获取用户 UID)"
|
||||||
if not auth.is_sign_ready():
|
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)
|
self.assertIs(result, response)
|
||||||
db.execute.assert_not_awaited()
|
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):
|
async def test_legacy_cookie_request_is_guarded_for_existing_desktop_clients(self):
|
||||||
account = self._account()
|
account = self._account()
|
||||||
|
|||||||
@@ -1546,7 +1546,7 @@ const formatCookieTime = (value) => {
|
|||||||
return date.toLocaleString('zh-CN')
|
return date.toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyCookieResponse = (data) => {
|
const applyCookieResponse = (data, { preserveRuntimeCredential = false } = {}) => {
|
||||||
editForm.value.cookie_updated_at = data.cookie_updated_at
|
editForm.value.cookie_updated_at = data.cookie_updated_at
|
||||||
editForm.value.cookie_count = data.cookie_count
|
editForm.value.cookie_count = data.cookie_count
|
||||||
editForm.value.cookie_valid = data.cookie_valid
|
editForm.value.cookie_valid = data.cookie_valid
|
||||||
@@ -1556,10 +1556,12 @@ const applyCookieResponse = (data) => {
|
|||||||
editForm.value.has_sessionid = !!data.has_sessionid
|
editForm.value.has_sessionid = !!data.has_sessionid
|
||||||
editForm.value.sessionid = data.sessionid || ''
|
editForm.value.sessionid = data.sessionid || ''
|
||||||
editForm.value.sessionid_ss = data.sessionid_ss || ''
|
editForm.value.sessionid_ss = data.sessionid_ss || ''
|
||||||
editForm.value.im_ready = !!data.im_ready
|
if (!preserveRuntimeCredential) {
|
||||||
editForm.value.im_status = data.im_status || ''
|
editForm.value.im_ready = !!data.im_ready
|
||||||
editForm.value.can_skip_browser = !!data.can_skip_browser
|
editForm.value.im_status = data.im_status || ''
|
||||||
editForm.value.should_reset = !!data.should_reset
|
editForm.value.can_skip_browser = !!data.can_skip_browser
|
||||||
|
editForm.value.should_reset = !!data.should_reset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEditModal = async (acc) => {
|
const openEditModal = async (acc) => {
|
||||||
@@ -1628,7 +1630,7 @@ const refreshCredential = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cookieRes = await api.get(`/accounts/${editForm.value.id}/cookie?purpose=management`)
|
const cookieRes = await api.get(`/accounts/${editForm.value.id}/cookie?purpose=management`)
|
||||||
applyCookieResponse(cookieRes.data)
|
applyCookieResponse(cookieRes.data, { preserveRuntimeCredential: true })
|
||||||
message.success('凭证检测完成')
|
message.success('凭证检测完成')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.response?.data?.detail || '凭证检测失败')
|
message.error(error.response?.data?.detail || '凭证检测失败')
|
||||||
|
|||||||
Reference in New Issue
Block a user