This commit is contained in:
OpenClaw-User 2026-03-14 17:12:18 +08:00
commit f712d803f4
13 changed files with 776 additions and 6 deletions

View file

@ -283,11 +283,12 @@ Et voilà ! Vous avez un assistant IA fonctionnel en 2 minutes.
## 💬 Applications de Chat
Discutez avec votre PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom
Discutez avec votre PicoClaw via Telegram, Zalo, Discord, DingTalk, LINE ou WeCom
| Canal | Configuration |
| ------------ | -------------------------------------- |
| **Telegram** | Facile (juste un token) |
| **Zalo** | Moyen (token + secret_token + webhook URL) |
| **Discord** | Facile (token bot + intents) |
| **QQ** | Facile (AppID + AppSecret) |
| **DingTalk** | Moyen (identifiants de l'application) |
@ -327,6 +328,55 @@ picoclaw gateway
</details>
<details>
<summary><b>Zalo</b> (Webhook)</summary>
**1. Préparer**
* Récupérez votre `BOT_TOKEN` Zalo
* Choisissez un `secret_token` (8256 caractères). Zalo le renverra dans l'en-tête `X-Bot-Api-Secret-Token`.
* Pour plus de détails sur le webhook, voir la documentation officielle [`setWebhook`](https://bot.zapps.me/docs/apis/setWebhook/).
**2. Configurer**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. Démarrer**
```bash
picoclaw gateway
```
**4. HTTPS local (ngrok)**
```bash
ngrok http 18790
```
Webhook URL: `https://<ngrok-domain>/webhook/zalo`
**5. Enregistrer le webhook**
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
</details>
<details>
<summary><b>Discord</b></summary>

View file

@ -252,11 +252,12 @@ picoclaw agent -m "What is 2+2?"
## 💬 チャットアプリ
Telegram、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話できます
Telegram、Zalo、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話できます
| チャネル | セットアップ |
|---------|------------|
| **Telegram** | 簡単(トークンのみ) |
| **Zalo** | 普通token + secret_token + webhook URL |
| **Discord** | 簡単Bot トークン + Intents |
| **QQ** | 簡単AppID + AppSecret |
| **DingTalk** | 普通(アプリ認証情報) |
@ -295,6 +296,55 @@ picoclaw gateway
```
</details>
<details>
<summary><b>Zalo</b>Webhook</summary>
**1. 準備**
- Zalo の `BOT_TOKEN` を用意
- `secret_token`8256 文字を決めるZalo が `X-Bot-Api-Secret-Token` ヘッダーで返します)
- Webhook の詳細は公式ドキュメント [`setWebhook`](https://bot.zapps.me/docs/apis/setWebhook/) を参照してください。
**2. 設定**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. 起動**
```bash
picoclaw gateway
```
**4. ローカルで HTTPSngrok**
```bash
ngrok http 18790
```
Webhook URL: `https://<ngrok-domain>/webhook/zalo`
**5. Webhook 登録**
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
</details>
<details>
<summary><b>Discord</b></summary>

View file

@ -325,13 +325,14 @@ That's it! You have a working AI assistant in 2 minutes.
## 💬 Chat Apps
Talk to your picoclaw through Telegram, Discord, WhatsApp, Matrix, QQ, DingTalk, LINE, or WeCom
Talk to your picoclaw through Telegram, Zalo, Discord, WhatsApp, Matrix, QQ, DingTalk, LINE, or WeCom
> **Note**: All webhook-based channels (LINE, WeCom, etc.) are served on a single shared Gateway HTTP server (`gateway.host`:`gateway.port`, default `127.0.0.1:18790`). There are no per-channel ports to configure. Note: Feishu uses WebSocket/SDK mode and does not use the shared HTTP webhook server.
| Channel | Setup |
| ------------ | ---------------------------------- |
| **Telegram** | Easy (just a token) |
| **Zalo** | Medium (token + secret_token + webhook URL) |
| **Discord** | Easy (bot token + intents) |
| **WhatsApp** | Easy (native: QR scan; or bridge URL) |
| **Matrix** | Medium (homeserver + bot access token) |
@ -380,6 +381,66 @@ If command registration fails (network/API transient errors), the channel still
</details>
<details>
<summary><b>Zalo</b> (Webhook)</summary>
PicoClaw integrates with Zalo Bot via the official Zalo Bot API webhook flow.
**1. Create a Zalo Bot**
* Create a bot in Zalo Developer / Bot console
* Copy your **Bot Token** (`BOT_TOKEN`)
* Choose a strong `secret_token` (8256 chars). This is your own secret and will be sent back by Zalo in the `X-Bot-Api-Secret-Token` header.
* For full webhook options, see Zalos official [`setWebhook` docs](https://bot.zapps.me/docs/apis/setWebhook/).
**2. Configure**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. Run the Gateway**
```bash
picoclaw gateway
```
**4. Expose HTTPS locally (ngrok)**
Zalo requires an HTTPS webhook URL. For local development you can use ngrok:
```bash
ngrok http 18790
```
Then your webhook URL becomes:
`https://<your-ngrok-domain>/webhook/zalo`
**5. Register the webhook with Zalo**
Call Zalo `setWebhook` with your HTTPS URL and the same `secret_token` you configured above:
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<your-ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
After that, send a message to your bot on Zalo and PicoClaw will reply.
</details>
<details>
<summary><b>Discord</b></summary>

View file

@ -277,11 +277,12 @@ Pronto! Você tem um assistente de IA funcionando em 2 minutos.
## 💬 Integração com Apps de Chat
Converse com seu PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom.
Converse com seu PicoClaw via Telegram, Zalo, Discord, DingTalk, LINE ou WeCom.
| Canal | Nível de Configuração |
| --- | --- |
| **Telegram** | Fácil (apenas um token) |
| **Zalo** | Médio (token + secret_token + webhook URL) |
| **Discord** | Fácil (bot token + intents) |
| **QQ** | Fácil (AppID + AppSecret) |
| **DingTalk** | Médio (credenciais do app) |
@ -321,6 +322,55 @@ picoclaw gateway
</details>
<details>
<summary><b>Zalo</b> (Webhook)</summary>
**1. Preparar**
* Pegue seu `BOT_TOKEN` do Zalo
* Defina um `secret_token` (8256 chars). O Zalo enviará esse valor no header `X-Bot-Api-Secret-Token`.
* Para mais detalhes de webhook, veja a documentação oficial [`setWebhook`](https://bot.zapps.me/docs/apis/setWebhook/).
**2. Configurar**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. Executar**
```bash
picoclaw gateway
```
**4. HTTPS local (ngrok)**
```bash
ngrok http 18790
```
Webhook URL: `https://<ngrok-domain>/webhook/zalo`
**5. Registrar webhook**
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
</details>
<details>
<summary><b>Discord</b></summary>

View file

@ -251,11 +251,12 @@ Vậy là xong! Bạn đã có một trợ lý AI hoạt động chỉ trong 2 p
## 💬 Tích hợp ứng dụng Chat
Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk, LINE hoặc WeCom.
Trò chuyện với PicoClaw qua Telegram, Zalo, Discord, DingTalk, LINE hoặc WeCom.
| Kênh | Mức độ thiết lập |
| --- | --- |
| **Telegram** | Dễ (chỉ cần token) |
| **Zalo** | Trung bình (token + secret_token + webhook URL) |
| **Discord** | Dễ (bot token + intents) |
| **QQ** | Dễ (AppID + AppSecret) |
| **DingTalk** | Trung bình (app credentials) |
@ -295,6 +296,55 @@ picoclaw gateway
</details>
<details>
<summary><b>Zalo</b> (Webhook)</summary>
**1. Chuẩn bị**
* Lấy `BOT_TOKEN` của Zalo Bot
* Tạo `secret_token` (8256 ký tự). Zalo sẽ gửi lại trong header `X-Bot-Api-Secret-Token`.
* Xem thêm chi tiết webhook tại tài liệu chính thức [`setWebhook`](https://bot.zapps.me/docs/apis/setWebhook/).
**2. Cấu hình**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. Chạy**
```bash
picoclaw gateway
```
**4. HTTPS local (ngrok)**
```bash
ngrok http 18790
```
Webhook URL: `https://<ngrok-domain>/webhook/zalo`
**5. Đăng ký webhook**
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
</details>
<details>
<summary><b>Discord</b></summary>

View file

@ -301,6 +301,7 @@ PicoClaw 支持多种聊天平台,使您的 Agent 能够连接到任何地方
| 渠道 | 设置难度 | 特性说明 | 文档链接 |
| -------------------- | ----------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Telegram** | ⭐ 简单 | 推荐,支持语音转文字,长轮询无需公网 | [查看文档](docs/channels/telegram/README.zh.md) |
| **Zalo** | ⭐⭐ 中等 | Webhook 模式(需要 HTTPS适合本地 ngrok 调试 | 参见主 README 中的 Zalo(Webhook) 章节 |
| **Discord** | ⭐ 简单 | Socket Mode支持群组/私信Bot 生态成熟 | [查看文档](docs/channels/discord/README.zh.md) |
| **Slack** | ⭐ 简单 | **Socket Mode** (无需公网 IP),企业级支持 | [查看文档](docs/channels/slack/README.zh.md) |
| **Matrix** | ⭐⭐ 中等 | 联邦协议,支持自建 homeserver 与公开服务器 | [查看文档](docs/channels/matrix/README.zh.md) |
@ -319,6 +320,66 @@ Telegram 侧保留的是命令菜单注册能力;通用命令的实际执行
如果注册因网络或 API 短暂异常失败,不会阻塞 channel 启动;系统会在后台自动重试。
<details>
<summary><b>Zalo</b> (Webhook)</summary>
PicoClaw 通过官方 Zalo Bot API webhook 流程与 Zalo Bot 集成。
**1. 创建 Zalo Bot**
* 在 Zalo Developer / Bot 控制台创建机器人
* 复制您的 **Bot Token** (`BOT_TOKEN`)
* 选择一个强密码 `secret_token`8256 字符。这是您自己的密钥Zalo 会在 `X-Bot-Api-Secret-Token` 请求头中返回此值。
* 有关完整的 webhook 选项,请参阅 Zalo 官方 [`setWebhook` 文档](https://bot.zapps.me/docs/apis/setWebhook/)。
**2. 配置**
```json
{
"channels": {
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": []
}
}
}
```
**3. 运行 Gateway**
```bash
picoclaw gateway
```
**4. 本地暴露 HTTPS (ngrok)**
Zalo 需要 HTTPS webhook URL。对于本地开发您可以使用 ngrok
```bash
ngrok http 18790
```
然后您的 webhook URL 变为:
`https://<your-ngrok-domain>/webhook/zalo`
**5. 向 Zalo 注册 webhook**
使用您的 HTTPS URL 和上面配置的相同 `secret_token` 调用 Zalo `setWebhook`
```bash
curl -X POST "https://bot-api.zaloplatforms.com/bot${BOT_TOKEN}/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url":"https://<your-ngrok-domain>/webhook/zalo","secret_token":"YOUR_SECRET_TOKEN"}'
```
之后,在 Zalo 上向您的机器人发送消息PicoClaw 将回复。
</details>
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。

View file

@ -28,6 +28,7 @@ import (
_ "github.com/sipeed/picoclaw/pkg/channels/wecom"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp"
_ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native"
_ "github.com/sipeed/picoclaw/pkg/channels/zalo"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices"

View file

@ -77,6 +77,14 @@
],
"reasoning_channel_id": ""
},
"zalo": {
"enabled": true,
"token": "YOUR_ZALO_BOT_TOKEN",
"secret_token": "YOUR_SECRET_TOKEN",
"webhook_path": "/webhook/zalo",
"allow_from": [],
"reasoning_channel_id": ""
},
"discord": {
"enabled": false,
"token": "YOUR_DISCORD_BOT_TOKEN",
@ -514,4 +522,4 @@
"host": "127.0.0.1",
"port": 18790
}
}
}

View file

@ -59,6 +59,7 @@ type placeholderEntry struct {
// channelRateConfig maps channel name to per-second rate limit.
var channelRateConfig = map[string]float64{
"telegram": 20,
"zalo": 10,
"discord": 1,
"slack": 1,
"matrix": 2,
@ -239,6 +240,11 @@ func (m *Manager) initChannels() error {
m.initChannel("telegram", "Telegram")
}
if m.config.Channels.Zalo.Enabled && m.config.Channels.Zalo.Token != "" &&
m.config.Channels.Zalo.SecretToken != "" {
m.initChannel("zalo", "Zalo")
}
if m.config.Channels.WhatsApp.Enabled {
waCfg := m.config.Channels.WhatsApp
if waCfg.UseNative {

13
pkg/channels/zalo/init.go Normal file
View file

@ -0,0 +1,13 @@
package zalo
import (
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config"
)
func init() {
channels.RegisterFactory("zalo", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
return NewZaloChannel(cfg, b)
})
}

402
pkg/channels/zalo/zalo.go Normal file
View file

@ -0,0 +1,402 @@
package zalo
import (
"bytes"
"context"
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"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"
"github.com/sipeed/picoclaw/pkg/utils"
)
const zaloAPIBase = "https://bot-api.zaloplatforms.com/bot"
type ZaloChannel struct {
*channels.BaseChannel
cfg config.ZaloConfig
httpClient *http.Client
}
func NewZaloChannel(cfg *config.Config, messageBus *bus.MessageBus) (*ZaloChannel, error) {
zc := cfg.Channels.Zalo
if strings.TrimSpace(zc.Token) == "" {
return nil, fmt.Errorf("zalo token is required")
}
if strings.TrimSpace(zc.SecretToken) == "" {
return nil, fmt.Errorf("zalo secret_token is required")
}
base := channels.NewBaseChannel(
"zalo",
zc,
messageBus,
zc.AllowFrom,
channels.WithMaxMessageLength(2000),
channels.WithGroupTrigger(zc.GroupTrigger),
channels.WithReasoningChannelID(zc.ReasoningChannelID),
)
return &ZaloChannel{
BaseChannel: base,
cfg: zc,
httpClient: &http.Client{Timeout: 30 * time.Second},
}, nil
}
func (c *ZaloChannel) Start(ctx context.Context) error {
if _, err := c.getMe(ctx); err != nil {
logger.ErrorCF("zalo", "Failed to start channel", map[string]any{
"error": err.Error(),
})
return err
}
c.SetRunning(true)
logger.InfoC("zalo", "Zalo channel started successfully")
return nil
}
func (c *ZaloChannel) Stop(ctx context.Context) error {
c.SetRunning(false)
return nil
}
func (c *ZaloChannel) WebhookPath() string {
if p := strings.TrimSpace(c.cfg.WebhookPath); p != "" {
if strings.HasPrefix(p, "/") {
return p
}
return "/" + p
}
return "/webhook/zalo"
}
func (c *ZaloChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
logger.ErrorCF("zalo", "Failed to read request body", map[string]any{
"error": err.Error(),
})
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
secret := r.Header.Get("X-Bot-Api-Secret-Token")
if subtle.ConstantTimeCompare([]byte(secret), []byte(c.cfg.SecretToken)) != 1 {
logger.WarnC("zalo", "Invalid webhook secret token")
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
logger.ErrorCF("zalo", "Failed to parse webhook payload", map[string]any{
"error": err.Error(),
})
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
go func() {
c.processPayload(context.Background(), payload)
}()
}
func (c *ZaloChannel) processPayload(ctx context.Context, payload map[string]any) {
logger.DebugCF("zalo", "Processing webhook payload", map[string]any{
"has_result": payload["result"] != nil,
})
if v, ok := payload["result"]; ok {
if arr, ok := v.([]any); ok {
for _, item := range arr {
if m, ok := item.(map[string]any); ok {
c.processUpdate(ctx, m)
}
}
return
}
}
c.processUpdate(ctx, payload)
}
func (c *ZaloChannel) processUpdate(ctx context.Context, upd map[string]any) {
msgAny, _ := upd["message"]
msg, _ := msgAny.(map[string]any)
if msg == nil {
msg = upd
}
content := getString(msg, "text")
if strings.TrimSpace(content) == "" {
return
}
messageID := getString(msg, "message_id")
if messageID == "" {
messageID = getString(msg, "id")
}
chatID := getString(msg, "chat_id")
if chatID == "" {
chatID = getStringPath(msg, "chat", "id")
}
if chatID == "" {
return
}
senderID := getStringPath(msg, "from", "id")
if senderID == "" {
senderID = getString(msg, "from_id")
}
if senderID == "" {
return
}
peerKind := "direct"
if t := getStringPath(msg, "chat", "type"); t != "" {
if strings.Contains(strings.ToLower(t), "group") {
peerKind = "group"
}
}
if peerKind == "group" {
should, cleaned := c.ShouldRespondInGroup(false, content)
if !should {
return
}
content = cleaned
}
sender := bus.SenderInfo{
Platform: "zalo",
PlatformID: senderID,
CanonicalID: identity.BuildCanonicalID("zalo", senderID),
}
if !c.IsAllowedSender(sender) {
logger.DebugCF("zalo", "Message from disallowed sender", map[string]any{
"sender_id": senderID,
})
return
}
peer := bus.Peer{
Kind: peerKind,
ID: chatID,
}
logger.DebugCF("zalo", "Received message", map[string]any{
"sender_id": senderID,
"chat_id": chatID,
"peer_kind": peerKind,
"preview": utils.Truncate(content, 50),
})
c.HandleMessage(ctx, peer, messageID, senderID, chatID, content, nil, nil, sender)
}
func (c *ZaloChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
if !c.IsRunning() {
return channels.ErrNotRunning
}
return c.sendMessage(ctx, msg.ChatID, msg.Content)
}
func (c *ZaloChannel) StartTyping(ctx context.Context, chatID string) (func(), error) {
if !c.IsRunning() {
return func() {}, channels.ErrNotRunning
}
err := c.sendChatAction(ctx, chatID, "typing")
return func() {}, err
}
func (c *ZaloChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) error {
if !c.IsRunning() {
return channels.ErrNotRunning
}
for _, part := range msg.Parts {
if part.Type != "image" {
continue
}
ref := strings.TrimSpace(part.Ref)
if strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") {
if err := c.sendPhoto(ctx, msg.ChatID, ref, part.Caption); err != nil {
return err
}
continue
}
caption := strings.TrimSpace(part.Caption)
if caption == "" {
caption = part.Filename
}
if caption != "" {
if err := c.sendMessage(ctx, msg.ChatID, caption); err != nil {
return err
}
}
}
return nil
}
func (c *ZaloChannel) apiURL(method string) string {
return strings.TrimRight(zaloAPIBase, "/") + c.cfg.Token + "/" + strings.TrimLeft(method, "/")
}
func (c *ZaloChannel) doPOST(ctx context.Context, method string, payload any) (int, []byte, error) {
b, err := json.Marshal(payload)
if err != nil {
return 0, nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL(method), bytes.NewReader(b))
if err != nil {
return 0, nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return 0, nil, channels.ClassifyNetError(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, nil, channels.ClassifyNetError(err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return resp.StatusCode, data, channels.ClassifySendError(
resp.StatusCode,
fmt.Errorf("http %d", resp.StatusCode),
)
}
return resp.StatusCode, data, nil
}
func (c *ZaloChannel) getMe(ctx context.Context) (map[string]any, error) {
_, data, err := c.doPOST(ctx, "getMe", map[string]any{})
if err != nil {
logger.ErrorCF("zalo", "Failed to call getMe API", map[string]any{
"error": err.Error(),
})
return nil, err
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
logger.ErrorCF("zalo", "Failed to parse getMe response", map[string]any{
"error": err.Error(),
})
return nil, err
}
if ok, _ := out["ok"].(bool); !ok {
err := fmt.Errorf("zalo getMe returned ok=false")
logger.ErrorCF("zalo", "getMe API returned error", map[string]any{
"response": out,
})
return out, err
}
return out, nil
}
func (c *ZaloChannel) sendMessage(ctx context.Context, chatID, text string) error {
_, data, err := c.doPOST(ctx, "sendMessage", map[string]any{
"chat_id": chatID,
"text": text,
})
if err != nil {
logger.ErrorCF("zalo", "Failed to send message", map[string]any{
"chat_id": chatID,
"error": err.Error(),
})
return err
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
logger.ErrorCF("zalo", "Failed to parse sendMessage response", map[string]any{
"error": err.Error(),
})
return err
}
if ok, _ := out["ok"].(bool); !ok {
err := fmt.Errorf("zalo sendMessage returned ok=false")
logger.ErrorCF("zalo", "sendMessage API returned error", map[string]any{
"chat_id": chatID,
"response": out,
})
return err
}
return nil
}
func (c *ZaloChannel) sendPhoto(ctx context.Context, chatID, photoURL, caption string) error {
payload := map[string]any{
"chat_id": chatID,
"photo": photoURL,
}
if strings.TrimSpace(caption) != "" {
payload["caption"] = caption
}
_, _, err := c.doPOST(ctx, "sendPhoto", payload)
return err
}
func (c *ZaloChannel) sendChatAction(ctx context.Context, chatID, action string) error {
_, _, err := c.doPOST(ctx, "sendChatAction", map[string]any{
"chat_id": chatID,
"action": action,
})
return err
}
func getString(m map[string]any, key string) string {
if m == nil {
return ""
}
v, ok := m[key]
if !ok || v == nil {
return ""
}
switch t := v.(type) {
case string:
return t
case float64:
if t == float64(int64(t)) {
return fmt.Sprintf("%.0f", t)
}
return fmt.Sprintf("%v", t)
default:
return fmt.Sprintf("%v", t)
}
}
func getStringPath(m map[string]any, k1, k2 string) string {
v, ok := m[k1]
if !ok || v == nil {
return ""
}
m2, ok := v.(map[string]any)
if !ok {
return ""
}
return getString(m2, k2)
}

View file

@ -257,6 +257,7 @@ func (d *AgentDefaults) GetModelName() string {
type ChannelsConfig struct {
WhatsApp WhatsAppConfig `json:"whatsapp"`
Telegram TelegramConfig `json:"telegram"`
Zalo ZaloConfig `json:"zalo"`
Feishu FeishuConfig `json:"feishu"`
Discord DiscordConfig `json:"discord"`
MaixCam MaixCamConfig `json:"maixcam"`
@ -311,6 +312,16 @@ type TelegramConfig struct {
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
}
type ZaloConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ZALO_ENABLED"`
Token string `json:"token" env:"PICOCLAW_CHANNELS_ZALO_TOKEN"`
SecretToken string `json:"secret_token" env:"PICOCLAW_CHANNELS_ZALO_SECRET_TOKEN"`
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_ZALO_WEBHOOK_PATH"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ZALO_ALLOW_FROM"`
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ZALO_REASONING_CHANNEL_ID"`
}
type FeishuConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`

View file

@ -59,6 +59,13 @@ func DefaultConfig() *Config {
Text: "Thinking... 💭",
},
},
Zalo: ZaloConfig{
Enabled: false,
Token: "",
SecretToken: "",
WebhookPath: "/webhook/zalo",
AllowFrom: FlexibleStringSlice{},
},
Feishu: FeishuConfig{
Enabled: false,
AppID: "",