zalo-agent-cli 1.0.31 → 1.2.0
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/README.md +148 -738
- package/package.json +6 -3
- package/src/cli.test.js +1 -0
- package/src/commands/listen.js +20 -3
- package/src/commands/login.js +15 -4
- package/src/commands/msg.js +9 -3
- 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/core/oa-client.js +391 -0
- package/src/index.js +7 -4
- package/src/utils/qr-display.js +27 -16
- package/src/utils/qr-display.test.js +98 -0
- package/src/utils/qr-http-server.js +19 -5
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Account (OA) commands — manage Zalo OA via REST API v3.0.
|
|
3
|
+
* Independent from zca-js personal account; uses separate OA access token.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { registerOAListenCommand } from "./oa-listen.js";
|
|
8
|
+
import { registerOAInitCommand } from "./oa-init.js";
|
|
9
|
+
import {
|
|
10
|
+
saveOAToken,
|
|
11
|
+
saveOACreds,
|
|
12
|
+
loadOACreds,
|
|
13
|
+
loadOAToken,
|
|
14
|
+
getOAuthUrl,
|
|
15
|
+
exchangeCode,
|
|
16
|
+
refreshAccessToken,
|
|
17
|
+
sendText,
|
|
18
|
+
sendImage,
|
|
19
|
+
sendFile,
|
|
20
|
+
sendList,
|
|
21
|
+
getMessageStatus,
|
|
22
|
+
getOAProfile,
|
|
23
|
+
getFollowerInfo,
|
|
24
|
+
getFollowers,
|
|
25
|
+
updateFollowerInfo,
|
|
26
|
+
getTags,
|
|
27
|
+
assignTag,
|
|
28
|
+
removeTag,
|
|
29
|
+
removeFollowerFromTag,
|
|
30
|
+
uploadImage,
|
|
31
|
+
uploadFile,
|
|
32
|
+
getRecentChat,
|
|
33
|
+
getConversation,
|
|
34
|
+
updateMenu,
|
|
35
|
+
createArticle,
|
|
36
|
+
getArticleList,
|
|
37
|
+
getArticleDetail,
|
|
38
|
+
createProduct,
|
|
39
|
+
getProductList,
|
|
40
|
+
getProductInfo,
|
|
41
|
+
createCategory,
|
|
42
|
+
getCategoryList,
|
|
43
|
+
createOrder,
|
|
44
|
+
} from "../core/oa-client.js";
|
|
45
|
+
import { success, error, info, output } from "../utils/output.js";
|
|
46
|
+
|
|
47
|
+
export function registerOACommands(program) {
|
|
48
|
+
const oa = program.command("oa").description("Zalo Official Account API v3.0 commands");
|
|
49
|
+
const json = () => program.opts().json;
|
|
50
|
+
|
|
51
|
+
// ─── Login & Auth ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
oa.command("login")
|
|
54
|
+
.description("Login to Zalo OA via OAuth (opens browser, starts callback server)")
|
|
55
|
+
.requiredOption("--app-id <id>", "Zalo App ID from developers.zalo.me")
|
|
56
|
+
.requiredOption("--secret <key>", "Zalo App Secret Key")
|
|
57
|
+
.option("-p, --port <port>", "Callback server port", "3456")
|
|
58
|
+
.option("--callback-host <host>", "Callback host for VPS (e.g. https://your-vps.com)")
|
|
59
|
+
.option("--oa-id <id>", "OA identifier for multi-OA support", "default")
|
|
60
|
+
.action(async (opts) => {
|
|
61
|
+
const { createServer } = await import("node:http");
|
|
62
|
+
const callbackBase = opts.callbackHost || `http://localhost:${opts.port}`;
|
|
63
|
+
const redirectUri = `${callbackBase}/callback`;
|
|
64
|
+
const authUrl = getOAuthUrl(opts.appId, redirectUri);
|
|
65
|
+
|
|
66
|
+
if (!json()) {
|
|
67
|
+
if (opts.callbackHost) {
|
|
68
|
+
info("VPS mode — open this URL in your local browser:");
|
|
69
|
+
} else {
|
|
70
|
+
info("Opening browser for Zalo OA authorization...");
|
|
71
|
+
}
|
|
72
|
+
info(`Auth URL:\n ${authUrl}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Open browser (skip on VPS/headless — user copies URL manually)
|
|
76
|
+
if (!opts.callbackHost) {
|
|
77
|
+
const { exec } = await import("node:child_process");
|
|
78
|
+
const openCmd =
|
|
79
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
80
|
+
exec(`${openCmd} "${authUrl}"`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Start callback server to receive authorization code
|
|
84
|
+
const server = createServer(async (req, res) => {
|
|
85
|
+
if (!req.url?.startsWith("/callback")) {
|
|
86
|
+
res.writeHead(404);
|
|
87
|
+
res.end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const url = new URL(req.url, `http://localhost:${opts.port}`);
|
|
91
|
+
const code = url.searchParams.get("code");
|
|
92
|
+
const oaId = url.searchParams.get("oa_id");
|
|
93
|
+
|
|
94
|
+
if (!code) {
|
|
95
|
+
const errMsg = url.searchParams.get("error_description") || "No authorization code received";
|
|
96
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
97
|
+
res.end(`<h2>Login failed</h2><p>${errMsg}</p>`);
|
|
98
|
+
if (!json()) error(errMsg);
|
|
99
|
+
server.close();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Exchange code for tokens
|
|
105
|
+
const tokenData = await exchangeCode(code, opts.appId, opts.secret, redirectUri);
|
|
106
|
+
|
|
107
|
+
if (tokenData.error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Token error ${tokenData.error}: ${tokenData.error_description || tokenData.error_name}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Save all credentials
|
|
114
|
+
saveOACreds(
|
|
115
|
+
{
|
|
116
|
+
appId: opts.appId,
|
|
117
|
+
secretKey: opts.secret,
|
|
118
|
+
accessToken: tokenData.access_token,
|
|
119
|
+
refreshToken: tokenData.refresh_token,
|
|
120
|
+
expiresIn: tokenData.expires_in,
|
|
121
|
+
oaId: oaId || opts.oaId,
|
|
122
|
+
},
|
|
123
|
+
opts.oaId,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
127
|
+
res.end("<h2>Login successful!</h2><p>You can close this tab and return to the terminal.</p>");
|
|
128
|
+
|
|
129
|
+
output({ ok: true, oaId: oaId || opts.oaId, expires_in: tokenData.expires_in }, json(), () => {
|
|
130
|
+
success(`OA logged in successfully (OA ID: ${oaId || opts.oaId})`);
|
|
131
|
+
info(
|
|
132
|
+
`Token expires in ${Math.round((tokenData.expires_in || 86400) / 3600)}h — use 'oa refresh' to renew`,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
|
|
137
|
+
res.end(`<h2>Login failed</h2><p>${e.message}</p>`);
|
|
138
|
+
error(e.message);
|
|
139
|
+
} finally {
|
|
140
|
+
server.close();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Bind to 0.0.0.0 on VPS so external traffic can reach callback
|
|
145
|
+
const bindHost = opts.callbackHost ? "0.0.0.0" : "127.0.0.1";
|
|
146
|
+
server.listen(Number(opts.port), bindHost, () => {
|
|
147
|
+
if (!json()) info(`Waiting for callback on ${bindHost}:${opts.port}...`);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Timeout after 2 minutes
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (!json()) warning("Login timed out (2 min). Try again.");
|
|
153
|
+
server.close();
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}, 120_000);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
oa.command("refresh")
|
|
159
|
+
.description("Refresh OA access token using stored refresh token")
|
|
160
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
161
|
+
.action(async (opts) => {
|
|
162
|
+
try {
|
|
163
|
+
const creds = loadOACreds(opts.oaId);
|
|
164
|
+
if (!creds?.refreshToken || !creds?.appId || !creds?.secretKey) {
|
|
165
|
+
throw new Error("Missing credentials. Run: zalo-agent oa login --app-id <id> --secret <key>");
|
|
166
|
+
}
|
|
167
|
+
const tokenData = await refreshAccessToken(creds.refreshToken, creds.appId, creds.secretKey);
|
|
168
|
+
if (tokenData.error) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Refresh error ${tokenData.error}: ${tokenData.error_description || tokenData.error_name}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
saveOACreds(
|
|
174
|
+
{
|
|
175
|
+
accessToken: tokenData.access_token,
|
|
176
|
+
refreshToken: tokenData.refresh_token,
|
|
177
|
+
expiresIn: tokenData.expires_in,
|
|
178
|
+
},
|
|
179
|
+
opts.oaId,
|
|
180
|
+
);
|
|
181
|
+
output({ ok: true, expires_in: tokenData.expires_in }, json(), () =>
|
|
182
|
+
success(`Token refreshed. Expires in ${Math.round((tokenData.expires_in || 86400) / 3600)}h`),
|
|
183
|
+
);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
error(e.message);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
oa.command("setup <access-token>")
|
|
190
|
+
.description("Manually set OA access token (skip OAuth)")
|
|
191
|
+
.option("--oa-id <id>", "OA identifier for multi-OA support", "default")
|
|
192
|
+
.action(async (accessToken, opts) => {
|
|
193
|
+
try {
|
|
194
|
+
saveOAToken(accessToken, opts.oaId);
|
|
195
|
+
output({ ok: true, oaId: opts.oaId }, json(), () => success(`OA token saved for "${opts.oaId}"`));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
error(e.message);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
oa.command("whoami")
|
|
202
|
+
.description("Show current OA profile info")
|
|
203
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
204
|
+
.action(async (opts) => {
|
|
205
|
+
try {
|
|
206
|
+
const result = await getOAProfile(opts.oaId);
|
|
207
|
+
output(result, json(), () => {
|
|
208
|
+
const d = result.data || result;
|
|
209
|
+
info(`OA: ${d.name || "N/A"} (ID: ${d.oa_id || "N/A"})`);
|
|
210
|
+
if (d.description) info(`Description: ${d.description}`);
|
|
211
|
+
if (d.num_follower != null) info(`Followers: ${d.num_follower}`);
|
|
212
|
+
});
|
|
213
|
+
} catch (e) {
|
|
214
|
+
error(e.message);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ─── Messaging ───────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const msg = oa.command("msg").description("Send messages to OA followers");
|
|
221
|
+
|
|
222
|
+
msg.command("text <user-id> <text>")
|
|
223
|
+
.description("Send text message to a follower")
|
|
224
|
+
.option("-m, --msg-type <type>", "Message type: cs | transaction | promotion", "cs")
|
|
225
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
226
|
+
.action(async (userId, text, opts) => {
|
|
227
|
+
try {
|
|
228
|
+
const result = await sendText(userId, text, opts.msgType, opts.oaId);
|
|
229
|
+
output(result, json(), () => success("Text message sent"));
|
|
230
|
+
} catch (e) {
|
|
231
|
+
error(e.message);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
msg.command("image <user-id>")
|
|
236
|
+
.description("Send image message to a follower")
|
|
237
|
+
.option("--image-url <url>", "Image URL")
|
|
238
|
+
.option("--image-id <id>", "Pre-uploaded image attachment_id")
|
|
239
|
+
.option("-m, --msg-type <type>", "Message type: cs | transaction | promotion", "cs")
|
|
240
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
241
|
+
.action(async (userId, opts) => {
|
|
242
|
+
try {
|
|
243
|
+
const result = await sendImage(
|
|
244
|
+
userId,
|
|
245
|
+
{ imageUrl: opts.imageUrl, imageId: opts.imageId },
|
|
246
|
+
opts.msgType,
|
|
247
|
+
opts.oaId,
|
|
248
|
+
);
|
|
249
|
+
output(result, json(), () => success("Image message sent"));
|
|
250
|
+
} catch (e) {
|
|
251
|
+
error(e.message);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
msg.command("file <user-id> <file-id>")
|
|
256
|
+
.description("Send file message (requires pre-uploaded file token)")
|
|
257
|
+
.option("-m, --msg-type <type>", "Message type: cs | transaction | promotion", "cs")
|
|
258
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
259
|
+
.action(async (userId, fileId, opts) => {
|
|
260
|
+
try {
|
|
261
|
+
const result = await sendFile(userId, fileId, opts.msgType, opts.oaId);
|
|
262
|
+
output(result, json(), () => success("File message sent"));
|
|
263
|
+
} catch (e) {
|
|
264
|
+
error(e.message);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
msg.command("list <user-id> <elements-json>")
|
|
269
|
+
.description('Send list message. elements-json: \'[{"title":"...", "subtitle":"..."}]\'')
|
|
270
|
+
.option("-m, --msg-type <type>", "Message type: cs | transaction | promotion", "cs")
|
|
271
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
272
|
+
.action(async (userId, elementsJson, opts) => {
|
|
273
|
+
try {
|
|
274
|
+
const elements = JSON.parse(elementsJson);
|
|
275
|
+
const result = await sendList(userId, elements, opts.msgType, opts.oaId);
|
|
276
|
+
output(result, json(), () => success("List message sent"));
|
|
277
|
+
} catch (e) {
|
|
278
|
+
error(e.message);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
msg.command("status <message-id>")
|
|
283
|
+
.description("Check message delivery status")
|
|
284
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
285
|
+
.action(async (messageId, opts) => {
|
|
286
|
+
try {
|
|
287
|
+
const result = await getMessageStatus(messageId, opts.oaId);
|
|
288
|
+
output(result, json(), () => {
|
|
289
|
+
const d = result.data || result;
|
|
290
|
+
info(`Status: ${d.status || JSON.stringify(d)}`);
|
|
291
|
+
});
|
|
292
|
+
} catch (e) {
|
|
293
|
+
error(e.message);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ─── Followers ───────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
const follower = oa.command("follower").description("Manage OA followers");
|
|
300
|
+
|
|
301
|
+
follower
|
|
302
|
+
.command("info <user-id>")
|
|
303
|
+
.description("Get follower details")
|
|
304
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
305
|
+
.action(async (userId, opts) => {
|
|
306
|
+
try {
|
|
307
|
+
const result = await getFollowerInfo(userId, opts.oaId);
|
|
308
|
+
output(result, json(), () => {
|
|
309
|
+
const d = result.data || result;
|
|
310
|
+
info(`Name: ${d.display_name || d.name || "N/A"}`);
|
|
311
|
+
if (d.user_id) info(`User ID: ${d.user_id}`);
|
|
312
|
+
if (d.user_id_by_app) info(`App User ID: ${d.user_id_by_app}`);
|
|
313
|
+
});
|
|
314
|
+
} catch (e) {
|
|
315
|
+
error(e.message);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
follower
|
|
320
|
+
.command("list")
|
|
321
|
+
.description("List OA followers (paginated)")
|
|
322
|
+
.option("--offset <n>", "Offset", "0")
|
|
323
|
+
.option("--count <n>", "Count per page", "50")
|
|
324
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
325
|
+
.action(async (opts) => {
|
|
326
|
+
try {
|
|
327
|
+
const result = await getFollowers(Number(opts.offset), Number(opts.count), opts.oaId);
|
|
328
|
+
output(result, json(), () => {
|
|
329
|
+
const d = result.data || result;
|
|
330
|
+
const users = d.users || d.followers || [];
|
|
331
|
+
info(`Followers: ${d.total || users.length}`);
|
|
332
|
+
users.forEach((u) => console.log(` - ${u.user_id}: ${u.display_name || "N/A"}`));
|
|
333
|
+
});
|
|
334
|
+
} catch (e) {
|
|
335
|
+
error(e.message);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
follower
|
|
340
|
+
.command("update <user-id> <updates-json>")
|
|
341
|
+
.description('Update follower info. updates-json: \'{"name":"...","phone":"..."}\'')
|
|
342
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
343
|
+
.action(async (userId, updatesJson, opts) => {
|
|
344
|
+
try {
|
|
345
|
+
const updates = JSON.parse(updatesJson);
|
|
346
|
+
const result = await updateFollowerInfo(userId, updates, opts.oaId);
|
|
347
|
+
output(result, json(), () => success("Follower info updated"));
|
|
348
|
+
} catch (e) {
|
|
349
|
+
error(e.message);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ─── Tags ────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
const tag = oa.command("tag").description("Manage OA tags");
|
|
356
|
+
|
|
357
|
+
tag.command("list")
|
|
358
|
+
.description("List all tags")
|
|
359
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
360
|
+
.action(async (opts) => {
|
|
361
|
+
try {
|
|
362
|
+
const result = await getTags(opts.oaId);
|
|
363
|
+
output(result, json(), () => {
|
|
364
|
+
const tags = result.data?.tags || result.tags || [];
|
|
365
|
+
info(`Tags (${tags.length}):`);
|
|
366
|
+
tags.forEach((t) => console.log(` - ${t.name} (${t.count || 0} followers)`));
|
|
367
|
+
});
|
|
368
|
+
} catch (e) {
|
|
369
|
+
error(e.message);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
tag.command("assign <user-id> <tag-name>")
|
|
374
|
+
.description("Assign tag to a follower")
|
|
375
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
376
|
+
.action(async (userId, tagName, opts) => {
|
|
377
|
+
try {
|
|
378
|
+
const result = await assignTag(userId, tagName, opts.oaId);
|
|
379
|
+
output(result, json(), () => success(`Tag "${tagName}" assigned`));
|
|
380
|
+
} catch (e) {
|
|
381
|
+
error(e.message);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
tag.command("remove <tag-name>")
|
|
386
|
+
.description("Delete a tag")
|
|
387
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
388
|
+
.action(async (tagName, opts) => {
|
|
389
|
+
try {
|
|
390
|
+
const result = await removeTag(tagName, opts.oaId);
|
|
391
|
+
output(result, json(), () => success(`Tag "${tagName}" removed`));
|
|
392
|
+
} catch (e) {
|
|
393
|
+
error(e.message);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
tag.command("untag <user-id> <tag-name>")
|
|
398
|
+
.description("Remove follower from a tag")
|
|
399
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
400
|
+
.action(async (userId, tagName, opts) => {
|
|
401
|
+
try {
|
|
402
|
+
const result = await removeFollowerFromTag(userId, tagName, opts.oaId);
|
|
403
|
+
output(result, json(), () => success(`Removed "${tagName}" from user`));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
error(e.message);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ─── Media Upload ────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
const upload = oa.command("upload").description("Upload media to OA");
|
|
412
|
+
|
|
413
|
+
upload
|
|
414
|
+
.command("image <file-path>")
|
|
415
|
+
.description("Upload image (returns attachment_id for sending)")
|
|
416
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
417
|
+
.action(async (filePath, opts) => {
|
|
418
|
+
try {
|
|
419
|
+
const result = await uploadImage(resolve(filePath), opts.oaId);
|
|
420
|
+
output(result, json(), () => {
|
|
421
|
+
const d = result.data || result;
|
|
422
|
+
success(`Image uploaded: ${d.attachment_id || JSON.stringify(d)}`);
|
|
423
|
+
});
|
|
424
|
+
} catch (e) {
|
|
425
|
+
error(e.message);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
upload
|
|
430
|
+
.command("file <file-path>")
|
|
431
|
+
.description("Upload file (returns token for sending)")
|
|
432
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
433
|
+
.action(async (filePath, opts) => {
|
|
434
|
+
try {
|
|
435
|
+
const result = await uploadFile(resolve(filePath), opts.oaId);
|
|
436
|
+
output(result, json(), () => {
|
|
437
|
+
const d = result.data || result;
|
|
438
|
+
success(`File uploaded: ${d.token || JSON.stringify(d)}`);
|
|
439
|
+
});
|
|
440
|
+
} catch (e) {
|
|
441
|
+
error(e.message);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ─── Conversations ───────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
const conv = oa.command("conv").description("OA conversations");
|
|
448
|
+
|
|
449
|
+
conv.command("recent")
|
|
450
|
+
.description("List recent conversations")
|
|
451
|
+
.option("--offset <n>", "Offset", "0")
|
|
452
|
+
.option("--count <n>", "Count", "10")
|
|
453
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
454
|
+
.action(async (opts) => {
|
|
455
|
+
try {
|
|
456
|
+
const result = await getRecentChat(Number(opts.offset), Number(opts.count), opts.oaId);
|
|
457
|
+
output(result, json(), () => {
|
|
458
|
+
const chats = result.data || [];
|
|
459
|
+
info(`Recent conversations: ${Array.isArray(chats) ? chats.length : "see JSON"}`);
|
|
460
|
+
});
|
|
461
|
+
} catch (e) {
|
|
462
|
+
error(e.message);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
conv.command("history <user-id>")
|
|
467
|
+
.description("Get conversation history with a follower")
|
|
468
|
+
.option("--offset <n>", "Offset", "0")
|
|
469
|
+
.option("--count <n>", "Count", "10")
|
|
470
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
471
|
+
.action(async (userId, opts) => {
|
|
472
|
+
try {
|
|
473
|
+
const result = await getConversation(userId, Number(opts.offset), Number(opts.count), opts.oaId);
|
|
474
|
+
output(result, json(), () => {
|
|
475
|
+
const msgs = result.data || [];
|
|
476
|
+
info(`Messages: ${Array.isArray(msgs) ? msgs.length : "see JSON"}`);
|
|
477
|
+
});
|
|
478
|
+
} catch (e) {
|
|
479
|
+
error(e.message);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ─── Menu ────────────────────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
oa.command("menu <menu-json>")
|
|
486
|
+
.description("Update OA menu. menu-json: '{\"buttons\":[...]}'")
|
|
487
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
488
|
+
.action(async (menuJson, opts) => {
|
|
489
|
+
try {
|
|
490
|
+
const menuData = JSON.parse(menuJson);
|
|
491
|
+
const result = await updateMenu(menuData, opts.oaId);
|
|
492
|
+
output(result, json(), () => success("OA menu updated"));
|
|
493
|
+
} catch (e) {
|
|
494
|
+
error(e.message);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ─── Articles ────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
const article = oa.command("article").description("Manage OA articles/broadcasts");
|
|
501
|
+
|
|
502
|
+
article
|
|
503
|
+
.command("create <article-json>")
|
|
504
|
+
.description("Create article (broadcast)")
|
|
505
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
506
|
+
.action(async (articleJson, opts) => {
|
|
507
|
+
try {
|
|
508
|
+
const data = JSON.parse(articleJson);
|
|
509
|
+
const result = await createArticle(data, opts.oaId);
|
|
510
|
+
output(result, json(), () => success("Article created"));
|
|
511
|
+
} catch (e) {
|
|
512
|
+
error(e.message);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
article
|
|
517
|
+
.command("list")
|
|
518
|
+
.description("List articles")
|
|
519
|
+
.option("--offset <n>", "Offset", "0")
|
|
520
|
+
.option("--limit <n>", "Limit", "10")
|
|
521
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
522
|
+
.action(async (opts) => {
|
|
523
|
+
try {
|
|
524
|
+
const result = await getArticleList(Number(opts.offset), Number(opts.limit), opts.oaId);
|
|
525
|
+
output(result, json(), () => {
|
|
526
|
+
const articles = result.data?.articles || [];
|
|
527
|
+
info(`Articles: ${articles.length}`);
|
|
528
|
+
articles.forEach((a) => console.log(` - ${a.id}: ${a.title || "N/A"}`));
|
|
529
|
+
});
|
|
530
|
+
} catch (e) {
|
|
531
|
+
error(e.message);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
article
|
|
536
|
+
.command("detail <article-id>")
|
|
537
|
+
.description("Get article details")
|
|
538
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
539
|
+
.action(async (articleId, opts) => {
|
|
540
|
+
try {
|
|
541
|
+
const result = await getArticleDetail(articleId, opts.oaId);
|
|
542
|
+
output(result, json(), () => {
|
|
543
|
+
const d = result.data || result;
|
|
544
|
+
info(`Title: ${d.title || "N/A"}`);
|
|
545
|
+
if (d.status) info(`Status: ${d.status}`);
|
|
546
|
+
});
|
|
547
|
+
} catch (e) {
|
|
548
|
+
error(e.message);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// ─── Store ───────────────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
const store = oa.command("store").description("Manage OA store (products, categories, orders)");
|
|
555
|
+
|
|
556
|
+
store
|
|
557
|
+
.command("product-create <product-json>")
|
|
558
|
+
.description("Create product in OA store")
|
|
559
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
560
|
+
.action(async (productJson, opts) => {
|
|
561
|
+
try {
|
|
562
|
+
const data = JSON.parse(productJson);
|
|
563
|
+
const result = await createProduct(data, opts.oaId);
|
|
564
|
+
output(result, json(), () => success("Product created"));
|
|
565
|
+
} catch (e) {
|
|
566
|
+
error(e.message);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
store
|
|
571
|
+
.command("product-list")
|
|
572
|
+
.description("List products")
|
|
573
|
+
.option("--offset <n>", "Offset", "0")
|
|
574
|
+
.option("--limit <n>", "Limit", "10")
|
|
575
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
576
|
+
.action(async (opts) => {
|
|
577
|
+
try {
|
|
578
|
+
const result = await getProductList(Number(opts.offset), Number(opts.limit), opts.oaId);
|
|
579
|
+
output(result, json(), () => {
|
|
580
|
+
const products = result.data?.products || [];
|
|
581
|
+
info(`Products: ${products.length}`);
|
|
582
|
+
products.forEach((p) => console.log(` - ${p.id}: ${p.name || "N/A"}`));
|
|
583
|
+
});
|
|
584
|
+
} catch (e) {
|
|
585
|
+
error(e.message);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
store
|
|
590
|
+
.command("product-info <product-id>")
|
|
591
|
+
.description("Get product details")
|
|
592
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
593
|
+
.action(async (productId, opts) => {
|
|
594
|
+
try {
|
|
595
|
+
const result = await getProductInfo(productId, opts.oaId);
|
|
596
|
+
output(result, json(), () => {
|
|
597
|
+
const d = result.data || result;
|
|
598
|
+
info(`Product: ${d.name || "N/A"} — ${d.price || "N/A"}`);
|
|
599
|
+
});
|
|
600
|
+
} catch (e) {
|
|
601
|
+
error(e.message);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
store
|
|
606
|
+
.command("category-create <category-json>")
|
|
607
|
+
.description("Create store category")
|
|
608
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
609
|
+
.action(async (categoryJson, opts) => {
|
|
610
|
+
try {
|
|
611
|
+
const data = JSON.parse(categoryJson);
|
|
612
|
+
const result = await createCategory(data, opts.oaId);
|
|
613
|
+
output(result, json(), () => success("Category created"));
|
|
614
|
+
} catch (e) {
|
|
615
|
+
error(e.message);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
store
|
|
620
|
+
.command("category-list")
|
|
621
|
+
.description("List store categories")
|
|
622
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
623
|
+
.action(async (opts) => {
|
|
624
|
+
try {
|
|
625
|
+
const result = await getCategoryList(opts.oaId);
|
|
626
|
+
output(result, json(), () => {
|
|
627
|
+
const cats = result.data?.categories || [];
|
|
628
|
+
info(`Categories: ${cats.length}`);
|
|
629
|
+
cats.forEach((c) => console.log(` - ${c.id}: ${c.name || "N/A"}`));
|
|
630
|
+
});
|
|
631
|
+
} catch (e) {
|
|
632
|
+
error(e.message);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
store
|
|
637
|
+
.command("order-create <order-json>")
|
|
638
|
+
.description("Create order")
|
|
639
|
+
.option("--oa-id <id>", "OA identifier", "default")
|
|
640
|
+
.action(async (orderJson, opts) => {
|
|
641
|
+
try {
|
|
642
|
+
const data = JSON.parse(orderJson);
|
|
643
|
+
const result = await createOrder(data, opts.oaId);
|
|
644
|
+
output(result, json(), () => success("Order created"));
|
|
645
|
+
} catch (e) {
|
|
646
|
+
error(e.message);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// ─── Webhook Listener ────────────────────────────────────────────
|
|
651
|
+
|
|
652
|
+
registerOAListenCommand(oa, program);
|
|
653
|
+
|
|
654
|
+
// ─── Guided Setup ────────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
registerOAInitCommand(oa, program);
|
|
657
|
+
}
|