@ardennguyen/zalo-agent-cli 2.0.0 → 2.1.1
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/.env.example +30 -0
- package/LICENSE +456 -21
- package/NOTICE +127 -0
- package/README.md +31 -9
- package/package.json +5 -4
- package/src/commands/conv.js +5 -8
- package/src/commands/listen.js +23 -9
- package/src/commands/mcp.js +47 -20
- package/src/commands/msg.js +32 -23
- package/src/commands/sync.js +10 -10
- package/src/core/accounts.js +11 -7
- package/src/core/credentials.js +152 -4
- package/src/core/current-user-names.js +104 -0
- package/src/core/daemon-account.js +61 -0
- package/src/core/daemon-sync.js +2 -2
- package/src/core/db.js +15 -0
- package/src/core/logout.js +3 -3
- package/src/core/oa-client.js +27 -15
- package/src/core/sync-v2/gid.js +5 -2
- package/src/core/sync-v2/index.js +66 -12
- package/src/core/sync-v2/media.js +4 -3
- package/src/core/sync-v2/senders.js +61 -0
- package/src/core/sync.js +2 -2
- package/src/index.js +25 -2
- package/src/mcp/mcp-config.js +6 -4
- package/src/mcp/mcp-http-transport.js +2 -1
- package/src/mcp/mcp-server.js +3 -2
- package/src/mcp/mcp-tools.js +140 -10
- package/src/mcp/sync-jobs.js +82 -0
- package/src/mcp/thread-name-cache.js +14 -1
- package/src/mcp/thread-name-cache.test.js +4 -1
- package/src/utils/qr-display.js +7 -8
- package/src/utils/qr-http-server.js +1 -2
- package/src/utils/thread-type.js +2 -3
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
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, Official Account API v3.0, và MCP server cho AI agent.
|
|
8
8
|
Xây dựng trên [zca-js](https://github.com/RFS-ADRENO/zca-js).
|
|
9
9
|
|
|
10
|
+
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
|
11
|
+
|
|
10
12
|
**[Tiếng Việt](#bắt-đầu-nhanh)** | **[English](#english)**
|
|
11
13
|
|
|
12
14
|
> [!WARNING]
|
|
@@ -47,7 +49,9 @@ Xây dựng trên [zca-js](https://github.com/RFS-ADRENO/zca-js).
|
|
|
47
49
|
> zalo-agent mcp start # stdio (local Claude Code)
|
|
48
50
|
> zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
|
|
49
51
|
> ```
|
|
50
|
-
>
|
|
52
|
+
> 14 tools: `zalo_get_messages` · `zalo_send_message` · `zalo_list_threads` · `zalo_search_threads` · `zalo_mark_read` · `zalo_get_history` · `zalo_view_media` · `zalo_react` · `zalo_undo` · `zalo_get_group_members` · `zalo_list_conversations` · `zalo_coverage` · `zalo_sync` · `zalo_sync_status`
|
|
53
|
+
|
|
54
|
+
[Sync qua MCP và điều kiện phát hành](docs/mcp-sync.vi.md).
|
|
51
55
|
> Auto-reconnect · thread filter · noise reduction · tự tải media · thông báo qua nhóm Zalo
|
|
52
56
|
> Xem [MCP Guide](skill/references/mcp-guide.md) · [INSTALLATION.md](INSTALLATION.md)
|
|
53
57
|
|
|
@@ -119,7 +123,7 @@ Tất cả lệnh hỗ trợ `--json`. Tài liệu đầy đủ: **[Wiki](https:
|
|
|
119
123
|
| `label` | 2 | Nhãn hội thoại | [Nhãn](https://github.com/ardennguyen/zalo-agent-cli/wiki/Nh%C3%A3n) |
|
|
120
124
|
| `catalog` | 9 | zBusiness — danh mục sản phẩm | [zBusiness](https://github.com/ardennguyen/zalo-agent-cli/wiki/zBusiness) |
|
|
121
125
|
| `listen` | 1 | Lắng nghe real-time, webhook, JSONL, ghi vào cache cục bộ | [Lắng nghe](https://github.com/ardennguyen/zalo-agent-cli/wiki/L%E1%BA%AFng-Nghe) |
|
|
122
|
-
| `mcp` | 1 | MCP server (stdio/HTTP) —
|
|
126
|
+
| `mcp` | 1 | MCP server (stdio/HTTP) — 14 tools cho AI agent | [MCP Server](https://github.com/ardennguyen/zalo-agent-cli/wiki/MCP-Server-(VN)) |
|
|
123
127
|
| **`oa`** | **32** | **Zalo Official Account API v3.0 — OAuth, tin nhắn, follower, tag, bài viết, cửa hàng, webhook** | **[Official Account](https://github.com/ardennguyen/zalo-agent-cli/wiki/Official-Account-(VN))** |
|
|
124
128
|
|
|
125
129
|
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) · [Bộ nhớ đệm & Đồng bộ](https://github.com/ardennguyen/zalo-agent-cli/wiki/B%E1%BB%99-Nh%E1%BB%9B-%C4%90%E1%BB%87m-&-%C4%90%E1%BB%93ng-B%E1%BB%99) · [Bảo mật](https://github.com/ardennguyen/zalo-agent-cli/wiki/B%E1%BA%A3o-M%E1%BA%ADt)
|
|
@@ -132,9 +136,9 @@ Xem thêm: [Đa tài khoản & Proxy](https://github.com/ardennguyen/zalo-agent-
|
|
|
132
136
|
- Đa tài khoản với proxy riêng biệt (1:1), device fingerprint riêng cho từng tài khoản
|
|
133
137
|
- **190 lệnh** phủ hết tính năng Zalo cá nhân
|
|
134
138
|
- **Zalo Official Account (OA) API v3.0** — OAuth login, gửi tin nhắn, follower, tag, bài viết, cửa hàng, webhook listener, multi-OA
|
|
135
|
-
- **MCP server** (stdio + HTTP) —
|
|
139
|
+
- **MCP server** (stdio + HTTP) — 14 tools cho Claude Code và các MCP client: đọc, gửi (kể cả vào My Documents, đánh dấu Quan trọng/Khẩn cấp), thả cảm xúc, thu hồi tin, xem thành viên nhóm, liệt kê hội thoại gần đây và kiểm tra độ đầy đủ của cache
|
|
136
140
|
- **Bộ nhớ đệm cục bộ (SQLite)** — `listen` ghi mọi tin nhắn vào `zalo.db`, `msg history` đọc từ cache, `sync` (hoặc `sync-mobile`) khôi phục toàn bộ lịch sử từ điện thoại vào `zalo.db` (giải mã transfer-sync-v2; xác nhận một lần trên điện thoại), mặc định lấy **toàn bộ** lịch sử (không còn mốc chặn 2024-01-01); thêm `--days <n>` hoặc `--from <YYYY-MM-DD>` nếu chỉ cần một khoảng. Dọn dẹp: `sync-media --prune <days|all>` giải phóng dung lượng nhưng giữ nguyên nội dung tin nhắn, `--prune-orphans` nhắm vào hội thoại tài khoản không còn, và `conv forget` xoá hẳn bản sao cục bộ của một hội thoại — không cái nào gửi gì tới Zalo. Luồng đồng bộ chỉ mang **liên kết** media chứ không mang file, nên sau khi lưu tin nhắn, `sync-mobile` **tự động tải media về** (thêm `--messages-only` nếu chỉ muốn tin nhắn); `sync-media` chạy lại/tiếp tục bước tải này, không cần xác nhận trên điện thoại. Ghi chú, tin ghim, bình chọn và nhắc hẹn nằm ngoài luồng tin nhắn — dùng `sync-boards`
|
|
137
|
-
- Tự động tải media (ảnh/audio/video) — một downloader duy nhất cho `listen`, `msg history`, `mcp start` và các lệnh sync, lưu vào `~/.zalo-agent-cli/accounts/<id>/media/<threadId>/` (
|
|
141
|
+
- Tự động tải media (ảnh/audio/video) — một downloader duy nhất cho `listen`, `msg history`, `mcp start` và các lệnh sync, lưu vào `~/.zalo-agent-cli/accounts/<id>/media/<threadId>/` (hoặc tách riêng credentials, database `zalo.db`, media và OA sang các ổ đĩa/mount khác nhau qua `--credentials-dir`, `--data-dir`, `--media-dir`, `--oa-dir` / biến môi trường `ZALO_*_DIR` trong `~/.zalo-agent-cli/.env`; nếu dùng kèm `zalo-mcp`, chỉ nên giữ cấu hình PORT trong `zalo-mcp/.env` và đặt toàn bộ đường dẫn lưu trữ vào `<CONFIG_DIR>/.env`)
|
|
138
142
|
- Thẻ chuyển khoản (55+ ngân hàng VN) & QR VietQR
|
|
139
143
|
- Lắng nghe real-time với webhook & lưu JSONL local
|
|
140
144
|
- `logout` hủy phiên **phía server** thật sự; `logout --purge` / `account remove` xóa sạch dữ liệu cục bộ
|
|
@@ -159,6 +163,16 @@ Xem thêm: [Đa tài khoản & Proxy](https://github.com/ardennguyen/zalo-agent-
|
|
|
159
163
|
| [AGENTS.md](AGENTS.md) | Quy tắc dự án cho AI coding agent |
|
|
160
164
|
| [Wiki](https://github.com/ardennguyen/zalo-agent-cli/wiki) | Tài liệu công khai, song ngữ EN + VN |
|
|
161
165
|
|
|
166
|
+
## Giấy phép
|
|
167
|
+
|
|
168
|
+
zalo-agent-cli © 2026 Arden Nguyen Duc Huy được cấp phép theo [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Xem [LICENSE](LICENSE) và [NOTICE](NOTICE). Điều khoản đầy đủ và cảnh báo rủi ro nằm trong [DISCLAIMER.md](DISCLAIMER.md).
|
|
169
|
+
|
|
170
|
+
- **Miễn phí cho mục đích phi thương mại**: cá nhân, học thuật, nghiên cứu. **Không cấp phép cho mục đích thương mại** — ví dụ dùng trong sản phẩm thương mại, để cung cấp dịch vụ có thu phí, hoặc trong hoạt động kinh doanh của một công ty. Muốn xin giấy phép thương mại, hãy [mở một issue](https://github.com/ardennguyen/zalo-agent-cli/issues).
|
|
171
|
+
- **Ghi công**: khi chia sẻ dự án này hoặc một phần của nó (nguyên bản hay đã sửa đổi, kể cả bên trong dự án khác), hãy ghi công theo mẫu trong [NOTICE](NOTICE), nêu rõ bạn có sửa đổi hay không, và dẫn link giấy phép.
|
|
172
|
+
- **Chia sẻ tương tự (ShareAlike)**: bản sửa đổi được chia sẻ phải dùng CC BY-NC-SA 4.0 (hoặc phiên bản mới hơn, hoặc giấy phép tương thích do Creative Commons liệt kê).
|
|
173
|
+
- Các phần dẫn xuất từ phần mềm MIT — [zalo-agent-cli của PhucMPham](https://github.com/PhucMPham/zalo-agent-cli) và bản vá của [zca-js](https://github.com/RFS-ADRENO/zca-js) được đóng gói kèm — vẫn thuộc giấy phép MIT; xem [NOTICE](NOTICE).
|
|
174
|
+
- Các phiên bản phát hành trước thay đổi này (2.0.0 trở về trước) được phát hành theo giấy phép MIT và giữ nguyên các điều khoản đó.
|
|
175
|
+
|
|
162
176
|
---
|
|
163
177
|
|
|
164
178
|
## English
|
|
@@ -199,7 +213,9 @@ CLI tool for Zalo automation — multi-account, proxy support, bank transfers, Q
|
|
|
199
213
|
> zalo-agent mcp start # stdio (local Claude Code)
|
|
200
214
|
> zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
|
|
201
215
|
> ```
|
|
202
|
-
>
|
|
216
|
+
> 14 tools: `zalo_get_messages` · `zalo_send_message` · `zalo_list_threads` · `zalo_search_threads` · `zalo_mark_read` · `zalo_get_history` · `zalo_view_media` · `zalo_react` · `zalo_undo` · `zalo_get_group_members` · `zalo_list_conversations` · `zalo_coverage` · `zalo_sync` · `zalo_sync_status`
|
|
217
|
+
|
|
218
|
+
[MCP sync and release prerequisites](docs/mcp-sync.md).
|
|
203
219
|
> Auto-reconnect · thread filter · noise reduction · media auto-download · Zalo-group notifications
|
|
204
220
|
> See [MCP Guide](skill/references/mcp-guide.md) · [INSTALLATION.md](INSTALLATION.md)
|
|
205
221
|
|
|
@@ -237,7 +253,7 @@ Full docs: **[Wiki](https://github.com/ardennguyen/zalo-agent-cli/wiki)** · [Fu
|
|
|
237
253
|
| `label` | 2 | Conversation labels | [Labels](https://github.com/ardennguyen/zalo-agent-cli/wiki/Labels) |
|
|
238
254
|
| `catalog` | 9 | zBusiness catalogs & products | [Catalog](https://github.com/ardennguyen/zalo-agent-cli/wiki/Catalog) |
|
|
239
255
|
| `listen` | 1 | Real-time listener, webhook, JSONL, local cache writes | [Listener](https://github.com/ardennguyen/zalo-agent-cli/wiki/Listener) |
|
|
240
|
-
| `mcp` | 1 | MCP server (stdio/HTTP) —
|
|
256
|
+
| `mcp` | 1 | MCP server (stdio/HTTP) — 14 tools for AI agents | [MCP Server](https://github.com/ardennguyen/zalo-agent-cli/wiki/MCP-Server) |
|
|
241
257
|
| **`oa`** | **32** | **Zalo Official Account API v3.0 — OAuth, messaging, followers, tags, articles, store, webhook** | **[Official Account](https://github.com/ardennguyen/zalo-agent-cli/wiki/Official-Account)** |
|
|
242
258
|
|
|
243
259
|
See also: [Multi-Account & Proxy](https://github.com/ardennguyen/zalo-agent-cli/wiki/Multi-Account-&-Proxy) · [VPS Setup](https://github.com/ardennguyen/zalo-agent-cli/wiki/VPS-Setup) · [Bank Card & QR Payments](https://github.com/ardennguyen/zalo-agent-cli/wiki/Bank-Card-&-QR-Payments) · [Local Cache & Sync](https://github.com/ardennguyen/zalo-agent-cli/wiki/Local-Cache-&-Sync) · [Security](https://github.com/ardennguyen/zalo-agent-cli/wiki/Security)
|
|
@@ -248,9 +264,9 @@ See also: [Multi-Account & Proxy](https://github.com/ardennguyen/zalo-agent-cli/
|
|
|
248
264
|
- Multi-account with a dedicated proxy (1:1) and a per-account device fingerprint
|
|
249
265
|
- **190 commands** covering the personal-account Zalo surface
|
|
250
266
|
- **Zalo Official Account (OA) API v3.0** — OAuth login, messaging, followers, tags, articles, store, webhook listener, multi-OA
|
|
251
|
-
- **MCP server** (stdio + HTTP) —
|
|
267
|
+
- **MCP server** (stdio + HTTP) — 14 tools for Claude Code and other MCP clients: read, send (to My Documents too, marked Important/Urgent), react, recall, list a group's members, list recent conversations, and check how complete the cache is
|
|
252
268
|
- **Local SQLite cache** — `listen` writes every message to `zalo.db`, `msg history` reads from it, and `sync` (or `sync-mobile`) restores full history from the phone into `zalo.db` (transfer-sync-v2 decrypt; one confirmation on the phone), defaulting to **all** history (the old 2024-01-01 floor is gone), with `--days <n>` or `--from <YYYY-MM-DD>` to narrow it. Housekeeping: `sync-media --prune <days|all>` reclaims disk while keeping message text, `--prune-orphans` targets conversations the account no longer has, and `conv forget` removes a conversation's local copy entirely — none of which contact Zalo. The sync stream carries media **links**, not files, so a `sync-mobile` run **downloads its attachments automatically** once the messages are stored (`--messages-only` skips it); `sync-media` re-runs or resumes that fetch on its own, with no phone confirmation. Notes, pinned messages, polls and reminders live outside the message stream — `sync-boards` fetches those
|
|
253
|
-
- Automatic media download — one downloader shared by `listen`, `msg history`, `mcp start` and the sync commands, saving to `~/.zalo-agent-cli/accounts/<id>/media/<threadId>/` (
|
|
269
|
+
- Automatic media download — one downloader shared by `listen`, `msg history`, `mcp start` and the sync commands, saving to `~/.zalo-agent-cli/accounts/<id>/media/<threadId>/` (or separate credentials, `zalo.db`, media, and OA credentials onto independent volumes/mounts via `--credentials-dir`, `--data-dir`, `--media-dir`, `--oa-dir` / `ZALO_*_DIR` in `~/.zalo-agent-cli/.env`; when using `zalo-mcp`, keep `zalo-mcp/.env` for PORT settings only and place all storage/engine settings in `<CONFIG_DIR>/.env`)
|
|
254
270
|
- Bank cards (55+ VN banks) and VietQR payment images
|
|
255
271
|
- Real-time listener with webhook forwarding and local JSONL archival
|
|
256
272
|
- `logout` performs a real **server-side** session invalidation; `logout --purge` / `account remove` wipe all local data
|
|
@@ -279,4 +295,10 @@ See also: [Multi-Account & Proxy](https://github.com/ardennguyen/zalo-agent-cli/
|
|
|
279
295
|
|
|
280
296
|
## License
|
|
281
297
|
|
|
282
|
-
[
|
|
298
|
+
zalo-agent-cli © 2026 Arden Nguyen Duc Huy is licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). See [LICENSE](LICENSE) and [NOTICE](NOTICE). See [DISCLAIMER.md](DISCLAIMER.md) for full terms and risk warnings.
|
|
299
|
+
|
|
300
|
+
- **Free for non-commercial use** (personal, academic, research). **Commercial use is not licensed** — for example in a commercial product, to provide paid services, or in a company's business operations. To request a commercial license, [open an issue](https://github.com/ardennguyen/zalo-agent-cli/issues).
|
|
301
|
+
- **Credit required:** if you share this project or any part of it, as-is or modified (including inside another project), credit it as shown in [NOTICE](NOTICE), say whether you made changes, and link to the license.
|
|
302
|
+
- **ShareAlike:** shared modifications must be released under CC BY-NC-SA 4.0 (or a later version, or a compatible license listed by Creative Commons).
|
|
303
|
+
- Portions derived from MIT-licensed software — [zalo-agent-cli by PhucMPham](https://github.com/PhucMPham/zalo-agent-cli) and the patched [zca-js](https://github.com/RFS-ADRENO/zca-js) shipped inside the package — remain under the MIT License; see [NOTICE](NOTICE).
|
|
304
|
+
- Versions published before this change (2.0.0 and earlier) were released under the MIT License and keep those terms.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ardennguyen/zalo-agent-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "CLI tool for Zalo automation — multi-account, proxy, bank transfers, QR payments, Official Account API v3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "./src/index.js",
|
|
10
10
|
"files": [
|
|
11
|
+
".env.example",
|
|
11
12
|
"DISCLAIMER.md",
|
|
12
13
|
"LICENSE",
|
|
14
|
+
"NOTICE",
|
|
13
15
|
"patches/",
|
|
14
16
|
"src/"
|
|
15
17
|
],
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
"url": "https://github.com/ardennguyen/zalo-agent-cli"
|
|
46
48
|
},
|
|
47
49
|
"author": "ardennguyen",
|
|
48
|
-
"license": "
|
|
50
|
+
"license": "CC-BY-NC-SA-4.0",
|
|
49
51
|
"publishConfig": {
|
|
50
52
|
"access": "public"
|
|
51
53
|
},
|
|
@@ -61,7 +63,6 @@
|
|
|
61
63
|
"google-protobuf": "^3.21.4",
|
|
62
64
|
"https-proxy-agent": "^9.1.0",
|
|
63
65
|
"image-size": "^2.0.4",
|
|
64
|
-
"node-fetch": "^3.3.2",
|
|
65
66
|
"undici": "^8.5.0",
|
|
66
67
|
"zca-js": "2.2.0",
|
|
67
68
|
"zod": "^4.4.3"
|
|
@@ -76,6 +77,6 @@
|
|
|
76
77
|
},
|
|
77
78
|
"allowScripts": {
|
|
78
79
|
"better-sqlite3@13.0.3": true,
|
|
79
|
-
"@ardennguyen/zalo-agent-cli@2.
|
|
80
|
+
"@ardennguyen/zalo-agent-cli@2.1.1": true
|
|
80
81
|
}
|
|
81
82
|
}
|
package/src/commands/conv.js
CHANGED
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* mute, unmute, read, unread, delete.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { join } from "path";
|
|
7
6
|
import { MuteAction } from "zca-js";
|
|
8
7
|
import { getApi } from "../core/zalo-client.js";
|
|
9
8
|
import { success, error, info, output, warning } from "../utils/output.js";
|
|
10
9
|
import { getActive } from "../core/accounts.js";
|
|
11
|
-
import {
|
|
10
|
+
import { getAccountDbPath } from "../core/credentials.js";
|
|
12
11
|
import { initDb, getMessages, getReadStates, markThreadGone, getOrphanThreads, forgetThread } from "../core/db.js";
|
|
13
12
|
import { recentConversations } from "../core/recent-conversations.js";
|
|
14
13
|
import { describeReadState, readStateLabel } from "../core/read-state.js";
|
|
@@ -41,7 +40,7 @@ async function newestMessageAnchor(api, threadId, type) {
|
|
|
41
40
|
try {
|
|
42
41
|
const activeAcc = getActive();
|
|
43
42
|
if (activeAcc) {
|
|
44
|
-
initDb(
|
|
43
|
+
initDb(getAccountDbPath(activeAcc.ownId));
|
|
45
44
|
cacheOpen = true;
|
|
46
45
|
}
|
|
47
46
|
} catch {
|
|
@@ -231,8 +230,7 @@ export function registerConvCommands(program, deps = {}) {
|
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
try {
|
|
234
|
-
|
|
235
|
-
initDb(join(accountDir, "zalo.db"));
|
|
233
|
+
initDb(getAccountDbPath(activeAcc.ownId));
|
|
236
234
|
// `-n` is documented as "max results per type", and the live
|
|
237
235
|
// path below honors that (up to `limit` friends AND up to
|
|
238
236
|
// `limit` groups). The cache path used to apply `limit`
|
|
@@ -524,7 +522,7 @@ export function registerConvCommands(program, deps = {}) {
|
|
|
524
522
|
|
|
525
523
|
let result;
|
|
526
524
|
try {
|
|
527
|
-
initDb(
|
|
525
|
+
initDb(getAccountDbPath(activeAcc.ownId));
|
|
528
526
|
const rows = getMessages(String(threadId), SEEN_ANCHOR_SCAN_DEPTH);
|
|
529
527
|
result = await markConversationRead(zaloApi(), { threadId, type, ownId: activeAcc.ownId, rows });
|
|
530
528
|
} catch (e) {
|
|
@@ -749,8 +747,7 @@ export function registerConvCommands(program, deps = {}) {
|
|
|
749
747
|
error("Give a threadId, or --orphans to forget every conversation the account no longer has.");
|
|
750
748
|
process.exit(1);
|
|
751
749
|
}
|
|
752
|
-
|
|
753
|
-
initDb(join(accountDir, "zalo.db"));
|
|
750
|
+
initDb(getAccountDbPath(activeAcc.ownId));
|
|
754
751
|
|
|
755
752
|
const targets = opts.orphans
|
|
756
753
|
? getOrphanThreads().map((t) => ({ threadId: String(t.threadId), name: t.name, ...t }))
|
package/src/commands/listen.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import { appendFileSync, mkdirSync, existsSync } from "fs";
|
|
7
7
|
import { resolve, join } from "path";
|
|
8
|
-
import { getApi,
|
|
8
|
+
import { getApi, clearSession } from "../core/zalo-client.js";
|
|
9
9
|
import { success, error, info, warning } from "../utils/output.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { parseAccountId, selectDaemonAccount, loginDaemonAccount } from "../core/daemon-account.js";
|
|
11
|
+
import { getAccountDir } from "../core/credentials.js";
|
|
12
12
|
import { acquireLock, releaseLock } from "../core/lock.js";
|
|
13
13
|
import { createStageLock, startDaemonChannel } from "../core/daemon-channel.js";
|
|
14
14
|
import { createSyncRunners } from "../core/daemon-sync.js";
|
|
@@ -55,6 +55,11 @@ const CLOSE_DUPLICATE = 3000;
|
|
|
55
55
|
export function registerListenCommand(program) {
|
|
56
56
|
program
|
|
57
57
|
.command("listen")
|
|
58
|
+
.option(
|
|
59
|
+
"--account <id>",
|
|
60
|
+
"Pin this daemon to an exact registered account ID (default: active at startup)",
|
|
61
|
+
parseAccountId,
|
|
62
|
+
)
|
|
58
63
|
.description(
|
|
59
64
|
"Listen for all Zalo events (messages, friend requests, group events) via one WebSocket. Auto-reconnect enabled.",
|
|
60
65
|
)
|
|
@@ -77,12 +82,14 @@ export function registerListenCommand(program) {
|
|
|
77
82
|
"Do not pull what a dropped socket missed from Zalo's offline queue on reconnect (the default does, as Zalo Web does, on this socket and with no phone tap)",
|
|
78
83
|
)
|
|
79
84
|
.action(async (opts) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
let activeAcc;
|
|
86
|
+
try {
|
|
87
|
+
activeAcc = selectDaemonAccount(opts.account);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
error(e.message);
|
|
83
90
|
process.exit(1);
|
|
84
91
|
}
|
|
85
|
-
const accountDir =
|
|
92
|
+
const accountDir = getAccountDir(activeAcc.ownId);
|
|
86
93
|
if (!acquireLock(accountDir)) {
|
|
87
94
|
error(`Another listen daemon is already running for account ${activeAcc.ownId}.`);
|
|
88
95
|
process.exit(1);
|
|
@@ -104,6 +111,13 @@ export function registerListenCommand(program) {
|
|
|
104
111
|
console.error(`[listen] Failed to release lock: ${e.message}`);
|
|
105
112
|
}
|
|
106
113
|
};
|
|
114
|
+
try {
|
|
115
|
+
await loginDaemonAccount(activeAcc);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
releaseLock(accountDir);
|
|
118
|
+
error(`Pinned account login failed: ${e.message}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
107
121
|
try {
|
|
108
122
|
initDb(join(accountDir, "zalo.db"));
|
|
109
123
|
info(`Local database initialized at ${accountDir}/zalo.db`);
|
|
@@ -703,7 +717,7 @@ export function registerListenCommand(program) {
|
|
|
703
717
|
await new Promise((r) => setTimeout(r, 5000));
|
|
704
718
|
try {
|
|
705
719
|
clearSession();
|
|
706
|
-
await
|
|
720
|
+
await loginDaemonAccount(activeAcc);
|
|
707
721
|
info("Re-login successful. Restarting listener...");
|
|
708
722
|
// Attach ALL handlers to the NEW api (including lifecycle),
|
|
709
723
|
// and repoint the SyncManager at it so its bookkeeping
|
|
@@ -717,7 +731,7 @@ export function registerListenCommand(program) {
|
|
|
717
731
|
await new Promise((r) => setTimeout(r, 30000));
|
|
718
732
|
try {
|
|
719
733
|
clearSession();
|
|
720
|
-
await
|
|
734
|
+
await loginDaemonAccount(activeAcc);
|
|
721
735
|
const retryApi = getApi();
|
|
722
736
|
syncManager.api = retryApi;
|
|
723
737
|
attachAllHandlers(retryApi);
|
package/src/commands/mcp.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { join } from "path";
|
|
9
|
-
import { getApi,
|
|
9
|
+
import { getApi, clearSession } from "../core/zalo-client.js";
|
|
10
10
|
import { liveApi } from "../core/live-api.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { parseAccountId, selectDaemonAccount, loginDaemonAccount } from "../core/daemon-account.js";
|
|
12
|
+
import { getAccountDir, MEDIA_DIR } from "../core/credentials.js";
|
|
13
13
|
import { acquireLock, releaseLock } from "../core/lock.js";
|
|
14
14
|
import { createStageLock, startDaemonChannel } from "../core/daemon-channel.js";
|
|
15
15
|
import { createSyncRunners } from "../core/daemon-sync.js";
|
|
16
|
+
import { createSyncJobs } from "../mcp/sync-jobs.js";
|
|
16
17
|
import { createSocketTap } from "../core/socket-tap.js";
|
|
17
18
|
import { createSelfHeal } from "../core/self-heal.js";
|
|
18
19
|
import { createReadStateSync } from "../core/read-state.js";
|
|
@@ -93,6 +94,11 @@ export function registerMCPCommands(program) {
|
|
|
93
94
|
const mcp = program.command("mcp").description("MCP server for AI agent integration");
|
|
94
95
|
|
|
95
96
|
mcp.command("start")
|
|
97
|
+
.option(
|
|
98
|
+
"--account <id>",
|
|
99
|
+
"Pin this server to an exact registered account ID (default: active at startup)",
|
|
100
|
+
parseAccountId,
|
|
101
|
+
)
|
|
96
102
|
.description("Start MCP server (stdio or HTTP transport)")
|
|
97
103
|
.option("--config <path>", "Config file path (default: ~/.zalo-agent-cli/mcp-config.json)")
|
|
98
104
|
.option("--http <port>", "Use HTTP transport on specified port (default: stdio)")
|
|
@@ -134,21 +140,15 @@ export function registerMCPCommands(program) {
|
|
|
134
140
|
process.exit(1);
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
//
|
|
138
|
-
|
|
143
|
+
// Select once; acquire the selected account's lock before authentication.
|
|
144
|
+
let activeAcc;
|
|
139
145
|
try {
|
|
140
|
-
|
|
146
|
+
activeAcc = selectDaemonAccount(opts.account);
|
|
141
147
|
} catch (e) {
|
|
142
|
-
console.error("[mcp]
|
|
148
|
+
console.error("[mcp] Account selection failed:", e.message);
|
|
143
149
|
process.exit(1);
|
|
144
150
|
}
|
|
145
|
-
|
|
146
|
-
const activeAcc = getActive();
|
|
147
|
-
if (!activeAcc) {
|
|
148
|
-
console.error("[mcp] No active account. Run `zalo-agent login` first.");
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
const accountDir = join(CONFIG_DIR, "accounts", activeAcc.ownId);
|
|
151
|
+
const accountDir = getAccountDir(activeAcc.ownId);
|
|
152
152
|
|
|
153
153
|
// This server now opens the account's WebSocket *and* writes to
|
|
154
154
|
// zalo.db, which is exactly what the `listen` daemon does. Zalo
|
|
@@ -182,6 +182,13 @@ export function registerMCPCommands(program) {
|
|
|
182
182
|
}
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
try {
|
|
186
|
+
await loginDaemonAccount(activeAcc);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
dropLock();
|
|
189
|
+
console.error("[mcp] Pinned account login failed:", e.message);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
185
192
|
try {
|
|
186
193
|
initDb(join(accountDir, "zalo.db"));
|
|
187
194
|
console.error(`[mcp] Local cache: ${join(accountDir, "zalo.db")}`);
|
|
@@ -211,6 +218,8 @@ export function registerMCPCommands(program) {
|
|
|
211
218
|
// self-heal catch-up and the tools' own history fetch. Created before
|
|
212
219
|
// the server so the tools can hold it too.
|
|
213
220
|
const stageLock = createStageLock();
|
|
221
|
+
const syncRunners = createSyncRunners({ getApi, accountName: activeAcc.ownId });
|
|
222
|
+
const syncJobs = createSyncJobs({ runners: syncRunners, lock: stageLock });
|
|
214
223
|
|
|
215
224
|
// What the tools and the notifier call. Not getApi()'s object:
|
|
216
225
|
// a re-login after a dropped connection replaces it, and they
|
|
@@ -231,7 +240,16 @@ export function registerMCPCommands(program) {
|
|
|
231
240
|
process.exit(1);
|
|
232
241
|
}
|
|
233
242
|
const host = opts.host || "127.0.0.1";
|
|
234
|
-
const deps = {
|
|
243
|
+
const deps = {
|
|
244
|
+
api: sessionApi,
|
|
245
|
+
buffer,
|
|
246
|
+
filter,
|
|
247
|
+
config,
|
|
248
|
+
nameCache,
|
|
249
|
+
accountDir,
|
|
250
|
+
stageLock,
|
|
251
|
+
syncJobs,
|
|
252
|
+
};
|
|
235
253
|
const authToken = opts.auth?.trim() || null;
|
|
236
254
|
httpServer = createHTTPServer(registerTools, deps, port, authToken, host);
|
|
237
255
|
console.error(`[mcp] HTTP server started on port ${port}`);
|
|
@@ -242,7 +260,16 @@ export function registerMCPCommands(program) {
|
|
|
242
260
|
);
|
|
243
261
|
}
|
|
244
262
|
} else {
|
|
245
|
-
await createMCPServer(
|
|
263
|
+
await createMCPServer(
|
|
264
|
+
sessionApi,
|
|
265
|
+
buffer,
|
|
266
|
+
filter,
|
|
267
|
+
config,
|
|
268
|
+
nameCache,
|
|
269
|
+
accountDir,
|
|
270
|
+
stageLock,
|
|
271
|
+
syncJobs,
|
|
272
|
+
);
|
|
246
273
|
}
|
|
247
274
|
} catch (e) {
|
|
248
275
|
dropLock();
|
|
@@ -358,7 +385,7 @@ export function registerMCPCommands(program) {
|
|
|
358
385
|
threadId,
|
|
359
386
|
limit: 5,
|
|
360
387
|
concurrency: 2,
|
|
361
|
-
mediaRoot: config.media?.downloadDir || undefined,
|
|
388
|
+
mediaRoot: MEDIA_DIR || config.media?.downloadDir || undefined,
|
|
362
389
|
}).catch((e) => console.error(`[mcp] media download failed: ${e.message}`));
|
|
363
390
|
}
|
|
364
391
|
|
|
@@ -471,7 +498,7 @@ export function registerMCPCommands(program) {
|
|
|
471
498
|
await new Promise((r) => setTimeout(r, 5000));
|
|
472
499
|
try {
|
|
473
500
|
clearSession();
|
|
474
|
-
await
|
|
501
|
+
await loginDaemonAccount(activeAcc);
|
|
475
502
|
console.error("[mcp] Re-login successful. Restarting listener...");
|
|
476
503
|
const newApi = getApi();
|
|
477
504
|
attachListenerHandlers(newApi);
|
|
@@ -481,7 +508,7 @@ export function registerMCPCommands(program) {
|
|
|
481
508
|
await new Promise((r) => setTimeout(r, 30000));
|
|
482
509
|
try {
|
|
483
510
|
clearSession();
|
|
484
|
-
await
|
|
511
|
+
await loginDaemonAccount(activeAcc);
|
|
485
512
|
const retryApi = getApi();
|
|
486
513
|
attachListenerHandlers(retryApi);
|
|
487
514
|
retryApi.listener.start({ retryOnClose: true });
|
|
@@ -518,7 +545,7 @@ export function registerMCPCommands(program) {
|
|
|
518
545
|
getApi,
|
|
519
546
|
accountDir,
|
|
520
547
|
onLog: (m) => console.error(`[mcp] ${m}`),
|
|
521
|
-
runners:
|
|
548
|
+
runners: syncRunners,
|
|
522
549
|
// The self-heal's lock: a stage and a catch-up never share the socket.
|
|
523
550
|
lock: stageLock,
|
|
524
551
|
});
|
package/src/commands/msg.js
CHANGED
|
@@ -7,10 +7,11 @@ import { existsSync } from "node:fs";
|
|
|
7
7
|
import { resolve, join } from "path";
|
|
8
8
|
import { InvalidArgumentError } from "commander";
|
|
9
9
|
import { getApi, getOwnId } from "../core/zalo-client.js";
|
|
10
|
+
import { withCurrentSenderNames } from "../core/current-user-names.js";
|
|
10
11
|
import { success, error, info, output, warning } from "../utils/output.js";
|
|
11
12
|
import { parseIntOption } from "../utils/parse-options.js";
|
|
12
13
|
import { getActive } from "../core/accounts.js";
|
|
13
|
-
import {
|
|
14
|
+
import { getAccountDir, getAccountDbPath } from "../core/credentials.js";
|
|
14
15
|
import { initDb, getMessages, getMessageById, getDisplayName, getThreadType } from "../core/db.js";
|
|
15
16
|
import { sendViaDaemon, getDaemonChannel, getSyncChannel, syncViaDaemon } from "../core/daemon-channel.js";
|
|
16
17
|
import { fetchAndCacheHistory } from "../core/history-fetch.js";
|
|
@@ -46,7 +47,7 @@ function openAccountDb() {
|
|
|
46
47
|
try {
|
|
47
48
|
const acc = getActive();
|
|
48
49
|
if (!acc) return false;
|
|
49
|
-
initDb(
|
|
50
|
+
initDb(getAccountDbPath(acc.ownId));
|
|
50
51
|
return true;
|
|
51
52
|
} catch {
|
|
52
53
|
return false;
|
|
@@ -446,7 +447,7 @@ async function sendAttachments(api, absPaths, threadId, type, opts) {
|
|
|
446
447
|
if (needsListener) {
|
|
447
448
|
const acc = getActive();
|
|
448
449
|
if (acc) {
|
|
449
|
-
const viaDaemon = await sendViaDaemon(
|
|
450
|
+
const viaDaemon = await sendViaDaemon(getAccountDir(acc.ownId), {
|
|
450
451
|
paths: absPaths,
|
|
451
452
|
threadId,
|
|
452
453
|
type,
|
|
@@ -1210,7 +1211,7 @@ export function registerMsgCommands(program) {
|
|
|
1210
1211
|
|
|
1211
1212
|
let row;
|
|
1212
1213
|
try {
|
|
1213
|
-
initDb(
|
|
1214
|
+
initDb(getAccountDbPath(activeAcc.ownId));
|
|
1214
1215
|
row = getMessageById(msgId);
|
|
1215
1216
|
} catch (e) {
|
|
1216
1217
|
error(
|
|
@@ -1529,7 +1530,7 @@ export function registerMsgCommands(program) {
|
|
|
1529
1530
|
let dbActive = false;
|
|
1530
1531
|
|
|
1531
1532
|
try {
|
|
1532
|
-
const accountDir =
|
|
1533
|
+
const accountDir = getAccountDir(activeAcc.ownId);
|
|
1533
1534
|
initDb(join(accountDir, "zalo.db"));
|
|
1534
1535
|
dbActive = true;
|
|
1535
1536
|
|
|
@@ -1556,16 +1557,20 @@ export function registerMsgCommands(program) {
|
|
|
1556
1557
|
} catch {
|
|
1557
1558
|
/* showing history must not fail because media did */
|
|
1558
1559
|
}
|
|
1559
|
-
const messages =
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1560
|
+
const messages = await withCurrentSenderNames(
|
|
1561
|
+
api,
|
|
1562
|
+
localMsgs.map((m) => ({
|
|
1563
|
+
msgId: m.msgId,
|
|
1564
|
+
threadId: m.threadId,
|
|
1565
|
+
senderId: m.senderId,
|
|
1566
|
+
senderName: m.senderName,
|
|
1567
|
+
text: m.text,
|
|
1568
|
+
timestamp: m.timestamp,
|
|
1569
|
+
type: m.type,
|
|
1570
|
+
localPath: m.localPath,
|
|
1571
|
+
})),
|
|
1572
|
+
threadType === 0 ? [threadId] : [],
|
|
1573
|
+
);
|
|
1569
1574
|
|
|
1570
1575
|
output(
|
|
1571
1576
|
{
|
|
@@ -1580,7 +1585,7 @@ export function registerMsgCommands(program) {
|
|
|
1580
1585
|
success(`${messages.length} message(s) from ${threadId} (Local Cache)`);
|
|
1581
1586
|
for (const m of messages) {
|
|
1582
1587
|
const date = m.timestamp ? new Date(m.timestamp).toLocaleString() : "?";
|
|
1583
|
-
const name = m.
|
|
1588
|
+
const name = m.senderDisplayName || m.senderId || "?";
|
|
1584
1589
|
const mediaInfo = m.localPath ? ` [Media: ${m.localPath}]` : "";
|
|
1585
1590
|
console.log(` [${date}] ${name}: ${(m.text || "").slice(0, 200)}${mediaInfo}`);
|
|
1586
1591
|
}
|
|
@@ -1620,7 +1625,7 @@ export function registerMsgCommands(program) {
|
|
|
1620
1625
|
// session the scan needs and is the one db writer (AGENTS.md
|
|
1621
1626
|
// §13). With a daemon up that is the daemon, and this command
|
|
1622
1627
|
// only displays what it returns.
|
|
1623
|
-
const daemonDir =
|
|
1628
|
+
const daemonDir = getAccountDir(activeAcc.ownId);
|
|
1624
1629
|
const daemon = getDaemonChannel(daemonDir);
|
|
1625
1630
|
let fetched = null;
|
|
1626
1631
|
let handledByDaemon = false;
|
|
@@ -1707,11 +1712,15 @@ export function registerMsgCommands(program) {
|
|
|
1707
1712
|
const result = mergedArray.slice(0, limit);
|
|
1708
1713
|
|
|
1709
1714
|
// Format final output
|
|
1710
|
-
const cleanResult =
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
+
const cleanResult = await withCurrentSenderNames(
|
|
1716
|
+
api,
|
|
1717
|
+
result.map((m) => {
|
|
1718
|
+
const r = { ...m };
|
|
1719
|
+
delete r.raw_data;
|
|
1720
|
+
return r;
|
|
1721
|
+
}),
|
|
1722
|
+
threadType === 0 ? [threadId] : [],
|
|
1723
|
+
);
|
|
1715
1724
|
|
|
1716
1725
|
output(
|
|
1717
1726
|
{
|
|
@@ -1728,7 +1737,7 @@ export function registerMsgCommands(program) {
|
|
|
1728
1737
|
success(`${cleanResult.length} message(s) from ${threadId}`);
|
|
1729
1738
|
for (const m of cleanResult) {
|
|
1730
1739
|
const date = m.timestamp ? new Date(m.timestamp).toLocaleString() : "?";
|
|
1731
|
-
const name = m.
|
|
1740
|
+
const name = m.senderDisplayName || m.senderId || "?";
|
|
1732
1741
|
const mediaInfo = m.localPath ? ` [Media: ${m.localPath}]` : "";
|
|
1733
1742
|
console.log(` [${date}] ${name}: ${(m.text || "").slice(0, 200)}${mediaInfo}`);
|
|
1734
1743
|
}
|