@ardennguyen/zalo-agent-cli 2.2.2 → 2.5.1
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 +6 -6
- package/package.json +1 -1
- package/src/commands/conv.js +8 -1
- package/src/commands/listen.js +144 -42
- package/src/commands/login.js +16 -3
- package/src/commands/mcp.js +335 -77
- package/src/commands/profile.js +6 -2
- package/src/commands/sync.js +3 -1
- package/src/core/accounts.js +20 -0
- package/src/core/daemon-channel.js +419 -2
- package/src/core/daemon-sync.js +13 -2
- package/src/core/db.js +60 -3
- package/src/core/live-store.js +58 -1
- package/src/core/sync-v2/index.js +27 -3
- package/src/index.js +3 -0
- package/src/mcp/mcp-config.js +110 -8
- package/src/mcp/mcp-http-transport.js +529 -14
- package/src/mcp/mcp-tools.js +305 -14
package/src/mcp/mcp-tools.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tool registrations for Zalo message access, sending, user lookup, and group productivity.
|
|
3
|
-
* Registers
|
|
3
|
+
* Registers 23 tools: zalo_get_messages, zalo_get_history, zalo_send_message, zalo_list_threads,
|
|
4
4
|
* zalo_search_threads, zalo_mark_read, zalo_view_media, zalo_react, zalo_undo, zalo_get_group_members,
|
|
5
5
|
* zalo_list_conversations, zalo_coverage, zalo_sync, zalo_sync_status, zalo_search_messages,
|
|
6
|
-
* zalo_lookup_user, zalo_forward_message, zalo_delete_message, zalo_reminder, zalo_note, zalo_poll
|
|
6
|
+
* zalo_lookup_user, zalo_forward_message, zalo_delete_message, zalo_reminder, zalo_note, zalo_poll,
|
|
7
|
+
* zalo_wait_for_message, zalo_label.
|
|
7
8
|
*
|
|
8
9
|
* A tool that mirrors a CLI command calls the same code the command does (src/core/cached-message.js,
|
|
9
10
|
* forward.js, pin.js, group-members.js, recent-conversations.js, src/utils/my-documents.js, urgency.js),
|
|
@@ -459,8 +460,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
459
460
|
{
|
|
460
461
|
title: "Send Zalo Message",
|
|
461
462
|
description:
|
|
462
|
-
"Send a text message,
|
|
463
|
-
"(DM or group). threadType: 0=DM(User), 1=Group. " +
|
|
463
|
+
"Send a text message, file/image attachment (`filePath` or `files`), link preview card (`link`), or " +
|
|
464
|
+
"contact card (`cardUserId`) to a Zalo thread (DM or group). threadType: 0=DM(User), 1=Group. " +
|
|
464
465
|
"To @-mention someone in a group, put `@[uid]` in the text where the tag belongs — " +
|
|
465
466
|
"`@[123456]` becomes `@Their Name` and notifies them, and `@[-1]` is @All. " +
|
|
466
467
|
"Use the `senderId` from zalo_get_messages or zalo_get_history as the uid; the display name " +
|
|
@@ -480,8 +481,22 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
480
481
|
.min(1)
|
|
481
482
|
.optional()
|
|
482
483
|
.describe(
|
|
483
|
-
"Message text to send (required unless `
|
|
484
|
-
"`@[uid]` tokens expand to @-mentions; `@[-1]` is @All.",
|
|
484
|
+
"Message text to send (required unless `filePath`, `files`, `link`, or `cardUserId` is set; " +
|
|
485
|
+
"optional caption when `filePath`, `files`, or `link` is set). `@[uid]` tokens expand to @-mentions; `@[-1]` is @All.",
|
|
486
|
+
),
|
|
487
|
+
filePath: z
|
|
488
|
+
.string()
|
|
489
|
+
.min(1)
|
|
490
|
+
.optional()
|
|
491
|
+
.describe(
|
|
492
|
+
"Optional local file or image path to send as an attachment (uses `text` as optional caption)",
|
|
493
|
+
),
|
|
494
|
+
files: z
|
|
495
|
+
.array(z.string().min(1))
|
|
496
|
+
.min(1)
|
|
497
|
+
.optional()
|
|
498
|
+
.describe(
|
|
499
|
+
"Optional list of local file or image paths to send as attachments in one call (uses `text` as optional caption)",
|
|
485
500
|
),
|
|
486
501
|
link: z
|
|
487
502
|
.string()
|
|
@@ -519,17 +534,27 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
519
534
|
),
|
|
520
535
|
})
|
|
521
536
|
.refine(
|
|
522
|
-
(val) =>
|
|
523
|
-
|
|
537
|
+
(val) =>
|
|
538
|
+
Boolean(
|
|
539
|
+
val.text ||
|
|
540
|
+
val.link ||
|
|
541
|
+
val.cardUserId ||
|
|
542
|
+
val.filePath ||
|
|
543
|
+
(Array.isArray(val.files) && val.files.length > 0),
|
|
544
|
+
),
|
|
545
|
+
"Provide at least one of text, filePath, files, link, or cardUserId",
|
|
524
546
|
)
|
|
525
|
-
.refine(
|
|
526
|
-
(val
|
|
527
|
-
|
|
528
|
-
|
|
547
|
+
.refine((val) => {
|
|
548
|
+
const hasFile = Boolean(val.filePath || (Array.isArray(val.files) && val.files.length > 0));
|
|
549
|
+
const modes = [Boolean(val.link), Boolean(val.cardUserId), hasFile].filter(Boolean).length;
|
|
550
|
+
return modes <= 1;
|
|
551
|
+
}, "Cannot combine link, cardUserId, and file attachments in a single call"),
|
|
529
552
|
},
|
|
530
553
|
async ({
|
|
531
554
|
threadId: threadIdIn,
|
|
532
555
|
text,
|
|
556
|
+
filePath,
|
|
557
|
+
files,
|
|
533
558
|
link,
|
|
534
559
|
cardUserId,
|
|
535
560
|
threadType: threadTypeIn,
|
|
@@ -576,8 +601,44 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
576
601
|
});
|
|
577
602
|
}
|
|
578
603
|
|
|
604
|
+
const rawFiles = [...(filePath ? [filePath] : []), ...(Array.isArray(files) ? files : [])];
|
|
605
|
+
if (rawFiles.length > 0) {
|
|
606
|
+
const absPaths = rawFiles.map((p) => resolve(String(p).trim()));
|
|
607
|
+
const missing = absPaths.filter((p) => !existsSync(p));
|
|
608
|
+
if (missing.length > 0) {
|
|
609
|
+
return err(`File not found: ${missing.join(", ")}`);
|
|
610
|
+
}
|
|
611
|
+
const result = await api.sendMessage(
|
|
612
|
+
{ msg: text || "", attachments: absPaths },
|
|
613
|
+
threadId,
|
|
614
|
+
Number(threadType),
|
|
615
|
+
);
|
|
616
|
+
const rawAtt = result?.attachment;
|
|
617
|
+
const attRows = rawAtt ? (Array.isArray(rawAtt) ? rawAtt : [rawAtt]) : [];
|
|
618
|
+
const sent = attRows
|
|
619
|
+
.filter((r) => (r?.msgId ?? null) !== null)
|
|
620
|
+
.map((r) => ({
|
|
621
|
+
msgId: String(r.msgId),
|
|
622
|
+
cliMsgId: (r.cliMsgId ?? null) === null ? null : String(r.cliMsgId),
|
|
623
|
+
}));
|
|
624
|
+
const messageId =
|
|
625
|
+
result?.message?.msgId ?? result?.msgId ?? (sent.length > 0 ? sent[0].msgId : null);
|
|
626
|
+
const cliMsgId =
|
|
627
|
+
result?.message?.cliMsgId ?? result?.cliMsgId ?? (sent.length > 0 ? sent[0].cliMsgId : null);
|
|
628
|
+
return ok({
|
|
629
|
+
success: true,
|
|
630
|
+
kind: "attachment",
|
|
631
|
+
files: absPaths,
|
|
632
|
+
sent,
|
|
633
|
+
messageId,
|
|
634
|
+
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
635
|
+
threadType,
|
|
636
|
+
...(thread.notice && { threadId, notice: thread.notice }),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
579
640
|
if (!text) {
|
|
580
|
-
return err("Provide text, link, or cardUserId.");
|
|
641
|
+
return err("Provide text, filePath, files, link, or cardUserId.");
|
|
581
642
|
}
|
|
582
643
|
|
|
583
644
|
const warnings = [];
|
|
@@ -1490,9 +1551,11 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1490
1551
|
if (self) {
|
|
1491
1552
|
const info = await api.fetchAccountInfo();
|
|
1492
1553
|
const p = info?.profile || info || {};
|
|
1554
|
+
const ownId = ownIdOf(api) || p.userId || null;
|
|
1555
|
+
if (p.displayName || p.zaloName) syncJobs?.onSelfName?.(p.displayName || p.zaloName);
|
|
1493
1556
|
return ok({
|
|
1494
1557
|
mode: "self",
|
|
1495
|
-
ownId
|
|
1558
|
+
ownId,
|
|
1496
1559
|
displayName: p.displayName || p.zaloName || null,
|
|
1497
1560
|
zaloName: p.zaloName || null,
|
|
1498
1561
|
phoneNumber: p.phoneNumber || null,
|
|
@@ -2258,4 +2321,232 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2258
2321
|
}
|
|
2259
2322
|
},
|
|
2260
2323
|
);
|
|
2324
|
+
|
|
2325
|
+
// --- zalo_wait_for_message ---
|
|
2326
|
+
server.registerTool(
|
|
2327
|
+
"zalo_wait_for_message",
|
|
2328
|
+
{
|
|
2329
|
+
title: "Wait for Incoming Zalo Message",
|
|
2330
|
+
description:
|
|
2331
|
+
"Long-poll the live MCP message buffer for the next incoming watched message without manual polling. " +
|
|
2332
|
+
"Returns immediately if a buffered message newer than `since` is already waiting; otherwise waits up to " +
|
|
2333
|
+
"`timeoutSec` seconds (default 55, max 300) for a new message to arrive. Mirrors GET /events/wait for " +
|
|
2334
|
+
"remote HTTPS/OAuth2 MCP clients.",
|
|
2335
|
+
inputSchema: z.object({
|
|
2336
|
+
threadId: z
|
|
2337
|
+
.string()
|
|
2338
|
+
.min(1)
|
|
2339
|
+
.optional()
|
|
2340
|
+
.describe("Optional thread ID to wait on (omit to wait for a message in any watched thread)"),
|
|
2341
|
+
since: z
|
|
2342
|
+
.number()
|
|
2343
|
+
.int()
|
|
2344
|
+
.min(0)
|
|
2345
|
+
.optional()
|
|
2346
|
+
.describe(
|
|
2347
|
+
"Optional buffer cursor (`_cursor` from a prior message or `cursor` from a prior wait). " +
|
|
2348
|
+
"If a buffered message with `_cursor > since` already exists, returns immediately.",
|
|
2349
|
+
),
|
|
2350
|
+
timeoutSec: z
|
|
2351
|
+
.number()
|
|
2352
|
+
.int()
|
|
2353
|
+
.min(1)
|
|
2354
|
+
.max(300)
|
|
2355
|
+
.default(55)
|
|
2356
|
+
.describe("Maximum seconds to wait before returning `status: 'timeout'` (default 55, max 300)"),
|
|
2357
|
+
}),
|
|
2358
|
+
},
|
|
2359
|
+
async ({ threadId, since, timeoutSec = 55 }) => {
|
|
2360
|
+
try {
|
|
2361
|
+
if (typeof buffer?.waitForMessage !== "function") {
|
|
2362
|
+
return err("Live message buffer does not support waitForMessage in this context.");
|
|
2363
|
+
}
|
|
2364
|
+
const cleanThreadId = typeof threadId === "string" && threadId.trim() ? threadId.trim() : null;
|
|
2365
|
+
const clampedSec = Math.max(1, Math.min(300, Number(timeoutSec) || 55));
|
|
2366
|
+
const msg = await buffer.waitForMessage({
|
|
2367
|
+
threadId: cleanThreadId,
|
|
2368
|
+
since: typeof since === "number" && Number.isFinite(since) ? since : undefined,
|
|
2369
|
+
timeoutMs: clampedSec * 1000,
|
|
2370
|
+
});
|
|
2371
|
+
if (msg) {
|
|
2372
|
+
const tid = String(msg.threadId || "");
|
|
2373
|
+
let threadName = nameCache?.get(tid)?.name || null;
|
|
2374
|
+
if (!threadName && tid) {
|
|
2375
|
+
try {
|
|
2376
|
+
threadName = getDisplayName(tid) || null;
|
|
2377
|
+
} catch {
|
|
2378
|
+
threadName = null;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
return ok({
|
|
2382
|
+
status: "message",
|
|
2383
|
+
message: {
|
|
2384
|
+
...msg,
|
|
2385
|
+
...(threadName ? { threadName } : {}),
|
|
2386
|
+
},
|
|
2387
|
+
cursor: msg._cursor,
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return ok({
|
|
2391
|
+
status: "timeout",
|
|
2392
|
+
message: null,
|
|
2393
|
+
cursor: Math.max(
|
|
2394
|
+
typeof since === "number" && Number.isFinite(since) ? since : 0,
|
|
2395
|
+
Number(buffer._globalCursor) || 0,
|
|
2396
|
+
typeof buffer.readCursor === "function" ? buffer.readCursor() : 0,
|
|
2397
|
+
),
|
|
2398
|
+
});
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
console.error("[mcp-tools] zalo_wait_for_message error:", e.message);
|
|
2401
|
+
return err(e.message);
|
|
2402
|
+
}
|
|
2403
|
+
},
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
// --- zalo_label ---
|
|
2407
|
+
server.registerTool(
|
|
2408
|
+
"zalo_label",
|
|
2409
|
+
{
|
|
2410
|
+
title: "Manage Zalo Conversation Labels",
|
|
2411
|
+
description:
|
|
2412
|
+
"List, assign, unassign, or update Zalo conversation/contact labels (tags such as 'Nội bộ', 'Khách hàng', 'Đối tác'). " +
|
|
2413
|
+
"Actions: 'list' (returns all labels with their IDs, text, colors, conversations array, and version), " +
|
|
2414
|
+
"'assign' (adds threadId or threadIds to a label identified by labelId or labelText), " +
|
|
2415
|
+
"'unassign' (removes threadId or threadIds from a label identified by labelId or labelText), " +
|
|
2416
|
+
"'update' (raw update with full labelData array and version, matching 'label update').",
|
|
2417
|
+
inputSchema: z.object({
|
|
2418
|
+
action: z
|
|
2419
|
+
.enum(["list", "assign", "unassign", "update"])
|
|
2420
|
+
.describe("Label operation to perform: 'list', 'assign', 'unassign', or 'update'"),
|
|
2421
|
+
labelId: z
|
|
2422
|
+
.number()
|
|
2423
|
+
.int()
|
|
2424
|
+
.optional()
|
|
2425
|
+
.describe("Numeric label ID (for 'assign' or 'unassign'; optional if labelText is provided)"),
|
|
2426
|
+
labelText: z
|
|
2427
|
+
.string()
|
|
2428
|
+
.min(1)
|
|
2429
|
+
.optional()
|
|
2430
|
+
.describe(
|
|
2431
|
+
"Label name/text, e.g. 'Nội bộ', 'Khách hàng', 'Đối tác' (case-insensitive match for 'assign' or 'unassign')",
|
|
2432
|
+
),
|
|
2433
|
+
threadId: z.string().min(1).optional().describe("Single thread ID to assign or unassign"),
|
|
2434
|
+
threadIds: z
|
|
2435
|
+
.array(z.string().min(1))
|
|
2436
|
+
.optional()
|
|
2437
|
+
.describe("Multiple thread IDs to assign or unassign in one call"),
|
|
2438
|
+
labelData: z
|
|
2439
|
+
.array(z.any())
|
|
2440
|
+
.optional()
|
|
2441
|
+
.describe("Full labelData array from action='list' (required for raw action='update')"),
|
|
2442
|
+
version: z
|
|
2443
|
+
.number()
|
|
2444
|
+
.int()
|
|
2445
|
+
.optional()
|
|
2446
|
+
.describe("Label version integer from action='list' (required for raw action='update')"),
|
|
2447
|
+
}),
|
|
2448
|
+
},
|
|
2449
|
+
async ({ action, labelId, labelText, threadId, threadIds, labelData, version }) => {
|
|
2450
|
+
try {
|
|
2451
|
+
if (action === "list") {
|
|
2452
|
+
const res = await api.getLabels();
|
|
2453
|
+
return ok({
|
|
2454
|
+
action: "list",
|
|
2455
|
+
labelData: Array.isArray(res?.labelData) ? res.labelData : [],
|
|
2456
|
+
version: res?.version ?? 0,
|
|
2457
|
+
lastUpdateTime: res?.lastUpdateTime ?? null,
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
if (action === "update") {
|
|
2462
|
+
if (!Array.isArray(labelData) || typeof version !== "number" || !Number.isFinite(version)) {
|
|
2463
|
+
return err("labelData (array) and version (number) are required for action='update'.");
|
|
2464
|
+
}
|
|
2465
|
+
const updated = await api.updateLabels({
|
|
2466
|
+
labelData,
|
|
2467
|
+
version: Number(version),
|
|
2468
|
+
});
|
|
2469
|
+
return ok({
|
|
2470
|
+
action: "update",
|
|
2471
|
+
success: true,
|
|
2472
|
+
version: updated?.version ?? version,
|
|
2473
|
+
labelData: updated?.labelData ?? labelData,
|
|
2474
|
+
result: updated,
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
if (action === "assign" || action === "unassign") {
|
|
2479
|
+
const targets = [
|
|
2480
|
+
...new Set(
|
|
2481
|
+
[
|
|
2482
|
+
...(threadId ? [String(threadId).trim()] : []),
|
|
2483
|
+
...(Array.isArray(threadIds) ? threadIds.map((t) => String(t).trim()) : []),
|
|
2484
|
+
].filter(Boolean),
|
|
2485
|
+
),
|
|
2486
|
+
];
|
|
2487
|
+
if (targets.length === 0) {
|
|
2488
|
+
return err(`threadId (or threadIds) is required for action='${action}'.`);
|
|
2489
|
+
}
|
|
2490
|
+
if (labelId === undefined && (!labelText || !labelText.trim())) {
|
|
2491
|
+
return err(`labelId or labelText is required for action='${action}'.`);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const current = await api.getLabels();
|
|
2495
|
+
const list = Array.isArray(current?.labelData) ? current.labelData : [];
|
|
2496
|
+
const needle = labelText ? labelText.trim().toLowerCase() : null;
|
|
2497
|
+
const matched =
|
|
2498
|
+
labelId !== undefined
|
|
2499
|
+
? list.find((l) => Number(l.id) === Number(labelId))
|
|
2500
|
+
: list.find(
|
|
2501
|
+
(l) =>
|
|
2502
|
+
String(l.text ?? "")
|
|
2503
|
+
.trim()
|
|
2504
|
+
.toLowerCase() === needle ||
|
|
2505
|
+
String(l.textKey ?? "")
|
|
2506
|
+
.trim()
|
|
2507
|
+
.toLowerCase() === needle,
|
|
2508
|
+
);
|
|
2509
|
+
if (!matched) {
|
|
2510
|
+
const available = list.map((l) => `${l.id}:${l.text}`).join(", ") || "none";
|
|
2511
|
+
return err(
|
|
2512
|
+
`Label not found (${labelId !== undefined ? `labelId=${labelId}` : `labelText="${labelText}"`}). Available labels: ${available}`,
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
const targetSet = new Set(targets);
|
|
2517
|
+
let updatedConvList = [];
|
|
2518
|
+
const nextLabelData = list.map((lbl) => {
|
|
2519
|
+
if (Number(lbl.id) !== Number(matched.id)) return lbl;
|
|
2520
|
+
const existing = Array.isArray(lbl.conversations) ? lbl.conversations.map(String) : [];
|
|
2521
|
+
const nextConvs =
|
|
2522
|
+
action === "assign"
|
|
2523
|
+
? [...new Set([...existing, ...targets])]
|
|
2524
|
+
: existing.filter((c) => !targetSet.has(String(c)));
|
|
2525
|
+
updatedConvList = nextConvs;
|
|
2526
|
+
return { ...lbl, conversations: nextConvs };
|
|
2527
|
+
});
|
|
2528
|
+
const nextVersion = Number(current?.version ?? 0);
|
|
2529
|
+
const updated = await api.updateLabels({
|
|
2530
|
+
labelData: nextLabelData,
|
|
2531
|
+
version: nextVersion,
|
|
2532
|
+
});
|
|
2533
|
+
return ok({
|
|
2534
|
+
action,
|
|
2535
|
+
success: true,
|
|
2536
|
+
labelId: matched.id,
|
|
2537
|
+
labelText: matched.text,
|
|
2538
|
+
threadIds: targets,
|
|
2539
|
+
conversations: updatedConvList,
|
|
2540
|
+
version: updated?.version ?? nextVersion,
|
|
2541
|
+
result: updated,
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
return err(`Unsupported label action: ${action}`);
|
|
2546
|
+
} catch (e) {
|
|
2547
|
+
console.error("[mcp-tools] zalo_label error:", e.message);
|
|
2548
|
+
return err(e.message);
|
|
2549
|
+
}
|
|
2550
|
+
},
|
|
2551
|
+
);
|
|
2261
2552
|
}
|