* 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)
In the session detail view, messages from users were labeled with the
literal role string "user" rather than the sender's actual name.
UserName is already stored in sessionRecord.UserName (populated from
UserMeta during loadAllSessions), so we now use it for the role badge.
Also compute the continuation-line indent dynamically via
lipgloss.Width so it stays correct regardless of username length,
fixing the pre-existing misalignment between "user" and "assistant".
- Normalize user IDs to lowercase in ValidateRoleInputs and
validateUsersConfig to catch case-insensitive duplicates (e.g.
"User1" in admin + "user1" in member)
- Apply disabled_commands ACL check to custom commands — previously
only builtin commands were checked, so explicitly disabling a custom
command name had no effect
- Add audit: log entries for custom command and skill execution
- Add 2 new tests for case-insensitive validation and custom command ACL
- Add userRolesMu (sync.RWMutex) to protect concurrent read/write of
userRoles and disabledCmds on the Engine
- Add ValidateRoleInputs() and call it from management API PATCH /users
to prevent duplicate user IDs, multiple wildcards, and invalid default_role
- Fix empty UserID bypassing role-based ACL and rate limiting — now
resolves to default/wildcard role instead of falling back to less
restrictive project-level policies
- Extract buildUserRoleManager() helper in main.go to eliminate code
duplication between initial setup and config reload
- Add 7 new tests for validation and empty UserID behavior
* 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
When creating a cron job via `cron add`, if no session_key was provided
(neither via --session-key flag nor CC_SESSION_KEY env), the job was
created with an empty session_key. At runtime, ExecuteCronJob would fail
silently because it couldn't determine the target platform from an empty
session key.
Add server-side validation and auto-detection in handleCronAdd:
- If session_key is empty, attempt to auto-detect from the engine's
active interactive sessions (works when exactly one session exists)
- If auto-detection fails, return a clear error to the client
Add Engine.ActiveSessionKeys() helper method to expose active session
keys for the auto-detection logic. Uses platform != nil check (instead
of agent session alive check) so sessions are discoverable even before
an agent process is started.
Claude Code supports a `dontAsk` permission mode that auto-denies tools
unless pre-approved via `allowed_tools` or `settings.json` allow rules.
This is the recommended mode for bot deployments where no human operator
is available to approve tool calls.
Unlike `bypassPermissions` (which auto-approves everything),
`dontAsk` implements a "default deny, explicit allow" model — only
whitelisted tools execute, everything else is silently rejected.
This also enables PreToolUse hooks (which are skipped in
bypassPermissions mode), providing an additional security layer.
Changes:
- Add dontAsk + aliases (dont-ask, dont_ask) to normalizePermissionMode
- Add dontAsk to PermissionModes() display list
- Add documentation in config.example.toml
- Add TestNormalizePermissionMode covering all modes and aliases
Previously, stripMentions() removed all @mention placeholders from
message text, including mentions of other users. This meant messages
like "@Bot create a task, assign to @张三" arrived at the agent as
"create a task, assign to" — losing the referenced user entirely.
Now only the bot's own mention is stripped. Other user mentions are
replaced with their display name (e.g. @_user_2 → @张三), so the
agent can see who was referenced in the original message.
Closes#51
Add opt-in `inject_sender` config option for projects. When enabled,
cc-connect prepends a metadata line to each message before forwarding
to the agent:
[cc-connect sender_id=ou_abc123 platform=feishu]
<original message>
This allows agents (Claude Code, Codex, etc.) to identify who sent the
message and adjust behavior accordingly — e.g. personal task views,
role-based access control, or per-user tone.
The feature is off by default and can be enabled per-project:
[[projects]]
inject_sender = true
Closes#91