zalo-agent-cli 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/commands/msg.js +124 -24
package/package.json
CHANGED
package/src/commands/msg.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { resolve } from "path";
|
|
7
|
-
import { getApi } from "../core/zalo-client.js";
|
|
8
|
-
import { success, error, info, output } from "../utils/output.js";
|
|
7
|
+
import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
|
|
8
|
+
import { success, error, info, warning, output } from "../utils/output.js";
|
|
9
9
|
|
|
10
10
|
export function registerMsgCommands(program) {
|
|
11
11
|
const msg = program.command("msg").description("Send and manage messages");
|
|
@@ -13,7 +13,10 @@ export function registerMsgCommands(program) {
|
|
|
13
13
|
msg.command("send <threadId> <message>")
|
|
14
14
|
.description("Send a text message")
|
|
15
15
|
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
16
|
-
.option(
|
|
16
|
+
.option(
|
|
17
|
+
"--react <icon>",
|
|
18
|
+
"Auto-react to sent message. Codes: :> (haha), /-heart (heart), /-strong (like), :o (wow), :-(( (cry), :-h (angry)",
|
|
19
|
+
)
|
|
17
20
|
.action(async (threadId, message, opts) => {
|
|
18
21
|
try {
|
|
19
22
|
// Capture clientId before send — zca-js uses Date.now() internally
|
|
@@ -204,9 +207,14 @@ export function registerMsgCommands(program) {
|
|
|
204
207
|
});
|
|
205
208
|
|
|
206
209
|
msg.command("react <msgId> <threadId> <reaction>")
|
|
207
|
-
.description(
|
|
210
|
+
.description(
|
|
211
|
+
"React to a message. Reaction codes: :> (haha), /-heart (heart), /-strong (like), :o (wow), :-(( (cry), :-h (angry)",
|
|
212
|
+
)
|
|
208
213
|
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
209
|
-
.option(
|
|
214
|
+
.option(
|
|
215
|
+
"-c, --cli-msg-id <id>",
|
|
216
|
+
"Client message ID (required for reaction to appear, get from msg listen --json)",
|
|
217
|
+
)
|
|
210
218
|
.action(async (msgId, threadId, reaction, opts) => {
|
|
211
219
|
try {
|
|
212
220
|
// zca-js addReaction(icon, dest) — dest needs msgId + cliMsgId
|
|
@@ -223,14 +231,32 @@ export function registerMsgCommands(program) {
|
|
|
223
231
|
});
|
|
224
232
|
|
|
225
233
|
msg.command("listen")
|
|
226
|
-
.description(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
.description(
|
|
235
|
+
"Listen for incoming messages in real-time via WebSocket. Outputs msgId + cliMsgId needed for react. Use --json for machine parsing.",
|
|
236
|
+
)
|
|
237
|
+
.option("-f, --filter <type>", "Filter messages: user (DM only), group (groups only), all (default)", "all")
|
|
238
|
+
.option("-w, --webhook <url>", "POST each message as JSON to this URL (for n8n, Make, etc.)")
|
|
239
|
+
.option("--no-self", "Exclude messages sent by this account")
|
|
240
|
+
.action(async (opts) => {
|
|
241
|
+
const jsonMode = program.opts().json;
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
let reconnectCount = 0;
|
|
244
|
+
|
|
245
|
+
/** Format uptime as human-readable string */
|
|
246
|
+
function uptime() {
|
|
247
|
+
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
248
|
+
const h = Math.floor(s / 3600);
|
|
249
|
+
const m = Math.floor((s % 3600) / 60);
|
|
250
|
+
return h > 0 ? `${h}h${m}m` : `${m}m${s % 60}s`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Attach message handler to current API listener */
|
|
254
|
+
function attachMessageHandler(api) {
|
|
255
|
+
api.listener.on("message", async (msg) => {
|
|
256
|
+
if (opts.filter === "user" && msg.type !== 0) return;
|
|
257
|
+
if (opts.filter === "group" && msg.type !== 1) return;
|
|
258
|
+
if (!opts.self && msg.isSelf) return;
|
|
232
259
|
|
|
233
|
-
api.listener.on("message", (msg) => {
|
|
234
260
|
const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
|
|
235
261
|
const data = {
|
|
236
262
|
msgId: msg.data.msgId,
|
|
@@ -240,27 +266,101 @@ export function registerMsgCommands(program) {
|
|
|
240
266
|
isSelf: msg.isSelf,
|
|
241
267
|
content,
|
|
242
268
|
};
|
|
243
|
-
|
|
269
|
+
|
|
270
|
+
if (jsonMode) {
|
|
244
271
|
console.log(JSON.stringify(data));
|
|
245
272
|
} else {
|
|
246
273
|
const dir = msg.isSelf ? "→" : "←";
|
|
247
|
-
|
|
274
|
+
const typeLabel = msg.type === 0 ? "DM" : "GR";
|
|
275
|
+
console.log(` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (opts.webhook) {
|
|
279
|
+
try {
|
|
280
|
+
await fetch(opts.webhook, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/json" },
|
|
283
|
+
body: JSON.stringify(data),
|
|
284
|
+
});
|
|
285
|
+
} catch {
|
|
286
|
+
// Silent webhook failure
|
|
287
|
+
}
|
|
248
288
|
}
|
|
249
289
|
});
|
|
290
|
+
}
|
|
250
291
|
|
|
251
|
-
|
|
292
|
+
/** Start listener with auto-reconnect and re-login */
|
|
293
|
+
async function startListener() {
|
|
294
|
+
try {
|
|
295
|
+
const api = getApi();
|
|
252
296
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
info("Listener stopped.");
|
|
258
|
-
resolve();
|
|
297
|
+
api.listener.on("connected", () => {
|
|
298
|
+
if (reconnectCount > 0) {
|
|
299
|
+
info(`Reconnected (attempt #${reconnectCount}, uptime: ${uptime()})`);
|
|
300
|
+
}
|
|
259
301
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
302
|
+
|
|
303
|
+
api.listener.on("disconnected", (code, _reason) => {
|
|
304
|
+
warning(`Disconnected (code: ${code}). Auto-retrying...`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// "closed" = permanent close (retries exhausted or duplicate connection)
|
|
308
|
+
api.listener.on("closed", async (code, _reason) => {
|
|
309
|
+
if (code === 3000) {
|
|
310
|
+
// Duplicate connection — someone opened Zalo Web
|
|
311
|
+
error("Another Zalo Web session opened. Listener stopped.");
|
|
312
|
+
error("Close the other session and restart listener.");
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reconnectCount++;
|
|
317
|
+
warning(`Connection closed (code: ${code}). Re-login in 5s... (uptime: ${uptime()})`);
|
|
318
|
+
|
|
319
|
+
// Re-login and restart
|
|
320
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
321
|
+
try {
|
|
322
|
+
clearSession();
|
|
323
|
+
await autoLogin(jsonMode);
|
|
324
|
+
info(`Re-login successful. Restarting listener...`);
|
|
325
|
+
attachMessageHandler(getApi());
|
|
326
|
+
getApi().listener.start({ retryOnClose: true });
|
|
327
|
+
} catch (e) {
|
|
328
|
+
error(`Re-login failed: ${e.message}. Retrying in 30s...`);
|
|
329
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
330
|
+
startListener();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
api.listener.on("error", (_err) => {
|
|
335
|
+
// Log but don't crash — WS errors are usually followed by close/disconnect
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
attachMessageHandler(api);
|
|
339
|
+
// retryOnClose: true = zca-js auto-retries on temporary disconnects
|
|
340
|
+
api.listener.start({ retryOnClose: true });
|
|
341
|
+
|
|
342
|
+
info("Listening for messages... Press Ctrl+C to stop.");
|
|
343
|
+
info("Auto-reconnect enabled. Will survive network drops.");
|
|
344
|
+
if (opts.filter !== "all") info(`Filter: ${opts.filter} messages only`);
|
|
345
|
+
if (opts.webhook) info(`Webhook: POST to ${opts.webhook}`);
|
|
346
|
+
} catch (e) {
|
|
347
|
+
error(`Listen failed: ${e.message}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
263
350
|
}
|
|
351
|
+
|
|
352
|
+
await startListener();
|
|
353
|
+
|
|
354
|
+
// Keep alive until Ctrl+C
|
|
355
|
+
await new Promise((resolve) => {
|
|
356
|
+
process.on("SIGINT", () => {
|
|
357
|
+
try {
|
|
358
|
+
getApi().listener.stop();
|
|
359
|
+
} catch {}
|
|
360
|
+
info(`Listener stopped. Uptime: ${uptime()}, reconnects: ${reconnectCount}`);
|
|
361
|
+
resolve();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
264
364
|
});
|
|
265
365
|
|
|
266
366
|
msg.command("delete <msgId> <threadId>")
|