@ardennguyen/zalo-agent-cli 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +27 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +64 -0
- package/src/cli.test.js +98 -0
- package/src/commands/account.js +178 -0
- package/src/commands/auto-reply.js +83 -0
- package/src/commands/catalog.js +143 -0
- package/src/commands/conv.js +264 -0
- package/src/commands/friend.js +384 -0
- package/src/commands/group.js +606 -0
- package/src/commands/label.js +39 -0
- package/src/commands/listen.js +293 -0
- package/src/commands/login.js +165 -0
- package/src/commands/mcp.js +219 -0
- package/src/commands/msg.js +608 -0
- package/src/commands/oa-init.js +503 -0
- package/src/commands/oa-listen.js +215 -0
- package/src/commands/oa.js +657 -0
- package/src/commands/poll.js +159 -0
- package/src/commands/profile.js +274 -0
- package/src/commands/quick-msg.js +55 -0
- package/src/commands/reminder.js +195 -0
- package/src/core/accounts.js +93 -0
- package/src/core/credentials.js +63 -0
- package/src/core/oa-client.js +407 -0
- package/src/core/zalo-client.js +201 -0
- package/src/index.js +95 -0
- package/src/mcp/mcp-config.js +81 -0
- package/src/mcp/mcp-http-transport.js +84 -0
- package/src/mcp/mcp-server.js +40 -0
- package/src/mcp/mcp-tools.js +353 -0
- package/src/mcp/media-downloader.js +191 -0
- package/src/mcp/media-downloader.test.js +45 -0
- package/src/mcp/message-buffer.js +127 -0
- package/src/mcp/message-buffer.test.js +308 -0
- package/src/mcp/notifier.js +141 -0
- package/src/mcp/notifier.test.js +269 -0
- package/src/mcp/thread-filter.js +79 -0
- package/src/mcp/thread-filter.test.js +206 -0
- package/src/mcp/thread-name-cache.js +162 -0
- package/src/mcp/thread-name-cache.test.js +139 -0
- package/src/utils/bank-helpers.js +233 -0
- package/src/utils/bank-helpers.test.js +66 -0
- package/src/utils/extract-message-text.js +21 -0
- package/src/utils/output.js +21 -0
- package/src/utils/proxy-helpers.js +14 -0
- package/src/utils/proxy-helpers.test.js +36 -0
- package/src/utils/qr-display.js +91 -0
- package/src/utils/qr-display.test.js +97 -0
- package/src/utils/qr-http-server.js +190 -0
- package/src/utils/update-check.js +56 -0
package/DISCLAIMER.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Disclaimer
|
|
2
|
+
|
|
3
|
+
## ⚠️ Cảnh báo quan trọng / Important Warning
|
|
4
|
+
|
|
5
|
+
**Tool này sử dụng [zca-js](https://github.com/AKAspanion/zca-js) — một thư viện gọi API Zalo không chính thức. Zalo không hỗ trợ và tài khoản của bạn có thể bị khóa (ban) khi sử dụng.**
|
|
6
|
+
|
|
7
|
+
**This tool uses [zca-js](https://github.com/AKAspanion/zca-js) — an unofficial Zalo API library. Zalo does not support this and your account may be banned.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
This is an **unofficial** project and is **not affiliated with, endorsed by, or connected to Zalo or VNG Corporation** in any way.
|
|
12
|
+
|
|
13
|
+
This tool is provided for **research, educational, and personal automation purposes only**. By using this tool, you acknowledge and accept that:
|
|
14
|
+
|
|
15
|
+
- **Your Zalo account may be permanently banned or restricted** — Zalo actively detects and blocks unofficial API usage
|
|
16
|
+
- This tool relies on **reverse-engineered, undocumented APIs** via the zca-js library, which can break at any time
|
|
17
|
+
- There is **no guarantee of stability, compatibility, or continued functionality**
|
|
18
|
+
|
|
19
|
+
Users are solely responsible for ensuring their use of this tool complies with:
|
|
20
|
+
|
|
21
|
+
- Zalo's Terms of Service
|
|
22
|
+
- Applicable local and international laws and regulations
|
|
23
|
+
- Data privacy requirements
|
|
24
|
+
|
|
25
|
+
The authors and contributors of this project accept **no responsibility or liability** for any misuse, account restrictions, bans, or legal consequences arising from the use of this software.
|
|
26
|
+
|
|
27
|
+
**Sử dụng trên tinh thần tự chịu trách nhiệm. Use at your own risk.**
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/mascot.png" width="120" alt="zalo-agent-cli mascot" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# zalo-agent-cli
|
|
6
|
+
|
|
7
|
+
Công cụ CLI tự động hóa Zalo — đa tài khoản, proxy, chuyển khoản ngân hàng, thanh toán QR.
|
|
8
|
+
Xây dựng trên [zca-js](https://github.com/RFS-ADRENO/zca-js).
|
|
9
|
+
|
|
10
|
+
**[Tiếng Việt](#bắt-đầu-nhanh)** | **[English](#english)**
|
|
11
|
+
|
|
12
|
+
> [!WARNING]
|
|
13
|
+
> Tool này sử dụng API Zalo **không chính thức** ([zca-js](https://github.com/RFS-ADRENO/zca-js)). Zalo không hỗ trợ và **tài khoản của bạn có thể bị khóa hoặc ban**. Tự chịu trách nhiệm. Không liên kết với Zalo hay VNG. Xem [DISCLAIMER.md](DISCLAIMER.md).
|
|
14
|
+
|
|
15
|
+
> [!TIP]
|
|
16
|
+
> **AI Agent Skill** — Dùng với OpenClaw, Claude Code, hoặc bất kỳ agent nào hỗ trợ SKILL.md:
|
|
17
|
+
> ```bash
|
|
18
|
+
> clawhub install zalo-agent # OpenClaw (từ ClawHub registry)
|
|
19
|
+
> cp -r skill/ ~/.claude/skills/zalo-agent/ # Claude Code
|
|
20
|
+
> ```
|
|
21
|
+
> 15+ nhóm lệnh · listen mode + webhook · 55+ ngân hàng VN · đa tài khoản + proxy
|
|
22
|
+
> Xem [skill/SKILL.md](skill/SKILL.md) · [Eval scenarios](skill/evals/)
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> **Zalo Official Account (OA)** — Zalo OA API v3.0 chính thức:
|
|
26
|
+
> ```bash
|
|
27
|
+
> zalo-agent oa init # Setup wizard (interactive)
|
|
28
|
+
> zalo-agent oa init --app-id <ID> --secret <KEY> --skip-webhook # Non-interactive (AI agent)
|
|
29
|
+
> zalo-agent oa whoami # Xem thông tin OA
|
|
30
|
+
> zalo-agent oa msg text <user-id> "Xin chào" # Gửi tin nhắn
|
|
31
|
+
> zalo-agent oa listen -p 3000 # Webhook listener
|
|
32
|
+
> ```
|
|
33
|
+
> OAuth login · gửi tin nhắn · quản lý follower · tag · webhook listener · VPS support
|
|
34
|
+
> Xem [docs/official-account.md](docs/official-account.md)
|
|
35
|
+
|
|
36
|
+
> [!TIP]
|
|
37
|
+
> **MCP Server (AI Agent Integration)** — Model Context Protocol cho Claude Code và các MCP client:
|
|
38
|
+
> ```bash
|
|
39
|
+
> zalo-agent mcp start # stdio (local Claude Code)
|
|
40
|
+
> zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
|
|
41
|
+
> ```
|
|
42
|
+
> 4 tools: get_messages · send_message · list_threads · mark_read
|
|
43
|
+
> Auto-reconnect · thread filter · noise reduction · group notifications
|
|
44
|
+
> Xem [MCP Guide](skill/references/mcp-guide.md)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Cài đặt
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g @ardennguyen/zalo-agent-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Bắt đầu nhanh
|
|
55
|
+
|
|
56
|
+
### 1. Đăng nhập
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
zalo-agent login
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Quét QR bằng **Zalo app > Quét mã QR**. Thông tin tự động lưu.
|
|
63
|
+
|
|
64
|
+
### 2. Tìm bạn bè
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
zalo-agent friend search "Phúc"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 3. Lắng nghe tin nhắn (lấy thread ID)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
zalo-agent listen
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Mỗi tin nhắn đến sẽ hiện `threadId`. Dùng `--json` để lấy dạng JSON.
|
|
77
|
+
|
|
78
|
+
### 4. Gửi tin nhắn
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Gửi cho cá nhân
|
|
82
|
+
zalo-agent msg send <THREAD_ID> "Xin chào!"
|
|
83
|
+
|
|
84
|
+
# Gửi vào nhóm
|
|
85
|
+
zalo-agent msg send <THREAD_ID> "Xin chào nhóm!" -t 1
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Danh sách lệnh
|
|
91
|
+
|
|
92
|
+
Tất cả lệnh hỗ trợ `--json`. Tài liệu đầy đủ: **[Wiki](https://github.com/ardennguyen/zalo-agent-cli/wiki)**
|
|
93
|
+
|
|
94
|
+
| Nhóm lệnh | Mô tả | Docs |
|
|
95
|
+
|------------|--------|------|
|
|
96
|
+
| `msg` | Gửi tin nhắn, hình, file, voice, video, sticker, link, thẻ chuyển khoản, QR | [Tin nhắn](https://github.com/ardennguyen/zalo-agent-cli/wiki/Tin-Nh%E1%BA%AFn) |
|
|
97
|
+
| `friend` | Danh sách, tìm, thêm, xóa, chặn, biệt danh, gợi ý | [Bạn bè](https://github.com/ardennguyen/zalo-agent-cli/wiki/B%E1%BA%A1n-B%C3%A8) |
|
|
98
|
+
| `group` | Tạo, đổi tên, thành viên, cài đặt, link, ghi chú, lời mời | [Nhóm & Cộng đồng](https://github.com/ardennguyen/zalo-agent-cli/wiki/Nh%C3%B3m) |
|
|
99
|
+
| `conv` | Tắt thông báo, ghim, lưu trữ, ẩn hội thoại, tự xóa | [Hội thoại](https://github.com/ardennguyen/zalo-agent-cli/wiki/H%E1%BB%99i-Tho%E1%BA%A1i) |
|
|
100
|
+
| `profile` | Xem/cập nhật hồ sơ, ảnh đại diện, quyền riêng tư | [Hồ sơ](https://github.com/ardennguyen/zalo-agent-cli/wiki/H%E1%BB%93-S%C6%A1) |
|
|
101
|
+
| `poll` | Tạo, bỏ phiếu, đóng khảo sát | [Khảo sát](https://github.com/ardennguyen/zalo-agent-cli/wiki/Kh%E1%BA%A3o-S%C3%A1t) |
|
|
102
|
+
| `reminder` | Tạo, sửa, xóa nhắc nhở | [Nhắc nhở](https://github.com/ardennguyen/zalo-agent-cli/wiki/Nh%E1%BA%AFc-Nh%E1%BB%9F) |
|
|
103
|
+
| `auto-reply` | Quản lý trả lời tự động | [Trả lời tự động](https://github.com/ardennguyen/zalo-agent-cli/wiki/Tr%E1%BA%A3-L%E1%BB%9Di-T%E1%BB%B1-%C4%90%E1%BB%99ng) |
|
|
104
|
+
| `quick-msg` | Tin nhắn nhanh đã lưu | [Tin nhắn nhanh](https://github.com/ardennguyen/zalo-agent-cli/wiki/Tin-Nh%E1%BA%AFn-Nhanh) |
|
|
105
|
+
| `label` | Nhãn hội thoại | [Nhãn](https://github.com/ardennguyen/zalo-agent-cli/wiki/Nh%C3%A3n) |
|
|
106
|
+
| `catalog` | zBusiness — danh mục sản phẩm | [zBusiness](https://github.com/ardennguyen/zalo-agent-cli/wiki/zBusiness) |
|
|
107
|
+
| `listen` | Lắng nghe tin nhắn real-time, webhook, lưu JSONL | [Lắng nghe](https://github.com/ardennguyen/zalo-agent-cli/wiki/L%E1%BA%AFng-Nghe) |
|
|
108
|
+
| `account` | Đa tài khoản & proxy | [Tài khoản](https://github.com/ardennguyen/zalo-agent-cli/wiki/T%C3%A0i-Kho%E1%BA%A3n) |
|
|
109
|
+
| **`oa`** | **Zalo Official Account API v3.0 — OAuth, tin nhắn, follower, tag, webhook** | **[Official Account](https://github.com/ardennguyen/zalo-agent-cli/wiki/Official-Account)** |
|
|
110
|
+
|
|
111
|
+
Xem thêm: [Đa tài khoản & Proxy](https://github.com/ardennguyen/zalo-agent-cli/wiki/%C4%90a-T%C3%A0i-Kho%E1%BA%A3n-&-Proxy) · [Cài đặt VPS](https://github.com/ardennguyen/zalo-agent-cli/wiki/C%C3%A0i-%C4%90%E1%BA%B7t-VPS) · [Thẻ chuyển khoản & QR](https://github.com/ardennguyen/zalo-agent-cli/wiki/Th%E1%BA%BB-Chuy%E1%BB%83n-Kho%E1%BA%A3n-&-QR) · [Official Account](https://github.com/ardennguyen/zalo-agent-cli/wiki/Official-Account)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Tính năng
|
|
116
|
+
|
|
117
|
+
- Đăng nhập QR qua HTTP server tự động (browser + terminal)
|
|
118
|
+
- Đa tài khoản với proxy riêng biệt (1:1)
|
|
119
|
+
- 90+ lệnh phủ hết tính năng Zalo
|
|
120
|
+
- **Zalo Official Account (OA) API v3.0** — OAuth login, gửi tin nhắn, quản lý follower, webhook listener
|
|
121
|
+
- Thẻ chuyển khoản (55+ ngân hàng VN) & QR VietQR
|
|
122
|
+
- Lắng nghe real-time với webhook & lưu JSONL local
|
|
123
|
+
- Output `--json` cho mọi lệnh — scripting & AI agents
|
|
124
|
+
- Credentials mã hóa tại chỗ (quyền 0600)
|
|
125
|
+
- **Dual mode**: interactive (human) + non-interactive (AI agents, CI/CD)
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## English
|
|
134
|
+
|
|
135
|
+
CLI tool for Zalo automation — multi-account, proxy support, bank transfers, QR payments.
|
|
136
|
+
|
|
137
|
+
> [!TIP]
|
|
138
|
+
> **AI Agent Skill** — Use with OpenClaw, Claude Code, or any SKILL.md-compatible agent:
|
|
139
|
+
> ```bash
|
|
140
|
+
> clawhub install zalo-agent # OpenClaw (from ClawHub registry)
|
|
141
|
+
> cp -r skill/ ~/.claude/skills/zalo-agent/ # Claude Code
|
|
142
|
+
> ```
|
|
143
|
+
> 15+ command groups · listen mode + webhook · 55+ VN banks · multi-account + proxy
|
|
144
|
+
> See [skill/SKILL.md](skill/SKILL.md) · [Eval scenarios](skill/evals/)
|
|
145
|
+
|
|
146
|
+
> [!NOTE]
|
|
147
|
+
> **Zalo Official Account (OA)** — Official Zalo OA API v3.0:
|
|
148
|
+
> ```bash
|
|
149
|
+
> zalo-agent oa init # Setup wizard (interactive)
|
|
150
|
+
> zalo-agent oa init --app-id <ID> --secret <KEY> --skip-webhook # Non-interactive (AI agent)
|
|
151
|
+
> zalo-agent oa whoami # OA profile
|
|
152
|
+
> zalo-agent oa msg text <user-id> "Hello" # Send message
|
|
153
|
+
> zalo-agent oa listen -p 3000 # Webhook listener
|
|
154
|
+
> ```
|
|
155
|
+
> OAuth login · messaging · follower management · tags · webhook listener · VPS support
|
|
156
|
+
> See [docs/official-account.md](docs/official-account.md)
|
|
157
|
+
|
|
158
|
+
> [!TIP]
|
|
159
|
+
> **MCP Server (AI Agent Integration)** — Model Context Protocol support for Claude Code and MCP clients:
|
|
160
|
+
> ```bash
|
|
161
|
+
> zalo-agent mcp start # stdio (local Claude Code)
|
|
162
|
+
> zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
|
|
163
|
+
> ```
|
|
164
|
+
> 4 tools: get_messages · send_message · list_threads · mark_read
|
|
165
|
+
> Auto-reconnect · thread filter · noise reduction · group notifications
|
|
166
|
+
> See [MCP Guide](skill/references/mcp-guide.md)
|
|
167
|
+
|
|
168
|
+
### Quick Start
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm install -g @ardennguyen/zalo-agent-cli
|
|
172
|
+
zalo-agent login # 1. Login via QR
|
|
173
|
+
zalo-agent friend search "Name" # 2. Find a friend
|
|
174
|
+
zalo-agent listen # 3. Listen for threadId
|
|
175
|
+
zalo-agent msg send <THREAD_ID> "Hello!" # 4. Send a message
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Commands
|
|
179
|
+
|
|
180
|
+
Full docs: **[Wiki](https://github.com/ardennguyen/zalo-agent-cli/wiki)**
|
|
181
|
+
|
|
182
|
+
| Group | Description | Docs |
|
|
183
|
+
|-------|-------------|------|
|
|
184
|
+
| `msg` | Text, images, files, voice, video, stickers, links, bank cards, QR | [Messages](https://github.com/ardennguyen/zalo-agent-cli/wiki/Messages) |
|
|
185
|
+
| `friend` | List, find, add, remove, block, alias, recommendations | [Friends](https://github.com/ardennguyen/zalo-agent-cli/wiki/Friends) |
|
|
186
|
+
| `group` | Create, rename, members, settings, links, notes, invites | [Groups](https://github.com/ardennguyen/zalo-agent-cli/wiki/Groups) |
|
|
187
|
+
| `conv` | Mute, pin, archive, hidden, auto-delete | [Conversations](https://github.com/ardennguyen/zalo-agent-cli/wiki/Conversations) |
|
|
188
|
+
| `profile` | Profile, avatar gallery, privacy | [Profile](https://github.com/ardennguyen/zalo-agent-cli/wiki/Profile) |
|
|
189
|
+
| `poll` | Create, vote, lock polls | [Polls](https://github.com/ardennguyen/zalo-agent-cli/wiki/Polls) |
|
|
190
|
+
| `reminder` | Create, edit, remove reminders | [Reminders](https://github.com/ardennguyen/zalo-agent-cli/wiki/Reminders) |
|
|
191
|
+
| `auto-reply` | Auto-reply rules | [Auto-Reply](https://github.com/ardennguyen/zalo-agent-cli/wiki/Auto-Reply) |
|
|
192
|
+
| `quick-msg` | Saved quick messages | [Quick Messages](https://github.com/ardennguyen/zalo-agent-cli/wiki/Quick-Messages) |
|
|
193
|
+
| `label` | Conversation labels | [Labels](https://github.com/ardennguyen/zalo-agent-cli/wiki/Labels) |
|
|
194
|
+
| `catalog` | zBusiness catalogs & products | [Catalog](https://github.com/ardennguyen/zalo-agent-cli/wiki/Catalog) |
|
|
195
|
+
| `listen` | Real-time listener, webhook, JSONL | [Listener](https://github.com/ardennguyen/zalo-agent-cli/wiki/Listener) |
|
|
196
|
+
| `account` | Multi-account & proxy | [Accounts](https://github.com/ardennguyen/zalo-agent-cli/wiki/Accounts) |
|
|
197
|
+
| **`oa`** | **Zalo Official Account API v3.0 — OAuth, messaging, followers, webhook** | **[Official Account](https://github.com/ardennguyen/zalo-agent-cli/wiki/Official-Account)** |
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[MIT](LICENSE) · See [DISCLAIMER.md](DISCLAIMER.md) for full terms.
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ardennguyen/zalo-agent-cli",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "CLI tool for Zalo automation — multi-account, proxy, bank transfers, QR payments, Official Account API v3.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zalo-agent": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"DISCLAIMER.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test src/*.test.js src/**/*.test.js",
|
|
17
|
+
"lint": "eslint src/",
|
|
18
|
+
"lint:fix": "eslint src/ --fix",
|
|
19
|
+
"format": "prettier --write src/",
|
|
20
|
+
"format:check": "prettier --check src/",
|
|
21
|
+
"release": "npm version patch -m 'release: v%s' && git push origin main --tags",
|
|
22
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"zalo",
|
|
26
|
+
"cli",
|
|
27
|
+
"automation",
|
|
28
|
+
"messaging",
|
|
29
|
+
"vietnam",
|
|
30
|
+
"proxy",
|
|
31
|
+
"vietqr",
|
|
32
|
+
"zca-js",
|
|
33
|
+
"official-account",
|
|
34
|
+
"zalo-oa",
|
|
35
|
+
"webhook"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/ardennguyen/zalo-agent-cli"
|
|
40
|
+
},
|
|
41
|
+
"author": "ardennguyen",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
|
+
"chalk": "^5.6.2",
|
|
52
|
+
"commander": "^15.0.0",
|
|
53
|
+
"express": "^5.2.1",
|
|
54
|
+
"https-proxy-agent": "^9.1.0",
|
|
55
|
+
"node-fetch": "^3.3.2",
|
|
56
|
+
"undici": "^8.5.0",
|
|
57
|
+
"zca-js": "^2.1.2",
|
|
58
|
+
"zod": "^4.4.3"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"eslint": "^10.0.3",
|
|
62
|
+
"prettier": "^3.8.1"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cli.test.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI interface tests — verify command parsing, help output, and basic behavior
|
|
3
|
+
* without requiring a Zalo session.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
|
|
11
|
+
const CLI = resolve(import.meta.dirname, "index.js");
|
|
12
|
+
|
|
13
|
+
function run(...args) {
|
|
14
|
+
return execFileSync("node", [CLI, ...args], {
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
timeout: 10000,
|
|
17
|
+
env: { ...process.env, HOME: "/tmp/zalo-agent-cli-test-home" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("CLI interface", () => {
|
|
22
|
+
it("--version outputs 1.0.0", () => {
|
|
23
|
+
const out = run("--version");
|
|
24
|
+
assert.match(out.trim(), /^\d+\.\d+\.\d+$/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("--help lists all command groups", () => {
|
|
28
|
+
const out = run("--help");
|
|
29
|
+
assert.match(out, /login/);
|
|
30
|
+
assert.match(out, /msg/);
|
|
31
|
+
assert.match(out, /friend/);
|
|
32
|
+
assert.match(out, /group/);
|
|
33
|
+
assert.match(out, /conv/);
|
|
34
|
+
assert.match(out, /account/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("msg --help lists all subcommands", () => {
|
|
38
|
+
const out = run("msg", "--help");
|
|
39
|
+
assert.match(out, /send /);
|
|
40
|
+
assert.match(out, /send-image/);
|
|
41
|
+
assert.match(out, /send-file/);
|
|
42
|
+
assert.match(out, /send-card/);
|
|
43
|
+
assert.match(out, /send-bank/);
|
|
44
|
+
assert.match(out, /send-qr-transfer/);
|
|
45
|
+
assert.match(out, /sticker/);
|
|
46
|
+
assert.match(out, /react/);
|
|
47
|
+
assert.match(out, /delete/);
|
|
48
|
+
assert.match(out, /forward/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("friend --help lists subcommands", () => {
|
|
52
|
+
const out = run("friend", "--help");
|
|
53
|
+
assert.match(out, /list/);
|
|
54
|
+
assert.match(out, /find/);
|
|
55
|
+
assert.match(out, /info/);
|
|
56
|
+
assert.match(out, /block/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("group --help lists subcommands", () => {
|
|
60
|
+
const out = run("group", "--help");
|
|
61
|
+
assert.match(out, /create/);
|
|
62
|
+
assert.match(out, /members/);
|
|
63
|
+
assert.match(out, /rename/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("conv --help lists subcommands", () => {
|
|
67
|
+
const out = run("conv", "--help");
|
|
68
|
+
assert.match(out, /mute/);
|
|
69
|
+
assert.match(out, /pinned/);
|
|
70
|
+
assert.match(out, /archived/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("account --help lists subcommands", () => {
|
|
74
|
+
const out = run("account", "--help");
|
|
75
|
+
assert.match(out, /login/);
|
|
76
|
+
assert.match(out, /switch/);
|
|
77
|
+
assert.match(out, /export/);
|
|
78
|
+
assert.match(out, /remove/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("login --help shows all flags", () => {
|
|
82
|
+
const out = run("login", "--help");
|
|
83
|
+
assert.match(out, /--proxy/);
|
|
84
|
+
assert.match(out, /--credentials/);
|
|
85
|
+
assert.match(out, /--qr-url/);
|
|
86
|
+
assert.match(out, /--qr-port/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("logout --help shows --purge", () => {
|
|
90
|
+
const out = run("logout", "--help");
|
|
91
|
+
assert.match(out, /--purge/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("account list on clean state shows no accounts", () => {
|
|
95
|
+
const out = run("account", "list");
|
|
96
|
+
assert.match(out, /No accounts/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account commands — multi-account management with per-account proxy.
|
|
3
|
+
* Includes export for headless/CI credential transfer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, chmodSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import { loginWithQR, loginWithCredentials, extractCredentials, clearSession } from "../core/zalo-client.js";
|
|
9
|
+
import { saveCredentials, loadCredentials } from "../core/credentials.js";
|
|
10
|
+
import { listAccounts, getActive, setActive, addAccount, removeAccount, getAccount } from "../core/accounts.js";
|
|
11
|
+
import { maskProxy } from "../utils/proxy-helpers.js";
|
|
12
|
+
import { displayQR, getQRPath } from "../utils/qr-display.js";
|
|
13
|
+
import { startQrServer } from "../utils/qr-http-server.js";
|
|
14
|
+
import { success, error, info, warning, output } from "../utils/output.js";
|
|
15
|
+
|
|
16
|
+
export function registerAccountCommands(program) {
|
|
17
|
+
const account = program.command("account").description("Manage multiple Zalo accounts with proxy");
|
|
18
|
+
|
|
19
|
+
account
|
|
20
|
+
.command("list")
|
|
21
|
+
.description("List all registered accounts")
|
|
22
|
+
.action(() => {
|
|
23
|
+
const accounts = listAccounts();
|
|
24
|
+
const safe = accounts.map((a) => ({ ...a, proxy: maskProxy(a.proxy) }));
|
|
25
|
+
output(safe, program.opts().json, () => {
|
|
26
|
+
if (!accounts.length) {
|
|
27
|
+
info("No accounts. Use: zalo-agent account login");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(` ${"Active".padEnd(8)} ${"Owner ID".padEnd(22)} ${"Name".padEnd(20)} Proxy`);
|
|
31
|
+
console.log(` ${"─".repeat(75)}`);
|
|
32
|
+
for (const a of accounts) {
|
|
33
|
+
const marker = a.active ? " ★" : " ";
|
|
34
|
+
console.log(
|
|
35
|
+
` ${marker.padEnd(8)} ${a.ownId.padEnd(22)} ${(a.name || "").padEnd(20)} ${maskProxy(a.proxy)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
account
|
|
42
|
+
.command("login")
|
|
43
|
+
.description("Login a new Zalo account via QR code")
|
|
44
|
+
.option("-p, --proxy <url>", "Dedicated proxy URL for this account")
|
|
45
|
+
.option("-n, --name <label>", "Friendly label", "")
|
|
46
|
+
.option("--qr-url", "Start local HTTP server to view QR in browser (for VPS/headless)")
|
|
47
|
+
.action(async (opts) => {
|
|
48
|
+
if (opts.proxy) info(`Using proxy: ${maskProxy(opts.proxy)}`);
|
|
49
|
+
info("Generating QR code... Scan with Zalo mobile app.");
|
|
50
|
+
|
|
51
|
+
let qrServer = null;
|
|
52
|
+
try {
|
|
53
|
+
const { ownId } = await loginWithQR(opts.proxy, (event) => {
|
|
54
|
+
displayQR(event);
|
|
55
|
+
if (!qrServer) {
|
|
56
|
+
qrServer = startQrServer(getQRPath());
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Fetch display name from Zalo profile
|
|
61
|
+
let displayName = opts.name || "";
|
|
62
|
+
try {
|
|
63
|
+
const { getApi } = await import("../core/zalo-client.js");
|
|
64
|
+
const accountInfo = await getApi().fetchAccountInfo();
|
|
65
|
+
displayName = accountInfo?.profile?.displayName || displayName || ownId;
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
const creds = extractCredentials();
|
|
69
|
+
saveCredentials(ownId, creds);
|
|
70
|
+
addAccount(ownId, displayName, opts.proxy);
|
|
71
|
+
success(
|
|
72
|
+
`Account logged in: ${displayName} (${ownId})${opts.proxy ? ` via ${maskProxy(opts.proxy)}` : ""}`,
|
|
73
|
+
);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error(`Login failed: ${e.message}`);
|
|
76
|
+
} finally {
|
|
77
|
+
if (qrServer) qrServer.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
account
|
|
82
|
+
.command("switch <ownerId>")
|
|
83
|
+
.description("Switch active account (restarts connection with account proxy)")
|
|
84
|
+
.action(async (ownerId) => {
|
|
85
|
+
let acc = getAccount(ownerId);
|
|
86
|
+
if (!acc) {
|
|
87
|
+
const all = listAccounts();
|
|
88
|
+
const matches = all.filter((a) => a.ownId.includes(ownerId) || (a.name || "").includes(ownerId));
|
|
89
|
+
if (matches.length === 1) {
|
|
90
|
+
ownerId = matches[0].ownId;
|
|
91
|
+
acc = matches[0];
|
|
92
|
+
} else {
|
|
93
|
+
error(`Account not found: ${ownerId}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const creds = loadCredentials(ownerId);
|
|
99
|
+
if (!creds) {
|
|
100
|
+
error(`No credentials for ${ownerId}. Re-login needed.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
info(`Switching to ${ownerId} (${acc?.name || ""})`);
|
|
105
|
+
if (acc?.proxy) info(`Proxy: ${maskProxy(acc.proxy)}`);
|
|
106
|
+
|
|
107
|
+
clearSession();
|
|
108
|
+
try {
|
|
109
|
+
await loginWithCredentials(creds, acc?.proxy || null);
|
|
110
|
+
setActive(ownerId);
|
|
111
|
+
success(`Switched to ${ownerId}`);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
error(`Switch failed: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
account
|
|
118
|
+
.command("remove <ownerId>")
|
|
119
|
+
.description("Remove account and delete its credentials")
|
|
120
|
+
.action((ownerId) => {
|
|
121
|
+
if (removeAccount(ownerId)) {
|
|
122
|
+
success(`Account ${ownerId} removed`);
|
|
123
|
+
} else {
|
|
124
|
+
error(`Account not found: ${ownerId}`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
account
|
|
129
|
+
.command("info")
|
|
130
|
+
.description("Show currently active account")
|
|
131
|
+
.action(() => {
|
|
132
|
+
const active = getActive();
|
|
133
|
+
const safe = active ? { ...active, proxy: maskProxy(active.proxy) } : null;
|
|
134
|
+
output(safe, program.opts().json, () => {
|
|
135
|
+
if (!active) {
|
|
136
|
+
info("No active account.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(` Owner ID: ${active.ownId}`);
|
|
140
|
+
console.log(` Name: ${active.name || "-"}`);
|
|
141
|
+
console.log(` Proxy: ${maskProxy(active.proxy)}`);
|
|
142
|
+
console.log(` Active: yes`);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
account
|
|
147
|
+
.command("export [ownerId]")
|
|
148
|
+
.description("Export account credentials for transfer to another machine")
|
|
149
|
+
.option("-o, --output <path>", "Output file path", "./zalo-creds.json")
|
|
150
|
+
.action((ownerId, opts) => {
|
|
151
|
+
const acc = ownerId ? getAccount(ownerId) : getActive();
|
|
152
|
+
if (!acc) {
|
|
153
|
+
error("No account found to export.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const creds = loadCredentials(acc.ownId);
|
|
158
|
+
if (!creds) {
|
|
159
|
+
error(`No credentials for ${acc.ownId}.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const exportData = {
|
|
164
|
+
...creds,
|
|
165
|
+
proxy: acc.proxy || null,
|
|
166
|
+
ownId: acc.ownId,
|
|
167
|
+
name: acc.name || "",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const outPath = resolve(opts.output);
|
|
171
|
+
writeFileSync(outPath, JSON.stringify(exportData, null, 2), "utf-8");
|
|
172
|
+
chmodSync(outPath, 0o600);
|
|
173
|
+
|
|
174
|
+
success(`Exported to ${outPath}`);
|
|
175
|
+
warning("This file contains login credentials. Keep it secure and do not commit to git.");
|
|
176
|
+
info(`Import on another machine: zalo-agent login --credentials ${opts.output}`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-reply commands — list, create, update, delete auto-reply messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerAutoReplyCommands(program) {
|
|
9
|
+
const ar = program.command("auto-reply").description("Manage auto-reply messages");
|
|
10
|
+
|
|
11
|
+
ar.command("list")
|
|
12
|
+
.description("List all auto-reply rules")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getApi().getAutoReplyList();
|
|
16
|
+
output(result, program.opts().json);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error(`Get auto-reply list failed: ${e.message}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
ar.command("create <content>")
|
|
23
|
+
.description("Create an auto-reply rule")
|
|
24
|
+
.option("--enable", "Enable the rule (default: true)", true)
|
|
25
|
+
.option("--no-enable", "Create disabled")
|
|
26
|
+
.option("--start <ms>", "Start time (epoch ms)", parseInt, 0)
|
|
27
|
+
.option("--end <ms>", "End time (epoch ms)", parseInt, 0)
|
|
28
|
+
.option("--scope <n>", "Scope: 0=all, 1=friends, 2=strangers", parseInt, 0)
|
|
29
|
+
.option("--uids <ids...>", "Specific user IDs to auto-reply to")
|
|
30
|
+
.action(async (content, opts) => {
|
|
31
|
+
try {
|
|
32
|
+
const payload = {
|
|
33
|
+
content,
|
|
34
|
+
isEnable: opts.enable,
|
|
35
|
+
startTime: opts.start,
|
|
36
|
+
endTime: opts.end,
|
|
37
|
+
scope: opts.scope,
|
|
38
|
+
};
|
|
39
|
+
if (opts.uids) payload.uids = opts.uids;
|
|
40
|
+
const result = await getApi().createAutoReply(payload);
|
|
41
|
+
output(result, program.opts().json, () => success("Auto-reply created"));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
error(`Create auto-reply failed: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
ar.command("update <id> <content>")
|
|
48
|
+
.description("Update an auto-reply rule")
|
|
49
|
+
.option("--enable", "Enable the rule")
|
|
50
|
+
.option("--no-enable", "Disable the rule")
|
|
51
|
+
.option("--start <ms>", "Start time (epoch ms)", parseInt, 0)
|
|
52
|
+
.option("--end <ms>", "End time (epoch ms)", parseInt, 0)
|
|
53
|
+
.option("--scope <n>", "Scope: 0=all, 1=friends, 2=strangers", parseInt, 0)
|
|
54
|
+
.option("--uids <ids...>", "Specific user IDs")
|
|
55
|
+
.action(async (id, content, opts) => {
|
|
56
|
+
try {
|
|
57
|
+
const payload = {
|
|
58
|
+
id: Number(id),
|
|
59
|
+
content,
|
|
60
|
+
isEnable: opts.enable ?? true,
|
|
61
|
+
startTime: opts.start,
|
|
62
|
+
endTime: opts.end,
|
|
63
|
+
scope: opts.scope,
|
|
64
|
+
};
|
|
65
|
+
if (opts.uids) payload.uids = opts.uids;
|
|
66
|
+
const result = await getApi().updateAutoReply(payload);
|
|
67
|
+
output(result, program.opts().json, () => success(`Auto-reply ${id} updated`));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
error(`Update auto-reply failed: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ar.command("delete <id>")
|
|
74
|
+
.description("Delete an auto-reply rule")
|
|
75
|
+
.action(async (id) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await getApi().deleteAutoReply(Number(id));
|
|
78
|
+
output(result, program.opts().json, () => success(`Auto-reply ${id} deleted`));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
error(`Delete auto-reply failed: ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|