Skip to content

Commit a286100

Browse files
authored
Merge branch 'main' into patch-1
2 parents 15e3c7d + 45351a6 commit a286100

File tree

9 files changed

+391
-148
lines changed

9 files changed

+391
-148
lines changed

README.md

Lines changed: 78 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@
6565

6666
🤖 **AI-Bootstrapped**: Autonomous Go-native implementation — 95% Agent-generated core with human-in-the-loop refinement.
6767

68-
| | OpenClaw | NanoBot | **PicoClaw** |
69-
| --- | --- | --- |--- |
70-
| **Language** | TypeScript | Python | **Go** |
71-
| **RAM** | >1GB |>100MB| **< 10MB** |
72-
| **Startup**</br>(0.8GHz core) | >500s | >30s | **<1s** |
73-
| **Cost** | Mac Mini 599$ | Most Linux SBC </br>~50$ |**Any Linux Board**</br>**As low as 10$** |
68+
| | OpenClaw | NanoBot | **PicoClaw** |
69+
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
70+
| **Language** | TypeScript | Python | **Go** |
71+
| **RAM** | >1GB | >100MB | **< 10MB** |
72+
| **Startup**</br>(0.8GHz core) | >500s | >30s | **<1s** |
73+
| **Cost** | Mac Mini 599$ | Most Linux SBC </br>~50$ | **Any Linux Board**</br>**As low as 10$** |
7474

7575
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
7676

@@ -100,9 +100,9 @@
100100

101101
PicoClaw can be deployed on almost any Linux device!
102102

103-
* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant
104-
* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance
105-
* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring
103+
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant
104+
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance
105+
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring
106106

107107
<https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4>
108108

@@ -177,7 +177,7 @@ docker compose --profile gateway up -d
177177
> [!TIP]
178178
> Set your API key in `~/.picoclaw/config.json`.
179179
> Get API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
180-
> Web search is **optional** - get free [Brave Search API](https://brave.com/search/api) (2000 free queries/month)
180+
> Web search is **optional** - get free [Brave Search API](https://brave.com/search/api) (2000 free queries/month) or use built-in auto fallback.
181181
182182
**1. Initialize**
183183

@@ -206,9 +206,14 @@ picoclaw onboard
206206
},
207207
"tools": {
208208
"web": {
209-
"search": {
209+
"brave": {
210+
"enabled": false,
210211
"api_key": "YOUR_BRAVE_API_KEY",
211212
"max_results": 5
213+
},
214+
"duckduckgo": {
215+
"enabled": true,
216+
"max_results": 5
212217
}
213218
}
214219
}
@@ -236,12 +241,12 @@ That's it! You have a working AI assistant in 2 minutes.
236241

237242
Talk to your picoclaw through Telegram, Discord, or DingTalk
238243

239-
| Channel | Setup |
240-
|---------|-------|
241-
| **Telegram** | Easy (just a token) |
242-
| **Discord** | Easy (bot token + intents) |
243-
| **QQ** | Easy (AppID + AppSecret) |
244-
| **DingTalk** | Medium (app credentials) |
244+
| Channel | Setup |
245+
| ------------ | -------------------------- |
246+
| **Telegram** | Easy (just a token) |
247+
| **Discord** | Easy (bot token + intents) |
248+
| **QQ** | Easy (AppID + AppSecret) |
249+
| **DingTalk** | Medium (app credentials) |
245250

246251
<details>
247252
<summary><b>Telegram</b> (Recommended)</summary>
@@ -596,15 +601,15 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
596601
> [!NOTE]
597602
> Groq provides free voice transcription via Whisper. If configured, Telegram voice messages will be automatically transcribed.
598603
599-
| Provider | Purpose | Get API Key |
600-
|----------|---------|-------------|
601-
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
602-
| `zhipu` | LLM (Zhipu direct) | [bigmodel.cn](bigmodel.cn) |
603-
| `openrouter(To be tested)` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
604-
| `anthropic(To be tested)` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
605-
| `openai(To be tested)` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
606-
| `deepseek(To be tested)` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
607-
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
604+
| Provider | Purpose | Get API Key |
605+
| -------------------------- | --------------------------------------- | ------------------------------------------------------ |
606+
| `gemini` | LLM (Gemini direct) | [aistudio.google.com](https://aistudio.google.com) |
607+
| `zhipu` | LLM (Zhipu direct) | [bigmodel.cn](bigmodel.cn) |
608+
| `openrouter(To be tested)` | LLM (recommended, access to all models) | [openrouter.ai](https://openrouter.ai) |
609+
| `anthropic(To be tested)` | LLM (Claude direct) | [console.anthropic.com](https://console.anthropic.com) |
610+
| `openai(To be tested)` | LLM (GPT direct) | [platform.openai.com](https://platform.openai.com) |
611+
| `deepseek(To be tested)` | LLM (DeepSeek direct) | [platform.deepseek.com](https://platform.deepseek.com) |
612+
| `groq` | LLM + **Voice transcription** (Whisper) | [console.groq.com](https://console.groq.com) |
608613

609614
<details>
610615
<summary><b>Zhipu</b></summary>
@@ -630,8 +635,8 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
630635
"zhipu": {
631636
"api_key": "Your API Key",
632637
"api_base": "https://open.bigmodel.cn/api/paas/v4"
633-
},
634-
},
638+
}
639+
}
635640
}
636641
```
637642

@@ -692,8 +697,14 @@ picoclaw agent -m "Hello"
692697
},
693698
"tools": {
694699
"web": {
695-
"search": {
696-
"api_key": "BSA..."
700+
"brave": {
701+
"enabled": false,
702+
"api_key": "BSA...",
703+
"max_results": 5
704+
},
705+
"duckduckgo": {
706+
"enabled": true,
707+
"max_results": 5
697708
}
698709
}
699710
},
@@ -708,15 +719,15 @@ picoclaw agent -m "Hello"
708719

709720
## CLI Reference
710721

711-
| Command | Description |
712-
|---------|-------------|
713-
| `picoclaw onboard` | Initialize config & workspace |
714-
| `picoclaw agent -m "..."` | Chat with the agent |
715-
| `picoclaw agent` | Interactive chat mode |
716-
| `picoclaw gateway` | Start the gateway |
717-
| `picoclaw status` | Show status |
718-
| `picoclaw cron list` | List all scheduled jobs |
719-
| `picoclaw cron add ...` | Add a scheduled job |
722+
| Command | Description |
723+
| ------------------------- | ----------------------------- |
724+
| `picoclaw onboard` | Initialize config & workspace |
725+
| `picoclaw agent -m "..."` | Chat with the agent |
726+
| `picoclaw agent` | Interactive chat mode |
727+
| `picoclaw gateway` | Start the gateway |
728+
| `picoclaw status` | Show status |
729+
| `picoclaw cron list` | List all scheduled jobs |
730+
| `picoclaw cron add ...` | Add a scheduled job |
720731

721732
### Scheduled Tasks / Reminders
722733

@@ -750,21 +761,28 @@ This is normal if you haven't configured a search API key yet. PicoClaw will pro
750761

751762
To enable web search:
752763

753-
1. Get a free API key at [https://brave.com/search/api](https://brave.com/search/api) (2000 free queries/month)
754-
2. Add to `~/.picoclaw/config.json`:
755-
756-
```json
757-
{
758-
"tools": {
759-
"web": {
760-
"search": {
761-
"api_key": "YOUR_BRAVE_API_KEY",
762-
"max_results": 5
763-
}
764-
}
765-
}
766-
}
767-
```
764+
1. **Option 1 (Recommended)**: Get a free API key at [https://brave.com/search/api](https://brave.com/search/api) (2000 free queries/month) for the best results.
765+
2. **Option 2 (No Credit Card)**: If you don't have a key, we automatically fall back to **DuckDuckGo** (no key required).
766+
767+
Add the key to `~/.picoclaw/config.json` if using Brave:
768+
769+
```json
770+
{
771+
"tools": {
772+
"web": {
773+
"brave": {
774+
"enabled": false,
775+
"api_key": "YOUR_BRAVE_API_KEY",
776+
"max_results": 5
777+
},
778+
"duckduckgo": {
779+
"enabled": true,
780+
"max_results": 5
781+
}
782+
}
783+
}
784+
}
785+
```
768786

769787
### Getting content filtering errors
770788

@@ -778,9 +796,9 @@ This happens when another instance of the bot is running. Make sure only one `pi
778796

779797
## 📝 API Key Comparison
780798

781-
| Service | Free Tier | Use Case |
782-
|---------|-----------|-----------|
783-
| **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) |
784-
| **Zhipu** | 200K tokens/month | Best for Chinese users |
785-
| **Brave Search** | 2000 queries/month | Web search functionality |
786-
| **Groq** | Free tier available | Fast inference (Llama, Mixtral) |
799+
| Service | Free Tier | Use Case |
800+
| ---------------- | ------------------- | ------------------------------------- |
801+
| **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) |
802+
| **Zhipu** | 200K tokens/month | Best for Chinese users |
803+
| **Brave Search** | 2000 queries/month | Web search functionality |
804+
| **Groq** | Free tier available | Fast inference (Llama, Mixtral) |

pkg/agent/loop.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,15 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
7070
// Shell execution
7171
registry.Register(tools.NewExecTool(workspace, restrict))
7272

73-
// Web tools
74-
braveAPIKey := cfg.Tools.Web.Search.APIKey
75-
registry.Register(tools.NewWebSearchTool(braveAPIKey, cfg.Tools.Web.Search.MaxResults))
73+
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
74+
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
75+
BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
76+
BraveEnabled: cfg.Tools.Web.Brave.Enabled,
77+
DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
78+
DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
79+
}); searchTool != nil {
80+
registry.Register(searchTool)
81+
}
7682
registry.Register(tools.NewWebFetchTool(50000))
7783

7884
// Message tool - available to both agent and subagent

pkg/channels/base.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,22 @@ func (c *BaseChannel) IsAllowed(senderID string) bool {
5959
for _, allowed := range c.allowList {
6060
// Strip leading "@" from allowed value for username matching
6161
trimmed := strings.TrimPrefix(allowed, "@")
62-
if senderID == allowed || idPart == allowed || senderID == trimmed || idPart == trimmed || (userPart != "" && (userPart == allowed || userPart == trimmed)) {
62+
allowedID := trimmed
63+
allowedUser := ""
64+
if idx := strings.Index(trimmed, "|"); idx > 0 {
65+
allowedID = trimmed[:idx]
66+
allowedUser = trimmed[idx+1:]
67+
}
68+
69+
// Support either side using "id|username" compound form.
70+
// This keeps backward compatibility with legacy Telegram allowlist entries.
71+
if senderID == allowed ||
72+
idPart == allowed ||
73+
senderID == trimmed ||
74+
idPart == trimmed ||
75+
idPart == allowedID ||
76+
(allowedUser != "" && senderID == allowedUser) ||
77+
(userPart != "" && (userPart == allowed || userPart == trimmed || userPart == allowedUser)) {
6378
return true
6479
}
6580
}

pkg/channels/base_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package channels
2+
3+
import "testing"
4+
5+
func TestBaseChannelIsAllowed(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
allowList []string
9+
senderID string
10+
want bool
11+
}{
12+
{
13+
name: "empty allowlist allows all",
14+
allowList: nil,
15+
senderID: "anyone",
16+
want: true,
17+
},
18+
{
19+
name: "compound sender matches numeric allowlist",
20+
allowList: []string{"123456"},
21+
senderID: "123456|alice",
22+
want: true,
23+
},
24+
{
25+
name: "compound sender matches username allowlist",
26+
allowList: []string{"@alice"},
27+
senderID: "123456|alice",
28+
want: true,
29+
},
30+
{
31+
name: "numeric sender matches legacy compound allowlist",
32+
allowList: []string{"123456|alice"},
33+
senderID: "123456",
34+
want: true,
35+
},
36+
{
37+
name: "non matching sender is denied",
38+
allowList: []string{"123456"},
39+
senderID: "654321|bob",
40+
want: false,
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
ch := NewBaseChannel("test", nil, nil, tt.allowList)
47+
if got := ch.IsAllowed(tt.senderID); got != tt.want {
48+
t.Fatalf("IsAllowed(%q) = %v, want %v", tt.senderID, got, tt.want)
49+
}
50+
})
51+
}
52+
}
53+

pkg/channels/telegram.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,17 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
177177
return
178178
}
179179

180-
senderID := fmt.Sprintf("%d", user.ID)
180+
userID := fmt.Sprintf("%d", user.ID)
181+
senderID := userID
181182
if user.Username != "" {
182-
senderID = fmt.Sprintf("%d|%s", user.ID, user.Username)
183+
senderID = fmt.Sprintf("%s|%s", userID, user.Username)
183184
}
184185

185186
// 检查白名单,避免为被拒绝的用户下载附件
186-
if !c.IsAllowed(senderID) {
187+
if !c.IsAllowed(userID) && !c.IsAllowed(senderID) {
187188
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]interface{}{
188-
"user_id": senderID,
189+
"user_id": userID,
190+
"username": user.Username,
189191
})
190192
return
191193
}
@@ -359,7 +361,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, update telego.Updat
359361
"is_group": fmt.Sprintf("%t", message.Chat.Type != "private"),
360362
}
361363

362-
c.HandleMessage(fmt.Sprintf("%d", user.ID), fmt.Sprintf("%d", chatID), content, mediaPaths, metadata)
364+
c.HandleMessage(senderID, fmt.Sprintf("%d", chatID), content, mediaPaths, metadata)
363365
}
364366

365367
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {

pkg/config/config.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,20 @@ type GatewayConfig struct {
164164
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
165165
}
166166

167-
type WebSearchConfig struct {
168-
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_SEARCH_API_KEY"`
169-
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_SEARCH_MAX_RESULTS"`
167+
type BraveConfig struct {
168+
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_BRAVE_ENABLED"`
169+
APIKey string `json:"api_key" env:"PICOCLAW_TOOLS_WEB_BRAVE_API_KEY"`
170+
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_BRAVE_MAX_RESULTS"`
171+
}
172+
173+
type DuckDuckGoConfig struct {
174+
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_ENABLED"`
175+
MaxResults int `json:"max_results" env:"PICOCLAW_TOOLS_WEB_DUCKDUCKGO_MAX_RESULTS"`
170176
}
171177

172178
type WebToolsConfig struct {
173-
Search WebSearchConfig `json:"search"`
179+
Brave BraveConfig `json:"brave"`
180+
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
174181
}
175182

176183
type ToolsConfig struct {
@@ -257,10 +264,15 @@ func DefaultConfig() *Config {
257264
},
258265
Tools: ToolsConfig{
259266
Web: WebToolsConfig{
260-
Search: WebSearchConfig{
267+
Brave: BraveConfig{
268+
Enabled: false,
261269
APIKey: "",
262270
MaxResults: 5,
263271
},
272+
DuckDuckGo: DuckDuckGoConfig{
273+
Enabled: true,
274+
MaxResults: 5,
275+
},
264276
},
265277
},
266278
Heartbeat: HeartbeatConfig{

0 commit comments

Comments
 (0)