fix(qqbot): handle op 7/9 and expand fatal close code set

1. Handle op 7 (Server Reconnect): close WS to trigger reconnect loop
   while preserving session for Resume
2. Handle op 9 (Invalid Session): check d value to determine if session
   is resumable; clear session only when not resumable
3. Remove 4009 from session-clearing set (connection timeout is resumable)
4. Expand fatal close codes: 4001/4002/4010-4014 now stop reconnect
   immediately instead of retrying uselessly
5. Add unit tests
This commit is contained in:
walli
2026-05-18 17:37:20 +08:00
committed by Teknium
parent bbd77d165c
commit a54f5afc70
2 changed files with 210 additions and 4 deletions

View File

@@ -534,9 +534,30 @@ class QQAdapter(BasePlatformAdapter):
self._mark_transport_disconnected()
self._fail_pending("Connection closed")
# Stop reconnecting for fatal codes
if code in {4914, 4915}:
desc = "offline/sandbox-only" if code == 4914 else "banned"
# Stop reconnecting for fatal codes (unrecoverable errors)
if code in {
4001, # Invalid opcode
4002, # Invalid payload
4010, # Invalid shard
4011, # Sharding required
4012, # Invalid API version
4013, # Invalid intent
4014, # Intent not authorized
4914, # Offline/sandbox-only
4915, # Banned
}:
fatal_descriptions = {
4001: "invalid opcode",
4002: "invalid payload",
4010: "invalid shard",
4011: "sharding required",
4012: "invalid API version",
4013: "invalid intent",
4014: "intent not authorized",
4914: "offline/sandbox-only",
4915: "banned",
}
desc = fatal_descriptions.get(code, f"fatal error (code={code})")
logger.error(
"[%s] Bot is %s. Check QQ Open Platform.", self._log_tag, desc
)
@@ -573,10 +594,11 @@ class QQAdapter(BasePlatformAdapter):
self._token_expires_at = 0.0
# Session invalid → clear session, will re-identify on next Hello
# Note: 4009 (connection timeout) is NOT included here — it is
# resumable per the QQ protocol and should preserve session state.
if code in {
4006,
4007,
4009,
4900,
4901,
4902,
@@ -825,6 +847,32 @@ class QQAdapter(BasePlatformAdapter):
if op == 11:
return
# op 7 = Server Reconnect — server asks client to reconnect (e.g.
# load-balancing, maintenance). Close the WS so _read_events raises
# and the outer loop triggers a reconnect with Resume.
if op == 7:
logger.info("[%s] Server requested reconnect (op 7)", self._log_tag)
if self._ws and not self._ws.closed:
self._create_task(self._ws.close())
return
# op 9 = Invalid Session — d=True means session is resumable,
# d=False means we must re-identify from scratch.
if op == 9:
resumable = bool(d) if d is not None else False
if not resumable:
logger.info(
"[%s] Invalid session (op 9, not resumable), clearing session",
self._log_tag,
)
self._session_id = None
self._last_seq = None
else:
logger.info("[%s] Invalid session (op 9, resumable)", self._log_tag)
if self._ws and not self._ws.closed:
self._create_task(self._ws.close())
return
logger.debug("[%s] Unknown op: %s", self._log_tag, op)
def _handle_ready(self, d: Any) -> None: