feat: add Zalo channel and docs

This commit is contained in:
trandangtrungduc 2026-03-11 15:33:09 +07:00
parent 9cd2d21800
commit 5d71c2d946
13 changed files with 659 additions and 6 deletions

View file

@ -279,11 +279,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) |
@ -323,6 +324,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

@ -248,11 +248,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** | 普通(アプリ認証情報) |
@ -291,6 +292,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

@ -321,13 +321,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) |
@ -376,6 +377,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

@ -273,11 +273,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) |
@ -317,6 +318,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

@ -247,11 +247,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) |
@ -291,6 +292,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

@ -297,6 +297,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) |

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

@ -59,6 +59,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",
@ -477,6 +485,9 @@
"enabled": false,
"monitor_usb": true
},
"voice": {
"echo_transcription": false
},
"gateway": {
"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,
@ -213,6 +214,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)
})
}

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

@ -0,0 +1,342 @@
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"
)
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 {
return err
}
c.SetRunning(true)
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 {
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 {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
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)
}()
}
func (c *ZaloChannel) processPayload(ctx context.Context, payload map[string]any) {
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),
}
peer := bus.Peer{
Kind: peerKind,
ID: chatID,
}
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 {
return nil, err
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
return nil, err
}
if ok, _ := out["ok"].(bool); !ok {
return out, fmt.Errorf("zalo getMe returned ok=false")
}
return out, nil
}
func (c *ZaloChannel) sendMessage(ctx context.Context, chatID, text string) error {
_, _, err := c.doPOST(ctx, "sendMessage", map[string]any{
"chat_id": chatID,
"text": text,
})
return err
}
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

@ -230,6 +230,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"`
@ -284,6 +285,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: "",