diff --git a/digital-avatar-app/backend/services/takeover_service.py b/digital-avatar-app/backend/services/takeover_service.py index 9a96f9d..d3cbd76 100644 --- a/digital-avatar-app/backend/services/takeover_service.py +++ b/digital-avatar-app/backend/services/takeover_service.py @@ -201,13 +201,20 @@ class TakeoverService: def _forget_boxim_session(self, user_id: str): self._sessions.pop(user_id, None) - def _disable_after_connection_failure( + def _record_connection_failure( self, db: Session, avatar: Avatar, cursor: TakeoverCursor, message: str, + *, + disable_takeover: bool, ): + cursor.last_error = message + cursor.last_polled_at = self.now() + if not disable_takeover: + return + permissions = (avatar.config or {}).get("authorizationPermissions", []) avatar.config = { **(avatar.config or {}), @@ -217,8 +224,6 @@ class TakeoverService: if permission != TAKEOVER_PERMISSION ], } - cursor.last_error = message - cursor.last_polled_at = self.now() tasks = ( db.query(TakeoverReplyTask) .filter( @@ -245,11 +250,12 @@ class TakeoverService: db.add(cursor) db.flush() if not user or not user.huihui_token: - self._disable_after_connection_failure( + self._record_connection_failure( db, avatar, cursor, "请重新登录会会生产账号后再开启主动接管", + disable_takeover=True, ) db.commit() return False @@ -268,11 +274,24 @@ class TakeoverService: if isinstance(exc, BoxIMError) and exc.auth_error: self._forget_boxim_session(user.id) message = "BOXIM 授权已失效,请重新登录会会生产账号" + disable_takeover = True else: message = f"BOXIM 暂时连接失败:{str(exc)[:160]}" - self._disable_after_connection_failure(db, avatar, cursor, message) + disable_takeover = False + self._record_connection_failure( + db, + avatar, + cursor, + message, + disable_takeover=disable_takeover, + ) db.commit() - logger.warning("BOXIM sync failed for avatar %s: %s", avatar.id, exc) + logger.warning( + "BOXIM sync failed for avatar %s (will_retry=%s): %s", + avatar.id, + not disable_takeover, + exc, + ) return False messages.sort(key=lambda item: (_numeric_id(item.get("id")), item.get("sendTime") or 0)) diff --git a/digital-avatar-app/backend/tests/test_takeover_service.py b/digital-avatar-app/backend/tests/test_takeover_service.py index f8e850f..9c80596 100644 --- a/digital-avatar-app/backend/tests/test_takeover_service.py +++ b/digital-avatar-app/backend/tests/test_takeover_service.py @@ -291,5 +291,41 @@ async def test_connection_failure_disables_takeover_and_stops_retrying(service_c boxim.exchange_access_token.assert_awaited_once_with("prod-huihui-token") +@pytest.mark.asyncio +async def test_transient_connection_failure_keeps_takeover_and_recovers(service_context): + session_factory, service, boxim, _ = service_context + boxim.exchange_access_token = AsyncMock( + side_effect=[ + BoxIMError("连接超时"), + {"accessToken": "box-token", "accessTokenExpiresIn": 3600}, + ] + ) + + await service.poll_messages() + + db = session_factory() + try: + avatar = db.query(Avatar).one() + cursor = db.query(TakeoverCursor).one() + assert "takeover" in avatar.config["authorizationPermissions"] + assert cursor.initialized is False + assert "暂时连接失败" in cursor.last_error + finally: + db.close() + + await service.poll_messages() + + db = session_factory() + try: + avatar = db.query(Avatar).one() + cursor = db.query(TakeoverCursor).one() + assert "takeover" in avatar.config["authorizationPermissions"] + assert cursor.initialized is True + assert cursor.last_error == "" + finally: + db.close() + assert boxim.exchange_access_token.await_count == 2 + + def test_plain_text_reply_removes_markdown_and_empty_lines(): assert _plain_text_reply("## 建议\n\n**不能自行用药**\n`必要时就医`") == "建议\n不能自行用药\n必要时就医"