Add SendFile() using DingTalk oToMessages API with sampleFile msgKey.
Follows the same uploadMedia + batchSend pattern as SendImage.
Closes#284 (partial — DingTalk file sending)
Made-with: Cursor
Discord's MessageCreate handler only routed audio/* and image attachments
(detected via Width/Height) into core.Message, silently dropping PDFs,
documents, archives, and any other file type. Tighten image detection to
image/* content type (with a legacy width/height fallback when ContentType
is missing), and fall through to Message.Files for everything else so
downstream agents receive the bytes the same way telegram/feishu already do.
Refactored the attachment loop into classifyAttachments() for testability
and added regression coverage for PDFs, zips, legacy images without
ContentType, and per-attachment download failures.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The go-telegram/bot library sends getUpdates with Timeout=59s (pollTimeout
minus 1s). With the HTTP client also set to 60s, any network latency causes
the client to timeout before the server responds, producing unexpected EOF
every 1-3 minutes. Increasing the HTTP client timeout to 90s gives 30s of
headroom while keeping the Telegram API long-poll at 59s.
Closes#738
Co-authored-by: Claude <noreply@anthropic.com>
Previously the post-content parser silently dropped `at` and `markdown`
elements. Messages combining an @mention with an image (which arrive as
post type rather than text type) lost the mention entirely because
stripMentions runs against a placeholder that was never extracted. Any
post whose text lives inside a `markdown` element was also dropped.
Also add handlers for `sticker` and `media` (video) message types so
they are no longer silently ignored. Stickers are delivered as image
attachments; videos are surfaced as `[video: <name>, Xs]` with the
thumbnail as an image when available.
Changes:
- postElement struct: add UserId / UserName fields for at elements.
- extractPostParts: handle at (resolves bot strip, @all, @<name>) and
markdown (passthrough text). Affects the main inbound post path.
- extractPostPlainText (quoted/reply chain path): handle a, at,
markdown and img so quoted context is no longer truncated.
- dispatchMessage: add sticker and media cases with graceful
fallbacks on download failure.
- Tests for the new at/markdown behavior in both paths.
Co-authored-by: Cigarrr <cigarrr@users.noreply.github.com>
* feat(platform/max): add MAX messenger bridge with bidirectional file and voice support
- Implement core.Platform plus InlineButtonSender, ImageSender, FileSender,
AudioSender, MessageUpdater, FormattingInstructionProvider,
ReplyContextReconstructor
- Two-step attachment upload via POST /uploads + multipart to CDN
- Route incoming audio (type=audio and audio/* file mime) to
core.Message.Audio so the speech pipeline transcribes it for the agent
- Outgoing voice replies via SendAudio for the TTS pipeline
- Retry on attachment.not.ready with exponential backoff
- Long poll /updates with reconnect backoff and allow_from filtering
- Register max in Makefile ALL_PLATFORMS and document in config.example.toml
- Tighten .gitignore: ignore config.*.toml (except example) and *.old
- 19 unit/integration tests with httptest mock, passes -race
Refs #741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(platform/max): handle real MAX CDN response shapes and audio download
Issues discovered when testing end-to-end with the live MAX API:
1. Image CDN returns nested {"photos": {<id>: {"token": ...}}}, not a flat
{"token": ...}. Parse the right shape per kind.
2. Audio CDN returns XML "<retval>1</retval>" rather than JSON. The real
attachment token is delivered in the /uploads response itself for
video/audio, so we use urlInfo.Token as the source of truth and only
scan the CDN body opportunistically.
3. /audios/{token} endpoint does not exist in MAX (returns 404). Incoming
voice payloads already carry the download URL directly, mirroring
images/files. Use payload.url; fall back to resolveMediaURL only if
empty (for video where token resolution may still apply).
Also raise the speech HTTP client timeout from 60s to 5m — enough for the
first-request cold-start case when a local Whisper backend has to download
or JIT-load the model before responding.
Tests updated to emit the real CDN response shapes per attachment type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(platform/max): address review — dedup + idiomatic session key parse
- Add core.MessageDedup to Platform and check msg.Body.Mid / callback ID
in handleMessage and handleCallback. Guards against reprocessing on
poll-loop reconnect edge cases, matching the pattern used by feishu/
dingtalk/wecom platforms.
- Replace fmt.Sscanf in ReconstructReplyCtx with strings.CutPrefix +
strings.Cut — more idiomatic Go, simpler to read, and handles the
"max:" prefix and optional ":userID" suffix without a manual loop.
- Add TestHandleMessageDedupsByID regression test.
Addresses review feedback on #742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
triggerReconnect was called with the per-connection context (connCtx),
but reconnectLoop immediately cancels it via p.connCancel(). This caused
the reconnect backoff loop to exit on the first iteration because
ctx.Done() was already closed. Fix by storing the platform lifetime
context and using it for reconnect operations.
Made-with: Cursor
Add MarkdownToSlackMrkdwn() converter that transforms standard Markdown
to Slack's mrkdwn format before sending messages:
- **bold** → *bold*
- ***bold italic*** → *_bold italic_*
- ~~strike~~ → ~strike~
- [text](url) → <url|text>
- # Heading → *Heading*
- Code blocks and inline code are preserved untouched.
Applied in both Reply() and Send() methods of the Slack platform.
Closes#667
Co-authored-by: Claude <noreply@anthropic.com>
Add a per-connection context (connCancel) that is cancelled before
closing the WebSocket and spawning new heartbeatLoop/readLoop
goroutines during reconnection. This prevents stale goroutines from
the previous connection from racing with the new pair.
Closes#524
Co-authored-by: Claude <noreply@anthropic.com>
The Telegram platform sends Markdown content by first converting it
to a small HTML subset and posting with parse_mode=HTML. When
Telegram rejects the HTML (typically with "can't parse entities:
..."), the platform silently retries the request with parse_mode=""
and the original Markdown text — which means the user sees literal
**bold** and # heading characters in the IM with no indication of
why or which message triggered the fallback.
This change upgrades the four send-path fallbacks and the one
edit-path fallback from slog.Debug (or no log at all) to slog.Warn,
with the underlying Telegram error and a 200-char prefix of the
rejected HTML included as structured fields. Operators can now
diagnose specific HTML-rendering failures by tailing logs at the
default level instead of having to enable debug logging system-wide.
Each warning carries a "method" tag so the originating code path is
unambiguous when a single message goes through multiple sites
(e.g. SendPreviewStart followed by repeated UpdateMessage).
No behavior change for users; same plain-text fallback, just visible.
No new dependencies, no test changes (the fallback path is exercised
only by live Telegram API rejections).
Touched send/edit sites: Reply, Send, SendWithButtons,
SendPreviewStart, UpdateMessage.
Generated with [Devin](https://cli.devin.ai/docs)
Co-authored-by: youngshook <youngshook@users.noreply.github.com>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(feishu): add allow_chat option to filter by group chat_id
Allow binding specific group chats to a project via the allow_chat config
option, so multiple projects can share one Feishu app while each only
responds to its designated groups. Private (P2P) chats are unaffected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(feishu): share WebSocket connection across projects with same app_id
When multiple projects use the same Feishu app_id, Feishu's server
load-balances messages across WebSocket connections, causing messages
to randomly reach the wrong project. This change deduplicates WebSocket
connections per app_id: the first platform creates the connection
(primary), subsequent platforms reuse it (secondary). All events
(messages, card actions, bot menu) are fanned out to every platform
in the group, letting each platform's own allow_chat/allow_from
filters decide what to process.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(feishu): add group_only option to ignore P2P messages
When group_only=true, the platform skips private (P2P) messages,
useful for projects that should only operate in group chats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: retrigger after flaky opencode test failure
The unit-test failure was in agent/opencode (TempDir cleanup race),
unrelated to this PR's changes. Retriggering CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(feishu): address review feedback on shared WS and allow_chat
1. Card action fan-out now checks allow_chat before processing, so
the wrong project no longer claims card interactions from other chats.
2. Lark webhook mode respects isPrimary — secondary platforms no longer
start their own webhook, preventing double-processing.
3. Primary Stop() warns when secondaries will lose their event source.
4. Document group_only option in config.example.toml.
5. Add unit tests for ws_shared.go (register/unregister/allPlatforms).
6. Move event handler creation after isPrimary check so secondaries
skip building unused handlers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: xiongyuanwen <xiongyuanwen@bytedance.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In webhook mode with private/self-hosted Feishu deployments, the bot
open_id API call may fail due to different auth flows or API support.
This failure previously prevented the webhook server from starting.
This change defers bot open_id discovery to WebSocket mode only, allowing
webhook mode to start successfully even when the bot info API is unavailable.
The webhook server can still receive and process events correctly without
the bot open_id (it's only used for group chat filtering).
Fixes compatibility with older private Feishu/Lark deployments that use
webhook mode with custom domains.
Co-authored-by: mrzhao <mrzhao@iflytek.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore(config): 添加 demo 目录到 git 忽略列表
- 防止 demo 相关文件被意外提交到仓库
- 保持仓库整洁,避免临时演示文件污染版本历史
* feat(qqbot): add file attachment send and receive support
- Add downloadAttachmentFiles() to download non-image attachments
- Populate core.Message.Files in group and C2C message handlers
- Implement core.FileSender via SendFile() using rich media API (file_type=4)
* ci: add test-build workflow for tag-triggered linux/amd64 builds
* fix(ci): add contents:write permission for release creation
* fix(ci): separate build and release jobs with artifact passing
* fix(qqbot): pass file_name in rich media upload for correct filename display
* chore: remove test-build workflow and restore gitignore
* fix(qqbot): add HTTP status check, LimitReader, and tests for file download
- Check resp.StatusCode in downloadAttachmentImages and downloadAttachmentFiles
- Use io.LimitReader with 20MB cap to prevent unbounded memory reads
- Add unit tests for download functions and uploadRichMedia file_name branch
- Restore test-build.yml workflow
* chore: remove test-build workflow
* test(qqbot): use httptest in SkipsImages test instead of example.com
* fix(qqbot): extract quoted message content from msg_elements for quote replies
When a user quotes/replies to a message, QQ Bot API sends message_type=103
with the referenced content in msg_elements[0]. Previously only message_reference
inline fields and cache were checked, which often came up empty.
* chore: restore test-build workflow for tag-based CI
* chore: remove test-build workflow
---------
Co-authored-by: ferock <ferock@example.com>
- Parse inbound `input` array with `input_text`, `input_image`,
`input_file` content parts (base64 encoded, per Weibo Open IM protocol)
- Implement `core.ImageSender` (SendImage) and `core.FileSender`
(SendFile) by encoding media as base64 in the `send_message` payload
- Add `normalizeInboundInput` to extract text/images/files from the
multi-modal input structure; input_text parts override payload.text
- Support image-only or file-only messages (no text required)
- Filter unsupported image MIME types; enforce size limits (10MB img, 5MB file)
- Extract `writeWS` helper to unify WebSocket write logic
- Add comprehensive tests: inbound image/file parsing, outbound image/file
send, MIME filtering, role filtering, interface compliance checks
Made-with: Cursor
- Rewrite SaveActiveProvider, SaveAgentModel, SaveProviderModel,
SaveLanguage, SaveDisplayConfig, SaveTTSMode to use surgical
text-level TOML editing instead of full Unmarshal→Encode cycle.
This preserves comments, unknown fields, and formatting.
- Add patchProjectAgentOption, patchTopLevelField, patchSectionField
helpers for targeted config edits.
- Extend rawProjectSpan to track [projects.agent] and
[projects.agent.options] sections.
- Add resetAllSessions to Engine; call it from management API
provider activate endpoint so sessions reset properly.
- Update docs/feishu.md: add card.action.trigger to required event
subscriptions; add FAQ for card button errors.
- Add startup log when feishu interactive card mode is enabled.
- Add tests for comment/field preservation in save functions.
Made-with: Cursor
Quoted message prefix was prepended directly to Content, making
the first character not '/' and breaking command parsing. Move the
quoted prefix to ExtraContent which is merged after command detection.
Made-with: Cursor
Implement Weibo private message platform via the Open IM WebSocket
protocol (open-im.api.weibo.com). Supports app_id/app_secret auth,
automatic token refresh, heartbeat keep-alive, reconnect with
exponential backoff, message dedup, and chunked sending for long
messages (2000 char limit).
Includes plugin registration, build tag (no_weibo), unit tests,
setup documentation, and cross-references in all platform docs.
Made-with: Cursor
Trace parent_id links up to 5 levels deep when a user replies to a message,
providing the full conversation chain as context for the agent.
- Add fetchSingleMessage() to retrieve a single message with parent_id
- Add fetchReplyChain() for iterative chain traversal with cycle detection
- Add formatReplyChain() with backward-compatible single-message format
- Refactor fetchQuotedMessage() as a thin wrapper
- Show 'Bot' for app senders, 'User' for unresolved user names
Co-authored-by: MeiShaoqing <meishaoqing@xiaomi.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the agent's response is delivered via in-place card update (Feishu
streaming preview), the user gets no push notification. This adds a
configurable done_emoji option (e.g. "Done") that adds a reaction after
the "OnIt" reaction is removed, so the user knows the agent finished.
Only activates in quiet mode when the preview card was updated in-place
at least once (via UpdateMessage). Non-quiet mode is unaffected as
intermediate messages already trigger push notifications. Single-round
responses that only use SendPreviewStart are also unaffected, as the
initial send already triggers a push notification.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
buildReplyContent previously used a three-tier strategy: card only for
"complex" markdown (code blocks/tables), post for everything else. This
caused most markdown content (headings, blockquotes, links, dividers,
lists) to be rendered via post format, which has poor markdown support —
links aren't clickable, blockquotes don't render, dividers are ignored.
Simplify to two paths:
- Has markdown → card (best rendering, supports all markdown features)
- No markdown → text
- Fallback to post md tag only when table count exceeds card limit (>5)
Removes the hasComplexMarkdown gate and the buildPostJSON path from
the rendering decision. hasComplexMarkdown and buildPostJSON are kept
as functions for backward compatibility but are no longer called by
buildReplyContent.
Co-authored-by: leoliang <leoliang@futunn.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
H1: Prevent workspace path traversal via ../
Add resolveLocalDirPath() that validates resolved path stays under
baseDir, expands ~/... via os.UserHomeDir, rejects escaping paths.
M2: Sanitize sender_name in inject_sender header
Replace quotes with single quotes, strip newlines/CR to prevent
prompt injection via display names.
M3: Warn on unset env var placeholders in config
resolveEnvPlaceholders now logs slog.Warn when ${VAR} references
an environment variable that is not set.
M4: Validate WeCom api_base_url
Reject non-http(s) or malformed URLs at New() to prevent credential
exfiltration via misconfigured base URL.
M5: Quote systemd Environment= values
Wrap values in double quotes to handle spaces, #, and special chars
in proxy URLs and paths.
M7: Discord table detection now requires separator row
Only wrap pipe-delimited blocks that contain a markdown separator
row (| --- | --- |), reducing false positives on decorative pipes.
Made-with: Cursor
H1: Prevent double-Close on concurrent cleanupInteractiveState calls
by nil'ing state.agentSession under the lock before closing.
H2: Weixin StartTyping sends typingStatusStop on ctx.Done() and on
repeat-failure exit, preventing stuck "typing" indicator.
M3: Feishu @mention resolution: downgrade PII log to Debug, HTML-escape
display names in at tags, skip ambiguous (duplicate) names.
M4: Narrow segmentStart==0 condition in EventResult to only apply when
tool messages are hidden (!e.display.ToolMessages).
M5: Log warning when cron workspace agent creation fails instead of
silently falling back to global agent.
M7: Discord file download capped at 50 MiB via io.LimitReader.
M8: Weixin getConfig now checks ret/errcode fields and returns error
on non-zero values.
Made-with: Cursor
When resolve_mentions is enabled in feishu platform options, outgoing
messages are scanned for @displayName and replaced with native Feishu
at tags by reverse-matching against the group member list.
- New config option: resolve_mentions (default false)
- Lazy-load group members via chat members API with 1h cache TTL
- Match names longest-first to avoid partial matches
- Auto-select at syntax based on message type (text vs card)
- No special markup needed — just @name in natural text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Weixin platform now implements core.AudioSender interface, enabling TTS
audio output for WeChat personal chat via ilink API.
- Added ConvertAudioToAMR in core/speech.go for audio conversion
- Implemented SendAudio in platform/weixin/media_outbound.go
- Converts WAV/MP3/TTS output to AMR format (Weixin voice codec)
- Uploads via CDN and sends as voice message type
Fixes#587: TTS audio was generated but silently dropped because
weixin did not implement AudioSender interface.
Test plan:
- go build ./... passes
- go test ./... passes
- Unit tests for SendAudio edge cases (empty audio, invalid context)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Synchronous resolveUserName/resolveChatName calls in the SDK dispatcher
callback blocked the event loop for 300-700ms on cache misses, causing
intermittent message drops in WebSocket mode.
The fix moves name resolution into the dispatchMessage goroutine, ensuring
the SDK dispatcher can process events without blocking. The dedup and
old-message checks remain synchronous for correctness.
Fixes#542
Co-authored-by: Claude <noreply@anthropic.com>
* feat(observe): add --observe CLI flag and engine config
Add observe fields to the Engine struct and a SetObserveConfig setter
to enable terminal session observation in a later task. Define --observe
and --observe-channel CLI flags (wiring deferred).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add ObserverTarget interface, implement for Slack
Define ObserverTarget optional interface in core/ for platforms that can
receive terminal observation messages. Implement SendObservation on Slack
platform using PostMessageContext with link unfurl disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): implement JSONL session file watcher and parser
Adds sessionObserver to core/observer.go which tails ~/.claude/projects/{projectKey}/*.jsonl
files, parses user/assistant events, filters out sdk-cli sessions (cc-connect's own),
and forwards new messages to ObserverTarget. Includes full test coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire observer into engine startup and shutdown
Add findObserverTarget to locate the first ObserverTarget platform,
startObserver to launch the JSONL watcher goroutine when configured,
wired into Engine.Start() and cancelled in Engine.Stop().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): wire --observe and --observe-channel flags in CLI
Removes the TODO suppressor and connects the --observe/--observe-channel
CLI flags to engine.SetObserveConfig(), resolving the Claude Code project
directory from the configured work_dir via the new resolveClaudeProjectDir helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(observe): add config.toml support for observe settings
Add ObserveConfig struct to ProjectConfig so users can configure terminal
session observation via [projects.observe] in config.toml, with CLI flags
(--observe, --observe-channel) taking precedence over config file values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(observe): rune-safe truncation, robust offset tracking, interface assertion
- Use file size for offset tracking instead of line-length calculation
to avoid drift from \r\n line endings or partial final lines
- Truncate messages at valid rune boundaries to prevent garbled UTF-8
- Add compile-time ObserverTarget interface assertion for Slack
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement the core.TypingIndicator interface for the Weixin (personal
WeChat) platform, showing a "typing..." bubble while the agent is
processing.
- Add getConfig API call to fetch typing_ticket from ilink
- Add sendTyping API call (start/stop) to ilink/bot/sendtyping
- Cache typing tickets per peer with 10-minute TTL
- Proactively refresh ticket on inbound message receipt
- Repeat typing status every 5 seconds (matching Telegram behavior)
- Send best-effort stop signal when processing completes
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Previously the Discord platform only handled audio and image attachments
from incoming messages. Non-image, non-audio attachments (PDFs, documents,
code files, etc.) were silently dropped. This adds an else branch in the
attachment loop to capture them as core.FileAttachment, matching the
behavior already present in the Telegram platform.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(feishu): format TodoWrite tool input as readable list
Parse TodoWrite JSON input and display as a formatted list with status
icons (✅ completed, 🔄 in_progress, ⏳ pending) instead of raw JSON.
- Added isTodoWriteToolName() to detect TodoWrite tool
- Added formatTodoWriteInput() to parse and format todos
- Shows content, status icon, and activeForm for each todo
- Falls back to default formatting if JSON parsing fails
Fixes task t-20260401-3c8no7
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): TodoWrite fallback uses text block instead of inline code
When formatTodoWriteInput returns empty (invalid JSON or empty todos),
return a ```text block directly instead of falling through to the
generic formatter which uses inline backticks for short strings.
Fixes CI test failures for TestFormatProgressToolInput.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
When ilink API returns ret=-2, it typically indicates:
- context_token expired or invalid
- temporary rate limiting
This fix adds:
- Retry mechanism (3 attempts) for sendMessage when ret=-2 is returned
- context_token refresh between retries
- Delay between message chunks (100ms) to avoid rate limiting
- Delay between retries (500ms) for temporary failures
- More detailed logging including content_len for debugging
Fixes#365
Co-authored-by: Claude <noreply@anthropic.com>
Discord allows at most 100 application commands per bulk overwrite
(guild or global). The previous limit of 200 caused silent HTTP 400
failures when users had >100 commands. Now truncates at 100 and logs
the count of dropped commands.
Closes#504
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add reply enrichment: when user replies to a message, the original
message content (text, image, voice, file, location) is included
as context for the AI agent
- Add location message support: convert Telegram location attachments
to text content that AI agents can understand
- Add ExtraContent field to core.Message for platform-enriched context
- Add LocationAttachment type to core.Message
- Refactor dispatchMessage to support platform-specific enrichment
When Feishu WebSocket connections experience rapid resets, HTTP API calls
for sending messages also fail with transient network errors (EOF,
connection reset, timeout). Previously these errors were not retried,
causing messages to be silently lost.
Add withTransientRetry wrapper with exponential backoff + jitter
(500ms→1s→2s, max 3 retries, +25% jitter) around all Feishu API calls:
reply, create, patch, delete, upload image/file/audio, and preview.
Transient detection uses typed syscall checks (ECONNRESET, EPIPE),
net.Error timeout, io.EOF, and string matching as fallback.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(weixin): handle upload_full_url in getUploadUrl response
WeChat iLink API changed the getUploadUrl response format: it now
returns `upload_full_url` (a complete CDN URL with `encrypted_query_param`
as a query parameter) instead of the old `upload_param` field.
This caused all file/image uploads to fail with "empty upload_param"
because cc-connect only checked the old field name.
Fix: fallback to parsing `encrypted_query_param` from `upload_full_url`
when `upload_param` is empty.
Fixes#369
Ref: wong2/weixin-agent-sdk#21, hao-ji-xing/openclaw-weixin#11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(weixin): fix CDN upload with three issues
1. Handle upload_full_url in getUploadUrl response
WeChat iLink API returns upload_full_url instead of upload_param.
Extract encrypted_query_param as fallback, and use upload_full_url
directly for CDN uploads to avoid URL re-encoding issues.
2. Use dedicated no-proxy HTTP client for CDN uploads
WeChat CDN (novac2c.cdn.weixin.qq.com) is a China-domestic service
that times out when accessed through a proxy. Create a separate
HTTP client with Proxy: nil for CDN operations.
3. Fix AES key encoding in sendMessage
The aes_key field must be base64(hex_string), not base64(raw_bytes).
WeChat client expects base64-encoded hex representation of the key,
matching the official openclaw-weixin plugin behavior.
Fixes#369
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address adversarial review findings
1. Make upload_full_url a standalone success path in getUploadURL.
No longer require extracting encrypted_query_param from
upload_full_url — if the server returns an opaque/signed URL,
it is accepted as-is. Only fail when both upload_param and
upload_full_url are empty.
2. Respect configured proxy for non-WeChat CDN hosts.
Only bypass proxy when upload_full_url points to a known WeChat
domestic CDN domain (*.weixin.qq.com, *.wechat.com). For custom
cdn_base_url or other hosts, the configured httpClient (with
proxy settings) is preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: simplify CDN upload — remove duplicate function, extract helper
- Remove uploadBufferToCDNDirect (50-line duplicate of uploadBufferToCDN)
- Unify uploadBufferToCDN to accept a pre-built URL (caller handles URL construction)
- Extract formatAesKeyForAPI() helper for the base64(hex(key)) encoding
- Add cdnHttpClient comment clarifying why env proxy bypass is needed
Net -43 lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(weixin): add unit tests for CDN upload fixes
- TestFormatAesKeyForAPI: verify base64(hex) output matches Python SDK
- TestFormatAesKeyForAPI_NotRawBase64: regression guard against old bug
- TestFormatAesKeyForAPI (round-trip): verify formatAesKeyForAPI ↔ parseAesKey
- TestIsWeixinCDNHost: table-driven test for WeChat CDN domain detection
- TestGetUploadURLResponse_Validation: table-driven test for upload_param/upload_full_url acceptance logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(weixin): add unit tests for CDN upload fixes
- TestFormatAesKeyForAPI: verify base64(hex(key)) output matches Python SDK
- TestFormatAesKeyForAPI_NotRawBase64: ensure old bug format is rejected
- TestIsWeixinCDNHost: table-driven test for CDN host detection
- TestGetUploadURLResponse_Validation: verify both-empty-fails, either-present-passes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: jiangbowen <robin@jiangbowendeMacBook-Air.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PR #445: auto-compress skips when agent has no CompressCommand;
--max-context-tokens is now configurable via agent options instead
of being hardcoded to 900000
- PR #448: codex app-server RPC timeout cleans pending map entry;
emit() uses non-blocking send to prevent readLoop stall;
readLoop uses scanner with 10MB max line size
- PR #462: restore backward-compatible quiet config — maps to
display.thinking_messages/tool_messages when [display] not set
- PR #479: Gemini STT sends API key via x-goog-api-key header
instead of URL query param; model name is URL-escaped
- PR #489: feishu fetchQuotedMessage uses caller context instead
of context.Background; extractPostPlainText includes code_block
- PR #447/#474: SendToSessionWithAttachments uses workspace-prefix
stripping fallback matching cron/heartbeat logic
- PR #446: PATH prepend for custom commands limited to Windows only
All changes are backward-compatible; new behavior gated by config.
Tests added for quiet config, workspace prefix, Gemini header, and
feishu code_block extraction.
Made-with: Cursor
Adds SendFile to the Slack platform, mirroring SendImage. Both use
Slack's UploadFileV2Context, which already handles arbitrary file
types. The engine routes attachments from `cc-connect send --file`
(invoked by agents via the system-prompt-documented CLI) to this
method via the core.FileSender interface assertion.
https://claude.ai/code/session_01WyqXEst7Z8tiMbzeh4rTfX
Co-authored-by: Claude <noreply@anthropic.com>