@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog commands — manage Zalo Shop catalogs and products.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { getApi } from "../core/zalo-client.js";
|
|
7
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
8
|
+
|
|
9
|
+
export function registerCatalogCommands(program) {
|
|
10
|
+
const catalog = program.command("catalog").description("Manage Zalo Shop catalogs and products");
|
|
11
|
+
|
|
12
|
+
catalog
|
|
13
|
+
.command("list")
|
|
14
|
+
.description("List all catalogs")
|
|
15
|
+
.option("-l, --limit <n>", "Items per page", parseInt, 20)
|
|
16
|
+
.option("-p, --page <n>", "Page number", parseInt, 0)
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
try {
|
|
19
|
+
const result = await getApi().getCatalogList({ limit: opts.limit, page: opts.page });
|
|
20
|
+
output(result, program.opts().json);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
error(`Get catalogs failed: ${e.message}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
catalog
|
|
27
|
+
.command("create <name>")
|
|
28
|
+
.description("Create a new catalog")
|
|
29
|
+
.action(async (name) => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await getApi().createCatalog(name);
|
|
32
|
+
output(result, program.opts().json, () => success(`Catalog "${name}" created`));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
error(`Create catalog failed: ${e.message}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
catalog
|
|
39
|
+
.command("rename <catalogId> <name>")
|
|
40
|
+
.description("Rename a catalog")
|
|
41
|
+
.action(async (catalogId, name) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await getApi().updateCatalog({ catalogId, catalogName: name });
|
|
44
|
+
output(result, program.opts().json, () => success(`Catalog renamed to "${name}"`));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
error(`Rename catalog failed: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
catalog
|
|
51
|
+
.command("delete <catalogId>")
|
|
52
|
+
.description("Delete a catalog")
|
|
53
|
+
.action(async (catalogId) => {
|
|
54
|
+
try {
|
|
55
|
+
const result = await getApi().deleteCatalog(catalogId);
|
|
56
|
+
output(result, program.opts().json, () => success(`Catalog ${catalogId} deleted`));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
error(`Delete catalog failed: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
catalog
|
|
63
|
+
.command("products <catalogId>")
|
|
64
|
+
.description("List products in a catalog")
|
|
65
|
+
.option("-l, --limit <n>", "Items per page", parseInt, 100)
|
|
66
|
+
.option("-p, --page <n>", "Page number", parseInt, 0)
|
|
67
|
+
.action(async (catalogId, opts) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await getApi().getProductCatalogList({
|
|
70
|
+
catalogId,
|
|
71
|
+
limit: opts.limit,
|
|
72
|
+
page: opts.page,
|
|
73
|
+
});
|
|
74
|
+
output(result, program.opts().json);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
error(`Get products failed: ${e.message}`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
catalog
|
|
81
|
+
.command("add-product <catalogId> <name> <price> <description>")
|
|
82
|
+
.description("Add a product to a catalog")
|
|
83
|
+
.option("--photos <urls...>", "Product photo URLs (up to 5)")
|
|
84
|
+
.action(async (catalogId, name, price, description, opts) => {
|
|
85
|
+
try {
|
|
86
|
+
const payload = { catalogId, productName: name, price, description };
|
|
87
|
+
if (opts.photos) payload.product_photos = opts.photos;
|
|
88
|
+
const result = await getApi().createProductCatalog(payload);
|
|
89
|
+
output(result, program.opts().json, () => success(`Product "${name}" added to catalog`));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
error(`Add product failed: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
catalog
|
|
96
|
+
.command("update-product <catalogId> <productId> <name> <price> <description>")
|
|
97
|
+
.description("Update a product in a catalog")
|
|
98
|
+
.option("--photos <urls...>", "Product photo URLs (up to 5)")
|
|
99
|
+
.action(async (catalogId, productId, name, price, description, opts) => {
|
|
100
|
+
try {
|
|
101
|
+
const payload = {
|
|
102
|
+
catalogId,
|
|
103
|
+
productId,
|
|
104
|
+
productName: name,
|
|
105
|
+
price,
|
|
106
|
+
description,
|
|
107
|
+
createTime: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
if (opts.photos) payload.product_photos = opts.photos;
|
|
110
|
+
const result = await getApi().updateProductCatalog(payload);
|
|
111
|
+
output(result, program.opts().json, () => success(`Product ${productId} updated`));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
error(`Update product failed: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
catalog
|
|
118
|
+
.command("delete-product <catalogId> <productIds...>")
|
|
119
|
+
.description("Delete product(s) from a catalog")
|
|
120
|
+
.action(async (catalogId, productIds) => {
|
|
121
|
+
try {
|
|
122
|
+
const result = await getApi().deleteProductCatalog({ catalogId, productIds });
|
|
123
|
+
output(result, program.opts().json, () => success(`Deleted ${productIds.length} product(s)`));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
error(`Delete product failed: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
catalog
|
|
130
|
+
.command("upload-photo <filePath>")
|
|
131
|
+
.description("Upload a product photo (returns URL for use with add-product --photos)")
|
|
132
|
+
.action(async (filePath) => {
|
|
133
|
+
try {
|
|
134
|
+
const absPath = resolve(filePath);
|
|
135
|
+
const result = await getApi().uploadProductPhoto({ file: absPath });
|
|
136
|
+
output(result, program.opts().json, () => {
|
|
137
|
+
info("Photo uploaded. Use the URL with: catalog add-product --photos <url>");
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
error(`Upload photo failed: ${e.message}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation commands — pinned, archived, mute, unmute, read, unread, delete.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerConvCommands(program) {
|
|
9
|
+
const conv = program.command("conv").description("Manage conversations");
|
|
10
|
+
|
|
11
|
+
conv.command("recent")
|
|
12
|
+
.description("List recent conversations with thread_id (friends + groups)")
|
|
13
|
+
.option("-n, --limit <n>", "Max results per type", "20")
|
|
14
|
+
.option("--friends-only", "Show only friend conversations")
|
|
15
|
+
.option("--groups-only", "Show only group conversations")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
try {
|
|
18
|
+
const api = getApi();
|
|
19
|
+
const limit = Number(opts.limit);
|
|
20
|
+
const conversations = [];
|
|
21
|
+
|
|
22
|
+
// Fetch friends (sorted by lastActionTime = most recent interaction)
|
|
23
|
+
if (!opts.groupsOnly) {
|
|
24
|
+
const friends = await api.getAllFriends();
|
|
25
|
+
const list = Array.isArray(friends) ? friends : [];
|
|
26
|
+
const sorted = list
|
|
27
|
+
.filter((f) => f.lastActionTime > 0)
|
|
28
|
+
.sort((a, b) => b.lastActionTime - a.lastActionTime)
|
|
29
|
+
.slice(0, limit);
|
|
30
|
+
for (const f of sorted) {
|
|
31
|
+
conversations.push({
|
|
32
|
+
threadId: f.userId,
|
|
33
|
+
name: f.displayName || f.zaloName || "?",
|
|
34
|
+
type: "User",
|
|
35
|
+
typeFlag: 0,
|
|
36
|
+
lastActive: new Date(f.lastActionTime * 1000).toLocaleString(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fetch groups
|
|
42
|
+
if (!opts.friendsOnly) {
|
|
43
|
+
const groupsResult = await api.getAllGroups();
|
|
44
|
+
const groupIds = Object.keys(groupsResult?.gridVerMap || {});
|
|
45
|
+
if (groupIds.length > 0) {
|
|
46
|
+
const batchSize = 50;
|
|
47
|
+
const batches = [];
|
|
48
|
+
const limitedIds = groupIds.slice(0, limit); // cap to limit BEFORE batching
|
|
49
|
+
for (let i = 0; i < limitedIds.length; i += batchSize) {
|
|
50
|
+
batches.push(limitedIds.slice(i, i + batchSize));
|
|
51
|
+
}
|
|
52
|
+
for (const batch of batches) {
|
|
53
|
+
try {
|
|
54
|
+
const groupInfo = await api.getGroupInfo(batch);
|
|
55
|
+
const map = groupInfo?.gridInfoMap || {};
|
|
56
|
+
for (const [gid, g] of Object.entries(map)) {
|
|
57
|
+
conversations.push({
|
|
58
|
+
threadId: gid,
|
|
59
|
+
name: g.name || "?",
|
|
60
|
+
type: "Group",
|
|
61
|
+
typeFlag: 1,
|
|
62
|
+
memberCount: g.totalMember || 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Skip failed batch
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
output(conversations, program.opts().json, () => {
|
|
73
|
+
if (conversations.length === 0) {
|
|
74
|
+
error("No conversations found.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
info(`${conversations.length} conversation(s):`);
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(" THREAD_ID TYPE NAME");
|
|
80
|
+
console.log(" " + "-".repeat(60));
|
|
81
|
+
for (const c of conversations) {
|
|
82
|
+
const typeLabel = c.type === "Group" ? `Group(${c.memberCount})` : "User";
|
|
83
|
+
const id = c.threadId.padEnd(22);
|
|
84
|
+
console.log(` ${id} ${typeLabel.padEnd(12)} ${c.name}`);
|
|
85
|
+
}
|
|
86
|
+
console.log();
|
|
87
|
+
info("Use thread_id with messaging commands:");
|
|
88
|
+
info(' zalo-agent msg send <thread_id> "Hello" (User)');
|
|
89
|
+
info(' zalo-agent msg send <thread_id> "Hello" -t 1 (Group)');
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
error(e.message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
conv.command("pinned")
|
|
97
|
+
.description("List pinned conversations")
|
|
98
|
+
.action(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await getApi().getPinConversations();
|
|
101
|
+
output(result, program.opts().json);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
error(e.message);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
conv.command("archived")
|
|
108
|
+
.description("List archived conversations")
|
|
109
|
+
.action(async () => {
|
|
110
|
+
try {
|
|
111
|
+
const result = await getApi().getArchivedChatList();
|
|
112
|
+
output(result, program.opts().json);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
error(e.message);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
conv.command("mute <threadId>")
|
|
119
|
+
.description("Mute a conversation")
|
|
120
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
121
|
+
.option("-d, --duration <secs>", "Duration in seconds (-1 = forever)", "-1")
|
|
122
|
+
.action(async (threadId, opts) => {
|
|
123
|
+
try {
|
|
124
|
+
const result = await getApi().setMute(threadId, Number(opts.type), Number(opts.duration));
|
|
125
|
+
output(result, program.opts().json, () => success("Conversation muted"));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
error(e.message);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
conv.command("unmute <threadId>")
|
|
132
|
+
.description("Unmute a conversation")
|
|
133
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
134
|
+
.action(async (threadId, opts) => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await getApi().setMute(threadId, Number(opts.type), 0);
|
|
137
|
+
output(result, program.opts().json, () => success("Conversation unmuted"));
|
|
138
|
+
} catch (e) {
|
|
139
|
+
error(e.message);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
conv.command("read <threadId>")
|
|
144
|
+
.description("Mark conversation as read")
|
|
145
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
146
|
+
.action(async (threadId, opts) => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await getApi().sendSeenEvent(threadId, Number(opts.type));
|
|
149
|
+
output(result, program.opts().json, () => success("Marked as read"));
|
|
150
|
+
} catch (e) {
|
|
151
|
+
error(e.message);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
conv.command("unread <threadId>")
|
|
156
|
+
.description("Mark conversation as unread")
|
|
157
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
158
|
+
.action(async (threadId, opts) => {
|
|
159
|
+
try {
|
|
160
|
+
const result = await getApi().markAsUnread(threadId, Number(opts.type));
|
|
161
|
+
output(result, program.opts().json, () => success("Marked as unread"));
|
|
162
|
+
} catch (e) {
|
|
163
|
+
error(e.message);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
conv.command("hidden")
|
|
168
|
+
.description("List hidden conversations")
|
|
169
|
+
.action(async () => {
|
|
170
|
+
try {
|
|
171
|
+
const result = await getApi().getHiddenConversations();
|
|
172
|
+
output(result, program.opts().json);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
error(`Get hidden conversations failed: ${e.message}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
conv.command("hide <threadIds...>")
|
|
179
|
+
.description("Hide conversation(s)")
|
|
180
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
181
|
+
.action(async (threadIds, opts) => {
|
|
182
|
+
try {
|
|
183
|
+
const result = await getApi().setHiddenConversations(true, threadIds, Number(opts.type));
|
|
184
|
+
output(result, program.opts().json, () => success(`Hidden ${threadIds.length} conversation(s)`));
|
|
185
|
+
} catch (e) {
|
|
186
|
+
error(`Hide failed: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
conv.command("unhide <threadIds...>")
|
|
191
|
+
.description("Unhide conversation(s)")
|
|
192
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
193
|
+
.action(async (threadIds, opts) => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await getApi().setHiddenConversations(false, threadIds, Number(opts.type));
|
|
196
|
+
output(result, program.opts().json, () => success(`Unhidden ${threadIds.length} conversation(s)`));
|
|
197
|
+
} catch (e) {
|
|
198
|
+
error(`Unhide failed: ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
conv.command("hidden-pin <pin>")
|
|
203
|
+
.description("Set or update PIN for hidden conversations (4 digits)")
|
|
204
|
+
.action(async (pin) => {
|
|
205
|
+
try {
|
|
206
|
+
const result = await getApi().updateHiddenConversPin(pin);
|
|
207
|
+
output(result, program.opts().json, () => success("Hidden conversation PIN updated"));
|
|
208
|
+
} catch (e) {
|
|
209
|
+
error(`Update PIN failed: ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
conv.command("hidden-pin-reset")
|
|
214
|
+
.description("Reset hidden conversations PIN")
|
|
215
|
+
.action(async () => {
|
|
216
|
+
try {
|
|
217
|
+
const result = await getApi().resetHiddenConversPin();
|
|
218
|
+
output(result, program.opts().json, () => success("Hidden conversation PIN reset"));
|
|
219
|
+
} catch (e) {
|
|
220
|
+
error(`Reset PIN failed: ${e.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
conv.command("auto-delete-status")
|
|
225
|
+
.description("View auto-delete chat settings")
|
|
226
|
+
.action(async () => {
|
|
227
|
+
try {
|
|
228
|
+
const result = await getApi().getAutoDeleteChat();
|
|
229
|
+
output(result, program.opts().json);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
error(`Get auto-delete status failed: ${e.message}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
conv.command("auto-delete <threadId> <ttl>")
|
|
236
|
+
.description("Set auto-delete for a conversation (off, 1d, 7d, 14d)")
|
|
237
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
238
|
+
.action(async (threadId, ttl, opts) => {
|
|
239
|
+
try {
|
|
240
|
+
const ttlMap = { off: 0, "1d": 86400000, "7d": 604800000, "14d": 1209600000 };
|
|
241
|
+
const ttlValue = ttlMap[ttl];
|
|
242
|
+
if (ttlValue === undefined) {
|
|
243
|
+
error(`Invalid TTL "${ttl}". Valid: off, 1d, 7d, 14d`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const result = await getApi().updateAutoDeleteChat(ttlValue, threadId, Number(opts.type));
|
|
247
|
+
output(result, program.opts().json, () => success(`Auto-delete set to ${ttl} for ${threadId}`));
|
|
248
|
+
} catch (e) {
|
|
249
|
+
error(`Set auto-delete failed: ${e.message}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
conv.command("delete <threadId>")
|
|
254
|
+
.description("Delete conversation history")
|
|
255
|
+
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
256
|
+
.action(async (threadId, opts) => {
|
|
257
|
+
try {
|
|
258
|
+
const result = await getApi().deleteConversation(threadId, Number(opts.type));
|
|
259
|
+
output(result, program.opts().json, () => success("Conversation deleted"));
|
|
260
|
+
} catch (e) {
|
|
261
|
+
error(e.message);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|