mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(wechat): persist confirmed QR auth before cancellation (#6199)
* fix(wechat): drain QR auth state writes on cancellation * test(wechat): pin cancelled QR credential persistence * fix(wechat): preserve auth persistence contract
This commit is contained in:
parent
9b16792168
commit
b782a3b173
@ -27,6 +27,7 @@ from app.channels.base import Channel
|
||||
from app.channels.commands import is_known_channel_command
|
||||
from app.channels.connection_identity import attach_connection_identity
|
||||
from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment
|
||||
from deerflow.utils.file_io import await_drained
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -777,6 +778,32 @@ class WechatChannel(Channel):
|
||||
params["verify_code"] = verify_code
|
||||
return await self._request_public_get_json("/ilink/bot/get_qrcode_status", params=params, timeout=timeout)
|
||||
|
||||
async def _save_auth_state_drained(
|
||||
self,
|
||||
*,
|
||||
status: str,
|
||||
bot_token: str | None = None,
|
||||
ilink_bot_id: str | None = None,
|
||||
qrcode: str | None = None,
|
||||
qrcode_img_content: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Persist QR auth state before propagating caller cancellation.
|
||||
|
||||
The write is a small local state-file update and is intentionally drained
|
||||
while _auth_lock is held so cancellation cannot expose an in-memory
|
||||
credential before its durable state settles.
|
||||
"""
|
||||
return await await_drained(
|
||||
asyncio.to_thread(
|
||||
self._save_auth_state,
|
||||
status=status,
|
||||
bot_token=bot_token,
|
||||
ilink_bot_id=ilink_bot_id,
|
||||
qrcode=qrcode,
|
||||
qrcode_img_content=qrcode_img_content,
|
||||
)
|
||||
)
|
||||
|
||||
async def _bind_via_qrcode(self) -> dict[str, Any]:
|
||||
qrcode_data = await self.request_login_qrcode()
|
||||
qrcode = str(qrcode_data.get("qrcode") or "").strip()
|
||||
@ -788,8 +815,7 @@ class WechatChannel(Channel):
|
||||
if qrcode_img_content:
|
||||
logger.warning("[WeChat] qrcode_img_content=%s", qrcode_img_content)
|
||||
|
||||
await asyncio.to_thread(
|
||||
self._save_auth_state,
|
||||
await self._save_auth_state_drained(
|
||||
status="pending",
|
||||
qrcode=qrcode,
|
||||
qrcode_img_content=qrcode_img_content or None,
|
||||
@ -808,8 +834,7 @@ class WechatChannel(Channel):
|
||||
if ilink_bot_id:
|
||||
self._ilink_bot_id = ilink_bot_id
|
||||
|
||||
return await asyncio.to_thread(
|
||||
self._save_auth_state,
|
||||
return await self._save_auth_state_drained(
|
||||
status="confirmed",
|
||||
bot_token=token,
|
||||
ilink_bot_id=self._ilink_bot_id,
|
||||
@ -818,8 +843,7 @@ class WechatChannel(Channel):
|
||||
)
|
||||
|
||||
if status in {"expired", "canceled", "cancelled", "invalid", "failed"}:
|
||||
await asyncio.to_thread(
|
||||
self._save_auth_state,
|
||||
await self._save_auth_state_drained(
|
||||
status=status,
|
||||
qrcode=qrcode,
|
||||
qrcode_img_content=qrcode_img_content or None,
|
||||
@ -828,8 +852,7 @@ class WechatChannel(Channel):
|
||||
|
||||
await asyncio.sleep(max(self._qrcode_poll_interval, 0.1))
|
||||
|
||||
await asyncio.to_thread(
|
||||
self._save_auth_state,
|
||||
await self._save_auth_state_drained(
|
||||
status="timeout",
|
||||
qrcode=qrcode,
|
||||
qrcode_img_content=qrcode_img_content or None,
|
||||
|
||||
@ -7,6 +7,7 @@ import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
@ -1490,6 +1491,66 @@ def test_qrcode_login_binds_and_persists_auth_state(monkeypatch, tmp_path: Path)
|
||||
_run(go())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qrcode_confirmation_drains_auth_persistence_across_cancellation(monkeypatch, tmp_path: Path):
|
||||
from app.channels.wechat import WechatChannel
|
||||
|
||||
state_dir = tmp_path / "wechat-state"
|
||||
channel = WechatChannel(
|
||||
bus=MessageBus(),
|
||||
config={
|
||||
"state_dir": str(state_dir),
|
||||
"qrcode_login_enabled": True,
|
||||
"qrcode_poll_interval": 0.01,
|
||||
"qrcode_poll_timeout": 1,
|
||||
},
|
||||
)
|
||||
started = threading.Event()
|
||||
release = threading.Event()
|
||||
original_save_auth_state = channel._save_auth_state
|
||||
|
||||
async def request_qrcode():
|
||||
return {"qrcode": "qr-cancel"}
|
||||
|
||||
async def request_status(_qrcode, **_kwargs):
|
||||
return {"status": "confirmed", "bot_token": "durable-token", "ilink_bot_id": "bot-cancel"}
|
||||
|
||||
def blocking_save_auth_state(**kwargs):
|
||||
if kwargs.get("status") == "confirmed":
|
||||
started.set()
|
||||
assert release.wait(timeout=5)
|
||||
return original_save_auth_state(**kwargs)
|
||||
|
||||
monkeypatch.setattr(channel, "request_login_qrcode", request_qrcode)
|
||||
monkeypatch.setattr(channel, "request_login_status", request_status)
|
||||
monkeypatch.setattr(channel, "_save_auth_state", blocking_save_auth_state)
|
||||
|
||||
task = asyncio.create_task(channel._bind_via_qrcode())
|
||||
try:
|
||||
assert await asyncio.to_thread(started.wait, 5)
|
||||
assert channel._bot_token == "durable-token"
|
||||
|
||||
task.cancel()
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await asyncio.sleep(0)
|
||||
assert not task.done()
|
||||
|
||||
release.set()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
finally:
|
||||
release.set()
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
auth_state = json.loads((state_dir / "wechat-auth.json").read_text(encoding="utf-8"))
|
||||
assert auth_state["status"] == "confirmed"
|
||||
assert auth_state["bot_token"] == "durable-token"
|
||||
assert auth_state["ilink_bot_id"] == "bot-cancel"
|
||||
|
||||
|
||||
def test_save_auth_state_tightens_preexisting_loose_file(tmp_path: Path):
|
||||
"""A world-readable auth file is replaced by an owner-only one, atomically.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user