14 Commits
Author SHA1 Message Date
Sean 1d15effd2e feat(platform): implement ImageSender for 6 platforms (#222)
* 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)
2026-03-19 13:02:41 +08:00
Sean 5e46bc4ae0 fix(sessions): show actual username instead of generic "user" label (#177)
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".
2026-03-17 10:21:55 +08:00
sean2077 5be00c702f fix: case-insensitive duplicate detection, custom command ACL, and audit logging
- 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
2026-03-16 17:04:19 +08:00
sean2077 62fbe44fc0 fix: address multi-user review findings — race conditions, validation, and edge cases
- 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
2026-03-16 17:04:19 +08:00
sean2077 8c9ed6c5fa feat: add multi-user mode with per-user rate limits, role-based ACL, and audit logging
Add project-level multi-user support via [projects.users] config section:
- Role-based command ACL with disabled_commands wildcard ("*") support
- Per-user rate limiting keyed by role configuration
- Structured audit logging for command execution and blocking events
- Management API endpoints (GET/PATCH /api/v1/projects/{name}/users)
- Fully backward compatible: no [projects.users] = unchanged behavior

Closes #149
2026-03-16 17:03:52 +08:00
Sean c79d8e9077 feat: add 'sessions' subcommand with TUI viewer (#143)
* 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
2026-03-15 21:41:20 +08:00
Sean bbd5d56197 fix(cron): validate session_key in cron add to prevent silent runtime failures (#126)
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.
2026-03-15 18:20:07 +08:00
sean2077 0f2c8607b3 feat(claudecode): add dontAsk permission mode support
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
2026-03-14 17:58:23 +08:00
sean2077 5f763cbaab test(feishu): add table-driven unit tests for stripMentions
Cover bot mention removal, non-bot mention preservation with display
name, nil key skipping, nil name fallback, and empty botOpenID cases.
2026-03-13 16:34:20 +08:00
sean2077 35e013c8e7 fix(feishu): preserve non-bot @mentions in messages sent to agent
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
2026-03-13 11:52:15 +08:00
sean2077 55b624deab feat: add chat_id to inject_sender preamble
Include chat_id (extracted from session key) in the agent prompt
preamble so the agent can distinguish which group/channel a message
came from.
2026-03-13 09:02:56 +08:00
sean2077 5d580276d5 fix: align inject_sender init style with Quiet pattern
Use `if proj.InjectSender != nil` to explicitly pass the value,
consistent with how Quiet is wired during initial startup.
2026-03-12 21:23:48 +08:00
sean2077 c09f7c97c7 fix(slack): remove duplicate AppMentionEvent case causing compilation error
The type switch had two `case *slackevents.AppMentionEvent` blocks.
Keep the second one which includes bot mention prefix stripping.
2026-03-12 21:10:32 +08:00
sean2077 a1f70177ac feat: inject message sender identity into agent prompt
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
2026-03-12 20:59:42 +08:00