* feat(telegram): migrate to go-telegram/bot and add forum topic support
Migrate the Telegram platform from the unmaintained go-telegram-bot-api/v5
(last release 2021) to go-telegram/bot v1.20.0, which is actively maintained
and supports the full Telegram Bot API including forum topics.
Key changes:
- **Library migration**: Replace go-telegram-bot-api/v5 with go-telegram/bot.
Redesign the telegramBot interface around the new library's typed API methods
(SendMessage, SendPhoto, EditMessageText, etc.) instead of the old generic
Send/Request pattern. The *bot.Bot type satisfies the interface directly.
- **Forum topic support**: Add threadID to replyContext and session key.
Messages in forum groups (IsForum=true) now include the MessageThreadID
in the session key (telegram:{chatID}:{threadID}:{userID}), giving each
topic its own independent agent session. All send methods propagate
MessageThreadID so replies land in the correct topic. Non-forum groups
and private chats are unaffected.
- **Per-topic workspace binding**: Add ChannelKey field to core.Message so
platforms can provide an explicit channel identifier for workspace binding.
Telegram sets this to "chatID:threadID" for forum topics, enabling
multi-workspace mode to bind different topics to different project
directories. Add effectiveChannelID/effectiveWorkspaceChannelKey helpers
and update all workspace resolution call sites in the engine.
- **Skill command registration fix**: GetAllCommands now respects
disabled_commands for skills, matching the existing behavior for
built-in commands. This prevents disabled skills from being registered
as Telegram bot menu commands.
- **Connection management**: Simplify the reconnect architecture.
bot.New() handles initial validation, bot.Start(ctx) manages long
polling internally. Remove the stale bot detection (generation
comparison in handlers) since the new library's blocking Start()
ensures handlers are never called from a superseded bot instance.
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
* fix: resolve lint errors exposed by engine.go and telegram_test.go changes
- Remove unused assignment to `s` in cmdNew (SA4006)
- Remove unused stubBotFactory and failingBotFactory helpers in telegram tests
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
---------
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
* feat(wecom): handle inbound file messages over HTTP callback
Parse MsgType=file (FileName + MediaId), download via media/get API,
and forward as core.FileAttachment for agents. Add MIME inference helper
and unit tests. Document inbound capabilities in config.example.toml.
WebSocket AI bot mode unchanged; file receive there depends on WeChat policy.
* chore(wecom): add Info/Warn logs for callback diagnostics
Log every POST (size, query flags), parsed msg_type/msg_id/from_user,
duplicate/old/allow_from drops, unsupported types at Warn, and Debug
preview of decrypted XML (truncated). Helps trace missing file pushes.
* feat(wecom-ws): receive image, file, mixed and quote media over AI bot WebSocket
Parse msgtype image/file/mixed and quote (text, voice, image, file, mixed)
per WeCom smart-bot protocol. Download encrypted media from signed URLs
and decrypt with AES-256-CBC (IV = first 16 bytes of base64 key), matching
@wecom/aibot-node-sdk. Voice accepts both content and legacy text fields.
Adds wsCollectInboundParts, deliverWSMediaInbound, unit tests for collect,
Content-Disposition parsing, and round-trip decryption.
* fix(wecom-ws): handle file in mixed payloads and normalize aeskey decoding
- Collect file items inside mixed.msg_item and merge top-level file/image
when msgtype is file/image but a non-empty mixed block is also present.
- decodeWeComAESKey: trim whitespace, URL-safe base64, padding, optional hex.
- Add regression tests for mixed+file and aeskey variants.
* fix: satisfy golangci-lint (gosimple, staticcheck)
- wecom: use := for walkMixed/walkQuote closures (S1021).
- engine: drop unused assignment from sessions.NewSession in cmdNew (SA4006).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Feishu interactive cards support at most 5 table components.
When the agent response contains more than 5 markdown tables,
buildReplyContent now falls back to post (md tag) format instead
of card format, preventing API error 11310 (card table number
over limit).
Add countMarkdownTables helper and corresponding tests.
Implement Discord FileSender so cc-connect can deliver generated files through bot replies and slash-command interactions.
Co-authored-by: Claude Code <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(slack): pass non-image file uploads to agent as FileAttachment
- Map PDF/text and other non-audio/non-image Slack shares to core.FileAttachment
so the engine persists them and has_files is true for the agent.
- Parse files from app_mention raw inner JSON (slack-go omits Files on AppMentionEvent).
- Allow app_mention with only attachments and empty stripped text.
Fixes#292
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(discord): run slash commands when defer interaction fails (#258)
If InteractionRespond(deferred) errors (e.g. unknown interaction), still invoke
the engine and reply via channel messages, with thread-aware replyContext for
thread channels.
Fixes#258
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(codex): avoid closing events channel before readLoop exits
Close() previously closed cs.events after an 8s wait even when wg.Wait
had not finished, while readLoop could still send in flushPendingAsText
(turn.completed), causing "panic: send on closed channel" after /stop
then /new. Defer channel close until readLoop completes.
Fixes https://github.com/chenhg5/cc-connect/issues/281
* fix(telegram): UTF-8-safe bot command descriptions and table rows (#119)
- Truncate BotCommand.Description by runes (Telegram 256-char limit); byte
slicing broke CJK and caused API errors.
- Make truncateForLog rune-aware for debug prefixes.
- Parse markdown table rows without byte-slicing the outer pipes.
Fixes#119
Made-with: Cursor
---------
Co-authored-by: Claude <noreply@anthropic.com>
WeCom may include @robot_id in message content. Core permission matching
uses exact string comparison, so replies like "允许 @bot" failed to match.
Strip ASCII and fullwidth @ mentions for configured bot/aibot/agent IDs
before forwarding to the engine.
Fixes#98
Made-with: Cursor
Co-authored-by: Claude <noreply@anthropic.com>
When cc-connect send --image or --file is called without an explicit
--session flag and there are multiple active sessions, the code would
arbitrarily pick the first session from a map (non-deterministic order).
This could cause attachments to be sent to the wrong chat.
Now returns an error when multiple sessions exist and no session key is
specified for attachment sends. Also adds debug logging to Telegram SendImage
to aid future diagnosis.
Fixes#242
Co-authored-by: Dev Claude <dev@cc-connect.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(agent): add ACP stdio JSON-RPC adapter (MVP)
Implement agent/acp as an ACP client over newline-delimited JSON-RPC: initialize,
session/new or session/load, session/prompt, session/update mapping to core.Event,
and session/request_permission wired to RespondPermission.
Includes plugin_agent_acp.go (!no_acp), Makefile ALL_AGENTS entry, config example,
unit tests for transport and mapping, and AgentDoctorInfo for doctor.
Made-with: Cursor
* fix(acp): Cursor CLI compatibility — authenticate + cursor/* RPC ack
- Optional auth_method (e.g. cursor_login) triggers JSON-RPC authenticate after initialize
- initialize includes minimal clientCapabilities (fs/terminal) per Cursor ACP docs
- Acknowledge cursor/* server requests with empty result to avoid blocking tool flows
- Add integration test against ~/.local/bin/agent when present
- Document auth_method and args in config.example.toml
* fix(telegram): use Request for sendChatAction typing indicator
Telegram returns result=true for sendChatAction; BotAPI.Send unmarshals
result as Message and fails. Request only checks ok and avoids that decode.
* fix(core): surface EventToolResult in IM; extend ACP tool/thinking mapping
- Engine: handle EventToolResult in interactive loop (was silently dropped);
compress and relay paths append tool output too. Add MsgToolResult i18n.
- ACP: stream tool_call_update for in_progress/pending; optional reasoning_*
sessionUpdate kinds -> EventThinking; fallback hook for unknown kinds.
* fix(core): run agent Send concurrently with interactive event loop
Agents that block inside Send (e.g. ACP session/prompt) can emit
EventPermissionRequest while blocked; the engine must consume Events()
in parallel or permission prompts never reach the platform.
- processInteractiveMessageWith: async Send + pass sendDone into processInteractiveEvents
- processInteractiveEvents: select on sendDone for errors and sync at turn boundaries
- drainPendingMessages: same async Send pattern as the main path
- Add TestProcessInteractiveEvents_PermissionWhileSendBlocked regression test
* docs(acp): document OpenClaw and Copilot CLI; add display_name option
- config.example.toml: Cursor, OpenClaw (`openclaw acp`), Copilot (`copilot --acp --stdio`) examples with upstream doc links
- acp agent: optional display_name for doctor (AgentDoctorInfo)
- agent_test.go: cover default and custom display_name
* fix(core): remove ineffectual pendingSend assignments (ineffassign)
pendingSend was set to nil immediately before return or before
reassignment to nextSend; golangci-lint flags these as ineffectual.
* test(core): fix race in PermissionWhileSendBlocked under -race
The test goroutine polled stubInlineButtonPlatform.buttonRows while
processInteractiveEvents wrote them from another goroutine. Use a
permSignalInlinePlatform wrapper that signals on perm:allow via a channel
instead of racy field reads.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(discord): add mode and reasoning button controls
Use Discord buttons for /mode and /reasoning, and apply Claude Code mode changes to the current session when supported.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(discord): clean up mode labels and log button fallback errors
Remove the extra mode marker symbol, show the current mode with text instead, and include the underlying error when permission button fallbacks trigger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
- Add file_path validation to catch empty paths
- Add HTTP status code check (resp.StatusCode != 200)
- Prevents silent failures when file downloads fail
Fixes issue where Telegram images were not being delivered to Claude Code
due to missing error handling for non-200 HTTP responses.
- core: interactive /dir card (nav/act) on CardSender platforms; dirApply refactor
- core+i18n: card strings for dir history UI
- config: projects.show_context_indicator (hide [ctx: ~N%] suffix)
- feishu/lark: reply_to_trigger=false posts via Create instead of Reply
- codex: skip flush when ctx done; sync.Once on session Close
- dingtalk: stream runner reconnect loop; cancel on Stop
- go.mod: mark rsc.io/qr as direct require
ilink returns HTTP 200 with business errors in the response body; parse
ret/errcode/errmsg and return an error so the engine logs platform send
failures (e.g. quota). Add sendMessageResp and a small JSON unmarshal test.
- Skip empty Send/Reply text to avoid empty sendMessage item_list
- Reject sendText with no items; cap ilink JSON read at 64MiB
- Dedup key adds create_time_ms and client_id; CDN encrypt_query_param dedupe per message
- Extra raw voice segments attach as voice_N.silk files when Audio already set
Previously, responses were sent as standalone messages unless
reply_in_thread was explicitly enabled in config. Now Send,
SendPreviewStart, and SendAudio always thread replies to the
original user message when messageID is available, making
conversations easier to follow.
Co-authored-by: Kevin Wang Sheng <kevinWangSheng@users.noreply.github.com>
* feat(discord): support @everyone/@here as bot mention via respond_to_everyone config
When multiple cc-connect bots share a Discord channel, users had to
@mention each bot individually. With respond_to_everyone = true, the bot
treats @everyone/@here as a direct mention, allowing broadcast questions
to all opted-in bots at once.
Closes#132
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor: rename config to respond_to_at_everyone_and_here
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(discord): strip @everyone/@here from message content
The pi agent treated the raw "@everyone cat or dog?" as a file path,
causing "File not found" errors. Now @everyone and @here are stripped
from the content before passing to the agent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The onMessage callback runs synchronously in the Feishu SDK event loop.
IO-heavy operations inside it (image/audio download, handler HTTP calls,
voice transcription) block the entire event loop, causing message backlog
and potential WebSocket timeouts.
Extract message content parsing, media download, and handler invocation
into a dispatchMessage method that runs in its own goroutine. The dedup,
old-message, and permission checks remain synchronous in onMessage to
guarantee correctness before spawning the goroutine.
The changes add error handling for previously ignored operations and introduce logging for critical errors and warnings. This includes:
- Adding error checks for file operations, JSON unmarshaling, system calls
- Logging warnings/errors for failed operations using slog
- Properly handling return values from functions that were previously ignored
- Adding context to error messages for better debugging
- Cleaning up unused code and variables
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes potential nil panic in 401 retry path by properly checking http.NewRequest error. Also updates error message wrapping to match convention.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
Add detailed user guidance for Feishu bot setup, including allow_from clarification and bot menu configuration instructions. Implement handler for bot custom menu click events, treating menu items as slash commands. Default set-allow-from-empty to false to prevent inaccurate open_id usage.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
Add a sanitizingLogger wrapper that masks sensitive query parameters
in URLs logged by the Lark SDK websocket client. Parameters masked:
device_id, access_key, ticket, conn_id, secret, token, password, key.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(platform): implement ImageSender for Discord, Slack, DingTalk, WeChat Work, QQ, QQBot
Add SendImage support to 6 platforms that previously only supported
receiving images. Each implementation follows the platform's native
API for uploading and sending images:
- Discord: ChannelMessageSendComplex with discordgo.File attachment
- Slack: UploadFileV2Context with thread support
- DingTalk: media upload + oToMessages API (refactored uploadMedia
to support both voice and image media types)
- WeChat Work: media upload + message send API
- QQ (OneBot v11): base64-encoded image segments via WebSocket
- QQBot (Official API v2): rich media upload + msg_type 7
LINE is excluded as its API requires externally hosted image URLs.
* fix(platform): address code review findings in SendImage
Discord:
- Fix io.Reader exhaustion: use newFile() factory for fresh reader on fallback
- Handle firstDone state machine for interactions (mirror sendInteraction pattern)
DingTalk:
- Read response body on success for debugging (mirror SendAudio pattern)
- Use json.Marshal for msgParam instead of fmt.Sprintf
QQBot:
- Handle http.NewRequest error on 401 retry path (prevent nil panic)
Discord auto-creates a managed role for each bot. When a bot is not
visible in the channel member list (common in new channels or when
multiple bots share a guild), users can only @mention the role instead
of the user. The role mention format <@&roleID> was not recognized,
causing the bot to silently ignore the message.
Fix by:
- Caching bot's managed role ID on Ready and GuildCreate events
- Checking m.MentionRoles and <@&roleID> content in addition to
m.Mentions and <@botID> content
- Stripping role mention tags from message content
- Adding IntentsGuilds to receive GuildCreate events for role lookup
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workspace init flow would swallow slash commands (e.g. /workspace bind)
on platforms like Discord where commands include the "/" prefix. The guard
at handleMessage only checked for "workspace" without "/", which was
designed for Slack where the prefix was stripped.
Three fixes:
- Add "/workspace" to the init flow bypass guard in handleMessage
- Let slash commands escape an existing init flow in handleWorkspaceInitFlow
- Clean up stale init flow state when workspace commands bypass the flow
Also implement ChannelNameResolver for Discord so convention-based
workspace matching works automatically by channel name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sendInteraction splits messages using byte length (len) and slices at
byte offsets, which can cut multi-byte UTF-8 characters (emoji, CJK)
in half, producing invalid strings. This causes Discord API errors or
garbled messages.
Replace the inline byte-based splitting with SplitMessageCodeFenceAware,
which is already used by sendChannelReply and sendChannel. This also
gives interaction responses proper code fence handling for free.
Remove the now-unused lastIndexBefore helper.
When downloading Slack files via url_private_download, the code was
already setting the Bearer token correctly. However, there was no
error detection for cases where Slack returns an HTML login page
instead of the actual file content.
This commit adds:
- Fallback to url_private when url_private_download is empty
- Status code validation (fail on non-200 responses)
- HTML content detection (fail with clear error message)
- Improved error logging with redacted URL
- Unit tests for the downloadSlackFile function
The HTML detection is particularly important because when auth
fails (e.g., wrong bot token, expired token), Slack returns a
login page with HTTP 200 instead of an error code. Without this
check, the HTML content would be passed to the AI agent as if
it were an image, causing confusing errors.
Fixes#185
Co-authored-by: Shawn <shawn@chenhg5.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Added max attachment size limit (50MB) with validation. Implemented path sanitization for attachments. Added related tests for size limit and path cleaning. Also added i18n messages for BTW command responses and improved connection cleanup in WeCom websocket.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
When the WebSocket connection drops, entries in pendingAcks from
in-flight requests are never cleaned up. Each reconnection accumulates
more orphaned entries, causing unbounded memory growth over time.
Drain all pending ACK channels in the runConnection defer block,
sending errors to unblock waiting goroutines and deleting stale entries.
When reconnectLoop fails at waitForHello, sendIdentify, or waitForReady,
the connection is closed but p.wsConn still points to the closed conn.
This leaves a stale reference that Stop() may double-close and that
sendHeartbeat/readLoop may write to if they race with the reconnect.
Reset p.wsConn to nil under the mutex before closing the failed
connection, consistent with how connectGateway and the reconnect
loop prologue handle cleanup.
* feat: add Slack typing indicator and mrkdwn formatting instructions
Implement TypingIndicator interface for Slack using emoji reactions as a
heartbeat (eyes immediately, clock after 2min, extras every 5min). All
reactions are cleaned up when the agent completes.
Also update the agent system prompt to instruct Slack mrkdwn formatting
instead of standard Markdown (single *bold*, no ## headings, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make formatting instructions platform-specific via optional interfaces
Move Slack mrkdwn formatting from the global AgentSystemPrompt() into a
platform-specific FormattingInstructionProvider interface. The engine
wires platform formatting to agents via PlatformPromptInjector before
session start, so only Slack sessions receive Slack formatting.
Also fix StartTyping comment: "random emoji" → "sequential from extras list".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the agent is processing and the user sends /btw <message>, the
content is injected into the running session mid-turn instead of being
rejected. The busy message now tells users about /btw.
Also adds a Slack slash command handler (socketmode.EventTypeSlashCommand)
so native Slack slash commands work without needing text prefixes.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(feishu): add Webhook mode for Lark international version
Lark international version does not support WebSocket long-connection mode
like Feishu domestic version. This commit adds Webhook HTTP server mode
for Lark platform to receive event callbacks.
Changes:
- Add Webhook mode fields to Platform struct (port, callbackPath, encryptKey, server)
- Split Start method into startWebSocketMode and startWebhookMode
- Lark platform now uses HTTP Webhook server instead of WebSocket
- Add webhookHandler to process HTTP callback requests
- Update Stop method to shutdown Webhook server
- Add Lark configuration example to config.example.toml
Fixes#63
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(feishu): fix unused variable ctx in startWebhookMode
The ctx variable was declared but not used, causing build failure.
Changed to use blank identifier (_) to ignore the unused context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Feishu SDK's GetMessageResource response can have a nil File field
even when resp.Success() returns true (Code == 0). Calling
io.ReadAll(nil) panics with a nil pointer dereference.
Add nil checks on resp.File before reading in both downloadImage and
downloadResource to convert a potential panic into a handled error.
Add mutex protection to stubPlatformEngine's sent messages and introduce getSent() helper. Fix shell command test to wait for async completion before assertions. Also adjust feishu platform tests to use pointer receivers consistently.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
* feat: add 'sessions' subcommand with TUI viewer (WIP)
* feat: add 'sessions' subcommand with TUI viewer for browsing session history
Three modes: interactive TUI browser (default), non-interactive list,
and non-interactive show. Reads session JSON files directly from
~/.cc-connect/sessions/ without requiring the daemon to be running.
- TUI: bubbles/table overview + bubbles/viewport detail with lipgloss styling
- `sessions list`: pipe-friendly tabwriter output
- `sessions show <id> [-n N]`: view specific session by composite ID or index
* feat: persist user/group names for sessions viewer
- Add UserMeta (UserName/ChatName) to SessionManager with persistence
- Capture metadata in engine.handleMessage() on every incoming message
- Fix feishu userName bug: was using sender.SenderType ("user"), now
calls resolveUserName() for actual display names
- Populate ChatName with Chat.Title for Telegram group chats
- Display User and Group/Chat columns in sessions list/show/TUI
- Return copy from GetUserMeta() for thread safety
- Use rune-based truncation to avoid corrupting multi-byte UTF-8
* feat: add user/chat name resolution for all platforms
- Slack: resolve UserName via GetUserInfo API, ChatName via channel name
- Discord: resolve ChatName via Channel API with sync.Map cache
- LINE: resolve UserName via GetProfile API with sync.Map cache
- DingTalk: populate ChatName from ConversationTitle (SDK provides it)
- QQ (OneBot): resolve ChatName via get_group_info API with cache
- WeChat Work (HTTP): resolve UserName via user/get API with cache
Platforms with API limitations (left as raw ID):
- QQ Bot: official API does not provide user nicknames
- WeChat Work (WS): no REST API access from WebSocket mode
* fix: consistent fallback to raw ID when name resolution fails
- Slack: resolveChannelNameForMsg returns channelID (not "") on error
- Discord: resolveChannelName returns channelID (not "") on error,
skip caching empty names
- QQ: resolveGroupName returns groupID string (not "") on error
- WeChat Work WS: add comment documenting why raw userID is used
* feat: add chat/group name resolution for feishu and LINE
- Feishu: resolve chat name via Im.Chat.Get API with sync.Map cache,
populate ChatName for all message types
- LINE: resolve group name via GetGroupSummary API with sync.Map cache,
populate ChatName for group messages
* fix: populate UserName/ChatName in all remaining message paths
- Feishu: add UserName + ChatName to onCardAction handler (perm/askq/cmd)
- QQ Bot: add GroupOpenID as ChatName fallback in group messages
- WeChat Work WS: add ChatID as ChatName for group chats