Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,59 @@ clawlet gateway

</details>

<details>
<summary><b>Feishu (Lark)</b></summary>

Uses **WebSocket** (no public URL required). clawlet connects to Feishu's WebSocket gateway to receive events in real-time.

1. Create a Feishu/Lark app
- Go to https://open.feishu.cn/ and create a new enterprise app
- Copy `App ID` (cli_xxx) and `App Secret` from credentials

2. Configure event subscription
- Enable **Event Subscription** and subscribe to `im.message.receive_v1`
- No webhook URL needed (WebSocket mode)

3. Add bot permissions
- Bot read/write message permissions
- Install the app to your workspace

4. Get user OpenID (for allowFrom)
- In Feishu, user IDs are OpenIDs (format: `ou_xxxxx`)
- Empty `allowFrom` allows all users

5. Configure clawlet
- Set `channels.feishu.enabled=true`
- Configure `appId` and `appSecret`

Example config (merge into `~/.clawlet/config.json`):

```json
{
"channels": {
"feishu": {
"enabled": true,
"appId": "cli_xxxxxxxxxxxxx",
"appSecret": "your-app-secret-here",
"allowFrom": ["ou_xxxxxxxxxxxxx"]
}
}
}
```

Then run:

```bash
clawlet gateway
```

Notes:
- The connection is automatic once the app is properly configured
- Both private messages and group messages are supported
- For group messages, the bot needs to be @mentioned by default

</details>

## CLI Reference

| Command | Description |
Expand Down
1 change: 1 addition & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func New(opts Options) (*Agent, error) {
MaxTokens: opts.Config.Agents.Defaults.MaxTokensValue(),
Temperature: opts.Config.Agents.Defaults.Temperature,
Headers: opts.Config.LLM.Headers,
LogLevel: opts.Config.Log.LevelValue(),
}

treg := &tools.Registry{
Expand Down
1 change: 1 addition & 0 deletions agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func NewLoop(opts LoopOptions) (*Loop, error) {
MaxTokens: opts.Config.Agents.Defaults.MaxTokensValue(),
Temperature: opts.Config.Agents.Defaults.Temperature,
Headers: opts.Config.LLM.Headers,
LogLevel: opts.Config.Log.LevelValue(),
}

treg := &tools.Registry{
Expand Down
11 changes: 6 additions & 5 deletions bus/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ type InboundMessage struct {
}

type OutboundMessage struct {
Channel string
ChatID string
Content string
ReplyTo string
Delivery Delivery
Channel string
ChatID string
Content string
ReplyTo string
Delivery Delivery
Attachments []Attachment
}

type Bus struct {
Expand Down
49 changes: 37 additions & 12 deletions channels/discord/discord.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package discord

import (
"bytes"
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -103,7 +105,8 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error {
return fmt.Errorf("chat_id is empty")
}
content := strings.TrimSpace(msg.Content)
if content == "" {
attachments := msg.Attachments
if content == "" && len(attachments) == 0 {
return nil
}

Expand All @@ -125,7 +128,7 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error {
replyToID := resolveDiscordReplyTarget(msg)
const maxAttempts = 3
for attempt := 1; attempt <= maxAttempts; attempt++ {
err := sendDiscordMessage(dg, chID, content, replyToID)
err := sendDiscordMessage(dg, chID, content, replyToID, attachments)
if err == nil {
return nil
}
Expand Down Expand Up @@ -233,21 +236,43 @@ func buildDiscordDelivery(m *discordgo.MessageCreate) bus.Delivery {
return d
}

func sendDiscordMessage(dg *discordgo.Session, chID, content, replyToID string) error {
if replyToID == "" {
_, err := dg.ChannelMessageSend(chID, content)
return err
}
_, err := dg.ChannelMessageSendComplex(chID, &discordgo.MessageSend{
func sendDiscordMessage(dg *discordgo.Session, chID, content, replyToID string, attachments []bus.Attachment) error {
msgSend := &discordgo.MessageSend{
Content: content,
Reference: &discordgo.MessageReference{
}

// Add reply reference if needed
if replyToID != "" {
msgSend.Reference = &discordgo.MessageReference{
MessageID: replyToID,
ChannelID: chID,
},
AllowedMentions: &discordgo.MessageAllowedMentions{
}
msgSend.AllowedMentions = &discordgo.MessageAllowedMentions{
RepliedUser: false,
},
}
}

// Add attachments
for _, att := range attachments {
if att.Data != nil {
msgSend.Files = append(msgSend.Files, &discordgo.File{
Name: att.Name,
Reader: bytes.NewReader(att.Data),
})
} else if att.LocalPath != "" {
file, err := os.Open(att.LocalPath)
if err != nil {
return err
}
defer file.Close()
msgSend.Files = append(msgSend.Files, &discordgo.File{
Name: att.Name,
Reader: file,
})
}
}

_, err := dg.ChannelMessageSendComplex(chID, msgSend)
return err
}

Expand Down
Loading