From 4cc8e0851545cfb41a8303abdc5d5220547b52b6 Mon Sep 17 00:00:00 2001 From: Ivan Agosto Date: Sun, 29 Mar 2026 15:43:41 -0600 Subject: [PATCH] feat: add Chatmail channel for Delta Chat integration - Add new channel 'chatmail' using rpc-client-go library - Implement Channel interface with Start, Stop, Send methods - Implement ReactionCapable interface for message reactions - Auto-mark messages as seen on receipt - Reject unauthorized users with helpful message containing their account ID - Ignore pending messages on startup to avoid responding to old messages - Add comprehensive documentation in docs/channels/chatmail/README.md Configuration: - enabled: Enable/disable the channel - account_path: Path to store Delta Chat account (default: ~/.accounts/chatmail) - allow_from: List of authorized account IDs - group_trigger: Trigger configuration for group chats - reasoning_channel_id: Channel ID for reasoning output --- docs/channels/chatmail/README.md | 197 ++++++++++++++++++ go.mod | 3 + go.sum | 6 + pkg/channels/chatmail/chatmail.go | 322 ++++++++++++++++++++++++++++++ pkg/channels/chatmail/init.go | 16 ++ pkg/channels/manager.go | 4 + pkg/config/config.go | 9 + pkg/gateway/gateway.go | 1 + 8 files changed, 558 insertions(+) create mode 100644 docs/channels/chatmail/README.md create mode 100644 pkg/channels/chatmail/chatmail.go create mode 100644 pkg/channels/chatmail/init.go diff --git a/docs/channels/chatmail/README.md b/docs/channels/chatmail/README.md new file mode 100644 index 000000000..a0aa4ad08 --- /dev/null +++ b/docs/channels/chatmail/README.md @@ -0,0 +1,197 @@ +> Back to [README](../../../README.md) + +# Chatmail (Delta Chat) + +The Chatmail channel enables PicoClaw to communicate via [Delta Chat](https://delta.chat/), a decentralized messaging platform based on email. It uses the [chatmail/rpc-client-go](https://github.com/chatmail/rpc-client-go) library to interact with Delta Chat through its RPC interface. + +## Prerequisites + +- **deltachat-rpc-server**: The Delta Chat RPC server binary must be installed and available in your system's PATH. +- **Compatible chatemail account**: Check compatibility in [https://providers.delta.chat/](https://providers.delta.chat/). + +### Installing deltachat-rpc-server + +#### Cargo + +```bash +# Server +cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server +# Repl for configure account +cargo install --git https://github.com/chatmail/core/ deltachat-repl +``` + +#### From Source + +```bash +# Server +git clone https://github.com/chatmail/core.git +cd deltachat-rpc-server +cargo build --release +sudo cp target/release/deltachat-rpc-server /usr/local/bin/ +# Repl for configure account +cd ../deltachat-repl +cargo build --release +sudo cp target/release/deltachat-repl /usr/local/bin/ +``` + +## Configuration + +Add this to your `config.json`: + +```json +{ + "channels": { + "chatmail": { + "enabled": true, + "account_path": "", + "allow_from": [], + "group_trigger": { + "mention_only": false, + "prefixes": [] + }, + "reasoning_channel_id": "" + } + } +} +``` + +## Field Reference + +| Field | Type | Required | Description | +|----------------------|----------|----------|------------------------------------------------------------------------------------------------------| +| enabled | bool | Yes | Whether to enable the Chatmail channel | +| account_path | string | No | Path to store the Delta Chat account database. Default: `~/.accounts/chatmail` | +| allow_from | []string | No | Allowlist of contact IDs; empty means all contacts are allowed | +| group_trigger | object | No | Group trigger strategy (`mention_only` / `prefixes`) | +| reasoning_channel_id | string | No | Target channel ID for reasoning output | + +### Group Trigger Configuration + +| Field | Type | Description | +|--------------|----------|------------------------------------------------------------------------------------------------| +| mention_only | bool | When `true`, the bot only responds when mentioned in group chats | +| prefixes | []string | List of prefixes that trigger bot responses in groups (e.g., `["!", "/"]`) | + +## Setup + +### Step 1: Enable the Channel + +Set `enabled: true` in the configuration file. + +### Step 2: Start PicoClaw + +When you start PicoClaw with the Chatmail channel enabled, you will see an invite link in the console: + +``` +Chatmail invite link: https://i.delta.chat/#B2AE34... +Scan this QR code with your Delta Chat app to start chatting. +``` + +Open the url and scan the QR code. + +### Step 3: Configure Your Bot Account + +1. **First Run**: On the first startup, the channel creates a new Delta Chat account. +2. **Scan the QR Code**: Use your Delta Chat app to scan the invite link QR code or manually enter the invite code. +3. **Start Chatting**: Once connected, you can send messages to the bot from your Delta Chat app. + +### Step 4: Send Messages + +- **Direct Messages**: Send any message directly to the bot's chat. +- **Group Chats**: Add the bot to a group chat. Configure `group_trigger` if you want the bot to only respond when mentioned. + +## Account Storage + +The Delta Chat account is stored at the path specified by `account_path` (default: `~/.accounts/chatmail`). This includes: + +- Account database +- Encryption keys +- Message cache + +**Important**: Keep this directory secure as it contains your private keys. + +## Behavior + +### Startup Behavior + +When the channel starts: + +1. Creates or loads the Delta Chat account +2. Generates and displays an invite link for pairing +3. **Ignores all pending messages** - only processes new messages received after startup +4. Starts listening for incoming messages + +### Message Handling + +- **Direct messages**: All messages are processed +- **Group messages**: Controlled by `group_trigger` configuration +- **Bot replies**: Sent as regular chat messages with full markdown support + +## Supported Features + +| Feature | Status | Notes | +|----------------------|--------|----------------------------------------------------------| +| Text messages | ✅ | Send and receive | +| Direct messages | ✅ | Full support | +| Group chats | ✅ | With trigger configuration | +| Markdown rendering | ✅ | Messages formatted with markdown | +| Reactions | ✅ | 👀 reaction on incoming messages, removed after response | +| Media attachments | ❌ | Not yet supported | +| Typing indicators | ❌ | Not yet supported | +| Message editing | ❌ | Not yet supported | + +## Security Considerations + +1. **End-to-End Encryption**: Delta Chat uses Autocrypt for automatic E2E encryption. Messages are encrypted by default. +2. **Account Keys**: Private keys are stored in `account_path`. Protect this directory. +3. **Allowlist**: Use `allow_from` to restrict which contacts can interact with your bot. + +## Troubleshooting + +### "deltachat-rpc-server not found" + +Ensure `deltachat-rpc-server` is installed and in your PATH: + +```bash +which deltachat-rpc-server +``` + +### Bot not receiving messages + +1. Verify the account is properly configured by checking the logs +2. Ensure you've scanned the QR code or entered the invite code +3. Check that the contact sending messages is in the `allow_from` list (if configured) + +### Invite link not appearing + +If no invite link appears in the logs: + +1. Check that the channel is enabled in the configuration +2. Verify write permissions for `account_path` +3. Check for errors in the PicoClaw logs + +## Example Configuration with Group Trigger + +```json +{ + "channels": { + "chatmail": { + "enabled": true, + "account_path": "/var/lib/picoclaw/chatmail", + "allow_from": [], + "group_trigger": { + "mention_only": true, + "prefixes": ["!", "/"] + } + } + } +} +``` + +With this configuration, the bot will only respond in group chats when: +- The bot is directly mentioned (`@botname your message`), OR +- The message starts with one of the configured prefixes (`!command` or `/command`) + +## Multiple Accounts + +Each PicoClaw instance can have one Chatmail channel configured. To run multiple bot accounts, use separate configuration files with different `account_path` values. diff --git a/go.mod b/go.mod index 54c275102..4d960f564 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.2 github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.4.0 + github.com/chatmail/rpc-client-go/v2 v2.44.0 github.com/creack/pty v1.1.24 github.com/ergochat/irc-go v0.6.0 github.com/ergochat/readline v0.1.3 @@ -61,6 +62,8 @@ require ( github.com/aws/smithy-go v1.24.2 // indirect github.com/beeper/argo-go v1.1.2 // indirect github.com/coder/websocket v1.8.14 // indirect + github.com/creachadair/jrpc2 v1.3.5 // indirect + github.com/creachadair/mds v0.26.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect diff --git a/go.sum b/go.sum index ae12473f3..dd55b6c71 100644 --- a/go.sum +++ b/go.sum @@ -63,12 +63,18 @@ github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoG github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chatmail/rpc-client-go/v2 v2.44.0 h1:DjuI2jZVdLLU5/jEp8QvaR0z1QpKrDOESOdne+s3nu8= +github.com/chatmail/rpc-client-go/v2 v2.44.0/go.mod h1:FQq2gE3wIWj48/uunoDjaRjwdsSDAr4I8QJvYVcHGHU= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creachadair/jrpc2 v1.3.5 h1:onJko+1u6xoiRph3xwWmfNISR91teCRhbJwSyS9Svzo= +github.com/creachadair/jrpc2 v1.3.5/go.mod h1:YXDmS53AavsiytbAwskrczJPcVHvKC9GoyWzwfSQXoE= +github.com/creachadair/mds v0.26.1 h1:CQG8f4cueHX/c20q5Sy/Ubk8Bvy+aRzVgbpxVieMBAs= +github.com/creachadair/mds v0.26.1/go.mod h1:dMBTCSy3iS3dwh4Rb1zxeZz2d7K8+N24GCTsayWtQRI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= diff --git a/pkg/channels/chatmail/chatmail.go b/pkg/channels/chatmail/chatmail.go new file mode 100644 index 000000000..6ccaf4fd4 --- /dev/null +++ b/pkg/channels/chatmail/chatmail.go @@ -0,0 +1,322 @@ +package chatmail + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/chatmail/rpc-client-go/v2/deltachat" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/identity" + "github.com/sipeed/picoclaw/pkg/logger" +) + +type ChatmailChannel struct { + *channels.BaseChannel + config config.ChatmailConfig + rpc *deltachat.Rpc + bot *deltachat.Bot + transport *deltachat.IOTransport + ctx context.Context + cancel context.CancelFunc + accId uint32 + mu sync.RWMutex +} + +func NewChatmailChannel(cfg config.ChatmailConfig, messageBus *bus.MessageBus) (*ChatmailChannel, error) { + base := channels.NewBaseChannel("chatmail", cfg, messageBus, cfg.AllowFrom, + channels.WithGroupTrigger(cfg.GroupTrigger), + channels.WithReasoningChannelID(cfg.ReasoningChannelID), + ) + + return &ChatmailChannel{ + BaseChannel: base, + config: cfg, + }, nil +} + +func (c *ChatmailChannel) Start(ctx context.Context) error { + logger.InfoC("chatmail", "Starting Chatmail channel") + + c.ctx, c.cancel = context.WithCancel(ctx) + + accountPath := c.config.AccountPath + if accountPath == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + accountPath = filepath.Join(homeDir, ".accounts", "chatmail") + } + + if err := os.MkdirAll(accountPath, 0700); err != nil { + return fmt.Errorf("failed to create account directory: %w", err) + } + + transport := deltachat.NewIOTransport() + transport.AccountsDir = accountPath + + if err := transport.Open(); err != nil { + return fmt.Errorf("failed to open transport: %w", err) + } + c.transport = transport + + c.rpc = &deltachat.Rpc{Context: c.ctx, Transport: transport} + + accounts, err := c.rpc.GetAllAccountIds() + if err != nil { + transport.Close() + return fmt.Errorf("failed to get accounts: %w", err) + } + + var accId uint32 + if len(accounts) == 0 { + accId, err = c.rpc.AddAccount() + if err != nil { + transport.Close() + return fmt.Errorf("failed to add account: %w", err) + } + logger.InfoCF("chatmail", "Created new account", map[string]any{"account_id": accId}) + } else { + accId = accounts[0] + logger.InfoCF("chatmail", "Using existing account", map[string]any{"account_id": accId}) + } + c.accId = accId + + isConfigured, err := c.rpc.IsConfigured(accId) + if err != nil { + transport.Close() + return fmt.Errorf("failed to check account configuration: %w", err) + } + + if !isConfigured { + botFlag := "1" + if err := c.rpc.SetConfig(accId, "bot", &botFlag); err != nil { + transport.Close() + return fmt.Errorf("failed to set bot flag: %w", err) + } + logger.InfoC("chatmail", "Account configured as bot") + } + + inviteLink, err := c.rpc.GetChatSecurejoinQrCode(accId, nil) + if err != nil { + logger.WarnCF("chatmail", "Failed to get invite link", map[string]any{"error": err.Error()}) + } else { + logger.InfoCF("chatmail", "Invite link", map[string]any{"link": inviteLink}) + fmt.Printf("\nChatmail invite link: %s\n", inviteLink) + fmt.Println("Scan this QR code with your Delta Chat app to start chatting.") + } + + msgIds, err := c.rpc.GetNextMsgs(accId) + if err != nil { + logger.WarnCF("chatmail", "Failed to fetch pending messages", map[string]any{"error": err.Error()}) + } else if len(msgIds) > 0 { + lastMsgId := fmt.Sprintf("%v", msgIds[len(msgIds)-1]) + if err := c.rpc.SetConfig(accId, "last_msg_id", &lastMsgId); err != nil { + logger.WarnCF("chatmail", "Failed to set last_msg_id", map[string]any{"error": err.Error()}) + } else { + // Mark all pending messages as seen + if err := c.rpc.MarkseenMsgs(accId, msgIds); err != nil { + logger.DebugCF("chatmail", "Failed to mark pending messages as seen", map[string]any{ + "count": len(msgIds), + "error": err.Error(), + }) + } + logger.InfoCF("chatmail", "Ignored pending messages on startup", map[string]any{"count": len(msgIds)}) + } + } + + c.bot = deltachat.NewBot(c.rpc) + c.bot.OnNewMsg(c.onNewMessage) + + go func() { + if err := c.bot.Run(); err != nil { + logger.ErrorCF("chatmail", "Bot run error", map[string]any{"error": err.Error()}) + } + }() + + c.SetRunning(true) + logger.InfoC("chatmail", "Chatmail channel started") + + return nil +} + +func (c *ChatmailChannel) Stop(ctx context.Context) error { + logger.InfoC("chatmail", "Stopping Chatmail channel") + c.SetRunning(false) + + if c.bot != nil { + c.bot.Stop() + } + + if c.cancel != nil { + c.cancel() + } + + if c.transport != nil { + c.transport.Close() + } + + logger.InfoC("chatmail", "Chatmail channel stopped") + return nil +} + +func (c *ChatmailChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { + if !c.IsRunning() { + return channels.ErrNotRunning + } + + chatId, err := c.parseChatID(msg.ChatID) + if err != nil { + return fmt.Errorf("invalid chat ID: %w", channels.ErrSendFailed) + } + + if msg.Content == "" { + return nil + } + + text := msg.Content + msgData := deltachat.MessageData{Text: &text} + + _, err = c.rpc.SendMsg(c.accId, chatId, msgData) + if err != nil { + logger.ErrorCF("chatmail", "Failed to send message", map[string]any{ + "chat_id": chatId, + "error": err.Error(), + }) + return fmt.Errorf("send failed: %w", channels.ErrSendFailed) + } + + logger.DebugCF("chatmail", "Message sent", map[string]any{"chat_id": chatId}) + return nil +} + +func (c *ChatmailChannel) parseChatID(chatID string) (uint32, error) { + var chatId uint32 + _, err := fmt.Sscanf(chatID, "%d", &chatId) + if err != nil { + return 0, err + } + return chatId, nil +} + +func (c *ChatmailChannel) parseMsgID(messageID string) (uint32, error) { + var msgId uint32 + _, err := fmt.Sscanf(messageID, "%d", &msgId) + if err != nil { + return 0, err + } + return msgId, nil +} + +func (c *ChatmailChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) { + if !c.IsRunning() { + return func() {}, nil + } + + msgId, err := c.parseMsgID(messageID) + if err != nil { + return func() {}, nil + } + + reactions := []string{"\U0001F440"} + if _, err := c.rpc.SendReaction(c.accId, msgId, reactions); err != nil { + logger.DebugCF("chatmail", "Failed to add reaction", map[string]any{"error": err.Error()}) + return func() {}, nil + } + + // Keep the reaction permanently - return no-op undo function + return func() {}, nil +} + +func (c *ChatmailChannel) onNewMessage(bot *deltachat.Bot, accId uint32, msgId uint32) { + msg, err := c.rpc.GetMessage(accId, msgId) + if err != nil { + logger.ErrorCF("chatmail", "Failed to get message", map[string]any{ + "msg_id": msgId, + "error": err.Error(), + }) + return + } + + if msg.FromId <= deltachat.ContactLastSpecial { + return + } + + // Build sender info early for permission check + senderID := fmt.Sprintf("%d", msg.FromId) + sender := bus.SenderInfo{ + Platform: "chatmail", + PlatformID: senderID, + CanonicalID: identity.BuildCanonicalID("chatmail", senderID), + } + + contact, _ := c.rpc.GetContact(accId, msg.FromId) + if contact.DisplayName != "" { + sender.DisplayName = contact.DisplayName + } + + // Check authorization BEFORE processing + if !c.IsAllowedSender(sender) { + accountInfo := senderID + + rejectionText := "⛔ Access denied. You are not authorized to use this bot.\n\n" + + "To get access, ask the administrator to add this account to the configuration:\n\n" + + "Account ID: " + accountInfo + + text := rejectionText + msgData := deltachat.MessageData{Text: &text} + c.rpc.SendMsg(c.accId, msg.ChatId, msgData) + c.rpc.MarkseenMsgs(c.accId, []uint32{msgId}) + + logger.InfoCF("chatmail", "Unauthorized user rejected", map[string]any{ + "sender_id": senderID, + "display_name": sender.DisplayName, + }) + return + } + + // Authorized - continue with normal flow + chatIdStr := fmt.Sprintf("%d", msg.ChatId) + chat, err := c.rpc.GetBasicChatInfo(accId, msg.ChatId) + if err != nil { + logger.ErrorCF("chatmail", "Failed to get chat info", map[string]any{ + "chat_id": msg.ChatId, + "error": err.Error(), + }) + return + } + + var peer bus.Peer + isGroup := chat.ChatType == deltachat.ChatTypeGroup + if isGroup { + peer = bus.Peer{Kind: "group", ID: chatIdStr} + } else { + peer = bus.Peer{Kind: "direct", ID: chatIdStr} + } + + content := msg.Text + metadata := map[string]string{ + "platform": "chatmail", + "chat_type": string(chat.ChatType), + } + + c.mu.RLock() + ctx := c.ctx + c.mu.RUnlock() + + c.HandleMessage(ctx, peer, fmt.Sprintf("%d", msgId), senderID, chatIdStr, content, nil, metadata, sender) + + // Mark the message as seen after processing + if err := c.rpc.MarkseenMsgs(accId, []uint32{msgId}); err != nil { + logger.DebugCF("chatmail", "Failed to mark message as seen", map[string]any{ + "msg_id": msgId, + "error": err.Error(), + }) + } +} diff --git a/pkg/channels/chatmail/init.go b/pkg/channels/chatmail/init.go new file mode 100644 index 000000000..7f8d2475f --- /dev/null +++ b/pkg/channels/chatmail/init.go @@ -0,0 +1,16 @@ +package chatmail + +import ( + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/channels" + "github.com/sipeed/picoclaw/pkg/config" +) + +func init() { + channels.RegisterFactory("chatmail", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) { + if !cfg.Channels.Chatmail.Enabled { + return nil, nil + } + return NewChatmailChannel(cfg.Channels.Chatmail, b) + }) +} diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 4e8074189..7b24b3114 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -425,6 +425,10 @@ func (m *Manager) initChannels(channels *config.ChannelsConfig) error { m.initChannel("irc", "IRC") } + if channels.Chatmail.Enabled { + m.initChannel("chatmail", "Chatmail") + } + logger.InfoCF("channels", "Channel initialization completed", map[string]any{ "enabled_channels": len(m.channels), }) diff --git a/pkg/config/config.go b/pkg/config/config.go index 533f45a44..fc13869a2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -374,6 +374,7 @@ type ChannelsConfig struct { Pico PicoConfig `json:"pico" yaml:"pico,omitempty"` PicoClient PicoClientConfig `json:"pico_client" yaml:"pico_client,omitempty"` IRC IRCConfig `json:"irc" yaml:"irc,omitempty"` + Chatmail ChatmailConfig `json:"chatmail" yaml:"chatmail,omitempty"` } // GroupTriggerConfig controls when the bot responds in group chats. @@ -628,6 +629,14 @@ type IRCConfig struct { ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-"` } +type ChatmailConfig struct { + Enabled bool `json:"enabled" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ENABLED"` + AccountPath string `json:"account_path" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ACCOUNT_PATH"` + AllowFrom FlexibleStringSlice `json:"allow_from" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_ALLOW_FROM"` + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty" yaml:"-"` + ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-" env:"PICOCLAW_CHANNELS_CHATMAIL_REASONING_CHANNEL_ID"` +} + type HeartbeatConfig struct { Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"` Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5 diff --git a/pkg/gateway/gateway.go b/pkg/gateway/gateway.go index c35b3e744..e580b28e1 100644 --- a/pkg/gateway/gateway.go +++ b/pkg/gateway/gateway.go @@ -14,6 +14,7 @@ import ( "github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/channels" + _ "github.com/sipeed/picoclaw/pkg/channels/chatmail" _ "github.com/sipeed/picoclaw/pkg/channels/dingtalk" _ "github.com/sipeed/picoclaw/pkg/channels/discord" _ "github.com/sipeed/picoclaw/pkg/channels/feishu"