zalo-agent-cli 1.0.12 → 1.0.13
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/listen.js +122 -121
- package/src/commands/msg.js +1 -4
package/package.json
CHANGED
package/src/commands/listen.js
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
|
|
7
7
|
import { success, error, info, warning } from "../utils/output.js";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** Thread types matching zca-js ThreadType enum */
|
|
10
|
+
const THREAD_USER = 0;
|
|
11
|
+
const THREAD_GROUP = 1;
|
|
12
|
+
|
|
13
|
+
/** Friend event type → readable label (matches zca-js FriendEventType enum order) */
|
|
10
14
|
const FRIEND_EVENT_LABELS = {
|
|
11
15
|
0: "friend_added",
|
|
12
16
|
1: "friend_removed",
|
|
@@ -17,6 +21,10 @@ const FRIEND_EVENT_LABELS = {
|
|
|
17
21
|
6: "blocked",
|
|
18
22
|
7: "unblocked",
|
|
19
23
|
};
|
|
24
|
+
const FRIEND_REQUEST_TYPE = 2;
|
|
25
|
+
|
|
26
|
+
/** Zalo close code for duplicate web session */
|
|
27
|
+
const CLOSE_DUPLICATE = 3000;
|
|
20
28
|
|
|
21
29
|
export function registerListenCommand(program) {
|
|
22
30
|
program
|
|
@@ -47,30 +55,36 @@ export function registerListenCommand(program) {
|
|
|
47
55
|
return h > 0 ? `${h}h${m}m` : `${m}m${s % 60}s`;
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
58
|
+
/** Fire-and-forget webhook POST — never blocks event processing */
|
|
59
|
+
function postWebhook(data) {
|
|
52
60
|
if (!opts.webhook) return;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
fetch(opts.webhook, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify(data),
|
|
65
|
+
}).catch(() => {});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Output event as JSON or human-readable, then post to webhook */
|
|
69
|
+
function emitEvent(data, humanMsg) {
|
|
70
|
+
eventCount++;
|
|
71
|
+
if (jsonMode) {
|
|
72
|
+
console.log(JSON.stringify(data));
|
|
73
|
+
} else {
|
|
74
|
+
info(humanMsg);
|
|
61
75
|
}
|
|
76
|
+
postWebhook(data);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
/** Attach
|
|
65
|
-
function
|
|
79
|
+
/** Attach ALL handlers (data + lifecycle) to current API listener */
|
|
80
|
+
function attachAllHandlers(api) {
|
|
66
81
|
// --- Message events ---
|
|
67
82
|
if (enabledEvents.has("message")) {
|
|
68
83
|
api.listener.on("message", async (msg) => {
|
|
69
|
-
if (opts.filter === "user" && msg.type !==
|
|
70
|
-
if (opts.filter === "group" && msg.type !==
|
|
84
|
+
if (opts.filter === "user" && msg.type !== THREAD_USER) return;
|
|
85
|
+
if (opts.filter === "group" && msg.type !== THREAD_GROUP) return;
|
|
71
86
|
if (!opts.self && msg.isSelf) return;
|
|
72
87
|
|
|
73
|
-
eventCount++;
|
|
74
88
|
const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
|
|
75
89
|
const data = {
|
|
76
90
|
event: "message",
|
|
@@ -81,24 +95,18 @@ export function registerListenCommand(program) {
|
|
|
81
95
|
isSelf: msg.isSelf,
|
|
82
96
|
content,
|
|
83
97
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.log(
|
|
91
|
-
` ${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
await postWebhook(data);
|
|
98
|
+
const dir = msg.isSelf ? "→" : "←";
|
|
99
|
+
const typeLabel = msg.type === THREAD_USER ? "DM" : "GR";
|
|
100
|
+
emitEvent(
|
|
101
|
+
data,
|
|
102
|
+
`${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`,
|
|
103
|
+
);
|
|
95
104
|
});
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
// --- Friend events ---
|
|
99
108
|
if (enabledEvents.has("friend")) {
|
|
100
109
|
api.listener.on("friend_event", async (event) => {
|
|
101
|
-
eventCount++;
|
|
102
110
|
const label = FRIEND_EVENT_LABELS[event.type] || "friend_unknown";
|
|
103
111
|
const data = {
|
|
104
112
|
event: label,
|
|
@@ -106,20 +114,14 @@ export function registerListenCommand(program) {
|
|
|
106
114
|
isSelf: event.isSelf,
|
|
107
115
|
data: event.data,
|
|
108
116
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
info(msg);
|
|
118
|
-
}
|
|
119
|
-
await postWebhook(data);
|
|
120
|
-
|
|
121
|
-
// Auto-accept
|
|
122
|
-
if (opts.autoAccept && event.type === 2 && !event.isSelf) {
|
|
117
|
+
const humanMsg =
|
|
118
|
+
event.type === FRIEND_REQUEST_TYPE
|
|
119
|
+
? `Friend request from ${event.data.fromUid}: "${event.data.message || ""}"`
|
|
120
|
+
: `${label} — ${event.threadId}`;
|
|
121
|
+
emitEvent(data, humanMsg);
|
|
122
|
+
|
|
123
|
+
// Auto-accept incoming friend requests
|
|
124
|
+
if (opts.autoAccept && event.type === FRIEND_REQUEST_TYPE && !event.isSelf) {
|
|
123
125
|
try {
|
|
124
126
|
await api.acceptFriendRequest(event.data.fromUid);
|
|
125
127
|
success(`Auto-accepted friend request from ${event.data.fromUid}`);
|
|
@@ -132,104 +134,103 @@ export function registerListenCommand(program) {
|
|
|
132
134
|
|
|
133
135
|
// --- Group events ---
|
|
134
136
|
if (enabledEvents.has("group")) {
|
|
135
|
-
api.listener.on("group_event",
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(JSON.stringify(data));
|
|
146
|
-
} else {
|
|
147
|
-
info(`Group: ${event.type} — ${event.threadId}`);
|
|
148
|
-
}
|
|
149
|
-
await postWebhook(data);
|
|
137
|
+
api.listener.on("group_event", (event) => {
|
|
138
|
+
emitEvent(
|
|
139
|
+
{
|
|
140
|
+
event: `group_${event.type}`,
|
|
141
|
+
threadId: event.threadId,
|
|
142
|
+
isSelf: event.isSelf,
|
|
143
|
+
data: event.data,
|
|
144
|
+
},
|
|
145
|
+
`Group: ${event.type} — ${event.threadId}`,
|
|
146
|
+
);
|
|
150
147
|
});
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
// --- Reaction events ---
|
|
154
151
|
if (enabledEvents.has("reaction")) {
|
|
155
|
-
api.listener.on("reaction",
|
|
152
|
+
api.listener.on("reaction", (reaction) => {
|
|
156
153
|
if (!opts.self && reaction.isSelf) return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.log(JSON.stringify(data));
|
|
168
|
-
} else {
|
|
169
|
-
info(`Reaction in ${reaction.threadId}`);
|
|
170
|
-
}
|
|
171
|
-
await postWebhook(data);
|
|
154
|
+
emitEvent(
|
|
155
|
+
{
|
|
156
|
+
event: "reaction",
|
|
157
|
+
threadId: reaction.threadId,
|
|
158
|
+
isSelf: reaction.isSelf,
|
|
159
|
+
isGroup: reaction.isGroup,
|
|
160
|
+
data: reaction.data,
|
|
161
|
+
},
|
|
162
|
+
`Reaction in ${reaction.threadId}`,
|
|
163
|
+
);
|
|
172
164
|
});
|
|
173
165
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Start listener with auto-reconnect */
|
|
177
|
-
async function startListener() {
|
|
178
|
-
try {
|
|
179
|
-
const api = getApi();
|
|
180
166
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
}
|
|
167
|
+
// --- Lifecycle events (MUST be on same listener for reconnect to work) ---
|
|
168
|
+
api.listener.on("connected", () => {
|
|
169
|
+
if (reconnectCount > 0) {
|
|
170
|
+
info(`Reconnected (#${reconnectCount}, uptime: ${uptime()}, events: ${eventCount})`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
186
173
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
api.listener.on("disconnected", (code, _reason) => {
|
|
175
|
+
warning(`Disconnected (code: ${code}). Auto-retrying...`);
|
|
176
|
+
});
|
|
190
177
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
178
|
+
api.listener.on("closed", async (code, _reason) => {
|
|
179
|
+
if (code === CLOSE_DUPLICATE) {
|
|
180
|
+
error("Another Zalo Web session opened. Listener stopped.");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
reconnectCount++;
|
|
184
|
+
warning(`Connection closed (code: ${code}). Re-login in 5s... (uptime: ${uptime()})`);
|
|
185
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
186
|
+
try {
|
|
187
|
+
clearSession();
|
|
188
|
+
await autoLogin(jsonMode);
|
|
189
|
+
info("Re-login successful. Restarting listener...");
|
|
190
|
+
// Attach ALL handlers to the NEW api (including lifecycle)
|
|
191
|
+
const newApi = getApi();
|
|
192
|
+
attachAllHandlers(newApi);
|
|
193
|
+
newApi.listener.start({ retryOnClose: true });
|
|
194
|
+
} catch (e) {
|
|
195
|
+
error(`Re-login failed: ${e.message}. Retrying in 30s...`);
|
|
196
|
+
await new Promise((r) => setTimeout(r, 30000));
|
|
199
197
|
try {
|
|
200
198
|
clearSession();
|
|
201
199
|
await autoLogin(jsonMode);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
const retryApi = getApi();
|
|
201
|
+
attachAllHandlers(retryApi);
|
|
202
|
+
retryApi.listener.start({ retryOnClose: true });
|
|
203
|
+
info("Re-login successful on retry.");
|
|
204
|
+
} catch (e2) {
|
|
205
|
+
error(`Re-login retry failed: ${e2.message}. Exiting.`);
|
|
206
|
+
process.exit(1);
|
|
209
207
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
api.listener.on("error", (_err) => {
|
|
213
|
-
// Log but don't crash
|
|
214
|
-
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
215
210
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
info("Listening for Zalo events... Press Ctrl+C to stop.");
|
|
220
|
-
info(`Events: ${opts.events}`);
|
|
221
|
-
info("Auto-reconnect enabled.");
|
|
222
|
-
if (opts.filter !== "all") info(`Message filter: ${opts.filter}`);
|
|
223
|
-
if (opts.webhook) info(`Webhook: ${opts.webhook}`);
|
|
224
|
-
if (opts.autoAccept) info("Auto-accept friend requests: ON");
|
|
225
|
-
} catch (e) {
|
|
226
|
-
error(`Listen failed: ${e.message}`);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
211
|
+
api.listener.on("error", (_err) => {
|
|
212
|
+
// WS errors are followed by close/disconnect — don't crash
|
|
213
|
+
});
|
|
229
214
|
}
|
|
230
215
|
|
|
231
|
-
|
|
216
|
+
// --- Initial start ---
|
|
217
|
+
try {
|
|
218
|
+
const api = getApi();
|
|
219
|
+
attachAllHandlers(api);
|
|
220
|
+
api.listener.start({ retryOnClose: true });
|
|
221
|
+
|
|
222
|
+
info("Listening for Zalo events... Press Ctrl+C to stop.");
|
|
223
|
+
info(`Events: ${opts.events}`);
|
|
224
|
+
info("Auto-reconnect enabled.");
|
|
225
|
+
if (opts.filter !== "all") info(`Message filter: ${opts.filter}`);
|
|
226
|
+
if (opts.webhook) info(`Webhook: ${opts.webhook}`);
|
|
227
|
+
if (opts.autoAccept) info("Auto-accept friend requests: ON");
|
|
228
|
+
} catch (e) {
|
|
229
|
+
error(`Listen failed: ${e.message}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
232
|
|
|
233
|
+
// Keep alive until Ctrl+C
|
|
233
234
|
await new Promise((resolve) => {
|
|
234
235
|
process.on("SIGINT", () => {
|
|
235
236
|
try {
|
package/src/commands/msg.js
CHANGED
|
@@ -211,10 +211,7 @@ export function registerMsgCommands(program) {
|
|
|
211
211
|
"React to a message. Reaction codes: :> (haha), /-heart (heart), /-strong (like), :o (wow), :-(( (cry), :-h (angry)",
|
|
212
212
|
)
|
|
213
213
|
.option("-t, --type <n>", "Thread type: 0=User, 1=Group", "0")
|
|
214
|
-
.option(
|
|
215
|
-
"-c, --cli-msg-id <id>",
|
|
216
|
-
"Client message ID (required for reaction to appear, get from msg listen --json)",
|
|
217
|
-
)
|
|
214
|
+
.option("-c, --cli-msg-id <id>", "Client message ID (required for reaction to appear, get from listen --json)")
|
|
218
215
|
.action(async (msgId, threadId, reaction, opts) => {
|
|
219
216
|
try {
|
|
220
217
|
// zca-js addReaction(icon, dest) — dest needs msgId + cliMsgId
|