zalo-agent-cli 1.0.30 → 1.1.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 +122 -676
- package/package.json +6 -3
- package/src/commands/auto-reply.js +83 -0
- package/src/commands/catalog.js +143 -0
- package/src/commands/conv.js +86 -0
- package/src/commands/label.js +39 -0
- package/src/commands/msg.js +47 -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/commands/profile.js +64 -0
- package/src/commands/quick-msg.js +55 -0
- package/src/core/oa-client.js +391 -0
- package/src/index.js +15 -4
package/src/commands/profile.js
CHANGED
|
@@ -182,6 +182,70 @@ export function registerProfileCommands(program) {
|
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
profile
|
|
186
|
+
.command("avatars")
|
|
187
|
+
.description("List your avatar gallery")
|
|
188
|
+
.option("-c, --count <n>", "Page size", parseInt, 50)
|
|
189
|
+
.option("-p, --page <n>", "Page number", parseInt, 1)
|
|
190
|
+
.action(async (opts) => {
|
|
191
|
+
try {
|
|
192
|
+
const result = await getApi().getAvatarList(opts.count, opts.page);
|
|
193
|
+
output(result, program.opts().json);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
error(`Get avatars failed: ${e.message}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
profile
|
|
200
|
+
.command("full-avatar <friendId>")
|
|
201
|
+
.description("Get full-size avatar URL for a user")
|
|
202
|
+
.action(async (friendId) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await getApi().getFullAvatar(friendId);
|
|
205
|
+
output(result, program.opts().json, () => {
|
|
206
|
+
info(`Avatar: ${result?.avatar || "?"}`);
|
|
207
|
+
});
|
|
208
|
+
} catch (e) {
|
|
209
|
+
error(`Get full avatar failed: ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
profile
|
|
214
|
+
.command("avatar-url <friendIds...>")
|
|
215
|
+
.description("Get avatar URLs for one or more users")
|
|
216
|
+
.action(async (friendIds) => {
|
|
217
|
+
try {
|
|
218
|
+
const result = await getApi().getAvatarUrlProfile(friendIds);
|
|
219
|
+
output(result, program.opts().json);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
error(`Get avatar URLs failed: ${e.message}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
profile
|
|
226
|
+
.command("delete-avatar <photoIds...>")
|
|
227
|
+
.description("Delete avatar(s) from your gallery")
|
|
228
|
+
.action(async (photoIds) => {
|
|
229
|
+
try {
|
|
230
|
+
const result = await getApi().deleteAvatar(photoIds);
|
|
231
|
+
output(result, program.opts().json, () => success(`Deleted ${photoIds.length} avatar(s)`));
|
|
232
|
+
} catch (e) {
|
|
233
|
+
error(`Delete avatar failed: ${e.message}`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
profile
|
|
238
|
+
.command("reuse-avatar <photoId>")
|
|
239
|
+
.description("Reuse a previous avatar from your gallery")
|
|
240
|
+
.action(async (photoId) => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await getApi().reuseAvatar(photoId);
|
|
243
|
+
output(result, program.opts().json, () => success("Avatar reused"));
|
|
244
|
+
} catch (e) {
|
|
245
|
+
error(`Reuse avatar failed: ${e.message}`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
185
249
|
profile
|
|
186
250
|
.command("set <setting> <value>")
|
|
187
251
|
.description("Update a privacy setting (use 'profile settings' to see options)")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick message commands — list, add, update, remove saved quick messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getApi } from "../core/zalo-client.js";
|
|
6
|
+
import { success, error, output } from "../utils/output.js";
|
|
7
|
+
|
|
8
|
+
export function registerQuickMsgCommands(program) {
|
|
9
|
+
const qm = program.command("quick-msg").description("Manage quick/saved messages");
|
|
10
|
+
|
|
11
|
+
qm.command("list")
|
|
12
|
+
.description("List all quick messages")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const result = await getApi().getQuickMessageList();
|
|
16
|
+
output(result, program.opts().json);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
error(`Get quick messages failed: ${e.message}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
qm.command("add <keyword> <title>")
|
|
23
|
+
.description("Add a quick message")
|
|
24
|
+
.action(async (keyword, title) => {
|
|
25
|
+
try {
|
|
26
|
+
const result = await getApi().addQuickMessage({ keyword, title });
|
|
27
|
+
output(result, program.opts().json, () => success(`Quick message added: "${keyword}" → "${title}"`));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
error(`Add quick message failed: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
qm.command("update <itemId> <keyword> <title>")
|
|
34
|
+
.description("Update a quick message")
|
|
35
|
+
.action(async (itemId, keyword, title) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await getApi().updateQuickMessage({ keyword, title }, Number(itemId));
|
|
38
|
+
output(result, program.opts().json, () => success(`Quick message ${itemId} updated`));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
error(`Update quick message failed: ${e.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
qm.command("remove <itemIds...>")
|
|
45
|
+
.description("Remove quick message(s)")
|
|
46
|
+
.action(async (itemIds) => {
|
|
47
|
+
try {
|
|
48
|
+
const ids = itemIds.map(Number);
|
|
49
|
+
const result = await getApi().removeQuickMessage(ids);
|
|
50
|
+
output(result, program.opts().json, () => success(`Removed ${ids.length} quick message(s)`));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
error(`Remove quick message failed: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zalo Official Account API v3.0 client.
|
|
3
|
+
* Wraps OA REST endpoints with node-fetch. No dependency on zca-js.
|
|
4
|
+
* Credentials stored in ~/.zalo-agent/oa-credentials.json.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import nodefetch from "node-fetch";
|
|
11
|
+
|
|
12
|
+
const V3_BASE = "https://openapi.zalo.me/v3.0/oa";
|
|
13
|
+
const V2_BASE = "https://openapi.zalo.me/v2.0/oa";
|
|
14
|
+
const DATA_DIR = join(homedir(), ".zalo-agent");
|
|
15
|
+
const OA_CREDS_FILE = join(DATA_DIR, "oa-credentials.json");
|
|
16
|
+
|
|
17
|
+
/** Ensure data directory exists. */
|
|
18
|
+
function ensureDir() {
|
|
19
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const OAUTH_URL = "https://oauth.zaloapp.com/v4/oa/permission";
|
|
23
|
+
const TOKEN_URL = "https://oauth.zaloapp.com/v4/oa/access_token";
|
|
24
|
+
|
|
25
|
+
/** Save full OA credentials to disk with restricted file permissions. */
|
|
26
|
+
export function saveOACreds(data, oaId = "default") {
|
|
27
|
+
ensureDir();
|
|
28
|
+
let creds = {};
|
|
29
|
+
if (fs.existsSync(OA_CREDS_FILE)) {
|
|
30
|
+
creds = JSON.parse(fs.readFileSync(OA_CREDS_FILE, "utf8"));
|
|
31
|
+
}
|
|
32
|
+
creds[oaId] = { ...creds[oaId], ...data, updatedAt: new Date().toISOString() };
|
|
33
|
+
fs.writeFileSync(OA_CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Save OA access token to disk (backward compat). */
|
|
37
|
+
export function saveOAToken(accessToken, oaId = "default") {
|
|
38
|
+
saveOACreds({ accessToken }, oaId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Load full OA credentials from disk. */
|
|
42
|
+
export function loadOACreds(oaId = "default") {
|
|
43
|
+
if (!fs.existsSync(OA_CREDS_FILE)) return null;
|
|
44
|
+
const creds = JSON.parse(fs.readFileSync(OA_CREDS_FILE, "utf8"));
|
|
45
|
+
return creds[oaId] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Load OA access token from disk. */
|
|
49
|
+
export function loadOAToken(oaId = "default") {
|
|
50
|
+
return loadOACreds(oaId)?.accessToken || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Get token or throw. */
|
|
54
|
+
function getToken(oaId) {
|
|
55
|
+
const creds = loadOACreds(oaId);
|
|
56
|
+
if (!creds?.accessToken) {
|
|
57
|
+
throw new Error("OA not configured. Run: zalo-agent oa login --app-id <id> --secret <key>");
|
|
58
|
+
}
|
|
59
|
+
return creds.accessToken;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Build OAuth authorization URL. */
|
|
63
|
+
export function getOAuthUrl(appId, redirectUri = "http://localhost:3456/callback") {
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
app_id: appId,
|
|
66
|
+
redirect_uri: redirectUri,
|
|
67
|
+
});
|
|
68
|
+
return `${OAUTH_URL}?${params}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Exchange authorization code for access + refresh tokens. */
|
|
72
|
+
export async function exchangeCode(code, appId, secretKey, redirectUri = "http://localhost:3456/callback") {
|
|
73
|
+
const res = await nodefetch(TOKEN_URL, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", secret_key: secretKey },
|
|
76
|
+
body: new URLSearchParams({
|
|
77
|
+
code,
|
|
78
|
+
app_id: appId,
|
|
79
|
+
grant_type: "authorization_code",
|
|
80
|
+
redirect_uri: redirectUri,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
return res.json();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Refresh access token using refresh token. */
|
|
87
|
+
export async function refreshAccessToken(refreshToken, appId, secretKey) {
|
|
88
|
+
const res = await nodefetch(TOKEN_URL, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", secret_key: secretKey },
|
|
91
|
+
body: new URLSearchParams({
|
|
92
|
+
refresh_token: refreshToken,
|
|
93
|
+
app_id: appId,
|
|
94
|
+
grant_type: "refresh_token",
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Make authenticated request to Zalo OA API. */
|
|
101
|
+
async function oaFetch(url, { method = "GET", body, token, isFormData = false } = {}) {
|
|
102
|
+
const headers = { access_token: token };
|
|
103
|
+
if (!isFormData) headers["Content-Type"] = "application/json";
|
|
104
|
+
|
|
105
|
+
const opts = { method, headers };
|
|
106
|
+
if (body && !isFormData) opts.body = JSON.stringify(body);
|
|
107
|
+
if (body && isFormData) opts.body = body;
|
|
108
|
+
|
|
109
|
+
const res = await nodefetch(url, opts);
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (data.error && data.error !== 0) {
|
|
112
|
+
throw new Error(`OA API error ${data.error}: ${data.message || JSON.stringify(data)}`);
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Messaging (v3.0) ────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const VALID_MSG_TYPES = ["cs", "transaction", "promotion"];
|
|
120
|
+
|
|
121
|
+
/** Validate messageType to prevent path injection. */
|
|
122
|
+
function validateMsgType(messageType) {
|
|
123
|
+
if (!VALID_MSG_TYPES.includes(messageType)) {
|
|
124
|
+
throw new Error(`Invalid message type "${messageType}". Must be: ${VALID_MSG_TYPES.join(", ")}`);
|
|
125
|
+
}
|
|
126
|
+
return messageType;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Send text message via OA. messageType: cs | transaction | promotion */
|
|
130
|
+
export async function sendText(userId, text, messageType = "cs", oaId = "default") {
|
|
131
|
+
const token = getToken(oaId);
|
|
132
|
+
return oaFetch(`${V3_BASE}/message/${validateMsgType(messageType)}`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
token,
|
|
135
|
+
body: { recipient: { user_id: userId }, message: { text } },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Send image message via OA (by URL or attachment_id). */
|
|
140
|
+
export async function sendImage(userId, { imageUrl, imageId }, messageType = "cs", oaId = "default") {
|
|
141
|
+
const token = getToken(oaId);
|
|
142
|
+
const element = { media_type: "image" };
|
|
143
|
+
if (imageUrl) element.url = imageUrl;
|
|
144
|
+
else if (imageId) element.attachment_id = imageId;
|
|
145
|
+
else throw new Error("Provide --image-url or --image-id");
|
|
146
|
+
|
|
147
|
+
return oaFetch(`${V3_BASE}/message/${validateMsgType(messageType)}`, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
token,
|
|
150
|
+
body: {
|
|
151
|
+
recipient: { user_id: userId },
|
|
152
|
+
message: {
|
|
153
|
+
attachment: {
|
|
154
|
+
type: "template",
|
|
155
|
+
payload: { template_type: "media", elements: [element] },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Send file message via OA (requires pre-uploaded file attachment_id). */
|
|
163
|
+
export async function sendFile(userId, fileId, messageType = "cs", oaId = "default") {
|
|
164
|
+
const token = getToken(oaId);
|
|
165
|
+
return oaFetch(`${V3_BASE}/message/${validateMsgType(messageType)}`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
token,
|
|
168
|
+
body: {
|
|
169
|
+
recipient: { user_id: userId },
|
|
170
|
+
message: { attachment: { type: "file", payload: { token: fileId } } },
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Send list message via OA. elements: [{ title, subtitle, image_url, default_action }] */
|
|
176
|
+
export async function sendList(userId, elements, messageType = "cs", oaId = "default") {
|
|
177
|
+
const token = getToken(oaId);
|
|
178
|
+
return oaFetch(`${V3_BASE}/message/${validateMsgType(messageType)}`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
token,
|
|
181
|
+
body: {
|
|
182
|
+
recipient: { user_id: userId },
|
|
183
|
+
message: {
|
|
184
|
+
attachment: {
|
|
185
|
+
type: "template",
|
|
186
|
+
payload: { template_type: "list", elements },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Get message delivery status. */
|
|
194
|
+
export async function getMessageStatus(messageId, oaId = "default") {
|
|
195
|
+
const token = getToken(oaId);
|
|
196
|
+
return oaFetch(`${V3_BASE}/message/status?message_id=${messageId}`, { token });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── User / Follower Management ──────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/** Get OA profile info. */
|
|
202
|
+
export async function getOAProfile(oaId = "default") {
|
|
203
|
+
const token = getToken(oaId);
|
|
204
|
+
// v2 fallback — v3 docs incomplete for getoa
|
|
205
|
+
return oaFetch(`${V2_BASE}/getoa`, { token });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Get follower info by user_id. */
|
|
209
|
+
export async function getFollowerInfo(userId, oaId = "default") {
|
|
210
|
+
const token = getToken(oaId);
|
|
211
|
+
return oaFetch(`${V3_BASE}/user/detail?user_id=${userId}`, { token });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Get followers list (paginated). */
|
|
215
|
+
export async function getFollowers(offset = 0, count = 50, oaId = "default") {
|
|
216
|
+
const token = getToken(oaId);
|
|
217
|
+
return oaFetch(`${V3_BASE}/user/getlist?offset=${offset}&count=${count}`, { token });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Update follower info (name, phone, address, etc.). */
|
|
221
|
+
export async function updateFollowerInfo(userId, updates, oaId = "default") {
|
|
222
|
+
const token = getToken(oaId);
|
|
223
|
+
return oaFetch(`${V3_BASE}/user/update`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
token,
|
|
226
|
+
body: { user_id: userId, ...updates },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Tags ────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/** Get all tags. */
|
|
233
|
+
export async function getTags(oaId = "default") {
|
|
234
|
+
const token = getToken(oaId);
|
|
235
|
+
// v2 endpoint — v3 not documented
|
|
236
|
+
return oaFetch(`${V2_BASE}/tag/gettagsofoa`, { token });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Assign tag to follower. */
|
|
240
|
+
export async function assignTag(userId, tagName, oaId = "default") {
|
|
241
|
+
const token = getToken(oaId);
|
|
242
|
+
return oaFetch(`${V3_BASE}/tag/taguser`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
token,
|
|
245
|
+
body: { user_id: userId, tag_name: tagName },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Remove tag. */
|
|
250
|
+
export async function removeTag(tagName, oaId = "default") {
|
|
251
|
+
const token = getToken(oaId);
|
|
252
|
+
return oaFetch(`${V3_BASE}/tag/rmtag`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
token,
|
|
255
|
+
body: { tag_name: tagName },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Remove follower from tag. */
|
|
260
|
+
export async function removeFollowerFromTag(userId, tagName, oaId = "default") {
|
|
261
|
+
const token = getToken(oaId);
|
|
262
|
+
return oaFetch(`${V3_BASE}/tag/rmfollowerfromtag`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
token,
|
|
265
|
+
body: { user_id: userId, tag_name: tagName },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Media Upload ────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/** Upload image to OA (returns attachment_id). */
|
|
272
|
+
export async function uploadImage(filePath, oaId = "default") {
|
|
273
|
+
const token = getToken(oaId);
|
|
274
|
+
const { default: FormData } = await import("node-fetch");
|
|
275
|
+
// Use native FormData-like via node-fetch's Blob
|
|
276
|
+
const fileData = fs.readFileSync(filePath);
|
|
277
|
+
const blob = new (await import("node:buffer")).Blob([fileData]);
|
|
278
|
+
const form = new globalThis.FormData();
|
|
279
|
+
form.append("file", blob, filePath.split("/").pop());
|
|
280
|
+
|
|
281
|
+
return oaFetch(`${V3_BASE}/upload/image`, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
token,
|
|
284
|
+
body: form,
|
|
285
|
+
isFormData: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Upload file to OA (returns token/attachment_id). */
|
|
290
|
+
export async function uploadFile(filePath, oaId = "default") {
|
|
291
|
+
const token = getToken(oaId);
|
|
292
|
+
const fileData = fs.readFileSync(filePath);
|
|
293
|
+
const blob = new (await import("node:buffer")).Blob([fileData]);
|
|
294
|
+
const form = new globalThis.FormData();
|
|
295
|
+
form.append("file", blob, filePath.split("/").pop());
|
|
296
|
+
|
|
297
|
+
return oaFetch(`${V3_BASE}/upload/file`, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
token,
|
|
300
|
+
body: form,
|
|
301
|
+
isFormData: true,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Conversations ───────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
/** Get recent chat list (v2 API). */
|
|
308
|
+
export async function getRecentChat(offset = 0, count = 10, oaId = "default") {
|
|
309
|
+
const token = getToken(oaId);
|
|
310
|
+
return oaFetch(`${V2_BASE}/listrecentchat?offset=${offset}&count=${count}`, { token });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Get conversation history with a user (v2 API). */
|
|
314
|
+
export async function getConversation(userId, offset = 0, count = 10, oaId = "default") {
|
|
315
|
+
const token = getToken(oaId);
|
|
316
|
+
return oaFetch(`${V2_BASE}/conversation?user_id=${userId}&offset=${offset}&count=${count}`, {
|
|
317
|
+
token,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Menu ────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
/** Update OA menu. */
|
|
324
|
+
export async function updateMenu(menuData, oaId = "default") {
|
|
325
|
+
const token = getToken(oaId);
|
|
326
|
+
return oaFetch(`${V3_BASE}/menu`, { method: "POST", token, body: menuData });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Articles ────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
/** Create article (broadcast). */
|
|
332
|
+
export async function createArticle(articleData, oaId = "default") {
|
|
333
|
+
const token = getToken(oaId);
|
|
334
|
+
return oaFetch(`${V3_BASE}/article/create`, { method: "POST", token, body: articleData });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Get article list. */
|
|
338
|
+
export async function getArticleList(offset = 0, limit = 10, oaId = "default") {
|
|
339
|
+
const token = getToken(oaId);
|
|
340
|
+
return oaFetch(`${V3_BASE}/article/getlist?offset=${offset}&limit=${limit}`, { token });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Get article detail. */
|
|
344
|
+
export async function getArticleDetail(articleId, oaId = "default") {
|
|
345
|
+
const token = getToken(oaId);
|
|
346
|
+
return oaFetch(`${V3_BASE}/article/getdetail?id=${articleId}`, { token });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── Store ───────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
/** Create product in OA store. */
|
|
352
|
+
export async function createProduct(productData, oaId = "default") {
|
|
353
|
+
const token = getToken(oaId);
|
|
354
|
+
return oaFetch(`${V3_BASE}/store/product/create`, { method: "POST", token, body: productData });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Get product list. */
|
|
358
|
+
export async function getProductList(offset = 0, limit = 10, oaId = "default") {
|
|
359
|
+
const token = getToken(oaId);
|
|
360
|
+
return oaFetch(`${V3_BASE}/store/product/getproductofoa?offset=${offset}&limit=${limit}`, {
|
|
361
|
+
token,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Get product detail. */
|
|
366
|
+
export async function getProductInfo(productId, oaId = "default") {
|
|
367
|
+
const token = getToken(oaId);
|
|
368
|
+
return oaFetch(`${V3_BASE}/store/product/getproduct?id=${productId}`, { token });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Create category in OA store. */
|
|
372
|
+
export async function createCategory(categoryData, oaId = "default") {
|
|
373
|
+
const token = getToken(oaId);
|
|
374
|
+
return oaFetch(`${V3_BASE}/store/category/create`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
token,
|
|
377
|
+
body: categoryData,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Get category list. */
|
|
382
|
+
export async function getCategoryList(oaId = "default") {
|
|
383
|
+
const token = getToken(oaId);
|
|
384
|
+
return oaFetch(`${V3_BASE}/store/category/getcategoryofoa`, { token });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Create order. */
|
|
388
|
+
export async function createOrder(orderData, oaId = "default") {
|
|
389
|
+
const token = getToken(oaId);
|
|
390
|
+
return oaFetch(`${V3_BASE}/store/order/create`, { method: "POST", token, body: orderData });
|
|
391
|
+
}
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,12 @@ import { registerAccountCommands } from "./commands/account.js";
|
|
|
21
21
|
import { registerProfileCommands } from "./commands/profile.js";
|
|
22
22
|
import { registerPollCommands } from "./commands/poll.js";
|
|
23
23
|
import { registerReminderCommands } from "./commands/reminder.js";
|
|
24
|
+
import { registerAutoReplyCommands } from "./commands/auto-reply.js";
|
|
25
|
+
import { registerQuickMsgCommands } from "./commands/quick-msg.js";
|
|
26
|
+
import { registerLabelCommands } from "./commands/label.js";
|
|
27
|
+
import { registerCatalogCommands } from "./commands/catalog.js";
|
|
24
28
|
import { registerListenCommand } from "./commands/listen.js";
|
|
29
|
+
import { registerOACommands } from "./commands/oa.js";
|
|
25
30
|
import { autoLogin } from "./core/zalo-client.js";
|
|
26
31
|
import { checkForUpdates, selfUpdate } from "./utils/update-check.js";
|
|
27
32
|
import { success, error, warning } from "./utils/output.js";
|
|
@@ -37,16 +42,17 @@ program
|
|
|
37
42
|
.version(pkg.version)
|
|
38
43
|
.option("--json", "Output results as JSON (machine-readable)")
|
|
39
44
|
.hook("preAction", async (thisCommand) => {
|
|
45
|
+
const cmdName = thisCommand.args?.[0] || thisCommand.name();
|
|
40
46
|
// Suppress zca-js internal logs in JSON mode to keep stdout clean for piping
|
|
41
47
|
if (program.opts().json) {
|
|
42
48
|
process.env.ZALO_JSON_MODE = "1";
|
|
43
|
-
} else {
|
|
49
|
+
} else if (cmdName !== "oa") {
|
|
50
|
+
// OA commands use official Zalo API — no disclaimer needed
|
|
44
51
|
warning(DISCLAIMER);
|
|
45
52
|
console.log();
|
|
46
53
|
}
|
|
47
|
-
// Auto-login before any command that needs it (skip for login/account commands)
|
|
48
|
-
const
|
|
49
|
-
const skipAutoLogin = ["login", "account", "help", "version", "update"].includes(cmdName);
|
|
54
|
+
// Auto-login before any command that needs it (skip for login/account/oa commands)
|
|
55
|
+
const skipAutoLogin = ["login", "account", "help", "version", "update", "oa"].includes(cmdName);
|
|
50
56
|
if (!skipAutoLogin) {
|
|
51
57
|
await autoLogin(program.opts().json);
|
|
52
58
|
}
|
|
@@ -76,6 +82,11 @@ registerAccountCommands(program);
|
|
|
76
82
|
registerProfileCommands(program);
|
|
77
83
|
registerPollCommands(program);
|
|
78
84
|
registerReminderCommands(program);
|
|
85
|
+
registerAutoReplyCommands(program);
|
|
86
|
+
registerQuickMsgCommands(program);
|
|
87
|
+
registerLabelCommands(program);
|
|
88
|
+
registerCatalogCommands(program);
|
|
79
89
|
registerListenCommand(program);
|
|
90
|
+
registerOACommands(program);
|
|
80
91
|
|
|
81
92
|
program.parse();
|