The catch-all except Exception in QQ send() was swallowing
aiohttp.ClientError and OSError that _send_media correctly
re-raises. Add explicit catch for network errors before the
generic handler.
Audited all channel implementations for overly broad exception handling
that causes retry amplification or silent message loss during network
errors. This is the same class of bug as #3050 (Telegram _send_text).
Fixes by channel:
Telegram (send_delta):
- _stream_end path used except Exception for HTML edit fallback
- Network errors (TimedOut, NetworkError) triggered redundant plain
text edit, doubling connection demand during pool exhaustion
- Changed to except BadRequest, matching the _send_text fix
Discord:
- send() caught all exceptions without re-raising
- ChannelManager._send_with_retry() saw successful return, never retried
- Messages silently dropped on any send failure
- Added raise after error logging
DingTalk:
- _send_batch_message() returned False on all exceptions including
network errors — no retry, fallback text sent unnecessarily
- _read_media_bytes() and _upload_media() swallowed transport errors,
causing _send_media_ref() to cascade through doomed fallback attempts
- Added except httpx.TransportError handlers that re-raise immediately
WeChat:
- Media send failure triggered text fallback even for network errors
- During network issues: 3×(media + text) = 6 API calls per message
- Added specific catches: TimeoutException/TransportError re-raise,
5xx HTTPStatusError re-raises, 4xx falls back to text
QQ:
- _send_media() returned False on all exceptions
- Network errors triggered fallback text instead of retry
- Added except (aiohttp.ClientError, OSError) that re-raises
Tests: 331 passed (283 existing + 48 new across 5 channel test files)
Fixes: #3054
Related: #3050, #3053
QQ channel improvements (on top of nightly):
- Add top-level try/except in _on_message and send() for resilience
- Use defensive getattr() for attachment attributes (botpy version compat)
- Skip file_name for image uploads to avoid QQ rendering as file attachment
- Extract only file_info from upload response to avoid extra fields
- Handle protocol-relative URLs (//...) in attachment downloads
WeCom channel improvements:
- Add _upload_media_ws() for WebSocket 3-step media upload protocol
- Send media files (image/video/voice/file) via WeCom rich media API
- Support progress messages (plain reply) vs final response (streaming)
- Support proactive send when no frame available (cron push)
- Pass media_paths to message bus for downstream processing
Add ack_message config field to QQConfig (default: Processing...). When non-empty, sends an instant text reply before agent processing begins, filling the silence gap for users. Uses existing _send_text_only method; failure is logged but never blocks normal message handling.
Made-with: Cursor
- Fix _read_media_bytes treating local paths as URLs: local file
handling code was dead code placed after an early return inside the
HTTP try/except block. Restructure to check for local paths (plain
path or file:// URI) before URL validation, so files like
/home/.../.nanobot/workspace/generated_image.svg can be read and
sent correctly.
- Add .svg to _IMAGE_EXTS so SVG files are uploaded as file_type=1
(image) instead of file_type=4 (file).
- Add tests for local path, file:// URI, and missing file cases.
Fixes: https://github.com/HKUDS/nanobot/pull/1667#issuecomment-4096400955
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When nanobot is run as a systemd service with ProtectSystem=strict,
the process cwd defaults to the read-only root filesystem (/). botpy's
default Client configuration includes a TimedRotatingFileHandler that
writes 'botpy.log' to os.getcwd(), which raises [Errno 30] Read-only
file system.
Pass ext_handlers=False when constructing the botpy Client subclass to
suppress the file handler. nanobot already routes all log output through
loguru, so botpy's file handler is redundant.
Fixes#1343
Co-Authored-By: Claude <noreply@anthropic.com>
QQ's bot API requires a msg_id (original inbound message ID) to send a
passive reply. Without it the request is treated as a proactive message
and fails with error 40034102 (无权限). The message_id was already stored
in InboundMessage.metadata and forwarded to OutboundMessage, but was never
read in send().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QQ channel's start() created a background task and returned immediately,
violating the base Channel contract which specifies start() should be
"a long-running async task". This caused the gateway to exit prematurely
when QQ was the only enabled channel.
Now directly awaits _run_bot() to stay alive like other channels.
Fixes#894
Add official QQ platform support using botpy SDK with WebSocket connection.
Features:
- C2C (private message) support via QQ Open Platform
- WebSocket-based bot connection (no public IP required)
- Message deduplication with efficient deque-based LRU cache
- User whitelist support via allow_from configuration
- Clean async architecture using single event loop
Changes:
- Add QQChannel implementation in nanobot/channels/qq.py
- Add QQConfig schema with appId and secret fields
- Register QQ channel in ChannelManager
- Update README with QQ setup instructions
- Add qq-botpy dependency to pyproject.toml
- Add botpy.log to .gitignore
Setup:
1. Get AppID and Secret from q.qq.com
2. Configure in ~/.nanobot/config.json:
{
"channels": {
"qq": {
"enabled": true,
"appId": "YOUR_APP_ID",
"secret": "YOUR_APP_SECRET",
"allowFrom": []
}
}
}
3. Run: nanobot gateway
Note: Group chat support will be added in future updates.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>