fix(zalo): address PR review comments
- Remove unrelated voice config from config.example.json - Add IsAllowedSender check in processUpdate - Add comprehensive logging throughout zalo.go - Fix context handling in ServeHTTP - Validate API response body in sendMessage - Complete Chinese documentation in README.zh.md
This commit is contained in:
parent
5d71c2d946
commit
7d4c009d0a
3 changed files with 126 additions and 9 deletions
60
README.zh.md
60
README.zh.md
|
|
@ -316,6 +316,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`(8–256 字符)。这是您自己的密钥,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 社交网络。
|
||||
|
|
|
|||
|
|
@ -485,9 +485,6 @@
|
|||
"enabled": false,
|
||||
"monitor_usb": true
|
||||
},
|
||||
"voice": {
|
||||
"echo_transcription": false
|
||||
},
|
||||
"gateway": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 18790
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"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"
|
||||
|
|
@ -53,9 +55,13 @@ func NewZaloChannel(cfg *config.Config, messageBus *bus.MessageBus) (*ZaloChanne
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -82,32 +88,40 @@ func (c *ZaloChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
c.processPayload(ctx, payload)
|
||||
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 {
|
||||
|
|
@ -175,11 +189,25 @@ func (c *ZaloChannel) processUpdate(ctx context.Context, upd map[string]any) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -268,24 +296,56 @@ func (c *ZaloChannel) doPOST(ctx context.Context, method string, payload any) (i
|
|||
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 {
|
||||
return out, fmt.Errorf("zalo getMe returned ok=false")
|
||||
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 {
|
||||
_, _, err := c.doPOST(ctx, "sendMessage", map[string]any{
|
||||
_, data, err := c.doPOST(ctx, "sendMessage", map[string]any{
|
||||
"chat_id": chatID,
|
||||
"text": text,
|
||||
})
|
||||
return err
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue