fix(gateway): keep QQBot reconnect loop alive

This commit is contained in:
magic524 2026-05-13 23:13:19 -07:00 committed by Teknium
parent f0e46c5e9e
commit 8199ec3803
2 changed files with 30 additions and 2 deletions

View file

@ -176,6 +176,28 @@ class QQAdapter(BasePlatformAdapter):
fut.set_exception(RuntimeError(reason)) fut.set_exception(RuntimeError(reason))
self._pending_responses.clear() self._pending_responses.clear()
def _mark_transport_disconnected(self) -> None:
"""Mark QQ WS down without stopping the reconnect loop.
BasePlatformAdapter uses _running for both process lifecycle and
connection status. QQBot needs to keep the listener task alive across
transient transport drops so it can continue reconnect attempts after a
short-lived gateway or network failure.
"""
if self.has_fatal_error:
return
self._write_runtime_status_safe(
"disconnected",
platform_state="disconnected",
error_code=None,
error_message=None,
)
@property
def is_connected(self) -> bool:
"""Return True only when the QQ WebSocket transport is usable."""
return bool(self._running and self._ws and not self._ws.closed)
def __init__(self, config: PlatformConfig): def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.QQBOT) super().__init__(config, Platform.QQBOT)
@ -509,7 +531,7 @@ class QQAdapter(BasePlatformAdapter):
else: else:
quick_disconnect_count = 0 quick_disconnect_count = 0
self._mark_disconnected() self._mark_transport_disconnected()
self._fail_pending("Connection closed") self._fail_pending("Connection closed")
# Stop reconnecting for fatal codes # Stop reconnecting for fatal codes
@ -531,6 +553,7 @@ class QQAdapter(BasePlatformAdapter):
RATE_LIMIT_DELAY, RATE_LIMIT_DELAY,
) )
if backoff_idx >= MAX_RECONNECT_ATTEMPTS: if backoff_idx >= MAX_RECONNECT_ATTEMPTS:
self._mark_disconnected()
return return
await asyncio.sleep(RATE_LIMIT_DELAY) await asyncio.sleep(RATE_LIMIT_DELAY)
if await self._reconnect(backoff_idx): if await self._reconnect(backoff_idx):
@ -584,17 +607,19 @@ class QQAdapter(BasePlatformAdapter):
backoff_idx += 1 backoff_idx += 1
if backoff_idx >= MAX_RECONNECT_ATTEMPTS: if backoff_idx >= MAX_RECONNECT_ATTEMPTS:
logger.error("[%s] Max reconnect attempts reached (QQCloseError)", self._log_tag) logger.error("[%s] Max reconnect attempts reached (QQCloseError)", self._log_tag)
self._mark_disconnected()
return return
except Exception as exc: except Exception as exc:
if not self._running: if not self._running:
return return
logger.warning("[%s] WebSocket error: %s", self._log_tag, exc) logger.warning("[%s] WebSocket error: %s", self._log_tag, exc)
self._mark_disconnected() self._mark_transport_disconnected()
self._fail_pending("Connection interrupted") self._fail_pending("Connection interrupted")
if backoff_idx >= MAX_RECONNECT_ATTEMPTS: if backoff_idx >= MAX_RECONNECT_ATTEMPTS:
logger.error("[%s] Max reconnect attempts reached", self._log_tag) logger.error("[%s] Max reconnect attempts reached", self._log_tag)
self._mark_disconnected()
return return
if await self._reconnect(backoff_idx): if await self._reconnect(backoff_idx):

View file

@ -4,6 +4,7 @@ import asyncio
import json import json
import os import os
import sys import sys
from types import SimpleNamespace
from unittest import mock from unittest import mock
import pytest import pytest
@ -578,6 +579,7 @@ class TestWaitForReconnection:
async def reconnect_after_delay(): async def reconnect_after_delay():
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
adapter._running = True adapter._running = True
adapter._ws = SimpleNamespace(closed=False)
asyncio.get_event_loop().create_task(reconnect_after_delay()) asyncio.get_event_loop().create_task(reconnect_after_delay())
@ -603,6 +605,7 @@ class TestWaitForReconnection:
"""send() should not wait when already connected.""" """send() should not wait when already connected."""
adapter = self._make_adapter(app_id="a", client_secret="b") adapter = self._make_adapter(app_id="a", client_secret="b")
adapter._running = True adapter._running = True
adapter._ws = SimpleNamespace(closed=False)
adapter._http_client = mock.MagicMock() adapter._http_client = mock.MagicMock()
async def fake_api_request(*args, **kwargs): async def fake_api_request(*args, **kwargs):