@ardennguyen/zalo-agent-cli 2.5.6 → 2.5.8
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/mcp.js +3 -0
- package/src/commands/msg.js +37 -13
- package/src/commands/sync.js +12 -1
- package/src/core/sync-v2/index.js +16 -3
- package/src/core/zalo-client.js +7 -3
- package/src/mcp/instructions.js +32 -0
- package/src/mcp/mcp-http-transport.js +2 -2
- package/src/mcp/mcp-server.js +3 -6
- package/src/mcp/mcp-tools.js +229 -158
- package/src/mcp/new-server.js +14 -0
- package/src/utils/literal-newline.js +24 -0
- package/src/utils/split-message.js +166 -0
package/src/mcp/mcp-tools.js
CHANGED
|
@@ -47,6 +47,8 @@ import { expandMentions, shiftStyles, ALL_MENTION_UID } from "../utils/mentions.
|
|
|
47
47
|
import { resolveSelfThread } from "../utils/my-documents.js";
|
|
48
48
|
import { buildQuote, resolveQuoteSender } from "../utils/quote.js";
|
|
49
49
|
import { urgencyLevel } from "../utils/urgency.js";
|
|
50
|
+
import { splitMessage, sendParts } from "../utils/split-message.js";
|
|
51
|
+
import { hasLiteralNewline, LITERAL_NEWLINE_WARNING } from "../utils/literal-newline.js";
|
|
50
52
|
import { markdownToZalo, resolveSendFormat } from "../utils/markdown.js";
|
|
51
53
|
import { MAX_BATCH, MAX_SETTLE_MS, batchParams, waitResult } from "./wait-result.js";
|
|
52
54
|
import {
|
|
@@ -412,21 +414,31 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
412
414
|
{
|
|
413
415
|
title: "Get Zalo Messages",
|
|
414
416
|
description:
|
|
415
|
-
"
|
|
416
|
-
"
|
|
417
|
-
"Each message
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"then UID); senderDisplayNameSource reports alias/zalo/uid. senderName remains the historical snapshot.",
|
|
417
|
+
"Read the new incoming Zalo messages (DMs and groups) from the live buffer, in order. It returns the messages this consumer has not yet marked read; when you have handled them, call zalo_mark_read with the reply's `cursor`. " +
|
|
418
|
+
"To continue exactly after the last reply, pass its `cursor` as `since`. To wait for the next message instead of polling, use zalo_wait_for_message. " +
|
|
419
|
+
"Each message has `msgId` (pass it as `msgId` or `quoteMsgId` to other tools; `id` is the same value, kept for older clients), `threadId`, `threadType` (dm or group), `senderId`, `senderDisplayName` (the current name: your alias, else the Zalo profile name, else the uid; `senderDisplayNameSource` says which), `senderName` (the name when the message was first seen), `text`, `timestamp` (epoch ms), `type`, `threadName` and `readOnZalo`. " +
|
|
420
|
+
"`readOnZalo` is true or false when the account owner has or has not read the message on Zalo (phone or Zalo Web), and null when that is unknown; zalo_mark_read does not change it. " +
|
|
421
|
+
"Returns `{ messages, cursor, hasMore }`.",
|
|
421
422
|
inputSchema: z.object({
|
|
422
|
-
threadId: z
|
|
423
|
+
threadId: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("Only this conversation (its threadId). Omit for all conversations the server watches."),
|
|
423
427
|
since: z
|
|
424
428
|
.number()
|
|
425
429
|
.int()
|
|
426
430
|
.min(0)
|
|
427
431
|
.default(0)
|
|
428
|
-
.describe(
|
|
429
|
-
|
|
432
|
+
.describe(
|
|
433
|
+
"The `cursor` of your last reply, to continue after it. 0 (default) continues after what you marked read with zalo_mark_read.",
|
|
434
|
+
),
|
|
435
|
+
limit: z
|
|
436
|
+
.number()
|
|
437
|
+
.int()
|
|
438
|
+
.min(1)
|
|
439
|
+
.max(100)
|
|
440
|
+
.default(maxPerPoll)
|
|
441
|
+
.describe("Most messages to return in one call (1 to 100)."),
|
|
430
442
|
consumer: consumerSchema,
|
|
431
443
|
}),
|
|
432
444
|
},
|
|
@@ -479,54 +491,50 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
479
491
|
{
|
|
480
492
|
title: "Send Zalo Message",
|
|
481
493
|
description:
|
|
482
|
-
"Send a text
|
|
483
|
-
|
|
484
|
-
"
|
|
485
|
-
"
|
|
486
|
-
|
|
487
|
-
"
|
|
488
|
-
"(reported back as `unresolvedMentions`). Mentions only deliver in groups (threadType 1). " +
|
|
489
|
-
"To reply to a specific message, pass its msgId as `quoteMsgId` — Zalo supports quote-replies " +
|
|
490
|
-
"to text messages only, and the quoted message must already be in the local cache. " +
|
|
491
|
-
'`urgency` marks the message Important or Urgent, as the apps do. `threadId: "me"` sends to ' +
|
|
492
|
-
"My Documents (your own cloud chat); the reply carries its real thread id.",
|
|
494
|
+
"Send a message to a Zalo conversation: text, files or images, a link preview card, or a contact card. Give `threadId` and one kind of content: `text`, `filePath` or `files`, `link`, or `cardUserId`. With files or a link, `text` is an optional caption. " +
|
|
495
|
+
'Text: use real line breaks, never the two characters backslash and n (Zalo shows those as typed). Markdown is converted to Zalo formatting by default; `format: "plain"` sends the text exactly as written. A text over 3000 bytes (a Chinese character is 3) is split into several messages at paragraph or sentence ends, as Zalo Web does, and the reply then lists every part (`splitInto`, `parts`). ' +
|
|
496
|
+
"Mention in a group: put `@[userId]` in the text where the tag belongs (`@[-1]` is @All). Take the user id from a message's `senderId` (zalo_get_messages, zalo_get_history) or from zalo_get_group_members. It is sent as `@Their Name` and notifies that person; a user id the cache cannot name is tagged as the bare id and listed in `unresolvedMentions`. Mentions only work in groups. " +
|
|
497
|
+
"Reply to a message: pass its msgId as `quoteMsgId` (text messages only, and the message must be in the local cache). " +
|
|
498
|
+
'`urgency` marks the message Important or Urgent, as the apps do. `threadId: "me"` sends to the owner\'s My Documents. ' +
|
|
499
|
+
"Returns `{ success, msgId, cliMsgId, threadType }` (`messageId` is the same value as `msgId`, kept for older clients) and, when they apply, `parts` and `warnings`. Keep `msgId` and `cliMsgId` if you may need to react to or recall this message (zalo_react, zalo_undo).",
|
|
493
500
|
inputSchema: z
|
|
494
501
|
.object({
|
|
495
502
|
threadId: z
|
|
496
503
|
.string()
|
|
497
|
-
.describe(
|
|
504
|
+
.describe(
|
|
505
|
+
"The conversation: the other person's user id for a DM, the group id for a group (find one with zalo_search_threads, zalo_list_conversations or zalo_lookup_user). `me` (or your own uid) is My Documents.",
|
|
506
|
+
),
|
|
498
507
|
text: z
|
|
499
508
|
.string()
|
|
500
509
|
.min(1)
|
|
501
510
|
.optional()
|
|
502
511
|
.describe(
|
|
503
|
-
"
|
|
504
|
-
"optional caption when `filePath`, `files`, or `link` is set). `@[uid]` tokens expand to @-mentions; `@[-1]` is @All.",
|
|
512
|
+
"The message. Real line breaks only: never the two characters backslash and n. Markdown is converted unless `format` is 'plain'. Required unless `filePath`, `files`, `link` or `cardUserId` is given; with those it is the caption. `@[userId]` tokens become @-mentions (`@[-1]` is @All).",
|
|
505
513
|
),
|
|
506
514
|
filePath: z
|
|
507
515
|
.string()
|
|
508
516
|
.min(1)
|
|
509
517
|
.optional()
|
|
510
518
|
.describe(
|
|
511
|
-
"
|
|
519
|
+
"Local path of ONE file or image to send, on the machine this server runs on. `text` becomes its caption.",
|
|
512
520
|
),
|
|
513
521
|
files: z
|
|
514
522
|
.array(z.string().min(1))
|
|
515
523
|
.min(1)
|
|
516
524
|
.optional()
|
|
517
525
|
.describe(
|
|
518
|
-
"
|
|
526
|
+
"Local paths of several files or images to send in one call, on the machine this server runs on. `text` is the caption.",
|
|
519
527
|
),
|
|
520
528
|
link: z
|
|
521
529
|
.string()
|
|
522
530
|
.url()
|
|
523
531
|
.optional()
|
|
524
|
-
.describe("
|
|
532
|
+
.describe("A URL to send as a link preview card. `text` is the caption."),
|
|
525
533
|
cardUserId: z
|
|
526
534
|
.string()
|
|
527
535
|
.min(1)
|
|
528
536
|
.optional()
|
|
529
|
-
.describe("
|
|
537
|
+
.describe("The user id whose contact card to share in this conversation."),
|
|
530
538
|
threadType: z
|
|
531
539
|
.number()
|
|
532
540
|
.int()
|
|
@@ -534,15 +542,13 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
534
542
|
.max(1)
|
|
535
543
|
.optional()
|
|
536
544
|
.describe(
|
|
537
|
-
"
|
|
538
|
-
"used, which is what the CLI does — pass a value only to override that.",
|
|
545
|
+
"0 = DM, 1 = group. Leave it out: the type stored for this thread is used. Pass it only to override that.",
|
|
539
546
|
),
|
|
540
547
|
quoteMsgId: z
|
|
541
548
|
.string()
|
|
542
549
|
.optional()
|
|
543
550
|
.describe(
|
|
544
|
-
"msgId of a
|
|
545
|
-
"Get it from zalo_get_messages or zalo_get_history.",
|
|
551
|
+
"msgId of a TEXT message in this conversation to quote in your reply (from zalo_get_messages or zalo_get_history; it must be in the local cache). Photos, stickers and other non-text messages cannot be quoted.",
|
|
546
552
|
),
|
|
547
553
|
format: z
|
|
548
554
|
.enum(["markdown", "plain"])
|
|
@@ -609,6 +615,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
609
615
|
kind: "card",
|
|
610
616
|
cardUserId,
|
|
611
617
|
messageId,
|
|
618
|
+
msgId: messageId,
|
|
612
619
|
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
613
620
|
threadType,
|
|
614
621
|
...(thread.notice && { threadId, notice: thread.notice }),
|
|
@@ -625,6 +632,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
625
632
|
kind: "link",
|
|
626
633
|
link,
|
|
627
634
|
messageId,
|
|
635
|
+
msgId: messageId,
|
|
628
636
|
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
629
637
|
threadType,
|
|
630
638
|
...(thread.notice && { threadId, notice: thread.notice }),
|
|
@@ -670,6 +678,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
670
678
|
files: absPaths,
|
|
671
679
|
sent,
|
|
672
680
|
messageId,
|
|
681
|
+
msgId: messageId,
|
|
673
682
|
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
674
683
|
threadType,
|
|
675
684
|
...(thread.notice && { threadId, notice: thread.notice }),
|
|
@@ -747,27 +756,38 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
747
756
|
}
|
|
748
757
|
}
|
|
749
758
|
|
|
759
|
+
// Never changes the text: it only tells the agent, in this reply, what it sent.
|
|
760
|
+
if (hasLiteralNewline(text)) warnings.push(LITERAL_NEWLINE_WARNING);
|
|
761
|
+
|
|
750
762
|
// 1 (important) or 2 (urgent), mapped exactly as `msg send
|
|
751
763
|
// --urgency` maps it; zca-js turns it into the `metaData:
|
|
752
764
|
// {urgency}` Zalo Web sends. "normal" is no urgency at all.
|
|
753
765
|
const urgency = urgencyIn ? urgencyLevel(urgencyIn) : null;
|
|
754
766
|
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
+
// Zalo refuses one oversized message (114 plain, 112 styled), so a long text goes out as Web sends
|
|
768
|
+
// it: split at 3000 bytes of text plus styles, in order, 300 ms apart. The quote and the urgency
|
|
769
|
+
// belong to the first part only.
|
|
770
|
+
const parts = splitMessage({ text: expanded.text, styles, mentions });
|
|
771
|
+
const contentOf = (part, index) => {
|
|
772
|
+
const first = index === 0;
|
|
773
|
+
const extras = part.mentions.length > 0 || part.styles.length > 0 || (first && (quote || urgency));
|
|
774
|
+
return extras
|
|
775
|
+
? {
|
|
776
|
+
msg: part.text,
|
|
777
|
+
...(part.mentions.length > 0 && { mentions: part.mentions }),
|
|
778
|
+
...(part.styles.length > 0 && { styles: part.styles }),
|
|
779
|
+
...(first && quote && { quote }),
|
|
780
|
+
...(first && urgency && { urgency }),
|
|
781
|
+
}
|
|
782
|
+
: part.text;
|
|
783
|
+
};
|
|
767
784
|
|
|
768
785
|
for (const w of warnings) console.error("[mcp-tools] zalo_send_message warning:", w);
|
|
769
786
|
|
|
770
|
-
const
|
|
787
|
+
const results = await sendParts(parts, (part, index) =>
|
|
788
|
+
api.sendMessage(contentOf(part, index), threadId, Number(threadType)),
|
|
789
|
+
);
|
|
790
|
+
const result = results[0];
|
|
771
791
|
const messageId = result?.message?.msgId ?? result?.msgId ?? null;
|
|
772
792
|
// `msg react`, `msg undo` and a later quote all key off cliMsgId,
|
|
773
793
|
// and it appears in no other output. Without it an agent cannot
|
|
@@ -776,6 +796,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
776
796
|
return ok({
|
|
777
797
|
success: true,
|
|
778
798
|
messageId,
|
|
799
|
+
msgId: messageId,
|
|
779
800
|
...(cliMsgId !== null && { cliMsgId: String(cliMsgId) }),
|
|
780
801
|
threadType,
|
|
781
802
|
// Only for `me`: the thread it really went to, which is
|
|
@@ -786,6 +807,19 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
786
807
|
...(expanded.text !== text && { text: expanded.text }),
|
|
787
808
|
...(styles.length > 0 && { formatted: styles.length }),
|
|
788
809
|
...(unresolved.length > 0 && { unresolvedMentions: unresolved }),
|
|
810
|
+
// Only when it was split: every part's ids, in order (msgId/cliMsgId above are the first).
|
|
811
|
+
...(parts.length > 1 && {
|
|
812
|
+
splitInto: parts.length,
|
|
813
|
+
parts: results.map((r) => {
|
|
814
|
+
const partId = r?.message?.msgId ?? r?.msgId ?? null;
|
|
815
|
+
const partCli = r?.message?.cliMsgId ?? r?.cliMsgId ?? null;
|
|
816
|
+
return {
|
|
817
|
+
msgId: partId,
|
|
818
|
+
messageId: partId,
|
|
819
|
+
...(partCli !== null && { cliMsgId: String(partCli) }),
|
|
820
|
+
};
|
|
821
|
+
}),
|
|
822
|
+
}),
|
|
789
823
|
...(warnings.length > 0 && { warnings }),
|
|
790
824
|
});
|
|
791
825
|
} catch (e) {
|
|
@@ -801,9 +835,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
801
835
|
{
|
|
802
836
|
title: "List Zalo Threads",
|
|
803
837
|
description:
|
|
804
|
-
"List
|
|
805
|
-
"
|
|
806
|
-
"has read the conversation on Zalo (the human, on any device), null when the listener has not heard.",
|
|
838
|
+
"List the conversations that have received a message since this server started (the live buffer), each with this consumer's unread count. Use it to see what is active right now; use zalo_list_conversations for recent conversations whenever they last had a message, and zalo_search_threads to find one by name. " +
|
|
839
|
+
"Returns `{ threads, total }`; each thread has `threadId`, `name`, `threadType`, `unread` (this consumer's own count), `total`, `lastActivity` and `readState` (how far the account owner has read it on Zalo, on any device; null when unknown).",
|
|
807
840
|
inputSchema: z.object({
|
|
808
841
|
type: z
|
|
809
842
|
.enum(["group", "dm", "all"])
|
|
@@ -851,7 +884,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
851
884
|
{
|
|
852
885
|
title: "Search Zalo Threads",
|
|
853
886
|
description:
|
|
854
|
-
"
|
|
887
|
+
"Find a conversation by name, to get its `threadId`. It is a fuzzy search that ignores case and Vietnamese accents, over the names this server knows (cached contacts and groups). To find a person by phone number or username instead, use zalo_lookup_user. " +
|
|
888
|
+
"Returns `{ results, total }`; use a result's `threadId` in zalo_send_message, zalo_get_history and the other tools.",
|
|
855
889
|
inputSchema: z.object({
|
|
856
890
|
query: z.string().min(1).describe("Search keyword (fuzzy match, case-insensitive, accent-insensitive)"),
|
|
857
891
|
type: z
|
|
@@ -882,9 +916,9 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
882
916
|
{
|
|
883
917
|
title: "Mark Zalo Messages Read",
|
|
884
918
|
description:
|
|
885
|
-
"Mark
|
|
886
|
-
"
|
|
887
|
-
"
|
|
919
|
+
"Mark messages as read for this consumer, up to and including `cursor` (the `cursor` of the last zalo_get_messages or zalo_wait_for_message reply). Call it once a batch is handled, or the same messages come back. " +
|
|
920
|
+
"It only moves this consumer's read cursor: other bots keep their own, nothing is deleted (messages leave the buffer by age or size), and nothing is marked read on Zalo, so the owner's own unread state is unchanged. " +
|
|
921
|
+
"Returns `{ success, marked, readCursor }`.",
|
|
888
922
|
inputSchema: z.object({
|
|
889
923
|
cursor: z
|
|
890
924
|
.number()
|
|
@@ -911,19 +945,14 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
911
945
|
{
|
|
912
946
|
title: "Get Zalo Message History",
|
|
913
947
|
description:
|
|
914
|
-
"
|
|
915
|
-
"
|
|
916
|
-
"(your alias,
|
|
917
|
-
"
|
|
918
|
-
"have stored, which can be the full history), and falls back to asking the Zalo server " +
|
|
919
|
-
"when the cache has nothing for the thread: a group's cloud-message store first, then the " +
|
|
920
|
-
"socket stream, caching what comes back. Zalo serves only messages since this login that " +
|
|
921
|
-
"way; `filtered: true` means it withheld older ones, which only `zalo-agent sync` restores. " +
|
|
922
|
-
"Page the cache with 'before' (epoch ms, from the previous response's cursor) and the server " +
|
|
923
|
-
"path with 'lastMsgId'. " +
|
|
924
|
-
"WARNING: Large limits may consume significant memory/bandwidth. Start with a small limit and paginate.",
|
|
948
|
+
"Get the stored messages of one conversation (DM or group), for context older than the live buffer. It reads the local cache first (everything the listener and `zalo-agent sync` have saved, which can be the full history) and asks the Zalo server only when the cache has nothing for the thread. " +
|
|
949
|
+
"Page back with `before` (epoch ms: the `cursor` of the previous reply) or, on the server path, `lastMsgId` (the previous reply's `cursor`). Zalo's server serves only messages since this login: `filtered: true` in the reply means older ones exist that only `zalo-agent sync` can restore, and that needs the owner's phone. " +
|
|
950
|
+
"Each message has `msgId`, `senderId`, `senderDisplayName` (the current name: your alias, else the Zalo profile name, else the uid; `senderDisplayNameSource` says which), `senderName` (the name when first seen), `text`, `timestamp` and `type`. Reply: `{ threadId, threadType, count, messages, cursor, hasMore }`. " +
|
|
951
|
+
"Start with a small `limit` and page; a large one costs memory and time. To find messages by their text, use zalo_search_messages.",
|
|
925
952
|
inputSchema: z.object({
|
|
926
|
-
threadId: z
|
|
953
|
+
threadId: z
|
|
954
|
+
.string()
|
|
955
|
+
.describe("The conversation: the other person's user id for a DM, the group id for a group."),
|
|
927
956
|
threadType: z
|
|
928
957
|
.number()
|
|
929
958
|
.int()
|
|
@@ -931,21 +960,29 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
931
960
|
.max(1)
|
|
932
961
|
.optional()
|
|
933
962
|
.describe(
|
|
934
|
-
"
|
|
935
|
-
"
|
|
963
|
+
"0 = DM, 1 = group. Leave it out: the type stored for this thread is used. " +
|
|
964
|
+
"Pass it only to override that.",
|
|
936
965
|
),
|
|
937
|
-
limit: z
|
|
966
|
+
limit: z
|
|
967
|
+
.number()
|
|
968
|
+
.int()
|
|
969
|
+
.min(1)
|
|
970
|
+
.max(200)
|
|
971
|
+
.default(50)
|
|
972
|
+
.describe("Most messages to return (1 to 200; default 50)."),
|
|
938
973
|
lastMsgId: z
|
|
939
974
|
.string()
|
|
940
975
|
.optional()
|
|
941
976
|
.nullable()
|
|
942
|
-
.describe("
|
|
977
|
+
.describe("Server path only: the `cursor` of the previous reply, to page further back."),
|
|
943
978
|
before: z
|
|
944
979
|
.number()
|
|
945
980
|
.int()
|
|
946
981
|
.positive()
|
|
947
982
|
.optional()
|
|
948
|
-
.describe(
|
|
983
|
+
.describe(
|
|
984
|
+
"Cache path: only messages older than this epoch-ms timestamp (the `cursor` of the previous reply), to page back.",
|
|
985
|
+
),
|
|
949
986
|
}),
|
|
950
987
|
},
|
|
951
988
|
async ({ threadId, threadType: threadTypeIn, limit, lastMsgId, before }) => {
|
|
@@ -1056,19 +1093,24 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1056
1093
|
{
|
|
1057
1094
|
title: "View Zalo Media",
|
|
1058
1095
|
description:
|
|
1059
|
-
"
|
|
1060
|
-
"
|
|
1061
|
-
"If not yet downloaded, downloads first then opens.",
|
|
1096
|
+
"Get a received photo, voice message, video or file as a local file, and by default open it with the viewer of the machine this server runs on. Media is downloaded when a message arrives; if the file is not on disk yet it is downloaded first. " +
|
|
1097
|
+
"Give the message's `msgId`. Returns `{ success, path, mediaType }`: `path` is where the file is on the server's machine. Pass `open: false` when you only need the path (for example on a remote or headless server).",
|
|
1062
1098
|
inputSchema: z.object({
|
|
1063
|
-
|
|
1064
|
-
|
|
1099
|
+
msgId: z
|
|
1100
|
+
.string()
|
|
1101
|
+
.describe(
|
|
1102
|
+
"msgId of a message that has a photo, voice message, video or file (from zalo_get_messages, zalo_wait_for_message or zalo_get_history).",
|
|
1103
|
+
),
|
|
1104
|
+
threadId: z.string().optional().describe("Only look in this conversation. Omit to search all of them."),
|
|
1065
1105
|
open: z
|
|
1066
1106
|
.boolean()
|
|
1067
1107
|
.default(mediaConfig.autoOpen ?? true)
|
|
1068
|
-
.describe(
|
|
1108
|
+
.describe(
|
|
1109
|
+
"Also open the file with the viewer of the machine this server runs on. Pass false when you only need the path.",
|
|
1110
|
+
),
|
|
1069
1111
|
}),
|
|
1070
1112
|
},
|
|
1071
|
-
async ({
|
|
1113
|
+
async ({ msgId, threadId, open }) => {
|
|
1072
1114
|
try {
|
|
1073
1115
|
// The cached row is authoritative: it records where a fetched
|
|
1074
1116
|
// file actually landed, so a message that arrived before this
|
|
@@ -1076,14 +1118,14 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1076
1118
|
// fallback for a thread id.
|
|
1077
1119
|
let row = null;
|
|
1078
1120
|
try {
|
|
1079
|
-
row = getMessageById(
|
|
1121
|
+
row = getMessageById(msgId);
|
|
1080
1122
|
} catch (e) {
|
|
1081
1123
|
console.error("[mcp-tools] cache unavailable:", e.message);
|
|
1082
1124
|
}
|
|
1083
|
-
const buffered = buffer.read(threadId, 0, 9999).messages.find((m) => m.id ===
|
|
1084
|
-
if (!row && !buffered) return err(`Message ${
|
|
1125
|
+
const buffered = buffer.read(threadId, 0, 9999).messages.find((m) => m.id === msgId) || null;
|
|
1126
|
+
if (!row && !buffered) return err(`Message ${msgId} not found in cache or buffer`);
|
|
1085
1127
|
if (row && !row.has_attachment && !buffered?.attachment?.url) {
|
|
1086
|
-
return err(`Message ${
|
|
1128
|
+
return err(`Message ${msgId} has no media attachment`);
|
|
1087
1129
|
}
|
|
1088
1130
|
|
|
1089
1131
|
let localPath = row?.localPath || null;
|
|
@@ -1098,10 +1140,10 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1098
1140
|
concurrency: 2,
|
|
1099
1141
|
mediaRoot: process.env.ZALO_MEDIA_DIR || mediaConfig.downloadDir || undefined,
|
|
1100
1142
|
});
|
|
1101
|
-
localPath = getMessageById(
|
|
1143
|
+
localPath = getMessageById(msgId)?.localPath || null;
|
|
1102
1144
|
if (!localPath) {
|
|
1103
1145
|
return err(
|
|
1104
|
-
`Could not fetch media for ${
|
|
1146
|
+
`Could not fetch media for ${msgId} (${stats.expired} expired, ` +
|
|
1105
1147
|
`${stats.throttled} rate limited, ${stats.unknown} unexplained, ` +
|
|
1106
1148
|
`${stats.failed} failed). Rate limiting clears on a retry; an unexplained ` +
|
|
1107
1149
|
`failure is usually a 403, which Zalo returns for a lapsed signature and ` +
|
|
@@ -1128,8 +1170,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1128
1170
|
.max(1)
|
|
1129
1171
|
.optional()
|
|
1130
1172
|
.describe(
|
|
1131
|
-
"
|
|
1132
|
-
"
|
|
1173
|
+
"0 = DM, 1 = group. Leave it out: the type stored for this thread is used. " +
|
|
1174
|
+
"Pass it only to override that.",
|
|
1133
1175
|
);
|
|
1134
1176
|
const cliMsgIdSchema = z
|
|
1135
1177
|
.string()
|
|
@@ -1146,19 +1188,28 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1146
1188
|
{
|
|
1147
1189
|
title: "React to a Zalo Message",
|
|
1148
1190
|
description:
|
|
1149
|
-
"
|
|
1150
|
-
"
|
|
1151
|
-
"
|
|
1152
|
-
"cliMsgId as well as its msgId, so the cliMsgId comes from `cliMsgId` or the local cache; when " +
|
|
1153
|
-
"neither has it the reaction is refused and nothing is sent — Zalo accepts a reaction keyed on the " +
|
|
1154
|
-
"msgId alone and never shows it. `threadId` may be `me` for My Documents.",
|
|
1191
|
+
"React to a message with a Zalo reaction. `reaction` is a reaction code, not an emoji character: `/-strong` like, `/-heart` heart, `:>` haha, `:o` wow, `:-((` cry, `:-h` angry, `:-*` kiss, `:')` tears of joy, `/-weak` dislike. " +
|
|
1192
|
+
"Give the message's `msgId` (the `msgId` of a message from zalo_get_messages, zalo_wait_for_message, zalo_get_history or zalo_search_messages, or of a zalo_send_message reply). Zalo also keys a reaction on the message's `cliMsgId`, which is read from the local cache unless you pass it (zalo_send_message returns it for your own messages); when neither has it the call is refused and nothing is sent, because Zalo accepts a reaction without it and never shows it. " +
|
|
1193
|
+
"`threadId` may be `me` for My Documents. Returns `{ success, reaction, msgId, cliMsgId, threadId, threadType }`.",
|
|
1155
1194
|
inputSchema: z.object({
|
|
1156
1195
|
msgId: z
|
|
1157
1196
|
.string()
|
|
1158
1197
|
.min(1)
|
|
1159
|
-
.describe(
|
|
1160
|
-
|
|
1161
|
-
|
|
1198
|
+
.describe(
|
|
1199
|
+
"msgId of the message to react to: the `msgId` of a message from zalo_get_messages, zalo_wait_for_message, zalo_get_history or zalo_search_messages, or of a zalo_send_message reply.",
|
|
1200
|
+
),
|
|
1201
|
+
threadId: z
|
|
1202
|
+
.string()
|
|
1203
|
+
.min(1)
|
|
1204
|
+
.describe(
|
|
1205
|
+
"The conversation the message is in: the user id for a DM, the group id for a group. `me` is My Documents.",
|
|
1206
|
+
),
|
|
1207
|
+
reaction: z
|
|
1208
|
+
.string()
|
|
1209
|
+
.min(1)
|
|
1210
|
+
.describe(
|
|
1211
|
+
"A Zalo reaction code, not an emoji character: `/-strong` like, `/-heart` heart, `:>` haha, `:o` wow, `:-((` cry, `:-h` angry, `:-*` kiss, `:')` tears of joy, `/-weak` dislike.",
|
|
1212
|
+
),
|
|
1162
1213
|
threadType: threadTypeSchema,
|
|
1163
1214
|
cliMsgId: cliMsgIdSchema,
|
|
1164
1215
|
}),
|
|
@@ -1203,13 +1254,21 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1203
1254
|
{
|
|
1204
1255
|
title: "Recall a Zalo Message",
|
|
1205
1256
|
description:
|
|
1206
|
-
|
|
1207
|
-
"
|
|
1208
|
-
"the local cache; when neither has it nothing is sent. Zalo accepts a recall only for a while after " +
|
|
1209
|
-
"the message was sent. `threadId` may be `me` for My Documents.",
|
|
1257
|
+
'Recall (unsend) one of your own messages for everyone in the conversation, as the apps\' "Thu hồi" does. Zalo accepts a recall only for a while after the message was sent. ' +
|
|
1258
|
+
"Give the message's `msgId` (from the zalo_send_message reply, or from zalo_get_history) and its `cliMsgId` (also in the send reply; read from the local cache when you leave it out); with neither, nothing is sent. To remove a message from your own view only, or someone else's message in a group as an admin, use zalo_delete_message. `threadId` may be `me` for My Documents.",
|
|
1210
1259
|
inputSchema: z.object({
|
|
1211
|
-
msgId: z
|
|
1212
|
-
|
|
1260
|
+
msgId: z
|
|
1261
|
+
.string()
|
|
1262
|
+
.min(1)
|
|
1263
|
+
.describe(
|
|
1264
|
+
"msgId of your own message to recall (from the zalo_send_message reply, or from zalo_get_history).",
|
|
1265
|
+
),
|
|
1266
|
+
threadId: z
|
|
1267
|
+
.string()
|
|
1268
|
+
.min(1)
|
|
1269
|
+
.describe(
|
|
1270
|
+
"The conversation the message is in: the user id for a DM, the group id for a group. `me` is My Documents.",
|
|
1271
|
+
),
|
|
1213
1272
|
threadType: threadTypeSchema,
|
|
1214
1273
|
cliMsgId: cliMsgIdSchema,
|
|
1215
1274
|
}),
|
|
@@ -1250,9 +1309,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1250
1309
|
{
|
|
1251
1310
|
title: "List Zalo Group Members",
|
|
1252
1311
|
description:
|
|
1253
|
-
"List a group's
|
|
1254
|
-
"the local cache, or from Zalo for members the cache cannot name. `totalMember` is Zalo's own " +
|
|
1255
|
-
"count. A member's uid can be @-mentioned in zalo_send_message as `@[uid]`.",
|
|
1312
|
+
"List the members of a group: each member's `userId` (`uid` is the same value, kept for older clients) and `displayName` (from the cache, or from Zalo for members the cache cannot name), and `totalMember`, Zalo's own count (it can be larger than the members listed). Use a `userId` as `@[userId]` in zalo_send_message to mention that member.",
|
|
1256
1313
|
inputSchema: z.object({
|
|
1257
1314
|
groupId: z
|
|
1258
1315
|
.string()
|
|
@@ -1291,7 +1348,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1291
1348
|
}
|
|
1292
1349
|
for (const w of warnings) console.error("[mcp-tools] zalo_get_group_members warning:", w);
|
|
1293
1350
|
|
|
1294
|
-
const members = group.uids.map((uid) => ({ uid, displayName: names.get(uid) ?? null }));
|
|
1351
|
+
const members = group.uids.map((uid) => ({ userId: uid, uid, displayName: names.get(uid) ?? null }));
|
|
1295
1352
|
return ok({
|
|
1296
1353
|
groupId,
|
|
1297
1354
|
name: group.name,
|
|
@@ -1313,13 +1370,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1313
1370
|
{
|
|
1314
1371
|
title: "List Recent Zalo Conversations",
|
|
1315
1372
|
description:
|
|
1316
|
-
"List the conversations this account was most recently active in, newest first, from the local " +
|
|
1317
|
-
"
|
|
1318
|
-
"zalo_list_threads it is not limited to what arrived since this server started. `limit` is per " +
|
|
1319
|
-
"type, as `conv recent -n` is: with type 'all' that is up to `limit` DMs and `limit` groups. " +
|
|
1320
|
-
"`readState` is how far the account itself has read each conversation on Zalo (the human, on any " +
|
|
1321
|
-
"device): the newest message read, how many cached messages from others came after it, and whether " +
|
|
1322
|
-
"it is marked unread; null when the listener has not heard.",
|
|
1373
|
+
"List the conversations this account was most recently active in, newest first, from the local cache, to get a `threadId` or to see what is current. Unlike zalo_list_threads it is not limited to messages that arrived since this server started. " +
|
|
1374
|
+
"Each has its thread id, type, name and last activity, plus `readState`: how far the account owner has read it on Zalo (on any device), meaning the newest message read, how many cached messages from others came after it, and whether it is marked unread (null when unknown). `limit` is per type: with `type` 'all' that is up to `limit` DMs and `limit` groups.",
|
|
1323
1375
|
inputSchema: z.object({
|
|
1324
1376
|
type: z
|
|
1325
1377
|
.enum(["group", "dm", "all"])
|
|
@@ -1379,12 +1431,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1379
1431
|
{
|
|
1380
1432
|
title: "Zalo Cache Coverage",
|
|
1381
1433
|
description:
|
|
1382
|
-
"
|
|
1383
|
-
"this account's connection to Zalo was down, so messages from them may be missing — with each " +
|
|
1384
|
-
"gap's from/to and reason, counts the gaps already resolved, and names the `zalo-agent sync --from " +
|
|
1385
|
-
"<date>` run that restores the pending ones from the owner's phone (it needs a tap on the phone, " +
|
|
1386
|
-
"so the owner must approve it). Use zalo_sync only when the owner asks to restore. " +
|
|
1387
|
-
"Read-only: nothing is sent to Zalo.",
|
|
1434
|
+
"Check how complete the local message cache is. Lists the coverage gaps still pending (windows in which this account's connection to Zalo was down, so messages from them may be missing), each with its from/to and reason, counts the gaps already resolved, and names the `zalo-agent sync --from <date>` run that restores the pending ones from the owner's phone. That run needs a tap on the phone, so the owner must approve it; call zalo_sync only when the owner asks. Read-only: nothing is sent to Zalo.",
|
|
1388
1435
|
inputSchema: z.object({}),
|
|
1389
1436
|
},
|
|
1390
1437
|
async () => {
|
|
@@ -1403,11 +1450,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1403
1450
|
{
|
|
1404
1451
|
title: "Request Zalo Sync",
|
|
1405
1452
|
description:
|
|
1406
|
-
"Start a messages
|
|
1407
|
-
"
|
|
1408
|
-
"Returns a job immediately: poll zalo_sync_status every few seconds. Reuse requestId for retries. " +
|
|
1409
|
-
"This updates the local cache, not Zalo messages. Messages restores all conversations in the window; " +
|
|
1410
|
-
"it is not a per-thread sync. No media download, boards, cloud index or conversation-state sync.",
|
|
1453
|
+
"OWNER ONLY: call it only when the owner explicitly asks. Start a restore of messages (or of the reaction backlog) on this server's existing Zalo connection. The `messages` stage makes the owner's phone ask for a confirmation, which the owner must tap. " +
|
|
1454
|
+
"It returns a job at once: poll zalo_sync_status every few seconds. Reuse the same `requestId` and arguments when retrying, so a retry does not start a second sync. It updates the local cache, not Zalo itself. `messages` restores every conversation in the window; it is not per conversation, and it downloads no media, boards, cloud index or conversation state.",
|
|
1411
1455
|
inputSchema: z
|
|
1412
1456
|
.object({
|
|
1413
1457
|
requestId: z
|
|
@@ -1418,7 +1462,12 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1418
1462
|
.describe(
|
|
1419
1463
|
"Unique request key; reuse the same key and arguments after a timeout to avoid duplicate sync.",
|
|
1420
1464
|
),
|
|
1421
|
-
stage: z
|
|
1465
|
+
stage: z
|
|
1466
|
+
.enum(["messages", "reactions"])
|
|
1467
|
+
.default("messages")
|
|
1468
|
+
.describe(
|
|
1469
|
+
"`messages` restores chat history from the owner's phone (the phone asks for a confirmation); `reactions` drains Zalo's backlog of reactions (no phone tap).",
|
|
1470
|
+
),
|
|
1422
1471
|
days: z
|
|
1423
1472
|
.number()
|
|
1424
1473
|
.int()
|
|
@@ -1477,10 +1526,16 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1477
1526
|
{
|
|
1478
1527
|
title: "Zalo Sync Status",
|
|
1479
1528
|
description:
|
|
1480
|
-
"Read
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1529
|
+
"Read the progress and final result of a zalo_sync job without starting or repeating anything. Omit `jobId` for the latest job (or idle). `completed` only means the runner returned: read `result.reason` for partial, timeout or socket loss; it is not proof that all history was restored. The last ten jobs are kept in memory until the server restarts.",
|
|
1530
|
+
inputSchema: z
|
|
1531
|
+
.object({
|
|
1532
|
+
jobId: z
|
|
1533
|
+
.string()
|
|
1534
|
+
.uuid()
|
|
1535
|
+
.optional()
|
|
1536
|
+
.describe("The job to read: the `jobId` that zalo_sync returned. Omit for the latest job."),
|
|
1537
|
+
})
|
|
1538
|
+
.strict(),
|
|
1484
1539
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1485
1540
|
},
|
|
1486
1541
|
async ({ jobId }) => {
|
|
@@ -1499,13 +1554,20 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1499
1554
|
{
|
|
1500
1555
|
title: "Search Zalo Messages",
|
|
1501
1556
|
description:
|
|
1502
|
-
"Search
|
|
1503
|
-
"
|
|
1504
|
-
"Results are ordered newest-first.",
|
|
1557
|
+
"Search the text of messages in the local cache (`zalo.db`), across all conversations or one. No network call. It is a case-insensitive substring match. " +
|
|
1558
|
+
"Narrow it with `threadId`, `senderId`, `since` or `until` (epoch ms). Results are newest first.",
|
|
1505
1559
|
inputSchema: z.object({
|
|
1506
1560
|
query: z.string().min(1).describe("Case-insensitive text substring to search for in cached messages"),
|
|
1507
|
-
threadId: z
|
|
1508
|
-
|
|
1561
|
+
threadId: z
|
|
1562
|
+
.string()
|
|
1563
|
+
.min(1)
|
|
1564
|
+
.optional()
|
|
1565
|
+
.describe("Only this conversation (its threadId). Omit to search all of them."),
|
|
1566
|
+
senderId: z
|
|
1567
|
+
.string()
|
|
1568
|
+
.min(1)
|
|
1569
|
+
.optional()
|
|
1570
|
+
.describe("Only messages from this user (the `senderId` of a message)"),
|
|
1509
1571
|
since: z
|
|
1510
1572
|
.number()
|
|
1511
1573
|
.int()
|
|
@@ -1568,9 +1630,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1568
1630
|
{
|
|
1569
1631
|
title: "Lookup Zalo User",
|
|
1570
1632
|
description:
|
|
1571
|
-
"Look up a Zalo user by phone number (`phone`), username (`username`)
|
|
1572
|
-
"this logged-in account's own profile (`self: true`). Useful for finding a contact's `userId` (which is " +
|
|
1573
|
-
"also their 1-1 DM `threadId`) before sending a message or contact card.",
|
|
1633
|
+
"Look up a Zalo user by phone number (`phone`), username (`username`) or user id (`userId`), or read this logged-in account's own profile (`self: true`). Use it to find someone's `userId`, which is also the `threadId` of your 1-1 conversation with them, before sending a message or a contact card.",
|
|
1574
1634
|
inputSchema: z
|
|
1575
1635
|
.object({
|
|
1576
1636
|
self: z
|
|
@@ -1583,7 +1643,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1583
1643
|
.optional()
|
|
1584
1644
|
.describe("Phone number to search for on Zalo (e.g. '0901234567' or '+84901234567')"),
|
|
1585
1645
|
username: z.string().min(1).optional().describe("Zalo username to search for"),
|
|
1586
|
-
userId: z.string().min(1).optional().describe("Zalo user
|
|
1646
|
+
userId: z.string().min(1).optional().describe("A Zalo user id, to fetch that user's full profile."),
|
|
1587
1647
|
})
|
|
1588
1648
|
.refine(
|
|
1589
1649
|
(v) =>
|
|
@@ -1658,9 +1718,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1658
1718
|
{
|
|
1659
1719
|
title: "Forward Zalo Message",
|
|
1660
1720
|
description:
|
|
1661
|
-
"Forward a
|
|
1662
|
-
"through Zalo's forward API with the 'forwarded' badge; cards, stickers, links, videos, voices, and " +
|
|
1663
|
-
"locally downloaded media attachments are re-sent as a message of their own kind, matching `msg forward`.",
|
|
1721
|
+
"Forward a message from the local cache (`msgId`) to another conversation (`threadId`). Text goes through Zalo's forward API with the 'forwarded' badge; cards, stickers, links, videos, voice messages and downloaded media are re-sent as a message of their own kind, as `msg forward` does.",
|
|
1664
1722
|
inputSchema: z.object({
|
|
1665
1723
|
msgId: z
|
|
1666
1724
|
.string()
|
|
@@ -1710,7 +1768,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1710
1768
|
const resendLocal = async () => {
|
|
1711
1769
|
if (!row.localPath || !existsSync(row.localPath)) {
|
|
1712
1770
|
throw new Error(
|
|
1713
|
-
`Message ${msgId} is "${row.type}" but its media is not downloaded locally. Call zalo_view_media with
|
|
1771
|
+
`Message ${msgId} is "${row.type}" but its media is not downloaded locally. Call zalo_view_media with msgId="${msgId}" first.`,
|
|
1714
1772
|
);
|
|
1715
1773
|
}
|
|
1716
1774
|
return api.sendMessage(
|
|
@@ -1820,12 +1878,16 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1820
1878
|
{
|
|
1821
1879
|
title: "Delete Zalo Message",
|
|
1822
1880
|
description:
|
|
1823
|
-
"Delete a message from your own view (`everyone: false`, the default) or for everyone in
|
|
1824
|
-
"(`everyone: true`, only valid for someone else's message in a group — to recall your own sent " +
|
|
1825
|
-
"message for both sides, use `zalo_undo`). Resolves `cliMsgId` and `uidFrom` from the local cache when omitted.",
|
|
1881
|
+
"Delete a message from your own view only (`everyone: false`, the default), or, as a group admin, delete someone else's message for everyone in the group (`everyone: true`). To recall your own message for both sides, use zalo_undo. `cliMsgId` and `uidFrom` are read from the local cache when you leave them out.",
|
|
1826
1882
|
inputSchema: z.object({
|
|
1827
|
-
msgId: z
|
|
1828
|
-
|
|
1883
|
+
msgId: z
|
|
1884
|
+
.string()
|
|
1885
|
+
.min(1)
|
|
1886
|
+
.describe("msgId of the message to delete (from zalo_get_messages or zalo_get_history)."),
|
|
1887
|
+
threadId: z
|
|
1888
|
+
.string()
|
|
1889
|
+
.min(1)
|
|
1890
|
+
.describe("The conversation the message is in: the user id for a DM, the group id for a group."),
|
|
1829
1891
|
threadType: z
|
|
1830
1892
|
.number()
|
|
1831
1893
|
.int()
|
|
@@ -1838,12 +1900,12 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1838
1900
|
.min(1)
|
|
1839
1901
|
.optional()
|
|
1840
1902
|
.describe("Client message ID (`cliMsgId`). Looked up in the local cache when omitted."),
|
|
1841
|
-
|
|
1903
|
+
senderId: z
|
|
1842
1904
|
.string()
|
|
1843
1905
|
.min(1)
|
|
1844
1906
|
.optional()
|
|
1845
1907
|
.describe(
|
|
1846
|
-
"
|
|
1908
|
+
"The `senderId` of the message: its author's user id. Looked up in the local cache, or your own user id when omitted.",
|
|
1847
1909
|
),
|
|
1848
1910
|
everyone: z
|
|
1849
1911
|
.boolean()
|
|
@@ -1856,7 +1918,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1856
1918
|
threadId: threadIdIn,
|
|
1857
1919
|
threadType: threadTypeIn,
|
|
1858
1920
|
cliMsgId: cliMsgIdIn,
|
|
1859
|
-
|
|
1921
|
+
senderId: senderIdIn,
|
|
1860
1922
|
everyone,
|
|
1861
1923
|
}) => {
|
|
1862
1924
|
const thread = actionThread(api, threadIdIn, threadTypeIn);
|
|
@@ -1867,7 +1929,7 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
1867
1929
|
const { threadId, threadType } = thread;
|
|
1868
1930
|
try {
|
|
1869
1931
|
let cliMsgId = cliMsgIdIn;
|
|
1870
|
-
let uidFrom =
|
|
1932
|
+
let uidFrom = senderIdIn;
|
|
1871
1933
|
if (!cliMsgId || !uidFrom) {
|
|
1872
1934
|
const cached = cachedMessageById(threadId, msgId);
|
|
1873
1935
|
cliMsgId = cliMsgId || cached?.cliMsgId;
|
|
@@ -2088,7 +2150,11 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2088
2150
|
.describe(
|
|
2089
2151
|
"Thread type for 'pin_message' / 'unpin_message': 0=DM(User), 1=Group. Inferred from cache when omitted.",
|
|
2090
2152
|
),
|
|
2091
|
-
noteId: z
|
|
2153
|
+
noteId: z
|
|
2154
|
+
.string()
|
|
2155
|
+
.min(1)
|
|
2156
|
+
.optional()
|
|
2157
|
+
.describe("The note's `topicId`, as listed by action 'list' (required for 'edit')."),
|
|
2092
2158
|
title: z.string().min(1).optional().describe("Note text/title (required for 'create' and 'edit')"),
|
|
2093
2159
|
pin: z
|
|
2094
2160
|
.boolean()
|
|
@@ -2098,7 +2164,9 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2098
2164
|
.string()
|
|
2099
2165
|
.min(1)
|
|
2100
2166
|
.optional()
|
|
2101
|
-
.describe(
|
|
2167
|
+
.describe(
|
|
2168
|
+
"msgId of a cached text message to pin or unpin (required for 'pin_message' and 'unpin_message').",
|
|
2169
|
+
),
|
|
2102
2170
|
page: z.number().int().min(1).default(1).describe("Page number for 'list' (default 1)"),
|
|
2103
2171
|
count: z.number().int().min(1).max(100).default(20).describe("Items per page for 'list' (default 20)"),
|
|
2104
2172
|
}),
|
|
@@ -2236,7 +2304,11 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2236
2304
|
action: z
|
|
2237
2305
|
.enum(["create", "info", "vote", "unvote", "add_options", "lock", "share"])
|
|
2238
2306
|
.describe("Poll operation to perform"),
|
|
2239
|
-
groupId: z
|
|
2307
|
+
groupId: z
|
|
2308
|
+
.string()
|
|
2309
|
+
.min(1)
|
|
2310
|
+
.optional()
|
|
2311
|
+
.describe("The group's id, which is its threadId (required for 'create')."),
|
|
2240
2312
|
pollId: z
|
|
2241
2313
|
.union([z.number().int().positive(), z.string().regex(/^\d+$/)])
|
|
2242
2314
|
.optional()
|
|
@@ -2375,13 +2447,8 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2375
2447
|
{
|
|
2376
2448
|
title: "Wait for Incoming Zalo Message",
|
|
2377
2449
|
description:
|
|
2378
|
-
|
|
2379
|
-
"
|
|
2380
|
-
"`timeoutSec` seconds (default 55, max 300) for a new message to arrive. `limit` (default 1, max 100) returns " +
|
|
2381
|
-
"up to that many buffered messages in one call, in order, in `messages` (`message` is the first and `cursor` " +
|
|
2382
|
-
"the last, so `since: cursor` continues where the batch ended; `hasMore` says more are already waiting); " +
|
|
2383
|
-
"`settleMs` waits that long after the first for the rest of a burst. Mirrors GET /events/wait for " +
|
|
2384
|
-
"remote HTTPS/OAuth2 MCP clients.",
|
|
2450
|
+
'Wait for the next incoming message, without polling. It returns at once when a buffered message newer than `since` is already waiting; otherwise it waits up to `timeoutSec` seconds (default 55, max 300) and then returns `status: "timeout"` with no message. Call it in a loop to listen, and call zalo_mark_read with the `cursor` when a message is handled. ' +
|
|
2451
|
+
"`limit` (default 1, max 100) returns up to that many buffered messages in one call, in order, in `messages` (`message` is the first, `cursor` is the last, so `since: cursor` continues where the batch ended; `hasMore` says more are already waiting); `settleMs` waits that long after the first message for the rest of a burst. A message's `id` is its msgId. Mirrors GET /events/wait for remote HTTPS/OAuth2 MCP clients.",
|
|
2385
2452
|
inputSchema: z.object({
|
|
2386
2453
|
threadId: z
|
|
2387
2454
|
.string()
|
|
@@ -2493,7 +2560,11 @@ export function registerTools(server, api, buffer, filter, config, nameCache, ac
|
|
|
2493
2560
|
.describe(
|
|
2494
2561
|
"Label name/text, e.g. 'Nội bộ', 'Khách hàng', 'Đối tác' (case-insensitive match for 'assign' or 'unassign')",
|
|
2495
2562
|
),
|
|
2496
|
-
threadId: z
|
|
2563
|
+
threadId: z
|
|
2564
|
+
.string()
|
|
2565
|
+
.min(1)
|
|
2566
|
+
.optional()
|
|
2567
|
+
.describe("One conversation (its threadId) to assign to or remove from the label."),
|
|
2497
2568
|
threadIds: z
|
|
2498
2569
|
.array(z.string().min(1))
|
|
2499
2570
|
.optional()
|