mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(wechat): extend QR-code status request timeout (#6783)
* fix(wechat): extend QR status polling timeout * fix(wechat): bound QR polling request timeout * fix(wechat): enforce QR status deadline
This commit is contained in:
parent
b5bcf64741
commit
c645f65b3a
@ -31,6 +31,7 @@ from app.channels.message_bus import InboundMessage, InboundMessageType, Message
|
||||
from deerflow.utils.file_io import await_drained
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_monotonic = time.monotonic
|
||||
|
||||
|
||||
class MessageItemType(IntEnum):
|
||||
@ -227,6 +228,9 @@ class WechatChannel(Channel):
|
||||
DEFAULT_QRCODE_POLL_INTERVAL = 2.0
|
||||
DEFAULT_QRCODE_POLL_TIMEOUT = 180.0
|
||||
DEFAULT_QRCODE_BOT_TYPE = 3
|
||||
DEFAULT_QRCODE_STATUS_TIMEOUT = 30.0
|
||||
# Browser QR login allows a small cushion over the channel polling timeout.
|
||||
DEFAULT_QRCODE_ADMIN_STATUS_TIMEOUT = DEFAULT_QRCODE_STATUS_TIMEOUT + 5.0
|
||||
DEFAULT_API_TIMEOUT = 15.0
|
||||
DEFAULT_CONFIG_TIMEOUT = 10.0
|
||||
DEFAULT_CDN_TIMEOUT = 30.0
|
||||
@ -836,7 +840,11 @@ class WechatChannel(Channel):
|
||||
params = {"qrcode": qrcode}
|
||||
if verify_code:
|
||||
params["verify_code"] = verify_code
|
||||
return await self._request_public_get_json("/ilink/bot/get_qrcode_status", params=params, timeout=timeout)
|
||||
return await self._request_public_get_json(
|
||||
"/ilink/bot/get_qrcode_status",
|
||||
params=params,
|
||||
timeout=self.DEFAULT_QRCODE_STATUS_TIMEOUT if timeout is None else timeout,
|
||||
)
|
||||
|
||||
async def _save_auth_state_drained(
|
||||
self,
|
||||
@ -881,9 +889,19 @@ class WechatChannel(Channel):
|
||||
qrcode_img_content=qrcode_img_content or None,
|
||||
)
|
||||
|
||||
deadline = time.monotonic() + max(self._qrcode_poll_timeout, 1.0)
|
||||
while time.monotonic() < deadline:
|
||||
status_data = await self.request_login_status(qrcode)
|
||||
deadline = _monotonic() + max(self._qrcode_poll_timeout, 1.0)
|
||||
while True:
|
||||
remaining = deadline - _monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
try:
|
||||
async with asyncio.timeout(remaining):
|
||||
status_data = await self.request_login_status(
|
||||
qrcode,
|
||||
timeout=min(remaining, self.DEFAULT_QRCODE_STATUS_TIMEOUT),
|
||||
)
|
||||
except TimeoutError:
|
||||
break
|
||||
status = str(status_data.get("status") or "").strip().lower()
|
||||
if status == "confirmed":
|
||||
token = str(status_data.get("bot_token") or "").strip()
|
||||
|
||||
@ -73,7 +73,11 @@ class WechatQRLogin:
|
||||
try:
|
||||
if qrcode is None:
|
||||
return await channel.request_login_qrcode()
|
||||
return await channel.request_login_status(qrcode, timeout=35, verify_code=verify_code)
|
||||
return await channel.request_login_status(
|
||||
qrcode,
|
||||
timeout=WechatChannel.DEFAULT_QRCODE_ADMIN_STATUS_TIMEOUT,
|
||||
verify_code=verify_code,
|
||||
)
|
||||
finally:
|
||||
await channel.stop()
|
||||
|
||||
|
||||
@ -117,6 +117,80 @@ def test_timing_config_requires_positive_finite_values():
|
||||
assert channel._retry_delay == 0.25
|
||||
|
||||
|
||||
def test_qrcode_status_uses_dedicated_request_timeout():
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
async def go():
|
||||
channel = WechatChannel(bus=MessageBus(), config={"bot_token": "test-token"})
|
||||
request = AsyncMock(return_value={"status": "wait"})
|
||||
channel._request_public_get_json = request # type: ignore[method-assign]
|
||||
|
||||
await channel.request_login_status("qr-token")
|
||||
|
||||
request.assert_awaited_once_with(
|
||||
"/ilink/bot/get_qrcode_status",
|
||||
params={"qrcode": "qr-token"},
|
||||
timeout=WechatChannel.DEFAULT_QRCODE_STATUS_TIMEOUT,
|
||||
)
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
def test_qrcode_poll_request_timeout_is_bounded_by_remaining_deadline(monkeypatch):
|
||||
from app.channels import wechat as wechat_module
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
async def go():
|
||||
channel = WechatChannel(
|
||||
bus=MessageBus(),
|
||||
config={"bot_token": "test-token", "qrcode_poll_timeout": 10},
|
||||
)
|
||||
channel.request_login_qrcode = AsyncMock(return_value={"qrcode": "qr-token"}) # type: ignore[method-assign]
|
||||
channel.request_login_status = AsyncMock( # type: ignore[method-assign]
|
||||
return_value={"status": "confirmed", "bot_token": "bound-token"}
|
||||
)
|
||||
channel._save_auth_state_drained = AsyncMock(return_value={"status": "confirmed"}) # type: ignore[method-assign]
|
||||
|
||||
monotonic_values = iter([0.0, 9.0])
|
||||
monkeypatch.setattr(wechat_module, "_monotonic", lambda: next(monotonic_values))
|
||||
|
||||
result = await channel._bind_via_qrcode()
|
||||
|
||||
assert result == {"status": "confirmed"}
|
||||
channel.request_login_status.assert_awaited_once_with("qr-token", timeout=1.0)
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
def test_qrcode_poll_request_is_cancelled_at_total_deadline(monkeypatch):
|
||||
from app.channels import wechat as wechat_module
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
async def go():
|
||||
channel = WechatChannel(
|
||||
bus=MessageBus(),
|
||||
config={"bot_token": "test-token", "qrcode_poll_timeout": 1},
|
||||
)
|
||||
channel.request_login_qrcode = AsyncMock(return_value={"qrcode": "qr-token"}) # type: ignore[method-assign]
|
||||
|
||||
async def request_status(_qrcode, **_kwargs):
|
||||
await asyncio.sleep(10)
|
||||
|
||||
channel.request_login_status = request_status # type: ignore[method-assign]
|
||||
save_auth_state = AsyncMock(return_value={"status": "timeout"})
|
||||
channel._save_auth_state_drained = save_auth_state # type: ignore[method-assign]
|
||||
|
||||
monotonic_values = iter([0.0, 0.0])
|
||||
monkeypatch.setattr(wechat_module, "_monotonic", lambda: next(monotonic_values))
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
await asyncio.wait_for(channel._bind_via_qrcode(), timeout=2)
|
||||
|
||||
assert save_auth_state.await_args_list[-1].kwargs["status"] == "timeout"
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
def test_handle_update_publishes_private_chat_message():
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
|
||||
@ -137,6 +137,26 @@ async def test_browser_transport_closes_http_client_without_starting_channel(mon
|
||||
start.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_browser_status_request_uses_channel_qrcode_timeout(monkeypatch):
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
assert WechatChannel.DEFAULT_QRCODE_ADMIN_STATUS_TIMEOUT == 35.0
|
||||
request = AsyncMock(return_value={"status": "wait"})
|
||||
stop = AsyncMock()
|
||||
monkeypatch.setattr(WechatChannel, "request_login_status", request)
|
||||
monkeypatch.setattr(WechatChannel, "stop", stop)
|
||||
|
||||
await WechatQRLogin().request({}, "qr-token")
|
||||
|
||||
request.assert_awaited_once_with(
|
||||
"qr-token",
|
||||
timeout=WechatChannel.DEFAULT_QRCODE_ADMIN_STATUS_TIMEOUT,
|
||||
verify_code=None,
|
||||
)
|
||||
stop.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_polls_only_apply_once(login):
|
||||
session = await login.start("alice", {})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user