zalo-agent-cli 1.0.8 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/msg.js +86 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zalo-agent-cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "CLI tool for Zalo automation — multi-account, proxy support, bank transfers, QR payments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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");
@@ -238,19 +238,23 @@ export function registerMsgCommands(program) {
238
238
  .option("-w, --webhook <url>", "POST each message as JSON to this URL (for n8n, Make, etc.)")
239
239
  .option("--no-self", "Exclude messages sent by this account")
240
240
  .action(async (opts) => {
241
- try {
242
- const api = getApi();
243
- const jsonMode = program.opts().json;
244
- info("Listening for messages... Press Ctrl+C to stop.");
245
- info("Note: Only one web listener per account. Browser Zalo will disconnect.");
246
- if (opts.filter !== "all") info(`Filter: ${opts.filter} messages only`);
247
- if (opts.webhook) info(`Webhook: POST to ${opts.webhook}`);
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
+ }
248
252
 
253
+ /** Attach message handler to current API listener */
254
+ function attachMessageHandler(api) {
249
255
  api.listener.on("message", async (msg) => {
250
- // Filter by type: 0=User, 1=Group
251
256
  if (opts.filter === "user" && msg.type !== 0) return;
252
257
  if (opts.filter === "group" && msg.type !== 1) return;
253
- // Filter self messages
254
258
  if (!opts.self && msg.isSelf) return;
255
259
 
256
260
  const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
@@ -263,7 +267,6 @@ export function registerMsgCommands(program) {
263
267
  content,
264
268
  };
265
269
 
266
- // Output to stdout
267
270
  if (jsonMode) {
268
271
  console.log(JSON.stringify(data));
269
272
  } else {
@@ -272,7 +275,6 @@ export function registerMsgCommands(program) {
272
275
  console.log(` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`);
273
276
  }
274
277
 
275
- // POST to webhook if configured
276
278
  if (opts.webhook) {
277
279
  try {
278
280
  await fetch(opts.webhook, {
@@ -281,24 +283,84 @@ export function registerMsgCommands(program) {
281
283
  body: JSON.stringify(data),
282
284
  });
283
285
  } catch {
284
- // Silent webhook failure — don't block listener
286
+ // Silent webhook failure
285
287
  }
286
288
  }
287
289
  });
290
+ }
288
291
 
289
- api.listener.start();
292
+ /** Start listener with auto-reconnect and re-login */
293
+ async function startListener() {
294
+ try {
295
+ const api = getApi();
290
296
 
291
- // Keep alive until Ctrl+C
292
- await new Promise((resolve) => {
293
- process.on("SIGINT", () => {
294
- api.listener.stop();
295
- info("Listener stopped.");
296
- resolve();
297
+ api.listener.on("connected", () => {
298
+ if (reconnectCount > 0) {
299
+ info(`Reconnected (attempt #${reconnectCount}, uptime: ${uptime()})`);
300
+ }
297
301
  });
298
- });
299
- } catch (e) {
300
- error(`Listen failed: ${e.message}`);
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
+ }
301
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
+ });
302
364
  });
303
365
 
304
366
  msg.command("delete <msgId> <threadId>")