fix(qqbot): fix media send-back, interaction events, and WebSocket resilience - #27944
fix(qqbot): fix media send-back, interaction events, and WebSocket resilience#27944WideLee wants to merge 5 commits into
Conversation
1. Add INTERACTION intent bit (1<<26) to _send_identify, fixing approval button clicks not being received (INTERACTION_CREATE events were never dispatched by the gateway) 2. Include local cached path in video/file attachment descriptions so the LLM can reference files for re-sending to users 3. Add unit tests (TestIdentifyIntents, TestProcessAttachmentsPathExposure)
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
Add original_name parameter to _download_and_cache, preferring the attachment metadata filename over the CDN URL path basename. Previously files were cached with meaningless QQ CDN hash names (e.g. qqdownload_...oadftnv5), causing ugly filenames when sent back to users. Aligns with qqbot-agent-sdk's AttachmentDownloader.download_document.
_guess_ext_from_data: data[:5] == b"#!SILK" -> data[:6] (6-byte string) _looks_like_silk: data[:4] == b"#!SILK" -> data[:6] The previous slices were too short to ever match the 6-byte "#!SILK" literal, relying entirely on the "#!SILK_V3" (9-byte) and 0x02! (2-byte) fallback paths for SILK format detection.
|
BoardJames triage: this looks shared/systemic rather than branch-local. The PR-specific checks (lint/nix/e2e/builds/attribution/history) are green where completed; the remaining blocker is the main |
Critical threading/concurrency fixes for the QQBot adapter, plus media delivery support for the send_message tool. ## WebSocket dedicated thread Move the QQBot WebSocket lifecycle (listen loop, heartbeat, reconnect) to a dedicated daemon thread with its own asyncio event loop. This prevents QQBot's reconnect backoff and heartbeat from blocking the main gateway event loop shared with Discord, Feishu, Telegram, etc. Same pattern as qqbot-agent-sdk's QQWebSocket and the Feishu adapter's _run_official_feishu_ws_client. Inbound message callbacks are dispatched back to the main loop via asyncio.run_coroutine_threadsafe(). ## Heartbeat restart on Hello (op 10) Add _restart_heartbeat() — cancels the current heartbeat task and creates a fresh one every time a Hello is received (both initial connect and reconnect). This ensures the new heartbeat_interval takes effect immediately instead of waiting for a stale asyncio.sleep() to expire, which could delay the first heartbeat by up to 41s and trigger a 4009 timeout from the QQ gateway. ## Thread-safe token management - Replace asyncio.Lock with threading.Lock for _token_lock - Add _ensure_token_sync() using httpx.Client (sync) for WS thread - Add _get_gateway_url_sync() for WS thread - _send_identify / _send_resume / _reconnect use sync token methods - async _ensure_token() delegates to run_in_executor(sync_version) to avoid holding a threading.Lock while awaiting I/O ## QQBot media delivery via send_message tool - Add _send_qqbot_media() — routes media through the running gateway adapter's upload pipeline (send_image_file/send_video/send_voice/ send_document) - Add QQBot media early-return block before the "Non-media platforms" error check - Update error messages to include qqbot in supported platforms list ## Other fixes - disconnect() no longer awaits tasks from the WS thread's loop (would raise "cannot await on a future created in a different loop") - _ws_thread_main only awaits _listen_task; heartbeat is cancelled in finally (handles dynamic _restart_heartbeat replacements correctly) - _dispatch_to_main_loop adds done_callback to log exceptions from dispatched coroutines (prevents silent exception swallowing) Tests: 273 passed (158 qqbot + 115 send_message)
|
Salvaged 4 of your 5 commits onto current main via #30843 — rebase-merged with your authorship preserved (walli@tencent.com). Landed:
Deferred the 5th commit (310cd3e — dedicated WS thread + media send support) for a separate review with live gateway validation; that commit is a substantial concurrency rewrite (asyncio.Lock → threading.Lock, run_coroutine_threadsafe cross-loop dispatch) that supersedes #28167 and warrants live testing before merge. Thanks for the fixes! |
What does this PR do?
Fixes multiple QQBot platform issues discovered during integration testing: video/file media cannot be sent back to users, interaction button events (approvals) are silently dropped, WebSocket reconnection doesn't handle server-initiated disconnects properly, and cached files have unreadable CDN hash names instead of their original filenames.
Root causes identified by diffing against the reference
qqbot-agent-sdkimplementation.Related Issue
Fixes internal QQBot platform test failures (video send-back, file send-back, quoted video/file, approval button clicks).
Type of Change
Changes Made
1. Add INTERACTION intent (
gateway/platforms/qqbot/adapter.py)_send_identify()intents was missing1 << 26(INTERACTION bit), causing the QQ WebSocket gateway to never dispatchINTERACTION_CREATEevents — approval button clicks were silently lost.2. Expose video/file cached paths to LLM (
gateway/platforms/qqbot/adapter.py)_process_attachments()now formats non-image/voice attachments as[video: name (path)]or[file: name (path)]instead of the opaque[Attachment: name], so the LLM can reference local paths when the user asks to send media back.3. Handle WebSocket op 7 and op 9 (
gateway/platforms/qqbot/adapter.py)payload.dto decide whether to Resume or re-Identify — previously ignored entirely.4. Fix close code classification (
gateway/platforms/qqbot/adapter.py)4009from session-clearing set (connection timeout is resumable per QQ protocol).4001, 4002, 4010-4014(unrecoverable errors that should not retry).5. Use original attachment filename for cached files (
gateway/platforms/qqbot/adapter.py)_download_and_cache()now acceptsoriginal_nameparameter, preferring the attachment metadatafilenameover the CDN URL path basename (which is a meaningless hash likeqqdownload_...oadftnv5).6. Fix SILK magic byte detection (
gateway/platforms/qqbot/adapter.py)_guess_ext_from_data:data[:5]→data[:6]for the 6-byteb"#!SILK"literal._looks_like_silk:data[:4]→data[:6](same issue).7. Tests (
tests/gateway/test_qqbot.py)TestIdentifyIntents— verifies all 4 intent bits including INTERACTION.TestProcessAttachmentsPathExposure— video/file path in descriptions, quoted message paths, download failure handling.TestOp7ServerReconnect— op 7 triggers WS close, preserves session.TestOp9InvalidSession— resumable vs non-resumable session handling.TestCloseCodeClassification— 4009 not in clear set, fatal codes complete.How to Test
INTERACTION_CREATE)pytest tests/gateway/test_qqbot.py -q→ all 158 tests passChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Test results: