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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,5 @@ tasks/

# Added by goreleaser init:
dist/

.qoder/
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ uninstall-all:
@echo "Removed workspace: $(PICOCLAW_HOME)"
@echo "Complete uninstallation done!"

## build-riscv64: Build picoclaw for RISC-V 64-bit
build-riscv64: generate
@mkdir -p $(BUILD_DIR)
GOOS=linux GOARCH=riscv64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64 ./$(CMD_DIR)
@echo "RISC-V build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-riscv64"

## clean: Remove build artifacts
clean:
@echo "Cleaning build artifacts..."
Expand Down
74 changes: 64 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,17 @@ That's it! You have a working AI assistant in 2 minutes.

## 💬 Chat Apps

Talk to your picoclaw through Telegram, Discord, DingTalk, LINE, or WeCom

| Channel | Setup |
| ------------ | ---------------------------------- |
| **Telegram** | Easy (just a token) |
| **Discord** | Easy (bot token + intents) |
| **QQ** | Easy (AppID + AppSecret) |
| **DingTalk** | Medium (app credentials) |
| **LINE** | Medium (credentials + webhook URL) |
| **WeCom** | Medium (CorpID + webhook setup) |
Talk to your picoclaw through Telegram, Discord, DingTalk, LINE, WeCom or WebSocket

| Channel | Setup |
| ------------- | ---------------------------------- |
| **Telegram** | Easy (just a token) |
| **Discord** | Easy (bot token + intents) |
| **QQ** | Easy (AppID + AppSecret) |
| **DingTalk** | Medium (app credentials) |
| **LINE** | Medium (credentials + webhook URL) |
| **WebSocket** | Easy (local LAN web chat) |
| **WeCom** | Medium (CorpID + webhook setup) |

<details>
<summary><b>Telegram</b> (Recommended)</summary>
Expand Down Expand Up @@ -559,6 +560,59 @@ picoclaw gateway

</details>

<details>
<summary><b>WebSocket</b> (Local LAN Web Chat)</summary>

**1. Configure**

WebSocket channel provides a web-based chat interface accessible from your local network:

```json
{
"channels": {
"websocket": {
"enabled": true,
"host": "0.0.0.0",
"port": 8080,
"allow_from": []
}
}
}
```

**Configuration Options:**

- `host`: Bind address (use `0.0.0.0` to allow connections from LAN, or `127.0.0.1` for localhost only)
- `port`: HTTP server port (default: 8080)
- `allow_from`: List of allowed client IPs (empty list = allow all)

**2. Run**

```bash
picoclaw gateway
```

**3. Access the Web Interface**

Open your browser and navigate to:

- Local: `http://localhost:8080`
- LAN: `http://YOUR_SERVER_IP:8080`

The chat interface features:

- Modern, responsive design with dark mode
- Real-time messaging via WebSocket
- Automatic reconnection on connection loss
- Connection status indicator
- Multi-language support (English/中文)

> **Note**: The WebSocket channel is designed for local LAN use. For internet-facing deployments, consider setting up proper authentication and using HTTPS with a reverse proxy.

> **Docker Compose**: Add `ports: ["8080:8080"]` to the `picoclaw-gateway` service to expose the WebSocket port.

</details>

## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Join the Agent Social Network

Connect Picoclaw to the Agent Social Network simply by sending a single message via the CLI or any integrated Chat App.
Expand Down
59 changes: 59 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,65 @@ PicoClaw 支持多种聊天平台,使您的 Agent 能够连接到任何地方
| **OneBot** | ⭐⭐ 中等 | 兼容 NapCat/Go-CQHTTP,社区生态丰富 | [查看文档](docs/channels/onebot/README.zh.md) |
| **MaixCam** | ⭐ 简单 | 专为 AI 摄像头设计的硬件集成通道 | [查看文档](docs/channels/maixcam/README.zh.md) |





<details>
<summary><b>WebSocket</b> (局域网 Web 聊天)</summary>

**1. 配置**

WebSocket 频道提供了一个可从局域网访问的 Web 聊天界面:

```json
{
"channels": {
"websocket": {
"enabled": true,
"host": "0.0.0.0",
"port": 8080,
"allow_from": []
}
}
}
```

**配置选项:**

- `host`: 绑定地址(使用 `0.0.0.0` 允许局域网连接,或 `127.0.0.1` 仅允许本地连接)
- `port`: HTTP 服务器端口(默认:8080)
- `allow_from`: 允许的客户端 IP 列表(空列表 = 允许所有)

**2. 运行**

```bash
picoclaw gateway
```

**3. 访问 Web 界面**

打开浏览器并访问:

- 本地:`http://localhost:8080`
- 局域网:`http://您的服务器IP:8080`

聊天界面功能:

- 现代化响应式设计,支持深色模式
- 通过 WebSocket 实现实时消息传输
- 连接丢失时自动重连
- 连接状态指示器
- 多语言支持(中文/English)

> **注意**:WebSocket 频道设计用于局域网使用。如果需要通过互联网访问,请考虑设置适当的身份验证,并使用反向代理配置 HTTPS。

> **Docker Compose**:在 `picoclaw-gateway` 服务中添加 `ports: ["8080:8080"]` 以暴露 WebSocket 端口。

</details>



## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络

只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
Expand Down
52 changes: 52 additions & 0 deletions S99picoclaw_gateway
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/sh
PATH=/usr/sbin:/usr/bin:/sbin:/bin
DAEMON="/root/picoclaw"
DAEMON_ARGS="gateway"
LOG="/var/log/picoclaw_gateway.log"
PIDFILE="/var/run/picoclaw_gateway.pid"
RUN_USER="root"

start() {
[ -x "$DAEMON" ] || { echo "picoclaw 未找到"; exit 1; }
mkdir -p /var/run
printf "启动 picoclaw: "
su - $RUN_USER -c "\"$DAEMON\" $DAEMON_ARGS >\"$LOG\" 2>&1 & echo \$! >\"$PIDFILE\""
sleep 1
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "OK"
else
echo "失败"
rm -f "$PIDFILE"
exit 1
fi
}

stop() {
printf "停止 picoclaw: "
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
kill "$(cat "$PIDFILE")" 2>/dev/null || true
sleep 2
kill -0 "$(cat "$PIDFILE")" 2>/dev/null && kill -9 "$(cat "$PIDFILE")" 2>/dev/null || true
rm -f "$PIDFILE"
echo "OK"
else
su - $RUN_USER -c "killall picoclaw 2>/dev/null" && echo "OK" || echo "进程未运行"
fi
}

status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "picoclaw 正在运行(pid $(cat "$PIDFILE"))"
else
echo "picoclaw 已停止"; exit 1
fi
}

case "$1" in
start) start ;;
stop) stop ;;
restart|reload) stop; start ;;
status) status ;;
*) echo "用法: $0 {start|stop|restart|status}"; exit 1 ;;
esac
exit 0
91 changes: 78 additions & 13 deletions cmd/picoclaw/cmd_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ package main

import (
"fmt"
"net"
"os"
"strconv"
"strings"

"github.com/sipeed/picoclaw/pkg/auth"
)
Expand Down Expand Up @@ -43,19 +46,49 @@ func statusCmd() {
if _, err := os.Stat(configPath); err == nil {
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.Model)

hasOpenRouter := cfg.Providers.OpenRouter.APIKey != ""
hasAnthropic := cfg.Providers.Anthropic.APIKey != ""
hasOpenAI := cfg.Providers.OpenAI.APIKey != ""
hasGemini := cfg.Providers.Gemini.APIKey != ""
hasZhipu := cfg.Providers.Zhipu.APIKey != ""
hasQwen := cfg.Providers.Qwen.APIKey != ""
hasGroq := cfg.Providers.Groq.APIKey != ""
hasVLLM := cfg.Providers.VLLM.APIBase != ""
hasMoonshot := cfg.Providers.Moonshot.APIKey != ""
hasDeepSeek := cfg.Providers.DeepSeek.APIKey != ""
hasVolcEngine := cfg.Providers.VolcEngine.APIKey != ""
hasNvidia := cfg.Providers.Nvidia.APIKey != ""
hasOllama := cfg.Providers.Ollama.APIBase != ""
// Build a map of providers from model_list
modelProviders := make(map[string]bool)
for _, model := range cfg.ModelList {
if model.APIKey == "" {
continue
}
modelStr := strings.ToLower(model.Model)
// Extract provider name (before "/")
if idx := strings.Index(modelStr, "/"); idx > 0 {
provider := modelStr[:idx]
modelProviders[provider] = true
// Add aliases
switch provider {
case "doubao":
modelProviders["volcengine"] = true
case "claude":
modelProviders["anthropic"] = true
case "gpt":
modelProviders["openai"] = true
case "tongyi":
modelProviders["qwen"] = true
case "kimi":
modelProviders["moonshot"] = true
case "glm":
modelProviders["zhipu"] = true
}
}
}

// Check providers (legacy) or model_list (new)
hasOpenRouter := cfg.Providers.OpenRouter.APIKey != "" || modelProviders["openrouter"]
hasAnthropic := cfg.Providers.Anthropic.APIKey != "" || modelProviders["anthropic"]
hasOpenAI := cfg.Providers.OpenAI.APIKey != "" || modelProviders["openai"]
hasGemini := cfg.Providers.Gemini.APIKey != "" || modelProviders["gemini"]
hasZhipu := cfg.Providers.Zhipu.APIKey != "" || modelProviders["zhipu"]
hasQwen := cfg.Providers.Qwen.APIKey != "" || modelProviders["qwen"]
hasGroq := cfg.Providers.Groq.APIKey != "" || modelProviders["groq"]
hasVLLM := cfg.Providers.VLLM.APIBase != "" || modelProviders["vllm"]
hasMoonshot := cfg.Providers.Moonshot.APIKey != "" || modelProviders["moonshot"]
hasDeepSeek := cfg.Providers.DeepSeek.APIKey != "" || modelProviders["deepseek"]
hasVolcEngine := cfg.Providers.VolcEngine.APIKey != "" || modelProviders["volcengine"]
hasNvidia := cfg.Providers.Nvidia.APIKey != "" || modelProviders["nvidia"]
hasOllama := cfg.Providers.Ollama.APIBase != "" || modelProviders["ollama"]

status := func(enabled bool) string {
if enabled {
Expand Down Expand Up @@ -98,5 +131,37 @@ func statusCmd() {
fmt.Printf(" %s (%s): %s\n", provider, cred.AuthMethod, status)
}
}

// Display channel status
fmt.Println("\nChannels:")
channelStatus := func(enabled bool, name string, details ...string) {
if enabled {
if len(details) > 0 {
fmt.Printf(" %s: ✓ %s\n", name, details[0])
} else {
fmt.Printf(" %s: ✓\n", name)
}
} else {
fmt.Printf(" %s: disabled\n", name)
}
}

channelStatus(cfg.Channels.Telegram.Enabled, "Telegram")
channelStatus(cfg.Channels.Discord.Enabled, "Discord")
channelStatus(cfg.Channels.Feishu.Enabled, "Feishu")
channelStatus(cfg.Channels.DingTalk.Enabled, "DingTalk")
channelStatus(cfg.Channels.Slack.Enabled, "Slack")
channelStatus(cfg.Channels.WhatsApp.Enabled, "WhatsApp")
channelStatus(cfg.Channels.QQ.Enabled, "QQ")
channelStatus(cfg.Channels.LINE.Enabled, "LINE")
channelStatus(cfg.Channels.OneBot.Enabled, "OneBot")
channelStatus(cfg.Channels.MaixCam.Enabled, "MaixCam")
if cfg.Channels.WebSocket.Enabled {
hostPort := net.JoinHostPort(cfg.Channels.WebSocket.Host, strconv.Itoa(cfg.Channels.WebSocket.Port))
addr := "http://" + hostPort
channelStatus(true, "WebSocket", addr)
} else {
channelStatus(false, "WebSocket")
}
}
}
6 changes: 6 additions & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@
"group_trigger_prefix": [],
"allow_from": []
},
"websocket": {
"enabled": false,
"host": "0.0.0.0",
"port": 8080,
"allow_from": []
},
"wecom": {
"_comment": "WeCom Bot (智能机器人) - Easier setup, supports group chats",
"enabled": false,
Expand Down
14 changes: 14 additions & 0 deletions pkg/channels/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"sync"

"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels/websocket"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/constants"
"github.com/sipeed/picoclaw/pkg/logger"
Expand Down Expand Up @@ -202,6 +203,19 @@ func (m *Manager) initChannels() error {
}
}

if m.config.Channels.WebSocket.Enabled {
logger.DebugC("channels", "Attempting to initialize WebSocket channel")
ws, err := websocket.NewChannel(m.config.Channels.WebSocket, m.bus)
if err != nil {
logger.ErrorCF("channels", "Failed to initialize WebSocket channel", map[string]any{
"error": err.Error(),
})
} else {
m.channels["websocket"] = ws
logger.InfoC("channels", "WebSocket channel enabled successfully")
}
}

logger.InfoCF("channels", "Channel initialization completed", map[string]any{
"enabled_channels": len(m.channels),
})
Expand Down
Loading