Replace the flatten/unflatten approach (merging extra_content.google.*
into provider_specific_fields then reconstructing) with direct pass-through:
parse extra_content as-is, store on ToolCallRequest.extra_content, serialize
back untouched. This is lossless, requires no hardcoded field names, and
covers all three parsing branches (str, dict, SDK object) plus streaming.
Handle string and dict-shaped responses from OpenAI-compatible backends so non-standard providers no longer crash on missing choices fields. Add regression tests to keep SDK, dict, and plain-text parsing paths aligned.
- Prevent repeated retries on expired sessions in the polling thread
- Stop sending messages to invalid agent sessions to eliminate noise logs and unnecessary requests
* feat(feishu): add streaming support via CardKit PATCH API
Implement send_delta() for Feishu channel using interactive card
progressive editing:
- First delta creates a card with markdown content and typing cursor
- Subsequent deltas throttled at 0.5s to respect 5 QPS PATCH limit
- stream_end finalizes with full formatted card (tables, rich markdown)
Also refactors _send_message_sync to return message_id (str | None)
and adds _patch_card_sync for card updates.
Includes 17 new unit tests covering streaming lifecycle, config,
card building, and edge cases.
Made-with: Cursor
* feat(feishu): close CardKit streaming_mode on stream end
Call cardkit card.settings after final content update so chat preview
leaves default [生成中...] summary (Feishu streaming docs).
Made-with: Cursor
* style: polish Feishu streaming (PEP8 spacing, drop unused test imports)
Made-with: Cursor
* docs(feishu): document cardkit:card:write for streaming
- README: permissions, upgrade note for existing apps, streaming toggle
- CHANNEL_PLUGIN_GUIDE: Feishu CardKit scope and when to disable streaming
Made-with: Cursor
* docs: address PR 2382 review (test path, plugin guide, README, English docstrings)
- Move Feishu streaming tests to tests/channels/
- Remove Feishu CardKit scope from CHANNEL_PLUGIN_GUIDE (plugin-dev doc only)
- README Feishu permissions: consistent English
- feishu.py: replace Chinese in streaming docstrings/comments
Made-with: Cursor
Keep the mypy-friendly optional execute signatures while returning clearer errors for missing arguments and locking that behavior with regression tests.
Made-with: Cursor
Keep the channel enhancements aligned with the current codebase while preserving a simpler product surface. This keeps QQ, Feishu, Telegram, and WhatsApp improvements together, removes the extra Telegram-only tool hint toggle, and makes WhatsApp mention-only groups actually work.
Keep cron state workspace-scoped while only migrating legacy jobs into the default workspace. This preserves seamless upgrades for existing installs without polluting intentionally new workspaces.
Move channel-specific login logic from CLI into each channel class via a
new `login(force=False)` method on BaseChannel. The `channels login <name>`
command now dynamically loads the channel and calls its login() method.
- WeixinChannel.login(): calls existing _qr_login(), with force to clear saved token
- WhatsAppChannel.login(): sets up bridge and spawns npm process for QR login
- CLI no longer contains duplicate login logic per channel
- Update CHANNEL_PLUGIN_GUIDE to document the login() hook
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously the WeChat channel's send() method only handled text messages,
completely ignoring msg.media. When the agent called message(media=[...]),
the file was never delivered to the user.
Implement the full WeChat CDN upload protocol following the reference
@tencent-weixin/openclaw-weixin v1.0.2:
1. Generate a client-side AES-128 key (16 random bytes)
2. Call getuploadurl with file metadata + hex-encoded AES key
3. AES-128-ECB encrypt the file and POST to CDN with filekey param
4. Read x-encrypted-param from CDN response header as download param
5. Send message with the media item (image/video/file) referencing
the CDN upload
Also adds:
- _encrypt_aes_ecb() for AES-128-ECB encryption (reverse of existing
_decrypt_aes_ecb)
- Media type detection from file extension (image/video/file)
- Graceful error handling: failed media sends notify the user via text
without blocking subsequent text delivery
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
During testing, we discovered that when a user requests the agent to
send a file (e.g., "send me IMG_1115.png"), the agent would call
read_file to view the content and then reply with text claiming
"file sent" — but never actually deliver the file to the user.
Root cause: The system prompt stated "Reply directly with text for
conversations. Only use the 'message' tool to send to a specific
chat channel", which led the LLM to believe text replies were
sufficient for all responses, including file delivery.
Fix: Add an explicit IMPORTANT instruction in the system prompt
telling the LLM it MUST use the 'message' tool with the 'media'
parameter to send files, and that read_file only reads content
for its own analysis.
Co-Authored-By: qulllee <qullkui@tencent.com>
Add a new WeChat (微信) channel that connects to personal WeChat using
the ilinkai.weixin.qq.com HTTP long-poll API. Protocol reverse-engineered
from @tencent-weixin/openclaw-weixin v1.0.2.
Features:
- QR code login flow (nanobot weixin login)
- HTTP long-poll message receiving (getupdates)
- Text message sending with proper WeixinMessage format
- Media download with AES-128-ECB decryption (image/voice/file/video)
- Voice-to-text from WeChat + Groq Whisper fallback
- Quoted message (ref_msg) support
- Session expiry detection and auto-pause
- Server-suggested poll timeout adaptation
- Context token caching for replies
- Auto-discovery via channel registry
No WebSocket, no Node.js bridge, no local WeChat client needed — pure
HTTP with a bot token obtained via QR code scan.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single _processing_lock (asyncio.Lock) with per-session locks
so that different sessions can process LLM requests concurrently, while
messages within the same session remain serialised.
An optional global concurrency cap is available via the
NANOBOT_MAX_CONCURRENT_REQUESTS env var (default 3, <=0 for unlimited).
Also re-binds tool context before each tool execution round to prevent
concurrent sessions from clobbering each other's routing info.
Tested in production and manually reviewed.
(cherry picked from commit c397bb4229e8c3b7f99acea7ffe4bea15e73e957)